@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.
- package/README.md +35 -10
- package/admission-files.mjs +1 -1
- package/app-check.mjs +94 -20
- package/assemble-app-command.mjs +151 -0
- package/assembled-app.mjs +445 -0
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +6 -0
- package/bin/rightkit-gate.mjs +957 -83
- package/build-launch.mjs +88 -0
- package/build-ownership.mjs +1231 -0
- package/build-release.mjs +2 -1
- package/cargo-contract.mjs +3 -1
- package/cargo-target-shim.mjs +166 -0
- package/class-a-launch-guard.mjs +126 -0
- package/cli/right-release.mjs +3 -1
- package/codesign-shim.mjs +11 -1
- package/deterministic-scans.mjs +807 -56
- package/development-signing.mjs +4 -1
- package/development.mjs +110 -20
- package/door-policy.mjs +61 -6
- package/door.mjs +23 -0
- package/gate-executor.mjs +14 -2
- package/gate-stages.mjs +15 -5
- package/heavy-command.mjs +3 -2
- package/install-app.mjs +11 -2
- package/install-assembled-app.mjs +423 -0
- package/ios-dev.mjs +70 -3
- package/linux-artifacts.mjs +93 -0
- package/local-publish.mjs +161 -26
- package/managed-cargo.mjs +47 -5
- package/managed-root-guard.mjs +34 -0
- package/native-helpers.mjs +355 -0
- package/package-recipe-targets.mjs +52 -7
- package/package.json +5 -3
- package/pipeline-receipts.mjs +302 -302
- package/project-type.mjs +2 -2
- package/provenance-verify.mjs +115 -0
- package/publish-cargo.mjs +8 -6
- package/qa-admission-policy.json +10 -1
- package/recipe-build.mjs +73 -5
- package/release-config.mjs +1 -1
- package/release.mjs +15 -5
- package/rightkit-versions.json +13 -7
- package/snapshot-check.mjs +306 -0
- package/snapshot-git.mjs +166 -0
- package/snapshot-request.mjs +161 -0
- package/snapshot-root-config.mjs +76 -0
- package/snapshot.mjs +856 -0
- package/stage-commands.mjs +60 -0
- package/stage-sidecar.mjs +57 -7
- package/test-command.mjs +17 -4
- package/unit-gate.mjs +259 -0
- package/web-build.mjs +38 -6
package/development-signing.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash, X509Certificate } from "node:crypto";
|
|
2
|
+
import { nativeHelperEntitlementMap, nativeHelpersFromConfig } from "./native-helpers.mjs";
|
|
2
3
|
import { closeSync, copyFileSync, existsSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
4
|
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
@@ -192,7 +193,9 @@ export function readDevSigningSettings(descriptor = {}, config = {}, { root, exi
|
|
|
192
193
|
};
|
|
193
194
|
const signMap = typeof sign.entitlements === "object" && sign.entitlements !== null && !Array.isArray(sign.entitlements) ? sign.entitlements : undefined;
|
|
194
195
|
const entitlements = [...compileBundlePathMap(descriptor.entitlements, "install.entitlements", { reject: invalidConfig, value: plistPath }),
|
|
195
|
-
...compileBundlePathMap(signMap, "sign.entitlements", { reject: invalidConfig, value: plistPath })
|
|
196
|
+
...compileBundlePathMap(signMap, "sign.entitlements", { reject: invalidConfig, value: plistPath }),
|
|
197
|
+
// Native helpers embedded in the dev .app carry their own entitlements, exactly as the release lane signs them.
|
|
198
|
+
...compileBundlePathMap((() => { const h = nativeHelpersFromConfig(config); return h ? nativeHelperEntitlementMap(h) : undefined; })(), "macPackage.nativeHelpers", { reject: invalidConfig, value: plistPath })];
|
|
196
199
|
if (descriptor.timestamp !== undefined && typeof descriptor.timestamp !== "boolean") invalidConfig("install.timestamp must be true or false");
|
|
197
200
|
if (descriptor.assertTeamSignedNested !== undefined && typeof descriptor.assertTeamSignedNested !== "boolean") invalidConfig("install.assertTeamSignedNested must be true or false");
|
|
198
201
|
const rawAssertions = descriptor.assert ?? [];
|
package/development.mjs
CHANGED
|
@@ -2,12 +2,13 @@ import { discoverBrokerSocket, resolveManagedRoots } from './managed-roots.mjs';
|
|
|
2
2
|
import { spawnSync } from "./background-process.mjs";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, lstatSync, readdirSync, realpathSync, renameSync, rmSync, statSync } from "node:fs";
|
|
5
|
+
import { buildLaunch } from "./build-launch.mjs";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
7
8
|
import { defaultReleaseConfigFile, importReleaseConfig } from "./release-config.mjs";
|
|
8
9
|
import { assertPrimaryReleaseCheckout } from "./release-invocation.mjs";
|
|
9
10
|
import { assertReleaseExecutionAllowed } from "./execution-policy.mjs";
|
|
10
|
-
import { acquireManagedRunLease, managedRuntimeClient, managedRuntimeCommand, runCargoSync } from "./managed-cargo.mjs";
|
|
11
|
+
import { acquireManagedRunLease, directCargoEnv, directPlatform, managedRuntimeClient, managedRuntimeCommand, runCargoSync } from "./managed-cargo.mjs";
|
|
11
12
|
import { BUNDLE_MARKERS, bundleMarkerBuffer, readTauriBundleMarkers } from "./tauri-bundle-marker.mjs";
|
|
12
13
|
import { installPortableDevelopment } from "./portable-development.mjs";
|
|
13
14
|
import { previousPathOf, stagingPathOf, swapInPlace } from "./in-place-install.mjs";
|
|
@@ -15,7 +16,10 @@ import { stageDevelopmentBinaries, validateStageEntries } from "./stage-binary.m
|
|
|
15
16
|
import { assertStableDesignatedRequirement, readBundleExecutable, requestedCiAdhocSigning, signMacosDevelopmentBundle } from "./development-signing.mjs";
|
|
16
17
|
import { macosCodesignEnvironment } from "./codesign-environment.mjs";
|
|
17
18
|
import { installTauriDevId, resolveTauriDevIdDescriptor, TAURI_DEVID_KIND } from "./install-tauri-devid.mjs";
|
|
18
|
-
import {
|
|
19
|
+
import { ASSEMBLED_APP_KIND, installAssembledMac, installAssembledWindows, resolveAssembledInstall } from "./install-assembled-app.mjs";
|
|
20
|
+
import { assertIosDevelopmentSession, installIosDevelopment, iosUiTestCommands, prepareIosDevelopment, prepareIosUiTests, validateIosDevelopment } from "./ios-dev.mjs";
|
|
21
|
+
import { packLinuxArtifacts, validateLinuxArchive } from "./linux-artifacts.mjs";
|
|
22
|
+
import { buildAndEmbedNativeHelpers, nativeHelpersFromConfig } from "./native-helpers.mjs";
|
|
19
23
|
import { androidDevelopmentNdkCommand, androidDevelopmentSerial, installAndroidDevelopment, validateAndroidDevelopment } from "./android-dev.mjs";
|
|
20
24
|
|
|
21
25
|
// One standard Cargo profile for every app (templates/cargo-profiles.toml): Cargo's built-in `dev`.
|
|
@@ -30,9 +34,9 @@ export function cargoProfileDirectory(profile) {
|
|
|
30
34
|
}
|
|
31
35
|
const RELEASE_ONLY_APPLE_ENV = ["APPLE_ID", "APPLE_PASSWORD", "APPLE_API_KEY", "APPLE_API_ISSUER", "APPLE_API_KEY_PATH", "APPLE_CERTIFICATE", "APPLE_CERTIFICATE_PASSWORD"];
|
|
32
36
|
|
|
33
|
-
export function developmentEnvironment(env = process.env) {
|
|
37
|
+
export function developmentEnvironment(env = process.env, platform = process.platform) {
|
|
34
38
|
const result = {
|
|
35
|
-
...env,
|
|
39
|
+
...(directPlatform(platform) ? directCargoEnv(env, platform) : env),
|
|
36
40
|
RIGHTKIT_BUILD_MODE: "dev",
|
|
37
41
|
RIGHTKIT_CARGO_PROFILE: DEVELOPMENT_PROFILE,
|
|
38
42
|
TAURI_SIGNING_PRIVATE_KEY: "",
|
|
@@ -79,22 +83,36 @@ export function validateDevelopmentConfig(config, platform, { root = process.cwd
|
|
|
79
83
|
if (platform !== "mac") throw new Error(`the ${TAURI_DEVID_KIND} development installer is for --platform mac`);
|
|
80
84
|
resolveTauriDevIdDescriptor(target.install, { appRoot: root });
|
|
81
85
|
if (!target.install.targetTriple && !target.install.bundleDirectory) throw new Error(`development.targets.${platform}.install requires targetTriple (or bundleDirectory) to find the built .app`);
|
|
86
|
+
} else if (target.install?.kind === ASSEMBLED_APP_KIND) {
|
|
87
|
+
if (platform !== "mac" && platform !== "win") throw new Error(`the ${ASSEMBLED_APP_KIND} development installer is for --platform mac or win`);
|
|
88
|
+
// The assembly (assembly.<platform>) and the install keys are validated together, before any build.
|
|
89
|
+
resolveAssembledInstall({ config, platform });
|
|
82
90
|
} else if (target.install?.kind === "adb") {
|
|
83
91
|
if (platform !== "android") throw new Error("the adb development installer is for --platform android");
|
|
84
92
|
} else if (target.install) throw new Error(`unsupported development installer kind: ${target.install.kind}`);
|
|
93
|
+
// Linux is build-only: its artifacts are the staged sidecars bound in the stage receipt; nothing is installed on a Linux host.
|
|
94
|
+
if (platform === "linux" && target.install) throw new Error("development.targets.linux is build-only: it takes no install (the staged artifacts are the result)");
|
|
95
|
+
if (target.archive !== undefined && platform !== "linux") throw new Error(`development.targets.${platform}.archive: only the linux target packs an archive`);
|
|
96
|
+
if (platform === "linux") validateLinuxArchive(target.archive);
|
|
85
97
|
validateStageEntries(target.stage, `development.targets.${platform}.stage`);
|
|
86
98
|
if (platform === "android") return { ...target, ...validateAndroidDevelopment(target, { root }) };
|
|
87
99
|
return target;
|
|
88
100
|
}
|
|
89
101
|
|
|
90
|
-
export function runDevelopment({ root, platform, config, buildOnly = false, dryRun = false, env = process.env, run = spawnSync,
|
|
91
|
-
codesignEnvironment = macosCodesignEnvironment, serial }) {
|
|
92
|
-
|
|
102
|
+
export function runDevelopment({ root, platform, config, buildOnly = false, dryRun = false, env = process.env, run = spawnSync, door = buildLaunch,
|
|
103
|
+
codesignEnvironment = macosCodesignEnvironment, serial, uiTests = false }) {
|
|
104
|
+
// UI test journeys (`right-release test --platform ios`): the declared XCUITest journeys on the dev device, under the same guards.
|
|
105
|
+
if (uiTests && platform !== "ios") throw new Error("UI test journeys run for --platform ios only");
|
|
106
|
+
if (!new Set(["win", "mac", "linux", "android", "ios"]).has(platform)) throw new Error("--platform must be win, mac, linux, android or ios");
|
|
93
107
|
// iOS: --dry-run validates the config and prints the plan on any host; everything else needs macOS with Xcode.
|
|
94
108
|
if (!(platform === "ios" && dryRun)) assertNativeDevelopmentHost(platform);
|
|
95
109
|
const target = validateDevelopmentConfig(config, platform, { root });
|
|
96
110
|
const repoRoot = git(root, ["rev-parse", "--show-toplevel"]);
|
|
97
111
|
assertPrimaryReleaseCheckout(repoRoot, env);
|
|
112
|
+
if (dryRun && uiTests) {
|
|
113
|
+
if (!target.uiTests) throw new Error("development.targets.ios.uiTests is not declared: list the journeys in uiTests.onlyTesting");
|
|
114
|
+
return { built: false, installed: false, plan: { tests: iosUiTestCommands(target, { root, storage: { derivedData: "<rustright storage xcode-args -derivedDataPath>" }, destinationId: "<device or simulator id>", resultBundle: "<fresh .xcresult per run>" }) } };
|
|
115
|
+
}
|
|
98
116
|
if (dryRun) return { built: false, installed: false, plan: { ...(target.ndk ? { ndk: androidDevelopmentNdkCommand(target.ndk) } : {}), build: target.build, install: buildOnly ? null : target.install ?? null, profile: DEVELOPMENT_PROFILE } };
|
|
99
117
|
assertReleaseExecutionAllowed({ cwd: repoRoot, environment: env, command: `right-release dev --platform ${platform}` });
|
|
100
118
|
// Refuse an invalid or off-CI ad hoc opt-in before the (long) build, not after it.
|
|
@@ -103,7 +121,8 @@ export function runDevelopment({ root, platform, config, buildOnly = false, dryR
|
|
|
103
121
|
let iosPrepared = null;
|
|
104
122
|
if (platform === "ios") {
|
|
105
123
|
assertIosDevelopmentSession(target, { env, run });
|
|
106
|
-
|
|
124
|
+
if (uiTests && !target.uiTests) throw new Error("development.targets.ios.uiTests is not declared: list the journeys in uiTests.onlyTesting");
|
|
125
|
+
iosPrepared = uiTests ? prepareIosUiTests(target, { root, env, run, serial }) : prepareIosDevelopment(target, { root, env, run, serial });
|
|
107
126
|
}
|
|
108
127
|
// Android: the device serial is required, so a missing one is refused before the (long) build too.
|
|
109
128
|
if (!buildOnly && platform === "android" && target.install) androidDevelopmentSerial({ descriptor: target.install, serial, env });
|
|
@@ -114,34 +133,84 @@ export function runDevelopment({ root, platform, config, buildOnly = false, dryR
|
|
|
114
133
|
try {
|
|
115
134
|
const startedMs = Date.now();
|
|
116
135
|
// Android: cargo-ndk (Cargo dev profile) fills the generated jniLibs, then the Gradle debug build packages it.
|
|
117
|
-
if (target.ndk) { rmSync(target.ndk.jniLibs, { recursive: true, force: true }); execute(androidDevelopmentNdkCommand(target.ndk), { root, env: childEnv, dryRun, run, label: "native build" }); }
|
|
118
|
-
|
|
136
|
+
if (target.ndk) { rmSync(target.ndk.jniLibs, { recursive: true, force: true }); execute(androidDevelopmentNdkCommand(target.ndk), { root, env: childEnv, dryRun, run, door, label: "native build" }); }
|
|
137
|
+
if (uiTests) {
|
|
138
|
+
for (const command of iosPrepared.tests) execute(command, { root, env: childEnv, dryRun, run, door, label: "ui tests" });
|
|
139
|
+
console.log(`right-release test: ios journeys passed (${target.uiTests.onlyTesting.join(", ")}); result bundle ${iosPrepared.resultBundle}`);
|
|
140
|
+
return { built: true, installed: false, tested: true, resultBundle: iosPrepared.resultBundle };
|
|
141
|
+
}
|
|
142
|
+
for (const command of iosPrepared ? iosPrepared.build : target.build) execute(command, { root, env: childEnv, dryRun, run, door, label: "build" });
|
|
119
143
|
// Dev and QA binaries run from one stable staged path, copied in place (no backup of the old copy), never from the
|
|
120
144
|
// broker's per-generation target folder or a hashed dev-targets folder.
|
|
121
145
|
const staged = target.stage?.length ? stageDevelopmentBinaries(target.stage, { root, platform: platform === "win" ? "win32" : process.platform }) : [];
|
|
122
146
|
for (const file of staged) console.log(`right-release dev: staged ${file}`);
|
|
147
|
+
// Linux: the declared files become one tarball per architecture plus the digest manifest a guest link pins (rightkit#1230).
|
|
148
|
+
// Only from a clean checkout: an artifact others install must name the exact commit it was built from.
|
|
149
|
+
if (platform === "linux" && target.archive && !dryRun) {
|
|
150
|
+
if (git(repoRoot, ["status", "--porcelain", "--untracked-files=normal"])) throw new Error("linux artifacts are packed only from a clean checkout: commit first, so the manifest names the exact source");
|
|
151
|
+
let version;
|
|
152
|
+
try { version = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")).version; } catch { version = undefined; }
|
|
153
|
+
packLinuxArtifacts({ root, archive: validateLinuxArchive(target.archive), app: target.archive.name, version, commit: git(repoRoot, ["rev-parse", "HEAD"]), run });
|
|
154
|
+
}
|
|
123
155
|
if (buildOnly || !target.install) return { built: true, installed: false, ...(staged.length ? { staged } : {}) };
|
|
124
156
|
if (iosPrepared) return { built: true, installed: true, ios: installIosDevelopment(iosPrepared.install, { root, env: childEnv, run }) };
|
|
125
157
|
if (target.install.kind === TAURI_DEVID_KIND) {
|
|
126
158
|
// installTauriDevId is async: the lease is held until it settles, so a broker cutover cannot land mid-install.
|
|
127
159
|
const built = resolveTauriArtifact(target.install, { root, platform, env: childEnv });
|
|
160
|
+
// Dev = production + QA: the same native helpers the release embeds are built, embedded and signed into the dev .app too.
|
|
161
|
+
const helpers = platform === "mac" ? nativeHelpersFromConfig(config) : undefined;
|
|
162
|
+
if (helpers) {
|
|
163
|
+
let version;
|
|
164
|
+
try { version = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")).version; } catch { version = undefined; }
|
|
165
|
+
buildAndEmbedNativeHelpers({ appRoot: root, app: built, helpers, version, forceArch: String(target.install.targetTriple).startsWith("x86_64") ? "x86_64" : "arm64", configuration: "debug", replaceExisting: true, env: childEnv });
|
|
166
|
+
}
|
|
128
167
|
releaseLease = false;
|
|
129
168
|
return installTauriDevId({ appRoot: root, config: target.install, releaseConfig: config, built, env: childEnv })
|
|
130
169
|
.then((outcome) => ({ built: true, installed: true, ...(staged.length ? { staged } : {}), devid: outcome }))
|
|
131
170
|
.finally(() => lease.release());
|
|
132
171
|
}
|
|
133
|
-
|
|
172
|
+
if (target.install.kind === ASSEMBLED_APP_KIND) {
|
|
173
|
+
const { spec, descriptor } = resolveAssembledInstall({ config, platform });
|
|
174
|
+
if (platform === "win") {
|
|
175
|
+
const assembled = installAssembledWindows({ root, config, descriptor, spec, env: childEnv });
|
|
176
|
+
return { built: true, installed: true, ...(staged.length ? { staged } : {}), assembled };
|
|
177
|
+
}
|
|
178
|
+
// installAssembledMac is async: the lease is held until it settles, so a broker cutover cannot land mid-install.
|
|
179
|
+
releaseLease = false;
|
|
180
|
+
return installAssembledMac({ root, config, descriptor, spec, env: childEnv })
|
|
181
|
+
.then((outcome) => ({ built: true, installed: true, ...(staged.length ? { staged } : {}), assembled: outcome }))
|
|
182
|
+
.finally(() => lease.release());
|
|
183
|
+
}
|
|
184
|
+
const signing = install(target.install, { root, platform, config, env: childEnv, run, door, sinceMs: startedMs, serial });
|
|
134
185
|
return { built: true, installed: true, ...(staged.length ? { staged } : {}), ...(platform === "mac" && target.install.kind === "tauri" ? { signing } : {}) };
|
|
135
186
|
} finally { if (releaseLease) lease.release(); }
|
|
136
187
|
}
|
|
137
188
|
|
|
138
|
-
|
|
139
|
-
|
|
189
|
+
/** WSL reports `linux` as its platform under both generations; which one it is decides whether a Linux kernel runs at all. */
|
|
190
|
+
export function isWsl({ env = process.env, readFile = (f) => readFileSync(f, "utf8") } = {}) {
|
|
191
|
+
if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) return true;
|
|
192
|
+
try { return /microsoft|wsl/i.test(readFile("/proc/sys/kernel/osrelease")) || /microsoft|wsl/i.test(readFile("/proc/version")); } catch { return false; }
|
|
140
193
|
}
|
|
141
194
|
|
|
142
|
-
|
|
195
|
+
/**
|
|
196
|
+
* WSL1 translates Linux system calls onto the Windows kernel (osrelease like `4.4.0-19041-Microsoft`), so it is not a Linux host.
|
|
197
|
+
* WSL2 runs a genuine Linux kernel in a Hyper-V utility VM (osrelease `…-microsoft-standard-WSL2`): a Linux VM, which is what
|
|
198
|
+
* native Linux builds need (rightkit#1230: "x86_64 on a Linux VM on the Dell"; Astra 2026-10-28). Unknown WSL counts as WSL1.
|
|
199
|
+
*/
|
|
200
|
+
export function isWsl1({ env = process.env, readFile = (f) => readFileSync(f, "utf8") } = {}) {
|
|
201
|
+
if (!isWsl({ env, readFile })) return false;
|
|
202
|
+
try { return !/microsoft-standard-WSL2|WSL2/i.test(readFile("/proc/sys/kernel/osrelease")); } catch { return true; }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function assertNativeDevelopmentHost(platform, host = process.platform, { wsl1 = () => isWsl1() } = {}) {
|
|
206
|
+
// Linux builds run on a Linux kernel of the target's architecture (no cross-compiling); WSL1 has no Linux kernel.
|
|
207
|
+
if ((platform === "win" && host !== "win32") || ((platform === "mac" || platform === "ios") && host !== "darwin") || (platform === "linux" && host !== "linux")) throw new Error(`development target ${platform} requires its native host`);
|
|
208
|
+
if (platform === "linux" && wsl1()) throw new Error("development target linux requires a Linux kernel; this is WSL1 (Linux system calls translated onto the Windows kernel). Use WSL2 or a Linux VM");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function install(descriptor, { root, platform, config, env, run, door, sinceMs = null, serial }) {
|
|
143
212
|
if (descriptor.kind === "adb") return installAndroidDevelopment(descriptor, { root, env, run, serial });
|
|
144
|
-
if (descriptor.kind === "command") return execute(descriptor, { root, env, run, label: "install" });
|
|
213
|
+
if (descriptor.kind === "command") return execute(descriptor, { root, env, run, door, label: "install" });
|
|
145
214
|
if (descriptor.kind === "portable") return installPortableDevelopment(descriptor, { root, platform: platform === "win" ? "win32" : "darwin", env });
|
|
146
215
|
if (platform === "win") {
|
|
147
216
|
if (!descriptor.expectedInstalledPath) throw new Error("Windows Tauri installer requires expectedInstalledPath for installed verification");
|
|
@@ -286,20 +355,41 @@ function resolveBundleRoot(descriptor, { root, env, runCargo }) {
|
|
|
286
355
|
|
|
287
356
|
function platformBundleKind(descriptor) { return descriptor.targetTriple.includes("apple") ? "macos" : "nsis"; }
|
|
288
357
|
function validateCommand(command, label) { if (!command || typeof command.cmd !== "string" || !command.cmd.trim() || (command.args !== undefined && !Array.isArray(command.args))) throw new Error(`${label} must declare cmd and args`); }
|
|
289
|
-
function execute(command, { root, env, dryRun, run, label }) {
|
|
358
|
+
function execute(command, { root, env, dryRun, run, label, door = null }) {
|
|
290
359
|
validateCommand(command, `development ${label}`);
|
|
291
360
|
const cwd = path.resolve(root, command.cwd ?? ".");
|
|
292
361
|
if (!inside(root, cwd)) throw new Error(`development ${label} cwd must stay inside app root`);
|
|
293
362
|
const args = (command.args ?? []).map(String);
|
|
294
363
|
if (dryRun) return { cmd: command.cmd, args, cwd };
|
|
295
|
-
const
|
|
364
|
+
const resolved = resolveCommand(command.cmd, args, { root, env });
|
|
365
|
+
// Every dev build step goes through the one door (build-launch.mjs): the door's own right-release stays in process order, a pnpm step
|
|
366
|
+
// is the rightkit-pnpm broker job (by its own name, not the node entry resolveCommand finds), anything else as resolved.
|
|
367
|
+
// A configured command can run code regardless of its label, so installation commands use the door too.
|
|
368
|
+
const own = resolved.command === process.execPath && resolved.args[0] === OWN_RIGHT_RELEASE;
|
|
369
|
+
const launch = own || !door ? resolved : { ...resolved, ...door(command.cmd === "pnpm" ? "pnpm" : resolved.command, command.cmd === "pnpm" ? args : resolved.args, { env: resolved.env ?? env, cwd, exists: existsSync }) };
|
|
296
370
|
const result = run(launch.command, launch.args, { cwd, env: launch.env ?? env, stdio: "inherit", windowsHide: true, shell: false, timeout: command.timeoutMs });
|
|
297
371
|
if (result.status !== 0) throw new Error(`development ${label} command failed (${result.status}): ${command.name ?? command.cmd}`);
|
|
298
372
|
}
|
|
299
|
-
|
|
373
|
+
/** This package's own right-release entry: the door's RightKit, whatever the app's node_modules holds. */
|
|
374
|
+
const OWN_RIGHT_RELEASE = fileURLToPath(new URL("./cli/right-release.mjs", import.meta.url));
|
|
375
|
+
/** `rustright <tool>` forms that map to the real toolchain on a direct (Linux) host. */
|
|
376
|
+
const DIRECT_TOOLS = new Set(["cargo", "rustc", "rustdoc"]);
|
|
377
|
+
export function resolveCommand(command, args, { root, env = process.env, platform = process.platform } = {}) {
|
|
300
378
|
if (command === "node") return { command: process.execPath, args };
|
|
379
|
+
// A dev target's `right-release …` step runs the door's own right-release (this package), never the app's node_modules copy:
|
|
380
|
+
// `pnpm exec right-release` puts the app's bin first on PATH, so PATH order cannot decide it (HeardRight on the Dell, 2026-10-27:
|
|
381
|
+
// the app's older guard refused what the door had admitted). `pnpm exec right-release …` and `pnpm right-release …` are the same step.
|
|
382
|
+
if (command === "right-release") return { command: process.execPath, args: [OWN_RIGHT_RELEASE, ...args] };
|
|
383
|
+
if (command === "pnpm" && ((args[0] === "exec" && args[1] === "right-release") || args[0] === "right-release")) {
|
|
384
|
+
return { command: process.execPath, args: [OWN_RIGHT_RELEASE, ...args.slice(args[0] === "exec" ? 2 : 1)] };
|
|
385
|
+
}
|
|
301
386
|
if (command === "pnpm") return { command: process.execPath, args: [resolvePnpmCli({ root, env }), ...args] };
|
|
302
387
|
if (command === "rightkit") throw new Error("`rightkit` is the retired name of the RustRight client (rustright#91); use rustright (cmd: \"rustright\")");
|
|
388
|
+
// A direct (Linux) host has no broker: `rustright cargo <args>` runs the real cargo, RustRight shims off PATH (managed-cargo directPlatform).
|
|
389
|
+
if (command === "rustright" && directPlatform(platform)) {
|
|
390
|
+
if (!DIRECT_TOOLS.has(args[0])) throw new Error(`\`rustright ${args[0] ?? ""}\` has no direct form on this ${platform} host (no broker on Linux); only \`rustright cargo|rustc|rustdoc\` map to the toolchain`);
|
|
391
|
+
return { command: args[0], args: args.slice(1), env: directCargoEnv(env, platform) };
|
|
392
|
+
}
|
|
303
393
|
if (command === "rustright") {
|
|
304
394
|
const client = managedRuntimeClient(env);
|
|
305
395
|
if (!client) throw new Error("installed RightKit runtime client is unavailable for development command");
|
|
@@ -351,7 +441,7 @@ function inside(root, child) { const rel = path.relative(path.resolve(root), pat
|
|
|
351
441
|
export async function main(args = process.argv.slice(2)) {
|
|
352
442
|
let platform, configPath = defaultReleaseConfigFile(), dryRun = false, buildOnly = false, serial;
|
|
353
443
|
for (let i = 0; i < args.length; i++) {
|
|
354
|
-
if (args[i] === "-h" || args[i] === "--help") { console.log("usage: right-release dev --platform win|mac|android|ios [--config right-release.config.json] [--dry-run] [--build-only] [--serial <adb serial | iOS device id>]"); return; }
|
|
444
|
+
if (args[i] === "-h" || args[i] === "--help") { console.log("usage: right-release dev --platform win|mac|linux|android|ios [--config right-release.config.json] [--dry-run] [--build-only] [--serial <adb serial | iOS device id>]"); return; }
|
|
355
445
|
if (args[i] === "--platform") platform = args[++i];
|
|
356
446
|
else if (args[i] === "--config") configPath = args[++i];
|
|
357
447
|
else if (args[i] === "--dry-run") dryRun = true;
|
|
@@ -359,7 +449,7 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
359
449
|
else if (args[i] === "--serial") serial = args[++i];
|
|
360
450
|
else throw new Error(`unknown dev option: ${args[i]}`);
|
|
361
451
|
}
|
|
362
|
-
if (!platform) throw new Error("right-release dev requires --platform win|mac|android|ios");
|
|
452
|
+
if (!platform) throw new Error("right-release dev requires --platform win|mac|linux|android|ios");
|
|
363
453
|
const absoluteConfig = path.resolve(configPath);
|
|
364
454
|
const safeEnv = developmentEnvironment(process.env);
|
|
365
455
|
for (const [key, value] of Object.entries(safeEnv)) process.env[key] = value;
|
package/door-policy.mjs
CHANGED
|
@@ -17,7 +17,25 @@ const WRAPPERS = new Set(["env", "command", "exec", "time", "nice", "nohup", "su
|
|
|
17
17
|
// Shell reserved words and grouping that can precede a command in the same simple command (`if true; then cargo build; fi`,
|
|
18
18
|
// `{ cargo build; }`, `f() { cargo build; }`, `! cargo test`): they are skipped, never taken for the command.
|
|
19
19
|
const KEYWORDS = new Set(["if", "then", "elif", "else", "fi", "do", "done", "while", "until", "{", "}", "!", "function"]);
|
|
20
|
-
|
|
20
|
+
// Adrian, 2026-10-26: every native and frontend build tool an app can be built with, not only cargo. xcodebuild's actions, swift's
|
|
21
|
+
// compiling subcommands, Gradle tasks, the frontend bundlers, and a package script whose name says it builds.
|
|
22
|
+
const XCODE_ACTIONS = new Set(["build", "archive", "test", "build-for-testing", "test-without-building", "analyze", "install", "installsrc", "clean"]);
|
|
23
|
+
const XCODE_READONLY = new Set(["-version", "-list", "-showsdks", "-showBuildSettings", "-showdestinations", "-showTestPlans", "-help", "-usage", "-checkFirstLaunchStatus", "-exportLocalizations"]);
|
|
24
|
+
const SWIFT_COMPILES = new Set(["build", "test", "run"]);
|
|
25
|
+
const GRADLE_READONLY = new Set(["--version", "-v", "-version", "--help", "-h", "-?", "help", "tasks", "dependencies", "projects", "properties", "--stop", "--status"]);
|
|
26
|
+
const FRONTEND_BUNDLERS = new Set(["webpack", "rollup", "esbuild", "tsup", "parcel"]);
|
|
27
|
+
// A package script whose name says it builds (`pnpm build`, `pnpm run build:mac`, `npm run tauri`): the apps have no scripts, and a
|
|
28
|
+
// script that builds would be a second door even if they did.
|
|
29
|
+
const SCRIPT_BUILDS = /^(build|bundle|package|dist|compile|release|tauri|android|ios|xcode|gradle|test|tests|e2e|qa)(:|$)/;
|
|
30
|
+
// Tools whose own options take a value, so the word after them is not the subcommand.
|
|
31
|
+
const XCRUN_VALUE_OPTIONS = new Set(["--sdk", "-sdk", "--toolchain", "-toolchain", "--log", "-f", "--find", "--run"]);
|
|
32
|
+
const SWIFT_VALUE_OPTIONS = new Set(["--package-path", "-c", "--configuration", "--build-path", "--scratch-path", "--cache-path", "--sdk", "--toolchain", "-Xswiftc", "-Xcc", "-Xlinker", "--triple", "--arch", "-j", "--jobs"]);
|
|
33
|
+
const GRADLE_VALUE_OPTIONS = new Set(["-p", "--project-dir", "-b", "--build-file", "-c", "--settings-file", "-g", "--gradle-user-home", "-I", "--init-script", "--max-workers", "--console", "-x", "--exclude-task", "--project-cache-dir"]);
|
|
34
|
+
const VITE_VALUE_OPTIONS = new Set(["--config", "-c", "--mode", "-m", "--base", "--logLevel", "-l", "--outDir", "--target", "--assetsDir", "--filter"]);
|
|
35
|
+
/** The non-option words of argv, skipping the value of each option named in `valued` (`--config x`, and `--config=x` as one word). */
|
|
36
|
+
const wordsOf = (argv, valued) => { const out = []; for (let i = 0; i < argv.length; i++) { const w = argv[i]; if (w.startsWith("-")) { if (valued.has(w)) i++; continue; } out.push(w); } return out; };
|
|
37
|
+
const firstWord = (argv, valued) => wordsOf(argv, valued)[0] ?? null;
|
|
38
|
+
const DOOR_TOOLS = new Set(["cargo", "tauri", "right-release", "xcodebuild", "swift", "swiftc", "gradle", "gradlew", "vite", ...FRONTEND_BUNDLERS]);
|
|
21
39
|
|
|
22
40
|
/** Splits a shell command into simple commands (on ; && || | and newlines), each an argv of words. Quotes are respected. */
|
|
23
41
|
export function simpleCommands(command) {
|
|
@@ -98,7 +116,8 @@ function argvViolation(argv, depth = 0, stdin = null) {
|
|
|
98
116
|
let tool = base(a[0]);
|
|
99
117
|
// A command string handed to another shell (`bash -c "cargo build"`, `eval "..."`) is judged as the command it is.
|
|
100
118
|
if (depth < 4 && (SHELLS.has(tool.toLowerCase()) || tool === "eval")) {
|
|
101
|
-
|
|
119
|
+
// `-c`, and combined short flags that carry it (`-lc`, `-xec`); pwsh `-Command`; cmd `/c`.
|
|
120
|
+
const flag = a.findIndex((w, i) => i > 0 && /^(-[a-zA-Z]*c[a-zA-Z]*|\/c|-Command|-command)$/.test(w));
|
|
102
121
|
const script = tool === "eval" ? a.slice(1).join(" ") : flag > 0 ? a.slice(flag + 1).join(" ") : null;
|
|
103
122
|
return script ? doorViolation(script, depth + 1) : null;
|
|
104
123
|
}
|
|
@@ -115,12 +134,43 @@ function argvViolation(argv, depth = 0, stdin = null) {
|
|
|
115
134
|
if (RUNNERS.has(tool)) {
|
|
116
135
|
let rest = a.slice(1).filter((w, i, all) => !(w.startsWith("-") || (i > 0 && ["--filter", "-F", "--dir", "-C", "--prefix"].includes(all[i - 1]))));
|
|
117
136
|
if (["exec", "dlx", "x"].includes(rest[0])) rest = rest.slice(1);
|
|
118
|
-
|
|
137
|
+
const script = rest[0] === "run" || rest[0] === "run-script";
|
|
138
|
+
if (script) rest = rest.slice(1);
|
|
119
139
|
if (!rest.length) return null;
|
|
120
|
-
|
|
140
|
+
// A script whose name says it builds is denied as the runner's script (`pnpm build`, `npm run tauri`); any other script name is
|
|
141
|
+
// not a tool (the apps have none). Without `run`, the word is judged as the tool it names (`pnpm tauri build`, `pnpm exec vite build`).
|
|
142
|
+
if (!script && DOOR_TOOLS.has(base(rest[0]))) { a = rest; tool = base(a[0]); }
|
|
143
|
+
else if (SCRIPT_BUILDS.test(rest[0])) return `${tool} ${rest[0]}`;
|
|
144
|
+
else if (script) return null;
|
|
145
|
+
else { a = rest; tool = base(a[0]); }
|
|
121
146
|
}
|
|
122
147
|
if (tool === "rightkit-gate" || tool === "rustright") return null;
|
|
123
148
|
if (tool === "tauri" && TAURI_BUILDS.has(a[1])) return `tauri ${a[1]}`;
|
|
149
|
+
// Apple: xcrun wraps the tool it names; xcodebuild builds by default (no action word is `build`); swift build|test|run and swiftc compile.
|
|
150
|
+
if (tool === "xcrun" && a.length > 1) {
|
|
151
|
+
// xcrun's own options come first (`--sdk iphoneos`, `--toolchain x`, `-f`); the tool it runs is the first word after them.
|
|
152
|
+
let rest = a.slice(1);
|
|
153
|
+
while (rest.length && rest[0].startsWith("-")) rest = XCRUN_VALUE_OPTIONS.has(rest[0]) ? rest.slice(2) : rest.slice(1);
|
|
154
|
+
return rest.length ? argvViolation(rest, depth + 1, stdin) : null;
|
|
155
|
+
}
|
|
156
|
+
if (tool === "xcodebuild") {
|
|
157
|
+
const words = a.slice(1);
|
|
158
|
+
const action = words.find((w) => XCODE_ACTIONS.has(w));
|
|
159
|
+
if (action) return `xcodebuild ${action}`;
|
|
160
|
+
return words.some((w) => XCODE_READONLY.has(w)) ? null : "xcodebuild build";
|
|
161
|
+
}
|
|
162
|
+
if (tool === "swift") { const sub = firstWord(a.slice(1), SWIFT_VALUE_OPTIONS); if (sub && SWIFT_COMPILES.has(sub)) return `swift ${sub}`; }
|
|
163
|
+
if (tool === "swiftc") return "swiftc";
|
|
164
|
+
// Android: a Gradle task that is not one of the read-only reports.
|
|
165
|
+
if (tool === "gradle" || tool === "gradlew") {
|
|
166
|
+
// Every task named counts (`gradle tasks assembleRelease` builds); a bare call prints help.
|
|
167
|
+
const tasks = wordsOf(a.slice(1), GRADLE_VALUE_OPTIONS);
|
|
168
|
+
const building = tasks.find((t) => !GRADLE_READONLY.has(t));
|
|
169
|
+
return building ? `gradle ${building}` : null;
|
|
170
|
+
}
|
|
171
|
+
// Frontend: a production bundle is the compile stage's frontend build.
|
|
172
|
+
if (tool === "vite" && wordsOf(a.slice(1), VITE_VALUE_OPTIONS).includes("build")) return "vite build";
|
|
173
|
+
if (FRONTEND_BUNDLERS.has(tool)) return tool;
|
|
124
174
|
if (tool === "cargo") {
|
|
125
175
|
let sub = null;
|
|
126
176
|
for (let i = 1; i < a.length; i++) {
|
|
@@ -213,9 +263,14 @@ export function doorViolation(command, depth = 0) {
|
|
|
213
263
|
}
|
|
214
264
|
|
|
215
265
|
export function doorDenial(violation) {
|
|
266
|
+
const mobile = /^(cargo )?tauri (android|ios)$|^xcodebuild (archive|install)|^gradle (assemble|bundle|build|publish|install)/.test(violation) ? (/ios|xcodebuild/.test(violation) ? "ios" : "android") : null;
|
|
216
267
|
const stage = /^right-release (dev|test)$/.test(violation) ? violation.slice("right-release ".length) : /^(cargo )?tauri dev$/.test(violation) ? "dev"
|
|
217
|
-
: /^right-release (upload|publish|publish-update|github)$/.test(violation) ? "release" : /^right-release qa$/.test(violation) ? "qualify"
|
|
218
|
-
|
|
268
|
+
: /^right-release (upload|publish|publish-update|github)$/.test(violation) ? "release" : /^right-release qa$/.test(violation) ? "qualify"
|
|
269
|
+
: /^right-release /.test(violation) || /^(cargo )?tauri (build|bundle|android|ios)$/.test(violation) || /^xcodebuild (archive|install)/.test(violation) || /^gradle /.test(violation) ? "package"
|
|
270
|
+
: "compile";
|
|
271
|
+
const platform = mobile ?? "mac|win";
|
|
272
|
+
const tail = stage === "package" || stage === "qualify" ? ` --platform ${platform}` : stage === "release" ? ` --platform ${platform}${mobile ? "" : " --tier patch|update"}` : stage === "dev" || stage === "test" ? " -- <arguments>" : "";
|
|
273
|
+
return `[HOOK:one-door:block]\nWhy: \`${violation}\` builds outside the one door. Every check, compile, test, package and release goes through rightkit-gate, class A first.\nRequired: rightkit-gate --to ${stage}${tail} (or rustright cargo ... on a broker host, which runs class A first).`;
|
|
219
274
|
}
|
|
220
275
|
|
|
221
276
|
/** Claude Code PreToolUse decision for one hook input. */
|
package/door.mjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// The door check for an app's own build step: a command its right-release.config.mjs declares (a payload assembler, an installer
|
|
2
|
+
// packager) that right-release runs inside a stage. The step proceeds only when a live `rightkit-gate --to` run for HEAD launched it,
|
|
3
|
+
// the same check right-release makes for itself (gate-stages.mjs requireDoorRun), so `node scripts/release/<step>.mjs` run by hand is
|
|
4
|
+
// refused instead of becoming a side door around class A.
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { spawnSync } from "./background-process.mjs";
|
|
7
|
+
import { requireDoorRun } from "./gate-stages.mjs";
|
|
8
|
+
|
|
9
|
+
/** null when a live door run for HEAD launched this process, else the refusal message. */
|
|
10
|
+
export function doorRefusal({ cwd = process.cwd(), env = process.env, command = "this build step", run = spawnSync, alive } = {}) {
|
|
11
|
+
const git = (args) => { const r = run("git", args, { cwd, encoding: "utf8" }); return r.status === 0 ? String(r.stdout).trim() : ""; };
|
|
12
|
+
const common = git(["rev-parse", "--git-common-dir"]);
|
|
13
|
+
if (!common) return `${command} runs only in a git checkout, through the one door: rightkit-gate --to <stage>`;
|
|
14
|
+
return requireDoorRun({ env, commonDir: path.resolve(cwd, common), commit: git(["rev-parse", "HEAD"]), command, ...(alive ? { alive } : {}) });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Exits 2 with the refusal unless a live door run launched this process. */
|
|
18
|
+
export function requireDoor(command, options = {}) {
|
|
19
|
+
const refusal = doorRefusal({ ...options, command });
|
|
20
|
+
if (!refusal) return;
|
|
21
|
+
process.stderr.write(`${refusal}\n`);
|
|
22
|
+
process.exit(2);
|
|
23
|
+
}
|
package/gate-executor.mjs
CHANGED
|
@@ -2,16 +2,28 @@
|
|
|
2
2
|
// broker - `rustright` is on PATH, or the first `cargo` is the broker's managed shim: every cargo call goes through the broker.
|
|
3
3
|
// refuse - a broker-managed machine (Mac/Dell markers, broker socket, managed agent-bin) whose broker cannot be reached from here:
|
|
4
4
|
// the gate never compiles around it.
|
|
5
|
-
// direct -
|
|
5
|
+
// direct - every Linux host (WSL2, VM, cloud, CI, Linux hosted on the Dell) always, and any unmanaged Mac or Windows host: cargo
|
|
6
|
+
// runs directly, through the same gates and stages.
|
|
6
7
|
import { brokerManagedHost, managedCargoShimOnPath } from "./cargo-contract.mjs";
|
|
8
|
+
import { directCargoPath, directPlatform } from "./managed-cargo.mjs";
|
|
9
|
+
|
|
10
|
+
export { directCargoPath };
|
|
7
11
|
|
|
8
12
|
export const EXECUTOR_KINDS = Object.freeze(["broker", "refuse", "direct"]);
|
|
9
13
|
|
|
10
14
|
/**
|
|
11
15
|
* @param {{ env?: object, platform?: string, exists?: (p: string) => boolean, which: (cmd: string) => string[], readFile: (p: string) => string }} o
|
|
12
|
-
* @returns {{ kind: "broker" | "refuse" | "direct", cargo: string[] | null, problem?: string }}
|
|
16
|
+
* @returns {{ kind: "broker" | "refuse" | "direct", cargo: string[] | null, problem?: string, env?: { PATH: string }, note?: string }}
|
|
13
17
|
*/
|
|
14
18
|
export function resolveExecutor({ env = process.env, platform = process.platform, exists, which, readFile }) {
|
|
19
|
+
// Every Linux (and any other non-Mac, non-Windows) execution host runs direct, whatever is on PATH or in the environment: WSL2, a VM,
|
|
20
|
+
// a cloud or CI runner, or Linux hosted on the Dell (GATE-CONTRACT §1 "One entry"; joint plan step 4). Only Adrian's Mac and Windows
|
|
21
|
+
// hosts use the broker.
|
|
22
|
+
// `cargo` there must be a real toolchain: RustRight shim directories are taken off the PATH the gate hands its children (`env`).
|
|
23
|
+
if (directPlatform(platform)) {
|
|
24
|
+
const direct = directCargoPath({ env, exists });
|
|
25
|
+
return { kind: "direct", cargo: ["cargo"], ...(direct.dropped.length ? { env: { PATH: direct.path }, note: `RustRight shims taken off PATH for direct cargo: ${direct.dropped.join(", ")}` } : {}), ...(direct.cargo ? {} : { note: `no direct cargo on PATH${direct.dropped.length ? ` (RustRight shims skipped: ${direct.dropped.join(", ")})` : ""}; cargo scans fail as a missing tool` }) };
|
|
26
|
+
}
|
|
15
27
|
if (which("rustright").length) return { kind: "broker", cargo: ["rustright", "cargo"] };
|
|
16
28
|
const first = which("cargo")[0];
|
|
17
29
|
if (first) {
|
package/gate-stages.mjs
CHANGED
|
@@ -63,11 +63,21 @@ function pidAlive(pid) {
|
|
|
63
63
|
try { process.kill(pid, 0); return true; } catch (e) { return e.code === "EPERM"; }
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
66
|
+
/**
|
|
67
|
+
* The receipt file for `commit` and the app it was run for: a repository with one app at its root keeps `<commit>.json`; an app in a
|
|
68
|
+
* nested directory (CodeRight's apps/coderight-tauri and apps/coderight-ios, CutRight's apps/cutright, HeardRight's tauri-app-next)
|
|
69
|
+
* gets `<commit>--<dir, _ as _u, / as __>.json`, so two apps of one repository never share or overwrite a receipt (Astra, 2026-10-26).
|
|
70
|
+
*/
|
|
71
|
+
export function receiptKey(commit, app = ".") {
|
|
72
|
+
// Reversible: "_" becomes "_u" before "/" becomes "__", so apps/a__b (apps__a_u_ub) and apps/a/b (apps__a__b) never share a key.
|
|
73
|
+
return app && app !== "." ? `${commit}--${app.replaceAll("_", "_u").replaceAll("/", "__")}` : commit;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Reads the receipt for `commit` (and `app`), or null. */
|
|
77
|
+
export function readReceipt({ commonDir, commit, app = ".", readFile = (f) => fs.readFileSync(f, "utf8") }) {
|
|
68
78
|
try {
|
|
69
|
-
const r = JSON.parse(readFile(path.join(gateStateDir(commonDir), "receipts", `${commit}.json`)));
|
|
70
|
-
if (r.schema === RECEIPT_SCHEMA && r.commit === commit) return r;
|
|
79
|
+
const r = JSON.parse(readFile(path.join(gateStateDir(commonDir), "receipts", `${receiptKey(commit, app)}.json`)));
|
|
80
|
+
if (r.schema === RECEIPT_SCHEMA && r.commit === commit && (r.app ?? ".") === (app ?? ".")) return r;
|
|
71
81
|
} catch { /* none yet */ }
|
|
72
82
|
return null;
|
|
73
83
|
}
|
|
@@ -75,7 +85,7 @@ export function readReceipt({ commonDir, commit, readFile = (f) => fs.readFileSy
|
|
|
75
85
|
export function writeReceipt({ commonDir, receipt, write = fs.writeFileSync, mkdir = fs.mkdirSync }) {
|
|
76
86
|
const dir = path.join(gateStateDir(commonDir), "receipts");
|
|
77
87
|
mkdir(dir, { recursive: true });
|
|
78
|
-
write(path.join(dir, `${receipt.commit}.json`), `${JSON.stringify(receipt, null, 2)}\n`);
|
|
88
|
+
write(path.join(dir, `${receiptKey(receipt.commit, receipt.app ?? ".")}.json`), `${JSON.stringify(receipt, null, 2)}\n`);
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
/**
|
package/heavy-command.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, r
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { spawn, spawnSync } from "./background-process.mjs";
|
|
6
|
+
import { spawnUnderOwnership } from "./build-ownership.mjs";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
7
8
|
import { randomUUID } from "node:crypto";
|
|
8
9
|
|
|
@@ -208,7 +209,7 @@ export async function watchOwnedProcessTree(
|
|
|
208
209
|
|
|
209
210
|
function spawnOwnerWatcher(details) {
|
|
210
211
|
const payload = Buffer.from(JSON.stringify(details)).toString("base64url");
|
|
211
|
-
const watcher = spawn
|
|
212
|
+
const watcher = spawnUnderOwnership(spawn, process.execPath, [SELF, "--watch-owner-base64", payload], {
|
|
212
213
|
detached: true, env: process.env, stdio: "ignore", windowsHide: true,
|
|
213
214
|
});
|
|
214
215
|
watcher.once("error", () => {
|
|
@@ -335,7 +336,7 @@ export async function runHeavyCommand(args, { env = process.env } = {}) {
|
|
|
335
336
|
|
|
336
337
|
function spawnAttached(command, args, env, slot) {
|
|
337
338
|
return new Promise((resolve, reject) => {
|
|
338
|
-
const child = spawn
|
|
339
|
+
const child = spawnUnderOwnership(spawn, command, args, { cwd: process.cwd(), env, stdio: "inherit", windowsHide: true, detached: heavyChildDetached({ slot }) });
|
|
339
340
|
try {
|
|
340
341
|
if (child.pid && slot) slot.trackChild(child.pid, command);
|
|
341
342
|
} catch (error) {
|
package/install-app.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import os from "node:os";
|
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
14
14
|
|
|
15
|
+
const LSREGISTER = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister";
|
|
15
16
|
const SIGTERM_WAIT_MS = 15_000;
|
|
16
17
|
const POLL_MS = 250;
|
|
17
18
|
|
|
@@ -632,12 +633,20 @@ export async function installApp(opts, deps = {}) {
|
|
|
632
633
|
swap = null;
|
|
633
634
|
event("replaced", { installed, previous: null, swap: swapMode });
|
|
634
635
|
|
|
636
|
+
// A dev install from a build folder (--app) leaves that product registered with Launch Services, which then resolves the bundle
|
|
637
|
+
// id to it instead of the installed copy (rightkit#1225): unregister it so only the installed copy is known.
|
|
638
|
+
if (opts.app && path.resolve(sourceApp) !== path.resolve(installed)) {
|
|
639
|
+
try { await run(LSREGISTER, ["-u", sourceApp]); event("unregistered", { path: sourceApp }); }
|
|
640
|
+
catch (error) { event("unregister-failed", { path: sourceApp, reason: errText(error) }); }
|
|
641
|
+
}
|
|
642
|
+
|
|
635
643
|
// Relaunch hidden and in the background only.
|
|
636
644
|
const relaunch = opts.relaunch === null ? wasRunning : opts.relaunch;
|
|
637
645
|
if (relaunch) {
|
|
638
646
|
failedStage = "relaunch";
|
|
639
|
-
|
|
640
|
-
|
|
647
|
+
// By the installed path, never by bundle id: Launch Services may resolve the id to another registered copy (rightkit#1225).
|
|
648
|
+
await run("/usr/bin/open", ["-g", "-j", installed]);
|
|
649
|
+
event("relaunched", { bundleId, installed });
|
|
641
650
|
}
|
|
642
651
|
const result = { ok: true, app: appName, installed, bundleId, teamId: newTeam, previous: null, swap: swapMode, relaunched: relaunch, ...(dev ? { dev: true } : {}) };
|
|
643
652
|
event("done", result);
|