jettypod 4.4.120 โ†’ 4.4.121

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 (208) hide show
  1. package/.env +2 -1
  2. package/Cargo.lock +6450 -0
  3. package/Cargo.toml +35 -0
  4. package/README.md +5 -1
  5. package/TAURI-MIGRATION-PLAN.md +840 -0
  6. package/apps/dashboard/app/connect-claude/page.tsx +5 -6
  7. package/apps/dashboard/app/decision/[id]/page.tsx +54 -49
  8. package/apps/dashboard/app/demo/gates/page.tsx +3 -5
  9. package/apps/dashboard/app/design-system/page.tsx +1 -1
  10. package/apps/dashboard/app/globals.css +74 -2
  11. package/apps/dashboard/app/install-claude/page.tsx +3 -5
  12. package/apps/dashboard/app/login/page.tsx +17 -20
  13. package/apps/dashboard/app/page.tsx +101 -48
  14. package/apps/dashboard/app/settings/page.tsx +60 -12
  15. package/apps/dashboard/app/signup/page.tsx +14 -17
  16. package/apps/dashboard/app/subscribe/page.tsx +0 -2
  17. package/apps/dashboard/app/tests/page.tsx +37 -4
  18. package/apps/dashboard/app/welcome/page.tsx +12 -15
  19. package/apps/dashboard/app/work/[id]/page.tsx +90 -75
  20. package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
  21. package/apps/dashboard/components/AppShell.tsx +70 -61
  22. package/apps/dashboard/components/CardMenu.tsx +0 -1
  23. package/apps/dashboard/components/ClaudePanel.tsx +541 -283
  24. package/apps/dashboard/components/ClaudePanelInput.tsx +23 -4
  25. package/apps/dashboard/components/ConnectClaudeScreen.tsx +1 -5
  26. package/apps/dashboard/components/CopyableId.tsx +1 -2
  27. package/apps/dashboard/components/DetailReviewActions.tsx +11 -20
  28. package/apps/dashboard/components/DragContext.tsx +132 -62
  29. package/apps/dashboard/components/DraggableCard.tsx +3 -5
  30. package/apps/dashboard/components/DropZone.tsx +5 -6
  31. package/apps/dashboard/components/EditableDetailDescription.tsx +6 -12
  32. package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
  33. package/apps/dashboard/components/EditableTitle.tsx +0 -1
  34. package/apps/dashboard/components/ElapsedTimer.tsx +15 -3
  35. package/apps/dashboard/components/EpicGroup.tsx +100 -70
  36. package/apps/dashboard/components/GateCard.tsx +0 -1
  37. package/apps/dashboard/components/GateChoiceCard.tsx +1 -2
  38. package/apps/dashboard/components/InstallClaudeScreen.tsx +1 -5
  39. package/apps/dashboard/components/JettyLoader.tsx +0 -1
  40. package/apps/dashboard/components/KanbanBoard.tsx +319 -173
  41. package/apps/dashboard/components/KanbanCard.tsx +341 -107
  42. package/apps/dashboard/components/LazyCard.tsx +62 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +0 -1
  44. package/apps/dashboard/components/MainNav.tsx +24 -25
  45. package/apps/dashboard/components/MessageBlock.tsx +93 -16
  46. package/apps/dashboard/components/ModeStartCard.tsx +0 -1
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +0 -1
  48. package/apps/dashboard/components/PlaceholderCard.tsx +0 -1
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +20 -20
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +47 -26
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +308 -223
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +303 -160
  53. package/apps/dashboard/components/ReviewFooter.tsx +12 -14
  54. package/apps/dashboard/components/SessionList.tsx +0 -1
  55. package/apps/dashboard/components/SubscribeContent.tsx +40 -11
  56. package/apps/dashboard/components/TestTree.tsx +1 -2
  57. package/apps/dashboard/components/TipCard.tsx +2 -4
  58. package/apps/dashboard/components/Toast.tsx +0 -1
  59. package/apps/dashboard/components/TypeIcon.tsx +7 -8
  60. package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +5 -17
  62. package/apps/dashboard/components/WelcomeScreen.tsx +2 -6
  63. package/apps/dashboard/components/WorkItemHeader.tsx +0 -1
  64. package/apps/dashboard/components/WorkItemTree.tsx +2 -4
  65. package/apps/dashboard/components/settings/AccountSection.tsx +27 -13
  66. package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
  67. package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
  68. package/apps/dashboard/components/settings/EnvVarsSection.tsx +20 -73
  69. package/apps/dashboard/components/settings/GeneralSection.tsx +137 -26
  70. package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
  71. package/apps/dashboard/components/settings/SettingsLayout.tsx +0 -1
  72. package/apps/dashboard/components/ui/Button.tsx +1 -1
  73. package/apps/dashboard/components/ui/Input.tsx +1 -1
  74. package/apps/dashboard/components.json +1 -1
  75. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +611 -358
  76. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +0 -1
  77. package/apps/dashboard/contexts/UsageContext.tsx +62 -31
  78. package/apps/dashboard/dev.sh +35 -0
  79. package/apps/dashboard/eslint.config.mjs +9 -9
  80. package/apps/dashboard/hooks/useWebSocket.ts +138 -83
  81. package/apps/dashboard/index.html +73 -0
  82. package/apps/dashboard/lib/data-bridge.ts +722 -0
  83. package/apps/dashboard/lib/db.ts +69 -1302
  84. package/apps/dashboard/lib/environment-config.ts +173 -0
  85. package/apps/dashboard/lib/environment-verification.ts +119 -0
  86. package/apps/dashboard/lib/kanban-utils.ts +226 -26
  87. package/apps/dashboard/lib/proof-run.ts +495 -0
  88. package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
  89. package/apps/dashboard/lib/service-recovery.ts +326 -0
  90. package/apps/dashboard/lib/session-state-machine.ts +1 -0
  91. package/apps/dashboard/lib/session-state-utils.ts +0 -164
  92. package/apps/dashboard/lib/session-stream-manager.ts +253 -122
  93. package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
  94. package/apps/dashboard/lib/tauri-bridge.ts +102 -0
  95. package/apps/dashboard/lib/tauri.ts +106 -0
  96. package/apps/dashboard/lib/utils.ts +3 -3
  97. package/apps/dashboard/next-env.d.ts +1 -1
  98. package/apps/dashboard/package.json +21 -33
  99. package/apps/dashboard/public/bug-icon.png +0 -0
  100. package/apps/dashboard/public/buoy-icon.png +0 -0
  101. package/apps/dashboard/public/in-flight-seagull.png +0 -0
  102. package/apps/dashboard/public/pier-icon.png +0 -0
  103. package/apps/dashboard/public/star-icon.png +0 -0
  104. package/apps/dashboard/public/wrench-icon.png +0 -0
  105. package/apps/dashboard/scripts/tauri-build.js +228 -0
  106. package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
  107. package/apps/dashboard/src/main.tsx +12 -0
  108. package/apps/dashboard/src/router.tsx +107 -0
  109. package/apps/dashboard/src/vite-env.d.ts +1 -0
  110. package/apps/dashboard/tsconfig.json +7 -12
  111. package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
  112. package/apps/dashboard/vite.config.ts +33 -0
  113. package/apps/update-server/src/index.ts +167 -30
  114. package/claude-hooks/global-guardrails.js +14 -13
  115. package/crates/jettypod-cli/Cargo.toml +19 -0
  116. package/crates/jettypod-cli/src/commands.rs +1249 -0
  117. package/crates/jettypod-cli/src/main.rs +595 -0
  118. package/crates/jettypod-core/Cargo.toml +26 -0
  119. package/crates/jettypod-core/build.rs +98 -0
  120. package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
  121. package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
  122. package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
  123. package/crates/jettypod-core/src/auth.rs +294 -0
  124. package/crates/jettypod-core/src/config.rs +397 -0
  125. package/crates/jettypod-core/src/db/mod.rs +507 -0
  126. package/crates/jettypod-core/src/db/recovery.rs +114 -0
  127. package/crates/jettypod-core/src/db/startup.rs +101 -0
  128. package/crates/jettypod-core/src/db/validate.rs +149 -0
  129. package/crates/jettypod-core/src/error.rs +76 -0
  130. package/crates/jettypod-core/src/git.rs +458 -0
  131. package/crates/jettypod-core/src/lib.rs +20 -0
  132. package/crates/jettypod-core/src/sessions.rs +625 -0
  133. package/crates/jettypod-core/src/skills.rs +556 -0
  134. package/crates/jettypod-core/src/work.rs +1086 -0
  135. package/crates/jettypod-core/src/worktree.rs +628 -0
  136. package/crates/jettypod-core/src/ws.rs +767 -0
  137. package/cucumber-test.cjs +6 -0
  138. package/jettypod.js +96 -4
  139. package/lib/bdd-preflight.js +96 -0
  140. package/lib/merge-lock.js +111 -253
  141. package/lib/migrations/030-rejection-round-columns.js +54 -0
  142. package/lib/migrations/031-session-isolation-index.js +17 -0
  143. package/lib/work-commands/index.js +58 -16
  144. package/lib/work-tracking/index.js +108 -8
  145. package/package.json +1 -1
  146. package/skills-templates/bug-mode/SKILL.md +43 -1
  147. package/skills-templates/chore-mode/SKILL.md +40 -1
  148. package/skills-templates/design-system-selection/SKILL.md +273 -0
  149. package/skills-templates/epic-planning/SKILL.md +14 -0
  150. package/skills-templates/feature-planning/SKILL.md +90 -1
  151. package/skills-templates/production-mode/SKILL.md +20 -0
  152. package/skills-templates/simple-improvement/SKILL.md +39 -2
  153. package/skills-templates/speed-mode/SKILL.md +10 -15
  154. package/skills-templates/stable-mode/SKILL.md +47 -0
  155. package/apps/dashboard/README.md +0 -36
  156. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -446
  157. package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
  158. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -280
  159. package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
  160. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -525
  161. package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
  162. package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
  163. package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
  164. package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
  165. package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
  166. package/apps/dashboard/app/api/kanban/route.ts +0 -15
  167. package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
  168. package/apps/dashboard/app/api/settings/general/route.ts +0 -21
  169. package/apps/dashboard/app/api/tests/route.ts +0 -9
  170. package/apps/dashboard/app/api/tests/run/route.ts +0 -82
  171. package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
  172. package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
  173. package/apps/dashboard/app/api/usage/route.ts +0 -17
  174. package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
  175. package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
  176. package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
  177. package/apps/dashboard/app/api/work/[id]/route.ts +0 -35
  178. package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -63
  179. package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
  180. package/apps/dashboard/app/layout.tsx +0 -55
  181. package/apps/dashboard/components/UpgradeBanner.tsx +0 -30
  182. package/apps/dashboard/electron/ipc-handlers.js +0 -1026
  183. package/apps/dashboard/electron/main.js +0 -2306
  184. package/apps/dashboard/electron/preload.js +0 -125
  185. package/apps/dashboard/electron/session-manager.js +0 -163
  186. package/apps/dashboard/electron-builder.config.js +0 -357
  187. package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
  188. package/apps/dashboard/lib/backlog-parser.ts +0 -50
  189. package/apps/dashboard/lib/claude-process-manager.ts +0 -529
  190. package/apps/dashboard/lib/db-bridge.ts +0 -283
  191. package/apps/dashboard/lib/prototypes.ts +0 -202
  192. package/apps/dashboard/lib/test-results-db.ts +0 -307
  193. package/apps/dashboard/lib/tests.ts +0 -282
  194. package/apps/dashboard/next.config.js +0 -66
  195. package/apps/dashboard/postcss.config.mjs +0 -7
  196. package/apps/dashboard/public/bug-icon.svg +0 -9
  197. package/apps/dashboard/public/buoy-icon.svg +0 -9
  198. package/apps/dashboard/public/file.svg +0 -1
  199. package/apps/dashboard/public/globe.svg +0 -1
  200. package/apps/dashboard/public/in-flight-seagull.svg +0 -9
  201. package/apps/dashboard/public/next.svg +0 -1
  202. package/apps/dashboard/public/pier-icon.svg +0 -14
  203. package/apps/dashboard/public/star-icon.svg +0 -9
  204. package/apps/dashboard/public/vercel.svg +0 -1
  205. package/apps/dashboard/public/window.svg +0 -1
  206. package/apps/dashboard/public/wrench-icon.svg +0 -9
  207. package/apps/dashboard/scripts/download-node.js +0 -104
  208. package/apps/dashboard/scripts/upload-to-r2.js +0 -89
