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.
Files changed (122) hide show
  1. package/.claude/PROTECT_SKILLS.md +28 -0
  2. package/.claude/settings.json +24 -0
  3. package/.claude/settings.local.json +16 -0
  4. package/.claude/skills/epic-discover/SKILL.md +262 -0
  5. package/.claude/skills/feature-discover/SKILL.md +393 -0
  6. package/.claude/skills/speed-mode/SKILL.md +364 -0
  7. package/.claude/skills/stable-mode/SKILL.md +591 -0
  8. package/.github/workflows/test-safety.yml +85 -0
  9. package/README.md +25 -0
  10. package/SPEED-STABLE-AUDIT.md +853 -0
  11. package/SYSTEM-BEHAVIOR.md +1241 -0
  12. package/TEST_SAFETY_AUDIT.md +314 -0
  13. package/TEST_SAFETY_IMPLEMENTATION.md +97 -0
  14. package/cucumber.js +8 -0
  15. package/docs/COMMAND_REFERENCE.md +903 -0
  16. package/docs/DECISIONS.md +68 -0
  17. package/docs/README.md +48 -0
  18. package/docs/STANDARDS-SYSTEM-DOCUMENTATION.md +374 -0
  19. package/docs/TEST-REWRITE-PLAN.md +261 -0
  20. package/docs/ai-test-writing-requirements.md +219 -0
  21. package/docs/claude-code-skills.md +607 -0
  22. package/docs/core-jettypod-methodology/comprehensive-jettypod-methodology.md +582 -0
  23. package/docs/core-jettypod-methodology/deprecated/jettypod-comprehensive-standards.md +1222 -0
  24. package/docs/core-jettypod-methodology/deprecated/jettypod-operating-guide.md +3399 -0
  25. package/docs/core-jettypod-methodology/deprecated/jettypod-technical-checklist.md +1325 -0
  26. package/docs/core-jettypod-methodology/deprecated/jettypod-vibe-coding-framework.md +1544 -0
  27. package/docs/core-jettypod-methodology/deprecated/prompt-engineering-guide.md +320 -0
  28. package/docs/core-jettypod-methodology/deprecated/vibe-coding-cheatsheet (1).md +516 -0
  29. package/docs/core-jettypod-methodology/deprecated/vibe-coding-framework.md +1544 -0
  30. package/docs/features/jettypod-standards-explained.md +543 -0
  31. package/docs/features/standards-inventory.md +257 -0
  32. package/docs/gap-analysis-current-vs-comprehensive-methodology.md +939 -0
  33. package/docs/jettypod-system-overview.md +409 -0
  34. package/features/auto-generate-production-chores.feature +14 -0
  35. package/features/claude-md-protection/steps.js +487 -0
  36. package/features/decisions/index.js +490 -0
  37. package/features/decisions/index.test.js +208 -0
  38. package/features/git-hooks/git-hooks.feature +30 -0
  39. package/features/git-hooks/index.js +93 -0
  40. package/features/git-hooks/index.test.js +137 -0
  41. package/features/git-hooks/post-commit +56 -0
  42. package/features/git-hooks/post-merge +47 -0
  43. package/features/git-hooks/pre-commit +28 -0
  44. package/features/git-hooks/simple-steps.js +53 -0
  45. package/features/git-hooks/simple-test.feature +10 -0
  46. package/features/git-hooks/steps.js +196 -0
  47. package/features/jettypod-update-command.feature +46 -0
  48. package/features/mode-prompts/index.js +95 -0
  49. package/features/mode-prompts/simple-steps.js +44 -0
  50. package/features/mode-prompts/simple-test.feature +9 -0
  51. package/features/mode-prompts/validation.test.js +120 -0
  52. package/features/refactor-mode/steps.js +217 -0
  53. package/features/refactor-mode.feature +49 -0
  54. package/features/skills-update/index.test.js +216 -0
  55. package/features/step_definitions/auto-generate-production-chores.steps.js +162 -0
  56. package/features/step_definitions/terminal-logo.steps.js +145 -0
  57. package/features/step_definitions/update-command.steps.js +183 -0
  58. package/features/terminal-logo/index.js +39 -0
  59. package/features/terminal-logo/terminal-logo.feature +30 -0
  60. package/features/update-command/index.js +181 -0
  61. package/features/update-command/index.test.js +225 -0
  62. package/features/work-commands/bug-workflow-display.feature +22 -0
  63. package/features/work-commands/index.js +311 -0
  64. package/features/work-commands/simple-steps.js +69 -0
  65. package/features/work-commands/stable-tests.feature +57 -0
  66. package/features/work-commands/steps.js +1120 -0
  67. package/features/work-commands/validation.test.js +88 -0
  68. package/features/work-commands/work-commands.feature +13 -0
  69. package/features/work-tracking/discovery-validation.test.js +228 -0
  70. package/features/work-tracking/index.js +1511 -0
  71. package/features/work-tracking/mode-required.feature +112 -0
  72. package/features/work-tracking/phase-tracking.test.js +482 -0
  73. package/features/work-tracking/prototype-tracking.test.js +485 -0
  74. package/features/work-tracking/tree-view.test.js +310 -0
  75. package/features/work-tracking/work-set-mode.feature +71 -0
  76. package/features/work-tracking/work-start-mode.feature +88 -0
  77. package/full-test.txt +0 -0
  78. package/install.sh +89 -0
  79. package/jettypod.js +1640 -0
  80. package/lib/bug-workflow.js +94 -0
  81. package/lib/bug-workflow.test.js +177 -0
  82. package/lib/claudemd.js +130 -0
  83. package/lib/claudemd.test.js +195 -0
  84. package/lib/comprehensive-standards-full.json +1778 -0
  85. package/lib/config.js +181 -0
  86. package/lib/config.test.js +511 -0
  87. package/lib/constants.js +107 -0
  88. package/lib/constants.test.js +164 -0
  89. package/lib/current-work.js +130 -0
  90. package/lib/current-work.test.js +146 -0
  91. package/lib/database-project-config.test.js +107 -0
  92. package/lib/database.js +256 -0
  93. package/lib/database.test.js +106 -0
  94. package/lib/decisions-generator.js +102 -0
  95. package/lib/decisions-generator.test.js +457 -0
  96. package/lib/decisions-helpers.js +119 -0
  97. package/lib/decisions-helpers.test.js +310 -0
  98. package/lib/discovery-checkpoint.js +83 -0
  99. package/lib/docs-generator.js +280 -0
  100. package/lib/external-checklist.js +177 -0
  101. package/lib/git.js +142 -0
  102. package/lib/git.test.js +145 -0
  103. package/lib/logo.js +3 -0
  104. package/lib/migrations/001-epic-to-parent.js +24 -0
  105. package/lib/migrations/002-default-work-item-modes.js +37 -0
  106. package/lib/migrations/002-default-work-item-modes.test.js +351 -0
  107. package/lib/migrations/003-epic-discovery-fields.js +52 -0
  108. package/lib/migrations/004-discovery-decisions-table.js +32 -0
  109. package/lib/migrations/005-migrate-decision-data.js +62 -0
  110. package/lib/migrations/006-feature-phase-field.js +61 -0
  111. package/lib/migrations/007-prototype-tracking.js +38 -0
  112. package/lib/migrations/008-scenario-file-field.js +24 -0
  113. package/lib/migrations/index.js +74 -0
  114. package/lib/production-helpers.js +69 -0
  115. package/lib/project-state.test.js +92 -0
  116. package/lib/test-helpers.js +184 -0
  117. package/lib/test-helpers.test.js +255 -0
  118. package/package.json +36 -0
  119. package/prototypes/test/index.html +1 -0
  120. package/setup-dist-repo.sh +68 -0
  121. package/test-safety-check.sh +80 -0
  122. 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
+ });