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.
- package/.env +4 -3
- package/Cargo.lock +6450 -0
- package/Cargo.toml +35 -0
- package/README.md +5 -1
- package/TAURI-MIGRATION-PLAN.md +840 -0
- package/apps/dashboard/app/connect-claude/page.tsx +5 -6
- package/apps/dashboard/app/decision/[id]/page.tsx +63 -58
- package/apps/dashboard/app/demo/gates/page.tsx +43 -45
- package/apps/dashboard/app/design-system/page.tsx +868 -0
- package/apps/dashboard/app/globals.css +80 -4
- package/apps/dashboard/app/install-claude/page.tsx +4 -6
- package/apps/dashboard/app/login/page.tsx +72 -54
- package/apps/dashboard/app/page.tsx +101 -48
- package/apps/dashboard/app/settings/page.tsx +61 -13
- package/apps/dashboard/app/signup/page.tsx +242 -0
- package/apps/dashboard/app/subscribe/page.tsx +0 -2
- package/apps/dashboard/app/tests/page.tsx +37 -4
- package/apps/dashboard/app/welcome/page.tsx +13 -16
- package/apps/dashboard/app/work/[id]/page.tsx +117 -118
- package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
- package/apps/dashboard/components/AppShell.tsx +92 -85
- package/apps/dashboard/components/CardMenu.tsx +45 -12
- package/apps/dashboard/components/ClaudePanel.tsx +771 -850
- package/apps/dashboard/components/ClaudePanelInput.tsx +43 -15
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +17 -34
- package/apps/dashboard/components/CopyableId.tsx +3 -4
- package/apps/dashboard/components/DetailReviewActions.tsx +100 -0
- package/apps/dashboard/components/DragContext.tsx +134 -63
- package/apps/dashboard/components/DraggableCard.tsx +3 -5
- package/apps/dashboard/components/DropZone.tsx +6 -7
- package/apps/dashboard/components/EditableDetailDescription.tsx +7 -13
- package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
- package/apps/dashboard/components/EditableTitle.tsx +26 -7
- package/apps/dashboard/components/ElapsedTimer.tsx +66 -0
- package/apps/dashboard/components/EpicGroup.tsx +359 -0
- package/apps/dashboard/components/GateCard.tsx +79 -17
- package/apps/dashboard/components/GateChoiceCard.tsx +15 -18
- package/apps/dashboard/components/InstallClaudeScreen.tsx +15 -32
- package/apps/dashboard/components/JettyLoader.tsx +37 -0
- package/apps/dashboard/components/KanbanBoard.tsx +368 -958
- package/apps/dashboard/components/KanbanCard.tsx +740 -0
- package/apps/dashboard/components/LazyCard.tsx +62 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +11 -0
- package/apps/dashboard/components/MainNav.tsx +38 -73
- package/apps/dashboard/components/MessageBlock.tsx +468 -0
- package/apps/dashboard/components/ModeStartCard.tsx +15 -16
- package/apps/dashboard/components/OnboardingWelcome.tsx +213 -0
- package/apps/dashboard/components/PlaceholderCard.tsx +3 -4
- package/apps/dashboard/components/ProjectSwitcher.tsx +30 -30
- package/apps/dashboard/components/PrototypeTimeline.tsx +72 -51
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +406 -388
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +373 -235
- package/apps/dashboard/components/ReviewFooter.tsx +139 -0
- package/apps/dashboard/components/SessionList.tsx +19 -19
- package/apps/dashboard/components/SubscribeContent.tsx +91 -47
- package/apps/dashboard/components/TestTree.tsx +16 -16
- package/apps/dashboard/components/TipCard.tsx +16 -17
- package/apps/dashboard/components/Toast.tsx +5 -6
- package/apps/dashboard/components/TypeIcon.tsx +55 -0
- package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +52 -65
- package/apps/dashboard/components/WelcomeScreen.tsx +19 -35
- package/apps/dashboard/components/WorkItemHeader.tsx +4 -5
- package/apps/dashboard/components/WorkItemTree.tsx +11 -32
- package/apps/dashboard/components/settings/AccountSection.tsx +55 -35
- package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
- package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
- package/apps/dashboard/components/settings/EnvVarsSection.tsx +74 -152
- package/apps/dashboard/components/settings/GeneralSection.tsx +162 -56
- package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
- package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -5
- package/apps/dashboard/components/ui/Button.tsx +104 -0
- package/apps/dashboard/components/ui/Input.tsx +78 -0
- package/apps/dashboard/components.json +1 -1
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +711 -418
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -5
- package/apps/dashboard/contexts/UsageContext.tsx +87 -32
- package/apps/dashboard/dev.sh +35 -0
- package/apps/dashboard/eslint.config.mjs +9 -9
- package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
- package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
- package/apps/dashboard/hooks/useWebSocket.ts +138 -83
- package/apps/dashboard/index.html +73 -0
- package/apps/dashboard/lib/constants.ts +43 -0
- package/apps/dashboard/lib/data-bridge.ts +722 -0
- package/apps/dashboard/lib/db.ts +69 -1265
- package/apps/dashboard/lib/environment-config.ts +173 -0
- package/apps/dashboard/lib/environment-verification.ts +119 -0
- package/apps/dashboard/lib/kanban-utils.ts +270 -0
- package/apps/dashboard/lib/proof-run.ts +495 -0
- package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
- package/apps/dashboard/lib/run-migrations.js +27 -2
- package/apps/dashboard/lib/service-recovery.ts +326 -0
- package/apps/dashboard/lib/session-state-machine.ts +1 -0
- package/apps/dashboard/lib/session-state-utils.ts +0 -164
- package/apps/dashboard/lib/session-stream-manager.ts +308 -134
- package/apps/dashboard/lib/shadows.ts +7 -0
- package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
- package/apps/dashboard/lib/tauri-bridge.ts +102 -0
- package/apps/dashboard/lib/tauri.ts +106 -0
- package/apps/dashboard/lib/utils.ts +6 -0
- package/apps/dashboard/next-env.d.ts +1 -1
- package/apps/dashboard/package.json +21 -32
- package/apps/dashboard/public/bug-icon.png +0 -0
- package/apps/dashboard/public/buoy-icon.png +0 -0
- package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
- package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
- package/apps/dashboard/public/in-flight-seagull.png +0 -0
- package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
- package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
- package/apps/dashboard/public/jettypod_logo.png +0 -0
- package/apps/dashboard/public/pier-icon.png +0 -0
- package/apps/dashboard/public/star-icon.png +0 -0
- package/apps/dashboard/public/wrench-icon.png +0 -0
- package/apps/dashboard/scripts/tauri-build.js +228 -0
- package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
- package/apps/dashboard/scripts/ws-server.js +191 -0
- package/apps/dashboard/src/main.tsx +12 -0
- package/apps/dashboard/src/router.tsx +107 -0
- package/apps/dashboard/src/vite-env.d.ts +1 -0
- package/apps/dashboard/tsconfig.json +7 -12
- package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
- package/apps/dashboard/vite.config.ts +33 -0
- package/apps/update-server/src/index.ts +228 -80
- package/claude-hooks/global-guardrails.js +14 -13
- package/crates/jettypod-cli/Cargo.toml +19 -0
- package/crates/jettypod-cli/src/commands.rs +1249 -0
- package/crates/jettypod-cli/src/main.rs +595 -0
- package/crates/jettypod-core/Cargo.toml +26 -0
- package/crates/jettypod-core/build.rs +98 -0
- package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
- package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
- package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
- package/crates/jettypod-core/src/auth.rs +294 -0
- package/crates/jettypod-core/src/config.rs +397 -0
- package/crates/jettypod-core/src/db/mod.rs +507 -0
- package/crates/jettypod-core/src/db/recovery.rs +114 -0
- package/crates/jettypod-core/src/db/startup.rs +101 -0
- package/crates/jettypod-core/src/db/validate.rs +149 -0
- package/crates/jettypod-core/src/error.rs +76 -0
- package/crates/jettypod-core/src/git.rs +458 -0
- package/crates/jettypod-core/src/lib.rs +20 -0
- package/crates/jettypod-core/src/sessions.rs +625 -0
- package/crates/jettypod-core/src/skills.rs +556 -0
- package/crates/jettypod-core/src/work.rs +1086 -0
- package/crates/jettypod-core/src/worktree.rs +628 -0
- package/crates/jettypod-core/src/ws.rs +767 -0
- package/cucumber-test.cjs +6 -0
- package/cucumber.js +9 -3
- package/docs/COMMAND_REFERENCE.md +34 -0
- package/hooks/post-checkout +32 -75
- package/hooks/post-merge +111 -10
- package/jest.setup.js +1 -0
- package/jettypod.js +145 -116
- package/lib/bdd-preflight.js +96 -0
- package/lib/chore-taxonomy.js +33 -10
- package/lib/database.js +36 -16
- package/lib/db-watcher.js +1 -1
- package/lib/git-hooks/pre-commit +1 -1
- package/lib/jettypod-backup.js +27 -4
- package/lib/merge-lock.js +111 -253
- package/lib/migrations/027-plan-at-creation-column.js +3 -1
- package/lib/migrations/029-remove-autoincrement.js +307 -0
- package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
- package/lib/migrations/030-rejection-round-columns.js +54 -0
- package/lib/migrations/031-session-isolation-index.js +17 -0
- package/lib/migrations/index.js +47 -4
- package/lib/schema.js +10 -5
- package/lib/seed-onboarding.js +1 -1
- package/lib/update-command/index.js +9 -175
- package/lib/work-commands/index.js +144 -19
- package/lib/work-tracking/index.js +148 -27
- package/lib/worktree-diagnostics.js +16 -16
- package/lib/worktree-facade.js +1 -1
- package/lib/worktree-manager.js +8 -8
- package/lib/worktree-reconciler.js +5 -5
- package/package.json +9 -2
- package/scripts/ndjson-to-cucumber-json.js +152 -0
- package/scripts/postinstall.js +25 -0
- package/skills-templates/bug-mode/SKILL.md +79 -20
- package/skills-templates/bug-planning/SKILL.md +25 -29
- package/skills-templates/chore-mode/SKILL.md +171 -69
- package/skills-templates/chore-mode/verification.js +51 -10
- package/skills-templates/chore-planning/SKILL.md +47 -18
- package/skills-templates/design-system-selection/SKILL.md +273 -0
- package/skills-templates/epic-planning/SKILL.md +82 -48
- package/skills-templates/external-transition/SKILL.md +47 -47
- package/skills-templates/feature-planning/SKILL.md +173 -74
- package/skills-templates/production-mode/SKILL.md +69 -49
- package/skills-templates/request-routing/SKILL.md +4 -4
- package/skills-templates/simple-improvement/SKILL.md +74 -29
- package/skills-templates/speed-mode/SKILL.md +217 -141
- package/skills-templates/stable-mode/SKILL.md +148 -89
- package/apps/dashboard/README.md +0 -36
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -386
- package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -167
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -378
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
- package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
- package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
- package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
- package/apps/dashboard/app/api/kanban/route.ts +0 -15
- package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
- package/apps/dashboard/app/api/settings/general/route.ts +0 -21
- package/apps/dashboard/app/api/tests/route.ts +0 -9
- package/apps/dashboard/app/api/tests/run/route.ts +0 -82
- package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
- package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
- package/apps/dashboard/app/api/usage/route.ts +0 -17
- package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
- package/apps/dashboard/app/layout.tsx +0 -43
- package/apps/dashboard/components/UpgradeBanner.tsx +0 -29
- package/apps/dashboard/electron/ipc-handlers.js +0 -1028
- package/apps/dashboard/electron/main.js +0 -2124
- package/apps/dashboard/electron/preload.js +0 -123
- package/apps/dashboard/electron/session-manager.js +0 -141
- package/apps/dashboard/electron-builder.config.js +0 -357
- package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
- package/apps/dashboard/lib/claude-process-manager.ts +0 -492
- package/apps/dashboard/lib/db-bridge.ts +0 -282
- package/apps/dashboard/lib/prototypes.ts +0 -202
- package/apps/dashboard/lib/test-results-db.ts +0 -307
- package/apps/dashboard/lib/tests.ts +0 -282
- package/apps/dashboard/next.config.js +0 -50
- package/apps/dashboard/postcss.config.mjs +0 -7
- package/apps/dashboard/public/file.svg +0 -1
- package/apps/dashboard/public/globe.svg +0 -1
- package/apps/dashboard/public/next.svg +0 -1
- package/apps/dashboard/public/vercel.svg +0 -1
- package/apps/dashboard/public/window.svg +0 -1
- package/apps/dashboard/scripts/download-node.js +0 -104
- package/apps/dashboard/scripts/upload-to-r2.js +0 -89
- package/docs/bdd-guidance.md +0 -390
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment config loader — reads user-defined service configuration
|
|
3
|
+
* from .jettypod/config.json via Tauri IPC.
|
|
4
|
+
*
|
|
5
|
+
* The config.json "environment" key is captured by the Rust config reader's
|
|
6
|
+
* `extra` field (#[serde(flatten)]), so it's returned by db_get_config.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { invoke, isTauri } from './tauri';
|
|
10
|
+
import type { ServiceConfig } from './proof-run';
|
|
11
|
+
|
|
12
|
+
// ─── Config Types ────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export type ServiceCategory = 'server' | 'infrastructure' | 'managed' | 'test-runner' | 'build-tool';
|
|
15
|
+
|
|
16
|
+
const VALID_CATEGORIES = new Set<ServiceCategory>(['server', 'infrastructure', 'managed', 'test-runner', 'build-tool']);
|
|
17
|
+
|
|
18
|
+
/** Coerce unknown category to valid ServiceCategory, defaulting to 'server' */
|
|
19
|
+
export function validateCategory(value: unknown): ServiceCategory {
|
|
20
|
+
if (typeof value === 'string' && VALID_CATEGORIES.has(value as ServiceCategory)) {
|
|
21
|
+
return value as ServiceCategory;
|
|
22
|
+
}
|
|
23
|
+
return 'server';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Validate faIcon string — must start with 'fa-' */
|
|
27
|
+
export function validateFaIcon(value: unknown): string | undefined {
|
|
28
|
+
if (typeof value === 'string' && value.startsWith('fa-')) return value;
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ServiceDefinition {
|
|
33
|
+
/** Display name shown in Environment Bar */
|
|
34
|
+
name: string;
|
|
35
|
+
/** Shell command to start this service */
|
|
36
|
+
command: string;
|
|
37
|
+
/** Port to health-check against */
|
|
38
|
+
port: number;
|
|
39
|
+
/** Health check strategy: http, ws, or tcp */
|
|
40
|
+
healthCheck: 'http' | 'ws' | 'tcp';
|
|
41
|
+
/** Optional: stdout pattern that means "ready" (faster than polling port) */
|
|
42
|
+
readyPattern?: string;
|
|
43
|
+
/** Optional services don't block "Ready for QA" */
|
|
44
|
+
optional?: boolean;
|
|
45
|
+
/** Launch order (lower = first). Same order = parallel. */
|
|
46
|
+
order: number;
|
|
47
|
+
/** Category for two-zone layout grouping */
|
|
48
|
+
category: ServiceCategory;
|
|
49
|
+
/** FontAwesome icon class (e.g. 'fa-database', 'fa-server') */
|
|
50
|
+
faIcon?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Non-launchable stack items (managed services, test runners, build tools) */
|
|
54
|
+
export interface ContextItem {
|
|
55
|
+
/** Display name */
|
|
56
|
+
name: string;
|
|
57
|
+
/** Category determines zone placement and UX controls */
|
|
58
|
+
category: Extract<ServiceCategory, 'managed' | 'test-runner' | 'build-tool'>;
|
|
59
|
+
/** FontAwesome icon class */
|
|
60
|
+
faIcon?: string;
|
|
61
|
+
/** Shell command (e.g. test runner run command, connection test) */
|
|
62
|
+
command?: string;
|
|
63
|
+
/** Connection string or URL for managed services */
|
|
64
|
+
connectionUrl?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface EnvironmentConfig {
|
|
68
|
+
services: ServiceDefinition[];
|
|
69
|
+
/** Non-launchable stack items displayed in the context zone */
|
|
70
|
+
contextItems: ContextItem[];
|
|
71
|
+
/** 'required' = all non-optional services must be up. 'all' = everything. */
|
|
72
|
+
readyWhen: 'required' | 'all';
|
|
73
|
+
/** Kill spawned processes when QA page unmounts. Default true. */
|
|
74
|
+
teardownOnClose: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── Loader ──────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Load environment config from .jettypod/config.json via Tauri IPC.
|
|
81
|
+
* Returns null if no environment section is configured or not running in Tauri.
|
|
82
|
+
*/
|
|
83
|
+
export async function loadEnvironmentConfig(): Promise<EnvironmentConfig | null> {
|
|
84
|
+
if (!isTauri()) return null;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const fullConfig = await invoke<Record<string, unknown>>('db_get_config');
|
|
88
|
+
const env = fullConfig?.environment as Record<string, unknown> | undefined;
|
|
89
|
+
if (!env || !Array.isArray(env.services) || env.services.length === 0) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const contextItems = Array.isArray(env.contextItems)
|
|
94
|
+
? (env.contextItems as ContextItem[]).map(item => ({
|
|
95
|
+
name: item.name,
|
|
96
|
+
category: item.category,
|
|
97
|
+
faIcon: item.faIcon,
|
|
98
|
+
command: item.command,
|
|
99
|
+
connectionUrl: item.connectionUrl,
|
|
100
|
+
}))
|
|
101
|
+
: [];
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
services: (env.services as ServiceDefinition[]).map((svc, i) => ({
|
|
105
|
+
name: svc.name,
|
|
106
|
+
command: svc.command,
|
|
107
|
+
port: svc.port,
|
|
108
|
+
healthCheck: svc.healthCheck || 'http',
|
|
109
|
+
readyPattern: svc.readyPattern,
|
|
110
|
+
optional: svc.optional ?? false,
|
|
111
|
+
order: svc.order ?? i + 1,
|
|
112
|
+
category: validateCategory(svc.category),
|
|
113
|
+
faIcon: validateFaIcon(svc.faIcon),
|
|
114
|
+
})),
|
|
115
|
+
contextItems,
|
|
116
|
+
readyWhen: (env.readyWhen as EnvironmentConfig['readyWhen']) || 'required',
|
|
117
|
+
teardownOnClose: env.teardownOnClose !== false,
|
|
118
|
+
};
|
|
119
|
+
} catch {
|
|
120
|
+
// IPC failure (db not initialized, config read error) — fall back to passive detection
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ─── Health Check Helpers ────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
function checkHttp(port: number): Promise<boolean> {
|
|
128
|
+
return new Promise((resolve) => {
|
|
129
|
+
const controller = new AbortController();
|
|
130
|
+
const timeout = setTimeout(() => controller.abort(), 2000);
|
|
131
|
+
fetch(`http://localhost:${port}/`, { signal: controller.signal, mode: 'no-cors' })
|
|
132
|
+
.then(() => { clearTimeout(timeout); resolve(true); })
|
|
133
|
+
.catch(() => { clearTimeout(timeout); resolve(false); });
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function checkWs(port: number): Promise<boolean> {
|
|
138
|
+
return new Promise((resolve) => {
|
|
139
|
+
const timeout = setTimeout(() => { ws.close(); resolve(false); }, 2000);
|
|
140
|
+
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
141
|
+
ws.onopen = () => { clearTimeout(timeout); ws.close(); resolve(true); };
|
|
142
|
+
ws.onerror = () => { clearTimeout(timeout); resolve(false); };
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ─── Converter ───────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Convert EnvironmentConfig into ServiceConfig[] that ProofRunManager accepts.
|
|
150
|
+
* Maps the string-based healthCheck type to actual check functions.
|
|
151
|
+
*/
|
|
152
|
+
/** Categories that are part of the QA launch sequence */
|
|
153
|
+
const LAUNCHABLE_CATEGORIES: ServiceCategory[] = ['server', 'infrastructure'];
|
|
154
|
+
|
|
155
|
+
export function toServiceConfigs(config: EnvironmentConfig): ServiceConfig[] {
|
|
156
|
+
const checkers: Record<string, (port: number) => Promise<boolean>> = {
|
|
157
|
+
http: checkHttp,
|
|
158
|
+
ws: checkWs,
|
|
159
|
+
tcp: checkHttp, // TCP check uses same HTTP probe (any response = port open)
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return config.services
|
|
163
|
+
.filter(svc => LAUNCHABLE_CATEGORIES.includes(svc.category))
|
|
164
|
+
.map(svc => ({
|
|
165
|
+
name: svc.name,
|
|
166
|
+
port: svc.port,
|
|
167
|
+
order: svc.order,
|
|
168
|
+
healthCheck: () => {
|
|
169
|
+
const checker = checkers[svc.healthCheck] || checkHttp;
|
|
170
|
+
return checker(svc.port);
|
|
171
|
+
},
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Verification — fire-and-teardown wrapper around ProofRunManager.
|
|
3
|
+
*
|
|
4
|
+
* Used by ProjectStackSection after saving a new environment config to verify
|
|
5
|
+
* that all services actually start and pass health checks before transitioning
|
|
6
|
+
* to the "configured" state. Services are torn down immediately after verification.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ProofRunManager } from './proof-run';
|
|
10
|
+
import type { ProofRunStatus, ServiceStatus } from './proof-run';
|
|
11
|
+
import type { EnvironmentConfig } from './environment-config';
|
|
12
|
+
|
|
13
|
+
// ─── Types ───────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export interface VerificationServiceResult {
|
|
16
|
+
name: string;
|
|
17
|
+
port: number | null;
|
|
18
|
+
passed: boolean;
|
|
19
|
+
error?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface VerificationResult {
|
|
23
|
+
allPassed: boolean;
|
|
24
|
+
services: VerificationServiceResult[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type VerificationState = 'idle' | 'verifying' | 'done';
|
|
28
|
+
|
|
29
|
+
export interface VerificationUpdate {
|
|
30
|
+
state: VerificationState;
|
|
31
|
+
services: ServiceStatus[];
|
|
32
|
+
result?: VerificationResult;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─── Verification Runner ─────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
/** Default timeout for verification (ms) */
|
|
38
|
+
const DEFAULT_VERIFICATION_TIMEOUT_MS = 60_000;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Launch all services from an EnvironmentConfig, health-check them,
|
|
42
|
+
* report per-service results, then tear everything down.
|
|
43
|
+
*
|
|
44
|
+
* @param config - The environment config to verify
|
|
45
|
+
* @param projectCwd - The project working directory for spawning processes
|
|
46
|
+
* @param onUpdate - Callback for live status updates during verification
|
|
47
|
+
* @param timeoutMs - Maximum time to wait for all services (default 60s)
|
|
48
|
+
* @returns VerificationResult with per-service pass/fail
|
|
49
|
+
*/
|
|
50
|
+
export async function runVerification(
|
|
51
|
+
config: EnvironmentConfig,
|
|
52
|
+
projectCwd: string | null,
|
|
53
|
+
onUpdate: (update: VerificationUpdate) => void,
|
|
54
|
+
timeoutMs: number = DEFAULT_VERIFICATION_TIMEOUT_MS,
|
|
55
|
+
): Promise<VerificationResult> {
|
|
56
|
+
let lastStatus: ProofRunStatus | null = null;
|
|
57
|
+
|
|
58
|
+
// Create a ProofRunManager to handle the actual launch + health checks
|
|
59
|
+
const manager = new ProofRunManager(
|
|
60
|
+
undefined,
|
|
61
|
+
(status: ProofRunStatus) => {
|
|
62
|
+
lastStatus = status;
|
|
63
|
+
onUpdate({
|
|
64
|
+
state: 'verifying',
|
|
65
|
+
services: status.services,
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
config,
|
|
69
|
+
projectCwd,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Notify: starting verification
|
|
73
|
+
onUpdate({ state: 'verifying', services: manager.getStatus().services });
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
// Launch services with a timeout to prevent hanging forever
|
|
77
|
+
const timeoutPromise = new Promise<'timeout'>((resolve) =>
|
|
78
|
+
setTimeout(() => resolve('timeout'), timeoutMs),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const raceResult = await Promise.race([
|
|
82
|
+
manager.startServices().then(() => 'done' as const),
|
|
83
|
+
timeoutPromise,
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
// Build result from final service statuses
|
|
87
|
+
const finalStatus = lastStatus ?? manager.getStatus();
|
|
88
|
+
|
|
89
|
+
const result: VerificationResult = {
|
|
90
|
+
allPassed: raceResult !== 'timeout' && finalStatus.services.every(s => s.status === 'running'),
|
|
91
|
+
services: finalStatus.services.map(s => {
|
|
92
|
+
const isTimedOut = raceResult === 'timeout' && s.status !== 'running';
|
|
93
|
+
return {
|
|
94
|
+
name: s.name,
|
|
95
|
+
port: s.port,
|
|
96
|
+
passed: s.status === 'running',
|
|
97
|
+
error: isTimedOut
|
|
98
|
+
? `Verification timed out waiting for ${s.name} on port ${s.port}`
|
|
99
|
+
: s.status === 'crashed'
|
|
100
|
+
? `Service failed to start on port ${s.port}`
|
|
101
|
+
: undefined,
|
|
102
|
+
};
|
|
103
|
+
}),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Notify with result before teardown
|
|
107
|
+
onUpdate({
|
|
108
|
+
state: 'done',
|
|
109
|
+
services: finalStatus.services,
|
|
110
|
+
result,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return result;
|
|
114
|
+
} finally {
|
|
115
|
+
// Always tear down — verification is fire-and-forget
|
|
116
|
+
await manager.stopServices();
|
|
117
|
+
manager.destroy();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import type { InFlightItem, KanbanGroup, WorkItem } from '@/lib/db';
|
|
2
|
+
|
|
3
|
+
export interface KanbanData {
|
|
4
|
+
inFlight: InFlightItem[];
|
|
5
|
+
backlog: Map<string, KanbanGroup>;
|
|
6
|
+
done: Map<string, KanbanGroup>;
|
|
7
|
+
/** O(1) item lookup by ID — built once during transform in data-bridge */
|
|
8
|
+
itemMap: Map<number, InFlightItem>;
|
|
9
|
+
/** O(1) status lookup by ID — built once during transform in data-bridge */
|
|
10
|
+
statusMap: Map<number, string>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// O(1) item lookup using the pre-built itemMap from data-bridge
|
|
14
|
+
export function findItemById(data: KanbanData, id: number): { item: InFlightItem; status: string } | null {
|
|
15
|
+
const item = data.itemMap.get(id);
|
|
16
|
+
if (!item) return null;
|
|
17
|
+
const status = data.statusMap.get(id) ?? item.status;
|
|
18
|
+
return { item, status };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Extract onboarding chore items from kanban data for the OnboardingWelcome component
|
|
22
|
+
export function getOnboardingItems(data: KanbanData): WorkItem[] {
|
|
23
|
+
for (const group of data.backlog.values()) {
|
|
24
|
+
if (group.epicTitle === 'Project Planning') {
|
|
25
|
+
return group.items;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Returns the pre-built statusMap from data-bridge (O(1) — no iteration needed)
|
|
32
|
+
export function buildStatusMap(kanbanData: KanbanData): Map<number, string> {
|
|
33
|
+
return kanbanData.statusMap;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---- Optimistic update helpers ----
|
|
37
|
+
// Pure functions that return new KanbanData with a mutation applied locally.
|
|
38
|
+
// Used by RealTimeKanbanWrapper to update the UI instantly before IPC completes.
|
|
39
|
+
|
|
40
|
+
/** Determine which column a status belongs to */
|
|
41
|
+
function statusColumn(status: string): 'inFlight' | 'backlog' | 'done' {
|
|
42
|
+
if (status === 'in_progress' || status === 'blocked') return 'inFlight';
|
|
43
|
+
if (status === 'done' || status === 'cancelled') return 'done';
|
|
44
|
+
return 'backlog';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Remove an item from grouped columns (backlog or done Maps) */
|
|
48
|
+
function removeFromGroups(groups: Map<string, KanbanGroup>, id: number): Map<string, KanbanGroup> {
|
|
49
|
+
const result = new Map<string, KanbanGroup>();
|
|
50
|
+
for (const [key, group] of groups) {
|
|
51
|
+
const filtered = group.items.filter(i => i.id !== id);
|
|
52
|
+
if (filtered.length > 0) {
|
|
53
|
+
result.set(key, { ...group, items: filtered });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Add an item to the appropriate epic group in a grouped column */
|
|
60
|
+
function addToGroups(groups: Map<string, KanbanGroup>, item: InFlightItem, sort: 'display_order' | 'completed_at_desc' = 'display_order'): Map<string, KanbanGroup> {
|
|
61
|
+
const result = new Map(groups);
|
|
62
|
+
const key = item.parent_id ? String(item.parent_id) : 'ungrouped';
|
|
63
|
+
const compareFn = sort === 'completed_at_desc'
|
|
64
|
+
? (a: InFlightItem, b: InFlightItem) => (b.completed_at ?? '').localeCompare(a.completed_at ?? '')
|
|
65
|
+
: (a: InFlightItem, b: InFlightItem) => (a.display_order ?? a.id * 10) - (b.display_order ?? b.id * 10);
|
|
66
|
+
const existing = result.get(key);
|
|
67
|
+
if (existing) {
|
|
68
|
+
const items = [...existing.items, item].sort(compareFn);
|
|
69
|
+
result.set(key, { ...existing, items });
|
|
70
|
+
} else {
|
|
71
|
+
result.set(key, {
|
|
72
|
+
epicId: item.parent_id,
|
|
73
|
+
epicTitle: item.parent_title,
|
|
74
|
+
items: [item],
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Replace an item in a flat array */
|
|
81
|
+
function replaceInArray(arr: InFlightItem[], id: number, updated: InFlightItem): InFlightItem[] {
|
|
82
|
+
return arr.map(i => i.id === id ? updated : i);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Replace an item in grouped columns */
|
|
86
|
+
function replaceInGroups(groups: Map<string, KanbanGroup>, id: number, updated: InFlightItem): Map<string, KanbanGroup> {
|
|
87
|
+
const result = new Map<string, KanbanGroup>();
|
|
88
|
+
for (const [key, group] of groups) {
|
|
89
|
+
if (group.items.some(i => i.id === id)) {
|
|
90
|
+
result.set(key, { ...group, items: group.items.map(i => i.id === id ? updated : i) });
|
|
91
|
+
} else {
|
|
92
|
+
result.set(key, group);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Apply a status change optimistically. Moves item to the correct column. */
|
|
99
|
+
export function applyStatusChange(
|
|
100
|
+
data: KanbanData,
|
|
101
|
+
id: number,
|
|
102
|
+
newStatus: string,
|
|
103
|
+
rejection?: { reason: string },
|
|
104
|
+
): KanbanData {
|
|
105
|
+
const existing = data.itemMap.get(id);
|
|
106
|
+
if (!existing) return data;
|
|
107
|
+
|
|
108
|
+
const updatedItem: InFlightItem = {
|
|
109
|
+
...existing,
|
|
110
|
+
status: newStatus,
|
|
111
|
+
...(rejection && {
|
|
112
|
+
rejection_reason: rejection.reason,
|
|
113
|
+
rejection_count: existing.rejection_count + 1,
|
|
114
|
+
ready_for_review: 0,
|
|
115
|
+
}),
|
|
116
|
+
// Set completed_at when moving to done/cancelled so sort-by-recent works
|
|
117
|
+
...(statusColumn(newStatus) === 'done' && !existing.completed_at && {
|
|
118
|
+
completed_at: new Date().toISOString(),
|
|
119
|
+
}),
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Remove from all columns
|
|
123
|
+
let newInFlight = data.inFlight.filter(i => i.id !== id);
|
|
124
|
+
const newBacklog = removeFromGroups(data.backlog, id);
|
|
125
|
+
const newDone = removeFromGroups(data.done, id);
|
|
126
|
+
|
|
127
|
+
// Add to correct column
|
|
128
|
+
const col = statusColumn(newStatus);
|
|
129
|
+
let finalBacklog = newBacklog;
|
|
130
|
+
let finalDone = newDone;
|
|
131
|
+
if (col === 'inFlight') {
|
|
132
|
+
newInFlight = [...newInFlight, updatedItem].sort((a, b) => (a.display_order ?? a.id * 10) - (b.display_order ?? b.id * 10));
|
|
133
|
+
} else if (col === 'done') {
|
|
134
|
+
finalDone = addToGroups(newDone, updatedItem, 'completed_at_desc');
|
|
135
|
+
} else {
|
|
136
|
+
finalBacklog = addToGroups(newBacklog, updatedItem);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const newItemMap = new Map(data.itemMap);
|
|
140
|
+
newItemMap.set(id, updatedItem);
|
|
141
|
+
const newStatusMap = new Map(data.statusMap);
|
|
142
|
+
newStatusMap.set(id, newStatus);
|
|
143
|
+
|
|
144
|
+
return { inFlight: newInFlight, backlog: finalBacklog, done: finalDone, itemMap: newItemMap, statusMap: newStatusMap };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Apply a title change optimistically. */
|
|
148
|
+
export function applyTitleChange(data: KanbanData, id: number, newTitle: string): KanbanData {
|
|
149
|
+
const existing = data.itemMap.get(id);
|
|
150
|
+
if (!existing) return data;
|
|
151
|
+
|
|
152
|
+
const updatedItem: InFlightItem = { ...existing, title: newTitle };
|
|
153
|
+
const col = statusColumn(existing.status);
|
|
154
|
+
|
|
155
|
+
const newItemMap = new Map(data.itemMap);
|
|
156
|
+
newItemMap.set(id, updatedItem);
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
inFlight: col === 'inFlight' ? replaceInArray(data.inFlight, id, updatedItem) : data.inFlight,
|
|
160
|
+
backlog: col === 'backlog' ? replaceInGroups(data.backlog, id, updatedItem) : data.backlog,
|
|
161
|
+
done: col === 'done' ? replaceInGroups(data.done, id, updatedItem) : data.done,
|
|
162
|
+
itemMap: newItemMap,
|
|
163
|
+
statusMap: data.statusMap,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Apply a display order change optimistically. */
|
|
168
|
+
export function applyOrderChange(data: KanbanData, id: number, newOrder: number): KanbanData {
|
|
169
|
+
const existing = data.itemMap.get(id);
|
|
170
|
+
if (!existing) return data;
|
|
171
|
+
|
|
172
|
+
const updatedItem: InFlightItem = { ...existing, display_order: newOrder };
|
|
173
|
+
const col = statusColumn(existing.status);
|
|
174
|
+
|
|
175
|
+
const newItemMap = new Map(data.itemMap);
|
|
176
|
+
newItemMap.set(id, updatedItem);
|
|
177
|
+
|
|
178
|
+
if (col === 'inFlight') {
|
|
179
|
+
return {
|
|
180
|
+
...data,
|
|
181
|
+
inFlight: replaceInArray(data.inFlight, id, updatedItem).sort((a, b) => (a.display_order ?? a.id * 10) - (b.display_order ?? b.id * 10)),
|
|
182
|
+
itemMap: newItemMap,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const groups = col === 'backlog' ? data.backlog : data.done;
|
|
187
|
+
const newGroups = new Map<string, KanbanGroup>();
|
|
188
|
+
for (const [key, group] of groups) {
|
|
189
|
+
if (group.items.some(i => i.id === id)) {
|
|
190
|
+
const items = group.items.map(i => i.id === id ? updatedItem : i).sort((a, b) => (a.display_order ?? a.id * 10) - (b.display_order ?? b.id * 10));
|
|
191
|
+
newGroups.set(key, { ...group, items });
|
|
192
|
+
} else {
|
|
193
|
+
newGroups.set(key, group);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
...data,
|
|
199
|
+
backlog: col === 'backlog' ? newGroups : data.backlog,
|
|
200
|
+
done: col === 'done' ? newGroups : data.done,
|
|
201
|
+
itemMap: newItemMap,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Apply an epic assignment optimistically. Moves item between epic groups. */
|
|
206
|
+
export function applyEpicAssign(data: KanbanData, id: number, epicId: number | null): KanbanData {
|
|
207
|
+
const existing = data.itemMap.get(id);
|
|
208
|
+
if (!existing) return data;
|
|
209
|
+
|
|
210
|
+
// Look up epic title from existing groups
|
|
211
|
+
let epicTitle: string | null = null;
|
|
212
|
+
if (epicId !== null) {
|
|
213
|
+
const key = String(epicId);
|
|
214
|
+
const group = data.backlog.get(key) || data.done.get(key);
|
|
215
|
+
if (group) {
|
|
216
|
+
epicTitle = group.epicTitle;
|
|
217
|
+
} else {
|
|
218
|
+
const sibling = data.inFlight.find(i => i.parent_id === epicId);
|
|
219
|
+
epicTitle = sibling?.parent_title ?? null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const updatedItem: InFlightItem = {
|
|
224
|
+
...existing,
|
|
225
|
+
parent_id: epicId,
|
|
226
|
+
parent_title: epicTitle,
|
|
227
|
+
epic_id: epicId,
|
|
228
|
+
epicTitle: epicTitle,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const col = statusColumn(existing.status);
|
|
232
|
+
const newItemMap = new Map(data.itemMap);
|
|
233
|
+
newItemMap.set(id, updatedItem);
|
|
234
|
+
|
|
235
|
+
if (col === 'inFlight') {
|
|
236
|
+
return {
|
|
237
|
+
...data,
|
|
238
|
+
inFlight: replaceInArray(data.inFlight, id, updatedItem),
|
|
239
|
+
itemMap: newItemMap,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Remove from current group, add to new group
|
|
244
|
+
const groups = col === 'backlog' ? data.backlog : data.done;
|
|
245
|
+
const sortMode = col === 'done' ? 'completed_at_desc' as const : 'display_order' as const;
|
|
246
|
+
const newGroups = addToGroups(removeFromGroups(groups, id), updatedItem, sortMode);
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
...data,
|
|
250
|
+
backlog: col === 'backlog' ? newGroups : data.backlog,
|
|
251
|
+
done: col === 'done' ? newGroups : data.done,
|
|
252
|
+
itemMap: newItemMap,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Build a mode map from kanban data (feature id -> mode) for detecting mode transitions
|
|
257
|
+
export function buildModeMap(kanbanData: KanbanData): Map<number, string | null> {
|
|
258
|
+
const map = new Map<number, string | null>();
|
|
259
|
+
const collectFeatures = (items: WorkItem[]) => {
|
|
260
|
+
for (const item of items) {
|
|
261
|
+
if (item.type === 'feature') {
|
|
262
|
+
map.set(item.id, item.mode);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
collectFeatures(kanbanData.inFlight);
|
|
267
|
+
for (const group of kanbanData.backlog.values()) collectFeatures(group.items);
|
|
268
|
+
for (const group of kanbanData.done.values()) collectFeatures(group.items);
|
|
269
|
+
return map;
|
|
270
|
+
}
|