@rightkit/release 0.2.134 → 0.2.136
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 +62 -0
- package/addon-command.mjs +53 -34
- package/apple-profile-renew.mjs +30 -12
- package/artifact-storage.mjs +53 -12
- package/background-process.mjs +18 -0
- package/build-release.mjs +4 -3
- package/cache-command.mjs +4 -3
- package/cargo-contract.mjs +1 -1
- package/cargo-guard.mjs +4 -3
- package/cargo-registry-readback.mjs +51 -0
- package/cargo-target.mjs +3 -1
- package/cli/right-release.mjs +26 -0
- package/codesign-argv.mjs +64 -0
- package/codesign-environment.mjs +58 -0
- package/codesign-shim.mjs +51 -0
- package/committed-source.mjs +3 -3
- package/development.mjs +11 -9
- package/direct-bootstrap.mjs +19 -9
- package/dmg-build.mjs +117 -0
- package/dmg-layout.mjs +123 -0
- package/github-release.mjs +74 -37
- package/heavy-command.mjs +3 -2
- package/install-app.mjs +358 -0
- package/inventory-check.mjs +980 -0
- package/macos-notarization.mjs +24 -8
- package/macos-signing-session.mjs +50 -2
- package/managed-cargo.mjs +23 -19
- package/managed-host-secrets.mjs +30 -0
- package/managed-roots.mjs +39 -0
- package/model-promote.mjs +31 -37
- package/notarize-macos.mjs +5 -5
- package/notary-auth.mjs +4 -0
- package/osascript-guard.mjs +24 -0
- package/ownership-approval-command.mjs +15 -4
- package/ownership-approval.mjs +114 -49
- package/ownership-availability.mjs +25 -0
- package/ownership-catalog-config.json +18 -0
- package/ownership-catalog.mjs +94 -0
- package/ownership-command.mjs +17 -10
- package/ownership-declaration.json +15 -0
- package/ownership-default-policy.json +1 -11
- package/ownership-evidence.mjs +10 -8
- package/ownership-git-inventory.mjs +29 -1
- package/ownership-helper-paths.json +4 -4
- package/ownership-hooks.mjs +107 -0
- package/ownership-internal-owners.json +44 -0
- package/ownership-key-authority.mjs +112 -7
- package/ownership-key-command.mjs +76 -19
- package/ownership-key-trust.mjs +97 -9
- package/ownership-lockfiles.mjs +311 -0
- package/ownership-report-command.mjs +12 -3
- package/ownership-report.mjs +20 -17
- package/ownership-repositories.json +19 -0
- package/ownership-scan-worker.mjs +96 -11
- package/ownership-source.mjs +39 -0
- package/ownership-staged.mjs +193 -0
- package/ownership-verdict.mjs +74 -0
- package/ownership.mjs +215 -107
- package/package.json +1 -1
- package/protected-credentials.mjs +111 -0
- package/prune-r2.mjs +4 -3
- package/pub-upload.mjs +3 -85
- package/publication-artifacts.mjs +108 -0
- package/publication-gate.mjs +28 -7
- package/publish-cargo.mjs +47 -16
- package/publish-swift.mjs +46 -17
- package/publish-update.mjs +61 -39
- package/r2-verified-upload.mjs +41 -0
- package/registry-parity.mjs +5 -3
- package/release-state.mjs +1 -0
- package/release-token.mjs +2 -0
- package/release.mjs +2 -1
- package/rightkit-drift.mjs +3 -2
- package/rightkit-ownership.json +2028 -139
- package/rightkit-signing-agent.mjs +161 -19
- package/rightkit-sync.mjs +6 -2
- package/rightkit-versions.json +220 -106
- package/runtime-artifact-manifest.mjs +20 -6
- package/sign-updater.mjs +5 -0
- package/signing-agent-client.mjs +78 -22
- package/signing-agent-command.mjs +5 -4
- package/signing-agent-install.mjs +31 -4
- package/standalone-clone-verify.mjs +3 -2
- package/tauri-bundle-marker.mjs +3 -2
- package/upload-large.mjs +36 -82
- package/verifier.mjs +5 -1
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @rightkit/release
|
|
2
|
+
|
|
3
|
+
Portable release CLI and SDK for Right Suite products: builds, signs, and publishes to public GitHub Releases or to private R2 with RightApps distribution. It also runs the ownership catalog and lockfile checks and the release gates.
|
|
4
|
+
|
|
5
|
+
| Item | Value |
|
|
6
|
+
|---|---|
|
|
7
|
+
| Where it runs | Build and release tooling. Native signing runs on the build host. |
|
|
8
|
+
| Rust pair | None. The Rust build broker files in this package (`managed-cargo.mjs`, `managed-roots.mjs`) are moving to the separate RustRight product. Do not extend them here. |
|
|
9
|
+
| Version | see INDEX.md |
|
|
10
|
+
| License | MIT OR Apache-2.0 |
|
|
11
|
+
|
|
12
|
+
Signing and publication rules are in the workspace file `docs/rules/release-signing.md`. Public releases use RightKit CI. Private builds run on the native host.
|
|
13
|
+
|
|
14
|
+
## Commands
|
|
15
|
+
|
|
16
|
+
`right-release` maps these subcommands to the modules in this package:
|
|
17
|
+
|
|
18
|
+
| Group | Subcommands |
|
|
19
|
+
|---|---|
|
|
20
|
+
| Build | `release`, `build`, `package` |
|
|
21
|
+
| Install | `install-app` |
|
|
22
|
+
| Publish | `publish`, `upload`, `publish-update`, `github`, `mirror-root-artifact` |
|
|
23
|
+
| Signing | `sign-windows`, `sign-updater`, `signing-agent`, `create-mac-updater` |
|
|
24
|
+
| Checks | `deps`, `hardening`, `harden`, `hardeningscan`, `cargo-target` |
|
|
25
|
+
| Ownership | `ownership`, `ownership-approval`, `ownership-report` |
|
|
26
|
+
|
|
27
|
+
## Local app install (macOS)
|
|
28
|
+
|
|
29
|
+
Agents must not run `osascript`, `open`, `screencapture`, or `notifyutil -p` directly. Install a signed build through:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
right-release install-app --dmg <path.dmg> [--app-name Pulse.app] [--bundle-id dev.orthic.pulse] [--dest /Applications] [--relaunch|--no-relaunch] [--json]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
It mounts the image read-only and hidden (`-nobrowse`), verifies the app (`codesign --verify --deep --strict`, `spctl --assess`, `stapler validate`, bundle id, and Team ID against the installed copy; a Team ID change is refused) before touching the installed copy, quits the running instance with SIGTERM (15 s, then SIGKILL), moves the old bundle to `~/.Trash/<App> <timestamp>.app`, copies the new one with `ditto` (restoring the old bundle on failure), re-verifies, and relaunches only if it was running or `--relaunch` is given, using `open -g -j -b` (background, hidden, never activated). `--json` emits one lifecycle event per line (`start`, `verified`, `quit`, `replaced`, `relaunched`, `done`/`failed`). Win32 exits with "not implemented on win32". Exit codes: 0 ok, 1 failed, 2 usage, 3 unsupported platform.
|
|
36
|
+
|
|
37
|
+
## Entry points
|
|
38
|
+
|
|
39
|
+
Bin: `right-release` (`cli/right-release.mjs`). The package has no `exports` map. The CLI is the supported surface.
|
|
40
|
+
|
|
41
|
+
## Dependencies
|
|
42
|
+
|
|
43
|
+
| Dependency | Range | Note |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `smol-toml` | `1.7.0` (exact) | Used to parse lock and manifest files |
|
|
46
|
+
|
|
47
|
+
Peer dependencies: none.
|
|
48
|
+
|
|
49
|
+
## Example
|
|
50
|
+
|
|
51
|
+
Illustrative. Dependency checks, as described in the shared-packages doc:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
pnpm add -D @rightkit/release
|
|
55
|
+
pnpm exec right-release deps --check
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Known issues
|
|
59
|
+
|
|
60
|
+
- `smol-toml` is pinned exactly. A consumer that adds another `smol-toml` gets a second copy (npm audit 2026-10-08, release finding).
|
|
61
|
+
- Eleven apps pin this package at different versions, from 0.2.109 to 0.2.135 (npm audit 2026-10-08).
|
|
62
|
+
- Prepublish runs the test suite, the Windows signer integration test and the registry parity check.
|
package/addon-command.mjs
CHANGED
|
@@ -7,28 +7,35 @@ import { spawnSync } from "./background-process.mjs";
|
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
8
|
import { addonManifestSha256, addonRoutes, canonicalAddonManifest, createAddonManifest, downloadImmutableAddOn, immutableAddonManifestUrl, validateAddonConfig, validateAddonManifest } from "./addon-contract.mjs";
|
|
9
9
|
import { assertPrimaryReleaseCheckout } from "./release-invocation.mjs";
|
|
10
|
+
import { readVerifiedArtifact } from "./publication-artifacts.mjs";
|
|
11
|
+
import { resolveCloudflareApiToken } from "./protected-credentials.mjs";
|
|
12
|
+
import { uploadVerifiedR2Artifact } from "./r2-verified-upload.mjs";
|
|
10
13
|
import { assertCleanSource } from "./source-gate.mjs";
|
|
11
14
|
|
|
12
15
|
const ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
13
16
|
const SIGN_WINDOWS = path.join(ROOT, "sign-windows.mjs");
|
|
14
17
|
const HARDEN = path.join(ROOT, "hardeningscan.mjs");
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
if (!new Set(["
|
|
18
|
+
async function main() {
|
|
19
|
+
const [operation, ...args] = process.argv.slice(2);
|
|
20
|
+
const options = parse(args);
|
|
21
|
+
if (!new Set(["doctor", "build", "upload", "adopt"]).has(operation)) usage(2);
|
|
22
|
+
if (!new Set(["mac", "win"]).has(options.platform)) fail("--platform must be mac or win");
|
|
23
|
+
|
|
24
|
+
if (operation === "adopt") await adopt(options);
|
|
25
|
+
else {
|
|
26
|
+
if (!options.config) fail("--config is required");
|
|
27
|
+
const configPath = path.resolve(options.config);
|
|
28
|
+
const root = path.dirname(configPath);
|
|
29
|
+
const repoRoot = git(root, ["rev-parse", "--show-toplevel"]);
|
|
30
|
+
assertPrimaryReleaseCheckout(repoRoot);
|
|
31
|
+
const config = (await import(`${pathToFileURL(configPath).href}?addon=${Date.now()}`)).default;
|
|
32
|
+
if (operation === "doctor") doctor({ config, configPath, root, repoRoot, options });
|
|
33
|
+
else if (operation === "build") await build({ config, configPath, root, repoRoot, options });
|
|
34
|
+
else await upload({ config, root, repoRoot, options });
|
|
35
|
+
}
|
|
19
36
|
|
|
20
|
-
if (operation === "adopt") await adopt(options);
|
|
21
|
-
else {
|
|
22
|
-
if (!options.config) fail("--config is required");
|
|
23
|
-
const configPath = path.resolve(options.config);
|
|
24
|
-
const root = path.dirname(configPath);
|
|
25
|
-
const repoRoot = git(root, ["rev-parse", "--show-toplevel"]);
|
|
26
|
-
assertPrimaryReleaseCheckout(repoRoot);
|
|
27
|
-
const config = (await import(`${pathToFileURL(configPath).href}?addon=${Date.now()}`)).default;
|
|
28
|
-
if (operation === "doctor") doctor({ config, configPath, root, repoRoot, options });
|
|
29
|
-
else if (operation === "build") await build({ config, configPath, root, repoRoot, options });
|
|
30
|
-
else await upload({ config, root, repoRoot, options });
|
|
31
37
|
}
|
|
38
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await main();
|
|
32
39
|
|
|
33
40
|
function doctor({ config, configPath, root, repoRoot, options }) {
|
|
34
41
|
validateAddonConfig(config, { root, platform: options.platform, allowMissingExecutables: true });
|
|
@@ -84,33 +91,45 @@ async function upload({ config, root, repoRoot, options }) {
|
|
|
84
91
|
if (!new Set(["patch", "update"]).has(options.tier)) fail("add-on upload requires --tier patch|update");
|
|
85
92
|
const commit = git(repoRoot, ["rev-parse", "HEAD"]);
|
|
86
93
|
const sealed = path.join(repoRoot, ".right-release", "addons", config.addon, config.version, commit.slice(0, 8), options.platform);
|
|
87
|
-
const
|
|
88
|
-
validateAddonManifest(manifest);
|
|
89
|
-
const routes = addonRoutes(manifest);
|
|
94
|
+
const routes = publishR2Addon({ manifestPath: path.join(sealed, "addon-manifest.json"), repoRoot, config, commit, platform: options.platform, dryRun: options.dryRun });
|
|
90
95
|
if (options.dryRun) {
|
|
91
96
|
for (const route of routes) console.log(`would upload ${route.key}${route.manifest ? " (last)" : ""}`);
|
|
92
97
|
return;
|
|
93
98
|
}
|
|
94
|
-
if (!process.env.CLOUDFLARE_API_TOKEN) fail("CLOUDFLARE_API_TOKEN is required before R2 mutation");
|
|
95
|
-
for (const route of routes) {
|
|
96
|
-
const source = route.manifest ? path.join(sealed, "addon-manifest.json") : path.join(sealed, route.name);
|
|
97
|
-
await putImmutable({ source, route, repoRoot });
|
|
98
|
-
}
|
|
99
99
|
console.log(`right-release addon upload: verified ${config.addon} ${options.platform} tier=${options.tier}`);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
export function publishR2Addon({ manifestPath, repoRoot, config, commit, platform, dryRun = false }, { run = spawnSync, env = process.env, credentialStore } = {}) {
|
|
103
|
+
// Credential resolution precedes any read or mutation; dry runs only enforce managed-host refusal.
|
|
104
|
+
const token = resolveCloudflareApiToken({ env, credentialStore, required: !dryRun });
|
|
105
|
+
const manifest = validateAddonManifest(JSON.parse(readFileSync(manifestPath, "utf8")));
|
|
106
|
+
if (manifest.addon !== config.addon || manifest.version !== config.version || manifest.platform !== platform || manifest.commit !== commit) throw new Error("sealed add-on identity mismatch");
|
|
107
|
+
const routes = addonRoutes(manifest);
|
|
108
|
+
const artifacts = new Map(routes.map((route) => {
|
|
109
|
+
const source = path.join(path.dirname(manifestPath), route.name);
|
|
110
|
+
const entry = manifest.files.find((file) => file.name === route.name);
|
|
111
|
+
return [route.key, readVerifiedArtifact(source, { sha256: route.sha256, sizeBytes: entry?.sizeBytes })];
|
|
112
|
+
}));
|
|
113
|
+
if (dryRun) return routes;
|
|
114
|
+
// The token reaches only the Wrangler child environment, never the caller's env object.
|
|
115
|
+
const uploadEnv = { ...env, CLOUDFLARE_API_TOKEN: token };
|
|
116
|
+
for (const route of routes) putImmutable({ artifact: artifacts.get(route.key), route, repoRoot, run, env: uploadEnv });
|
|
117
|
+
return routes;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function putImmutable({ artifact, route, repoRoot, run, env }) {
|
|
103
121
|
const target = `rightapps-downloads/${route.key}`;
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
122
|
+
const temporary = mkdtempSync(path.join(os.tmpdir(), "right-addon-r2-"));
|
|
123
|
+
const temp = path.join(temporary, "object");
|
|
124
|
+
try {
|
|
125
|
+
const existing = run("pnpm", ["dlx", "wrangler@4", "r2", "object", "get", target, "--file", temp, "--remote"], { cwd: repoRoot, env, encoding: "utf8", windowsHide: true, shell: process.platform === "win32" });
|
|
126
|
+
if (existing.status === 0) {
|
|
127
|
+
if (sha256(temp) !== route.sha256) throw new Error(`immutable object exists with different bytes; remove remote key ${target}`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!/not found|does not exist|404/i.test(`${existing.stdout}\n${existing.stderr}`)) throw new Error(`cannot inspect immutable object: ${route.key}`);
|
|
131
|
+
uploadVerifiedR2Artifact(artifact, route.key, "public", { cwd: repoRoot, run, env });
|
|
132
|
+
} finally { rmSync(temporary, { recursive: true, force: true }); }
|
|
114
133
|
}
|
|
115
134
|
|
|
116
135
|
async function adopt(options) {
|
package/apple-profile-renew.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createHash, createPrivateKey, randomUUID, sign, X509Certificate } from "node:crypto";
|
|
2
|
-
import { constants, copyFileSync, existsSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { constants, copyFileSync, existsSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, realpathSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
import { spawnSync } from "./background-process.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { isManagedHost, refuseManagedEnvSecret } from "./managed-host-secrets.mjs";
|
|
7
|
+
import os from "node:os";
|
|
7
8
|
|
|
8
9
|
const ASC = "https://api.appstoreconnect.apple.com";
|
|
9
10
|
export class AppleProfileRenewError extends Error {
|
|
@@ -14,13 +15,29 @@ const sha1 = (bytes) => createHash("sha1").update(bytes).digest("hex").toUpperCa
|
|
|
14
15
|
const serial = (value) => String(value ?? "").replace(/[^a-f0-9]/gi, "").replace(/^0+/, "").toUpperCase();
|
|
15
16
|
const query = (endpoint, params) => `${endpoint}?${new URLSearchParams(params)}`;
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
const PEM_MARKER = "-----BEGIN";
|
|
19
|
+
const trimmed = (value) => String(value ?? "").trim();
|
|
20
|
+
const expandHome = (value) => value === "~" ? os.homedir() : value.startsWith("~/") ? path.join(os.homedir(), value.slice(2)) : value;
|
|
21
|
+
|
|
22
|
+
// Key ID and issuer are identifiers: the release config is read first, then the
|
|
23
|
+
// APPLE_API_KEY / APPLE_API_ISSUER environment on every host. The .p8 is a file:
|
|
24
|
+
// the config path is read first, then APPLE_API_KEY_PATH. No Keychain entry is
|
|
25
|
+
// needed. Managed hosts refuse any Apple environment value that carries inline
|
|
26
|
+
// PEM key material, because key material must never ride in the environment.
|
|
27
|
+
export function appleProfileAuth(env = process.env, { config = {}, root = process.cwd() } = {}) {
|
|
28
|
+
for (const name of ["APPLE_API_KEY_PATH", "APPLE_API_KEY", "APPLE_API_ISSUER"]) {
|
|
29
|
+
if (isManagedHost(env) && trimmed(env[name]).includes(PEM_MARKER)) refuseManagedEnvSecret(name);
|
|
30
|
+
}
|
|
31
|
+
const asc = config.appStoreConnect ?? {};
|
|
32
|
+
const keyId = trimmed(asc.keyId) || trimmed(env.APPLE_API_KEY);
|
|
33
|
+
const issuer = trimmed(asc.issuer) || trimmed(env.APPLE_API_ISSUER);
|
|
34
|
+
if (!issuer) fail("APPLE_PROFILE_ISSUER_MISSING", "Set appStoreConnect.issuer in the release config or APPLE_API_ISSUER.");
|
|
35
|
+
if (!keyId) fail("APPLE_PROFILE_KEY_MISSING", "Set appStoreConnect.keyId in the release config or APPLE_API_KEY.");
|
|
36
|
+
const configuredPath = trimmed(asc.keyPath);
|
|
37
|
+
const keyPath = configuredPath ? path.resolve(root, expandHome(configuredPath)) : expandHome(trimmed(env.APPLE_API_KEY_PATH));
|
|
38
|
+
if (!keyPath) fail("APPLE_PROFILE_KEY_MISSING", "Set appStoreConnect.keyPath in the release config or APPLE_API_KEY_PATH.");
|
|
39
|
+
if (!existsSync(keyPath) || !statSync(keyPath).isFile()) fail("APPLE_PROFILE_KEY_MISSING", "Configured App Store Connect API key file is missing.");
|
|
40
|
+
return { keyPath, keyId, issuer };
|
|
24
41
|
}
|
|
25
42
|
|
|
26
43
|
// Only this in-process signer reads key material. Never send it to a subprocess,
|
|
@@ -124,7 +141,7 @@ export async function renewAppleProfile({ root, config = {}, profilePath, dryRun
|
|
|
124
141
|
const previous = decodeAppleProfile(file, { run });
|
|
125
142
|
const info = describeAppleProfile(previous);
|
|
126
143
|
const certificate = findAppleSigningCertificate(info, { env, run, now });
|
|
127
|
-
const auth = appleProfileAuth(env);
|
|
144
|
+
const auth = appleProfileAuth(env, { config, root });
|
|
128
145
|
const token = await jwtSigner(auth, now);
|
|
129
146
|
const calls = [];
|
|
130
147
|
const request = async (method, endpoint, body) => {
|
|
@@ -215,6 +232,7 @@ export async function main(args = process.argv.slice(2), options = {}) {
|
|
|
215
232
|
return renewAppleProfile({ ...options, root: path.dirname(absolute), config: loaded.default ?? loaded.config ?? loaded, profilePath, dryRun });
|
|
216
233
|
}
|
|
217
234
|
|
|
218
|
-
|
|
235
|
+
const realOrSelf = (file) => { try { return realpathSync(file); } catch { return path.resolve(file); } };
|
|
236
|
+
if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
|
|
219
237
|
main().catch((error) => { console.error(`right-release apple profile renew: [${error.code ?? "APPLE_PROFILE_FAILED"}] ${error instanceof AppleProfileRenewError ? error.message : "Renewal failed; check app configuration & local file access."}`); process.exitCode = 1; });
|
|
220
238
|
}
|
package/artifact-storage.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveManagedRoots } from './managed-roots.mjs';
|
|
1
2
|
import {
|
|
2
3
|
existsSync,
|
|
3
4
|
copyFileSync,
|
|
@@ -11,17 +12,20 @@ import {
|
|
|
11
12
|
writeFileSync,
|
|
12
13
|
} from "node:fs";
|
|
13
14
|
import path from "node:path";
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import { spawnSync } from "./background-process.mjs";
|
|
14
17
|
|
|
15
18
|
export const ARTIFACT_MARKER_SCHEMA = 1;
|
|
16
|
-
export const DEFAULT_MANAGED_ROOT =
|
|
17
|
-
? "D:\\.rightkit-managed"
|
|
18
|
-
: "/Volumes/D/.rightkit-managed";
|
|
19
|
+
export const DEFAULT_MANAGED_ROOT = resolveManagedRoots().dataRoot;
|
|
19
20
|
|
|
20
21
|
const SAFE_COMPONENT = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,159}$/;
|
|
21
22
|
|
|
22
|
-
export function resolveManagedRoot({ env = process.env } = {}) {
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
export function resolveManagedRoot({ env = process.env, exists = existsSync } = {}) {
|
|
24
|
+
if (env.RIGHTKIT_DATA_ROOT || env.RIGHTKIT_MANAGED_ROOT || env.RIGHTKIT_EXTERNAL_ROOT) return path.resolve(resolveManagedRoots({ env }).dataRoot);
|
|
25
|
+
// Workstation default only when its drive exists; hosted CI runners fall back to RUNNER_TEMP, then the system temp dir.
|
|
26
|
+
const volume = process.platform === "win32" ? "D:\\" : "/Volumes/D";
|
|
27
|
+
if (exists(volume)) return path.resolve(DEFAULT_MANAGED_ROOT);
|
|
28
|
+
return path.join(env.RUNNER_TEMP && exists(env.RUNNER_TEMP) ? env.RUNNER_TEMP : os.tmpdir(), "rightkit-managed");
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
export function resolveArtifactRoot({ app, managedRoot } = {}) {
|
|
@@ -55,8 +59,11 @@ export function publishedPointerPath({ app, managedRoot } = {}) {
|
|
|
55
59
|
* Create or refresh the lease marker before a build writes managed artifacts.
|
|
56
60
|
* Marker fields intentionally remain the small cross-process contract.
|
|
57
61
|
*/
|
|
58
|
-
export function beginArtifactLease({ app, version, managedRoot, now = Date.now(), ownerPid = process.pid,
|
|
59
|
-
|
|
62
|
+
export function beginArtifactLease({ app, version, managedRoot, now = Date.now(), ownerPid = process.pid,
|
|
63
|
+
ownerHost = os.hostname(), processIdentity = artifactProcessIdentity,
|
|
64
|
+
log = (event) => console.error(JSON.stringify(event)), leaseMs = 2 * 60 * 60 * 1000 } = {}) {
|
|
65
|
+
if (!Number.isFinite(now) || !Number.isSafeInteger(ownerPid) || ownerPid < 1 || !Number.isFinite(leaseMs) || leaseMs <= 0
|
|
66
|
+
|| typeof ownerHost !== "string" || !ownerHost) {
|
|
60
67
|
throw new TypeError("artifact lease requires finite now/leaseMs and positive ownerPid");
|
|
61
68
|
}
|
|
62
69
|
const root = resolveArtifactRoot({ app, managedRoot });
|
|
@@ -68,14 +75,24 @@ export function beginArtifactLease({ app, version, managedRoot, now = Date.now()
|
|
|
68
75
|
ensureDirectory(directory);
|
|
69
76
|
const file = markerPath({ app, version, managedRoot });
|
|
70
77
|
assertSafeMutation(file);
|
|
78
|
+
const identity = processIdentity(ownerPid);
|
|
79
|
+
let reclaimed;
|
|
71
80
|
if (existsSync(file)) {
|
|
72
81
|
const previous = readArtifactMarker(file);
|
|
73
|
-
|
|
82
|
+
const state = leaseOwnerState(previous, { host: ownerHost, processIdentity });
|
|
83
|
+
const sameOwner = previous.ownerPid === ownerPid && (!previous.ownerHost || previous.ownerHost === ownerHost)
|
|
84
|
+
&& (!previous.ownerStartTime || !identity.startTime || previous.ownerStartTime === identity.startTime);
|
|
85
|
+
if (state !== "dead-pid" && state !== "pid-reused" && isActiveLease(previous, now, { host: ownerHost, processIdentity }) && !sameOwner) {
|
|
74
86
|
throw new Error(`artifact lease is held by PID ${previous.ownerPid}: ${app}@${version}`);
|
|
75
87
|
}
|
|
88
|
+
if (state === "dead-pid" || state === "pid-reused") reclaimed = { event: "artifact-lease-reclaimed", app, version,
|
|
89
|
+
reason: state, ownerPid: previous.ownerPid, ownerHost: previous.ownerHost ?? ownerHost,
|
|
90
|
+
ownerStartTime: previous.ownerStartTime ?? null, newOwnerPid: ownerPid, timestamp: now };
|
|
76
91
|
}
|
|
77
|
-
const marker = { schemaVersion: ARTIFACT_MARKER_SCHEMA, app, version, createdAt: now, ownerPid,
|
|
92
|
+
const marker = { schemaVersion: ARTIFACT_MARKER_SCHEMA, app, version, createdAt: now, ownerPid,
|
|
93
|
+
ownerHost, ownerStartTime: identity.startTime ?? null, leaseUntil: now + leaseMs };
|
|
78
94
|
writeJson(file, marker);
|
|
95
|
+
if (reclaimed) log(reclaimed);
|
|
79
96
|
return marker;
|
|
80
97
|
} finally {
|
|
81
98
|
lock.release();
|
|
@@ -108,6 +125,8 @@ export function readArtifactMarker(file) {
|
|
|
108
125
|
if (!Number.isFinite(marker[key])) throw new Error(`artifact marker ${key} must be numeric: ${file}`);
|
|
109
126
|
}
|
|
110
127
|
if (marker.completedAt != null && !Number.isFinite(marker.completedAt)) throw new Error(`artifact marker completedAt must be numeric: ${file}`);
|
|
128
|
+
if (marker.ownerHost != null && (typeof marker.ownerHost !== "string" || !marker.ownerHost)) throw new Error(`artifact marker ownerHost must be a nonempty string: ${file}`);
|
|
129
|
+
if (marker.ownerStartTime != null && (typeof marker.ownerStartTime !== "string" || !marker.ownerStartTime)) throw new Error(`artifact marker ownerStartTime must be a nonempty string: ${file}`);
|
|
111
130
|
return marker;
|
|
112
131
|
}
|
|
113
132
|
|
|
@@ -312,11 +331,33 @@ function safeRemove(target) {
|
|
|
312
331
|
rmSync(target, { recursive: true, force: true });
|
|
313
332
|
}
|
|
314
333
|
|
|
315
|
-
function
|
|
316
|
-
if (Number(marker.ownerPid)
|
|
334
|
+
function leaseOwnerState(marker, { host = os.hostname(), processIdentity = artifactProcessIdentity } = {}) {
|
|
335
|
+
if (Number(marker.ownerPid) <= 0 || (marker.ownerHost && marker.ownerHost !== host)) return "unknown";
|
|
336
|
+
const identity = processIdentity(marker.ownerPid);
|
|
337
|
+
if (!identity.alive) return "dead-pid";
|
|
338
|
+
if (marker.ownerStartTime && identity.startTime && marker.ownerStartTime !== identity.startTime) return "pid-reused";
|
|
339
|
+
return "live";
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function isActiveLease(marker, now, options) {
|
|
343
|
+
const state = leaseOwnerState(marker, options);
|
|
344
|
+
if (state === "live") return true;
|
|
345
|
+
if (state === "dead-pid" || state === "pid-reused") return false;
|
|
317
346
|
return Number(marker.leaseUntil) > Number(now);
|
|
318
347
|
}
|
|
319
348
|
|
|
349
|
+
/** Start identity is queried on the owning host; denied inspection stays conservative. */
|
|
350
|
+
export function artifactProcessIdentity(pid, { platform = process.platform, run = spawnSync, alive = processAlive } = {}) {
|
|
351
|
+
if (!alive(pid)) return { alive: false, startTime: null };
|
|
352
|
+
const result = platform === "win32"
|
|
353
|
+
? run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command",
|
|
354
|
+
`(Get-Process -Id ${Number(pid)} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks`], { encoding: "utf8", timeout: 3000, shell: false })
|
|
355
|
+
: run("/bin/ps", ["-p", String(pid), "-o", "lstart="], {
|
|
356
|
+
encoding: "utf8", timeout: 1000, shell: false, env: { ...process.env, LC_ALL: "C", TZ: "UTC" },
|
|
357
|
+
});
|
|
358
|
+
return { alive: true, startTime: result.status === 0 && result.stdout?.trim() || null };
|
|
359
|
+
}
|
|
360
|
+
|
|
320
361
|
function acquireRetentionLock(root, now) {
|
|
321
362
|
ensureDirectory(root);
|
|
322
363
|
const directory = path.join(root, ".retention-lock");
|
package/background-process.mjs
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import childProcess from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
|
|
4
|
+
// Windows PowerShell's inbox ACL cmdlets can fail while importing legacy
|
|
5
|
+
// module type data when inherited PSModulePath entries are present.
|
|
6
|
+
export function windowsPowerShellEnvironment(overrides = {}, base = process.env) {
|
|
7
|
+
const environment = { ...base, ...overrides };
|
|
8
|
+
for (const key of Object.keys(environment)) {
|
|
9
|
+
if (key.toLowerCase() === "psmodulepath") delete environment[key];
|
|
10
|
+
}
|
|
11
|
+
return environment;
|
|
12
|
+
}
|
|
13
|
+
|
|
4
14
|
// Release tooling has no interactive console children. Keep this policy at
|
|
5
15
|
// every Node launch boundary, including fixtures & nested release commands.
|
|
6
16
|
function hiddenArguments(args, options) {
|
|
@@ -17,6 +27,14 @@ export function spawnSync(command, args, options) {
|
|
|
17
27
|
return childProcess.spawnSync(command, ...hiddenArguments(args, options));
|
|
18
28
|
}
|
|
19
29
|
|
|
30
|
+
// Native ownership ceremonies must be able to show their Windows Hello UI.
|
|
31
|
+
// Keep this exception explicit so ordinary release subprocesses stay hidden.
|
|
32
|
+
export function spawnSyncInteractive(command, args, options) {
|
|
33
|
+
return childProcess.spawnSync(command, ...(Array.isArray(args)
|
|
34
|
+
? [args, { ...options, shell: false, windowsHide: false }]
|
|
35
|
+
: [[], { ...(args ?? options), shell: false, windowsHide: false }]));
|
|
36
|
+
}
|
|
37
|
+
|
|
20
38
|
export function execFileSync(file, args, options) {
|
|
21
39
|
return childProcess.execFileSync(file, ...hiddenArguments(args, options));
|
|
22
40
|
}
|
package/build-release.mjs
CHANGED
|
@@ -39,13 +39,14 @@ import { acquireHeavyWorkSlot, heavyCommandEnvironment, summarizePreBrokerWaits,
|
|
|
39
39
|
import { processGroupCpuSeconds } from "./process-liveness.mjs";
|
|
40
40
|
import { cancelAndReapManagedRequest, firstManagedRequestId, releaseProgressLimits } from "./progress-control.mjs";
|
|
41
41
|
import { beginArtifactLease, completeArtifactLease, materializeArtifacts, retainArtifacts, resolveArtifactDir, resolveManagedRoot } from "./artifact-storage.mjs";
|
|
42
|
+
import { macosCodesignEnvironment } from "./codesign-environment.mjs";
|
|
42
43
|
|
|
43
44
|
// Fingerprint of the pipeline code that can change the bytes we ship or the way
|
|
44
45
|
// they are signed — deliberately NOT the package version, which moves for docs
|
|
45
46
|
// and test-only edits and would force a cold Rust rebuild of every app each time.
|
|
46
47
|
// Cached target directories must not outlive changes to these signing helpers.
|
|
47
48
|
|
|
48
|
-
const PIPELINE_FINGERPRINT_SOURCES = ["release.mjs", "sign-windows.mjs", "sign-macos.mjs", "macos-signing-session.mjs", "notarize-macos.mjs", "macos-notarization.mjs", "signing-receipts.mjs", "tauri-bundle-marker.mjs", "nsis-payload.mjs"];
|
|
49
|
+
const PIPELINE_FINGERPRINT_SOURCES = ["release.mjs", "sign-windows.mjs", "sign-macos.mjs", "macos-signing-session.mjs", "codesign-environment.mjs", "codesign-shim.mjs", "codesign-argv.mjs", "osascript-guard.mjs", "dmg-build.mjs", "dmg-layout.mjs", "rightkit-signing-agent.mjs", "notarize-macos.mjs", "macos-notarization.mjs", "signing-receipts.mjs", "tauri-bundle-marker.mjs", "nsis-payload.mjs"];
|
|
49
50
|
const PIPELINE_FINGERPRINT = createHash("sha256")
|
|
50
51
|
.update(PIPELINE_FINGERPRINT_SOURCES.map((file) => `${file}:${createHash("sha256").update(readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), file))).digest("hex")}`).join("\n"))
|
|
51
52
|
.digest("hex")
|
|
@@ -251,7 +252,7 @@ try {
|
|
|
251
252
|
// resolves to MSYS Perl, which cannot configure OpenSSL. Put the real Windows
|
|
252
253
|
// Perl first so the build behaves the same from any shell.
|
|
253
254
|
const pathPrefix = buildPathPrefix({ platform });
|
|
254
|
-
const env = {
|
|
255
|
+
const env = macosCodesignEnvironment({
|
|
255
256
|
...heavyCommandEnvironment(process.env),
|
|
256
257
|
...releaseEnvironment({ root: vaultRoot, cacheRoot: sharedCacheRoot, platform, architecture: cacheIdentity.architecture, app: config.app, cacheKey, kind: "release", appRoot, mode: cacheMode }),
|
|
257
258
|
...(pathPrefix ? { PATH: `${pathPrefix}${path.delimiter}${process.env.PATH ?? ""}` } : {}),
|
|
@@ -259,7 +260,7 @@ try {
|
|
|
259
260
|
RIGHT_RELEASE_APP_ROOT: layout.appRoot,
|
|
260
261
|
RIGHTKIT_MANAGED_ROOT: resolveManagedRoot({ env: process.env }),
|
|
261
262
|
RIGHT_RELEASE_ARTIFACT_DIR: resolveArtifactDir({ app: config.app, version: config.version, managedRoot: resolveManagedRoot({ env: process.env }) }),
|
|
262
|
-
};
|
|
263
|
+
});
|
|
263
264
|
if (managedCargoTarget) {
|
|
264
265
|
for (const name of ["CARGO_HOME", "CARGO_TARGET_DIR", "RUSTC_WRAPPER", "SCCACHE_DIR", "SCCACHE_BASEDIRS", "SCCACHE_CACHE_SIZE", "RUSTFLAGS"]) {
|
|
265
266
|
delete env[name];
|
package/cache-command.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
import {
|
|
6
6
|
applyCachePrune,
|
|
7
7
|
inspectCache,
|
|
@@ -87,6 +87,7 @@ function rejectUnknown(args, allowed) { for (const arg of args) if (!allowed.has
|
|
|
87
87
|
function platformFrom(env) { return env.RIGHT_RELEASE_PLATFORM || (process.platform === "win32" ? "win" : process.platform === "darwin" ? "mac" : process.platform); }
|
|
88
88
|
function findRepoRoot(start) { let current = path.resolve(start); for (;;) { if (existsSync(path.join(current, ".git"))) return current; const parent = path.dirname(current); if (parent === current) throw new Error(`cache migrate could not find Git repository for ${start}`); current = parent; } }
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
const realOrSelf = (file) => { try { return realpathSync(file); } catch { return path.resolve(file); } };
|
|
91
|
+
if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
|
|
91
92
|
runCacheCommand().catch((error) => { console.error(error.message); process.exitCode = 2; });
|
|
92
93
|
}
|
package/cargo-contract.mjs
CHANGED
|
@@ -21,7 +21,7 @@ export function managedCargoShimOnPath(env = process.env, platform = process.pla
|
|
|
21
21
|
for (const extension of extensions) {
|
|
22
22
|
const candidate = pathApi.join(directory, `cargo${extension.toLowerCase()}`);
|
|
23
23
|
if (!exists(candidate)) continue;
|
|
24
|
-
return /(?:^|\/)(?:\.rightkit-managed|rightkitmanagedagent)\/agent-bin\/cargo(?:\.[^/]*)?$/i
|
|
24
|
+
return /(?:^|\/)(?:\.rightkit-managed|rightkitmanagedagent|rightkit\/build-control)\/agent-bin\/cargo(?:\.[^/]*)?$/i
|
|
25
25
|
.test(candidate.replaceAll("\\", "/"));
|
|
26
26
|
}
|
|
27
27
|
}
|
package/cargo-guard.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn, spawnSync } from "./background-process.mjs";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -136,7 +136,7 @@ export function resolveRealRustc({ env = process.env, run = spawnSync } = {}) {
|
|
|
136
136
|
export function resolveManagedCargoShim({ env = process.env, platform = process.platform, exists = existsSync } = {}) {
|
|
137
137
|
const win = platform === "win32" || platform === "win";
|
|
138
138
|
const delimiter = win ? ";" : ":";
|
|
139
|
-
const agentBin = /(?:^|[\\/])(?:\.rightkit-managed|rightkitmanagedagent)[\\/]agent-bin[\\/]?$/i;
|
|
139
|
+
const agentBin = /(?:^|[\\/])(?:\.rightkit-managed|rightkitmanagedagent|rightkit[\\/]build-control)[\\/]agent-bin[\\/]?$/i;
|
|
140
140
|
for (const directory of String(env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
141
141
|
if (!agentBin.test(directory)) continue;
|
|
142
142
|
for (const name of win ? ["cargo.exe", "cargo.cmd"] : ["cargo"]) {
|
|
@@ -254,7 +254,8 @@ export async function runCargoGuard(args, { env = process.env, policyOptions = {
|
|
|
254
254
|
return runLight(cargo, args, env);
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
|
|
257
|
+
const realOrSelf = (file) => { try { return realpathSync(file); } catch { return path.resolve(file); } };
|
|
258
|
+
if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
|
|
258
259
|
const args = process.argv.slice(2);
|
|
259
260
|
if (args[0] === "--check-hook-base64") {
|
|
260
261
|
try { checkHookInput(Buffer.from(args[1] ?? "", "base64url").toString("utf8")); }
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
2
|
+
import { PublicationError } from "./publication-artifacts.mjs";
|
|
3
|
+
|
|
4
|
+
// https://doc.rust-lang.org/cargo/reference/registry-index.html#index-files
|
|
5
|
+
export function cargoIndexUrl(name) {
|
|
6
|
+
if (!/^[a-z0-9_-]+$/i.test(name)) throw new Error(`invalid crate name: ${name}`);
|
|
7
|
+
const lower = name.toLowerCase();
|
|
8
|
+
const prefix = lower.length === 1 ? "1" : lower.length === 2 ? "2"
|
|
9
|
+
: lower.length === 3 ? `3/${lower[0]}` : `${lower.slice(0, 2)}/${lower.slice(2, 4)}`;
|
|
10
|
+
return `https://index.crates.io/${prefix}/${lower}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function verifyCargoRegistry({ name, version, sha256 }, deps = {}) {
|
|
14
|
+
const fetchImpl = deps.fetch ?? fetch;
|
|
15
|
+
const wait = deps.sleep ?? sleep;
|
|
16
|
+
const now = deps.now ?? (() => performance.now());
|
|
17
|
+
const timeoutMs = deps.timeoutMs ?? 300_000;
|
|
18
|
+
const deadline = now() + timeoutMs;
|
|
19
|
+
const url = cargoIndexUrl(name);
|
|
20
|
+
let delayMs = 1_000;
|
|
21
|
+
while (now() < deadline) {
|
|
22
|
+
let entry;
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetchImpl(url, {
|
|
25
|
+
headers: { Accept: "text/plain", "Cache-Control": "no-cache", "User-Agent": "rightkit-release" },
|
|
26
|
+
signal: AbortSignal.timeout(Math.max(1, Math.ceil(Math.min(30_000, deadline - now())))), redirect: "error",
|
|
27
|
+
});
|
|
28
|
+
if (response.ok) {
|
|
29
|
+
entry = (await response.text()).split(/\r?\n/).filter((line) => line.trim())
|
|
30
|
+
.map((line) => JSON.parse(line)).find((record) => record.name === name && record.vers === version);
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
// Index propagation, HTTP errors & transient transport failures share this bounded retry.
|
|
34
|
+
}
|
|
35
|
+
if (entry) {
|
|
36
|
+
if (entry.cksum !== sha256) {
|
|
37
|
+
const error = new PublicationError("PUBLICATION_REGISTRY_MISMATCH",
|
|
38
|
+
`${name}@${version}: verified sha256 ${sha256}, registry sha256 ${entry.cksum ?? "<missing>"}. Operator must yank this version with cargo yank --vers ${version} ${name}; no automatic yank was performed.`);
|
|
39
|
+
error.expectedSha256 = sha256;
|
|
40
|
+
error.registrySha256 = entry.cksum;
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
return { name, version, sha256 };
|
|
44
|
+
}
|
|
45
|
+
const remainingMs = deadline - now();
|
|
46
|
+
if (remainingMs <= 0) break;
|
|
47
|
+
await wait(Math.min(delayMs, remainingMs));
|
|
48
|
+
delayMs = Math.min(delayMs * 2, 15_000);
|
|
49
|
+
}
|
|
50
|
+
throw new PublicationError("PUBLICATION_REGISTRY_TIMEOUT", `${name}@${version} checksum was not visible at ${url} within ${timeoutMs}ms`);
|
|
51
|
+
}
|
package/cargo-target.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// (see cargo-contract.mjs) remains a separate, cache-policy-only decision:
|
|
10
10
|
// whether Cache V2's lease/slot/prune/floor machinery should run at all, not
|
|
11
11
|
// where the target directory lives.
|
|
12
|
+
import { realpathSync } from "node:fs";
|
|
12
13
|
import { execFileSync } from "./background-process.mjs";
|
|
13
14
|
import { runCargoSync } from "./managed-cargo.mjs";
|
|
14
15
|
import path from "node:path";
|
|
@@ -38,7 +39,8 @@ export function resolveTargetRoot(manifestPath, options = {}) {
|
|
|
38
39
|
return path.resolve(directory);
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
const realOrSelf = (file) => { try { return realpathSync(file); } catch { return path.resolve(file); } };
|
|
43
|
+
if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
|
|
42
44
|
const manifestPath = process.argv[2];
|
|
43
45
|
if (!manifestPath) {
|
|
44
46
|
console.error("usage: cargo-target.mjs <path-to-Cargo.toml>");
|