@indigoai-us/hq-cli 5.152.0 → 5.152.1
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/CHANGELOG.md +6 -0
- package/dist/lib/bot/runtime/claude.d.ts +1 -1
- package/dist/lib/bot/runtime/claude.js +16 -3
- package/dist/lib/bot/runtime/codex.js +8 -2
- package/dist/lib/bot/runtime/grok-acp.d.ts +40 -0
- package/dist/lib/bot/runtime/grok-acp.js +265 -0
- package/dist/lib/bot/runtime/grok.d.ts +2 -0
- package/dist/lib/bot/runtime/grok.js +25 -1
- package/dist/lib/bot/runtime/index.d.ts +1 -0
- package/dist/lib/bot/runtime/index.js +131 -5
- package/dist/lib/bot/runtime/types.d.ts +16 -1
- package/dist/lib/bot/runtime/windows.d.ts +10 -0
- package/dist/lib/bot/runtime/windows.js +53 -0
- package/dist/utils/windows-spawn.d.ts +6 -0
- package/dist/utils/windows-spawn.js +37 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.152.1] — 2026-09-23
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Windows bot runtimes resolve provider binaries through PATH and PATHEXT, safely launch npm .cmd and .bat shims, pass multiline and long prompts through stdin or Grok ACP, and stop the full process tree.
|
|
10
|
+
|
|
5
11
|
## [5.152.0] — 2026-09-23
|
|
6
12
|
|
|
7
13
|
### Fixed
|
|
@@ -44,6 +44,6 @@ export declare function onPath(name: string, env: NodeJS.ProcessEnv): boolean;
|
|
|
44
44
|
* skipped), matching the HQ desktop app's own lookup.
|
|
45
45
|
*/
|
|
46
46
|
export declare function bundledClaudeBin(homeDir?: string): string | null;
|
|
47
|
-
export declare function resolveClaudeBinary(env: NodeJS.ProcessEnv, homeDir?: string): string;
|
|
47
|
+
export declare function resolveClaudeBinary(env: NodeJS.ProcessEnv, homeDir?: string, platform?: NodeJS.Platform, cwd?: string): string;
|
|
48
48
|
export declare function createClaudeRuntime(env?: NodeJS.ProcessEnv): BotRuntime;
|
|
49
49
|
//# sourceMappingURL=claude.d.ts.map
|
|
@@ -18,6 +18,7 @@ import * as fs from "node:fs";
|
|
|
18
18
|
import * as os from "node:os";
|
|
19
19
|
import * as path from "node:path";
|
|
20
20
|
import { messagesStreamEvents, messagesStreamFailure, messagesStreamResult } from "./messages-stream.js";
|
|
21
|
+
import { resolveRuntimeBinary } from "./windows.js";
|
|
21
22
|
export const CLAUDE_PERMISSION_MODE_ENV = "HQ_BOT_CLAUDE_PERMISSION_MODE";
|
|
22
23
|
export const DEFAULT_CLAUDE_PERMISSION_MODE = "bypassPermissions";
|
|
23
24
|
export const GATED_CLAUDE_PERMISSION_MODE = "acceptEdits";
|
|
@@ -102,7 +103,9 @@ export function bundledClaudeBin(homeDir = os.homedir()) {
|
|
|
102
103
|
});
|
|
103
104
|
return candidates.at(-1)?.bin ?? null;
|
|
104
105
|
}
|
|
105
|
-
export function resolveClaudeBinary(env, homeDir) {
|
|
106
|
+
export function resolveClaudeBinary(env, homeDir, platform = process.platform, cwd = process.cwd()) {
|
|
107
|
+
if (platform === "win32")
|
|
108
|
+
return resolveRuntimeBinary("claude", env, platform, fs.existsSync, cwd);
|
|
106
109
|
if (onPath("claude", env))
|
|
107
110
|
return "claude";
|
|
108
111
|
return bundledClaudeBin(homeDir ?? env.HOME ?? os.homedir()) ?? "claude";
|
|
@@ -111,13 +114,15 @@ export function createClaudeRuntime(env = process.env) {
|
|
|
111
114
|
return {
|
|
112
115
|
id: "claude",
|
|
113
116
|
binary: "claude",
|
|
114
|
-
resolveBinary: (turnEnv) => resolveClaudeBinary(turnEnv),
|
|
117
|
+
resolveBinary: (turnEnv, cwd, platform) => resolveClaudeBinary(turnEnv, undefined, platform ?? process.platform, cwd),
|
|
115
118
|
buildArgs(input, scratch) {
|
|
119
|
+
const usesWindowsStdin = input.platform === "win32";
|
|
116
120
|
const args = [
|
|
117
121
|
"-p",
|
|
118
|
-
input.prompt,
|
|
122
|
+
...(usesWindowsStdin ? [] : [input.prompt]),
|
|
119
123
|
"--output-format",
|
|
120
124
|
"stream-json",
|
|
125
|
+
...(usesWindowsStdin ? ["--input-format", "stream-json"] : []),
|
|
121
126
|
"--verbose",
|
|
122
127
|
"--permission-mode",
|
|
123
128
|
claudePermissionMode(env, input.autoApprove !== false, input.permissionMode),
|
|
@@ -137,6 +142,14 @@ export function createClaudeRuntime(env = process.env) {
|
|
|
137
142
|
args.push("--session-id", scratch.newSessionId);
|
|
138
143
|
return args;
|
|
139
144
|
},
|
|
145
|
+
stdinInput(input) {
|
|
146
|
+
return input.platform === "win32"
|
|
147
|
+
? JSON.stringify({
|
|
148
|
+
type: "user",
|
|
149
|
+
message: { role: "user", content: [{ type: "text", text: input.prompt }] },
|
|
150
|
+
}) + "\n"
|
|
151
|
+
: undefined;
|
|
152
|
+
},
|
|
140
153
|
parseOutput(stdout) {
|
|
141
154
|
return messagesStreamResult(stdout, "claude");
|
|
142
155
|
},
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* cannot run under this machine's account).
|
|
15
15
|
*/
|
|
16
16
|
import { parseJsonLine, RuntimeError } from "./types.js";
|
|
17
|
+
import { resolveRuntimeBinary } from "./windows.js";
|
|
17
18
|
/** Progress events in one codex JSONL line: finished agent messages and tool starts. */
|
|
18
19
|
export function codexStreamEvents(line) {
|
|
19
20
|
const ev = parseJsonLine(line);
|
|
@@ -82,7 +83,9 @@ export function createCodexRuntime() {
|
|
|
82
83
|
return {
|
|
83
84
|
id: "codex",
|
|
84
85
|
binary: "codex",
|
|
86
|
+
resolveBinary: (env, cwd, platform) => resolveRuntimeBinary("codex", env, platform ?? process.platform, undefined, cwd),
|
|
85
87
|
buildArgs(input, scratch) {
|
|
88
|
+
const promptArg = input.platform === "win32" ? "-" : codexPrompt(input);
|
|
86
89
|
const common = ["--json", "--skip-git-repo-check", "-o", scratch.outputFile];
|
|
87
90
|
if (input.autoApprove !== false) {
|
|
88
91
|
// Headless: nobody can answer an approval prompt, so a gated command
|
|
@@ -96,9 +99,12 @@ export function createCodexRuntime() {
|
|
|
96
99
|
common.push("-m", input.model);
|
|
97
100
|
common.push("-c", `model_reasoning_effort=${JSON.stringify(input.effort?.trim() || "medium")}`);
|
|
98
101
|
if (input.sessionId) {
|
|
99
|
-
return ["exec", "resume", ...common, input.sessionId,
|
|
102
|
+
return ["exec", "resume", ...common, input.sessionId, promptArg];
|
|
100
103
|
}
|
|
101
|
-
return ["exec", ...common, "-C", input.cwd,
|
|
104
|
+
return ["exec", ...common, "-C", input.cwd, promptArg];
|
|
105
|
+
},
|
|
106
|
+
stdinInput(input) {
|
|
107
|
+
return input.platform === "win32" ? codexPrompt(input) : undefined;
|
|
102
108
|
},
|
|
103
109
|
parseOutput(stdout, extra) {
|
|
104
110
|
let sessionId;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface GrokAcpClientOptions {
|
|
2
|
+
cwd: string;
|
|
3
|
+
env: NodeJS.ProcessEnv;
|
|
4
|
+
prompt: string;
|
|
5
|
+
sessionId?: string;
|
|
6
|
+
send: (message: Record<string, unknown>) => void;
|
|
7
|
+
onText: (line: string) => void;
|
|
8
|
+
onComplete: (sessionId: string, text: string) => void;
|
|
9
|
+
onFailure: (message: string) => void;
|
|
10
|
+
}
|
|
11
|
+
/** A minimal ACP client for Grok's documented stdio runtime. */
|
|
12
|
+
export declare class GrokAcpClient {
|
|
13
|
+
private readonly options;
|
|
14
|
+
private nextId;
|
|
15
|
+
private readonly pending;
|
|
16
|
+
private readonly terminals;
|
|
17
|
+
private sessionId;
|
|
18
|
+
private text;
|
|
19
|
+
private finished;
|
|
20
|
+
constructor(options: GrokAcpClientOptions);
|
|
21
|
+
start(): void;
|
|
22
|
+
receive(line: string): void;
|
|
23
|
+
private request;
|
|
24
|
+
private notify;
|
|
25
|
+
private authenticate;
|
|
26
|
+
private openSession;
|
|
27
|
+
private prompt;
|
|
28
|
+
private receiveUpdate;
|
|
29
|
+
private complete;
|
|
30
|
+
private fail;
|
|
31
|
+
private receiveRequest;
|
|
32
|
+
private handleClientRequest;
|
|
33
|
+
private createTerminal;
|
|
34
|
+
private getTerminal;
|
|
35
|
+
private terminalOutput;
|
|
36
|
+
private waitForTerminal;
|
|
37
|
+
private killTerminal;
|
|
38
|
+
private releaseTerminal;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=grok-acp.d.ts.map
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { planBotRuntimeSpawn } from "./windows.js";
|
|
4
|
+
/** A minimal ACP client for Grok's documented stdio runtime. */
|
|
5
|
+
export class GrokAcpClient {
|
|
6
|
+
options;
|
|
7
|
+
nextId = 1;
|
|
8
|
+
pending = new Map();
|
|
9
|
+
terminals = new Map();
|
|
10
|
+
sessionId;
|
|
11
|
+
text = "";
|
|
12
|
+
finished = false;
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
}
|
|
16
|
+
start() {
|
|
17
|
+
this.request("initialize", {
|
|
18
|
+
protocolVersion: 1,
|
|
19
|
+
clientCapabilities: { terminal: true },
|
|
20
|
+
}, (result) => this.authenticate(result));
|
|
21
|
+
}
|
|
22
|
+
receive(line) {
|
|
23
|
+
let message;
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(line);
|
|
26
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
27
|
+
return;
|
|
28
|
+
message = parsed;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (message.method === "session/update") {
|
|
34
|
+
this.receiveUpdate(message);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (typeof message.method === "string" && (typeof message.id === "number" || typeof message.id === "string")) {
|
|
38
|
+
void this.receiveRequest(message);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (typeof message.id !== "number" && typeof message.id !== "string")
|
|
42
|
+
return;
|
|
43
|
+
const resolve = this.pending.get(message.id);
|
|
44
|
+
if (!resolve)
|
|
45
|
+
return;
|
|
46
|
+
this.pending.delete(message.id);
|
|
47
|
+
if (message.error && typeof message.error === "object") {
|
|
48
|
+
const error = message.error;
|
|
49
|
+
this.fail(typeof error.message === "string" ? error.message : "Grok ACP request failed");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const result = message.result && typeof message.result === "object" ? message.result : {};
|
|
53
|
+
resolve(result);
|
|
54
|
+
}
|
|
55
|
+
request(method, params, complete) {
|
|
56
|
+
if (this.finished)
|
|
57
|
+
return;
|
|
58
|
+
const id = this.nextId++;
|
|
59
|
+
this.pending.set(id, complete);
|
|
60
|
+
this.options.send({ jsonrpc: "2.0", id, method, params });
|
|
61
|
+
}
|
|
62
|
+
notify(method, params = {}) {
|
|
63
|
+
this.options.send({ jsonrpc: "2.0", method, params });
|
|
64
|
+
}
|
|
65
|
+
authenticate(result) {
|
|
66
|
+
const methods = Array.isArray(result.authMethods) ? result.authMethods : [];
|
|
67
|
+
const authMethods = new Set(methods.flatMap((method) => method && typeof method === "object" && typeof method.id === "string"
|
|
68
|
+
? [String(method.id)]
|
|
69
|
+
: []));
|
|
70
|
+
const apiKey = Object.entries(this.options.env).some(([key, value]) => key.toUpperCase() === "XAI_API_KEY" && Boolean(value?.trim()));
|
|
71
|
+
const methodId = apiKey && authMethods.has("xai.api_key") ? "xai.api_key" : authMethods.has("cached_token") ? "cached_token" : null;
|
|
72
|
+
if (!methodId) {
|
|
73
|
+
this.fail("Grok has no cached login; run `grok login` or configure XAI_API_KEY before starting the bot.");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this.request("authenticate", { methodId, _meta: { headless: true } }, () => this.openSession());
|
|
77
|
+
}
|
|
78
|
+
openSession() {
|
|
79
|
+
if (this.options.sessionId) {
|
|
80
|
+
this.request("session/load", { sessionId: this.options.sessionId, cwd: this.options.cwd, mcpServers: [], _meta: { yoloMode: true } }, (result) => this.prompt(result));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
this.request("session/new", { cwd: this.options.cwd, mcpServers: [], _meta: { yoloMode: true } }, (result) => this.prompt(result));
|
|
84
|
+
}
|
|
85
|
+
prompt(result) {
|
|
86
|
+
if (typeof result.sessionId !== "string" || !result.sessionId) {
|
|
87
|
+
this.fail("Grok ACP did not return a session ID");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
this.sessionId = result.sessionId;
|
|
91
|
+
this.request("session/prompt", { sessionId: this.sessionId, prompt: [{ type: "text", text: this.options.prompt }] }, (response) => this.complete(response));
|
|
92
|
+
}
|
|
93
|
+
receiveUpdate(message) {
|
|
94
|
+
const params = message.params && typeof message.params === "object" ? message.params : {};
|
|
95
|
+
const update = params.update && typeof params.update === "object" ? params.update : {};
|
|
96
|
+
const content = update.content && typeof update.content === "object" ? update.content : {};
|
|
97
|
+
if (update.sessionUpdate !== "agent_message_chunk" || typeof content.text !== "string")
|
|
98
|
+
return;
|
|
99
|
+
this.text += content.text;
|
|
100
|
+
this.options.onText(JSON.stringify({
|
|
101
|
+
type: "assistant",
|
|
102
|
+
message: { role: "assistant", content: [{ type: "text", text: content.text }] },
|
|
103
|
+
parent_tool_use_id: null,
|
|
104
|
+
session_id: this.sessionId,
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
complete(response) {
|
|
108
|
+
if (!this.sessionId) {
|
|
109
|
+
this.fail("Grok ACP completed without a session ID");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (typeof response.stopReason === "string" && response.stopReason.toLowerCase() === "cancelled") {
|
|
113
|
+
this.fail("Grok ACP cancelled the bot turn");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.finished = true;
|
|
117
|
+
this.notify("exit");
|
|
118
|
+
this.options.onComplete(this.sessionId, this.text);
|
|
119
|
+
}
|
|
120
|
+
fail(message) {
|
|
121
|
+
if (this.finished)
|
|
122
|
+
return;
|
|
123
|
+
this.finished = true;
|
|
124
|
+
this.pending.clear();
|
|
125
|
+
this.options.onFailure(message);
|
|
126
|
+
}
|
|
127
|
+
async receiveRequest(message) {
|
|
128
|
+
const method = message.method;
|
|
129
|
+
const params = message.params && typeof message.params === "object" ? message.params : {};
|
|
130
|
+
try {
|
|
131
|
+
const result = await this.handleClientRequest(method, params);
|
|
132
|
+
this.options.send({ jsonrpc: "2.0", id: message.id, result });
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
this.options.send({
|
|
136
|
+
jsonrpc: "2.0",
|
|
137
|
+
id: message.id,
|
|
138
|
+
error: { code: -32000, message: err instanceof Error ? err.message : String(err) },
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async handleClientRequest(method, params) {
|
|
143
|
+
const shortMethod = method.startsWith("x.ai/") ? method.slice("x.ai/".length) : method;
|
|
144
|
+
if (shortMethod === "session/request_permission") {
|
|
145
|
+
const options = Array.isArray(params.options) ? params.options : [];
|
|
146
|
+
const allow = options.find((option) => {
|
|
147
|
+
if (!option || typeof option !== "object")
|
|
148
|
+
return false;
|
|
149
|
+
const candidate = option;
|
|
150
|
+
return typeof candidate.optionId === "string" && /allow|approve/i.test(String(candidate.kind ?? candidate.name ?? ""));
|
|
151
|
+
});
|
|
152
|
+
if (!allow || typeof allow.optionId !== "string")
|
|
153
|
+
return { outcome: "cancelled" };
|
|
154
|
+
return { outcome: "selected", optionId: allow.optionId };
|
|
155
|
+
}
|
|
156
|
+
if (shortMethod === "terminal/create")
|
|
157
|
+
return this.createTerminal(params);
|
|
158
|
+
if (shortMethod === "terminal/output")
|
|
159
|
+
return this.terminalOutput(params);
|
|
160
|
+
if (shortMethod === "terminal/wait_for_exit")
|
|
161
|
+
return this.waitForTerminal(params);
|
|
162
|
+
if (shortMethod === "terminal/kill")
|
|
163
|
+
return this.killTerminal(params);
|
|
164
|
+
if (shortMethod === "terminal/release")
|
|
165
|
+
return this.releaseTerminal(params);
|
|
166
|
+
throw new Error(`Unsupported Grok ACP client request: ${method}`);
|
|
167
|
+
}
|
|
168
|
+
createTerminal(params) {
|
|
169
|
+
if (typeof params.command !== "string" || !params.command)
|
|
170
|
+
throw new Error("Grok ACP terminal request has no command");
|
|
171
|
+
const args = Array.isArray(params.args) ? params.args.filter((arg) => typeof arg === "string") : [];
|
|
172
|
+
const env = { ...this.options.env };
|
|
173
|
+
if (Array.isArray(params.env)) {
|
|
174
|
+
for (const entry of params.env) {
|
|
175
|
+
if (!entry || typeof entry !== "object")
|
|
176
|
+
continue;
|
|
177
|
+
const item = entry;
|
|
178
|
+
const name = item.name;
|
|
179
|
+
const value = item.value;
|
|
180
|
+
if (typeof name === "string" && typeof value === "string") {
|
|
181
|
+
const key = Object.keys(env).find((candidate) => candidate.toUpperCase() === name.toUpperCase()) ?? name;
|
|
182
|
+
env[key] = value;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const commandPlan = planBotRuntimeSpawn(params.command, args, env);
|
|
187
|
+
const child = spawn(commandPlan.cmd, commandPlan.args, {
|
|
188
|
+
cwd: typeof params.cwd === "string" ? params.cwd : this.options.cwd,
|
|
189
|
+
env,
|
|
190
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
191
|
+
...(commandPlan.shell ? { shell: commandPlan.shell } : {}),
|
|
192
|
+
...(commandPlan.windowsVerbatimArguments !== undefined
|
|
193
|
+
? { windowsVerbatimArguments: commandPlan.windowsVerbatimArguments }
|
|
194
|
+
: {}),
|
|
195
|
+
windowsHide: true,
|
|
196
|
+
});
|
|
197
|
+
let resolveExit;
|
|
198
|
+
const exited = new Promise((resolve) => {
|
|
199
|
+
resolveExit = resolve;
|
|
200
|
+
});
|
|
201
|
+
const handle = { child, output: "", truncated: false, exitCode: null, exited };
|
|
202
|
+
const append = (chunk) => {
|
|
203
|
+
const max = 1_000_000;
|
|
204
|
+
handle.output += chunk.toString();
|
|
205
|
+
if (Buffer.byteLength(handle.output) > max) {
|
|
206
|
+
const bytes = Buffer.from(handle.output);
|
|
207
|
+
handle.output = bytes.subarray(bytes.length - max).toString();
|
|
208
|
+
handle.truncated = true;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
child.stdout?.on("data", append);
|
|
212
|
+
child.stderr?.on("data", append);
|
|
213
|
+
child.once("error", (err) => {
|
|
214
|
+
handle.output += `\n[hq bot] terminal command failed to start: ${err.message}`;
|
|
215
|
+
handle.exitCode = 1;
|
|
216
|
+
resolveExit(1);
|
|
217
|
+
});
|
|
218
|
+
child.once("close", (code) => {
|
|
219
|
+
handle.exitCode = code;
|
|
220
|
+
resolveExit(code);
|
|
221
|
+
});
|
|
222
|
+
const terminalId = randomUUID();
|
|
223
|
+
this.terminals.set(terminalId, handle);
|
|
224
|
+
return { terminalId };
|
|
225
|
+
}
|
|
226
|
+
getTerminal(params) {
|
|
227
|
+
if (typeof params.terminalId !== "string")
|
|
228
|
+
throw new Error("Grok ACP terminal request has no terminalId");
|
|
229
|
+
const terminal = this.terminals.get(params.terminalId);
|
|
230
|
+
if (!terminal)
|
|
231
|
+
throw new Error("Grok ACP requested an unknown terminal");
|
|
232
|
+
return terminal;
|
|
233
|
+
}
|
|
234
|
+
terminalOutput(params) {
|
|
235
|
+
const terminal = this.getTerminal(params);
|
|
236
|
+
const limit = typeof params.outputByteLimit === "number" && params.outputByteLimit > 0 ? params.outputByteLimit : 65_536;
|
|
237
|
+
const bytes = Buffer.from(terminal.output);
|
|
238
|
+
const output = bytes.length > limit ? bytes.subarray(bytes.length - limit).toString() : terminal.output;
|
|
239
|
+
return {
|
|
240
|
+
output,
|
|
241
|
+
exitStatus: terminal.exitCode === null ? null : { exitCode: terminal.exitCode },
|
|
242
|
+
truncated: terminal.truncated || bytes.length > limit,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
async waitForTerminal(params) {
|
|
246
|
+
const terminal = this.getTerminal(params);
|
|
247
|
+
const exitCode = terminal.exitCode ?? (await terminal.exited);
|
|
248
|
+
return { exitCode };
|
|
249
|
+
}
|
|
250
|
+
killTerminal(params) {
|
|
251
|
+
const terminal = this.getTerminal(params);
|
|
252
|
+
if (terminal.child.exitCode === null && terminal.child.signalCode === null)
|
|
253
|
+
terminal.child.kill();
|
|
254
|
+
return {};
|
|
255
|
+
}
|
|
256
|
+
releaseTerminal(params) {
|
|
257
|
+
const terminal = this.getTerminal(params);
|
|
258
|
+
if (terminal.child.exitCode === null && terminal.child.signalCode === null) {
|
|
259
|
+
throw new Error("Grok ACP cannot release a terminal command that is still running");
|
|
260
|
+
}
|
|
261
|
+
this.terminals.delete(String(params.terminalId));
|
|
262
|
+
return {};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
//# sourceMappingURL=grok-acp.js.map
|
|
@@ -20,11 +20,13 @@
|
|
|
20
20
|
* still accepted.
|
|
21
21
|
*/
|
|
22
22
|
import { type BotRuntime, type RuntimeTurnInput } from "./types.js";
|
|
23
|
+
import type { SpawnPlan } from "../../../utils/windows-spawn.js";
|
|
23
24
|
export declare const GROK_OUTPUT_FORMAT_LEGACY = "streaming-messages-json";
|
|
24
25
|
export declare const GROK_OUTPUT_FORMAT_STREAMING_JSON = "streaming-json";
|
|
25
26
|
export declare function resetGrokOutputFormatCache(): void;
|
|
26
27
|
/** Pick the streaming `--output-format` grok's `--help` says it accepts. */
|
|
27
28
|
export declare function grokOutputFormatFromHelp(helpText: string): string;
|
|
29
|
+
export declare function grokHelpSpawnPlan(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, existsSync?: (candidate: string) => boolean): SpawnPlan;
|
|
28
30
|
/** Probe `grok --help` once and cache the streaming format. */
|
|
29
31
|
export declare function probeGrokOutputFormat(readHelp?: () => string): string;
|
|
30
32
|
/**
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { execFileSync } from "node:child_process";
|
|
23
23
|
import { messagesStreamEvents, messagesStreamFailure, messagesStreamResult } from "./messages-stream.js";
|
|
24
24
|
import { parseJsonLine, RuntimeError } from "./types.js";
|
|
25
|
+
import { planBotRuntimeSpawn, resolveRuntimeBinary } from "./windows.js";
|
|
25
26
|
export const GROK_OUTPUT_FORMAT_LEGACY = "streaming-messages-json";
|
|
26
27
|
export const GROK_OUTPUT_FORMAT_STREAMING_JSON = "streaming-json";
|
|
27
28
|
let cachedOutputFormat;
|
|
@@ -45,12 +46,21 @@ export function grokOutputFormatFromHelp(helpText) {
|
|
|
45
46
|
return GROK_OUTPUT_FORMAT_STREAMING_JSON;
|
|
46
47
|
return GROK_OUTPUT_FORMAT_LEGACY;
|
|
47
48
|
}
|
|
49
|
+
export function grokHelpSpawnPlan(env = process.env, platform = process.platform, existsSync) {
|
|
50
|
+
const command = resolveRuntimeBinary("grok", env, platform, existsSync);
|
|
51
|
+
return planBotRuntimeSpawn(command, ["--help"], env, platform);
|
|
52
|
+
}
|
|
48
53
|
function defaultGrokHelp() {
|
|
54
|
+
const plan = grokHelpSpawnPlan();
|
|
49
55
|
try {
|
|
50
|
-
return execFileSync(
|
|
56
|
+
return execFileSync(plan.cmd, plan.args, {
|
|
51
57
|
encoding: "utf8",
|
|
52
58
|
timeout: 5_000,
|
|
53
59
|
stdio: ["ignore", "pipe", "pipe"],
|
|
60
|
+
...(plan.shell ? { shell: plan.shell } : {}),
|
|
61
|
+
...(plan.windowsVerbatimArguments !== undefined
|
|
62
|
+
? { windowsVerbatimArguments: plan.windowsVerbatimArguments }
|
|
63
|
+
: {}),
|
|
54
64
|
});
|
|
55
65
|
}
|
|
56
66
|
catch (err) {
|
|
@@ -99,7 +109,16 @@ export function createGrokRuntime(opts = {}) {
|
|
|
99
109
|
return {
|
|
100
110
|
id: "grok",
|
|
101
111
|
binary: "grok",
|
|
112
|
+
resolveBinary: (env, cwd, platform) => resolveRuntimeBinary("grok", env, platform ?? process.platform, undefined, cwd),
|
|
102
113
|
buildArgs(input) {
|
|
114
|
+
if (input.platform === "win32") {
|
|
115
|
+
const args = ["--no-auto-update", "agent", "--always-approve", "--no-subagents"];
|
|
116
|
+
if (input.model)
|
|
117
|
+
args.push("--model", input.model);
|
|
118
|
+
args.push("--effort", input.effort?.trim() || "medium");
|
|
119
|
+
args.push("stdio");
|
|
120
|
+
return args;
|
|
121
|
+
}
|
|
103
122
|
const args = [
|
|
104
123
|
"-p",
|
|
105
124
|
grokPrompt(input),
|
|
@@ -117,6 +136,11 @@ export function createGrokRuntime(opts = {}) {
|
|
|
117
136
|
args.push("-r", input.sessionId);
|
|
118
137
|
return args;
|
|
119
138
|
},
|
|
139
|
+
spawnProtocol(input) {
|
|
140
|
+
return input.platform === "win32"
|
|
141
|
+
? { kind: "grok-acp", prompt: grokPrompt(input), ...(input.sessionId ? { sessionId: input.sessionId } : {}) }
|
|
142
|
+
: undefined;
|
|
143
|
+
},
|
|
120
144
|
parseOutput(stdout) {
|
|
121
145
|
// Older grok: one `{ text, sessionId }` object.
|
|
122
146
|
const legacy = stdout
|
|
@@ -3,6 +3,8 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
6
|
+
import { planBotRuntimeSpawn } from "./windows.js";
|
|
7
|
+
import { GrokAcpClient } from "./grok-acp.js";
|
|
6
8
|
import { createClaudeRuntime } from "./claude.js";
|
|
7
9
|
import { createCodexRuntime } from "./codex.js";
|
|
8
10
|
import { createGrokRuntime, grokRetryArgsAfterInvalidOutputFormat, probeGrokOutputFormat } from "./grok.js";
|
|
@@ -72,12 +74,19 @@ export const defaultSpawn = (command, args, options) => new Promise((resolve, re
|
|
|
72
74
|
let stderr = "";
|
|
73
75
|
let partial = "";
|
|
74
76
|
let settled = false;
|
|
75
|
-
const
|
|
77
|
+
const plan = planBotRuntimeSpawn(command, args, options.env);
|
|
78
|
+
const child = spawn(plan.cmd, plan.args, {
|
|
76
79
|
cwd: options.cwd,
|
|
77
80
|
env: options.env,
|
|
78
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
81
|
+
stdio: [options.stdin !== undefined || options.protocol ? "pipe" : "ignore", "pipe", "pipe"],
|
|
82
|
+
...(plan.shell ? { shell: plan.shell } : {}),
|
|
83
|
+
...(plan.windowsVerbatimArguments !== undefined
|
|
84
|
+
? { windowsVerbatimArguments: plan.windowsVerbatimArguments }
|
|
85
|
+
: {}),
|
|
86
|
+
windowsHide: true,
|
|
79
87
|
});
|
|
80
88
|
let stopping = false;
|
|
89
|
+
let protocolComplete = false;
|
|
81
90
|
const signalTree = (sig, pids) => {
|
|
82
91
|
for (const p of pids) {
|
|
83
92
|
try {
|
|
@@ -90,6 +99,55 @@ export const defaultSpawn = (command, args, options) => new Promise((resolve, re
|
|
|
90
99
|
};
|
|
91
100
|
const kill = () => {
|
|
92
101
|
stopping = true;
|
|
102
|
+
if (process.platform === "win32" && child.pid !== undefined) {
|
|
103
|
+
const rootKey = Object.keys(options.env).find((key) => key.toUpperCase() === "SYSTEMROOT");
|
|
104
|
+
const systemRoot = rootKey ? options.env[rootKey] : undefined;
|
|
105
|
+
const taskkill = path.win32.join(systemRoot || "C:\\Windows", "System32", "taskkill.exe");
|
|
106
|
+
let finished = false;
|
|
107
|
+
const fallbackKill = (reason) => {
|
|
108
|
+
if (finished)
|
|
109
|
+
return;
|
|
110
|
+
finished = true;
|
|
111
|
+
stderr += `\n[hq bot] could not stop the Windows process tree with taskkill.exe: ${reason}`;
|
|
112
|
+
try {
|
|
113
|
+
child.kill();
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
stderr += `\n[hq bot] could not stop the runtime wrapper: ${err instanceof Error ? err.message : String(err)}`;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
let killer;
|
|
120
|
+
try {
|
|
121
|
+
killer = spawn(taskkill, ["/PID", String(child.pid), "/T", "/F"], {
|
|
122
|
+
env: options.env,
|
|
123
|
+
stdio: "ignore",
|
|
124
|
+
windowsHide: true,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
fallbackKill(err instanceof Error ? err.message : String(err));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const fallbackTimer = setTimeout(() => fallbackKill("taskkill.exe did not finish within 5 seconds"), 5_000);
|
|
132
|
+
killer.once("error", (err) => {
|
|
133
|
+
clearTimeout(fallbackTimer);
|
|
134
|
+
fallbackKill(err.message);
|
|
135
|
+
});
|
|
136
|
+
killer.once("close", (code) => {
|
|
137
|
+
clearTimeout(fallbackTimer);
|
|
138
|
+
finished = true;
|
|
139
|
+
if (code !== 0 && child.exitCode === null && child.signalCode === null) {
|
|
140
|
+
stderr += `\n[hq bot] taskkill.exe exited with code ${code} while stopping the runtime process tree`;
|
|
141
|
+
try {
|
|
142
|
+
child.kill();
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
stderr += `\n[hq bot] could not stop the runtime wrapper: ${err instanceof Error ? err.message : String(err)}`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
93
151
|
// Snapshot the tree before the parent dies (its children get re-parented).
|
|
94
152
|
const tree = process.platform === "win32" || child.pid === undefined ? [] : descendantPids(child.pid);
|
|
95
153
|
try {
|
|
@@ -139,13 +197,63 @@ export const defaultSpawn = (command, args, options) => new Promise((resolve, re
|
|
|
139
197
|
/* a listener must never break the turn */
|
|
140
198
|
}
|
|
141
199
|
};
|
|
200
|
+
const emitProtocolLine = (line) => {
|
|
201
|
+
stdout += line + "\n";
|
|
202
|
+
emit(line);
|
|
203
|
+
};
|
|
142
204
|
const cleanup = () => {
|
|
143
205
|
if (timer)
|
|
144
206
|
clearTimeout(timer);
|
|
145
207
|
options.signal?.removeEventListener("abort", onAbort);
|
|
146
208
|
};
|
|
209
|
+
let protocolPartial = "";
|
|
210
|
+
const protocol = options.protocol?.kind === "grok-acp"
|
|
211
|
+
? new GrokAcpClient({
|
|
212
|
+
cwd: options.cwd,
|
|
213
|
+
env: options.env,
|
|
214
|
+
prompt: options.protocol.prompt,
|
|
215
|
+
...(options.protocol.sessionId ? { sessionId: options.protocol.sessionId } : {}),
|
|
216
|
+
send: (message) => {
|
|
217
|
+
try {
|
|
218
|
+
child.stdin?.write(JSON.stringify(message) + "\n");
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
stderr += `\n[hq bot] could not write to the Grok ACP process: ${err instanceof Error ? err.message : String(err)}`;
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
onText: emitProtocolLine,
|
|
225
|
+
onComplete: (sessionId, text) => {
|
|
226
|
+
protocolComplete = true;
|
|
227
|
+
emitProtocolLine(JSON.stringify({ type: "result", subtype: "success", is_error: false, result: text, session_id: sessionId }));
|
|
228
|
+
child.stdin?.end();
|
|
229
|
+
setTimeout(() => {
|
|
230
|
+
if (!settled && child.exitCode === null && child.signalCode === null)
|
|
231
|
+
kill();
|
|
232
|
+
}, STOP_SETTLE_GRACE_MS).unref?.();
|
|
233
|
+
},
|
|
234
|
+
onFailure: (message) => {
|
|
235
|
+
stderr += `\n[hq bot] ${message}`;
|
|
236
|
+
stopping = true;
|
|
237
|
+
child.stdin?.end();
|
|
238
|
+
setTimeout(() => {
|
|
239
|
+
if (!settled && child.exitCode === null && child.signalCode === null)
|
|
240
|
+
kill();
|
|
241
|
+
}, STOP_SETTLE_GRACE_MS).unref?.();
|
|
242
|
+
},
|
|
243
|
+
})
|
|
244
|
+
: undefined;
|
|
147
245
|
child.stdout?.on("data", (d) => {
|
|
148
246
|
const chunk = d.toString();
|
|
247
|
+
if (protocol) {
|
|
248
|
+
protocolPartial += chunk;
|
|
249
|
+
let nl = protocolPartial.indexOf("\n");
|
|
250
|
+
while (nl >= 0) {
|
|
251
|
+
protocol.receive(protocolPartial.slice(0, nl).replace(/\r$/, ""));
|
|
252
|
+
protocolPartial = protocolPartial.slice(nl + 1);
|
|
253
|
+
nl = protocolPartial.indexOf("\n");
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
149
257
|
stdout += chunk;
|
|
150
258
|
if (!options.onStdoutLine)
|
|
151
259
|
return;
|
|
@@ -160,6 +268,10 @@ export const defaultSpawn = (command, args, options) => new Promise((resolve, re
|
|
|
160
268
|
child.stderr?.on("data", (d) => {
|
|
161
269
|
stderr += d.toString();
|
|
162
270
|
});
|
|
271
|
+
child.stdin?.on("error", (err) => {
|
|
272
|
+
if (!settled)
|
|
273
|
+
stderr += `\n[hq bot] could not write input to ${command}: ${err.message}`;
|
|
274
|
+
});
|
|
163
275
|
child.on("error", (err) => {
|
|
164
276
|
if (settled)
|
|
165
277
|
return;
|
|
@@ -167,6 +279,10 @@ export const defaultSpawn = (command, args, options) => new Promise((resolve, re
|
|
|
167
279
|
cleanup();
|
|
168
280
|
reject(err);
|
|
169
281
|
});
|
|
282
|
+
if (protocol && !options.signal?.aborted)
|
|
283
|
+
protocol.start();
|
|
284
|
+
else if (options.stdin !== undefined)
|
|
285
|
+
child.stdin?.end(options.stdin);
|
|
170
286
|
// A stopped process can exit while something it started (and we could not
|
|
171
287
|
// reach) still holds stdout open; settle shortly after the exit instead of
|
|
172
288
|
// waiting for that pipe.
|
|
@@ -192,7 +308,12 @@ export const defaultSpawn = (command, args, options) => new Promise((resolve, re
|
|
|
192
308
|
cleanup();
|
|
193
309
|
if (partial)
|
|
194
310
|
emit(partial);
|
|
195
|
-
resolve({
|
|
311
|
+
resolve({
|
|
312
|
+
code: protocol ? (protocolComplete ? 0 : 1) : code,
|
|
313
|
+
signal: protocolComplete ? null : signal,
|
|
314
|
+
stdout,
|
|
315
|
+
stderr,
|
|
316
|
+
});
|
|
196
317
|
});
|
|
197
318
|
});
|
|
198
319
|
/**
|
|
@@ -204,9 +325,12 @@ export async function runRuntimeTurn(runtime, input, deps = {}) {
|
|
|
204
325
|
const scratchDir = deps.scratchDir ?? fs.mkdtempSync(path.join(os.tmpdir(), "hq-bot-turn-"));
|
|
205
326
|
const outputFile = path.join(scratchDir, "last-message.txt");
|
|
206
327
|
const newSessionId = input.newSessionId ?? (deps.newSessionId ?? randomUUID)();
|
|
207
|
-
const
|
|
328
|
+
const turnInput = { ...input, platform: deps.platform ?? input.platform ?? process.platform };
|
|
329
|
+
const args = runtime.buildArgs(turnInput, { outputFile, newSessionId });
|
|
208
330
|
const env = input.env ?? deps.env ?? process.env;
|
|
209
|
-
const binary = runtime.resolveBinary?.(env) ?? runtime.binary;
|
|
331
|
+
const binary = runtime.resolveBinary?.(env, input.cwd, turnInput.platform) ?? runtime.binary;
|
|
332
|
+
const stdin = runtime.stdinInput?.(turnInput);
|
|
333
|
+
const protocol = runtime.spawnProtocol?.(turnInput);
|
|
210
334
|
const onEvent = input.onEvent;
|
|
211
335
|
const onStdoutLine = onEvent && runtime.parseStreamLine
|
|
212
336
|
? (line) => {
|
|
@@ -224,6 +348,8 @@ export async function runRuntimeTurn(runtime, input, deps = {}) {
|
|
|
224
348
|
const spawnOptions = {
|
|
225
349
|
cwd: input.cwd,
|
|
226
350
|
env,
|
|
351
|
+
...(stdin !== undefined ? { stdin } : {}),
|
|
352
|
+
...(protocol ? { protocol } : {}),
|
|
227
353
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
228
354
|
...(onStdoutLine ? { onStdoutLine } : {}),
|
|
229
355
|
...(input.signal ? { signal: input.signal } : {}),
|
|
@@ -42,6 +42,8 @@ export interface RuntimeTurnInput {
|
|
|
42
42
|
onEvent?: (event: RuntimeStreamEvent) => void;
|
|
43
43
|
/** Aborting kills the runtime process (owner stop); the turn then fails with `aborted`. */
|
|
44
44
|
signal?: AbortSignal;
|
|
45
|
+
/** Runtime host platform; set by runRuntimeTurn and injectable for platform tests. */
|
|
46
|
+
platform?: NodeJS.Platform;
|
|
45
47
|
}
|
|
46
48
|
/**
|
|
47
49
|
* What a runtime's streaming output means to the bot, per stdout line:
|
|
@@ -76,7 +78,16 @@ export interface SpawnOptions {
|
|
|
76
78
|
onStdoutLine?: (line: string) => void;
|
|
77
79
|
/** Kill the process when aborted. */
|
|
78
80
|
signal?: AbortSignal;
|
|
81
|
+
/** Optional full input stream for CLIs that accept prompts through stdin. */
|
|
82
|
+
stdin?: string;
|
|
83
|
+
/** Optional interactive protocol implemented over the child stdin/stdout streams. */
|
|
84
|
+
protocol?: SpawnProtocol;
|
|
79
85
|
}
|
|
86
|
+
export type SpawnProtocol = {
|
|
87
|
+
kind: "grok-acp";
|
|
88
|
+
prompt: string;
|
|
89
|
+
sessionId?: string;
|
|
90
|
+
};
|
|
80
91
|
export interface SpawnLike {
|
|
81
92
|
(command: string, args: string[], options: SpawnOptions): Promise<SpawnResult>;
|
|
82
93
|
}
|
|
@@ -101,7 +112,11 @@ export interface BotRuntime {
|
|
|
101
112
|
* PATH (e.g. Claude Code that only exists inside the Claude desktop app).
|
|
102
113
|
* Resolved per turn so an app update that swaps the version folder is picked up.
|
|
103
114
|
*/
|
|
104
|
-
resolveBinary?(env: NodeJS.ProcessEnv): string;
|
|
115
|
+
resolveBinary?(env: NodeJS.ProcessEnv, cwd?: string, platform?: NodeJS.Platform): string;
|
|
116
|
+
/** Optional full prompt stream sent through stdin instead of argv. */
|
|
117
|
+
stdinInput?(input: RuntimeTurnInput): string | undefined;
|
|
118
|
+
/** Optional bidirectional process protocol for this runtime. */
|
|
119
|
+
spawnProtocol?(input: RuntimeTurnInput): SpawnProtocol | undefined;
|
|
105
120
|
/** Build the argv for one turn. Pure. */
|
|
106
121
|
buildArgs(input: RuntimeTurnInput, scratch: {
|
|
107
122
|
outputFile: string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type SpawnPlan } from "../../../utils/windows-spawn.js";
|
|
2
|
+
type ExistsSync = (candidate: string) => boolean;
|
|
3
|
+
/** Resolve an npm-installed provider command using Windows PATH and PATHEXT. */
|
|
4
|
+
export declare function resolveWindowsExecutable(name: string, env: NodeJS.ProcessEnv, existsSync?: ExistsSync, cwd?: string): string | null;
|
|
5
|
+
/** Preserve non-Windows lookup. Windows returns the first PATH/PATHEXT match. */
|
|
6
|
+
export declare function resolveRuntimeBinary(name: string, env: NodeJS.ProcessEnv, platform?: NodeJS.Platform, existsSync?: ExistsSync, cwd?: string): string;
|
|
7
|
+
/** Directly spawn native files; pass only batch shims through cmd.exe. */
|
|
8
|
+
export declare function planBotRuntimeSpawn(command: string, args: readonly string[], env: NodeJS.ProcessEnv, platform?: NodeJS.Platform): SpawnPlan;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=windows.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { buildCmdExeSpawnPlan, isWindowsBatchFile, } from "../../../utils/windows-spawn.js";
|
|
4
|
+
const DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
|
|
5
|
+
const SUPPORTED_PATHEXT = new Set([".com", ".exe", ".cmd", ".bat"]);
|
|
6
|
+
function envValue(env, name) {
|
|
7
|
+
const key = Object.keys(env).find((candidate) => candidate.toUpperCase() === name);
|
|
8
|
+
return key ? env[key] : undefined;
|
|
9
|
+
}
|
|
10
|
+
/** Resolve an npm-installed provider command using Windows PATH and PATHEXT. */
|
|
11
|
+
export function resolveWindowsExecutable(name, env, existsSync = fs.existsSync, cwd = process.cwd()) {
|
|
12
|
+
const pathValue = envValue(env, "PATH") ?? "";
|
|
13
|
+
const rawExtensions = (envValue(env, "PATHEXT") || DEFAULT_PATHEXT)
|
|
14
|
+
.split(";")
|
|
15
|
+
.map((extension) => {
|
|
16
|
+
const trimmed = extension.trim();
|
|
17
|
+
if (!trimmed)
|
|
18
|
+
return "";
|
|
19
|
+
return (trimmed.startsWith(".") ? trimmed : "." + trimmed).toLowerCase();
|
|
20
|
+
});
|
|
21
|
+
const extensions = [...new Set(rawExtensions.filter((extension) => SUPPORTED_PATHEXT.has(extension)))];
|
|
22
|
+
if (extensions.includes(".exe")) {
|
|
23
|
+
extensions.splice(extensions.indexOf(".exe"), 1);
|
|
24
|
+
extensions.unshift(".exe");
|
|
25
|
+
}
|
|
26
|
+
const searchExtensions = path.win32.extname(name) ? [""] : extensions;
|
|
27
|
+
for (const rawDir of pathValue.split(";")) {
|
|
28
|
+
const dir = rawDir.trim().replace(/^"(.*)"$/, "$1");
|
|
29
|
+
if (!dir)
|
|
30
|
+
continue;
|
|
31
|
+
const absoluteDir = path.win32.resolve(cwd, dir);
|
|
32
|
+
for (const extension of searchExtensions) {
|
|
33
|
+
const candidate = path.win32.join(absoluteDir, name + extension);
|
|
34
|
+
if (existsSync(candidate))
|
|
35
|
+
return candidate;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
/** Preserve non-Windows lookup. Windows returns the first PATH/PATHEXT match. */
|
|
41
|
+
export function resolveRuntimeBinary(name, env, platform = process.platform, existsSync = fs.existsSync, cwd = process.cwd()) {
|
|
42
|
+
if (platform !== "win32")
|
|
43
|
+
return name;
|
|
44
|
+
return resolveWindowsExecutable(name, env, existsSync, cwd) ?? name;
|
|
45
|
+
}
|
|
46
|
+
/** Directly spawn native files; pass only batch shims through cmd.exe. */
|
|
47
|
+
export function planBotRuntimeSpawn(command, args, env, platform = process.platform) {
|
|
48
|
+
if (isWindowsBatchFile(command, platform)) {
|
|
49
|
+
return buildCmdExeSpawnPlan(command, args, env, platform);
|
|
50
|
+
}
|
|
51
|
+
return { cmd: command, args: [...args], shell: false };
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=windows.js.map
|
|
@@ -2,7 +2,13 @@ export type SpawnPlan = {
|
|
|
2
2
|
cmd: string;
|
|
3
3
|
args: string[];
|
|
4
4
|
shell: boolean;
|
|
5
|
+
windowsVerbatimArguments?: boolean;
|
|
5
6
|
};
|
|
7
|
+
/**
|
|
8
|
+
* Explicit cmd.exe plan for a resolved .cmd/.bat target. Keeping cmd.exe's
|
|
9
|
+
* argv construction here makes the shell boundary testable without Windows.
|
|
10
|
+
*/
|
|
11
|
+
export declare function buildCmdExeSpawnPlan(command: string, args: readonly string[], env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): SpawnPlan;
|
|
6
12
|
/**
|
|
7
13
|
* Quote one entry for a Windows `cmd.exe` invocation. Node does not quote for
|
|
8
14
|
* `shell: true` on Windows — it joins the array with spaces — so a bare prefix
|
|
@@ -13,6 +13,43 @@
|
|
|
13
13
|
// This module is the single implementation of that policy. The self-update path
|
|
14
14
|
// (version-gate) and the qmd spawn path (search-index) both import it, so there
|
|
15
15
|
// is exactly one Windows spawn recipe in the repo.
|
|
16
|
+
const CMD_META_CHARS = /([()\][%!\x5e\x60"<>&|;, *?])/g;
|
|
17
|
+
function assertCmdArgument(value) {
|
|
18
|
+
if (/[\r\n\0]/.test(value)) {
|
|
19
|
+
throw Object.assign(new Error("Windows batch commands cannot accept newline or NUL characters"), {
|
|
20
|
+
code: "EINVAL",
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function escapeCmdCommand(value) {
|
|
25
|
+
assertCmdArgument(value);
|
|
26
|
+
return value.replace(CMD_META_CHARS, "^$1");
|
|
27
|
+
}
|
|
28
|
+
function escapeCmdArgument(value) {
|
|
29
|
+
assertCmdArgument(value);
|
|
30
|
+
let escaped = value.replace(/(?=(\\+?)?)\1"/g, '$1$1\\"');
|
|
31
|
+
escaped = escaped.replace(/(?=(\\+?)?)\1$/, "$1$1");
|
|
32
|
+
return ('"' + escaped + '"').replace(CMD_META_CHARS, "^$1");
|
|
33
|
+
}
|
|
34
|
+
function environmentValue(env, name) {
|
|
35
|
+
const key = Object.keys(env).find((candidate) => candidate.toUpperCase() === name);
|
|
36
|
+
return key ? env[key] : undefined;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Explicit cmd.exe plan for a resolved .cmd/.bat target. Keeping cmd.exe's
|
|
40
|
+
* argv construction here makes the shell boundary testable without Windows.
|
|
41
|
+
*/
|
|
42
|
+
export function buildCmdExeSpawnPlan(command, args, env = process.env, platform = process.platform) {
|
|
43
|
+
if (platform !== "win32")
|
|
44
|
+
return { cmd: command, args: [...args], shell: false };
|
|
45
|
+
const commandLine = [escapeCmdCommand(command), ...args.map(escapeCmdArgument)].join(" ");
|
|
46
|
+
return {
|
|
47
|
+
cmd: environmentValue(env, "COMSPEC")?.trim() || "cmd.exe",
|
|
48
|
+
args: ["/d", "/s", "/c", '"' + commandLine + '"'],
|
|
49
|
+
shell: false,
|
|
50
|
+
windowsVerbatimArguments: true,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
16
53
|
/**
|
|
17
54
|
* Quote one entry for a Windows `cmd.exe` invocation. Node does not quote for
|
|
18
55
|
* `shell: true` on Windows — it joins the array with spaces — so a bare prefix
|