jettypod 4.4.118 → 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 (240) hide show
  1. package/.env +4 -3
  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 +63 -58
  8. package/apps/dashboard/app/demo/gates/page.tsx +43 -45
  9. package/apps/dashboard/app/design-system/page.tsx +868 -0
  10. package/apps/dashboard/app/globals.css +80 -4
  11. package/apps/dashboard/app/install-claude/page.tsx +4 -6
  12. package/apps/dashboard/app/login/page.tsx +72 -54
  13. package/apps/dashboard/app/page.tsx +101 -48
  14. package/apps/dashboard/app/settings/page.tsx +61 -13
  15. package/apps/dashboard/app/signup/page.tsx +242 -0
  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 +13 -16
  19. package/apps/dashboard/app/work/[id]/page.tsx +117 -118
  20. package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
  21. package/apps/dashboard/components/AppShell.tsx +92 -85
  22. package/apps/dashboard/components/CardMenu.tsx +45 -12
  23. package/apps/dashboard/components/ClaudePanel.tsx +771 -850
  24. package/apps/dashboard/components/ClaudePanelInput.tsx +43 -15
  25. package/apps/dashboard/components/ConnectClaudeScreen.tsx +17 -34
  26. package/apps/dashboard/components/CopyableId.tsx +3 -4
  27. package/apps/dashboard/components/DetailReviewActions.tsx +100 -0
  28. package/apps/dashboard/components/DragContext.tsx +134 -63
  29. package/apps/dashboard/components/DraggableCard.tsx +3 -5
  30. package/apps/dashboard/components/DropZone.tsx +6 -7
  31. package/apps/dashboard/components/EditableDetailDescription.tsx +7 -13
  32. package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
  33. package/apps/dashboard/components/EditableTitle.tsx +26 -7
  34. package/apps/dashboard/components/ElapsedTimer.tsx +66 -0
  35. package/apps/dashboard/components/EpicGroup.tsx +359 -0
  36. package/apps/dashboard/components/GateCard.tsx +79 -17
  37. package/apps/dashboard/components/GateChoiceCard.tsx +15 -18
  38. package/apps/dashboard/components/InstallClaudeScreen.tsx +15 -32
  39. package/apps/dashboard/components/JettyLoader.tsx +37 -0
  40. package/apps/dashboard/components/KanbanBoard.tsx +368 -958
  41. package/apps/dashboard/components/KanbanCard.tsx +740 -0
  42. package/apps/dashboard/components/LazyCard.tsx +62 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +11 -0
  44. package/apps/dashboard/components/MainNav.tsx +38 -73
  45. package/apps/dashboard/components/MessageBlock.tsx +468 -0
  46. package/apps/dashboard/components/ModeStartCard.tsx +15 -16
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +213 -0
  48. package/apps/dashboard/components/PlaceholderCard.tsx +3 -4
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +30 -30
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +72 -51
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +406 -388
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +373 -235
  53. package/apps/dashboard/components/ReviewFooter.tsx +139 -0
  54. package/apps/dashboard/components/SessionList.tsx +19 -19
  55. package/apps/dashboard/components/SubscribeContent.tsx +91 -47
  56. package/apps/dashboard/components/TestTree.tsx +16 -16
  57. package/apps/dashboard/components/TipCard.tsx +16 -17
  58. package/apps/dashboard/components/Toast.tsx +5 -6
  59. package/apps/dashboard/components/TypeIcon.tsx +55 -0
  60. package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +52 -65
  62. package/apps/dashboard/components/WelcomeScreen.tsx +19 -35
  63. package/apps/dashboard/components/WorkItemHeader.tsx +4 -5
  64. package/apps/dashboard/components/WorkItemTree.tsx +11 -32
  65. package/apps/dashboard/components/settings/AccountSection.tsx +55 -35
  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 +74 -152
  69. package/apps/dashboard/components/settings/GeneralSection.tsx +162 -56
  70. package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
  71. package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -5
  72. package/apps/dashboard/components/ui/Button.tsx +104 -0
  73. package/apps/dashboard/components/ui/Input.tsx +78 -0
  74. package/apps/dashboard/components.json +1 -1
  75. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +711 -418
  76. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -5
  77. package/apps/dashboard/contexts/UsageContext.tsx +87 -32
  78. package/apps/dashboard/dev.sh +35 -0
  79. package/apps/dashboard/eslint.config.mjs +9 -9
  80. package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
  81. package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
  82. package/apps/dashboard/hooks/useWebSocket.ts +138 -83
  83. package/apps/dashboard/index.html +73 -0
  84. package/apps/dashboard/lib/constants.ts +43 -0
  85. package/apps/dashboard/lib/data-bridge.ts +722 -0
  86. package/apps/dashboard/lib/db.ts +69 -1265
  87. package/apps/dashboard/lib/environment-config.ts +173 -0
  88. package/apps/dashboard/lib/environment-verification.ts +119 -0
  89. package/apps/dashboard/lib/kanban-utils.ts +270 -0
  90. package/apps/dashboard/lib/proof-run.ts +495 -0
  91. package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
  92. package/apps/dashboard/lib/run-migrations.js +27 -2
  93. package/apps/dashboard/lib/service-recovery.ts +326 -0
  94. package/apps/dashboard/lib/session-state-machine.ts +1 -0
  95. package/apps/dashboard/lib/session-state-utils.ts +0 -164
  96. package/apps/dashboard/lib/session-stream-manager.ts +308 -134
  97. package/apps/dashboard/lib/shadows.ts +7 -0
  98. package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
  99. package/apps/dashboard/lib/tauri-bridge.ts +102 -0
  100. package/apps/dashboard/lib/tauri.ts +106 -0
  101. package/apps/dashboard/lib/utils.ts +6 -0
  102. package/apps/dashboard/next-env.d.ts +1 -1
  103. package/apps/dashboard/package.json +21 -32
  104. package/apps/dashboard/public/bug-icon.png +0 -0
  105. package/apps/dashboard/public/buoy-icon.png +0 -0
  106. package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
  107. package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
  108. package/apps/dashboard/public/in-flight-seagull.png +0 -0
  109. package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
  110. package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
  111. package/apps/dashboard/public/jettypod_logo.png +0 -0
  112. package/apps/dashboard/public/pier-icon.png +0 -0
  113. package/apps/dashboard/public/star-icon.png +0 -0
  114. package/apps/dashboard/public/wrench-icon.png +0 -0
  115. package/apps/dashboard/scripts/tauri-build.js +228 -0
  116. package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
  117. package/apps/dashboard/scripts/ws-server.js +191 -0
  118. package/apps/dashboard/src/main.tsx +12 -0
  119. package/apps/dashboard/src/router.tsx +107 -0
  120. package/apps/dashboard/src/vite-env.d.ts +1 -0
  121. package/apps/dashboard/tsconfig.json +7 -12
  122. package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
  123. package/apps/dashboard/vite.config.ts +33 -0
  124. package/apps/update-server/src/index.ts +228 -80
  125. package/claude-hooks/global-guardrails.js +14 -13
  126. package/crates/jettypod-cli/Cargo.toml +19 -0
  127. package/crates/jettypod-cli/src/commands.rs +1249 -0
  128. package/crates/jettypod-cli/src/main.rs +595 -0
  129. package/crates/jettypod-core/Cargo.toml +26 -0
  130. package/crates/jettypod-core/build.rs +98 -0
  131. package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
  132. package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
  133. package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
  134. package/crates/jettypod-core/src/auth.rs +294 -0
  135. package/crates/jettypod-core/src/config.rs +397 -0
  136. package/crates/jettypod-core/src/db/mod.rs +507 -0
  137. package/crates/jettypod-core/src/db/recovery.rs +114 -0
  138. package/crates/jettypod-core/src/db/startup.rs +101 -0
  139. package/crates/jettypod-core/src/db/validate.rs +149 -0
  140. package/crates/jettypod-core/src/error.rs +76 -0
  141. package/crates/jettypod-core/src/git.rs +458 -0
  142. package/crates/jettypod-core/src/lib.rs +20 -0
  143. package/crates/jettypod-core/src/sessions.rs +625 -0
  144. package/crates/jettypod-core/src/skills.rs +556 -0
  145. package/crates/jettypod-core/src/work.rs +1086 -0
  146. package/crates/jettypod-core/src/worktree.rs +628 -0
  147. package/crates/jettypod-core/src/ws.rs +767 -0
  148. package/cucumber-test.cjs +6 -0
  149. package/cucumber.js +9 -3
  150. package/docs/COMMAND_REFERENCE.md +34 -0
  151. package/hooks/post-checkout +32 -75
  152. package/hooks/post-merge +111 -10
  153. package/jest.setup.js +1 -0
  154. package/jettypod.js +145 -116
  155. package/lib/bdd-preflight.js +96 -0
  156. package/lib/chore-taxonomy.js +33 -10
  157. package/lib/database.js +36 -16
  158. package/lib/db-watcher.js +1 -1
  159. package/lib/git-hooks/pre-commit +1 -1
  160. package/lib/jettypod-backup.js +27 -4
  161. package/lib/merge-lock.js +111 -253
  162. package/lib/migrations/027-plan-at-creation-column.js +3 -1
  163. package/lib/migrations/029-remove-autoincrement.js +307 -0
  164. package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
  165. package/lib/migrations/030-rejection-round-columns.js +54 -0
  166. package/lib/migrations/031-session-isolation-index.js +17 -0
  167. package/lib/migrations/index.js +47 -4
  168. package/lib/schema.js +10 -5
  169. package/lib/seed-onboarding.js +1 -1
  170. package/lib/update-command/index.js +9 -175
  171. package/lib/work-commands/index.js +144 -19
  172. package/lib/work-tracking/index.js +148 -27
  173. package/lib/worktree-diagnostics.js +16 -16
  174. package/lib/worktree-facade.js +1 -1
  175. package/lib/worktree-manager.js +8 -8
  176. package/lib/worktree-reconciler.js +5 -5
  177. package/package.json +9 -2
  178. package/scripts/ndjson-to-cucumber-json.js +152 -0
  179. package/scripts/postinstall.js +25 -0
  180. package/skills-templates/bug-mode/SKILL.md +79 -20
  181. package/skills-templates/bug-planning/SKILL.md +25 -29
  182. package/skills-templates/chore-mode/SKILL.md +171 -69
  183. package/skills-templates/chore-mode/verification.js +51 -10
  184. package/skills-templates/chore-planning/SKILL.md +47 -18
  185. package/skills-templates/design-system-selection/SKILL.md +273 -0
  186. package/skills-templates/epic-planning/SKILL.md +82 -48
  187. package/skills-templates/external-transition/SKILL.md +47 -47
  188. package/skills-templates/feature-planning/SKILL.md +173 -74
  189. package/skills-templates/production-mode/SKILL.md +69 -49
  190. package/skills-templates/request-routing/SKILL.md +4 -4
  191. package/skills-templates/simple-improvement/SKILL.md +74 -29
  192. package/skills-templates/speed-mode/SKILL.md +217 -141
  193. package/skills-templates/stable-mode/SKILL.md +148 -89
  194. package/apps/dashboard/README.md +0 -36
  195. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -386
  196. package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
  197. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -167
  198. package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
  199. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -378
  200. package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
  201. package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
  202. package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
  203. package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
  204. package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
  205. package/apps/dashboard/app/api/kanban/route.ts +0 -15
  206. package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
  207. package/apps/dashboard/app/api/settings/general/route.ts +0 -21
  208. package/apps/dashboard/app/api/tests/route.ts +0 -9
  209. package/apps/dashboard/app/api/tests/run/route.ts +0 -82
  210. package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
  211. package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
  212. package/apps/dashboard/app/api/usage/route.ts +0 -17
  213. package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
  214. package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
  215. package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
  216. package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -21
  217. package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
  218. package/apps/dashboard/app/layout.tsx +0 -43
  219. package/apps/dashboard/components/UpgradeBanner.tsx +0 -29
  220. package/apps/dashboard/electron/ipc-handlers.js +0 -1028
  221. package/apps/dashboard/electron/main.js +0 -2124
  222. package/apps/dashboard/electron/preload.js +0 -123
  223. package/apps/dashboard/electron/session-manager.js +0 -141
  224. package/apps/dashboard/electron-builder.config.js +0 -357
  225. package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
  226. package/apps/dashboard/lib/claude-process-manager.ts +0 -492
  227. package/apps/dashboard/lib/db-bridge.ts +0 -282
  228. package/apps/dashboard/lib/prototypes.ts +0 -202
  229. package/apps/dashboard/lib/test-results-db.ts +0 -307
  230. package/apps/dashboard/lib/tests.ts +0 -282
  231. package/apps/dashboard/next.config.js +0 -50
  232. package/apps/dashboard/postcss.config.mjs +0 -7
  233. package/apps/dashboard/public/file.svg +0 -1
  234. package/apps/dashboard/public/globe.svg +0 -1
  235. package/apps/dashboard/public/next.svg +0 -1
  236. package/apps/dashboard/public/vercel.svg +0 -1
  237. package/apps/dashboard/public/window.svg +0 -1
  238. package/apps/dashboard/scripts/download-node.js +0 -104
  239. package/apps/dashboard/scripts/upload-to-r2.js +0 -89
  240. package/docs/bdd-guidance.md +0 -390
