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
|
@@ -1,1028 +0,0 @@
|
|
|
1
|
-
const { ipcMain, dialog, BrowserWindow, app, shell } = require('electron');
|
|
2
|
-
const Database = require('better-sqlite3');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const { execSync } = require('child_process');
|
|
6
|
-
const { runMigrations } = require('../lib/run-migrations');
|
|
7
|
-
|
|
8
|
-
// ==================== Recent Projects Storage ====================
|
|
9
|
-
const MAX_RECENT_PROJECTS = 10;
|
|
10
|
-
|
|
11
|
-
function getRecentProjectsPath() {
|
|
12
|
-
return path.join(app.getPath('userData'), 'recent-projects.json');
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function readRecentProjects() {
|
|
16
|
-
const recentPath = getRecentProjectsPath();
|
|
17
|
-
if (fs.existsSync(recentPath)) {
|
|
18
|
-
try {
|
|
19
|
-
return JSON.parse(fs.readFileSync(recentPath, 'utf-8'));
|
|
20
|
-
} catch {
|
|
21
|
-
return [];
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return [];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function writeRecentProjects(projects) {
|
|
28
|
-
const recentPath = getRecentProjectsPath();
|
|
29
|
-
// Ensure the directory exists
|
|
30
|
-
const dir = path.dirname(recentPath);
|
|
31
|
-
if (!fs.existsSync(dir)) {
|
|
32
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
33
|
-
}
|
|
34
|
-
fs.writeFileSync(recentPath, JSON.stringify(projects, null, 2));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function addRecentProject(projectPath) {
|
|
38
|
-
const projects = readRecentProjects();
|
|
39
|
-
|
|
40
|
-
// Get project name from config or folder name
|
|
41
|
-
let name = path.basename(projectPath);
|
|
42
|
-
const configPath = path.join(projectPath, '.jettypod', 'config.json');
|
|
43
|
-
if (fs.existsSync(configPath)) {
|
|
44
|
-
try {
|
|
45
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
46
|
-
if (config.name) {
|
|
47
|
-
name = config.name;
|
|
48
|
-
}
|
|
49
|
-
} catch {
|
|
50
|
-
// Use folder name as fallback
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Remove existing entry with same path
|
|
55
|
-
const filtered = projects.filter(p => p.path !== projectPath);
|
|
56
|
-
|
|
57
|
-
// Add new entry at the beginning
|
|
58
|
-
const newEntry = {
|
|
59
|
-
name,
|
|
60
|
-
path: projectPath,
|
|
61
|
-
lastOpened: new Date().toISOString()
|
|
62
|
-
};
|
|
63
|
-
filtered.unshift(newEntry);
|
|
64
|
-
|
|
65
|
-
// Keep only the most recent projects
|
|
66
|
-
const limited = filtered.slice(0, MAX_RECENT_PROJECTS);
|
|
67
|
-
|
|
68
|
-
writeRecentProjects(limited);
|
|
69
|
-
return newEntry;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Singleton database connection
|
|
73
|
-
let cachedDb = null;
|
|
74
|
-
let projectRoot = null;
|
|
75
|
-
|
|
76
|
-
function getProjectRoot() {
|
|
77
|
-
if (projectRoot) return projectRoot;
|
|
78
|
-
|
|
79
|
-
// Use JETTYPOD_PROJECT_PATH if set
|
|
80
|
-
if (process.env.JETTYPOD_PROJECT_PATH) {
|
|
81
|
-
projectRoot = process.env.JETTYPOD_PROJECT_PATH;
|
|
82
|
-
return projectRoot;
|
|
83
|
-
}
|
|
84
|
-
try {
|
|
85
|
-
projectRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
|
|
86
|
-
return projectRoot;
|
|
87
|
-
} catch {
|
|
88
|
-
throw new Error('Not in a git repository and JETTYPOD_PROJECT_PATH not set');
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function getDbPath() {
|
|
93
|
-
return path.join(getProjectRoot(), '.jettypod', 'work.db');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function getDb() {
|
|
97
|
-
if (!cachedDb) {
|
|
98
|
-
const dbPath = getDbPath();
|
|
99
|
-
cachedDb = new Database(dbPath);
|
|
100
|
-
cachedDb.pragma('journal_mode = WAL');
|
|
101
|
-
cachedDb.pragma('foreign_keys = ON');
|
|
102
|
-
// Run pending migrations to handle old project databases
|
|
103
|
-
try {
|
|
104
|
-
runMigrations(cachedDb);
|
|
105
|
-
} catch (err) {
|
|
106
|
-
console.error('Failed to run migrations:', err);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return cachedDb;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function getConfigPath() {
|
|
113
|
-
return path.join(getProjectRoot(), '.jettypod', 'config.json');
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function readConfig() {
|
|
117
|
-
const configPath = getConfigPath();
|
|
118
|
-
if (fs.existsSync(configPath)) {
|
|
119
|
-
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
120
|
-
}
|
|
121
|
-
return {};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function writeConfig(config) {
|
|
125
|
-
const configPath = getConfigPath();
|
|
126
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Initialize JettyPod in a directory (replaces CLI 'jettypod init')
|
|
130
|
-
// This allows the packaged Electron app to work without the CLI being installed
|
|
131
|
-
function initializeJettypod(projectPath) {
|
|
132
|
-
const jettypodDir = path.join(projectPath, '.jettypod');
|
|
133
|
-
|
|
134
|
-
// Create .jettypod directory if it doesn't exist
|
|
135
|
-
if (!fs.existsSync(jettypodDir)) {
|
|
136
|
-
fs.mkdirSync(jettypodDir, { recursive: true });
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Create config.json with minimal initial values if it doesn't exist
|
|
140
|
-
const configPath = path.join(jettypodDir, 'config.json');
|
|
141
|
-
if (!fs.existsSync(configPath)) {
|
|
142
|
-
const projectName = path.basename(projectPath);
|
|
143
|
-
const initialConfig = {
|
|
144
|
-
name: projectName,
|
|
145
|
-
project_state: 'internal',
|
|
146
|
-
created_at: new Date().toISOString()
|
|
147
|
-
};
|
|
148
|
-
fs.writeFileSync(configPath, JSON.stringify(initialConfig, null, 2));
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// The database (work.db) will be created automatically when getDb() is called
|
|
152
|
-
// runMigrations() in getDb() will create all necessary tables
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Register all IPC handlers
|
|
157
|
-
function registerIpcHandlers() {
|
|
158
|
-
// ==================== Kanban ====================
|
|
159
|
-
ipcMain.handle('db:getKanbanData', (event, doneLimit = 50) => {
|
|
160
|
-
const db = getDb();
|
|
161
|
-
|
|
162
|
-
// Get all epics for lookup
|
|
163
|
-
const epics = db.prepare(`
|
|
164
|
-
SELECT id, title FROM work_items WHERE type = 'epic'
|
|
165
|
-
`).all();
|
|
166
|
-
const epicMap = new Map(epics.map(e => [e.id, e.title]));
|
|
167
|
-
|
|
168
|
-
// Get all chores that belong to features
|
|
169
|
-
const featureChores = db.prepare(`
|
|
170
|
-
SELECT c.id, c.type, c.title, c.description, c.status, c.parent_id, c.epic_id,
|
|
171
|
-
c.branch_name, c.mode, c.phase, c.completed_at, c.created_at,
|
|
172
|
-
wc.current_step, wc.total_steps
|
|
173
|
-
FROM work_items c
|
|
174
|
-
INNER JOIN work_items f ON c.parent_id = f.id
|
|
175
|
-
LEFT JOIN workflow_checkpoints wc ON wc.work_item_id = c.id
|
|
176
|
-
WHERE c.type = 'chore' AND f.type = 'feature'
|
|
177
|
-
ORDER BY c.id
|
|
178
|
-
`).all();
|
|
179
|
-
|
|
180
|
-
const choresByFeature = new Map();
|
|
181
|
-
for (const chore of featureChores) {
|
|
182
|
-
if (chore.parent_id) {
|
|
183
|
-
const existing = choresByFeature.get(chore.parent_id) || [];
|
|
184
|
-
existing.push(chore);
|
|
185
|
-
choresByFeature.set(chore.parent_id, existing);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Get all bugs that belong to features
|
|
190
|
-
const featureBugs = db.prepare(`
|
|
191
|
-
SELECT b.id, b.type, b.title, b.description, b.status, b.parent_id, b.epic_id,
|
|
192
|
-
b.branch_name, b.mode, b.phase, b.completed_at, b.created_at
|
|
193
|
-
FROM work_items b
|
|
194
|
-
INNER JOIN work_items f ON b.parent_id = f.id
|
|
195
|
-
WHERE b.type = 'bug' AND f.type = 'feature'
|
|
196
|
-
ORDER BY b.id
|
|
197
|
-
`).all();
|
|
198
|
-
|
|
199
|
-
const bugsByFeature = new Map();
|
|
200
|
-
for (const bug of featureBugs) {
|
|
201
|
-
if (bug.parent_id) {
|
|
202
|
-
const existing = bugsByFeature.get(bug.parent_id) || [];
|
|
203
|
-
existing.push(bug);
|
|
204
|
-
bugsByFeature.set(bug.parent_id, existing);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Get kanban-eligible items
|
|
209
|
-
const allItems = db.prepare(`
|
|
210
|
-
SELECT w.id, w.type, w.title, w.description, w.status, w.parent_id, w.epic_id,
|
|
211
|
-
w.branch_name, w.mode, w.phase, w.completed_at, w.created_at, w.display_order,
|
|
212
|
-
p.type as parent_type,
|
|
213
|
-
wc.current_step, wc.total_steps
|
|
214
|
-
FROM work_items w
|
|
215
|
-
LEFT JOIN work_items p ON w.parent_id = p.id
|
|
216
|
-
LEFT JOIN workflow_checkpoints wc ON wc.work_item_id = w.id
|
|
217
|
-
WHERE w.type IN ('feature', 'chore', 'bug')
|
|
218
|
-
AND (w.parent_id IS NULL OR p.type = 'epic')
|
|
219
|
-
ORDER BY COALESCE(w.display_order, w.id)
|
|
220
|
-
`).all();
|
|
221
|
-
|
|
222
|
-
const inFlight = [];
|
|
223
|
-
const backlogGroups = new Map();
|
|
224
|
-
const doneGroups = new Map();
|
|
225
|
-
|
|
226
|
-
function getGroup(groups, item) {
|
|
227
|
-
const epicId = item.parent_id || item.epic_id;
|
|
228
|
-
const key = epicId ? `epic-${epicId}` : 'ungrouped';
|
|
229
|
-
|
|
230
|
-
if (!groups.has(key)) {
|
|
231
|
-
groups.set(key, {
|
|
232
|
-
epicId: epicId,
|
|
233
|
-
epicTitle: epicId ? epicMap.get(epicId) || null : null,
|
|
234
|
-
items: []
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
return groups.get(key);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
for (const item of allItems) {
|
|
241
|
-
const { parent_type, ...cleanItem } = item;
|
|
242
|
-
|
|
243
|
-
if (cleanItem.type === 'feature') {
|
|
244
|
-
cleanItem.chores = choresByFeature.get(cleanItem.id) || [];
|
|
245
|
-
cleanItem.bugs = bugsByFeature.get(cleanItem.id) || [];
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (cleanItem.status === 'in_progress') {
|
|
249
|
-
const epicId = cleanItem.parent_id || cleanItem.epic_id;
|
|
250
|
-
const epicTitle = epicId ? epicMap.get(epicId) || null : null;
|
|
251
|
-
inFlight.push({ ...cleanItem, epicTitle });
|
|
252
|
-
} else if (cleanItem.status === 'backlog' || cleanItem.status === 'todo' || cleanItem.status === null) {
|
|
253
|
-
const group = getGroup(backlogGroups, cleanItem);
|
|
254
|
-
group.items.push(cleanItem);
|
|
255
|
-
} else if (cleanItem.status === 'done') {
|
|
256
|
-
const epicId = cleanItem.parent_id || cleanItem.epic_id;
|
|
257
|
-
if (epicId) {
|
|
258
|
-
const group = getGroup(doneGroups, cleanItem);
|
|
259
|
-
group.items.push(cleanItem);
|
|
260
|
-
} else {
|
|
261
|
-
const key = `item-${cleanItem.id}`;
|
|
262
|
-
doneGroups.set(key, {
|
|
263
|
-
epicId: null,
|
|
264
|
-
epicTitle: null,
|
|
265
|
-
items: [cleanItem]
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (!backlogGroups.has('ungrouped')) {
|
|
272
|
-
backlogGroups.set('ungrouped', { epicId: null, epicTitle: null, items: [] });
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Sort done items
|
|
276
|
-
for (const [, group] of doneGroups) {
|
|
277
|
-
group.items.sort((a, b) => {
|
|
278
|
-
const dateA = a.completed_at ? new Date(a.completed_at).getTime() : 0;
|
|
279
|
-
const dateB = b.completed_at ? new Date(b.completed_at).getTime() : 0;
|
|
280
|
-
return dateB - dateA;
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Sort and limit done groups
|
|
285
|
-
const sortedDoneGroups = new Map(
|
|
286
|
-
Array.from(doneGroups.entries()).sort(([, groupA], [, groupB]) => {
|
|
287
|
-
const mostRecentA = groupA.items[0]?.completed_at ? new Date(groupA.items[0].completed_at).getTime() : 0;
|
|
288
|
-
const mostRecentB = groupB.items[0]?.completed_at ? new Date(groupB.items[0].completed_at).getTime() : 0;
|
|
289
|
-
return mostRecentB - mostRecentA;
|
|
290
|
-
})
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
// Limit done items per group to ensure every group gets representation
|
|
294
|
-
const perGroupLimit = doneLimit;
|
|
295
|
-
const limitedDoneGroups = new Map();
|
|
296
|
-
for (const [key, group] of sortedDoneGroups) {
|
|
297
|
-
if (group.items.length > perGroupLimit) {
|
|
298
|
-
limitedDoneGroups.set(key, { ...group, items: group.items.slice(0, perGroupLimit) });
|
|
299
|
-
} else {
|
|
300
|
-
limitedDoneGroups.set(key, group);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Convert Maps to arrays for serialization
|
|
305
|
-
return {
|
|
306
|
-
inFlight,
|
|
307
|
-
backlog: Array.from(backlogGroups.entries()),
|
|
308
|
-
done: Array.from(limitedDoneGroups.entries())
|
|
309
|
-
};
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
// ==================== Work Items ====================
|
|
313
|
-
ipcMain.handle('db:getAllWorkItems', () => {
|
|
314
|
-
const db = getDb();
|
|
315
|
-
return db.prepare(`
|
|
316
|
-
SELECT id, type, title, description, status, parent_id, epic_id,
|
|
317
|
-
branch_name, mode, phase, completed_at, created_at
|
|
318
|
-
FROM work_items ORDER BY id
|
|
319
|
-
`).all();
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
ipcMain.handle('db:getWorkItem', (event, id) => {
|
|
323
|
-
const db = getDb();
|
|
324
|
-
return db.prepare(`
|
|
325
|
-
SELECT id, type, title, description, status, parent_id, epic_id,
|
|
326
|
-
branch_name, mode, phase, completed_at, created_at
|
|
327
|
-
FROM work_items WHERE id = ?
|
|
328
|
-
`).get(id) || null;
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
ipcMain.handle('db:getChildWorkItems', (event, parentId) => {
|
|
332
|
-
const db = getDb();
|
|
333
|
-
return db.prepare(`
|
|
334
|
-
SELECT id, type, title, description, status, parent_id, epic_id,
|
|
335
|
-
branch_name, mode, phase, completed_at, created_at
|
|
336
|
-
FROM work_items WHERE parent_id = ? ORDER BY id
|
|
337
|
-
`).all(parentId);
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
ipcMain.handle('db:updateWorkItemTitle', (event, id, title) => {
|
|
341
|
-
const db = getDb();
|
|
342
|
-
const result = db.prepare(`UPDATE work_items SET title = ? WHERE id = ?`).run(title, id);
|
|
343
|
-
return result.changes > 0;
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
ipcMain.handle('db:updateWorkItemStatus', (event, id, status) => {
|
|
347
|
-
const db = getDb();
|
|
348
|
-
const completedAt = status === 'done' ? new Date().toISOString() : null;
|
|
349
|
-
const result = db.prepare(`
|
|
350
|
-
UPDATE work_items SET status = ?, completed_at = ? WHERE id = ?
|
|
351
|
-
`).run(status, completedAt, id);
|
|
352
|
-
|
|
353
|
-
if (status === 'cancelled' && result.changes > 0) {
|
|
354
|
-
db.prepare(`
|
|
355
|
-
UPDATE claude_sessions SET status = 'orphaned', completed_at = datetime('now')
|
|
356
|
-
WHERE work_item_id = ? AND status = 'active'
|
|
357
|
-
`).run(id);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
return result.changes > 0;
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
ipcMain.handle('db:updateWorkItemOrder', (event, id, displayOrder) => {
|
|
364
|
-
const db = getDb();
|
|
365
|
-
const result = db.prepare(`UPDATE work_items SET display_order = ? WHERE id = ?`).run(displayOrder, id);
|
|
366
|
-
return result.changes > 0;
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
ipcMain.handle('db:updateWorkItemEpic', (event, id, epicId) => {
|
|
370
|
-
const db = getDb();
|
|
371
|
-
const result = db.prepare(`
|
|
372
|
-
UPDATE work_items SET parent_id = ? WHERE id = ? AND type IN ('feature', 'chore', 'bug')
|
|
373
|
-
`).run(epicId, id);
|
|
374
|
-
return result.changes > 0;
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
// ==================== Decisions ====================
|
|
378
|
-
ipcMain.handle('db:getDecision', (event, id) => {
|
|
379
|
-
const db = getDb();
|
|
380
|
-
if (id > 0) {
|
|
381
|
-
return db.prepare(`
|
|
382
|
-
SELECT d.id, d.work_item_id, d.aspect, d.decision, d.rationale, d.created_at,
|
|
383
|
-
w.title as work_item_title
|
|
384
|
-
FROM discovery_decisions d
|
|
385
|
-
LEFT JOIN work_items w ON d.work_item_id = w.id
|
|
386
|
-
WHERE d.id = ?
|
|
387
|
-
`).get(id) || null;
|
|
388
|
-
} else {
|
|
389
|
-
const workItemId = Math.abs(id);
|
|
390
|
-
return db.prepare(`
|
|
391
|
-
SELECT id * -1 as id, id as work_item_id, 'UX Approach' as aspect,
|
|
392
|
-
discovery_winner as decision, discovery_rationale as rationale,
|
|
393
|
-
COALESCE(discovery_completed_at, created_at) as created_at,
|
|
394
|
-
title as work_item_title
|
|
395
|
-
FROM work_items WHERE id = ? AND discovery_winner IS NOT NULL
|
|
396
|
-
`).get(workItemId) || null;
|
|
397
|
-
}
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
ipcMain.handle('db:getDecisionsForWorkItem', (event, workItemId) => {
|
|
401
|
-
const db = getDb();
|
|
402
|
-
return db.prepare(`
|
|
403
|
-
SELECT id, work_item_id, aspect, decision, rationale, created_at
|
|
404
|
-
FROM discovery_decisions WHERE work_item_id = ? ORDER BY created_at
|
|
405
|
-
`).all(workItemId);
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
// ==================== Sessions ====================
|
|
409
|
-
ipcMain.handle('db:listSessions', () => {
|
|
410
|
-
const db = getDb();
|
|
411
|
-
return db.prepare(`
|
|
412
|
-
SELECT cs.id, COALESCE(w.title, cs.session_title, 'Untitled Session') as title,
|
|
413
|
-
cs.work_item_id as featureId, w.title as featureTitle, cs.status,
|
|
414
|
-
COALESCE(cs.completed_at, cs.started_at) as updatedAt
|
|
415
|
-
FROM claude_sessions cs
|
|
416
|
-
LEFT JOIN work_items w ON cs.work_item_id = w.id
|
|
417
|
-
WHERE cs.status = 'active'
|
|
418
|
-
ORDER BY COALESCE(cs.completed_at, cs.started_at) DESC
|
|
419
|
-
`).all();
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
ipcMain.handle('db:createSession', (event, title) => {
|
|
423
|
-
const db = getDb();
|
|
424
|
-
const result = db.prepare(`
|
|
425
|
-
INSERT INTO claude_sessions (work_item_id, title, session_title, status, started_at)
|
|
426
|
-
VALUES (NULL, ?, ?, 'active', datetime('now'))
|
|
427
|
-
`).run(title, title);
|
|
428
|
-
return result.lastInsertRowid;
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
ipcMain.handle('db:getSession', (event, sessionId) => {
|
|
432
|
-
const db = getDb();
|
|
433
|
-
return db.prepare(`
|
|
434
|
-
SELECT id, work_item_id, title, session_title, status, started_at, completed_at
|
|
435
|
-
FROM claude_sessions WHERE id = ?
|
|
436
|
-
`).get(sessionId) || null;
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
ipcMain.handle('db:closeSession', (event, sessionId) => {
|
|
440
|
-
const db = getDb();
|
|
441
|
-
const result = db.prepare(`
|
|
442
|
-
UPDATE claude_sessions SET status = 'completed', completed_at = datetime('now') WHERE id = ?
|
|
443
|
-
`).run(sessionId);
|
|
444
|
-
return result.changes > 0;
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
ipcMain.handle('db:closeSessionByWorkItem', (event, workItemId) => {
|
|
448
|
-
const db = getDb();
|
|
449
|
-
const result = db.prepare(`
|
|
450
|
-
UPDATE claude_sessions SET status = 'completed', completed_at = datetime('now')
|
|
451
|
-
WHERE work_item_id = ? AND status = 'active'
|
|
452
|
-
`).run(workItemId);
|
|
453
|
-
return result.changes > 0;
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
ipcMain.handle('db:linkSession', (event, sessionId, workItemId) => {
|
|
457
|
-
const db = getDb();
|
|
458
|
-
const workItem = db.prepare(`SELECT title FROM work_items WHERE id = ?`).get(workItemId);
|
|
459
|
-
if (!workItem) return { success: false, error: 'Work item not found' };
|
|
460
|
-
|
|
461
|
-
const result = db.prepare(`
|
|
462
|
-
UPDATE claude_sessions SET work_item_id = ?, title = ?
|
|
463
|
-
WHERE id = ? AND work_item_id IS NULL
|
|
464
|
-
`).run(workItemId, workItem.title, sessionId);
|
|
465
|
-
|
|
466
|
-
if (result.changes === 0) {
|
|
467
|
-
return { success: false, error: 'Session already linked or not found' };
|
|
468
|
-
}
|
|
469
|
-
return { success: true };
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
ipcMain.handle('db:isLinkableWorkItem', (event, workItemId) => {
|
|
473
|
-
const db = getDb();
|
|
474
|
-
const workItem = db.prepare(`
|
|
475
|
-
SELECT w.id, w.type, w.parent_id, p.type as parent_type
|
|
476
|
-
FROM work_items w LEFT JOIN work_items p ON w.parent_id = p.id WHERE w.id = ?
|
|
477
|
-
`).get(workItemId);
|
|
478
|
-
|
|
479
|
-
if (!workItem) return { linkable: false, reason: 'Work item not found' };
|
|
480
|
-
if (workItem.type === 'epic') return { linkable: false, reason: 'Epics cannot have sessions' };
|
|
481
|
-
if (workItem.type === 'chore' && workItem.parent_type === 'feature') {
|
|
482
|
-
return { linkable: false, reason: "Chores under features use the feature's session", redirectToId: workItem.parent_id };
|
|
483
|
-
}
|
|
484
|
-
return { linkable: true };
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
ipcMain.handle('db:getActiveSessionByWorkItem', (event, workItemId) => {
|
|
488
|
-
const db = getDb();
|
|
489
|
-
return db.prepare(`
|
|
490
|
-
SELECT id, work_item_id, title, session_title, status, started_at, completed_at
|
|
491
|
-
FROM claude_sessions WHERE work_item_id = ? AND status = 'active'
|
|
492
|
-
`).get(workItemId) || null;
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
ipcMain.handle('db:getOrCreateSessionForWorkItem', (event, workItemId) => {
|
|
496
|
-
const db = getDb();
|
|
497
|
-
|
|
498
|
-
// Check if linkable
|
|
499
|
-
const workItem = db.prepare(`
|
|
500
|
-
SELECT w.id, w.type, w.parent_id, w.title, p.type as parent_type
|
|
501
|
-
FROM work_items w LEFT JOIN work_items p ON w.parent_id = p.id WHERE w.id = ?
|
|
502
|
-
`).get(workItemId);
|
|
503
|
-
|
|
504
|
-
if (!workItem) return { session: null, created: false, error: 'Work item not found' };
|
|
505
|
-
if (workItem.type === 'epic') return { session: null, created: false, error: 'Epics cannot have sessions' };
|
|
506
|
-
if (workItem.type === 'chore' && workItem.parent_type === 'feature') {
|
|
507
|
-
return { session: null, created: false, error: "Chores under features use the feature's session", redirectToId: workItem.parent_id };
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// Check for existing
|
|
511
|
-
const existing = db.prepare(`
|
|
512
|
-
SELECT id, work_item_id, title, session_title, status, started_at, completed_at
|
|
513
|
-
FROM claude_sessions WHERE work_item_id = ? AND status = 'active'
|
|
514
|
-
`).get(workItemId);
|
|
515
|
-
if (existing) return { session: existing, created: false };
|
|
516
|
-
|
|
517
|
-
// Create new
|
|
518
|
-
const result = db.prepare(`
|
|
519
|
-
INSERT INTO claude_sessions (work_item_id, title, status, started_at)
|
|
520
|
-
VALUES (?, ?, 'active', datetime('now'))
|
|
521
|
-
`).run(workItemId, workItem.title);
|
|
522
|
-
|
|
523
|
-
const newSession = db.prepare(`
|
|
524
|
-
SELECT id, work_item_id, title, session_title, status, started_at, completed_at
|
|
525
|
-
FROM claude_sessions WHERE id = ?
|
|
526
|
-
`).get(result.lastInsertRowid);
|
|
527
|
-
|
|
528
|
-
return { session: newSession, created: true };
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
ipcMain.handle('db:countActiveSessions', () => {
|
|
532
|
-
const db = getDb();
|
|
533
|
-
const result = db.prepare(`SELECT COUNT(*) as count FROM claude_sessions WHERE status = 'active'`).get();
|
|
534
|
-
return result.count;
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
ipcMain.handle('db:cleanupStaleSessions', (event, retentionDays = 30) => {
|
|
538
|
-
const db = getDb();
|
|
539
|
-
const result = db.prepare(`
|
|
540
|
-
DELETE FROM claude_sessions
|
|
541
|
-
WHERE status = 'orphaned'
|
|
542
|
-
OR (status = 'completed' AND completed_at < datetime('now', '-' || ? || ' days'))
|
|
543
|
-
OR (status = 'error' AND completed_at < datetime('now', '-' || ? || ' days'))
|
|
544
|
-
`).run(retentionDays, retentionDays);
|
|
545
|
-
return result.changes;
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
// ==================== Session Content ====================
|
|
549
|
-
ipcMain.handle('db:getSessionContent', (event, sessionId) => {
|
|
550
|
-
const db = getDb();
|
|
551
|
-
const session = db.prepare(`SELECT content FROM claude_sessions WHERE id = ?`).get(sessionId);
|
|
552
|
-
if (!session?.content) return [];
|
|
553
|
-
try {
|
|
554
|
-
return JSON.parse(session.content);
|
|
555
|
-
} catch {
|
|
556
|
-
return [];
|
|
557
|
-
}
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
ipcMain.handle('db:getSessionContentByWorkItem', (event, workItemId) => {
|
|
561
|
-
const db = getDb();
|
|
562
|
-
const session = db.prepare(`
|
|
563
|
-
SELECT content FROM claude_sessions WHERE work_item_id = ? AND status = 'active'
|
|
564
|
-
`).get(workItemId);
|
|
565
|
-
if (!session?.content) return [];
|
|
566
|
-
try {
|
|
567
|
-
return JSON.parse(session.content);
|
|
568
|
-
} catch {
|
|
569
|
-
return [];
|
|
570
|
-
}
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
ipcMain.handle('db:appendSessionContent', (event, sessionId, turn) => {
|
|
574
|
-
const db = getDb();
|
|
575
|
-
const result = db.prepare(`
|
|
576
|
-
UPDATE claude_sessions
|
|
577
|
-
SET content = json_insert(COALESCE(content, '[]'), '$[#]', json(?))
|
|
578
|
-
WHERE id = ?
|
|
579
|
-
`).run(JSON.stringify(turn), sessionId);
|
|
580
|
-
return result.changes > 0;
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
ipcMain.handle('db:appendSessionContentByWorkItem', (event, workItemId, turn) => {
|
|
584
|
-
const db = getDb();
|
|
585
|
-
const result = db.prepare(`
|
|
586
|
-
UPDATE claude_sessions
|
|
587
|
-
SET content = json_insert(COALESCE(content, '[]'), '$[#]', json(?))
|
|
588
|
-
WHERE work_item_id = ? AND status = 'active'
|
|
589
|
-
`).run(JSON.stringify(turn), workItemId);
|
|
590
|
-
return result.changes > 0;
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
// ==================== Environment Variables ====================
|
|
594
|
-
ipcMain.handle('db:getEnvVars', () => {
|
|
595
|
-
const config = readConfig();
|
|
596
|
-
const envVars = config.envVars || {};
|
|
597
|
-
return Object.entries(envVars).map(([name, value]) => ({ name, value }));
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
ipcMain.handle('db:setEnvVar', (event, name, value) => {
|
|
601
|
-
const config = readConfig();
|
|
602
|
-
const envVars = config.envVars || {};
|
|
603
|
-
envVars[name] = value;
|
|
604
|
-
config.envVars = envVars;
|
|
605
|
-
writeConfig(config);
|
|
606
|
-
return true;
|
|
607
|
-
});
|
|
608
|
-
|
|
609
|
-
ipcMain.handle('db:deleteEnvVar', (event, name) => {
|
|
610
|
-
const config = readConfig();
|
|
611
|
-
const envVars = config.envVars || {};
|
|
612
|
-
delete envVars[name];
|
|
613
|
-
config.envVars = envVars;
|
|
614
|
-
writeConfig(config);
|
|
615
|
-
return true;
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
// ==================== Project Info ====================
|
|
619
|
-
ipcMain.handle('db:getProjectName', () => {
|
|
620
|
-
const configPath = getConfigPath();
|
|
621
|
-
if (fs.existsSync(configPath)) {
|
|
622
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
623
|
-
return config.name || 'Untitled Project';
|
|
624
|
-
}
|
|
625
|
-
return 'Untitled Project';
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
// ==================== Claude Subprocess ====================
|
|
629
|
-
// Import Claude functions from main process
|
|
630
|
-
const { spawnClaude, writeToClaudeStdin, killClaude } = require('./main');
|
|
631
|
-
|
|
632
|
-
ipcMain.handle('claude:spawn', (event, sessionId, cwd) => {
|
|
633
|
-
return spawnClaude(sessionId, cwd);
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
ipcMain.handle('claude:write', (event, sessionId, text) => {
|
|
637
|
-
return writeToClaudeStdin(sessionId, text);
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
ipcMain.handle('claude:kill', (event, sessionId) => {
|
|
641
|
-
return killClaude(sessionId);
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
// ==================== Dev Server ====================
|
|
645
|
-
const { spawnDevServer, killDevServer, getDevServerStatus, listDevServers } = require('./main');
|
|
646
|
-
|
|
647
|
-
ipcMain.handle('devServer:spawn', (event, projectPath, command, port) => {
|
|
648
|
-
return spawnDevServer(projectPath, command, port);
|
|
649
|
-
});
|
|
650
|
-
|
|
651
|
-
ipcMain.handle('devServer:kill', (event, projectPath) => {
|
|
652
|
-
return killDevServer(projectPath);
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
ipcMain.handle('devServer:status', (event, projectPath) => {
|
|
656
|
-
return getDevServerStatus(projectPath);
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
ipcMain.handle('devServer:list', () => {
|
|
660
|
-
return listDevServers();
|
|
661
|
-
});
|
|
662
|
-
|
|
663
|
-
// ==================== New Project Dialog ====================
|
|
664
|
-
ipcMain.handle('dialog:newProject', async () => {
|
|
665
|
-
const mainWindow = BrowserWindow.getFocusedWindow();
|
|
666
|
-
|
|
667
|
-
const result = await dialog.showSaveDialog(mainWindow, {
|
|
668
|
-
title: 'Create New Project',
|
|
669
|
-
buttonLabel: 'Create',
|
|
670
|
-
nameFieldLabel: 'Project Name:',
|
|
671
|
-
showsTagField: false,
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
if (result.canceled || !result.filePath) {
|
|
675
|
-
return { success: false, canceled: true };
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
const projectPath = result.filePath;
|
|
679
|
-
|
|
680
|
-
// Create the directory
|
|
681
|
-
try {
|
|
682
|
-
fs.mkdirSync(projectPath, { recursive: true });
|
|
683
|
-
} catch (mkdirError) {
|
|
684
|
-
return {
|
|
685
|
-
success: false,
|
|
686
|
-
error: `Failed to create directory: ${mkdirError.message}`
|
|
687
|
-
};
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
// Initialize JettyPod in the new directory
|
|
691
|
-
try {
|
|
692
|
-
initializeJettypod(projectPath);
|
|
693
|
-
} catch (initError) {
|
|
694
|
-
return {
|
|
695
|
-
success: false,
|
|
696
|
-
error: `Failed to initialize JettyPod: ${initError.message}`
|
|
697
|
-
};
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
const { setProjectRoot } = require('./main');
|
|
701
|
-
await setProjectRoot(projectPath);
|
|
702
|
-
|
|
703
|
-
closeDb();
|
|
704
|
-
cachedDb = null;
|
|
705
|
-
projectRoot = projectPath;
|
|
706
|
-
|
|
707
|
-
addRecentProject(projectPath);
|
|
708
|
-
|
|
709
|
-
return { success: true, path: projectPath };
|
|
710
|
-
});
|
|
711
|
-
|
|
712
|
-
// ==================== Open Project Dialog ====================
|
|
713
|
-
ipcMain.handle('dialog:openProject', async () => {
|
|
714
|
-
const mainWindow = BrowserWindow.getFocusedWindow();
|
|
715
|
-
|
|
716
|
-
const result = await dialog.showOpenDialog(mainWindow, {
|
|
717
|
-
title: 'Select JettyPod Project',
|
|
718
|
-
properties: ['openDirectory'],
|
|
719
|
-
buttonLabel: 'Open Project'
|
|
720
|
-
});
|
|
721
|
-
|
|
722
|
-
if (result.canceled || result.filePaths.length === 0) {
|
|
723
|
-
return { success: false, canceled: true };
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
const selectedPath = result.filePaths[0];
|
|
727
|
-
const dbPath = path.join(selectedPath, '.jettypod', 'work.db');
|
|
728
|
-
|
|
729
|
-
// Auto-initialize JettyPod if not already set up
|
|
730
|
-
if (!fs.existsSync(dbPath)) {
|
|
731
|
-
try {
|
|
732
|
-
initializeJettypod(selectedPath);
|
|
733
|
-
} catch (initError) {
|
|
734
|
-
return {
|
|
735
|
-
success: false,
|
|
736
|
-
error: `Failed to initialize JettyPod: ${initError.message}`
|
|
737
|
-
};
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
// Update the project root (also persists as last-selected and sets env var)
|
|
742
|
-
// Await ensures Next.js dev server is synced before renderer reloads
|
|
743
|
-
const { setProjectRoot } = require('./main');
|
|
744
|
-
await setProjectRoot(selectedPath);
|
|
745
|
-
|
|
746
|
-
// Reset the cached database connection so it reconnects to the new project
|
|
747
|
-
closeDb();
|
|
748
|
-
cachedDb = null;
|
|
749
|
-
projectRoot = selectedPath;
|
|
750
|
-
|
|
751
|
-
// Add to recent projects list
|
|
752
|
-
addRecentProject(selectedPath);
|
|
753
|
-
|
|
754
|
-
return { success: true, path: selectedPath };
|
|
755
|
-
});
|
|
756
|
-
|
|
757
|
-
// ==================== Recent Projects ====================
|
|
758
|
-
ipcMain.handle('projects:getRecent', () => {
|
|
759
|
-
return readRecentProjects();
|
|
760
|
-
});
|
|
761
|
-
|
|
762
|
-
ipcMain.handle('projects:addRecent', (event, projectPath) => {
|
|
763
|
-
return addRecentProject(projectPath);
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
ipcMain.handle('projects:openRecent', async (event, projectPath) => {
|
|
767
|
-
const dbPath = path.join(projectPath, '.jettypod', 'work.db');
|
|
768
|
-
|
|
769
|
-
// Validate the project directory exists
|
|
770
|
-
if (!fs.existsSync(projectPath)) {
|
|
771
|
-
return {
|
|
772
|
-
success: false,
|
|
773
|
-
error: 'Project directory no longer exists.'
|
|
774
|
-
};
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
// Auto-initialize JettyPod if not already set up
|
|
778
|
-
if (!fs.existsSync(dbPath)) {
|
|
779
|
-
try {
|
|
780
|
-
initializeJettypod(projectPath);
|
|
781
|
-
} catch (initError) {
|
|
782
|
-
return {
|
|
783
|
-
success: false,
|
|
784
|
-
error: `Failed to initialize JettyPod: ${initError.message}`
|
|
785
|
-
};
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
// Update the project root (also persists as last-selected and sets env var)
|
|
790
|
-
// Await ensures Next.js dev server is synced before renderer reloads
|
|
791
|
-
const { setProjectRoot } = require('./main');
|
|
792
|
-
await setProjectRoot(projectPath);
|
|
793
|
-
|
|
794
|
-
// Reset the cached database connection so it reconnects to the new project
|
|
795
|
-
closeDb();
|
|
796
|
-
cachedDb = null;
|
|
797
|
-
projectRoot = projectPath;
|
|
798
|
-
|
|
799
|
-
// Update recent projects (move to top)
|
|
800
|
-
addRecentProject(projectPath);
|
|
801
|
-
|
|
802
|
-
return { success: true, path: projectPath };
|
|
803
|
-
});
|
|
804
|
-
|
|
805
|
-
// ==================== Claude Code Installation ====================
|
|
806
|
-
ipcMain.handle('claudeCode:install', async () => {
|
|
807
|
-
const { installClaudeCode } = require('./main');
|
|
808
|
-
return await installClaudeCode();
|
|
809
|
-
});
|
|
810
|
-
|
|
811
|
-
ipcMain.handle('claudeCode:isInstalled', () => {
|
|
812
|
-
const { getClaudeCodeInstalled } = require('./main');
|
|
813
|
-
return getClaudeCodeInstalled();
|
|
814
|
-
});
|
|
815
|
-
|
|
816
|
-
ipcMain.handle('claudeCode:update', async () => {
|
|
817
|
-
const { updateClaudeCode } = require('./main');
|
|
818
|
-
return await updateClaudeCode();
|
|
819
|
-
});
|
|
820
|
-
|
|
821
|
-
ipcMain.handle('claudeCode:isAuthenticated', () => {
|
|
822
|
-
const { checkClaudeCodeAuthenticated } = require('./main');
|
|
823
|
-
return checkClaudeCodeAuthenticated();
|
|
824
|
-
});
|
|
825
|
-
|
|
826
|
-
ipcMain.handle('claudeCode:login', async () => {
|
|
827
|
-
const { loginClaudeCode } = require('./main');
|
|
828
|
-
return await loginClaudeCode();
|
|
829
|
-
});
|
|
830
|
-
|
|
831
|
-
// ==================== Subscription Gating ====================
|
|
832
|
-
|
|
833
|
-
const UPDATE_SERVER_URL = 'https://jettypod-update-server.spangbaryn2.workers.dev';
|
|
834
|
-
|
|
835
|
-
ipcMain.handle('subscription:createCheckout', async (event, plan) => {
|
|
836
|
-
try {
|
|
837
|
-
// Include JWT auth header so checkout session gets user_id in metadata
|
|
838
|
-
const headers = { 'Content-Type': 'application/json' };
|
|
839
|
-
const authPath = path.join(app.getPath('userData'), 'auth.json');
|
|
840
|
-
if (fs.existsSync(authPath)) {
|
|
841
|
-
try {
|
|
842
|
-
const authData = JSON.parse(fs.readFileSync(authPath, 'utf-8'));
|
|
843
|
-
if (authData.token) {
|
|
844
|
-
headers['Authorization'] = `Bearer ${authData.token}`;
|
|
845
|
-
}
|
|
846
|
-
} catch {
|
|
847
|
-
// Continue without auth if read fails
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
const response = await fetch(`${UPDATE_SERVER_URL}/checkout/create-session`, {
|
|
852
|
-
method: 'POST',
|
|
853
|
-
headers,
|
|
854
|
-
body: JSON.stringify({ plan: plan || 'monthly' }),
|
|
855
|
-
});
|
|
856
|
-
|
|
857
|
-
if (!response.ok) {
|
|
858
|
-
return { success: false, error: 'Failed to create checkout session' };
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
const data = await response.json();
|
|
862
|
-
if (data.url) {
|
|
863
|
-
const { shell } = require('electron');
|
|
864
|
-
shell.openExternal(data.url);
|
|
865
|
-
return { success: true };
|
|
866
|
-
}
|
|
867
|
-
return { success: false, error: 'No checkout URL returned' };
|
|
868
|
-
} catch (error) {
|
|
869
|
-
return { success: false, error: `Checkout failed: ${error.message}` };
|
|
870
|
-
}
|
|
871
|
-
});
|
|
872
|
-
|
|
873
|
-
ipcMain.handle('billing:openCustomerPortal', async () => {
|
|
874
|
-
try {
|
|
875
|
-
const headers = { 'Content-Type': 'application/json' };
|
|
876
|
-
const authPath = path.join(app.getPath('userData'), 'auth.json');
|
|
877
|
-
if (fs.existsSync(authPath)) {
|
|
878
|
-
try {
|
|
879
|
-
const authData = JSON.parse(fs.readFileSync(authPath, 'utf-8'));
|
|
880
|
-
if (authData.token) {
|
|
881
|
-
headers['Authorization'] = `Bearer ${authData.token}`;
|
|
882
|
-
}
|
|
883
|
-
} catch {
|
|
884
|
-
// Continue without auth if read fails
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
const response = await fetch(`${UPDATE_SERVER_URL}/billing/customer-portal`, {
|
|
889
|
-
method: 'POST',
|
|
890
|
-
headers,
|
|
891
|
-
});
|
|
892
|
-
|
|
893
|
-
if (!response.ok) {
|
|
894
|
-
return { success: false, error: 'Failed to open billing portal' };
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
const data = await response.json();
|
|
898
|
-
if (data.url) {
|
|
899
|
-
shell.openExternal(data.url);
|
|
900
|
-
return { success: true };
|
|
901
|
-
}
|
|
902
|
-
return { success: false, error: 'No portal URL returned' };
|
|
903
|
-
} catch (error) {
|
|
904
|
-
return { success: false, error: `Billing portal failed: ${error.message}` };
|
|
905
|
-
}
|
|
906
|
-
});
|
|
907
|
-
|
|
908
|
-
ipcMain.handle('subscription:activate', async (event, customerId) => {
|
|
909
|
-
if (!customerId || !customerId.trim().startsWith('cus_')) {
|
|
910
|
-
return { success: false, error: 'Invalid customer ID. It should start with cus_' };
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
try {
|
|
914
|
-
// Validate against the update server
|
|
915
|
-
const response = await fetch(`${UPDATE_SERVER_URL}/subscription/validate`, {
|
|
916
|
-
method: 'POST',
|
|
917
|
-
headers: { 'Content-Type': 'application/json' },
|
|
918
|
-
body: JSON.stringify({ customerId: customerId.trim() }),
|
|
919
|
-
});
|
|
920
|
-
|
|
921
|
-
const result = await response.json();
|
|
922
|
-
if (!result.valid) {
|
|
923
|
-
return { success: false, error: result.error || 'No active subscription found' };
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
// Store the subscription
|
|
927
|
-
const subPath = path.join(app.getPath('userData'), 'subscription.json');
|
|
928
|
-
const subData = {
|
|
929
|
-
customerId: customerId.trim(),
|
|
930
|
-
activatedAt: new Date().toISOString(),
|
|
931
|
-
};
|
|
932
|
-
|
|
933
|
-
const dir = path.dirname(subPath);
|
|
934
|
-
if (!fs.existsSync(dir)) {
|
|
935
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
936
|
-
}
|
|
937
|
-
fs.writeFileSync(subPath, JSON.stringify(subData, null, 2));
|
|
938
|
-
return { success: true };
|
|
939
|
-
} catch (error) {
|
|
940
|
-
return { success: false, error: `Activation failed: ${error.message}` };
|
|
941
|
-
}
|
|
942
|
-
});
|
|
943
|
-
|
|
944
|
-
ipcMain.handle('subscription:getStatus', () => {
|
|
945
|
-
const subPath = path.join(app.getPath('userData'), 'subscription.json');
|
|
946
|
-
if (!fs.existsSync(subPath)) {
|
|
947
|
-
return { active: false };
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
try {
|
|
951
|
-
const data = JSON.parse(fs.readFileSync(subPath, 'utf-8'));
|
|
952
|
-
return { active: !!data.customerId, customerId: data.customerId, activatedAt: data.activatedAt };
|
|
953
|
-
} catch {
|
|
954
|
-
return { active: false };
|
|
955
|
-
}
|
|
956
|
-
});
|
|
957
|
-
|
|
958
|
-
// ==================== Auth (JWT-based) ====================
|
|
959
|
-
|
|
960
|
-
ipcMain.handle('auth:loginWithGoogle', () => {
|
|
961
|
-
const { shell } = require('electron');
|
|
962
|
-
shell.openExternal(`${UPDATE_SERVER_URL}/auth/google`);
|
|
963
|
-
return { success: true };
|
|
964
|
-
});
|
|
965
|
-
|
|
966
|
-
ipcMain.handle('auth:saveToken', (event, token, user) => {
|
|
967
|
-
const authPath = path.join(app.getPath('userData'), 'auth.json');
|
|
968
|
-
const dir = path.dirname(authPath);
|
|
969
|
-
if (!fs.existsSync(dir)) {
|
|
970
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
971
|
-
}
|
|
972
|
-
fs.writeFileSync(authPath, JSON.stringify({ token, user, savedAt: new Date().toISOString() }, null, 2));
|
|
973
|
-
return { success: true };
|
|
974
|
-
});
|
|
975
|
-
|
|
976
|
-
ipcMain.handle('auth:getStatus', () => {
|
|
977
|
-
const authPath = path.join(app.getPath('userData'), 'auth.json');
|
|
978
|
-
if (!fs.existsSync(authPath)) {
|
|
979
|
-
return { authenticated: false };
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
try {
|
|
983
|
-
const data = JSON.parse(fs.readFileSync(authPath, 'utf-8'));
|
|
984
|
-
return { authenticated: !!data.token, token: data.token, user: data.user };
|
|
985
|
-
} catch {
|
|
986
|
-
return { authenticated: false };
|
|
987
|
-
}
|
|
988
|
-
});
|
|
989
|
-
|
|
990
|
-
ipcMain.handle('auth:getToken', () => {
|
|
991
|
-
const authPath = path.join(app.getPath('userData'), 'auth.json');
|
|
992
|
-
if (!fs.existsSync(authPath)) return null;
|
|
993
|
-
|
|
994
|
-
try {
|
|
995
|
-
const data = JSON.parse(fs.readFileSync(authPath, 'utf-8'));
|
|
996
|
-
return data.token || null;
|
|
997
|
-
} catch {
|
|
998
|
-
return null;
|
|
999
|
-
}
|
|
1000
|
-
});
|
|
1001
|
-
|
|
1002
|
-
ipcMain.handle('auth:logout', () => {
|
|
1003
|
-
const authPath = path.join(app.getPath('userData'), 'auth.json');
|
|
1004
|
-
if (fs.existsSync(authPath)) {
|
|
1005
|
-
fs.unlinkSync(authPath);
|
|
1006
|
-
}
|
|
1007
|
-
return { success: true };
|
|
1008
|
-
});
|
|
1009
|
-
|
|
1010
|
-
// ==================== Shell Operations ====================
|
|
1011
|
-
ipcMain.handle('shell:openPath', async (event, filePath) => {
|
|
1012
|
-
// Use openExternal with file:// URL to open in default browser
|
|
1013
|
-
const fileUrl = `file://${filePath}`;
|
|
1014
|
-
return shell.openExternal(fileUrl);
|
|
1015
|
-
});
|
|
1016
|
-
|
|
1017
|
-
console.log('[IPC] All handlers registered');
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
// Close database connection (for graceful shutdown)
|
|
1021
|
-
function closeDb() {
|
|
1022
|
-
if (cachedDb) {
|
|
1023
|
-
cachedDb.close();
|
|
1024
|
-
cachedDb = null;
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
module.exports = { registerIpcHandlers, closeDb };
|