@rightkit/release 0.6.8 → 0.6.11

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.
Files changed (96) hide show
  1. package/README.md +30 -12
  2. package/admission-files.mjs +4 -1
  3. package/admission.mjs +2 -2
  4. package/advisory-exceptions.mjs +41 -5
  5. package/android-play-upload.mjs +16 -15
  6. package/app-check.mjs +105 -27
  7. package/assemble-app-command.mjs +151 -0
  8. package/assembled-app.mjs +445 -0
  9. package/bin/darwin/rightkit-swap +0 -0
  10. package/bin/darwin/rightkit-swap.sign.json +6 -0
  11. package/bin/rightkit-gate.mjs +1608 -345
  12. package/broker-pins.mjs +152 -0
  13. package/build-launch.mjs +88 -0
  14. package/build-ownership.mjs +1231 -0
  15. package/build-release.mjs +82 -28
  16. package/cargo-contract.mjs +3 -1
  17. package/cargo-ledger.mjs +62 -0
  18. package/cargo-target-shim.mjs +238 -0
  19. package/class-a-launch-guard.mjs +126 -0
  20. package/cli/right-release.mjs +31 -5
  21. package/clippy-equivalence.mjs +202 -0
  22. package/clippy-route.mjs +543 -0
  23. package/codesign-shim.mjs +11 -1
  24. package/committed-source.mjs +39 -5
  25. package/crate-workspaces.mjs +87 -0
  26. package/create-mac-updater.mjs +14 -2
  27. package/deterministic-scans.mjs +997 -71
  28. package/development-signing.mjs +4 -1
  29. package/development.mjs +120 -21
  30. package/direct-receipt.mjs +135 -0
  31. package/door-policy.mjs +71 -6
  32. package/door.mjs +6 -6
  33. package/freshness-census.mjs +257 -0
  34. package/gate-executor.mjs +14 -2
  35. package/gate-stages.mjs +91 -15
  36. package/heavy-command.mjs +3 -2
  37. package/hook-retirement.mjs +65 -0
  38. package/init-command.mjs +1 -1
  39. package/install-app.mjs +11 -2
  40. package/install-assembled-app.mjs +423 -0
  41. package/inventory-check.mjs +11 -8
  42. package/ios-dev.mjs +70 -3
  43. package/ios-upload.mjs +20 -15
  44. package/linux-artifacts.mjs +93 -0
  45. package/local-publish.mjs +224 -53
  46. package/local-qualify.mjs +5 -3
  47. package/local-runtime.mjs +5 -4
  48. package/mac-package-recipe.mjs +4 -1
  49. package/managed-cargo.mjs +47 -5
  50. package/managed-root-guard.mjs +34 -0
  51. package/mirror-root-artifact.mjs +18 -5
  52. package/native-helpers.mjs +355 -0
  53. package/ownership-class-rules.mjs +1 -1
  54. package/ownership-evidence.mjs +37 -13
  55. package/ownership-hooks.mjs +20 -59
  56. package/ownership-lockfiles.mjs +14 -1
  57. package/ownership-policy-bundle.mjs +3 -2
  58. package/ownership-staged.mjs +2 -1
  59. package/ownership.mjs +24 -0
  60. package/package-recipe-targets.mjs +84 -10
  61. package/package.json +4 -3
  62. package/pipeline-admission.mjs +4 -4
  63. package/pipeline-receipts.mjs +302 -302
  64. package/preflight.mjs +27 -4
  65. package/project-type.mjs +2 -2
  66. package/provenance-verify.mjs +115 -0
  67. package/publish-cargo.mjs +12 -9
  68. package/qa-admission-policy.json +10 -1
  69. package/recipe-build.mjs +106 -14
  70. package/release-artifact-path.mjs +341 -0
  71. package/release-config.mjs +1 -1
  72. package/release-invocation.mjs +8 -1
  73. package/release-qa-command.mjs +25 -9
  74. package/release-source-brand.mjs +17 -0
  75. package/release-source-identity.mjs +171 -0
  76. package/release.mjs +59 -22
  77. package/rightkit-versions.json +73 -47
  78. package/runtime-budgets.mjs +6 -2
  79. package/scan-digest.mjs +71 -5
  80. package/sign-updater.mjs +6 -3
  81. package/snapshot-broker.mjs +172 -0
  82. package/snapshot-check.mjs +312 -0
  83. package/snapshot-git.mjs +166 -0
  84. package/snapshot-request.mjs +363 -0
  85. package/snapshot-root-config.mjs +76 -0
  86. package/snapshot.mjs +969 -0
  87. package/source-split.mjs +73 -0
  88. package/stage-commands.mjs +60 -0
  89. package/stage-ledger.mjs +204 -0
  90. package/stage-sidecar-command.mjs +5 -1
  91. package/stage-sidecar.mjs +157 -27
  92. package/swap-helper-bundle.mjs +5 -2
  93. package/test-command.mjs +17 -4
  94. package/unit-gate.mjs +301 -0
  95. package/web-build.mjs +40 -6
  96. package/win-package-recipe.mjs +20 -7
