larkin 0.5.8 → 0.5.9

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.
@@ -30,7 +30,7 @@ async function runClaudeControl(call) {
30
30
  };
31
31
  const timer = setTimeout(() => finish(new Error("Claude model catalog timed out")), call.timeout);
32
32
  timer.unref?.();
33
- child.on("error", () => finish(new Error("Claude model catalog command failed")));
33
+ child.on("error", (error) => finish(new Error(`Claude model catalog command failed: ${error.message}`)));
34
34
  child.on("exit", () => finish(new Error("Claude model catalog control channel exited before response")));
35
35
  child.stderr.resume();
36
36
  child.stdout.on("data", (chunk) => {
@@ -27,7 +27,7 @@ async function runClaudeControl(call) {
27
27
  };
28
28
  const timer = setTimeout(() => finish(new Error("Claude model catalog timed out")), call.timeout);
29
29
  timer.unref?.();
30
- child.on("error", () => finish(new Error("Claude model catalog command failed")));
30
+ child.on("error", (error) => finish(new Error(`Claude model catalog command failed: ${error.message}`)));
31
31
  child.on("exit", () => finish(new Error("Claude model catalog control channel exited before response")));
32
32
  child.stderr.resume();
33
33
  child.stdout.on("data", (chunk) => {
@@ -31,7 +31,7 @@ async function runCodexAppServer(call) {
31
31
  };
32
32
  const timer = setTimeout(() => finish(new Error("Codex model catalog timed out")), call.timeout);
33
33
  timer.unref?.();
34
- child.on("error", () => finish(new Error("Codex model catalog command failed")));
34
+ child.on("error", (error) => finish(new Error(`Codex model catalog command failed: ${error.message}`)));
35
35
  child.on("exit", () => finish(new Error("Codex model catalog app-server exited before response")));
36
36
  child.stderr.resume();
37
37
  child.stdout.on("data", (chunk) => {
@@ -28,7 +28,7 @@ async function runCodexAppServer(call) {
28
28
  };
29
29
  const timer = setTimeout(() => finish(new Error("Codex model catalog timed out")), call.timeout);
30
30
  timer.unref?.();
31
- child.on("error", () => finish(new Error("Codex model catalog command failed")));
31
+ child.on("error", (error) => finish(new Error(`Codex model catalog command failed: ${error.message}`)));
32
32
  child.on("exit", () => finish(new Error("Codex model catalog app-server exited before response")));
33
33
  child.stderr.resume();
34
34
  child.stdout.on("data", (chunk) => {
@@ -47,11 +47,15 @@ class PiRpcClient {
47
47
  this.stderrBuffer = Buffer.concat([this.stderrBuffer, value.subarray(0, this.maxStderrBytes - this.stderrBuffer.length)]);
48
48
  });
49
49
  process.once("error", (error) => this.fail(new Error(`Pi RPC process failed: ${error.message}`), true));
50
- process.once("exit", (code, signal) => {
50
+ const settle = (code, signal) => {
51
51
  this.exited = true;
52
52
  const stderr = this.stderrBuffer.toString("utf8").trim().slice(0, 2_000);
53
53
  this.fail(new Error(`Pi RPC process exited (${code ?? signal ?? "unknown"})${stderr ? `: ${stderr}` : ""}`), false);
54
- });
54
+ };
55
+ process.once("exit", settle);
56
+ // A spawn that never started emits "error" + "close" and never "exit", so
57
+ // "close" is the only terminal event Node guarantees for failed spawns.
58
+ process.once("close", settle);
55
59
  }
56
60
  subscribe(listener) {
57
61
  this.eventListeners.add(listener);
@@ -245,6 +249,9 @@ class PiRpcClient {
245
249
  resolve();
246
250
  };
247
251
  this.process.once("exit", finish);
252
+ // A failed spawn never emits "exit": settle on "close" instead of waiting
253
+ // out the grace timers (or never, when nothing else keeps the loop alive).
254
+ this.process.once("close", finish);
248
255
  this.process.kill("SIGTERM");
249
256
  forceTimer = setTimeout(() => {
250
257
  if (this.exited) {
@@ -44,11 +44,15 @@ export class PiRpcClient {
44
44
  this.stderrBuffer = Buffer.concat([this.stderrBuffer, value.subarray(0, this.maxStderrBytes - this.stderrBuffer.length)]);
45
45
  });
46
46
  process.once("error", (error) => this.fail(new Error(`Pi RPC process failed: ${error.message}`), true));
47
- process.once("exit", (code, signal) => {
47
+ const settle = (code, signal) => {
48
48
  this.exited = true;
49
49
  const stderr = this.stderrBuffer.toString("utf8").trim().slice(0, 2_000);
50
50
  this.fail(new Error(`Pi RPC process exited (${code ?? signal ?? "unknown"})${stderr ? `: ${stderr}` : ""}`), false);
51
- });
51
+ };
52
+ process.once("exit", settle);
53
+ // A spawn that never started emits "error" + "close" and never "exit", so
54
+ // "close" is the only terminal event Node guarantees for failed spawns.
55
+ process.once("close", settle);
52
56
  }
53
57
  subscribe(listener) {
54
58
  this.eventListeners.add(listener);
@@ -242,6 +246,9 @@ export class PiRpcClient {
242
246
  resolve();
243
247
  };
244
248
  this.process.once("exit", finish);
249
+ // A failed spawn never emits "exit": settle on "close" instead of waiting
250
+ // out the grace timers (or never, when nothing else keeps the loop alive).
251
+ this.process.once("close", finish);
245
252
  this.process.kill("SIGTERM");
246
253
  forceTimer = setTimeout(() => {
247
254
  if (this.exited) {
@@ -181,12 +181,22 @@ function resolveRuntimeExecutable(command, env = process.env) {
181
181
  }
182
182
  function classifyRuntimePrerequisite(runtime, error, executable) {
183
183
  const reason = (error instanceof Error ? error.message : String(error)).replace(/[\r\n]+/g, " ").slice(0, 500);
184
- if (/ENOENT|not found|no such file|spawn .* failed/i.test(reason))
184
+ if (/ENOENT|not found|no such file|spawn .* failed/i.test(reason)) {
185
+ // A resolved executable that still exists on disk is not a missing install:
186
+ // the spawn failure comes from the launch environment (working directory,
187
+ // interpreter, permissions). Keep the raw reason instead of sending the
188
+ // user to reinstall the runtime.
189
+ if (executable && node_fs_1.default.existsSync(executable))
190
+ return {
191
+ runtime, state: "unavailable", executable, reason,
192
+ nextAction: `Verify the ${runtime} launch environment (working directory, interpreter, permissions), then retry.`,
193
+ };
185
194
  return {
186
195
  runtime, state: "missing", ...(executable ? { executable } : {}),
187
196
  reason: `${runtime} is not installed`,
188
197
  nextAction: runtimeInstallNextAction(runtime),
189
198
  };
199
+ }
190
200
  if (/no authenticated available models|login|credential|unauthenticated|unauthorized|oauth/i.test(reason))
191
201
  return {
192
202
  runtime, state: "unauthenticated", ...(executable ? { executable } : {}), reason,
@@ -128,12 +128,22 @@ export function resolveRuntimeExecutable(command, env = process.env) {
128
128
  }
129
129
  export function classifyRuntimePrerequisite(runtime, error, executable) {
130
130
  const reason = (error instanceof Error ? error.message : String(error)).replace(/[\r\n]+/g, " ").slice(0, 500);
131
- if (/ENOENT|not found|no such file|spawn .* failed/i.test(reason))
131
+ if (/ENOENT|not found|no such file|spawn .* failed/i.test(reason)) {
132
+ // A resolved executable that still exists on disk is not a missing install:
133
+ // the spawn failure comes from the launch environment (working directory,
134
+ // interpreter, permissions). Keep the raw reason instead of sending the
135
+ // user to reinstall the runtime.
136
+ if (executable && fs.existsSync(executable))
137
+ return {
138
+ runtime, state: "unavailable", executable, reason,
139
+ nextAction: `Verify the ${runtime} launch environment (working directory, interpreter, permissions), then retry.`,
140
+ };
132
141
  return {
133
142
  runtime, state: "missing", ...(executable ? { executable } : {}),
134
143
  reason: `${runtime} is not installed`,
135
144
  nextAction: runtimeInstallNextAction(runtime),
136
145
  };
146
+ }
137
147
  if (/no authenticated available models|login|credential|unauthenticated|unauthorized|oauth/i.test(reason))
138
148
  return {
139
149
  runtime, state: "unauthenticated", ...(executable ? { executable } : {}), reason,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "larkin",
3
- "version": "0.5.8",
3
+ "version": "0.5.9",
4
4
  "private": false,
5
5
  "larkinPackageRole": "npm-published",
6
6
  "files": [