diff --git a/gadget-drone/src/gadget-drone.ts b/gadget-drone/src/gadget-drone.ts index 9f9e9f9..4fea0fa 100644 --- a/gadget-drone/src/gadget-drone.ts +++ b/gadget-drone/src/gadget-drone.ts @@ -696,6 +696,11 @@ class GadgetDrone extends GadgetProcess { process.exit(exitCode); }); + process.on("uncaughtException", (error) => { + this.log.fatal("uncaught exception", { err: error }); + process.exit(1); + }); + process.on("warning", (error) => { if (error.name === "DeprecationWarning") return; this.log.alert("warning", { error }); diff --git a/gadget-drone/src/services/subprocess.ts b/gadget-drone/src/services/subprocess.ts index bb807a0..ce8cf61 100644 --- a/gadget-drone/src/services/subprocess.ts +++ b/gadget-drone/src/services/subprocess.ts @@ -2,8 +2,6 @@ // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 -import assert from "node:assert"; - import path from "node:path"; import fs from "node:fs"; @@ -63,17 +61,24 @@ class SubProcessService extends GadgetService { args?: string[], ): Promise { const NOW = new Date(); - assert( - WorkspaceService.gadgetDir, - "Gadget workspace directory not initialized", - ); + const gadgetDir = WorkspaceService.gadgetDir; + if (!gadgetDir) { + throw new Error("Gadget workspace directory not initialized"); + } - const logDir = path.join(WorkspaceService.gadgetDir, "subprocess-logs"); + const logDir = path.join(gadgetDir, "subprocess-logs"); await fs.promises.mkdir(logDir, { recursive: true }); const projectDir = WorkspaceService.getProjectDirectory(project.slug); - const child = spawn(cmd, args, { cwd: projectDir }); - assert(child.pid, "subprocess did not define a process id"); + const spawnArgs = args ?? []; + const useShell = spawnArgs.length === 0; + const child = spawn(cmd, spawnArgs, { cwd: projectDir, shell: useShell }); + child.on("error", (err) => { + this.log.error("subprocess spawn failed", { cmd, args, error: err.message }); + }); + if (!child.pid) { + throw new Error("Failed to spawn subprocess: child process has no pid"); + } const pid = child.pid; const stdoutFileName = path.join(logDir, `subproc-${pid}.stdout.log`); diff --git a/gadget-drone/src/tools/system/subprocess.ts b/gadget-drone/src/tools/system/subprocess.ts index 0f890fc..a8a98f5 100644 --- a/gadget-drone/src/tools/system/subprocess.ts +++ b/gadget-drone/src/tools/system/subprocess.ts @@ -45,7 +45,7 @@ export class SubprocessTool extends GadgetTool { command: { type: "string", description: - "The shell command to execute (required for cmd=create). Example: 'node', 'npm', 'python', './my-server'.", + "The full shell command to execute (required for cmd=create). Examples: 'npm run dev', 'pnpm dev:frontend', 'node server.js'. The command is executed via /bin/sh.", }, args: { type: "array", @@ -128,7 +128,7 @@ export class SubprocessTool extends GadgetTool { message: "The 'command' parameter is required for cmd=create.", parameter: "command", recoveryHint: - "Provide the command to execute, e.g. 'node', 'npm', 'python'.", + "Provide the full command string to execute, e.g. 'npm run dev', 'pnpm dev:frontend'. The command is executed via /bin/sh.", }); } @@ -168,7 +168,7 @@ export class SubprocessTool extends GadgetTool { }); return formatError({ code: "OPERATION_FAILED", - message: `Failed to spawn subprocess: ${message}`, + message: `Failed to spawn subprocess: ${message}. Make sure the command is installed and available in PATH, or use the full path to the executable.`, }); } }