agent-yes 1.192.1 → 1.192.3
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/{SUPPORTED_CLIS-5N015orJ.js → SUPPORTED_CLIS--9MiXfni.js} +2 -2
- package/dist/SUPPORTED_CLIS-CZnCl1OU.js +8 -0
- package/dist/{agentShare-C4LxU2xT.js → agentShare-B6znJtFD.js} +3 -3
- package/dist/cli.js +7 -7
- package/dist/index.js +2 -2
- package/dist/nodeRuntime-DZRjxpNp.js +56 -0
- package/dist/{notifyDaemon--OxBe8nG.js → notifyDaemon-RhUYKA0l.js} +3 -3
- package/dist/{remotes-WVJBNajZ.js → remotes-CW-yxSgW.js} +1 -1
- package/dist/{remotes-CgT91bMo.js → remotes-oSmwSYaV.js} +2 -2
- package/dist/{rustBinary-DFoLw-3W.js → rustBinary-DUEd_K1i.js} +2 -2
- package/dist/{schedule-AZACZY6R.js → schedule-Dx_x0HRj.js} +24 -15
- package/dist/{serve-CSijvUDO.js → serve-BAeHlY-f.js} +61 -62
- package/dist/{setup-ChMQrv6t.js → setup-C0Lc_Mui.js} +2 -2
- package/dist/{spawnGate-BejtWiMJ.js → spawnGate-CWbdnJ5K.js} +1 -1
- package/dist/{spawnGate-CA5QD7D4.js → spawnGate-DRWa_Fzj.js} +1 -1
- package/dist/{subcommands-czdlkI5W.js → subcommands-BM61pkrL.js} +2 -2
- package/dist/{subcommands-DMe0U83W.js → subcommands-CH7ZKLkx.js} +9 -9
- package/dist/{tray-PMOWFOfk.js → tray-BKTZ5gIf.js} +1 -1
- package/dist/{ts-7XqYx0eW.js → ts-9Wbluc4o.js} +2 -2
- package/dist/{versionChecker-CUMd5SWt.js → versionChecker-DzFI7V_S.js} +2 -2
- package/dist/{webrtcRemote-CD5EQ3uX.js → webrtcRemote-BmuVKcrx.js} +1 -1
- package/lab/ui/index.html +58 -6
- package/package.json +1 -1
- package/ts/nodeRuntime.spec.ts +99 -0
- package/ts/nodeRuntime.ts +58 -0
- package/ts/oxmgrService.ts +6 -0
- package/ts/schedule.ts +8 -0
- package/ts/serve.spec.ts +34 -81
- package/ts/serve.ts +21 -46
- package/dist/SUPPORTED_CLIS-W067Yn4T.js +0 -8
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from "url";
|
|
|
7
7
|
|
|
8
8
|
//#region package.json
|
|
9
9
|
var name = "agent-yes";
|
|
10
|
-
var version = "1.192.
|
|
10
|
+
var version = "1.192.3";
|
|
11
11
|
|
|
12
12
|
//#endregion
|
|
13
13
|
//#region ts/versionChecker.ts
|
|
@@ -215,4 +215,4 @@ async function displayVersion() {
|
|
|
215
215
|
|
|
216
216
|
//#endregion
|
|
217
217
|
export { versionString as i, displayVersion as n, getInstalledPackage as r, checkAndAutoUpdate as t };
|
|
218
|
-
//# sourceMappingURL=versionChecker-
|
|
218
|
+
//# sourceMappingURL=versionChecker-DzFI7V_S.js.map
|
package/lab/ui/index.html
CHANGED
|
@@ -1947,6 +1947,7 @@
|
|
|
1947
1947
|
</p>
|
|
1948
1948
|
<div class="srow">
|
|
1949
1949
|
<button class="primary" id="exposeGo" type="button">Expose & open</button>
|
|
1950
|
+
<button id="exposeCopy" type="button">Copy link</button>
|
|
1950
1951
|
<button id="exposeCancel" type="button">Cancel</button>
|
|
1951
1952
|
</div>
|
|
1952
1953
|
</div>
|
|
@@ -3364,11 +3365,9 @@
|
|
|
3364
3365
|
|
|
3365
3366
|
// Agree → mint the exposure on its host and open the claim link (which sets
|
|
3366
3367
|
// the 8h cookie and redirects to the app), then refresh the manager list.
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
$("exposePrompt").hidden = true;
|
|
3371
|
-
if (!port) return;
|
|
3368
|
+
// Create (or reuse) the exposure and mint a fresh single-use claim link.
|
|
3369
|
+
// Returns the {url, claim, …} info, or null on failure (with a message).
|
|
3370
|
+
async function exposeNow(port, tx) {
|
|
3372
3371
|
let info = null;
|
|
3373
3372
|
try {
|
|
3374
3373
|
const r = await tx.post("/api/expose", { port });
|
|
@@ -3377,11 +3376,48 @@
|
|
|
3377
3376
|
if (!info || !info.claim) {
|
|
3378
3377
|
$("exposeEmpty").hidden = false;
|
|
3379
3378
|
$("exposeEmpty").textContent = "Couldn't expose port " + port + " (is `ay serve` reachable?).";
|
|
3380
|
-
return;
|
|
3379
|
+
return null;
|
|
3381
3380
|
}
|
|
3381
|
+
return info;
|
|
3382
|
+
}
|
|
3383
|
+
async function copyText(s) {
|
|
3384
|
+
try {
|
|
3385
|
+
await navigator.clipboard.writeText(s);
|
|
3386
|
+
return true;
|
|
3387
|
+
} catch {
|
|
3388
|
+
return false;
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
// Briefly show "Copied!" on a button, then restore its label.
|
|
3392
|
+
function flashCopied(btn) {
|
|
3393
|
+
if (!btn) return;
|
|
3394
|
+
const orig = btn.textContent;
|
|
3395
|
+
btn.textContent = "Copied!";
|
|
3396
|
+
setTimeout(() => {
|
|
3397
|
+
btn.textContent = orig;
|
|
3398
|
+
}, 1200);
|
|
3399
|
+
}
|
|
3400
|
+
async function doExpose() {
|
|
3401
|
+
const tx = exposePendingTx || localTx;
|
|
3402
|
+
const port = exposePendingPort;
|
|
3403
|
+
$("exposePrompt").hidden = true;
|
|
3404
|
+
if (!port) return;
|
|
3405
|
+
const info = await exposeNow(port, tx);
|
|
3406
|
+
if (!info) return;
|
|
3382
3407
|
window.open(info.claim, "_blank", "noopener,noreferrer");
|
|
3383
3408
|
refreshExposeList();
|
|
3384
3409
|
}
|
|
3410
|
+
async function doExposeCopy() {
|
|
3411
|
+
const tx = exposePendingTx || localTx;
|
|
3412
|
+
const port = exposePendingPort;
|
|
3413
|
+
if (!port) return;
|
|
3414
|
+
const info = await exposeNow(port, tx);
|
|
3415
|
+
if (!info) return;
|
|
3416
|
+
await copyText(info.claim);
|
|
3417
|
+
flashCopied($("exposeCopy"));
|
|
3418
|
+
$("exposePrompt").hidden = true;
|
|
3419
|
+
refreshExposeList();
|
|
3420
|
+
}
|
|
3385
3421
|
|
|
3386
3422
|
// Gather active exposures from every live host and render the manager rows.
|
|
3387
3423
|
async function refreshExposeList() {
|
|
@@ -3449,6 +3485,21 @@
|
|
|
3449
3485
|
};
|
|
3450
3486
|
row.appendChild(openBtn);
|
|
3451
3487
|
|
|
3488
|
+
const copyBtn = document.createElement("button");
|
|
3489
|
+
copyBtn.textContent = "Copy";
|
|
3490
|
+
// Mint a fresh single-use claim link and copy it (to share access).
|
|
3491
|
+
copyBtn.onclick = async () => {
|
|
3492
|
+
let link = ex.url;
|
|
3493
|
+
try {
|
|
3494
|
+
const r = await ex._tx.post("/api/expose", { port: ex.port });
|
|
3495
|
+
const info = r.ok ? JSON.parse(r.text) : null;
|
|
3496
|
+
if (info && info.claim) link = info.claim;
|
|
3497
|
+
} catch {}
|
|
3498
|
+
await copyText(link);
|
|
3499
|
+
flashCopied(copyBtn);
|
|
3500
|
+
};
|
|
3501
|
+
row.appendChild(copyBtn);
|
|
3502
|
+
|
|
3452
3503
|
const revoke = document.createElement("button");
|
|
3453
3504
|
revoke.className = "danger";
|
|
3454
3505
|
revoke.textContent = "Revoke";
|
|
@@ -3514,6 +3565,7 @@
|
|
|
3514
3565
|
(function exposeModalBoot() {
|
|
3515
3566
|
$("portsbtn")?.addEventListener("click", () => openExposeManager(true));
|
|
3516
3567
|
$("exposeGo")?.addEventListener("click", doExpose);
|
|
3568
|
+
$("exposeCopy")?.addEventListener("click", doExposeCopy);
|
|
3517
3569
|
$("exposeCancel")?.addEventListener("click", () => {
|
|
3518
3570
|
$("exposePrompt").hidden = true;
|
|
3519
3571
|
$("exposeTitle").textContent = "Exposed ports";
|
package/package.json
CHANGED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm, stat } from "fs/promises";
|
|
2
|
+
import { tmpdir } from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { ensureNodeRuntime, liveEnv } from "./nodeRuntime.ts";
|
|
6
|
+
|
|
7
|
+
// process.platform is stubbed per test (instead of skipIf) so every branch of
|
|
8
|
+
// ensureNodeRuntime is exercised on every CI OS — the win32 early-return on
|
|
9
|
+
// POSIX runners and the POSIX shim path on Windows runners (mkdir/writeFile/
|
|
10
|
+
// chmod all work there; the shim just isn't consulted by real spawns).
|
|
11
|
+
const realPlatform = process.platform;
|
|
12
|
+
function setPlatform(p: string) {
|
|
13
|
+
Object.defineProperty(process, "platform", { value: p, configurable: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// The bun-only-no-node path (sym003): pm2/oxmgr bins are `#!/usr/bin/env node`
|
|
17
|
+
// scripts, so with no node on PATH they die at the shebang. ensureNodeRuntime
|
|
18
|
+
// must bridge that with a node→bun shim in ay's own bin dir.
|
|
19
|
+
describe("ensureNodeRuntime", () => {
|
|
20
|
+
let home: string;
|
|
21
|
+
let savedPath: string | undefined;
|
|
22
|
+
let savedHome: string | undefined;
|
|
23
|
+
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
home = await mkdtemp(path.join(tmpdir(), "ay-node-shim-"));
|
|
26
|
+
savedPath = process.env.PATH;
|
|
27
|
+
savedHome = process.env.AGENT_YES_HOME;
|
|
28
|
+
process.env.AGENT_YES_HOME = home;
|
|
29
|
+
setPlatform("linux");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(async () => {
|
|
33
|
+
setPlatform(realPlatform);
|
|
34
|
+
process.env.PATH = savedPath;
|
|
35
|
+
if (savedHome === undefined) delete process.env.AGENT_YES_HOME;
|
|
36
|
+
else process.env.AGENT_YES_HOME = savedHome;
|
|
37
|
+
await rm(home, { recursive: true, force: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const noNode = (cmd: string) => (cmd === "bun" ? "/fake/bin/bun" : null);
|
|
41
|
+
|
|
42
|
+
it("writes an executable node→bun shim and prepends it to PATH when node is absent", async () => {
|
|
43
|
+
const shim = await ensureNodeRuntime(noNode);
|
|
44
|
+
expect(shim).toBe(path.join(home, "bin", "node"));
|
|
45
|
+
const body = await readFile(shim!, "utf-8");
|
|
46
|
+
expect(body).toBe("#!/bin/sh\nexec '/fake/bin/bun' \"$@\"\n");
|
|
47
|
+
if (realPlatform !== "win32") {
|
|
48
|
+
expect((await stat(shim!)).mode & 0o111).not.toBe(0); // executable
|
|
49
|
+
}
|
|
50
|
+
expect(process.env.PATH!.split(path.delimiter)[0]).toBe(path.join(home, "bin"));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("does not duplicate the PATH entry", async () => {
|
|
54
|
+
await ensureNodeRuntime(noNode);
|
|
55
|
+
await ensureNodeRuntime(noNode);
|
|
56
|
+
const binDir = path.join(home, "bin");
|
|
57
|
+
const hits = process.env.PATH!.split(path.delimiter).filter((p) => p === binDir);
|
|
58
|
+
expect(hits).toHaveLength(1);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("leaves an up-to-date shim untouched on repeat calls (read-only paths stay read-only)", async () => {
|
|
62
|
+
const shim = (await ensureNodeRuntime(noNode))!;
|
|
63
|
+
const before = await stat(shim);
|
|
64
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
65
|
+
await ensureNodeRuntime(noNode);
|
|
66
|
+
const after = await stat(shim);
|
|
67
|
+
expect(after.mtimeMs).toBe(before.mtimeMs); // not rewritten
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("single-quotes a bun path containing sh-special characters", async () => {
|
|
71
|
+
const shim = await ensureNodeRuntime((cmd) => (cmd === "bun" ? "/opt/we$ird `dir'/bun" : null));
|
|
72
|
+
const body = await readFile(shim!, "utf-8");
|
|
73
|
+
expect(body).toBe("#!/bin/sh\nexec '/opt/we$ird `dir'\\''/bun' \"$@\"\n");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("is a no-op when a real node exists", async () => {
|
|
77
|
+
expect(await ensureNodeRuntime(() => "/usr/bin/whatever")).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("is a no-op when bun is missing too (nothing to shim to)", async () => {
|
|
81
|
+
expect(await ensureNodeRuntime(() => null)).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("is a no-op on Windows (npm .cmd shims invoke node.exe directly)", async () => {
|
|
85
|
+
setPlatform("win32");
|
|
86
|
+
expect(await ensureNodeRuntime(noNode)).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("liveEnv", () => {
|
|
91
|
+
it("carries post-startup process.env mutations (unlike Bun.spawn's implicit env)", () => {
|
|
92
|
+
process.env.AY_LIVE_ENV_TEST = "set-after-start";
|
|
93
|
+
try {
|
|
94
|
+
expect(liveEnv().AY_LIVE_ENV_TEST).toBe("set-after-start");
|
|
95
|
+
} finally {
|
|
96
|
+
delete process.env.AY_LIVE_ENV_TEST;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, writeFile } from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { agentYesHome } from "./agentYesHome.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Snapshot of the CURRENT process.env for child spawns. Bun.spawn without an
|
|
7
|
+
* explicit `env` hands the child the environ captured at PROCESS STARTUP — not
|
|
8
|
+
* the live process.env — so post-startup mutations (ensureNodeRuntime's shim
|
|
9
|
+
* PATH prepend, installAndVerify's global-bin-dir prepend) silently never reach
|
|
10
|
+
* the child. That is exactly how `ay serve install` kept dying with
|
|
11
|
+
* `env: node: No such file or directory` at the `pm2 start` spawn on a bun-only
|
|
12
|
+
* box even though the probe (which passes env explicitly) had just succeeded.
|
|
13
|
+
* Every spawn of a process-manager binary must pass `env: liveEnv()`.
|
|
14
|
+
*/
|
|
15
|
+
export function liveEnv(): Record<string, string | undefined> {
|
|
16
|
+
return { ...process.env };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Both oxmgr's and pm2's global bins are `#!/usr/bin/env node` JS scripts, so on
|
|
21
|
+
* a bun-only box (setup.sh installs no node) exec dies with "env: node: No such
|
|
22
|
+
* file or directory" — even though pm2 is pure JS and runs perfectly under bun.
|
|
23
|
+
* When node is missing but bun exists, write a node→bun shim into ay's own bin
|
|
24
|
+
* dir and prepend it to PATH so `env node` resolves. NOT solvable with
|
|
25
|
+
* `bun add -g node`: that package (node-bin-gen) lands a broken arch-specific
|
|
26
|
+
* stub and `Bun.which("node")` still finds nothing. POSIX-only — on Windows the
|
|
27
|
+
* npm .cmd shims invoke node.exe directly and an sh script can't stand in.
|
|
28
|
+
* Read-only paths (`ay serve status`) also reach this via the manager probes:
|
|
29
|
+
* an up-to-date shim is left untouched there (a read, not a write), so only the
|
|
30
|
+
* very first run on a node-less box materializes the file.
|
|
31
|
+
* Returns the shim path when the shim is in effect, null when unneeded/impossible.
|
|
32
|
+
*/
|
|
33
|
+
export async function ensureNodeRuntime(
|
|
34
|
+
which: (cmd: string) => string | null = Bun.which,
|
|
35
|
+
): Promise<string | null> {
|
|
36
|
+
if (process.platform === "win32") return null;
|
|
37
|
+
if (which("node")) return null;
|
|
38
|
+
const bun = which("bun");
|
|
39
|
+
if (!bun) return null;
|
|
40
|
+
const binDir = path.join(agentYesHome(), "bin");
|
|
41
|
+
const shim = path.join(binDir, "node");
|
|
42
|
+
// Single-quote the bun path: inside sh double quotes `$`, backtick and `\`
|
|
43
|
+
// would still expand, corrupting the shim for a path containing them.
|
|
44
|
+
const body = `#!/bin/sh\nexec '${bun.replace(/'/g, `'\\''`)}' "$@"\n`;
|
|
45
|
+
try {
|
|
46
|
+
if ((await readFile(shim, "utf-8").catch(() => null)) !== body) {
|
|
47
|
+
await mkdir(binDir, { recursive: true });
|
|
48
|
+
await writeFile(shim, body);
|
|
49
|
+
await chmod(shim, 0o755);
|
|
50
|
+
}
|
|
51
|
+
if (!(process.env.PATH ?? "").split(path.delimiter).includes(binDir)) {
|
|
52
|
+
process.env.PATH = `${binDir}${path.delimiter}${process.env.PATH ?? ""}`;
|
|
53
|
+
}
|
|
54
|
+
return shim;
|
|
55
|
+
} catch {
|
|
56
|
+
return null; // best-effort: the exec probe will fail and name the reason
|
|
57
|
+
}
|
|
58
|
+
}
|
package/ts/oxmgrService.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { liveEnv } from "./nodeRuntime.ts";
|
|
1
2
|
// Register oxmgr's daemon with the platform init system (launchd on macOS,
|
|
2
3
|
// systemd on Linux, Task Scheduler on Windows) so managed processes survive a
|
|
3
4
|
// *reboot*, not just a crash.
|
|
@@ -18,8 +19,12 @@ export async function ensureBootAutostart(oxmgrBin: string): Promise<boolean> {
|
|
|
18
19
|
try {
|
|
19
20
|
// Already registered with the init system? Then we're done — don't bounce
|
|
20
21
|
// the daemon (and all its children) just to re-assert what's already true.
|
|
22
|
+
// env: liveEnv() so the node→bun shim's PATH prepend reaches oxmgr's
|
|
23
|
+
// `#!/usr/bin/env node` launcher (implicit inheritance uses the startup
|
|
24
|
+
// environ, missing post-startup mutations).
|
|
21
25
|
const status = Bun.spawn([oxmgrBin, "service", "status"], {
|
|
22
26
|
stdio: ["ignore", "ignore", "ignore"],
|
|
27
|
+
env: liveEnv(),
|
|
23
28
|
});
|
|
24
29
|
if ((await status.exited) === 0) return true;
|
|
25
30
|
|
|
@@ -28,6 +33,7 @@ export async function ensureBootAutostart(oxmgrBin: string): Promise<boolean> {
|
|
|
28
33
|
// so passing it after `install` is rejected.
|
|
29
34
|
const svc = Bun.spawn([oxmgrBin, "service", "install"], {
|
|
30
35
|
stdio: ["ignore", "ignore", "ignore"],
|
|
36
|
+
env: liveEnv(),
|
|
31
37
|
});
|
|
32
38
|
return (await svc.exited) === 0;
|
|
33
39
|
} catch {
|
package/ts/schedule.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { SUPPORTED_CLIS } from "./SUPPORTED_CLIS.ts";
|
|
3
3
|
import { resolveSpawnCwd } from "./workspaceConfig.ts";
|
|
4
4
|
import { ensureBootAutostart } from "./oxmgrService.ts";
|
|
5
|
+
import { ensureNodeRuntime, liveEnv } from "./nodeRuntime.ts";
|
|
5
6
|
|
|
6
7
|
// `ay schedule` — run an agent on a recurring schedule via oxmgr's cron support.
|
|
7
8
|
// A scheduled job runs once immediately AND on every cron tick (with
|
|
@@ -33,16 +34,23 @@ function schedName(explicit: string | undefined, cli: string, key: string): stri
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
async function run(cmd: string[], capture = false): Promise<{ code: number; out: string }> {
|
|
37
|
+
// env: liveEnv() — oxmgr's global bin is a `#!/usr/bin/env node` script, and
|
|
38
|
+
// ensureNodeRuntime's shim PATH prepend only reaches children given the LIVE
|
|
39
|
+
// process.env (Bun.spawn's implicit inheritance uses the startup environ).
|
|
36
40
|
const p = Bun.spawn(cmd, {
|
|
37
41
|
stdin: "ignore",
|
|
38
42
|
stdout: capture ? "pipe" : "inherit",
|
|
39
43
|
stderr: capture ? "pipe" : "inherit",
|
|
44
|
+
env: liveEnv(),
|
|
40
45
|
});
|
|
41
46
|
const out = capture ? await new Response(p.stdout).text() : "";
|
|
42
47
|
return { code: (await p.exited) ?? 1, out };
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
export async function cmdSchedule(rest: string[]): Promise<number> {
|
|
51
|
+
// On a bun-only box oxmgr's `#!/usr/bin/env node` launcher needs the node→bun
|
|
52
|
+
// shim before ANY oxmgr call below can exec (same fix as serve-install).
|
|
53
|
+
await ensureNodeRuntime();
|
|
46
54
|
const oxmgrBin = Bun.which("oxmgr");
|
|
47
55
|
if (!oxmgrBin) {
|
|
48
56
|
process.stderr.write(
|
package/ts/serve.spec.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
-
import { ensureNodeRuntime, isNoNodeExecError, oxmgrVersionHasWindowsFix } from "./serve.ts";
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { isNoNodeExecError, oxmgrVersionHasWindowsFix } from "./serve.ts";
|
|
6
4
|
|
|
7
5
|
// Guards the Windows daemon-manager selection: on Windows we only PREFER oxmgr
|
|
8
6
|
// when the installed build carries the daemon-socket-inheritance fix. Stock
|
|
@@ -35,82 +33,6 @@ describe("oxmgrVersionHasWindowsFix", () => {
|
|
|
35
33
|
});
|
|
36
34
|
});
|
|
37
35
|
|
|
38
|
-
// The bun-only-no-node path (sym003): pm2/oxmgr bins are `#!/usr/bin/env node`
|
|
39
|
-
// scripts, so with no node on PATH they die at the shebang. ensureNodeRuntime
|
|
40
|
-
// must bridge that with a node→bun shim in ay's own bin dir.
|
|
41
|
-
describe("ensureNodeRuntime", () => {
|
|
42
|
-
let home: string;
|
|
43
|
-
let savedPath: string | undefined;
|
|
44
|
-
let savedHome: string | undefined;
|
|
45
|
-
|
|
46
|
-
beforeEach(async () => {
|
|
47
|
-
home = await mkdtemp(path.join(tmpdir(), "ay-node-shim-"));
|
|
48
|
-
savedPath = process.env.PATH;
|
|
49
|
-
savedHome = process.env.AGENT_YES_HOME;
|
|
50
|
-
process.env.AGENT_YES_HOME = home;
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
afterEach(async () => {
|
|
54
|
-
process.env.PATH = savedPath;
|
|
55
|
-
if (savedHome === undefined) delete process.env.AGENT_YES_HOME;
|
|
56
|
-
else process.env.AGENT_YES_HOME = savedHome;
|
|
57
|
-
await rm(home, { recursive: true, force: true });
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
const noNode = (cmd: string) => (cmd === "bun" ? "/fake/bin/bun" : null);
|
|
61
|
-
|
|
62
|
-
it.skipIf(process.platform === "win32")(
|
|
63
|
-
"writes an executable node→bun shim and prepends it to PATH when node is absent",
|
|
64
|
-
async () => {
|
|
65
|
-
const shim = await ensureNodeRuntime(noNode);
|
|
66
|
-
expect(shim).toBe(path.join(home, "bin", "node"));
|
|
67
|
-
const body = await readFile(shim!, "utf-8");
|
|
68
|
-
expect(body).toBe("#!/bin/sh\nexec '/fake/bin/bun' \"$@\"\n");
|
|
69
|
-
expect((await stat(shim!)).mode & 0o111).not.toBe(0); // executable
|
|
70
|
-
expect(process.env.PATH!.split(path.delimiter)[0]).toBe(path.join(home, "bin"));
|
|
71
|
-
},
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
it.skipIf(process.platform === "win32")("does not duplicate the PATH entry", async () => {
|
|
75
|
-
await ensureNodeRuntime(noNode);
|
|
76
|
-
await ensureNodeRuntime(noNode);
|
|
77
|
-
const binDir = path.join(home, "bin");
|
|
78
|
-
const hits = process.env.PATH!.split(path.delimiter).filter((p) => p === binDir);
|
|
79
|
-
expect(hits).toHaveLength(1);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it.skipIf(process.platform === "win32")(
|
|
83
|
-
"leaves an up-to-date shim untouched on repeat calls (read-only paths stay read-only)",
|
|
84
|
-
async () => {
|
|
85
|
-
const shim = (await ensureNodeRuntime(noNode))!;
|
|
86
|
-
const before = await stat(shim);
|
|
87
|
-
await new Promise((r) => setTimeout(r, 20));
|
|
88
|
-
await ensureNodeRuntime(noNode);
|
|
89
|
-
const after = await stat(shim);
|
|
90
|
-
expect(after.mtimeMs).toBe(before.mtimeMs); // not rewritten
|
|
91
|
-
},
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
it.skipIf(process.platform === "win32")(
|
|
95
|
-
"single-quotes a bun path containing sh-special characters",
|
|
96
|
-
async () => {
|
|
97
|
-
const shim = await ensureNodeRuntime((cmd) =>
|
|
98
|
-
cmd === "bun" ? "/opt/we$ird `dir'/bun" : null,
|
|
99
|
-
);
|
|
100
|
-
const body = await readFile(shim!, "utf-8");
|
|
101
|
-
expect(body).toBe("#!/bin/sh\nexec '/opt/we$ird `dir'\\''/bun' \"$@\"\n");
|
|
102
|
-
},
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
it("is a no-op when a real node exists", async () => {
|
|
106
|
-
expect(await ensureNodeRuntime(() => "/usr/bin/whatever")).toBeNull();
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("is a no-op when bun is missing too (nothing to shim to)", async () => {
|
|
110
|
-
expect(await ensureNodeRuntime(() => null)).toBeNull();
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
36
|
// Guards the serve-install diagnostic: a missing node runtime must be reported
|
|
115
37
|
// as such, never as a "glibc mismatch" (glibc doesn't even exist on macOS).
|
|
116
38
|
describe("isNoNodeExecError", () => {
|
|
@@ -128,6 +50,12 @@ describe("isNoNodeExecError", () => {
|
|
|
128
50
|
).toBe(true);
|
|
129
51
|
});
|
|
130
52
|
|
|
53
|
+
it("matches localized-then-normalized output (probe pins LC_ALL=C)", () => {
|
|
54
|
+
// managerProbe spawns with LC_ALL=C precisely so this English form is what
|
|
55
|
+
// we always see; this test documents the coupling.
|
|
56
|
+
expect(isNoNodeExecError("env: node: No such file or directory")).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
131
59
|
it("does not match a real native/glibc failure", () => {
|
|
132
60
|
expect(
|
|
133
61
|
isNoNodeExecError(
|
|
@@ -137,3 +65,28 @@ describe("isNoNodeExecError", () => {
|
|
|
137
65
|
expect(isNoNodeExecError("")).toBe(false);
|
|
138
66
|
});
|
|
139
67
|
});
|
|
68
|
+
|
|
69
|
+
// Bun.spawn without an explicit `env` hands the child the environ captured at
|
|
70
|
+
// process startup, NOT the live process.env — so ensureNodeRuntime's shim PATH
|
|
71
|
+
// prepend never reached children spawned that way (`pm2 start` died with
|
|
72
|
+
// `env: node: No such file or directory` right after the probe passed). Guard
|
|
73
|
+
// the fix pattern: every spawn of a daemon-manager binary in serve.ts must pass
|
|
74
|
+
// an explicit env.
|
|
75
|
+
describe("manager spawns pass a live env snapshot", () => {
|
|
76
|
+
it("every Bun.spawn of mgr.bin/startArgv/installer carries an env option", async () => {
|
|
77
|
+
const src = await readFile(new URL("./serve.ts", import.meta.url), "utf-8");
|
|
78
|
+
// Grab each Bun.spawn(...) call whose argv mentions a manager binary.
|
|
79
|
+
// Capture from the call opening through the options object's closing `})`.
|
|
80
|
+
const calls = src.match(
|
|
81
|
+
/Bun\.spawn\((?:\[[^\]]*mgr\.bin[^\]]*\]|startArgv|installer),[\s\S]*?\}\)/g,
|
|
82
|
+
);
|
|
83
|
+
expect(calls?.length).toBeGreaterThanOrEqual(7);
|
|
84
|
+
for (const call of calls!) {
|
|
85
|
+
// Require a real liveEnv() env option, not just the substring "env:"
|
|
86
|
+
// (which a comment inside the options object could satisfy).
|
|
87
|
+
expect(call, `missing explicit env in: ${call.slice(0, 80)}`).toMatch(
|
|
88
|
+
/env: (?:\{\s*(?:\.\.\.)?)?liveEnv\(\)/,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
package/ts/serve.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdir, open, readFile, stat, unlink, writeFile } from "fs/promises";
|
|
2
2
|
import { renameSync, watch, writeFileSync } from "node:fs";
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
type CommonOpts,
|
|
26
26
|
} from "./subcommands.ts";
|
|
27
27
|
import { TYPING_BADGE } from "./badges.ts";
|
|
28
|
+
import { ensureNodeRuntime, liveEnv } from "./nodeRuntime.ts";
|
|
28
29
|
import { updateGlobalPidStatus } from "./globalPidIndex.ts";
|
|
29
30
|
import { spawnRejectionReason } from "./spawnGate.ts";
|
|
30
31
|
import { findSpawnHiddenLauncher } from "./rustBinary.ts";
|
|
@@ -359,6 +360,7 @@ function oxmgrHasWindowsFix(bin: string): boolean {
|
|
|
359
360
|
encoding: "utf-8",
|
|
360
361
|
timeout: 3000,
|
|
361
362
|
stdio: ["ignore", "pipe", "ignore"],
|
|
363
|
+
env: { ...process.env } as NodeJS.ProcessEnv,
|
|
362
364
|
});
|
|
363
365
|
oxmgrWinFixCache = oxmgrVersionHasWindowsFix(out);
|
|
364
366
|
} catch {
|
|
@@ -405,45 +407,6 @@ async function globalBinDir(installer: string[]): Promise<string | null> {
|
|
|
405
407
|
return isBun ? out : path.join(out, "bin");
|
|
406
408
|
}
|
|
407
409
|
|
|
408
|
-
// Both oxmgr's and pm2's global bins are `#!/usr/bin/env node` JS scripts, so on
|
|
409
|
-
// a bun-only box (setup.sh installs no node) exec dies with "env: node: No such
|
|
410
|
-
// file or directory" — even though pm2 is pure JS and runs perfectly under bun.
|
|
411
|
-
// When node is missing but bun exists, write a node→bun shim into ay's own bin
|
|
412
|
-
// dir and prepend it to PATH so `env node` resolves. NOT solvable with
|
|
413
|
-
// `bun add -g node`: that package (node-bin-gen) lands a broken arch-specific
|
|
414
|
-
// stub and `Bun.which("node")` still finds nothing. POSIX-only — on Windows the
|
|
415
|
-
// npm .cmd shims invoke node.exe directly and an sh script can't stand in.
|
|
416
|
-
// Called from managerRunnable, which read-only paths (`ay serve status`) also
|
|
417
|
-
// reach: an up-to-date shim is left untouched there (a read, not a write), so
|
|
418
|
-
// only the very first run on a node-less box materializes the file.
|
|
419
|
-
// Returns the shim path when the shim is in effect, null when unneeded/impossible.
|
|
420
|
-
export async function ensureNodeRuntime(
|
|
421
|
-
which: (cmd: string) => string | null = Bun.which,
|
|
422
|
-
): Promise<string | null> {
|
|
423
|
-
if (process.platform === "win32") return null;
|
|
424
|
-
if (which("node")) return null;
|
|
425
|
-
const bun = which("bun");
|
|
426
|
-
if (!bun) return null;
|
|
427
|
-
const binDir = path.join(agentYesHome(), "bin");
|
|
428
|
-
const shim = path.join(binDir, "node");
|
|
429
|
-
// Single-quote the bun path: inside sh double quotes `$`, backtick and `\`
|
|
430
|
-
// would still expand, corrupting the shim for a path containing them.
|
|
431
|
-
const body = `#!/bin/sh\nexec '${bun.replace(/'/g, `'\\''`)}' "$@"\n`;
|
|
432
|
-
try {
|
|
433
|
-
if ((await readFile(shim, "utf-8").catch(() => null)) !== body) {
|
|
434
|
-
await mkdir(binDir, { recursive: true });
|
|
435
|
-
await writeFile(shim, body);
|
|
436
|
-
await chmod(shim, 0o755);
|
|
437
|
-
}
|
|
438
|
-
if (!(process.env.PATH ?? "").split(path.delimiter).includes(binDir)) {
|
|
439
|
-
process.env.PATH = `${binDir}${path.delimiter}${process.env.PATH ?? ""}`;
|
|
440
|
-
}
|
|
441
|
-
return shim;
|
|
442
|
-
} catch {
|
|
443
|
-
return null; // best-effort: the exec probe will fail and name the reason
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
410
|
// Did an exec attempt die because `env` couldn't find a node runtime? macOS/BSD
|
|
448
411
|
// env says `env: node: No such file or directory`, GNU coreutils quotes it
|
|
449
412
|
// (`env: 'node': …`), Windows cmd says `'node' is not recognized`.
|
|
@@ -463,7 +426,7 @@ async function managerProbe(mgr: DaemonManager): Promise<{ ok: boolean; stderr:
|
|
|
463
426
|
const p = Bun.spawn([mgr.bin, "--version"], {
|
|
464
427
|
stdout: "ignore",
|
|
465
428
|
stderr: "pipe",
|
|
466
|
-
env: { ...
|
|
429
|
+
env: { ...liveEnv(), LC_ALL: "C" },
|
|
467
430
|
});
|
|
468
431
|
const [code, stderr] = await Promise.all([p.exited, new Response(p.stderr).text()]);
|
|
469
432
|
return { ok: (code ?? 1) === 0, stderr };
|
|
@@ -517,7 +480,11 @@ async function installAndVerify(pkg: "oxmgr" | "pm2"): Promise<DaemonManager | n
|
|
|
517
480
|
const installer = bun ? [bun, "add", "-g", pkg] : npm ? [npm, "install", "-g", pkg] : null;
|
|
518
481
|
if (!installer) return null;
|
|
519
482
|
process.stderr.write(`ay serve install: installing ${pkg}…\n`);
|
|
520
|
-
const code =
|
|
483
|
+
const code =
|
|
484
|
+
(await Bun.spawn(installer, {
|
|
485
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
486
|
+
env: liveEnv(),
|
|
487
|
+
}).exited) ?? 1;
|
|
521
488
|
if (code !== 0) {
|
|
522
489
|
process.stderr.write(`ay serve install: '${installer.join(" ")}' failed (exit ${code})\n`);
|
|
523
490
|
return null;
|
|
@@ -662,7 +629,9 @@ async function ensureBootAutostart(mgr: DaemonManager): Promise<boolean> {
|
|
|
662
629
|
|
|
663
630
|
async function spawnExit(cmd: string[]): Promise<number> {
|
|
664
631
|
try {
|
|
665
|
-
return (
|
|
632
|
+
return (
|
|
633
|
+
(await Bun.spawn(cmd, { stdio: ["ignore", "ignore", "ignore"], env: liveEnv() }).exited) ?? 1
|
|
634
|
+
);
|
|
666
635
|
} catch {
|
|
667
636
|
return 1;
|
|
668
637
|
}
|
|
@@ -675,7 +644,11 @@ async function spawnExit(cmd: string[]): Promise<number> {
|
|
|
675
644
|
async function readDaemonServeArgs(mgr: DaemonManager): Promise<string[] | null> {
|
|
676
645
|
try {
|
|
677
646
|
if (mgr.id === "oxmgr") {
|
|
678
|
-
const p = Bun.spawn([mgr.bin, "status", DAEMON_NAME], {
|
|
647
|
+
const p = Bun.spawn([mgr.bin, "status", DAEMON_NAME], {
|
|
648
|
+
stdout: "pipe",
|
|
649
|
+
stderr: "ignore",
|
|
650
|
+
env: liveEnv(),
|
|
651
|
+
});
|
|
679
652
|
const out = await new Response(p.stdout).text();
|
|
680
653
|
if ((await p.exited) !== 0) return null;
|
|
681
654
|
const m = /Command:\s*(.+)/.exec(out);
|
|
@@ -683,7 +656,7 @@ async function readDaemonServeArgs(mgr: DaemonManager): Promise<string[] | null>
|
|
|
683
656
|
const after = /\bserve\b\s*(.*)$/.exec(m[1]!.trim());
|
|
684
657
|
return after ? after[1]!.split(/\s+/).filter(Boolean) : [];
|
|
685
658
|
}
|
|
686
|
-
const p = Bun.spawn([mgr.bin, "jlist"], { stdout: "pipe", stderr: "ignore" });
|
|
659
|
+
const p = Bun.spawn([mgr.bin, "jlist"], { stdout: "pipe", stderr: "ignore", env: liveEnv() });
|
|
687
660
|
const out = await new Response(p.stdout).text();
|
|
688
661
|
if ((await p.exited) !== 0) return null;
|
|
689
662
|
const list = JSON.parse(out) as Array<{ name?: string; pm2_env?: { args?: string[] } }>;
|
|
@@ -911,7 +884,7 @@ async function cmdServeDaemon(sub: string, args: string[]): Promise<number> {
|
|
|
911
884
|
"--",
|
|
912
885
|
...managedArgv.slice(1),
|
|
913
886
|
];
|
|
914
|
-
const proc = Bun.spawn(startArgv, { stdio: ["ignore", "inherit", "inherit"] });
|
|
887
|
+
const proc = Bun.spawn(startArgv, { stdio: ["ignore", "inherit", "inherit"], env: liveEnv() });
|
|
915
888
|
const code = await proc.exited;
|
|
916
889
|
if (code === 0) {
|
|
917
890
|
const onBoot = await ensureBootAutostart(mgr);
|
|
@@ -962,6 +935,7 @@ async function cmdServeDaemon(sub: string, args: string[]): Promise<number> {
|
|
|
962
935
|
if (sub === "uninstall") {
|
|
963
936
|
const proc = Bun.spawn([mgr.bin, "delete", DAEMON_NAME], {
|
|
964
937
|
stdio: ["ignore", "inherit", "inherit"],
|
|
938
|
+
env: liveEnv(),
|
|
965
939
|
});
|
|
966
940
|
const code = (await proc.exited) ?? 1;
|
|
967
941
|
if (mgr.id === "pm2" && code === 0) {
|
|
@@ -977,6 +951,7 @@ async function cmdServeDaemon(sub: string, args: string[]): Promise<number> {
|
|
|
977
951
|
if (sub === "logs") {
|
|
978
952
|
const proc = Bun.spawn([mgr.bin, "logs", DAEMON_NAME, ...args], {
|
|
979
953
|
stdio: ["ignore", "inherit", "inherit"],
|
|
954
|
+
env: liveEnv(),
|
|
980
955
|
});
|
|
981
956
|
return (await proc.exited) ?? 1;
|
|
982
957
|
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import "./ts-7XqYx0eW.js";
|
|
2
|
-
import "./logger-CDIsZ-Pp.js";
|
|
3
|
-
import "./versionChecker-CUMd5SWt.js";
|
|
4
|
-
import "./pidStore-BIvsBQ8X.js";
|
|
5
|
-
import "./globalPidIndex-CoNr7tS8.js";
|
|
6
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-5N015orJ.js";
|
|
7
|
-
|
|
8
|
-
export { SUPPORTED_CLIS };
|