jettypod 4.4.19 → 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.
package/jettypod.js CHANGED
@@ -166,7 +166,7 @@ const claude = {
166
166
  let currentWork = null;
167
167
  let currentWorkSection = '';
168
168
  try {
169
- const workTracking = require('./features/work-tracking/index.js');
169
+ const workTracking = require('./lib/work-tracking/index.js');
170
170
  currentWork = await workTracking.getCurrentWork();
171
171
 
172
172
  if (currentWork) {
@@ -674,7 +674,7 @@ async function generateClaude(options = {}) {
674
674
 
675
675
  // Initialize project (used by both 'jettypod init' and 'jettypod' with no args)
676
676
  async function initializeProject() {
677
- const { showLogo } = require('./features/terminal-logo');
677
+ const { showLogo } = require('./lib/terminal-logo');
678
678
  showLogo();
679
679
 
680
680
  if (!fs.existsSync('.jettypod')) {
@@ -740,7 +740,7 @@ async function initializeProject() {
740
740
  await generateClaude();
741
741
 
742
742
  // Install git hooks
743
- const gitHooks = require('./features/git-hooks/index.js');
743
+ const gitHooks = require('./lib/git-hooks/index.js');
744
744
  gitHooks.installHooks();
745
745
 
746
746
  // Install Claude Code hooks
@@ -1082,7 +1082,7 @@ const [,, command, ...args] = process.argv;
1082
1082
  switch (command) {
1083
1083
  case 'update': {
1084
1084
  // Update jettypod to latest version
1085
- const updateCommand = require('./features/update-command');
1085
+ const updateCommand = require('./lib/update-command');
1086
1086
  const success = await updateCommand.runUpdate();
1087
1087
 
1088
1088
  // Always refresh skills in current project after update attempt
@@ -1189,7 +1189,7 @@ switch (command) {
1189
1189
  const subcommand = args[0];
1190
1190
 
1191
1191
  if (subcommand === 'start') {
1192
- const workCommands = require('./features/work-commands/index.js');
1192
+ const workCommands = require('./lib/work-commands/index.js');
1193
1193
  const id = parseInt(args[1]);
1194
1194
  try {
1195
1195
  await workCommands.startWork(id);
@@ -1210,7 +1210,7 @@ switch (command) {
1210
1210
  process.exit(1);
1211
1211
  }
1212
1212
 
1213
- const workCommands = require('./features/work-commands/index.js');
1213
+ const workCommands = require('./lib/work-commands/index.js');
1214
1214
 
1215
1215
  // Check if status was provided as argument: work stop <id> <status>
1216
1216
  const statusArg = args[1]; // args[0] is the id (optional), args[1] is status
@@ -1244,7 +1244,7 @@ switch (command) {
1244
1244
  });
1245
1245
  }
1246
1246
  } else if (subcommand === 'cleanup') {
1247
- const workCommands = require('./features/work-commands/index.js');
1247
+ const workCommands = require('./lib/work-commands/index.js');
1248
1248
  const dryRun = args[0] === '--dry-run';
1249
1249
 
1250
1250
  try {
@@ -1264,7 +1264,7 @@ switch (command) {
1264
1264
  process.exit(1);
1265
1265
  }
1266
1266
  } else if (subcommand === 'merge') {
1267
- const workCommands = require('./features/work-commands/index.js');
1267
+ const workCommands = require('./lib/work-commands/index.js');
1268
1268
  try {
1269
1269
  // Parse merge flags and optional work item ID from args
1270
1270
  const withTransition = args.includes('--with-transition');
@@ -1291,7 +1291,7 @@ switch (command) {
1291
1291
  }
1292
1292
 
1293
1293
  // Delegate to work tracking module
1294
- const workTracking = require('./features/work-tracking/index.js');
1294
+ const workTracking = require('./lib/work-tracking/index.js');
1295
1295
  process.argv = ['node', 'work', ...args];
1296
1296
  workTracking.main();
1297
1297
  }
@@ -1299,7 +1299,7 @@ switch (command) {
1299
1299
 
1300
1300
  case 'backlog':
1301
1301
  // Backlog viewing - delegates to work tracking module
1302
- const workTracking = require('./features/work-tracking/index.js');
1302
+ const workTracking = require('./lib/work-tracking/index.js');
1303
1303
  process.argv = ['node', 'work-tracking', 'backlog', ...args];
1304
1304
  workTracking.main();
1305
1305
  break;
@@ -1343,7 +1343,7 @@ switch (command) {
1343
1343
  break;
1344
1344
 
1345
1345
  case 'decisions': {
1346
- const decisions = require('./features/decisions');
1346
+ const decisions = require('./lib/decisions');
1347
1347
 
1348
1348
  // Check for command-line flags
1349
1349
  const hasFlag = args.some(arg => arg.startsWith('--'));
@@ -1420,7 +1420,7 @@ switch (command) {
1420
1420
  }
1421
1421
 
1422
1422
  // Create Infrastructure Readiness epic with features and chores
1423
- const { create } = require('./features/work-tracking');
1423
+ const { create } = require('./lib/work-tracking');
1424
1424
  const { readStandards } = require('./lib/production-standards-reader');
1425
1425
  const { generateInfrastructureChores } = require('./lib/production-chore-generator');
1426
1426
 
@@ -0,0 +1,490 @@
1
+ // Stable Mode: decisions command with error handling and edge cases
2
+ const readline = require('readline');
3
+ const { getDb } = require('../../lib/database');
4
+ const config = require('../../lib/config');
5
+
6
+ let db;
7
+ try {
8
+ db = getDb();
9
+ } catch (err) {
10
+ console.error('❌ Database initialization failed');
11
+ console.error(`Error: ${err.message}`);
12
+ console.log('');
13
+ console.log('This could mean:');
14
+ console.log(' - JettyPod is not initialized (run jettypod init)');
15
+ console.log(' - Database file is corrupted');
16
+ console.log(' - Insufficient permissions');
17
+ process.exit(1);
18
+ }
19
+
20
+ /**
21
+ * Show interactive decisions menu
22
+ */
23
+ async function showDecisionsMenu() {
24
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
25
+ console.log('📋 DECISIONS');
26
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
27
+ console.log('');
28
+ console.log('What would you like to see?');
29
+ console.log('');
30
+ console.log(' 1. All decisions (chronological)');
31
+ console.log(' 2. Project-level decisions (UX, tech stack)');
32
+ console.log(' 3. Epic-level decisions (architecture, patterns)');
33
+ console.log(' 4. Decisions for a specific epic');
34
+ console.log(' 5. View DECISIONS.md');
35
+ console.log('');
36
+
37
+ return new Promise((resolve) => {
38
+ const rl = readline.createInterface({
39
+ input: process.stdin,
40
+ output: process.stdout
41
+ });
42
+
43
+ rl.question('Enter your choice (1-5): ', async (answer) => {
44
+ rl.close();
45
+
46
+ const trimmed = answer.trim();
47
+
48
+ // Handle empty input
49
+ if (!trimmed) {
50
+ console.log('');
51
+ console.log('❌ No choice entered');
52
+ console.log('Please run the command again and enter a number between 1-5');
53
+ console.log('');
54
+ process.exit(1);
55
+ }
56
+
57
+ const choice = parseInt(trimmed);
58
+
59
+ // Handle non-numeric input
60
+ if (isNaN(choice)) {
61
+ console.log('');
62
+ console.log(`❌ Invalid input: "${trimmed}"`);
63
+ console.log('Please enter a number between 1-5');
64
+ console.log('');
65
+ process.exit(1);
66
+ }
67
+
68
+ // Handle out of range
69
+ if (choice < 1 || choice > 5) {
70
+ console.log('');
71
+ console.log(`❌ Choice ${choice} is not valid`);
72
+ console.log('Please enter a number between 1-5:');
73
+ console.log(' 1 = All decisions');
74
+ console.log(' 2 = Project-level decisions');
75
+ console.log(' 3 = Epic-level decisions');
76
+ console.log(' 4 = Decisions for specific epic');
77
+ console.log(' 5 = View DECISIONS.md');
78
+ console.log('');
79
+ process.exit(1);
80
+ }
81
+
82
+ try {
83
+ switch (choice) {
84
+ case 1:
85
+ await showAllDecisions();
86
+ break;
87
+ case 2:
88
+ showProjectDecisions();
89
+ break;
90
+ case 3:
91
+ await showEpicDecisions();
92
+ break;
93
+ case 4:
94
+ await promptForEpicId();
95
+ break;
96
+ case 5:
97
+ viewDecisionsFile();
98
+ break;
99
+ }
100
+ resolve();
101
+ } catch (err) {
102
+ console.error('');
103
+ console.error('❌ An error occurred while displaying decisions');
104
+ console.error(`Error: ${err.message}`);
105
+ console.error('');
106
+ reject(err);
107
+ }
108
+ });
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Show all decisions chronologically
114
+ */
115
+ async function showAllDecisions() {
116
+ console.log('');
117
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
118
+ console.log('📋 ALL DECISIONS (Chronological)');
119
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
120
+ console.log('');
121
+
122
+ // Show project decisions first
123
+ let projectConfig;
124
+ try {
125
+ projectConfig = config.read();
126
+ } catch (err) {
127
+ console.error('⚠️ Unable to read project configuration');
128
+ console.error(`Error: ${err.message}`);
129
+ console.log('');
130
+ console.log('Skipping project-level decisions...');
131
+ console.log('');
132
+ }
133
+
134
+ if (projectConfig && projectConfig.project_discovery && projectConfig.project_discovery.winner) {
135
+ console.log('🎯 PROJECT DECISIONS');
136
+ console.log('');
137
+ console.log(`Winner: ${projectConfig.project_discovery.winner}`);
138
+ if (projectConfig.project_discovery.rationale) {
139
+ console.log(`Rationale: ${projectConfig.project_discovery.rationale}`);
140
+ }
141
+ if (projectConfig.project_discovery.started_date) {
142
+ try {
143
+ console.log(`Decided: ${new Date(projectConfig.project_discovery.started_date).toLocaleDateString()}`);
144
+ } catch (err) {
145
+ console.log(`Decided: ${projectConfig.project_discovery.started_date}`);
146
+ }
147
+ }
148
+ console.log('');
149
+ }
150
+
151
+ // Show epic decisions
152
+ return new Promise((resolve, reject) => {
153
+ db.all(`
154
+ SELECT dd.*, w.title as epic_title
155
+ FROM discovery_decisions dd
156
+ JOIN work_items w ON dd.work_item_id = w.id
157
+ ORDER BY dd.created_at ASC
158
+ `, [], (err, rows) => {
159
+ if (err) {
160
+ console.error('❌ Database error while retrieving epic decisions');
161
+ console.error(`Error: ${err.message}`);
162
+ console.log('');
163
+ console.log('This could mean:');
164
+ console.log(' - Database schema is out of date (run migrations)');
165
+ console.log(' - Database is corrupted');
166
+ console.log(' - Table discovery_decisions or work_items does not exist');
167
+ console.log('');
168
+ return reject(err);
169
+ }
170
+
171
+ if (rows && rows.length > 0) {
172
+ console.log('🎯 EPIC DECISIONS');
173
+ console.log('');
174
+
175
+ rows.forEach(row => {
176
+ console.log(`Epic #${row.work_item_id}: ${row.epic_title}`);
177
+ console.log(`├─ ${row.aspect}: ${row.decision}`);
178
+ console.log(`├─ Rationale: ${row.rationale || 'No rationale provided'}`);
179
+ try {
180
+ console.log(`└─ Decided: ${new Date(row.created_at).toLocaleDateString()}`);
181
+ } catch (err) {
182
+ console.log(`└─ Decided: ${row.created_at}`);
183
+ }
184
+ console.log('');
185
+ });
186
+ } else {
187
+ console.log('No epic decisions yet.');
188
+ console.log('');
189
+ console.log('💡 Tip: Make architectural decisions during epic planning:');
190
+ console.log(' jettypod work epic-planning <epic-id>');
191
+ console.log('');
192
+ }
193
+
194
+ resolve();
195
+ });
196
+ });
197
+ }
198
+
199
+ /**
200
+ * Show project-level decisions
201
+ */
202
+ function showProjectDecisions() {
203
+ console.log('');
204
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
205
+ console.log('📋 PROJECT-LEVEL DECISIONS');
206
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
207
+ console.log('');
208
+
209
+ let projectConfig;
210
+ try {
211
+ projectConfig = config.read();
212
+ } catch (err) {
213
+ console.error('❌ Unable to read project configuration');
214
+ console.error(`Error: ${err.message}`);
215
+ console.log('');
216
+ console.log('This could mean:');
217
+ console.log(' - .jettypod/config.json is missing or corrupted');
218
+ console.log(' - JettyPod is not initialized (run jettypod init)');
219
+ console.log(' - Insufficient permissions to read config file');
220
+ console.log('');
221
+ return;
222
+ }
223
+
224
+ if (projectConfig && projectConfig.project_discovery && projectConfig.project_discovery.winner) {
225
+ console.log(`Winner: ${projectConfig.project_discovery.winner}`);
226
+ if (projectConfig.project_discovery.rationale) {
227
+ console.log(`Rationale: ${projectConfig.project_discovery.rationale}`);
228
+ }
229
+ if (projectConfig.project_discovery.started_date) {
230
+ try {
231
+ console.log(`Decided: ${new Date(projectConfig.project_discovery.started_date).toLocaleDateString()}`);
232
+ } catch (err) {
233
+ console.log(`Decided: ${projectConfig.project_discovery.started_date}`);
234
+ }
235
+ }
236
+ } else {
237
+ console.log('No project-level decisions yet.');
238
+ console.log('');
239
+ console.log('💡 Tip: Start project discovery to make UX and tech stack decisions:');
240
+ console.log(' Talk to Claude Code about what you want to build');
241
+ console.log('');
242
+ }
243
+
244
+ console.log('');
245
+ }
246
+
247
+ /**
248
+ * Show epic-level decisions
249
+ */
250
+ async function showEpicDecisions() {
251
+ console.log('');
252
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
253
+ console.log('📋 EPIC-LEVEL DECISIONS');
254
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
255
+ console.log('');
256
+
257
+ return new Promise((resolve, reject) => {
258
+ db.all(`
259
+ SELECT dd.*, w.title as epic_title
260
+ FROM discovery_decisions dd
261
+ JOIN work_items w ON dd.work_item_id = w.id
262
+ ORDER BY dd.created_at DESC
263
+ `, [], (err, rows) => {
264
+ if (err) {
265
+ console.error('❌ Database error while retrieving epic decisions');
266
+ console.error(`Error: ${err.message}`);
267
+ console.log('');
268
+ console.log('This could mean:');
269
+ console.log(' - Database schema is out of date (run migrations)');
270
+ console.log(' - Database is corrupted');
271
+ console.log(' - Table discovery_decisions or work_items does not exist');
272
+ console.log('');
273
+ return reject(err);
274
+ }
275
+
276
+ if (rows && rows.length > 0) {
277
+ rows.forEach(row => {
278
+ console.log(`Epic #${row.work_item_id}: ${row.epic_title}`);
279
+ console.log(`├─ ${row.aspect}: ${row.decision}`);
280
+ console.log(`├─ Rationale: ${row.rationale || 'No rationale provided'}`);
281
+ try {
282
+ console.log(`└─ Decided: ${new Date(row.created_at).toLocaleDateString()}`);
283
+ } catch (err) {
284
+ console.log(`└─ Decided: ${row.created_at}`);
285
+ }
286
+ console.log('');
287
+ });
288
+ } else {
289
+ console.log('No epic decisions yet.');
290
+ console.log('');
291
+ console.log('💡 Tip: Make architectural decisions during epic planning:');
292
+ console.log(' jettypod work epic-planning <epic-id>');
293
+ console.log('');
294
+ }
295
+
296
+ resolve();
297
+ });
298
+ });
299
+ }
300
+
301
+ /**
302
+ * Prompt for epic ID and show its decisions
303
+ */
304
+ async function promptForEpicId() {
305
+ return new Promise((resolve) => {
306
+ const rl = readline.createInterface({
307
+ input: process.stdin,
308
+ output: process.stdout
309
+ });
310
+
311
+ rl.question('Which epic? (enter ID): ', async (answer) => {
312
+ rl.close();
313
+
314
+ const trimmed = answer.trim();
315
+
316
+ // Handle empty input
317
+ if (!trimmed) {
318
+ console.log('');
319
+ console.log('❌ No epic ID entered');
320
+ console.log('Please run the command again and enter an epic ID');
321
+ console.log('');
322
+ console.log('💡 Tip: See your epics with: jettypod backlog');
323
+ console.log('');
324
+ process.exit(1);
325
+ }
326
+
327
+ const epicId = parseInt(trimmed);
328
+
329
+ // Handle non-numeric input
330
+ if (isNaN(epicId)) {
331
+ console.log('');
332
+ console.log(`❌ Invalid epic ID: "${trimmed}"`);
333
+ console.log('Please enter a numeric epic ID');
334
+ console.log('');
335
+ console.log('💡 Tip: See your epics with: jettypod backlog');
336
+ console.log('');
337
+ process.exit(1);
338
+ }
339
+
340
+ // Handle negative/zero IDs
341
+ if (epicId <= 0) {
342
+ console.log('');
343
+ console.log(`❌ Invalid epic ID: ${epicId}`);
344
+ console.log('Epic IDs must be positive numbers');
345
+ console.log('');
346
+ process.exit(1);
347
+ }
348
+
349
+ try {
350
+ await showDecisionsForEpic(epicId);
351
+ resolve();
352
+ } catch (err) {
353
+ console.error('');
354
+ console.error('❌ An error occurred while displaying decisions');
355
+ console.error(`Error: ${err.message}`);
356
+ console.error('');
357
+ process.exit(1);
358
+ }
359
+ });
360
+ });
361
+ }
362
+
363
+ /**
364
+ * Show decisions for a specific epic
365
+ */
366
+ async function showDecisionsForEpic(epicId) {
367
+ return new Promise((resolve, reject) => {
368
+ // First get the epic title
369
+ db.get('SELECT title FROM work_items WHERE id = ? AND type = ?', [epicId, 'epic'], (err, epic) => {
370
+ if (err) {
371
+ console.error('❌ Database error while looking up epic');
372
+ console.error(`Error: ${err.message}`);
373
+ console.log('');
374
+ console.log('This could mean:');
375
+ console.log(' - Database is corrupted');
376
+ console.log(' - Table work_items does not exist');
377
+ console.log('');
378
+ return reject(err);
379
+ }
380
+
381
+ if (!epic) {
382
+ console.log('');
383
+ console.log(`❌ Epic #${epicId} not found`);
384
+ console.log('');
385
+ console.log('This could mean:');
386
+ console.log(` - Epic #${epicId} does not exist`);
387
+ console.log(` - Work item #${epicId} is not an epic`);
388
+ console.log('');
389
+ console.log('💡 Tip: See your epics with: jettypod backlog');
390
+ console.log('');
391
+ return resolve();
392
+ }
393
+
394
+ console.log('');
395
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
396
+ console.log(`📋 DECISIONS FOR EPIC #${epicId}`);
397
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
398
+ console.log('');
399
+ console.log(`Epic: ${epic.title}`);
400
+ console.log('');
401
+
402
+ db.all('SELECT * FROM discovery_decisions WHERE work_item_id = ? ORDER BY created_at DESC', [epicId], (err, rows) => {
403
+ if (err) {
404
+ console.error('❌ Database error while retrieving decisions');
405
+ console.error(`Error: ${err.message}`);
406
+ console.log('');
407
+ console.log('This could mean:');
408
+ console.log(' - Database is corrupted');
409
+ console.log(' - Table discovery_decisions does not exist');
410
+ console.log('');
411
+ return reject(err);
412
+ }
413
+
414
+ if (rows && rows.length > 0) {
415
+ rows.forEach(row => {
416
+ console.log(`${row.aspect}: ${row.decision}`);
417
+ console.log(`├─ Rationale: ${row.rationale || 'No rationale provided'}`);
418
+ try {
419
+ console.log(`└─ Decided: ${new Date(row.created_at).toLocaleDateString()}`);
420
+ } catch (err) {
421
+ console.log(`└─ Decided: ${row.created_at}`);
422
+ }
423
+ console.log('');
424
+ });
425
+ } else {
426
+ console.log('No decisions for this epic yet.');
427
+ console.log('');
428
+ console.log('💡 Tip: Make architectural decisions during epic planning:');
429
+ console.log(` jettypod work epic-planning ${epicId}`);
430
+ console.log('');
431
+ }
432
+
433
+ resolve();
434
+ });
435
+ });
436
+ });
437
+ }
438
+
439
+ /**
440
+ * View DECISIONS.md file
441
+ */
442
+ function viewDecisionsFile() {
443
+ const fs = require('fs');
444
+ const path = require('path');
445
+
446
+ const decisionsPath = path.join(process.cwd(), 'docs', 'DECISIONS.md');
447
+
448
+ try {
449
+ if (fs.existsSync(decisionsPath)) {
450
+ try {
451
+ const content = fs.readFileSync(decisionsPath, 'utf8');
452
+ console.log('');
453
+ console.log(content);
454
+ } catch (readErr) {
455
+ console.error('❌ Unable to read DECISIONS.md');
456
+ console.error(`Error: ${readErr.message}`);
457
+ console.log('');
458
+ console.log('This could mean:');
459
+ console.log(' - File is corrupted');
460
+ console.log(' - Insufficient permissions to read file');
461
+ console.log(' - File is locked by another process');
462
+ console.log('');
463
+ }
464
+ } else {
465
+ console.log('');
466
+ console.log('docs/DECISIONS.md not found.');
467
+ console.log('');
468
+ console.log('💡 This file will be created automatically when:');
469
+ console.log(' - You complete project discovery');
470
+ console.log(' - You make epic architectural decisions');
471
+ console.log('');
472
+ console.log('To make decisions:');
473
+ console.log(' jettypod work epic-planning <epic-id>');
474
+ console.log('');
475
+ }
476
+ } catch (err) {
477
+ console.error('❌ Error checking for DECISIONS.md');
478
+ console.error(`Error: ${err.message}`);
479
+ console.log('');
480
+ }
481
+ }
482
+
483
+ module.exports = {
484
+ showDecisionsMenu,
485
+ showAllDecisions,
486
+ showProjectDecisions,
487
+ showEpicDecisions,
488
+ showDecisionsForEpic,
489
+ viewDecisionsFile
490
+ };
@@ -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