@melaya/runner 1.1.68 → 1.1.70

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/cli.js CHANGED
@@ -17,7 +17,10 @@ import { Command } from "commander";
17
17
  import chalk from "chalk";
18
18
  import { reportInstallFailure } from "./installReport.js";
19
19
  import { connect } from "./connection.js";
20
- import { detectModels } from "./detect.js";
20
+ import { scanProviders } from "./detect.js";
21
+ import { isHosted, hostStatus, onHostMessage, sendToHost, waitForHostToken, exitHostedSoon } from "./hostChannel.js";
22
+ import { markStage, onStage, startupComplete, startupTimings, formatStartupTimings } from "./startupTimings.js";
23
+ import { readStartupCache, writeStartupCache } from "./startupCache.js";
21
24
  // Logo colors: cyan → blue → indigo gradient (matching melaya.org)
22
25
  const c1 = chalk.hex("#22D3EE").bold;
23
26
  const c2 = chalk.hex("#10B0F0").bold;
@@ -43,15 +46,55 @@ const BANNER = `
43
46
  ${br("▸")} ${dm("Local AI Pipeline Runner")} ${chalk.hex("#333")(`v${version}`)}
44
47
  ${br("─────────────────────────────────────────────────────")}
45
48
  `;
49
+ // Inside the desktop app: give the last host message time to leave before exit.
50
+ const flushHost = () => (isHosted() ? new Promise((r) => setTimeout(r, 300)) : Promise.resolve());
51
+ // Desktop app "stop" (pause, disconnect, quit): run the graceful shutdown the
52
+ // CLI already has for Ctrl+C. Newest SIGTERM handler first, so connection.ts
53
+ // stops runs and assistant hosts before singleInstance.ts exits; the lock is
54
+ // released on "exit" either way. Hard exit if nothing is listening yet.
55
+ function stopHosted() {
56
+ const handlers = process.listeners("SIGTERM").slice().reverse();
57
+ exitHostedSoon(0, 5000);
58
+ for (const h of handlers) {
59
+ try {
60
+ h("SIGTERM");
61
+ }
62
+ catch { /* keep going */ }
63
+ }
64
+ if (handlers.length === 0)
65
+ process.exit(0);
66
+ }
46
67
  async function main() {
68
+ const hosted = isHosted();
47
69
  const program = new Command()
48
70
  .name("melaya-runner")
49
- .description("Run Melaya AI pipelines locally with your own models")
50
- .requiredOption("--token <token>", "Runner token from the Melaya platform")
71
+ .description("Run Melaya AI pipelines locally with your own models");
72
+ // In the desktop app the token arrives over the host channel, never argv.
73
+ if (hosted)
74
+ program.option("--token <token>", "Runner token from the Melaya platform");
75
+ else
76
+ program.requiredOption("--token <token>", "Runner token from the Melaya platform");
77
+ program
51
78
  .option("--server <url>", "Server URL", "wss://api.melaya.org")
52
79
  .option("--verbose", "Show detailed logs", false)
80
+ .option("--computer", "Let Melaya agents operate allowed apps on this machine (headless Linux / VM)", false)
81
+ .option("--computer-helper <path>", "Path to the melaya-computer helper binary (default: MELAYA_COMPUTER_HELPER)")
82
+ .option("--diagnostics", "Print startup stage timings (config, socket, online, Python, model discovery)", false)
53
83
  .parse(process.argv);
54
84
  const opts = program.opts();
85
+ // --computer: this runner hosts the computer executor itself (computerHostCli.ts); the desktop app hosts its own.
86
+ if (opts.computer && !hosted) {
87
+ const { enableCliComputerHost } = await import("./computerHostCli.js");
88
+ enableCliComputerHost({ helperPath: opts.computerHelper, verbose: opts.verbose });
89
+ }
90
+ if (hosted) {
91
+ hostStatus("starting");
92
+ sendToHost({ type: "version", runnerVersion: version });
93
+ onHostMessage((msg) => { if (msg.type === "stop")
94
+ stopHosted(); });
95
+ opts.token = await waitForHostToken();
96
+ }
97
+ markStage("config");
55
98
  console.log(BANNER);
56
99
  // Single-instance guard: refuse to start if another runner is already running
57
100
  // on this machine. Two runners for one user fight over a single server-side
@@ -61,63 +104,139 @@ async function main() {
61
104
  const { acquireSingleInstanceLock } = await import("./singleInstance.js");
62
105
  const lock = acquireSingleInstanceLock();
63
106
  if (!lock.ok) {
107
+ hostStatus("blocked", `Another Melaya runner is already running on this computer (process ${lock.ownerPid}). Close it, then resume the runner.`);
64
108
  console.log(chalk.red(` ✗ A Melaya runner is already running on this machine (pid ${lock.ownerPid}).`));
65
109
  console.log(chalk.gray(" Only one runner can run per machine. Stop the other one first"));
66
110
  console.log(chalk.gray(` (e.g. \`kill ${lock.ownerPid}\`, or close its terminal), then start again.`));
67
111
  await reportInstallFailure("single_instance_locked", { token: opts.token, server: opts.server });
112
+ await flushHost();
68
113
  process.exit(1);
69
114
  }
70
- // Detect Python. Fast path: a supported host interpreter (3.10–3.12). If the
71
- // host only has an unsupported/newer Python (Homebrew now ships 3.13/3.14 as
72
- // `python3`) — or none at all — we PROVISION a managed CPython 3.12 via uv, so
73
- // the runner needs zero Python setup on the box. `venv` can't do this (it only
74
- // wraps an existing interpreter); uv downloads a standalone one.
75
- let python;
76
- const py = await findPython();
77
- if (py.ok) {
78
- python = py.cmd;
79
- console.log(chalk.green(` ✓ Python: ${python} (3.${py.minor})`));
115
+ // Readiness (audit P1.2): connect FIRST with the last good model list from
116
+ // disk (or none), then resolve Python and scan model providers in parallel
117
+ // and publish the fresh list through `runner:models`. Before, Python probing,
118
+ // provisioning and a serial provider scan (up to ~24 s of timeouts) all ran
119
+ // before the socket even opened. Requests that need Python wait for it.
120
+ const cache = readStartupCache();
121
+ const pythonReady = resolvePython(cache.python);
122
+ const discovery = scanProviders();
123
+ const handle = await connect({
124
+ token: opts.token,
125
+ serverUrl: opts.server,
126
+ pythonPath: pythonReady,
127
+ models: cache.models ?? [],
128
+ verbose: opts.verbose,
129
+ });
130
+ say = (l) => handle.print(l);
131
+ // Desktop app: "rescan_models" after an engine launch or sign-in, so its models
132
+ // show up in seconds instead of at the next 15 min rescan.
133
+ if (hosted) {
134
+ onHostMessage((msg) => {
135
+ if (msg.type !== "rescan_models")
136
+ return;
137
+ void handle.rescanModels()
138
+ .then((models) => writeStartupCache({ models, modelsAt: Date.now() }))
139
+ .catch(() => { });
140
+ });
80
141
  }
81
- else {
82
- console.log(chalk.yellow(` ⚠ No supported host Python (need 3.${PY_MIN_MINOR}–3.${PY_MAX_MINOR}` +
83
- `${py.foundVersion ? `; host has ${py.foundVersion}` : ""}) — provisioning one automatically`));
84
- const { provisionPython } = await import("./uvBootstrap.js");
85
- const provisioned = await provisionPython(`3.${PY_MAX_MINOR}`, (m) => console.log(chalk.gray(` ${m}`)));
86
- if (!provisioned) {
87
- console.log(chalk.red(` ✗ Could not auto-provision Python 3.${PY_MAX_MINOR}`));
88
- console.log(chalk.gray(" Install a supported Python manually, then restart the runner:"));
89
- console.log(chalk.gray(" macOS: brew install python@3.12"));
90
- console.log(chalk.gray(" Linux: sudo apt install python3.12 python3.12-venv (or: pyenv install 3.12)"));
91
- console.log(chalk.gray(" Windows: winget install Python.Python.3.12"));
92
- await reportInstallFailure("python_missing", {
93
- token: opts.token, server: opts.server,
94
- detail: py.foundVersion ? `host=${py.foundVersion}` : "none",
95
- });
96
- process.exit(1);
142
+ // Fresh discovery: print it, publish it, remember it for the next start.
143
+ void discovery.then((scan) => {
144
+ reportModels(scan.models, (l) => handle.print(l));
145
+ if (opts.verbose || opts.diagnostics) {
146
+ const slow = scan.timedOut.length ? `; timed out: ${scan.timedOut.join(", ")}` : "";
147
+ handle.print(chalk.gray(` model discovery ${scan.ms}ms (${Object.entries(scan.probeMs).map(([k, v]) => `${k} ${v}ms`).join(", ")}${slow})`));
148
+ }
149
+ handle.publishModels(scan.models);
150
+ writeStartupCache({ models: scan.models, modelsAt: Date.now() });
151
+ markStage("discovery_done");
152
+ }).catch(() => { markStage("discovery_done"); });
153
+ void pythonReady.then(async (python) => {
154
+ if (python) {
155
+ markStage("python_ready");
156
+ return;
157
+ }
158
+ // Same terminal failure as before the reorder: no usable Python, no runner.
159
+ console.log(chalk.red(` ✗ Could not auto-provision Python 3.${PY_MAX_MINOR}`));
160
+ console.log(chalk.gray(" Install a supported Python manually, then restart the runner:"));
161
+ console.log(chalk.gray(" macOS: brew install python@3.12"));
162
+ console.log(chalk.gray(" Linux: sudo apt install python3.12 python3.12-venv (or: pyenv install 3.12)"));
163
+ console.log(chalk.gray(" Windows: winget install Python.Python.3.12"));
164
+ await reportInstallFailure("python_missing", {
165
+ token: opts.token, server: opts.server,
166
+ detail: lastPythonFound ? `host=${lastPythonFound}` : "none",
167
+ });
168
+ hostStatus("error", `Could not set up Python 3.${PY_MAX_MINOR} for local AI runs. Check your internet connection, then resume the runner.`);
169
+ await flushHost();
170
+ process.exit(1);
171
+ });
172
+ // --diagnostics (and --verbose): the stage timeline once every stage is in, or after 2 min.
173
+ if (opts.diagnostics || opts.verbose) {
174
+ let printed = false;
175
+ const print = () => {
176
+ if (printed)
177
+ return;
178
+ printed = true;
179
+ const lines = formatStartupTimings().split("\n").map((l) => ` ${l}`).join("\n");
180
+ handle.print(chalk.gray(`\n Startup timings (from process start):\n${lines}`));
181
+ if (opts.diagnostics)
182
+ handle.print(`startup-timings ${JSON.stringify(startupTimings().sinceStartMs)}`);
183
+ };
184
+ if (startupComplete())
185
+ print();
186
+ else {
187
+ const off = onStage(() => { if (startupComplete()) {
188
+ off();
189
+ print();
190
+ } });
191
+ setTimeout(() => { off(); print(); }, 120_000).unref?.();
97
192
  }
98
- python = provisioned;
99
- console.log(chalk.green(` ✓ Python: ${python} (auto-provisioned 3.${PY_MAX_MINOR})`));
100
193
  }
101
- // Detect local models
102
- const models = await detectModels();
194
+ }
195
+ function reportModels(models, print) {
103
196
  if (models.length === 0) {
104
- console.log(chalk.yellow(" ⚠ No local models detected (LM Studio / Ollama not running; Claude Code / Codex / GitHub Copilot not logged in)"));
105
- console.log(chalk.gray(" Start LM Studio or Ollama, log in to Claude Code / Codex, or run `melaya-runner copilot login`, then restart the runner"));
197
+ print(chalk.yellow(" ⚠ No local models detected (LM Studio / Ollama not running; Claude Code / Codex / GitHub Copilot not logged in)"));
198
+ print(chalk.gray(" Start LM Studio or Ollama, log in to Claude Code / Codex, or run `melaya-runner copilot login`; the runner picks them up automatically"));
199
+ return;
106
200
  }
107
- else {
108
- for (const m of models) {
109
- console.log(chalk.green(` ✓ ${m.provider}: ${m.name}`));
201
+ for (const m of models)
202
+ print(chalk.green(` ✓ ${m.provider}: ${m.name}`));
203
+ }
204
+ // The unsupported interpreter found during the last scan (for the failure report).
205
+ let lastPythonFound;
206
+ // Lines printed while the connect spinner may be spinning go through the
207
+ // connection's printer once it exists (keeps the spinner line intact).
208
+ let say = (l) => console.log(l);
209
+ // Detect Python. Fast path: the interpreter that worked last time (one probe).
210
+ // Then every candidate is probed IN PARALLEL (async, so the socket keeps
211
+ // running) and the best one by preference order wins. If the host only has an
212
+ // unsupported/newer Python (Homebrew now ships 3.13/3.14 as `python3`), or
213
+ // none at all, we PROVISION a managed CPython 3.12 via uv, so the runner needs
214
+ // zero Python setup on the box. `venv` can't do this (it only wraps an
215
+ // existing interpreter); uv downloads a standalone one.
216
+ async function resolvePython(cached) {
217
+ if (cached?.cmd) {
218
+ const v = await probePyVersion(cached.cmd);
219
+ if (v && v.major === 3 && v.minor >= PY_MIN_MINOR && v.minor <= PY_MAX_MINOR) {
220
+ say(chalk.green(` ✓ Python: ${cached.cmd} (3.${v.minor}${cached.provisioned ? ", auto-provisioned" : ""})`));
221
+ return cached.cmd;
110
222
  }
111
223
  }
112
- console.log();
113
- // Connect to the server
114
- await connect({
115
- token: opts.token,
116
- serverUrl: opts.server,
117
- pythonPath: python,
118
- models,
119
- verbose: opts.verbose,
120
- });
224
+ const py = await findPython();
225
+ if (py.ok) {
226
+ say(chalk.green(` ✓ Python: ${py.cmd} (3.${py.minor})`));
227
+ writeStartupCache({ python: { cmd: py.cmd, minor: py.minor } });
228
+ return py.cmd;
229
+ }
230
+ lastPythonFound = py.foundVersion;
231
+ say(chalk.yellow(` ⚠ No supported host Python (need 3.${PY_MIN_MINOR}–3.${PY_MAX_MINOR}` +
232
+ `${py.foundVersion ? `; host has ${py.foundVersion}` : ""}) — provisioning one automatically`));
233
+ const { provisionPython } = await import("./uvBootstrap.js");
234
+ const provisioned = await provisionPython(`3.${PY_MAX_MINOR}`, (m) => say(chalk.gray(` ${m}`)));
235
+ if (!provisioned)
236
+ return null;
237
+ say(chalk.green(` ✓ Python: ${provisioned} (auto-provisioned 3.${PY_MAX_MINOR})`));
238
+ writeStartupCache({ python: { cmd: provisioned, minor: PY_MAX_MINOR, provisioned: true } });
239
+ return provisioned;
121
240
  }
122
241
  // Supported CPython range for the runner's venv. agentscope and its native
123
242
  // deps (tiktoken, numpy, pydantic-core, curl_cffi via scrapling, sentence-
@@ -132,17 +251,21 @@ async function main() {
132
251
  const PY_MIN_MINOR = 10;
133
252
  const PY_MAX_MINOR = 12;
134
253
  async function probePyVersion(cmd) {
135
- const { execSync } = await import("child_process");
136
- try {
137
- const out = execSync(`"${cmd}" --version`, { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] }).trim();
138
- const m = out.match(/Python (\d+)\.(\d+)/);
139
- if (!m)
140
- return null;
141
- return { major: Number(m[1]), minor: Number(m[2]) };
142
- }
143
- catch {
144
- return null;
145
- }
254
+ const { execFile } = await import("child_process");
255
+ // Async (never blocks the socket) and no shell: the command is a fixed name or a cached path.
256
+ return new Promise((resolve) => {
257
+ try {
258
+ execFile(cmd, ["--version"], { encoding: "utf-8", timeout: 5000, windowsHide: true }, (err, stdout, stderr) => {
259
+ if (err)
260
+ return resolve(null);
261
+ const m = `${stdout || ""}${stderr || ""}`.trim().match(/Python (\d+)\.(\d+)/);
262
+ resolve(m ? { major: Number(m[1]), minor: Number(m[2]) } : null);
263
+ });
264
+ }
265
+ catch {
266
+ resolve(null);
267
+ }
268
+ });
146
269
  }
147
270
  async function findPython() {
148
271
  const candidates = [];
@@ -165,17 +288,16 @@ async function findPython() {
165
288
  }
166
289
  // 3. Generic names last — accepted only if they resolve into the supported range.
167
290
  candidates.push("python3", "python");
291
+ // Probe all candidates at once; the preference order above still decides.
292
+ const unique = [...new Set(candidates)];
293
+ const versions = await Promise.all(unique.map((cmd) => probePyVersion(cmd)));
168
294
  let fallback; // a real 3.x that exists but is out of range
169
- const seen = new Set();
170
- for (const cmd of candidates) {
171
- if (seen.has(cmd))
172
- continue;
173
- seen.add(cmd);
174
- const v = await probePyVersion(cmd);
295
+ for (let i = 0; i < unique.length; i++) {
296
+ const v = versions[i];
175
297
  if (!v || v.major !== 3)
176
298
  continue;
177
299
  if (v.minor >= PY_MIN_MINOR && v.minor <= PY_MAX_MINOR)
178
- return { ok: true, cmd, minor: v.minor };
300
+ return { ok: true, cmd: unique[i], minor: v.minor };
179
301
  if (!fallback)
180
302
  fallback = `Python ${v.major}.${v.minor}`;
181
303
  }
@@ -200,6 +322,8 @@ async function bootstrap() {
200
322
  }
201
323
  bootstrap().catch(async (e) => {
202
324
  console.error(chalk.red(`\n Fatal: ${e.message}\n`));
325
+ // The full error goes to the app's log (stderr above); the status line stays generic.
326
+ hostStatus("error", "The runner stopped unexpectedly. Copy diagnostics for details.");
203
327
  // Token is parsed inside main(); recover it from argv so a crash BEFORE the
204
328
  // socket still reports. Only the STAGE is sent — never the error text, which
205
329
  // routinely carries file paths and usernames.
@@ -209,5 +333,6 @@ bootstrap().catch(async (e) => {
209
333
  token: tokArg ? tokArg.replace(/^--token[= ]?/, "") : undefined,
210
334
  server: srvArg ? srvArg.replace(/^--server[= ]?/, "") : undefined,
211
335
  });
336
+ await flushHost();
212
337
  process.exit(1);
213
338
  });
@@ -63,8 +63,8 @@
63
63
  // kill verification (poll for exit; escalate; report kill_unverified if
64
64
  // the OS will not confirm death).
65
65
  import { spawn } from "node:child_process";
66
- import { writeFileSync, mkdirSync, existsSync } from "node:fs";
67
- import { join } from "node:path";
66
+ import { writeFileSync, mkdirSync, existsSync, realpathSync } from "node:fs";
67
+ import { join, dirname } from "node:path";
68
68
  import { createInterface } from "node:readline";
69
69
  export const DEFAULT_LIMITS = {
70
70
  wallClockMs: 60_000,
@@ -253,6 +253,14 @@ function buildSandboxedCommand(scratchDir, entryPath, limits) {
253
253
  // The node dyld shared cache path varies by macOS version; the
254
254
  // /System/Volumes/Preboot allowance covers macOS 13+.
255
255
  const nodeDir = nodeBin.replace(/\/[^/]+$/, "");
256
+ // Node resolves the entry script with realpathSync, which lstat()s
257
+ // every ancestor directory: allow metadata (stat only, no contents)
258
+ // on exactly those ancestors, nothing else. scratchDir is already a
259
+ // real path here (see runSandboxedScript).
260
+ const ancestorMeta = [];
261
+ for (let p = dirname(scratchDir); p !== "/"; p = dirname(p)) {
262
+ ancestorMeta.push(`(allow file-read-metadata (literal "${p}"))`);
263
+ }
256
264
  const profile = [
257
265
  "(version 1)",
258
266
  "(deny default)",
@@ -263,6 +271,10 @@ function buildSandboxedCommand(scratchDir, entryPath, limits) {
263
271
  // Node's own scratch dir: full read/write.
264
272
  `(allow file-read* (subpath "${scratchDir}"))`,
265
273
  `(allow file-write* (subpath "${scratchDir}"))`,
274
+ ...ancestorMeta,
275
+ // Node aborts during process init (SIGABRT) unless it can read
276
+ // the root directory itself (not its subtree).
277
+ "(allow file-read-data (literal \"/\"))",
266
278
  // macOS system libraries and dyld shared cache.
267
279
  "(allow file-read* (subpath \"/System/Library\"))",
268
280
  "(allow file-read* (subpath \"/System/Volumes\"))",
@@ -283,23 +295,23 @@ function buildSandboxedCommand(scratchDir, entryPath, limits) {
283
295
  "(allow ipc-posix-shm-read-data)",
284
296
  "(allow ipc-posix-shm-write-data)",
285
297
  ].join("\n");
286
- // Memory and CPU caps via a shell `ulimit` wrapper around sandbox-exec.
287
- // ulimit -v: virtual address space in KB (4x heap for runtime headroom).
288
- // ulimit -t: hard CPU time in seconds.
289
- // We use /bin/sh -c "ulimit ...; exec sandbox-exec ..." so both limits
290
- // apply before the Node process starts. This is a soft-then-hard pair;
291
- // SIGKILL from the parent wall-clock timer is the backstop.
292
- const asKb = memMb * 4 * 1024;
293
- const shellCmd = `ulimit -v ${asKb} -t ${cpuSec}; exec /usr/bin/sandbox-exec -p '${profile.replace(/'/g, "'\\''")}' ${JSON.stringify(nodeBin)} ${nodeArgs.map((a) => JSON.stringify(a)).join(" ")}`;
298
+ // CPU cap via a shell `ulimit` wrapper around sandbox-exec.
299
+ // ulimit -t: hard CPU time in seconds, applied before Node starts;
300
+ // `&&` refuses to start the worker if the cap cannot be set.
301
+ // HONEST LIMIT: macOS does not support RLIMIT_AS (`ulimit -v` fails
302
+ // with EINVAL and, chained with -t, used to skip the CPU cap too), so
303
+ // memory is capped by --max-old-space-size only. SIGKILL from the
304
+ // parent wall-clock timer is the backstop.
305
+ const shellCmd = `ulimit -t ${cpuSec} && exec /usr/bin/sandbox-exec -p '${profile.replace(/'/g, "'\\''")}' ${JSON.stringify(nodeBin)} ${nodeArgs.map((a) => JSON.stringify(a)).join(" ")}`;
294
306
  return {
295
307
  cmd: "/bin/sh",
296
308
  args: ["-c", shellCmd],
297
309
  sandbox: "seatbelt",
298
310
  };
299
311
  }
300
- // No sandbox-exec: fall back to permission model + ulimit wrapper.
301
- const asKb = memMb * 4 * 1024;
302
- const shellCmd = `ulimit -v ${asKb} -t ${cpuSec}; exec ${JSON.stringify(nodeBin)} ${nodeArgs.map((a) => JSON.stringify(a)).join(" ")}`;
312
+ // No sandbox-exec: fall back to permission model + ulimit wrapper
313
+ // (CPU cap only, see the RLIMIT_AS note above).
314
+ const shellCmd = `ulimit -t ${cpuSec} && exec ${JSON.stringify(nodeBin)} ${nodeArgs.map((a) => JSON.stringify(a)).join(" ")}`;
303
315
  return {
304
316
  cmd: "/bin/sh",
305
317
  args: ["-c", shellCmd],
@@ -348,14 +360,18 @@ export async function runSandboxedScript(opts) {
348
360
  const consoleLines = [];
349
361
  const traces = [];
350
362
  mkdirSync(opts.scratchDir, { recursive: true });
351
- const entryPath = join(opts.scratchDir, "melaya-code-worker.cjs");
363
+ // macOS: os.tmpdir() is /var/folders/..., a symlink to /private/var/...
364
+ // Seatbelt and the Node permission model both check resolved paths, so
365
+ // the worker is given the real path everywhere.
366
+ const scratchDir = process.platform === "darwin" ? realpathSync(opts.scratchDir) : opts.scratchDir;
367
+ const entryPath = join(scratchDir, "melaya-code-worker.cjs");
352
368
  writeFileSync(entryPath, WORKER_ENTRY_SOURCE, "utf-8");
353
- const { cmd, args, sandbox } = buildSandboxedCommand(opts.scratchDir, entryPath, limits);
369
+ const { cmd, args, sandbox } = buildSandboxedCommand(scratchDir, entryPath, limits);
354
370
  let child;
355
371
  try {
356
372
  child = spawn(cmd, args, {
357
- cwd: opts.scratchDir,
358
- env: sanitizedEnv(opts.scratchDir),
373
+ cwd: scratchDir,
374
+ env: sanitizedEnv(scratchDir),
359
375
  // ONLY stdio pipes are inherited; Node does not pass extra fds
360
376
  // unless they are listed here, which closes the inherited-fd
361
377
  // channel (plan 0.7).
@@ -0,0 +1,5 @@
1
+ export declare const COMPUTER_HELPER_VERSION = "1.0.1";
2
+ export declare const COMPUTER_HELPER_MANIFEST: Record<string, {
3
+ url: string;
4
+ sha256: string;
5
+ } | undefined>;
@@ -0,0 +1,18 @@
1
+ // GENERATED by .github/scripts/write-computer-helper-manifest.mjs in desktop-release.yml.
2
+ // Do not edit by hand: commit the file the release job uploads (artifact
3
+ // "computer-helper-manifest") once the helpers are published at these URLs.
4
+ //
5
+ // Pinned computer helper releases for the runner CLI host (computerHostCli.ts),
6
+ // keyed by `${process.platform}-${process.arch}`. A download is only accepted
7
+ // when its sha256 matches.
8
+ export const COMPUTER_HELPER_VERSION = "1.0.1";
9
+ export const COMPUTER_HELPER_MANIFEST = {
10
+ "linux-arm64": {
11
+ "url": "https://api.melaya.org/desktop/helpers/1.0.1/melaya-computer-linux-aarch64",
12
+ "sha256": "f5e6825fc00b589178b0540ed6f2e350dc8ac77bb97e08ae17395282465fec36"
13
+ },
14
+ "linux-x64": {
15
+ "url": "https://api.melaya.org/desktop/helpers/1.0.1/melaya-computer-linux-x86_64",
16
+ "sha256": "795933f10803f2bc25c95207e5a196ff6413e2e55f038ccd54c3f6f19e7c764c"
17
+ }
18
+ };