diff --git a/packages/app/package.json b/packages/app/package.json index 68d5e2d71..a47191801 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -5,8 +5,8 @@ "scripts": { "start": "expo start", "reset-project": "node ./scripts/reset-project.js", - "android": "ANDROID_VARIANT=${ANDROID_VARIANT:-devDebug} expo run:android --variant $ANDROID_VARIANT", - "android:release": "ANDROID_VARIANT=${ANDROID_VARIANT:-productionRelease} expo run:android --variant $ANDROID_VARIANT", + "android": "ANDROID_VARIANT=${ANDROID_VARIANT:-devDebug} expo run:android --variant=${ANDROID_VARIANT:-devDebug}", + "android:release": "ANDROID_VARIANT=${ANDROID_VARIANT:-productionRelease} expo run:android --variant=${ANDROID_VARIANT:-productionRelease}", "ios": "expo run:ios", "ios:release": "expo run:ios --configuration Release", "web": "expo start --web", diff --git a/packages/app/src/contexts/daemon-registry-context.tsx b/packages/app/src/contexts/daemon-registry-context.tsx index e2c3e4720..b011b74b0 100644 --- a/packages/app/src/contexts/daemon-registry-context.tsx +++ b/packages/app/src/contexts/daemon-registry-context.tsx @@ -6,6 +6,10 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; const REGISTRY_STORAGE_KEY = "@paseo:daemon-registry"; const LEGACY_SETTINGS_KEY = "@paseo:settings"; const FALLBACK_DAEMON_URL = "ws://localhost:6767/ws"; +const DEFAULT_DAEMONS: Array<{ label: string; wsUrl: string; restUrl?: string | null }> = [ + { label: "dev", wsUrl: "ws://dev:6767/ws" }, + { label: "macbook", wsUrl: "ws://mohameds-macbook-pro:6767/ws" }, +]; const DAEMON_REGISTRY_QUERY_KEY = ["daemon-registry"]; export type DaemonProfile = { @@ -221,7 +225,9 @@ async function loadDaemonRegistryFromStorage(): Promise { return envDefaults; } - const fallback = [createProfile("Local Host", FALLBACK_DAEMON_URL)]; + const fallback = DEFAULT_DAEMONS.length > 0 + ? DEFAULT_DAEMONS.map((entry) => createProfile(entry.label, entry.wsUrl)) + : [createProfile("Local Host", FALLBACK_DAEMON_URL)]; await AsyncStorage.setItem(REGISTRY_STORAGE_KEY, JSON.stringify(fallback)); return fallback; } catch (error) { diff --git a/packages/server/package.json b/packages/server/package.json index c6e9620a5..d65086202 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -4,7 +4,7 @@ "description": "Paseo backend server", "type": "module", "scripts": { - "dev": "NODE_ENV=development node --import tsx src/server/index.ts", + "dev": "NODE_ENV=development tsx scripts/dev-runner.ts", "dev:tsx": "NODE_ENV=development tsx watch --ignore '**/*.timestamp-*' src/server/index.ts", "build": "tsc -p tsconfig.server.json", "start": "NODE_ENV=production node dist/server/index.js", diff --git a/packages/server/scripts/dev-runner.ts b/packages/server/scripts/dev-runner.ts new file mode 100644 index 000000000..00cd4c234 --- /dev/null +++ b/packages/server/scripts/dev-runner.ts @@ -0,0 +1,60 @@ +import { fork, type ChildProcess } from "child_process"; +import { fileURLToPath } from "url"; +import path from "path"; + +const serverEntry = fileURLToPath( + new URL("../src/server/index.ts", import.meta.url) +); + +let child: ChildProcess | null = null; +let restarting = false; + +function spawnServer() { + child = fork(serverEntry, { + stdio: "inherit", + env: process.env, + execArgv: ["--import", "tsx"], + }); + + child.on("message", (msg: any) => { + if (msg?.type === "paseo:restart") { + restartServer(); + } + }); + + child.on("exit", (code, signal) => { + if (restarting) { + restarting = false; + spawnServer(); + return; + } + + const exitDescriptor = + signal ?? (typeof code === "number" ? `code ${code}` : "unknown"); + console.warn(`[DevRunner] Server exited (${exitDescriptor}). Shutting down.`); + process.exit(typeof code === "number" ? code : 0); + }); +} + +function restartServer() { + if (!child || restarting) { + return; + } + + restarting = true; + console.warn("[DevRunner] Restart requested. Stopping current server..."); + child.kill("SIGTERM"); +} + +function forwardSignal(signal: NodeJS.Signals) { + if (!child) { + process.exit(0); + } + child.kill(signal); +} + +process.on("SIGINT", () => forwardSignal("SIGINT")); +process.on("SIGTERM", () => forwardSignal("SIGTERM")); + +console.log("[DevRunner] Starting server with tsx (explicit restarts only)"); +spawnServer(); diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index fe72b9206..99b95a4cf 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -1,6 +1,6 @@ import { v4 as uuidv4 } from "uuid"; import { readFile, mkdir, writeFile } from "fs/promises"; -import { exec } from "child_process"; +import { exec, spawn } from "child_process"; import { promisify, inspect } from "util"; import { join } from "path"; import invariant from "tiny-invariant"; @@ -75,6 +75,7 @@ const execAsync = promisify(exec); const ACTIVE_TITLE_GENERATIONS = new Set(); let restartRequested = false; const KNOWN_AGENT_PROVIDERS: AgentProvider[] = ["claude", "codex"]; +const RESTART_EXIT_DELAY_MS = 250; type ProcessingPhase = "idle" | "transcribing" | "llm"; @@ -886,9 +887,17 @@ export class Session { payload, }); + if (typeof process.send === "function") { + process.send({ + type: "paseo:restart", + ...(reason ? { reason } : {}), + }); + return; + } + setTimeout(() => { process.exit(0); - }, 250); + }, RESTART_EXIT_DELAY_MS); } private async handleDeleteAgentRequest(agentId: string): Promise {