jettypod 4.4.120 → 4.4.121
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +2 -1
- 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 +54 -49
- package/apps/dashboard/app/demo/gates/page.tsx +3 -5
- package/apps/dashboard/app/design-system/page.tsx +1 -1
- package/apps/dashboard/app/globals.css +74 -2
- package/apps/dashboard/app/install-claude/page.tsx +3 -5
- package/apps/dashboard/app/login/page.tsx +17 -20
- package/apps/dashboard/app/page.tsx +101 -48
- package/apps/dashboard/app/settings/page.tsx +60 -12
- package/apps/dashboard/app/signup/page.tsx +14 -17
- 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 +12 -15
- package/apps/dashboard/app/work/[id]/page.tsx +90 -75
- package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
- package/apps/dashboard/components/AppShell.tsx +70 -61
- package/apps/dashboard/components/CardMenu.tsx +0 -1
- package/apps/dashboard/components/ClaudePanel.tsx +541 -283
- package/apps/dashboard/components/ClaudePanelInput.tsx +23 -4
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +1 -5
- package/apps/dashboard/components/CopyableId.tsx +1 -2
- package/apps/dashboard/components/DetailReviewActions.tsx +11 -20
- package/apps/dashboard/components/DragContext.tsx +132 -62
- package/apps/dashboard/components/DraggableCard.tsx +3 -5
- package/apps/dashboard/components/DropZone.tsx +5 -6
- package/apps/dashboard/components/EditableDetailDescription.tsx +6 -12
- package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
- package/apps/dashboard/components/EditableTitle.tsx +0 -1
- package/apps/dashboard/components/ElapsedTimer.tsx +15 -3
- package/apps/dashboard/components/EpicGroup.tsx +100 -70
- package/apps/dashboard/components/GateCard.tsx +0 -1
- package/apps/dashboard/components/GateChoiceCard.tsx +1 -2
- package/apps/dashboard/components/InstallClaudeScreen.tsx +1 -5
- package/apps/dashboard/components/JettyLoader.tsx +0 -1
- package/apps/dashboard/components/KanbanBoard.tsx +319 -173
- package/apps/dashboard/components/KanbanCard.tsx +341 -107
- package/apps/dashboard/components/LazyCard.tsx +62 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +0 -1
- package/apps/dashboard/components/MainNav.tsx +24 -25
- package/apps/dashboard/components/MessageBlock.tsx +93 -16
- package/apps/dashboard/components/ModeStartCard.tsx +0 -1
- package/apps/dashboard/components/OnboardingWelcome.tsx +0 -1
- package/apps/dashboard/components/PlaceholderCard.tsx +0 -1
- package/apps/dashboard/components/ProjectSwitcher.tsx +20 -20
- package/apps/dashboard/components/PrototypeTimeline.tsx +47 -26
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +308 -223
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +303 -160
- package/apps/dashboard/components/ReviewFooter.tsx +12 -14
- package/apps/dashboard/components/SessionList.tsx +0 -1
- package/apps/dashboard/components/SubscribeContent.tsx +40 -11
- package/apps/dashboard/components/TestTree.tsx +1 -2
- package/apps/dashboard/components/TipCard.tsx +2 -4
- package/apps/dashboard/components/Toast.tsx +0 -1
- package/apps/dashboard/components/TypeIcon.tsx +7 -8
- package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +5 -17
- package/apps/dashboard/components/WelcomeScreen.tsx +2 -6
- package/apps/dashboard/components/WorkItemHeader.tsx +0 -1
- package/apps/dashboard/components/WorkItemTree.tsx +2 -4
- package/apps/dashboard/components/settings/AccountSection.tsx +27 -13
- 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 +20 -73
- package/apps/dashboard/components/settings/GeneralSection.tsx +137 -26
- package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
- package/apps/dashboard/components/settings/SettingsLayout.tsx +0 -1
- package/apps/dashboard/components/ui/Button.tsx +1 -1
- package/apps/dashboard/components/ui/Input.tsx +1 -1
- package/apps/dashboard/components.json +1 -1
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +611 -358
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +0 -1
- package/apps/dashboard/contexts/UsageContext.tsx +62 -31
- package/apps/dashboard/dev.sh +35 -0
- package/apps/dashboard/eslint.config.mjs +9 -9
- package/apps/dashboard/hooks/useWebSocket.ts +138 -83
- package/apps/dashboard/index.html +73 -0
- package/apps/dashboard/lib/data-bridge.ts +722 -0
- package/apps/dashboard/lib/db.ts +69 -1302
- 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 +226 -26
- package/apps/dashboard/lib/proof-run.ts +495 -0
- package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
- 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 +253 -122
- 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 +3 -3
- package/apps/dashboard/next-env.d.ts +1 -1
- package/apps/dashboard/package.json +21 -33
- package/apps/dashboard/public/bug-icon.png +0 -0
- package/apps/dashboard/public/buoy-icon.png +0 -0
- package/apps/dashboard/public/in-flight-seagull.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/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 +167 -30
- 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/jettypod.js +96 -4
- package/lib/bdd-preflight.js +96 -0
- package/lib/merge-lock.js +111 -253
- package/lib/migrations/030-rejection-round-columns.js +54 -0
- package/lib/migrations/031-session-isolation-index.js +17 -0
- package/lib/work-commands/index.js +58 -16
- package/lib/work-tracking/index.js +108 -8
- package/package.json +1 -1
- package/skills-templates/bug-mode/SKILL.md +43 -1
- package/skills-templates/chore-mode/SKILL.md +40 -1
- package/skills-templates/design-system-selection/SKILL.md +273 -0
- package/skills-templates/epic-planning/SKILL.md +14 -0
- package/skills-templates/feature-planning/SKILL.md +90 -1
- package/skills-templates/production-mode/SKILL.md +20 -0
- package/skills-templates/simple-improvement/SKILL.md +39 -2
- package/skills-templates/speed-mode/SKILL.md +10 -15
- package/skills-templates/stable-mode/SKILL.md +47 -0
- package/apps/dashboard/README.md +0 -36
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -446
- package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -280
- 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 -525
- 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]/route.ts +0 -35
- package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -63
- package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
- package/apps/dashboard/app/layout.tsx +0 -55
- package/apps/dashboard/components/UpgradeBanner.tsx +0 -30
- package/apps/dashboard/electron/ipc-handlers.js +0 -1026
- package/apps/dashboard/electron/main.js +0 -2306
- package/apps/dashboard/electron/preload.js +0 -125
- package/apps/dashboard/electron/session-manager.js +0 -163
- package/apps/dashboard/electron-builder.config.js +0 -357
- package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
- package/apps/dashboard/lib/backlog-parser.ts +0 -50
- package/apps/dashboard/lib/claude-process-manager.ts +0 -529
- package/apps/dashboard/lib/db-bridge.ts +0 -283
- 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 -66
- package/apps/dashboard/postcss.config.mjs +0 -7
- package/apps/dashboard/public/bug-icon.svg +0 -9
- package/apps/dashboard/public/buoy-icon.svg +0 -9
- package/apps/dashboard/public/file.svg +0 -1
- package/apps/dashboard/public/globe.svg +0 -1
- package/apps/dashboard/public/in-flight-seagull.svg +0 -9
- package/apps/dashboard/public/next.svg +0 -1
- package/apps/dashboard/public/pier-icon.svg +0 -14
- package/apps/dashboard/public/star-icon.svg +0 -9
- package/apps/dashboard/public/vercel.svg +0 -1
- package/apps/dashboard/public/window.svg +0 -1
- package/apps/dashboard/public/wrench-icon.svg +0 -9
- package/apps/dashboard/scripts/download-node.js +0 -104
- package/apps/dashboard/scripts/upload-to-r2.js +0 -89
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import { listSessions, createSession, linkSession, isLinkableWorkItem, getActiveSessionByWorkItem, getOrCreateSessionForWorkItem, closeSession, closeSessionByWorkItem, countActiveSessions, cleanupStaleSessions } from '@/lib/db';
|
|
3
|
-
import { killProcess } from '@/lib/claude-process-manager';
|
|
4
|
-
|
|
5
|
-
export const dynamic = 'force-dynamic';
|
|
6
|
-
|
|
7
|
-
// Maximum number of concurrent active sessions
|
|
8
|
-
const MAX_SESSIONS = 10;
|
|
9
|
-
|
|
10
|
-
// Session title constraints
|
|
11
|
-
const MAX_TITLE_LENGTH = 100;
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Sanitize and validate session title
|
|
15
|
-
* - Trims whitespace
|
|
16
|
-
* - Removes control characters
|
|
17
|
-
* - Enforces max length
|
|
18
|
-
*/
|
|
19
|
-
function sanitizeTitle(title: string | undefined): string {
|
|
20
|
-
if (!title) return 'New Session';
|
|
21
|
-
|
|
22
|
-
// Remove control characters and trim whitespace
|
|
23
|
-
const sanitized = title
|
|
24
|
-
.replace(/[\x00-\x1F\x7F]/g, '') // Remove control characters
|
|
25
|
-
.trim();
|
|
26
|
-
|
|
27
|
-
// Return default if empty after sanitization
|
|
28
|
-
if (!sanitized) return 'New Session';
|
|
29
|
-
|
|
30
|
-
// Enforce max length
|
|
31
|
-
return sanitized.slice(0, MAX_TITLE_LENGTH);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// GET /api/claude/sessions - List all sessions with feature info
|
|
35
|
-
export async function GET() {
|
|
36
|
-
try {
|
|
37
|
-
const sessions = listSessions();
|
|
38
|
-
// Convert numeric IDs to strings for frontend consistency
|
|
39
|
-
const normalizedSessions = sessions.map(session => ({
|
|
40
|
-
...session,
|
|
41
|
-
id: String(session.id),
|
|
42
|
-
featureId: session.featureId ? String(session.featureId) : null,
|
|
43
|
-
}));
|
|
44
|
-
return NextResponse.json(normalizedSessions);
|
|
45
|
-
} catch (error) {
|
|
46
|
-
// Log error for debugging but return empty array to client
|
|
47
|
-
// This ensures dashboard renders without crashing when DB is unavailable
|
|
48
|
-
console.error('Failed to list sessions:', error);
|
|
49
|
-
return NextResponse.json([]);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// POST /api/claude/sessions - Create new unlinked session or get/create for work item
|
|
54
|
-
export async function POST(request: Request) {
|
|
55
|
-
try {
|
|
56
|
-
const body = await request.json();
|
|
57
|
-
const { title, workItemId } = body;
|
|
58
|
-
|
|
59
|
-
// Opportunistically clean up stale sessions before checking limits
|
|
60
|
-
// This ensures old orphaned/completed sessions don't block new session creation
|
|
61
|
-
cleanupStaleSessions();
|
|
62
|
-
|
|
63
|
-
// Check session limit before creating new sessions
|
|
64
|
-
const currentCount = countActiveSessions();
|
|
65
|
-
if (currentCount >= MAX_SESSIONS) {
|
|
66
|
-
return NextResponse.json({
|
|
67
|
-
error: `Session limit reached (${MAX_SESSIONS}). Close existing sessions to create new ones.`,
|
|
68
|
-
code: 'SESSION_LIMIT_REACHED',
|
|
69
|
-
currentCount,
|
|
70
|
-
maxSessions: MAX_SESSIONS
|
|
71
|
-
}, { status: 429 });
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// If workItemId provided, use get-or-create pattern
|
|
75
|
-
if (workItemId) {
|
|
76
|
-
const result = getOrCreateSessionForWorkItem(workItemId);
|
|
77
|
-
if (!result.session) {
|
|
78
|
-
return NextResponse.json({
|
|
79
|
-
error: result.error,
|
|
80
|
-
redirectToId: result.redirectToId ? String(result.redirectToId) : undefined
|
|
81
|
-
}, { status: 400 });
|
|
82
|
-
}
|
|
83
|
-
return NextResponse.json({
|
|
84
|
-
id: String(result.session.id),
|
|
85
|
-
title: result.session.title,
|
|
86
|
-
created: result.created,
|
|
87
|
-
workItemId: result.session.work_item_id ? String(result.session.work_item_id) : null
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Otherwise create unlinked session with sanitized title
|
|
92
|
-
const sessionTitle = sanitizeTitle(title);
|
|
93
|
-
const sessionId = createSession(sessionTitle);
|
|
94
|
-
return NextResponse.json({ id: String(sessionId), title: sessionTitle, created: true });
|
|
95
|
-
} catch (error) {
|
|
96
|
-
console.error('Failed to create session:', error);
|
|
97
|
-
return NextResponse.json({ error: 'Failed to create session' }, { status: 500 });
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// PATCH /api/claude/sessions - Link session to work item
|
|
102
|
-
// Only links to: features, standalone chores, bugs
|
|
103
|
-
// Rejects: epics, chores under features (redirects to parent feature)
|
|
104
|
-
export async function PATCH(request: Request) {
|
|
105
|
-
try {
|
|
106
|
-
const body = await request.json();
|
|
107
|
-
const { sessionId, workItemId } = body;
|
|
108
|
-
|
|
109
|
-
if (!sessionId || !workItemId) {
|
|
110
|
-
return NextResponse.json({ error: 'sessionId and workItemId required' }, { status: 400 });
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Check if this work item type can have sessions
|
|
114
|
-
const linkCheck = isLinkableWorkItem(workItemId);
|
|
115
|
-
if (!linkCheck.linkable) {
|
|
116
|
-
return NextResponse.json({
|
|
117
|
-
error: linkCheck.reason,
|
|
118
|
-
redirectToId: linkCheck.redirectToId ? String(linkCheck.redirectToId) : undefined
|
|
119
|
-
}, { status: 400 });
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Check if work item already has an ACTIVE session
|
|
123
|
-
const existingSession = getActiveSessionByWorkItem(workItemId);
|
|
124
|
-
if (existingSession) {
|
|
125
|
-
return NextResponse.json({
|
|
126
|
-
error: 'Work item already has an active session',
|
|
127
|
-
existingSessionId: String(existingSession.id)
|
|
128
|
-
}, { status: 409 });
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const result = linkSession(sessionId, workItemId);
|
|
132
|
-
if (!result.success) {
|
|
133
|
-
return NextResponse.json({
|
|
134
|
-
error: result.error,
|
|
135
|
-
redirectToId: result.redirectToId ? String(result.redirectToId) : undefined
|
|
136
|
-
}, { status: 400 });
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return NextResponse.json({ success: true });
|
|
140
|
-
} catch (error) {
|
|
141
|
-
console.error('Failed to link session:', error);
|
|
142
|
-
return NextResponse.json({ error: 'Failed to link session' }, { status: 500 });
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// DELETE /api/claude/sessions - Close a session
|
|
147
|
-
// Use ?sessionId=X&type=standalone for standalone sessions (closes by claude_sessions.id)
|
|
148
|
-
// Use ?sessionId=X&type=workitem for work-item sessions (closes by work_item_id)
|
|
149
|
-
export async function DELETE(request: Request) {
|
|
150
|
-
try {
|
|
151
|
-
const { searchParams } = new URL(request.url);
|
|
152
|
-
const sessionId = searchParams.get('sessionId');
|
|
153
|
-
const sessionType = searchParams.get('type') || 'standalone'; // default to standalone for backwards compat
|
|
154
|
-
|
|
155
|
-
if (!sessionId) {
|
|
156
|
-
return NextResponse.json({ error: 'sessionId required' }, { status: 400 });
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const id = parseInt(sessionId, 10);
|
|
160
|
-
let success: boolean;
|
|
161
|
-
|
|
162
|
-
// Kill any active Claude process for this session before closing
|
|
163
|
-
// For standalone sessions, the sessionId IS the process key
|
|
164
|
-
// For work-item sessions, we'd need to look up the session ID first
|
|
165
|
-
killProcess(sessionId);
|
|
166
|
-
|
|
167
|
-
if (sessionType === 'workitem') {
|
|
168
|
-
// Work-item sessions: sessionId is the work_item_id
|
|
169
|
-
success = closeSessionByWorkItem(id);
|
|
170
|
-
} else {
|
|
171
|
-
// Standalone sessions: sessionId is the claude_sessions.id
|
|
172
|
-
success = closeSession(id);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (!success) {
|
|
176
|
-
return NextResponse.json({ error: 'Session not found' }, { status: 404 });
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return NextResponse.json({ success: true });
|
|
180
|
-
} catch (error) {
|
|
181
|
-
console.error('Failed to close session:', error);
|
|
182
|
-
return NextResponse.json({ error: 'Failed to close session' }, { status: 500 });
|
|
183
|
-
}
|
|
184
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import { getDecision } from '@/lib/db';
|
|
3
|
-
|
|
4
|
-
export const dynamic = 'force-dynamic';
|
|
5
|
-
|
|
6
|
-
interface RouteContext {
|
|
7
|
-
params: Promise<{ id: string }>;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export async function GET(request: Request, context: RouteContext) {
|
|
11
|
-
const { id } = await context.params;
|
|
12
|
-
const decisionId = parseInt(id, 10);
|
|
13
|
-
|
|
14
|
-
if (isNaN(decisionId)) {
|
|
15
|
-
return NextResponse.json({ error: 'Invalid decision ID' }, { status: 400 });
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const decision = getDecision(decisionId);
|
|
19
|
-
|
|
20
|
-
if (!decision) {
|
|
21
|
-
return NextResponse.json({ error: 'Decision not found' }, { status: 404 });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return NextResponse.json(decision);
|
|
25
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import { closeDb } from '@/lib/db';
|
|
3
|
-
|
|
4
|
-
// Internal endpoint called by the Electron main process to update the project path
|
|
5
|
-
// in the Next.js child process after a project switch. In dev mode, Next.js runs as
|
|
6
|
-
// a separate process and doesn't share process.env with Electron.
|
|
7
|
-
export async function POST(request: Request) {
|
|
8
|
-
const { projectPath } = await request.json();
|
|
9
|
-
|
|
10
|
-
// Update env var so all subsequent db.ts / test-results-db.ts calls use the new path
|
|
11
|
-
process.env.JETTYPOD_PROJECT_PATH = projectPath || '';
|
|
12
|
-
|
|
13
|
-
// Close the cached DB connection so it reconnects to the new project on next access
|
|
14
|
-
closeDb();
|
|
15
|
-
|
|
16
|
-
return NextResponse.json({ success: true });
|
|
17
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import { getKanbanData } from '@/lib/db';
|
|
3
|
-
|
|
4
|
-
export const dynamic = 'force-dynamic';
|
|
5
|
-
|
|
6
|
-
export async function GET() {
|
|
7
|
-
const data = getKanbanData();
|
|
8
|
-
|
|
9
|
-
// Convert Maps to arrays for JSON serialization
|
|
10
|
-
return NextResponse.json({
|
|
11
|
-
inFlight: data.inFlight,
|
|
12
|
-
backlog: Array.from(data.backlog.entries()),
|
|
13
|
-
done: Array.from(data.done.entries()),
|
|
14
|
-
});
|
|
15
|
-
}
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import {
|
|
3
|
-
getEnvVars,
|
|
4
|
-
setEnvVar,
|
|
5
|
-
deleteEnvVar,
|
|
6
|
-
discoverEnvFiles,
|
|
7
|
-
getSelectedEnvFile,
|
|
8
|
-
setSelectedEnvFile,
|
|
9
|
-
createEnvFile,
|
|
10
|
-
validateEnvVarName,
|
|
11
|
-
validateEnvVarValue,
|
|
12
|
-
checkDuplicateEnvVar,
|
|
13
|
-
} from '@/lib/db';
|
|
14
|
-
|
|
15
|
-
// GET /api/settings/env-vars
|
|
16
|
-
// Query params: ?file=.env.local (optional)
|
|
17
|
-
// Also accepts ?action=discover to list available .env files
|
|
18
|
-
// Also accepts ?action=selected to get the currently selected file
|
|
19
|
-
export async function GET(request: NextRequest) {
|
|
20
|
-
try {
|
|
21
|
-
const { searchParams } = new URL(request.url);
|
|
22
|
-
const action = searchParams.get('action');
|
|
23
|
-
|
|
24
|
-
if (action === 'discover') {
|
|
25
|
-
const files = discoverEnvFiles();
|
|
26
|
-
return NextResponse.json({ files });
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (action === 'selected') {
|
|
30
|
-
const selected = getSelectedEnvFile();
|
|
31
|
-
return NextResponse.json({ selected });
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const file = searchParams.get('file') || undefined;
|
|
35
|
-
const envVars = getEnvVars(file);
|
|
36
|
-
return NextResponse.json(envVars);
|
|
37
|
-
} catch (error) {
|
|
38
|
-
return NextResponse.json(
|
|
39
|
-
{ error: 'Failed to read environment variables' },
|
|
40
|
-
{ status: 500 }
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// POST /api/settings/env-vars
|
|
46
|
-
// Body: { name, value, file? } to add/update a variable
|
|
47
|
-
// Body: { action: 'select', file } to set selected file
|
|
48
|
-
// Body: { action: 'create', file? } to create a new .env file
|
|
49
|
-
export async function POST(request: NextRequest) {
|
|
50
|
-
try {
|
|
51
|
-
const body = await request.json();
|
|
52
|
-
|
|
53
|
-
if (body.action === 'select') {
|
|
54
|
-
setSelectedEnvFile(body.file);
|
|
55
|
-
return NextResponse.json({ success: true });
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (body.action === 'create') {
|
|
59
|
-
const filename = body.file || '.env';
|
|
60
|
-
createEnvFile(filename);
|
|
61
|
-
return NextResponse.json({ success: true, file: filename });
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const { name, value, file } = body;
|
|
65
|
-
|
|
66
|
-
// Validate name
|
|
67
|
-
const nameCheck = validateEnvVarName(name);
|
|
68
|
-
if (!nameCheck.valid) {
|
|
69
|
-
return NextResponse.json(
|
|
70
|
-
{ error: nameCheck.error },
|
|
71
|
-
{ status: 400 }
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Validate value
|
|
76
|
-
const valueCheck = validateEnvVarValue(value);
|
|
77
|
-
if (!valueCheck.valid) {
|
|
78
|
-
return NextResponse.json(
|
|
79
|
-
{ error: valueCheck.error },
|
|
80
|
-
{ status: 400 }
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Check for duplicates when adding (not updating)
|
|
85
|
-
if (body.action === 'add' && checkDuplicateEnvVar(name, file)) {
|
|
86
|
-
return NextResponse.json(
|
|
87
|
-
{ error: 'Variable already exists' },
|
|
88
|
-
{ status: 409 }
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
setEnvVar(name, value, file);
|
|
93
|
-
return NextResponse.json({ success: true });
|
|
94
|
-
} catch (error) {
|
|
95
|
-
return NextResponse.json(
|
|
96
|
-
{ error: 'Failed to save environment variable' },
|
|
97
|
-
{ status: 500 }
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// DELETE /api/settings/env-vars
|
|
103
|
-
// Body: { name, file? }
|
|
104
|
-
export async function DELETE(request: NextRequest) {
|
|
105
|
-
try {
|
|
106
|
-
const body = await request.json();
|
|
107
|
-
const { name, file } = body;
|
|
108
|
-
|
|
109
|
-
const nameCheck = validateEnvVarName(name);
|
|
110
|
-
if (!nameCheck.valid) {
|
|
111
|
-
return NextResponse.json(
|
|
112
|
-
{ error: nameCheck.error },
|
|
113
|
-
{ status: 400 }
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
deleteEnvVar(name, file);
|
|
118
|
-
return NextResponse.json({ success: true });
|
|
119
|
-
} catch (error) {
|
|
120
|
-
return NextResponse.json(
|
|
121
|
-
{ error: 'Failed to delete environment variable' },
|
|
122
|
-
{ status: 500 }
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import { getMainBranch, setMainBranch } from '@/lib/db';
|
|
3
|
-
|
|
4
|
-
// GET /api/settings/general
|
|
5
|
-
export async function GET() {
|
|
6
|
-
const mainBranch = getMainBranch();
|
|
7
|
-
return NextResponse.json({ mainBranch });
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
// POST /api/settings/general
|
|
11
|
-
// Body: { mainBranch: string | null }
|
|
12
|
-
export async function POST(request: NextRequest) {
|
|
13
|
-
const body = await request.json();
|
|
14
|
-
|
|
15
|
-
if ('mainBranch' in body) {
|
|
16
|
-
setMainBranch(body.mainBranch);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const mainBranch = getMainBranch();
|
|
20
|
-
return NextResponse.json({ mainBranch });
|
|
21
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import { ingestCucumberResults } from '@/lib/test-results-db';
|
|
3
|
-
|
|
4
|
-
export const dynamic = 'force-dynamic';
|
|
5
|
-
|
|
6
|
-
// Shared test execution engine (CommonJS)
|
|
7
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
8
|
-
const { runScenario } = require('../../../../../../lib/test-runner');
|
|
9
|
-
|
|
10
|
-
export async function POST(request: Request) {
|
|
11
|
-
let body: { featureFile?: unknown; scenarioTitle?: unknown };
|
|
12
|
-
try {
|
|
13
|
-
body = await request.json();
|
|
14
|
-
} catch {
|
|
15
|
-
return NextResponse.json(
|
|
16
|
-
{ error: 'Invalid JSON in request body' },
|
|
17
|
-
{ status: 400 }
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const { featureFile, scenarioTitle } = body;
|
|
22
|
-
|
|
23
|
-
if (!featureFile || !scenarioTitle) {
|
|
24
|
-
return NextResponse.json(
|
|
25
|
-
{ error: 'featureFile and scenarioTitle are required' },
|
|
26
|
-
{ status: 400 }
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (typeof featureFile !== 'string' || typeof scenarioTitle !== 'string') {
|
|
31
|
-
return NextResponse.json(
|
|
32
|
-
{ error: 'featureFile and scenarioTitle must be strings' },
|
|
33
|
-
{ status: 400 }
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (!featureFile.endsWith('.feature')) {
|
|
38
|
-
return NextResponse.json(
|
|
39
|
-
{ error: 'featureFile must be a .feature file' },
|
|
40
|
-
{ status: 400 }
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (featureFile.includes('..')) {
|
|
45
|
-
return NextResponse.json(
|
|
46
|
-
{ error: 'featureFile must not contain path traversal' },
|
|
47
|
-
{ status: 400 }
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (scenarioTitle.length > 500) {
|
|
52
|
-
return NextResponse.json(
|
|
53
|
-
{ error: 'scenarioTitle must be 500 characters or less' },
|
|
54
|
-
{ status: 400 }
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
const result = await runScenario(featureFile, scenarioTitle, {
|
|
60
|
-
timeout: 60000,
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// Ingest results into SQLite so dashboard updates immediately
|
|
64
|
-
try {
|
|
65
|
-
ingestCucumberResults(result.resultsPath);
|
|
66
|
-
} catch {
|
|
67
|
-
// Non-fatal: results file may not exist if cucumber crashed
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return NextResponse.json({
|
|
71
|
-
success: result.success,
|
|
72
|
-
exitCode: result.exitCode,
|
|
73
|
-
stdout: result.stdout,
|
|
74
|
-
stderr: result.stderr,
|
|
75
|
-
});
|
|
76
|
-
} catch (err) {
|
|
77
|
-
return NextResponse.json(
|
|
78
|
-
{ error: 'Failed to execute test', details: String(err) },
|
|
79
|
-
{ status: 500 }
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { NextRequest } from 'next/server';
|
|
2
|
-
import { ingestCucumberResults } from '@/lib/test-results-db';
|
|
3
|
-
|
|
4
|
-
export const dynamic = 'force-dynamic';
|
|
5
|
-
|
|
6
|
-
// Shared test execution engine (CommonJS)
|
|
7
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
8
|
-
const { runScenario, runFeature } = require('../../../../../../../lib/test-runner');
|
|
9
|
-
|
|
10
|
-
export async function GET(request: NextRequest) {
|
|
11
|
-
const { searchParams } = new URL(request.url);
|
|
12
|
-
const featureFile = searchParams.get('featureFile');
|
|
13
|
-
const scenarioTitle = searchParams.get('scenarioTitle');
|
|
14
|
-
|
|
15
|
-
if (!featureFile) {
|
|
16
|
-
return new Response('featureFile is required', { status: 400 });
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (!featureFile.endsWith('.feature') || featureFile.includes('..')) {
|
|
20
|
-
return new Response('Invalid featureFile', { status: 400 });
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const encoder = new TextEncoder();
|
|
24
|
-
const stream = new ReadableStream({
|
|
25
|
-
start(controller) {
|
|
26
|
-
function send(event: string, data: Record<string, unknown>) {
|
|
27
|
-
controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Hold back test_complete events from the runner — we need to ingest
|
|
31
|
-
// results into SQLite BEFORE the frontend refreshes, otherwise it reads stale data.
|
|
32
|
-
let heldTestComplete: Record<string, unknown> | null = null;
|
|
33
|
-
|
|
34
|
-
const onEvent = (event: { type: string; [key: string]: unknown }) => {
|
|
35
|
-
const { type, ...data } = event;
|
|
36
|
-
if (type === 'test_complete') {
|
|
37
|
-
heldTestComplete = data;
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
send(type, data);
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// If scenarioTitle provided, run single scenario; otherwise run entire feature
|
|
44
|
-
const run = scenarioTitle
|
|
45
|
-
? runScenario(featureFile, scenarioTitle, { onEvent, signal: request.signal })
|
|
46
|
-
: runFeature(featureFile, { onEvent, signal: request.signal });
|
|
47
|
-
|
|
48
|
-
run.then((result: { resultsPath: string }) => {
|
|
49
|
-
// Ingest results into SQLite BEFORE sending test_complete
|
|
50
|
-
try {
|
|
51
|
-
ingestCucumberResults(result.resultsPath);
|
|
52
|
-
} catch {
|
|
53
|
-
// Non-fatal
|
|
54
|
-
}
|
|
55
|
-
send('test_complete', heldTestComplete || { status: 'fail' });
|
|
56
|
-
controller.close();
|
|
57
|
-
}).catch(() => {
|
|
58
|
-
send('test_complete', { status: 'fail' });
|
|
59
|
-
controller.close();
|
|
60
|
-
});
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
return new Response(stream, {
|
|
65
|
-
headers: {
|
|
66
|
-
'Content-Type': 'text/event-stream',
|
|
67
|
-
'Cache-Control': 'no-cache',
|
|
68
|
-
'Connection': 'keep-alive',
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import { getUndefinedSteps } from '@/lib/tests';
|
|
3
|
-
|
|
4
|
-
export const dynamic = 'force-dynamic';
|
|
5
|
-
|
|
6
|
-
export async function GET() {
|
|
7
|
-
const undefinedSteps = getUndefinedSteps();
|
|
8
|
-
return NextResponse.json(undefinedSteps);
|
|
9
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import { getWeeklyUsage } from '@/lib/db';
|
|
3
|
-
|
|
4
|
-
export const dynamic = 'force-dynamic';
|
|
5
|
-
|
|
6
|
-
const SAFE_DEFAULT = { used: 0, limit: 20, remaining: 20, allowed: true };
|
|
7
|
-
|
|
8
|
-
export async function GET() {
|
|
9
|
-
try {
|
|
10
|
-
const usage = getWeeklyUsage();
|
|
11
|
-
console.log('[usage] /api/usage responding with', usage);
|
|
12
|
-
return NextResponse.json(usage);
|
|
13
|
-
} catch (err) {
|
|
14
|
-
console.error('[usage] /api/usage ERROR — returning safe default', err);
|
|
15
|
-
return NextResponse.json(SAFE_DEFAULT);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import { updateWorkItemDescription } from '@/lib/db';
|
|
3
|
-
|
|
4
|
-
export async function PATCH(
|
|
5
|
-
request: NextRequest,
|
|
6
|
-
{ params }: { params: Promise<{ id: string }> }
|
|
7
|
-
) {
|
|
8
|
-
const { id } = await params;
|
|
9
|
-
const workItemId = parseInt(id, 10);
|
|
10
|
-
|
|
11
|
-
const body = await request.json();
|
|
12
|
-
const { description } = body;
|
|
13
|
-
|
|
14
|
-
const updated = updateWorkItemDescription(workItemId, description);
|
|
15
|
-
|
|
16
|
-
if (updated) {
|
|
17
|
-
return NextResponse.json({ success: true, id: workItemId, description });
|
|
18
|
-
} else {
|
|
19
|
-
return NextResponse.json({ success: false, error: 'Work item not found' }, { status: 404 });
|
|
20
|
-
}
|
|
21
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import { updateWorkItemEpic } from '@/lib/db';
|
|
3
|
-
|
|
4
|
-
export async function PATCH(
|
|
5
|
-
request: NextRequest,
|
|
6
|
-
{ params }: { params: Promise<{ id: string }> }
|
|
7
|
-
) {
|
|
8
|
-
const { id } = await params;
|
|
9
|
-
const workItemId = parseInt(id, 10);
|
|
10
|
-
|
|
11
|
-
const body = await request.json();
|
|
12
|
-
const { epic_id } = body;
|
|
13
|
-
|
|
14
|
-
const updated = updateWorkItemEpic(workItemId, epic_id);
|
|
15
|
-
|
|
16
|
-
if (updated) {
|
|
17
|
-
return NextResponse.json({ success: true, id: workItemId, epic_id });
|
|
18
|
-
} else {
|
|
19
|
-
return NextResponse.json({ success: false, error: 'Work item not found or cannot be assigned to epic' }, { status: 404 });
|
|
20
|
-
}
|
|
21
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import { updateWorkItemOrder } from '@/lib/db';
|
|
3
|
-
|
|
4
|
-
export async function PATCH(
|
|
5
|
-
request: NextRequest,
|
|
6
|
-
{ params }: { params: Promise<{ id: string }> }
|
|
7
|
-
) {
|
|
8
|
-
const { id } = await params;
|
|
9
|
-
const workItemId = parseInt(id, 10);
|
|
10
|
-
|
|
11
|
-
const body = await request.json();
|
|
12
|
-
const { display_order } = body;
|
|
13
|
-
|
|
14
|
-
const updated = updateWorkItemOrder(workItemId, display_order);
|
|
15
|
-
|
|
16
|
-
if (updated) {
|
|
17
|
-
return NextResponse.json({ success: true, id: workItemId, display_order });
|
|
18
|
-
} else {
|
|
19
|
-
return NextResponse.json({ success: false, error: 'Work item not found' }, { status: 404 });
|
|
20
|
-
}
|
|
21
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import { getWorkItem } from '@/lib/db';
|
|
3
|
-
|
|
4
|
-
export const dynamic = 'force-dynamic';
|
|
5
|
-
|
|
6
|
-
export async function GET(
|
|
7
|
-
request: NextRequest,
|
|
8
|
-
{ params }: { params: Promise<{ id: string }> }
|
|
9
|
-
) {
|
|
10
|
-
const { id } = await params;
|
|
11
|
-
const workItemId = parseInt(id, 10);
|
|
12
|
-
|
|
13
|
-
const item = getWorkItem(workItemId);
|
|
14
|
-
|
|
15
|
-
if (item) {
|
|
16
|
-
// For chores/bugs with a parent, check the parent's ready_for_review too.
|
|
17
|
-
// The ready_for_review flag is set on the parent feature, not on individual chores.
|
|
18
|
-
let readyForReview = item.ready_for_review;
|
|
19
|
-
if (!readyForReview && item.parent_id) {
|
|
20
|
-
const parent = getWorkItem(item.parent_id);
|
|
21
|
-
if (parent) {
|
|
22
|
-
readyForReview = parent.ready_for_review;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return NextResponse.json({
|
|
27
|
-
id: item.id,
|
|
28
|
-
status: item.status,
|
|
29
|
-
ready_for_review: readyForReview,
|
|
30
|
-
rejection_reason: item.rejection_reason,
|
|
31
|
-
});
|
|
32
|
-
} else {
|
|
33
|
-
return NextResponse.json({ error: 'Work item not found' }, { status: 404 });
|
|
34
|
-
}
|
|
35
|
-
}
|