@rightkit/release 0.2.60 → 0.2.62

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/build-release.mjs CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  closeSync,
6
6
  copyFileSync,
7
7
  existsSync,
8
+ mkdtempSync,
8
9
  mkdirSync,
9
10
  openSync,
10
11
  readFileSync,
@@ -17,6 +18,7 @@ import {
17
18
  unlinkSync,
18
19
  writeFileSync,
19
20
  } from "node:fs";
21
+ import { tmpdir } from "node:os";
20
22
  import path from "node:path";
21
23
  import { spawn, spawnSync } from "node:child_process";
22
24
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -163,6 +165,9 @@ try {
163
165
  if (signingIdentity) inputHashes[".right-release/signing-identity.json"] = hashFileText(JSON.stringify(signingIdentity));
164
166
  const cargoLock = requiredInputs.find((file) => /Cargo\.lock$/i.test(file));
165
167
  const cargoToml = requiredInputs.find((file) => /Cargo\.toml$/i.test(file));
168
+ const managedCargoTarget = process.env.RIGHTKIT_BUILD_BROKER_SOCKET
169
+ ? resolveManagedCargoTarget(cargoToml)
170
+ : null;
166
171
  const cacheIdentity = resolveSharedCacheIdentity({ cargoLockPath: cargoLock, cargoTomlPath: cargoToml, rustcVerbose: toolVersions.rustc, targetTriple: target.cargoTarget ?? target.targetTriple ?? target.rustTarget ?? toolVersions.rustHost, profile: target.profile ?? "release" });
167
172
  const cacheKey = hashFileText(JSON.stringify({ cache: cacheIdentity.fingerprint, signingIdentity })).slice(0, 16);
168
173
  const cacheMode = process.env.RIGHT_RELEASE_CACHE_MODE ?? (platform === "mac" || platform === "win" ? "shared" : "legacy");
@@ -197,6 +202,11 @@ try {
197
202
  RIGHT_RELEASE_REPO_ROOT: layout.repoRoot,
198
203
  RIGHT_RELEASE_APP_ROOT: layout.appRoot,
199
204
  };
205
+ if (managedCargoTarget) {
206
+ for (const name of ["CARGO_HOME", "CARGO_TARGET_DIR", "RUSTC_WRAPPER", "SCCACHE_DIR", "SCCACHE_BASEDIRS", "SCCACHE_CACHE_SIZE", "RUSTFLAGS"]) {
207
+ delete env[name];
208
+ }
209
+ }
200
210
  if (cacheMode === "legacy") delete env.RIGHT_RELEASE_CACHE_OWNER;
201
211
  if (cacheMode === "shared" && env.RIGHT_RELEASE_CACHE_OWNER !== "rightkit-v2") fail("shared cache ownership token was not configured");
202
212
  if (cacheMode === "legacy" && !commandExists("sccache")) delete env.RUSTC_WRAPPER;
@@ -208,7 +218,7 @@ try {
208
218
  throwIfInterrupted();
209
219
  const releaseId = `${config.app}-${config.version}-${shortCommit}`;
210
220
  const platformDir = platform === "win" ? "windows" : "mac";
211
- const buildRoot = path.join(vaultRoot, "build", config.app, config.version, shortCommit, platformDir);
221
+ const buildRoot = managedCargoTarget ? null : path.join(vaultRoot, "build", config.app, config.version, shortCommit, platformDir);
212
222
  const stateRoot = path.join(vaultRoot, "state", releaseId, platformDir);
213
223
  const lockPayload = {
214
224
  schema: 1,
@@ -229,8 +239,8 @@ try {
229
239
  // instead of hard-stopping the build.
230
240
  const targetBridge = createTargetBridge({
231
241
  link: targetLink,
232
- target: env.CARGO_TARGET_DIR,
233
- ownedRoot: path.dirname(env.CARGO_TARGET_DIR),
242
+ target: managedCargoTarget ?? env.CARGO_TARGET_DIR,
243
+ ownedRoot: path.dirname(managedCargoTarget ?? env.CARGO_TARGET_DIR),
234
244
  });
235
245
 
236
246
  const result = await targetBridge.run(async () => runBuildStateMachine({
@@ -256,15 +266,15 @@ try {
256
266
  assertExecutables(["git", config.packageManager ?? "pnpm", "cargo", "rustc", ...(target.preflight?.executables ?? [])]);
257
267
  for (const name of target.preflight?.env ?? []) if (!process.env[name]) fail(`missing required environment variable: ${name}`);
258
268
  for (const command of target.preflight?.commands ?? []) runChecked(command.cmd, command.args ?? [], path.resolve(appRoot, command.cwd ?? "."), env);
259
- mkdirSync(buildRoot, { recursive: true });
269
+ if (buildRoot) mkdirSync(buildRoot, { recursive: true });
260
270
  mkdirSync(stateRoot, { recursive: true });
261
- writeJson(path.join(buildRoot, "release-inputs.lock.json"), lockPayload);
271
+ if (buildRoot) writeJson(path.join(buildRoot, "release-inputs.lock.json"), lockPayload);
262
272
  writeJson(path.join(stateRoot, "release-inputs.lock.json"), lockPayload);
263
273
  checkpoint(stateRoot, "preflight_complete");
264
274
  },
265
275
  prepare: async () => {
266
276
  throwIfInterrupted();
267
- const cacheTarget = env.CARGO_TARGET_DIR;
277
+ const cacheTarget = managedCargoTarget ?? env.CARGO_TARGET_DIR;
268
278
  mkdirSync(cacheTarget, { recursive: true });
269
279
  if (cacheMode === "shared") targetBridge.ensure();
270
280
  else if (!existsSync(targetLink)) symlinkSync(cacheTarget, targetLink, process.platform === "win32" ? "junction" : "dir");
@@ -280,7 +290,7 @@ try {
280
290
  [WORKER, "--config", configPath, "--platform", platform, "--no-upload"],
281
291
  appRoot,
282
292
  env,
283
- [env.CARGO_TARGET_DIR, path.join(appRoot, ".cache")],
293
+ [managedCargoTarget ?? env.CARGO_TARGET_DIR, path.join(appRoot, ".cache")],
284
294
  );
285
295
  checkpoint(stateRoot, "build_complete");
286
296
  checkpoint(stateRoot, "signed");
@@ -391,15 +401,32 @@ function sealRelease({ configRoot, sealedDir, releaseId, config, target, platfor
391
401
  }
392
402
 
393
403
  function collectToolVersions(packageManager) {
394
- const rustcVerbose = commandOutput("rustc", ["-vV"]);
395
- return {
396
- node: process.version,
397
- packageManager: `${packageManager} ${commandOutput(packageManager, ["--version"])}`,
398
- cargo: commandOutput("cargo", ["--version"]),
399
- rustc: rustcVerbose,
400
- rustHost: rustcVerbose.match(/^host:\s*(.+)$/m)?.[1] ?? "unknown",
401
- sccache: commandExists("sccache") ? commandOutput("sccache", ["--version"]) : null,
402
- };
404
+ const probeDir = mkdtempSync(path.join(tmpdir(), "right-release-tool-probe-"));
405
+ try {
406
+ const rustcVerbose = commandOutputAt("rustc", ["-vV"], probeDir);
407
+ return {
408
+ node: process.version,
409
+ packageManager: `${packageManager} ${commandOutput(packageManager, ["--version"])}`,
410
+ cargo: commandOutputAt("cargo", ["--version"], probeDir),
411
+ rustc: rustcVerbose,
412
+ rustHost: rustcVerbose.match(/^host:\s*(.+)$/m)?.[1] ?? "unknown",
413
+ sccache: commandExists("sccache") ? commandOutput("sccache", ["--version"]) : null,
414
+ };
415
+ } finally {
416
+ rmSync(probeDir, { recursive: true, force: true });
417
+ }
418
+ }
419
+
420
+ function resolveManagedCargoTarget(manifestPath) {
421
+ if (!manifestPath) fail("managed release requires a Cargo.toml build input");
422
+ const output = commandOutputAt(
423
+ "cargo",
424
+ ["metadata", "--no-deps", "--offline", "--format-version", "1", "--manifest-path", manifestPath],
425
+ path.dirname(manifestPath),
426
+ );
427
+ const target = JSON.parse(output).target_directory;
428
+ if (!target || !path.isAbsolute(target)) fail("managed Cargo metadata returned no absolute target directory");
429
+ return target;
403
430
  }
404
431
 
405
432
  async function runProgress(cmd, runArgs, cwd, env, watchDir) {
@@ -66,11 +66,11 @@ test("dirty release config is rejected before the config module is imported", ()
66
66
  });
67
67
 
68
68
  test("shared Cache V2 target bridge is owned by a finally-cleaned lifecycle", () => {
69
- assert.match(source, /createTargetBridge\(\{\s*link: targetLink,\s*target: env\.CARGO_TARGET_DIR,/);
69
+ assert.match(source, /createTargetBridge\(\{\s*link: targetLink,\s*target: managedCargoTarget \?\? env\.CARGO_TARGET_DIR,/);
70
70
  // ownedRoot must stay wired: without it the bridge refuses every link left by
71
71
  // an earlier fingerprint, so any Cargo.lock or version bump hard-stops the
72
72
  // next build until someone deletes the link by hand.
73
- assert.match(source, /ownedRoot: path\.dirname\(env\.CARGO_TARGET_DIR\)/);
73
+ assert.match(source, /ownedRoot: path\.dirname\(managedCargoTarget \?\? env\.CARGO_TARGET_DIR\)/);
74
74
  assert.match(source, /targetBridge\.run\(.*runBuildStateMachine/s);
75
75
  assert.match(source, /if \(cacheMode === "shared"\) targetBridge\.ensure\(\)/);
76
76
  });
@@ -13,11 +13,16 @@ const CACHE_V2_ENVIRONMENT = ["CARGO_HOME", "CARGO_TARGET_DIR", "RUSTC_WRAPPER",
13
13
  export function isolatedCargoMetadataEnv(cargoHome, env = process.env) {
14
14
  const metadataEnv = { ...env };
15
15
  for (const name of CACHE_V2_ENVIRONMENT) delete metadataEnv[name];
16
+ if (metadataEnv.RIGHTKIT_BUILD_BROKER_SOCKET) return metadataEnv;
16
17
  return { ...metadataEnv, CARGO_HOME: cargoHome };
17
18
  }
18
19
 
19
20
  export function cargoExecutable(platform = process.platform) {
20
- return platform === "win32" ? "cargo.exe" : "cargo";
21
+ return platform === "win32" ? "rustup" : "cargo";
22
+ }
23
+
24
+ export function cargoArguments(args, platform = process.platform) {
25
+ return platform === "win32" ? ["run", "stable", "cargo", ...args] : args;
21
26
  }
22
27
 
23
28
  export function validateRightKitCargoContract(root, publishedVersions, label = path.basename(root)) {
@@ -77,7 +82,7 @@ export function assertNoRightKitCargoOverrides(manifestPath, repoRoot, label) {
77
82
  function readCargoManifestDependencies(manifestPath, cargoHome, label) {
78
83
  const result = spawnSync(
79
84
  cargoExecutable(),
80
- ["metadata", "--no-deps", "--offline", "--format-version", "1", "--manifest-path", manifestPath],
85
+ cargoArguments(["metadata", "--no-deps", "--offline", "--format-version", "1", "--manifest-path", manifestPath]),
81
86
  {
82
87
  cwd: path.dirname(manifestPath),
83
88
  encoding: "utf8",
@@ -84,6 +84,7 @@ test("Cargo guard routes heavy and light commands without recursion", async () =
84
84
  env: { TEST: "1", ...cacheEnv() },
85
85
  policyOptions: { exists: () => false },
86
86
  resolveCargo: () => "/real/cargo",
87
+ resolveRustc: () => "/real/rustc",
87
88
  runHeavy: async (args, config) => { calls.push(["heavy", args, config.env]); return 0; },
88
89
  runLight: async (command, args, env) => { calls.push(["light", command, args, env]); return 0; },
89
90
  };
@@ -149,8 +149,8 @@ Commands:
149
149
  generate-dmg-background <options> Generate the branded multi-resolution DMG background
150
150
  mirror-root-artifact --file <path> --package-root <dir>
151
151
  Persist a worktree artifact in the primary repo root
152
- github --release <id> --platform mac|win --repo owner/repo [--dry-run]
153
- Attach one sealed installer to a verified GitHub Release
152
+ github --release <id> --platform mac|win [--repo owner/repo] [--dry-run]
153
+ Attach sealed signed artifacts to a verified GitHub Release
154
154
 
155
155
  Direct flags are treated as: right-release build <flags>.
156
156
  Build is tier-neutral. Upload requires an explicit tier. Unsigned/local smoke builds stay app-local.`);
@@ -24,24 +24,32 @@ export function prepareGitHubRelease({ repoRoot, releaseId, platform, repo, stat
24
24
  const manifestAsset = path.join(releaseState, `${sealed.manifest.app}-${sealed.manifest.version}-${platform}-release-manifest.json`);
25
25
  const checksumsAsset = path.join(releaseState, `${sealed.manifest.app}-${sealed.manifest.version}-${platform}-SHA256SUMS.txt`);
26
26
  copyFileSync(sealed.manifestPath, manifestAsset);
27
- writeFileSync(checksumsAsset, `${installers[0].sha256} ${installers[0].name}\n`);
27
+ writeFileSync(checksumsAsset, `${sealed.manifest.files.map((file) => `${file.sha256} ${file.name}`).join("\n")}\n`);
28
28
  const installer = path.join(sealed.sealedDir, installers[0].name);
29
+ const assets = [...sealed.manifest.files.map((file) => path.join(sealed.sealedDir, file.name)), manifestAsset, checksumsAsset];
29
30
  return {
30
31
  sealed,
31
32
  installer,
32
- assets: [installer, manifestAsset, checksumsAsset],
33
+ assets,
33
34
  tag: `v${sealed.manifest.version}`,
34
35
  title: `${sealed.manifest.app === "cutright" ? "CutRight Studio" : sealed.manifest.app} ${sealed.manifest.version}`,
35
36
  notes: [
36
37
  `Signed ${platform === "mac" ? "& notarized universal macOS" : "Windows"} installer.`,
37
38
  "",
38
39
  `Build commit: \`${sealed.manifest.commit}\``,
39
- `SHA-256: \`${installers[0].sha256}\``,
40
+ `Artifacts: ${sealed.manifest.files.map((file) => `\`${file.name}\``).join(", ")}`,
40
41
  ].join("\n"),
41
42
  notesFile: path.join(releaseState, "release-notes.md"),
42
43
  };
43
44
  }
44
45
 
46
+ export function repositoryFromRemote(repoRoot) {
47
+ const remote = runCommand("git", ["remote", "get-url", "origin"], { cwd: repoRoot });
48
+ const match = remote.trim().match(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/i);
49
+ if (!match || !REPO_RE.test(match[1])) throw new Error(`origin is not a GitHub repository: ${remote.trim()}`);
50
+ return match[1];
51
+ }
52
+
45
53
  export function prepareGitHubAddonRelease({ repoRoot, config, configRoot = repoRoot, platform, repo }) {
46
54
  if (platform !== "mac" && platform !== "win") throw new Error("platform must be mac or win");
47
55
  if (!REPO_RE.test(repo)) throw new Error(`invalid GitHub repository: ${repo}`);
@@ -72,12 +80,16 @@ export function prepareGitHubAddonRelease({ repoRoot, config, configRoot = repoR
72
80
 
73
81
  export function publishGitHubRelease(plan, { repo, dryRun = false, run = runCommand } = {}) {
74
82
  const visibility = JSON.parse(run("gh", ["repo", "view", repo, "--json", "visibility"]));
75
- if (visibility.visibility !== "PUBLIC") throw new Error(`GitHub releases require a public repository: ${repo}`);
76
- if (plan.kind === "addon") verifyAddonTrust(plan);
77
- else verifyPlatformTrust(plan);
83
+ if (visibility.visibility !== "PUBLIC" && !(plan.kind !== "addon" && visibility.visibility === "PRIVATE")) {
84
+ throw new Error(`GitHub releases require a public or private repository: ${repo}`);
85
+ }
86
+ if (!dryRun) {
87
+ if (plan.kind === "addon") verifyAddonTrust(plan);
88
+ else verifyPlatformTrust(plan);
89
+ }
78
90
  const existing = run("gh", ["release", "view", plan.tag, "--repo", repo, "--json", "tagName"], { allowFailure: true });
79
91
  if (dryRun) return { status: existing.ok ? plan.kind === "addon" ? "would-verify" : "would-update" : "would-create", tag: plan.tag, assets: plan.assets };
80
- if (plan.kind === "addon" && existing.ok) {
92
+ if (existing.ok) {
81
93
  const missing = plan.assets.filter((asset) => inspectRemoteAsset(plan.tag, repo, asset, run) === "missing");
82
94
  if (missing.length === 0) return { status: "already-verified", tag: plan.tag, assets: plan.assets, manifestUrl: plan.manifestUrl };
83
95
  run("gh", ["release", "upload", plan.tag, ...missing, "--repo", repo]);
@@ -90,7 +102,7 @@ export function publishGitHubRelease(plan, { repo, dryRun = false, run = runComm
90
102
  run("gh", ["release", "create", plan.tag, "--repo", repo, "--title", plan.title, "--notes-file", notesFile]);
91
103
  rmSync(notesFile, { force: true });
92
104
  }
93
- run("gh", ["release", "upload", plan.tag, ...plan.assets, "--repo", repo, ...(plan.kind === "addon" ? [] : ["--clobber"])]);
105
+ run("gh", ["release", "upload", plan.tag, ...plan.assets, "--repo", repo]);
94
106
  for (const asset of plan.assets) verifyRemoteAsset(plan.tag, repo, asset, run);
95
107
  return { status: "verified", tag: plan.tag, assets: plan.assets, manifestUrl: plan.manifestUrl };
96
108
  }
@@ -107,7 +119,10 @@ function verifyAddonTrust(plan) {
107
119
  }
108
120
 
109
121
  function verifyPlatformTrust(plan) {
110
- if (plan.sealed.manifest.platform !== "mac") return;
122
+ if (plan.sealed.manifest.platform === "win") {
123
+ runCommand(process.execPath, [path.join(path.dirname(fileURLToPath(import.meta.url)), "sign-windows.mjs"), "--verify-only", plan.installer]);
124
+ return;
125
+ }
111
126
  runCommand("spctl", ["--assess", "--type", "open", "--context", "context:primary-signature", "--verbose=2", plan.installer]);
112
127
  runCommand("xcrun", ["stapler", "validate", plan.installer]);
113
128
  }
@@ -179,7 +194,7 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
179
194
  if (config.distribution?.provider !== "github-releases") throw new Error("add-on config must select github-releases distribution");
180
195
  plan = prepareGitHubAddonRelease({ repoRoot, config, configRoot: path.dirname(configPath), ...options });
181
196
  } else {
182
- if (!options.repo) usage(2);
197
+ options.repo ||= repositoryFromRemote(repoRoot);
183
198
  plan = prepareGitHubRelease({ repoRoot, ...options });
184
199
  }
185
200
  const result = publishGitHubRelease(plan, options);
package/heavy-command.mjs CHANGED
@@ -133,6 +133,30 @@ function writeTrackedChild(lockDir, owner, child) {
133
133
  renameSync(temporary, file);
134
134
  }
135
135
 
136
+ function matchingProcessIdentity(pid, expectedStartedAtMs, { alive = processAlive, startedAt = processStartedAt, assumeCurrentWhenStartUnavailable = false } = {}) {
137
+ if (!alive(pid)) return false;
138
+ if (!Number.isFinite(Number(expectedStartedAtMs))) return assumeCurrentWhenStartUnavailable;
139
+ const observedStartedAtMs = startedAt(pid);
140
+ if (!Number.isFinite(observedStartedAtMs)) return assumeCurrentWhenStartUnavailable;
141
+ return Math.abs(observedStartedAtMs - Number(expectedStartedAtMs)) <= 2000;
142
+ }
143
+
144
+ export function sweepTrackedHeavyWorkOrphan({ root = heavyWorkRoot(), alive = processAlive, startedAt = processStartedAt, terminate = terminateProcessTree, log = (message) => console.error(`[heavy-work] ${message}`) } = {}) {
145
+ const lockDir = path.join(root, "slot");
146
+ const owner = readOwner(lockDir);
147
+ if (!owner) return false;
148
+ if (matchingProcessIdentity(owner.pid, owner.ownerStartedAtMs, { alive, startedAt, assumeCurrentWhenStartUnavailable: true })) return false;
149
+ if (owner.childPid && alive(owner.childPid)) {
150
+ if (!matchingProcessIdentity(owner.childPid, owner.childStartedAtMs, { alive, startedAt })) {
151
+ throw new Error(`dead heavy-work owner pid ${owner.pid}; tracked child pid ${owner.childPid} identity changed; refusing to kill or admit`);
152
+ }
153
+ log(`reaping owned process tree pid ${owner.childPid} after stale owner pid ${owner.pid}`);
154
+ terminate(owner.childPid);
155
+ }
156
+ rmSync(lockDir, { recursive: true, force: true });
157
+ return true;
158
+ }
159
+
136
160
  export function processStartedAt(pid, { platform = process.platform, run = spawnSync } = {}) {
137
161
  if (!Number.isInteger(Number(pid)) || Number(pid) <= 0) return null;
138
162
  const result = platform === "win32" || platform === "win"
@@ -206,23 +230,15 @@ export function acquireHeavyWorkSlot({
206
230
  try {
207
231
  mkdirSync(lockDir);
208
232
  const fd = openSync(path.join(lockDir, "owner.json"), "wx");
209
- writeFileSync(fd, `${JSON.stringify({ schema: 1, pid: Number(pid), token, argv: [...argv], createdAt: new Date().toISOString() })}\n`);
233
+ writeFileSync(fd, `${JSON.stringify({ schema: 1, pid: Number(pid), ownerStartedAtMs: startedAt(pid), token, argv: [...argv], createdAt: new Date().toISOString() })}\n`);
210
234
  closeSync(fd);
211
235
  break;
212
236
  } catch (error) {
213
237
  if (error.code !== "EEXIST") throw error;
214
238
  const owner = readOwner(lockDir);
215
239
  const incompleteAge = (() => { try { return Date.now() - statSync(lockDir).mtimeMs; } catch { return 0; } })();
216
- if (owner ? !alive(owner.pid) : incompleteAge > 10_000) {
217
- if (owner?.childPid && alive(owner.childPid)) {
218
- const observedStart = startedAt(owner.childPid);
219
- if (!Number.isFinite(observedStart) || Math.abs(observedStart - Number(owner.childStartedAtMs)) > 2000) {
220
- throw new Error(`dead heavy-work owner pid ${owner.pid}; tracked child pid ${owner.childPid} identity changed; refusing to kill or admit`);
221
- }
222
- log(`reaping owned process tree pid ${owner.childPid} after owner pid ${owner.pid} exited`);
223
- terminate(owner.childPid);
224
- }
225
- rmSync(lockDir, { recursive: true, force: true });
240
+ if (owner ? sweepTrackedHeavyWorkOrphan({ root, alive, startedAt, terminate, log }) : incompleteAge > 10_000) {
241
+ if (!owner) rmSync(lockDir, { recursive: true, force: true });
226
242
  continue;
227
243
  }
228
244
  if (Date.now() - started >= waitMs) throw new Error(`heavy-work slot timed out after ${waitMs}ms; holder pid ${owner?.pid ?? "starting"}`);
@@ -15,6 +15,7 @@ import {
15
15
  parseWindowsResourceSnapshot,
16
16
  resourceBlockers,
17
17
  runHeavyCommand,
18
+ sweepTrackedHeavyWorkOrphan,
18
19
  systemResourceSnapshot,
19
20
  terminateProcessTree,
20
21
  watchOwnedProcessTree,
@@ -147,6 +148,54 @@ test("dead slot owner refuses to kill a reused child PID", () => {
147
148
  first.release();
148
149
  });
149
150
 
151
+ test("preflight sweep reaps only a tracked child after an owner PID is reused", () => {
152
+ const root = mkdtempSync(path.join(os.tmpdir(), "rightsuite-heavy-preflight-"));
153
+ const lockDir = path.join(root, "slot");
154
+ mkdirSync(lockDir);
155
+ writeFileSync(path.join(lockDir, "owner.json"), JSON.stringify({ pid: 101, ownerStartedAtMs: 100, token: "stale" }));
156
+ writeFileSync(path.join(lockDir, "child.json"), JSON.stringify({ childPid: 202, childStartedAtMs: 200 }));
157
+ const killed = [];
158
+ assert.equal(sweepTrackedHeavyWorkOrphan({
159
+ root,
160
+ alive: (pid) => pid === 101 || pid === 202,
161
+ startedAt: (pid) => pid === 101 ? 5_000 : pid === 202 ? 200 : null,
162
+ terminate: (pid) => killed.push(pid),
163
+ log: () => {},
164
+ }), true);
165
+ assert.deepEqual(killed, [202]);
166
+ assert.equal(existsSync(lockDir), false);
167
+ });
168
+
169
+ test("preflight sweep refuses a reused tracked child PID instead of killing broadly", () => {
170
+ const root = mkdtempSync(path.join(os.tmpdir(), "rightsuite-heavy-preflight-reused-"));
171
+ const lockDir = path.join(root, "slot");
172
+ mkdirSync(lockDir);
173
+ writeFileSync(path.join(lockDir, "owner.json"), JSON.stringify({ pid: 101, ownerStartedAtMs: 100, token: "stale" }));
174
+ writeFileSync(path.join(lockDir, "child.json"), JSON.stringify({ childPid: 202, childStartedAtMs: 200 }));
175
+ assert.throws(() => sweepTrackedHeavyWorkOrphan({
176
+ root,
177
+ alive: (pid) => pid === 101 || pid === 202,
178
+ startedAt: (pid) => pid === 101 ? 5_000 : pid === 202 ? 4_000 : null,
179
+ terminate: () => assert.fail("reused child PID must not be terminated"),
180
+ log: () => {},
181
+ }), /identity changed/);
182
+ });
183
+
184
+ test("preflight sweep refuses a tracked child without a verifiable start identity", () => {
185
+ const root = mkdtempSync(path.join(os.tmpdir(), "rightsuite-heavy-preflight-unverified-"));
186
+ const lockDir = path.join(root, "slot");
187
+ mkdirSync(lockDir);
188
+ writeFileSync(path.join(lockDir, "owner.json"), JSON.stringify({ pid: 101, ownerStartedAtMs: 100, token: "stale" }));
189
+ writeFileSync(path.join(lockDir, "child.json"), JSON.stringify({ childPid: 202, childStartedAtMs: 200 }));
190
+ assert.throws(() => sweepTrackedHeavyWorkOrphan({
191
+ root,
192
+ alive: (pid) => pid === 101 || pid === 202,
193
+ startedAt: (pid) => pid === 101 ? 5_000 : null,
194
+ terminate: () => assert.fail("unverified child PID must not be terminated"),
195
+ log: () => {},
196
+ }), /identity changed/);
197
+ });
198
+
150
199
  test("detached watcher reaps an owned child as soon as its owner disappears", async () => {
151
200
  const root = mkdtempSync(path.join(os.tmpdir(), "rightsuite-heavy-watch-"));
152
201
  const lockDir = path.join(root, "slot");
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.2.60",
4
- "description": "Portable Right Suite release CLI/SDK: signed installers, updater artifacts, patch/update routing, hardening, R2 publish, and RightApps registration.",
3
+ "version": "0.2.62",
4
+ "description": "Portable Right Suite release CLI/SDK: native-host signed installers, updater artifacts, hardening, immutable GitHub Release upload, and add-on adoption.",
5
+ "license": "MIT OR Apache-2.0",
5
6
  "type": "module",
6
7
  "bin": {
7
8
  "right-release": "cli/right-release.mjs"
@@ -20,7 +21,7 @@
20
21
  },
21
22
  "repository": {
22
23
  "type": "git",
23
- "url": "git+https://github.com/adrdsouza/claude.git",
24
+ "url": "git+https://github.com/bogusyogi/claude.git",
24
25
  "directory": "tools/rightkit/packages/release"
25
26
  },
26
27
  "scripts": {
@@ -61,12 +61,14 @@ test("upload requires an explicit patch or update tier before reading a release"
61
61
  assert.match(result.stderr, /tier is required.*patch\|update/i);
62
62
  });
63
63
 
64
- test("Windows upload trust verification uses signtool instead of PowerShell modules", () => {
64
+ test("GitHub upload delegates platform trust verification to the shared release lane", () => {
65
65
  const uploadSource = readFileSync(upload, "utf8");
66
+ const githubSource = readFileSync(path.join(root, "github-release.mjs"), "utf8");
66
67
  const signingSource = readFileSync(signWindows, "utf8");
67
- assert.match(uploadSource, /SIGN_WINDOWS/);
68
- assert.match(uploadSource, /--verify-only/);
69
- assert.doesNotMatch(uploadSource, /Get-AuthenticodeSignature/);
68
+ assert.match(uploadSource, /publishGitHubRelease/);
69
+ assert.match(githubSource, /sign-windows\.mjs/);
70
+ assert.match(githubSource, /--verify-only/);
71
+ assert.doesNotMatch(uploadSource, /CLOUDFLARE_API_TOKEN|wrangler|R2/i);
70
72
  assert.match(signingSource, /verifyOnly/);
71
73
  assert.match(signingSource, /\["verify", "\/pa", "\/v", file\]/);
72
74
  });
@@ -101,8 +103,17 @@ test("Windows signer verification accepts CRLF subjects and retains identity che
101
103
  );
102
104
  });
103
105
 
104
- test("upload uses only the pinned pnpm runner", () => {
106
+ test("upload uses GitHub CLI and never invokes the R2 runner", () => {
105
107
  const source = readFileSync(upload, "utf8");
106
- assert.match(source, /spawnSync\("pnpm", \["dlx", "wrangler@4"/);
108
+ const githubSource = readFileSync(path.join(root, "github-release.mjs"), "utf8");
109
+ assert.match(githubSource, /run\("gh", \["release", "upload"/);
110
+ assert.doesNotMatch(source, /wrangler|CLOUDFLARE_API_TOKEN|RIGHTAPPS_API_URL/i);
107
111
  assert.doesNotMatch(source, /\bnpx\b/);
108
112
  });
113
+
114
+ test("upload accepts only GitHub Releases as configured product distribution", () => {
115
+ const source = readFileSync(upload, "utf8");
116
+ assert.match(source, /distribution\.provider/);
117
+ assert.match(source, /github-releases/);
118
+ assert.match(source, /releaseConfig\?\.distribution\?\.repository/);
119
+ });
package/release-state.mjs CHANGED
@@ -46,14 +46,16 @@ export function commandOutputPortable(cmd, args, { cwd, env = process.env } = {}
46
46
  return result.stdout.trim();
47
47
  }
48
48
 
49
- export function watchProgress(paths, onProgress) {
49
+ export function watchProgress(paths, onProgress, { maxFallbackWatchers = 128, watchFactory = watch, readDirectory = readdirSync } = {}) {
50
+ if (!Number.isInteger(maxFallbackWatchers) || maxFallbackWatchers < 1) throw new TypeError("maxFallbackWatchers must be a positive integer");
50
51
  const watchers = [];
52
+ const fallback = { remaining: maxFallbackWatchers };
51
53
  for (const candidate of paths) {
52
54
  if (!candidate || !existsSync(candidate)) continue;
53
55
  try {
54
- watchers.push(watch(candidate, { recursive: true }, onProgress));
56
+ watchers.push(watchFactory(candidate, { recursive: true }, onProgress));
55
57
  } catch {
56
- try { watchers.push(watch(candidate, onProgress)); } catch { /* output still counts as progress */ }
58
+ watchDirectoryTree(candidate, onProgress, watchers, fallback, { watchFactory, readDirectory });
57
59
  }
58
60
  }
59
61
  return () => {
@@ -61,6 +63,23 @@ export function watchProgress(paths, onProgress) {
61
63
  };
62
64
  }
63
65
 
66
+ function watchDirectoryTree(root, onProgress, watchers, fallback, { watchFactory = watch, readDirectory = readdirSync } = {}) {
67
+ const pending = [root];
68
+ const visited = new Set();
69
+ while (pending.length) {
70
+ const directory = pending.pop();
71
+ if (visited.has(directory)) continue;
72
+ visited.add(directory);
73
+ if (fallback.remaining <= 0) return;
74
+ try { watchers.push(watchFactory(directory, onProgress)); fallback.remaining -= 1; } catch { /* output still counts as progress */ }
75
+ let entries;
76
+ try { entries = readDirectory(directory, { withFileTypes: true }); } catch { continue; }
77
+ for (const entry of entries) {
78
+ if (entry.isDirectory()) pending.push(path.join(directory, entry.name));
79
+ }
80
+ }
81
+ }
82
+
64
83
  export function releaseEnvironment({ root, cacheRoot, platform, architecture, app, cacheKey, kind = "release", appRoot, mode = "legacy", env = process.env }) {
65
84
  if (kind !== "release" && kind !== "test") throw new Error(`invalid target kind: ${kind}`);
66
85
  if (mode !== "legacy" && mode !== "shared") throw new Error(`invalid cache mode: ${mode}`);
@@ -23,20 +23,59 @@ test("progress watcher observes writes in nested Cargo target directories", asyn
23
23
  const root = mkdtempSync(path.join(os.tmpdir(), "right-release-watch-"));
24
24
  const nested = path.join(root, "release", "build", "openssl");
25
25
  mkdirSync(nested, { recursive: true });
26
+ const readyFile = path.join(root, "watch-ready");
27
+ const nestedFile = path.join(nested, "object.lib");
28
+ let resolveReady;
26
29
  let resolveProgress;
30
+ const ready = new Promise((resolve) => { resolveReady = resolve; });
27
31
  const progress = new Promise((resolve) => { resolveProgress = resolve; });
28
- const close = watchProgress([root], resolveProgress);
32
+ const close = watchProgress([root], (_eventType, filename) => {
33
+ const eventName = String(filename ?? "");
34
+ if (eventName === path.basename(readyFile)) resolveReady();
35
+ if (eventName.endsWith(path.basename(nestedFile))) resolveProgress();
36
+ });
29
37
  try {
30
- await writeFile(path.join(nested, "object.lib"), "progress");
31
- await Promise.race([
32
- progress,
33
- new Promise((_, reject) => setTimeout(() => reject(new Error("nested progress event missing")), 2_000)),
34
- ]);
38
+ await new Promise((resolve) => setTimeout(resolve, 50));
39
+ await writeFile(readyFile, "ready");
40
+ await waitForEvent(ready, "watcher readiness event missing");
41
+ await writeFile(nestedFile, "progress");
42
+ await waitForEvent(progress, "nested progress event missing");
43
+ } finally {
44
+ close();
45
+ }
46
+ });
47
+
48
+ test("fallback progress watchers cap open handles when recursive watching is unavailable", () => {
49
+ const root = mkdtempSync(path.join(os.tmpdir(), "right-release-watch-cap-"));
50
+ for (let index = 0; index < 10; index += 1) mkdirSync(path.join(root, `dir-${index}`));
51
+ const opened = [];
52
+ const close = watchProgress([root], () => {}, {
53
+ maxFallbackWatchers: 3,
54
+ watchFactory: (directory, options) => {
55
+ if (options?.recursive) throw new Error("recursive watch unsupported");
56
+ opened.push(directory);
57
+ return { close: () => {} };
58
+ },
59
+ });
60
+ try {
61
+ assert.equal(opened.length, 3);
35
62
  } finally {
36
63
  close();
37
64
  }
38
65
  });
39
66
 
67
+ async function waitForEvent(event, message) {
68
+ let timer;
69
+ try {
70
+ await Promise.race([
71
+ event,
72
+ new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(message)), 5_000); }),
73
+ ]);
74
+ } finally {
75
+ clearTimeout(timer);
76
+ }
77
+ }
78
+
40
79
  test("portable command capture resolves Windows command shims", { skip: process.platform !== "win32" }, () => {
41
80
  assert.equal(commandOutputPortable("pnpm", ["--version"]), expectedPnpmVersion);
42
81
  });
package/release.mjs CHANGED
@@ -12,6 +12,7 @@ import { assertPrimaryReleaseCheckout, dirtyBuildInputs, resolveReleaseBuildInpu
12
12
  import { collectPreflight, formatPreflight, preflightFailures } from "./preflight.mjs";
13
13
  import { patchTauriBundleType } from "./tauri-bundle-marker.mjs";
14
14
  import { verifyNsisEmbeddedBinary } from "./nsis-payload.mjs";
15
+ import { terminateProcessTree } from "./heavy-command.mjs";
15
16
 
16
17
  const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
17
18
  const HARDENING_SCAN = path.resolve(TOOL_ROOT, "hardeningscan.mjs");
@@ -29,6 +30,7 @@ const RIGHTKIT_ALLOWED = buildAllowedVersions(
29
30
  const RIGHTKIT_CARGO_ALLOWED = buildAllowedVersions(RIGHTKIT_VERSIONS.cargo, RIGHTKIT_VERSIONS.stagedCargo);
30
31
  const FORBIDDEN_RIGHTKIT_SPEC = /^(?:git|file|link|workspace):|github|github\.com/i;
31
32
  const WINDOWS_SIGNING_CONTRACT = "windows-raw-exe-authenticode-before-nsis-v1";
33
+ const DEFAULT_NOTARIZATION_TIMEOUT_MS = 30 * 60 * 1000;
32
34
 
33
35
  const args = process.argv.slice(2);
34
36
  const opts = {
@@ -344,7 +346,18 @@ async function runPackageScript(pm, script, cwd) {
344
346
 
345
347
  async function runCommand(command, root) {
346
348
  const cwd = path.resolve(root, command.cwd ?? ".");
347
- await run(command.cmd, command.args ?? [], cwd, await commandEnv(command, root), command);
349
+ await run(command.cmd, command.args ?? [], cwd, await commandEnv(command, root), {
350
+ ...command,
351
+ timeoutMs: commandTimeoutMs(command),
352
+ });
353
+ }
354
+
355
+ function commandTimeoutMs(command) {
356
+ if (Object.hasOwn(command, "timeoutMs")) return command.timeoutMs;
357
+ const tokens = [command.cmd, ...(command.args ?? [])].map((value) => String(value));
358
+ return command.notarize === true || command.notarization === true || tokens.some((value) => /notari[sz]/i.test(value))
359
+ ? DEFAULT_NOTARIZATION_TIMEOUT_MS
360
+ : undefined;
348
361
  }
349
362
 
350
363
  async function validateRightKitPackageContract(root, appName) {
@@ -624,18 +637,5 @@ function processCommandLine(pid) {
624
637
  }
625
638
 
626
639
  function killProcessTree(pid) {
627
- if (!pid) return;
628
- if (process.platform === "win32") {
629
- spawnSync("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore" });
630
- return;
631
- }
632
- const children = spawnSync("pgrep", ["-P", String(pid)], { encoding: "utf8" });
633
- for (const child of (children.stdout ?? "").split(/\s+/).filter(Boolean)) {
634
- killProcessTree(Number(child));
635
- }
636
- try {
637
- process.kill(pid, "SIGKILL");
638
- } catch {
639
- // already gone
640
- }
640
+ terminateProcessTree(pid);
641
641
  }
package/release.test.mjs CHANGED
@@ -16,7 +16,7 @@ function git(cwd, ...args) {
16
16
  return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
17
17
  }
18
18
 
19
- function fixture({ signed = true, publish = false, signingContract = "windows-raw-exe-authenticode-before-nsis-v1", prePackageFiles = ["raw.exe"], packageJson, cargoManifest, cargoConfig, repoCargoConfig, siblingCargoManifest, buildInputs = defaultBuildInputs, platform = "win" } = {}) {
19
+ function fixture({ signed = true, publish = false, signingContract = "windows-raw-exe-authenticode-before-nsis-v1", prePackageFiles = ["raw.exe"], packageJson, packageCommand, cargoManifest, cargoConfig, repoCargoConfig, siblingCargoManifest, buildInputs = defaultBuildInputs, platform = "win" } = {}) {
20
20
  const fixtureRoot = mkdtempSync(path.join(os.tmpdir(), "right-release-test-"));
21
21
  const dir = repoCargoConfig ? path.join(fixtureRoot, "apps", "fixture") : fixtureRoot;
22
22
  mkdirSync(dir, { recursive: true });
@@ -49,7 +49,7 @@ function fixture({ signed = true, publish = false, signingContract = "windows-ra
49
49
  [platform]: {
50
50
  ...(buildInputs ? { buildInputs } : {}),
51
51
  signed,
52
- package: { cmd: "node", args: ["-e", "process.exit(0)"] },
52
+ package: packageCommand ?? { cmd: "node", args: ["-e", "process.exit(0)"] },
53
53
  ...(publish ? { publish: { cmd: "node", args: ["publish-update.mjs"] } } : {}),
54
54
  artifacts: [],
55
55
  ...(platform === "win" ? { signingContract, prePackage: { cmd: "node", args: ["-e", "process.exit(0)"] }, sign: { prePackageFiles, files: ["fixture.exe"] } } : {}),
@@ -159,6 +159,28 @@ test("accepts a tier-neutral internal build", () => {
159
159
  assert.doesNotMatch(result.stdout, /RIGHT_RELEASE_TIER=/);
160
160
  });
161
161
 
162
+ test("notarization commands default to a bounded 30-minute timeout without changing explicit overrides", () => {
163
+ const defaultResult = runRaw(fixture({
164
+ platform: "mac",
165
+ packageCommand: { cmd: "node", args: ["-e", "process.exit(0)", "--notarize"] },
166
+ }), "--platform", "mac", "--dry-run");
167
+ assert.equal(defaultResult.status, 0, defaultResult.stderr);
168
+ assert.match(defaultResult.stdout, /timeout=1800000ms/);
169
+
170
+ const explicitResult = runRaw(fixture({
171
+ platform: "mac",
172
+ packageCommand: { cmd: "node", args: ["-e", "process.exit(0)", "--notarize"], timeoutMs: 1234 },
173
+ }), "--platform", "mac", "--dry-run");
174
+ assert.equal(explicitResult.status, 0, explicitResult.stderr);
175
+ assert.match(explicitResult.stdout, /timeout=1234ms/);
176
+ });
177
+
178
+ test("release worker reuses owned process-tree termination for Windows and remote command descendants", () => {
179
+ const source = readFileSync(release, "utf8");
180
+ assert.match(source, /import \{ terminateProcessTree \} from "\.\/heavy-command\.mjs"/);
181
+ assert.match(source, /function killProcessTree\(pid\) \{\s*terminateProcessTree\(pid\);\s*\}/s);
182
+ });
183
+
162
184
  test("accepts patch and exposes it to the signed package command", () => {
163
185
  const result = run(fixture(), "--tier=patch");
164
186
  assert.equal(result.status, 0, result.stderr);
@@ -8,7 +8,9 @@ import test, { after } from "node:test";
8
8
  import {
9
9
  assertNoRightKitCargoOverrides,
10
10
  assertPublishedRightKitCargoDependencies,
11
+ cargoArguments,
11
12
  cargoExecutable,
13
+ isolatedCargoMetadataEnv,
12
14
  validateRightKitCargoContract,
13
15
  } from "./cargo-contract.mjs";
14
16
  import { assertAsrAdapterPair } from "./asr-artifact-adoption.mjs";
@@ -125,7 +127,7 @@ function readCargoManifestContract(manifestPath, label) {
125
127
  {
126
128
  cwd: path.dirname(manifestPath),
127
129
  encoding: "utf8",
128
- env: { ...process.env, CARGO_HOME: isolatedCargoHome },
130
+ env: isolatedCargoMetadataEnv(isolatedCargoHome),
129
131
  windowsHide: true,
130
132
  },
131
133
  );
@@ -461,15 +463,16 @@ test("RightKit exposes one current version manifest", () => {
461
463
  assert.deepEqual(versions.stagedNpm, {
462
464
  "@rightkit/ax": "0.2.0",
463
465
  "@rightkit/git": "0.2.0",
466
+ "@rightkit/hooks": "0.1.0",
464
467
  "@rightkit/legal": "0.3.0",
465
468
  "@rightkit/legal-ui": "0.1.1",
466
469
  "@rightkit/license": "0.1.6",
467
- "@rightkit/release": "0.2.60",
470
+ "@rightkit/release": "0.2.62",
468
471
  "@rightkit/qa": "0.2.0",
469
472
  });
470
473
  assert.deepEqual(versions.legacyNpm, {
471
474
  "@rightkit/legal-ui": ["0.1.0"],
472
- "@rightkit/release": ["0.2.22", "0.2.29", "0.2.30", "0.2.31", "0.2.41", "0.2.42", "0.2.43", "0.2.44", "0.2.45", "0.2.46", "0.2.49", "0.2.50", "0.2.51", "0.2.53", "0.2.54", "0.2.55", "0.2.56"],
475
+ "@rightkit/release": ["0.2.22", "0.2.29", "0.2.30", "0.2.31", "0.2.41", "0.2.42", "0.2.43", "0.2.44", "0.2.45", "0.2.46", "0.2.49", "0.2.50", "0.2.51", "0.2.53", "0.2.54", "0.2.55", "0.2.56", "0.2.61"],
473
476
  "@rightkit/qa": ["0.1.0"],
474
477
  });
475
478
  assert.ok(
@@ -495,11 +498,26 @@ test("RightKit exposes one current version manifest", () => {
495
498
  getCurrentCargoVersionContract();
496
499
  });
497
500
 
498
- test("Cargo metadata uses the native Windows executable", () => {
499
- assert.equal(cargoExecutable("win32"), "cargo.exe");
501
+ test("Cargo metadata uses native rustup on Windows", () => {
502
+ assert.equal(cargoExecutable("win32"), "rustup");
503
+ assert.deepEqual(cargoArguments(["metadata"], "win32"), ["run", "stable", "cargo", "metadata"]);
500
504
  assert.equal(cargoExecutable("darwin"), "cargo");
501
505
  });
502
506
 
507
+ test("Cargo metadata delegates controlled storage to managed RightKit", () => {
508
+ const ambient = {
509
+ CARGO_HOME: "/ambient/cargo",
510
+ CARGO_TARGET_DIR: "/ambient/target",
511
+ RIGHTKIT_BUILD_BROKER_SOCKET: "/managed/broker.sock",
512
+ };
513
+ assert.deepEqual(isolatedCargoMetadataEnv("/isolated/cargo", ambient), {
514
+ RIGHTKIT_BUILD_BROKER_SOCKET: "/managed/broker.sock",
515
+ });
516
+ assert.deepEqual(isolatedCargoMetadataEnv("/isolated/cargo", { CARGO_HOME: "/ambient/cargo" }), {
517
+ CARGO_HOME: "/isolated/cargo",
518
+ });
519
+ });
520
+
503
521
  test("license v2 public vector is identical at every portable consumer boundary", () => {
504
522
  const canonical = readFileSync(
505
523
  path.join(workspace, "tools/rightkit/crates/rightkit-license/test-vectors/license-v2.json"),
@@ -15,10 +15,11 @@
15
15
  "stagedNpm": {
16
16
  "@rightkit/ax": "0.2.0",
17
17
  "@rightkit/git": "0.2.0",
18
+ "@rightkit/hooks": "0.1.0",
18
19
  "@rightkit/legal": "0.3.0",
19
20
  "@rightkit/legal-ui": "0.1.1",
20
21
  "@rightkit/license": "0.1.6",
21
- "@rightkit/release": "0.2.60",
22
+ "@rightkit/release": "0.2.62",
22
23
  "@rightkit/qa": "0.2.0"
23
24
  },
24
25
  "legacyNpm": {
@@ -42,7 +43,8 @@
42
43
  "0.2.53",
43
44
  "0.2.54",
44
45
  "0.2.55",
45
- "0.2.56"
46
+ "0.2.56",
47
+ "0.2.61"
46
48
  ],
47
49
  "@rightkit/qa": [
48
50
  "0.1.0"
@@ -0,0 +1,32 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "generatedAt": "2026-08-10T04:42:36.444Z",
4
+ "workRoot": "C:\\Users\\adrds\\AppData\\Local\\Temp\\rightkit-standalone-sBI0iR",
5
+ "apps": [
6
+ {
7
+ "key": "viewright",
8
+ "remote": "https://github.com/bogusyogi/viewright.git",
9
+ "appDir": ".",
10
+ "revision": "21a4171fa8add2d8114bc5f07498b60d7e8eafe5",
11
+ "packageManager": "pnpm@11.18.0",
12
+ "clone": {
13
+ "command": "git clone --depth 1 --single-branch https://github.com/bogusyogi/viewright.git C:\\Users\\adrds\\AppData\\Local\\Temp\\rightkit-standalone-sBI0iR\\viewright",
14
+ "status": 0,
15
+ "stdout": "",
16
+ "stderr": "Cloning into 'C:\\Users\\adrds\\AppData\\Local\\Temp\\rightkit-standalone-sBI0iR\\viewright'...\nUpdating files: 91% (1902/2080)\rUpdating files: 92% (1914/2080)\rUpdating files: 93% (1935/2080)\rUpdating files: 94% (1956/2080)\rUpdating files: 95% (1976/2080)\rUpdating files: 96% (1997/2080)\rUpdating files: 97% (2018/2080)\rUpdating files: 98% (2039/2080)\rUpdating files: 99% (2060/2080)\rUpdating files: 100% (2080/2080)\rUpdating files: 100% (2080/2080), done."
17
+ },
18
+ "install": {
19
+ "command": "C:\\nvm4w\\nodejs\\node.exe C:\\nvm4w\\nodejs\\node_modules\\pnpm\\bin\\pnpm.mjs install --frozen-lockfile",
20
+ "status": 0,
21
+ "stdout": "✓ Lockfile passes supply-chain policies (verified 8h ago)\nLockfile is up to date, resolution step is skipped\nProgress: resolved 1, reused 0, downloaded 0, added 0\nPackages: +619\n++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\nProgress: resolved 619, reused 0, downloaded 0, added 0\nProgress: resolved 619, reused 60, downloaded 0, added 0\nPackages are hard linked from the content-addressable store to the virtual store.\n Content-addressable store is at: C:\\Users\\adrds\\AppData\\Local\\pnpm\\store\\v11\n Virtual store is at: node_modules/.pnpm\nProgress: resolved 619, reused 573, downloaded 7, added 7\nProgress: resolved 619, reused 574, downloaded 9, added 10\nProgress: resolved 619, reused 574, downloaded 22, added 11\nProgress: resolved 619, reused 574, downloaded 23, added 11\nProgress: resolved 619, reused 574, downloaded 26, added 14\nProgress: resolved 619, reused 574, downloaded 30, added 24\nProgress: resolved 619, reused 574, downloaded 31, added 57\nProgress: resolved 619, reused 574, downloaded 34, added 128\nProgress: resolved 619, reused 574, downloaded 38, added 135\nProgress: resolved 619, reused 574, downloaded 38, added 136\nProgress: resolved 619, reused 574, downloaded 39, added 141\nProgress: resolved 619, reused 574, downloaded 39, added 148\nProgress: resolved 619, reused 574, downloaded 39, added 182\nProgress: resolved 619, reused 574, downloaded 40, added 201\nProgress: resolved 619, reused 574, downloaded 41, added 218\nProgress: resolved 619, reused 574, downloaded 41, added 242\nProgress: resolved 619, reused 574, downloaded 42, added 312\nProgress: resolved 619, reused 574, downloaded 42, added 371\nProgress: resolved 619, reused 574, downloaded 42, added 430\nProgress: resolved 619, reused 574, downloaded 42, added 486\nProgress: resolved 619, reused 574, downloaded 43, added 543\nProgress: resolved 619, reused 574, downloaded 43, added 578\nProgress: resolved 619, reused 574, downloaded 44, added 596\nProgress: resolved 619, reused 574, downloaded 44, added 611\nProgress: resolved 619, reused 574, downloaded 44, added 612\nProgress: resolved 619, reused 574, downloaded 44, added 613\nProgress: resolved 619, reused 574, downloaded 44, added 615\nProgress: resolved 619, reused 574, downloaded 44, added 616\nProgress: resolved 619, reused 574, downloaded 44, added 617\nProgress: resolved 619, reused 574, downloaded 44, added 618\nProgress: resolved 619, reused 574, downloaded 45, added 618\nProgress: resolved 619, reused 574, downloaded 45, added 619\nProgress: resolved 619, reused 574, downloaded 45, added 619, done\n\ndependencies:\n+ @codemirror/autocomplete 6.20.3\n+ @codemirror/commands 6.10.4\n+ @codemirror/lang-html 6.4.11\n+ @codemirror/lang-javascript 6.2.5\n+ @codemirror/lang-markdown 6.5.0\n+ @codemirror/language 6.12.4\n+ @codemirror/lint 6.9.7\n+ @codemirror/search 6.7.1\n+ @codemirror/state 6.7.1\n+ @codemirror/view 6.43.6\n+ @eigenpal/docx-editor-agents @eigenpal/docx-editor-agents@file:vendor/docx-editor/packages/agents(react@19.2.7)\n+ @eigenpal/docx-editor-core @eigenpal/docx-editor-core@file:vendor/docx-editor/packages/core(prosemirror-commands@1.7.1)(prosemirror-dropcursor@1.8.2)(prosemirror-history@1.5.0)(prosemirror-keymap@1.2.3)(prosemirror-model@1.25.10)(prosemirror-state@1.4.4)(prosemirror-tables@1.8.5)(prosemirror-transform@1.12.0)(prosemirror-view@1.42.0)\n+ @eigenpal/docx-editor-i18n @eigenpal/docx-editor-i18n@file:vendor/docx-editor/packages/i18n\n+ @eigenpal/docx-editor-react @eigenpal/docx-editor-react@file:vendor/docx-editor/packages/react(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(prosemirror-commands@1.7.1)(prosemirror-dropcursor@1.8.2)(prosemirror-history@1.5.0)(prosemirror-keymap@1.2.3)(prosemirror-model@1.25.10)(prosemirror-state@1.4.4)(prosemirror-tables@1.8.5)(prosemirror-transform@1.12.0)(prosemirror-view@1.42.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)\n+ @lezer/highlight 1.2.3\n+ @mdx-js/mdx 3.1.1\n+ @phosphor-icons/react 2.1.10\n+ @radix-ui/react-select 2.3.2\n+ @rightkit/legal-ui 0.1.0\n+ @rightkit/license 0.1.6\n+ @rightkit/logs 0.1.3\n+ @rightkit/tauri 0.1.0\n+ @rightkit/updates 0.2.3\n+ @tauri-apps/api 2.11.1\n+ @tauri-apps/plugin-http 2.5.9\n+ @tauri-apps/plugin-process 2.3.1\n+ @tauri-apps/plugin-updater 2.10.1\n+ clsx 2.1.1\n+ docxtemplater 3.69.0\n+ dompurify 3.4.13\n+ fabric 7.4.0\n+ github-slugger 2.0.0\n+ jszip 3.10.1\n+ katex 0.17.0\n+ mermaid 11.16.1\n+ pdfjs-dist 6.2.108\n+ pizzip 3.2.0\n+ prosemirror-commands 1.7.1\n+ prosemirror-dropcursor 1.8.2\n+ prosemirror-history 1.5.0\n+ prosemirror-keymap 1.2.3\n+ prosemirror-model 1.25.10\n+ prosemirror-state 1.4.4\n+ prosemirror-tables 1.8.5\n+ prosemirror-transform 1.12.0\n+ prosemirror-view 1.42.0\n+ react 19.2.7\n+ react-dom 19.2.7\n+ react-image-crop 11.1.2\n+ rehype-stringify 10.0.1\n+ remark-frontmatter 5.0.0\n+ remark-gfm 4.0.1\n+ remark-math 6.0.0\n+ remark-parse 11.0.0\n+ remark-rehype 11.1.2\n+ remark-smartypants 3.0.2\n+ shiki 4.3.1\n+ sonner 2.0.7\n+ sucrase 3.35.1\n+ unified 11.0.5\n+ xml-js 1.6.11\n+ yaml 2.9.0\n\ndevDependencies:\n+ @biomejs/biome 2.5.3\n+ @rightkit/legal 0.3.0\n+ @rightkit/release 0.2.50\n+ @tailwindcss/vite 4.3.2\n+ @tauri-apps/cli 2.11.4\n+ @testing-library/dom 10.4.1\n+ @testing-library/jest-dom 6.9.1\n+ @testing-library/react 16.3.2\n+ @types/mdast 4.0.4\n+ @types/react 19.2.17\n+ @types/react-dom 19.2.3\n+ @types/ws 8.18.1\n+ @vitejs/plugin-react 6.0.3\n+ happy-dom 20.10.6\n+ jscpd 5.0.12\n+ jsdom 29.1.1\n+ knip 6.25.0\n+ tailwindcss 4.3.2\n+ typescript 6.0.3\n+ vite 8.1.4\n+ vitest 4.1.10\n+ ws 8.21.0\n\nDone in 40.8s using pnpm v11.18.0",
22
+ "stderr": ""
23
+ },
24
+ "doctor": {
25
+ "command": "C:\\nvm4w\\nodejs\\node.exe C:\\nvm4w\\nodejs\\node_modules\\pnpm\\bin\\pnpm.mjs release:doctor",
26
+ "status": 0,
27
+ "stdout": "right-release 0.2.50\nconfig: C:\\Users\\adrds\\AppData\\Local\\Temp\\rightkit-standalone-sBI0iR\\viewright\\right-release.config.mjs\napp: viewright\nplatform: win\ntier: <required for release/publish>\npackageManager: pnpm\nworkdir: C:\\Users\\adrds\\AppData\\Local\\Temp\\rightkit-standalone-sBI0iR\\viewright\nhardeningscan: C:\\Users\\adrds\\AppData\\Local\\Temp\\rightkit-standalone-sBI0iR\\viewright\\node_modules\\.pnpm\\@rightkit+release@0.2.50\\node_modules\\@rightkit\\release\\hardeningscan.mjs\nlegal: C:\\Users\\adrds\\AppData\\Local\\Temp\\rightkit-standalone-sBI0iR\\viewright\\legal\\legal-manifest.json\nlegalAcceptance: viewright-2026-07-17-v3\nlegalManifestSha256: 035ea7cbd040ef001dbdc1385f114c8bb126178fea9a9f705016a726f96a7830\nsign: src-tauri/target/release/bundle/nsis/ViewRight_0.1.60_x64-setup.exe\npreflight:\n [ok ] target-bridge: src-tauri/target is ready for the shared cache bridge\n [ok ] version: 0.1.60 is free to build\n [ok ] windows-sdk: makeappx.exe from SDK 10.0.26100.0\n [ok ] signtool: signtool.exe from SDK 10.0.26100.0\n [ok ] sccache: sccache 0.17.0\n [ok ] disk: 628.7GB free",
28
+ "stderr": "$ right-release doctor"
29
+ }
30
+ }
31
+ ]
32
+ }
@@ -1,32 +1,29 @@
1
1
  #!/usr/bin/env node
2
- import { createHash } from "node:crypto";
3
- import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
4
3
  import path from "node:path";
5
4
  import { spawnSync } from "node:child_process";
6
- import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import { pathToFileURL } from "node:url";
6
+ import { prepareGitHubRelease, publishGitHubRelease, repositoryFromRemote } from "./github-release.mjs";
7
7
  import { assertPrimaryReleaseCheckout } from "./release-invocation.mjs";
8
- import { runUploadStateMachine, verifySealedRelease } from "./release-state.mjs";
8
+ import { verifySealedRelease } from "./release-state.mjs";
9
9
  import { assertCleanSource } from "./source-gate.mjs";
10
10
 
11
- const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
12
- const PUBLISH_UPDATE = path.join(TOOL_ROOT, "publish-update.mjs");
13
- const HARDENING = path.join(TOOL_ROOT, "hardeningscan.mjs");
14
- const SIGN_WINDOWS = path.join(TOOL_ROOT, "sign-windows.mjs");
15
- const UPLOAD = path.join(TOOL_ROOT, "upload-large.mjs");
16
- const PUBLIC_BASE = (process.env.RIGHTAPPS_PUBLIC_DOWNLOAD_BASE || "https://pub-6c73208d46c245a9b4881d5e02f6b618.r2.dev").replace(/\/$/, "");
17
- const API_BASE = (process.env.RIGHTAPPS_API_URL || "https://api.spoares.com").replace(/\/$/, "");
18
11
  const args = process.argv.slice(2);
19
12
  let platform = process.platform === "win32" ? "win" : process.platform === "darwin" ? "mac" : process.platform;
20
13
  let releaseId = "";
21
14
  let tier = "";
15
+ let repo = "";
16
+ let configName = "right-release.config.mjs";
22
17
  let dryRun = false;
23
18
 
24
- for (let i = 0; i < args.length; i += 1) {
25
- const arg = args[i];
26
- if (arg === "--platform") platform = args[++i];
27
- else if (arg === "--release") releaseId = args[++i];
28
- else if (arg === "--tier") tier = args[++i];
19
+ for (let index = 0; index < args.length; index += 1) {
20
+ const arg = args[index];
21
+ if (arg === "--platform") platform = args[++index];
22
+ else if (arg === "--release") releaseId = args[++index];
23
+ else if (arg === "--tier") tier = args[++index];
29
24
  else if (arg.startsWith("--tier=")) tier = arg.slice("--tier=".length);
25
+ else if (arg === "--repo") repo = args[++index];
26
+ else if (arg === "--config") configName = args[++index];
30
27
  else if (arg === "--dry-run") dryRun = true;
31
28
  else if (arg === "-h" || arg === "--help") usage(0);
32
29
  else fail(`unknown argument: ${arg}`);
@@ -43,183 +40,52 @@ assertCleanSource({
43
40
  commandId: "right-release upload",
44
41
  });
45
42
  const platformDir = platform === "win" ? "windows" : "mac";
46
- const sealedDir = path.join(repoRoot, ".right-release", "sealed", releaseId, platformDir);
47
- const sealed = verifySealedRelease(sealedDir);
43
+ const sealed = verifySealedRelease(path.join(repoRoot, ".right-release", "sealed", releaseId, platformDir));
48
44
  if (sealed.manifest.commit !== candidateCommit) fail("sealed release was built from a different commit than the current checkout");
45
+
49
46
  const stateRoot = path.join(repoRoot, ".right-release", "state", releaseId, platformDir);
50
- const backupRoot = path.join(stateRoot, "rollback", tier);
51
47
  const verifiedMarker = path.join(stateRoot, `verified-${tier}.json`);
52
48
  if (existsSync(verifiedMarker)) {
53
49
  console.log(`right-release upload: already verified ${releaseId} tier=${tier}`);
54
50
  process.exit(0);
55
51
  }
56
- if (!dryRun && !process.env.CLOUDFLARE_API_TOKEN) fail("CLOUDFLARE_API_TOKEN is required before R2 mutation");
57
52
 
58
- await runUploadStateMachine({
59
- root: repoRoot,
53
+ const configPath = path.resolve(configName);
54
+ if (existsSync(configPath)) {
55
+ const releaseConfig = (await import(`${pathToFileURL(configPath).href}?upload=${Date.now()}`)).default;
56
+ if (releaseConfig?.distribution?.provider && releaseConfig.distribution.provider !== "github-releases") {
57
+ fail("release config distribution.provider must be github-releases");
58
+ }
59
+ repo ||= releaseConfig?.distribution?.repository ?? "";
60
+ }
61
+ repo ||= repositoryFromRemote(repoRoot);
62
+ const plan = prepareGitHubRelease({ repoRoot, releaseId, platform, repo });
63
+ const result = publishGitHubRelease(plan, { repo, dryRun });
64
+ if (!dryRun) writeJson(verifiedMarker, {
65
+ schema: 1,
60
66
  releaseId,
61
67
  platform,
62
68
  tier,
63
- ops: {
64
- verifyAuthenticode: async (release) => verifyPlatformTrust(release),
65
- harden: async (release) => {
66
- const files = release.manifest.files.filter((file) => file.role !== "updater-signature").map((file) => path.join(release.sealedDir, file.name));
67
- runChecked(process.execPath, [HARDENING, ...files], repoRoot);
68
- },
69
- backup: async (release, routes) => backupStableObjects(release, routes),
70
- upload: async (release) => publishSealed(release),
71
- register: async () => {},
72
- verifyRemote: async (release, routes) => verifyUploaded(release, routes),
73
- restore: async (_release, routes) => restoreStableObjects(routes),
74
- discardBackup: async () => rmSync(backupRoot, { recursive: true, force: true }),
75
- },
69
+ provider: "github-releases",
70
+ repository: repo,
71
+ tag: result.tag,
72
+ verifiedAt: new Date().toISOString(),
76
73
  });
77
- if (!dryRun) writeJson(verifiedMarker, { schema: 1, releaseId, platform, tier, verifiedAt: new Date().toISOString() });
78
- console.log(`right-release upload: ${dryRun ? "dry-run " : ""}verified ${releaseId} tier=${tier}`);
79
-
80
- function verifyPlatformTrust(release) {
81
- if (dryRun) return;
82
- const artifacts = release.manifest.files.filter((file) => file.role !== "updater-signature");
83
- if (platform === "win") {
84
- for (const artifact of artifacts) {
85
- const file = path.join(release.sealedDir, artifact.name);
86
- runChecked(process.execPath, [SIGN_WINDOWS, "--verify-only", file], repoRoot);
87
- }
88
- } else {
89
- for (const artifact of artifacts.filter((file) => /\.dmg$/i.test(file.name))) {
90
- const file = path.join(release.sealedDir, artifact.name);
91
- runChecked("codesign", ["--verify", "--strict", "--verbose=2", file], repoRoot);
92
- runChecked("xcrun", ["stapler", "validate", file], repoRoot);
93
- }
94
- }
95
- }
96
-
97
- function backupStableObjects(release, routes) {
98
- if (dryRun) return;
99
- if (existsSync(path.join(backupRoot, "backup-manifest.json"))) restoreStableObjects(routes);
100
- mkdirSync(backupRoot, { recursive: true });
101
- const entries = [];
102
- for (const route of uniqueRoutes(routes)) {
103
- const bucket = bucketName(route.bucket);
104
- const file = path.join(backupRoot, `${createHash("sha256").update(`${bucket}/${route.key}`).digest("hex").slice(0, 12)}.bin`);
105
- const result = wrangler(["r2", "object", "get", `${bucket}/${route.key}`, "--file", file, "--remote"]);
106
- if (result.status === 0) entries.push({ bucket: route.bucket, key: route.key, existed: true, file: path.basename(file), sha256: hashFile(file) });
107
- else if (/not found|does not exist|404/i.test(`${result.stdout}\n${result.stderr}`)) entries.push({ bucket: route.bucket, key: route.key, existed: false, file: null, sha256: null });
108
- else throw new Error(`failed to back up R2 object ${bucket}/${route.key}: ${result.stderr || result.stdout}`);
109
- }
110
- writeJson(path.join(backupRoot, "backup-manifest.json"), { schema: 1, releaseId: release.manifest.releaseId, entries });
111
- }
112
-
113
- function publishSealed(release) {
114
- if (dryRun) return;
115
- const generated = path.join(stateRoot, `sealed-upload-${tier}.config.mjs`);
116
- const config = sealedUploadConfig(release);
117
- writeFileSync(generated, `export default ${JSON.stringify(config, null, 2)};\n`);
118
- runChecked(process.execPath, [PUBLISH_UPDATE, "--config", generated, "--platform", platform], repoRoot, { ...process.env, RIGHT_RELEASE_TIER: tier });
119
- }
120
-
121
- function sealedUploadConfig(release) {
122
- const fileByName = new Map(release.manifest.files.map((file) => [file.name, path.join(release.sealedDir, file.name)]));
123
- const patchRoutes = release.manifest.routes.patch ?? [];
124
- const updateRoutes = release.manifest.routes.update ?? [];
125
- const installers = patchRoutes.filter((route) => route.role === "installer").map((route) => ({ file: fileByName.get(route.name), key: route.key }));
126
- const updaterRoutes = updateRoutes.map((route) => {
127
- const patch = patchRoutes.find((candidate) => candidate.name === route.name && candidate.platform === route.platform);
128
- return { file: fileByName.get(route.name), signature: fileByName.get(route.signature), platform: route.platform, key: route.key, patchKey: patch?.key };
129
- });
130
- return {
131
- schema: 1,
132
- app: release.manifest.app,
133
- version: release.manifest.version,
134
- channel: "stable",
135
- targets: { [platform]: { installer: { artifacts: installers }, updater: { artifacts: updaterRoutes } } },
136
- };
137
- }
138
-
139
- async function verifyUploaded(release, routes) {
140
- if (dryRun) return;
141
- const files = new Map(release.manifest.files.map((file) => [file.name, file]));
142
- for (const route of uniqueRoutes(routes)) {
143
- const expected = files.get(route.name);
144
- if (!expected) throw new Error(`route references unsealed file: ${route.name}`);
145
- const verifyFile = path.join(stateRoot, `remote-${createHash("sha256").update(route.key).digest("hex").slice(0, 12)}.bin`);
146
- if (route.bucket === "public") {
147
- const response = await fetch(`${PUBLIC_BASE}/${route.key}`, { cache: "no-store" });
148
- if (!response.ok) throw new Error(`public artifact verification failed: ${response.status} ${route.key}`);
149
- writeFileSync(verifyFile, Buffer.from(await response.arrayBuffer()));
150
- } else {
151
- const result = wrangler(["r2", "object", "get", `${bucketName(route.bucket)}/${route.key}`, "--file", verifyFile, "--remote"]);
152
- if (result.status !== 0) throw new Error(`private artifact verification failed: ${route.key}`);
153
- }
154
- if (statSync(verifyFile).size !== expected.sizeBytes || hashFile(verifyFile) !== expected.sha256) throw new Error(`remote hash mismatch: ${route.key}`);
155
- rmSync(verifyFile, { force: true });
156
- }
157
- if (!routes.some((route) => route.role === "updater")) return;
158
- const apiPlatform = platform === "win" ? "windows-x86_64" : "darwin-aarch64";
159
- const response = await fetch(`${API_BASE}/v1/apps/${release.manifest.app}/releases/latest.json?platform=${apiPlatform}`, { cache: "no-store" });
160
- if (!response.ok) throw new Error(`updater API verification failed: ${response.status}`);
161
- const body = await response.json();
162
- if (body.version !== release.manifest.version) throw new Error(`updater API version mismatch: expected ${release.manifest.version}, got ${body.version}`);
163
- const expectedRoutes = new Set(routes.map((route) => route.key));
164
- const urls = Object.values(body.platforms ?? {}).map((entry) => entry.url).filter(Boolean);
165
- if (!urls.some((url) => [...expectedRoutes].some((key) => url.endsWith(key)))) throw new Error("updater API returned the wrong artifact URL");
166
- }
167
-
168
- function restoreStableObjects(routes) {
169
- const manifestPath = path.join(backupRoot, "backup-manifest.json");
170
- if (!existsSync(manifestPath)) return;
171
- const backup = JSON.parse(readFileSync(manifestPath, "utf8"));
172
- for (const entry of backup.entries) {
173
- if (entry.existed) {
174
- const file = path.join(backupRoot, entry.file);
175
- if (hashFile(file) !== entry.sha256) throw new Error(`rollback copy hash mismatch: ${entry.key}`);
176
- runChecked(process.execPath, [UPLOAD, file, entry.key, entry.bucket], repoRoot);
177
- } else {
178
- const result = wrangler(["r2", "object", "delete", `${bucketName(entry.bucket)}/${entry.key}`, "--remote"]);
179
- if (result.status !== 0 && !/not found|404/i.test(`${result.stdout}\n${result.stderr}`)) throw new Error(`rollback delete failed: ${entry.key}`);
180
- }
181
- }
182
- rmSync(backupRoot, { recursive: true, force: true });
183
- }
184
-
185
- function uniqueRoutes(routes) {
186
- return [...new Map(routes.map((route) => [`${route.bucket}/${route.key}`, route])).values()];
187
- }
188
-
189
- function bucketName(bucket) {
190
- return bucket === "public" ? "rightapps-downloads" : "rightapps-updates";
191
- }
192
-
193
- function wrangler(runArgs) {
194
- return spawnSync("pnpm", ["dlx", "wrangler@4", ...runArgs], { cwd: repoRoot, env: process.env, encoding: "utf8", windowsHide: true, shell: process.platform === "win32" });
195
- }
196
-
197
- function hashFile(file) {
198
- return createHash("sha256").update(readFileSync(file)).digest("hex");
199
- }
74
+ console.log(`right-release upload: ${dryRun ? "dry-run " : ""}${result.status} ${releaseId} tier=${tier} repo=${repo}`);
200
75
 
201
76
  function git(cwd, runArgs) {
202
- return commandOutput("git", runArgs, cwd);
203
- }
204
-
205
- function commandOutput(cmd, runArgs, cwd = process.cwd()) {
206
- const result = spawnSync(cmd, runArgs, { cwd, encoding: "utf8", windowsHide: true });
207
- if (result.status !== 0) fail(`${cmd} ${runArgs.join(" ")} failed: ${result.stderr}`);
77
+ const result = spawnSync("git", runArgs, { cwd, encoding: "utf8", windowsHide: true });
78
+ if (result.status !== 0) fail(`git ${runArgs.join(" ")} failed: ${result.stderr}`);
208
79
  return result.stdout.trim();
209
80
  }
210
81
 
211
- function runChecked(cmd, runArgs, cwd, env = process.env) {
212
- const result = spawnSync(cmd, runArgs, { cwd, env, stdio: "inherit", windowsHide: true, shell: false });
213
- if (result.status !== 0) throw new Error(`${cmd} exited ${result.status}`);
214
- }
215
-
216
82
  function writeJson(file, value) {
217
83
  mkdirSync(path.dirname(file), { recursive: true });
218
84
  writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
219
85
  }
220
86
 
221
87
  function usage(code) {
222
- console.log("usage: right-release upload --release <sealed-id> --platform win|mac --tier patch|update [--dry-run]");
88
+ console.log("usage: right-release upload --release <sealed-id> --platform win|mac --tier patch|update [--config right-release.config.mjs] [--repo owner/repo] [--dry-run]");
223
89
  process.exit(code);
224
90
  }
225
91