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.
Files changed (179) hide show
  1. package/.nvmrc +1 -0
  2. package/docs/COMPLETE-TESTING-STRATEGY.md +970 -0
  3. package/docs/DECISIONS.md +10 -12
  4. package/docs/NODE_VERSION.md +83 -0
  5. package/docs/TDD-INFRASTRUCTURE-STRATEGY.md +1374 -0
  6. package/docs/TESTING-FOR-NON-ENGINEERS.md +1588 -0
  7. package/docs/TESTING-STRATEGY-AUDIT.md +698 -0
  8. package/hooks/post-checkout +17 -0
  9. package/hooks/post-merge +17 -0
  10. package/hooks/pre-commit +30 -0
  11. package/jettypod.js +259 -120
  12. package/lib/coverage-tracker.js +218 -0
  13. package/lib/database.js +2 -0
  14. package/lib/db-export.js +192 -0
  15. package/lib/db-import.js +193 -0
  16. package/lib/external-transition-handler.js +32 -0
  17. package/lib/git-hook-helpers.js +174 -0
  18. package/lib/git-root.js +90 -0
  19. package/lib/infrastructure-chore-generator.js +45 -0
  20. package/lib/install-hooks.js +52 -0
  21. package/lib/jettypod-backup.js +238 -0
  22. package/lib/merge-lock.js +193 -0
  23. package/lib/migrations/012-add-worktree-path.js +38 -0
  24. package/lib/migrations/013-worktrees-table.js +86 -0
  25. package/lib/migrations/014-migrate-worktree-data.js +161 -0
  26. package/lib/migrations/015-merge-locks-table.js +67 -0
  27. package/lib/pattern-finder.js +152 -0
  28. package/lib/process-manager.js +140 -0
  29. package/lib/production-standards-reader.js +13 -2
  30. package/lib/production-standards-writer.js +85 -0
  31. package/lib/skills/feature-planning/dry-run-validator.js +135 -0
  32. package/lib/skills/feature-planning/validation-formatter.js +160 -0
  33. package/lib/smart-conflict-detection.js +168 -0
  34. package/lib/smart-fetch-rebase.js +614 -0
  35. package/lib/step-definition-parser.js +76 -0
  36. package/lib/unit-test-generator.js +232 -0
  37. package/lib/verification-command-generator.js +66 -0
  38. package/lib/worktree-diagnostics.js +413 -0
  39. package/lib/worktree-facade.js +174 -0
  40. package/lib/worktree-manager.js +636 -0
  41. package/lib/worktree-reconciler.js +429 -0
  42. package/package.json +30 -3
  43. package/skills-templates/external-transition/SKILL.md +34 -3
  44. package/skills-templates/feature-planning/SKILL.md +190 -24
  45. package/skills-templates/production-mode/SKILL.md +127 -9
  46. package/skills-templates/speed-mode/SKILL.md +454 -51
  47. package/skills-templates/stable-mode/SKILL.md +285 -76
  48. package/.claude/PROTECT_SKILLS.md +0 -28
  49. package/.claude/settings.json +0 -24
  50. package/.claude/settings.local.json +0 -16
  51. package/.claude/skills/epic-planning/SKILL.md +0 -297
  52. package/.claude/skills/external-transition/SKILL.md +0 -384
  53. package/.claude/skills/feature-planning/SKILL.md +0 -464
  54. package/.claude/skills/production-mode/SKILL.md +0 -369
  55. package/.claude/skills/speed-mode/SKILL.md +0 -481
  56. package/.claude/skills/stable-mode/SKILL.md +0 -713
  57. package/.claude/skills.backup-2025-11-10T23-33-09-368Z/epic-planning/SKILL.md +0 -297
  58. package/.claude/skills.backup-2025-11-10T23-33-09-368Z/feature-planning/SKILL.md +0 -464
  59. package/.claude/skills.backup-2025-11-10T23-33-09-368Z/speed-mode/SKILL.md +0 -467
  60. package/.claude/skills.backup-2025-11-10T23-33-09-368Z/stable-mode/SKILL.md +0 -673
  61. package/.claude/skills.backup-2025-11-11T16-15-10-070Z/epic-discover/SKILL.md +0 -297
  62. package/.claude/skills.backup-2025-11-11T16-42-43-212Z/epic-planning/SKILL.md +0 -297
  63. package/.claude/skills.backup-2025-11-11T16-42-43-212Z/feature-planning/SKILL.md +0 -464
  64. package/.claude/skills.backup-2025-11-11T16-42-43-212Z/speed-mode/SKILL.md +0 -467
  65. package/.claude/skills.backup-2025-11-11T16-42-43-212Z/stable-mode/SKILL.md +0 -673
  66. package/.claude/skills.backup-2025-11-11T17-06-09-783Z/epic-planning/SKILL.md +0 -297
  67. package/.claude/skills.backup-2025-11-11T17-06-09-783Z/feature-planning/SKILL.md +0 -464
  68. package/.claude/skills.backup-2025-11-11T17-06-09-783Z/speed-mode/SKILL.md +0 -467
  69. package/.claude/skills.backup-2025-11-11T17-06-09-783Z/stable-mode/SKILL.md +0 -673
  70. package/.devpod/current-work.json +0 -10
  71. package/.devpod/work.db +0 -0
  72. package/.github/workflows/test-safety.yml +0 -85
  73. package/.jettypod/config.json +0 -5
  74. package/.jettypod/current-work.json +0 -10
  75. package/.jettypod/hooks/README.md +0 -77
  76. package/.jettypod/hooks/protect-claude-md.js +0 -338
  77. package/.jettypod/test-work.db +0 -0
  78. package/.jettypod/work.db +0 -0
  79. package/CLAUDE.md +0 -49
  80. package/SPEED-STABLE-AUDIT.md +0 -853
  81. package/SYSTEM-BEHAVIOR.md +0 -2199
  82. package/TEST_SAFETY_AUDIT.md +0 -314
  83. package/TEST_SAFETY_IMPLEMENTATION.md +0 -97
  84. package/cucumber-report.html +0 -45
  85. package/dist/devpod-linux +0 -0
  86. package/dist/devpod-macos +0 -0
  87. package/dist/devpod-win.exe +0 -0
  88. package/docs/features/jettypod-standards-explained.md +0 -543
  89. package/docs/features/standards-inventory.md +0 -257
  90. package/features/auto-generate-production-chores.feature +0 -13
  91. package/features/backlog-command.feature +0 -26
  92. package/features/backlog-filtering-production.feature +0 -10
  93. package/features/claude-md-protection/steps.js +0 -498
  94. package/features/decisions/index.js +0 -490
  95. package/features/decisions/index.test.js +0 -208
  96. package/features/fix-text-wrapping.feature +0 -42
  97. package/features/git-hooks/git-hooks.feature +0 -30
  98. package/features/git-hooks/index.js +0 -93
  99. package/features/git-hooks/index.test.js +0 -137
  100. package/features/git-hooks/post-commit +0 -56
  101. package/features/git-hooks/post-merge +0 -47
  102. package/features/git-hooks/pre-commit +0 -28
  103. package/features/git-hooks/simple-steps.js +0 -53
  104. package/features/git-hooks/simple-test.feature +0 -10
  105. package/features/git-hooks/steps.js +0 -196
  106. package/features/jettypod-update-command.feature +0 -46
  107. package/features/mode-prompts/index.js +0 -95
  108. package/features/mode-prompts/simple-steps.js +0 -44
  109. package/features/mode-prompts/simple-test.feature +0 -9
  110. package/features/mode-prompts/validation.test.js +0 -120
  111. package/features/multiple-claude-instances.feature +0 -121
  112. package/features/production-mode-skill.feature +0 -121
  113. package/features/refactor-mode/steps.js +0 -217
  114. package/features/refactor-mode.feature +0 -49
  115. package/features/simplify-external-transition.feature +0 -166
  116. package/features/skills-update/index.test.js +0 -216
  117. package/features/step_definitions/backlog-command.steps.js +0 -37
  118. package/features/step_definitions/fix-text-wrapping.steps.js +0 -271
  119. package/features/step_definitions/multiple-claude-instances.steps.js +0 -621
  120. package/features/step_definitions/production-mode-skill.steps.js +0 -862
  121. package/features/step_definitions/simplify-external-transition.steps.js +0 -370
  122. package/features/step_definitions/terminal-logo.steps.js +0 -145
  123. package/features/step_definitions/update-command.steps.js +0 -183
  124. package/features/support/hooks.js +0 -9
  125. package/features/terminal-logo/index.js +0 -39
  126. package/features/terminal-logo/terminal-logo.feature +0 -30
  127. package/features/update-command/index.js +0 -181
  128. package/features/update-command/index.test.js +0 -225
  129. package/features/work-commands/bug-workflow-display.feature +0 -22
  130. package/features/work-commands/index.js +0 -498
  131. package/features/work-commands/simple-steps.js +0 -69
  132. package/features/work-commands/stable-tests.feature +0 -57
  133. package/features/work-commands/steps.js +0 -1174
  134. package/features/work-commands/validation.test.js +0 -88
  135. package/features/work-commands/work-commands.feature +0 -13
  136. package/features/work-tracking/discovery-validation.test.js +0 -228
  137. package/features/work-tracking/index.js +0 -1921
  138. package/features/work-tracking/mode-required.feature +0 -112
  139. package/features/work-tracking/phase-tracking.test.js +0 -482
  140. package/features/work-tracking/prototype-tracking.test.js +0 -485
  141. package/features/work-tracking/tree-view.test.js +0 -310
  142. package/features/work-tracking/work-set-mode.feature +0 -71
  143. package/features/work-tracking/work-start-mode.feature +0 -88
  144. package/full-test.txt +0 -0
  145. package/lib/bug-workflow.test.js +0 -177
  146. package/lib/claudemd.test.js +0 -195
  147. package/lib/config.test.js +0 -511
  148. package/lib/constants.test.js +0 -164
  149. package/lib/current-work.test.js +0 -146
  150. package/lib/database-project-config.test.js +0 -111
  151. package/lib/database.test.js +0 -106
  152. package/lib/decisions-generator.test.js +0 -457
  153. package/lib/decisions-helpers.test.js +0 -310
  154. package/lib/git-coordinator.js +0 -167
  155. package/lib/git.test.js +0 -145
  156. package/lib/migrations/002-default-work-item-modes.test.js +0 -351
  157. package/lib/production-chore-generator.test.js +0 -432
  158. package/lib/production-context-detector.test.js +0 -277
  159. package/lib/production-scenario-appender.test.js +0 -235
  160. package/lib/production-scenario-validator.test.js +0 -246
  161. package/lib/production-standards-reader.test.js +0 -270
  162. package/lib/project-state.test.js +0 -92
  163. package/lib/push-queue.js +0 -417
  164. package/lib/queue-processor.js +0 -74
  165. package/lib/test-helpers.js +0 -202
  166. package/lib/test-helpers.test.js +0 -255
  167. package/prototypes/2025-01-11-production-mode-autonomous.js +0 -119
  168. package/prototypes/2025-01-11-production-mode-collaborative.js +0 -166
  169. package/prototypes/2025-01-11-production-mode-guided.js +0 -217
  170. package/prototypes/2025-01-11-production-mode-smart-context.js +0 -347
  171. package/prototypes/2025-01-11-production-standards-example.md +0 -204
  172. package/prototypes/2025-11-10-backlog-filtering-tree-aware.js +0 -242
  173. package/prototypes/test/index.html +0 -1
  174. package/setup-dist-repo.sh +0 -68
  175. package/test-production-standards-engine.js +0 -130
  176. package/test-results.json +0 -2195
  177. package/test-safety-check.sh +0 -80
  178. package/work-item-tracking-plan.md +0 -199
  179. /package/{.jettypod/devpod.db → jettypod.db} +0 -0
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Git hook helpers for context-aware testing
3
+ *
4
+ * Provides utilities for detecting work context and running appropriate tests
5
+ * based on the current workflow phase (speed/stable/production mode).
6
+ */
7
+
8
+ const { execSync } = require('child_process');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ /**
13
+ * Detect current work context from CLAUDE.md
14
+ *
15
+ * @returns {{mode: string, feature_id: number, feature_title: string, scenario_file: string} | null}
16
+ */
17
+ function detectWorkContext() {
18
+ try {
19
+ const projectRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();
20
+ const claudeMdPath = path.join(projectRoot, 'CLAUDE.md');
21
+
22
+ if (!fs.existsSync(claudeMdPath)) {
23
+ return null;
24
+ }
25
+
26
+ const claudeMd = fs.readFileSync(claudeMdPath, 'utf8');
27
+
28
+ // Extract current work from <current_work> tag
29
+ const currentWorkMatch = claudeMd.match(/<current_work>([\s\S]*?)<\/current_work>/);
30
+ if (!currentWorkMatch) {
31
+ return null;
32
+ }
33
+
34
+ const currentWorkBlock = currentWorkMatch[1];
35
+
36
+ // Parse work item ID from "Working on: [#123] Title (type)"
37
+ const workingOnMatch = currentWorkBlock.match(/Working on:\s*\[#(\d+)\]/);
38
+ if (!workingOnMatch) {
39
+ return null;
40
+ }
41
+
42
+ const workItemId = parseInt(workingOnMatch[1], 10);
43
+
44
+ // Get work item details from database
45
+ const { getDb } = require('./database');
46
+ const db = getDb();
47
+
48
+ return new Promise((resolve, reject) => {
49
+ db.get(`
50
+ SELECT
51
+ w.id,
52
+ w.title,
53
+ w.mode,
54
+ w.parent_id,
55
+ f.title as feature_title,
56
+ f.scenario_file
57
+ FROM work_items w
58
+ LEFT JOIN work_items f ON w.parent_id = f.id
59
+ WHERE w.id = ?
60
+ `, [workItemId], (err, row) => {
61
+ db.close();
62
+
63
+ if (err) {
64
+ reject(err);
65
+ return;
66
+ }
67
+
68
+ if (!row) {
69
+ resolve(null);
70
+ return;
71
+ }
72
+
73
+ // Determine mode (use work item's mode, or parent feature's mode if chore)
74
+ const mode = row.mode || 'speed';
75
+
76
+ resolve({
77
+ mode,
78
+ feature_id: row.parent_id || row.id,
79
+ feature_title: row.feature_title || row.title,
80
+ scenario_file: row.scenario_file
81
+ });
82
+ });
83
+ });
84
+ } catch (err) {
85
+ console.error('Error detecting work context:', err.message);
86
+ return null;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Run context-aware tests based on workflow phase
92
+ *
93
+ * @param {{mode: string, feature_id: number, feature_title: string, scenario_file: string}} context
94
+ * @returns {{success: boolean, error?: string}}
95
+ */
96
+ function runContextualTests(context) {
97
+ try {
98
+ const {
99
+ runBddTestWithTimeout,
100
+ runBddScenarioWithTimeout,
101
+ getFirstScenarioLine
102
+ } = require('../.claude/skills/speed-mode/test-runner');
103
+
104
+ if (!context.scenario_file) {
105
+ return { success: true }; // No scenario file, skip tests
106
+ }
107
+
108
+ const projectRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();
109
+ const scenarioPath = path.join(projectRoot, context.scenario_file);
110
+
111
+ if (!fs.existsSync(scenarioPath)) {
112
+ return { success: true }; // Scenario file doesn't exist yet, skip tests
113
+ }
114
+
115
+ let result;
116
+
117
+ switch (context.mode) {
118
+ case 'speed':
119
+ // Speed mode: Quick smoke test (happy path scenario only)
120
+ console.log('Speed mode: Running happy path scenario...');
121
+ const happyPathLine = getFirstScenarioLine(context.scenario_file);
122
+ if (!happyPathLine) {
123
+ return { success: true }; // No scenarios yet
124
+ }
125
+ result = runBddScenarioWithTimeout(context.scenario_file, happyPathLine, 30000); // 30s timeout
126
+ break;
127
+
128
+ case 'stable':
129
+ // Stable mode: Feature-specific tests (all scenarios in current feature)
130
+ console.log('Stable mode: Running all feature scenarios...');
131
+ result = runBddTestWithTimeout(context.scenario_file, 60000); // 60s timeout
132
+ break;
133
+
134
+ case 'production':
135
+ // Production mode: Comprehensive tests (entire suite)
136
+ console.log('Production mode: Running full test suite...');
137
+ result = runBddTestWithTimeout(context.scenario_file, 120000); // 120s timeout
138
+ break;
139
+
140
+ default:
141
+ // Unknown mode, skip tests
142
+ return { success: true };
143
+ }
144
+
145
+ // Check if tests timed out
146
+ if (result.timedOut) {
147
+ return {
148
+ success: false,
149
+ error: 'Tests timed out. Check for infinite loops or hanging processes.'
150
+ };
151
+ }
152
+
153
+ // Check if tests passed
154
+ if (result.exitCode !== 0) {
155
+ return {
156
+ success: false,
157
+ error: `Tests failed with exit code ${result.exitCode}.\n\n${result.stderr || result.stdout}`
158
+ };
159
+ }
160
+
161
+ return { success: true };
162
+
163
+ } catch (err) {
164
+ return {
165
+ success: false,
166
+ error: `Test execution error: ${err.message}`
167
+ };
168
+ }
169
+ }
170
+
171
+ module.exports = {
172
+ detectWorkContext,
173
+ runContextualTests
174
+ };
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Git Root Detection - SAFE VERSION
3
+ *
4
+ * Calculates the REAL git root directory ONCE at startup, before any
5
+ * worktree operations. This prevents catastrophic bugs where process.cwd()
6
+ * returns a worktree path instead of the main repo.
7
+ *
8
+ * CRITICAL: This module MUST be required at the top of jettypod.js BEFORE
9
+ * any worktree operations.
10
+ */
11
+
12
+ const { execSync } = require('child_process');
13
+ const path = require('path');
14
+ const fs = require('fs');
15
+
16
+ let cachedGitRoot = null;
17
+
18
+ /**
19
+ * Get the REAL git root directory (main repository, not worktree)
20
+ *
21
+ * This function:
22
+ * 1. Uses --git-common-dir to find the shared .git directory
23
+ * 2. Verifies it's actually the main repo (not a worktree)
24
+ * 3. Caches the result to prevent re-calculation
25
+ *
26
+ * @returns {string} Absolute path to git root
27
+ * @throws {Error} If not in a git repository or can't determine root
28
+ */
29
+ function getGitRoot() {
30
+ if (cachedGitRoot) {
31
+ return cachedGitRoot;
32
+ }
33
+
34
+ try {
35
+ // Step 1: Get the common git directory (shared across worktrees)
36
+ const gitCommonDir = execSync('git rev-parse --git-common-dir', {
37
+ cwd: process.cwd(),
38
+ encoding: 'utf8'
39
+ }).trim();
40
+
41
+ // Step 2: Resolve to absolute path
42
+ const absoluteGitDir = path.isAbsolute(gitCommonDir)
43
+ ? gitCommonDir
44
+ : path.resolve(process.cwd(), gitCommonDir);
45
+
46
+ // Step 3: Get the directory containing .git
47
+ const gitRoot = path.dirname(absoluteGitDir);
48
+
49
+ // Step 4: SAFETY VERIFICATION - ensure this is the MAIN repository
50
+ // Check 1: .jettypod directory must exist
51
+ const jettypodPath = path.join(gitRoot, '.jettypod');
52
+ if (!fs.existsSync(jettypodPath)) {
53
+ throw new Error(`SAFETY: .jettypod not found in ${gitRoot} - may be a worktree`);
54
+ }
55
+
56
+ // Check 2: Verify with git that this is the top-level directory
57
+ const gitTopLevel = execSync('git rev-parse --show-toplevel', {
58
+ cwd: gitRoot,
59
+ encoding: 'utf8'
60
+ }).trim();
61
+
62
+ if (gitTopLevel !== gitRoot) {
63
+ throw new Error(`SAFETY: Calculated git root ${gitRoot} does not match git top-level ${gitTopLevel}`);
64
+ }
65
+
66
+ // Check 3: Ensure we're not in a worktree subdirectory
67
+ if (gitRoot.includes('.jettypod-work')) {
68
+ throw new Error(`SAFETY: Calculated git root contains .jettypod-work - this is a worktree, not main repo`);
69
+ }
70
+
71
+ // Success - cache and return
72
+ cachedGitRoot = gitRoot;
73
+ return cachedGitRoot;
74
+
75
+ } catch (err) {
76
+ throw new Error(`Failed to determine git root: ${err.message}`);
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Reset the cached git root (for testing only)
82
+ */
83
+ function resetCache() {
84
+ cachedGitRoot = null;
85
+ }
86
+
87
+ module.exports = {
88
+ getGitRoot,
89
+ resetCache
90
+ };
@@ -0,0 +1,45 @@
1
+ // Infrastructure chore generator - creates chores from infrastructure-scoped standards
2
+ const { getDb } = require('./database');
3
+
4
+ /**
5
+ * Generate chores from infrastructure-scoped standards
6
+ * @param {number} epicId - ID of the parent epic
7
+ * @param {Array} standards - Array of all production standards
8
+ * @returns {Array<number>} Array of created chore IDs
9
+ */
10
+ function generateInfrastructureChores(epicId, standards) {
11
+ const db = getDb();
12
+ const infraStandards = standards.filter(s => s.scope === 'infrastructure');
13
+
14
+ // Handle empty infrastructure standards - emit warning
15
+ if (infraStandards.length === 0) {
16
+ console.log('Warning: No infrastructure standards found');
17
+ return [];
18
+ }
19
+
20
+ const choreIds = [];
21
+
22
+ for (const standard of infraStandards) {
23
+ const description = [
24
+ `**Acceptance Criteria:**`,
25
+ standard.acceptance,
26
+ ``,
27
+ `**Reasoning:**`,
28
+ standard.reasoning,
29
+ ``,
30
+ `**Implementation Pattern:**`,
31
+ standard.pattern
32
+ ].join('\n');
33
+
34
+ const result = db.prepare(`
35
+ INSERT INTO work (type, title, description, status, parent_id)
36
+ VALUES (?, ?, ?, ?, ?)
37
+ `).run('chore', standard.id, description, 'backlog', epicId);
38
+
39
+ choreIds.push(result.lastInsertRowid);
40
+ }
41
+
42
+ return choreIds;
43
+ }
44
+
45
+ module.exports = { generateInfrastructureChores };
@@ -0,0 +1,52 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execSync } = require('child_process');
4
+
5
+ /**
6
+ * Find the actual git directory (handles worktrees)
7
+ */
8
+ function findGitDir() {
9
+ try {
10
+ const gitDir = execSync('git rev-parse --git-dir', { encoding: 'utf8' }).trim();
11
+ return path.resolve(gitDir);
12
+ } catch (err) {
13
+ throw new Error('Not in a git repository');
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Install git hooks from hooks/ directory to .git/hooks/
19
+ */
20
+ function installHooks() {
21
+ const hooksDir = path.join(__dirname, '..', 'hooks');
22
+ const gitDir = findGitDir();
23
+ const gitHooksDir = path.join(gitDir, 'hooks');
24
+
25
+ // Ensure .git/hooks exists
26
+ if (!fs.existsSync(gitHooksDir)) {
27
+ fs.mkdirSync(gitHooksDir, { recursive: true });
28
+ }
29
+
30
+ // Copy all hooks
31
+ const hookFiles = fs.readdirSync(hooksDir);
32
+
33
+ hookFiles.forEach(hookFile => {
34
+ const sourcePath = path.join(hooksDir, hookFile);
35
+ const destPath = path.join(gitHooksDir, hookFile);
36
+
37
+ // Copy the hook
38
+ fs.copyFileSync(sourcePath, destPath);
39
+
40
+ // Make it executable
41
+ fs.chmodSync(destPath, '755');
42
+
43
+ console.log(`✓ Installed ${hookFile}`);
44
+ });
45
+ }
46
+
47
+ module.exports = { installHooks };
48
+
49
+ // Run if called directly
50
+ if (require.main === module) {
51
+ installHooks();
52
+ }
@@ -0,0 +1,238 @@
1
+ /**
2
+ * JettyPod Backup Manager
3
+ *
4
+ * Automatic backups of .jettypod directory before any destructive operation.
5
+ * This is a critical safety measure to prevent catastrophic data loss.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { execSync } = require('child_process');
11
+
12
+ /**
13
+ * Create a backup of the .jettypod directory
14
+ *
15
+ * @param {string} gitRoot - Absolute path to git repository root
16
+ * @param {string} reason - Human-readable reason for backup (e.g., "cleanup-worktree-1234")
17
+ * @returns {Promise<Object>} Result with success status and backup path
18
+ */
19
+ async function createBackup(gitRoot, reason = 'unknown') {
20
+ const jettypodPath = path.join(gitRoot, '.jettypod');
21
+
22
+ // Verify .jettypod exists
23
+ if (!fs.existsSync(jettypodPath)) {
24
+ return {
25
+ success: false,
26
+ error: '.jettypod directory does not exist',
27
+ backupPath: null
28
+ };
29
+ }
30
+
31
+ // Create backup directory if it doesn't exist
32
+ const backupBaseDir = path.join(gitRoot, '.git', 'jettypod-backups');
33
+ if (!fs.existsSync(backupBaseDir)) {
34
+ fs.mkdirSync(backupBaseDir, { recursive: true });
35
+ }
36
+
37
+ // Generate timestamped backup name
38
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
39
+ const sanitizedReason = reason.replace(/[^a-zA-Z0-9-]/g, '-');
40
+ const backupName = `jettypod-${timestamp}-${sanitizedReason}`;
41
+ const backupPath = path.join(backupBaseDir, backupName);
42
+
43
+ try {
44
+ // Copy entire .jettypod directory
45
+ execSync(`cp -R "${jettypodPath}" "${backupPath}"`, {
46
+ stdio: 'pipe'
47
+ });
48
+
49
+ // Verify backup was created
50
+ if (!fs.existsSync(backupPath)) {
51
+ throw new Error('Backup directory was not created');
52
+ }
53
+
54
+ // Clean up old backups (keep last 10)
55
+ await cleanupOldBackups(backupBaseDir, 10);
56
+
57
+ return {
58
+ success: true,
59
+ backupPath: backupPath,
60
+ timestamp: timestamp
61
+ };
62
+
63
+ } catch (err) {
64
+ return {
65
+ success: false,
66
+ error: err.message,
67
+ backupPath: null
68
+ };
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Clean up old backups, keeping only the N most recent
74
+ *
75
+ * @param {string} backupDir - Directory containing backups
76
+ * @param {number} keepCount - Number of backups to keep
77
+ */
78
+ async function cleanupOldBackups(backupDir, keepCount = 10) {
79
+ try {
80
+ const backups = fs.readdirSync(backupDir)
81
+ .filter(name => name.startsWith('jettypod-'))
82
+ .map(name => ({
83
+ name: name,
84
+ path: path.join(backupDir, name),
85
+ mtime: fs.statSync(path.join(backupDir, name)).mtime
86
+ }))
87
+ .sort((a, b) => b.mtime - a.mtime); // Sort newest first
88
+
89
+ // Delete old backups
90
+ const toDelete = backups.slice(keepCount);
91
+ for (const backup of toDelete) {
92
+ fs.rmSync(backup.path, { recursive: true, force: true });
93
+ }
94
+ } catch (err) {
95
+ // Non-fatal - just log warning
96
+ console.warn(`Warning: Could not cleanup old backups: ${err.message}`);
97
+ }
98
+ }
99
+
100
+ /**
101
+ * List available backups
102
+ *
103
+ * @param {string} gitRoot - Absolute path to git repository root
104
+ * @returns {Array} List of backup objects with name, path, timestamp
105
+ */
106
+ function listBackups(gitRoot) {
107
+ const backupBaseDir = path.join(gitRoot, '.git', 'jettypod-backups');
108
+
109
+ if (!fs.existsSync(backupBaseDir)) {
110
+ return [];
111
+ }
112
+
113
+ try {
114
+ return fs.readdirSync(backupBaseDir)
115
+ .filter(name => name.startsWith('jettypod-'))
116
+ .map(name => {
117
+ const backupPath = path.join(backupBaseDir, name);
118
+ const stat = fs.statSync(backupPath);
119
+ return {
120
+ name: name,
121
+ path: backupPath,
122
+ created: stat.mtime,
123
+ size: getDirectorySize(backupPath)
124
+ };
125
+ })
126
+ .sort((a, b) => b.created - a.created); // Sort newest first
127
+ } catch (err) {
128
+ console.error(`Error listing backups: ${err.message}`);
129
+ return [];
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Restore from a backup
135
+ *
136
+ * @param {string} gitRoot - Absolute path to git repository root
137
+ * @param {string} backupName - Name of backup to restore (or 'latest')
138
+ * @returns {Promise<Object>} Result with success status
139
+ */
140
+ async function restoreBackup(gitRoot, backupName = 'latest') {
141
+ const backupBaseDir = path.join(gitRoot, '.git', 'jettypod-backups');
142
+
143
+ if (!fs.existsSync(backupBaseDir)) {
144
+ return {
145
+ success: false,
146
+ error: 'No backups directory found'
147
+ };
148
+ }
149
+
150
+ let backupPath;
151
+
152
+ if (backupName === 'latest') {
153
+ const backups = listBackups(gitRoot);
154
+ if (backups.length === 0) {
155
+ return {
156
+ success: false,
157
+ error: 'No backups available'
158
+ };
159
+ }
160
+ backupPath = backups[0].path;
161
+ backupName = backups[0].name;
162
+ } else {
163
+ backupPath = path.join(backupBaseDir, backupName);
164
+ if (!fs.existsSync(backupPath)) {
165
+ return {
166
+ success: false,
167
+ error: `Backup not found: ${backupName}`
168
+ };
169
+ }
170
+ }
171
+
172
+ const jettypodPath = path.join(gitRoot, '.jettypod');
173
+
174
+ try {
175
+ // Create backup of current .jettypod before restoring
176
+ if (fs.existsSync(jettypodPath)) {
177
+ const preRestoreBackup = await createBackup(gitRoot, 'pre-restore');
178
+ if (!preRestoreBackup.success) {
179
+ console.warn(`Warning: Could not backup current .jettypod: ${preRestoreBackup.error}`);
180
+ }
181
+ }
182
+
183
+ // Remove current .jettypod
184
+ if (fs.existsSync(jettypodPath)) {
185
+ fs.rmSync(jettypodPath, { recursive: true, force: true });
186
+ }
187
+
188
+ // Copy backup to .jettypod
189
+ execSync(`cp -R "${backupPath}" "${jettypodPath}"`, {
190
+ stdio: 'pipe'
191
+ });
192
+
193
+ // Verify restore
194
+ if (!fs.existsSync(jettypodPath)) {
195
+ throw new Error('Restore failed - .jettypod directory was not created');
196
+ }
197
+
198
+ return {
199
+ success: true,
200
+ restoredFrom: backupName
201
+ };
202
+
203
+ } catch (err) {
204
+ return {
205
+ success: false,
206
+ error: err.message
207
+ };
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Get total size of a directory
213
+ */
214
+ function getDirectorySize(dirPath) {
215
+ let totalSize = 0;
216
+
217
+ function traverse(currentPath) {
218
+ const stats = fs.statSync(currentPath);
219
+
220
+ if (stats.isFile()) {
221
+ totalSize += stats.size;
222
+ } else if (stats.isDirectory()) {
223
+ const files = fs.readdirSync(currentPath);
224
+ for (const file of files) {
225
+ traverse(path.join(currentPath, file));
226
+ }
227
+ }
228
+ }
229
+
230
+ traverse(dirPath);
231
+ return totalSize;
232
+ }
233
+
234
+ module.exports = {
235
+ createBackup,
236
+ listBackups,
237
+ restoreBackup
238
+ };