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
@@ -1,307 +0,0 @@
1
- // Test results database layer
2
- // Ingests cucumber-results.json into SQLite with upsert semantics
3
- // so results survive git operations and file resets.
4
-
5
- import Database from 'better-sqlite3';
6
- import path from 'path';
7
- import fs from 'fs';
8
- import { execSync } from 'child_process';
9
-
10
- // eslint-disable-next-line @typescript-eslint/no-require-imports
11
- const { runMigrations } = require('./run-migrations');
12
-
13
- export interface TestResultRow {
14
- scenario_name: string;
15
- feature_file: string;
16
- status: 'passed' | 'failed' | 'pending';
17
- duration_ms: number;
18
- error_message: string | null;
19
- failed_step: string | null;
20
- run_at: string;
21
- }
22
-
23
- export interface TestRunRow {
24
- id: number;
25
- run_at: string;
26
- total_scenarios: number;
27
- passed: number;
28
- failed: number;
29
- pending: number;
30
- duration_ms: number;
31
- }
32
-
33
- // --- Database connection (reuses existing singleton pattern) ---
34
-
35
- let cachedDb: Database.Database | null = null;
36
- let cachedDbPath: string | null = null;
37
-
38
- function getProjectRoot(): string {
39
- if (process.env.JETTYPOD_PROJECT_PATH) {
40
- return process.env.JETTYPOD_PROJECT_PATH;
41
- }
42
- try {
43
- return execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
44
- } catch {
45
- throw new Error('Not in a git repository and JETTYPOD_PROJECT_PATH not set');
46
- }
47
- }
48
-
49
- function getDb(): Database.Database {
50
- const dbPath = path.join(getProjectRoot(), '.jettypod', 'work.db');
51
-
52
- if (cachedDb && cachedDbPath !== dbPath) {
53
- cachedDb.close();
54
- cachedDb = null;
55
- cachedDbPath = null;
56
- }
57
-
58
- if (!cachedDb) {
59
- cachedDb = new Database(dbPath);
60
- cachedDbPath = dbPath;
61
- cachedDb.pragma('journal_mode = WAL');
62
- cachedDb.pragma('foreign_keys = ON');
63
- try {
64
- runMigrations(cachedDb);
65
- } catch (err) {
66
- console.error('[test-results-db] Failed to run migrations on', dbPath, err);
67
- }
68
- }
69
- return cachedDb;
70
- }
71
-
72
- // --- Ingestion ---
73
-
74
- interface CucumberStep {
75
- keyword?: string;
76
- name?: string;
77
- result?: {
78
- status: string;
79
- duration?: number;
80
- error_message?: string;
81
- };
82
- }
83
-
84
- interface CucumberElement {
85
- type: string;
86
- name: string;
87
- steps?: CucumberStep[];
88
- }
89
-
90
- interface CucumberFeature {
91
- uri: string;
92
- elements?: CucumberElement[];
93
- }
94
-
95
- /**
96
- * Ingest cucumber-results.json into SQLite.
97
- * Uses upsert semantics: each scenario's latest result is kept,
98
- * unrelated scenarios are not affected.
99
- *
100
- * @param jsonPath - Path to cucumber-results.json (defaults to project root)
101
- * @returns Number of scenarios ingested, or 0 if file doesn't exist
102
- */
103
- export function ingestCucumberResults(jsonPath?: string): number {
104
- const projectRoot = getProjectRoot();
105
- const resultsPath = jsonPath || path.join(projectRoot, 'cucumber-results.json');
106
-
107
- if (!fs.existsSync(resultsPath)) {
108
- return 0;
109
- }
110
-
111
- let cucumberJson: CucumberFeature[];
112
- try {
113
- const raw = fs.readFileSync(resultsPath, 'utf-8');
114
- cucumberJson = JSON.parse(raw);
115
- } catch {
116
- return 0;
117
- }
118
-
119
- if (!Array.isArray(cucumberJson) || cucumberJson.length === 0) {
120
- return 0;
121
- }
122
-
123
- return ingestCucumberJson(cucumberJson);
124
- }
125
-
126
- /**
127
- * Ingest parsed cucumber JSON data into SQLite.
128
- * Separated from file reading for testability.
129
- */
130
- export function ingestCucumberJson(cucumberJson: CucumberFeature[]): number {
131
- const db = getDb();
132
-
133
- // Parse scenarios from cucumber JSON
134
- const scenarios: {
135
- feature_file: string;
136
- scenario_name: string;
137
- status: 'passed' | 'failed' | 'pending';
138
- duration_ms: number;
139
- error_message: string | null;
140
- failed_step: string | null;
141
- }[] = [];
142
-
143
- for (const feature of cucumberJson) {
144
- for (const element of feature.elements || []) {
145
- if (element.type !== 'scenario') continue;
146
-
147
- const steps = element.steps || [];
148
- const failed = steps.some(s => s.result?.status === 'failed');
149
- const pending = steps.some(s =>
150
- s.result?.status === 'pending' || s.result?.status === 'undefined'
151
- );
152
- const totalDuration = steps.reduce(
153
- (sum, s) => sum + (s.result?.duration || 0), 0
154
- );
155
-
156
- let errorMessage: string | null = null;
157
- let failedStepName: string | null = null;
158
- if (failed) {
159
- const failedStepObj = steps.find(s => s.result?.status === 'failed');
160
- errorMessage = failedStepObj?.result?.error_message || null;
161
- failedStepName = failedStepObj
162
- ? `${failedStepObj.keyword || ''}${failedStepObj.name || ''}`.trim()
163
- : null;
164
- }
165
-
166
- scenarios.push({
167
- feature_file: feature.uri,
168
- scenario_name: element.name,
169
- status: failed ? 'failed' : pending ? 'pending' : 'passed',
170
- duration_ms: Math.round(totalDuration / 1000000), // nanoseconds → ms
171
- error_message: errorMessage,
172
- failed_step: failedStepName,
173
- });
174
- }
175
- }
176
-
177
- if (scenarios.length === 0) {
178
- return 0;
179
- }
180
-
181
- // Write in a transaction for atomicity
182
- const insertRun = db.prepare(`
183
- INSERT INTO test_runs (run_at, total_scenarios, passed, failed, pending, duration_ms)
184
- VALUES (datetime('now'), ?, ?, ?, ?, ?)
185
- `);
186
-
187
- const upsertScenario = db.prepare(`
188
- INSERT INTO test_scenarios (feature_file, scenario_name)
189
- VALUES (?, ?)
190
- ON CONFLICT(feature_file, scenario_name) DO NOTHING
191
- `);
192
-
193
- const getScenarioId = db.prepare(`
194
- SELECT id FROM test_scenarios WHERE feature_file = ? AND scenario_name = ?
195
- `);
196
-
197
- const insertResult = db.prepare(`
198
- INSERT INTO test_results (test_run_id, scenario_id, status, duration_ms, error_message, failed_step, run_at)
199
- VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
200
- `);
201
-
202
- const transaction = db.transaction(() => {
203
- const passed = scenarios.filter(s => s.status === 'passed').length;
204
- const failed = scenarios.filter(s => s.status === 'failed').length;
205
- const pending = scenarios.filter(s => s.status === 'pending').length;
206
- const totalDuration = scenarios.reduce((sum, s) => sum + s.duration_ms, 0);
207
-
208
- const runResult = insertRun.run(scenarios.length, passed, failed, pending, totalDuration);
209
- const runId = runResult.lastInsertRowid as number;
210
-
211
- for (const scenario of scenarios) {
212
- upsertScenario.run(scenario.feature_file, scenario.scenario_name);
213
- const row = getScenarioId.get(scenario.feature_file, scenario.scenario_name) as { id: number };
214
- insertResult.run(
215
- runId,
216
- row.id,
217
- scenario.status,
218
- scenario.duration_ms,
219
- scenario.error_message,
220
- scenario.failed_step
221
- );
222
- }
223
-
224
- return scenarios.length;
225
- });
226
-
227
- return transaction();
228
- }
229
-
230
- // --- Query functions ---
231
-
232
- /**
233
- * Get the latest result for each scenario.
234
- * Returns one row per scenario with its most recent test result.
235
- */
236
- export function getLatestResults(): TestResultRow[] {
237
- const db = getDb();
238
- return db.prepare(`
239
- SELECT
240
- ts.scenario_name,
241
- ts.feature_file,
242
- tr.status,
243
- tr.duration_ms,
244
- tr.error_message,
245
- tr.failed_step,
246
- tr.run_at
247
- FROM test_results tr
248
- INNER JOIN test_scenarios ts ON tr.scenario_id = ts.id
249
- WHERE tr.id = (
250
- SELECT tr2.id FROM test_results tr2
251
- WHERE tr2.scenario_id = tr.scenario_id
252
- ORDER BY tr2.run_at DESC
253
- LIMIT 1
254
- )
255
- ORDER BY ts.feature_file, ts.scenario_name
256
- `).all() as TestResultRow[];
257
- }
258
-
259
- /**
260
- * Get the latest result for a specific scenario by name.
261
- */
262
- export function getLatestResultForScenario(scenarioName: string): TestResultRow | null {
263
- const db = getDb();
264
- const row = db.prepare(`
265
- SELECT
266
- ts.scenario_name,
267
- ts.feature_file,
268
- tr.status,
269
- tr.duration_ms,
270
- tr.error_message,
271
- tr.failed_step,
272
- tr.run_at
273
- FROM test_results tr
274
- INNER JOIN test_scenarios ts ON tr.scenario_id = ts.id
275
- WHERE ts.scenario_name = ?
276
- ORDER BY tr.run_at DESC
277
- LIMIT 1
278
- `).get(scenarioName) as TestResultRow | undefined;
279
- return row || null;
280
- }
281
-
282
- /**
283
- * Get the most recent test run metadata.
284
- */
285
- export function getLatestTestRun(): TestRunRow | null {
286
- const db = getDb();
287
- const row = db.prepare(`
288
- SELECT id, run_at, total_scenarios, passed, failed, pending, duration_ms
289
- FROM test_runs
290
- ORDER BY run_at DESC
291
- LIMIT 1
292
- `).get() as TestRunRow | undefined;
293
- return row || null;
294
- }
295
-
296
- /**
297
- * Get all test runs (for history display).
298
- */
299
- export function getTestRuns(limit: number = 50): TestRunRow[] {
300
- const db = getDb();
301
- return db.prepare(`
302
- SELECT id, run_at, total_scenarios, passed, failed, pending, duration_ms
303
- FROM test_runs
304
- ORDER BY run_at DESC
305
- LIMIT ?
306
- `).all(limit) as TestRunRow[];
307
- }
@@ -1,282 +0,0 @@
1
- // Test data layer for the test dashboard
2
- // Reads BDD test structure from feature files and results from SQLite
3
-
4
- import path from 'path';
5
- import fs from 'fs';
6
- import { execSync } from 'child_process';
7
- import { getLatestResults, getLatestTestRun } from './test-results-db';
8
-
9
- // Types for test data structure
10
- export interface TestScenario {
11
- id: string;
12
- title: string;
13
- status: 'pass' | 'fail' | 'pending';
14
- duration: string;
15
- lastRun: string | null;
16
- error?: string;
17
- failedStep?: string;
18
- steps: string[];
19
- undefinedSteps: string[];
20
- }
21
-
22
- export interface TestFeature {
23
- id: string;
24
- title: string;
25
- description: string;
26
- featureFile: string;
27
- scenarios: TestScenario[];
28
- }
29
-
30
- export interface TestEpic {
31
- id: string;
32
- title: string;
33
- healthBadge: { passing: number; failing: number };
34
- features: TestFeature[];
35
- }
36
-
37
- export interface TestSummary {
38
- total: number;
39
- passing: number;
40
- failing: number;
41
- pending: number;
42
- lastRun: string | null;
43
- }
44
-
45
- export interface TestDashboardData {
46
- summary: TestSummary;
47
- epics: TestEpic[];
48
- standaloneFeatures: TestFeature[];
49
- }
50
-
51
- function getProjectRoot(): string {
52
- if (process.env.JETTYPOD_PROJECT_PATH) {
53
- return process.env.JETTYPOD_PROJECT_PATH;
54
- }
55
- try {
56
- return execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
57
- } catch {
58
- throw new Error('Not in a git repository and JETTYPOD_PROJECT_PATH not set');
59
- }
60
- }
61
-
62
- // Parse a .feature file to extract scenarios with steps and description
63
- function parseFeatureFile(filePath: string): { title: string; description: string; scenarios: { name: string; steps: string[] }[] } {
64
- const content = fs.readFileSync(filePath, 'utf-8');
65
- const lines = content.split('\n');
66
-
67
- let title = '';
68
- const descriptionLines: string[] = [];
69
- const scenarios: { name: string; steps: string[] }[] = [];
70
- let inDescription = false;
71
- let currentScenario: { name: string; steps: string[] } | null = null;
72
-
73
- for (const line of lines) {
74
- const trimmed = line.trim();
75
- if (trimmed.startsWith('Feature:')) {
76
- title = trimmed.replace('Feature:', '').trim();
77
- inDescription = true;
78
- } else if (trimmed.startsWith('Scenario:')) {
79
- inDescription = false;
80
- if (currentScenario) scenarios.push(currentScenario);
81
- currentScenario = { name: trimmed.replace('Scenario:', '').trim(), steps: [] };
82
- } else if (currentScenario && (trimmed.startsWith('Given ') || trimmed.startsWith('When ') || trimmed.startsWith('Then ') || trimmed.startsWith('And ') || trimmed.startsWith('But '))) {
83
- currentScenario.steps.push(trimmed);
84
- } else if (inDescription && trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('@')) {
85
- descriptionLines.push(trimmed);
86
- }
87
- }
88
- if (currentScenario) scenarios.push(currentScenario);
89
-
90
- return { title, description: descriptionLines.join(' ').trim(), scenarios };
91
- }
92
-
93
- // Try to read Cucumber JSON results if available
94
- function getCucumberResults(projectRoot: string): Map<string, { status: string; duration: number; error?: string; failedStep?: string }> | null {
95
- const resultsPath = path.join(projectRoot, 'cucumber-results.json');
96
- if (!fs.existsSync(resultsPath)) {
97
- return null;
98
- }
99
-
100
- try {
101
- const results = JSON.parse(fs.readFileSync(resultsPath, 'utf-8'));
102
- const scenarioResults = new Map<string, { status: string; duration: number; error?: string; failedStep?: string }>();
103
-
104
- for (const feature of results) {
105
- for (const element of feature.elements || []) {
106
- if (element.type === 'scenario') {
107
- const steps = element.steps || [];
108
- const failed = steps.some((s: { result?: { status: string } }) => s.result?.status === 'failed');
109
- const pending = steps.some((s: { result?: { status: string } }) => s.result?.status === 'pending' || s.result?.status === 'undefined');
110
- const totalDuration = steps.reduce((sum: number, s: { result?: { duration: number } }) => sum + (s.result?.duration || 0), 0);
111
-
112
- let error: string | undefined;
113
- let failedStepName: string | undefined;
114
- if (failed) {
115
- const failedStepObj = steps.find((s: { result?: { status: string; error_message?: string }; keyword?: string; name?: string }) => s.result?.status === 'failed');
116
- error = failedStepObj?.result?.error_message;
117
- failedStepName = failedStepObj ? `${failedStepObj.keyword}${failedStepObj.name}`.trim() : undefined;
118
- }
119
-
120
- scenarioResults.set(element.name, {
121
- status: failed ? 'failed' : pending ? 'pending' : 'passed',
122
- duration: totalDuration / 1000000, // Convert nanoseconds to milliseconds
123
- error,
124
- failedStep: failedStepName,
125
- });
126
- }
127
- }
128
- }
129
-
130
- return scenarioResults;
131
- } catch {
132
- return null;
133
- }
134
- }
135
-
136
- // Get all feature files from the project
137
- function getFeatureFiles(projectRoot: string): string[] {
138
- const featuresDir = path.join(projectRoot, 'features');
139
- if (!fs.existsSync(featuresDir)) {
140
- return [];
141
- }
142
-
143
- const files: string[] = [];
144
- const entries = fs.readdirSync(featuresDir, { withFileTypes: true });
145
-
146
- for (const entry of entries) {
147
- if (entry.isFile() && entry.name.endsWith('.feature')) {
148
- files.push(path.join(featuresDir, entry.name));
149
- }
150
- }
151
-
152
- return files;
153
- }
154
-
155
- // Run cucumber dry-run to detect undefined steps (exported for separate async endpoint)
156
- export function getUndefinedSteps(projectRoot?: string): Record<string, string[]> {
157
- const root = projectRoot || getProjectRoot();
158
- const undefinedMap: Record<string, string[]> = {};
159
-
160
- try {
161
- const result = execSync(
162
- 'NODE_ENV=test npx cucumber-js --dry-run --format json 2>/dev/null',
163
- { cwd: root, encoding: 'utf-8', timeout: 30000 }
164
- );
165
-
166
- const parsed = JSON.parse(result);
167
- for (const feature of parsed) {
168
- for (const element of (feature.elements || [])) {
169
- if (element.type === 'scenario') {
170
- const steps = element.steps || [];
171
- const undefinedSteps: string[] = [];
172
- for (const step of steps) {
173
- if (step.result?.status === 'undefined') {
174
- undefinedSteps.push(`${step.keyword}${step.name}`.trim());
175
- }
176
- }
177
- if (undefinedSteps.length > 0) {
178
- undefinedMap[element.name] = undefinedSteps;
179
- }
180
- }
181
- }
182
- }
183
- } catch (err: unknown) {
184
- const error = err as { killed?: boolean; stderr?: string; message?: string };
185
- if (error.killed) {
186
- console.warn('[getUndefinedSteps] dry-run timed out');
187
- } else if (error.stderr && error.stderr.includes('ambiguous')) {
188
- console.warn('[getUndefinedSteps] ambiguous step definitions detected');
189
- } else {
190
- console.warn('[getUndefinedSteps] dry-run failed:', error.message || 'unknown error');
191
- }
192
- }
193
-
194
- return undefinedMap;
195
- }
196
-
197
- // Main function to get test dashboard data
198
- // Reads scenario structure from feature files, results from SQLite database
199
- export function getTestDashboardData(): TestDashboardData {
200
- const projectRoot = getProjectRoot();
201
- const featureFiles = getFeatureFiles(projectRoot);
202
-
203
- // Build a map of scenario name → latest DB result
204
- let dbResults: Map<string, { status: string; duration_ms: number; error_message: string | null; failed_step: string | null; run_at: string }>;
205
- let lastRun: string | null = null;
206
- try {
207
- const rows = getLatestResults();
208
- dbResults = new Map(rows.map(r => [r.scenario_name, r]));
209
- const latestRun = getLatestTestRun();
210
- lastRun = latestRun?.run_at || null;
211
- } catch {
212
- // DB not available (tables not created yet) — fall back to empty
213
- dbResults = new Map();
214
- }
215
-
216
- const features: TestFeature[] = [];
217
- let totalTests = 0;
218
- let passingTests = 0;
219
- let failingTests = 0;
220
- let pendingTests = 0;
221
-
222
- for (const filePath of featureFiles) {
223
- const { title, description, scenarios: parsedScenarios } = parseFeatureFile(filePath);
224
- const featureId = path.basename(filePath, '.feature');
225
-
226
- const scenarios: TestScenario[] = parsedScenarios.map((scenario, index) => {
227
- const result = dbResults.get(scenario.name);
228
- let status: 'pass' | 'fail' | 'pending' = 'pending';
229
- let duration = '0s';
230
- let error: string | undefined;
231
- let failedStep: string | undefined;
232
-
233
- if (result) {
234
- status = result.status === 'passed' ? 'pass' : result.status === 'failed' ? 'fail' : 'pending';
235
- duration = result.duration_ms < 1000 ? `${Math.round(result.duration_ms)}ms` : `${(result.duration_ms / 1000).toFixed(1)}s`;
236
- error = result.error_message || undefined;
237
- failedStep = result.failed_step || undefined;
238
- }
239
-
240
- totalTests++;
241
- if (status === 'pass') passingTests++;
242
- else if (status === 'fail') failingTests++;
243
- else pendingTests++;
244
-
245
- const undefinedSteps: string[] = [];
246
-
247
- return {
248
- id: `${featureId}-${index}`,
249
- title: scenario.name,
250
- status,
251
- duration,
252
- lastRun: result?.run_at || null,
253
- error,
254
- failedStep,
255
- steps: scenario.steps,
256
- undefinedSteps,
257
- };
258
- });
259
-
260
- features.push({
261
- id: featureId,
262
- title,
263
- description,
264
- featureFile: path.relative(projectRoot, filePath),
265
- scenarios,
266
- });
267
- }
268
-
269
- const summary: TestSummary = {
270
- total: totalTests,
271
- passing: passingTests,
272
- failing: failingTests,
273
- pending: pendingTests,
274
- lastRun,
275
- };
276
-
277
- return {
278
- summary,
279
- epics: [],
280
- standaloneFeatures: features,
281
- };
282
- }
@@ -1,50 +0,0 @@
1
- const path = require('path');
2
-
3
- /** @type {import('next').NextConfig} */
4
- const nextConfig = {
5
- // Externalize modules with native bindings or dynamic requires
6
- // - better-sqlite3: Used by dashboard's lib/db.ts (sync)
7
- // - sqlite3: Used by lib/database.js (async) via worktree-facade
8
- serverExternalPackages: ['better-sqlite3', 'sqlite3'],
9
-
10
- // Set monorepo root for correct file tracing
11
- outputFileTracingRoot: path.resolve(process.cwd(), '../..'),
12
-
13
- // Note: Turbopack disabled - it can't handle dynamic requires in lib/migrations/index.js
14
- // The webpack externals below properly externalize the jettypod lib chain
15
-
16
- webpack: (config, { isServer }) => {
17
- if (isServer) {
18
- config.externals = config.externals || [];
19
- config.externals.push(({ request }, callback) => {
20
- // Externalize worktree-facade with RUNTIME path resolution.
21
- // Uses 'var' external type so the path expression evaluates at runtime,
22
- // not at build time (which would bake in the build machine's absolute path).
23
- // Packaged app: JETTYPOD_RESOURCES_PATH/bin/lib/<module>
24
- // Dev mode: process.cwd()/../../lib/<module>
25
- if (request && request.includes('lib/worktree-facade')) {
26
- const moduleName = request.split('lib/')[1];
27
- return callback(null,
28
- `var require(process.env.JETTYPOD_RESOURCES_PATH ` +
29
- `? require('path').join(process.env.JETTYPOD_RESOURCES_PATH, 'bin', 'lib', '${moduleName}') ` +
30
- `: require('path').resolve(process.cwd(), '../../lib', '${moduleName}'))`
31
- );
32
- }
33
- // Externalize run-migrations.js with RUNTIME path resolution.
34
- // It uses dynamic require() to load migration files, which webpack
35
- // replaces with a dead stub if bundled.
36
- // In both packaged and dev: process.cwd() is the dashboard dir,
37
- // and run-migrations.js is at lib/run-migrations.js relative to it.
38
- if (request && request.includes('run-migrations')) {
39
- return callback(null,
40
- `var require(require('path').join(process.cwd(), 'lib', 'run-migrations'))`
41
- );
42
- }
43
- callback(undefined);
44
- });
45
- }
46
- return config;
47
- },
48
- };
49
-
50
- module.exports = nextConfig;
@@ -1,7 +0,0 @@
1
- const config = {
2
- plugins: {
3
- "@tailwindcss/postcss": {},
4
- },
5
- };
6
-
7
- export default config;
@@ -1 +0,0 @@
1
- <svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
@@ -1 +0,0 @@
1
- <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
@@ -1 +0,0 @@
1
- <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
@@ -1 +0,0 @@
1
- <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>