@@ -0,0 +1,346 @@
1
+ /**
2
+ * Proof Scenario Runner — spawns cucumber-js and streams BDD results
3
+ * in real time to the proof dashboard.
4
+ *
5
+ * Uses Tauri IPC (spawn_process) when available, simulated behavior otherwise.
6
+ * Parses cucumber-js message format output for step-by-step progress.
7
+ */
8
+
9
+ import { invoke, listen, isTauri } from './tauri';
10
+
11
+ // ─── Types ───────────────────────────────────────────────────
12
+
13
+ export interface ScenarioStep {
14
+ keyword: string;
15
+ text: string;
16
+ status: 'passed' | 'running' | 'pending' | 'failed' | 'skipped';
17
+ duration?: number;
18
+ }
19
+
20
+ export interface Scenario {
21
+ title: string;
22
+ status: 'passed' | 'running' | 'pending' | 'failed';
23
+ steps: ScenarioStep[];
24
+ duration?: number;
25
+ }
26
+
27
+ export interface ScenarioRunnerStatus {
28
+ scenarios: Scenario[];
29
+ logs: string[];
30
+ state: 'idle' | 'running' | 'complete' | 'failed';
31
+ }
32
+
33
+ export type ScenarioRunnerCallback = (status: ScenarioRunnerStatus) => void;
34
+
35
+ // ─── Simulated scenario data ─────────────────────────────────
36
+
37
+ const SIMULATED_SCENARIOS: { title: string; steps: { keyword: string; text: string }[] }[] = [
38
+ {
39
+ title: 'User can reach proof dashboard from kanban card',
40
+ steps: [
41
+ { keyword: 'Given', text: 'I am viewing the kanban board' },
42
+ { keyword: 'And', text: 'there is a work item that is ready for review' },
43
+ { keyword: 'When', text: 'I click the QA button on that card' },
44
+ { keyword: 'Then', text: 'I am on the proof dashboard for that work item' },
45
+ { keyword: 'And', text: 'I see the service health strip' },
46
+ { keyword: 'And', text: 'I see the scenario list' },
47
+ ],
48
+ },
49
+ {
50
+ title: 'Services auto-launch when proof starts',
51
+ steps: [
52
+ { keyword: 'Given', text: 'I am on the proof dashboard for a work item' },
53
+ { keyword: 'When', text: 'I click the Run Proof button' },
54
+ { keyword: 'Then', text: 'required services start in dependency order' },
55
+ { keyword: 'And', text: 'each service shows a health indicator' },
56
+ { keyword: 'And', text: 'the proof waits until all services are healthy' },
57
+ ],
58
+ },
59
+ {
60
+ title: 'BDD scenarios stream results in real time',
61
+ steps: [
62
+ { keyword: 'Given', text: 'I am on the proof dashboard for a work item' },
63
+ { keyword: 'And', text: 'all required services are healthy' },
64
+ { keyword: 'When', text: 'the proof run begins' },
65
+ { keyword: 'Then', text: 'each scenario appears in the scenario list' },
66
+ { keyword: 'And', text: 'each step streams its status as it executes' },
67
+ { keyword: 'And', text: 'the progress bar updates with each completed step' },
68
+ ],
69
+ },
70
+ {
71
+ title: 'Inspector tabs show proof details',
72
+ steps: [
73
+ { keyword: 'Given', text: 'a proof run is in progress' },
74
+ { keyword: 'When', text: 'I switch between inspector tabs' },
75
+ { keyword: 'Then', text: 'the Flow tab shows chronological event traces' },
76
+ { keyword: 'And', text: 'the Scenarios tab shows step-by-step BDD progress' },
77
+ { keyword: 'And', text: 'the API tab shows HTTP and IPC calls with latency' },
78
+ { keyword: 'And', text: 'the DB tab shows table mutations with before and after values' },
79
+ { keyword: 'And', text: 'the Logs tab shows raw service output' },
80
+ ],
81
+ },
82
+ ];
83
+
84
+ // ─── Scenario Runner ─────────────────────────────────────────
85
+
86
+ export class ScenarioRunner {
87
+ private scenarios: Scenario[] = [];
88
+ private logs: string[] = [];
89
+ private state: ScenarioRunnerStatus['state'] = 'idle';
90
+ private onUpdate: ScenarioRunnerCallback;
91
+ private unlisteners: (() => void)[] = [];
92
+ private pid: number | null = null;
93
+ private outputBuffer = '';
94
+
95
+ constructor(onUpdate: ScenarioRunnerCallback) {
96
+ this.onUpdate = onUpdate;
97
+ }
98
+
99
+ getStatus(): ScenarioRunnerStatus {
100
+ return {
101
+ scenarios: this.scenarios.map(s => ({ ...s, steps: [...s.steps] })),
102
+ logs: [...this.logs],
103
+ state: this.state,
104
+ };
105
+ }
106
+
107
+ async run(featureFile: string | null): Promise<void> {
108
+ this.state = 'running';
109
+ this.scenarios = [];
110
+ this.logs = [];
111
+ this.notify();
112
+
113
+ if (featureFile && isTauri()) {
114
+ await this.runReal(featureFile);
115
+ } else if (featureFile && !isTauri()) {
116
+ // Outside Tauri with a real feature file — can't spawn processes,
117
+ // fall back to simulated run
118
+ this.addLog(`Feature file: ${featureFile} (simulated — no Tauri IPC)`);
119
+ await this.runSimulated();
120
+ } else {
121
+ // No feature file — simulated run
122
+ this.addLog('No feature file linked to this work item — running simulated scenarios');
123
+ await this.runSimulated();
124
+ }
125
+ }
126
+
127
+ async stop(): Promise<void> {
128
+ if (this.pid && isTauri()) {
129
+ try {
130
+ await invoke('kill_process', { pid: this.pid });
131
+ } catch { /* best effort */ }
132
+ }
133
+ this.state = 'idle';
134
+ this.pid = null;
135
+ this.notify();
136
+ }
137
+
138
+ destroy(): void {
139
+ for (const unlisten of this.unlisteners) {
140
+ unlisten();
141
+ }
142
+ this.unlisteners = [];
143
+ }
144
+
145
+ // ─── Real Tauri IPC implementation ──────────────────────
146
+
147
+ private async runReal(featureFile: string): Promise<void> {
148
+ // Listen for process output to parse cucumber messages
149
+ const outputUnlisten = await listen<{ pid: number; lines: string[] }>(
150
+ 'process-output-batch',
151
+ (event) => {
152
+ if (event.payload.pid !== this.pid) return;
153
+
154
+ for (const line of event.payload.lines) {
155
+ this.addLog(line);
156
+ this.outputBuffer += line;
157
+ this.parseCucumberOutput();
158
+ }
159
+ },
160
+ );
161
+ this.unlisteners.push(outputUnlisten);
162
+
163
+ const exitUnlisten = await listen<{ pid: number; code: number }>(
164
+ 'process-exit',
165
+ (event) => {
166
+ if (event.payload.pid !== this.pid) return;
167
+ this.state = event.payload.code === 0 ? 'complete' : 'failed';
168
+ this.pid = null;
169
+ this.notify();
170
+ },
171
+ );
172
+ this.unlisteners.push(exitUnlisten);
173
+
174
+ // Spawn cucumber-js with message format for streaming
175
+ this.pid = await invoke<number>('spawn_process', {
176
+ command: 'npx',
177
+ args: ['cucumber-js', featureFile, '--format', 'message'],
178
+ label: 'cucumber-bdd',
179
+ kind: 'DevServer',
180
+ cwd: null,
181
+ envVars: {},
182
+ });
183
+ }
184
+
185
+ private parseCucumberOutput(): void {
186
+ // Parse newline-delimited JSON messages from cucumber
187
+ const lines = this.outputBuffer.split('\n');
188
+ this.outputBuffer = lines.pop() || ''; // Keep incomplete last line
189
+
190
+ for (const line of lines) {
191
+ const trimmed = line.trim();
192
+ if (!trimmed || !trimmed.startsWith('{')) continue;
193
+
194
+ try {
195
+ const msg = JSON.parse(trimmed);
196
+ this.handleCucumberMessage(msg);
197
+ } catch {
198
+ // Not valid JSON, skip
199
+ }
200
+ }
201
+ }
202
+
203
+ private handleCucumberMessage(msg: Record<string, unknown>): void {
204
+ // Cucumber message format events
205
+ if (msg.pickle) {
206
+ const pickle = msg.pickle as { name: string; steps: { text: string; keyword?: string }[] };
207
+ this.scenarios.push({
208
+ title: pickle.name,
209
+ status: 'pending',
210
+ steps: pickle.steps.map(s => ({
211
+ keyword: (s.keyword || 'Step').trim(),
212
+ text: s.text,
213
+ status: 'pending' as const,
214
+ })),
215
+ });
216
+ this.notify();
217
+ }
218
+
219
+ if (msg.testCaseStarted) {
220
+ // Mark the next pending scenario as running
221
+ const pendingScenario = this.scenarios.find(s => s.status === 'pending');
222
+ if (pendingScenario) {
223
+ pendingScenario.status = 'running';
224
+ if (pendingScenario.steps.length > 0) {
225
+ pendingScenario.steps[0].status = 'running';
226
+ }
227
+ this.notify();
228
+ }
229
+ }
230
+
231
+ if (msg.testStepFinished) {
232
+ const result = msg.testStepFinished as { testStepResult: { status: string; duration?: { nanos: number } } };
233
+ const runningScenario = this.scenarios.find(s => s.status === 'running');
234
+ if (runningScenario) {
235
+ const runningStep = runningScenario.steps.find(s => s.status === 'running');
236
+ if (runningStep) {
237
+ const status = result.testStepResult.status.toLowerCase();
238
+ runningStep.status = (status === 'passed' ? 'passed' : 'failed') as ScenarioStep['status'];
239
+ if (result.testStepResult.duration) {
240
+ runningStep.duration = Math.round(result.testStepResult.duration.nanos / 1_000_000);
241
+ }
242
+
243
+ if (runningStep.status === 'failed') {
244
+ // Skip all remaining pending steps in this scenario
245
+ for (const step of runningScenario.steps) {
246
+ if (step.status === 'pending') {
247
+ step.status = 'skipped';
248
+ }
249
+ }
250
+ } else {
251
+ // Mark next pending step as running
252
+ const nextPending = runningScenario.steps.find(s => s.status === 'pending');
253
+ if (nextPending) {
254
+ nextPending.status = 'running';
255
+ }
256
+ }
257
+ }
258
+ this.notify();
259
+ }
260
+ }
261
+
262
+ if (msg.testCaseFinished) {
263
+ const runningScenario = this.scenarios.find(s => s.status === 'running');
264
+ if (runningScenario) {
265
+ const allPassed = runningScenario.steps.every(s => s.status === 'passed');
266
+ runningScenario.status = allPassed ? 'passed' : 'failed';
267
+
268
+ // Calculate total duration
269
+ const totalDuration = runningScenario.steps.reduce(
270
+ (acc, s) => acc + (s.duration || 0), 0
271
+ );
272
+ runningScenario.duration = totalDuration;
273
+ this.notify();
274
+ }
275
+ }
276
+ }
277
+
278
+ // ─── Simulated implementation (dev mode) ────────────────
279
+
280
+ private async runSimulated(): Promise<void> {
281
+ // Create all scenarios in pending state first
282
+ this.scenarios = SIMULATED_SCENARIOS.map(s => ({
283
+ title: s.title,
284
+ status: 'pending' as const,
285
+ steps: s.steps.map(step => ({
286
+ ...step,
287
+ status: 'pending' as const,
288
+ })),
289
+ }));
290
+ this.notify();
291
+
292
+ // Run through each scenario
293
+ for (const scenario of this.scenarios) {
294
+ scenario.status = 'running';
295
+ this.notify();
296
+
297
+ const scenarioStart = Date.now();
298
+
299
+ let scenarioFailed = false;
300
+
301
+ for (const step of scenario.steps) {
302
+ if (scenarioFailed) {
303
+ step.status = 'skipped';
304
+ this.addLog(` - ${step.keyword} ${step.text} (skipped)`);
305
+ this.notify();
306
+ continue;
307
+ }
308
+
309
+ step.status = 'running';
310
+ this.notify();
311
+
312
+ // Simulate step execution (100-400ms per step)
313
+ const stepStart = Date.now();
314
+ const delay = 100 + Math.random() * 300;
315
+ await new Promise(resolve => setTimeout(resolve, delay));
316
+
317
+ step.status = 'passed';
318
+ step.duration = Date.now() - stepStart;
319
+ this.addLog(` ✓ ${step.keyword} ${step.text} (${step.duration}ms)`);
320
+ this.notify();
321
+ }
322
+
323
+ scenario.status = scenarioFailed ? 'failed' : 'passed';
324
+ scenario.duration = Date.now() - scenarioStart;
325
+ this.addLog(`${scenarioFailed ? '✗' : '✓'} Scenario: ${scenario.title} (${scenario.duration}ms)`);
326
+ this.notify();
327
+ }
328
+
329
+ this.state = 'complete';
330
+ this.notify();
331
+ }
332
+
333
+ // ─── Helpers ────────────────────────────────────────────
334
+
335
+ private addLog(line: string): void {
336
+ this.logs.push(line);
337
+ // Keep last 500 lines
338
+ if (this.logs.length > 500) {
339
+ this.logs = this.logs.slice(-500);
340
+ }
341
+ }
342
+
343
+ private notify(): void {
344
+ this.onUpdate(this.getStatus());
345
+ }
346
+ }
@@ -99,14 +99,36 @@ function runMigrations(betterDb) {
99
99
  )
