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,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coverage tracker for unit tests
|
|
3
|
+
*
|
|
4
|
+
* Tracks and reports unit test coverage after stable mode completion.
|
|
5
|
+
* Uses Jest's built-in coverage capabilities.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { execSync } = require('child_process');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Run Jest with coverage for specific test files
|
|
14
|
+
*
|
|
15
|
+
* @param {string[]} testFiles - Array of test file paths
|
|
16
|
+
* @param {Object} options - Coverage options
|
|
17
|
+
* @param {number} options.coverageThreshold - Minimum coverage percentage (default: 80)
|
|
18
|
+
* @param {boolean} options.verbose - Show detailed coverage report (default: false)
|
|
19
|
+
* @returns {{success: boolean, coverage: Object, error?: string}}
|
|
20
|
+
*/
|
|
21
|
+
function runCoverageForTests(testFiles, options = {}) {
|
|
22
|
+
const {
|
|
23
|
+
coverageThreshold = 80,
|
|
24
|
+
verbose = false
|
|
25
|
+
} = options;
|
|
26
|
+
|
|
27
|
+
if (!testFiles || testFiles.length === 0) {
|
|
28
|
+
return {
|
|
29
|
+
success: true,
|
|
30
|
+
coverage: { statements: 0, branches: 0, functions: 0, lines: 0 },
|
|
31
|
+
message: 'No test files to run coverage on'
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
// Build Jest command with coverage
|
|
37
|
+
const testPattern = testFiles.join('|');
|
|
38
|
+
const jestCommand = [
|
|
39
|
+
'npx jest',
|
|
40
|
+
`--testMatch="**/{${testPattern}}"`,
|
|
41
|
+
'--coverage',
|
|
42
|
+
'--coverageReporters=json-summary',
|
|
43
|
+
'--coverageReporters=text',
|
|
44
|
+
verbose ? '--verbose' : '--silent',
|
|
45
|
+
'--bail=false' // Run all tests even if some fail
|
|
46
|
+
].join(' ');
|
|
47
|
+
|
|
48
|
+
const output = execSync(jestCommand, {
|
|
49
|
+
cwd: process.cwd(),
|
|
50
|
+
encoding: 'utf8',
|
|
51
|
+
stdio: 'pipe'
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Parse coverage summary
|
|
55
|
+
const coverageSummaryPath = path.join(process.cwd(), 'coverage', 'coverage-summary.json');
|
|
56
|
+
|
|
57
|
+
if (!fs.existsSync(coverageSummaryPath)) {
|
|
58
|
+
return {
|
|
59
|
+
success: false,
|
|
60
|
+
error: 'Coverage summary not generated'
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const coverageSummary = JSON.parse(fs.readFileSync(coverageSummaryPath, 'utf8'));
|
|
65
|
+
const totalCoverage = coverageSummary.total;
|
|
66
|
+
|
|
67
|
+
const coverage = {
|
|
68
|
+
statements: totalCoverage.statements.pct,
|
|
69
|
+
branches: totalCoverage.branches.pct,
|
|
70
|
+
functions: totalCoverage.functions.pct,
|
|
71
|
+
lines: totalCoverage.lines.pct
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Check if coverage meets threshold
|
|
75
|
+
const meetsThreshold = Object.values(coverage).every(pct => pct >= coverageThreshold);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
success: meetsThreshold,
|
|
79
|
+
coverage,
|
|
80
|
+
threshold: coverageThreshold,
|
|
81
|
+
output: verbose ? output : undefined
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
} catch (error) {
|
|
85
|
+
// Jest exits with non-zero if tests fail or coverage is low
|
|
86
|
+
// Parse coverage even on failure
|
|
87
|
+
const coverageSummaryPath = path.join(process.cwd(), 'coverage', 'coverage-summary.json');
|
|
88
|
+
|
|
89
|
+
if (fs.existsSync(coverageSummaryPath)) {
|
|
90
|
+
const coverageSummary = JSON.parse(fs.readFileSync(coverageSummaryPath, 'utf8'));
|
|
91
|
+
const totalCoverage = coverageSummary.total;
|
|
92
|
+
|
|
93
|
+
const coverage = {
|
|
94
|
+
statements: totalCoverage.statements.pct,
|
|
95
|
+
branches: totalCoverage.branches.pct,
|
|
96
|
+
functions: totalCoverage.functions.pct,
|
|
97
|
+
lines: totalCoverage.lines.pct
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
success: false,
|
|
102
|
+
coverage,
|
|
103
|
+
threshold: coverageThreshold,
|
|
104
|
+
error: error.message,
|
|
105
|
+
output: error.stdout || error.stderr
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
success: false,
|
|
111
|
+
error: `Coverage execution failed: ${error.message}`
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get test files for implementation files
|
|
118
|
+
*
|
|
119
|
+
* @param {string[]} implementationFiles - Array of implementation file paths
|
|
120
|
+
* @returns {string[]} Array of test file paths
|
|
121
|
+
*/
|
|
122
|
+
function getTestFilesForImplementation(implementationFiles) {
|
|
123
|
+
const { getTestFilePath } = require('./unit-test-generator');
|
|
124
|
+
|
|
125
|
+
const testFiles = [];
|
|
126
|
+
|
|
127
|
+
for (const implFile of implementationFiles) {
|
|
128
|
+
const testFile = getTestFilePath(implFile);
|
|
129
|
+
|
|
130
|
+
if (fs.existsSync(testFile)) {
|
|
131
|
+
testFiles.push(testFile);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return testFiles;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Format coverage report for display
|
|
140
|
+
*
|
|
141
|
+
* @param {Object} coverageResult - Result from runCoverageForTests
|
|
142
|
+
* @returns {string} Formatted coverage report
|
|
143
|
+
*/
|
|
144
|
+
function formatCoverageReport(coverageResult) {
|
|
145
|
+
if (!coverageResult.coverage) {
|
|
146
|
+
return 'No coverage data available';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const { coverage, threshold, success } = coverageResult;
|
|
150
|
+
|
|
151
|
+
const lines = [
|
|
152
|
+
'\n📊 Unit Test Coverage Report',
|
|
153
|
+
'─────────────────────────────',
|
|
154
|
+
`Statements: ${formatPercentage(coverage.statements, threshold)}`,
|
|
155
|
+
`Branches: ${formatPercentage(coverage.branches, threshold)}`,
|
|
156
|
+
`Functions: ${formatPercentage(coverage.functions, threshold)}`,
|
|
157
|
+
`Lines: ${formatPercentage(coverage.lines, threshold)}`,
|
|
158
|
+
'─────────────────────────────',
|
|
159
|
+
success
|
|
160
|
+
? `✅ All coverage metrics meet ${threshold}% threshold`
|
|
161
|
+
: `⚠️ Some metrics below ${threshold}% threshold`
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
return lines.join('\n');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Format percentage with color indicator
|
|
169
|
+
*
|
|
170
|
+
* @param {number} percentage - Coverage percentage
|
|
171
|
+
* @param {number} threshold - Threshold percentage
|
|
172
|
+
* @returns {string} Formatted string
|
|
173
|
+
*/
|
|
174
|
+
function formatPercentage(percentage, threshold) {
|
|
175
|
+
const pct = percentage.toFixed(2);
|
|
176
|
+
const status = percentage >= threshold ? '✅' : '⚠️ ';
|
|
177
|
+
return `${status} ${pct}%`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Track coverage for feature implementation
|
|
182
|
+
*
|
|
183
|
+
* @param {string[]} implementationFiles - Files modified during implementation
|
|
184
|
+
* @param {Object} options - Coverage options
|
|
185
|
+
* @returns {{success: boolean, report: string, coverage: Object}}
|
|
186
|
+
*/
|
|
187
|
+
function trackFeatureCoverage(implementationFiles, options = {}) {
|
|
188
|
+
// Get test files for implementation
|
|
189
|
+
const testFiles = getTestFilesForImplementation(implementationFiles);
|
|
190
|
+
|
|
191
|
+
if (testFiles.length === 0) {
|
|
192
|
+
return {
|
|
193
|
+
success: true,
|
|
194
|
+
report: 'No unit test files found for implementation',
|
|
195
|
+
coverage: null
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Run coverage
|
|
200
|
+
const result = runCoverageForTests(testFiles, options);
|
|
201
|
+
|
|
202
|
+
// Format report
|
|
203
|
+
const report = formatCoverageReport(result);
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
success: result.success,
|
|
207
|
+
report,
|
|
208
|
+
coverage: result.coverage,
|
|
209
|
+
testFiles
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
runCoverageForTests,
|
|
215
|
+
getTestFilesForImplementation,
|
|
216
|
+
formatCoverageReport,
|
|
217
|
+
trackFeatureCoverage
|
|
218
|
+
};
|
package/lib/database.js
CHANGED
|
@@ -190,6 +190,8 @@ function initSchema() {
|
|
|
190
190
|
// Do NOT add it here - the migration includes important data migration logic
|
|
191
191
|
// NOTE: completed_at column is handled by migration 011-add-completed-at.js
|
|
192
192
|
// Do NOT add it here - the migration system handles it properly
|
|
193
|
+
// NOTE: worktree_path column is handled by migration 012-add-worktree-path.js
|
|
194
|
+
db.run(`ALTER TABLE work_items ADD COLUMN worktree_path TEXT`, () => {});
|
|
193
195
|
db.run(`ALTER TABLE work_items ADD COLUMN architectural_decision TEXT`, async () => {
|
|
194
196
|
// Run data migrations after all schema operations complete (skip in test environments)
|
|
195
197
|
if (process.env.NODE_ENV !== 'test') {
|
package/lib/db-export.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { getDb, getJettypodDir } = require('./database');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Ensure .jettypod/snapshots directory exists
|
|
7
|
+
*/
|
|
8
|
+
function ensureSnapshotsDir() {
|
|
9
|
+
const snapshotsDir = path.join(getJettypodDir(), 'snapshots');
|
|
10
|
+
if (!fs.existsSync(snapshotsDir)) {
|
|
11
|
+
fs.mkdirSync(snapshotsDir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
return snapshotsDir;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Export all tables from a database to a JSON structure
|
|
18
|
+
* @param {sqlite3.Database} db - Database connection
|
|
19
|
+
* @param {string[]} tables - Array of table names to export
|
|
20
|
+
* @returns {Promise<Object>} Object with table names as keys and row arrays as values
|
|
21
|
+
*/
|
|
22
|
+
function exportTables(db, tables) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const result = {};
|
|
25
|
+
let completed = 0;
|
|
26
|
+
|
|
27
|
+
if (tables.length === 0) {
|
|
28
|
+
resolve(result);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
tables.forEach(tableName => {
|
|
33
|
+
db.all(`SELECT * FROM ${tableName}`, [], (err, rows) => {
|
|
34
|
+
if (err) {
|
|
35
|
+
reject(new Error(`Failed to export table ${tableName}: ${err.message}`));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
result[tableName] = rows;
|
|
40
|
+
completed++;
|
|
41
|
+
|
|
42
|
+
if (completed === tables.length) {
|
|
43
|
+
resolve(result);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get list of all tables in a database
|
|
52
|
+
* @param {sqlite3.Database} db - Database connection
|
|
53
|
+
* @returns {Promise<string[]>} Array of table names
|
|
54
|
+
*/
|
|
55
|
+
function getTables(db) {
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
db.all(
|
|
58
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
|
|
59
|
+
[],
|
|
60
|
+
(err, rows) => {
|
|
61
|
+
if (err) {
|
|
62
|
+
reject(new Error(`Failed to get table list: ${err.message}`));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
resolve(rows.map(row => row.name));
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Export work.db to .jettypod/snapshots/work.json
|
|
73
|
+
* @returns {Promise<string>} Path to exported file
|
|
74
|
+
*/
|
|
75
|
+
async function exportWorkDb() {
|
|
76
|
+
const snapshotsDir = ensureSnapshotsDir();
|
|
77
|
+
const outputPath = path.join(snapshotsDir, 'work.json');
|
|
78
|
+
|
|
79
|
+
const db = getDb();
|
|
80
|
+
const tables = await getTables(db);
|
|
81
|
+
const data = await exportTables(db, tables);
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
fs.writeFileSync(outputPath, JSON.stringify(data, null, 2), 'utf8');
|
|
85
|
+
} catch (err) {
|
|
86
|
+
// Log warning but don't throw - we don't want to block git operations
|
|
87
|
+
console.error('Pre-commit hook warning: Failed to export work.json');
|
|
88
|
+
if (err.code === 'EACCES') {
|
|
89
|
+
console.error(' Permission denied - check directory permissions');
|
|
90
|
+
} else if (err.code === 'ENOSPC') {
|
|
91
|
+
console.error(' No space left on device');
|
|
92
|
+
} else {
|
|
93
|
+
console.error(` ${err.message}`);
|
|
94
|
+
}
|
|
95
|
+
console.error(' Commit will proceed but snapshots were not updated');
|
|
96
|
+
// Return path anyway so commit can continue
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return outputPath;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Export database.db to .jettypod/snapshots/database.json
|
|
104
|
+
* @returns {Promise<string>} Path to exported file
|
|
105
|
+
*/
|
|
106
|
+
async function exportDatabaseDb() {
|
|
107
|
+
const sqlite3 = require('sqlite3').verbose();
|
|
108
|
+
const snapshotsDir = ensureSnapshotsDir();
|
|
109
|
+
const outputPath = path.join(snapshotsDir, 'database.json');
|
|
110
|
+
const dbPath = path.join(getJettypodDir(), 'database.db');
|
|
111
|
+
|
|
112
|
+
// Check if database.db exists
|
|
113
|
+
if (!fs.existsSync(dbPath)) {
|
|
114
|
+
// Create empty export if database doesn't exist
|
|
115
|
+
try {
|
|
116
|
+
fs.writeFileSync(outputPath, JSON.stringify({}, null, 2), 'utf8');
|
|
117
|
+
} catch (err) {
|
|
118
|
+
console.error('Pre-commit hook warning: Failed to export database.json');
|
|
119
|
+
if (err.code === 'EACCES') {
|
|
120
|
+
console.error(' Permission denied - check directory permissions');
|
|
121
|
+
} else if (err.code === 'ENOSPC') {
|
|
122
|
+
console.error(' No space left on device');
|
|
123
|
+
} else {
|
|
124
|
+
console.error(` ${err.message}`);
|
|
125
|
+
}
|
|
126
|
+
console.error(' Commit will proceed but snapshots were not updated');
|
|
127
|
+
}
|
|
128
|
+
return outputPath;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
const db = new sqlite3.Database(dbPath, sqlite3.OPEN_READONLY, async (err) => {
|
|
133
|
+
if (err) {
|
|
134
|
+
reject(new Error(`Failed to open database.db: ${err.message}`));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const tables = await getTables(db);
|
|
140
|
+
const data = await exportTables(db, tables);
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
fs.writeFileSync(outputPath, JSON.stringify(data, null, 2), 'utf8');
|
|
144
|
+
} catch (writeErr) {
|
|
145
|
+
console.error('Pre-commit hook warning: Failed to export database.json');
|
|
146
|
+
if (writeErr.code === 'EACCES') {
|
|
147
|
+
console.error(' Permission denied - check directory permissions');
|
|
148
|
+
} else if (writeErr.code === 'ENOSPC') {
|
|
149
|
+
console.error(' No space left on device');
|
|
150
|
+
} else {
|
|
151
|
+
console.error(` ${writeErr.message}`);
|
|
152
|
+
}
|
|
153
|
+
console.error(' Commit will proceed but snapshots were not updated');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
db.close((closeErr) => {
|
|
157
|
+
if (closeErr) {
|
|
158
|
+
reject(new Error(`Failed to close database.db: ${closeErr.message}`));
|
|
159
|
+
} else {
|
|
160
|
+
resolve(outputPath);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
} catch (exportErr) {
|
|
164
|
+
db.close();
|
|
165
|
+
reject(exportErr);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Export both work.db and database.db
|
|
173
|
+
* @returns {Promise<{work: string, database: string}>} Paths to exported files
|
|
174
|
+
*/
|
|
175
|
+
async function exportAll() {
|
|
176
|
+
const [workPath, databasePath] = await Promise.all([
|
|
177
|
+
exportWorkDb(),
|
|
178
|
+
exportDatabaseDb()
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
work: workPath,
|
|
183
|
+
database: databasePath
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = {
|
|
188
|
+
exportWorkDb,
|
|
189
|
+
exportDatabaseDb,
|
|
190
|
+
exportAll,
|
|
191
|
+
ensureSnapshotsDir
|
|
192
|
+
};
|
package/lib/db-import.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { getDb, getJettypodDir } = require('./database');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Import tables from JSON structure into a database
|
|
7
|
+
* @param {sqlite3.Database} db - Database connection
|
|
8
|
+
* @param {Object} data - Object with table names as keys and row arrays as values
|
|
9
|
+
* @returns {Promise<void>}
|
|
10
|
+
*/
|
|
11
|
+
function importTables(db, data) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const tableNames = Object.keys(data);
|
|
14
|
+
|
|
15
|
+
if (tableNames.length === 0) {
|
|
16
|
+
resolve();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let completed = 0;
|
|
21
|
+
|
|
22
|
+
tableNames.forEach(tableName => {
|
|
23
|
+
const rows = data[tableName];
|
|
24
|
+
|
|
25
|
+
// Clear existing data
|
|
26
|
+
db.run(`DELETE FROM ${tableName}`, (err) => {
|
|
27
|
+
if (err) {
|
|
28
|
+
reject(new Error(`Failed to clear table ${tableName}: ${err.message}`));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// If no rows to import, we're done with this table
|
|
33
|
+
if (rows.length === 0) {
|
|
34
|
+
completed++;
|
|
35
|
+
if (completed === tableNames.length) {
|
|
36
|
+
resolve();
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Get column names from first row
|
|
42
|
+
const columns = Object.keys(rows[0]);
|
|
43
|
+
const placeholders = columns.map(() => '?').join(', ');
|
|
44
|
+
const columnNames = columns.join(', ');
|
|
45
|
+
const insertSql = `INSERT INTO ${tableName} (${columnNames}) VALUES (${placeholders})`;
|
|
46
|
+
|
|
47
|
+
// Insert all rows
|
|
48
|
+
let inserted = 0;
|
|
49
|
+
rows.forEach(row => {
|
|
50
|
+
const values = columns.map(col => row[col]);
|
|
51
|
+
|
|
52
|
+
db.run(insertSql, values, (err) => {
|
|
53
|
+
if (err) {
|
|
54
|
+
reject(new Error(`Failed to insert into ${tableName}: ${err.message}`));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
inserted++;
|
|
59
|
+
if (inserted === rows.length) {
|
|
60
|
+
completed++;
|
|
61
|
+
if (completed === tableNames.length) {
|
|
62
|
+
resolve();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Import work.db from .jettypod/snapshots/work.json
|
|
74
|
+
* @returns {Promise<string>} Path to imported file
|
|
75
|
+
*/
|
|
76
|
+
async function importWorkDb() {
|
|
77
|
+
const snapshotsDir = path.join(getJettypodDir(), 'snapshots');
|
|
78
|
+
const jsonPath = path.join(snapshotsDir, 'work.json');
|
|
79
|
+
|
|
80
|
+
// Skip if JSON doesn't exist
|
|
81
|
+
if (!fs.existsSync(jsonPath)) {
|
|
82
|
+
return jsonPath;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const jsonContent = fs.readFileSync(jsonPath, 'utf8');
|
|
87
|
+
const data = JSON.parse(jsonContent);
|
|
88
|
+
|
|
89
|
+
const db = getDb();
|
|
90
|
+
await importTables(db, data);
|
|
91
|
+
|
|
92
|
+
return jsonPath;
|
|
93
|
+
} catch (err) {
|
|
94
|
+
// Log warning but don't throw - we don't want to block git operations
|
|
95
|
+
console.error('Post-checkout hook warning: Failed to import work.json');
|
|
96
|
+
if (err instanceof SyntaxError) {
|
|
97
|
+
console.error(' Malformed JSON - file may be corrupted');
|
|
98
|
+
} else {
|
|
99
|
+
console.error(` ${err.message}`);
|
|
100
|
+
}
|
|
101
|
+
console.error(' Database will remain in previous state');
|
|
102
|
+
return jsonPath;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Import database.db from .jettypod/snapshots/database.json
|
|
108
|
+
* @returns {Promise<string>} Path to imported file
|
|
109
|
+
*/
|
|
110
|
+
async function importDatabaseDb() {
|
|
111
|
+
const sqlite3 = require('sqlite3').verbose();
|
|
112
|
+
const snapshotsDir = path.join(getJettypodDir(), 'snapshots');
|
|
113
|
+
const jsonPath = path.join(snapshotsDir, 'database.json');
|
|
114
|
+
|
|
115
|
+
// Skip if JSON doesn't exist
|
|
116
|
+
if (!fs.existsSync(jsonPath)) {
|
|
117
|
+
return jsonPath;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const jsonContent = fs.readFileSync(jsonPath, 'utf8');
|
|
122
|
+
const data = JSON.parse(jsonContent);
|
|
123
|
+
|
|
124
|
+
// If no tables to import, we're done
|
|
125
|
+
if (Object.keys(data).length === 0) {
|
|
126
|
+
return jsonPath;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const dbPath = path.join(getJettypodDir(), 'database.db');
|
|
130
|
+
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
const db = new sqlite3.Database(dbPath, async (err) => {
|
|
133
|
+
if (err) {
|
|
134
|
+
console.error('Post-checkout hook warning: Failed to open database.db');
|
|
135
|
+
console.error(` ${err.message}`);
|
|
136
|
+
console.error(' Database will remain in previous state');
|
|
137
|
+
resolve(jsonPath);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
await importTables(db, data);
|
|
143
|
+
|
|
144
|
+
db.close((closeErr) => {
|
|
145
|
+
if (closeErr) {
|
|
146
|
+
console.error('Post-checkout hook warning: Failed to close database.db');
|
|
147
|
+
console.error(` ${closeErr.message}`);
|
|
148
|
+
}
|
|
149
|
+
resolve(jsonPath);
|
|
150
|
+
});
|
|
151
|
+
} catch (importErr) {
|
|
152
|
+
db.close();
|
|
153
|
+
console.error('Post-checkout hook warning: Failed to import database.json');
|
|
154
|
+
console.error(` ${importErr.message}`);
|
|
155
|
+
console.error(' Database will remain in previous state');
|
|
156
|
+
resolve(jsonPath);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
} catch (err) {
|
|
161
|
+
// Log warning but don't throw - we don't want to block git operations
|
|
162
|
+
console.error('Post-checkout hook warning: Failed to import database.json');
|
|
163
|
+
if (err instanceof SyntaxError) {
|
|
164
|
+
console.error(' Malformed JSON - file may be corrupted');
|
|
165
|
+
} else {
|
|
166
|
+
console.error(` ${err.message}`);
|
|
167
|
+
}
|
|
168
|
+
console.error(' Database will remain in previous state');
|
|
169
|
+
return jsonPath;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Import both work.db and database.db
|
|
175
|
+
* @returns {Promise<{work: string, database: string}>} Paths to imported files
|
|
176
|
+
*/
|
|
177
|
+
async function importAll() {
|
|
178
|
+
const [workPath, databasePath] = await Promise.all([
|
|
179
|
+
importWorkDb(),
|
|
180
|
+
importDatabaseDb()
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
work: workPath,
|
|
185
|
+
database: databasePath
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = {
|
|
190
|
+
importWorkDb,
|
|
191
|
+
importDatabaseDb,
|
|
192
|
+
importAll
|
|
193
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// External transition handler - orchestrates external transition
|
|
2
|
+
const { readStandards } = require('./production-standards-reader');
|
|
3
|
+
const { generateInfrastructureChores } = require('./infrastructure-chore-generator');
|
|
4
|
+
const { getDb } = require('./database');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Handle external transition - creates Infrastructure Readiness epic and generates chores
|
|
8
|
+
* @returns {Promise<number>} Created epic ID
|
|
9
|
+
*/
|
|
10
|
+
async function handleExternalTransition() {
|
|
11
|
+
// Read production standards (throws if file doesn't exist)
|
|
12
|
+
const productionStandards = await readStandards();
|
|
13
|
+
|
|
14
|
+
// Create Infrastructure Readiness epic
|
|
15
|
+
const db = getDb();
|
|
16
|
+
const result = db.prepare(`
|
|
17
|
+
INSERT INTO work (type, title, description, status)
|
|
18
|
+
VALUES (?, ?, ?, ?)
|
|
19
|
+
`).run('epic', 'Infrastructure Readiness',
|
|
20
|
+
'Production infrastructure chores generated from standards', 'backlog');
|
|
21
|
+
|
|
22
|
+
const epicId = result.lastInsertRowid;
|
|
23
|
+
|
|
24
|
+
// Generate chores from infrastructure-scoped standards
|
|
25
|
+
// This will emit warning if no infrastructure standards found
|
|
26
|
+
generateInfrastructureChores(epicId, productionStandards.standards);
|
|
27
|
+
|
|
28
|
+
console.log(`Created epic #${epicId}: Infrastructure Readiness`);
|
|
29
|
+
return epicId;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { handleExternalTransition };
|