Version: 3.0.0 Last Updated: 2025-10-26 Status: In Development
- Project Overview
- Complete File Structure
- TypeScript Interfaces & Types
- Command Specifications
- Core Module Specifications
- Configuration System
- Audit Logging System
- Backup Management System
- UI/Display Systems
- Workflows & Algorithms
- Error Handling Strategy
- Testing Requirements
- Documentation Requirements
- Implementation Phases
- Edge Cases & Security
- Migration from v2.0
WorkForge is a CLI tool for managing Git worktrees with intelligent environment variable synchronization, comprehensive audit logging, and automatic dependency management. Version 3.0 adds worktree closing, environment sync, configuration management, and audit trails.
v2.0 Features (Existing):
- Automatic worktree creation with organized folder structure
- Smart repository detection (main repo and existing worktrees)
- Jira integration for internal Bitbucket repositories
- Package manager auto-detection (npm, pnpm, yarn)
- Auto dependency installation
- Environment file copying (.env)
- Default branch detection
- Pre-flight validation checks
v3.0 Features (New):
- Worktree closing with safety checks
- Intelligent .env synchronization with diff display
- Side-by-side environment variable comparison
- Audit logging (human and machine-readable)
- Backup management with auto-cleanup
- Configuration system
- Worktree listing and management
- Cleanup utilities
- Standalone environment sync between any two worktrees
- Language: TypeScript 5.0+
- Runtime: Node.js 16.0+
- Build Tool: tsc (TypeScript Compiler)
- Dependencies:
- yargs: CLI argument parsing
- chalk: Terminal colors
- inquirer: Interactive prompts
- crypto: Hashing (built-in)
- fs/path: File operations (built-in)
- child_process: Git commands (built-in)
- macOS (primary)
- Linux
- Windows (with Git Bash or WSL)
git-worktree-creator/
βββ src/
β βββ index.ts # Main entry point, CLI router
β β
β βββ commands/
β β βββ create.ts # Workspace creation (refactored)
β β βββ close.ts # Close worktree command
β β βββ sync-env.ts # Sync environment command
β β βββ list.ts # List worktrees command
β β βββ cleanup.ts # Cleanup backups/logs command
β β
β βββ core/
β β βββ WorktreeResolver.ts # Worktree discovery & resolution
β β βββ SafetyChecker.ts # Pre-close safety validation
β β βββ EnvFileParser.ts # .env file parsing
β β βββ EnvDiffer.ts # Environment diff generation
β β βββ EnvSyncer.ts # Environment sync execution
β β βββ WorktreeRemover.ts # Worktree removal logic
β β βββ BranchCleaner.ts # Branch cleanup logic
β β βββ ConfigManager.ts # Configuration management
β β βββ AuditLogger.ts # Audit trail logging
β β βββ BackupManager.ts # Backup creation & cleanup
β β βββ ProjectIdentifier.ts # Project ID generation
β β
β βββ ui/
β β βββ DiffDisplay.ts # Side-by-side diff rendering
β β βββ SyncPrompt.ts # Interactive sync prompts
β β βββ ListDisplay.ts # Worktree list formatting
β β βββ Logger.ts # Colored console output
β β
β βββ types/
β βββ index.ts # All TypeScript interfaces
β
βββ dist/ # Compiled JavaScript output
β
βββ node_modules/ # Dependencies
β
βββ package.json # Project metadata & scripts
βββ package-lock.json # Dependency lock file
βββ tsconfig.json # TypeScript configuration
β
βββ README.md # Project overview
βββ INSTALLATION_GUIDE.md # Installation instructions
βββ CLAUDE.md # AI assistant guidance
βββ SPEC.md # This document
β
βββ docs/
βββ configuration.md # Config file reference
βββ advanced-usage.md # Advanced features
βββ sync-operations.md # Environment sync guide
βββ troubleshooting.md # Common issues
~/.workforge/
βββ config.json # Global configuration
β
βββ projects/
β βββ a1b2c3d4/ # Project ID (SHA-256 hash)
β β βββ .meta.json # Project metadata
β β βββ audit.log # Human-readable audit log
β β βββ sync-history.json # Machine-readable history
β β
β βββ e5f6g7h8/ # Another project
β β βββ .meta.json
β β βββ audit.log
β β βββ sync-history.json
β β
β βββ ...
β
βββ backups/
βββ a1b2c3d4/ # Project ID matches
β βββ .env.backup.2025-10-26T14-30-45
β βββ .env.backup.2025-10-26T15-00-12
β βββ ... (up to 10 backups per project)
β
βββ e5f6g7h8/
β βββ .env.backup.2025-10-27T09-15-33
β
βββ ...
// src/types/index.ts
/**
* Workspace configuration for create command
*/
export interface WorkspaceConfig {
type: string; // Branch type (feat, fix, doc, etc.)
name: string; // Branch name (kebab-case)
base: string; // Base branch to checkout from
yes: boolean; // Skip confirmations
ticketId?: string; // Optional Jira ticket ID
}
/**
* Path configuration for worktree
*/
export interface PathConfig {
repoRoot: string; // Absolute path to main repository
workspaceParent: string; // Parent directory (e.g., ../feat)
workspacePath: string; // Full worktree path
branchName: string; // Git branch name
}
/**
* Information about a worktree
*/
export interface WorktreeInfo {
path: string; // Absolute path to worktree
branchName: string; // Associated branch name
isMainRepo: boolean; // True if this is the main repository
commitHash: string; // Current HEAD commit SHA
remoteUrl?: string; // Git remote origin URL
isLocked: boolean; // Whether worktree is locked
isPrunable: boolean; // Whether worktree can be pruned
}
/**
* Safety check results before closing worktree
*/
export interface SafetyCheckResult {
hasUncommittedChanges: boolean;
uncommittedFiles: string[];
hasUnpushedCommits: boolean;
commitsAhead: number;
commitsBehind: number;
isBranchMerged: boolean;
mergedInto: string[]; // Branches this is merged into
isRemoteBranch: boolean;
remoteName?: string; // e.g., 'origin/feat/user-auth'
isDetachedHead: boolean;
isMergeInProgress: boolean;
isRebaseInProgress: boolean;
}
/**
* Parsed environment variable
*/
export interface EnvVariable {
key: string; // Variable name
value: string; // Variable value
lineNumber: number; // Line number in original file
comment?: string; // Inline comment if present
hasQuotes: boolean; // Whether value was quoted
quoteType?: 'single' | 'double';
}
/**
* Environment file diff result
*/
export interface EnvDiff {
added: EnvVariable[]; // In target, not in source
removed: EnvVariable[]; // In source, not in target
modified: EnvModification[];
unchanged: EnvVariable[];
}
/**
* Modified environment variable
*/
export interface EnvModification {
key: string;
sourceValue: string;
targetValue: string;
sourceLineNumber: number;
targetLineNumber: number;
}
/**
* User's sync decision
*/
export interface SyncDecision {
syncAdded: string[]; // Keys of added vars to sync
syncModified: string[]; // Keys of modified vars to sync
syncRemoved: string[]; // Keys of removed vars to sync (delete)
skipAll: boolean; // User chose to skip sync entirely
}
/**
* Sync operation result
*/
export interface SyncResult {
backupPath?: string;
addedCount: number;
modifiedCount: number;
removedCount: number;
errors: string[];
success: boolean;
}
/**
* Source and target for sync operation
*/
export interface SyncTargets {
source: {
path: string; // Directory path
envPath: string; // Full path to .env file
type: 'main-repo' | 'worktree';
branchName?: string;
};
target: {
path: string;
envPath: string;
type: 'main-repo' | 'worktree';
branchName?: string;
};
}
/**
* Audit log operation record
*/
export interface AuditOperation {
id?: string; // UUID (added by logger)
timestamp: string; // ISO 8601 timestamp
operation: 'create' | 'close' | 'sync';
source: {
path: string;
branch?: string;
commit?: string;
};
target: {
path: string;
branch?: string;
commit?: string;
};
user: string; // System username
changes?: {
added: Array<{key: string; value: string}>;
modified: Array<{key: string; oldValue: string; newValue: string}>;
removed: Array<{key: string; value: string}>;
};
backup?: {
path: string;
size: number;
};
status: 'success' | 'failure' | 'partial';
error?: string;
}
/**
* Project metadata
*/
export interface ProjectMetadata {
projectId: string; // SHA-256 hash of remote URL
remoteUrl: string; // Git remote origin URL
repoPath: string; // Absolute path to repository
repoName: string; // Repository directory name
createdAt: string; // ISO 8601 timestamp
lastAccessed: string; // ISO 8601 timestamp
}
/**
* Backup information
*/
export interface BackupInfo {
name: string; // Filename
path: string; // Absolute path
size: number; // File size in bytes
created: Date; // Creation timestamp
}
/**
* Global configuration
*/
export interface Config {
version: string;
preferences: {
defaultBaseBranch: string;
autoDeleteBranch: boolean;
skipConfirmations: boolean;
packageManager: 'auto' | 'npm' | 'pnpm' | 'yarn';
showExistingWorktrees: boolean;
};
backup: {
enabled: boolean;
maxBackupsPerProject: number;
autoCleanup: boolean;
};
sync: {
createBackupBeforeSync: boolean;
defaultSyncDirection: 'ask' | 'to-main' | 'to-worktree';
};
audit: {
enabled: boolean;
includeVariableValues: boolean;
retentionDays: number;
};
display: {
colorEnabled: boolean;
verboseOutput: boolean;
showProgressIndicators: boolean;
};
}
/**
* Command options for close
*/
export interface CloseOptions {
path?: string;
name?: string;
force: boolean;
deleteBranch?: boolean;
keepBranch?: boolean;
noSync: boolean;
yes: boolean;
dryRun: boolean;
}
/**
* Command options for sync-env
*/
export interface SyncEnvOptions {
from?: string;
to?: string;
fromName?: string;
toName?: string;
between?: string[];
yes: boolean;
dryRun: boolean;
noBackup: boolean;
}
/**
* Command options for list
*/
export interface ListOptions {
all: boolean;
format: 'table' | 'json' | 'simple';
sort: 'name' | 'date' | 'branch' | 'type';
}
/**
* Command options for cleanup
*/
export interface CleanupOptions {
project?: string;
olderThan?: number; // Days
dryRun: boolean;
yes: boolean;
}
/**
* Package manager detection result
*/
export interface PackageManagerInfo {
manager: 'npm' | 'pnpm' | 'yarn';
command: string;
args: string[];
lockFile: string;
}File: src/commands/create.ts
workforge create --type <type> --name <name> [options]
workforge <type> <name> [options] # Shorthand
Options:
-t, --type <type> Branch type (feat, fix, doc, etc.) [required]
-n, --name <name> Branch name in kebab-case [required]
-b, --base <branch> Base branch to checkout from [default: auto-detect]
-j, --ticket <id> Jira ticket ID (format: BZ-12345)
-y, --yes Skip confirmations
--no-install Skip dependency installation
--no-env-copy Skip environment file copying- Load configuration (ConfigManager)
- Validate inputs (type: letters only, name: kebab-case, ticket: format)
- Discover repository (walk up directory tree, handle worktrees)
- Detect default branch (symbolic-ref, common branches, current)
- Detect repository type (check remote URL for bitbucket.juspay.net)
- If internal repo and no ticket: prompt for Jira ticket ID
- Calculate paths (../type/name structure, branch name)
- [NEW] Show existing worktrees if config.showExistingWorktrees
- Run pre-flight checks (Git binary, branch exists, path exists)
- Confirm creation (unless --yes)
- Create worktree (fetch, git worktree add -B)
- Copy environment files (.env only)
- Detect package manager (lock files)
- Install dependencies
- [NEW] Create project metadata if first worktree
- [NEW] Log creation operation to audit
- Show success summary
π Existing worktrees (3):
β’ feat/user-auth (3 days ago) - Clean
β’ fix/memory-leak (2 hours ago) - Modified, 2 commits ahead
β’ doc/api-guide (5 days ago) - Unmerged
Creating new worktree...
- Uses ConfigManager for defaults
- Uses ProjectIdentifier to create/update metadata
- Uses AuditLogger to log creation
- Uses Logger for colored output
File: src/commands/close.ts
workforge close [path] # Close worktree by path
workforge close --name <name> # Close by worktree name
workforge close # Auto-detect current worktree
Options:
-n, --name <name> Close worktree by name
-f, --force Force close with uncommitted changes
-d, --delete-branch Delete branch after removing worktree
-k, --keep-branch Keep branch after removing worktree
--no-sync Skip environment variable sync
-y, --yes Skip all confirmations
--dry-run Preview actions without executing- Load configuration
- Discover worktree:
- Pattern 1: Explicit path provided
- Pattern 2: --name flag provided (search git worktree list)
- Pattern 3: No args (auto-detect from current directory)
- Safety checks:
- Check for uncommitted changes (git status --porcelain)
- Check for unpushed commits (git rev-list @{u}..HEAD)
- Check if branch is merged (git branch --merged)
- Check if branch exists on remote
- Check for detached HEAD
- Check for merge/rebase in progress
- Display warnings:
- Show uncommitted files with status
- Show commit count ahead/behind
- Show merge status
- Show remote status
- Confirm or force:
- If --force: skip confirmation
- Else: prompt user with safety info
- Environment sync (unless --no-sync):
- Parse .env from worktree and main repo
- Generate diff (added, modified, removed)
- Display side-by-side diff
- Interactive line-by-line selection
- Create backup of main repo .env
- Apply sync decisions
- Remove worktree:
- Execute git worktree remove [--force]
- Branch cleanup:
- Determine action: delete, keep, or prompt
- If merged: safe delete (git branch -d)
- If not merged: force delete with confirmation (git branch -D)
- Handle remote branch warning
- Audit logging:
- Log operation with all details
- Include sync changes, backup path, status
- Show summary:
- Worktree removed
- Branch deleted/kept
- Environment synced (counts)
- Backup location
β οΈ Warning: Uncommitted changes detected
Modified files (3):
β’ src/index.ts (modified)
β’ src/utils.ts (modified)
β’ new-file.ts (untracked)
β οΈ Warning: 2 unpushed commits
β οΈ Warning: Branch 'feat/user-auth' is NOT merged into 'main'
β Force close anyway? This may LOSE uncommitted work! [y/N]:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Environment Variable Changes (.env) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β ADDED (3 variables) β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β NEW_API_KEY=sk_live_abc123def456 β
β DEBUG_MODE=true β
β FEATURE_FLAG_X=enabled β
β β
β MODIFIED (2 variables) β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β DATABASE_URL β
β ββ Main Repo βββββββββββββββ¬β Worktree βββββββββββββββββ β
β β postgres://localhost/old β postgres://localhost/new β β
β ββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββ β
β β
β API_ENDPOINT β
β ββ Main Repo βββββββββββββββ¬β Worktree βββββββββββββββββ β
β β https://api.old.com β https://api.new.com β β
β ββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββ β
β β
β REMOVED (1 variable) β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β OLD_FEATURE_FLAG=true β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Sync ADDED variables to main repo? [all/none/select]:
β Sync ADDED variables to main repo? [all/none/select]: select
[β] NEW_API_KEY=sk_live_abc123def456
[β] DEBUG_MODE=true
[ ] FEATURE_FLAG_X=enabled
β Sync MODIFIED variables to main repo? [all/none/select]: select
DATABASE_URL:
Main: postgres://localhost/old
Worktree: postgres://localhost/new
β Use worktree value? [Y/n]: y
API_ENDPOINT:
Main: https://api.old.com
Worktree: https://api.new.com
β Use worktree value? [Y/n]: n
β Remove DELETED variables from main repo? [all/none/select]: none
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Worktree Close Summary β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
Worktree removed: ../feat/user-auth β
β β
Branch deleted: feat/user-auth β
β β
Environment synced: β
β β’ 2 variables added β
β β’ 1 variable modified β
β β’ 0 variables removed β
β π Backup saved: β
β ~/.workforge/backups/a1b2c3d4/.env.backup.2025-10-26... β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
File: src/commands/sync-env.ts
# Source to target patterns
workforge sync-env --from <path> --to <path>
workforge sync-env --from-name <name> --to-name <name>
workforge sync-env --to <path> # From main to worktree
workforge sync-env --from <path> # From worktree to main
# Bidirectional
workforge sync-env --between <path1> <path2>
Options:
--from <path> Source path
--to <path> Target path
--from-name <name> Source worktree name
--to-name <name> Target worktree name
--between <p1> <p2> Compare and choose direction
-y, --yes Auto-accept all changes
--dry-run Preview without executing
--no-backup Skip backup creation- Load configuration
- Resolve source and target:
- If --from and --to: use both
- If only --from: from=worktree, to=main
- If only --to: from=main, to=worktree
- If --between: parse both, offer bidirectional choice
- Support both path and name formats
- Validate .env files exist in both locations
- Parse .env files using EnvFileParser
- Generate diff using EnvDiffer
- Display diff using DiffDisplay (side-by-side)
- Get user decision:
- If --yes: sync all
- If --between: ask direction first, then sync
- Else: interactive line-by-line selection
- Create backup (unless --no-backup)
- Perform sync using EnvSyncer
- Audit logging with all details
- Show summary with counts and backup path
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Comparing: feat/user-auth β feat/payment β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Variables only in feat/user-auth (3): β
β NEW_AUTH_KEY=xyz β
β AUTH_TIMEOUT=5000 β
β JWT_SECRET=secret123 β
β β
β Variables only in feat/payment (2): β
β PAYMENT_API_KEY=abc β
β STRIPE_KEY=sk_test_123 β
β β
β Different values (1): β
β API_ENDPOINT: β
β feat/user-auth: https://api.auth.com β
β feat/payment: https://api.payment.com β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Sync direction:
1) feat/user-auth β feat/payment
2) feat/payment β feat/user-auth
3) Select variables individually
4) Cancel
Choice [1-4]:
File: src/commands/list.ts
workforge list # Current repo worktrees
workforge list --all # All repos' worktrees
Options:
-a, --all Show worktrees from all repos
--format <type> Output format: table, json, simple
--sort <field> Sort by: name, date, branch, type- Load configuration
- Determine scope:
- If --all: get all projects from ~/.workforge/projects
- Else: find current repo and get its worktrees
- Get worktree list:
- Execute git worktree list --porcelain
- Parse output into WorktreeInfo objects
- Enrich with metadata:
- Get branch status (clean, modified, ahead, behind)
- Get age (time since last commit)
- Get merge status
- Sort based on --sort option
- Display based on --format option
π Worktrees for my-repo (5 total)
ββββββββββββ¬ββββββββββββββββββββββββββ¬βββββββββββββββ¬ββββββββββββββ¬βββββββββββββ
β Type β Name β Branch β Age β Status β
ββββββββββββΌββββββββββββββββββββββββββΌβββββββββββββββΌββββββββββββββΌβββββββββββββ€
β feat β user-authentication β feat/user... β 3 days ago β Clean β
β feat β payment-integration β feat/paym... β 1 day ago β Modified β
β fix β memory-leak β fix/memor... β 2 hours ago β Ahead (2) β
β doc β api-guide β doc/api-g... β 5 days ago β Unmerged β
β refactor β code-cleanup β refactor/... β 1 week ago β Modified β
ββββββββββββ΄ββββββββββββββββββββββββββ΄βββββββββββββββ΄ββββββββββββββ΄βββββββββββββ
Main repository: /Users/user/projects/my-repo
{
"repository": "/Users/user/projects/my-repo",
"projectId": "a1b2c3d4",
"worktrees": [
{
"type": "feat",
"name": "user-authentication",
"path": "/Users/user/projects/feat/user-authentication",
"branch": "feat/user-authentication",
"age": "3 days",
"status": "clean",
"commit": "abc123f"
}
]
}File: src/commands/cleanup.ts
workforge cleanup backups # Clean old backups
workforge cleanup logs # Clean old audit logs
workforge cleanup all # Clean both
Options:
--project <name> Clean specific project only
--older-than <days> Clean items older than N days
--dry-run Preview without deleting
-y, --yes Skip confirmation- Load configuration
- Determine scope:
- If --project: find specific project
- Else: all projects in ~/.workforge/projects
- Find items to clean:
- For backups: list all .env.backup.* files
- For logs: parse sync-history.json by date
- Apply --older-than filter if specified
- Show preview:
- List what will be deleted
- Show total size to be freed
- Confirm (unless --yes or --dry-run)
- Perform cleanup (unless --dry-run):
- Delete files
- Update sync-history.json
- Show summary:
- Files deleted count
- Space freed
π§Ή Cleanup Preview
Backups to delete (12):
Project: my-repo (a1b2c3d4)
β’ .env.backup.2025-10-01T10-00-00 (2.1 KB) - 25 days old
β’ .env.backup.2025-10-05T14-30-00 (2.0 KB) - 21 days old
β’ ... (10 more)
Project: another-repo (e5f6g7h8)
β’ ... (no old backups)
Total size to free: 24.3 KB
β Proceed with cleanup? [y/N]:
File: src/core/WorktreeResolver.ts
Discover and resolve worktree information from various input patterns.
class WorktreeResolver {
/**
* Resolve worktree from path, name, or auto-detect
*/
async resolve(path?: string, name?: string): Promise<WorktreeInfo>
/**
* Get all worktrees for a repository
*/
async getWorktrees(repoRoot: string): Promise<WorktreeInfo[]>
/**
* Find worktree by branch name
*/
async findByBranchName(branchName: string): Promise<WorktreeInfo | null>
/**
* Check if current directory is a worktree
*/
async isWorktree(directory: string): Promise<boolean>
/**
* Get main repository path from worktree
*/
async getMainRepo(worktreePath: string): Promise<string>
}-
Pattern 1: Explicit path
- If
pathprovided: validate it exists - Check if it's a worktree or main repo
- Parse git worktree list to get details
- If
-
Pattern 2: Name-based lookup
- If
nameprovided: get current repo root - Execute git worktree list --porcelain
- Search for branch name containing the name
- Return matching worktree
- If
-
Pattern 3: Auto-detect
- Get current working directory
- Check if it's a worktree (has .git file)
- If yes: read .git file to get gitdir
- Parse worktree info from git worktree list
-
Parse git worktree list --porcelain:
worktree /path/to/main HEAD abc123def456 branch refs/heads/main worktree /path/to/worktree HEAD def456abc123 branch refs/heads/feat/user-auth
- Path doesn't exist: throw "Worktree not found at path"
- Name not found: throw "No worktree found with name"
- Not in a Git repo: throw "Not inside a Git repository"
- Multiple matches: throw "Multiple worktrees match, be more specific"
File: src/core/SafetyChecker.ts
Perform comprehensive safety checks before closing a worktree.
class SafetyChecker {
/**
* Run all safety checks on a worktree
*/
async check(worktree: WorktreeInfo): Promise<SafetyCheckResult>
/**
* Check for uncommitted changes
*/
private async checkUncommittedChanges(path: string): Promise<{
hasChanges: boolean;
files: string[];
}>
/**
* Check for unpushed commits
*/
private async checkUnpushedCommits(path: string): Promise<{
hasUnpushed: boolean;
ahead: number;
behind: number;
}>
/**
* Check if branch is merged
*/
private async checkBranchMerged(
repoRoot: string,
branchName: string
): Promise<{
isMerged: boolean;
mergedInto: string[];
}>
/**
* Check if branch exists on remote
*/
private async checkRemoteBranch(
repoRoot: string,
branchName: string
): Promise<{
exists: boolean;
remoteName?: string;
}>
}-
Uncommitted changes:
git -C <worktree-path> status --porcelain
- Parse output: each line is a file
- Extract status (M, A, D, ??) and filename
-
Unpushed commits:
git -C <worktree-path> rev-list --count @{u}..HEAD # ahead git -C <worktree-path> rev-list --count HEAD..@{u} # behind
-
Branch merged:
git -C <repo-root> branch --merged | grep <branch-name>
- If found in output: merged
- Check against main, master, develop
-
Remote branch:
git -C <repo-root> branch -r | grep <branch-name>
-
Git state:
- Detached HEAD: check .git/HEAD file
- Merge in progress: check .git/MERGE_HEAD exists
- Rebase in progress: check .git/rebase-merge exists
File: src/core/EnvFileParser.ts
Parse .env files into structured format, handling all edge cases.
class EnvFileParser {
/**
* Parse .env file into map of variables
*/
parse(filePath: string): Map<string, EnvVariable>
/**
* Convert variable map back to .env format
*/
stringify(vars: Map<string, EnvVariable>): string
/**
* Parse a single line
*/
private parseLine(line: string, lineNumber: number): EnvVariable | null
}-
Comments:
- Lines starting with
#or//are comments - Inline comments:
KEY=value # comment
- Lines starting with
-
Empty lines:
- Skip blank lines
-
KEY=VALUE pairs:
- Split on first
= - Trim whitespace from key
- Handle quoted values:
- Single quotes:
KEY='value' - Double quotes:
KEY="value" - No quotes:
KEY=value
- Single quotes:
- Split on first
-
Multi-line values:
- If value has opening quote but no closing: continue to next line
- Example:
KEY="line1 line2 line3"
-
Special characters:
- Preserve escape sequences in quoted values
- Handle spaces in values
-
Invalid lines:
- Log warning
- Skip line
- Continue parsing
parse(filePath: string): Map<string, EnvVariable> {
const content = readFileSync(filePath, 'utf8');
const lines = content.split('\n');
const variables = new Map<string, EnvVariable>();
let currentLine = 0;
let multilineBuffer: string | null = null;
let multilineStart = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Handle multiline continuation
if (multilineBuffer !== null) {
multilineBuffer += '\n' + line;
if (this.isMultilineComplete(multilineBuffer)) {
const variable = this.parseLine(multilineBuffer, multilineStart);
if (variable) variables.set(variable.key, variable);
multilineBuffer = null;
}
continue;
}
// Skip comments and empty lines
if (this.isComment(line) || line.trim() === '') {
continue;
}
// Check if line starts multiline value
if (this.startsMultiline(line)) {
multilineBuffer = line;
multilineStart = i + 1;
continue;
}
// Parse single line
const variable = this.parseLine(line, i + 1);
if (variable) {
variables.set(variable.key, variable);
}
}
return variables;
}File: src/core/EnvDiffer.ts
Compare two sets of environment variables and generate diff.
class EnvDiffer {
/**
* Compare two .env files and generate diff
*/
compare(
sourcePath: string,
targetPath: string
): EnvDiff
/**
* Compare two parsed variable maps
*/
compareVariables(
source: Map<string, EnvVariable>,
target: Map<string, EnvVariable>
): EnvDiff
}compareVariables(source, target): EnvDiff {
const added: EnvVariable[] = [];
const removed: EnvVariable[] = [];
const modified: EnvModification[] = [];
const unchanged: EnvVariable[] = [];
// Find added and modified
for (const [key, targetVar] of target) {
if (!source.has(key)) {
added.push(targetVar);
} else {
const sourceVar = source.get(key)!;
if (sourceVar.value !== targetVar.value) {
modified.push({
key,
sourceValue: sourceVar.value,
targetValue: targetVar.value,
sourceLineNumber: sourceVar.lineNumber,
targetLineNumber: targetVar.lineNumber
});
} else {
unchanged.push(targetVar);
}
}
}
// Find removed
for (const [key, sourceVar] of source) {
if (!target.has(key)) {
removed.push(sourceVar);
}
}
return { added, removed, modified, unchanged };
}File: src/core/EnvSyncer.ts
Apply sync decisions to update target .env file.
class EnvSyncer {
/**
* Sync environment variables based on decision
*/
async sync(
sourcePath: string,
targetPath: string,
decision: SyncDecision
): Promise<SyncResult>
/**
* Merge variables preserving comments and formatting
*/
private mergeVariables(
targetVars: Map<string, EnvVariable>,
sourceVars: Map<string, EnvVariable>,
decision: SyncDecision
): Map<string, EnvVariable>
}- Parse both files
- Create backup of target
- Apply sync decision:
- For each key in
decision.syncAdded: add to target - For each key in
decision.syncModified: update in target - For each key in
decision.syncRemoved: remove from target
- For each key in
- Preserve formatting:
- Keep comments above variables
- Keep inline comments
- Maintain empty lines between sections
- Write updated file
- Return result with counts
File: src/core/ConfigManager.ts
const DEFAULT_CONFIG: Config = {
version: '3.0.0',
preferences: {
defaultBaseBranch: 'main',
autoDeleteBranch: false,
skipConfirmations: false,
packageManager: 'auto',
showExistingWorktrees: true
},
backup: {
enabled: true,
maxBackupsPerProject: 10,
autoCleanup: true
},
sync: {
createBackupBeforeSync: true,
defaultSyncDirection: 'ask'
},
audit: {
enabled: true,
includeVariableValues: true,
retentionDays: 90
},
display: {
colorEnabled: true,
verboseOutput: false,
showProgressIndicators: true
}
};class ConfigManager {
private configPath: string;
constructor() {
this.configPath = path.join(
os.homedir(),
'.workforge',
'config.json'
);
}
/**
* Load configuration with defaults
*/
load(): Config {
if (!existsSync(this.configPath)) {
return DEFAULT_CONFIG;
}
const userConfig = JSON.parse(readFileSync(this.configPath, 'utf8'));
return this.merge(DEFAULT_CONFIG, userConfig);
}
/**
* Save configuration
*/
save(config: Config): void {
mkdirSync(path.dirname(this.configPath), { recursive: true});
writeFileSync(this.configPath, JSON.stringify(config, null, 2));
}
/**
* Get nested config value
*/
get(key: string): any {
const config = this.load();
return this.getNestedValue(config, key);
}
/**
* Set nested config value
*/
set(key: string, value: any): void {
const config = this.load();
this.setNestedValue(config, key, value);
this.save(config);
}
/**
* Deep merge two config objects
*/
private merge(defaults: Config, user: Partial<Config>): Config {
// Deep merge implementation
}
/**
* Get value from nested path (e.g., 'backup.maxBackupsPerProject')
*/
private getNestedValue(obj: any, path: string): any {
return path.split('.').reduce((current, key) => current?.[key], obj);
}
/**
* Set value at nested path
*/
private setNestedValue(obj: any, path: string, value: any): void {
const keys = path.split('.');
const lastKey = keys.pop()!;
const target = keys.reduce((current, key) => {
if (!current[key]) current[key] = {};
return current[key];
}, obj);
target[lastKey] = value;
}
}Commands should access config via ConfigManager:
const configManager = new ConfigManager();
const config = configManager.load();
// Use config values
const defaultBranch = config.preferences.defaultBaseBranch;
const maxBackups = config.backup.maxBackupsPerProject;File: src/core/AuditLogger.ts
class AuditLogger {
private projectId: string;
private projectDir: string;
private config: Config;
constructor(repoRoot: string) {
this.projectId = ProjectIdentifier.generateId(repoRoot);
this.projectDir = ProjectIdentifier.getProjectDir(repoRoot);
this.config = new ConfigManager().load();
}
/**
* Log an operation
*/
async log(operation: AuditOperation): Promise<void> {
if (!this.config.audit.enabled) return;
// Update project metadata
ProjectIdentifier.updateMetadata(operation.source.path);
// Append to human-readable log
await this.appendToLog(operation);
// Add to machine-readable history
await this.addToHistory(operation);
// Auto-cleanup old logs
await this.cleanupOldLogs();
}
/**
* Format and append to audit.log
*/
private async appendToLog(op: AuditOperation): Promise<void> {
const logPath = path.join(this.projectDir, 'audit.log');
const entry = this.formatLogEntry(op);
mkdirSync(this.projectDir, { recursive: true });
appendFileSync(logPath, entry + '\n\n');
}
/**
* Format operation for human-readable log
*/
private formatLogEntry(op: AuditOperation): string {
const lines: string[] = [];
lines.push(`[${op.timestamp}] ${op.operation.toUpperCase()} ${op.source.branch} β ${op.target.branch}`);
lines.push(` Operation: ${op.operation}`);
lines.push(` Source: ${op.source.path}`);
lines.push(` Target: ${op.target.path}`);
if (op.source.branch && op.target.branch) {
lines.push(` Branch: ${op.source.branch} β ${op.target.branch}`);
}
if (op.source.commit && op.target.commit) {
lines.push(` Commit: ${op.source.commit} β ${op.target.commit}`);
}
lines.push(` User: ${op.user}`);
if (op.changes) {
lines.push(` Changes:`);
for (const add of op.changes.added) {
const value = this.config.audit.includeVariableValues
? `=${add.value}`
: '';
lines.push(` + ${add.key}${value}`);
}
for (const mod of op.changes.modified) {
const values = this.config.audit.includeVariableValues
? `: ${mod.oldValue} β ${mod.newValue}`
: '';
lines.push(` ~ ${mod.key}${values}`);
}
for (const rem of op.changes.removed) {
const value = this.config.audit.includeVariableValues
? `=${rem.value}`
: '';
lines.push(` - ${rem.key}${value}`);
}
}
if (op.backup) {
lines.push(` Backup: ${op.backup.path}`);
}
lines.push(` Status: ${op.status.toUpperCase()}`);
if (op.error) {
lines.push(` Error: ${op.error}`);
}
return lines.join('\n');
}
/**
* Add to sync-history.json
*/
private async addToHistory(op: AuditOperation): Promise<void> {
const historyPath = path.join(this.projectDir, 'sync-history.json');
let history = { version: '3.0.0', operations: [] as any[] };
if (existsSync(historyPath)) {
history = JSON.parse(readFileSync(historyPath, 'utf8'));
}
history.operations.push({
id: crypto.randomUUID(),
...op
});
mkdirSync(this.projectDir, { recursive: true });
writeFileSync(historyPath, JSON.stringify(history, null, 2));
}
/**
* Clean up logs older than retentionDays
*/
private async cleanupOldLogs(): Promise<void> {
if (this.config.audit.retentionDays === 0) return;
const historyPath = path.join(this.projectDir, 'sync-history.json');
if (!existsSync(historyPath)) return;
const history = JSON.parse(readFileSync(historyPath, 'utf8'));
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - this.config.audit.retentionDays);
history.operations = history.operations.filter((op: AuditOperation) => {
const opDate = new Date(op.timestamp);
return opDate >= cutoffDate;
});
writeFileSync(historyPath, JSON.stringify(history, null, 2));
}
}File: src/core/BackupManager.ts
class BackupManager {
private projectId: string;
private backupDir: string;
private config: Config;
constructor(repoRoot: string) {
this.projectId = ProjectIdentifier.generateId(repoRoot);
this.backupDir = path.join(
os.homedir(),
'.workforge',
'backups',
this.projectId
);
this.config = new ConfigManager().load();
}
/**
* Create a backup of .env file
*/
async createBackup(envPath: string): Promise<string> {
if (!this.config.backup.enabled) {
throw new Error('Backups are disabled in configuration');
}
mkdirSync(this.backupDir, { recursive: true });
const timestamp = new Date()
.toISOString()
.replace(/[:.]/g, '-')
.replace('Z', '');
const backupName = `.env.backup.${timestamp}`;
const backupPath = path.join(this.backupDir, backupName);
copyFileSync(envPath, backupPath);
// Auto-cleanup if enabled
if (this.config.backup.autoCleanup) {
await this.cleanupOldBackups();
}
return backupPath;
}
/**
* Clean up old backups, keep last N
*/
async cleanupOldBackups(): Promise<void> {
const maxBackups = this.config.backup.maxBackupsPerProject;
if (!existsSync(this.backupDir)) return;
const backups = readdirSync(this.backupDir)
.filter(f => f.startsWith('.env.backup.'))
.map(f => ({
name: f,
path: path.join(this.backupDir, f),
stat: statSync(path.join(this.backupDir, f))
}))
.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs); // Newest first
// Delete backups beyond maxBackups
for (let i = maxBackups; i < backups.length; i++) {
unlinkSync(backups[i].path);
}
}
/**
* List all backups for this project
*/
listBackups(): BackupInfo[] {
if (!existsSync(this.backupDir)) return [];
return readdirSync(this.backupDir)
.filter(f => f.startsWith('.env.backup.'))
.map(f => {
const filePath = path.join(this.backupDir, f);
const stat = statSync(filePath);
return {
name: f,
path: filePath,
size: stat.size,
created: stat.mtime
};
})
.sort((a, b) => b.created.getTime() - a.created.getTime());
}
/**
* Restore a specific backup
*/
async restore(backupPath: string, targetPath: string): Promise<void> {
if (!existsSync(backupPath)) {
throw new Error(`Backup not found: ${backupPath}`);
}
// Create backup of current file before restoring
if (existsSync(targetPath)) {
const tempBackup = await this.createBackup(targetPath);
console.log(`Created safety backup: ${tempBackup}`);
}
copyFileSync(backupPath, targetPath);
}
/**
* Delete a specific backup
*/
async deleteBackup(backupPath: string): Promise<void> {
if (!existsSync(backupPath)) {
throw new Error(`Backup not found: ${backupPath}`);
}
unlinkSync(backupPath);
}
}File: src/ui/DiffDisplay.ts
Render beautiful side-by-side environment variable diffs.
class DiffDisplay {
/**
* Display diff in side-by-side format
*/
show(diff: EnvDiff, targets: SyncTargets): void {
this.printHeader(targets);
if (diff.added.length > 0) {
this.printAdded(diff.added);
}
if (diff.modified.length > 0) {
this.printModified(diff.modified);
}
if (diff.removed.length > 0) {
this.printRemoved(diff.removed);
}
if (diff.unchanged.length > 0) {
this.printUnchangedSummary(diff.unchanged.length);
}
this.printFooter();
}
private printHeader(targets: SyncTargets): void {
console.log(chalk.bold('\nβββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ'));
console.log(chalk.bold('β Environment Variable Changes (.env) β'));
console.log(chalk.bold('βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€'));
console.log(chalk.gray(` Source: ${targets.source.path}`));
console.log(chalk.gray(` Target: ${targets.target.path}`));
console.log(chalk.bold('βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ'));
}
private printAdded(added: EnvVariable[]): void {
console.log(chalk.green(`\nADDED (${added.length} variables)`));
console.log(chalk.gray('β'.repeat(65)));
for (const variable of added) {
console.log(chalk.green(` + ${variable.key}=${variable.value}`));
}
}
private printModified(modified: EnvModification[]): void {
console.log(chalk.yellow(`\nMODIFIED (${modified.length} variables)`));
console.log(chalk.gray('β'.repeat(65)));
for (const mod of modified) {
console.log(chalk.yellow(`\n ${mod.key}`));
const sourceLabel = chalk.gray('Source:');
const targetLabel = chalk.gray('Target:');
const maxValueLength = 40;
const sourceValue = this.truncate(mod.sourceValue, maxValueLength);
const targetValue = this.truncate(mod.targetValue, maxValueLength);
console.log(` ββ ${sourceLabel} ${'β'.repeat(15)}β¬β ${targetLabel} ${'β'.repeat(15)}β`);
console.log(` β ${this.pad(sourceValue, 25)} β ${this.pad(targetValue, 25)} β`);
console.log(` β${'β'.repeat(26)}β΄${'β'.repeat(26)}β`);
}
}
private printRemoved(removed: EnvVariable[]): void {
console.log(chalk.red(`\nREMOVED (${removed.length} variables)`));
console.log(chalk.gray('β'.repeat(65)));
for (const variable of removed) {
console.log(chalk.red(` - ${variable.key}=${variable.value}`));
}
}
private printUnchangedSummary(count: number): void {
console.log(chalk.gray(`\n${count} variables unchanged`));
}
private printFooter(): void {
console.log('');
}
private truncate(str: string, maxLength: number): string {
if (str.length <= maxLength) return str;
return str.substring(0, maxLength - 3) + '...';
}
private pad(str: string, length: number): string {
return str + ' '.repeat(Math.max(0, length - str.length));
}
}File: src/ui/SyncPrompt.ts
Interactive prompts for selecting which variables to sync.
class SyncPrompt {
/**
* Prompt user for sync decisions
*/
async prompt(diff: EnvDiff, autoYes: boolean = false): Promise<SyncDecision> {
if (autoYes) {
return this.autoAcceptAll(diff);
}
const decision: SyncDecision = {
syncAdded: [],
syncModified: [],
syncRemoved: [],
skipAll: false
};
// Prompt for added variables
if (diff.added.length > 0) {
decision.syncAdded = await this.promptAdded(diff.added);
}
// Prompt for modified variables
if (diff.modified.length > 0) {
decision.syncModified = await this.promptModified(diff.modified);
}
// Prompt for removed variables
if (diff.removed.length > 0) {
decision.syncRemoved = await this.promptRemoved(diff.removed);
}
return decision;
}
private async promptAdded(added: EnvVariable[]): Promise<string[]> {
const { choice } = await inquirer.prompt([
{
type: 'list',
name: 'choice',
message: `Sync ADDED variables to target? (${added.length} total)`,
choices: [
{ name: 'All - sync all added variables', value: 'all' },
{ name: 'None - skip all added variables', value: 'none' },
{ name: 'Select - choose which to sync', value: 'select' }
]
}
]);
if (choice === 'all') {
return added.map(v => v.key);
}
if (choice === 'none') {
return [];
}
// Select individual variables
const { selected } = await inquirer.prompt([
{
type: 'checkbox',
name: 'selected',
message: 'Select variables to sync:',
choices: added.map(v => ({
name: `${v.key}=${v.value}`,
value: v.key,
checked: true
}))
}
]);
return selected;
}
private async promptModified(modified: EnvModification[]): Promise<string[]> {
const { choice } = await inquirer.prompt([
{
type: 'list',
name: 'choice',
message: `Sync MODIFIED variables to target? (${modified.length} total)`,
choices: [
{ name: 'All - sync all modifications', value: 'all' },
{ name: 'None - keep target values', value: 'none' },
{ name: 'Select - choose which to sync', value: 'select' }
]
}
]);
if (choice === 'all') {
return modified.map(m => m.key);
}
if (choice === 'none') {
return [];
}
// Select individual modifications
const selected: string[] = [];
for (const mod of modified) {
console.log(chalk.yellow(`\n${mod.key}:`));
console.log(chalk.gray(` Source: ${mod.sourceValue}`));
console.log(chalk.gray(` Target: ${mod.targetValue}`));
const { useSource } = await inquirer.prompt([
{
type: 'confirm',
name: 'useSource',
message: 'Use source value?',
default: true
}
]);
if (useSource) {
selected.push(mod.key);
}
}
return selected;
}
private async promptRemoved(removed: EnvVariable[]): Promise<string[]> {
const { choice } = await inquirer.prompt([
{
type: 'list',
name: 'choice',
message: `Remove DELETED variables from target? (${removed.length} total)`,
choices: [
{ name: 'All - remove all from target', value: 'all' },
{ name: 'None - keep in target', value: 'none' },
{ name: 'Select - choose which to remove', value: 'select' }
]
}
]);
if (choice === 'all') {
return removed.map(v => v.key);
}
if (choice === 'none') {
return [];
}
// Select individual variables to remove
const { selected } = await inquirer.prompt([
{
type: 'checkbox',
name: 'selected',
message: 'Select variables to remove from target:',
choices: removed.map(v => ({
name: `${v.key}=${v.value}`,
value: v.key,
checked: false
}))
}
]);
return selected;
}
private autoAcceptAll(diff: EnvDiff): SyncDecision {
return {
syncAdded: diff.added.map(v => v.key),
syncModified: diff.modified.map(m => m.key),
syncRemoved: diff.removed.map(v => v.key),
skipAll: false
};
}
}START
β
ββ Load Configuration
β
ββ Discover Worktree
β ββ Pattern 1: Explicit path provided?
β β ββ Validate path exists
β ββ Pattern 2: --name flag provided?
β β ββ Search git worktree list
β ββ Pattern 3: Auto-detect
β ββ Check current directory
β
ββ Safety Checks
β ββ git status --porcelain (uncommitted changes)
β ββ git rev-list @{u}..HEAD (unpushed commits)
β ββ git branch --merged (merge status)
β ββ git branch -r (remote branch)
β ββ Check for detached HEAD, merge/rebase in progress
β
ββ Display Warnings
β ββ List uncommitted files
β ββ Show commit counts ahead/behind
β ββ Show merge status
β ββ Show remote branch info
β
ββ Confirm or Force?
β ββ If --force: continue
β ββ Else: prompt user
β ββ If declined: EXIT
β
ββ Environment Sync (unless --no-sync)
β ββ Parse .env from worktree (EnvFileParser)
β ββ Parse .env from main repo (EnvFileParser)
β ββ Generate diff (EnvDiffer)
β β ββ Added variables
β β ββ Modified variables
β β ββ Removed variables
β ββ Display side-by-side diff (DiffDisplay)
β ββ Interactive selection (SyncPrompt)
β β ββ Prompt for added: all/none/select
β β ββ Prompt for modified: all/none/select
β β ββ Prompt for removed: all/none/select
β ββ Create backup (BackupManager)
β β ββ Generate timestamp
β β ββ Copy .env to backup dir
β β ββ Auto-cleanup old backups (keep last 10)
β ββ Apply sync (EnvSyncer)
β ββ Add selected variables
β ββ Update modified variables
β ββ Remove selected variables
β ββ Write updated .env
β
ββ Remove Worktree
β ββ git worktree remove <path>
β ββ If --force: git worktree remove --force <path>
β
ββ Branch Cleanup
β ββ Determine action:
β β ββ If --delete-branch: delete
β β ββ If --keep-branch: keep
β β ββ Else: prompt based on merge status
β ββ If delete:
β β ββ If merged: git branch -d <branch>
β β ββ If not merged: confirm β git branch -D <branch>
β ββ If remote branch: warn user
β
ββ Audit Logging
β ββ Update project metadata
β ββ Append to audit.log (human-readable)
β ββ Add to sync-history.json (machine-readable)
β ββ Auto-cleanup old logs (based on retentionDays)
β
ββ Show Summary
β ββ Worktree removed: path
β ββ Branch: deleted/kept
β ββ Environment synced: counts
β ββ Backup saved: path
β
END
START
β
ββ Load Configuration
β
ββ Resolve Source and Target
β ββ Pattern 1: --from and --to provided
β ββ Pattern 2: Only --from (to=main repo)
β ββ Pattern 3: Only --to (from=main repo)
β ββ Pattern 4: --between (bidirectional)
β ββ Parse both paths, offer direction choice
β
ββ Validate .env Files
β ββ Check source .env exists
β ββ Check target .env exists
β ββ If missing: error
β
ββ Parse .env Files
β ββ Parse source (EnvFileParser)
β ββ Parse target (EnvFileParser)
β
ββ Generate Diff
β ββ Compare variables (EnvDiffer)
β
ββ Display Diff
β ββ Side-by-side comparison (DiffDisplay)
β
ββ Get User Decision
β ββ If --yes: auto-accept all
β ββ If --between: ask direction first
β ββ Else: interactive selection (SyncPrompt)
β
ββ Create Backup (unless --no-backup)
β ββ BackupManager.createBackup(target)
β
ββ Perform Sync
β ββ EnvSyncer.sync(source, target, decision)
β
ββ Audit Logging
β ββ AuditLogger.log(operation)
β
ββ Show Summary
β ββ Variables added: count
β ββ Variables modified: count
β ββ Variables removed: count
β ββ Backup saved: path
β
END
-
User Input Errors:
- Invalid command syntax
- Invalid option values
- Missing required arguments
- Invalid file paths
-
Git Errors:
- Not in a Git repository
- Git command failures
- Branch conflicts
- Worktree locked
-
File System Errors:
- File not found
- Permission denied
- Disk full
- File already exists
-
Parsing Errors:
- Malformed .env file
- Invalid variable syntax
- Encoding issues
-
Configuration Errors:
- Invalid config JSON
- Missing config values
- Type mismatches
// Graceful error handling with user-friendly messages
try {
// Operation
} catch (error) {
if (error instanceof GitError) {
console.error(chalk.red('β Git error:'), error.message);
console.log(chalk.gray('Hint:'), error.hint);
} else if (error instanceof FileSystemError) {
console.error(chalk.red('β File system error:'), error.message);
} else {
console.error(chalk.red('β Unexpected error:'), error);
}
process.exit(1);
}class GitError extends Error {
constructor(message: string, public hint?: string) {
super(message);
this.name = 'GitError';
}
}
class FileSystemError extends Error {
constructor(message: string, public path?: string) {
super(message);
this.name = 'FileSystemError';
}
}
class ParseError extends Error {
constructor(message: string, public line?: number) {
super(message);
this.name = 'ParseError';
}
}// EnvFileParser.test.ts
describe('EnvFileParser', () => {
test('parses simple KEY=VALUE', () => {});
test('parses quoted values', () => {});
test('parses multiline values', () => {});
test('handles comments', () => {});
test('handles inline comments', () => {});
test('handles malformed lines', () => {});
});
// EnvDiffer.test.ts
describe('EnvDiffer', () => {
test('detects added variables', () => {});
test('detects removed variables', () => {});
test('detects modified variables', () => {});
test('handles empty files', () => {});
});
// WorktreeResolver.test.ts
describe('WorktreeResolver', () => {
test('resolves by explicit path', () => {});
test('resolves by name', () => {});
test('auto-detects from current directory', () => {});
test('handles main repository', () => {});
test('throws when not in repo', () => {});
});// close.integration.test.ts
describe('Close Command Integration', () => {
test('closes worktree with clean state', () => {});
test('closes worktree with uncommitted changes (force)', () => {});
test('syncs environment variables', () => {});
test('deletes merged branch', () => {});
test('keeps unmerged branch with confirmation', () => {});
});
// sync-env.integration.test.ts
describe('Sync-Env Command Integration', () => {
test('syncs from worktree to main', () => {});
test('syncs from main to worktree', () => {});
test('syncs between two worktrees', () => {});
test('creates backup before sync', () => {});
});Add sections for:
- New v3.0 features
- Command reference (brief, link to docs)
- Configuration overview (link to docs/configuration.md)
- Examples of close and sync-env
Complete reference of:
- Config file location
- All configuration options
- Default values
- Examples
Guide covering:
- How environment sync works
- Close command with sync
- Sync-env command patterns
- Diff display explanation
- Conflict resolution strategies
- Backup and recovery
Topics:
- List command usage
- Cleanup operations
- Multiple worktree management
- Batch operations
- CI/CD integration
Common issues:
- Sync conflicts
- Permission errors
- Git state issues
- Configuration problems
- Debug mode
Add:
- New architecture (commands, core, ui)
- Configuration system
- Audit logging system
- Backup system
- Key algorithms (env parsing, diffing, syncing)
(See main plan above - phases 1-9)
- Empty .env files: Handle gracefully, show message
- Binary .env files: Detect and abort with error
- Very large .env files: Parse in chunks, show progress
- Concurrent modifications: Detect via checksum, re-diff
- Locked files: Retry with timeout
- Multiple worktrees same branch: Warn and require explicit path
- Detached HEAD: Warn but allow with confirmation
- Merge/rebase in progress: Block close, show clear message
- No internet (remote checks): Continue with warning
- Remote renamed: Detect and show old/new names
-
Variable values in audit logs:
- Config option:
audit.includeVariableValues - Default: true (user aware of logging)
- Sensitive values (API keys, passwords) are logged
- User should secure ~/.workforge directory (chmod 700)
- Config option:
-
Backup file permissions:
- Create with restrictive permissions (0600)
- Warn if .env contains obvious secrets
-
Config file security:
- Validate JSON to prevent injection
- Sanitize file paths
-
Command injection:
- Use execFileSync/spawnSync with array args
- Never use shell: true
- Validate all user inputs
All v2.0 commands continue to work:
workforge --type feat --name user-auth # Still works
workforge -t fix -n bug-123 # Still worksNew commands don't affect existing workflows:
close- New commandsync-env- New commandlist- New commandcleanup- New command
On first run of v3.0:
- Create ~/.workforge directory
- Create default config.json
- Show welcome message explaining new features
- Migrate any existing worktrees to new audit system (optional)
Document Status: Complete - Ready for Implementation Next Steps: Create detailed TODO list and begin Phase 1