@rightkit/release 0.2.135 → 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 +47 -10
- package/background-process.mjs +18 -0
- package/build-release.mjs +1 -1
- 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-environment.mjs +26 -2
- package/codesign-shim.mjs +2 -1
- package/committed-source.mjs +3 -3
- package/development.mjs +7 -7
- 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-signing-session.mjs +3 -0
- 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/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 +2 -2
- 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/rightkit-drift.mjs +3 -2
- package/rightkit-ownership.json +2028 -139
- package/rightkit-signing-agent.mjs +5 -3
- package/rightkit-sync.mjs +6 -2
- package/rightkit-versions.json +213 -103
- package/runtime-artifact-manifest.mjs +20 -6
- package/sign-updater.mjs +5 -0
- package/signing-agent-client.mjs +2 -1
- package/signing-agent-install.mjs +3 -2
- 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,
|
|
@@ -12,16 +13,15 @@ import {
|
|
|
12
13
|
} from "node:fs";
|
|
13
14
|
import path from "node:path";
|
|
14
15
|
import os from "node:os";
|
|
16
|
+
import { spawnSync } from "./background-process.mjs";
|
|
15
17
|
|
|
16
18
|
export const ARTIFACT_MARKER_SCHEMA = 1;
|
|
17
|
-
export const DEFAULT_MANAGED_ROOT =
|
|
18
|
-
? "D:\\.rightkit-managed"
|
|
19
|
-
: "/Volumes/D/.rightkit-managed";
|
|
19
|
+
export const DEFAULT_MANAGED_ROOT = resolveManagedRoots().dataRoot;
|
|
20
20
|
|
|
21
21
|
const SAFE_COMPONENT = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,159}$/;
|
|
22
22
|
|
|
23
23
|
export function resolveManagedRoot({ env = process.env, exists = existsSync } = {}) {
|
|
24
|
-
if (env.RIGHTKIT_MANAGED_ROOT) return path.resolve(env.
|
|
24
|
+
if (env.RIGHTKIT_DATA_ROOT || env.RIGHTKIT_MANAGED_ROOT || env.RIGHTKIT_EXTERNAL_ROOT) return path.resolve(resolveManagedRoots({ env }).dataRoot);
|
|
25
25
|
// Workstation default only when its drive exists; hosted CI runners fall back to RUNNER_TEMP, then the system temp dir.
|
|
26
26
|
const volume = process.platform === "win32" ? "D:\\" : "/Volumes/D";
|
|
27
27
|
if (exists(volume)) return path.resolve(DEFAULT_MANAGED_ROOT);
|
|
@@ -59,8 +59,11 @@ export function publishedPointerPath({ app, managedRoot } = {}) {
|
|
|
59
59
|
* Create or refresh the lease marker before a build writes managed artifacts.
|
|
60
60
|
* Marker fields intentionally remain the small cross-process contract.
|
|
61
61
|
*/
|
|
62
|
-
export function beginArtifactLease({ app, version, managedRoot, now = Date.now(), ownerPid = process.pid,
|
|
63
|
-
|
|
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) {
|
|
64
67
|
throw new TypeError("artifact lease requires finite now/leaseMs and positive ownerPid");
|
|
65
68
|
}
|
|
66
69
|
const root = resolveArtifactRoot({ app, managedRoot });
|
|
@@ -72,14 +75,24 @@ export function beginArtifactLease({ app, version, managedRoot, now = Date.now()
|
|
|
72
75
|
ensureDirectory(directory);
|
|
73
76
|
const file = markerPath({ app, version, managedRoot });
|
|
74
77
|
assertSafeMutation(file);
|
|
78
|
+
const identity = processIdentity(ownerPid);
|
|
79
|
+
let reclaimed;
|
|
75
80
|
if (existsSync(file)) {
|
|
76
81
|
const previous = readArtifactMarker(file);
|
|
77
|
-
|
|
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) {
|
|
78
86
|
throw new Error(`artifact lease is held by PID ${previous.ownerPid}: ${app}@${version}`);
|
|
79
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 };
|
|
80
91
|
}
|
|
81
|
-
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 };
|
|
82
94
|
writeJson(file, marker);
|
|
95
|
+
if (reclaimed) log(reclaimed);
|
|
83
96
|
return marker;
|
|
84
97
|
} finally {
|
|
85
98
|
lock.release();
|
|
@@ -112,6 +125,8 @@ export function readArtifactMarker(file) {
|
|
|
112
125
|
if (!Number.isFinite(marker[key])) throw new Error(`artifact marker ${key} must be numeric: ${file}`);
|
|
113
126
|
}
|
|
114
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}`);
|
|
115
130
|
return marker;
|
|
116
131
|
}
|
|
117
132
|
|
|
@@ -316,11 +331,33 @@ function safeRemove(target) {
|
|
|
316
331
|
rmSync(target, { recursive: true, force: true });
|
|
317
332
|
}
|
|
318
333
|
|
|
319
|
-
function
|
|
320
|
-
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;
|
|
321
346
|
return Number(marker.leaseUntil) > Number(now);
|
|
322
347
|
}
|
|
323
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
|
+
|
|
324
361
|
function acquireRetentionLock(root, now) {
|
|
325
362
|
ensureDirectory(root);
|
|
326
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
|
@@ -46,7 +46,7 @@ import { macosCodesignEnvironment } from "./codesign-environment.mjs";
|
|
|
46
46
|
// and test-only edits and would force a cold Rust rebuild of every app each time.
|
|
47
47
|
// Cached target directories must not outlive changes to these signing helpers.
|
|
48
48
|
|
|
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", "rightkit-signing-agent.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"];
|
|
50
50
|
const PIPELINE_FINGERPRINT = createHash("sha256")
|
|
51
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"))
|
|
52
52
|
.digest("hex")
|
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>");
|
package/cli/right-release.mjs
CHANGED
|
@@ -42,6 +42,7 @@ try {
|
|
|
42
42
|
if (githubBuildCommands.has(first)
|
|
43
43
|
|| first?.startsWith("-")
|
|
44
44
|
|| (first === "addon" && args[1] === "build")
|
|
45
|
+
|| (first === "dmg" && args[1] === "build")
|
|
45
46
|
|| (first === "publish" && ["cargo", "swift"].includes(args[1]))) {
|
|
46
47
|
assertReleaseExecutionAllowed({ command: `right-release ${args.join(" ")}`.trim() });
|
|
47
48
|
}
|
|
@@ -63,6 +64,8 @@ if (first === "--version" || first === "-v") {
|
|
|
63
64
|
} else {
|
|
64
65
|
run("release.mjs", ["--doctor", ...args.slice(1)]);
|
|
65
66
|
}
|
|
67
|
+
} else if (first === "hooks") {
|
|
68
|
+
run("ownership-hooks.mjs", args.slice(1));
|
|
66
69
|
} else if (first === "suite-doctor") {
|
|
67
70
|
runTest(
|
|
68
71
|
["--test", path.join(packageRoot, "right-suite-contract.test.mjs")],
|
|
@@ -70,6 +73,10 @@ if (first === "--version" || first === "-v") {
|
|
|
70
73
|
);
|
|
71
74
|
} else if (first === "cache") {
|
|
72
75
|
run("cache-command.mjs", args.slice(1));
|
|
76
|
+
} else if (first === "install-app") {
|
|
77
|
+
run("install-app.mjs", args.slice(1));
|
|
78
|
+
} else if (first === "dmg") {
|
|
79
|
+
run("dmg-build.mjs", args.slice(1));
|
|
73
80
|
} else if (first === "apple") {
|
|
74
81
|
run("apple-profile-renew.mjs", args.slice(1));
|
|
75
82
|
} else if (first === "publish") {
|
|
@@ -83,6 +90,12 @@ if (first === "--version" || first === "-v") {
|
|
|
83
90
|
} else {
|
|
84
91
|
run("upload-release.mjs", rest);
|
|
85
92
|
}
|
|
93
|
+
} else if (first === "inventory") {
|
|
94
|
+
if (args[1] !== "check") {
|
|
95
|
+
console.error("right-release inventory: expected check");
|
|
96
|
+
process.exit(2);
|
|
97
|
+
}
|
|
98
|
+
run("inventory-check.mjs", args.slice(2));
|
|
86
99
|
} else if (first === "model") {
|
|
87
100
|
const rest = args.slice(1);
|
|
88
101
|
if (rest[0] !== "promote") {
|
|
@@ -155,12 +168,20 @@ function printHelp() {
|
|
|
155
168
|
console.log(`right-release <command> [options]
|
|
156
169
|
|
|
157
170
|
Commands:
|
|
171
|
+
dmg build --app <.app> --out <dmg> --volname <name> [--background <png>]
|
|
172
|
+
[--icon-size N] [--app-position x,y] [--applications-position x,y] [--window w,h]
|
|
173
|
+
Build branded UDZO image without Finder
|
|
174
|
+
install-app --dmg <path.dmg> [--app-name Pulse.app] [--bundle-id <id>] [--dest /Applications]
|
|
175
|
+
[--relaunch|--no-relaunch] [--json]
|
|
176
|
+
Verify a signed DMG, quit, replace & relaunch hidden; no UI, no osascript (macOS)
|
|
158
177
|
build [--platform mac|win] [--skip-checks] [--resume-from <phase>]
|
|
159
178
|
Resume macOS at prepackage, sign, package, post-sign, or harden
|
|
160
179
|
dev --platform win|mac [--config <path>] [--dry-run] [--build-only]
|
|
161
180
|
Incremental unsigned build & optional local install
|
|
162
181
|
upload --release <id> --platform mac|win --tier patch|update
|
|
163
182
|
Upload one sealed release; never builds
|
|
183
|
+
publish-update --release <id> --platform mac|win --tier patch|update
|
|
184
|
+
Upload sealed bytes to R2 & register RightApps
|
|
164
185
|
release Compatibility alias for build
|
|
165
186
|
publish Compatibility alias for upload
|
|
166
187
|
publish npm --package @rightkit/<name> [--dry-run] Gate preflight for npm publish
|
|
@@ -169,6 +190,8 @@ Commands:
|
|
|
169
190
|
(npm-rightkit-publish) and verify registry bytes
|
|
170
191
|
publish cargo --crate <name> [--dry-run] Test, inspect, scan, and publish one crates.io package
|
|
171
192
|
publish swift [--repo <git-url>] [--dry-run] Test, tag, resolve a fresh exact-pin consumer, and publish RightKitSwift to its git repo
|
|
193
|
+
inventory check [--json] [--warn-only] [--md <file>] [--no-cache]
|
|
194
|
+
Registry, account, dependency, app & hygiene drift (public endpoints)
|
|
172
195
|
model promote --authority <app-key> --config <file> [--dry-run]
|
|
173
196
|
Sign and promote one runtime/model artifact
|
|
174
197
|
addon doctor|build|upload|adopt <options> Manage a signed headless add-on
|
|
@@ -179,10 +202,13 @@ Commands:
|
|
|
179
202
|
doctor --all Verify all Right Suite app release contracts
|
|
180
203
|
suite-doctor Verify all local Right Suite repositories
|
|
181
204
|
ownership [--app <dir>] [--json] Fail on app copies of RightKit-owned capabilities
|
|
205
|
+
ownership --staged [--app <dir>] [--json] Pre-commit gate: scan the staged index for block-tier findings
|
|
206
|
+
hooks install [--app <dir>] Install the advisory pre-commit ownership gate (pinned @rightkit/release)
|
|
182
207
|
ownership enroll [--root] [--name <device>] [--root-fingerprint <sha256>] [--app <dir>]
|
|
183
208
|
Enroll one native key or create a signed-device request
|
|
184
209
|
ownership approve-key <fingerprint|request.json> Approve one pending device key from an active host
|
|
185
210
|
ownership revoke-key <fingerprint> Revoke one active device key
|
|
211
|
+
ownership trust-sync Advance this host's protected trust to the committed key log
|
|
186
212
|
ownership-approval --draft <input.json> [--out <draft.json>]
|
|
187
213
|
Draft an exception for native owner approval
|
|
188
214
|
ownership-approval --verify <signed.json> --context <scan.json>
|