@@ -0,0 +1,6 @@
1
+ module.exports = {
2
+ default: {
3
+ require: ['features/step_definitions/claude-cli-subprocess-env.steps.js'],
4
+ format: ['progress'],
5
+ }
6
+ };
package/jettypod.js CHANGED
@@ -565,6 +565,39 @@ Be direct and opinionated. Skip hedging, apologies, and excessive politeness. Le
565
565
  // Default for missing project name
566
566
  const projectName = config.name || 'unknown-project';
567
567
 
568
+ // Design system context injection
569
+ let designSystemSection = '';
570
+ const designSystemDir = config.designSystemDir;
571
+ if (designSystemDir && typeof designSystemDir === 'string' && fs.existsSync(designSystemDir)) {
572
+ const allowedExts = ['.css', '.scss', '.less', '.ts', '.tsx', '.js', '.jsx', '.json', '.md', '.txt'];
573
+ const files = fs.readdirSync(designSystemDir).filter(f => {
574
+ const ext = path.extname(f).toLowerCase();
575
+ return allowedExts.includes(ext);
576
+ }).sort();
577
+
578
+ if (files.length > 0) {
579
+ let fileContents = '';
580
+ for (const file of files) {
581
+ const filePath = path.join(designSystemDir, file);
582
+ try {
583
+ const stat = fs.statSync(filePath);
584
+ if (stat.isFile()) {
585
+ const content = fs.readFileSync(filePath, 'utf-8');
586
+ fileContents += `\n### ${file}\n\`\`\`\n${content}\n\`\`\`\n`;
587
+ }
588
+ } catch (e) {
589
+ // Skip unreadable files
590
+ }
591
+ }
592
+ if (fileContents) {
593
+ designSystemSection = `
594
+ <design_system>
595
+ ## Design System Reference
596
+ ${fileContents}</design_system>`;
597
+ }
598
+ }
599
+ }
600
+
568
601
  return `<claude_context project="${projectName}">
569
602
  ${currentWorkSection}
570
603
  <project_state>
@@ -573,7 +606,7 @@ ${projectState} - ${projectStateDescription}
573
606
  <tech_stack>
574
607
  ${techStack}
575
608
  </tech_stack>${modeTag}
576
- </claude_context>${jettypodEssentials}
609
+ </claude_context>${designSystemSection}${jettypodEssentials}
577
610
  ${communicationStyle}
578
611
  ${outputRules}`;
