@rightkit/release 0.6.7 → 0.6.10

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 (53) hide show
  1. package/README.md +35 -10
  2. package/admission-files.mjs +1 -1
  3. package/app-check.mjs +94 -20
  4. package/assemble-app-command.mjs +151 -0
  5. package/assembled-app.mjs +445 -0
  6. package/bin/darwin/rightkit-swap +0 -0
  7. package/bin/darwin/rightkit-swap.sign.json +6 -0
  8. package/bin/rightkit-gate.mjs +957 -83
  9. package/build-launch.mjs +88 -0
  10. package/build-ownership.mjs +1231 -0
  11. package/build-release.mjs +2 -1
  12. package/cargo-contract.mjs +3 -1
  13. package/cargo-target-shim.mjs +166 -0
  14. package/class-a-launch-guard.mjs +126 -0
  15. package/cli/right-release.mjs +3 -1
  16. package/codesign-shim.mjs +11 -1
  17. package/deterministic-scans.mjs +807 -56
  18. package/development-signing.mjs +4 -1
  19. package/development.mjs +110 -20
  20. package/door-policy.mjs +61 -6
  21. package/door.mjs +23 -0
  22. package/gate-executor.mjs +14 -2
  23. package/gate-stages.mjs +15 -5
  24. package/heavy-command.mjs +3 -2
  25. package/install-app.mjs +11 -2
  26. package/install-assembled-app.mjs +423 -0
  27. package/ios-dev.mjs +70 -3
  28. package/linux-artifacts.mjs +93 -0
  29. package/local-publish.mjs +161 -26
  30. package/managed-cargo.mjs +47 -5
  31. package/managed-root-guard.mjs +34 -0
  32. package/native-helpers.mjs +355 -0
  33. package/package-recipe-targets.mjs +52 -7
  34. package/package.json +5 -3
  35. package/pipeline-receipts.mjs +302 -302
  36. package/project-type.mjs +2 -2
  37. package/provenance-verify.mjs +115 -0
  38. package/publish-cargo.mjs +8 -6
  39. package/qa-admission-policy.json +10 -1
  40. package/recipe-build.mjs +73 -5
  41. package/release-config.mjs +1 -1
  42. package/release.mjs +15 -5
  43. package/rightkit-versions.json +13 -7
  44. package/snapshot-check.mjs +306 -0
  45. package/snapshot-git.mjs +166 -0
  46. package/snapshot-request.mjs +161 -0
  47. package/snapshot-root-config.mjs +76 -0
  48. package/snapshot.mjs +856 -0
  49. package/stage-commands.mjs +60 -0
  50. package/stage-sidecar.mjs +57 -7
  51. package/test-command.mjs +17 -4
  52. package/unit-gate.mjs +259 -0
  53. package/web-build.mjs +38 -6
package/build-release.mjs CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  import path from "node:path";
21
21
  import { preflightOwnershipScope } from "./ownership-scope.mjs";
22
22
  import { spawn, spawnSync } from "./background-process.mjs";
23
+ import { spawnUnderOwnership } from "./build-ownership.mjs";
23
24
  import { RightKitRegistryError, SNAPSHOT_ENV, SNAPSHOT_FILE, assertManifestRegistryEvidence, loadAdmittedSnapshot, prepareRightKitResolution, registryManifestFields, rightKitLockInventory, writeAdmissionRecord, writeSnapshot } from "./rightkit-registry-resolver.mjs";
24
25
  import { formatAdmission, formatNotRun, runAdmission } from "./admission.mjs";
25
26
  import { sccacheRefusal } from "./admission-host.mjs";
