@rightkit/release 0.2.111 → 0.2.113
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/cargo-contract.mjs +230 -230
- package/cargo-target.mjs +53 -53
- package/cli/right-release.mjs +5 -1
- package/development.mjs +14 -3
- package/hardeningscan.mjs +144 -144
- package/macos-signing-identity.mjs +13 -13
- package/managed-cargo.mjs +8 -1
- package/package.json +1 -1
- package/publication-gate.mjs +92 -0
- package/publish-cargo.mjs +10 -3
- package/publish-swift.mjs +102 -54
- package/registry-parity.mjs +22 -4
- package/rightkit-versions.json +69 -5
package/cargo-contract.mjs
CHANGED
|
@@ -1,230 +1,230 @@
|
|
|
1
|
-
import { existsSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { spawnSync } from "./background-process.mjs";
|
|
5
|
-
import { managedRuntimeClient, runCargoSync } from "./managed-cargo.mjs";
|
|
6
|
-
|
|
7
|
-
const SKIP_DIRECTORIES = new Set([".audit", ".cache", ".git", ".claude", ".right-release", ".worktrees", "node_modules", "target", "vendor"]);
|
|
8
|
-
const CRATES_IO_SOURCES = new Set([
|
|
9
|
-
"registry+https://github.com/rust-lang/crates.io-index",
|
|
10
|
-
"registry+https://index.crates.io/",
|
|
11
|
-
]);
|
|
12
|
-
const CACHE_V2_ENVIRONMENT = ["CARGO_HOME", "CARGO_TARGET_DIR", "RUSTC_WRAPPER", "SCCACHE_DIR", "SCCACHE_BASEDIRS", "RIGHT_RELEASE_CACHE_OWNER"];
|
|
13
|
-
|
|
14
|
-
export function managedCargoShimOnPath(env = process.env, platform = process.platform, exists = existsSync) {
|
|
15
|
-
const pathApi = platform === "win32" ? path.win32 : path.posix;
|
|
16
|
-
const delimiter = platform === "win32" ? ";" : ":";
|
|
17
|
-
const extensions = platform === "win32"
|
|
18
|
-
? String(env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean)
|
|
19
|
-
: [""];
|
|
20
|
-
for (const directory of String(env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
21
|
-
for (const extension of extensions) {
|
|
22
|
-
const candidate = pathApi.join(directory, `cargo${extension.toLowerCase()}`);
|
|
23
|
-
if (!exists(candidate)) continue;
|
|
24
|
-
return /(?:^|\/)(?:\.rightkit-managed|rightkitmanagedagent)\/agent-bin\/cargo(?:\.[^/]*)?$/i
|
|
25
|
-
.test(candidate.replaceAll("\\", "/"));
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function brokerManagedHost(env = process.env, platform = process.platform, exists = existsSync) {
|
|
32
|
-
return Boolean(managedRuntimeClient(env, platform, exists)) || Boolean(env.RIGHTKIT_BUILD_BROKER_SOCKET) || managedCargoShimOnPath(env, platform, exists);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function isolatedCargoMetadataEnv(cargoHome, env = process.env, platform = process.platform, exists = existsSync) {
|
|
36
|
-
const metadataEnv = { ...env };
|
|
37
|
-
for (const name of CACHE_V2_ENVIRONMENT) delete metadataEnv[name];
|
|
38
|
-
if (brokerManagedHost(metadataEnv, platform, exists)) return metadataEnv;
|
|
39
|
-
return { ...metadataEnv, CARGO_HOME: cargoHome };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function cargoExecutable(platform = process.platform) {
|
|
43
|
-
return "cargo";
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function cargoArguments(args, platform = process.platform) {
|
|
47
|
-
return args;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function validateRightKitCargoContract(root, publishedVersions, label = path.basename(root)) {
|
|
51
|
-
const scanRoot = path.resolve(root);
|
|
52
|
-
const boundary = findRepositoryRoot(scanRoot);
|
|
53
|
-
const published = publishedVersions instanceof Map
|
|
54
|
-
? publishedVersions
|
|
55
|
-
: new Map(Object.entries(publishedVersions ?? {}));
|
|
56
|
-
const manifests = findCargoManifests(scanRoot);
|
|
57
|
-
const cargoHome = mkdtempSync(path.join(tmpdir(), "rightkit-cargo-home-"));
|
|
58
|
-
try {
|
|
59
|
-
for (const manifestPath of manifests) {
|
|
60
|
-
const manifestLabel = path.relative(boundary, manifestPath) || "Cargo.toml";
|
|
61
|
-
assertNoRightKitCargoOverrides(manifestPath, boundary, `${label}/${manifestLabel}`);
|
|
62
|
-
const dependencies = readCargoManifestDependencies(manifestPath, cargoHome, `${label}/${manifestLabel}`);
|
|
63
|
-
assertPublishedRightKitCargoDependencies(dependencies, published, `${label}/${manifestLabel}`);
|
|
64
|
-
}
|
|
65
|
-
} finally {
|
|
66
|
-
rmSync(cargoHome, { recursive: true, force: true });
|
|
67
|
-
}
|
|
68
|
-
return manifests.length;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function assertPublishedRightKitCargoDependencies(dependencies, published, label) {
|
|
72
|
-
let checked = 0;
|
|
73
|
-
for (const dependency of dependencies.filter(({ name }) => name.startsWith("rightkit-"))) {
|
|
74
|
-
checked += 1;
|
|
75
|
-
const configured = published.get(dependency.name);
|
|
76
|
-
if (!configured) throw new Error(`${label} ${dependency.name} is not a published RightKit crate`);
|
|
77
|
-
const allowed = configured instanceof Set ? configured : new Set(Array.isArray(configured) ? configured : [configured]);
|
|
78
|
-
const exactVersions = new Set([...allowed].map((version) => `=${version}`));
|
|
79
|
-
if (!CRATES_IO_SOURCES.has(dependency.source) || dependency.path || dependency.registry || !exactVersions.has(dependency.req)) {
|
|
80
|
-
throw new Error(`${label} ${dependency.name} must use exact crates.io versions (${[...exactVersions].join(" or ")})`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return checked;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function assertNoRightKitCargoOverrides(manifestPath, repoRoot, label) {
|
|
87
|
-
const boundary = path.resolve(repoRoot);
|
|
88
|
-
const manifest = path.resolve(manifestPath);
|
|
89
|
-
assertInsideBoundary(manifest, boundary);
|
|
90
|
-
assertNoRightKitOverrides(readToml(manifest, label), label, path.relative(boundary, manifest) || "Cargo.toml");
|
|
91
|
-
|
|
92
|
-
for (const configPath of findRepoCargoConfigs(path.dirname(manifest), boundary)) {
|
|
93
|
-
const source = path.relative(boundary, configPath);
|
|
94
|
-
const parsed = readToml(configPath, source);
|
|
95
|
-
assertNoRightKitOverrides(parsed, label, source);
|
|
96
|
-
const cratesIo = parsed.source?.["crates-io"];
|
|
97
|
-
if (cratesIo && typeof cratesIo === "object"
|
|
98
|
-
&& ["replace-with", "directory", "git", "registry", "local-registry"].some((key) => key in cratesIo)) {
|
|
99
|
-
throw new Error(`${label} has forbidden crates.io source replacement in ${source}`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function readCargoManifestDependencies(manifestPath, cargoHome, label) {
|
|
105
|
-
const result = runCargoSync(
|
|
106
|
-
cargoArguments(["metadata", "--no-deps", "--offline", "--format-version", "1", "--manifest-path", manifestPath]),
|
|
107
|
-
{
|
|
108
|
-
cwd: path.dirname(manifestPath),
|
|
109
|
-
encoding: "utf8",
|
|
110
|
-
env: isolatedCargoMetadataEnv(cargoHome),
|
|
111
|
-
windowsHide: true,
|
|
112
|
-
},
|
|
113
|
-
);
|
|
114
|
-
if (result.error) {
|
|
115
|
-
// spawnSync failed to launch the process at all (no exit code, no stderr) -
|
|
116
|
-
// a missing/unspawnable binary, not cargo rejecting the manifest. Reporting
|
|
117
|
-
// this as a version-contract violation sent a real diagnosis (a Windows
|
|
118
|
-
// rustup-proxy binary that can't be spawned from a non-console session)
|
|
119
|
-
// down the wrong path: it read as "fix your Cargo.toml" instead of "cargo
|
|
120
|
-
// could not be launched here".
|
|
121
|
-
throw new Error(`${label} could not launch ${cargoExecutable()} to read Cargo metadata: ${result.error.message}`);
|
|
122
|
-
}
|
|
123
|
-
if (result.status !== 0) {
|
|
124
|
-
throw new Error(`${label} Cargo metadata rejected manifest; RightKit dependencies must use exact crates.io versions: ${String(result.stderr ?? "").trim()}`);
|
|
125
|
-
}
|
|
126
|
-
const metadata = JSON.parse(result.stdout);
|
|
127
|
-
const expectedPath = path.resolve(manifestPath).toLowerCase();
|
|
128
|
-
const pkg = metadata.packages.find(({ manifest_path: candidate }) => path.resolve(candidate).toLowerCase() === expectedPath);
|
|
129
|
-
if (!pkg && path.resolve(metadata.workspace_root, "Cargo.toml").toLowerCase() === expectedPath) return [];
|
|
130
|
-
if (!pkg) throw new Error(`${label} was not returned by Cargo metadata`);
|
|
131
|
-
return pkg.dependencies;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function assertNoRightKitOverrides(parsed, label, source) {
|
|
135
|
-
for (const overrides of Object.values(parsed.patch ?? {})) {
|
|
136
|
-
if (!overrides || typeof overrides !== "object" || Array.isArray(overrides)) continue;
|
|
137
|
-
for (const [name, specifier] of Object.entries(overrides)) {
|
|
138
|
-
const packageName = typeof specifier === "object" && specifier ? specifier.package : undefined;
|
|
139
|
-
if (name.startsWith("rightkit-") || packageName?.startsWith("rightkit-")) {
|
|
140
|
-
throw new Error(`${label} has forbidden RightKit Cargo override ${name} in ${source}`);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
for (const [name, specifier] of Object.entries(parsed.replace ?? {})) {
|
|
145
|
-
const packageName = typeof specifier === "object" && specifier ? specifier.package : undefined;
|
|
146
|
-
if (name.split(":", 1)[0].startsWith("rightkit-") || packageName?.startsWith("rightkit-")) {
|
|
147
|
-
throw new Error(`${label} has forbidden RightKit Cargo override ${name} in ${source}`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// `tomllib` is Python 3.11+. Windows pins `py -3.11`, but a bare `python3` on macOS is the system
|
|
153
|
-
// 3.9, which has no tomllib — so every app's release:doctor died on a stock Mac, and reported the
|
|
154
|
-
// manifest as invalid TOML when the real problem was the interpreter. Probe for one that can.
|
|
155
|
-
let tomlRunner;
|
|
156
|
-
function resolveTomlRunner() {
|
|
157
|
-
if (tomlRunner) return tomlRunner;
|
|
158
|
-
const probe = "import sys,tomllib";
|
|
159
|
-
const candidates =
|
|
160
|
-
process.platform === "win32"
|
|
161
|
-
? [["py", ["-3.11"]], ["py", ["-3"]], ["python", []]]
|
|
162
|
-
: [["python3.13", []], ["python3.12", []], ["python3.11", []], ["python3", []]];
|
|
163
|
-
for (const [command, prefix] of candidates) {
|
|
164
|
-
const check = spawnSync(command, [...prefix, "-c", probe], { encoding: "utf8", windowsHide: true });
|
|
165
|
-
if (check.status === 0) {
|
|
166
|
-
tomlRunner = { command, prefix };
|
|
167
|
-
return tomlRunner;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
throw new Error(
|
|
171
|
-
"no Python with tomllib found (needs 3.11+). Install one, or put it on PATH: the release " +
|
|
172
|
-
"contract reads Cargo manifests through it.",
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function readToml(file, label) {
|
|
177
|
-
const script = "import json,pathlib,sys,tomllib; json.dump(tomllib.loads(pathlib.Path(sys.argv[1]).read_text(encoding='utf-8')),sys.stdout,default=str)";
|
|
178
|
-
const { command, prefix } = resolveTomlRunner();
|
|
179
|
-
const result = spawnSync(command, [...prefix, "-c", script, file], { encoding: "utf8", windowsHide: true });
|
|
180
|
-
if (result.status !== 0) throw new Error(`${label} is not valid TOML: ${String(result.stderr ?? "").trim()}`);
|
|
181
|
-
return JSON.parse(result.stdout);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function findCargoManifests(root) {
|
|
185
|
-
const found = [];
|
|
186
|
-
const visit = (dir) => {
|
|
187
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
188
|
-
if (entry.isDirectory() && SKIP_DIRECTORIES.has(entry.name)) continue;
|
|
189
|
-
const full = path.join(dir, entry.name);
|
|
190
|
-
if (entry.isDirectory()) visit(full);
|
|
191
|
-
else if (entry.isFile() && entry.name === "Cargo.toml") found.push(full);
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
visit(root);
|
|
195
|
-
return found;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function findRepoCargoConfigs(start, repoRoot) {
|
|
199
|
-
const boundary = path.resolve(repoRoot);
|
|
200
|
-
let current = path.resolve(start);
|
|
201
|
-
assertInsideBoundary(current, boundary);
|
|
202
|
-
const configs = [];
|
|
203
|
-
while (true) {
|
|
204
|
-
for (const filename of ["config", "config.toml"]) {
|
|
205
|
-
const candidate = path.join(current, ".cargo", filename);
|
|
206
|
-
if (existsSync(candidate)) configs.push(candidate);
|
|
207
|
-
}
|
|
208
|
-
if (current.toLowerCase() === boundary.toLowerCase()) break;
|
|
209
|
-
current = path.dirname(current);
|
|
210
|
-
}
|
|
211
|
-
return configs;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function assertInsideBoundary(candidate, boundary) {
|
|
215
|
-
const relative = path.relative(boundary, candidate);
|
|
216
|
-
if (relative !== "" && (relative.startsWith("..") || path.isAbsolute(relative))) {
|
|
217
|
-
throw new Error(`${candidate} is outside ${boundary}`);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function findRepositoryRoot(start) {
|
|
222
|
-
const original = path.resolve(start);
|
|
223
|
-
let current = original;
|
|
224
|
-
while (true) {
|
|
225
|
-
if (existsSync(path.join(current, ".git"))) return current;
|
|
226
|
-
const parent = path.dirname(current);
|
|
227
|
-
if (parent === current) return original;
|
|
228
|
-
current = parent;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
1
|
+
import { existsSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "./background-process.mjs";
|
|
5
|
+
import { managedRuntimeClient, runCargoSync } from "./managed-cargo.mjs";
|
|
6
|
+
|
|
7
|
+
const SKIP_DIRECTORIES = new Set([".audit", ".cache", ".git", ".claude", ".right-release", ".worktrees", "node_modules", "target", "vendor"]);
|
|
8
|
+
const CRATES_IO_SOURCES = new Set([
|
|
9
|
+
"registry+https://github.com/rust-lang/crates.io-index",
|
|
10
|
+
"registry+https://index.crates.io/",
|
|
11
|
+
]);
|
|
12
|
+
const CACHE_V2_ENVIRONMENT = ["CARGO_HOME", "CARGO_TARGET_DIR", "RUSTC_WRAPPER", "SCCACHE_DIR", "SCCACHE_BASEDIRS", "RIGHT_RELEASE_CACHE_OWNER"];
|
|
13
|
+
|
|
14
|
+
export function managedCargoShimOnPath(env = process.env, platform = process.platform, exists = existsSync) {
|
|
15
|
+
const pathApi = platform === "win32" ? path.win32 : path.posix;
|
|
16
|
+
const delimiter = platform === "win32" ? ";" : ":";
|
|
17
|
+
const extensions = platform === "win32"
|
|
18
|
+
? String(env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean)
|
|
19
|
+
: [""];
|
|
20
|
+
for (const directory of String(env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
21
|
+
for (const extension of extensions) {
|
|
22
|
+
const candidate = pathApi.join(directory, `cargo${extension.toLowerCase()}`);
|
|
23
|
+
if (!exists(candidate)) continue;
|
|
24
|
+
return /(?:^|\/)(?:\.rightkit-managed|rightkitmanagedagent)\/agent-bin\/cargo(?:\.[^/]*)?$/i
|
|
25
|
+
.test(candidate.replaceAll("\\", "/"));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function brokerManagedHost(env = process.env, platform = process.platform, exists = existsSync) {
|
|
32
|
+
return Boolean(managedRuntimeClient(env, platform, exists)) || Boolean(env.RIGHTKIT_BUILD_BROKER_SOCKET) || managedCargoShimOnPath(env, platform, exists);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isolatedCargoMetadataEnv(cargoHome, env = process.env, platform = process.platform, exists = existsSync) {
|
|
36
|
+
const metadataEnv = { ...env };
|
|
37
|
+
for (const name of CACHE_V2_ENVIRONMENT) delete metadataEnv[name];
|
|
38
|
+
if (brokerManagedHost(metadataEnv, platform, exists)) return metadataEnv;
|
|
39
|
+
return { ...metadataEnv, CARGO_HOME: cargoHome };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function cargoExecutable(platform = process.platform) {
|
|
43
|
+
return "cargo";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function cargoArguments(args, platform = process.platform) {
|
|
47
|
+
return args;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function validateRightKitCargoContract(root, publishedVersions, label = path.basename(root)) {
|
|
51
|
+
const scanRoot = path.resolve(root);
|
|
52
|
+
const boundary = findRepositoryRoot(scanRoot);
|
|
53
|
+
const published = publishedVersions instanceof Map
|
|
54
|
+
? publishedVersions
|
|
55
|
+
: new Map(Object.entries(publishedVersions ?? {}));
|
|
56
|
+
const manifests = findCargoManifests(scanRoot);
|
|
57
|
+
const cargoHome = mkdtempSync(path.join(tmpdir(), "rightkit-cargo-home-"));
|
|
58
|
+
try {
|
|
59
|
+
for (const manifestPath of manifests) {
|
|
60
|
+
const manifestLabel = path.relative(boundary, manifestPath) || "Cargo.toml";
|
|
61
|
+
assertNoRightKitCargoOverrides(manifestPath, boundary, `${label}/${manifestLabel}`);
|
|
62
|
+
const dependencies = readCargoManifestDependencies(manifestPath, cargoHome, `${label}/${manifestLabel}`);
|
|
63
|
+
assertPublishedRightKitCargoDependencies(dependencies, published, `${label}/${manifestLabel}`);
|
|
64
|
+
}
|
|
65
|
+
} finally {
|
|
66
|
+
rmSync(cargoHome, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
return manifests.length;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function assertPublishedRightKitCargoDependencies(dependencies, published, label) {
|
|
72
|
+
let checked = 0;
|
|
73
|
+
for (const dependency of dependencies.filter(({ name }) => name.startsWith("rightkit-"))) {
|
|
74
|
+
checked += 1;
|
|
75
|
+
const configured = published.get(dependency.name);
|
|
76
|
+
if (!configured) throw new Error(`${label} ${dependency.name} is not a published RightKit crate`);
|
|
77
|
+
const allowed = configured instanceof Set ? configured : new Set(Array.isArray(configured) ? configured : [configured]);
|
|
78
|
+
const exactVersions = new Set([...allowed].map((version) => `=${version}`));
|
|
79
|
+
if (!CRATES_IO_SOURCES.has(dependency.source) || dependency.path || dependency.registry || !exactVersions.has(dependency.req)) {
|
|
80
|
+
throw new Error(`${label} ${dependency.name} must use exact crates.io versions (${[...exactVersions].join(" or ")})`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return checked;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function assertNoRightKitCargoOverrides(manifestPath, repoRoot, label) {
|
|
87
|
+
const boundary = path.resolve(repoRoot);
|
|
88
|
+
const manifest = path.resolve(manifestPath);
|
|
89
|
+
assertInsideBoundary(manifest, boundary);
|
|
90
|
+
assertNoRightKitOverrides(readToml(manifest, label), label, path.relative(boundary, manifest) || "Cargo.toml");
|
|
91
|
+
|
|
92
|
+
for (const configPath of findRepoCargoConfigs(path.dirname(manifest), boundary)) {
|
|
93
|
+
const source = path.relative(boundary, configPath);
|
|
94
|
+
const parsed = readToml(configPath, source);
|
|
95
|
+
assertNoRightKitOverrides(parsed, label, source);
|
|
96
|
+
const cratesIo = parsed.source?.["crates-io"];
|
|
97
|
+
if (cratesIo && typeof cratesIo === "object"
|
|
98
|
+
&& ["replace-with", "directory", "git", "registry", "local-registry"].some((key) => key in cratesIo)) {
|
|
99
|
+
throw new Error(`${label} has forbidden crates.io source replacement in ${source}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function readCargoManifestDependencies(manifestPath, cargoHome, label) {
|
|
105
|
+
const result = runCargoSync(
|
|
106
|
+
cargoArguments(["metadata", "--no-deps", "--offline", "--format-version", "1", "--manifest-path", manifestPath]),
|
|
107
|
+
{
|
|
108
|
+
cwd: path.dirname(manifestPath),
|
|
109
|
+
encoding: "utf8",
|
|
110
|
+
env: isolatedCargoMetadataEnv(cargoHome),
|
|
111
|
+
windowsHide: true,
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
if (result.error) {
|
|
115
|
+
// spawnSync failed to launch the process at all (no exit code, no stderr) -
|
|
116
|
+
// a missing/unspawnable binary, not cargo rejecting the manifest. Reporting
|
|
117
|
+
// this as a version-contract violation sent a real diagnosis (a Windows
|
|
118
|
+
// rustup-proxy binary that can't be spawned from a non-console session)
|
|
119
|
+
// down the wrong path: it read as "fix your Cargo.toml" instead of "cargo
|
|
120
|
+
// could not be launched here".
|
|
121
|
+
throw new Error(`${label} could not launch ${cargoExecutable()} to read Cargo metadata: ${result.error.message}`);
|
|
122
|
+
}
|
|
123
|
+
if (result.status !== 0) {
|
|
124
|
+
throw new Error(`${label} Cargo metadata rejected manifest; RightKit dependencies must use exact crates.io versions: ${String(result.stderr ?? "").trim()}`);
|
|
125
|
+
}
|
|
126
|
+
const metadata = JSON.parse(result.stdout);
|
|
127
|
+
const expectedPath = path.resolve(manifestPath).toLowerCase();
|
|
128
|
+
const pkg = metadata.packages.find(({ manifest_path: candidate }) => path.resolve(candidate).toLowerCase() === expectedPath);
|
|
129
|
+
if (!pkg && path.resolve(metadata.workspace_root, "Cargo.toml").toLowerCase() === expectedPath) return [];
|
|
130
|
+
if (!pkg) throw new Error(`${label} was not returned by Cargo metadata`);
|
|
131
|
+
return pkg.dependencies;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function assertNoRightKitOverrides(parsed, label, source) {
|
|
135
|
+
for (const overrides of Object.values(parsed.patch ?? {})) {
|
|
136
|
+
if (!overrides || typeof overrides !== "object" || Array.isArray(overrides)) continue;
|
|
137
|
+
for (const [name, specifier] of Object.entries(overrides)) {
|
|
138
|
+
const packageName = typeof specifier === "object" && specifier ? specifier.package : undefined;
|
|
139
|
+
if (name.startsWith("rightkit-") || packageName?.startsWith("rightkit-")) {
|
|
140
|
+
throw new Error(`${label} has forbidden RightKit Cargo override ${name} in ${source}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
for (const [name, specifier] of Object.entries(parsed.replace ?? {})) {
|
|
145
|
+
const packageName = typeof specifier === "object" && specifier ? specifier.package : undefined;
|
|
146
|
+
if (name.split(":", 1)[0].startsWith("rightkit-") || packageName?.startsWith("rightkit-")) {
|
|
147
|
+
throw new Error(`${label} has forbidden RightKit Cargo override ${name} in ${source}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// `tomllib` is Python 3.11+. Windows pins `py -3.11`, but a bare `python3` on macOS is the system
|
|
153
|
+
// 3.9, which has no tomllib — so every app's release:doctor died on a stock Mac, and reported the
|
|
154
|
+
// manifest as invalid TOML when the real problem was the interpreter. Probe for one that can.
|
|
155
|
+
let tomlRunner;
|
|
156
|
+
function resolveTomlRunner() {
|
|
157
|
+
if (tomlRunner) return tomlRunner;
|
|
158
|
+
const probe = "import sys,tomllib";
|
|
159
|
+
const candidates =
|
|
160
|
+
process.platform === "win32"
|
|
161
|
+
? [["py", ["-3.11"]], ["py", ["-3"]], ["python", []]]
|
|
162
|
+
: [["python3.13", []], ["python3.12", []], ["python3.11", []], ["python3", []]];
|
|
163
|
+
for (const [command, prefix] of candidates) {
|
|
164
|
+
const check = spawnSync(command, [...prefix, "-c", probe], { encoding: "utf8", windowsHide: true });
|
|
165
|
+
if (check.status === 0) {
|
|
166
|
+
tomlRunner = { command, prefix };
|
|
167
|
+
return tomlRunner;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
throw new Error(
|
|
171
|
+
"no Python with tomllib found (needs 3.11+). Install one, or put it on PATH: the release " +
|
|
172
|
+
"contract reads Cargo manifests through it.",
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function readToml(file, label) {
|
|
177
|
+
const script = "import json,pathlib,sys,tomllib; json.dump(tomllib.loads(pathlib.Path(sys.argv[1]).read_text(encoding='utf-8')),sys.stdout,default=str)";
|
|
178
|
+
const { command, prefix } = resolveTomlRunner();
|
|
179
|
+
const result = spawnSync(command, [...prefix, "-c", script, file], { encoding: "utf8", windowsHide: true });
|
|
180
|
+
if (result.status !== 0) throw new Error(`${label} is not valid TOML: ${String(result.stderr ?? "").trim()}`);
|
|
181
|
+
return JSON.parse(result.stdout);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function findCargoManifests(root) {
|
|
185
|
+
const found = [];
|
|
186
|
+
const visit = (dir) => {
|
|
187
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
188
|
+
if (entry.isDirectory() && SKIP_DIRECTORIES.has(entry.name)) continue;
|
|
189
|
+
const full = path.join(dir, entry.name);
|
|
190
|
+
if (entry.isDirectory()) visit(full);
|
|
191
|
+
else if (entry.isFile() && entry.name === "Cargo.toml") found.push(full);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
visit(root);
|
|
195
|
+
return found;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function findRepoCargoConfigs(start, repoRoot) {
|
|
199
|
+
const boundary = path.resolve(repoRoot);
|
|
200
|
+
let current = path.resolve(start);
|
|
201
|
+
assertInsideBoundary(current, boundary);
|
|
202
|
+
const configs = [];
|
|
203
|
+
while (true) {
|
|
204
|
+
for (const filename of ["config", "config.toml"]) {
|
|
205
|
+
const candidate = path.join(current, ".cargo", filename);
|
|
206
|
+
if (existsSync(candidate)) configs.push(candidate);
|
|
207
|
+
}
|
|
208
|
+
if (current.toLowerCase() === boundary.toLowerCase()) break;
|
|
209
|
+
current = path.dirname(current);
|
|
210
|
+
}
|
|
211
|
+
return configs;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function assertInsideBoundary(candidate, boundary) {
|
|
215
|
+
const relative = path.relative(boundary, candidate);
|
|
216
|
+
if (relative !== "" && (relative.startsWith("..") || path.isAbsolute(relative))) {
|
|
217
|
+
throw new Error(`${candidate} is outside ${boundary}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function findRepositoryRoot(start) {
|
|
222
|
+
const original = path.resolve(start);
|
|
223
|
+
let current = original;
|
|
224
|
+
while (true) {
|
|
225
|
+
if (existsSync(path.join(current, ".git"))) return current;
|
|
226
|
+
const parent = path.dirname(current);
|
|
227
|
+
if (parent === current) return original;
|
|
228
|
+
current = parent;
|
|
229
|
+
}
|
|
230
|
+
}
|
package/cargo-target.mjs
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Resolves the Cargo target directory for a manifest by asking Cargo itself.
|
|
3
|
-
//
|
|
4
|
-
// `cargo metadata` is authoritative on BOTH broker-managed and unmanaged
|
|
5
|
-
// hosts: through a managed shim it reports the broker's own workspace target
|
|
6
|
-
// directory; without one it reports the local `target/` directory. LOCATING
|
|
7
|
-
// the target directory therefore never needs broker detection — the metadata
|
|
8
|
-
// command already reflects whichever host it runs on. `brokerManagedHost()`
|
|
9
|
-
// (see cargo-contract.mjs) remains a separate, cache-policy-only decision:
|
|
10
|
-
// whether Cache V2's lease/slot/prune/floor machinery should run at all, not
|
|
11
|
-
// where the target directory lives.
|
|
12
|
-
import { execFileSync } from "./background-process.mjs";
|
|
13
|
-
import { runCargoSync } from "./managed-cargo.mjs";
|
|
14
|
-
import path from "node:path";
|
|
15
|
-
import { fileURLToPath } from "node:url";
|
|
16
|
-
|
|
17
|
-
export function resolveTargetRoot(manifestPath, options = {}) {
|
|
18
|
-
if (!manifestPath) throw new Error("resolveTargetRoot requires a Cargo.toml manifest path");
|
|
19
|
-
const execute = options.execFileSync ?? execFileSync;
|
|
20
|
-
let output;
|
|
21
|
-
try {
|
|
22
|
-
const args = ["metadata", "--offline", "--format-version", "1", "--no-deps", "--manifest-path", manifestPath];
|
|
23
|
-
if (options.execFileSync) {
|
|
24
|
-
output = execute("cargo", args, { cwd: path.dirname(manifestPath), encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
25
|
-
} else {
|
|
26
|
-
const result = runCargoSync(args, { cwd: path.dirname(manifestPath), encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
27
|
-
if (result.error) throw result.error;
|
|
28
|
-
if (result.status !== 0) throw new Error(String(result.stderr ?? "").trim() || `exited ${result.status}`);
|
|
29
|
-
output = result.stdout;
|
|
30
|
-
}
|
|
31
|
-
} catch (error) {
|
|
32
|
-
throw new Error(`metadata failed for ${manifestPath}: ${error.message}`);
|
|
33
|
-
}
|
|
34
|
-
const { target_directory: directory } = JSON.parse(output);
|
|
35
|
-
if (typeof directory !== "string" || directory.length === 0 || !path.isAbsolute(directory)) {
|
|
36
|
-
throw new Error(`metadata for ${manifestPath} did not report an absolute target_directory`);
|
|
37
|
-
}
|
|
38
|
-
return path.resolve(directory);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
|
|
42
|
-
const manifestPath = process.argv[2];
|
|
43
|
-
if (!manifestPath) {
|
|
44
|
-
console.error("usage: cargo-target.mjs <path-to-Cargo.toml>");
|
|
45
|
-
process.exit(1);
|
|
46
|
-
}
|
|
47
|
-
try {
|
|
48
|
-
process.stdout.write(`${resolveTargetRoot(manifestPath)}\n`);
|
|
49
|
-
} catch (error) {
|
|
50
|
-
console.error(error.message);
|
|
51
|
-
process.exit(1);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Resolves the Cargo target directory for a manifest by asking Cargo itself.
|
|
3
|
+
//
|
|
4
|
+
// `cargo metadata` is authoritative on BOTH broker-managed and unmanaged
|
|
5
|
+
// hosts: through a managed shim it reports the broker's own workspace target
|
|
6
|
+
// directory; without one it reports the local `target/` directory. LOCATING
|
|
7
|
+
// the target directory therefore never needs broker detection — the metadata
|
|
8
|
+
// command already reflects whichever host it runs on. `brokerManagedHost()`
|
|
9
|
+
// (see cargo-contract.mjs) remains a separate, cache-policy-only decision:
|
|
10
|
+
// whether Cache V2's lease/slot/prune/floor machinery should run at all, not
|
|
11
|
+
// where the target directory lives.
|
|
12
|
+
import { execFileSync } from "./background-process.mjs";
|
|
13
|
+
import { runCargoSync } from "./managed-cargo.mjs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
export function resolveTargetRoot(manifestPath, options = {}) {
|
|
18
|
+
if (!manifestPath) throw new Error("resolveTargetRoot requires a Cargo.toml manifest path");
|
|
19
|
+
const execute = options.execFileSync ?? execFileSync;
|
|
20
|
+
let output;
|
|
21
|
+
try {
|
|
22
|
+
const args = ["metadata", "--offline", "--format-version", "1", "--no-deps", "--manifest-path", manifestPath];
|
|
23
|
+
if (options.execFileSync) {
|
|
24
|
+
output = execute("cargo", args, { cwd: path.dirname(manifestPath), encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
25
|
+
} else {
|
|
26
|
+
const result = runCargoSync(args, { cwd: path.dirname(manifestPath), encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
27
|
+
if (result.error) throw result.error;
|
|
28
|
+
if (result.status !== 0) throw new Error(String(result.stderr ?? "").trim() || `exited ${result.status}`);
|
|
29
|
+
output = result.stdout;
|
|
30
|
+
}
|
|
31
|
+
} catch (error) {
|
|
32
|
+
throw new Error(`metadata failed for ${manifestPath}: ${error.message}`);
|
|
33
|
+
}
|
|
34
|
+
const { target_directory: directory } = JSON.parse(output);
|
|
35
|
+
if (typeof directory !== "string" || directory.length === 0 || !path.isAbsolute(directory)) {
|
|
36
|
+
throw new Error(`metadata for ${manifestPath} did not report an absolute target_directory`);
|
|
37
|
+
}
|
|
38
|
+
return path.resolve(directory);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
|
|
42
|
+
const manifestPath = process.argv[2];
|
|
43
|
+
if (!manifestPath) {
|
|
44
|
+
console.error("usage: cargo-target.mjs <path-to-Cargo.toml>");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
process.stdout.write(`${resolveTargetRoot(manifestPath)}\n`);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error(error.message);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
package/cli/right-release.mjs
CHANGED
|
@@ -70,6 +70,8 @@ if (first === "--version" || first === "-v") {
|
|
|
70
70
|
const rest = args.slice(1);
|
|
71
71
|
if (rest[0] === "cargo") {
|
|
72
72
|
run("publish-cargo.mjs", rest.slice(1));
|
|
73
|
+
} else if (rest[0] === "npm") {
|
|
74
|
+
run("publication-gate.mjs", rest.slice(1));
|
|
73
75
|
} else if (rest[0] === "swift") {
|
|
74
76
|
run("publish-swift.mjs", rest.slice(1));
|
|
75
77
|
} else {
|
|
@@ -155,8 +157,10 @@ Commands:
|
|
|
155
157
|
Upload one sealed release; never builds
|
|
156
158
|
release Compatibility alias for build
|
|
157
159
|
publish Compatibility alias for upload
|
|
160
|
+
publish npm --package @rightkit/<name> [--dry-run] Gate preflight for npm publish (no publish)
|
|
161
|
+
publish npm --package @rightkit/<name> [--dry-run] Gate preflight for npm publish (never publishes)
|
|
158
162
|
publish cargo --crate <name> [--dry-run] Test, inspect, scan, and publish one crates.io package
|
|
159
|
-
publish swift [--
|
|
163
|
+
publish swift [--repo <git-url>] [--dry-run] Test, tag, resolve a fresh exact-pin consumer, and publish RightKitSwift to its git repo
|
|
160
164
|
model promote --authority heardright --config <file> [--dry-run]
|
|
161
165
|
Sign and promote one runtime/model artifact
|
|
162
166
|
addon doctor|build|upload|adopt <options> Manage a signed headless add-on
|
package/development.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from "./background-process.mjs";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
|
|
3
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
import { assertPrimaryReleaseCheckout } from "./release-invocation.mjs";
|
|
@@ -48,7 +48,8 @@ export function validateDevelopmentConfig(config, platform) {
|
|
|
48
48
|
if (target.install.manifestPath && path.isAbsolute(target.install.manifestPath)) throw new Error("install.manifestPath must be relative to app root");
|
|
49
49
|
} else if (target.install?.kind === "portable") {
|
|
50
50
|
const descriptor = target.install;
|
|
51
|
-
|
|
51
|
+
const absoluteInstallRoot = platform === "win" ? path.win32.isAbsolute(descriptor.installRoot ?? "") : path.isAbsolute(descriptor.installRoot ?? "");
|
|
52
|
+
if (descriptor.activation?.kind !== "current" || !absoluteInstallRoot) throw new Error("portable development installer requires absolute installRoot and current activation");
|
|
52
53
|
for (const key of ["sourceDirectory", "entrypoint"]) {
|
|
53
54
|
if (typeof descriptor[key] !== "string" || !descriptor[key] || path.isAbsolute(descriptor[key]) || path.win32.isAbsolute(descriptor[key]) || descriptor[key].split(/[\\/]/).includes("..")) throw new Error(`portable development ${key} must be a contained relative path`);
|
|
54
55
|
}
|
|
@@ -231,10 +232,20 @@ export function resolvePnpmCli({ root, env = process.env } = {}) {
|
|
|
231
232
|
const match = /"%~dp0\\([^"]*pnpm\.(?:mjs|cjs|js))"/i.exec(readFileSync(shim, "utf8"));
|
|
232
233
|
if (match) candidates.push(path.resolve(dir, match[1]));
|
|
233
234
|
}
|
|
234
|
-
|
|
235
|
+
// Unix global pnpm exposes its JavaScript entry through a PATH symlink.
|
|
236
|
+
for (const dir of String(env.PATH ?? env.Path ?? "").split(path.delimiter).filter(Boolean)) candidates.push(path.join(dir, "pnpm"));
|
|
237
|
+
const selected = candidates.map(resolveJavaScriptEntrypoint).find(Boolean);
|
|
235
238
|
if (!selected) throw new Error("could not resolve pnpm JavaScript CLI entrypoint; install/pin pnpm through Corepack");
|
|
236
239
|
return selected;
|
|
237
240
|
}
|
|
241
|
+
function resolveJavaScriptEntrypoint(candidate) {
|
|
242
|
+
try {
|
|
243
|
+
const resolved = realpathSync(candidate);
|
|
244
|
+
return /pnpm\.(?:mjs|cjs|js)$/i.test(resolved) && statSync(resolved).isFile() ? resolved : undefined;
|
|
245
|
+
} catch {
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
238
249
|
function git(cwd, args) { const result = spawnSync("git", args, { cwd, encoding: "utf8", windowsHide: true }); if (result.status !== 0) throw new Error(`git ${args.join(" ")} failed`); return result.stdout.trim(); }
|
|
239
250
|
function inside(root, child) { const rel = path.relative(path.resolve(root), path.resolve(child)); return rel === "" || (rel !== ".." && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel)); }
|
|
240
251
|
export async function main(args = process.argv.slice(2)) {
|