100
100
  `);
101
101
 
102
- // Get already-applied migrations
103
- const applied = betterDb.prepare('SELECT id FROM migrations').all().map(r => r.id);
102
+ // Create _meta table for schema versioning
103
+ betterDb.exec(`
104
+ CREATE TABLE IF NOT EXISTS _meta (
105
+ key TEXT PRIMARY KEY,
106
+ value TEXT
107
+ )
108
+ `);
104
109
 
105
110
  // Load migration files
106
111
  const files = fs.readdirSync(migrationsDir)
107
112
  .filter(f => f.endsWith('.js') && f !== 'index.js' && !f.endsWith('.test.js'))
108
113
  .sort();
109
114
 
115
+ const knownMigrationCount = files.length;
116
+
117
+ // Check schema version for forward-compatibility
118
+ const versionRow = betterDb.prepare('SELECT value FROM _meta WHERE key = ?').get('schema_version');
119
+ const currentVersion = versionRow ? parseInt(versionRow.value, 10) : 0;
120
+
121
+ if (currentVersion > knownMigrationCount) {
122
+ console.warn(
123
+ `⚠️ Database schema version (${currentVersion}) is newer than this version of JettyPod supports (${knownMigrationCount}).` +
124
+ '\n Please update JettyPod to avoid compatibility issues.'
125
+ );
126
+ return; // Don't run migrations — the DB is from a newer version
127
+ }
128
+
129
+ // Get already-applied migrations
130
+ const applied = betterDb.prepare('SELECT id FROM migrations').all().map(r => r.id);
131
+
110
132
  // Create shim for callback-based migration API
111
133
  const shim = createDbShim(betterDb);
112
134
 
@@ -140,6 +162,9 @@ function runMigrations(betterDb) {
140
162
  throw err;
141
163
  }
142
164
  }
165
+
166
+ // Update schema version to reflect current state
167
+ betterDb.prepare('INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)').run('schema_version', String(knownMigrationCount));
143
168
  }
144
169
 
145
170
  module.exports = { runMigrations };