@@ -875,7 +876,7 @@ async function runProgress(cmd, runArgs, cwd, env, watchDir, diagnosticPath = nu
875
876
  const settleResolve = () => { if (!settled) { settled = true; resolve(); } };
876
877
  const settleReject = (error) => { if (!settled) { settled = true; reject(error); } };
877
878
  const useShell = process.platform === "win32" && !/\.(?:exe|com)$/i.test(cmd);
878
- child = spawn(cmd, runArgs, { cwd, env, stdio: ["inherit", "pipe", "pipe"], windowsHide: true, shell: useShell, detached: process.platform !== "win32" });
879
+ child = spawnUnderOwnership(spawn, cmd, runArgs, { cwd, env, stdio: ["inherit", "pipe", "pipe"], windowsHide: true, shell: useShell, detached: process.platform !== "win32" });
879
880
  try {
880
881
  if (child.pid && heavySlot) heavySlot.trackChild(child.pid, cmd);
881
882
  } catch (error) {
@@ -2,7 +2,7 @@ import { existsSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import path from "node:path";
4
4
  import { spawnSync } from "./background-process.mjs";
5
- import { managedRuntimeClient, runCargoSync } from "./managed-cargo.mjs";
5
+ import { directPlatform, managedRuntimeClient, runCargoSync } from "./managed-cargo.mjs";
6
6
 
7
7
  const SKIP_DIRECTORIES = new Set([".audit", ".cache", ".git", ".claude", ".right-release", ".worktrees", "node_modules", "target", "vendor"]);
8
8
  const CRATES_IO_SOURCES = new Set([
@@ -12,6 +12,7 @@ const CRATES_IO_SOURCES = new Set([
12
12
  const CACHE_V2_ENVIRONMENT = ["CARGO_HOME", "CARGO_TARGET_DIR", "RUSTC_WRAPPER", "SCCACHE_DIR", "SCCACHE_BASEDIRS", "RIGHT_RELEASE_CACHE_OWNER"];
13
13
 
14
14
  export function managedCargoShimOnPath(env = process.env, platform = process.platform, exists = existsSync) {
15
+ if (directPlatform(platform)) return false; // Linux never uses a RustRight shim (managed-cargo directPlatform)
15
16
  const pathApi = platform === "win32" ? path.win32 : path.posix;
16
17
  const delimiter = platform === "win32" ? ";" : ":";
17
18
  const extensions = platform === "win32"
@@ -29,6 +30,7 @@ export function managedCargoShimOnPath(env = process.env, platform = process.pla
29
30
  }
30
31
 
31
32
  export function brokerManagedHost(env = process.env, platform = process.platform, exists = existsSync) {
33
+ if (directPlatform(platform)) return false; // Linux is never broker-managed, whatever socket or control root the env names
32
34
  return Boolean(managedRuntimeClient(env, platform, exists)) || Boolean(env.RIGHTKIT_BUILD_BROKER_SOCKET) || managedCargoShimOnPath(env, platform, exists);
33
35
  }
34
36
 
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+ // The runtime cargo wrapper of a commit-snapshot compile (item 7, GATE-CONTRACT §1 "Broker interface", output directory). Scope: the
3
+ // DIRECT POSIX (Linux) host only. On Mac and Dell the positive snapshot-compile path is the RustRight broker's snapshot carrier alone
4
+ // (the gate refuses a snapshot compile on a broker host until it is wired); on a direct Windows host the static walk
5
+ // (deterministic-scans.mjs cargoReachOf) is the fail-closed baseline, because a child started without a shell finds only an .exe.
6
+ //
7
+ // The gate puts this `cargo` first on PATH and names it in CARGO for every scan. Real cargo honours CARGO as its own path and hands
8
+ // it to build scripts and tests (env!("CARGO"), $CARGO), so those come back here too. Each invocation resolves its Cargo workspace
9
+ // (`cargo locate-project --workspace`, from its working directory or --manifest-path) and runs with that workspace's ORIGINAL checkout
10
+ // target directory as CARGO_TARGET_DIR. Refused with an RK_* code (the unit fails): a workspace the compile did not resolve (outside
11
+ // the snapshot, a fixture, a temporary project), an option that moves the workspace, configuration or output, a command that writes
12
+ // no bound output or rewrites the commit (clean, install, publish, update, an alias or plugin), a command that reads a workspace
13
+ // outside the bound ones or may write its Cargo.lock (tree, fetch, pkgid... without --locked/--frozen; fmt only as --check). A cargo
14
+ // that bypasses PATH and CARGO (an absolute path, `rustup run`, a reset PATH) inherits the scan-wide CARGO_TARGET_DIR: the one
15
+ // workspace's checkout target, or with several workspaces a per-scan sentinel directory that must stay empty (runScans fails the
16
+ // scan with RK_CARGO_UNBOUND otherwise). An absolute cargo that also passes its own --target-dir is deliberate evasion of the gate,
17
+ // out of scope under the standing rule (the gate binds what a test does, not what a test sets out to hide).
18
+ import fs from "node:fs";
19
+ import path from "node:path";
20
+ import { createHash } from "node:crypto";
21
+ import { spawnSync } from "node:child_process";
22
+ import { fileURLToPath } from "node:url";
23
+
24
+ export const SHIM_MAP_SCHEMA = "rightkit.cargo-target-shim/2";
25
+ // Exit status of a refusal: cargo's own error status, so every caller reads it as a failed cargo.
26
+ export const SHIM_REFUSAL_STATUS = 101;
27
+ // Cargo commands that compile or report on a workspace: run bound to its checkout target directory.
28
+ const COMPILE = new Set(["build", "b", "check", "c", "test", "t", "bench", "run", "r", "doc", "d", "rustc", "rustdoc", "clippy", "metadata", "package"]);
29
+ // Commands that only read a workspace but may resolve and write its Cargo.lock: bound, and only with --locked or --frozen.
30
+ const LOCKED = new Set(["fetch", "tree", "pkgid", "verify-project", "read-manifest", "locate-project", "deny"]);
31
+ // Commands with no lock flag (cargo fmt, cargo-audit): bound, and only where the workspace's committed Cargo.lock already exists.
32
+ const LOCKFILE = new Set(["fmt", "audit"]);
33
+ // Plugins whose options are their own, not cargo's (cargo-deny's --config is its config file, not a cargo redirect).
34
+ const PLUGIN = new Set(["deny", "audit"]);
35
+ const BOUND = new Set([...COMPILE, ...LOCKED, ...LOCKFILE]);
36
+ // Commands that read no workspace at all: run unchanged.
37
+ const PASSTHROUGH = new Set(["version", "help"]);
38
+ // Global options that take a value (`--color always`, `--explain E0001`); the subcommand is the first word after them.
39
+ const GLOBAL_VALUE = new Set(["--color", "--explain"]);
40
+ // Options that move the working directory, the configuration or the output (a short cluster holding C or Z too).
41
+ const REDIRECT = /^(--config|-Z|-C)(=|$)|^-[A-Za-z]*[CZ]/;
42
+
43
+ const refusal = (code, message) => ({ refuse: { code, message } });
44
+
45
+ /**
46
+ * What one cargo invocation may do: `{ exec: { args, env, workspace, targetDir } }` (bound), `{ exec: { args, env } }` (a command
47
+ * that writes nothing), or `{ refuse: { code, message } }`. `locate(manifestPath | null, cwd, toolchain | null)` returns the absolute
48
+ * path of the workspace root's Cargo.toml or throws; `realpath` resolves a directory. `map.shim` (the wrapper's own path) is set as
49
+ * CARGO for the real cargo, so its build scripts and tests reach the wrapper again.
50
+ */
51
+ export function decideCargoInvocation({ args, cwd, env, map, locate, realpath = (p) => fs.realpathSync.native(p), exists = fs.existsSync }) {
52
+ const words = [...args];
53
+ const toolchain = words[0]?.startsWith("+") ? words[0] : null;
54
+ const withCargo = (e) => (map.shim ? { ...e, CARGO: map.shim } : e);
55
+ let i = toolchain ? 1 : 0;
56
+ let sub = null;
57
+ for (; i < words.length; i += 1) {
58
+ const w = words[i];
59
+ if (w === "--") break;
60
+ if (REDIRECT.test(w)) return refusal("RK_CARGO_REDIRECT", `\`cargo ${words.join(" ")}\` passes ${w}, which moves its working directory, configuration or output; a snapshot compile's outputs must be the checkout's`);
61
+ if (GLOBAL_VALUE.has(w)) { i += 1; continue; }
62
+ if (w.startsWith("-")) continue;
63
+ sub = w;
64
+ break;
65
+ }
66
+ const end = sub === null ? -1 : words.indexOf("--", i + 1);
67
+ const own = sub === null ? [] : words.slice(i + 1, end < 0 ? undefined : end);
68
+ const globals = words.slice(toolchain ? 1 : 0, sub === null ? undefined : i);
69
+ if (sub === "fmt" && !own.includes("--check")) return refusal("RK_CARGO_UNBOUND_COMMAND", "`cargo fmt` without --check rewrites tracked files; a snapshot compile changes no file of the commit");
70
+ if (sub === null || PASSTHROUGH.has(sub)) return { exec: { args: words, env: withCargo({ ...env }) } };
71
+ if (!BOUND.has(sub)) return refusal("RK_CARGO_UNBOUND_COMMAND", `\`cargo ${sub}\` is not a command a snapshot compile binds (${[...BOUND].join(", ")}; or ${[...PASSTHROUGH].join(", ")}, which read no workspace): an alias, plugin, clean, install, publish or lock rewrite is refused`);
72
+ if (LOCKED.has(sub) && ![...globals, ...own].some((w) => w === "--locked" || w === "--frozen")) return refusal("RK_CARGO_UNBOUND_COMMAND", `\`cargo ${sub}\` without --locked or --frozen may write a Cargo.lock; a snapshot compile changes no file of the commit and writes nothing outside its bound outputs`);
73
+ if (!PLUGIN.has(sub)) for (const w of own) if (REDIRECT.test(w)) return refusal("RK_CARGO_REDIRECT", `\`cargo ${sub}\` passes ${w}, which moves its working directory, configuration or output; a snapshot compile's outputs must be the checkout's`);
74
+ const valueOf = (name) => {
75
+ const at = own.findIndex((w) => w === name || w.startsWith(`${name}=`));
76
+ if (at < 0) return undefined;
77
+ return own[at].includes("=") ? own[at].slice(name.length + 1) : (own[at + 1] ?? "");
78
+ };
79
+ const manifestPath = valueOf("--manifest-path");
80
+ let workspace;
81
+ try {
82
+ const manifest = locate(manifestPath === undefined ? null : path.resolve(cwd, manifestPath), cwd, toolchain);
83
+ if (typeof manifest !== "string" || !path.isAbsolute(manifest)) throw new Error(`locate-project reported ${JSON.stringify(manifest)}`);
84
+ workspace = realpath(path.dirname(manifest));
85
+ } catch (error) {
86
+ return refusal("RK_CARGO_UNBOUND_WORKSPACE", `\`cargo ${sub}\` from ${cwd}${manifestPath === undefined ? "" : ` (--manifest-path ${manifestPath})`}: its Cargo workspace cannot be resolved (${error.message}); a snapshot compile's outputs must be the checkout's`);
87
+ }
88
+ const targetDir = Object.hasOwn(map.workspaces ?? {}, workspace) ? map.workspaces[workspace] : null;
89
+ if (!targetDir) return refusal("RK_CARGO_UNBOUND_WORKSPACE", `\`cargo ${sub}\` builds the Cargo workspace ${workspace}, which is not one this snapshot compile resolved (${Object.keys(map.workspaces ?? {}).join(", ") || "none"}); its outputs would not be the checkout's`);
90
+ if (LOCKFILE.has(sub) && !exists(path.join(workspace, "Cargo.lock"))) return refusal("RK_CARGO_UNBOUND_COMMAND", `\`cargo ${sub}\` has no --locked, and workspace ${workspace} has no committed Cargo.lock it would only read; it may write one`);
91
+ const explicit = valueOf("--target-dir");
92
+ if (explicit !== undefined && path.resolve(cwd, explicit) !== targetDir) return refusal("RK_CARGO_REDIRECT", `\`cargo ${sub}\` passes --target-dir ${explicit}; workspace ${workspace} writes only to the checkout's ${targetDir}`);
93
+ return { exec: { args: words, env: withCargo({ ...env, CARGO_TARGET_DIR: targetDir, CARGO_BUILD_TARGET_DIR: targetDir }), workspace, targetDir } };
94
+ }
95
+
96
+ /**
97
+ * Writes the wrapper for one snapshot compile under `base`: the map file and `bin/cargo`, which runs this module with node. The
98
+ * directory is named by the sha256 of the map and this module's bytes (`identity`), so the same workspaces, targets and wrapper give
99
+ * the same path (and the same CARGO in a scan's cache key) and any change gives another. `targetDirs` is the gate's { [cargoDir]:
100
+ * absolute checkout target directory }; each workspace is keyed by its real path in the snapshot. `realCargo` is the absolute cargo
101
+ * the wrapper runs (never itself). POSIX hosts only: a Windows child started without a shell finds only an .exe on PATH.
102
+ */
103
+ export function installCargoShim({ base, snapshotRoot, targetDirs, realCargo, node = process.execPath, platform = process.platform, realpath = (p) => fs.realpathSync.native(p), onDir = () => {}, write = (file, text, mode) => fs.writeFileSync(file, text, { mode }) }) {
104
+ if (platform === "win32") throw Object.assign(new Error("the runtime cargo wrapper needs a POSIX host: a Windows child finds only an .exe on PATH"), { code: "RK_CARGO_SHIM_UNSUPPORTED" });
105
+ if (typeof realCargo !== "string" || !path.isAbsolute(realCargo)) throw Object.assign(new Error(`no absolute cargo to wrap (${JSON.stringify(realCargo)})`), { code: "RK_CARGO_SHIM_UNSUPPORTED" });
106
+ const workspaces = {};
107
+ for (const [cargoDir, targetDir] of Object.entries(targetDirs ?? {})) workspaces[realpath(path.join(snapshotRoot, ...(cargoDir === "." ? [] : cargoDir.split("/"))))] = targetDir;
108
+ const self = fileURLToPath(import.meta.url);
109
+ const sorted = Object.fromEntries(Object.entries(workspaces).sort(([a], [b]) => (a < b ? -1 : 1)));
110
+ const identity = createHash("sha256").update(JSON.stringify({ realCargo, node, workspaces: sorted })).update(fs.readFileSync(self)).digest("hex");
111
+ const dir = path.join(base, `rk-cargo-shim-${identity.slice(0, 16)}`);
112
+ const bin = path.join(dir, "bin");
113
+ const shim = path.join(bin, "cargo");
114
+ const mapFile = path.join(dir, "cargo-target-map.json");
115
+ // The caller learns the directory before anything is written into it (its cleanup owns it from here), and a failed install removes
116
+ // what it wrote before it throws.
117
+ onDir(dir);
118
+ try {
119
+ fs.mkdirSync(bin, { recursive: true });
120
+ const quote = (s) => `'${String(s).replaceAll("'", "'\\''")}'`;
121
+ const put = (file, text, mode) => { const tmp = `${file}.${process.pid}.tmp`; write(tmp, text, mode); fs.renameSync(tmp, file); };
122
+ put(mapFile, `${JSON.stringify({ schema: SHIM_MAP_SCHEMA, identity, realCargo, shim, workspaces: sorted }, null, 2)}\n`, 0o644);
123
+ put(shim, `#!/bin/sh\nexec ${quote(node)} ${quote(self)} --map ${quote(mapFile)} -- "$@"\n`, 0o755);
124
+ } catch (error) {
125
+ fs.rmSync(dir, { recursive: true, force: true });
126
+ throw error;
127
+ }
128
+ return { dir, bin, shim, mapFile, workspaces: sorted, identity, sentinel: Object.keys(sorted).length > 1 ? path.join(dir, "unbound") : null };
129
+ }
130
+
131
+ /**
132
+ * The environment every scan of a wrapped snapshot compile starts from: the wrapper first on PATH and named in CARGO, and, when the
133
+ * snapshot has exactly one Cargo workspace, its checkout target as the scan-wide CARGO_TARGET_DIR, so a cargo that bypasses the
134
+ * wrapper (an absolute path, `rustup run`, a reset PATH) still writes there. With several workspaces runScans gives each scan its own
135
+ * empty sentinel as CARGO_TARGET_DIR instead (installed.sentinel; options.cargoSentinel), which must still be empty afterwards.
136
+ */
137
+ export function shimScanEnv(env, installed) {
138
+ const targets = Object.values(installed.workspaces);
139
+ const pathKey = Object.keys(env).find((k) => k.toUpperCase() === "PATH") ?? "PATH";
140
+ return { ...env, [pathKey]: `${installed.bin}${path.delimiter}${env[pathKey] ?? ""}`, CARGO: installed.shim, ...(targets.length === 1 ? { CARGO_TARGET_DIR: targets[0] } : {}) };
141
+ }
142
+
143
+ /** The wrapper itself: `cargo-target-shim.mjs --map <file> -- <cargo args>`. Returns the exit status. */
144
+ export function runShim(argv, { env = process.env, cwd = process.cwd(), spawn = spawnSync, err = (m) => process.stderr.write(`${m}\n`) } = {}) {
145
+ const sep = argv.indexOf("--");
146
+ const mapAt = argv.indexOf("--map");
147
+ if (mapAt < 0 || sep < 0 || mapAt > sep) { err("rightkit-gate: RK_CARGO_SHIM_ERROR: usage: cargo-target-shim.mjs --map <file> -- <cargo args>"); return SHIM_REFUSAL_STATUS; }
148
+ let map;
149
+ try { map = JSON.parse(fs.readFileSync(argv[mapAt + 1], "utf8")); if (map?.schema !== SHIM_MAP_SCHEMA) throw new Error(`schema ${JSON.stringify(map?.schema)}`); }
150
+ catch (error) { err(`rightkit-gate: RK_CARGO_SHIM_ERROR: the cargo target map cannot be read (${error.message}); nothing ran`); return SHIM_REFUSAL_STATUS; }
151
+ // locate-project runs the real cargo with CARGO pointing at it, never at the wrapper.
152
+ const locateEnv = { ...env, CARGO: map.realCargo };
153
+ const locate = (manifestPath, from, toolchain) => {
154
+ const r = spawn(map.realCargo, [...(toolchain ? [toolchain] : []), "locate-project", "--workspace", "--message-format", "plain", ...(manifestPath ? ["--manifest-path", manifestPath] : [])], { cwd: from, env: locateEnv, encoding: "utf8" });
155
+ if (r.error) throw r.error;
156
+ if (r.status !== 0) throw new Error(String(r.stderr || `exit ${r.status}`).trim().split(/\r?\n/).at(-1));
157
+ return String(r.stdout).trim();
158
+ };
159
+ const decision = decideCargoInvocation({ args: argv.slice(sep + 1), cwd, env, map, locate });
160
+ if (decision.refuse) { err(`rightkit-gate: ${decision.refuse.code}: ${decision.refuse.message}`); return SHIM_REFUSAL_STATUS; }
161
+ const r = spawn(map.realCargo, decision.exec.args, { cwd, env: decision.exec.env, stdio: "inherit" });
162
+ if (r.error) { err(`rightkit-gate: RK_CARGO_SHIM_ERROR: ${map.realCargo} did not start (${r.error.message})`); return SHIM_REFUSAL_STATUS; }
163
+ return r.status ?? (r.signal ? 128 : SHIM_REFUSAL_STATUS);
164
+ }
165
+
166
+ if (process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url))) process.exit(runShim(process.argv.slice(2)));
@@ -0,0 +1,126 @@
1
+ // Class A launch guard: class A runs before any compile, so a class A test row may not launch a compiler or an emitting build.
2
+ //
3
+ // The gate runs every class A test row with NODE_OPTIONS carrying `--import=<this file>`. In every Node process of the row (the
4
+ // test runner, pnpm, workers, any Node child they launch, including one given a replacement environment) the child_process launchers are wrapped, and a launch is
5
+ // refused (it throws, so the row fails) when the program, or a command line it hands to a shell, is a compile or an emitting
6
+ // build: cargo, rustc, rustright, rightkit (broker client), tauri / cargo-tauri, tsc without --noEmit, tsup, esbuild, swc, rollup,
7
+ // webpack, `vite build`, `next build`, or a package-manager run of a build / build:* script.
8
+ //
9
+ // Scope, exactly: launches from Node processes of the row. Not covered: a compiler API called inside a process
10
+ // (typescript.createProgram().emit(), vite.build() / the rollup JS API), and launches by a non-Node program that does not load
11
+ // this hook (rightkit#1240). esbuild's JS API is covered: it launches the esbuild service binary.
12
+ //
13
+ // A Node child handed a replacement environment keeps the guard (withGuard restores it). There is no exemption: a unit whose tests
14
+ // launch stand-ins named like compilers (stub scripts can run anything) runs after class A instead (npm.<unit>.afterClassA).
15
+ import childProcess from "node:child_process";
16
+ import path from "node:path";
17
+ import { syncBuiltinESMExports } from "node:module";
18
+ import { fileURLToPath, pathToFileURL } from "node:url";
19
+
20
+ const INSTALLED = Symbol.for("rightkit.classA.launchGuard");
21
+ export const LAUNCH_GUARD_IMPORT = `--import=${pathToFileURL(fileURLToPath(import.meta.url)).href}`;
22
+ export const REFUSAL_CODE = "RK_CLASS_A_COMPILE_LAUNCH";
23
+ const COMPILERS = new Set(["cargo", "rustc", "rustright", "rightkit", "tauri", "cargo-tauri", "tsup", "esbuild", "swc", "rollup", "webpack"]);
24
+ const PACKAGE_MANAGERS = new Set(["pnpm", "npm", "yarn", "bun"]);
25
+ const SHELLS = new Set(["sh", "bash", "zsh", "dash", "cmd", "powershell", "pwsh"]);
26
+ const WRAPPERS = new Set(["env", "npx", "pnpx", "exec", "time", "nice"]);
27
+ // Options that take a value, per package manager: pnpm's `-w` is `--workspace-root` (a flag), npm's `-w` is `--workspace <name>`.
28
+ const OPTIONS_WITH_VALUES = Object.freeze({
29
+ pnpm: new Set(["--filter", "-F", "--dir", "-C"]),
30
+ npm: new Set(["--workspace", "-w", "--prefix"]),
31
+ yarn: new Set(["--cwd"]),
32
+ bun: new Set(["--cwd", "--filter"]),
33
+ });
34
+
35
+ const base = (p) => path.basename(String(p ?? "").replaceAll("\\", "/")).replace(/\.(exe|cmd|bat|ps1|js|mjs|cjs)$/i, "").toLowerCase();
36
+
37
+ /** A shell command line split into commands (on && || ; |) and words, quotes removed: enough to read what a `sh -c` text launches. */
38
+ export function shellWords(text) {
39
+ const commands = []; let words = []; let word = ""; let quote = null;
40
+ const end = () => { if (word) { words.push(word); word = ""; } };
41
+ const s = String(text);
42
+ for (let i = 0; i < s.length; i++) {
43
+ const c = s[i];
44
+ if (quote) { if (c === quote) quote = null; else word += c; continue; }
45
+ if (c === "'" || c === "\"") { quote = c; continue; }
46
+ if (/\s/.test(c)) { end(); continue; }
47
+ if (c === ";" || c === "|" || c === "&") { end(); if (words.length) commands.push(words); words = []; continue; }
48
+ word += c;
49
+ }
50
+ end(); if (words.length) commands.push(words);
51
+ return commands;
52
+ }
53
+
54
+ /** Why `[program, ...args]` is a compile or an emitting build, or null. */
55
+ export function compileLaunch(program, args = []) {
56
+ let words = [String(program), ...args.map(String)];
57
+ for (let guard = 0; guard < 8 && words.length; guard++) {
58
+ const name = base(words[0]);
59
+ if (name === "node" && words[1] && !String(words[1]).startsWith("-")) { words = words.slice(1); continue; }
60
+ if (WRAPPERS.has(name)) { words = words.slice(1).filter((w, i, a) => !(i === 0 && /^[A-Z_][A-Z0-9_]*=/.test(w))); continue; }
61
+ if (SHELLS.has(name)) {
62
+ const at = words.findIndex((w, i) => i > 0 && /^(-c|\/c|\/s|-Command|-command)$/i.test(w));
63
+ if (at < 0 || !words[at + 1]) return null;
64
+ for (const cmd of shellWords(words.slice(at + 1).join(" "))) { const why = compileLaunch(cmd[0], cmd.slice(1)); if (why) return why; }
65
+ return null;
66
+ }
67
+ const rest = words.slice(1);
68
+ if (COMPILERS.has(name)) return name;
69
+ if (name === "tsc") return rest.some((w) => /^--?noEmit(=true)?$/i.test(w)) ? null : "tsc (emits)";
70
+ if (name === "vite" || name === "next") return rest.find((w) => !w.startsWith("-")) === "build" ? `${name} build` : null;
71
+ if (PACKAGE_MANAGERS.has(name)) {
72
+ let next = null;
73
+ for (let j = 0; j < rest.length; j++) {
74
+ if (OPTIONS_WITH_VALUES[name].has(rest[j])) { j++; continue; }
75
+ if (rest[j].startsWith("-") || rest[j] === "run" || rest[j] === "run-script") continue;
76
+ if (/^build(:|$)/.test(rest[j])) return `${name} ${rest[j]}`;
77
+ if (rest[j] === "exec" || rest[j] === "dlx" || rest[j] === "x") next = rest.slice(j + 1);
78
+ break;
79
+ }
80
+ if (!next || !next.length) return null;
81
+ words = next; continue;
82
+ }
83
+ return null;
84
+ }
85
+ return null;
86
+ }
87
+
88
+ function refuse(why, program, args) {
89
+ const error = new Error(`class A refuses a compile/emit launch (${why}): ${[program, ...args].join(" ").slice(0, 300)}. Class A runs before any compile; this test row belongs after class A.`);
90
+ error.code = REFUSAL_CODE;
91
+ try { process.stderr.write(`rightkit-gate: ${error.message}\n`); } catch { /* stderr closed */ }
92
+ throw error;
93
+ }
94
+
95
+ function check(program, args, options) {
96
+ const argv = Array.isArray(args) ? args : [];
97
+ if (options?.shell) { for (const cmd of shellWords([program, ...argv].join(" "))) { const why = compileLaunch(cmd[0], cmd.slice(1)); if (why) refuse(why, program, argv); } return; }
98
+ const why = compileLaunch(program, argv);
99
+ if (why) refuse(why, program, argv);
100
+ }
101
+
102
+ /** `env` with the guard switched on and loaded (NODE_OPTIONS appended, never replaced). */
103
+ export function withGuard(env) {
104
+ const current = String(env.NODE_OPTIONS ?? "");
105
+ return { ...env, RIGHTKIT_CLASS_A_LAUNCH_GUARD: "1", NODE_OPTIONS: current.includes(LAUNCH_GUARD_IMPORT) ? current : `${current} ${LAUNCH_GUARD_IMPORT}`.trim() };
106
+ }
107
+
108
+ export function installLaunchGuard() {
109
+ if (globalThis[INSTALLED]) return;
110
+ globalThis[INSTALLED] = true;
111
+ const o = { ...childProcess };
112
+ const split = (args) => (Array.isArray(args[1]) ? [args[0], args[1], args[2]] : [args[0], [], args[1]]);
113
+ // A child handed a replacement environment keeps the guard: its NODE_OPTIONS (and the switch) are restored into that env.
114
+ // The caller's options object is never modified: the launch gets a copy carrying the restored env.
115
+ const keep = (a) => a.map((x) => (x && typeof x === "object" && !Array.isArray(x) && x.env && typeof x.env === "object" ? { ...x, env: withGuard(x.env) } : x));
116
+ childProcess.spawn = function (...a) { const [p, v, opt] = split(a); check(p, v, opt); return o.spawn.apply(this, keep(a)); };
117
+ childProcess.spawnSync = function (...a) { const [p, v, opt] = split(a); check(p, v, opt); return o.spawnSync.apply(this, keep(a)); };
118
+ childProcess.execFile = function (...a) { const [p, v, opt] = split(a); check(p, v, opt); return o.execFile.apply(this, keep(a)); };
119
+ childProcess.execFileSync = function (...a) { const [p, v, opt] = split(a); check(p, v, opt); return o.execFileSync.apply(this, keep(a)); };
120
+ childProcess.exec = function (cmd, ...a) { for (const c of shellWords(cmd)) { const why = compileLaunch(c[0], c.slice(1)); if (why) refuse(why, cmd, []); } return o.exec.call(this, cmd, ...keep(a)); };
121
+ childProcess.execSync = function (cmd, ...a) { for (const c of shellWords(cmd)) { const why = compileLaunch(c[0], c.slice(1)); if (why) refuse(why, cmd, []); } return o.execSync.call(this, cmd, ...keep(a)); };
122
+ childProcess.fork = function (modulePath, ...a) { const v = Array.isArray(a[0]) ? a[0] : []; check(modulePath, v, {}); return o.fork.call(this, modulePath, ...keep(a)); };
123
+ syncBuiltinESMExports();
124
+ }
125
+
126
+ if (process.env.RIGHTKIT_CLASS_A_LAUNCH_GUARD === "1") installLaunchGuard();
@@ -25,6 +25,7 @@ const commands = new Map([
25
25
  ["sign-candidate", "sign-candidate.mjs"],
26
26
  ["engine-locate", "engine-locator.mjs"],
27
27
  ["stage-sidecar", "stage-sidecar-command.mjs"],
28
+ ["assemble-app", "assemble-app-command.mjs"],
28
29
  ["init", "init-command.mjs"],
29
30
  ["pe", "pe-content.mjs"],
30
31
  ["icons", "icon-pack.mjs"],
@@ -269,6 +270,7 @@ Commands:
269
270
  icons ico|icns --out <file> --frame <PNG>... [--dir <folder>] Pack hand-tuned per-size PNG frames into an ICO or ICNS
270
271
  init --name <Product Name> --id <reverse.dns.bundle.id> [--slug <lowercase>] [--dir <root>] [--dry-run] Rename the desktop app template into a real app, in place
271
272
  stage-sidecar [--config <file>] [--target <triple>] [--json] Stage declarative sidecars into src-tauri/binaries (Tauri externalBin naming)
273
+ assemble-app --platform mac|win [--profile release|dev] [--build] [--out <dir>] | --verify-staged [--signed] | --package Assemble the declared composite app (assembly.<platform>) for production packaging; the same definition installs through dev
272
274
  recipe-build --plan <json> | --plan-file <path> [--app-root <dir>] Run a package recipe: prepare steps, the Tauri build, postBuild steps (argv, no shell, fail closed)
273
275
  dmg build --app <.app> --out <dmg> --volname <name> [--background <png>] [--spec <appdmg.json>]
274
276
  [--icon-size N] [--app-position x,y] [--applications-position x,y] [--window w,h] [--window-position x,y]
@@ -360,7 +362,7 @@ Commands:
360
362
  signing-agent install|uninstall|status Manage invisible Aqua-session signing service
361
363
  check [--repo <dir>] [--config <file>] [--commit <rev> | --worktree] [--only <rule,...>] [--list] [--print-baseline] [--baseline <file>]
362
364
  The offline app check, same as rightkit-gate check: committed files only, every finding in one run (exit 0 pass, 1 findings, 2 could not run).
363
- Build, package, dev, test, web-build, swift-package, recipe-build, stage-sidecar, qa, upload, publish, publish-update, github, doctor,
365
+ Build, package, dev, test, web-build, swift-package, recipe-build, assemble-app, stage-sidecar, qa, upload, publish, publish-update, github, doctor,
364
366
  sign-windows, sign-updater, sign-candidate, sign-dev-artifact, addon build and create-mac-updater run it first and start nothing when it is red; there is no flag that skips it.
365
367
  Baseline: .rightkit/app-check-baseline.json (entries carry issue, added and sunset; the sunset is at most 90 days after added)
366
368
  doctor [--platform mac|win] [--gates] [--inspect] [--strict]
package/codesign-shim.mjs CHANGED
@@ -23,7 +23,17 @@ export async function runCodesignShim(argv, { cwd = process.cwd(), env = process
23
23
  privatePath(paths.config, 0o600, { io });
24
24
  roots = JSON.parse(io.readFileSync(paths.config, "utf8")).roots;
25
25
  }
26
- const { targets } = parseCodesignArgv(argv, cwd);
26
+ const { targets, mode } = parseCodesignArgv(argv, cwd);
27
+ if (mode !== "sign") {
28
+ // Verify & display read the code they are given and need no identity, so
29
+ // they run the system codesign here. The agent's roots are the signing
30
+ // policy (where the identity may be applied), not a read policy: a dev
31
+ // install verifies its installed copy under /Applications, which no
32
+ // signing root contains.
33
+ const direct = run("/usr/bin/codesign", argv, { cwd, env, encoding: "utf8", shell: false, windowsHide: true });
34
+ if (direct.error) throw direct.error;
35
+ return { status: direct.status ?? 1, stdout: String(direct.stdout ?? ""), stderr: String(direct.stderr ?? "") };
36
+ }
27
37
  const report = await request({ schema: 1, type: "codesign-argv", argv, cwd, targets,
28
38
  allowedRoots: roots.filter((root) => io.existsSync(root)), timeoutMs: 180000 }, { paths });
29
39
  const result = report.results?.[0];