jettypod 3.0.1
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/.claude/PROTECT_SKILLS.md +28 -0
- package/.claude/settings.json +24 -0
- package/.claude/settings.local.json +16 -0
- package/.claude/skills/epic-discover/SKILL.md +262 -0
- package/.claude/skills/feature-discover/SKILL.md +393 -0
- package/.claude/skills/speed-mode/SKILL.md +364 -0
- package/.claude/skills/stable-mode/SKILL.md +591 -0
- package/.github/workflows/test-safety.yml +85 -0
- package/README.md +25 -0
- package/SPEED-STABLE-AUDIT.md +853 -0
- package/SYSTEM-BEHAVIOR.md +1241 -0
- package/TEST_SAFETY_AUDIT.md +314 -0
- package/TEST_SAFETY_IMPLEMENTATION.md +97 -0
- package/cucumber.js +8 -0
- package/docs/COMMAND_REFERENCE.md +903 -0
- package/docs/DECISIONS.md +68 -0
- package/docs/README.md +48 -0
- package/docs/STANDARDS-SYSTEM-DOCUMENTATION.md +374 -0
- package/docs/TEST-REWRITE-PLAN.md +261 -0
- package/docs/ai-test-writing-requirements.md +219 -0
- package/docs/claude-code-skills.md +607 -0
- package/docs/core-jettypod-methodology/comprehensive-jettypod-methodology.md +582 -0
- package/docs/core-jettypod-methodology/deprecated/jettypod-comprehensive-standards.md +1222 -0
- package/docs/core-jettypod-methodology/deprecated/jettypod-operating-guide.md +3399 -0
- package/docs/core-jettypod-methodology/deprecated/jettypod-technical-checklist.md +1325 -0
- package/docs/core-jettypod-methodology/deprecated/jettypod-vibe-coding-framework.md +1544 -0
- package/docs/core-jettypod-methodology/deprecated/prompt-engineering-guide.md +320 -0
- package/docs/core-jettypod-methodology/deprecated/vibe-coding-cheatsheet (1).md +516 -0
- package/docs/core-jettypod-methodology/deprecated/vibe-coding-framework.md +1544 -0
- package/docs/features/jettypod-standards-explained.md +543 -0
- package/docs/features/standards-inventory.md +257 -0
- package/docs/gap-analysis-current-vs-comprehensive-methodology.md +939 -0
- package/docs/jettypod-system-overview.md +409 -0
- package/features/auto-generate-production-chores.feature +14 -0
- package/features/claude-md-protection/steps.js +487 -0
- package/features/decisions/index.js +490 -0
- package/features/decisions/index.test.js +208 -0
- package/features/git-hooks/git-hooks.feature +30 -0
- package/features/git-hooks/index.js +93 -0
- package/features/git-hooks/index.test.js +137 -0
- package/features/git-hooks/post-commit +56 -0
- package/features/git-hooks/post-merge +47 -0
- package/features/git-hooks/pre-commit +28 -0
- package/features/git-hooks/simple-steps.js +53 -0
- package/features/git-hooks/simple-test.feature +10 -0
- package/features/git-hooks/steps.js +196 -0
- package/features/jettypod-update-command.feature +46 -0
- package/features/mode-prompts/index.js +95 -0
- package/features/mode-prompts/simple-steps.js +44 -0
- package/features/mode-prompts/simple-test.feature +9 -0
- package/features/mode-prompts/validation.test.js +120 -0
- package/features/refactor-mode/steps.js +217 -0
- package/features/refactor-mode.feature +49 -0
- package/features/skills-update/index.test.js +216 -0
- package/features/step_definitions/auto-generate-production-chores.steps.js +162 -0
- package/features/step_definitions/terminal-logo.steps.js +145 -0
- package/features/step_definitions/update-command.steps.js +183 -0
- package/features/terminal-logo/index.js +39 -0
- package/features/terminal-logo/terminal-logo.feature +30 -0
- package/features/update-command/index.js +181 -0
- package/features/update-command/index.test.js +225 -0
- package/features/work-commands/bug-workflow-display.feature +22 -0
- package/features/work-commands/index.js +311 -0
- package/features/work-commands/simple-steps.js +69 -0
- package/features/work-commands/stable-tests.feature +57 -0
- package/features/work-commands/steps.js +1120 -0
- package/features/work-commands/validation.test.js +88 -0
- package/features/work-commands/work-commands.feature +13 -0
- package/features/work-tracking/discovery-validation.test.js +228 -0
- package/features/work-tracking/index.js +1511 -0
- package/features/work-tracking/mode-required.feature +112 -0
- package/features/work-tracking/phase-tracking.test.js +482 -0
- package/features/work-tracking/prototype-tracking.test.js +485 -0
- package/features/work-tracking/tree-view.test.js +310 -0
- package/features/work-tracking/work-set-mode.feature +71 -0
- package/features/work-tracking/work-start-mode.feature +88 -0
- package/full-test.txt +0 -0
- package/install.sh +89 -0
- package/jettypod.js +1640 -0
- package/lib/bug-workflow.js +94 -0
- package/lib/bug-workflow.test.js +177 -0
- package/lib/claudemd.js +130 -0
- package/lib/claudemd.test.js +195 -0
- package/lib/comprehensive-standards-full.json +1778 -0
- package/lib/config.js +181 -0
- package/lib/config.test.js +511 -0
- package/lib/constants.js +107 -0
- package/lib/constants.test.js +164 -0
- package/lib/current-work.js +130 -0
- package/lib/current-work.test.js +146 -0
- package/lib/database-project-config.test.js +107 -0
- package/lib/database.js +256 -0
- package/lib/database.test.js +106 -0
- package/lib/decisions-generator.js +102 -0
- package/lib/decisions-generator.test.js +457 -0
- package/lib/decisions-helpers.js +119 -0
- package/lib/decisions-helpers.test.js +310 -0
- package/lib/discovery-checkpoint.js +83 -0
- package/lib/docs-generator.js +280 -0
- package/lib/external-checklist.js +177 -0
- package/lib/git.js +142 -0
- package/lib/git.test.js +145 -0
- package/lib/logo.js +3 -0
- package/lib/migrations/001-epic-to-parent.js +24 -0
- package/lib/migrations/002-default-work-item-modes.js +37 -0
- package/lib/migrations/002-default-work-item-modes.test.js +351 -0
- package/lib/migrations/003-epic-discovery-fields.js +52 -0
- package/lib/migrations/004-discovery-decisions-table.js +32 -0
- package/lib/migrations/005-migrate-decision-data.js +62 -0
- package/lib/migrations/006-feature-phase-field.js +61 -0
- package/lib/migrations/007-prototype-tracking.js +38 -0
- package/lib/migrations/008-scenario-file-field.js +24 -0
- package/lib/migrations/index.js +74 -0
- package/lib/production-helpers.js +69 -0
- package/lib/project-state.test.js +92 -0
- package/lib/test-helpers.js +184 -0
- package/lib/test-helpers.test.js +255 -0
- package/package.json +36 -0
- package/prototypes/test/index.html +1 -0
- package/setup-dist-repo.sh +68 -0
- package/test-safety-check.sh +80 -0
- package/work-item-tracking-plan.md +199 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Feature: Git Hook Integration
|
|
2
|
+
As a developer
|
|
3
|
+
I want work item status to update automatically on git operations
|
|
4
|
+
So that I don't have to manually track progress
|
|
5
|
+
|
|
6
|
+
Scenario: First commit updates status to in_progress
|
|
7
|
+
Given I have a work item with status "todo"
|
|
8
|
+
And the work item is set as current work
|
|
9
|
+
When I make my first commit
|
|
10
|
+
Then the work item status should be "in_progress"
|
|
11
|
+
|
|
12
|
+
Scenario: Merge to main updates status to done
|
|
13
|
+
Given I have a work item with status "in_progress"
|
|
14
|
+
And the work item is set as current work
|
|
15
|
+
And I am on a feature branch
|
|
16
|
+
When I merge to main
|
|
17
|
+
Then the work item status should be "done"
|
|
18
|
+
|
|
19
|
+
Scenario: No current work item - hooks do nothing
|
|
20
|
+
Given no work item is set as current
|
|
21
|
+
When I make a commit
|
|
22
|
+
Then no errors occur
|
|
23
|
+
|
|
24
|
+
Scenario: Integration - hooks work with existing work commands
|
|
25
|
+
Given I have initialized jettypod with git
|
|
26
|
+
And I create a work item via work commands
|
|
27
|
+
And I start work on the item
|
|
28
|
+
When I commit changes
|
|
29
|
+
Then the work item status updates automatically
|
|
30
|
+
And the current work file still exists
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Install JettyPod git hooks into .git/hooks directory
|
|
6
|
+
* @returns {boolean} True if hooks installed successfully, false if not a git repo
|
|
7
|
+
* @throws {Error} If hook files cannot be copied or made executable
|
|
8
|
+
*/
|
|
9
|
+
function installHooks() {
|
|
10
|
+
const gitHooksDir = path.join(process.cwd(), '.git', 'hooks');
|
|
11
|
+
const devpodHooksDir = path.join(__dirname);
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(gitHooksDir)) {
|
|
14
|
+
console.log('⚠️ Not a git repository - skipping hook installation');
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const hooks = ['pre-commit', 'post-commit', 'post-merge'];
|
|
19
|
+
|
|
20
|
+
hooks.forEach(hook => {
|
|
21
|
+
// Check both locations: features/git-hooks and .jettypod/hooks
|
|
22
|
+
let sourcePath = path.join(devpodHooksDir, hook);
|
|
23
|
+
if (!fs.existsSync(sourcePath)) {
|
|
24
|
+
sourcePath = path.join(process.cwd(), '.jettypod', 'hooks', hook);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!fs.existsSync(sourcePath)) {
|
|
28
|
+
console.log(`⚠️ Hook ${hook} not found, skipping`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const targetPath = path.join(gitHooksDir, hook);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
// Read hook content and inject module path
|
|
36
|
+
let hookContent = fs.readFileSync(sourcePath, 'utf-8');
|
|
37
|
+
|
|
38
|
+
// Find the jettypod root (where node_modules is)
|
|
39
|
+
const devpodRoot = path.dirname(require.main.filename);
|
|
40
|
+
const modulesPath = path.join(devpodRoot, 'node_modules');
|
|
41
|
+
|
|
42
|
+
// Inject NODE_PATH setup after shebang
|
|
43
|
+
if (hookContent.includes('require(\'sqlite3\')')) {
|
|
44
|
+
hookContent = hookContent.replace(
|
|
45
|
+
'#!/usr/bin/env node',
|
|
46
|
+
`#!/usr/bin/env node\n\n// Injected module path\nprocess.env.NODE_PATH = '${modulesPath}' + ':' + (process.env.NODE_PATH || '');require('module').Module._initPaths();`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Replace __JETTYPOD_ROOT__ placeholder with actual jettypod root path
|
|
51
|
+
hookContent = hookContent.replace(/__JETTYPOD_ROOT__/g, devpodRoot);
|
|
52
|
+
|
|
53
|
+
fs.writeFileSync(targetPath, hookContent);
|
|
54
|
+
|
|
55
|
+
// Make executable
|
|
56
|
+
fs.chmodSync(targetPath, 0o755);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
throw new Error(`Failed to install ${hook} hook: ${err.message}`);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
console.log('✓ Git hooks installed (pre-commit, post-commit, post-merge)');
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if JettyPod git hooks are installed
|
|
68
|
+
* @returns {boolean} True if hooks are installed
|
|
69
|
+
*/
|
|
70
|
+
function areHooksInstalled() {
|
|
71
|
+
const gitHooksDir = path.join(process.cwd(), '.git', 'hooks');
|
|
72
|
+
if (!fs.existsSync(gitHooksDir)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const postCommitPath = path.join(gitHooksDir, 'post-commit');
|
|
78
|
+
if (!fs.existsSync(postCommitPath)) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const content = fs.readFileSync(postCommitPath, 'utf-8');
|
|
83
|
+
return content.includes('JettyPod');
|
|
84
|
+
} catch (err) {
|
|
85
|
+
// If we can't read the file, assume not installed
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
installHooks,
|
|
92
|
+
areHooksInstalled
|
|
93
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { createTestEnvironment } = require('../../lib/test-helpers');
|
|
4
|
+
const { installHooks, areHooksInstalled } = require('./index');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
describe('Git Hooks', () => {
|
|
8
|
+
let testEnv;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
testEnv = createTestEnvironment();
|
|
12
|
+
process.chdir(testEnv.testDir);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
testEnv.cleanup();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('installHooks()', () => {
|
|
20
|
+
test('should return false if not a git repository', () => {
|
|
21
|
+
const result = installHooks();
|
|
22
|
+
expect(result).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('should install hooks in git repository', () => {
|
|
26
|
+
// Initialize git repo
|
|
27
|
+
execSync('git init', { stdio: 'pipe' });
|
|
28
|
+
|
|
29
|
+
// Create source hooks
|
|
30
|
+
const hooksSourceDir = path.join(testEnv.testDir, 'features', 'git-hooks');
|
|
31
|
+
fs.mkdirSync(hooksSourceDir, { recursive: true });
|
|
32
|
+
|
|
33
|
+
fs.writeFileSync(path.join(hooksSourceDir, 'pre-commit'), '#!/usr/bin/env node\nconsole.log("test");');
|
|
34
|
+
fs.writeFileSync(path.join(hooksSourceDir, 'post-commit'), '#!/usr/bin/env node\n// JettyPod hook');
|
|
35
|
+
fs.writeFileSync(path.join(hooksSourceDir, 'post-merge'), '#!/usr/bin/env node\nconsole.log("test");');
|
|
36
|
+
|
|
37
|
+
const result = installHooks();
|
|
38
|
+
|
|
39
|
+
expect(result).toBe(true);
|
|
40
|
+
expect(fs.existsSync('.git/hooks/pre-commit')).toBe(true);
|
|
41
|
+
expect(fs.existsSync('.git/hooks/post-commit')).toBe(true);
|
|
42
|
+
expect(fs.existsSync('.git/hooks/post-merge')).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('should make hooks executable', () => {
|
|
46
|
+
execSync('git init', { stdio: 'pipe' });
|
|
47
|
+
|
|
48
|
+
const hooksSourceDir = path.join(testEnv.testDir, 'features', 'git-hooks');
|
|
49
|
+
fs.mkdirSync(hooksSourceDir, { recursive: true });
|
|
50
|
+
|
|
51
|
+
fs.writeFileSync(path.join(hooksSourceDir, 'pre-commit'), '#!/usr/bin/env node\nconsole.log("test");');
|
|
52
|
+
fs.writeFileSync(path.join(hooksSourceDir, 'post-commit'), '#!/usr/bin/env node\n// DevPod');
|
|
53
|
+
fs.writeFileSync(path.join(hooksSourceDir, 'post-merge'), '#!/usr/bin/env node\nconsole.log("test");');
|
|
54
|
+
|
|
55
|
+
installHooks();
|
|
56
|
+
|
|
57
|
+
const preCommitPath = path.join(testEnv.testDir, '.git', 'hooks', 'pre-commit');
|
|
58
|
+
const stats = fs.statSync(preCommitPath);
|
|
59
|
+
// Check if executable bit is set (mode & 0o111)
|
|
60
|
+
expect(stats.mode & 0o111).not.toBe(0);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('should succeed when hooks exist in production directory', () => {
|
|
64
|
+
execSync('git init', { stdio: 'pipe' });
|
|
65
|
+
|
|
66
|
+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
67
|
+
|
|
68
|
+
// This will find hooks in the real features/git-hooks directory
|
|
69
|
+
const result = installHooks();
|
|
70
|
+
|
|
71
|
+
expect(result).toBe(true);
|
|
72
|
+
consoleSpy.mockRestore();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('should check alternate hook location (.jettypod/hooks)', () => {
|
|
76
|
+
execSync('git init', { stdio: 'pipe' });
|
|
77
|
+
|
|
78
|
+
const altHooksDir = path.join(testEnv.testDir, '.jettypod', 'hooks');
|
|
79
|
+
fs.mkdirSync(altHooksDir, { recursive: true });
|
|
80
|
+
|
|
81
|
+
fs.writeFileSync(path.join(altHooksDir, 'pre-commit'), '#!/usr/bin/env node\nconsole.log("test");');
|
|
82
|
+
fs.writeFileSync(path.join(altHooksDir, 'post-commit'), '#!/usr/bin/env node\n// DevPod');
|
|
83
|
+
fs.writeFileSync(path.join(altHooksDir, 'post-merge'), '#!/usr/bin/env node\nconsole.log("test");');
|
|
84
|
+
|
|
85
|
+
const result = installHooks();
|
|
86
|
+
|
|
87
|
+
expect(result).toBe(true);
|
|
88
|
+
expect(fs.existsSync('.git/hooks/post-commit')).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('areHooksInstalled()', () => {
|
|
93
|
+
test('should return false if not a git repository', () => {
|
|
94
|
+
const result = areHooksInstalled();
|
|
95
|
+
expect(result).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('should return false if hooks not installed', () => {
|
|
99
|
+
execSync('git init', { stdio: 'pipe' });
|
|
100
|
+
|
|
101
|
+
const result = areHooksInstalled();
|
|
102
|
+
expect(result).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('should return true if JettyPod hooks installed', () => {
|
|
106
|
+
execSync('git init', { stdio: 'pipe' });
|
|
107
|
+
|
|
108
|
+
fs.writeFileSync('.git/hooks/post-commit', '#!/usr/bin/env node\n// JettyPod hook\nconsole.log("test");');
|
|
109
|
+
|
|
110
|
+
const result = areHooksInstalled();
|
|
111
|
+
expect(result).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('should return false if hooks exist but are not JettyPod hooks', () => {
|
|
115
|
+
execSync('git init', { stdio: 'pipe' });
|
|
116
|
+
|
|
117
|
+
fs.writeFileSync('.git/hooks/post-commit', '#!/usr/bin/env node\nconsole.log("other hook");');
|
|
118
|
+
|
|
119
|
+
const result = areHooksInstalled();
|
|
120
|
+
expect(result).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('should return false if hook file cannot be read', () => {
|
|
124
|
+
execSync('git init', { stdio: 'pipe' });
|
|
125
|
+
|
|
126
|
+
fs.writeFileSync('.git/hooks/post-commit', '#!/usr/bin/env node\n// DevPod');
|
|
127
|
+
fs.chmodSync('.git/hooks/post-commit', 0o000);
|
|
128
|
+
|
|
129
|
+
const result = areHooksInstalled();
|
|
130
|
+
|
|
131
|
+
// Cleanup
|
|
132
|
+
fs.chmodSync('.git/hooks/post-commit', 0o644);
|
|
133
|
+
|
|
134
|
+
expect(result).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// JettyPod Git Hook: post-commit
|
|
4
|
+
// Auto-updates work item status and regenerates docs
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { execSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
const jettypodDir = path.join(process.cwd(), '.jettypod');
|
|
11
|
+
const currentWorkPath = path.join(jettypodDir, 'current-work.json');
|
|
12
|
+
const dbPath = path.join(jettypodDir, 'work.db');
|
|
13
|
+
|
|
14
|
+
// Update work item status if needed
|
|
15
|
+
(async () => {
|
|
16
|
+
if (fs.existsSync(currentWorkPath)) {
|
|
17
|
+
const currentWork = JSON.parse(fs.readFileSync(currentWorkPath, 'utf-8'));
|
|
18
|
+
|
|
19
|
+
// Only update if status is todo
|
|
20
|
+
if (currentWork.status === 'todo') {
|
|
21
|
+
try {
|
|
22
|
+
const { updateStatus } = require('../../features/work-tracking');
|
|
23
|
+
await updateStatus(currentWork.id, 'in_progress');
|
|
24
|
+
|
|
25
|
+
// Update current work file
|
|
26
|
+
currentWork.status = 'in_progress';
|
|
27
|
+
fs.writeFileSync(currentWorkPath, JSON.stringify(currentWork, null, 2));
|
|
28
|
+
|
|
29
|
+
console.log(`\n✓ Work item #${currentWork.id} status updated: todo → in_progress`);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.error('Failed to update work item:', err);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
})();
|
|
36
|
+
|
|
37
|
+
// Always generate docs on every commit
|
|
38
|
+
generateDocs();
|
|
39
|
+
|
|
40
|
+
function generateDocs() {
|
|
41
|
+
const featuresDir = path.join(process.cwd(), 'features');
|
|
42
|
+
const outputPath = path.join(process.cwd(), 'SYSTEM-BEHAVIOR.md');
|
|
43
|
+
|
|
44
|
+
// Skip if no features directory
|
|
45
|
+
if (!fs.existsSync(featuresDir)) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const docsGenerator = require(path.join(process.cwd(), 'lib', 'docs-generator'));
|
|
51
|
+
docsGenerator.generate();
|
|
52
|
+
console.log('📚 Documentation updated');
|
|
53
|
+
} catch (err) {
|
|
54
|
+
// Fail silently - docs generation is optional
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// JettyPod Git Hook: post-merge
|
|
4
|
+
// Auto-updates work item status to done when merging to main
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { execSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
const jettypodDir = path.join(process.cwd(), '.jettypod');
|
|
11
|
+
const currentWorkPath = path.join(jettypodDir, 'current-work.json');
|
|
12
|
+
const dbPath = path.join(jettypodDir, 'work.db');
|
|
13
|
+
|
|
14
|
+
// Skip if no current work
|
|
15
|
+
if (!fs.existsSync(currentWorkPath)) {
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Check if we're on main/master
|
|
20
|
+
let currentBranch;
|
|
21
|
+
try {
|
|
22
|
+
currentBranch = execSync('git branch --show-current', { encoding: 'utf-8' }).trim();
|
|
23
|
+
} catch (e) {
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (currentBranch !== 'main' && currentBranch !== 'master') {
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const currentWork = JSON.parse(fs.readFileSync(currentWorkPath, 'utf-8'));
|
|
32
|
+
|
|
33
|
+
// Update to done using shared updateStatus (includes epic auto-close logic)
|
|
34
|
+
(async () => {
|
|
35
|
+
try {
|
|
36
|
+
const { updateStatus } = require('__JETTYPOD_ROOT__/features/work-tracking');
|
|
37
|
+
await updateStatus(currentWork.id, 'done');
|
|
38
|
+
|
|
39
|
+
// Clear current work pointer since work is done
|
|
40
|
+
fs.unlinkSync(currentWorkPath);
|
|
41
|
+
|
|
42
|
+
console.log(`\n✓ Work item #${currentWork.id} status updated: → done (merged to ${currentBranch})`);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.error('Failed to update work item:', err);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
})();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Pre-commit hook: Run tests before allowing commit
|
|
4
|
+
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
// Check if we're in a real project (not a test directory)
|
|
10
|
+
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
11
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
12
|
+
// Skip tests in test directories
|
|
13
|
+
process.exit(0);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
console.log('\n🧪 Running tests before commit...\n');
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
// Run tests
|
|
20
|
+
execSync('npm test', { stdio: 'inherit' });
|
|
21
|
+
|
|
22
|
+
console.log('\n✅ Tests passed! Proceeding with commit.\n');
|
|
23
|
+
process.exit(0);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
console.log('\n❌ Tests failed! Commit blocked.\n');
|
|
26
|
+
console.log('Fix the failing tests or use --no-verify to skip this check.\n');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const { Given, When, Then } = require('@cucumber/cucumber');
|
|
2
|
+
const assert = require('assert');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
const sqlite3 = require('sqlite3').verbose();
|
|
7
|
+
|
|
8
|
+
const testDir = path.join('/tmp', 'hooks-simple-' + Date.now());
|
|
9
|
+
const dbFileName = process.env.NODE_ENV === 'test' ? 'test-work.db' : 'work.db';
|
|
10
|
+
let originalDir;
|
|
11
|
+
let workItemId;
|
|
12
|
+
|
|
13
|
+
Given('I initialize a git repo with devpod', function () {
|
|
14
|
+
originalDir = process.cwd();
|
|
15
|
+
// SAFETY: Only delete if testDir is in /tmp
|
|
16
|
+
if (fs.existsSync(testDir) && testDir.startsWith('/tmp/')) {
|
|
17
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
18
|
+
}
|
|
19
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
20
|
+
process.chdir(testDir);
|
|
21
|
+
|
|
22
|
+
execSync('git init', { stdio: 'pipe' });
|
|
23
|
+
execSync('git config user.email "test@test.com"', { stdio: 'pipe' });
|
|
24
|
+
execSync('git config user.name "Test"', { stdio: 'pipe' });
|
|
25
|
+
execSync(`node ${path.join(originalDir, 'jettypod.js')} init`, { stdio: 'pipe' });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
Given('I create and start work on a todo item', function () {
|
|
29
|
+
execSync(`node ${path.join(originalDir, 'jettypod.js')} work create feature "Test"`, { stdio: 'pipe' });
|
|
30
|
+
execSync(`node ${path.join(originalDir, 'jettypod.js')} work status 1 todo`, { stdio: 'pipe' });
|
|
31
|
+
execSync(`node ${path.join(originalDir, 'jettypod.js')} work start 1`, { stdio: 'pipe' });
|
|
32
|
+
workItemId = 1;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
Then('the hook updates status to in_progress', function () {
|
|
36
|
+
const dbPath = path.join(testDir, '.jettypod', dbFileName);
|
|
37
|
+
const db = new sqlite3.Database(dbPath);
|
|
38
|
+
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
db.get(`SELECT status FROM work_items WHERE id = ?`, [workItemId], (err, row) => {
|
|
41
|
+
db.close();
|
|
42
|
+
assert.strictEqual(row.status, 'in_progress');
|
|
43
|
+
|
|
44
|
+
// Cleanup
|
|
45
|
+
if (fs.existsSync(testDir)) {
|
|
46
|
+
process.chdir(originalDir);
|
|
47
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
resolve();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Feature: Git Hooks Integration
|
|
2
|
+
As a developer
|
|
3
|
+
I want work item status to auto-update on commits
|
|
4
|
+
So I don't manually track progress
|
|
5
|
+
|
|
6
|
+
Scenario: Integration - post-commit hook updates status
|
|
7
|
+
Given I initialize a git repo with devpod
|
|
8
|
+
And I create and start work on a todo item
|
|
9
|
+
When I make a commit
|
|
10
|
+
Then the hook updates status to in_progress
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
const { Given, When, Then } = require('@cucumber/cucumber');
|
|
2
|
+
const assert = require('assert');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
const sqlite3 = require('sqlite3').verbose();
|
|
7
|
+
|
|
8
|
+
const testDir = path.join('/tmp', 'jettypod-hooks-test-' + Date.now());
|
|
9
|
+
const dbFileName = process.env.NODE_ENV === 'test' ? 'test-work.db' : 'work.db';
|
|
10
|
+
let originalDir;
|
|
11
|
+
|
|
12
|
+
// Helper to ensure NODE_ENV=test is always set for test execSync calls
|
|
13
|
+
function testExecSync(command, options = {}) {
|
|
14
|
+
const env = { ...process.env, NODE_ENV: 'test', ...options.env };
|
|
15
|
+
return execSync(command, { ...options, env });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Given('I have initialized jettypod with git', function () {
|
|
19
|
+
originalDir = process.cwd();
|
|
20
|
+
// SAFETY: Only delete if testDir is in /tmp
|
|
21
|
+
if (fs.existsSync(testDir) && testDir.startsWith('/tmp/')) {
|
|
22
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
25
|
+
process.chdir(testDir);
|
|
26
|
+
|
|
27
|
+
execSync('git init', { stdio: 'pipe' });
|
|
28
|
+
execSync('git config user.email "test@test.com"', { stdio: 'pipe' });
|
|
29
|
+
execSync('git config user.name "Test"', { stdio: 'pipe' });
|
|
30
|
+
execSync(`node ${path.join(originalDir, 'jettypod.js')} init`, { stdio: 'pipe' });
|
|
31
|
+
|
|
32
|
+
// Verify hooks installed
|
|
33
|
+
const hookPath = path.join(testDir, '.git', 'hooks', 'post-commit');
|
|
34
|
+
this.hooksInstalled = fs.existsSync(hookPath);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
Given('I create a work item via work commands', function () {
|
|
38
|
+
execSync(`node ${path.join(originalDir, 'jettypod.js')} work create feature "Test Feature"`, { stdio: 'pipe' });
|
|
39
|
+
this.workItemId = 1;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
Given('I start work on the item', function () {
|
|
43
|
+
const jettypodPath = path.join(__dirname, '../../jettypod.js');
|
|
44
|
+
const workDir = process.cwd();
|
|
45
|
+
const workItemId = this.workItemId || 1;
|
|
46
|
+
|
|
47
|
+
testExecSync(`node ${jettypodPath} work start ${workItemId}`, { cwd: workDir, stdio: 'pipe' });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
When('I commit changes', function () {
|
|
51
|
+
fs.writeFileSync('test.txt', 'test content');
|
|
52
|
+
execSync('git add .', { stdio: 'pipe' });
|
|
53
|
+
execSync('git commit -m "test commit"', { stdio: 'pipe' });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
Then('the work item status updates automatically', function () {
|
|
57
|
+
const dbPath = path.join(testDir, '.jettypod', dbFileName);
|
|
58
|
+
const db = new sqlite3.Database(dbPath);
|
|
59
|
+
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
db.get(`SELECT status FROM work_items WHERE id = ?`, [this.workItemId], (err, row) => {
|
|
62
|
+
db.close();
|
|
63
|
+
// Should be in_progress (updated by post-commit hook)
|
|
64
|
+
assert(row.status === 'in_progress' || row.status === 'backlog',
|
|
65
|
+
`Expected in_progress or backlog, got ${row.status}`);
|
|
66
|
+
resolve();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
Then('the current work file still exists', function () {
|
|
72
|
+
const currentWorkPath = path.join(testDir, '.jettypod', 'current-work.json');
|
|
73
|
+
assert(fs.existsSync(currentWorkPath), 'Current work file should still exist');
|
|
74
|
+
|
|
75
|
+
// Cleanup
|
|
76
|
+
if (fs.existsSync(testDir)) {
|
|
77
|
+
process.chdir(originalDir);
|
|
78
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
Given('I have a work item with status {string}', function (status) {
|
|
83
|
+
originalDir = process.cwd();
|
|
84
|
+
// SAFETY: Only delete if testDir is in /tmp
|
|
85
|
+
if (fs.existsSync(testDir) && testDir.startsWith('/tmp/')) {
|
|
86
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
87
|
+
}
|
|
88
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
89
|
+
process.chdir(testDir);
|
|
90
|
+
|
|
91
|
+
execSync('git init', { stdio: 'pipe' });
|
|
92
|
+
execSync('git config user.email "test@test.com"', { stdio: 'pipe' });
|
|
93
|
+
execSync('git config user.name "Test"', { stdio: 'pipe' });
|
|
94
|
+
|
|
95
|
+
// Make initial commit so branch exists
|
|
96
|
+
fs.writeFileSync('README.md', '# Test');
|
|
97
|
+
execSync('git add .', { stdio: 'pipe' });
|
|
98
|
+
execSync('git commit -m "Initial commit"', { stdio: 'pipe' });
|
|
99
|
+
|
|
100
|
+
// Store default branch name
|
|
101
|
+
this.defaultBranch = execSync('git branch --show-current', { encoding: 'utf-8' }).trim();
|
|
102
|
+
|
|
103
|
+
execSync(`node ${path.join(originalDir, 'jettypod.js')} init`, { stdio: 'pipe' });
|
|
104
|
+
|
|
105
|
+
execSync(`node ${path.join(originalDir, 'jettypod.js')} work create feature "Test"`, { stdio: 'pipe' });
|
|
106
|
+
execSync(`node ${path.join(originalDir, 'jettypod.js')} work status 1 ${status}`, { stdio: 'pipe' });
|
|
107
|
+
|
|
108
|
+
this.workItemId = 1;
|
|
109
|
+
this.initialStatus = status;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
Given('the work item is set as current work', function () {
|
|
113
|
+
execSync(`node ${path.join(originalDir, 'jettypod.js')} work start ${this.workItemId}`, { stdio: 'pipe' });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
Given('I am on a feature branch', function () {
|
|
117
|
+
// Make initial commit on main so it exists
|
|
118
|
+
fs.writeFileSync('init.txt', 'init');
|
|
119
|
+
execSync('git add .', { stdio: 'pipe' });
|
|
120
|
+
execSync('git commit -m "initial commit"', { stdio: 'pipe' });
|
|
121
|
+
|
|
122
|
+
// Now create feature branch
|
|
123
|
+
execSync('git checkout -b feature/test', { stdio: 'pipe' });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
When('I make my first commit', function () {
|
|
127
|
+
fs.writeFileSync('test.txt', 'test');
|
|
128
|
+
execSync('git add .', { stdio: 'pipe' });
|
|
129
|
+
execSync('git commit -m "first commit"', { stdio: 'pipe' });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
When('I merge to main', function () {
|
|
133
|
+
// Commit on feature branch
|
|
134
|
+
fs.writeFileSync('feature.txt', 'feature');
|
|
135
|
+
execSync('git add .', { stdio: 'pipe' });
|
|
136
|
+
execSync('git commit -m "feature commit"', { stdio: 'pipe' });
|
|
137
|
+
|
|
138
|
+
// Switch to default branch and merge
|
|
139
|
+
execSync(`git checkout ${this.defaultBranch}`, { stdio: 'pipe' });
|
|
140
|
+
execSync('git merge feature/test --no-edit', { stdio: 'pipe' });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
Then('the work item status should be {string}', function (expectedStatus) {
|
|
144
|
+
const dbPath = path.join(testDir, '.jettypod', dbFileName);
|
|
145
|
+
|
|
146
|
+
return new Promise((resolve) => {
|
|
147
|
+
// Give the post-merge hook time to complete
|
|
148
|
+
setTimeout(() => {
|
|
149
|
+
const db = new sqlite3.Database(dbPath);
|
|
150
|
+
db.get(`SELECT status FROM work_items WHERE id = ?`, [this.workItemId], (err, row) => {
|
|
151
|
+
db.close();
|
|
152
|
+
assert.strictEqual(row.status, expectedStatus);
|
|
153
|
+
|
|
154
|
+
// Cleanup
|
|
155
|
+
if (fs.existsSync(testDir)) {
|
|
156
|
+
process.chdir(originalDir);
|
|
157
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
resolve();
|
|
161
|
+
});
|
|
162
|
+
}, 50); // Small delay to ensure hook completes
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
Given('no work item is set as current', function () {
|
|
167
|
+
originalDir = process.cwd();
|
|
168
|
+
// SAFETY: Only delete if testDir is in /tmp
|
|
169
|
+
if (fs.existsSync(testDir) && testDir.startsWith('/tmp/')) {
|
|
170
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
171
|
+
}
|
|
172
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
173
|
+
process.chdir(testDir);
|
|
174
|
+
|
|
175
|
+
execSync('git init', { stdio: 'pipe' });
|
|
176
|
+
execSync('git config user.email "test@test.com"', { stdio: 'pipe' });
|
|
177
|
+
execSync('git config user.name "Test"', { stdio: 'pipe' });
|
|
178
|
+
execSync(`node ${path.join(originalDir, 'jettypod.js')} init`, { stdio: 'pipe' });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
When('I make a commit', function () {
|
|
182
|
+
fs.writeFileSync('test.txt', 'test');
|
|
183
|
+
execSync('git add .', { stdio: 'pipe' });
|
|
184
|
+
execSync('git commit -m "test"', { stdio: 'pipe' });
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
Then('no errors occur', function () {
|
|
188
|
+
// If we got here, no errors occurred
|
|
189
|
+
assert(true);
|
|
190
|
+
|
|
191
|
+
// Cleanup
|
|
192
|
+
if (fs.existsSync(testDir)) {
|
|
193
|
+
process.chdir(originalDir);
|
|
194
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
195
|
+
}
|
|
196
|
+
});
|