579
612
  },
@@ -671,8 +704,58 @@ async function generateClaude(options = {}) {
671
704
  }
672
705
  }
673
706
 
707
+ /**
708
+ * Sync Claude hook files from claude-hooks/ source templates to .jettypod/hooks/
709
+ * Uses content hashing โ€” only copies when source differs from installed.
710
+ * Called on every CLI invocation via ensureClaudeHooks().
711
+ */
712
+ function syncClaudeHookFiles() {
713
+ const sourceDir = path.join(__dirname, 'claude-hooks');
714
+ const destDir = path.join('.jettypod', 'hooks');
715
+
716
+ if (!fs.existsSync(sourceDir) || !fs.existsSync('.jettypod')) {
717
+ return; // No source hooks or no jettypod project
718
+ }
719
+
720
+ if (!fs.existsSync(destDir)) {
721
+ fs.mkdirSync(destDir, { recursive: true });
722
+ }
723
+
724
+ // Only sync the 3 hooks that are installed by init
725
+ const hooksToSync = [
726
+ 'global-guardrails.js',
727
+ 'protect-claude-md.js',
728
+ 'enforce-skill-activation.js'
729
+ ];
730
+
731
+ const crypto = require('crypto');
732
+ const hash = (content) => crypto.createHash('md5').update(content).digest('hex');
733
+
734
+ for (const hookFile of hooksToSync) {
735
+ const sourcePath = path.join(sourceDir, hookFile);
736
+ const destPath = path.join(destDir, hookFile);
737
+
738
+ if (!fs.existsSync(sourcePath)) continue;
739
+
740
+ const sourceContent = fs.readFileSync(sourcePath, 'utf-8');
741
+
742
+ // Skip if installed hook is already up to date
743
+ if (fs.existsSync(destPath)) {
744
+ const destContent = fs.readFileSync(destPath, 'utf-8');
745
+ if (hash(sourceContent) === hash(destContent)) continue;
746
+ }
747
+
748
+ fs.copyFileSync(sourcePath, destPath);
749
+ fs.chmodSync(destPath, 0o755);
750
+ }
751
+ }
752
+
674
753
  // Ensure Claude Code hooks are up to date (called on every jettypod launch)
