@documentation-robotics/cli 0.1.7 → 0.1.8

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/dist/cli.js CHANGED
@@ -47,7 +47,7 @@ async function extractExitCode(error) {
47
47
  }
48
48
  return 1;
49
49
  }
50
- const cliVersion = "0.1.7", program = new Command();
50
+ const cliVersion = "0.1.8", program = new Command();
51
51
  (process.argv.includes("--version") || process.argv.includes("-V")) && (process.argv = ["node", "dr", "version"]), program.name("dr").description("Documentation Robotics CLI - Architecture Model Management").option("-v, --verbose", "Enable verbose output").option("--debug", "Enable debug mode").exitOverride().showSuggestionAfterError().hook("preAction", async (thisCommand) => {
52
52
  const options = thisCommand.opts();
53
53
  setGlobalOptions({
@@ -2,7 +2,7 @@ import ansis from "ansis";
2
2
  import { getCliBundledSpecVersion } from "../utils/spec-version.js";
3
3
  import { isTelemetryBuiltIn, isTelemetryConfigured } from "../telemetry/detect.js";
4
4
  import { getSpecReferencePath, getModelPath } from "../utils/project-paths.js";
5
- const gitHash = "6bb0c1f", cliVersion = "0.1.7";
5
+ const gitHash = "f413188", cliVersion = "0.1.8";
6
6
  async function versionCommand() {
7
7
  const specVersion = getCliBundledSpecVersion(), telemetryBuiltIn = isTelemetryBuiltIn(), telemetryConfigured = await isTelemetryConfigured();
8
8
  let telemetryStatus;
@@ -56,7 +56,7 @@ async function visualizeCommand(options) {
56
56
  DR_WITH_DANGER: String(withDanger)
57
57
  }, { isDebug: getDebugState, isVerbose } = await import("../utils/globals.js");
58
58
  getDebugState() && (env.DEBUG = "1", env.DR_TELEMETRY_DEBUG = "1", logDebug("Passing DEBUG=1 and DR_TELEMETRY_DEBUG=1 to subprocess")), isVerbose() && (env.VERBOSE = "1", logDebug("Passing VERBOSE=1 to subprocess")), authToken && (env.DR_AUTH_TOKEN = authToken), options.viewerPath && (env.DR_VIEWER_PATH = options.viewerPath, logDebug(`Using custom viewer from: ${options.viewerPath}`));
59
- const serverProcess = spawn("bun", ["run", serverEntryPath], {
59
+ const serverProcess = spawn(process.execPath, [serverEntryPath], {
60
60
  cwd: process.cwd(),
61
61
  stdio: ["pipe", "pipe", "pipe"],
62
62
  env
@@ -131,13 +131,7 @@ ${serverOutput}` : `Server exited with code ${code}`;
131
131
  }), serverProcess.on("error", (error) => {
132
132
  const errorCode = error.code;
133
133
  let message, suggestions;
134
- errorCode === "ENOENT" ? (message = "dr visualize requires the Bun runtime, which was not found on this system.", suggestions = [
135
- "Install Bun: https://bun.sh/docs/installation",
136
- " curl -fsSL https://bun.sh/install | bash (macOS/Linux)",
137
- ' powershell -c "irm bun.sh/install.ps1 | iex" (Windows)',
138
- "After installing, make sure `bun` is on your PATH, then re-run `dr visualize`.",
139
- "Tracking issue: https://github.com/tinkermonkey/documentation_robotics/issues/800"
140
- ]) : errorCode === "EACCES" ? (message = "dr visualize could not run the Bun runtime: permission denied.", suggestions = ["Check that the `bun` executable on your PATH is executable (chmod +x)."]) : message = `Failed to start visualization server: ${error.message}`, isTelemetryEnabled && serverStartupSpan && (async () => {
134
+ errorCode === "EACCES" ? (message = `dr visualize could not run the visualization server: permission denied executing ${process.execPath}.`, suggestions = [`Check that ${process.execPath} is executable (chmod +x).`]) : message = `Failed to start visualization server: ${error.message}`, isTelemetryEnabled && serverStartupSpan && (async () => {
141
135
  const { endSpan } = await import("../telemetry/index.js");
142
136
  "recordException" in serverStartupSpan && (serverStartupSpan.recordException(error), serverStartupSpan.setStatus({ code: 2, message })), endSpan(serverStartupSpan), serverStartupSpan = null;
143
137
  })();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/commands/visualize.ts"],
4
- "sourcesContent": ["/**\n * Visualize command - Launch visualization server\n */\n\nimport ansis from \"ansis\"\nimport { spawn } from \"child_process\"\nimport { fileURLToPath } from \"node:url\"\nimport { dirname } from \"node:path\"\nimport { Model } from \"../core/model.js\"\nimport { logVerbose, logDebug } from \"../utils/globals.js\"\nimport { CLIError } from \"../utils/errors.js\"\nimport { getSpecReferencePath, getModelPath } from \"../utils/project-paths.js\"\nimport { getErrorMessage } from \"../utils/errors.js\"\n\n// Conditional telemetry import based on compile-time flag\ndeclare const TELEMETRY_ENABLED: boolean | undefined\nconst isTelemetryEnabled = typeof TELEMETRY_ENABLED !== \"undefined\" ? TELEMETRY_ENABLED : false\n\nexport interface VisualizeOptions {\n port?: number\n noBrowser?: boolean\n noAuth?: boolean\n token?: string\n verbose?: boolean\n debug?: boolean\n withDanger?: boolean\n viewerPath?: string\n}\n\nasync function findAvailablePort(startPort: number): Promise<number> {\n const { createServer } = await import(\"net\")\n for (let port = startPort; port <= 65535; port++) {\n const available = await new Promise<boolean>((resolve) => {\n const server = createServer()\n server.once(\"error\", () => resolve(false))\n server.once(\"listening\", () => { server.close(); resolve(true) })\n server.listen(port, \"127.0.0.1\")\n })\n if (available) return port\n }\n throw new Error(\"No available ports found\")\n}\n\nexport async function visualizeCommand(\n options: VisualizeOptions & { port?: string }\n): Promise<void> {\n try {\n logDebug(\"Loading model for validation...\")\n\n // Load model to validate it exists (without full content to be quick)\n const model = await Model.load(process.cwd(), { lazyLoad: true })\n\n logVerbose(`Model validated: ${model.manifest.name}`)\n\n // Parse and validate port\n let port = 8080\n if (options.port) {\n const portNum = parseInt(String(options.port), 10)\n if (isNaN(portNum) || portNum < 1 || portNum > 65535) {\n throw new Error(`Invalid port number: ${options.port}. Port must be between 1 and 65535.`)\n }\n port = portNum\n }\n\n // Find an available port starting from the requested port\n port = await findAvailablePort(port)\n if (port !== (options.port ? parseInt(String(options.port), 10) : 8080)) {\n console.log(ansis.yellow(` Port ${options.port ?? 8080} is in use, using port ${port} instead`))\n }\n\n // Determine auth settings\n const authEnabled = !options.noAuth // Auth enabled by default unless --no-auth flag\n const authToken = options.token // Optional token for testing\n const withDanger = options.withDanger || false // Danger mode disabled by default\n\n logDebug(`Starting visualization server on port ${port}`)\n logDebug(`Authentication: ${authEnabled ? \"enabled\" : \"disabled\"}`)\n if (authEnabled && authToken) {\n logDebug(`Using provided token for authentication`)\n } else if (authEnabled) {\n logDebug(`Token will be auto-generated`)\n }\n if (withDanger) {\n logDebug(`Danger mode enabled - chat permissions will be skipped`)\n }\n\n // Get the directory where this command file is located (dist/commands/)\n const commandDir = dirname(fileURLToPath(import.meta.url))\n const distDir = dirname(commandDir) // Go up to dist/\n const serverEntryPath = `${distDir}/server-entry.js`\n\n logDebug(`Server entry path: ${serverEntryPath}`)\n\n // Create telemetry span for server startup\n let serverStartupSpan: any = null\n if (isTelemetryEnabled) {\n logDebug(\"[Telemetry] Creating visualize.server.startup span\")\n const { startSpan } = await import(\"../telemetry/index.js\")\n serverStartupSpan = startSpan(\"visualize.server.startup\", {\n \"server.port\": port,\n \"server.auth_enabled\": authEnabled,\n \"server.with_danger\": withDanger,\n })\n logDebug(`[Telemetry] Span created: ${serverStartupSpan ? \"success\" : \"failed\"}`)\n } else {\n logDebug(\"[Telemetry] TELEMETRY_ENABLED is false - no spans will be created\")\n }\n\n // Spawn server in Bun subprocess with environment variables\n const env: Record<string, string> = {\n ...process.env,\n DR_VISUALIZE_PORT: String(port),\n DR_PROJECT_PATH: process.cwd(),\n DR_AUTH_ENABLED: String(authEnabled),\n DR_WITH_DANGER: String(withDanger),\n }\n\n // Pass debug/verbose flags to subprocess\n // The CLI --debug flag sets globalOptions.debug, but subprocess checks process.env.DEBUG\n const { isDebug: getDebugState, isVerbose } = await import(\"../utils/globals.js\")\n if (getDebugState()) {\n env.DEBUG = \"1\"\n env.DR_TELEMETRY_DEBUG = \"1\" // Enable telemetry exporter debug logging\n logDebug(\"Passing DEBUG=1 and DR_TELEMETRY_DEBUG=1 to subprocess\")\n }\n if (isVerbose()) {\n env.VERBOSE = \"1\"\n logDebug(\"Passing VERBOSE=1 to subprocess\")\n }\n\n // Only set auth token if provided\n if (authToken) {\n env.DR_AUTH_TOKEN = authToken\n }\n\n // Set custom viewer path if provided\n if (options.viewerPath) {\n env.DR_VIEWER_PATH = options.viewerPath\n logDebug(`Using custom viewer from: ${options.viewerPath}`)\n }\n\n const serverProcess = spawn(\"bun\", [\"run\", serverEntryPath], {\n cwd: process.cwd(),\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n env,\n })\n\n let authTokenFromServer = \"\"\n let serverOutput = \"\"\n let serverStartupShown = false\n\n // Capture output from server process\n if (serverProcess.stdout) {\n serverProcess.stdout.on(\"data\", async (data) => {\n const chunk = data.toString()\n serverOutput += chunk\n\n // Process each line individually: Bun's pipe buffering often batches multiple\n // console.log calls (e.g. \"running at\" + \"TOKEN:\") into a single data event.\n // Using startsWith/substring on the whole chunk would either miss the TOKEN line\n // (when \"running at\" is first) or extract a malformed token (when trailing lines\n // are appended), causing the browser to open with the wrong token and get a 403.\n const lines = chunk.split(\"\\n\")\n for (const rawLine of lines) {\n const line = rawLine.trim()\n if (!line) continue\n\n // Extract token from output\n if (line.startsWith(\"TOKEN:\")) {\n authTokenFromServer = line.substring(6)\n logDebug(`Received auth token from server: ${authTokenFromServer.substring(0, 8)}...`)\n\n // If we haven't shown startup info yet and now have the token, show it\n if (serverStartupShown && authEnabled && authTokenFromServer) {\n // Update the URL display with actual token\n console.log(\n ansis.cyan(` Access URL: http://localhost:${port}?token=${authTokenFromServer}`)\n )\n console.log(ansis.dim(` Auth token: ${authTokenFromServer}`))\n\n // Now that we have the token, open browser if needed\n if (!options.noBrowser) {\n try {\n const command = getOpenCommand()\n const url = `http://localhost:${port}?token=${authTokenFromServer}`\n logDebug(`Opening browser with command: ${command} ${url}`)\n spawn(command, [url], {\n detached: true,\n stdio: \"ignore\",\n })\n } catch (error) {\n logVerbose(\"Could not auto-open browser (not critical)\")\n }\n }\n }\n }\n\n // Server started message - defer detailed output if auth enabled\n if (line.includes(\"running at http://localhost\")) {\n serverStartupShown = true\n\n // End telemetry span for server startup\n if (isTelemetryEnabled && serverStartupSpan) {\n logDebug(\n \"[Telemetry] Ending visualize.server.startup span (server started successfully)\"\n )\n const { endSpan } = await import(\"../telemetry/index.js\")\n endSpan(serverStartupSpan)\n serverStartupSpan = null\n logDebug(\"[Telemetry] Span ended\")\n }\n\n console.log(ansis.green(`\u2713 Visualization server started`))\n\n // Display access information (but may be incomplete if auth enabled and token not yet received)\n if (authEnabled) {\n const token = authTokenFromServer || authToken || \"(pending)\"\n if (authTokenFromServer) {\n // Token already received (same chunk), show full info immediately\n console.log(ansis.cyan(` Access URL: http://localhost:${port}?token=${token}`))\n console.log(ansis.dim(` Auth token: ${token}`))\n } else {\n // Token not yet received, just show port for now\n console.log(ansis.dim(` Open http://localhost:${port} in your browser`))\n logDebug(` (waiting for auth token...)`)\n }\n } else {\n console.log(ansis.dim(` Open http://localhost:${port} in your browser`))\n console.log(ansis.yellow(` \u26A0 Authentication disabled`))\n }\n\n if (withDanger) {\n console.log(\n ansis.yellow(` \u26A0 Danger mode enabled - chat permissions will be skipped`)\n )\n }\n\n console.log(ansis.blueBright(`Model name: ${model.manifest.name}`))\n const drFolder = await getSpecReferencePath()\n console.log(\n `DR folder: ${ansis.cyan(drFolder || \"Not found\")}`\n )\n console.log(`Model path: ${ansis.cyan((await getModelPath()) || \"Not found\")}`)\n\n // Optionally open browser\n // If auth is enabled, delay opening until we have the token\n const shouldOpenBrowser = !options.noBrowser\n logDebug(\n `Browser auto-open: ${shouldOpenBrowser ? \"enabled\" : \"disabled\"} (noBrowser=${options.noBrowser})`\n )\n\n if (shouldOpenBrowser && !authEnabled) {\n // Auth disabled: open immediately\n try {\n const command = getOpenCommand()\n const url = `http://localhost:${port}`\n logDebug(`Opening browser with command: ${command} ${url}`)\n spawn(command, [url], {\n detached: true,\n stdio: \"ignore\",\n })\n } catch (error) {\n logVerbose(\"Could not auto-open browser (not critical)\")\n }\n } else if (shouldOpenBrowser && authEnabled && !authTokenFromServer) {\n // Auth enabled but token not yet in this chunk: will open when TOKEN line is processed\n logDebug(\"Browser auto-open deferred until token is received\")\n } else {\n logDebug(\"Browser auto-open disabled\")\n }\n }\n\n // Log other output\n if (!line.startsWith(\"TOKEN:\")) {\n logDebug(`[server] ${line}`)\n }\n }\n })\n }\n\n // Capture stderr\n if (serverProcess.stderr) {\n serverProcess.stderr.on(\"data\", (data) => {\n const output = data.toString().trim()\n if (output) {\n serverOutput += output + \"\\n\"\n // Log stderr at debug level so --debug flag shows telemetry exporter logs\n logDebug(`[server stderr] ${output}`)\n }\n })\n }\n\n // Handle process close (fires after all stdio streams have closed)\n let rejectKeepAlive: ((reason: Error) => void) | null = null\n\n serverProcess.on(\"close\", (code) => {\n if (code !== 0) {\n // Include server output in error message if available (avoid separate logDebug to prevent minification issues)\n const message = serverOutput\n ? `Server exited with code ${code}\\nServer output:\\n${serverOutput}`\n : `Server exited with code ${code}`\n\n // Record error in startup span if still active\n if (isTelemetryEnabled && serverStartupSpan) {\n (async () => {\n const { endSpan } = await import(\"../telemetry/index.js\")\n if (\"recordException\" in serverStartupSpan) {\n (serverStartupSpan as any).recordException(new Error(message));\n (serverStartupSpan as any).setStatus({ code: 2, message }) // ERROR\n }\n endSpan(serverStartupSpan)\n serverStartupSpan = null\n })()\n }\n\n // Print error before rejecting (CLI wrapper expects CLIError messages to be pre-printed)\n console.error(message)\n const error = new CLIError(message, code || 1)\n if (rejectKeepAlive) {\n rejectKeepAlive(error)\n }\n }\n })\n\n // Handle process errors\n serverProcess.on(\"error\", (error) => {\n const errorCode = (error as NodeJS.ErrnoException).code\n let message: string\n let suggestions: string[] | undefined\n\n if (errorCode === \"ENOENT\") {\n // `dr visualize` currently spawns its server as a Bun subprocess (see #800 for\n // the plan to remove this dependency). On a fresh `npm install -g` there's no\n // reason Bun would be present, so this is an expected, actionable failure mode\n // rather than an unexpected crash - explain it instead of surfacing the raw\n // \"spawn bun ENOENT\".\n message = \"dr visualize requires the Bun runtime, which was not found on this system.\"\n suggestions = [\n \"Install Bun: https://bun.sh/docs/installation\",\n \" curl -fsSL https://bun.sh/install | bash (macOS/Linux)\",\n \" powershell -c \\\"irm bun.sh/install.ps1 | iex\\\" (Windows)\",\n \"After installing, make sure `bun` is on your PATH, then re-run `dr visualize`.\",\n \"Tracking issue: https://github.com/tinkermonkey/documentation_robotics/issues/800\",\n ]\n } else if (errorCode === \"EACCES\") {\n message = \"dr visualize could not run the Bun runtime: permission denied.\"\n suggestions = [\"Check that the `bun` executable on your PATH is executable (chmod +x).\"]\n } else {\n message = `Failed to start visualization server: ${error.message}`\n }\n\n // Record error in startup span if still active\n if (isTelemetryEnabled && serverStartupSpan) {\n (async () => {\n const { endSpan } = await import(\"../telemetry/index.js\")\n if (\"recordException\" in serverStartupSpan) {\n (serverStartupSpan as any).recordException(error);\n (serverStartupSpan as any).setStatus({ code: 2, message }) // ERROR\n }\n endSpan(serverStartupSpan)\n serverStartupSpan = null\n })()\n }\n\n // Don't print here: rejecting propagates this CLIError up through the catch block\n // below to the top-level CLI handler, which prints `.format()` (including\n // `suggestions`) exactly once. Printing here too would duplicate the whole block.\n const cliError = new CLIError(message, 1, suggestions)\n if (rejectKeepAlive) {\n rejectKeepAlive(cliError)\n }\n })\n\n // Setup periodic telemetry flushing for long-running command\n // Since the visualize command never completes (keep-alive pattern),\n // the cli.execute span would never end and export. We flush periodically\n // to ensure active spans are exported to the telemetry backend.\n let flushInterval: NodeJS.Timeout | null = null\n if (isTelemetryEnabled) {\n logDebug(\"[Telemetry] Setting up periodic flush (every 15 seconds)\")\n const { flushTelemetry } = await import(\"../telemetry/index.js\")\n\n flushInterval = setInterval(async () => {\n logDebug(\"[Telemetry] Periodic flush: flushing pending spans...\")\n try {\n await flushTelemetry()\n logDebug(\"[Telemetry] Periodic flush complete\")\n } catch (error) {\n // Silently ignore flush errors - telemetry should never break the app\n logDebug(`[Telemetry] Periodic flush failed: ${error}`)\n }\n }, 15000) // Flush every 15 seconds\n }\n\n // Handle graceful shutdown on SIGINT (Ctrl-C) and SIGTERM\n const handleShutdown = async (signal: string) => {\n logDebug(`\\n[${signal}] Shutting down visualization server gracefully...`)\n\n // Clear periodic flush interval\n if (flushInterval) {\n logDebug(\"[Telemetry] Stopping periodic flush\")\n clearInterval(flushInterval)\n flushInterval = null\n }\n\n // Forward signal to server process for graceful shutdown\n if (serverProcess && !serverProcess.killed) {\n logDebug(\"Sending termination signal to server process...\")\n serverProcess.kill(signal as NodeJS.Signals)\n\n // Wait for server to exit (with timeout)\n await new Promise<void>((resolve) => {\n const timeout = setTimeout(() => {\n logDebug(\"Server shutdown timeout, forcing exit...\")\n if (!serverProcess.killed) {\n serverProcess.kill(\"SIGKILL\")\n }\n resolve()\n }, 3000) // 3 second timeout\n\n serverProcess.on(\"exit\", () => {\n clearTimeout(timeout)\n resolve()\n })\n })\n }\n\n logDebug(\"Visualization server stopped\")\n process.exit(0)\n }\n\n // Register signal handlers\n process.on(\"SIGINT\", () => handleShutdown(\"SIGINT\"))\n process.on(\"SIGTERM\", () => handleShutdown(\"SIGTERM\"))\n\n // Keep parent process alive - reject promise if server fails\n await new Promise((_, reject) => {\n rejectKeepAlive = reject\n })\n } catch (error) {\n // Throw CLIError instead of process.exit to allow telemetry shutdown\n // Note: Avoid side effects (like logDebug) before throw to prevent minification issues\n if (error instanceof CLIError) {\n // CLIError should have already printed its message\n throw error\n }\n // Non-CLIError needs to be printed and wrapped\n const message = getErrorMessage(error)\n console.error(message)\n throw new CLIError(message, 1)\n }\n}\n\n/**\n * Get the appropriate command to open a URL\n */\nfunction getOpenCommand(): string {\n const platform = process.platform\n\n if (platform === \"darwin\") {\n return \"open\"\n }\n if (platform === \"win32\") {\n return \"start\"\n }\n // Linux and others\n return \"xdg-open\"\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Visualize command - Launch visualization server\n */\n\nimport ansis from \"ansis\"\nimport { spawn } from \"child_process\"\nimport { fileURLToPath } from \"node:url\"\nimport { dirname } from \"node:path\"\nimport { Model } from \"../core/model.js\"\nimport { logVerbose, logDebug } from \"../utils/globals.js\"\nimport { CLIError } from \"../utils/errors.js\"\nimport { getSpecReferencePath, getModelPath } from \"../utils/project-paths.js\"\nimport { getErrorMessage } from \"../utils/errors.js\"\n\n// Conditional telemetry import based on compile-time flag\ndeclare const TELEMETRY_ENABLED: boolean | undefined\nconst isTelemetryEnabled = typeof TELEMETRY_ENABLED !== \"undefined\" ? TELEMETRY_ENABLED : false\n\nexport interface VisualizeOptions {\n port?: number\n noBrowser?: boolean\n noAuth?: boolean\n token?: string\n verbose?: boolean\n debug?: boolean\n withDanger?: boolean\n viewerPath?: string\n}\n\nasync function findAvailablePort(startPort: number): Promise<number> {\n const { createServer } = await import(\"net\")\n for (let port = startPort; port <= 65535; port++) {\n const available = await new Promise<boolean>((resolve) => {\n const server = createServer()\n server.once(\"error\", () => resolve(false))\n server.once(\"listening\", () => { server.close(); resolve(true) })\n server.listen(port, \"127.0.0.1\")\n })\n if (available) return port\n }\n throw new Error(\"No available ports found\")\n}\n\nexport async function visualizeCommand(\n options: VisualizeOptions & { port?: string }\n): Promise<void> {\n try {\n logDebug(\"Loading model for validation...\")\n\n // Load model to validate it exists (without full content to be quick)\n const model = await Model.load(process.cwd(), { lazyLoad: true })\n\n logVerbose(`Model validated: ${model.manifest.name}`)\n\n // Parse and validate port\n let port = 8080\n if (options.port) {\n const portNum = parseInt(String(options.port), 10)\n if (isNaN(portNum) || portNum < 1 || portNum > 65535) {\n throw new Error(`Invalid port number: ${options.port}. Port must be between 1 and 65535.`)\n }\n port = portNum\n }\n\n // Find an available port starting from the requested port\n port = await findAvailablePort(port)\n if (port !== (options.port ? parseInt(String(options.port), 10) : 8080)) {\n console.log(ansis.yellow(` Port ${options.port ?? 8080} is in use, using port ${port} instead`))\n }\n\n // Determine auth settings\n const authEnabled = !options.noAuth // Auth enabled by default unless --no-auth flag\n const authToken = options.token // Optional token for testing\n const withDanger = options.withDanger || false // Danger mode disabled by default\n\n logDebug(`Starting visualization server on port ${port}`)\n logDebug(`Authentication: ${authEnabled ? \"enabled\" : \"disabled\"}`)\n if (authEnabled && authToken) {\n logDebug(`Using provided token for authentication`)\n } else if (authEnabled) {\n logDebug(`Token will be auto-generated`)\n }\n if (withDanger) {\n logDebug(`Danger mode enabled - chat permissions will be skipped`)\n }\n\n // Get the directory where this command file is located (dist/commands/)\n const commandDir = dirname(fileURLToPath(import.meta.url))\n const distDir = dirname(commandDir) // Go up to dist/\n const serverEntryPath = `${distDir}/server-entry.js`\n\n logDebug(`Server entry path: ${serverEntryPath}`)\n\n // Create telemetry span for server startup\n let serverStartupSpan: any = null\n if (isTelemetryEnabled) {\n logDebug(\"[Telemetry] Creating visualize.server.startup span\")\n const { startSpan } = await import(\"../telemetry/index.js\")\n serverStartupSpan = startSpan(\"visualize.server.startup\", {\n \"server.port\": port,\n \"server.auth_enabled\": authEnabled,\n \"server.with_danger\": withDanger,\n })\n logDebug(`[Telemetry] Span created: ${serverStartupSpan ? \"success\" : \"failed\"}`)\n } else {\n logDebug(\"[Telemetry] TELEMETRY_ENABLED is false - no spans will be created\")\n }\n\n // Build environment variables for the server subprocess\n const env: Record<string, string> = {\n ...process.env,\n DR_VISUALIZE_PORT: String(port),\n DR_PROJECT_PATH: process.cwd(),\n DR_AUTH_ENABLED: String(authEnabled),\n DR_WITH_DANGER: String(withDanger),\n }\n\n // Pass debug/verbose flags to subprocess\n // The CLI --debug flag sets globalOptions.debug, but subprocess checks process.env.DEBUG\n const { isDebug: getDebugState, isVerbose } = await import(\"../utils/globals.js\")\n if (getDebugState()) {\n env.DEBUG = \"1\"\n env.DR_TELEMETRY_DEBUG = \"1\" // Enable telemetry exporter debug logging\n logDebug(\"Passing DEBUG=1 and DR_TELEMETRY_DEBUG=1 to subprocess\")\n }\n if (isVerbose()) {\n env.VERBOSE = \"1\"\n logDebug(\"Passing VERBOSE=1 to subprocess\")\n }\n\n // Only set auth token if provided\n if (authToken) {\n env.DR_AUTH_TOKEN = authToken\n }\n\n // Set custom viewer path if provided\n if (options.viewerPath) {\n env.DR_VIEWER_PATH = options.viewerPath\n logDebug(`Using custom viewer from: ${options.viewerPath}`)\n }\n\n // Reuse whichever runtime is currently executing the CLI (node or bun) to launch\n // the server subprocess. server-entry.ts has no Bun-specific dependencies (see #800),\n // so this works identically under Node.js or Bun \u2014 and unlike spawning a literal\n // \"bun\" binary, process.execPath is guaranteed to exist since it's already running.\n const serverProcess = spawn(process.execPath, [serverEntryPath], {\n cwd: process.cwd(),\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n env,\n })\n\n let authTokenFromServer = \"\"\n let serverOutput = \"\"\n let serverStartupShown = false\n\n // Capture output from server process\n if (serverProcess.stdout) {\n serverProcess.stdout.on(\"data\", async (data) => {\n const chunk = data.toString()\n serverOutput += chunk\n\n // Process each line individually: stdio pipe buffering can batch multiple\n // console.log calls (e.g. \"running at\" + \"TOKEN:\") into a single data event,\n // regardless of runtime.\n // Using startsWith/substring on the whole chunk would either miss the TOKEN line\n // (when \"running at\" is first) or extract a malformed token (when trailing lines\n // are appended), causing the browser to open with the wrong token and get a 403.\n const lines = chunk.split(\"\\n\")\n for (const rawLine of lines) {\n const line = rawLine.trim()\n if (!line) continue\n\n // Extract token from output\n if (line.startsWith(\"TOKEN:\")) {\n authTokenFromServer = line.substring(6)\n logDebug(`Received auth token from server: ${authTokenFromServer.substring(0, 8)}...`)\n\n // If we haven't shown startup info yet and now have the token, show it\n if (serverStartupShown && authEnabled && authTokenFromServer) {\n // Update the URL display with actual token\n console.log(\n ansis.cyan(` Access URL: http://localhost:${port}?token=${authTokenFromServer}`)\n )\n console.log(ansis.dim(` Auth token: ${authTokenFromServer}`))\n\n // Now that we have the token, open browser if needed\n if (!options.noBrowser) {\n try {\n const command = getOpenCommand()\n const url = `http://localhost:${port}?token=${authTokenFromServer}`\n logDebug(`Opening browser with command: ${command} ${url}`)\n spawn(command, [url], {\n detached: true,\n stdio: \"ignore\",\n })\n } catch (error) {\n logVerbose(\"Could not auto-open browser (not critical)\")\n }\n }\n }\n }\n\n // Server started message - defer detailed output if auth enabled\n if (line.includes(\"running at http://localhost\")) {\n serverStartupShown = true\n\n // End telemetry span for server startup\n if (isTelemetryEnabled && serverStartupSpan) {\n logDebug(\n \"[Telemetry] Ending visualize.server.startup span (server started successfully)\"\n )\n const { endSpan } = await import(\"../telemetry/index.js\")\n endSpan(serverStartupSpan)\n serverStartupSpan = null\n logDebug(\"[Telemetry] Span ended\")\n }\n\n console.log(ansis.green(`\u2713 Visualization server started`))\n\n // Display access information (but may be incomplete if auth enabled and token not yet received)\n if (authEnabled) {\n const token = authTokenFromServer || authToken || \"(pending)\"\n if (authTokenFromServer) {\n // Token already received (same chunk), show full info immediately\n console.log(ansis.cyan(` Access URL: http://localhost:${port}?token=${token}`))\n console.log(ansis.dim(` Auth token: ${token}`))\n } else {\n // Token not yet received, just show port for now\n console.log(ansis.dim(` Open http://localhost:${port} in your browser`))\n logDebug(` (waiting for auth token...)`)\n }\n } else {\n console.log(ansis.dim(` Open http://localhost:${port} in your browser`))\n console.log(ansis.yellow(` \u26A0 Authentication disabled`))\n }\n\n if (withDanger) {\n console.log(\n ansis.yellow(` \u26A0 Danger mode enabled - chat permissions will be skipped`)\n )\n }\n\n console.log(ansis.blueBright(`Model name: ${model.manifest.name}`))\n const drFolder = await getSpecReferencePath()\n console.log(\n `DR folder: ${ansis.cyan(drFolder || \"Not found\")}`\n )\n console.log(`Model path: ${ansis.cyan((await getModelPath()) || \"Not found\")}`)\n\n // Optionally open browser\n // If auth is enabled, delay opening until we have the token\n const shouldOpenBrowser = !options.noBrowser\n logDebug(\n `Browser auto-open: ${shouldOpenBrowser ? \"enabled\" : \"disabled\"} (noBrowser=${options.noBrowser})`\n )\n\n if (shouldOpenBrowser && !authEnabled) {\n // Auth disabled: open immediately\n try {\n const command = getOpenCommand()\n const url = `http://localhost:${port}`\n logDebug(`Opening browser with command: ${command} ${url}`)\n spawn(command, [url], {\n detached: true,\n stdio: \"ignore\",\n })\n } catch (error) {\n logVerbose(\"Could not auto-open browser (not critical)\")\n }\n } else if (shouldOpenBrowser && authEnabled && !authTokenFromServer) {\n // Auth enabled but token not yet in this chunk: will open when TOKEN line is processed\n logDebug(\"Browser auto-open deferred until token is received\")\n } else {\n logDebug(\"Browser auto-open disabled\")\n }\n }\n\n // Log other output\n if (!line.startsWith(\"TOKEN:\")) {\n logDebug(`[server] ${line}`)\n }\n }\n })\n }\n\n // Capture stderr\n if (serverProcess.stderr) {\n serverProcess.stderr.on(\"data\", (data) => {\n const output = data.toString().trim()\n if (output) {\n serverOutput += output + \"\\n\"\n // Log stderr at debug level so --debug flag shows telemetry exporter logs\n logDebug(`[server stderr] ${output}`)\n }\n })\n }\n\n // Handle process close (fires after all stdio streams have closed)\n let rejectKeepAlive: ((reason: Error) => void) | null = null\n\n serverProcess.on(\"close\", (code) => {\n if (code !== 0) {\n // Include server output in error message if available (avoid separate logDebug to prevent minification issues)\n const message = serverOutput\n ? `Server exited with code ${code}\\nServer output:\\n${serverOutput}`\n : `Server exited with code ${code}`\n\n // Record error in startup span if still active\n if (isTelemetryEnabled && serverStartupSpan) {\n (async () => {\n const { endSpan } = await import(\"../telemetry/index.js\")\n if (\"recordException\" in serverStartupSpan) {\n (serverStartupSpan as any).recordException(new Error(message));\n (serverStartupSpan as any).setStatus({ code: 2, message }) // ERROR\n }\n endSpan(serverStartupSpan)\n serverStartupSpan = null\n })()\n }\n\n // Print error before rejecting (CLI wrapper expects CLIError messages to be pre-printed)\n console.error(message)\n const error = new CLIError(message, code || 1)\n if (rejectKeepAlive) {\n rejectKeepAlive(error)\n }\n }\n })\n\n // Handle process errors\n serverProcess.on(\"error\", (error) => {\n const errorCode = (error as NodeJS.ErrnoException).code\n let message: string\n let suggestions: string[] | undefined\n\n // The server subprocess is launched via process.execPath (the same node/bun\n // binary already running this CLI), so it's always present - ENOENT here would\n // indicate something unusual about the runtime environment rather than a missing\n // dependency (see #800: this used to hard-depend on a separately installed `bun`).\n if (errorCode === \"EACCES\") {\n message = `dr visualize could not run the visualization server: permission denied executing ${process.execPath}.`\n suggestions = [`Check that ${process.execPath} is executable (chmod +x).`]\n } else {\n message = `Failed to start visualization server: ${error.message}`\n }\n\n // Record error in startup span if still active\n if (isTelemetryEnabled && serverStartupSpan) {\n (async () => {\n const { endSpan } = await import(\"../telemetry/index.js\")\n if (\"recordException\" in serverStartupSpan) {\n (serverStartupSpan as any).recordException(error);\n (serverStartupSpan as any).setStatus({ code: 2, message }) // ERROR\n }\n endSpan(serverStartupSpan)\n serverStartupSpan = null\n })()\n }\n\n // Don't print here: rejecting propagates this CLIError up through the catch block\n // below to the top-level CLI handler, which prints `.format()` (including\n // `suggestions`) exactly once. Printing here too would duplicate the whole block.\n const cliError = new CLIError(message, 1, suggestions)\n if (rejectKeepAlive) {\n rejectKeepAlive(cliError)\n }\n })\n\n // Setup periodic telemetry flushing for long-running command\n // Since the visualize command never completes (keep-alive pattern),\n // the cli.execute span would never end and export. We flush periodically\n // to ensure active spans are exported to the telemetry backend.\n let flushInterval: NodeJS.Timeout | null = null\n if (isTelemetryEnabled) {\n logDebug(\"[Telemetry] Setting up periodic flush (every 15 seconds)\")\n const { flushTelemetry } = await import(\"../telemetry/index.js\")\n\n flushInterval = setInterval(async () => {\n logDebug(\"[Telemetry] Periodic flush: flushing pending spans...\")\n try {\n await flushTelemetry()\n logDebug(\"[Telemetry] Periodic flush complete\")\n } catch (error) {\n // Silently ignore flush errors - telemetry should never break the app\n logDebug(`[Telemetry] Periodic flush failed: ${error}`)\n }\n }, 15000) // Flush every 15 seconds\n }\n\n // Handle graceful shutdown on SIGINT (Ctrl-C) and SIGTERM\n const handleShutdown = async (signal: string) => {\n logDebug(`\\n[${signal}] Shutting down visualization server gracefully...`)\n\n // Clear periodic flush interval\n if (flushInterval) {\n logDebug(\"[Telemetry] Stopping periodic flush\")\n clearInterval(flushInterval)\n flushInterval = null\n }\n\n // Forward signal to server process for graceful shutdown\n if (serverProcess && !serverProcess.killed) {\n logDebug(\"Sending termination signal to server process...\")\n serverProcess.kill(signal as NodeJS.Signals)\n\n // Wait for server to exit (with timeout)\n await new Promise<void>((resolve) => {\n const timeout = setTimeout(() => {\n logDebug(\"Server shutdown timeout, forcing exit...\")\n if (!serverProcess.killed) {\n serverProcess.kill(\"SIGKILL\")\n }\n resolve()\n }, 3000) // 3 second timeout\n\n serverProcess.on(\"exit\", () => {\n clearTimeout(timeout)\n resolve()\n })\n })\n }\n\n logDebug(\"Visualization server stopped\")\n process.exit(0)\n }\n\n // Register signal handlers\n process.on(\"SIGINT\", () => handleShutdown(\"SIGINT\"))\n process.on(\"SIGTERM\", () => handleShutdown(\"SIGTERM\"))\n\n // Keep parent process alive - reject promise if server fails\n await new Promise((_, reject) => {\n rejectKeepAlive = reject\n })\n } catch (error) {\n // Throw CLIError instead of process.exit to allow telemetry shutdown\n // Note: Avoid side effects (like logDebug) before throw to prevent minification issues\n if (error instanceof CLIError) {\n // CLIError should have already printed its message\n throw error\n }\n // Non-CLIError needs to be printed and wrapped\n const message = getErrorMessage(error)\n console.error(message)\n throw new CLIError(message, 1)\n }\n}\n\n/**\n * Get the appropriate command to open a URL\n */\nfunction getOpenCommand(): string {\n const platform = process.platform\n\n if (platform === \"darwin\") {\n return \"open\"\n }\n if (platform === \"win32\") {\n return \"start\"\n }\n // Linux and others\n return \"xdg-open\"\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,11 +1,14 @@
1
1
  import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
2
2
  import { cors } from "hono/cors";
3
3
  import { swaggerUI } from "@hono/swagger-ui";
4
+ import { serve } from "@hono/node-server";
5
+ import { createNodeWebSocket } from "@hono/node-ws";
6
+ import { spawn, spawnSync } from "child_process";
7
+ import chokidar from "chokidar";
4
8
  import { randomBytes } from "crypto";
5
9
  import { createRequire } from "module";
6
10
  import { fileURLToPath } from "url";
7
11
  import path from "path";
8
- let upgradeWebSocket, websocket;
9
12
  const require2 = createRequire(import.meta.url);
10
13
  import { Model } from "../core/model.js";
11
14
  import { StagedChangesetStorage } from "../core/staged-changeset-storage.js";
@@ -31,7 +34,9 @@ class VisualizationServer {
31
34
  app;
32
35
  model;
33
36
  _server;
34
- // Bun.serve return type, loaded dynamically
37
+ // Node HTTP server returned by @hono/node-server
38
+ upgradeWebSocket;
39
+ injectWebSocket;
35
40
  clients = /* @__PURE__ */ new Set();
36
41
  watcher;
37
42
  annotations = /* @__PURE__ */ new Map();
@@ -50,7 +55,7 @@ class VisualizationServer {
50
55
  viewerPath;
51
56
  // Optional custom viewer path
52
57
  activeChatProcesses = /* @__PURE__ */ new Map();
53
- // conversationId -> Bun.spawn process
58
+ // conversationId -> spawned chat process
54
59
  chatConversationCounter = 0;
55
60
  selectedChatClient;
56
61
  // Selected chat client for server
@@ -119,7 +124,7 @@ class VisualizationServer {
119
124
  }), ids;
120
125
  })();
121
126
  constructor(model, options) {
122
- this.app = new OpenAPIHono(), this.model = model, this.loadBunAdapters(), this.authEnabled = options?.authEnabled ?? process.env.DR_AUTH_ENABLED !== "false", this.authToken = options?.authToken || process.env.DR_AUTH_TOKEN || this.generateAuthToken(), this.withDanger = options?.withDanger || !1, this.viewerPath = options?.viewerPath || new URL("../viewer", import.meta.url).pathname, !this.authEnabled && options?.authToken && console.warn("[VisualizationServer] Warning: authToken provided but authEnabled is false. The token will be generated but not used for authentication. Set authEnabled: true to enable authentication."), this.app.use("/*", cors()), this.app.use("/*", telemetryMiddleware), this.app.use("/api/*", async (c, next) => {
127
+ this.app = new OpenAPIHono(), this.model = model, this.loadWebSocketAdapter(), this.authEnabled = options?.authEnabled ?? process.env.DR_AUTH_ENABLED !== "false", this.authToken = options?.authToken || process.env.DR_AUTH_TOKEN || this.generateAuthToken(), this.withDanger = options?.withDanger || !1, this.viewerPath = options?.viewerPath || new URL("../viewer", import.meta.url).pathname, !this.authEnabled && options?.authToken && console.warn("[VisualizationServer] Warning: authToken provided but authEnabled is false. The token will be generated but not used for authentication. Set authEnabled: true to enable authentication."), this.app.use("/*", cors()), this.app.use("/*", telemetryMiddleware), this.app.use("/api/*", async (c, next) => {
123
128
  if (!this.authEnabled)
124
129
  return next();
125
130
  const authHeader = c.req.header("Authorization"), queryToken = c.req.query("token");
@@ -130,19 +135,21 @@ class VisualizationServer {
130
135
  });
131
136
  }
132
137
  /**
133
- * Load Bun-specific WebSocket adapters if available
134
- * This is called in the constructor to support WebSocket functionality in Bun runtime
135
- * In Node.js/tsx environments (e.g., generating OpenAPI spec), these imports are skipped
138
+ * Set up the WebSocket adapter for this server instance
139
+ * This is called in the constructor to support WebSocket functionality via @hono/node-ws,
140
+ * which works identically under Node.js and Bun (both implement Node's http module).
141
+ * Wrapped in try/catch as a defensive guard so a WS setup failure would degrade the
142
+ * server gracefully (no /ws route) rather than crash construction entirely, in case
143
+ * this ever becomes fallible again in the future.
136
144
  */
137
- loadBunAdapters() {
138
- if (typeof global.Bun < "u")
139
- try {
140
- const honoModule = require2("hono/bun");
141
- upgradeWebSocket = honoModule.upgradeWebSocket, websocket = honoModule.websocket;
142
- } catch (error) {
143
- const message = error instanceof Error ? error.message : String(error);
144
- console.warn(`[Server] WARNING: Could not load WebSocket adapters from hono/bun: ${message}. WebSocket endpoints (/ws) will not be available. Please ensure 'hono' is installed and the Bun runtime is properly configured.`), process.env.VERBOSE && error instanceof Error && error.stack && console.debug("[Server] Stack trace:", error.stack);
145
- }
145
+ loadWebSocketAdapter() {
146
+ try {
147
+ const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app: this.app });
148
+ this.injectWebSocket = injectWebSocket, this.upgradeWebSocket = upgradeWebSocket;
149
+ } catch (error) {
150
+ const message = error instanceof Error ? error.message : String(error);
151
+ console.warn(`[Server] WARNING: Could not set up WebSocket adapter: ${message}. WebSocket endpoints (/ws) will not be available.`), process.env.VERBOSE && error instanceof Error && error.stack && console.debug("[Server] Stack trace:", error.stack);
152
+ }
146
153
  }
147
154
  /**
148
155
  * Detect and initialize available chat clients
@@ -858,7 +865,7 @@ class VisualizationServer {
858
865
  { name: "Annotations", description: "User annotations on model elements" },
859
866
  { name: "WebSocket", description: "Real-time updates via WebSocket" }
860
867
  ]
861
- }), this.app.get("/api-docs", swaggerUI({ url: "/api-spec.yaml" })), upgradeWebSocket && this.app.get("/ws", async (c, next) => {
868
+ }), this.app.get("/api-docs", swaggerUI({ url: "/api-spec.yaml" })), this.upgradeWebSocket && this.app.get("/ws", async (c, next) => {
862
869
  if (this.authEnabled) {
863
870
  const queryToken = c.req.query("token"), wsProtocol = c.req.header("Sec-WebSocket-Protocol");
864
871
  let protocolToken = null;
@@ -876,7 +883,7 @@ class VisualizationServer {
876
883
  protocolToken && c.header("Sec-WebSocket-Protocol", "token");
877
884
  }
878
885
  return next();
879
- }, upgradeWebSocket(() => ({
886
+ }, this.upgradeWebSocket(() => ({
880
887
  onOpen: async (_evt, ws) => {
881
888
  await this.recordWebSocketEvent("ws.connection.open", {
882
889
  "ws.client_count": this.clients.size + 1
@@ -1274,23 +1281,33 @@ class VisualizationServer {
1274
1281
  try {
1275
1282
  const cmd = ["claude", "--agent", "dr-architect", "--print"];
1276
1283
  this.withDanger && cmd.push("--dangerously-skip-permissions"), cmd.push("--verbose", "--output-format", "stream-json");
1277
- const proc = Bun.spawn({
1278
- cmd,
1284
+ const proc = spawn(cmd[0], cmd.slice(1), {
1279
1285
  cwd: this.model.rootPath,
1280
- stdin: "pipe",
1281
- stdout: "pipe",
1282
- stderr: "pipe"
1286
+ stdio: ["pipe", "pipe", "pipe"]
1283
1287
  });
1284
- this.activeChatProcesses.set(conversationId, proc), proc.stdin?.write(new TextEncoder().encode(message)), proc.stdin?.end();
1285
- const stdoutReader = proc.stdout.getReader(), decoder = new TextDecoder();
1288
+ proc.on("error", (err) => {
1289
+ const errorMsg = getErrorMessage(err);
1290
+ console.error(`[Claude Code] Failed to spawn subprocess for conversation ${conversationId}: ${errorMsg}`), this.activeChatProcesses.delete(conversationId);
1291
+ try {
1292
+ ws.send(JSON.stringify({
1293
+ jsonrpc: "2.0",
1294
+ error: {
1295
+ code: JSONRPC_ERRORS.INTERNAL_ERROR,
1296
+ message: `Failed to launch Claude Code: ${errorMsg}`
1297
+ },
1298
+ id: requestId
1299
+ }));
1300
+ } catch (sendError) {
1301
+ console.error(`[Claude Code] Failed to send spawn-error notification: ${getErrorMessage(sendError)}`);
1302
+ }
1303
+ }), proc.stdin?.on("error", () => {
1304
+ }), this.activeChatProcesses.set(conversationId, proc), proc.stdin?.write(message), proc.stdin?.end();
1305
+ const decoder = new TextDecoder();
1286
1306
  (async () => {
1287
1307
  let accumulatedText = "", buffer = "";
1288
1308
  try {
1289
- for (; ; ) {
1290
- const { done, value } = await stdoutReader.read();
1291
- if (done)
1292
- break;
1293
- const chunk = decoder.decode(value, { stream: !0 });
1309
+ for await (const chunkBuf of proc.stdout) {
1310
+ const chunk = decoder.decode(chunkBuf, { stream: !0 });
1294
1311
  buffer += chunk;
1295
1312
  const lines = buffer.split(`
1296
1313
  `);
@@ -1354,7 +1371,7 @@ class VisualizationServer {
1354
1371
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1355
1372
  }
1356
1373
  }));
1357
- const exitCode = await proc.exited;
1374
+ const exitCode = await this.waitForExit(proc);
1358
1375
  this.activeChatProcesses.delete(conversationId), ws.send(JSON.stringify({
1359
1376
  jsonrpc: "2.0",
1360
1377
  result: {
@@ -1367,6 +1384,8 @@ class VisualizationServer {
1367
1384
  id: requestId
1368
1385
  }));
1369
1386
  } catch (error) {
1387
+ if (!proc.pid)
1388
+ return;
1370
1389
  const errorMsg = getErrorMessage(error);
1371
1390
  ws.send(JSON.stringify({
1372
1391
  jsonrpc: "2.0",
@@ -1416,28 +1435,37 @@ class VisualizationServer {
1416
1435
  async launchCopilotChat(ws, conversationId, message, requestId) {
1417
1436
  try {
1418
1437
  let cmd;
1419
- Bun.spawnSync({
1420
- cmd: ["gh", "copilot", "--version"],
1421
- stdout: "pipe",
1422
- stderr: "pipe"
1423
- }).exitCode === 0 ? (cmd = ["gh", "copilot", "explain"], this.withDanger && cmd.push("--allow-all-tools"), cmd.push(message)) : (cmd = ["copilot", "explain"], this.withDanger && cmd.push("--allow-all-tools"), cmd.push(message));
1424
- const proc = Bun.spawn({
1425
- cmd,
1438
+ const ghResult = spawnSync("gh", ["copilot", "--version"], {
1439
+ stdio: ["ignore", "pipe", "pipe"]
1440
+ });
1441
+ ghResult.error ? console.warn(`[Copilot] 'gh copilot --version' probe failed to spawn: ${getErrorMessage(ghResult.error)}`) : ghResult.status !== 0 && process.env.VERBOSE && console.log(`[Copilot] 'gh copilot' unavailable (exit ${ghResult.status}), falling back to standalone 'copilot'`), ghResult.status === 0 ? (cmd = ["gh", "copilot", "explain"], this.withDanger && cmd.push("--allow-all-tools"), cmd.push(message)) : (cmd = ["copilot", "explain"], this.withDanger && cmd.push("--allow-all-tools"), cmd.push(message));
1442
+ const proc = spawn(cmd[0], cmd.slice(1), {
1426
1443
  cwd: this.model.rootPath,
1427
- stdin: "pipe",
1428
- stdout: "pipe",
1429
- stderr: "pipe"
1444
+ stdio: ["pipe", "pipe", "pipe"]
1430
1445
  });
1431
- this.activeChatProcesses.set(conversationId, proc);
1432
- const stdoutReader = proc.stdout.getReader(), decoder = new TextDecoder();
1446
+ proc.on("error", (err) => {
1447
+ const errorMsg = getErrorMessage(err);
1448
+ console.error(`[Copilot] Failed to spawn subprocess for conversation ${conversationId}: ${errorMsg}`), this.activeChatProcesses.delete(conversationId);
1449
+ try {
1450
+ ws.send(JSON.stringify({
1451
+ jsonrpc: "2.0",
1452
+ error: {
1453
+ code: JSONRPC_ERRORS.INTERNAL_ERROR,
1454
+ message: `Failed to launch GitHub Copilot: ${errorMsg}`
1455
+ },
1456
+ id: requestId
1457
+ }));
1458
+ } catch (sendError) {
1459
+ console.error(`[Copilot] Failed to send spawn-error notification: ${getErrorMessage(sendError)}`);
1460
+ }
1461
+ }), proc.stdin?.on("error", () => {
1462
+ }), this.activeChatProcesses.set(conversationId, proc);
1463
+ const decoder = new TextDecoder();
1433
1464
  (async () => {
1434
1465
  let accumulatedText = "";
1435
1466
  try {
1436
- for (; ; ) {
1437
- const { done, value } = await stdoutReader.read();
1438
- if (done)
1439
- break;
1440
- const chunk = decoder.decode(value, { stream: !0 });
1467
+ for await (const chunkBuf of proc.stdout) {
1468
+ const chunk = decoder.decode(chunkBuf, { stream: !0 });
1441
1469
  accumulatedText += chunk, ws.send(JSON.stringify({
1442
1470
  jsonrpc: "2.0",
1443
1471
  method: "chat.response.chunk",
@@ -1449,7 +1477,7 @@ class VisualizationServer {
1449
1477
  }
1450
1478
  }));
1451
1479
  }
1452
- const exitCode = await proc.exited;
1480
+ const exitCode = await this.waitForExit(proc);
1453
1481
  this.activeChatProcesses.delete(conversationId), ws.send(JSON.stringify({
1454
1482
  jsonrpc: "2.0",
1455
1483
  result: {
@@ -1462,6 +1490,8 @@ class VisualizationServer {
1462
1490
  id: requestId
1463
1491
  }));
1464
1492
  } catch (error) {
1493
+ if (!proc.pid)
1494
+ return;
1465
1495
  const errorMsg = getErrorMessage(error);
1466
1496
  ws.send(JSON.stringify({
1467
1497
  jsonrpc: "2.0",
@@ -1496,35 +1526,33 @@ class VisualizationServer {
1496
1526
  * Setup file watcher for model changes
1497
1527
  */
1498
1528
  setupFileWatcher() {
1499
- const modelPath = `${this.model.rootPath}/documentation-robotics/model`, bunWatch = globalThis.Bun?.watch;
1500
- if (!bunWatch) {
1501
- console.warn("[Watcher] Bun.watch not available, file watching disabled");
1502
- return;
1503
- }
1529
+ const modelPath = `${this.model.rootPath}/documentation-robotics/model`;
1504
1530
  try {
1505
- this.watcher = bunWatch(modelPath, {
1506
- recursive: !0,
1507
- onChange: async (_event, path2) => {
1508
- process.env.VERBOSE && console.log(`[Watcher] File changed: ${path2}`);
1509
- try {
1510
- this.model = await Model.load(this.model.rootPath, { lazyLoad: !1 }), await this.loadChangesetsFromDisk();
1511
- const modelData = await this.serializeModel(), message = JSON.stringify({
1512
- type: "model.updated",
1513
- data: modelData,
1514
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1515
- });
1516
- for (const client of this.clients)
1517
- try {
1518
- client.send(message);
1519
- } catch (error) {
1520
- const msg = getErrorMessage(error);
1521
- console.warn(`[Watcher] Failed to send update: ${msg}`);
1522
- }
1523
- } catch (error) {
1524
- const message = getErrorMessage(error);
1525
- console.error(`[Watcher] Failed to reload model: ${message}`);
1526
- }
1531
+ this.watcher = chokidar.watch(modelPath, {
1532
+ persistent: !0,
1533
+ ignoreInitial: !0
1534
+ }), this.watcher.on("all", async (_event, path2) => {
1535
+ process.env.VERBOSE && console.log(`[Watcher] File changed: ${path2}`);
1536
+ try {
1537
+ this.model = await Model.load(this.model.rootPath, { lazyLoad: !1 }), await this.loadChangesetsFromDisk();
1538
+ const modelData = await this.serializeModel(), message = JSON.stringify({
1539
+ type: "model.updated",
1540
+ data: modelData,
1541
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1542
+ });
1543
+ for (const client of this.clients)
1544
+ try {
1545
+ client.send(message);
1546
+ } catch (error) {
1547
+ const msg = getErrorMessage(error);
1548
+ console.warn(`[Watcher] Failed to send update: ${msg}`);
1549
+ }
1550
+ } catch (error) {
1551
+ const message = getErrorMessage(error);
1552
+ console.error(`[Watcher] Failed to reload model: ${message}`);
1527
1553
  }
1554
+ }), this.watcher.on("error", (err) => {
1555
+ console.warn(`[Watcher] Could not watch ${modelPath}: ${getErrorMessage(err)}`);
1528
1556
  });
1529
1557
  } catch (err) {
1530
1558
  console.warn(`[Watcher] Could not watch ${modelPath}: ${err}`);
@@ -2226,14 +2254,14 @@ class VisualizationServer {
2226
2254
  * Start the server
2227
2255
  */
2228
2256
  async start(port = 8080) {
2229
- this.changesetStorage = new StagedChangesetStorage(this.model.rootPath), await this.loadChangesetsFromDisk(), this.setupFileWatcher();
2230
- const { serve } = await import("bun");
2231
- this._server = serve({
2257
+ this.changesetStorage = new StagedChangesetStorage(this.model.rootPath), await this.loadChangesetsFromDisk(), this.setupFileWatcher(), this._server = serve({
2232
2258
  port,
2233
- fetch: this.app.fetch,
2234
- websocket
2235
- // Add WebSocket handler from hono/bun
2236
- }), console.log(`\u2713 Visualization server running at http://localhost:${port}`);
2259
+ fetch: this.app.fetch
2260
+ }), await new Promise((resolve, reject) => {
2261
+ this._server.once("listening", resolve), this._server.once("error", (err) => {
2262
+ err.code === "EADDRINUSE" ? reject(new Error(`Port ${port} is already in use. Try a different port with --port <port>.`)) : reject(err);
2263
+ });
2264
+ }), this.injectWebSocket?.(this._server), console.log(`\u2713 Visualization server running at http://localhost:${port}`);
2237
2265
  }
2238
2266
  /**
2239
2267
  * Get the current auth token
@@ -2247,6 +2275,34 @@ class VisualizationServer {
2247
2275
  isAuthEnabled() {
2248
2276
  return this.authEnabled;
2249
2277
  }
2278
+ /**
2279
+ * Wait for a spawned child process to exit and resolve with its exit code
2280
+ * Node's ChildProcess doesn't expose a Bun-style `.exited` promise, so this
2281
+ * wraps the "exit" event (or the already-set exitCode, if it fired before
2282
+ * we started listening).
2283
+ */
2284
+ waitForExit(proc) {
2285
+ return new Promise((resolve) => {
2286
+ if (proc.exitCode !== null) {
2287
+ resolve(proc.exitCode);
2288
+ return;
2289
+ }
2290
+ proc.once("exit", (code) => resolve(code ?? 0));
2291
+ });
2292
+ }
2293
+ /**
2294
+ * Whether a child process has actually terminated.
2295
+ *
2296
+ * NOTE: Node's `ChildProcess.killed` means "kill() was successfully used to send a
2297
+ * signal" — it does NOT mean the process has exited (a process can ignore SIGTERM
2298
+ * and keep running with `.killed === true`). This differs from Bun's
2299
+ * `Subprocess.killed`, which does mean "has the process exited". Use exitCode/
2300
+ * signalCode (set only once the process has actually terminated) instead of
2301
+ * `.killed` to decide whether retry/escalation is still needed.
2302
+ */
2303
+ hasExited(proc) {
2304
+ return proc.exitCode !== null || proc.signalCode !== null;
2305
+ }
2250
2306
  /**
2251
2307
  * Kill a process with retry logic and exponential backoff
2252
2308
  * Attempts graceful SIGTERM first, then force SIGKILL if needed
@@ -2256,7 +2312,7 @@ class VisualizationServer {
2256
2312
  const baseDelay = 100;
2257
2313
  for (; attempt < 3; ) {
2258
2314
  try {
2259
- if (proc.killed || (proc.kill("SIGTERM"), await new Promise((resolve) => setTimeout(resolve, 500)), proc.killed))
2315
+ if (this.hasExited(proc) || (proc.kill("SIGTERM"), await new Promise((resolve) => setTimeout(resolve, 500)), this.hasExited(proc)))
2260
2316
  return;
2261
2317
  } catch {
2262
2318
  }
@@ -2266,7 +2322,7 @@ class VisualizationServer {
2266
2322
  }
2267
2323
  }
2268
2324
  try {
2269
- proc.killed || (proc.kill("SIGKILL"), console.warn(`[${label}] Process for conversation ${conversationId} did not respond to SIGTERM, force killed with SIGKILL`));
2325
+ this.hasExited(proc) || (proc.kill("SIGKILL"), console.warn(`[${label}] Process for conversation ${conversationId} did not respond to SIGTERM, force killed with SIGKILL`));
2270
2326
  } catch (error) {
2271
2327
  throw new Error(`Failed to kill process: ${error instanceof Error ? error.message : String(error)}`);
2272
2328
  }
@@ -2282,7 +2338,9 @@ class VisualizationServer {
2282
2338
  const errorMessage = error instanceof Error ? error.message : String(error);
2283
2339
  errorMessage.includes("already closed") || console.warn(`[SERVER] Unexpected error closing WebSocket client: ${errorMessage}`);
2284
2340
  }
2285
- this.clients.clear(), this.watcher && this.watcher.close?.(), this._server && this._server.stop();
2341
+ this.clients.clear(), this.watcher && this.watcher.close?.(), this._server && this._server.close((error) => {
2342
+ error && process.env.VERBOSE && console.warn(`[SERVER] Error while closing HTTP server: ${getErrorMessage(error)}`);
2343
+ });
2286
2344
  }
2287
2345
  }
2288
2346
  export {