This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
WorkForge is a TypeScript CLI tool for managing Git worktrees with intelligent environment variable synchronization, automatic backup management, and comprehensive audit logging. The tool provides five commands: create, close, sync-env, list, and cleanup.
Binary aliases: workforge and wf
Versioning: published as @juspay/workforge,
which started its public life at 1.0.0. "v3.0" appears in older prose here
and refers to the third internal rewrite — the modular architecture described
below — not to a released version. Never write a version number into source or
docs; read it from package.json, which is the only thing semantic-release
updates.
# Run the test suite (builds first — command tests drive the built CLI)
pnpm test
# Watch mode
pnpm run test:watch
# A single file, or a single test by name
npx vitest run test/env-parser.test.ts
npx vitest run test/create.test.ts -t "forks from the remote tip"
# Build TypeScript to JavaScript
pnpm run build
# Run in development mode (without building)
pnpm run dev
# Build before publishing
pnpm run prepublishOnly
# Test a specific command
pnpm run dev -- create -t feat -n test-feature
pnpm run dev -- sync-env --dry-run
pnpm run dev -- list --jsonReleases are cut automatically by semantic-release — never bump the version
or publish by hand. This mirrors juspay/neurolink.
Flow: push to release → .github/workflows/release.yml → npx semantic-release → version derived from the commit messages → published to npm
with provenance → CHANGELOG.md and package.json committed back with
[skip ci] → mirrored to GitHub Packages.
Auth is token-less. Publishing uses npm Trusted Publishing (OIDC): the
workflow declares id-token: write and upgrades npm (npm install -g npm@latest,
since OIDC needs npm >= 11.5.1). There is no NPM_TOKEN. This requires a
trusted publisher configured for @juspay/workforge on npmjs.com, pointing at
this repository and at release.yml by name — rename that file and publishing
breaks.
Commit types decide the version (.releaserc.json):
| Type | Effect |
|---|---|
feat |
minor |
fix, perf, revert, refactor, build |
patch |
docs, style, test, ci, chore |
no release |
BREAKING CHANGE: in the body, or type! |
major |
A ticket prefix is tolerated — BZ-123: fix: ... parses the same as fix: ....
Preview what a push would release, without publishing anything:
pnpm run release:dry-runWorkForge uses a modular architecture with separation of concerns across commands, core components, UI components, and types.
src/
├── commands/ # CLI command implementations
│ ├── create.ts # Create worktree command
│ ├── close.ts # Close worktree with sync
│ ├── sync-env.ts # Standalone env sync
│ ├── list.ts # List worktrees
│ └── cleanup.ts # Cleanup backups
├── core/ # Core business logic
│ ├── ConfigManager.ts # Global configuration
│ ├── ProjectIdentifier.ts # Project ID generation
│ ├── EnvFileParser.ts # .env parsing
│ ├── WorktreeResolver.ts # Worktree discovery
│ ├── BranchResolver.ts # Remote-aware branch resolution
│ ├── EnvDiffer.ts # Environment diff
│ ├── EnvSyncer.ts # Environment sync
│ ├── BackupManager.ts # Backup management
│ ├── SafetyChecker.ts # Pre-close checks
│ ├── WorktreeRemover.ts # Worktree removal
│ ├── BranchCleaner.ts # Branch deletion
│ ├── AuditLogger.ts # Audit logging
│ └── SyncTargetResolver.ts # Sync target resolution
├── ui/ # User interface components
│ ├── Logger.ts # Logging and progress
│ ├── DiffDisplay.ts # Diff visualization
│ ├── SyncPrompt.ts # Interactive prompts
│ └── ListDisplay.ts # List formatting
├── utils/ # Utility functions
│ ├── errors.ts # Error handling
│ └── strings.ts # String transformations
├── types/
│ └── index.ts # TypeScript interfaces
└── index.ts # CLI router (yargs)
test/
├── helpers/
│ └── fixtures.ts # Isolated bare remote + clone, CLI runner
├── env-parser.test.ts # Parser fidelity and sync safety (unit)
├── create.test.ts # create command (drives the built CLI)
├── close.test.ts # close, BranchCleaner, BackupManager, ProjectIdentifier
├── worktree-audit.test.ts # WorktreeResolver/Remover, audit log durability
└── cli-help.test.ts # --help/--version strings, asserted against package.json
pnpm test builds, then runs Vitest. 57 tests, ~23s.
How the fixtures work. makeRepo() builds a bare repository standing in for
the remote, a seed checkout used to push "someone else's" commits, and the
local clone under test. advanceRemote: true pushes an extra commit so the
clone's local branch starts out stale — that gap is what most of the
create-side regressions were about. .env is gitignored in the fixture, matching
real projects; without that it registers as an uncommitted change and blocks
close.
Command tests spawn the built CLI (dist/index.js) rather than importing it,
because the commands call process.exit. Unit tests import from src directly.
Serial by design. fileParallelism: false — fixtures share ~/.workforge
state (backups, audit logs, project metadata), so parallel files would race.
These tests are regression guards, not coverage. Every one of them maps to a
defect that shipped. Before trusting a change here, confirm the relevant test
still fails when the fix is reverted — the suite has been mutation-checked
against the #-comment, protected-branch, and remote-tip fixes.
Manages global configuration at ~/.workforge/config.json.
Key Methods:
load()- Load configuration from filesave(config)- Save configuration to fileget(key)- Get configuration value (dot notation supported)set(key, value)- Set configuration valuereset()- Reset to defaults
Features:
- Deep merge for user overrides
- Nested key access via dot notation
- Default values for all settings
- Type-safe interface
Configuration Sections:
preferences- General settings (base branch, confirmations, package manager)backup- Backup settings (enabled, retention, auto-cleanup)sync- Sync settings (backup before sync, default direction)audit- Audit settings (enabled, include values, retention)display- Display settings (colors, verbose, progress indicators)
Generates unique project IDs from Git remote URLs.
Key Method:
generateId()- SHA-256 hash (8 chars) of Git remote URLgetProjectDir()- Returns~/.workforge/projects/<project-id>/getBackupDir()- Returns~/.workforge/backups/<project-id>/updateMetadata()- Updates project metadata
Project ID Generation:
Remote URL: git@github.com:user/repo.git
SHA-256: abc123def456789...
Project ID: abc123de (first 8 chars)
Metadata Location: ~/.workforge/projects/<project-id>/.meta.json
Metadata Contents:
- Repository name
- Remote URL
- Created timestamp
- Last accessed timestamp
Comprehensive .env file parser with advanced features.
Key Methods:
parse(filePath)- Parse .env file to variable mapparseFile(filePath)- Same, plus the trivia after the last variableparseContent(content)- Parse from a string (used by the round-trip guard)stringify(variables, trailingTrivia?)- Convert variable map to .env format
Supported Features:
- Single-line variables:
KEY=value - Multi-line values with quotes:
KEY="value\nline2" - Comments:
# commentor// comment - Inline comments:
KEY=value # comment, requiring whitespace before the marker - Single and double quotes
- Escape sequences:
\n,\t,\\,\" - CRLF and bare-CR line endings, normalised on read
- Invalid line recovery (skips malformed lines, warns on duplicate keys)
Fidelity — why rewriting a file is safe:
Each variable keeps its exact original text in raw, plus the comment and blank
lines above it in leadingTrivia. stringify re-emits raw verbatim, so a
variable nobody changed is byte-identical after a rewrite; only added or
modified variables are serialised from their fields. Trivia after the last
variable is carried on ParsedEnvFile.trailingTrivia and must be passed back to
stringify — parse() alone returns a Map and cannot carry it.
Three consequences this design exists to prevent, all of which used to corrupt untouched variables on every sync:
KEY=sk_live_abc#def—#without preceding whitespace is part of the value, not a commentKEY="C:\\Users\\name"— escapes decode in one left-to-right pass; sequential regex replacements rescanned their own output and turned\nameinto a newline- Standalone comments and blank lines are preserved rather than dropped
Example:
# Database configuration
DATABASE_URL="postgres://localhost:5432/
database_name?
sslmode=require"
API_KEY=sk_test_abc123 # Production keyDiscovers worktrees using three-pattern resolution.
Key Method:
resolve(inputPath?, name?)- Resolve worktree with auto-detection
Three-Pattern Resolution:
- Explicit Path: Use provided path directly
- Name-Based Search: Search all worktrees for matching branch name
- Auto-Detection: Detect current directory as worktree
Example:
// Pattern 1: Explicit path
const wt = resolver.resolve('/path/to/worktree');
// Pattern 2: Name-based
const wt = resolver.resolve(undefined, 'feat-auth');
// Pattern 3: Auto-detect
const wt = resolver.resolve(); // From current directoryWorktree Information:
- Branch name
- Worktree path
- Main repository path
- Commit hash
- Is main repo flag
- Is locked flag
Treats the remote as the source of truth for branches; the local branch is a
possibly-stale cache. Used by create and by SafetyChecker.
Key Methods:
getRemoteName()- Prefersorigin, else the first remote, elsenullfetch(remote)-git fetch <remote> --prune; never throws, reports{ ok, error }refreshRemoteHead(remote)- Re-reads and caches<remote>/HEAD(git fetchnever updates it, so a renamed default branch would otherwise stick forever)detectPrimaryBranch(remote)- Five-rung ladder, see belowresolveStartPoint(base, remote)- What to actually branch fromlistLocalBranches()/listRemoteBranches(remote)- For error messagesdescribeCommit(revision)- Short SHA, for reporting
Primary branch ladder (falls through on every failure):
refs/remotes/<remote>/HEADgit ls-remote --symref <remote> HEAD, then cachedmain,master,release,develop,trunk,beta,dev,stableagainstrefs/remotes/<remote>/*- The same candidates against
refs/heads/* - The currently checked-out branch
Start point resolution:
| Condition | Start point | source |
|---|---|---|
refs/remotes/<remote>/<base> exists |
<remote>/<base> |
remote |
only refs/heads/<base> exists |
<base> (warns: may be stale) |
local |
| resolves as a tag or commit | as given | committish |
| nothing matches | null → actionable error |
missing |
Why: git fetch advances only refs/remotes/*. Branching from a local
branch name forks from wherever that branch was last left — commonly many
commits behind the remote, because worktree users rarely check out or pull the
primary branch in the main clone.
Compares .env files and generates structured diffs.
Key Methods:
compare(sourcePath, targetPath, targets)- Generate diffcompareVariables(source, target)- Compare variable mapsfilterDiff(diff, decision)- Filter diff by decisioninvertDiff(diff)- Invert source/target for bidirectional sync
Diff Structure:
{
added: [{ key: 'NEW_VAR', value: 'value' }],
removed: [{ key: 'OLD_VAR', value: 'value' }],
modified: [{
key: 'CHANGED_VAR',
oldValue: 'old',
newValue: 'new'
}],
unchanged: [{ key: 'SAME_VAR', value: 'value' }]
}Applies sync decisions to .env files.
Key Methods:
sync(sourcePath, targetPath, diff, decision)- Apply syncmergeVariables(target, diff, decision)- Merge variablesdryRun(sourcePath, targetPath, diff, decision)- Preview syncvalidateDecision(diff, decision)- Validate sync decision
Sync Process:
- Validate decision matches diff
- Load target .env (
parseFile, keeping trivia) - Merge variables based on decision — added variables are appended after everything already in the target, never inserted at the source file's line number
- Render the new content
- Round-trip guard: re-parse that content and assert every variable the user did not select is still present and unchanged. Any drift throws, so the target file is left untouched rather than silently corrupted
- Write updated .env
- Return sync result with stats
The guard exists because .env is normally gitignored: a bad write is often
unrecoverable, so a loud failure beats a silent one.
Manages automatic backups with rotation.
Key Methods:
createBackup(filePath)- Create timestamped backuplistBackups()- List all backups for projectrestore(backupId)- Restore from backupdeleteBackup(backupId)- Delete specific backupcleanupOldBackups()- Auto-cleanup old backups
Backup Format: .env.backup.YYYY-MM-DD_HH-mm-ss, with a -N suffix when
two backups land in the same second. Written with COPYFILE_EXCL so a backup
can never silently overwrite an earlier snapshot.
Backup Location: ~/.workforge/backups/<project-id>/
Auto-Cleanup:
- Maintains last N backups (default: 10)
- FIFO queue (oldest deleted first)
- Configurable via
maxBackupsPerProject
Validates worktree state before closing.
Key Method:
check(worktree)- Run all safety checks
Safety Checks:
- Uncommitted Changes: Detects modified, staged, or untracked files
- Unpushed Commits: Detects commits not pushed to remote. Indeterminate results (the remote-tracking ref is missing even after a targeted fetch) are reported as unpushed, never as clean
- Merge Status: Checks the branch against
<remote>/<primary>, where the primary branch comes fromBranchResolver— not a hardcodedmain/master, and not the local branch, which is normally stale in a worktree workflow - Detached HEAD: Detects detached HEAD state
- Merge/Rebase in Progress: Detects ongoing merge or rebase
Refs are refreshed with a best-effort git fetch before any of the remote
comparisons run.
Check Result:
{
canProceed: boolean,
blockingIssues: string[], // Must fix before closing
warnings: string[], // Can ignore with --force
recommendations: string[] // Suggestions
}Logs all sync operations in dual formats.
Key Methods:
log(operation, syncResult)- Log sync operationgetHistory()- Get sync historygetStatistics()- Get usage statistics
Durability. The history file is a read-modify-write shared by every
invocation against the same project, so addToHistory takes a mkdir-based
lock (atomic on all platforms, reclaimed after 10s if a holder dies) and writes
through a temp file plus rename. An unparseable history is moved aside as
sync-history.json.corrupt-<timestamp> rather than overwritten — it may be the
only record of months of operations. The human-readable log is rebuilt the same
way; it used to be deleted and re-appended entry by entry, so an interrupt
truncated it permanently.
Log Formats:
Human-Readable: ~/.workforge/projects/<project-id>/audit.log
────────────────────────────────────────
[2025-10-26T14:30:15.123Z] SYNC
────────────────────────────────────────
Source: Worktree (feat-auth)
Target: Main (project)
Changes:
Added: 2 variable(s)
Modified: 1 variable(s)
Removed: 0 variable(s)
Details:
Added Variables:
+ API_KEY
+ DEBUG_MODE
Modified Variables:
~ DATABASE_URL
Status: SUCCESS
Backup: Created
JSON: ~/.workforge/projects/<project-id>/sync-history.json
[
{
"timestamp": "2025-10-26T14:30:15.123Z",
"operation": "sync",
"source": "Worktree (feat-auth)",
"target": "Main (project)",
"changesApplied": {
"added": 2,
"modified": 1,
"removed": 0
},
"variableDetails": {
"added": ["API_KEY", "DEBUG_MODE"],
"modified": ["DATABASE_URL"],
"removed": []
},
"success": true,
"backupCreated": true
}
]Resolves source and target for sync operations.
Key Methods:
resolve(options)- Resolve sync targets from optionsvalidate(targets)- Validate sync targetsswap(targets)- Swap source and target (for bidirectional)
Four Sync Patterns:
-
Explicit Source and Target:
--from <source> --to <target>workforge sync-env --from feat-auth --to main
-
From Worktree to Main:
--from <source>workforge sync-env --from feat-auth # Target defaults to main -
From Main to Worktree:
--to <target>workforge sync-env --to feat-auth # Source defaults to main -
Bidirectional Sync:
--between <worktree>workforge sync-env --between feat-auth # Shows diff both ways, user chooses direction -
Auto-Detect: No arguments
# Run from inside worktree cd /path/to/worktree workforge sync-env # Auto-detects: main → current worktree
Creates new Git worktrees with environment setup.
Workflow:
- Validate inputs (type, name, ticket ID)
- Auto-convert name to kebab-case using toKebabCase()
- Log conversion if name changed
- Discover repository (via
git worktree list, so it works from worktrees too) - Sync with remote —
git fetch <remote> --prune, then refresh<remote>/HEAD. Runs before branch detection and preflight so both read fresh refs. Best effort: a missing or unreachable remote warns and continues. - Resolve base branch (see BranchResolver) — explicit
-b> auto-detected primary >preferences.defaultBaseBranch; resolves to<remote>/<base>when the base exists on the remote - Detect repository type (public vs internal)
- Calculate paths and branch names
- With Jira ticket: branch
type/TICKET-name, foldertype/TICKET-name - Without ticket: branch
type/name, foldertype/name
- With Jira ticket: branch
- Run preflight checks (branch collisions checked against fresh remote refs)
- Show existing worktrees (configurable)
- Confirm creation
- Create Git worktree —
git worktree add -b <branch> --no-track <path> <startPoint> - Copy environment files ┐
- Install dependencies ├ each isolated: a failure is recorded as a warning
- Update project metadata┘ and the remaining steps still run
Options:
-t, --type: Branch type (required)-n, --name: Branch name (required)-b, --base: Base branch (default: auto-detect)-j, --ticket: Jira ticket ID (internal repos)-y, --yes: Skip confirmations
Closes worktrees with intelligent environment sync.
Workflow:
- Discover worktree (path, name, or auto-detect)
- Run safety checks (fetches first; merge status is judged against
<remote>/<primary>) - Check environment differences. If the worktree has a
.envand the main repo does not, seed an empty one so the values are offered for sync instead of being destroyed with the worktree — the placeholder is removed again if nothing is synced - Show diff visualization
- Interactive variable selection
- Create backup — a backup failure aborts the close; the sync overwrites
main's
.envin place and.envis normally gitignored - Apply sync (guarded: unselected variables must survive unchanged)
- Remove worktree
- Delete branch (optional) — refuses the repository's primary branch, plus
main/master, and only force-deletes when--forceis given - Log audit trail
Options:
path: Worktree path (optional)-n, --name: Worktree name-d, --delete-branch: Delete branch-s, --skip-sync: Skip env sync-f, --force: Force close-y, --yes: Skip confirmations--dry-run: Preview only
Safety Features:
- Detects uncommitted changes
- Detects unpushed commits
- Checks merge status
- Validates worktree state
- Creates backup before sync
Standalone environment variable synchronization.
Workflow:
- Resolve sync targets (four patterns)
- Handle bidirectional sync (if --between)
- Generate environment diff
- Display side-by-side diff
- Interactive variable selection
- Create backup
- Apply sync
- Log audit trail
Options:
--from: Source path/name--to: Target path/name--between: Bidirectional sync-y, --yes: Auto-accept all--dry-run: Preview only
Lists all worktrees with status indicators.
Workflow:
- Parse git worktree list --porcelain
- Extract worktree information
- Calculate relative paths from current directory
- Sort by specified criteria
- Format output (table, JSON, or simple)
Options:
--json: JSON format--simple: Simple one-line format--sort: Sort by name, path, or age
Output Formats:
- Table: Formatted table with columns
- JSON: Machine-readable array
- Simple: One line per worktree
Output Features:
- Relative Paths: Shows paths relative to current directory (e.g.,
../feat/my-feature) - Copy-Paste Ready: Can directly use
cd <path>from output - JSON Format: Includes both absolute and relative paths
Sorting: --sort age uses each worktree's HEAD commit time
(WorktreeInfo.commitTimestamp, populated by WorktreeResolver). It previously
compared commit hashes, which carry no chronological information.
Status Indicators:
- MAIN: Main repository — detected by position (git always lists the main
worktree first), not by the porcelain
bareline, which only appears for bare repositories - ACTIVE: Normal worktree
- LOCKED: Locked worktree
- PRUNABLE: Directory missing
Cleans up old backups and logs.
Workflow:
- List all backups for project
- Filter by age (if --older-than)
- Show what will be deleted
- Confirm deletion (unless --yes)
- Delete backups
- Show summary
Options:
--older-than: Delete backups older than N days-y, --yes: Skip confirmation--dry-run: Preview only
Centralized logging with progress indicators.
Key Methods:
info(message)- Info messagesuccess(message)- Success message (green ✓)warning(message)- Warning message (yellow ⚠)error(message)- Error message (red ✗)startProgress(message)- Start animated spinnerstopProgress(message?)- Stop spinner
Features:
- Color-coded output (configurable)
- Animated progress spinners (configurable)
- Section headers and dividers
- Verbose output mode
Side-by-side diff visualization.
Key Methods:
show(diff, targets)- Show complete diffshowSummary(diff)- Show summary onlyshowTable(diff)- Show table formatshowNoChanges()- Show "no changes" message
Diff Sections:
- Added Variables (green): Variables to add
- Modified Variables (yellow): Variables with different values
- Removed Variables (red): Variables to remove
- Unchanged (gray): Summary of identical variables
Interactive variable selection prompts.
Key Methods:
prompt(diff, autoYes)- Main prompt flowpromptAdded(added)- Select added variablespromptModified(modified)- Select modified variablespromptRemoved(removed)- Select removed variablespromptDirection(diff)- Choose sync direction (bidirectional)
Features:
- Checkbox-based selection (inquirer.js)
- "Sync all" option
- "Cancel sync" option
- Direction selection for bidirectional sync
Target: ES2022
Module: ESNext with node resolution
Strict Mode: Enabled
Output: dist/ with source maps and declarations
Key tsconfig.json settings:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "node",
"strict": true,
"outDir": "./dist",
"sourceMap": true,
"declaration": true,
"esModuleInterop": true
}
}All validation happens before any filesystem or Git operations:
- Input Validation: Type, name, ticket format
- Repository Validation: Git repo exists, Git binary available
- Branch Validation: Branch doesn't exist locally or remotely
- Path Validation: Workspace path doesn't exist
- Base Branch Validation: Base branch exists
- Uncommitted changes
- Unpushed commits
- Merge status
- Detached HEAD state
- Merge/rebase in progress
- Automatic backup before every sync
- Backup rotation (keep last 10)
- Manual restore capability
- Audit trail for all operations
# Test create command
pnpm run dev -- create -t feat -n test-feature --yes
# Test list command
pnpm run dev -- list --json
# Test sync-env command (dry run)
pnpm run dev -- sync-env --dry-run
# Test cleanup command (dry run)
pnpm run dev -- cleanup --older-than 30 --dry-run
# Clean up test worktree
git worktree remove ../feat/test-feature
git branch -d feat/test-feature- Create command file in
src/commands/ - Implement command class with
run()method - Register in
src/index.tsCLI router - Update types in
src/types/index.ts
- Create component in
src/core/ - Define interfaces in
src/types/index.ts - Import and use in command implementations
- Update default config in
ConfigManager.ts - Add types to
src/types/index.ts - Document in
docs/configuration.md
Modify detectRepositoryType() in create.ts to check for different remote URL patterns.
Extend detectPackageManager() in create.ts with new lock file checks.
The src/utils/strings.ts module provides string transformation functions:
toKebabCase(input: string): string- Convert any string to kebab-case format- Handles spaces, underscores, special characters
- Idempotent (already kebab-case strings pass through unchanged)
- Used for branch name normalization in create command
Example:
import { toKebabCase } from '../utils/strings.js';
const branchName = toKebabCase('My Feature Name');
// Result: 'my-feature-name'- SPEC.md - Complete technical specification
- TODO.md - Development roadmap
- PROGRESS.md - Implementation progress
- README.md - User guide
- 23 major implementation files
- ~5,700 lines of production TypeScript
- 12 core components
- 4 UI components
- 5 commands
- Complete type safety with TypeScript 5.0+
- 4 comprehensive documentation guides
- Modular Architecture: Separation of concerns across commands, core, UI, types
- Type Safety: Comprehensive TypeScript interfaces throughout
- Configuration-Driven: Global settings with sensible defaults
- Safety First: Validation before operations, backups before sync
- Audit Trail: Complete logging of all operations
- User Experience: Interactive prompts, clear output, progress indicators
- Flexibility: Multiple sync patterns, configurable behavior
- Maintainability: Well-documented code, clear separation of concerns