jettypod 4.4.21 → 4.4.22

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.
@@ -0,0 +1,94 @@
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 jettypodHooksDir = 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(jettypodHooksDir, 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 (project root where jettypod.js is)
39
+ // Use __dirname which is reliable in this context (features/git-hooks/)
40
+ const jettypodRoot = path.resolve(__dirname, '..', '..');
41
+ const modulesPath = path.join(jettypodRoot, 'node_modules');
42
+
43
+ // Inject NODE_PATH setup after shebang
44
+ if (hookContent.includes('require(\'sqlite3\')')) {
45
+ hookContent = hookContent.replace(
46
+ '#!/usr/bin/env node',
47
+ `#!/usr/bin/env node\n\n// Injected module path\nprocess.env.NODE_PATH = '${modulesPath}' + ':' + (process.env.NODE_PATH || '');require('module').Module._initPaths();`
48
+ );
49
+ }
50
+
51
+ // Replace __JETTYPOD_ROOT__ placeholder with actual jettypod root path
52
+ hookContent = hookContent.replace(/__JETTYPOD_ROOT__/g, jettypodRoot);
53
+
54
+ fs.writeFileSync(targetPath, hookContent);
55
+
56
+ // Make executable
57
+ fs.chmodSync(targetPath, 0o755);
58
+ } catch (err) {
59
+ throw new Error(`Failed to install ${hook} hook: ${err.message}`);
60
+ }
61
+ });
62
+
63
+ console.log('✓ Git hooks installed (pre-commit, post-commit, post-merge)');
64
+ return true;
65
+ }
66
+
67
+ /**
68
+ * Check if JettyPod git hooks are installed
69
+ * @returns {boolean} True if hooks are installed
70
+ */
71
+ function areHooksInstalled() {
72
+ const gitHooksDir = path.join(process.cwd(), '.git', 'hooks');
73
+ if (!fs.existsSync(gitHooksDir)) {
74
+ return false;
75
+ }
76
+
77
+ try {
78
+ const postCommitPath = path.join(gitHooksDir, 'post-commit');
79
+ if (!fs.existsSync(postCommitPath)) {
80
+ return false;
81
+ }
82
+
83
+ const content = fs.readFileSync(postCommitPath, 'utf-8');
84
+ return content.includes('JettyPod');
85
+ } catch (err) {
86
+ // If we can't read the file, assume not installed
87
+ return false;
88
+ }
89
+ }
90
+
91
+ module.exports = {
92
+ installHooks,
93
+ areHooksInstalled
94
+ };
@@ -0,0 +1,59 @@
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
+ // This hook is for jettypod-source development only
23
+ const { updateStatus } = require(path.join(process.cwd(), 'features', 'work-tracking'));
24
+ await updateStatus(currentWork.id, 'in_progress');
25
+
26
+ // Update current work file
27
+ currentWork.status = 'in_progress';
28
+ fs.writeFileSync(currentWorkPath, JSON.stringify(currentWork, null, 2));
29
+
30
+ console.log(`\n✓ Work item #${currentWork.id} status updated: todo → in_progress`);
31
+ } catch (err) {
32
+ console.error('Failed to update work item:', err);
33
+ }
34
+ }
35
+ }
36
+ })();
37
+
38
+ // Only generate docs in worktrees (skip on main branch)
39
+ if (process.cwd().includes('.jettypod-work')) {
40
+ generateDocs();
41
+ }
42
+
43
+ function generateDocs() {
44
+ const featuresDir = path.join(process.cwd(), 'features');
45
+ const outputPath = path.join(process.cwd(), 'SYSTEM-BEHAVIOR.md');
46
+
47
+ // Skip if no features directory
48
+ if (!fs.existsSync(featuresDir)) {
49
+ return;
50
+ }
51
+
52
+ try {
53
+ const docsGenerator = require(path.join(process.cwd(), 'lib', 'docs-generator'));
54
+ docsGenerator.generate();
55
+ console.log('📚 Documentation updated');
56
+ } catch (err) {
57
+ // Fail silently - docs generation is optional
58
+ }
59
+ }
@@ -0,0 +1,71 @@
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
+ // Detect the default branch and check if we're on it
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
+ let defaultBranch;
28
+ try {
29
+ // Try to get the default branch from git config
30
+ defaultBranch = execSync('git symbolic-ref refs/remotes/origin/HEAD', {
31
+ encoding: 'utf8',
32
+ stdio: ['pipe', 'pipe', 'pipe']
33
+ }).trim().replace('refs/remotes/origin/', '');
34
+ } catch {
35
+ // Fallback: check which common branch names exist
36
+ try {
37
+ execSync('git rev-parse --verify main', { stdio: 'pipe' });
38
+ defaultBranch = 'main';
39
+ } catch {
40
+ try {
41
+ execSync('git rev-parse --verify master', { stdio: 'pipe' });
42
+ defaultBranch = 'master';
43
+ } catch {
44
+ // If neither exists, skip the hook
45
+ process.exit(0);
46
+ }
47
+ }
48
+ }
49
+
50
+ // Only update status when merging to the default branch
51
+ if (currentBranch !== defaultBranch) {
52
+ process.exit(0);
53
+ }
54
+
55
+ const currentWork = JSON.parse(fs.readFileSync(currentWorkPath, 'utf-8'));
56
+
57
+ // Update to done using shared updateStatus (includes epic auto-close logic)
58
+ (async () => {
59
+ try {
60
+ const { updateStatus } = require('__JETTYPOD_ROOT__/features/work-tracking');
61
+ await updateStatus(currentWork.id, 'done');
62
+
63
+ // Clear current work pointer since work is done
64
+ fs.unlinkSync(currentWorkPath);
65
+
66
+ console.log(`\n✓ Work item #${currentWork.id} status updated: → done (merged to ${currentBranch})`);
67
+ } catch (err) {
68
+ console.error('Failed to update work item:', err);
69
+ process.exit(1);
70
+ }
71
+ })();
@@ -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 jettypod', 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', env: { ...process.env, NODE_ENV: 'test' } });
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', env: { ...process.env, NODE_ENV: 'test' } });
30
+ execSync(`node ${path.join(originalDir, 'jettypod.js')} work status 1 todo`, { stdio: 'pipe', env: { ...process.env, NODE_ENV: 'test' } });
31
+ execSync(`node ${path.join(originalDir, 'jettypod.js')} work start 1`, { stdio: 'pipe', env: { ...process.env, NODE_ENV: 'test' } });
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 jettypod
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', env: { ...process.env, NODE_ENV: 'test' } });
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', env: { ...process.env, NODE_ENV: 'test' } });
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', env: { ...process.env, NODE_ENV: 'test' } });
104
+
105
+ execSync(`node ${path.join(originalDir, 'jettypod.js')} work create feature "Test"`, { stdio: 'pipe', env: { ...process.env, NODE_ENV: 'test' } });
106
+ execSync(`node ${path.join(originalDir, 'jettypod.js')} work status 1 ${status}`, { stdio: 'pipe', env: { ...process.env, NODE_ENV: 'test' } });
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', env: { ...process.env, NODE_ENV: 'test' } });
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', env: { ...process.env, NODE_ENV: 'test' } });
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
+ });
@@ -0,0 +1,95 @@
1
+ const readline = require('readline');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const sqlite3 = require('sqlite3').verbose();
5
+
6
+ /**
7
+ * Prompt user for work item status update
8
+ * @param {Object} currentWork - Current work item with id, title, and status
9
+ * @returns {Promise<string|null>} New status or null if skipped
10
+ * @throws {Error} If currentWork is invalid or missing required fields
11
+ */
12
+ function promptForStatusUpdate(currentWork) {
13
+ // Validate input
14
+ if (!currentWork || typeof currentWork !== 'object') {
15
+ return Promise.reject(new Error('Invalid currentWork: must be an object'));
16
+ }
17
+
18
+ if (!currentWork.id || !currentWork.title || !currentWork.status) {
19
+ return Promise.reject(new Error('Invalid currentWork: missing id, title, or status'));
20
+ }
21
+
22
+ return new Promise((resolve, reject) => {
23
+ const rl = readline.createInterface({
24
+ input: process.stdin,
25
+ output: process.stdout
26
+ });
27
+
28
+ console.log(`\nCurrent work: [#${currentWork.id}] ${currentWork.title} (${currentWork.status})`);
29
+ rl.question('Update status? [in_progress/blocked/done/skip]: ', (answer) => {
30
+ rl.close();
31
+
32
+ const status = answer.trim();
33
+ if (status === 'skip' || status === '') {
34
+ resolve(null);
35
+ } else if (['in_progress', 'blocked', 'done', 'todo', 'backlog'].includes(status)) {
36
+ resolve(status);
37
+ } else {
38
+ console.log('Invalid status, skipping update');
39
+ resolve(null);
40
+ }
41
+ });
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Update work item status in database and current work file
47
+ * Uses shared updateStatus from work-tracking for consistency and epic auto-close logic
48
+ * @param {number} workItemId - ID of work item to update
49
+ * @param {string} newStatus - New status value
50
+ * @returns {Promise<void>}
51
+ * @throws {Error} If workItemId is invalid, newStatus is invalid, database not found, or file operations fail
52
+ */
53
+ async function updateWorkItemStatus(workItemId, newStatus) {
54
+ // Validate inputs
55
+ if (!workItemId || isNaN(workItemId) || workItemId < 1) {
56
+ return Promise.reject(new Error('Invalid work item ID'));
57
+ }
58
+
59
+ const validStatuses = ['in_progress', 'blocked', 'done', 'todo', 'backlog', 'cancelled'];
60
+ if (!newStatus || !validStatuses.includes(newStatus)) {
61
+ return Promise.reject(new Error(`Invalid status: ${newStatus}`));
62
+ }
63
+
64
+ const jettypodDir = path.join(process.cwd(), '.jettypod');
65
+ const { getDbPath } = require('../../lib/database');
66
+ const dbPath = getDbPath();
67
+ const currentWorkPath = path.join(jettypodDir, 'current-work.json');
68
+
69
+ // Check database exists
70
+ if (!fs.existsSync(dbPath)) {
71
+ return Promise.reject(new Error('Work database not found. Run: jettypod init'));
72
+ }
73
+
74
+ // Use shared updateStatus from work-tracking (includes epic auto-close logic)
75
+ const { updateStatus } = require('../work-tracking');
76
+ await updateStatus(workItemId, newStatus);
77
+
78
+ // Update current work file if it exists
79
+ if (fs.existsSync(currentWorkPath)) {
80
+ try {
81
+ const currentWork = JSON.parse(fs.readFileSync(currentWorkPath, 'utf-8'));
82
+ currentWork.status = newStatus;
83
+ fs.writeFileSync(currentWorkPath, JSON.stringify(currentWork, null, 2));
84
+ } catch (fileErr) {
85
+ return Promise.reject(new Error(`Failed to update current work file: ${fileErr.message}`));
86
+ }
87
+ }
88
+
89
+ console.log(`✓ Work item #${workItemId} status updated to ${newStatus}`);
90
+ }
91
+
92
+ module.exports = {
93
+ promptForStatusUpdate,
94
+ updateWorkItemStatus
95
+ };
@@ -0,0 +1,44 @@
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
+
7
+ const testDir = path.join('/tmp', 'mode-prompts-' + Date.now());
8
+ let originalDir;
9
+
10
+ Given('I have jettypod with a work item in progress', function () {
11
+ originalDir = process.cwd();
12
+ // SAFETY: Only delete if testDir is in /tmp
13
+ if (fs.existsSync(testDir) && testDir.startsWith('/tmp/')) {
14
+ fs.rmSync(testDir, { recursive: true, force: true });
15
+ }
16
+ fs.mkdirSync(testDir, { recursive: true });
17
+ process.chdir(testDir);
18
+
19
+ execSync('git init', { stdio: 'pipe' });
20
+ execSync('git config user.email "test@test.com"', { stdio: 'pipe' });
21
+ execSync('git config user.name "Test"', { stdio: 'pipe' });
22
+ execSync(`node ${path.join(originalDir, 'jettypod.js')} init`, { stdio: 'pipe', env: { ...process.env, NODE_ENV: 'test' } });
23
+ execSync(`node ${path.join(originalDir, 'jettypod.js')} work create feature "Test"`, { stdio: 'pipe', env: { ...process.env, NODE_ENV: 'test' } });
24
+ execSync(`node ${path.join(originalDir, 'jettypod.js')} work start 1`, { stdio: 'pipe', env: { ...process.env, NODE_ENV: 'test' } });
25
+ });
26
+
27
+ When('I check current work exists', function () {
28
+ const currentWorkPath = path.join(testDir, '.jettypod', 'current-work.json');
29
+ this.currentWorkExists = fs.existsSync(currentWorkPath);
30
+ if (this.currentWorkExists) {
31
+ this.currentWork = JSON.parse(fs.readFileSync(currentWorkPath, 'utf-8'));
32
+ }
33
+ });
34
+
35
+ Then('the work item has a status', function () {
36
+ assert(this.currentWorkExists, 'Current work file should exist');
37
+ assert(this.currentWork.status, 'Work item should have a status');
38
+
39
+ // Cleanup
40
+ if (fs.existsSync(testDir)) {
41
+ process.chdir(originalDir);
42
+ fs.rmSync(testDir, { recursive: true, force: true });
43
+ }
44
+ });
@@ -0,0 +1,9 @@
1
+ Feature: Mode Change Prompts
2
+ As a developer
3
+ I want to update work status when switching modes
4
+ So my work state stays current
5
+
6
+ Scenario: Integration - mode change with work item present
7
+ Given I have jettypod with a work item in progress
8
+ When I check current work exists
9
+ Then the work item has a status