jettypod 4.1.2 → 4.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.nvmrc +1 -0
- package/docs/COMPLETE-TESTING-STRATEGY.md +970 -0
- package/docs/DECISIONS.md +10 -12
- package/docs/NODE_VERSION.md +83 -0
- package/docs/TDD-INFRASTRUCTURE-STRATEGY.md +1374 -0
- package/docs/TESTING-FOR-NON-ENGINEERS.md +1588 -0
- package/docs/TESTING-STRATEGY-AUDIT.md +698 -0
- package/hooks/post-checkout +17 -0
- package/hooks/post-merge +17 -0
- package/hooks/pre-commit +30 -0
- package/jettypod.js +259 -120
- package/lib/coverage-tracker.js +218 -0
- package/lib/database.js +2 -0
- package/lib/db-export.js +192 -0
- package/lib/db-import.js +193 -0
- package/lib/external-transition-handler.js +32 -0
- package/lib/git-hook-helpers.js +174 -0
- package/lib/git-root.js +90 -0
- package/lib/infrastructure-chore-generator.js +45 -0
- package/lib/install-hooks.js +52 -0
- package/lib/jettypod-backup.js +238 -0
- package/lib/merge-lock.js +193 -0
- package/lib/migrations/012-add-worktree-path.js +38 -0
- package/lib/migrations/013-worktrees-table.js +86 -0
- package/lib/migrations/014-migrate-worktree-data.js +161 -0
- package/lib/migrations/015-merge-locks-table.js +67 -0
- package/lib/pattern-finder.js +152 -0
- package/lib/process-manager.js +140 -0
- package/lib/production-standards-reader.js +13 -2
- package/lib/production-standards-writer.js +85 -0
- package/lib/skills/feature-planning/dry-run-validator.js +135 -0
- package/lib/skills/feature-planning/validation-formatter.js +160 -0
- package/lib/smart-conflict-detection.js +168 -0
- package/lib/smart-fetch-rebase.js +614 -0
- package/lib/step-definition-parser.js +76 -0
- package/lib/unit-test-generator.js +232 -0
- package/lib/verification-command-generator.js +66 -0
- package/lib/worktree-diagnostics.js +413 -0
- package/lib/worktree-facade.js +174 -0
- package/lib/worktree-manager.js +636 -0
- package/lib/worktree-reconciler.js +429 -0
- package/package.json +30 -3
- package/skills-templates/external-transition/SKILL.md +34 -3
- package/skills-templates/feature-planning/SKILL.md +190 -24
- package/skills-templates/production-mode/SKILL.md +127 -9
- package/skills-templates/speed-mode/SKILL.md +454 -51
- package/skills-templates/stable-mode/SKILL.md +285 -76
- package/.claude/PROTECT_SKILLS.md +0 -28
- package/.claude/settings.json +0 -24
- package/.claude/settings.local.json +0 -16
- package/.claude/skills/epic-planning/SKILL.md +0 -297
- package/.claude/skills/external-transition/SKILL.md +0 -384
- package/.claude/skills/feature-planning/SKILL.md +0 -464
- package/.claude/skills/production-mode/SKILL.md +0 -369
- package/.claude/skills/speed-mode/SKILL.md +0 -481
- package/.claude/skills/stable-mode/SKILL.md +0 -713
- package/.claude/skills.backup-2025-11-10T23-33-09-368Z/epic-planning/SKILL.md +0 -297
- package/.claude/skills.backup-2025-11-10T23-33-09-368Z/feature-planning/SKILL.md +0 -464
- package/.claude/skills.backup-2025-11-10T23-33-09-368Z/speed-mode/SKILL.md +0 -467
- package/.claude/skills.backup-2025-11-10T23-33-09-368Z/stable-mode/SKILL.md +0 -673
- package/.claude/skills.backup-2025-11-11T16-15-10-070Z/epic-discover/SKILL.md +0 -297
- package/.claude/skills.backup-2025-11-11T16-42-43-212Z/epic-planning/SKILL.md +0 -297
- package/.claude/skills.backup-2025-11-11T16-42-43-212Z/feature-planning/SKILL.md +0 -464
- package/.claude/skills.backup-2025-11-11T16-42-43-212Z/speed-mode/SKILL.md +0 -467
- package/.claude/skills.backup-2025-11-11T16-42-43-212Z/stable-mode/SKILL.md +0 -673
- package/.claude/skills.backup-2025-11-11T17-06-09-783Z/epic-planning/SKILL.md +0 -297
- package/.claude/skills.backup-2025-11-11T17-06-09-783Z/feature-planning/SKILL.md +0 -464
- package/.claude/skills.backup-2025-11-11T17-06-09-783Z/speed-mode/SKILL.md +0 -467
- package/.claude/skills.backup-2025-11-11T17-06-09-783Z/stable-mode/SKILL.md +0 -673
- package/.devpod/current-work.json +0 -10
- package/.devpod/work.db +0 -0
- package/.github/workflows/test-safety.yml +0 -85
- package/.jettypod/config.json +0 -5
- package/.jettypod/current-work.json +0 -10
- package/.jettypod/hooks/README.md +0 -77
- package/.jettypod/hooks/protect-claude-md.js +0 -338
- package/.jettypod/test-work.db +0 -0
- package/.jettypod/work.db +0 -0
- package/CLAUDE.md +0 -49
- package/SPEED-STABLE-AUDIT.md +0 -853
- package/SYSTEM-BEHAVIOR.md +0 -2199
- package/TEST_SAFETY_AUDIT.md +0 -314
- package/TEST_SAFETY_IMPLEMENTATION.md +0 -97
- package/cucumber-report.html +0 -45
- package/dist/devpod-linux +0 -0
- package/dist/devpod-macos +0 -0
- package/dist/devpod-win.exe +0 -0
- package/docs/features/jettypod-standards-explained.md +0 -543
- package/docs/features/standards-inventory.md +0 -257
- package/features/auto-generate-production-chores.feature +0 -13
- package/features/backlog-command.feature +0 -26
- package/features/backlog-filtering-production.feature +0 -10
- package/features/claude-md-protection/steps.js +0 -498
- package/features/decisions/index.js +0 -490
- package/features/decisions/index.test.js +0 -208
- package/features/fix-text-wrapping.feature +0 -42
- package/features/git-hooks/git-hooks.feature +0 -30
- package/features/git-hooks/index.js +0 -93
- package/features/git-hooks/index.test.js +0 -137
- package/features/git-hooks/post-commit +0 -56
- package/features/git-hooks/post-merge +0 -47
- package/features/git-hooks/pre-commit +0 -28
- package/features/git-hooks/simple-steps.js +0 -53
- package/features/git-hooks/simple-test.feature +0 -10
- package/features/git-hooks/steps.js +0 -196
- package/features/jettypod-update-command.feature +0 -46
- package/features/mode-prompts/index.js +0 -95
- package/features/mode-prompts/simple-steps.js +0 -44
- package/features/mode-prompts/simple-test.feature +0 -9
- package/features/mode-prompts/validation.test.js +0 -120
- package/features/multiple-claude-instances.feature +0 -121
- package/features/production-mode-skill.feature +0 -121
- package/features/refactor-mode/steps.js +0 -217
- package/features/refactor-mode.feature +0 -49
- package/features/simplify-external-transition.feature +0 -166
- package/features/skills-update/index.test.js +0 -216
- package/features/step_definitions/backlog-command.steps.js +0 -37
- package/features/step_definitions/fix-text-wrapping.steps.js +0 -271
- package/features/step_definitions/multiple-claude-instances.steps.js +0 -621
- package/features/step_definitions/production-mode-skill.steps.js +0 -862
- package/features/step_definitions/simplify-external-transition.steps.js +0 -370
- package/features/step_definitions/terminal-logo.steps.js +0 -145
- package/features/step_definitions/update-command.steps.js +0 -183
- package/features/support/hooks.js +0 -9
- package/features/terminal-logo/index.js +0 -39
- package/features/terminal-logo/terminal-logo.feature +0 -30
- package/features/update-command/index.js +0 -181
- package/features/update-command/index.test.js +0 -225
- package/features/work-commands/bug-workflow-display.feature +0 -22
- package/features/work-commands/index.js +0 -498
- package/features/work-commands/simple-steps.js +0 -69
- package/features/work-commands/stable-tests.feature +0 -57
- package/features/work-commands/steps.js +0 -1174
- package/features/work-commands/validation.test.js +0 -88
- package/features/work-commands/work-commands.feature +0 -13
- package/features/work-tracking/discovery-validation.test.js +0 -228
- package/features/work-tracking/index.js +0 -1921
- package/features/work-tracking/mode-required.feature +0 -112
- package/features/work-tracking/phase-tracking.test.js +0 -482
- package/features/work-tracking/prototype-tracking.test.js +0 -485
- package/features/work-tracking/tree-view.test.js +0 -310
- package/features/work-tracking/work-set-mode.feature +0 -71
- package/features/work-tracking/work-start-mode.feature +0 -88
- package/full-test.txt +0 -0
- package/lib/bug-workflow.test.js +0 -177
- package/lib/claudemd.test.js +0 -195
- package/lib/config.test.js +0 -511
- package/lib/constants.test.js +0 -164
- package/lib/current-work.test.js +0 -146
- package/lib/database-project-config.test.js +0 -111
- package/lib/database.test.js +0 -106
- package/lib/decisions-generator.test.js +0 -457
- package/lib/decisions-helpers.test.js +0 -310
- package/lib/git-coordinator.js +0 -167
- package/lib/git.test.js +0 -145
- package/lib/migrations/002-default-work-item-modes.test.js +0 -351
- package/lib/production-chore-generator.test.js +0 -432
- package/lib/production-context-detector.test.js +0 -277
- package/lib/production-scenario-appender.test.js +0 -235
- package/lib/production-scenario-validator.test.js +0 -246
- package/lib/production-standards-reader.test.js +0 -270
- package/lib/project-state.test.js +0 -92
- package/lib/push-queue.js +0 -417
- package/lib/queue-processor.js +0 -74
- package/lib/test-helpers.js +0 -202
- package/lib/test-helpers.test.js +0 -255
- package/prototypes/2025-01-11-production-mode-autonomous.js +0 -119
- package/prototypes/2025-01-11-production-mode-collaborative.js +0 -166
- package/prototypes/2025-01-11-production-mode-guided.js +0 -217
- package/prototypes/2025-01-11-production-mode-smart-context.js +0 -347
- package/prototypes/2025-01-11-production-standards-example.md +0 -204
- package/prototypes/2025-11-10-backlog-filtering-tree-aware.js +0 -242
- package/prototypes/test/index.html +0 -1
- package/setup-dist-repo.sh +0 -68
- package/test-production-standards-engine.js +0 -130
- package/test-results.json +0 -2195
- package/test-safety-check.sh +0 -80
- package/work-item-tracking-plan.md +0 -199
- /package/{.jettypod/devpod.db → jettypod.db} +0 -0
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worktree Manager - Database as Single Source of Truth
|
|
3
|
+
*
|
|
4
|
+
* This module manages all worktree operations with the database as the single
|
|
5
|
+
* source of truth. All state is persisted BEFORE any git/filesystem operations.
|
|
6
|
+
*
|
|
7
|
+
* Core principles:
|
|
8
|
+
* - Database is the source of truth (not git or filesystem)
|
|
9
|
+
* - All operations are transactional (rollback on failure)
|
|
10
|
+
* - Branch names are ALWAYS stored in DB (never lost)
|
|
11
|
+
* - Operations are idempotent (safe to retry)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// Database will be required lazily when implementing actual functions
|
|
15
|
+
// const getDb = require('./database');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Valid worktree statuses
|
|
19
|
+
*/
|
|
20
|
+
const VALID_STATUSES = ['active', 'merging', 'cleanup_pending', 'corrupted'];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a worktree for a work item
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} workItem - Work item from database (must have id and title)
|
|
26
|
+
* @param {Object} options - Optional configuration
|
|
27
|
+
* @param {string} options.repoPath - Path to git repository (defaults to process.cwd())
|
|
28
|
+
* @param {Object} options.db - Database connection (defaults to global database)
|
|
29
|
+
* @returns {Promise<Object>} Worktree record with id, work_item_id, branch_name, worktree_path, status
|
|
30
|
+
*/
|
|
31
|
+
async function createWorktree(workItem, options = {}) {
|
|
32
|
+
if (!workItem || !workItem.id) {
|
|
33
|
+
return Promise.reject(new Error('Invalid work item: must have id'));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const fs = require('fs');
|
|
37
|
+
const path = require('path');
|
|
38
|
+
const { execSync } = require('child_process');
|
|
39
|
+
const { slugify } = require('./git');
|
|
40
|
+
|
|
41
|
+
// CRITICAL SAFETY: repoPath MUST be provided - never auto-detect from cwd
|
|
42
|
+
// Auto-detection from worktrees causes catastrophic repo corruption
|
|
43
|
+
if (!options.repoPath) {
|
|
44
|
+
return Promise.reject(new Error('SAFETY: repoPath must be explicitly provided to createWorktree'));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const gitRoot = options.repoPath;
|
|
48
|
+
|
|
49
|
+
// SAFETY CHECK: Verify gitRoot is actually the main repository
|
|
50
|
+
try {
|
|
51
|
+
const gitRootCheck = execSync('git rev-parse --show-toplevel', {
|
|
52
|
+
cwd: gitRoot,
|
|
53
|
+
encoding: 'utf8'
|
|
54
|
+
}).trim();
|
|
55
|
+
|
|
56
|
+
// Resolve both paths to handle /var vs /private/var symlink on macOS
|
|
57
|
+
const resolvedGitRoot = fs.realpathSync(gitRoot);
|
|
58
|
+
const resolvedGitRootCheck = fs.realpathSync(gitRootCheck);
|
|
59
|
+
|
|
60
|
+
if (resolvedGitRootCheck !== resolvedGitRoot) {
|
|
61
|
+
return Promise.reject(new Error(`SAFETY: Provided repoPath ${gitRoot} does not match git root ${gitRootCheck}`));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Verify .jettypod exists in the git root
|
|
65
|
+
const jettypodPath = path.join(gitRoot, '.jettypod');
|
|
66
|
+
if (!fs.existsSync(jettypodPath)) {
|
|
67
|
+
return Promise.reject(new Error(`SAFETY: .jettypod directory not found in ${gitRoot} - refusing to proceed`));
|
|
68
|
+
}
|
|
69
|
+
} catch (err) {
|
|
70
|
+
return Promise.reject(new Error(`SAFETY: Failed to verify git root: ${err.message}`));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { getDb } = require('./database');
|
|
74
|
+
const db = options.db || getDb();
|
|
75
|
+
|
|
76
|
+
// Generate branch name and worktree path
|
|
77
|
+
const titleSlug = slugify(workItem.title || 'item');
|
|
78
|
+
const branchName = `feature/work-${workItem.id}-${titleSlug}`;
|
|
79
|
+
const worktreeBasePath = path.join(gitRoot, '.jettypod-work');
|
|
80
|
+
const worktreePath = path.join(worktreeBasePath, `${workItem.id}-${titleSlug}`);
|
|
81
|
+
|
|
82
|
+
let worktreeId = null;
|
|
83
|
+
let branchCreated = false;
|
|
84
|
+
let worktreeCreated = false;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
// Step 1: Create database entry with status='active'
|
|
88
|
+
// Note: We use 'active' from the start because if anything fails,
|
|
89
|
+
// we'll delete the entire entry during rollback
|
|
90
|
+
worktreeId = await new Promise((resolve, reject) => {
|
|
91
|
+
db.run(
|
|
92
|
+
`INSERT INTO worktrees (work_item_id, branch_name, worktree_path, status)
|
|
93
|
+
VALUES (?, ?, ?, 'active')`,
|
|
94
|
+
[workItem.id, branchName, worktreePath],
|
|
95
|
+
function(err) {
|
|
96
|
+
if (err) reject(err);
|
|
97
|
+
else resolve(this.lastID);
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Step 2: Create git worktree
|
|
103
|
+
// Always branch from 'main' to avoid nested worktrees
|
|
104
|
+
try {
|
|
105
|
+
execSync(`git worktree add -b "${branchName}" "${worktreePath}" main`, {
|
|
106
|
+
cwd: gitRoot,
|
|
107
|
+
stdio: 'pipe'
|
|
108
|
+
});
|
|
109
|
+
worktreeCreated = true;
|
|
110
|
+
branchCreated = true;
|
|
111
|
+
} catch (gitErr) {
|
|
112
|
+
throw new Error(`Failed to create git worktree: ${gitErr.message}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Step 3: Verify directory exists
|
|
116
|
+
if (!fs.existsSync(worktreePath)) {
|
|
117
|
+
throw new Error(`Worktree directory was not created: ${worktreePath}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Step 4: Create .jettypod symlink
|
|
121
|
+
// CRITICAL: Git creates .jettypod directory because snapshots are tracked
|
|
122
|
+
// We must remove it first to create the symlink to the shared database
|
|
123
|
+
const jettypodLink = path.join(worktreePath, '.jettypod');
|
|
124
|
+
const jettypodTarget = path.join(gitRoot, '.jettypod');
|
|
125
|
+
try {
|
|
126
|
+
// Remove the git-created .jettypod directory if it exists
|
|
127
|
+
if (fs.existsSync(jettypodLink)) {
|
|
128
|
+
const stats = fs.lstatSync(jettypodLink);
|
|
129
|
+
if (stats.isSymbolicLink()) {
|
|
130
|
+
// Already a symlink - verify it points to the right place
|
|
131
|
+
const linkTarget = fs.readlinkSync(jettypodLink);
|
|
132
|
+
const resolvedLink = path.resolve(path.dirname(jettypodLink), linkTarget);
|
|
133
|
+
const resolvedTarget = path.resolve(jettypodTarget);
|
|
134
|
+
if (resolvedLink === resolvedTarget) {
|
|
135
|
+
// Symlink already correct - nothing to do
|
|
136
|
+
} else {
|
|
137
|
+
// Wrong target - remove and recreate
|
|
138
|
+
fs.unlinkSync(jettypodLink);
|
|
139
|
+
fs.symlinkSync(jettypodTarget, jettypodLink, 'dir');
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
// It's a directory - remove it and create symlink
|
|
143
|
+
fs.rmSync(jettypodLink, { recursive: true, force: true });
|
|
144
|
+
fs.symlinkSync(jettypodTarget, jettypodLink, 'dir');
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
// Doesn't exist - create symlink
|
|
148
|
+
fs.symlinkSync(jettypodTarget, jettypodLink, 'dir');
|
|
149
|
+
}
|
|
150
|
+
} catch (symlinkErr) {
|
|
151
|
+
// Symlink failure is CRITICAL - the worktree won't have access to the database
|
|
152
|
+
throw new Error(`Failed to create .jettypod symlink: ${symlinkErr.message}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Step 5: Return the created worktree record
|
|
156
|
+
const worktree = await new Promise((resolve, reject) => {
|
|
157
|
+
db.get('SELECT * FROM worktrees WHERE id = ?', [worktreeId], (err, row) => {
|
|
158
|
+
if (err) reject(err);
|
|
159
|
+
else resolve(row);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return worktree;
|
|
164
|
+
|
|
165
|
+
} catch (err) {
|
|
166
|
+
// Transaction rollback
|
|
167
|
+
console.error(`Worktree creation failed, rolling back: ${err.message}`);
|
|
168
|
+
|
|
169
|
+
// Cleanup database entry
|
|
170
|
+
if (worktreeId) {
|
|
171
|
+
try {
|
|
172
|
+
await new Promise((resolve, reject) => {
|
|
173
|
+
db.run('DELETE FROM worktrees WHERE id = ?', [worktreeId], (err) => {
|
|
174
|
+
if (err) reject(err);
|
|
175
|
+
else resolve();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
} catch (dbErr) {
|
|
179
|
+
console.error(`Failed to rollback database entry: ${dbErr.message}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Cleanup git worktree
|
|
184
|
+
if (worktreeCreated) {
|
|
185
|
+
try {
|
|
186
|
+
execSync(`git worktree remove --force "${worktreePath}"`, {
|
|
187
|
+
cwd: gitRoot,
|
|
188
|
+
stdio: 'pipe'
|
|
189
|
+
});
|
|
190
|
+
} catch (gitErr) {
|
|
191
|
+
console.error(`Failed to cleanup git worktree: ${gitErr.message}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Cleanup branch (only if we created it)
|
|
196
|
+
if (branchCreated && !worktreeCreated) {
|
|
197
|
+
try {
|
|
198
|
+
execSync(`git branch -D "${branchName}"`, {
|
|
199
|
+
cwd: gitRoot,
|
|
200
|
+
stdio: 'pipe'
|
|
201
|
+
});
|
|
202
|
+
} catch (gitErr) {
|
|
203
|
+
console.error(`Failed to cleanup branch: ${gitErr.message}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Cleanup directory
|
|
208
|
+
if (fs.existsSync(worktreePath)) {
|
|
209
|
+
try {
|
|
210
|
+
fs.rmSync(worktreePath, { recursive: true, force: true });
|
|
211
|
+
} catch (fsErr) {
|
|
212
|
+
console.error(`Failed to cleanup worktree directory: ${fsErr.message}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
throw new Error(`Worktree creation failed: ${err.message}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get the worktree for a work item
|
|
222
|
+
*
|
|
223
|
+
* @param {number} workItemId - Work item ID
|
|
224
|
+
* @param {Object} options - Optional configuration
|
|
225
|
+
* @param {Object} options.db - Database connection (defaults to global database)
|
|
226
|
+
* @returns {Promise<Object|null>} Worktree record or null if not found
|
|
227
|
+
*/
|
|
228
|
+
async function getWorktreeForWorkItem(workItemId, options = {}) {
|
|
229
|
+
if (!workItemId || typeof workItemId !== 'number') {
|
|
230
|
+
return Promise.reject(new Error('Invalid work item ID: must be a number'));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const { getDb } = require('./database');
|
|
234
|
+
const db = options.db || getDb();
|
|
235
|
+
|
|
236
|
+
return new Promise((resolve, reject) => {
|
|
237
|
+
db.get(
|
|
238
|
+
'SELECT * FROM worktrees WHERE work_item_id = ? ORDER BY created_at DESC LIMIT 1',
|
|
239
|
+
[workItemId],
|
|
240
|
+
(err, row) => {
|
|
241
|
+
if (err) reject(err);
|
|
242
|
+
else resolve(row || null);
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get all active worktrees
|
|
250
|
+
*
|
|
251
|
+
* @param {Object} options - Optional configuration
|
|
252
|
+
* @param {Object} options.db - Database connection (defaults to global database)
|
|
253
|
+
* @returns {Promise<Array>} Array of worktree records with status='active'
|
|
254
|
+
*/
|
|
255
|
+
async function getAllActiveWorktrees(options = {}) {
|
|
256
|
+
const { getDb } = require('./database');
|
|
257
|
+
const db = options.db || getDb();
|
|
258
|
+
|
|
259
|
+
return new Promise((resolve, reject) => {
|
|
260
|
+
db.all(
|
|
261
|
+
'SELECT * FROM worktrees WHERE status = ? ORDER BY created_at DESC',
|
|
262
|
+
['active'],
|
|
263
|
+
(err, rows) => {
|
|
264
|
+
if (err) reject(err);
|
|
265
|
+
else resolve(rows || []);
|
|
266
|
+
}
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Mark worktree status
|
|
273
|
+
*
|
|
274
|
+
* @param {number} worktreeId - Worktree ID
|
|
275
|
+
* @param {string} status - New status (active|merging|cleanup_pending|corrupted)
|
|
276
|
+
* @param {Object} options - Optional configuration
|
|
277
|
+
* @param {Object} options.db - Database connection (defaults to global database)
|
|
278
|
+
* @returns {Promise<void>}
|
|
279
|
+
*/
|
|
280
|
+
async function markWorktreeStatus(worktreeId, status, options = {}) {
|
|
281
|
+
if (!worktreeId || typeof worktreeId !== 'number') {
|
|
282
|
+
return Promise.reject(new Error('Invalid worktree ID: must be a number'));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!VALID_STATUSES.includes(status)) {
|
|
286
|
+
return Promise.reject(new Error(`Invalid status: must be one of ${VALID_STATUSES.join(', ')}`));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const { getDb } = require('./database');
|
|
290
|
+
const db = options.db || getDb();
|
|
291
|
+
|
|
292
|
+
return new Promise((resolve, reject) => {
|
|
293
|
+
db.run(
|
|
294
|
+
'UPDATE worktrees SET status = ?, updated_at = datetime(\'now\') WHERE id = ?',
|
|
295
|
+
[status, worktreeId],
|
|
296
|
+
function(err) {
|
|
297
|
+
if (err) reject(err);
|
|
298
|
+
else if (this.changes === 0) reject(new Error(`Worktree not found: ${worktreeId}`));
|
|
299
|
+
else resolve();
|
|
300
|
+
}
|
|
301
|
+
);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Manual recursive directory deletion (last resort fallback)
|
|
307
|
+
* Walks directory tree and deletes files individually
|
|
308
|
+
*
|
|
309
|
+
* @param {string} dirPath - Directory to delete
|
|
310
|
+
* @returns {Promise<void>}
|
|
311
|
+
*/
|
|
312
|
+
async function manualRecursiveDelete(dirPath) {
|
|
313
|
+
const fs = require('fs');
|
|
314
|
+
const path = require('path');
|
|
315
|
+
|
|
316
|
+
if (!fs.existsSync(dirPath)) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
321
|
+
|
|
322
|
+
for (const entry of entries) {
|
|
323
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
324
|
+
|
|
325
|
+
if (entry.isDirectory()) {
|
|
326
|
+
await manualRecursiveDelete(fullPath);
|
|
327
|
+
} else {
|
|
328
|
+
try {
|
|
329
|
+
fs.unlinkSync(fullPath);
|
|
330
|
+
} catch (err) {
|
|
331
|
+
// Try changing permissions and retry
|
|
332
|
+
try {
|
|
333
|
+
fs.chmodSync(fullPath, 0o666);
|
|
334
|
+
fs.unlinkSync(fullPath);
|
|
335
|
+
} catch (retryErr) {
|
|
336
|
+
throw new Error(`Cannot delete file ${fullPath}: ${retryErr.message}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Delete the now-empty directory
|
|
343
|
+
fs.rmdirSync(dirPath);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Resilient directory removal with multi-stage escalation strategy
|
|
348
|
+
* Implements conditional verbosity: silent on success, detailed on escalation
|
|
349
|
+
*
|
|
350
|
+
* Stage 1: Standard git worktree remove
|
|
351
|
+
* Stage 2: Forced git worktree remove --force
|
|
352
|
+
* Stage 3: Filesystem fs.rmSync() with force
|
|
353
|
+
* Stage 4: Manual recursive directory deletion (fallback)
|
|
354
|
+
*
|
|
355
|
+
* @param {string} dirPath - Absolute path to directory to remove
|
|
356
|
+
* @param {string} gitRoot - Path to git repository root
|
|
357
|
+
* @returns {Promise<void>}
|
|
358
|
+
*/
|
|
359
|
+
async function removeDirectoryResilient(dirPath, gitRoot) {
|
|
360
|
+
const fs = require('fs');
|
|
361
|
+
const { execSync } = require('child_process');
|
|
362
|
+
|
|
363
|
+
// Stage 1: Try standard git worktree remove (silent)
|
|
364
|
+
try {
|
|
365
|
+
execSync(`git worktree remove "${dirPath}"`, {
|
|
366
|
+
cwd: gitRoot,
|
|
367
|
+
stdio: 'pipe'
|
|
368
|
+
});
|
|
369
|
+
return; // Success - exit silently
|
|
370
|
+
} catch (stage1Err) {
|
|
371
|
+
// Stage 1 failed - escalate with logging
|
|
372
|
+
console.log(`⚠️ Standard removal failed, escalating to forced removal...`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Stage 2: Try forced git worktree remove
|
|
376
|
+
try {
|
|
377
|
+
execSync(`git worktree remove --force "${dirPath}"`, {
|
|
378
|
+
cwd: gitRoot,
|
|
379
|
+
stdio: 'pipe'
|
|
380
|
+
});
|
|
381
|
+
console.log(`✓ Forced git removal succeeded`);
|
|
382
|
+
return;
|
|
383
|
+
} catch (stage2Err) {
|
|
384
|
+
console.log(`⚠️ Forced git removal failed, escalating to filesystem removal...`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Stage 3: Try filesystem removal with force
|
|
388
|
+
if (fs.existsSync(dirPath)) {
|
|
389
|
+
try {
|
|
390
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
391
|
+
console.log(`✓ Filesystem removal succeeded`);
|
|
392
|
+
|
|
393
|
+
// Clean up git metadata
|
|
394
|
+
try {
|
|
395
|
+
execSync('git worktree prune', { cwd: gitRoot, stdio: 'pipe' });
|
|
396
|
+
} catch (pruneErr) {
|
|
397
|
+
// Non-fatal
|
|
398
|
+
}
|
|
399
|
+
return;
|
|
400
|
+
} catch (stage3Err) {
|
|
401
|
+
console.log(`⚠️ Filesystem removal failed, escalating to manual deletion...`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Stage 4: Manual recursive deletion (last resort)
|
|
406
|
+
try {
|
|
407
|
+
await manualRecursiveDelete(dirPath);
|
|
408
|
+
console.log(`✓ Manual recursive deletion succeeded`);
|
|
409
|
+
|
|
410
|
+
// Clean up git metadata
|
|
411
|
+
try {
|
|
412
|
+
execSync('git worktree prune', { cwd: gitRoot, stdio: 'pipe' });
|
|
413
|
+
} catch (pruneErr) {
|
|
414
|
+
// Non-fatal
|
|
415
|
+
}
|
|
416
|
+
} catch (stage4Err) {
|
|
417
|
+
throw new Error(`All removal strategies failed: ${stage4Err.message}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Cleanup a worktree (remove from filesystem and update DB)
|
|
423
|
+
*
|
|
424
|
+
* @param {number} worktreeId - Worktree ID
|
|
425
|
+
* @param {Object} options - Optional configuration
|
|
426
|
+
* @param {string} options.repoPath - Path to git repository (defaults to process.cwd())
|
|
427
|
+
* @param {Object} options.db - Database connection (defaults to global database)
|
|
428
|
+
* @param {boolean} options.deleteBranch - Whether to delete the branch (defaults to false)
|
|
429
|
+
* @returns {Promise<void>}
|
|
430
|
+
*/
|
|
431
|
+
async function cleanupWorktree(worktreeId, options = {}) {
|
|
432
|
+
if (!worktreeId || typeof worktreeId !== 'number') {
|
|
433
|
+
return Promise.reject(new Error('Invalid worktree ID: must be a number'));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const fs = require('fs');
|
|
437
|
+
const path = require('path');
|
|
438
|
+
const { execSync } = require('child_process');
|
|
439
|
+
|
|
440
|
+
// CRITICAL SAFETY: repoPath MUST be provided - never auto-detect from cwd
|
|
441
|
+
// Auto-detection from worktrees causes catastrophic repo corruption
|
|
442
|
+
if (!options.repoPath) {
|
|
443
|
+
return Promise.reject(new Error('SAFETY: repoPath must be explicitly provided to cleanupWorktree'));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const gitRoot = options.repoPath;
|
|
447
|
+
const { getDb } = require('./database');
|
|
448
|
+
const db = options.db || getDb();
|
|
449
|
+
const deleteBranch = options.deleteBranch || false;
|
|
450
|
+
|
|
451
|
+
// SAFETY CHECK 1: Verify we're not running from within a worktree
|
|
452
|
+
try {
|
|
453
|
+
const currentGitDir = execSync('git rev-parse --git-dir', {
|
|
454
|
+
cwd: process.cwd(),
|
|
455
|
+
encoding: 'utf8'
|
|
456
|
+
}).trim();
|
|
457
|
+
|
|
458
|
+
if (currentGitDir.includes('.jettypod-work') || currentGitDir !== '.git') {
|
|
459
|
+
return Promise.reject(new Error('SAFETY: Cannot run cleanup from within a worktree'));
|
|
460
|
+
}
|
|
461
|
+
} catch (err) {
|
|
462
|
+
// Ignore - might not be in a git repo
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// SAFETY CHECK 2: Verify gitRoot is actually the main repository
|
|
466
|
+
try {
|
|
467
|
+
const gitRootCheck = execSync('git rev-parse --show-toplevel', {
|
|
468
|
+
cwd: gitRoot,
|
|
469
|
+
encoding: 'utf8'
|
|
470
|
+
}).trim();
|
|
471
|
+
|
|
472
|
+
// Resolve both paths to handle /var vs /private/var symlink on macOS
|
|
473
|
+
const resolvedGitRoot = fs.realpathSync(gitRoot);
|
|
474
|
+
const resolvedGitRootCheck = fs.realpathSync(gitRootCheck);
|
|
475
|
+
|
|
476
|
+
if (resolvedGitRootCheck !== resolvedGitRoot) {
|
|
477
|
+
return Promise.reject(new Error(`SAFETY: Provided repoPath ${gitRoot} does not match git root ${gitRootCheck}`));
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Verify .jettypod exists in the git root
|
|
481
|
+
const jettypodPath = path.join(gitRoot, '.jettypod');
|
|
482
|
+
if (!fs.existsSync(jettypodPath)) {
|
|
483
|
+
return Promise.reject(new Error(`SAFETY: .jettypod directory not found in ${gitRoot} - refusing to proceed`));
|
|
484
|
+
}
|
|
485
|
+
} catch (err) {
|
|
486
|
+
return Promise.reject(new Error(`SAFETY: Failed to verify git root: ${err.message}`));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// CRITICAL SAFETY: Create automatic backup of .jettypod before cleanup
|
|
490
|
+
const { createBackup } = require('./jettypod-backup');
|
|
491
|
+
const backupResult = await createBackup(gitRoot, `cleanup-worktree-${worktreeId}`);
|
|
492
|
+
if (!backupResult.success) {
|
|
493
|
+
console.warn(`⚠️ Warning: Could not create backup: ${backupResult.error}`);
|
|
494
|
+
console.warn(` Proceeding anyway, but .jettypod is not backed up!`);
|
|
495
|
+
} else {
|
|
496
|
+
console.log(`💾 Created backup: ${path.basename(backupResult.backupPath)}`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
// Step 1: Get worktree details from database
|
|
501
|
+
const worktree = await new Promise((resolve, reject) => {
|
|
502
|
+
db.get('SELECT * FROM worktrees WHERE id = ?', [worktreeId], (err, row) => {
|
|
503
|
+
if (err) reject(err);
|
|
504
|
+
else if (!row) reject(new Error(`Worktree not found: ${worktreeId}`));
|
|
505
|
+
else resolve(row);
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// Step 2: Mark as cleanup_pending
|
|
510
|
+
await new Promise((resolve, reject) => {
|
|
511
|
+
db.run(
|
|
512
|
+
'UPDATE worktrees SET status = ?, updated_at = datetime(\'now\') WHERE id = ?',
|
|
513
|
+
['cleanup_pending', worktreeId],
|
|
514
|
+
(err) => {
|
|
515
|
+
if (err) reject(err);
|
|
516
|
+
else resolve();
|
|
517
|
+
}
|
|
518
|
+
);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// Step 2.5: Smart conflict detection and auto-commit
|
|
522
|
+
const { checkForConflicts } = require('./smart-conflict-detection');
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
const conflictCheck = checkForConflicts(gitRoot);
|
|
526
|
+
|
|
527
|
+
// Check for recoverable errors
|
|
528
|
+
if (conflictCheck.error) {
|
|
529
|
+
if (conflictCheck.error.category === 'DETACHED_HEAD') {
|
|
530
|
+
console.warn(`⚠️ ${conflictCheck.error.message} - skipping auto-commit`);
|
|
531
|
+
} else {
|
|
532
|
+
console.warn(`⚠️ Conflict detection issue: ${conflictCheck.error.message}`);
|
|
533
|
+
}
|
|
534
|
+
// Continue with cleanup even if conflict detection had issues
|
|
535
|
+
}
|
|
536
|
+
// If no conflicts but uncommitted changes exist, auto-commit
|
|
537
|
+
else if (!conflictCheck.hasConflicts && conflictCheck.status.trim() !== '') {
|
|
538
|
+
try {
|
|
539
|
+
execSync('git add .', { cwd: gitRoot, stdio: 'pipe' });
|
|
540
|
+
execSync(`git commit -m "Merge work item #${worktree.work_item_id}"`, {
|
|
541
|
+
cwd: gitRoot,
|
|
542
|
+
stdio: 'pipe'
|
|
543
|
+
});
|
|
544
|
+
console.log(`✅ Auto-committed clean merge for work item #${worktree.work_item_id}`);
|
|
545
|
+
} catch (commitErr) {
|
|
546
|
+
// Non-fatal - log warning but continue
|
|
547
|
+
console.warn(`Warning: Could not auto-commit: ${commitErr.message}`);
|
|
548
|
+
}
|
|
549
|
+
} else if (conflictCheck.hasConflicts) {
|
|
550
|
+
console.warn(`⚠️ Conflicts detected in ${conflictCheck.conflictFiles.length} file(s)`);
|
|
551
|
+
console.warn(` Files: ${conflictCheck.conflictFiles.join(', ')}`);
|
|
552
|
+
}
|
|
553
|
+
} catch (conflictErr) {
|
|
554
|
+
// Fatal errors from checkForConflicts (invalid repo, permission denied)
|
|
555
|
+
// These should stop the cleanup process
|
|
556
|
+
console.error(`❌ Fatal error during conflict detection: ${conflictErr.message}`);
|
|
557
|
+
throw conflictErr;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Step 2.5: Terminate background processes in worktree directory
|
|
561
|
+
const { findProcessesInDirectory, terminateProcessGracefully } = require('./process-manager');
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
const processes = await findProcessesInDirectory(worktree.worktree_path);
|
|
565
|
+
|
|
566
|
+
if (processes.length > 0) {
|
|
567
|
+
console.log(`🔄 Found ${processes.length} background process(es) in worktree...`);
|
|
568
|
+
|
|
569
|
+
for (const proc of processes) {
|
|
570
|
+
const result = await terminateProcessGracefully(proc.pid, 2000);
|
|
571
|
+
console.log(` Terminated ${result.name} (PID ${result.pid})... ${result.method}`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
} catch (processErr) {
|
|
575
|
+
// Non-fatal - log warning but continue with cleanup
|
|
576
|
+
console.warn(`⚠️ Warning: Process termination failed: ${processErr.message}`);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Step 3: Remove directory with resilient multi-stage strategy
|
|
580
|
+
await removeDirectoryResilient(worktree.worktree_path, gitRoot);
|
|
581
|
+
|
|
582
|
+
// Step 4: Optionally delete branch
|
|
583
|
+
if (deleteBranch) {
|
|
584
|
+
try {
|
|
585
|
+
execSync(`git branch -D "${worktree.branch_name}"`, {
|
|
586
|
+
cwd: gitRoot,
|
|
587
|
+
stdio: 'pipe'
|
|
588
|
+
});
|
|
589
|
+
} catch (branchErr) {
|
|
590
|
+
// Non-fatal - log warning but continue
|
|
591
|
+
console.warn(`Warning: Could not delete branch: ${branchErr.message}`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Step 5: Mark as corrupted (we never delete DB entries)
|
|
596
|
+
// Using 'corrupted' as the final state for cleaned up worktrees
|
|
597
|
+
// This preserves the audit trail
|
|
598
|
+
await new Promise((resolve, reject) => {
|
|
599
|
+
db.run(
|
|
600
|
+
'UPDATE worktrees SET status = ?, updated_at = datetime(\'now\') WHERE id = ?',
|
|
601
|
+
['corrupted', worktreeId],
|
|
602
|
+
(err) => {
|
|
603
|
+
if (err) reject(err);
|
|
604
|
+
else resolve();
|
|
605
|
+
}
|
|
606
|
+
);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
} catch (err) {
|
|
610
|
+
// On any failure, mark as corrupted
|
|
611
|
+
try {
|
|
612
|
+
await new Promise((resolve, reject) => {
|
|
613
|
+
db.run(
|
|
614
|
+
'UPDATE worktrees SET status = ?, updated_at = datetime(\'now\') WHERE id = ?',
|
|
615
|
+
['corrupted', worktreeId],
|
|
616
|
+
(err) => {
|
|
617
|
+
if (err) reject(err);
|
|
618
|
+
else resolve();
|
|
619
|
+
}
|
|
620
|
+
);
|
|
621
|
+
});
|
|
622
|
+
} catch (dbErr) {
|
|
623
|
+
console.error(`Failed to mark worktree as corrupted: ${dbErr.message}`);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
throw new Error(`Worktree cleanup failed: ${err.message}`);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
module.exports = {
|
|
631
|
+
createWorktree,
|
|
632
|
+
getWorktreeForWorkItem,
|
|
633
|
+
getAllActiveWorktrees,
|
|
634
|
+
markWorktreeStatus,
|
|
635
|
+
cleanupWorktree
|
|
636
|
+
};
|