@hyperdrive.bot/fleet-server 0.3.157 → 0.3.158
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/server/utils/git-command-cache.d.ts +67 -0
- package/dist/server/utils/git-command-cache.js +436 -0
- package/dist/server/utils/run-git-command.js +14 -4
- package/dist/server/utils/spawn-broker-child.mjs +156 -0
- package/dist/server/utils/spawn-broker.d.ts +120 -0
- package/dist/server/utils/spawn-broker.js +299 -0
- package/dist/server/utils/spawn.d.ts +21 -0
- package/dist/server/utils/spawn.js +144 -10
- package/dist/server/web-ui/_expo/static/js/web/{index-7ca8a9a256b79485a77556fab01588ca.js → index-461552a3716a48e99f87634c93c7b220.js} +4 -4
- package/dist/server/web-ui/_expo/static/js/web/index-461552a3716a48e99f87634c93c7b220.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-461552a3716a48e99f87634c93c7b220.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-7ca8a9a256b79485a77556fab01588ca.js.map.br → index-461552a3716a48e99f87634c93c7b220.js.map.br} +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-7ca8a9a256b79485a77556fab01588ca.js.map.gz → index-461552a3716a48e99f87634c93c7b220.js.map.gz} +0 -0
- package/dist/server/web-ui/index.html +1 -1
- package/dist/server/web-ui/index.html.br +0 -0
- package/dist/server/web-ui/index.html.gz +0 -0
- package/dist/src/utils/spawn-broker-child.mjs +156 -0
- package/dist/src/utils/spawn-broker.js +299 -0
- package/dist/src/utils/spawn.js +144 -10
- package/package.json +8 -8
- package/dist/server/web-ui/_expo/static/js/web/index-7ca8a9a256b79485a77556fab01588ca.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-7ca8a9a256b79485a77556fab01588ca.js.gz +0 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { fork } from "node:child_process";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
const FAILURE_COOLDOWN_MS = 60000;
|
|
6
|
+
const MAX_CONSECUTIVE_FAILURES = 3;
|
|
7
|
+
export function isSpawnBrokerEnabled() {
|
|
8
|
+
if (process.platform === "win32")
|
|
9
|
+
return false;
|
|
10
|
+
const flag = process.env.PASEO_SPAWN_BROKER;
|
|
11
|
+
return flag !== "0" && flag !== "false";
|
|
12
|
+
}
|
|
13
|
+
function resolveBrokerChildPath() {
|
|
14
|
+
// The helper is a plain .mjs file that sits next to this module both in src/ (tests,
|
|
15
|
+
// tsx dev) and in dist/server/utils/ (build:lib copies it), so the published package
|
|
16
|
+
// ships it at node_modules/@hyperdrive.bot/fleet-server/dist/server/utils/.
|
|
17
|
+
return fileURLToPath(new URL("./spawn-broker-child.mjs", import.meta.url));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A process run by the broker. Emits the same events, in the same order, as a
|
|
21
|
+
* direct ChildProcess with stdio ["ignore", "pipe", "pipe"]. If the broker fails
|
|
22
|
+
* before the process is confirmed started, it transparently falls back to a
|
|
23
|
+
* direct spawn so the caller never sees the difference.
|
|
24
|
+
*/
|
|
25
|
+
export class BrokeredChildProcess extends EventEmitter {
|
|
26
|
+
constructor(id, broker, directSpawn, policy) {
|
|
27
|
+
super();
|
|
28
|
+
this.stdout = new EventEmitter();
|
|
29
|
+
this.stderr = new EventEmitter();
|
|
30
|
+
this.killed = false;
|
|
31
|
+
this.brokered = true;
|
|
32
|
+
this.started = false;
|
|
33
|
+
this.closed = false;
|
|
34
|
+
this.fallbackChild = null;
|
|
35
|
+
this.pendingKill = null;
|
|
36
|
+
this.id = id;
|
|
37
|
+
this.broker = broker;
|
|
38
|
+
this.directSpawn = directSpawn;
|
|
39
|
+
this.retryOnDirect = policy.retryOnDirect;
|
|
40
|
+
}
|
|
41
|
+
kill(signal = "SIGTERM") {
|
|
42
|
+
if (this.closed)
|
|
43
|
+
return false;
|
|
44
|
+
this.killed = true;
|
|
45
|
+
if (this.fallbackChild) {
|
|
46
|
+
return this.fallbackChild.kill(signal);
|
|
47
|
+
}
|
|
48
|
+
this.pendingKill = signal;
|
|
49
|
+
this.broker.sendKill(this.id, signal);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
/** @internal */
|
|
53
|
+
handleMessage(message) {
|
|
54
|
+
if (this.closed || this.fallbackChild)
|
|
55
|
+
return;
|
|
56
|
+
switch (message.type) {
|
|
57
|
+
case "spawned":
|
|
58
|
+
this.started = true;
|
|
59
|
+
this.pid = message.pid;
|
|
60
|
+
this.emit("spawn");
|
|
61
|
+
return;
|
|
62
|
+
case "data": {
|
|
63
|
+
const target = message.fd === 1 ? this.stdout : this.stderr;
|
|
64
|
+
target.emit("data", Buffer.from(message.data, "base64"));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
case "error": {
|
|
68
|
+
const error = Object.assign(new Error(message.error.message), {
|
|
69
|
+
code: message.error.code,
|
|
70
|
+
errno: message.error.errno,
|
|
71
|
+
syscall: message.error.syscall,
|
|
72
|
+
path: message.error.path,
|
|
73
|
+
});
|
|
74
|
+
this.emit("error", error);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
case "close":
|
|
78
|
+
this.closed = true;
|
|
79
|
+
this.broker.release(this.id);
|
|
80
|
+
this.emit("exit", message.exitCode, message.signal);
|
|
81
|
+
this.emit("close", message.exitCode, message.signal);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** @internal Broker died or could not be reached. */
|
|
86
|
+
handleBrokerLost() {
|
|
87
|
+
if (this.closed || this.fallbackChild)
|
|
88
|
+
return;
|
|
89
|
+
if (this.started) {
|
|
90
|
+
// The process really ran (the broker kills its children when it dies), so
|
|
91
|
+
// re-running it could repeat a side effect. Report it as killed instead.
|
|
92
|
+
this.closed = true;
|
|
93
|
+
this.emit("exit", null, "SIGKILL");
|
|
94
|
+
this.emit("close", null, "SIGKILL");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (this.retryOnDirect) {
|
|
98
|
+
this.startFallback();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// The request may already have run (and been killed) inside the helper, so a
|
|
102
|
+
// mutating command must not run a second time: surface the loss instead.
|
|
103
|
+
this.closed = true;
|
|
104
|
+
const error = Object.assign(new Error("Spawn broker exited before confirming the process started"), { code: "ERR_SPAWN_BROKER_LOST" });
|
|
105
|
+
queueMicrotask(() => this.emit("error", error));
|
|
106
|
+
}
|
|
107
|
+
/** @internal */
|
|
108
|
+
startFallback() {
|
|
109
|
+
this.brokered = false;
|
|
110
|
+
let child;
|
|
111
|
+
try {
|
|
112
|
+
child = this.directSpawn();
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
this.closed = true;
|
|
116
|
+
queueMicrotask(() => this.emit("error", error));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
this.fallbackChild = child;
|
|
120
|
+
this.pid = child.pid;
|
|
121
|
+
child.stdout?.on("data", (chunk) => this.stdout.emit("data", chunk));
|
|
122
|
+
child.stderr?.on("data", (chunk) => this.stderr.emit("data", chunk));
|
|
123
|
+
child.on("error", (error) => this.emit("error", error));
|
|
124
|
+
child.on("exit", (code, signal) => this.emit("exit", code, signal));
|
|
125
|
+
child.on("close", (code, signal) => {
|
|
126
|
+
this.closed = true;
|
|
127
|
+
this.emit("close", code, signal);
|
|
128
|
+
});
|
|
129
|
+
if (this.pendingKill !== null) {
|
|
130
|
+
child.kill(this.pendingKill);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export class SpawnBroker {
|
|
135
|
+
constructor(childPath = resolveBrokerChildPath()) {
|
|
136
|
+
this.child = null;
|
|
137
|
+
this.pending = new Map();
|
|
138
|
+
this.nextId = 1;
|
|
139
|
+
this.consecutiveFailures = 0;
|
|
140
|
+
this.disabledUntil = 0;
|
|
141
|
+
this.exitHookInstalled = false;
|
|
142
|
+
this.childPath = childPath;
|
|
143
|
+
}
|
|
144
|
+
get pendingCount() {
|
|
145
|
+
return this.pending.size;
|
|
146
|
+
}
|
|
147
|
+
get brokerPid() {
|
|
148
|
+
return this.child?.pid;
|
|
149
|
+
}
|
|
150
|
+
spawn(request, directSpawn, policy = { retryOnDirect: false }) {
|
|
151
|
+
const proc = new BrokeredChildProcess(this.nextId++, this, directSpawn, policy);
|
|
152
|
+
const child = this.ensureChild();
|
|
153
|
+
if (!child) {
|
|
154
|
+
// The request never left this process, so a direct spawn cannot double-run it.
|
|
155
|
+
proc.startFallback();
|
|
156
|
+
return proc;
|
|
157
|
+
}
|
|
158
|
+
this.pending.set(proc.id, proc);
|
|
159
|
+
this.updateRef();
|
|
160
|
+
try {
|
|
161
|
+
child.send({ type: "spawn", id: proc.id, ...request }, (error) => {
|
|
162
|
+
if (error)
|
|
163
|
+
this.handleChildLost(child);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
this.handleChildLost(child);
|
|
168
|
+
}
|
|
169
|
+
return proc;
|
|
170
|
+
}
|
|
171
|
+
/** @internal */
|
|
172
|
+
sendKill(id, signal) {
|
|
173
|
+
const child = this.child;
|
|
174
|
+
if (!child || !child.connected)
|
|
175
|
+
return;
|
|
176
|
+
try {
|
|
177
|
+
child.send({ type: "kill", id, signal });
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// handled by the disconnect path
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/** @internal */
|
|
184
|
+
release(id) {
|
|
185
|
+
this.pending.delete(id);
|
|
186
|
+
this.updateRef();
|
|
187
|
+
}
|
|
188
|
+
/** Stop the helper. Pending requests fall back or report SIGKILL. */
|
|
189
|
+
dispose() {
|
|
190
|
+
const child = this.child;
|
|
191
|
+
if (child) {
|
|
192
|
+
this.handleChildLost(child);
|
|
193
|
+
child.kill("SIGTERM");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** Test hook: make the helper exit abruptly. */
|
|
197
|
+
crashForTest() {
|
|
198
|
+
this.child?.send({ type: "crash-for-test" });
|
|
199
|
+
}
|
|
200
|
+
ensureChild() {
|
|
201
|
+
if (this.child)
|
|
202
|
+
return this.child;
|
|
203
|
+
if (Date.now() < this.disabledUntil)
|
|
204
|
+
return null;
|
|
205
|
+
if (!existsSync(this.childPath)) {
|
|
206
|
+
this.recordFailure();
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
let child;
|
|
210
|
+
try {
|
|
211
|
+
child = fork(this.childPath, [], {
|
|
212
|
+
execArgv: [],
|
|
213
|
+
stdio: ["ignore", "ignore", "inherit", "ipc"],
|
|
214
|
+
serialization: "json",
|
|
215
|
+
// Electron desktop: the daemon runs as the Electron binary with
|
|
216
|
+
// ELECTRON_RUN_AS_NODE=1, and Electron's fork re-injects that variable, so a
|
|
217
|
+
// PATH-only env still boots the helper as plain Node.
|
|
218
|
+
env: {
|
|
219
|
+
PATH: process.env.PATH ?? "",
|
|
220
|
+
...(process.env.PASEO_SPAWN_BROKER_TEST_HOOKS === "1"
|
|
221
|
+
? { PASEO_SPAWN_BROKER_TEST_HOOKS: "1" }
|
|
222
|
+
: {}),
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
this.recordFailure();
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
this.child = child;
|
|
231
|
+
child.on("message", (message) => {
|
|
232
|
+
this.consecutiveFailures = 0;
|
|
233
|
+
this.pending.get(message.id)?.handleMessage(message);
|
|
234
|
+
});
|
|
235
|
+
child.on("error", () => this.handleChildLost(child));
|
|
236
|
+
child.on("exit", () => this.handleChildLost(child));
|
|
237
|
+
child.on("disconnect", () => this.handleChildLost(child));
|
|
238
|
+
this.installExitHook();
|
|
239
|
+
this.updateRef();
|
|
240
|
+
return child;
|
|
241
|
+
}
|
|
242
|
+
handleChildLost(child) {
|
|
243
|
+
if (this.child !== child)
|
|
244
|
+
return;
|
|
245
|
+
this.child = null;
|
|
246
|
+
this.recordFailure();
|
|
247
|
+
const lost = [...this.pending.values()];
|
|
248
|
+
this.pending.clear();
|
|
249
|
+
for (const proc of lost) {
|
|
250
|
+
proc.handleBrokerLost();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
recordFailure() {
|
|
254
|
+
this.consecutiveFailures += 1;
|
|
255
|
+
if (this.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
|
|
256
|
+
this.disabledUntil = Date.now() + FAILURE_COOLDOWN_MS;
|
|
257
|
+
this.consecutiveFailures = 0;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
updateRef() {
|
|
261
|
+
const child = this.child;
|
|
262
|
+
if (!child)
|
|
263
|
+
return;
|
|
264
|
+
// Never keep the daemon alive just for the helper; hold a ref only while a
|
|
265
|
+
// request is in flight so awaiting callers are not cut off.
|
|
266
|
+
const channel = child.channel;
|
|
267
|
+
if (this.pending.size > 0) {
|
|
268
|
+
child.ref();
|
|
269
|
+
channel?.ref?.();
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
child.unref();
|
|
273
|
+
channel?.unref?.();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
installExitHook() {
|
|
277
|
+
if (this.exitHookInstalled)
|
|
278
|
+
return;
|
|
279
|
+
this.exitHookInstalled = true;
|
|
280
|
+
process.once("exit", () => {
|
|
281
|
+
try {
|
|
282
|
+
this.child?.kill("SIGKILL");
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
// already gone
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
let sharedBroker = null;
|
|
291
|
+
export function getSharedSpawnBroker() {
|
|
292
|
+
sharedBroker ?? (sharedBroker = new SpawnBroker());
|
|
293
|
+
return sharedBroker;
|
|
294
|
+
}
|
|
295
|
+
export function resetSharedSpawnBrokerForTest() {
|
|
296
|
+
sharedBroker?.dispose();
|
|
297
|
+
sharedBroker = null;
|
|
298
|
+
}
|
|
299
|
+
//# sourceMappingURL=spawn-broker.js.map
|
package/dist/src/utils/spawn.js
CHANGED
|
@@ -2,6 +2,7 @@ import { execFile, spawn } from "node:child_process";
|
|
|
2
2
|
import { extname } from "node:path";
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
4
|
import { createExternalCommandProcessEnv } from "../server/paseo-env.js";
|
|
5
|
+
import { getSharedSpawnBroker, isSpawnBrokerEnabled, } from "./spawn-broker.js";
|
|
5
6
|
import { isWindowsCommandScript, quoteWindowsArgument, quoteWindowsCommand, } from "./windows-command.js";
|
|
6
7
|
const execFileAsync = promisify(execFile);
|
|
7
8
|
function hasPathSeparator(value) {
|
|
@@ -16,17 +17,21 @@ function shouldUseWindowsShell(command, requestedShell) {
|
|
|
16
17
|
}
|
|
17
18
|
return process.platform === "win32" && !hasPathSeparator(command) && !extname(command);
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
-
const { baseEnv, env, envOverlay
|
|
20
|
+
function resolveChildEnv(command, options) {
|
|
21
|
+
const { baseEnv, env, envOverlay } = options ?? {};
|
|
21
22
|
const resolvedBaseEnv = env ?? baseEnv ?? process.env;
|
|
23
|
+
return options?.envMode === "internal"
|
|
24
|
+
? { ...resolvedBaseEnv, ...envOverlay }
|
|
25
|
+
: createExternalCommandProcessEnv(command, resolvedBaseEnv, ...(envOverlay ? [envOverlay] : []));
|
|
26
|
+
}
|
|
27
|
+
export function spawnProcess(command, args, options) {
|
|
28
|
+
const { baseEnv: _baseEnv, env: _env, envOverlay: _envOverlay, ...spawnOptions } = options ?? {};
|
|
22
29
|
const isWindows = process.platform === "win32";
|
|
23
30
|
const shell = shouldUseWindowsShell(command, spawnOptions.shell);
|
|
24
31
|
const shouldQuoteForShell = isWindows && shell !== false;
|
|
25
32
|
const resolvedCommand = shouldQuoteForShell ? quoteWindowsCommand(command) : command;
|
|
26
33
|
const resolvedArgs = shouldQuoteForShell ? args.map(quoteWindowsArgument) : args;
|
|
27
|
-
const childEnv = options
|
|
28
|
-
? { ...resolvedBaseEnv, ...envOverlay }
|
|
29
|
-
: createExternalCommandProcessEnv(command, resolvedBaseEnv, ...(envOverlay ? [envOverlay] : []));
|
|
34
|
+
const childEnv = resolveChildEnv(command, options);
|
|
30
35
|
return spawn(resolvedCommand, resolvedArgs, {
|
|
31
36
|
...spawnOptions,
|
|
32
37
|
env: childEnv,
|
|
@@ -34,17 +39,53 @@ export function spawnProcess(command, args, options) {
|
|
|
34
39
|
windowsHide: true,
|
|
35
40
|
});
|
|
36
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Spawn a short-lived command whose stdin is ignored and whose stdout/stderr are
|
|
44
|
+
* piped. Runs through the spawn broker helper process when available (so the
|
|
45
|
+
* daemon does not fork its own large image), and falls back to spawnProcess.
|
|
46
|
+
* Use spawnProcess instead when you need stdin, detached, a pty or a real
|
|
47
|
+
* ChildProcess handle.
|
|
48
|
+
*/
|
|
49
|
+
export function spawnNonInteractive(command, args, options) {
|
|
50
|
+
const direct = () => spawnProcess(command, args, {
|
|
51
|
+
cwd: options?.cwd,
|
|
52
|
+
env: options?.env,
|
|
53
|
+
baseEnv: options?.baseEnv,
|
|
54
|
+
envOverlay: options?.envOverlay,
|
|
55
|
+
envMode: options?.envMode,
|
|
56
|
+
shell: options?.shell,
|
|
57
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
58
|
+
});
|
|
59
|
+
const shell = shouldUseWindowsShell(command, options?.shell);
|
|
60
|
+
if (!isSpawnBrokerEnabled() || shell !== false) {
|
|
61
|
+
return direct();
|
|
62
|
+
}
|
|
63
|
+
return getSharedSpawnBroker().spawn({
|
|
64
|
+
command,
|
|
65
|
+
args,
|
|
66
|
+
cwd: options?.cwd,
|
|
67
|
+
env: resolveChildEnv(command, options),
|
|
68
|
+
shell: false,
|
|
69
|
+
maxStdoutBytes: options?.maxStdoutBytes,
|
|
70
|
+
maxStderrBytes: options?.maxStderrBytes,
|
|
71
|
+
}, direct, { retryOnDirect: options?.retryOnDirect ?? false });
|
|
72
|
+
}
|
|
73
|
+
const DEFAULT_EXEC_MAX_BUFFER = 1024 * 1024;
|
|
37
74
|
export async function execCommand(command, args, options) {
|
|
38
|
-
const { baseEnv, env, envOverlay } = options ?? {};
|
|
39
|
-
const resolvedBaseEnv = env ?? baseEnv ?? process.env;
|
|
40
75
|
const isWindows = process.platform === "win32";
|
|
41
76
|
const shell = shouldUseWindowsShell(command, options?.shell);
|
|
42
77
|
const shouldQuoteForShell = isWindows && shell !== false;
|
|
78
|
+
if (!isSpawnBrokerEnabled() || shell !== false || shouldQuoteForShell) {
|
|
79
|
+
return execCommandDirect(command, args, options, shell);
|
|
80
|
+
}
|
|
81
|
+
return execCommandNonInteractive(command, args, options);
|
|
82
|
+
}
|
|
83
|
+
function execCommandDirect(command, args, options, shell) {
|
|
84
|
+
const isWindows = process.platform === "win32";
|
|
85
|
+
const shouldQuoteForShell = isWindows && shell !== false;
|
|
43
86
|
const resolvedCommand = shouldQuoteForShell ? quoteWindowsCommand(command) : command;
|
|
44
87
|
const resolvedArgs = shouldQuoteForShell ? args.map(quoteWindowsArgument) : args;
|
|
45
|
-
const childEnv = options
|
|
46
|
-
? { ...resolvedBaseEnv, ...envOverlay }
|
|
47
|
-
: createExternalCommandProcessEnv(command, resolvedBaseEnv, ...(envOverlay ? [envOverlay] : []));
|
|
88
|
+
const childEnv = resolveChildEnv(command, options);
|
|
48
89
|
return execFileAsync(resolvedCommand, resolvedArgs, {
|
|
49
90
|
cwd: options?.cwd,
|
|
50
91
|
env: childEnv,
|
|
@@ -56,6 +97,99 @@ export async function execCommand(command, args, options) {
|
|
|
56
97
|
windowsHide: true,
|
|
57
98
|
});
|
|
58
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* execFile semantics (timeout + killSignal, maxBuffer, "Command failed" errors
|
|
102
|
+
* carrying code/killed/signal/stdout/stderr) on top of spawnNonInteractive.
|
|
103
|
+
*/
|
|
104
|
+
function execCommandNonInteractive(command, args, options) {
|
|
105
|
+
const encoding = options?.encoding ?? "utf8";
|
|
106
|
+
const maxBuffer = options?.maxBuffer ?? DEFAULT_EXEC_MAX_BUFFER;
|
|
107
|
+
const killSignal = options?.killSignal ?? "SIGTERM";
|
|
108
|
+
const cmd = [command, ...args].join(" ");
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
const child = spawnNonInteractive(command, args, {
|
|
111
|
+
cwd: options?.cwd,
|
|
112
|
+
env: options?.env,
|
|
113
|
+
baseEnv: options?.baseEnv,
|
|
114
|
+
envOverlay: options?.envOverlay,
|
|
115
|
+
envMode: options?.envMode,
|
|
116
|
+
shell: false,
|
|
117
|
+
// One byte over the cap so the overflow is still detected here.
|
|
118
|
+
maxStdoutBytes: maxBuffer + 1,
|
|
119
|
+
maxStderrBytes: maxBuffer + 1,
|
|
120
|
+
});
|
|
121
|
+
const stdoutChunks = [];
|
|
122
|
+
const stderrChunks = [];
|
|
123
|
+
let stdoutLength = 0;
|
|
124
|
+
let stderrLength = 0;
|
|
125
|
+
let killed = false;
|
|
126
|
+
let settled = false;
|
|
127
|
+
let exError = null;
|
|
128
|
+
let timer;
|
|
129
|
+
const decode = (chunks) => Buffer.concat(chunks).toString(encoding);
|
|
130
|
+
const kill = () => {
|
|
131
|
+
killed = true;
|
|
132
|
+
try {
|
|
133
|
+
child.kill(killSignal);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
exError = error;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const finish = (code, signal) => {
|
|
140
|
+
if (settled)
|
|
141
|
+
return;
|
|
142
|
+
settled = true;
|
|
143
|
+
if (timer)
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
const stdout = decode(stdoutChunks);
|
|
146
|
+
const stderr = decode(stderrChunks);
|
|
147
|
+
if (!exError && code === 0 && signal === null) {
|
|
148
|
+
resolve({ stdout, stderr });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const error = exError ?? new Error(`Command failed: ${cmd}\n${stderr}`);
|
|
152
|
+
if (!exError) {
|
|
153
|
+
error.code = code !== null && code < 0 ? String(code) : code;
|
|
154
|
+
error.killed = killed;
|
|
155
|
+
error.signal = signal;
|
|
156
|
+
}
|
|
157
|
+
error.cmd = cmd;
|
|
158
|
+
error.stdout = stdout;
|
|
159
|
+
error.stderr = stderr;
|
|
160
|
+
reject(error);
|
|
161
|
+
};
|
|
162
|
+
const onData = (chunks, stream) => (chunk) => {
|
|
163
|
+
if (settled)
|
|
164
|
+
return;
|
|
165
|
+
const length = stream === "stdout" ? stdoutLength : stderrLength;
|
|
166
|
+
const room = maxBuffer - length;
|
|
167
|
+
if (chunk.length > room) {
|
|
168
|
+
chunks.push(chunk.subarray(0, Math.max(0, room)));
|
|
169
|
+
const error = new RangeError(`${stream} maxBuffer length exceeded`);
|
|
170
|
+
error.code = "ERR_CHILD_PROCESS_STDIO_MAXBUFFER";
|
|
171
|
+
exError = error;
|
|
172
|
+
kill();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
chunks.push(chunk);
|
|
176
|
+
if (stream === "stdout")
|
|
177
|
+
stdoutLength += chunk.length;
|
|
178
|
+
else
|
|
179
|
+
stderrLength += chunk.length;
|
|
180
|
+
};
|
|
181
|
+
child.stdout?.on("data", onData(stdoutChunks, "stdout"));
|
|
182
|
+
child.stderr?.on("data", onData(stderrChunks, "stderr"));
|
|
183
|
+
child.on("error", (error) => {
|
|
184
|
+
exError = error;
|
|
185
|
+
finish(null, null);
|
|
186
|
+
});
|
|
187
|
+
child.on("close", (code, signal) => finish(code, signal));
|
|
188
|
+
if (options?.timeout && options.timeout > 0) {
|
|
189
|
+
timer = setTimeout(kill, options.timeout);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
59
193
|
export function platformShell() {
|
|
60
194
|
if (process.platform === "win32") {
|
|
61
195
|
return { command: "cmd.exe", flag: ["/c"] };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperdrive.bot/fleet-server",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.158",
|
|
4
4
|
"description": "Paseo backend server",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"files": [
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"clean": "node ../../scripts/clean-package-dist.mjs",
|
|
40
40
|
"build": "npm run build:lib && npm run build:scripts",
|
|
41
41
|
"build:clean": "npm run clean && npm run build",
|
|
42
|
-
"build:lib": "tsc -p tsconfig.server.json --incremental false && node -e \"const fs=require('node:fs'); fs.mkdirSync('dist/server/server/speech/providers/local/sherpa/assets',{recursive:true}); fs.copyFileSync('src/server/speech/providers/local/sherpa/assets/silero_vad.onnx','dist/server/server/speech/providers/local/sherpa/assets/silero_vad.onnx'); fs.cpSync('src/terminal/shell-integration','dist/server/terminal/shell-integration',{recursive:true}); fs.cpSync('src/terminal/shell-integration','dist/src/terminal/shell-integration',{recursive:true}); fs.copyFileSync('src/terminal/terminal-ts-loader.mjs','dist/server/terminal/terminal-ts-loader.mjs');\"",
|
|
43
|
-
"build:scripts": "tsc -p tsconfig.scripts.json --incremental false && node -e \"const fs=require('node:fs'); fs.mkdirSync('dist/scripts',{recursive:true}); fs.copyFileSync('scripts/mcp-stdio-socket-bridge-cli.mjs','dist/scripts/mcp-stdio-socket-bridge-cli.mjs');\"",
|
|
42
|
+
"build:lib": "tsc -p tsconfig.server.json --incremental false && node -e \"const fs=require('node:fs'); fs.mkdirSync('dist/server/server/speech/providers/local/sherpa/assets',{recursive:true}); fs.copyFileSync('src/server/speech/providers/local/sherpa/assets/silero_vad.onnx','dist/server/server/speech/providers/local/sherpa/assets/silero_vad.onnx'); fs.cpSync('src/terminal/shell-integration','dist/server/terminal/shell-integration',{recursive:true}); fs.cpSync('src/terminal/shell-integration','dist/src/terminal/shell-integration',{recursive:true}); fs.copyFileSync('src/terminal/terminal-ts-loader.mjs','dist/server/terminal/terminal-ts-loader.mjs'); fs.mkdirSync('dist/server/utils',{recursive:true}); fs.copyFileSync('src/utils/spawn-broker-child.mjs','dist/server/utils/spawn-broker-child.mjs');\"",
|
|
43
|
+
"build:scripts": "tsc -p tsconfig.scripts.json --incremental false && node -e \"const fs=require('node:fs'); fs.mkdirSync('dist/scripts',{recursive:true}); fs.copyFileSync('scripts/mcp-stdio-socket-bridge-cli.mjs','dist/scripts/mcp-stdio-socket-bridge-cli.mjs'); fs.mkdirSync('dist/src/utils',{recursive:true}); fs.copyFileSync('src/utils/spawn-broker-child.mjs','dist/src/utils/spawn-broker-child.mjs');\"",
|
|
44
44
|
"prepack": "npm run build:clean && npm --prefix ../.. run build:daemon-web-ui",
|
|
45
45
|
"start": "node dist/scripts/supervisor-entrypoint.js",
|
|
46
46
|
"typecheck": "tsgo -p tsconfig.server.typecheck.json --noEmit",
|
|
@@ -66,11 +66,11 @@
|
|
|
66
66
|
"@agentclientprotocol/sdk": "^0.17.1",
|
|
67
67
|
"@anthropic-ai/claude-agent-sdk": "^0.3.195",
|
|
68
68
|
"@anthropic-ai/sdk": "^0.104.2",
|
|
69
|
-
"@hyperdrive.bot/fleet-client": "0.3.
|
|
70
|
-
"@hyperdrive.bot/fleet-extension-sdk": "0.3.
|
|
71
|
-
"@hyperdrive.bot/fleet-highlight": "0.3.
|
|
72
|
-
"@hyperdrive.bot/fleet-protocol": "0.3.
|
|
73
|
-
"@hyperdrive.bot/fleet-relay": "0.3.
|
|
69
|
+
"@hyperdrive.bot/fleet-client": "0.3.158",
|
|
70
|
+
"@hyperdrive.bot/fleet-extension-sdk": "0.3.158",
|
|
71
|
+
"@hyperdrive.bot/fleet-highlight": "0.3.158",
|
|
72
|
+
"@hyperdrive.bot/fleet-protocol": "0.3.158",
|
|
73
|
+
"@hyperdrive.bot/fleet-relay": "0.3.158",
|
|
74
74
|
"@isaacs/ttlcache": "^2.1.4",
|
|
75
75
|
"@modelcontextprotocol/sdk": "^1.20.1",
|
|
76
76
|
"@opencode-ai/sdk": "1.2.6",
|
|
Binary file
|