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,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
|
+
};
|
package/lib/git-root.js
ADDED
|
@@ -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
|
+
};
|