675
754
  function ensureClaudeHooks() {
755
+ // Sync hook FILES from claude-hooks/ source templates to .jettypod/hooks/
756
+ // Uses content hashing โ€” only copies when source differs from installed
757
+ syncClaudeHookFiles();
758
+
676
759
  // Create .claude directory if needed
677
760
  if (!fs.existsSync('.claude')) {
678
761
  fs.mkdirSync('.claude', { recursive: true });
@@ -1233,6 +1316,15 @@ if (fs.existsSync('.git')) {
1233
1316
  // Runs independently of .git check since subdirectories don't have .git files
1234
1317
  ensureJettypodSymlink();
1235
1318
 
1319
+ // Sync Claude Code hook files and settings on every CLI invocation
1320
+ if (fs.existsSync('.jettypod')) {
1321
+ try {
1322
+ ensureClaudeHooks();
1323
+ } catch (err) {
1324
+ // Don't fail CLI if hook sync fails
1325
+ }
1326
+ }
1327
+
1236
1328
  if (!commandsWithoutDb.includes(command) && fs.existsSync('.jettypod')) {
1237
1329
  try {
1238
1330
  const { validateOnStartup } = require('./lib/database');
@@ -1250,7 +1342,8 @@ if (!commandsWithoutDb.includes(command) && fs.existsSync('.jettypod')) {
1250
1342
  console.log(`๐Ÿงน Cleaned ${cleaned} stale merge lock(s) from previous session`);
1251
1343
  }
1252
1344
  } catch (lockCleanupErr) {
1253
- // Non-fatal: don't block CLI startup if cleanup fails
1345
+ // Non-fatal: don't block CLI startup, but log so failures are visible
1346
+ console.warn(`Warning: Merge lock cleanup failed: ${lockCleanupErr.message}`);
1254
1347
  }
1255
1348
  } catch (err) {
1256
1349
  console.error('');
@@ -1384,12 +1477,11 @@ switch (command) {
1384
1477
  const workCommands = require('./lib/work-commands/index.js');
1385
1478
  try {
1386
1479
  // Parse merge flags and optional work item ID from args
1387
- const withTransition = args.includes('--with-transition');
1388
1480
  const releaseLock = args.includes('--release-lock');
1389
1481
  // Find numeric arg that isn't a flag
1390
1482
  const workItemId = args.find(a => /^\d+$/.test(a)) ? parseInt(args.find(a => /^\d+$/.test(a))) : null;
1391
1483
 
1392
- await workCommands.mergeWork({ withTransition, releaseLock, workItemId });
1484
+ await workCommands.mergeWork({ releaseLock, workItemId });
1393
1485
  } catch (err) {
1394
1486
  console.error(`Error: ${err.message}`);
1395
1487
  process.exit(1);
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function checkCucumberInstalled(projectRoot) {
7
+ const cucumberPath = path.join(projectRoot, 'node_modules', '@cucumber', 'cucumber');
8
+ const exists = fs.existsSync(cucumberPath);
9
+ return {
10
+ id: 'cucumber-installed',
11
+ label: '@cucumber/cucumber installed',
12
+ passed: exists,
13
+ error: exists ? null : '@cucumber/cucumber is not installed. Run: npm install --save-dev @cucumber/cucumber',
14
+ };
15
+ }
16
+
17
+ function checkTestBddScript(projectRoot) {
18
+ const pkgPath = path.join(projectRoot, 'package.json');
19
+ let hasScript = false;
20
+ if (fs.existsSync(pkgPath)) {
21
+ try {
22
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
23
+ hasScript = !!(pkg.scripts && pkg.scripts['test:bdd']);
24
+ } catch (_) {
25
+ // Corrupt or unreadable package.json โ€” treat as missing script
26
+ }
27
+ }
28
+ return {
29
+ id: 'test-bdd-script',
30
+ label: 'test:bdd script in package.json',
31
+ passed: hasScript,
32
+ error: hasScript ? null : 'Missing "test:bdd" script in package.json. Add: "test:bdd": "NODE_ENV=test cucumber-js"',
33
+ };
34
+ }
35
+
36
+ function checkCucumberConfig(projectRoot) {
37
+ const configPath = path.join(projectRoot, 'cucumber.js');
38
+ const exists = fs.existsSync(configPath);
39
+ return {
40
+ id: 'cucumber-config',
41
+ label: 'cucumber.js config file',
42
+ passed: exists,
43
+ error: exists ? null : 'Missing cucumber.js configuration file in project root',
44
+ };
45
+ }
46
+
47
+ function checkFeaturesDirectory(projectRoot) {
48
+ const featuresDir = path.join(projectRoot, 'features');
49
+ const stepDefsDir = path.join(projectRoot, 'features', 'step_definitions');
50
+ const exists = fs.existsSync(featuresDir) && fs.existsSync(stepDefsDir);
51
+ return {
52
+ id: 'features-directory',
53
+ label: 'features/ directory with step_definitions/',
54
+ passed: exists,
55
+ error: exists ? null : 'Missing features/ directory or features/step_definitions/ subdirectory',
56
+ };
57
+ }
58
+
59
+ function checkSupportDirectory(projectRoot) {
60
+ const supportDir = path.join(projectRoot, 'features', 'support');
61
+ const hooksFile = path.join(supportDir, 'hooks.js');
62
+ const exists = fs.existsSync(supportDir) && fs.existsSync(hooksFile);
63
+ return {
64
+ id: 'support-hooks',
65
+ label: 'features/support/hooks.js',
66
+ passed: exists,
67
+ error: exists ? null : 'Missing features/support/hooks.js file',
68
+ };
69
+ }
70
+
71
+ function buildSetupPrompt(projectRoot, failedChecks) {
72
+ const missing = failedChecks.map(c => `- ${c.label}: ${c.error}`).join('\n');
73
+ return `The following BDD test infrastructure is missing in this project:\n\n${missing}\n\nPlease set up the missing BDD test infrastructure so tests can run.`;
74
+ }
75
+
76
+ function checkBddPrerequisites(projectRoot) {
77
+ const checks = [
78
+ checkCucumberInstalled(projectRoot),
79
+ checkTestBddScript(projectRoot),
80
+ checkCucumberConfig(projectRoot),
81
+ checkFeaturesDirectory(projectRoot),
82
+ checkSupportDirectory(projectRoot),
83
+ ];
84
+
85
+ const ready = checks.every(c => c.passed);
86
+ const failed = checks.filter(c => !c.passed);
87
+
88
+ const result = { ready, checks };
89
+ if (!ready) {
90
+ result.setupPrompt = buildSetupPrompt(projectRoot, failed);
91
+ }
92
+
93
+ return result;
94
+ }
95
+
96
+ module.exports = { checkBddPrerequisites };