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,2124 +0,0 @@
|
|
|
1
|
-
const { app, BrowserWindow, Menu, dialog, shell } = require('electron');
|
|
2
|
-
const { spawn, execSync } = require('child_process');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const http = require('http');
|
|
6
|
-
const { WebSocketServer } = require('ws');
|
|
7
|
-
const { registerIpcHandlers, closeDb } = require('./ipc-handlers');
|
|
8
|
-
const { ingestCucumberResults, closeIngesterDb } = require('./test-results-ingester');
|
|
9
|
-
const { autoUpdater } = require('electron-updater');
|
|
10
|
-
const sessionManager = require('./session-manager');
|
|
11
|
-
|
|
12
|
-
// Track child processes and servers for cleanup
|
|
13
|
-
let nextProcess = null;
|
|
14
|
-
let nextServer = null; // HTTP server for production Next.js
|
|
15
|
-
let mainWindow = null;
|
|
16
|
-
const windows = new Set(); // Track all open windows
|
|
17
|
-
let wss = null;
|
|
18
|
-
let dbPollInterval = null;
|
|
19
|
-
let testResultsPollInterval = null;
|
|
20
|
-
let lastDbMtimes = { db: null, wal: null };
|
|
21
|
-
let lastTestResultsMtime = null;
|
|
22
|
-
|
|
23
|
-
// Track Claude processes by session ID
|
|
24
|
-
const claudeProcesses = new Map();
|
|
25
|
-
|
|
26
|
-
// Track dev server processes by project path
|
|
27
|
-
// Map: projectPath -> { process, port, command, status }
|
|
28
|
-
const devServerProcesses = new Map();
|
|
29
|
-
|
|
30
|
-
// Detect if running as packaged app
|
|
31
|
-
const isPackaged = app.isPackaged;
|
|
32
|
-
|
|
33
|
-
// Suppress noisy EGL GPU driver error logs (eglQueryDeviceAttribEXT: Bad attribute)
|
|
34
|
-
app.commandLine.appendSwitch('enable-logging', 'stderr');
|
|
35
|
-
app.commandLine.appendSwitch('log-level', '2');
|
|
36
|
-
app.commandLine.appendSwitch('use-angle', 'metal');
|
|
37
|
-
|
|
38
|
-
// Last-selected project persistence (for dev mode)
|
|
39
|
-
const LAST_SELECTED_PROJECT_PATH = path.join(
|
|
40
|
-
process.env.HOME || process.env.USERPROFILE || '',
|
|
41
|
-
'.config', 'JettyPod', 'last-selected-project.json'
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
function readLastSelectedProject() {
|
|
45
|
-
try {
|
|
46
|
-
if (fs.existsSync(LAST_SELECTED_PROJECT_PATH)) {
|
|
47
|
-
const data = JSON.parse(fs.readFileSync(LAST_SELECTED_PROJECT_PATH, 'utf-8'));
|
|
48
|
-
return data.projectPath || null;
|
|
49
|
-
}
|
|
50
|
-
} catch {
|
|
51
|
-
// Ignore read errors - treat as no project selected
|
|
52
|
-
}
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function writeLastSelectedProject(projectPath) {
|
|
57
|
-
const dir = path.dirname(LAST_SELECTED_PROJECT_PATH);
|
|
58
|
-
if (!fs.existsSync(dir)) {
|
|
59
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
60
|
-
}
|
|
61
|
-
fs.writeFileSync(LAST_SELECTED_PROJECT_PATH, JSON.stringify({ projectPath }));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Paths - different for development vs production
|
|
65
|
-
let dashboardDir;
|
|
66
|
-
let projectRoot;
|
|
67
|
-
let preloadPath;
|
|
68
|
-
|
|
69
|
-
if (isPackaged) {
|
|
70
|
-
// In packaged app, resources are in app.asar or app directory
|
|
71
|
-
// process.resourcesPath points to Contents/Resources
|
|
72
|
-
dashboardDir = path.join(process.resourcesPath, 'app');
|
|
73
|
-
// For packaged app, project root is the user's project (not the app bundle)
|
|
74
|
-
// We'll use an environment variable or default to null (no project)
|
|
75
|
-
projectRoot = process.env.JETTYPOD_PROJECT_PATH || null;
|
|
76
|
-
preloadPath = path.join(dashboardDir, 'electron', 'preload.js');
|
|
77
|
-
} else {
|
|
78
|
-
// Development mode - use last-selected project if available, otherwise show welcome
|
|
79
|
-
dashboardDir = path.join(__dirname, '..');
|
|
80
|
-
projectRoot = readLastSelectedProject();
|
|
81
|
-
if (projectRoot) {
|
|
82
|
-
process.env.JETTYPOD_PROJECT_PATH = projectRoot;
|
|
83
|
-
}
|
|
84
|
-
preloadPath = path.join(__dirname, 'preload.js');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Check if a valid JettyPod project exists at the given path
|
|
88
|
-
function hasValidProject(projectPath) {
|
|
89
|
-
if (!projectPath) return false;
|
|
90
|
-
const dbPath = path.join(projectPath, '.jettypod', 'work.db');
|
|
91
|
-
return fs.existsSync(dbPath);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Set the project root (called when user selects a project)
|
|
95
|
-
async function setProjectRoot(newPath) {
|
|
96
|
-
projectRoot = newPath;
|
|
97
|
-
process.env.JETTYPOD_PROJECT_PATH = newPath;
|
|
98
|
-
log(`Project root set to: ${projectRoot}`);
|
|
99
|
-
|
|
100
|
-
// Persist for next launch (dev mode remembers last project)
|
|
101
|
-
writeLastSelectedProject(newPath);
|
|
102
|
-
|
|
103
|
-
// Restart database and test results polling for the new project
|
|
104
|
-
restartPolling();
|
|
105
|
-
|
|
106
|
-
// In dev mode, Next.js runs as a child process with its own copy of process.env.
|
|
107
|
-
// Notify it of the project change so it can update its env and close cached DB connections.
|
|
108
|
-
if (!isPackaged && nextProcess) {
|
|
109
|
-
await syncProjectToNextServer(newPath);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Notify the Next.js dev server of a project path change
|
|
114
|
-
function syncProjectToNextServer(projectPath) {
|
|
115
|
-
return new Promise((resolve) => {
|
|
116
|
-
const payload = JSON.stringify({ projectPath: projectPath || '' });
|
|
117
|
-
const req = http.request({
|
|
118
|
-
hostname: 'localhost',
|
|
119
|
-
port: 3000,
|
|
120
|
-
path: '/api/internal/set-project',
|
|
121
|
-
method: 'POST',
|
|
122
|
-
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) }
|
|
123
|
-
}, (res) => {
|
|
124
|
-
if (res.statusCode === 200) {
|
|
125
|
-
log('Next.js dev server updated with new project path');
|
|
126
|
-
} else {
|
|
127
|
-
log(`Warning: Failed to update Next.js dev server (status ${res.statusCode})`);
|
|
128
|
-
}
|
|
129
|
-
res.resume(); // drain the response
|
|
130
|
-
resolve();
|
|
131
|
-
});
|
|
132
|
-
req.on('error', (err) => {
|
|
133
|
-
log(`Warning: Could not reach Next.js dev server to update project: ${err.message}`);
|
|
134
|
-
resolve(); // resolve anyway — don't block project switch if dev server is unreachable
|
|
135
|
-
});
|
|
136
|
-
req.write(payload);
|
|
137
|
-
req.end();
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Get the main window for reloading after project selection
|
|
142
|
-
function getMainWindow() {
|
|
143
|
-
return mainWindow;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// WebSocket configuration
|
|
147
|
-
const WS_PORT = 47808;
|
|
148
|
-
|
|
149
|
-
// Persistent log file for diagnostics
|
|
150
|
-
const LOG_DIR = path.join(app.getPath('home'), 'Library', 'Logs', 'jettypod');
|
|
151
|
-
const LOG_FILE = path.join(LOG_DIR, 'main.log');
|
|
152
|
-
const MAX_LOG_SIZE = 5 * 1024 * 1024; // 5MB
|
|
153
|
-
|
|
154
|
-
let logStream = null;
|
|
155
|
-
|
|
156
|
-
function initLogFile() {
|
|
157
|
-
try {
|
|
158
|
-
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
159
|
-
// Rotate if too large
|
|
160
|
-
if (fs.existsSync(LOG_FILE)) {
|
|
161
|
-
const stats = fs.statSync(LOG_FILE);
|
|
162
|
-
if (stats.size > MAX_LOG_SIZE) {
|
|
163
|
-
const rotated = `${LOG_FILE}.1`;
|
|
164
|
-
if (fs.existsSync(rotated)) fs.unlinkSync(rotated);
|
|
165
|
-
fs.renameSync(LOG_FILE, rotated);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
logStream = fs.createWriteStream(LOG_FILE, { flags: 'a' });
|
|
169
|
-
} catch {
|
|
170
|
-
// Fall back to console-only logging
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
initLogFile();
|
|
175
|
-
|
|
176
|
-
function log(msg) {
|
|
177
|
-
const line = `[${new Date().toISOString()}] [Electron] ${msg}`;
|
|
178
|
-
console.log(line);
|
|
179
|
-
if (logStream) {
|
|
180
|
-
logStream.write(line + '\n');
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Get path to bundled Node.js in the app resources.
|
|
186
|
-
* Returns null if not found (e.g., in dev mode).
|
|
187
|
-
*/
|
|
188
|
-
function getBundledNodePath() {
|
|
189
|
-
if (process.resourcesPath) {
|
|
190
|
-
const bundledPath = path.join(process.resourcesPath, 'node', 'bin');
|
|
191
|
-
if (fs.existsSync(path.join(bundledPath, 'node'))) {
|
|
192
|
-
return bundledPath;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Get path where Claude Code will be installed.
|
|
200
|
-
* Uses app-specific directory to avoid conflicts with system installs.
|
|
201
|
-
*/
|
|
202
|
-
function getClaudeInstallPath() {
|
|
203
|
-
const home = process.env.HOME || '';
|
|
204
|
-
return path.join(home, '.jettypod', 'node_modules', '.bin');
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Get shell environment with proper PATH for spawning CLI tools like npm/claude.
|
|
209
|
-
* GUI apps on macOS don't inherit the user's shell PATH, so we need to construct it.
|
|
210
|
-
* This includes common locations for npm, homebrew, nvm, and other tools.
|
|
211
|
-
*/
|
|
212
|
-
function getUserShell() {
|
|
213
|
-
return process.env.SHELL || '/bin/zsh';
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function getShellEnv() {
|
|
217
|
-
const home = process.env.HOME || '';
|
|
218
|
-
|
|
219
|
-
// Priority paths (checked first)
|
|
220
|
-
const priorityPaths = [];
|
|
221
|
-
|
|
222
|
-
// Add JettyPod's Claude install location (highest priority)
|
|
223
|
-
priorityPaths.push(getClaudeInstallPath());
|
|
224
|
-
|
|
225
|
-
// Add bundled Node.js (for running Claude)
|
|
226
|
-
const bundledNode = getBundledNodePath();
|
|
227
|
-
if (bundledNode) {
|
|
228
|
-
priorityPaths.push(bundledNode);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Common paths where npm/node might be installed (fallback for users who have it)
|
|
232
|
-
const additionalPaths = [
|
|
233
|
-
`${home}/.claude/local`, // Official Claude Code install script
|
|
234
|
-
'/usr/local/bin', // Homebrew (Intel Mac) / common location
|
|
235
|
-
'/opt/homebrew/bin', // Homebrew (Apple Silicon)
|
|
236
|
-
'/opt/homebrew/sbin',
|
|
237
|
-
`${home}/.nvm/current/bin`, // nvm with 'current' alias
|
|
238
|
-
`${home}/.volta/bin`, // Volta
|
|
239
|
-
`${home}/.npm-global/bin`, // npm global (user-configured)
|
|
240
|
-
`${home}/.local/bin`, // Common user bin
|
|
241
|
-
`${home}/bin`,
|
|
242
|
-
'/usr/bin',
|
|
243
|
-
'/bin',
|
|
244
|
-
'/usr/sbin',
|
|
245
|
-
'/sbin',
|
|
246
|
-
].filter(Boolean);
|
|
247
|
-
|
|
248
|
-
// Try to get active nvm node version if nvm is installed
|
|
249
|
-
const nvmDir = process.env.NVM_DIR || `${home}/.nvm`;
|
|
250
|
-
if (fs.existsSync(nvmDir)) {
|
|
251
|
-
// Check for .nvmrc or default version
|
|
252
|
-
const defaultPath = path.join(nvmDir, 'alias', 'default');
|
|
253
|
-
if (fs.existsSync(defaultPath)) {
|
|
254
|
-
try {
|
|
255
|
-
const defaultVersion = fs.readFileSync(defaultPath, 'utf-8').trim();
|
|
256
|
-
// Handle both direct versions and aliases like 'lts/*'
|
|
257
|
-
const versionDir = path.join(nvmDir, 'versions', 'node');
|
|
258
|
-
if (fs.existsSync(versionDir)) {
|
|
259
|
-
const versions = fs.readdirSync(versionDir).filter(v => v.startsWith('v'));
|
|
260
|
-
// Find matching version or latest
|
|
261
|
-
const match = versions.find(v => v.includes(defaultVersion)) || versions.sort().pop();
|
|
262
|
-
if (match) {
|
|
263
|
-
additionalPaths.unshift(path.join(versionDir, match, 'bin'));
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
} catch {
|
|
267
|
-
// Ignore errors reading nvm config
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const existingPath = process.env.PATH || '';
|
|
273
|
-
const newPath = [...priorityPaths, ...additionalPaths, existingPath].join(':');
|
|
274
|
-
|
|
275
|
-
return {
|
|
276
|
-
...process.env,
|
|
277
|
-
PATH: newPath,
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Clean up stale resources before starting servers
|
|
283
|
-
* This handles cases where the app was force-quit and left behind:
|
|
284
|
-
* - Next.js lock files
|
|
285
|
-
* - Orphaned processes on port 3000
|
|
286
|
-
* - Orphaned next-router-worker processes
|
|
287
|
-
*/
|
|
288
|
-
function cleanupStaleResources() {
|
|
289
|
-
log('Cleaning up stale resources...');
|
|
290
|
-
|
|
291
|
-
// 1. Remove Next.js lock file if it exists
|
|
292
|
-
const lockFilePath = path.join(dashboardDir, '.next', 'dev', 'lock');
|
|
293
|
-
if (fs.existsSync(lockFilePath)) {
|
|
294
|
-
try {
|
|
295
|
-
fs.unlinkSync(lockFilePath);
|
|
296
|
-
log(`Removed stale lock file: ${lockFilePath}`);
|
|
297
|
-
} catch (err) {
|
|
298
|
-
log(`Warning: Could not remove lock file: ${err.message}`);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// 2. Kill any process using port 3000 (Next.js dev server port)
|
|
303
|
-
try {
|
|
304
|
-
const pidsOnPort = execSync('lsof -ti:3000 2>/dev/null || true', { encoding: 'utf-8' }).trim();
|
|
305
|
-
if (pidsOnPort) {
|
|
306
|
-
const pids = pidsOnPort.split('\n').filter(Boolean);
|
|
307
|
-
for (const pid of pids) {
|
|
308
|
-
try {
|
|
309
|
-
process.kill(parseInt(pid, 10), 'SIGKILL');
|
|
310
|
-
log(`Killed stale process on port 3000: PID ${pid}`);
|
|
311
|
-
} catch (killErr) {
|
|
312
|
-
// Process may have already exited
|
|
313
|
-
log(`Could not kill PID ${pid}: ${killErr.message}`);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
} catch (err) {
|
|
318
|
-
// lsof command failed or no processes found - that's fine
|
|
319
|
-
log(`Port 3000 check: ${err.message || 'no stale processes'}`);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// 3. Kill orphaned next-router-worker processes
|
|
323
|
-
try {
|
|
324
|
-
const routerWorkers = execSync('pgrep -f "next-router-worker" 2>/dev/null || true', { encoding: 'utf-8' }).trim();
|
|
325
|
-
if (routerWorkers) {
|
|
326
|
-
const pids = routerWorkers.split('\n').filter(Boolean);
|
|
327
|
-
for (const pid of pids) {
|
|
328
|
-
try {
|
|
329
|
-
process.kill(parseInt(pid, 10), 'SIGKILL');
|
|
330
|
-
log(`Killed orphaned next-router-worker: PID ${pid}`);
|
|
331
|
-
} catch (killErr) {
|
|
332
|
-
log(`Could not kill router worker PID ${pid}: ${killErr.message}`);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
} catch (err) {
|
|
337
|
-
// No orphaned workers - that's fine
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// 4. Kill any orphaned node processes running our dev script
|
|
341
|
-
try {
|
|
342
|
-
const devProcesses = execSync(`pgrep -f "node.*next.*dev" 2>/dev/null || true`, { encoding: 'utf-8' }).trim();
|
|
343
|
-
if (devProcesses) {
|
|
344
|
-
const pids = devProcesses.split('\n').filter(Boolean);
|
|
345
|
-
for (const pid of pids) {
|
|
346
|
-
// Don't kill ourselves
|
|
347
|
-
if (parseInt(pid, 10) === process.pid) continue;
|
|
348
|
-
try {
|
|
349
|
-
process.kill(parseInt(pid, 10), 'SIGKILL');
|
|
350
|
-
log(`Killed orphaned Next.js dev process: PID ${pid}`);
|
|
351
|
-
} catch (killErr) {
|
|
352
|
-
log(`Could not kill dev process PID ${pid}: ${killErr.message}`);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
} catch (err) {
|
|
357
|
-
// No orphaned processes - that's fine
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
log('Stale resource cleanup complete');
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function startNextServer() {
|
|
364
|
-
return new Promise((resolve, reject) => {
|
|
365
|
-
if (isPackaged) {
|
|
366
|
-
// Production mode - run Next.js programmatically within Electron's Node
|
|
367
|
-
// (Can't spawn process.execPath as that's the Electron binary, not Node)
|
|
368
|
-
log('Starting Next.js production server (programmatic)...');
|
|
369
|
-
log(`Dashboard dir: ${dashboardDir}`);
|
|
370
|
-
|
|
371
|
-
// Set environment before requiring Next
|
|
372
|
-
process.env.NODE_ENV = 'production';
|
|
373
|
-
process.env.JETTYPOD_IS_PACKAGED = '1'; // Signal to Next.js this is packaged app
|
|
374
|
-
process.env.JETTYPOD_RESOURCES_PATH = process.resourcesPath; // For migrations to find bundled files
|
|
375
|
-
// Only set project path if we have one (don't set null which becomes string "null")
|
|
376
|
-
if (projectRoot) {
|
|
377
|
-
process.env.JETTYPOD_PROJECT_PATH = projectRoot;
|
|
378
|
-
}
|
|
379
|
-
// Set enhanced PATH so API routes can find claude CLI
|
|
380
|
-
process.env.PATH = getShellEnv().PATH;
|
|
381
|
-
|
|
382
|
-
// Change to dashboard dir so Next.js finds its config
|
|
383
|
-
const originalCwd = process.cwd();
|
|
384
|
-
process.chdir(dashboardDir);
|
|
385
|
-
|
|
386
|
-
try {
|
|
387
|
-
// Require Next.js from the packaged app's node_modules
|
|
388
|
-
const next = require(path.join(dashboardDir, 'node_modules', 'next'));
|
|
389
|
-
|
|
390
|
-
const nextApp = next({
|
|
391
|
-
dev: false,
|
|
392
|
-
dir: dashboardDir,
|
|
393
|
-
conf: {
|
|
394
|
-
distDir: '.next'
|
|
395
|
-
}
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
const handle = nextApp.getRequestHandler();
|
|
399
|
-
|
|
400
|
-
nextApp.prepare().then(() => {
|
|
401
|
-
nextServer = http.createServer((req, res) => {
|
|
402
|
-
handle(req, res);
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
nextServer.listen(3000, (err) => {
|
|
406
|
-
if (err) {
|
|
407
|
-
log(`Next.js server error: ${err.message}`);
|
|
408
|
-
reject(err);
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
log('Next.js production server ready on http://localhost:3000');
|
|
412
|
-
resolve();
|
|
413
|
-
});
|
|
414
|
-
}).catch((err) => {
|
|
415
|
-
log(`Next.js prepare error: ${err.message}`);
|
|
416
|
-
process.chdir(originalCwd);
|
|
417
|
-
reject(err);
|
|
418
|
-
});
|
|
419
|
-
} catch (err) {
|
|
420
|
-
log(`Next.js require error: ${err.message}`);
|
|
421
|
-
process.chdir(originalCwd);
|
|
422
|
-
reject(err);
|
|
423
|
-
}
|
|
424
|
-
} else {
|
|
425
|
-
// Development mode - run npm run dev as child process
|
|
426
|
-
log('Starting Next.js dev server...');
|
|
427
|
-
|
|
428
|
-
nextProcess = spawn('npm', ['run', 'dev'], {
|
|
429
|
-
cwd: dashboardDir,
|
|
430
|
-
shell: true,
|
|
431
|
-
detached: true, // Create process group for reliable cleanup
|
|
432
|
-
env: {
|
|
433
|
-
...process.env,
|
|
434
|
-
JETTYPOD_PROJECT_PATH: projectRoot
|
|
435
|
-
}
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
let resolved = false;
|
|
439
|
-
|
|
440
|
-
nextProcess.stdout.on('data', (data) => {
|
|
441
|
-
const text = data.toString();
|
|
442
|
-
process.stdout.write(`[Next.js] ${text}`);
|
|
443
|
-
|
|
444
|
-
// Detect when server is ready
|
|
445
|
-
if (!resolved && (text.includes('Ready in') || text.includes('localhost:3000') || text.includes('started server'))) {
|
|
446
|
-
resolved = true;
|
|
447
|
-
log('Next.js server ready');
|
|
448
|
-
resolve();
|
|
449
|
-
}
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
nextProcess.stderr.on('data', (data) => {
|
|
453
|
-
process.stderr.write(`[Next.js] ${data}`);
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
nextProcess.on('error', (err) => {
|
|
457
|
-
log(`Next.js spawn error: ${err.message}`);
|
|
458
|
-
if (!resolved) reject(err);
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
nextProcess.on('exit', (code) => {
|
|
462
|
-
log(`Next.js exited with code ${code}`);
|
|
463
|
-
nextProcess = null;
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
// Timeout fallback - assume ready after 10 seconds
|
|
467
|
-
setTimeout(() => {
|
|
468
|
-
if (!resolved) {
|
|
469
|
-
resolved = true;
|
|
470
|
-
log('Next.js server assumed ready (timeout)');
|
|
471
|
-
resolve();
|
|
472
|
-
}
|
|
473
|
-
}, 10000);
|
|
474
|
-
}
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// ==================== Embedded WebSocket Server ====================
|
|
479
|
-
// (Moved from apps/ws-server/server.js)
|
|
480
|
-
|
|
481
|
-
const wsClients = new Set();
|
|
482
|
-
const DB_POLL_MS = 50; // Poll database every 50ms (same as lib/db-watcher.js)
|
|
483
|
-
|
|
484
|
-
function broadcastToClients(message) {
|
|
485
|
-
const payload = JSON.stringify(message);
|
|
486
|
-
for (const client of wsClients) {
|
|
487
|
-
if (client.readyState === 1) { // WebSocket.OPEN
|
|
488
|
-
client.send(payload);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// Watch database file for changes using polling (WAL-aware)
|
|
494
|
-
// SQLite in WAL mode writes to the -wal file first, so we must watch both files
|
|
495
|
-
function startDatabasePolling() {
|
|
496
|
-
if (!projectRoot) {
|
|
497
|
-
log(`[WS] No project selected, skipping database watcher`);
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
const dbPath = path.join(projectRoot, '.jettypod', 'work.db');
|
|
502
|
-
|
|
503
|
-
if (!fs.existsSync(dbPath)) {
|
|
504
|
-
log(`[WS] Database not found at ${dbPath}, waiting...`);
|
|
505
|
-
setTimeout(startDatabasePolling, 5000);
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// Initialize last modification times
|
|
510
|
-
try {
|
|
511
|
-
const dbStats = fs.statSync(dbPath);
|
|
512
|
-
lastDbMtimes.db = dbStats.mtimeMs;
|
|
513
|
-
|
|
514
|
-
// Also check WAL file (SQLite in WAL mode writes here first)
|
|
515
|
-
const walPath = dbPath + '-wal';
|
|
516
|
-
if (fs.existsSync(walPath)) {
|
|
517
|
-
const walStats = fs.statSync(walPath);
|
|
518
|
-
lastDbMtimes.wal = walStats.mtimeMs;
|
|
519
|
-
}
|
|
520
|
-
} catch (err) {
|
|
521
|
-
log(`[WS] Failed to get initial db stats: ${err.message}`);
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
log(`[WS] Polling database for changes (WAL-aware)...`);
|
|
526
|
-
|
|
527
|
-
// Use polling instead of fs.watch - more reliable for SQLite WAL mode
|
|
528
|
-
dbPollInterval = setInterval(() => {
|
|
529
|
-
try {
|
|
530
|
-
let changed = false;
|
|
531
|
-
|
|
532
|
-
// Check main db file
|
|
533
|
-
const dbStats = fs.statSync(dbPath);
|
|
534
|
-
if (dbStats.mtimeMs !== lastDbMtimes.db) {
|
|
535
|
-
lastDbMtimes.db = dbStats.mtimeMs;
|
|
536
|
-
changed = true;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// Check WAL file (where SQLite writes first in WAL mode)
|
|
540
|
-
const walPath = dbPath + '-wal';
|
|
541
|
-
if (fs.existsSync(walPath)) {
|
|
542
|
-
const walStats = fs.statSync(walPath);
|
|
543
|
-
if (walStats.mtimeMs !== lastDbMtimes.wal) {
|
|
544
|
-
lastDbMtimes.wal = walStats.mtimeMs;
|
|
545
|
-
changed = true;
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
if (changed) {
|
|
550
|
-
log(`[WS] Database changed`);
|
|
551
|
-
broadcastToClients({
|
|
552
|
-
type: 'db_change',
|
|
553
|
-
timestamp: Date.now()
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
} catch {
|
|
557
|
-
// File might be temporarily locked during writes - ignore
|
|
558
|
-
}
|
|
559
|
-
}, DB_POLL_MS);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// Watch for test results file changes (cucumber-results.json)
|
|
563
|
-
function startTestResultsPolling() {
|
|
564
|
-
if (!projectRoot) {
|
|
565
|
-
log(`[WS] No project selected, skipping test results watcher`);
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
const testResultsPath = path.join(projectRoot, 'cucumber-results.json');
|
|
570
|
-
|
|
571
|
-
// Initialize last modification time if file exists
|
|
572
|
-
if (fs.existsSync(testResultsPath)) {
|
|
573
|
-
try {
|
|
574
|
-
const stats = fs.statSync(testResultsPath);
|
|
575
|
-
lastTestResultsMtime = stats.mtimeMs;
|
|
576
|
-
log(`[WS] Watching test results at ${testResultsPath}`);
|
|
577
|
-
} catch (err) {
|
|
578
|
-
log(`[WS] Failed to get initial test results stats: ${err.message}`);
|
|
579
|
-
}
|
|
580
|
-
} else {
|
|
581
|
-
log(`[WS] Test results file not found at ${testResultsPath}, will watch for creation`);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// Poll for test results changes
|
|
585
|
-
testResultsPollInterval = setInterval(() => {
|
|
586
|
-
try {
|
|
587
|
-
if (!fs.existsSync(testResultsPath)) {
|
|
588
|
-
// File doesn't exist yet - reset mtime so we detect when it's created
|
|
589
|
-
if (lastTestResultsMtime !== null) {
|
|
590
|
-
lastTestResultsMtime = null;
|
|
591
|
-
}
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
const stats = fs.statSync(testResultsPath);
|
|
596
|
-
if (stats.mtimeMs !== lastTestResultsMtime) {
|
|
597
|
-
lastTestResultsMtime = stats.mtimeMs;
|
|
598
|
-
log(`[WS] Test results changed`);
|
|
599
|
-
|
|
600
|
-
// Ingest results into SQLite before broadcasting
|
|
601
|
-
try {
|
|
602
|
-
const count = ingestCucumberResults(testResultsPath);
|
|
603
|
-
if (count > 0) {
|
|
604
|
-
log(`[WS] Ingested ${count} test results into database`);
|
|
605
|
-
}
|
|
606
|
-
} catch (err) {
|
|
607
|
-
log(`[WS] Failed to ingest test results: ${err.message}`);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
broadcastToClients({
|
|
611
|
-
type: 'test_change',
|
|
612
|
-
timestamp: Date.now()
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
|
-
} catch {
|
|
616
|
-
// File might be temporarily locked during writes - ignore
|
|
617
|
-
}
|
|
618
|
-
}, DB_POLL_MS);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// Stop and restart polling for the current projectRoot.
|
|
622
|
-
// Called when the user selects/switches a project after app startup.
|
|
623
|
-
function restartPolling() {
|
|
624
|
-
// Stop existing polling
|
|
625
|
-
if (dbPollInterval) {
|
|
626
|
-
clearInterval(dbPollInterval);
|
|
627
|
-
dbPollInterval = null;
|
|
628
|
-
lastDbMtimes = { db: null, wal: null };
|
|
629
|
-
}
|
|
630
|
-
if (testResultsPollInterval) {
|
|
631
|
-
clearInterval(testResultsPollInterval);
|
|
632
|
-
testResultsPollInterval = null;
|
|
633
|
-
lastTestResultsMtime = null;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
// Start polling for the new project
|
|
637
|
-
startDatabasePolling();
|
|
638
|
-
startTestResultsPolling();
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
function startEmbeddedWebSocketServer() {
|
|
642
|
-
return new Promise((resolve, reject) => {
|
|
643
|
-
log('Starting embedded WebSocket server...');
|
|
644
|
-
|
|
645
|
-
try {
|
|
646
|
-
wss = new WebSocketServer({ port: WS_PORT });
|
|
647
|
-
|
|
648
|
-
wss.on('connection', (ws) => {
|
|
649
|
-
wsClients.add(ws);
|
|
650
|
-
log(`[WS] Client connected. Total clients: ${wsClients.size}`);
|
|
651
|
-
|
|
652
|
-
// Send initial connection confirmation
|
|
653
|
-
ws.send(JSON.stringify({ type: 'connected', timestamp: Date.now() }));
|
|
654
|
-
|
|
655
|
-
ws.on('close', () => {
|
|
656
|
-
wsClients.delete(ws);
|
|
657
|
-
log(`[WS] Client disconnected. Total clients: ${wsClients.size}`);
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
ws.on('error', (error) => {
|
|
661
|
-
log(`[WS] WebSocket error: ${error.message}`);
|
|
662
|
-
wsClients.delete(ws);
|
|
663
|
-
});
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
wss.on('error', (err) => {
|
|
667
|
-
log(`[WS] Server error: ${err.message}`);
|
|
668
|
-
reject(err);
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
startDatabasePolling();
|
|
672
|
-
startTestResultsPolling();
|
|
673
|
-
|
|
674
|
-
log(`[WS] WebSocket server running on ws://localhost:${WS_PORT}`);
|
|
675
|
-
resolve();
|
|
676
|
-
} catch (err) {
|
|
677
|
-
log(`[WS] Failed to start WebSocket server: ${err.message}`);
|
|
678
|
-
reject(err);
|
|
679
|
-
}
|
|
680
|
-
});
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
function stopWebSocketServer() {
|
|
684
|
-
if (dbPollInterval) {
|
|
685
|
-
clearInterval(dbPollInterval);
|
|
686
|
-
dbPollInterval = null;
|
|
687
|
-
lastDbMtimes = { db: null, wal: null };
|
|
688
|
-
log('[WS] Database polling stopped');
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
if (testResultsPollInterval) {
|
|
692
|
-
clearInterval(testResultsPollInterval);
|
|
693
|
-
testResultsPollInterval = null;
|
|
694
|
-
lastTestResultsMtime = null;
|
|
695
|
-
log('[WS] Test results polling stopped');
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
if (wss) {
|
|
699
|
-
// Close all client connections
|
|
700
|
-
for (const client of wsClients) {
|
|
701
|
-
client.close();
|
|
702
|
-
}
|
|
703
|
-
wsClients.clear();
|
|
704
|
-
|
|
705
|
-
wss.close(() => {
|
|
706
|
-
log('[WS] WebSocket server closed');
|
|
707
|
-
});
|
|
708
|
-
wss = null;
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// ==================== Claude Subprocess Management ====================
|
|
713
|
-
|
|
714
|
-
function spawnClaude(sessionId, cwd) {
|
|
715
|
-
// Kill existing process for this session if any
|
|
716
|
-
if (claudeProcesses.has(sessionId)) {
|
|
717
|
-
killClaude(sessionId);
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
log(`[Claude] Spawning for session ${sessionId} in ${cwd}`);
|
|
721
|
-
|
|
722
|
-
const claudeProcess = spawn(getUserShell(), ['-lc', 'claude'], {
|
|
723
|
-
cwd: cwd || projectRoot,
|
|
724
|
-
env: {
|
|
725
|
-
...getShellEnv(),
|
|
726
|
-
JETTYPOD_PROJECT_PATH: projectRoot
|
|
727
|
-
}
|
|
728
|
-
});
|
|
729
|
-
|
|
730
|
-
claudeProcesses.set(sessionId, claudeProcess);
|
|
731
|
-
|
|
732
|
-
claudeProcess.stdout.on('data', (data) => {
|
|
733
|
-
const text = data.toString();
|
|
734
|
-
log(`[Claude:${sessionId}] stdout: ${text.substring(0, 100)}...`);
|
|
735
|
-
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
736
|
-
mainWindow.webContents.send('claude:output', {
|
|
737
|
-
sessionId,
|
|
738
|
-
type: 'stdout',
|
|
739
|
-
data: text
|
|
740
|
-
});
|
|
741
|
-
}
|
|
742
|
-
});
|
|
743
|
-
|
|
744
|
-
claudeProcess.stderr.on('data', (data) => {
|
|
745
|
-
const text = data.toString();
|
|
746
|
-
log(`[Claude:${sessionId}] stderr: ${text.substring(0, 100)}...`);
|
|
747
|
-
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
748
|
-
mainWindow.webContents.send('claude:output', {
|
|
749
|
-
sessionId,
|
|
750
|
-
type: 'stderr',
|
|
751
|
-
data: text
|
|
752
|
-
});
|
|
753
|
-
}
|
|
754
|
-
});
|
|
755
|
-
|
|
756
|
-
claudeProcess.on('error', (err) => {
|
|
757
|
-
log(`[Claude:${sessionId}] spawn error: ${err.message}`);
|
|
758
|
-
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
759
|
-
mainWindow.webContents.send('claude:output', {
|
|
760
|
-
sessionId,
|
|
761
|
-
type: 'error',
|
|
762
|
-
data: err.message
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
|
-
claudeProcesses.delete(sessionId);
|
|
766
|
-
});
|
|
767
|
-
|
|
768
|
-
claudeProcess.on('exit', (code, signal) => {
|
|
769
|
-
log(`[Claude:${sessionId}] exited with code ${code}, signal ${signal}`);
|
|
770
|
-
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
771
|
-
mainWindow.webContents.send('claude:output', {
|
|
772
|
-
sessionId,
|
|
773
|
-
type: 'exit',
|
|
774
|
-
code,
|
|
775
|
-
signal
|
|
776
|
-
});
|
|
777
|
-
}
|
|
778
|
-
claudeProcesses.delete(sessionId);
|
|
779
|
-
});
|
|
780
|
-
|
|
781
|
-
return { success: true, sessionId };
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
function writeToClaudeStdin(sessionId, text) {
|
|
785
|
-
const claudeProcess = claudeProcesses.get(sessionId);
|
|
786
|
-
if (!claudeProcess) {
|
|
787
|
-
log(`[Claude:${sessionId}] No process found for write`);
|
|
788
|
-
return { success: false, error: 'No process found' };
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
log(`[Claude:${sessionId}] Writing to stdin: ${text.substring(0, 50)}...`);
|
|
792
|
-
claudeProcess.stdin.write(text);
|
|
793
|
-
return { success: true };
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
function killClaude(sessionId) {
|
|
797
|
-
const claudeProcess = claudeProcesses.get(sessionId);
|
|
798
|
-
if (!claudeProcess) {
|
|
799
|
-
log(`[Claude:${sessionId}] No process found to kill`);
|
|
800
|
-
return { success: false, error: 'No process found' };
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
log(`[Claude:${sessionId}] Killing process`);
|
|
804
|
-
claudeProcess.kill('SIGTERM');
|
|
805
|
-
claudeProcesses.delete(sessionId);
|
|
806
|
-
return { success: true };
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
function killAllClaudeProcesses() {
|
|
810
|
-
log(`[Claude] Killing all ${claudeProcesses.size} processes`);
|
|
811
|
-
for (const [sessionId, process] of claudeProcesses) {
|
|
812
|
-
log(`[Claude] Killing session ${sessionId}`);
|
|
813
|
-
process.kill('SIGTERM');
|
|
814
|
-
}
|
|
815
|
-
claudeProcesses.clear();
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
// ==================== Dev Server Management ====================
|
|
819
|
-
|
|
820
|
-
function spawnDevServer(projectPath, command = 'npm run dev', port = 3001) {
|
|
821
|
-
// Kill existing server for this project if any
|
|
822
|
-
if (devServerProcesses.has(projectPath)) {
|
|
823
|
-
killDevServer(projectPath);
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
log(`[DevServer] Spawning for ${projectPath} on port ${port}`);
|
|
827
|
-
log(`[DevServer] Command: ${command}`);
|
|
828
|
-
|
|
829
|
-
// Parse command into command and args
|
|
830
|
-
const [cmd, ...args] = command.split(' ');
|
|
831
|
-
|
|
832
|
-
const devProcess = spawn(cmd, args, {
|
|
833
|
-
cwd: projectPath,
|
|
834
|
-
shell: true,
|
|
835
|
-
env: {
|
|
836
|
-
...process.env,
|
|
837
|
-
PORT: String(port),
|
|
838
|
-
JETTYPOD_PROJECT_PATH: projectRoot
|
|
839
|
-
}
|
|
840
|
-
});
|
|
841
|
-
|
|
842
|
-
const serverInfo = {
|
|
843
|
-
process: devProcess,
|
|
844
|
-
port,
|
|
845
|
-
command,
|
|
846
|
-
status: 'starting',
|
|
847
|
-
startedAt: Date.now()
|
|
848
|
-
};
|
|
849
|
-
|
|
850
|
-
devServerProcesses.set(projectPath, serverInfo);
|
|
851
|
-
|
|
852
|
-
devProcess.stdout.on('data', (data) => {
|
|
853
|
-
const text = data.toString();
|
|
854
|
-
log(`[DevServer:${port}] stdout: ${text.substring(0, 100)}...`);
|
|
855
|
-
|
|
856
|
-
// Detect common "ready" signals
|
|
857
|
-
if (serverInfo.status === 'starting') {
|
|
858
|
-
const readySignals = ['Ready', 'Listening', 'started', 'running on', 'localhost:'];
|
|
859
|
-
if (readySignals.some(signal => text.toLowerCase().includes(signal.toLowerCase()))) {
|
|
860
|
-
serverInfo.status = 'running';
|
|
861
|
-
log(`[DevServer:${port}] Server is ready`);
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
866
|
-
mainWindow.webContents.send('devServer:output', {
|
|
867
|
-
projectPath,
|
|
868
|
-
port,
|
|
869
|
-
type: 'stdout',
|
|
870
|
-
data: text
|
|
871
|
-
});
|
|
872
|
-
}
|
|
873
|
-
});
|
|
874
|
-
|
|
875
|
-
devProcess.stderr.on('data', (data) => {
|
|
876
|
-
const text = data.toString();
|
|
877
|
-
log(`[DevServer:${port}] stderr: ${text.substring(0, 100)}...`);
|
|
878
|
-
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
879
|
-
mainWindow.webContents.send('devServer:output', {
|
|
880
|
-
projectPath,
|
|
881
|
-
port,
|
|
882
|
-
type: 'stderr',
|
|
883
|
-
data: text
|
|
884
|
-
});
|
|
885
|
-
}
|
|
886
|
-
});
|
|
887
|
-
|
|
888
|
-
devProcess.on('error', (err) => {
|
|
889
|
-
log(`[DevServer:${port}] spawn error: ${err.message}`);
|
|
890
|
-
serverInfo.status = 'error';
|
|
891
|
-
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
892
|
-
mainWindow.webContents.send('devServer:output', {
|
|
893
|
-
projectPath,
|
|
894
|
-
port,
|
|
895
|
-
type: 'error',
|
|
896
|
-
data: err.message
|
|
897
|
-
});
|
|
898
|
-
}
|
|
899
|
-
devServerProcesses.delete(projectPath);
|
|
900
|
-
});
|
|
901
|
-
|
|
902
|
-
devProcess.on('exit', (code, signal) => {
|
|
903
|
-
log(`[DevServer:${port}] exited with code ${code}, signal ${signal}`);
|
|
904
|
-
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
905
|
-
mainWindow.webContents.send('devServer:output', {
|
|
906
|
-
projectPath,
|
|
907
|
-
port,
|
|
908
|
-
type: 'exit',
|
|
909
|
-
code,
|
|
910
|
-
signal
|
|
911
|
-
});
|
|
912
|
-
}
|
|
913
|
-
devServerProcesses.delete(projectPath);
|
|
914
|
-
});
|
|
915
|
-
|
|
916
|
-
return { success: true, projectPath, port };
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
function killDevServer(projectPath) {
|
|
920
|
-
const serverInfo = devServerProcesses.get(projectPath);
|
|
921
|
-
if (!serverInfo) {
|
|
922
|
-
log(`[DevServer] No server found for ${projectPath}`);
|
|
923
|
-
return { success: false, error: 'No server found' };
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
log(`[DevServer] Killing server for ${projectPath} on port ${serverInfo.port}`);
|
|
927
|
-
serverInfo.process.kill('SIGTERM');
|
|
928
|
-
devServerProcesses.delete(projectPath);
|
|
929
|
-
return { success: true };
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
function getDevServerStatus(projectPath) {
|
|
933
|
-
const serverInfo = devServerProcesses.get(projectPath);
|
|
934
|
-
if (!serverInfo) {
|
|
935
|
-
return { running: false };
|
|
936
|
-
}
|
|
937
|
-
return {
|
|
938
|
-
running: true,
|
|
939
|
-
port: serverInfo.port,
|
|
940
|
-
command: serverInfo.command,
|
|
941
|
-
status: serverInfo.status,
|
|
942
|
-
startedAt: serverInfo.startedAt
|
|
943
|
-
};
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
function listDevServers() {
|
|
947
|
-
const servers = [];
|
|
948
|
-
for (const [projectPath, serverInfo] of devServerProcesses) {
|
|
949
|
-
servers.push({
|
|
950
|
-
projectPath,
|
|
951
|
-
port: serverInfo.port,
|
|
952
|
-
command: serverInfo.command,
|
|
953
|
-
status: serverInfo.status,
|
|
954
|
-
startedAt: serverInfo.startedAt
|
|
955
|
-
});
|
|
956
|
-
}
|
|
957
|
-
return servers;
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
function killAllDevServers() {
|
|
961
|
-
log(`[DevServer] Killing all ${devServerProcesses.size} servers`);
|
|
962
|
-
for (const [projectPath, serverInfo] of devServerProcesses) {
|
|
963
|
-
log(`[DevServer] Killing server for ${projectPath}`);
|
|
964
|
-
serverInfo.process.kill('SIGTERM');
|
|
965
|
-
}
|
|
966
|
-
devServerProcesses.clear();
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
// Export for IPC handlers
|
|
970
|
-
module.exports = {
|
|
971
|
-
spawnClaude,
|
|
972
|
-
writeToClaudeStdin,
|
|
973
|
-
killClaude,
|
|
974
|
-
getClaudeProcesses: () => claudeProcesses,
|
|
975
|
-
spawnDevServer,
|
|
976
|
-
killDevServer,
|
|
977
|
-
getDevServerStatus,
|
|
978
|
-
listDevServers,
|
|
979
|
-
setProjectRoot,
|
|
980
|
-
getMainWindow,
|
|
981
|
-
writeLastSelectedProject,
|
|
982
|
-
installClaudeCode,
|
|
983
|
-
updateClaudeCode,
|
|
984
|
-
getClaudeCodeInstalled,
|
|
985
|
-
getClaudeCodeAuthenticated,
|
|
986
|
-
checkClaudeCodeAuthenticated,
|
|
987
|
-
loginClaudeCode
|
|
988
|
-
};
|
|
989
|
-
|
|
990
|
-
// ==================== Application Menu ====================
|
|
991
|
-
|
|
992
|
-
function buildApplicationMenu() {
|
|
993
|
-
const isMac = process.platform === 'darwin';
|
|
994
|
-
|
|
995
|
-
const template = [
|
|
996
|
-
// App menu (macOS only)
|
|
997
|
-
...(isMac ? [{
|
|
998
|
-
label: app.name,
|
|
999
|
-
submenu: [
|
|
1000
|
-
{ role: 'about' },
|
|
1001
|
-
{ type: 'separator' },
|
|
1002
|
-
{ role: 'services' },
|
|
1003
|
-
{ type: 'separator' },
|
|
1004
|
-
{ role: 'hide' },
|
|
1005
|
-
{ role: 'hideOthers' },
|
|
1006
|
-
{ role: 'unhide' },
|
|
1007
|
-
{ type: 'separator' },
|
|
1008
|
-
{ role: 'quit' }
|
|
1009
|
-
]
|
|
1010
|
-
}] : []),
|
|
1011
|
-
// File menu
|
|
1012
|
-
{
|
|
1013
|
-
label: 'File',
|
|
1014
|
-
submenu: [
|
|
1015
|
-
{
|
|
1016
|
-
label: 'New Window',
|
|
1017
|
-
accelerator: 'CmdOrCtrl+Shift+N',
|
|
1018
|
-
click: () => {
|
|
1019
|
-
createNewWindow();
|
|
1020
|
-
}
|
|
1021
|
-
},
|
|
1022
|
-
{
|
|
1023
|
-
label: 'Open Folder...',
|
|
1024
|
-
accelerator: 'CmdOrCtrl+O',
|
|
1025
|
-
click: async () => {
|
|
1026
|
-
await openFolder();
|
|
1027
|
-
}
|
|
1028
|
-
},
|
|
1029
|
-
{
|
|
1030
|
-
label: 'Switch Project...',
|
|
1031
|
-
accelerator: 'CmdOrCtrl+Shift+O',
|
|
1032
|
-
click: () => {
|
|
1033
|
-
if (mainWindow) {
|
|
1034
|
-
projectRoot = null;
|
|
1035
|
-
process.env.JETTYPOD_PROJECT_PATH = '';
|
|
1036
|
-
// Sync the cleared project to Next.js dev server
|
|
1037
|
-
if (!isPackaged && nextProcess) {
|
|
1038
|
-
syncProjectToNextServer('');
|
|
1039
|
-
}
|
|
1040
|
-
mainWindow.loadURL('http://localhost:3000/welcome');
|
|
1041
|
-
log('Navigated to project selection screen');
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
},
|
|
1045
|
-
{ type: 'separator' },
|
|
1046
|
-
isMac ? { role: 'close' } : { role: 'quit' }
|
|
1047
|
-
]
|
|
1048
|
-
},
|
|
1049
|
-
// Edit menu
|
|
1050
|
-
{
|
|
1051
|
-
label: 'Edit',
|
|
1052
|
-
submenu: [
|
|
1053
|
-
{ role: 'undo' },
|
|
1054
|
-
{ role: 'redo' },
|
|
1055
|
-
{ type: 'separator' },
|
|
1056
|
-
{ role: 'cut' },
|
|
1057
|
-
{ role: 'copy' },
|
|
1058
|
-
{ role: 'paste' },
|
|
1059
|
-
...(isMac ? [
|
|
1060
|
-
{ role: 'pasteAndMatchStyle' },
|
|
1061
|
-
{ role: 'delete' },
|
|
1062
|
-
{ role: 'selectAll' },
|
|
1063
|
-
] : [
|
|
1064
|
-
{ role: 'delete' },
|
|
1065
|
-
{ type: 'separator' },
|
|
1066
|
-
{ role: 'selectAll' }
|
|
1067
|
-
])
|
|
1068
|
-
]
|
|
1069
|
-
},
|
|
1070
|
-
// View menu
|
|
1071
|
-
{
|
|
1072
|
-
label: 'View',
|
|
1073
|
-
submenu: [
|
|
1074
|
-
{ role: 'reload' },
|
|
1075
|
-
{ role: 'forceReload' },
|
|
1076
|
-
{ role: 'toggleDevTools' },
|
|
1077
|
-
{ type: 'separator' },
|
|
1078
|
-
{ role: 'resetZoom' },
|
|
1079
|
-
{ role: 'zoomIn' },
|
|
1080
|
-
{ role: 'zoomOut' },
|
|
1081
|
-
{ type: 'separator' },
|
|
1082
|
-
{ role: 'togglefullscreen' }
|
|
1083
|
-
]
|
|
1084
|
-
},
|
|
1085
|
-
// Window menu
|
|
1086
|
-
{
|
|
1087
|
-
label: 'Window',
|
|
1088
|
-
submenu: [
|
|
1089
|
-
{ role: 'minimize' },
|
|
1090
|
-
{ role: 'zoom' },
|
|
1091
|
-
...(isMac ? [
|
|
1092
|
-
{ type: 'separator' },
|
|
1093
|
-
{ role: 'front' },
|
|
1094
|
-
{ type: 'separator' },
|
|
1095
|
-
{ role: 'window' }
|
|
1096
|
-
] : [
|
|
1097
|
-
{ role: 'close' }
|
|
1098
|
-
])
|
|
1099
|
-
]
|
|
1100
|
-
},
|
|
1101
|
-
// Help menu
|
|
1102
|
-
{
|
|
1103
|
-
label: 'Help',
|
|
1104
|
-
submenu: [
|
|
1105
|
-
{
|
|
1106
|
-
label: 'Check for Updates',
|
|
1107
|
-
click: async () => {
|
|
1108
|
-
const headers = sessionManager.getUpdaterHeaders();
|
|
1109
|
-
if (!headers.Authorization) {
|
|
1110
|
-
dialog.showMessageBox({
|
|
1111
|
-
type: 'warning',
|
|
1112
|
-
title: 'Update Check Unavailable',
|
|
1113
|
-
message: 'Not signed in.',
|
|
1114
|
-
detail: 'Please sign in to check for updates.',
|
|
1115
|
-
buttons: ['OK']
|
|
1116
|
-
});
|
|
1117
|
-
return;
|
|
1118
|
-
}
|
|
1119
|
-
log('[AutoUpdate] Manual check for updates...');
|
|
1120
|
-
autoUpdater.requestHeaders = headers;
|
|
1121
|
-
try {
|
|
1122
|
-
const result = await autoUpdater.checkForUpdates();
|
|
1123
|
-
if (result && result.updateInfo) {
|
|
1124
|
-
dialog.showMessageBox({
|
|
1125
|
-
type: 'info',
|
|
1126
|
-
title: 'Update Available',
|
|
1127
|
-
message: `Version ${result.updateInfo.version} is available.`,
|
|
1128
|
-
buttons: ['OK']
|
|
1129
|
-
});
|
|
1130
|
-
} else {
|
|
1131
|
-
dialog.showMessageBox({
|
|
1132
|
-
type: 'info',
|
|
1133
|
-
title: 'No Updates',
|
|
1134
|
-
message: "You're up to date!",
|
|
1135
|
-
buttons: ['OK']
|
|
1136
|
-
});
|
|
1137
|
-
}
|
|
1138
|
-
} catch (error) {
|
|
1139
|
-
log(`[AutoUpdate] Manual check failed: ${error.message}`);
|
|
1140
|
-
dialog.showMessageBox({
|
|
1141
|
-
type: 'error',
|
|
1142
|
-
title: 'Update Check Failed',
|
|
1143
|
-
message: 'Unable to check for updates.',
|
|
1144
|
-
detail: 'The update server is unavailable. Please check your internet connection and try again later.',
|
|
1145
|
-
buttons: ['OK']
|
|
1146
|
-
});
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
]
|
|
1151
|
-
}
|
|
1152
|
-
];
|
|
1153
|
-
|
|
1154
|
-
return Menu.buildFromTemplate(template);
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
// ==================== Window Management ====================
|
|
1158
|
-
|
|
1159
|
-
function createWindow() {
|
|
1160
|
-
log('Creating browser window...');
|
|
1161
|
-
|
|
1162
|
-
mainWindow = new BrowserWindow({
|
|
1163
|
-
width: 1400,
|
|
1164
|
-
height: 900,
|
|
1165
|
-
title: 'JettyPod',
|
|
1166
|
-
webPreferences: {
|
|
1167
|
-
nodeIntegration: false,
|
|
1168
|
-
contextIsolation: true,
|
|
1169
|
-
preload: preloadPath
|
|
1170
|
-
}
|
|
1171
|
-
});
|
|
1172
|
-
|
|
1173
|
-
windows.add(mainWindow);
|
|
1174
|
-
|
|
1175
|
-
// Determine start URL based on auth state, Claude Code, and project status
|
|
1176
|
-
let startUrl;
|
|
1177
|
-
if (isPackaged && !isAuthenticated()) {
|
|
1178
|
-
// Auth required - show login screen (production only)
|
|
1179
|
-
startUrl = 'http://localhost:3000/login';
|
|
1180
|
-
log('No auth token, showing login screen');
|
|
1181
|
-
} else if (!claudeCodeInstalled) {
|
|
1182
|
-
// Claude Code is required - show install screen
|
|
1183
|
-
startUrl = 'http://localhost:3000/install-claude';
|
|
1184
|
-
log('Claude Code not installed, showing install screen');
|
|
1185
|
-
} else if (!claudeCodeAuthenticated) {
|
|
1186
|
-
// Claude Code installed but not authenticated - show connect screen
|
|
1187
|
-
startUrl = 'http://localhost:3000/connect-claude';
|
|
1188
|
-
log('Claude Code not authenticated, showing connect screen');
|
|
1189
|
-
} else {
|
|
1190
|
-
// Check if we have a valid project configured
|
|
1191
|
-
const hasProject = hasValidProject(projectRoot);
|
|
1192
|
-
startUrl = hasProject ? 'http://localhost:3000' : 'http://localhost:3000/welcome';
|
|
1193
|
-
log(`Project configured: ${hasProject}, loading: ${startUrl}`);
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
mainWindow.loadURL(startUrl);
|
|
1197
|
-
|
|
1198
|
-
// Open DevTools in development
|
|
1199
|
-
if (process.env.NODE_ENV === 'development') {
|
|
1200
|
-
mainWindow.webContents.openDevTools();
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
mainWindow.on('closed', () => {
|
|
1204
|
-
windows.delete(mainWindow);
|
|
1205
|
-
mainWindow = null;
|
|
1206
|
-
});
|
|
1207
|
-
|
|
1208
|
-
log('Window created');
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
// Open a folder dialog and switch the current window to that project
|
|
1212
|
-
async function openFolder() {
|
|
1213
|
-
const focusedWindow = BrowserWindow.getFocusedWindow();
|
|
1214
|
-
if (!focusedWindow) {
|
|
1215
|
-
log('[openFolder] No focused window');
|
|
1216
|
-
return;
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
const result = await dialog.showOpenDialog(focusedWindow, {
|
|
1220
|
-
properties: ['openDirectory'],
|
|
1221
|
-
title: 'Open JettyPod Project'
|
|
1222
|
-
});
|
|
1223
|
-
|
|
1224
|
-
if (result.canceled || result.filePaths.length === 0) {
|
|
1225
|
-
log('[openFolder] Dialog canceled');
|
|
1226
|
-
return;
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
const selectedPath = result.filePaths[0];
|
|
1230
|
-
log(`[openFolder] Selected: ${selectedPath}`);
|
|
1231
|
-
|
|
1232
|
-
// Validate the folder has a .jettypod directory
|
|
1233
|
-
if (!hasValidProject(selectedPath)) {
|
|
1234
|
-
log('[openFolder] Invalid project - missing .jettypod directory');
|
|
1235
|
-
dialog.showErrorBox('Not a JettyPod project',
|
|
1236
|
-
'The selected folder does not contain a .jettypod directory.\n\nPlease select a folder that has been initialized with JettyPod.');
|
|
1237
|
-
return;
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
// Update the project root (await ensures Next.js dev server is synced before reload)
|
|
1241
|
-
await setProjectRoot(selectedPath);
|
|
1242
|
-
|
|
1243
|
-
// Reload the focused window to the main dashboard (not welcome)
|
|
1244
|
-
focusedWindow.loadURL('http://localhost:3000');
|
|
1245
|
-
log('[openFolder] Window reloaded with new project');
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
// Create a new window showing the welcome screen (for opening a second project)
|
|
1249
|
-
function createNewWindow() {
|
|
1250
|
-
log('Creating new window...');
|
|
1251
|
-
|
|
1252
|
-
const newWindow = new BrowserWindow({
|
|
1253
|
-
width: 1400,
|
|
1254
|
-
height: 900,
|
|
1255
|
-
title: 'JettyPod',
|
|
1256
|
-
webPreferences: {
|
|
1257
|
-
nodeIntegration: false,
|
|
1258
|
-
contextIsolation: true,
|
|
1259
|
-
preload: preloadPath
|
|
1260
|
-
}
|
|
1261
|
-
});
|
|
1262
|
-
|
|
1263
|
-
windows.add(newWindow);
|
|
1264
|
-
|
|
1265
|
-
// New windows always start at welcome screen so user can select a project
|
|
1266
|
-
newWindow.loadURL('http://localhost:3000/welcome');
|
|
1267
|
-
|
|
1268
|
-
newWindow.on('closed', () => {
|
|
1269
|
-
windows.delete(newWindow);
|
|
1270
|
-
log(`Window closed. ${windows.size} windows remaining.`);
|
|
1271
|
-
});
|
|
1272
|
-
|
|
1273
|
-
log(`New window created. Total windows: ${windows.size}`);
|
|
1274
|
-
return newWindow;
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
function cleanup() {
|
|
1278
|
-
log('Shutting down...');
|
|
1279
|
-
|
|
1280
|
-
if (nextProcess) {
|
|
1281
|
-
log('Killing Next.js dev server...');
|
|
1282
|
-
// Use SIGKILL and kill the entire process group to ensure all child processes are terminated
|
|
1283
|
-
const pid = nextProcess.pid;
|
|
1284
|
-
try {
|
|
1285
|
-
// Kill the process group (negative PID) to terminate all descendants
|
|
1286
|
-
process.kill(-pid, 'SIGKILL');
|
|
1287
|
-
log(`Killed Next.js process group (PID: ${pid})`);
|
|
1288
|
-
} catch (killErr) {
|
|
1289
|
-
// Fallback to killing just the process if process group kill fails
|
|
1290
|
-
try {
|
|
1291
|
-
nextProcess.kill('SIGKILL');
|
|
1292
|
-
log(`Killed Next.js process (PID: ${pid})`);
|
|
1293
|
-
} catch (fallbackErr) {
|
|
1294
|
-
log(`Warning: Could not kill Next.js process: ${fallbackErr.message}`);
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
nextProcess = null;
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
if (nextServer) {
|
|
1301
|
-
log('Closing Next.js production server...');
|
|
1302
|
-
nextServer.close();
|
|
1303
|
-
nextServer = null;
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
killAllClaudeProcesses();
|
|
1307
|
-
killAllDevServers();
|
|
1308
|
-
stopWebSocketServer();
|
|
1309
|
-
closeDb();
|
|
1310
|
-
closeIngesterDb();
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
// ==================== Claude Code Detection ====================
|
|
1314
|
-
|
|
1315
|
-
// Track whether Claude Code is installed and authenticated (checked on startup)
|
|
1316
|
-
let claudeCodeInstalled = false;
|
|
1317
|
-
let claudeCodeAuthenticated = false;
|
|
1318
|
-
let claudeCodeNeedsUpdate = false;
|
|
1319
|
-
|
|
1320
|
-
/**
|
|
1321
|
-
* Check if Claude Code CLI is installed
|
|
1322
|
-
* Uses 'which claude' to detect installation
|
|
1323
|
-
* @returns {boolean} true if Claude Code is installed
|
|
1324
|
-
*/
|
|
1325
|
-
function checkClaudeCodeInstalled() {
|
|
1326
|
-
const shellEnv = getShellEnv();
|
|
1327
|
-
log(`[ClaudeCode] Checking with PATH: ${shellEnv.PATH}`);
|
|
1328
|
-
try {
|
|
1329
|
-
const which = execSync('which claude', { encoding: 'utf-8', stdio: 'pipe', env: shellEnv }).trim();
|
|
1330
|
-
log(`[ClaudeCode] Claude Code found at: ${which}`);
|
|
1331
|
-
return true;
|
|
1332
|
-
} catch {
|
|
1333
|
-
log('[ClaudeCode] Claude Code is NOT installed (not found in PATH)');
|
|
1334
|
-
return false;
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
/**
|
|
1339
|
-
* Install Claude Code using the official Anthropic install script.
|
|
1340
|
-
* This is the recommended installation method that handles PATH setup automatically.
|
|
1341
|
-
* @returns {Promise<{success: boolean, error?: string}>}
|
|
1342
|
-
*/
|
|
1343
|
-
async function installClaudeCode() {
|
|
1344
|
-
log('[ClaudeCode] Starting installation via official script...');
|
|
1345
|
-
|
|
1346
|
-
const shellEnv = getShellEnv();
|
|
1347
|
-
|
|
1348
|
-
return new Promise((resolve) => {
|
|
1349
|
-
// Run the official Anthropic install script
|
|
1350
|
-
const installProcess = spawn(getUserShell(), ['-lc', 'curl -fsSL https://claude.ai/install.sh | bash'], {
|
|
1351
|
-
env: shellEnv
|
|
1352
|
-
});
|
|
1353
|
-
|
|
1354
|
-
let stderr = '';
|
|
1355
|
-
let stdout = '';
|
|
1356
|
-
|
|
1357
|
-
installProcess.stdout.on('data', (data) => {
|
|
1358
|
-
stdout += data.toString();
|
|
1359
|
-
log(`[ClaudeCode] stdout: ${data}`);
|
|
1360
|
-
});
|
|
1361
|
-
|
|
1362
|
-
installProcess.stderr.on('data', (data) => {
|
|
1363
|
-
stderr += data.toString();
|
|
1364
|
-
log(`[ClaudeCode] stderr: ${data}`);
|
|
1365
|
-
});
|
|
1366
|
-
|
|
1367
|
-
installProcess.on('error', (err) => {
|
|
1368
|
-
log(`[ClaudeCode] Install error: ${err.message}`);
|
|
1369
|
-
resolve({ success: false, error: err.message });
|
|
1370
|
-
});
|
|
1371
|
-
|
|
1372
|
-
installProcess.on('exit', (code) => {
|
|
1373
|
-
if (code === 0) {
|
|
1374
|
-
log('[ClaudeCode] Installation successful');
|
|
1375
|
-
claudeCodeInstalled = true;
|
|
1376
|
-
resolve({ success: true });
|
|
1377
|
-
} else {
|
|
1378
|
-
log(`[ClaudeCode] Installation failed with code ${code}`);
|
|
1379
|
-
const errorMsg = stderr || stdout || `Exit code ${code}`;
|
|
1380
|
-
resolve({ success: false, error: errorMsg });
|
|
1381
|
-
}
|
|
1382
|
-
});
|
|
1383
|
-
});
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
/**
|
|
1387
|
-
* Update Claude Code via `claude update`
|
|
1388
|
-
* @returns {Promise<{success: boolean, error?: string}>}
|
|
1389
|
-
*/
|
|
1390
|
-
async function updateClaudeCode() {
|
|
1391
|
-
log('[ClaudeCode] Starting update...');
|
|
1392
|
-
|
|
1393
|
-
const shellEnv = getShellEnv();
|
|
1394
|
-
log(`[ClaudeCode] Using PATH: ${shellEnv.PATH.substring(0, 200)}...`);
|
|
1395
|
-
|
|
1396
|
-
return new Promise((resolve) => {
|
|
1397
|
-
const updateProcess = spawn(getUserShell(), ['-lc', 'claude update'], {
|
|
1398
|
-
env: shellEnv
|
|
1399
|
-
});
|
|
1400
|
-
|
|
1401
|
-
let stderr = '';
|
|
1402
|
-
let stdout = '';
|
|
1403
|
-
|
|
1404
|
-
updateProcess.stdout.on('data', (data) => {
|
|
1405
|
-
stdout += data.toString();
|
|
1406
|
-
log(`[ClaudeCode] stdout: ${data}`);
|
|
1407
|
-
});
|
|
1408
|
-
|
|
1409
|
-
updateProcess.stderr.on('data', (data) => {
|
|
1410
|
-
stderr += data.toString();
|
|
1411
|
-
log(`[ClaudeCode] stderr: ${data}`);
|
|
1412
|
-
});
|
|
1413
|
-
|
|
1414
|
-
updateProcess.on('error', (err) => {
|
|
1415
|
-
log(`[ClaudeCode] Update error: ${err.message}`);
|
|
1416
|
-
resolve({ success: false, error: err.message });
|
|
1417
|
-
});
|
|
1418
|
-
|
|
1419
|
-
updateProcess.on('exit', (code) => {
|
|
1420
|
-
if (code === 0) {
|
|
1421
|
-
log('[ClaudeCode] Update successful');
|
|
1422
|
-
resolve({ success: true, output: stdout });
|
|
1423
|
-
} else {
|
|
1424
|
-
log(`[ClaudeCode] Update failed with code ${code}`);
|
|
1425
|
-
resolve({ success: false, error: stderr || stdout || `Exit code ${code}` });
|
|
1426
|
-
}
|
|
1427
|
-
});
|
|
1428
|
-
});
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
// Export for IPC handlers
|
|
1432
|
-
function getClaudeCodeInstalled() {
|
|
1433
|
-
return claudeCodeInstalled;
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
function getClaudeCodeAuthenticated() {
|
|
1437
|
-
return claudeCodeAuthenticated;
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
/**
|
|
1441
|
-
* Check if Claude Code CLI is authenticated.
|
|
1442
|
-
* Runs a lightweight command that requires auth to succeed.
|
|
1443
|
-
* On any unexpected failure, defaults to false (shows connect screen)
|
|
1444
|
-
* so the user can authenticate rather than hitting a broken state.
|
|
1445
|
-
* @returns {boolean} true if Claude Code is authenticated
|
|
1446
|
-
*/
|
|
1447
|
-
function checkClaudeCodeAuthenticated() {
|
|
1448
|
-
if (!claudeCodeInstalled) return false;
|
|
1449
|
-
const shellEnv = getShellEnv();
|
|
1450
|
-
try {
|
|
1451
|
-
// claude -p with a simple prompt fails fast if not authenticated
|
|
1452
|
-
execSync('claude -p "ping" --output-format text', {
|
|
1453
|
-
encoding: 'utf-8',
|
|
1454
|
-
stdio: 'pipe',
|
|
1455
|
-
env: shellEnv,
|
|
1456
|
-
timeout: 15000
|
|
1457
|
-
});
|
|
1458
|
-
log('[ClaudeCode] Claude Code is authenticated');
|
|
1459
|
-
return true;
|
|
1460
|
-
} catch (err) {
|
|
1461
|
-
const stderr = (err.stderr || '').toString();
|
|
1462
|
-
const stdout = (err.stdout || '').toString();
|
|
1463
|
-
const message = err.message || '';
|
|
1464
|
-
|
|
1465
|
-
// Timeout — cli hung, don't block startup, show connect screen
|
|
1466
|
-
if (err.killed || message.includes('ETIMEDOUT') || message.includes('timed out')) {
|
|
1467
|
-
log('[ClaudeCode] Auth check timed out, defaulting to unauthenticated');
|
|
1468
|
-
return false;
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
// Outdated CLI — flag for auto-update
|
|
1472
|
-
const combined = stderr + stdout + message;
|
|
1473
|
-
if (combined.includes('needs an update') || combined.includes('needs_update')) {
|
|
1474
|
-
log('[ClaudeCode] CLI is outdated, flagging for auto-update');
|
|
1475
|
-
claudeCodeNeedsUpdate = true;
|
|
1476
|
-
return false;
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
// Auth-related errors — definitely not authenticated
|
|
1480
|
-
if (stderr.includes('not logged in') || stderr.includes('login') ||
|
|
1481
|
-
stdout.includes('not logged in') || stdout.includes('login')) {
|
|
1482
|
-
log('[ClaudeCode] Claude Code is NOT authenticated');
|
|
1483
|
-
return false;
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
// Any other error (network, spawn failure, unexpected crash) —
|
|
1487
|
-
// default to unauthenticated so user sees the connect screen
|
|
1488
|
-
// rather than silently failing in the main app
|
|
1489
|
-
log(`[ClaudeCode] Auth check failed, defaulting to unauthenticated: ${stderr || stdout || message}`);
|
|
1490
|
-
return false;
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
/**
|
|
1495
|
-
* Spawn `claude` to trigger browser-based OAuth authentication.
|
|
1496
|
-
* The CLI auto-detects missing credentials and starts the OAuth flow,
|
|
1497
|
-
* printing an auth URL to stdout/stderr. We capture it and open it
|
|
1498
|
-
* with shell.openExternal() since the spawned process has no TTY.
|
|
1499
|
-
* @returns {Promise<{success: boolean, error?: string}>}
|
|
1500
|
-
*/
|
|
1501
|
-
function loginClaudeCode() {
|
|
1502
|
-
return new Promise((resolve) => {
|
|
1503
|
-
const shellEnv = getShellEnv();
|
|
1504
|
-
log('[ClaudeCode] Starting claude auth flow...');
|
|
1505
|
-
|
|
1506
|
-
const loginProcess = spawn(getUserShell(), ['-lc', 'claude'], {
|
|
1507
|
-
env: shellEnv,
|
|
1508
|
-
stdio: 'pipe'
|
|
1509
|
-
});
|
|
1510
|
-
|
|
1511
|
-
let stdout = '';
|
|
1512
|
-
let stderr = '';
|
|
1513
|
-
let urlOpened = false;
|
|
1514
|
-
|
|
1515
|
-
const tryOpenUrl = (text) => {
|
|
1516
|
-
if (urlOpened) return;
|
|
1517
|
-
// Match OAuth/auth URLs the CLI prints for non-TTY environments
|
|
1518
|
-
const urlMatch = text.match(/https?:\/\/\S+/);
|
|
1519
|
-
if (urlMatch) {
|
|
1520
|
-
const url = urlMatch[0];
|
|
1521
|
-
log(`[ClaudeCode] Found auth URL, opening in browser: ${url}`);
|
|
1522
|
-
shell.openExternal(url);
|
|
1523
|
-
urlOpened = true;
|
|
1524
|
-
}
|
|
1525
|
-
};
|
|
1526
|
-
|
|
1527
|
-
loginProcess.stdout.on('data', (data) => {
|
|
1528
|
-
const chunk = data.toString();
|
|
1529
|
-
stdout += chunk;
|
|
1530
|
-
log(`[ClaudeCode] stdout: ${chunk.trim()}`);
|
|
1531
|
-
tryOpenUrl(chunk);
|
|
1532
|
-
});
|
|
1533
|
-
|
|
1534
|
-
loginProcess.stderr.on('data', (data) => {
|
|
1535
|
-
const chunk = data.toString();
|
|
1536
|
-
stderr += chunk;
|
|
1537
|
-
log(`[ClaudeCode] stderr: ${chunk.trim()}`);
|
|
1538
|
-
tryOpenUrl(chunk);
|
|
1539
|
-
});
|
|
1540
|
-
|
|
1541
|
-
loginProcess.on('close', async (code) => {
|
|
1542
|
-
if (code === 0) {
|
|
1543
|
-
claudeCodeAuthenticated = true;
|
|
1544
|
-
log('[ClaudeCode] Auth completed successfully');
|
|
1545
|
-
resolve({ success: true });
|
|
1546
|
-
} else if (stderr.includes('needs an update') || stderr.includes('needs_update') ||
|
|
1547
|
-
stdout.includes('needs an update') || stdout.includes('needs_update')) {
|
|
1548
|
-
log('[ClaudeCode] CLI outdated during login, running auto-update...');
|
|
1549
|
-
const updateResult = await updateClaudeCode();
|
|
1550
|
-
if (updateResult.success) {
|
|
1551
|
-
log('[ClaudeCode] Auto-update succeeded, retrying login');
|
|
1552
|
-
claudeCodeNeedsUpdate = false;
|
|
1553
|
-
// Retry login after update
|
|
1554
|
-
const retryResult = await loginClaudeCode();
|
|
1555
|
-
resolve(retryResult);
|
|
1556
|
-
} else {
|
|
1557
|
-
log(`[ClaudeCode] Auto-update failed: ${updateResult.error}`);
|
|
1558
|
-
resolve({ success: false, error: 'Claude Code needs an update but the update failed. Please run "claude update" manually.' });
|
|
1559
|
-
}
|
|
1560
|
-
} else {
|
|
1561
|
-
log(`[ClaudeCode] Auth failed with code ${code}: ${stderr}`);
|
|
1562
|
-
resolve({ success: false, error: stderr || `Auth exited with code ${code}` });
|
|
1563
|
-
}
|
|
1564
|
-
});
|
|
1565
|
-
|
|
1566
|
-
loginProcess.on('error', (err) => {
|
|
1567
|
-
log(`[ClaudeCode] Auth spawn error: ${err.message}`);
|
|
1568
|
-
resolve({ success: false, error: err.message });
|
|
1569
|
-
});
|
|
1570
|
-
});
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
// ==================== PATH Setup ====================
|
|
1574
|
-
|
|
1575
|
-
const SYMLINK_PATH = '/usr/local/bin/jettypod';
|
|
1576
|
-
|
|
1577
|
-
/**
|
|
1578
|
-
* Get the path to the bundled jettypod CLI
|
|
1579
|
-
* In packaged app: Contents/Resources/bin/jettypod
|
|
1580
|
-
*/
|
|
1581
|
-
function getBundledCliPath() {
|
|
1582
|
-
if (isPackaged) {
|
|
1583
|
-
return path.join(process.resourcesPath, 'bin', 'jettypod');
|
|
1584
|
-
}
|
|
1585
|
-
// Development mode - use the bin/jettypod in project root
|
|
1586
|
-
return path.join(__dirname, '..', '..', '..', 'bin', 'jettypod');
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
/**
|
|
1590
|
-
* Setup PATH by creating symlink to /usr/local/bin
|
|
1591
|
-
* Uses osascript to request admin privileges
|
|
1592
|
-
*/
|
|
1593
|
-
async function setupPathOnFirstLaunch() {
|
|
1594
|
-
// Only run on macOS
|
|
1595
|
-
if (process.platform !== 'darwin') {
|
|
1596
|
-
log('[PATH] Not macOS, skipping PATH setup');
|
|
1597
|
-
return;
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
|
-
const bundledPath = getBundledCliPath();
|
|
1601
|
-
log(`[PATH] Bundled CLI path: ${bundledPath}`);
|
|
1602
|
-
|
|
1603
|
-
// Check if bundled CLI exists
|
|
1604
|
-
if (!fs.existsSync(bundledPath)) {
|
|
1605
|
-
log('[PATH] Bundled CLI not found, skipping PATH setup');
|
|
1606
|
-
return;
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
// Skip setup if symlink already exists (subsequent launches)
|
|
1610
|
-
if (fs.existsSync(SYMLINK_PATH)) {
|
|
1611
|
-
log('[PATH] Symlink already exists, skipping PATH setup');
|
|
1612
|
-
return;
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
log('[PATH] Setting up PATH with admin privileges...');
|
|
1616
|
-
|
|
1617
|
-
// Use osascript to run ln with admin privileges.
|
|
1618
|
-
// mkdir -p ensures the parent directory exists (e.g. /usr/local/bin may not
|
|
1619
|
-
// exist on fresh Apple Silicon Macs which use /opt/homebrew/bin by default).
|
|
1620
|
-
const symlinkDir = path.dirname(SYMLINK_PATH);
|
|
1621
|
-
const script = `do shell script "mkdir -p '${symlinkDir}' && ln -sf '${bundledPath}' '${SYMLINK_PATH}'" with administrator privileges`;
|
|
1622
|
-
|
|
1623
|
-
try {
|
|
1624
|
-
execSync(`osascript -e '${script}'`, { encoding: 'utf-8' });
|
|
1625
|
-
log('[PATH] Symlink created successfully');
|
|
1626
|
-
} catch (error) {
|
|
1627
|
-
// User cancelled or error occurred
|
|
1628
|
-
log(`[PATH] Setup failed or cancelled: ${error.message}`);
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
// ==================== Subscription & Access ====================
|
|
1633
|
-
|
|
1634
|
-
/**
|
|
1635
|
-
* Get path to subscription.json file
|
|
1636
|
-
* Stored in Electron's userData directory (~/.config/JettyPod/)
|
|
1637
|
-
*/
|
|
1638
|
-
function getSubscriptionPath() {
|
|
1639
|
-
return path.join(app.getPath('userData'), 'subscription.json');
|
|
1640
|
-
}
|
|
1641
|
-
|
|
1642
|
-
/**
|
|
1643
|
-
* Get the path to the auth state file.
|
|
1644
|
-
* Stored alongside subscription.json in Electron's userData directory.
|
|
1645
|
-
*/
|
|
1646
|
-
function getAuthPath() {
|
|
1647
|
-
return path.join(app.getPath('userData'), 'auth.json');
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
/**
|
|
1651
|
-
* Check if there's a stored auth token (JWT).
|
|
1652
|
-
* Does NOT validate the token — just checks if auth.json exists with a token.
|
|
1653
|
-
*/
|
|
1654
|
-
function isAuthenticated() {
|
|
1655
|
-
const authPath = getAuthPath();
|
|
1656
|
-
if (!fs.existsSync(authPath)) {
|
|
1657
|
-
log('[Auth] No auth.json found');
|
|
1658
|
-
return false;
|
|
1659
|
-
}
|
|
1660
|
-
|
|
1661
|
-
try {
|
|
1662
|
-
const data = JSON.parse(fs.readFileSync(authPath, 'utf-8'));
|
|
1663
|
-
if (data.token) {
|
|
1664
|
-
log('[Auth] Auth token found');
|
|
1665
|
-
return true;
|
|
1666
|
-
}
|
|
1667
|
-
} catch (error) {
|
|
1668
|
-
log(`[Auth] Error reading auth file: ${error.message}`);
|
|
1669
|
-
}
|
|
1670
|
-
|
|
1671
|
-
return false;
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
/**
|
|
1675
|
-
* Read the Stripe customer ID from subscription config.
|
|
1676
|
-
* Returns null if no subscription is stored.
|
|
1677
|
-
* Written by the Stripe checkout flow (chore #1000535).
|
|
1678
|
-
*/
|
|
1679
|
-
function getSubscriptionCustomerId() {
|
|
1680
|
-
const subPath = getSubscriptionPath();
|
|
1681
|
-
if (!fs.existsSync(subPath)) return null;
|
|
1682
|
-
try {
|
|
1683
|
-
const data = JSON.parse(fs.readFileSync(subPath, 'utf-8'));
|
|
1684
|
-
return data.customerId || null;
|
|
1685
|
-
} catch (error) {
|
|
1686
|
-
log(`[Subscription] Error reading subscription file: ${error.message}`);
|
|
1687
|
-
return null;
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
/**
|
|
1692
|
-
* Check if there's an active subscription stored locally.
|
|
1693
|
-
* Full validation against Stripe happens on update check / app refresh.
|
|
1694
|
-
*/
|
|
1695
|
-
function isSubscriptionActive() {
|
|
1696
|
-
const subPath = getSubscriptionPath();
|
|
1697
|
-
if (!fs.existsSync(subPath)) {
|
|
1698
|
-
log('[Subscription] No subscription.json found');
|
|
1699
|
-
return false;
|
|
1700
|
-
}
|
|
1701
|
-
|
|
1702
|
-
try {
|
|
1703
|
-
const data = JSON.parse(fs.readFileSync(subPath, 'utf-8'));
|
|
1704
|
-
if (data.customerId) {
|
|
1705
|
-
log('[Subscription] Active subscription found');
|
|
1706
|
-
return true;
|
|
1707
|
-
}
|
|
1708
|
-
} catch (error) {
|
|
1709
|
-
log(`[Subscription] Error reading subscription file: ${error.message}`);
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
return false;
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
// ==================== Skills Auto-Sync ====================
|
|
1716
|
-
|
|
1717
|
-
/**
|
|
1718
|
-
* Get the path to bundled skills
|
|
1719
|
-
* In packaged app: Contents/Resources/skills
|
|
1720
|
-
* In dev mode: ~/.claude/skills (the user's existing skills)
|
|
1721
|
-
*/
|
|
1722
|
-
function getBundledSkillsPath() {
|
|
1723
|
-
if (isPackaged) {
|
|
1724
|
-
return path.join(process.resourcesPath, 'skills');
|
|
1725
|
-
}
|
|
1726
|
-
// Development mode - use the user's existing skills directory
|
|
1727
|
-
return path.join(require('os').homedir(), '.claude', 'skills');
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
/**
|
|
1731
|
-
* Get the user's skills directory path
|
|
1732
|
-
* @returns {string} Path to ~/.claude/skills/
|
|
1733
|
-
*/
|
|
1734
|
-
function getUserSkillsPath() {
|
|
1735
|
-
return path.join(require('os').homedir(), '.claude', 'skills');
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
/**
|
|
1739
|
-
* Recursively copy a directory
|
|
1740
|
-
* @param {string} src - Source directory
|
|
1741
|
-
* @param {string} dest - Destination directory
|
|
1742
|
-
*/
|
|
1743
|
-
function copyDirSync(src, dest) {
|
|
1744
|
-
// Create destination directory if it doesn't exist
|
|
1745
|
-
if (!fs.existsSync(dest)) {
|
|
1746
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
1750
|
-
|
|
1751
|
-
for (const entry of entries) {
|
|
1752
|
-
const srcPath = path.join(src, entry.name);
|
|
1753
|
-
const destPath = path.join(dest, entry.name);
|
|
1754
|
-
|
|
1755
|
-
if (entry.isDirectory()) {
|
|
1756
|
-
copyDirSync(srcPath, destPath);
|
|
1757
|
-
} else {
|
|
1758
|
-
fs.copyFileSync(srcPath, destPath);
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
}
|
|
1762
|
-
|
|
1763
|
-
/**
|
|
1764
|
-
* Sync bundled skills to user's ~/.claude/skills/ directory
|
|
1765
|
-
* Called on every app launch to ensure skills are up to date
|
|
1766
|
-
*
|
|
1767
|
-
* Error handling: All errors are caught and logged - sync failures
|
|
1768
|
-
* should never prevent the app from launching.
|
|
1769
|
-
*/
|
|
1770
|
-
function syncSkills() {
|
|
1771
|
-
try {
|
|
1772
|
-
const bundledPath = getBundledSkillsPath();
|
|
1773
|
-
const userPath = getUserSkillsPath();
|
|
1774
|
-
|
|
1775
|
-
log(`[Skills] Syncing skills from ${bundledPath} to ${userPath}`);
|
|
1776
|
-
|
|
1777
|
-
// Check if bundled skills exist
|
|
1778
|
-
if (!fs.existsSync(bundledPath)) {
|
|
1779
|
-
log('[Skills] No bundled skills found, skipping sync');
|
|
1780
|
-
return;
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
// Create user skills directory if it doesn't exist
|
|
1784
|
-
if (!fs.existsSync(userPath)) {
|
|
1785
|
-
log('[Skills] Creating user skills directory');
|
|
1786
|
-
try {
|
|
1787
|
-
fs.mkdirSync(userPath, { recursive: true });
|
|
1788
|
-
} catch (dirError) {
|
|
1789
|
-
log(`[Skills] Warning: Failed to create skills directory: ${dirError.message}`);
|
|
1790
|
-
// Cannot proceed without directory - exit gracefully
|
|
1791
|
-
return;
|
|
1792
|
-
}
|
|
1793
|
-
}
|
|
1794
|
-
|
|
1795
|
-
// Get list of bundled skills (directories)
|
|
1796
|
-
let bundledSkills;
|
|
1797
|
-
try {
|
|
1798
|
-
bundledSkills = fs.readdirSync(bundledPath, { withFileTypes: true })
|
|
1799
|
-
.filter(entry => entry.isDirectory())
|
|
1800
|
-
.map(entry => entry.name);
|
|
1801
|
-
} catch (readError) {
|
|
1802
|
-
log(`[Skills] Warning: Failed to read bundled skills: ${readError.message}`);
|
|
1803
|
-
return;
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
|
-
log(`[Skills] Found ${bundledSkills.length} bundled skills to sync`);
|
|
1807
|
-
|
|
1808
|
-
// Copy each skill directory (overwrites existing)
|
|
1809
|
-
for (const skillName of bundledSkills) {
|
|
1810
|
-
const srcSkillPath = path.join(bundledPath, skillName);
|
|
1811
|
-
const destSkillPath = path.join(userPath, skillName);
|
|
1812
|
-
|
|
1813
|
-
try {
|
|
1814
|
-
log(`[Skills] Syncing skill: ${skillName}`);
|
|
1815
|
-
copyDirSync(srcSkillPath, destSkillPath);
|
|
1816
|
-
} catch (copyError) {
|
|
1817
|
-
// Log error but continue with other skills
|
|
1818
|
-
log(`[Skills] Warning: Failed to sync skill ${skillName}: ${copyError.message}`);
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
log('[Skills] Sync complete');
|
|
1823
|
-
} catch (error) {
|
|
1824
|
-
// Catch-all for any unexpected errors
|
|
1825
|
-
log(`[Skills] Warning: Skills sync failed: ${error.message}`);
|
|
1826
|
-
// Don't throw - app should continue launching
|
|
1827
|
-
}
|
|
1828
|
-
}
|
|
1829
|
-
|
|
1830
|
-
// ==================== Auto-Update Event Handlers ====================
|
|
1831
|
-
|
|
1832
|
-
// Track download state to detect download failures
|
|
1833
|
-
let isDownloading = false;
|
|
1834
|
-
|
|
1835
|
-
// Handle auto-updater errors (network failures, server unavailable, etc.)
|
|
1836
|
-
autoUpdater.on('error', (error) => {
|
|
1837
|
-
log(`[AutoUpdate] Error: ${error.message}`);
|
|
1838
|
-
|
|
1839
|
-
// If download was in progress, show user-friendly error with retry option
|
|
1840
|
-
if (isDownloading) {
|
|
1841
|
-
isDownloading = false;
|
|
1842
|
-
dialog.showMessageBox({
|
|
1843
|
-
type: 'error',
|
|
1844
|
-
title: 'Download Failed',
|
|
1845
|
-
message: 'Failed to download the update.',
|
|
1846
|
-
detail: 'Please check your internet connection and try again.',
|
|
1847
|
-
buttons: ['Retry', 'Cancel'],
|
|
1848
|
-
defaultId: 0
|
|
1849
|
-
}).then((result) => {
|
|
1850
|
-
if (result.response === 0) {
|
|
1851
|
-
log('[AutoUpdate] User chose to retry download');
|
|
1852
|
-
autoUpdater.downloadUpdate();
|
|
1853
|
-
} else {
|
|
1854
|
-
log('[AutoUpdate] User cancelled retry');
|
|
1855
|
-
}
|
|
1856
|
-
});
|
|
1857
|
-
}
|
|
1858
|
-
// Otherwise, this is a background check failure - just log it
|
|
1859
|
-
// App continues functioning normally
|
|
1860
|
-
});
|
|
1861
|
-
|
|
1862
|
-
autoUpdater.on('update-available', (info) => {
|
|
1863
|
-
isDownloading = true; // Mark download as starting
|
|
1864
|
-
log(`[AutoUpdate] Update available: ${info.version}`);
|
|
1865
|
-
dialog.showMessageBox({
|
|
1866
|
-
type: 'info',
|
|
1867
|
-
title: 'Update Available',
|
|
1868
|
-
message: `A new version (${info.version}) is available.`,
|
|
1869
|
-
detail: 'The update will be downloaded in the background.',
|
|
1870
|
-
buttons: ['OK']
|
|
1871
|
-
});
|
|
1872
|
-
});
|
|
1873
|
-
|
|
1874
|
-
autoUpdater.on('download-progress', (progressObj) => {
|
|
1875
|
-
log(`[AutoUpdate] Download progress: ${progressObj.percent.toFixed(1)}%`);
|
|
1876
|
-
});
|
|
1877
|
-
|
|
1878
|
-
autoUpdater.on('update-downloaded', (info) => {
|
|
1879
|
-
isDownloading = false; // Download complete
|
|
1880
|
-
log(`[AutoUpdate] Update downloaded: ${info.version}`);
|
|
1881
|
-
dialog.showMessageBox({
|
|
1882
|
-
type: 'info',
|
|
1883
|
-
title: 'Restart to Update',
|
|
1884
|
-
message: `Version ${info.version} has been downloaded.`,
|
|
1885
|
-
detail: 'Restart now to install the update?',
|
|
1886
|
-
buttons: ['Restart Now', 'Later'],
|
|
1887
|
-
defaultId: 0
|
|
1888
|
-
}).then((result) => {
|
|
1889
|
-
if (result.response === 0) {
|
|
1890
|
-
log('[AutoUpdate] User chose to restart');
|
|
1891
|
-
autoUpdater.quitAndInstall();
|
|
1892
|
-
} else {
|
|
1893
|
-
log('[AutoUpdate] User chose to restart later');
|
|
1894
|
-
}
|
|
1895
|
-
});
|
|
1896
|
-
});
|
|
1897
|
-
|
|
1898
|
-
// ==================== App Lifecycle ====================
|
|
1899
|
-
|
|
1900
|
-
// Register jettypod:// protocol for OAuth callback
|
|
1901
|
-
if (!isPackaged) {
|
|
1902
|
-
// Dev mode: pass app path so macOS can route the URL to this Electron instance
|
|
1903
|
-
app.setAsDefaultProtocolClient('jettypod', process.execPath, [path.resolve(process.argv[1])]);
|
|
1904
|
-
} else {
|
|
1905
|
-
app.setAsDefaultProtocolClient('jettypod');
|
|
1906
|
-
}
|
|
1907
|
-
|
|
1908
|
-
/**
|
|
1909
|
-
* Decode a JWT payload without verifying the signature.
|
|
1910
|
-
* Used to extract user info (email, plan) from the token before saving.
|
|
1911
|
-
*/
|
|
1912
|
-
function decodeJWTPayload(token) {
|
|
1913
|
-
try {
|
|
1914
|
-
const parts = token.split('.');
|
|
1915
|
-
if (parts.length !== 3) return null;
|
|
1916
|
-
const payload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
1917
|
-
return JSON.parse(Buffer.from(payload, 'base64').toString('utf-8'));
|
|
1918
|
-
} catch {
|
|
1919
|
-
return null;
|
|
1920
|
-
}
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
|
-
/**
|
|
1924
|
-
* Handle a jettypod:// protocol URL (OAuth callback).
|
|
1925
|
-
* Saves the token + decoded user info to auth.json and navigates to the dashboard.
|
|
1926
|
-
*/
|
|
1927
|
-
function handleProtocolUrl(url) {
|
|
1928
|
-
log(`[Auth] Protocol handler received: ${url}`);
|
|
1929
|
-
|
|
1930
|
-
try {
|
|
1931
|
-
const parsed = new URL(url);
|
|
1932
|
-
// jettypod://auth/callback parses as hostname='auth', pathname='/callback'
|
|
1933
|
-
const isAuthCallback =
|
|
1934
|
-
(parsed.hostname === 'auth' && parsed.pathname === '/callback') ||
|
|
1935
|
-
parsed.pathname === '//auth/callback' ||
|
|
1936
|
-
parsed.pathname === '/auth/callback';
|
|
1937
|
-
if (isAuthCallback) {
|
|
1938
|
-
const token = parsed.searchParams.get('token');
|
|
1939
|
-
if (token && mainWindow) {
|
|
1940
|
-
// Decode JWT to extract user info (sub, email, plan)
|
|
1941
|
-
const payload = decodeJWTPayload(token);
|
|
1942
|
-
const user = payload ? { id: payload.sub, email: payload.email, plan: payload.plan } : undefined;
|
|
1943
|
-
|
|
1944
|
-
// Save auth.json with token AND user data
|
|
1945
|
-
const authPath = path.join(app.getPath('userData'), 'auth.json');
|
|
1946
|
-
const dir = path.dirname(authPath);
|
|
1947
|
-
if (!fs.existsSync(dir)) {
|
|
1948
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
1949
|
-
}
|
|
1950
|
-
fs.writeFileSync(authPath, JSON.stringify({ token, user, savedAt: new Date().toISOString() }, null, 2));
|
|
1951
|
-
log('[Auth] Token + user data saved from OAuth callback');
|
|
1952
|
-
|
|
1953
|
-
// Navigate to main page
|
|
1954
|
-
mainWindow.loadURL('http://localhost:3000');
|
|
1955
|
-
mainWindow.focus();
|
|
1956
|
-
}
|
|
1957
|
-
}
|
|
1958
|
-
} catch (error) {
|
|
1959
|
-
log(`[Auth] Error handling protocol URL: ${error.message}`);
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1962
|
-
|
|
1963
|
-
// Single-instance lock: ensure deep links are routed to the existing instance
|
|
1964
|
-
const gotTheLock = app.requestSingleInstanceLock();
|
|
1965
|
-
|
|
1966
|
-
if (!gotTheLock) {
|
|
1967
|
-
// Another instance is already running — quit this one.
|
|
1968
|
-
// The URL (if any) will be forwarded to the existing instance via 'second-instance'.
|
|
1969
|
-
app.quit();
|
|
1970
|
-
} else {
|
|
1971
|
-
// On macOS, protocol URLs for a running app arrive via 'open-url'.
|
|
1972
|
-
app.on('open-url', (event, url) => {
|
|
1973
|
-
event.preventDefault();
|
|
1974
|
-
handleProtocolUrl(url);
|
|
1975
|
-
});
|
|
1976
|
-
|
|
1977
|
-
// On Windows/Linux (and macOS second-instance fallback), the URL arrives here.
|
|
1978
|
-
app.on('second-instance', (_event, commandLine) => {
|
|
1979
|
-
// The protocol URL is typically the last argument
|
|
1980
|
-
const url = commandLine.find(arg => arg.startsWith('jettypod://'));
|
|
1981
|
-
if (url) {
|
|
1982
|
-
handleProtocolUrl(url);
|
|
1983
|
-
}
|
|
1984
|
-
// Focus the main window
|
|
1985
|
-
if (mainWindow) {
|
|
1986
|
-
if (mainWindow.isMinimized()) mainWindow.restore();
|
|
1987
|
-
mainWindow.focus();
|
|
1988
|
-
}
|
|
1989
|
-
});
|
|
1990
|
-
}
|
|
1991
|
-
|
|
1992
|
-
app.whenReady().then(async () => {
|
|
1993
|
-
log('Electron app ready');
|
|
1994
|
-
|
|
1995
|
-
// Start session manager (handles auth heartbeat + auto-updater token)
|
|
1996
|
-
sessionManager.setLogger(log);
|
|
1997
|
-
if (isAuthenticated()) {
|
|
1998
|
-
sessionManager.start();
|
|
1999
|
-
}
|
|
2000
|
-
|
|
2001
|
-
// Check for updates in the background (only in packaged app)
|
|
2002
|
-
if (isPackaged && isAuthenticated()) {
|
|
2003
|
-
const headers = sessionManager.getUpdaterHeaders();
|
|
2004
|
-
if (headers.Authorization) {
|
|
2005
|
-
autoUpdater.requestHeaders = headers;
|
|
2006
|
-
log('[AutoUpdate] Checking for updates with JWT...');
|
|
2007
|
-
try {
|
|
2008
|
-
await autoUpdater.checkForUpdatesAndNotify();
|
|
2009
|
-
} catch (error) {
|
|
2010
|
-
log(`[AutoUpdate] Update check failed: ${error.message}`);
|
|
2011
|
-
}
|
|
2012
|
-
} else {
|
|
2013
|
-
log('[AutoUpdate] Skipping update check - no auth token');
|
|
2014
|
-
}
|
|
2015
|
-
}
|
|
2016
|
-
|
|
2017
|
-
// Check if Claude Code is installed (for dependency flow)
|
|
2018
|
-
// Updates the global claudeCodeInstalled variable
|
|
2019
|
-
claudeCodeInstalled = checkClaudeCodeInstalled();
|
|
2020
|
-
log(`Claude Code installed: ${claudeCodeInstalled}`);
|
|
2021
|
-
|
|
2022
|
-
// Check if Claude Code is authenticated (only if installed)
|
|
2023
|
-
// Wrapped in try/catch so a catastrophic failure here doesn't crash startup
|
|
2024
|
-
if (claudeCodeInstalled) {
|
|
2025
|
-
try {
|
|
2026
|
-
claudeCodeAuthenticated = checkClaudeCodeAuthenticated();
|
|
2027
|
-
} catch (err) {
|
|
2028
|
-
log(`[ClaudeCode] Auth check crashed unexpectedly: ${err.message}`);
|
|
2029
|
-
claudeCodeAuthenticated = false;
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
// Auto-update if CLI is outdated, then retry auth check
|
|
2033
|
-
if (!claudeCodeAuthenticated && claudeCodeNeedsUpdate) {
|
|
2034
|
-
log('[ClaudeCode] CLI outdated, running auto-update...');
|
|
2035
|
-
const updateResult = await updateClaudeCode();
|
|
2036
|
-
if (updateResult.success) {
|
|
2037
|
-
log('[ClaudeCode] Auto-update succeeded, retrying auth check');
|
|
2038
|
-
claudeCodeNeedsUpdate = false;
|
|
2039
|
-
try {
|
|
2040
|
-
claudeCodeAuthenticated = checkClaudeCodeAuthenticated();
|
|
2041
|
-
} catch (err) {
|
|
2042
|
-
log(`[ClaudeCode] Auth re-check crashed: ${err.message}`);
|
|
2043
|
-
claudeCodeAuthenticated = false;
|
|
2044
|
-
}
|
|
2045
|
-
} else {
|
|
2046
|
-
log(`[ClaudeCode] Auto-update failed: ${updateResult.error}`);
|
|
2047
|
-
}
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
|
-
log(`Claude Code authenticated: ${claudeCodeAuthenticated}`);
|
|
2051
|
-
}
|
|
2052
|
-
|
|
2053
|
-
// Setup PATH on first launch (creates symlink to /usr/local/bin)
|
|
2054
|
-
await setupPathOnFirstLaunch();
|
|
2055
|
-
|
|
2056
|
-
// Sync bundled skills to ~/.claude/skills/
|
|
2057
|
-
syncSkills();
|
|
2058
|
-
|
|
2059
|
-
// Set up application menu
|
|
2060
|
-
const menu = buildApplicationMenu();
|
|
2061
|
-
Menu.setApplicationMenu(menu);
|
|
2062
|
-
log('Application menu set');
|
|
2063
|
-
|
|
2064
|
-
// Register IPC handlers for database operations
|
|
2065
|
-
registerIpcHandlers();
|
|
2066
|
-
|
|
2067
|
-
// Clean up stale resources from previous runs (lock files, orphaned processes)
|
|
2068
|
-
cleanupStaleResources();
|
|
2069
|
-
|
|
2070
|
-
try {
|
|
2071
|
-
// Start servers in parallel
|
|
2072
|
-
await Promise.all([
|
|
2073
|
-
startNextServer(),
|
|
2074
|
-
startEmbeddedWebSocketServer()
|
|
2075
|
-
]);
|
|
2076
|
-
|
|
2077
|
-
// Small delay to ensure servers are fully ready
|
|
2078
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
2079
|
-
|
|
2080
|
-
createWindow();
|
|
2081
|
-
} catch (err) {
|
|
2082
|
-
log(`Failed to start servers: ${err.message}`);
|
|
2083
|
-
app.quit();
|
|
2084
|
-
}
|
|
2085
|
-
});
|
|
2086
|
-
|
|
2087
|
-
app.on('window-all-closed', () => {
|
|
2088
|
-
log('All windows closed');
|
|
2089
|
-
cleanup();
|
|
2090
|
-
app.quit();
|
|
2091
|
-
});
|
|
2092
|
-
|
|
2093
|
-
app.on('before-quit', () => {
|
|
2094
|
-
log('App quitting...');
|
|
2095
|
-
cleanup();
|
|
2096
|
-
});
|
|
2097
|
-
|
|
2098
|
-
// Handle Ctrl+C in terminal
|
|
2099
|
-
process.on('SIGINT', () => {
|
|
2100
|
-
log('Received SIGINT');
|
|
2101
|
-
cleanup();
|
|
2102
|
-
app.quit();
|
|
2103
|
-
});
|
|
2104
|
-
|
|
2105
|
-
process.on('SIGTERM', () => {
|
|
2106
|
-
log('Received SIGTERM');
|
|
2107
|
-
cleanup();
|
|
2108
|
-
app.quit();
|
|
2109
|
-
});
|
|
2110
|
-
|
|
2111
|
-
// Handle uncaught exceptions - ensure cleanup runs even on crashes
|
|
2112
|
-
process.on('uncaughtException', (err) => {
|
|
2113
|
-
log(`Uncaught exception: ${err.message}`);
|
|
2114
|
-
log(err.stack);
|
|
2115
|
-
cleanup();
|
|
2116
|
-
app.quit();
|
|
2117
|
-
});
|
|
2118
|
-
|
|
2119
|
-
// Handle unhandled promise rejections
|
|
2120
|
-
process.on('unhandledRejection', (reason, promise) => {
|
|
2121
|
-
log(`Unhandled rejection at: ${promise}, reason: ${reason}`);
|
|
2122
|
-
cleanup();
|
|
2123
|
-
app.quit();
|
|
2124
|
-
});
|