palmier 1.1.5 → 1.1.7

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.
@@ -4,7 +4,7 @@ import * as path from "path";
4
4
  import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, isTaskInList, appendHistory, createRunDir, appendRunMessage, getRunDir, writeFollowupStatus, readFollowupStatus, deleteFollowupStatus, isRunStarred, setRunStarred } from "./task.js";
5
5
  import { resolvePending, getPending, listPending } from "./pending-requests.js";
6
6
  import { getPlatform } from "./platform/index.js";
7
- import { spawnCommand } from "./spawn-command.js";
7
+ import { onChildDone, resolveCommand, spawnCommand } from "./spawn-command.js";
8
8
  import crossSpawn from "cross-spawn";
9
9
  import { getAgent, getPromptCommandLine, getNpmInstalledVersion } from "./agents/agent.js";
10
10
  import { validateClient, revokeClient } from "./client-store.js";
@@ -404,7 +404,8 @@ export function createRpcHandler(config, nc) {
404
404
  for (const f of followupFiles)
405
405
  fs.writeFileSync(path.join(followupRunDir, f.path), f.content, "utf-8");
406
406
  }
407
- const child = crossSpawn(cmd, cmdArgs, {
407
+ const resolved = resolveCommand(cmd, cmdArgs);
408
+ const child = crossSpawn(resolved.command, resolved.args, {
408
409
  cwd: followupRunDir,
409
410
  stdio: [stdin != null ? "pipe" : "ignore", "pipe", "pipe"],
410
411
  env: { ...process.env, ...followupAgentEnv },
@@ -418,7 +419,7 @@ export function createRpcHandler(config, nc) {
418
419
  const chunks = [];
419
420
  child.stdout?.on("data", (d) => chunks.push(d));
420
421
  child.stderr?.on("data", (d) => process.stderr.write(d));
421
- child.on("close", async (code) => {
422
+ onChildDone(child, async (code) => {
422
423
  activeFollowups.delete(followupKey);
423
424
  deleteFollowupStatus(followupRunDir);
424
425
  // stop_followup already wrote the stopped status.
@@ -1,4 +1,19 @@
1
1
  import { type ChildProcess } from "child_process";
2
+ /**
3
+ * cross-spawn runs Windows .cmd shims through cmd.exe, which caps the command
4
+ * line at 8191 chars and can't carry newlines. For npm-installed CLIs, run the
5
+ * shim's node script directly so long multi-line prompts survive intact.
6
+ */
7
+ export declare function resolveCommand(command: string, args: string[]): {
8
+ command: string;
9
+ args: string[];
10
+ };
11
+ /**
12
+ * Like the "close" event, but doesn't wait for grandchildren: a background process
13
+ * the child started (e.g. a browser daemon) can inherit its stdio pipes on Windows
14
+ * and hold them open long after the child itself exits.
15
+ */
16
+ export declare function onChildDone(child: ChildProcess, callback: (code: number | null) => void): void;
2
17
  export interface SpawnStreamingOptions {
3
18
  cwd: string;
4
19
  env?: Record<string, string>;
@@ -1,5 +1,44 @@
1
1
  import crossSpawn from "cross-spawn";
2
2
  import { execFileSync } from "child_process";
3
+ import * as fs from "fs";
4
+ import * as path from "path";
5
+ const NPM_NODE_SHIM_TARGET = /"%_prog%"\s+"%dp0%\\([^"]+)"\s+%\*\s*$/;
6
+ /**
7
+ * cross-spawn runs Windows .cmd shims through cmd.exe, which caps the command
8
+ * line at 8191 chars and can't carry newlines. For npm-installed CLIs, run the
9
+ * shim's node script directly so long multi-line prompts survive intact.
10
+ */
11
+ export function resolveCommand(command, args) {
12
+ if (process.platform !== "win32")
13
+ return { command, args };
14
+ const shim = findNpmNodeShim(command);
15
+ if (shim)
16
+ return { command: shim.node, args: [shim.script, ...args] };
17
+ return { command, args: args.map((a) => a.replace(/[\r\n]+/g, " ")) };
18
+ }
19
+ function findNpmNodeShim(command) {
20
+ if (path.basename(command) !== command || path.extname(command))
21
+ return null;
22
+ const exts = (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
23
+ for (const dir of (process.env.PATH ?? "").split(path.delimiter).filter(Boolean)) {
24
+ for (const ext of exts) {
25
+ const candidate = path.join(dir, command + ext);
26
+ if (!fs.existsSync(candidate))
27
+ continue;
28
+ if (ext.toLowerCase() !== ".cmd")
29
+ return null;
30
+ const match = fs.readFileSync(candidate, "utf-8").match(NPM_NODE_SHIM_TARGET);
31
+ if (!match)
32
+ return null;
33
+ const localNode = path.join(dir, "node.exe");
34
+ return {
35
+ node: fs.existsSync(localNode) ? localNode : process.execPath,
36
+ script: path.join(dir, match[1]),
37
+ };
38
+ }
39
+ }
40
+ return null;
41
+ }
3
42
  /** Kill a child process and its entire tree on Windows; plain kill elsewhere. */
4
43
  function treeKill(child) {
5
44
  if (process.platform === "win32" && child.pid) {
@@ -11,6 +50,32 @@ function treeKill(child) {
11
50
  }
12
51
  child.kill();
13
52
  }
53
+ const STDIO_DRAIN_GRACE_MS = 1000;
54
+ /**
55
+ * Like the "close" event, but doesn't wait for grandchildren: a background process
56
+ * the child started (e.g. a browser daemon) can inherit its stdio pipes on Windows
57
+ * and hold them open long after the child itself exits.
58
+ */
59
+ export function onChildDone(child, callback) {
60
+ let done = false;
61
+ let grace;
62
+ const finish = (code) => {
63
+ if (done)
64
+ return;
65
+ done = true;
66
+ if (grace)
67
+ clearTimeout(grace);
68
+ callback(code);
69
+ };
70
+ child.on("exit", (code) => {
71
+ grace = setTimeout(() => {
72
+ child.stdout?.destroy();
73
+ child.stderr?.destroy();
74
+ finish(code);
75
+ }, STDIO_DRAIN_GRACE_MS);
76
+ });
77
+ child.on("close", finish);
78
+ }
14
79
  /**
15
80
  * Spawn with shell interpretation for piped commands like "tail -f log | grep".
16
81
  * Returns the ChildProcess with stdout piped so the caller can read it directly
@@ -30,14 +95,11 @@ export function spawnStreamingCommand(command, opts) {
30
95
  }
31
96
  export function spawnCommand(command, args, opts) {
32
97
  return new Promise((resolve, reject) => {
33
- // cmd.exe can't handle literal newlines in arguments.
34
- const finalArgs = process.platform === "win32"
35
- ? args.map((a) => a.replace(/[\r\n]+/g, " "))
36
- : args;
98
+ const resolved = resolveCommand(command, args);
37
99
  const truncate = (s, max = 100) => s.length > max ? s.slice(0, max) + "..." : s;
38
- const displayArgs = finalArgs.map((arg) => truncate(arg));
39
- console.log(`[spawn] ${command} ${displayArgs.join(" ")}`);
40
- const child = crossSpawn(command, finalArgs, {
100
+ const displayArgs = resolved.args.map((arg) => truncate(arg));
101
+ console.log(`[spawn] ${resolved.command} ${displayArgs.join(" ")}`);
102
+ const child = crossSpawn(resolved.command, resolved.args, {
41
103
  cwd: opts.cwd,
42
104
  stdio: [opts.stdin != null ? "pipe" : "ignore", "pipe", "pipe"],
43
105
  env: opts.env ? { ...process.env, ...opts.env } : undefined,
@@ -68,7 +130,7 @@ export function spawnCommand(command, args, opts) {
68
130
  reject(new Error("command timed out"));
69
131
  }, opts.timeout);
70
132
  }
71
- child.on("close", (code) => {
133
+ onChildDone(child, (code) => {
72
134
  if (timer)
73
135
  clearTimeout(timer);
74
136
  const output = Buffer.concat(chunks).toString("utf-8");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palmier",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "description": "Palmier host CLI - provisions, executes tasks, and serves NATS RPC",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hongxu Cai",