@@ -0,0 +1,152 @@
1
+ // The gate's own record of the broker it trusts on this host (Sage decision A, item 7 row 7.21; AR 256-258: pins come from the
2
+ // consumer's own enrollment, "never from the receipt, request or project").
3
+ //
4
+ // record `<managed data root>/pins/broker-<platform>.json`, `{schema, platform, host, keyId, publicKey, brokerDigest, enrolledAt}`,
5
+ // outside every repository and snapshot, written only by `enrollBrokerPins` (an explicit operation; a second enrollment
6
+ // for the same platform is a rotation and says so).
7
+ // fallback RightKit's own checkout only: `ci/rk-pipeline.json` `attestation.<platform>`, read from the revision of the RUNNING gate
8
+ // (`git show <gate repository HEAD>:ci/rk-pipeline.json`), never from the selected commit, the snapshot or the working tree.
9
+ //
10
+ // Never read here: the broker's control root, its runtime manifest, `attestation/key.json` (it holds the SECRET key), an environment
11
+ // variable, a flag, the snapshot or a receipt. A missing record in any repository but RightKit refuses the snapshot compile; nothing
12
+ // is enrolled automatically.
13
+ import crypto from "node:crypto";
14
+ import fs from "node:fs";
15
+ import path from "node:path";
16
+ import { spawnSync } from "./background-process.mjs";
17
+ import { fileURLToPath } from "node:url";
18
+
19
+ export const PINS_SCHEMA = "rightkit.broker-pins/1";
20
+ export const PINS_DIR = "pins";
21
+ export const PIPELINE_FILE = "ci/rk-pipeline.json";
22
+ export const PIPELINE_SCHEMA = "rk-pipeline.v1";
23
+
24
+ export class BrokerPinsError extends Error {
25
+ constructor(code, message) { super(message); this.name = "BrokerPinsError"; this.code = code; }
26
+ }
27
+ const fail = (code, message) => { throw new BrokerPinsError(code, message); };
28
+ const sha256 = (data) => crypto.createHash("sha256").update(data).digest("hex");
29
+ const HEX = /^(?:[0-9a-f]{2})+$/;
30
+
31
+ /** The pin record of one platform under the managed data root. */
32
+ export const pinsFile = ({ dataRoot, platform = process.platform }) => {
33
+ if (!dataRoot || !path.isAbsolute(dataRoot)) fail("RK_BROKER_PINS_ROOT", "the managed data root must be an absolute path");
34
+ if (!["darwin", "win32"].includes(platform)) fail("RK_BROKER_PINS_PLATFORM", `${platform} is not a broker host (darwin and win32 only)`);
35
+ return path.join(dataRoot, PINS_DIR, `broker-${platform}.json`);
36
+ };
37
+
38
+ /** `{host, keyId, publicKey, brokerDigest}` or a throw naming what is wrong. The key must be an uncompressed P-256 point whose sha256 is the key id. */
39
+ export function validatePins(pins, label = "the broker pins") {
40
+ const bad = (why) => fail("RK_BROKER_PINS_INVALID", `${label}: ${why}`);
41
+ if (pins === null || typeof pins !== "object" || Array.isArray(pins)) bad("not an object");
42
+ const { host, keyId, publicKey, brokerDigest } = pins;
43
+ if (typeof host !== "string" || !host.trim()) bad("host is missing");
44
+ if (typeof publicKey !== "string" || !HEX.test(publicKey) || publicKey.length !== 130 || !publicKey.startsWith("04")) bad("publicKey is not an uncompressed SEC1 P-256 point in lowercase hex");
45
+ if (typeof keyId !== "string" || keyId !== sha256(Buffer.from(publicKey, "hex"))) bad("keyId is not the sha256 of the public key");
46
+ if (typeof brokerDigest !== "string" || !/^[0-9a-f]{64}$/.test(brokerDigest)) bad("brokerDigest is not a sha256 in lowercase hex");
47
+ return { host, keyId, publicKey, brokerDigest };
48
+ }
49
+
50
+ const isLink = (meta) => meta.isSymbolicLink();
51
+
52
+ /** The enrolled record, or null when none exists. A link, a non-file, another platform's record or a malformed one is a refusal, never null. */
53
+ export function readEnrolledPins({ dataRoot, platform = process.platform }) {
54
+ const file = pinsFile({ dataRoot, platform });
55
+ let meta;
56
+ try { meta = fs.lstatSync(file); } catch (e) { if (e.code === "ENOENT") return null; throw e; }
57
+ if (isLink(meta) || !meta.isFile()) fail("RK_BROKER_PINS_INVALID", `${file} is not a regular file`);
58
+ const dir = fs.lstatSync(path.dirname(file));
59
+ if (isLink(dir) || !dir.isDirectory()) fail("RK_BROKER_PINS_INVALID", `${path.dirname(file)} is not a real directory`);
60
+ if (process.platform !== "win32" && (meta.mode & 0o022 || dir.mode & 0o022)) fail("RK_BROKER_PINS_INVALID", `${file} or its directory is writable by group or others`);
61
+ let record;
62
+ try { record = JSON.parse(fs.readFileSync(file, "utf8")); } catch (e) { fail("RK_BROKER_PINS_INVALID", `${file} is not JSON (${e.message})`); }
63
+ if (record?.schema !== PINS_SCHEMA) fail("RK_BROKER_PINS_INVALID", `${file} is not a ${PINS_SCHEMA} record`);
64
+ if (record.platform !== platform) fail("RK_BROKER_PINS_INVALID", `${file} is the record of ${record.platform}, not ${platform}`);
65
+ return { ...validatePins(record, file), source: "enrolled", file };
66
+ }
67
+
68
+ const git = (spawn, cwd, args) => {
69
+ const r = spawn("git", args, { cwd, encoding: "utf8" });
70
+ if (r.error || r.status !== 0) return null;
71
+ return String(r.stdout ?? "").trim();
72
+ };
73
+
74
+ /**
75
+ * RightKit's own pins, from the revision of the running gate: the repository that holds `gateFile` and its HEAD commit, only when
76
+ * that repository IS the checkout being compiled (RightKit itself). Null when the gate is not run from a RightKit checkout.
77
+ */
78
+ export function readGateRevisionPins({ gateFile, checkout, platform = process.platform, spawn = spawnSync, realpath = (p) => fs.realpathSync.native(p) }) {
79
+ const here = path.dirname(gateFile);
80
+ const top = git(spawn, here, ["rev-parse", "--show-toplevel"]);
81
+ if (!top) return null;
82
+ let same = false;
83
+ try { same = realpath(top) === realpath(checkout); } catch { same = false; }
84
+ if (!same) return null;
85
+ const revision = git(spawn, here, ["rev-parse", "--verify", "HEAD^{commit}"]);
86
+ if (!revision || !/^[0-9a-f]{40,64}$/.test(revision)) fail("RK_BROKER_PINS_INVALID", "the running gate's revision cannot be read");
87
+ const blob = spawn("git", ["show", `${revision}:${PIPELINE_FILE}`], { cwd: here, encoding: "utf8", maxBuffer: 16 * 1024 * 1024 });
88
+ if (blob.error || blob.status !== 0) return null;
89
+ let pipeline;
90
+ try { pipeline = JSON.parse(String(blob.stdout)); } catch (e) { fail("RK_BROKER_PINS_INVALID", `${PIPELINE_FILE} at the gate's revision ${revision.slice(0, 12)} is not JSON (${e.message})`); }
91
+ if (pipeline?.schema !== PIPELINE_SCHEMA) fail("RK_BROKER_PINS_INVALID", `${PIPELINE_FILE} at the gate's revision is not ${PIPELINE_SCHEMA}`);
92
+ const entry = pipeline.attestation?.[platform];
93
+ if (!entry) return null;
94
+ return { ...validatePins(entry, `${PIPELINE_FILE} (${platform}) at the gate's revision ${revision.slice(0, 12)}`), source: "rk-pipeline", revision };
95
+ }
96
+
97
+ /**
98
+ * The pins this gate verifies broker receipts against: the enrolled record, else (RightKit only) the running gate's own revision.
99
+ * Throws RK_BROKER_PINS_MISSING when neither exists: an app repository without an enrollment compiles nothing through a broker.
100
+ */
101
+ export function loadBrokerPins({ dataRoot, platform = process.platform, gateFile, checkout, spawn, realpath }) {
102
+ const enrolled = readEnrolledPins({ dataRoot, platform });
103
+ if (enrolled) return enrolled;
104
+ const fallback = gateFile && checkout ? readGateRevisionPins({ gateFile, checkout, platform, spawn, realpath }) : null;
105
+ if (fallback) return fallback;
106
+ return fail("RK_BROKER_PINS_MISSING", `no broker enrollment for ${platform} (${pinsFile({ dataRoot, platform })}), and this is not the RightKit checkout whose gate revision carries ${PIPELINE_FILE} pins; enroll the host's broker explicitly, the gate never enrolls one itself`);
107
+ }
108
+
109
+ /**
110
+ * The explicit enroll operation. Values come from the caller (the output of `rustright-broker --generate-attestation-key` and the
111
+ * installed runtime's `brokerDigest`); nothing is read from the broker, its key file or the environment. An existing record is
112
+ * replaced only with `rotate: true`.
113
+ */
114
+ export function enrollBrokerPins({ dataRoot, platform = process.platform, host, keyId, publicKey, brokerDigest, rotate = false, now = () => new Date() }) {
115
+ const pins = validatePins({ host, keyId, publicKey, brokerDigest }, "the enrollment");
116
+ const file = pinsFile({ dataRoot, platform });
117
+ const dir = path.dirname(file);
118
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
119
+ const meta = fs.lstatSync(dir);
120
+ if (isLink(meta) || !meta.isDirectory()) fail("RK_BROKER_PINS_INVALID", `${dir} is not a real directory`);
121
+ let existing = null;
122
+ try { existing = readEnrolledPins({ dataRoot, platform }); } catch (e) { if (!rotate) throw e; }
123
+ if (existing && !rotate) fail("RK_BROKER_PINS_EXISTS", `${file} already enrolls key ${existing.keyId.slice(0, 12)}; replacing it is a rotation (pass rotate: true)`);
124
+ const record = { schema: PINS_SCHEMA, platform, ...pins, enrolledAt: now().toISOString(), ...(existing ? { rotatedFrom: { keyId: existing.keyId, brokerDigest: existing.brokerDigest } } : {}) };
125
+ const tmp = `${file}.${crypto.randomUUID()}.tmp`;
126
+ fs.writeFileSync(tmp, `${JSON.stringify(record, null, 2)}\n`, { flag: "wx", mode: 0o600 });
127
+ try { fs.renameSync(tmp, file); } finally { fs.rmSync(tmp, { force: true }); }
128
+ return { file, record, rotated: Boolean(existing) };
129
+ }
130
+
131
+ /** `node broker-pins.mjs enroll --platform <darwin|win32> --data-root <dir> --host H --key-id K --public-key P --broker-digest D [--rotate]` or `show`. */
132
+ export function main(argv, { out = (m) => process.stdout.write(`${m}\n`), err = (m) => process.stderr.write(`${m}\n`) } = {}) {
133
+ const [command, ...rest] = argv;
134
+ const opt = (name) => { const i = rest.indexOf(name); return i < 0 ? undefined : rest[i + 1]; };
135
+ try {
136
+ const dataRoot = opt("--data-root"); const platform = opt("--platform") ?? process.platform;
137
+ if (command === "enroll") {
138
+ const r = enrollBrokerPins({ dataRoot, platform, host: opt("--host"), keyId: opt("--key-id"), publicKey: opt("--public-key"), brokerDigest: opt("--broker-digest"), rotate: rest.includes("--rotate") });
139
+ out(`enrolled ${r.record.platform} ${r.record.host} key ${r.record.keyId} in ${r.file}${r.rotated ? " (rotation)" : ""}`);
140
+ return 0;
141
+ }
142
+ if (command === "show") {
143
+ const pins = readEnrolledPins({ dataRoot, platform });
144
+ out(pins ? JSON.stringify(pins) : "no enrollment");
145
+ return pins ? 0 : 1;
146
+ }
147
+ err("usage: broker-pins.mjs enroll|show --platform <darwin|win32> --data-root <dir> [--host H --key-id K --public-key P --broker-digest D [--rotate]]");
148
+ return 2;
149
+ } catch (e) { err(`${e.code ?? "error"}: ${e.message}`); return 1; }
150
+ }
151
+
152
+ if (process.argv[1] && (() => { try { return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return false; } })()) process.exit(main(process.argv.slice(2)));
@@ -0,0 +1,88 @@
1
+ // The one door for the build and test commands RightKit launches for an app or a unit (rightkit#1241): on a broker-managed host
2
+ // every such command is a broker job, so class A admission, the one-build-per-app claim, the in-place freeze and the host's
3
+ // resource limits apply to it as they do to a cargo build; on a host without a broker it runs directly, as before.
4
+ //
5
+ // - pnpm <args> -> <rustright> run rightkit-pnpm -- <args> (the broker's pnpm adapter; compile-flagged on the broker)
6
+ // - node <script> <args> -> <rustright> run rightkit-node -- <script> <args> (a script inside the job's workspace, relative path)
7
+ // - cargo <args> -> <rustright> cargo <args> (by the resolved broker client, whatever path the caller named: a real
8
+ // cargo binary is never launched around the broker)
9
+ // - rustright <args> -> <rustright> <args> (the resolved broker client)
10
+ // - rightkit-qa <args> -> <rustright> run rightkit-qa -- <args> (the whole QA run, its test binaries included, is one broker
11
+ // job holding the app's claim; its own `rustright cargo test --no-run` runs nested under that job)
12
+ // - this package's right-release -> unchanged (same NODE_OPTIONS as this process): it orchestrates, and every build it starts comes back
13
+ // through this door
14
+ // - anything else -> refused on a managed host: no broker adapter runs it yet, and running it directly would bypass the
15
+ // broker (fail closed; the per-app impact is mapped in rightkit#1241)
16
+ // A launch from inside a broker job is routed the same way: the broker attributes it to its parent job by the parent's own
17
+ // capability token (never by an environment marker this module reads), so no launch here decides to skip the broker.
18
+ import { readFileSync } from "node:fs";
19
+ import path from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+ import { resolveExecutor } from "./gate-executor.mjs";
22
+ import { resolveExecutable } from "./admission-host.mjs";
23
+
24
+ export const BUILD_LAUNCH_REFUSED = "RK_BUILD_LAUNCH_REFUSED";
25
+ const OWN_RIGHT_RELEASE = fileURLToPath(new URL("./cli/right-release.mjs", import.meta.url));
26
+ // Node flags that change no code path; everything else before the script (-e, --eval, -p, --import, -r, --require, --loader, …) refuses.
27
+ const NODE_FLAGS = /^--(?:disable-warning=[A-Za-z0-9_-]+|no-experimental-webstorage|no-warnings|enable-source-maps|max-old-space-size=\d+|stack-size=\d+)$/;
28
+ const name = (program) => path.basename(String(program ?? "").replaceAll("\\", "/")).replace(/\.(exe|cmd|bat)$/i, "").toLowerCase();
29
+
30
+ function refuse(message) {
31
+ const error = new Error(message);
32
+ error.code = BUILD_LAUNCH_REFUSED;
33
+ throw error;
34
+ }
35
+
36
+ /**
37
+ * `{ command, args }` to launch for a build or test command, or a refusal. `host` carries env, platform, exists, which and readFile
38
+ * (resolveExecutor's inputs); `which` defaults to a PATH lookup over `env`.
39
+ */
40
+ export function buildLaunch(command, args = [], host = {}) {
41
+ const env = host.env ?? process.env;
42
+ const platform = host.platform ?? process.platform;
43
+ const exists = host.exists;
44
+ const lookup = (cmd) => { const found = resolveExecutable(cmd, { env, hostPlatform: platform, exists: exists ?? (() => false) }); return found ? [found] : []; };
45
+ const executor = resolveExecutor({ env, platform, exists, which: host.which ?? lookup, readFile: host.readFile ?? ((file) => readFileSync(file, "utf8")) });
46
+ if (executor.kind === "refuse") refuse(`${name(command)} ${args.join(" ")}: ${executor.problem}`);
47
+ // A direct (Linux) host: the real toolchain, RustRight shim directories off PATH (`env`, to merge into the launch env); a configured
48
+ // `rustright cargo|rustc|rustdoc …` runs that tool directly; any other `rustright` command has no direct form and is refused.
49
+ if (executor.kind === "direct") {
50
+ const extra = executor.env ? { env: executor.env } : {};
51
+ if (name(command) === "rustright") {
52
+ if (!["cargo", "rustc", "rustdoc"].includes(String(args[0]))) refuse(`rustright ${args.join(" ")}: no broker on this ${platform} host, and only rustright cargo|rustc|rustdoc map to the toolchain`);
53
+ return { command: String(args[0]), args: args.slice(1).map(String), ...extra };
54
+ }
55
+ return { command, args, ...extra };
56
+ }
57
+ const program = name(command);
58
+ const client = () => lookup("rustright")[0] ?? refuse(`rustright is not on PATH: on a broker-managed host ${program} runs only as a broker job`);
59
+ if (program === "cargo") return { command: client(), args: ["cargo", ...args.map(String)] };
60
+ if (program === "rustright") return { command: client(), args: args.map(String) };
61
+ if (program === "rightkit-qa") return { command: client(), args: ["run", "rightkit-qa", "--", ...args.map(String)] };
62
+ if (program === "swift") return { command: client(), args: ["run", "rightkit-swift", "--", ...args.map(String)] };
63
+ // This package's own right-release, run by this very Node binary (not any executable named node), with no startup code added by
64
+ // the caller: NODE_OPTIONS is empty or exactly this process's own string (a hook's env could otherwise --import code that builds
65
+ // outside the broker; reordered tokens can re-pair an option with another value, so nothing short of equality is accepted).
66
+ if (command === process.execPath && path.resolve(String(args[0] ?? "")) === OWN_RIGHT_RELEASE) {
67
+ const options = String(env.NODE_OPTIONS ?? "");
68
+ if (options.trim() !== "" && options !== String(process.env.NODE_OPTIONS ?? "")) refuse(`right-release ${args.slice(1).join(" ")}: NODE_OPTIONS is neither empty nor exactly this process's own; on a broker-managed host the door's own right-release runs with no added startup code (rightkit#1241)`);
69
+ return { command, args };
70
+ }
71
+ if (program === "node") {
72
+ // Leading node flags are limited to ones that change no code path (warnings, web storage, heap/stack size); an eval, import,
73
+ // require or loader flag would run code that is not the script, so it is refused. The adapter takes the script as a path
74
+ // relative to the job's workspace (its cwd); an absolute or escaping path is refused here and by the broker's relpath check.
75
+ const at = args.findIndex((a) => !String(a).startsWith("-"));
76
+ const flags = (at < 0 ? args : args.slice(0, at)).map(String);
77
+ const bad = flags.find((f) => !NODE_FLAGS.test(f));
78
+ if (at < 0 || bad !== undefined) refuse(`node ${args.join(" ")}: ${bad !== undefined ? `node flag ${bad} is not allowed` : "no script"}; on a broker-managed host node runs only a workspace script as a broker job (rightkit-node, rightkit#1241)`);
79
+ const cwd = host.cwd ?? process.cwd();
80
+ const script = path.isAbsolute(String(args[at])) ? path.relative(cwd, String(args[at])) : String(args[at]);
81
+ if (!script || script.startsWith("..") || path.isAbsolute(script)) refuse(`node ${args[0]}: the script is outside the job's workspace (${cwd}); on a broker-managed host node runs only workspace scripts as broker jobs (rightkit-node, rightkit#1241)`);
82
+ return { command: client(), args: ["run", "rightkit-node", "--", ...flags, script.replaceAll("\\", "/"), ...args.slice(at + 1).map(String)] };
83
+ }
84
+ if (program === "pnpm") {
85
+ return { command: client(), args: ["run", "rightkit-pnpm", "--", ...args.map(String)] };
86
+ }
87
+ return refuse(`${program} ${args.join(" ")}: no broker adapter runs this command yet, and on a broker-managed host a build or test command never runs around the broker (rightkit#1241)`);
88
+ }