@rightkit/release 0.2.174 → 0.4.0
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 +200 -2
- package/addon-command.mjs +3 -4
- package/android-play-upload.mjs +2 -1
- package/apple-profile-renew.mjs +11 -3
- package/artifact-storage.mjs +35 -2
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +1 -1
- package/build-release.mjs +138 -47
- package/cli/right-release.mjs +40 -5
- package/codesign-environment.mjs +2 -2
- package/command-spec.mjs +80 -0
- package/content-digest.mjs +209 -0
- package/deps-cargo.mjs +55 -0
- package/deps.mjs +7 -0
- package/development.mjs +17 -6
- package/direct-bootstrap.mjs +5 -6
- package/disk-command.mjs +1 -0
- package/disk-policy.json +6 -2
- package/disk-stable-root.mjs +13 -9
- package/dist-scan.mjs +102 -0
- package/dmg-build.mjs +53 -4
- package/github-updater-release.mjs +70 -7
- package/icon-pack.mjs +117 -0
- package/in-place-install.mjs +17 -14
- package/init-command.mjs +116 -0
- package/install-app-windows.mjs +24 -7
- package/install-app.mjs +56 -42
- package/ios-asc-credentials.mjs +163 -0
- package/ios-build-context.mjs +129 -0
- package/ios-build-upload.mjs +282 -0
- package/ios-credentials-command.mjs +70 -0
- package/ios-dev.mjs +248 -0
- package/ios-host.mjs +76 -0
- package/ios-lock.mjs +75 -0
- package/ios-package-recipe.mjs +707 -0
- package/ios-testflight.mjs +266 -0
- package/ios-upload.mjs +313 -0
- package/local-publish.mjs +390 -47
- package/macho-approval-hash.mjs +181 -0
- package/managed-cargo.mjs +57 -1
- package/media-sidecar.mjs +20 -4
- package/mirror-root-artifact.mjs +74 -22
- package/native-cargo-layout.mjs +47 -4
- package/npm-license-notices.mjs +159 -0
- package/ownership-command.mjs +2 -1
- package/ownership-dependency-allowlist.json +2 -1
- package/ownership-evidence.mjs +30 -18
- package/ownership-key-command.mjs +69 -2
- package/ownership-mandatory-set.mjs +66 -16
- package/ownership-repositories.json +1 -1
- package/ownership-scope.mjs +94 -0
- package/ownership.mjs +86 -11
- package/package-recipe-targets.mjs +56 -2
- package/package.json +1 -1
- package/pe-content.mjs +133 -0
- package/preflight.mjs +14 -7
- package/project-type.mjs +3 -1
- package/protected-credentials.mjs +28 -1
- package/publication-artifacts.mjs +30 -5
- package/publication-gate.mjs +83 -5
- package/publish-cargo.mjs +124 -18
- package/publish-update.mjs +15 -2
- package/qa-build-features.mjs +16 -13
- package/qa-contract.mjs +30 -2
- package/qa-control-scan.mjs +4 -0
- package/release-dependencies-command.mjs +45 -0
- package/release-dependencies.mjs +60 -0
- package/release-invocation.mjs +118 -8
- package/release-switch-registry.json +126 -27
- package/release-switch-scan.mjs +62 -24
- package/release.mjs +144 -21
- package/rightkit-ownership.json +46 -2
- package/rightkit-versions.json +380 -189
- package/runtime-budgets.mjs +134 -23
- package/sign-updater.mjs +56 -59
- package/source-gate.mjs +8 -1
- package/stage-sidecar.mjs +634 -69
- package/target-distribution.mjs +5 -0
- package/test-command.mjs +190 -0
- package/toolchain-swift.mjs +239 -0
- package/upload-release.mjs +7 -2
- package/web-build.mjs +67 -0
package/command-spec.mjs
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Shared argv command shape for the declarative `tests` and `web` config sections (`right-release test`, `web-build`).
|
|
2
|
+
// A step is one command run without a shell:
|
|
3
|
+
// { cmd, args?, cwd?, env? } any executable ("node" means this Node binary)
|
|
4
|
+
// { node, args?, nodeFlags?, nodeOptions? } a Node script path inside the app, run by this Node binary
|
|
5
|
+
// `cwd` and `node` are app-relative and may not leave the app. `nodeFlags` go before the script, `nodeOptions` are merged
|
|
6
|
+
// (de-duplicated) into NODE_OPTIONS for the child. Unknown keys are refused so a typo cannot silently change a run.
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { pathToFileURL } from "node:url";
|
|
9
|
+
|
|
10
|
+
/** Load an app's `right-release.config.mjs`; the app root is the config file's directory. */
|
|
11
|
+
export async function loadAppConfig(configFile = "right-release.config.mjs") {
|
|
12
|
+
const configPath = path.resolve(configFile);
|
|
13
|
+
const loaded = await import(pathToFileURL(configPath).href);
|
|
14
|
+
return { config: loaded.default ?? loaded.config ?? loaded, appRoot: path.dirname(configPath), configPath };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class CommandSpecError extends Error {
|
|
18
|
+
constructor(message) { super(message); this.name = "CommandSpecError"; this.code = "RK_COMMAND_SPEC"; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const fail = (message) => { throw new CommandSpecError(message); };
|
|
22
|
+
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
23
|
+
export const STEP_KEYS = ["cmd", "node", "args", "cwd", "env", "nodeFlags", "nodeOptions"];
|
|
24
|
+
|
|
25
|
+
export function relativeInside(value, label) {
|
|
26
|
+
if (typeof value !== "string" || !value.trim()) fail(`${label} must be a non-empty relative path`);
|
|
27
|
+
const normalized = value.replaceAll("\\", "/");
|
|
28
|
+
if (path.isAbsolute(value) || /^[A-Za-z]:/.test(value) || normalized.split("/").includes("..")) fail(`${label} must be relative and stay inside the app: ${value}`);
|
|
29
|
+
return normalized;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const stringList = (value, label) => {
|
|
33
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) fail(`${label} must be a list of strings`);
|
|
34
|
+
return [...value];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Validate one step; `extraKeys` are accepted and left to the caller. Returns the step. */
|
|
38
|
+
export function validateStep(step, label, extraKeys = []) {
|
|
39
|
+
if (!isObject(step)) fail(`${label} must be an object`);
|
|
40
|
+
const allowed = new Set([...STEP_KEYS, ...extraKeys]);
|
|
41
|
+
const unknown = Object.keys(step).filter((key) => !allowed.has(key));
|
|
42
|
+
if (unknown.length) fail(`${label} has unknown key(s): ${unknown.join(", ")} (allowed: ${[...allowed].join(", ")})`);
|
|
43
|
+
if (step.cmd !== undefined && step.node !== undefined) fail(`${label} sets both cmd and node; use one`);
|
|
44
|
+
if (step.cmd !== undefined && (typeof step.cmd !== "string" || !step.cmd.trim())) fail(`${label}.cmd must be a non-empty string`);
|
|
45
|
+
if (step.node !== undefined) relativeInside(step.node, `${label}.node`);
|
|
46
|
+
if (step.cwd !== undefined) relativeInside(step.cwd, `${label}.cwd`);
|
|
47
|
+
if (step.args !== undefined) stringList(step.args, `${label}.args`);
|
|
48
|
+
if (step.nodeFlags !== undefined) stringList(step.nodeFlags, `${label}.nodeFlags`);
|
|
49
|
+
if (step.nodeOptions !== undefined) stringList(step.nodeOptions, `${label}.nodeOptions`);
|
|
50
|
+
if (step.env !== undefined && (!isObject(step.env) || Object.values(step.env).some((value) => typeof value !== "string"))) fail(`${label}.env must be an object of strings`);
|
|
51
|
+
return step;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Add flags to a NODE_OPTIONS string without repeating one already present. */
|
|
55
|
+
export function mergeNodeOptions(existing, flags) {
|
|
56
|
+
const present = (existing ?? "").trim().split(/\s+/).filter(Boolean);
|
|
57
|
+
for (const flag of flags) if (!present.includes(flag)) present.push(flag);
|
|
58
|
+
return present.join(" ");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolve a validated step to what to spawn: `{ cmd, args, cwd, env }`. `extraArgs` are appended after the step's own
|
|
63
|
+
* arguments (the `--` passthrough of `right-release test`). `cmd: "node"` and `node:` use `process.execPath`.
|
|
64
|
+
*/
|
|
65
|
+
export function resolveInvocation(step, { appRoot, baseEnv = process.env, extraArgs = [] }) {
|
|
66
|
+
const args = [...(step.args ?? []), ...extraArgs];
|
|
67
|
+
let cmd;
|
|
68
|
+
let argv;
|
|
69
|
+
if (step.node !== undefined) {
|
|
70
|
+
cmd = process.execPath;
|
|
71
|
+
argv = [...(step.nodeFlags ?? []), path.resolve(appRoot, step.node), ...args];
|
|
72
|
+
} else {
|
|
73
|
+
if (typeof step.cmd !== "string" || !step.cmd.trim()) fail("a step needs cmd or node");
|
|
74
|
+
cmd = step.cmd === "node" ? process.execPath : step.cmd;
|
|
75
|
+
argv = [...(step.cmd === "node" ? (step.nodeFlags ?? []) : []), ...args];
|
|
76
|
+
}
|
|
77
|
+
const env = { ...baseEnv, ...(step.env ?? {}) };
|
|
78
|
+
if (step.nodeOptions?.length) env.NODE_OPTIONS = mergeNodeOptions(env.NODE_OPTIONS, step.nodeOptions);
|
|
79
|
+
return { cmd, args: argv, cwd: path.resolve(appRoot, step.cwd ?? "."), env };
|
|
80
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Signature-independent content digest: exact JS port of crates/rightkit-ffmpeg/src/content_digest.rs.
|
|
2
|
+
// PE (Authenticode rule), thin 64-bit LE Mach-O (canonical unsigned image) & universal Mach-O (per-slice).
|
|
3
|
+
// A malformed recognized format FAILS (ContentDigestError); never falls back to a plain hash.
|
|
4
|
+
// Anything else is plain SHA-256. Archive/download SHA-256 remains a separate, independent value.
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
|
|
8
|
+
export class ContentDigestError extends Error {
|
|
9
|
+
constructor(message) { super(message); this.name = "ContentDigestError"; this.code = "CONTENT_DIGEST_MALFORMED"; }
|
|
10
|
+
}
|
|
11
|
+
const malformed = (why) => { throw new ContentDigestError(why); };
|
|
12
|
+
|
|
13
|
+
const MH_MAGIC_64 = 0xfeedfacf, LC_SEGMENT_64 = 0x19, LC_CODE_SIGNATURE = 0x1d;
|
|
14
|
+
const FAT_MAGIC = 0xcafebabe, FAT_MAGIC_64 = 0xcafebabf;
|
|
15
|
+
const CPU_X86_64 = 0x01000007, CPU_ARM64 = 0x0100000c;
|
|
16
|
+
const MAX_FAT_ARCHS = 16;
|
|
17
|
+
|
|
18
|
+
const sha256Parts = (parts) => { const h = createHash("sha256"); for (const p of parts) h.update(p); return h.digest("hex"); };
|
|
19
|
+
export const sha256Bytes = (bytes) => sha256Parts([Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes)]);
|
|
20
|
+
|
|
21
|
+
const le32 = (b, at) => (at + 4 <= b.length ? b.readUInt32LE(at) : null);
|
|
22
|
+
const le16 = (b, at) => (at + 2 <= b.length ? b.readUInt16LE(at) : null);
|
|
23
|
+
const be32 = (b, at) => (at + 4 <= b.length ? b.readUInt32BE(at) : null);
|
|
24
|
+
const le64n = (b, at) => (at + 8 <= b.length ? Number(b.readBigUInt64LE(at)) : null);
|
|
25
|
+
const be64n = (b, at) => (at + 8 <= b.length ? Number(b.readBigUInt64BE(at)) : null);
|
|
26
|
+
|
|
27
|
+
function archName(cputype) {
|
|
28
|
+
if (cputype === CPU_ARM64) return "aarch64";
|
|
29
|
+
if (cputype === CPU_X86_64) return "x86_64";
|
|
30
|
+
malformed("unsupported CPU type");
|
|
31
|
+
}
|
|
32
|
+
const roundUp = (v, to) => Math.floor((v + to - 1) / to) * to;
|
|
33
|
+
|
|
34
|
+
/** @returns {{format:"macho"|"macho-universal"|"pe"|"plain", slices:{arch:string|null, sha256:string}[]}} */
|
|
35
|
+
export function contentIdentity(input) {
|
|
36
|
+
const bytes = Buffer.isBuffer(input) ? input : Buffer.from(input);
|
|
37
|
+
const magicBe = be32(bytes, 0), magicLe = le32(bytes, 0);
|
|
38
|
+
if (magicBe === FAT_MAGIC || magicBe === FAT_MAGIC_64) return fatIdentity(bytes);
|
|
39
|
+
if (magicLe === MH_MAGIC_64) {
|
|
40
|
+
const { arch, sha256 } = machoSliceDigest(bytes);
|
|
41
|
+
return { format: "macho", slices: [{ arch, sha256 }] };
|
|
42
|
+
}
|
|
43
|
+
if ([0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe, 0xbebafeca, 0xbfbafeca].includes(magicBe)) {
|
|
44
|
+
malformed("unsupported Mach-O flavour (only 64-bit little-endian is catalogued)");
|
|
45
|
+
}
|
|
46
|
+
if (bytes.subarray(0, 2).toString("latin1") === "MZ") {
|
|
47
|
+
const { arch, sha256 } = peDigest(bytes);
|
|
48
|
+
return { format: "pe", slices: [{ arch, sha256 }] };
|
|
49
|
+
}
|
|
50
|
+
return { format: "plain", slices: [{ arch: null, sha256: sha256Bytes(bytes) }] };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function contentIdentityFile(file) { return contentIdentity(readFileSync(file)); }
|
|
54
|
+
export function digestFor(identity, arch) {
|
|
55
|
+
return identity.slices.find((s) => s.arch?.toLowerCase() === String(arch).toLowerCase())?.sha256 ?? null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function machoSliceDigest(slice) {
|
|
59
|
+
const bad = (why) => malformed(`Mach-O: ${why}`);
|
|
60
|
+
if (le32(slice, 0) !== MH_MAGIC_64) bad("not a 64-bit little-endian Mach-O");
|
|
61
|
+
const cputype = le32(slice, 4); if (cputype === null) bad("truncated header");
|
|
62
|
+
const arch = archName(cputype);
|
|
63
|
+
const ncmds = le32(slice, 16), sizeofcmds = le32(slice, 20);
|
|
64
|
+
if (ncmds === null || sizeofcmds === null) bad("truncated header");
|
|
65
|
+
const cmdsEnd = 32 + sizeofcmds;
|
|
66
|
+
if (cmdsEnd > slice.length) bad("load commands run past the end of the file");
|
|
67
|
+
const commands = [];
|
|
68
|
+
let offset = 32;
|
|
69
|
+
for (let i = 0; i < ncmds; i += 1) {
|
|
70
|
+
const cmd = le32(slice, offset), size = le32(slice, offset + 4);
|
|
71
|
+
if (cmd === null || size === null) bad("truncated load command");
|
|
72
|
+
if (size < 8 || size % 8 !== 0 || offset + size > cmdsEnd) bad("load command size is invalid");
|
|
73
|
+
commands.push({ offset, size, cmd });
|
|
74
|
+
offset += size;
|
|
75
|
+
}
|
|
76
|
+
if (offset !== cmdsEnd) bad("load commands do not fill sizeofcmds");
|
|
77
|
+
let signature = null, linkedit = null;
|
|
78
|
+
commands.forEach((c, index) => {
|
|
79
|
+
if (c.cmd === LC_CODE_SIGNATURE) {
|
|
80
|
+
if (c.size !== 16 || signature !== null) bad("invalid or repeated LC_CODE_SIGNATURE");
|
|
81
|
+
signature = index;
|
|
82
|
+
} else if (c.cmd === LC_SEGMENT_64) {
|
|
83
|
+
if (c.size < 72) bad("short LC_SEGMENT_64");
|
|
84
|
+
const name = slice.subarray(c.offset + 8, c.offset + 24);
|
|
85
|
+
const end = name.indexOf(0) === -1 ? name.length : name.indexOf(0);
|
|
86
|
+
if (name.subarray(0, end).toString("latin1") === "__LINKEDIT") {
|
|
87
|
+
if (linkedit !== null) bad("repeated __LINKEDIT");
|
|
88
|
+
linkedit = index;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
if (linkedit === null) bad("no __LINKEDIT segment");
|
|
93
|
+
const leOff = commands[linkedit].offset;
|
|
94
|
+
const vmsize = le64n(slice, leOff + 32), fileoff = le64n(slice, leOff + 40), filesize = le64n(slice, leOff + 48);
|
|
95
|
+
if (vmsize === null || fileoff === null || filesize === null) bad("truncated __LINKEDIT");
|
|
96
|
+
const len = slice.length;
|
|
97
|
+
if (fileoff < cmdsEnd || fileoff + filesize !== len || vmsize < filesize || vmsize % 0x1000 !== 0) bad("__LINKEDIT does not run to the end of the file");
|
|
98
|
+
let contentEnd = len;
|
|
99
|
+
if (signature !== null) {
|
|
100
|
+
const off = commands[signature].offset;
|
|
101
|
+
const dataoff = le32(slice, off + 8), datasize = le32(slice, off + 12);
|
|
102
|
+
if (dataoff === null || datasize === null) bad("truncated signature");
|
|
103
|
+
if (dataoff < fileoff || dataoff + datasize !== len) bad("code signature is not the tail of __LINKEDIT");
|
|
104
|
+
contentEnd = dataoff;
|
|
105
|
+
}
|
|
106
|
+
let end = contentEnd;
|
|
107
|
+
while (end > fileoff && slice[end - 1] === 0) end -= 1;
|
|
108
|
+
const canonicalFilesize = end - fileoff;
|
|
109
|
+
const page = cputype === CPU_ARM64 ? 0x4000 : 0x1000;
|
|
110
|
+
const canonicalVmsize = roundUp(canonicalFilesize, page);
|
|
111
|
+
const head = Buffer.alloc(cmdsEnd);
|
|
112
|
+
slice.copy(head, 0, 0, 32);
|
|
113
|
+
let cursor = 32, newLinkeditAt = 0, removed = 0;
|
|
114
|
+
commands.forEach((c, index) => {
|
|
115
|
+
if (index === signature) { removed = c.size; return; }
|
|
116
|
+
if (index === linkedit) newLinkeditAt = cursor;
|
|
117
|
+
slice.copy(head, cursor, c.offset, c.offset + c.size);
|
|
118
|
+
cursor += c.size;
|
|
119
|
+
});
|
|
120
|
+
head.writeBigUInt64LE(BigInt(canonicalVmsize), newLinkeditAt + 32);
|
|
121
|
+
head.writeBigUInt64LE(BigInt(canonicalFilesize), newLinkeditAt + 48);
|
|
122
|
+
head.writeUInt32LE(ncmds - (signature === null ? 0 : 1), 16);
|
|
123
|
+
head.writeUInt32LE(sizeofcmds - removed, 20);
|
|
124
|
+
return { arch, sha256: sha256Parts([head, slice.subarray(cmdsEnd, end)]) };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function fatIdentity(bytes) {
|
|
128
|
+
const bad = (why) => malformed(`universal Mach-O: ${why}`);
|
|
129
|
+
const wide = be32(bytes, 0) === FAT_MAGIC_64;
|
|
130
|
+
const count = be32(bytes, 4);
|
|
131
|
+
if (count === null) bad("truncated header");
|
|
132
|
+
if (count === 0 || count > MAX_FAT_ARCHS) bad("implausible architecture count");
|
|
133
|
+
const entrySize = wide ? 32 : 20;
|
|
134
|
+
const tableEnd = 8 + entrySize * count;
|
|
135
|
+
if (tableEnd > bytes.length) bad("architecture table runs past the end of the file");
|
|
136
|
+
const slices = [], seen = [], ranges = [];
|
|
137
|
+
for (let index = 0; index < count; index += 1) {
|
|
138
|
+
const at = 8 + index * entrySize;
|
|
139
|
+
const cputype = be32(bytes, at);
|
|
140
|
+
const off = wide ? be64n(bytes, at + 8) : be32(bytes, at + 8);
|
|
141
|
+
const size = wide ? be64n(bytes, at + 16) : be32(bytes, at + 12);
|
|
142
|
+
if (cputype === null || off === null || size === null) bad("truncated entry");
|
|
143
|
+
const end = off + size;
|
|
144
|
+
if (size === 0 || off < tableEnd || end > bytes.length) bad("slice is empty or out of bounds");
|
|
145
|
+
if (seen.includes(cputype)) bad("duplicate architecture");
|
|
146
|
+
if (ranges.some(([s, e]) => off < e && s < end)) bad("slices overlap");
|
|
147
|
+
seen.push(cputype); ranges.push([off, end]);
|
|
148
|
+
const { arch, sha256 } = machoSliceDigest(bytes.subarray(off, end));
|
|
149
|
+
if (arch !== archName(cputype)) bad("slice architecture differs from the fat header");
|
|
150
|
+
slices.push({ arch, sha256 });
|
|
151
|
+
}
|
|
152
|
+
return { format: "macho-universal", slices };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function peDigest(bytes) {
|
|
156
|
+
const bad = (why) => malformed(`PE: ${why}`);
|
|
157
|
+
const len = bytes.length;
|
|
158
|
+
const pe = le32(bytes, 0x3c);
|
|
159
|
+
if (pe === null) bad("no PE header offset");
|
|
160
|
+
if (bytes.subarray(pe, pe + 4).toString("latin1") !== "PE\0\0") bad("missing PE signature");
|
|
161
|
+
const machine = le16(bytes, pe + 4);
|
|
162
|
+
const arch = machine === 0x8664 ? "x86_64" : machine === 0xaa64 ? "aarch64" : bad("unsupported machine type");
|
|
163
|
+
const nsections = le16(bytes, pe + 6), optSize = le16(bytes, pe + 20);
|
|
164
|
+
if (nsections === null || optSize === null) bad("truncated COFF header");
|
|
165
|
+
const opt = pe + 24;
|
|
166
|
+
const sectionsAt = opt + optSize;
|
|
167
|
+
if (sectionsAt > len) bad("optional header runs past the end of the file");
|
|
168
|
+
const magic = le16(bytes, opt);
|
|
169
|
+
const dirs = magic === 0x10b ? 96 : magic === 0x20b ? 112 : bad("unknown optional header magic");
|
|
170
|
+
if (optSize < dirs) bad("optional header too short");
|
|
171
|
+
const checksumAt = opt + 64;
|
|
172
|
+
const sizeOfHeaders = le32(bytes, opt + 60), dirCount = le32(bytes, opt + dirs - 4);
|
|
173
|
+
if (sizeOfHeaders === null || dirCount === null) bad("truncated optional header");
|
|
174
|
+
const certEntry = dirCount > 4 && optSize >= dirs + 40 ? opt + dirs + 32 : null;
|
|
175
|
+
const tableEnd = sectionsAt + nsections * 40;
|
|
176
|
+
if (tableEnd > len) bad("section table runs past the end of the file");
|
|
177
|
+
let floor = checksumAt + 4;
|
|
178
|
+
if (certEntry !== null) floor = Math.max(floor, certEntry + 8);
|
|
179
|
+
if (sizeOfHeaders > len || sizeOfHeaders < Math.max(floor, tableEnd)) bad("SizeOfHeaders is inconsistent");
|
|
180
|
+
let certOffset = 0, certSize = 0;
|
|
181
|
+
if (certEntry !== null) {
|
|
182
|
+
certOffset = le32(bytes, certEntry); certSize = le32(bytes, certEntry + 4);
|
|
183
|
+
if (certOffset === null || certSize === null) bad("truncated directory");
|
|
184
|
+
}
|
|
185
|
+
if (certSize > 0 && (certOffset < sizeOfHeaders || certOffset + certSize !== len)) bad("certificate table is not the tail of the file");
|
|
186
|
+
const sections = [];
|
|
187
|
+
for (let i = 0; i < nsections; i += 1) {
|
|
188
|
+
const at = sectionsAt + i * 40;
|
|
189
|
+
const size = le32(bytes, at + 16), off = le32(bytes, at + 20);
|
|
190
|
+
if (size === null || off === null) bad("truncated section");
|
|
191
|
+
if (size > 0) sections.push([off, size]);
|
|
192
|
+
}
|
|
193
|
+
sections.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
194
|
+
const parts = [bytes.subarray(0, checksumAt)];
|
|
195
|
+
if (certEntry !== null) {
|
|
196
|
+
parts.push(bytes.subarray(checksumAt + 4, certEntry), bytes.subarray(certEntry + 8, sizeOfHeaders));
|
|
197
|
+
} else parts.push(bytes.subarray(checksumAt + 4, sizeOfHeaders));
|
|
198
|
+
let hashed = sizeOfHeaders, previousEnd = sizeOfHeaders;
|
|
199
|
+
const bodyEnd = len - certSize;
|
|
200
|
+
for (const [off, size] of sections) {
|
|
201
|
+
const end = off + size;
|
|
202
|
+
if (off < previousEnd || end > bodyEnd) bad("section overlaps the headers or the certificate table");
|
|
203
|
+
parts.push(bytes.subarray(off, end));
|
|
204
|
+
hashed += size; previousEnd = end;
|
|
205
|
+
}
|
|
206
|
+
if (hashed > bodyEnd) bad("hashed bytes exceed the file");
|
|
207
|
+
parts.push(bytes.subarray(hashed, bodyEnd));
|
|
208
|
+
return { arch, sha256: sha256Parts(parts) };
|
|
209
|
+
}
|
package/deps-cargo.mjs
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// `right-release deps --audit` for Cargo workdirs (Orthic-Labs/rightkit#674): replaces an app's own audit-rust script.
|
|
2
|
+
//
|
|
3
|
+
// deps: { workdirs: ["."], cargoWorkdirs: ["src-tauri"] } // app-relative folders that hold a Cargo.lock
|
|
4
|
+
//
|
|
5
|
+
// Declared only: an app with no `deps.cargoWorkdirs` audits no Rust, exactly as before. Each folder must be inside the app and
|
|
6
|
+
// hold a `Cargo.lock` (an unlocked workspace cannot be audited, so it is refused, never skipped). `cargo audit` runs through
|
|
7
|
+
// the managed cargo client (`runCargoSync`): on a managed host it goes through the RightKit broker, and a host with no
|
|
8
|
+
// broker or no `cargo-audit` fails closed with the reason; there is never a bare-cargo fallback on a managed Mac.
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { runCargoSync } from "./managed-cargo.mjs";
|
|
12
|
+
|
|
13
|
+
const isInside = (root, target) => {
|
|
14
|
+
const relative = path.relative(root, target);
|
|
15
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** The validated Cargo workdirs `[{ rel, cwd }]` of a release config (empty when none are declared). Throws on a bad declaration. */
|
|
19
|
+
export function cargoAuditWorkdirs(config, root, { exists = existsSync } = {}) {
|
|
20
|
+
const declared = config?.deps?.cargoWorkdirs;
|
|
21
|
+
if (declared === undefined) return [];
|
|
22
|
+
if (!Array.isArray(declared) || declared.some((item) => typeof item !== "string" || !item.trim())) {
|
|
23
|
+
throw new Error("deps.cargoWorkdirs must be a list of app-relative folders");
|
|
24
|
+
}
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
const workdirs = [];
|
|
27
|
+
for (const rel of declared) {
|
|
28
|
+
const normalized = rel.replaceAll("\\", "/");
|
|
29
|
+
if (path.isAbsolute(rel) || /^[A-Za-z]:/.test(rel) || normalized.split("/").includes("..")) {
|
|
30
|
+
throw new Error(`deps.cargoWorkdirs entry must be relative and stay inside the app: ${rel}`);
|
|
31
|
+
}
|
|
32
|
+
const cwd = path.resolve(root, normalized);
|
|
33
|
+
if (!isInside(root, cwd)) throw new Error(`deps.cargoWorkdirs entry must stay inside the app: ${rel}`);
|
|
34
|
+
if (seen.has(cwd)) continue;
|
|
35
|
+
seen.add(cwd);
|
|
36
|
+
if (!exists(path.join(cwd, "Cargo.lock"))) throw new Error(`deps.cargoWorkdirs entry ${rel} has no Cargo.lock (${path.join(cwd, "Cargo.lock")}); an unlocked workspace cannot be audited`);
|
|
37
|
+
workdirs.push({ rel, cwd });
|
|
38
|
+
}
|
|
39
|
+
return workdirs;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Run `cargo audit` in each workdir and collect failures so every workdir reports. `run(args, options)` is the managed
|
|
44
|
+
* cargo runner (injectable). Returns `{ failures: string[] }`: one entry per workdir that could not start or exited non-zero.
|
|
45
|
+
*/
|
|
46
|
+
export function runCargoAudits(workdirs, { run = runCargoSync, log = console.log, env = process.env } = {}) {
|
|
47
|
+
const failures = [];
|
|
48
|
+
for (const { rel, cwd } of workdirs) {
|
|
49
|
+
log(`\n> cargo audit (${cwd})`);
|
|
50
|
+
const result = run(["audit"], { cwd, env, stdio: "inherit" });
|
|
51
|
+
if (result?.error) failures.push(`${rel}: cargo audit could not start (${result.error.message})`);
|
|
52
|
+
else if (result?.status !== 0) failures.push(`${rel}: cargo audit exited ${result?.status ?? "without status"}`);
|
|
53
|
+
}
|
|
54
|
+
return { failures };
|
|
55
|
+
}
|
package/deps.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { spawnSync } from "./background-process.mjs";
|
|
5
5
|
import { existsSync } from "node:fs";
|
|
6
|
+
import { cargoAuditWorkdirs, runCargoAudits } from "./deps-cargo.mjs";
|
|
6
7
|
|
|
7
8
|
const args = process.argv.slice(2);
|
|
8
9
|
const opts = {
|
|
@@ -55,6 +56,12 @@ for (const { rel, cwd } of resolvedWorkdirs) {
|
|
|
55
56
|
run("pnpm", ["install", ...workspaceArgs, "--lockfile-only", "--ignore-scripts"], cwd, { ignoreScripts: true });
|
|
56
57
|
}
|
|
57
58
|
}
|
|
59
|
+
if (opts.mode === "audit") {
|
|
60
|
+
// Rust advisories (#674): every deps.cargoWorkdirs folder, through the managed cargo client; declared only.
|
|
61
|
+
let cargoWorkdirs;
|
|
62
|
+
try { cargoWorkdirs = cargoAuditWorkdirs(config, root); } catch (error) { fail(error.message); }
|
|
63
|
+
failures.push(...runCargoAudits(cargoWorkdirs).failures);
|
|
64
|
+
}
|
|
58
65
|
if (failures.length > 0) {
|
|
59
66
|
for (const failure of failures) console.error(`right-deps: ${failure}`);
|
|
60
67
|
fail(`${failures.length} dependency workdir(s) failed`);
|
package/development.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { stageDevelopmentBinaries, validateStageEntries } from "./stage-binary.m
|
|
|
14
14
|
import { assertStableDesignatedRequirement, readBundleExecutable, requestedCiAdhocSigning, signMacosDevelopmentBundle } from "./development-signing.mjs";
|
|
15
15
|
import { macosCodesignEnvironment } from "./codesign-environment.mjs";
|
|
16
16
|
import { installTauriDevId, resolveTauriDevIdDescriptor, TAURI_DEVID_KIND } from "./install-tauri-devid.mjs";
|
|
17
|
+
import { assertIosDevelopmentSession, installIosDevelopment, prepareIosDevelopment, validateIosDevelopment } from "./ios-dev.mjs";
|
|
17
18
|
import { androidDevelopmentNdkCommand, androidDevelopmentSerial, installAndroidDevelopment, validateAndroidDevelopment } from "./android-dev.mjs";
|
|
18
19
|
|
|
19
20
|
// One standard Cargo profile for every app (templates/cargo-profiles.toml): Cargo's built-in `dev`.
|
|
@@ -58,6 +59,8 @@ export function developmentEnvironment(env = process.env) {
|
|
|
58
59
|
|
|
59
60
|
export function validateDevelopmentConfig(config, platform, { root = process.cwd() } = {}) {
|
|
60
61
|
const target = config?.development?.targets?.[platform];
|
|
62
|
+
// iOS builds its commands from project, scheme and install (ios-dev.mjs); there is no hand-written build array.
|
|
63
|
+
if (platform === "ios") return validateIosDevelopment(target, { root });
|
|
61
64
|
if (!target || !Array.isArray(target.build) || target.build.length === 0) throw new Error(`development.targets.${platform}.build must be a non-empty command array`);
|
|
62
65
|
for (const [index, command] of target.build.entries()) validateCommand(command, `development.targets.${platform}.build[${index}]`);
|
|
63
66
|
if (target.install?.kind === "command") validateCommand(target.install, `development.targets.${platform}.install`);
|
|
@@ -85,8 +88,9 @@ export function validateDevelopmentConfig(config, platform, { root = process.cwd
|
|
|
85
88
|
|
|
86
89
|
export function runDevelopment({ root, platform, config, buildOnly = false, dryRun = false, env = process.env, run = spawnSync,
|
|
87
90
|
codesignEnvironment = macosCodesignEnvironment, serial }) {
|
|
88
|
-
if (!new Set(["win", "mac", "android"]).has(platform)) throw new Error("--platform must be win, mac or
|
|
89
|
-
|
|
91
|
+
if (!new Set(["win", "mac", "android", "ios"]).has(platform)) throw new Error("--platform must be win, mac, android or ios");
|
|
92
|
+
// iOS: --dry-run validates the config and prints the plan on any host; everything else needs macOS with Xcode.
|
|
93
|
+
if (!(platform === "ios" && dryRun)) assertNativeDevelopmentHost(platform);
|
|
90
94
|
const target = validateDevelopmentConfig(config, platform, { root });
|
|
91
95
|
const repoRoot = git(root, ["rev-parse", "--show-toplevel"]);
|
|
92
96
|
assertPrimaryReleaseCheckout(repoRoot, env);
|
|
@@ -94,6 +98,12 @@ export function runDevelopment({ root, platform, config, buildOnly = false, dryR
|
|
|
94
98
|
assertReleaseExecutionAllowed({ cwd: repoRoot, environment: env, command: `right-release dev --platform ${platform}` });
|
|
95
99
|
// Refuse an invalid or off-CI ad hoc opt-in before the (long) build, not after it.
|
|
96
100
|
if (!buildOnly && platform === "mac" && target.install?.kind === "tauri") requestedCiAdhocSigning({ descriptor: target.install, env });
|
|
101
|
+
// iOS: the device or simulator is resolved (and a device proven ready) before the build, and a device build needs the GUI signing session.
|
|
102
|
+
let iosPrepared = null;
|
|
103
|
+
if (platform === "ios") {
|
|
104
|
+
assertIosDevelopmentSession(target, { env, run });
|
|
105
|
+
iosPrepared = prepareIosDevelopment(target, { root, env, run, serial });
|
|
106
|
+
}
|
|
97
107
|
// Android: the device serial is required, so a missing one is refused before the (long) build too.
|
|
98
108
|
if (!buildOnly && platform === "android" && target.install) androidDevelopmentSerial({ descriptor: target.install, serial, env });
|
|
99
109
|
const childEnv = codesignEnvironment(developmentEnvironment(env));
|
|
@@ -104,12 +114,13 @@ export function runDevelopment({ root, platform, config, buildOnly = false, dryR
|
|
|
104
114
|
const startedMs = Date.now();
|
|
105
115
|
// Android: cargo-ndk (Cargo dev profile) fills the generated jniLibs, then the Gradle debug build packages it.
|
|
106
116
|
if (target.ndk) { rmSync(target.ndk.jniLibs, { recursive: true, force: true }); execute(androidDevelopmentNdkCommand(target.ndk), { root, env: childEnv, dryRun, run, label: "native build" }); }
|
|
107
|
-
for (const command of target.build) execute(command, { root, env: childEnv, dryRun, run, label: "build" });
|
|
117
|
+
for (const command of iosPrepared ? iosPrepared.build : target.build) execute(command, { root, env: childEnv, dryRun, run, label: "build" });
|
|
108
118
|
// Dev and QA binaries run from one stable staged path, copied in place (no backup of the old copy), never from the
|
|
109
119
|
// broker's per-generation target folder or a hashed dev-targets folder.
|
|
110
120
|
const staged = target.stage?.length ? stageDevelopmentBinaries(target.stage, { root, platform: platform === "win" ? "win32" : process.platform }) : [];
|
|
111
121
|
for (const file of staged) console.log(`right-release dev: staged ${file}`);
|
|
112
122
|
if (buildOnly || !target.install) return { built: true, installed: false, ...(staged.length ? { staged } : {}) };
|
|
123
|
+
if (iosPrepared) return { built: true, installed: true, ios: installIosDevelopment(iosPrepared.install, { root, env: childEnv, run }) };
|
|
113
124
|
if (target.install.kind === TAURI_DEVID_KIND) {
|
|
114
125
|
// installTauriDevId is async: the lease is held until it settles, so a broker cutover cannot land mid-install.
|
|
115
126
|
const built = resolveTauriArtifact(target.install, { root, platform, env: childEnv });
|
|
@@ -124,7 +135,7 @@ export function runDevelopment({ root, platform, config, buildOnly = false, dryR
|
|
|
124
135
|
}
|
|
125
136
|
|
|
126
137
|
export function assertNativeDevelopmentHost(platform, host = process.platform) {
|
|
127
|
-
if ((platform === "win" && host !== "win32") || (platform === "mac" && host !== "darwin")) throw new Error(`development target ${platform} requires its native host`);
|
|
138
|
+
if ((platform === "win" && host !== "win32") || ((platform === "mac" || platform === "ios") && host !== "darwin")) throw new Error(`development target ${platform} requires its native host`);
|
|
128
139
|
}
|
|
129
140
|
|
|
130
141
|
function install(descriptor, { root, platform, config, env, run, sinceMs = null, serial }) {
|
|
@@ -338,7 +349,7 @@ function inside(root, child) { const rel = path.relative(path.resolve(root), pat
|
|
|
338
349
|
export async function main(args = process.argv.slice(2)) {
|
|
339
350
|
let platform, configPath = "right-release.config.mjs", dryRun = false, buildOnly = false, serial;
|
|
340
351
|
for (let i = 0; i < args.length; i++) {
|
|
341
|
-
if (args[i] === "-h" || args[i] === "--help") { console.log("usage: right-release dev --platform win|mac|android [--config right-release.config.mjs] [--dry-run] [--build-only] [--serial <adb serial>]"); return; }
|
|
352
|
+
if (args[i] === "-h" || args[i] === "--help") { console.log("usage: right-release dev --platform win|mac|android|ios [--config right-release.config.mjs] [--dry-run] [--build-only] [--serial <adb serial | iOS device id>]"); return; }
|
|
342
353
|
if (args[i] === "--platform") platform = args[++i];
|
|
343
354
|
else if (args[i] === "--config") configPath = args[++i];
|
|
344
355
|
else if (args[i] === "--dry-run") dryRun = true;
|
|
@@ -346,7 +357,7 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
346
357
|
else if (args[i] === "--serial") serial = args[++i];
|
|
347
358
|
else throw new Error(`unknown dev option: ${args[i]}`);
|
|
348
359
|
}
|
|
349
|
-
if (!platform) throw new Error("right-release dev requires --platform win|mac|android");
|
|
360
|
+
if (!platform) throw new Error("right-release dev requires --platform win|mac|android|ios");
|
|
350
361
|
const absoluteConfig = path.resolve(configPath);
|
|
351
362
|
const safeEnv = developmentEnvironment(process.env);
|
|
352
363
|
for (const [key, value] of Object.entries(safeEnv)) process.env[key] = value;
|
package/direct-bootstrap.mjs
CHANGED
|
@@ -289,7 +289,7 @@ export function renderPowerShellBootstrap({
|
|
|
289
289
|
"$AcceptedSigners = @{}; $Contract.acceptedManifestSigners.PSObject.Properties | ForEach-Object { $AcceptedSigners[$_.Name] = $_.Value }",
|
|
290
290
|
"$InstallRoot = Join-Path $env:LOCALAPPDATA " + psQuote(installRootSubdir),
|
|
291
291
|
// In place at one fixed path: no versions/ folder, no per-version, per-digest or per-run directory, no junction.
|
|
292
|
-
// `current` is the real install; `current.staging` is written and verified but never run; `current.previous` is
|
|
292
|
+
// `current` is the real install; `current.staging` is written and verified but never run; `current.previous` is a transient park, deleted on success.
|
|
293
293
|
"$Current = Join-Path $InstallRoot 'current'",
|
|
294
294
|
"$Staging = Join-Path $InstallRoot 'current.staging'",
|
|
295
295
|
"$Previous = Join-Path $InstallRoot 'current.previous'",
|
|
@@ -436,7 +436,6 @@ function Invoke-BoundedExe($Path, [string[]]$Arguments, [int]$TimeoutSeconds) {
|
|
|
436
436
|
"$PriorCliCapable = $true; if ($Contract.priorExecutableMinVersion) { $PriorCliCapable = ($null -ne $PriorVersion -and ([version]$PriorVersion -ge [version]$Contract.priorExecutableMinVersion)) }",
|
|
437
437
|
"$PriorStatus = $null; if ($PriorTarget -and $PriorCliCapable) { $PriorExe = Join-Path $Current " + psQuote(executablePath.replaceAll("/", "\\")) + "; $PriorStatusArgs = @(Expand-BootstrapArgs $StatusArgsTemplate ([string]$PriorTarget) $PriorVersion); try { $PriorStatus = ((Invoke-BoundedExe $PriorExe $PriorStatusArgs 60) | Out-String).Trim() } catch {} }",
|
|
438
438
|
"@{ schemaVersion=1; priorTarget=$PriorTarget; priorVersion=$PriorVersion; targetVersion=$Version; priorStatus=$PriorStatus } | ConvertTo-Json -Depth 8 | Set-Content -Encoding UTF8 $Journal",
|
|
439
|
-
"$PriorWasJunction = $false; $CurrentItem = Get-Item -LiteralPath $Current -Force -ErrorAction SilentlyContinue; if ($CurrentItem -and $CurrentItem.LinkType -eq 'Junction') { $PriorWasJunction = $true }",
|
|
440
439
|
"Remove-Retired $Previous",
|
|
441
440
|
"$Switched = $false; $PriorDetached = $false",
|
|
442
441
|
"try {",
|
|
@@ -449,10 +448,10 @@ function Invoke-BoundedExe($Path, [string[]]$Arguments, [int]$TimeoutSeconds) {
|
|
|
449
448
|
" $HealthText = ((Invoke-BoundedExe $StableExe $StatusArgs 120) | Out-String).Trim(); if ($LASTEXITCODE -ne 0) { throw 'Health command failed' }; $Health = $HealthText | ConvertFrom-Json",
|
|
450
449
|
" Assert-Health $Health $Version",
|
|
451
450
|
" $StableBin = Split-Path $StableExe -Parent; $InstallRootResolved = [IO.Path]::GetFullPath($InstallRoot).TrimEnd([IO.Path]::DirectorySeparatorChar); $UserPath = [Environment]::GetEnvironmentVariable('Path','User'); $Kept = @($UserPath -split ';' | Where-Object { $Entry = $_.Trim(); $Entry -ne '' -and -not ($Entry.TrimEnd([IO.Path]::DirectorySeparatorChar).StartsWith(($InstallRootResolved + [IO.Path]::DirectorySeparatorChar), [StringComparison]::OrdinalIgnoreCase) -and -not $Entry.TrimEnd([IO.Path]::DirectorySeparatorChar).Equals($StableBin.TrimEnd([IO.Path]::DirectorySeparatorChar), [StringComparison]::OrdinalIgnoreCase)) }); $NewPath = ((@($StableBin) + $Kept) -join ';'); if ($NewPath -ne $UserPath) { [Environment]::SetEnvironmentVariable('Path', $NewPath, 'User') }",
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
|
|
455
|
-
" Remove-Item -Force $Journal; Write-Output (" + psQuote(product + " ") + " + $Version + ' installed')",
|
|
451
|
+
// Fixed-path install only (D01): no versioned folders, pointer junctions or legacy-layout cleanup in the
|
|
452
|
+
// generated script. A pre-existing legacy layout under $InstallRoot is left untouched, never deleted whole.
|
|
453
|
+
// No retained rollback copy (D01): $Previous is a transient park, deleted once health passes.
|
|
454
|
+
" Remove-Retired $Previous; Remove-Item -Force $Journal; Write-Output (" + psQuote(product + " ") + " + $Version + ' installed')",
|
|
456
455
|
"} catch {",
|
|
457
456
|
" $InstallError = $_; $RollbackErrors = New-Object 'System.Collections.Generic.List[string]'",
|
|
458
457
|
" try { if ($Switched) { Remove-Retired $Current }; if ($PriorDetached -and (Test-Path -LiteralPath $Previous)) { Rename-Item -LiteralPath $Previous -NewName (Split-Path -Leaf $Current) } } catch { $RollbackErrors.Add($_.Exception.Message) }",
|
package/disk-command.mjs
CHANGED
|
@@ -580,6 +580,7 @@ export async function runDiskCommand(args = process.argv.slice(2), { env = proce
|
|
|
580
580
|
if (!self.ok) {
|
|
581
581
|
for (const problem of self.problems) stdout(`ERROR: disk run self-check: ${problem}`);
|
|
582
582
|
logEvent(logFile, { event: "disk.run.selfcheck.fail", problems: self.problems, version: self.version });
|
|
583
|
+
return { exitCode: 2, selfCheck: self };
|
|
583
584
|
}
|
|
584
585
|
ensureScratchRoots({ policy, platform, env });
|
|
585
586
|
const before = check();
|
package/disk-policy.json
CHANGED
|
@@ -33,13 +33,14 @@
|
|
|
33
33
|
{"id": "cache-parakeet", "platform": "mac", "path": "~/Library/Caches/parakeet_*", "capBytes": 1073741824, "action": "sweep", "unit": "self"},
|
|
34
34
|
{"id": "cache-swiftright-factory", "platform": "mac", "path": "~/Library/Caches/SwiftRightFactory", "capBytes": 10737418240, "action": "sweep", "unit": "children"},
|
|
35
35
|
{"id": "cache-cutright-engine", "platform": "mac", "path": "~/Library/Caches/cutright-engine", "capBytes": 2147483648, "action": "report", "unit": "children", "escalate": {"factor": 2, "staleDays": 7}},
|
|
36
|
+
{"id": "cache-cutright-app", "platform": "mac", "path": "~/Library/Caches/app.orthiclabs.cutright", "capBytes": 2147483648, "action": "report", "unit": "children", "escalate": {"factor": 2, "staleDays": 7}},
|
|
36
37
|
{"id": "cache-heardright-engine", "platform": "mac", "path": "~/Library/Caches/heardright-engine", "capBytes": 2147483648, "action": "report", "unit": "children", "escalate": {"factor": 2, "staleDays": 7}},
|
|
37
38
|
{"id": "cache-pnpm", "platform": "mac", "path": "~/Library/Caches/pnpm", "capBytes": 3221225472, "action": "report", "unit": "children", "escalate": {"factor": 2, "staleDays": 7}},
|
|
38
39
|
{"id": "cache-python", "platform": "mac", "path": "~/Library/Caches/python", "capBytes": 3221225472, "action": "report", "unit": "children", "escalate": {"factor": 2, "staleDays": 7}},
|
|
39
40
|
{"id": "xcode-deriveddata", "platform": "mac", "path": "~/Library/Developer/Xcode/DerivedData", "capBytes": 10737418240, "action": "sweep", "unit": "children"},
|
|
40
41
|
{"id": "coresimulator", "platform": "mac", "path": "~/Library/Developer/CoreSimulator", "capBytes": 26843545600, "action": "report"},
|
|
41
42
|
{"id": "documents-scratch", "platform": "mac", "path": "~/Documents", "capBytes": 21474836480, "action": "report"},
|
|
42
|
-
{"id": "shared-models", "platform": "mac", "path": "/
|
|
43
|
+
{"id": "shared-models", "platform": "mac", "path": "~/Library/Application Support/Orthic Labs/shared/models", "capBytes": 107374182400, "action": "report"},
|
|
43
44
|
{"id": "stale-release-tarballs", "platform": "mac", "path": "/Volumes/D/claude/rightkit/packages/release/rightkit-release-*.tgz", "capBytes": 1073741824, "action": "sweep", "unit": "self"},
|
|
44
45
|
{"id": "win-temp", "platform": "win", "path": "${TEMP}", "capBytes": 10737418240, "action": "sweep", "unit": "children"},
|
|
45
46
|
{"id": "win-pnpm", "platform": "win", "path": "${LOCALAPPDATA}\\pnpm", "capBytes": 21474836480, "action": "report"},
|
|
@@ -61,7 +62,10 @@
|
|
|
61
62
|
{"id": "leftover-legion-final", "platform": "mac", "path": "/Volumes/D/claude/legion-final-*", "capBytes": 1073741824, "action": "sweep", "unit": "self"},
|
|
62
63
|
{"id": "leftover-pulse-bak", "platform": "win", "path": "${LOCALAPPDATA}\\Programs\\Pulse.bak-*", "capBytes": 1073741824, "action": "sweep", "unit": "self"},
|
|
63
64
|
{"id": "qa-evidence-runs", "platform": "mac", "path": "/Volumes/D/claude/*/.cache/rightkit-qa/*", "capBytes": 5368709120, "action": "sweep", "unit": "self", "staleDays": 3, "skipTracked": true},
|
|
64
|
-
{"id": "qa-evidence-runs", "platform": "win", "path": "D:\\claude\\*\\.cache\\rightkit-qa\\*", "capBytes": 5368709120, "action": "sweep", "unit": "self", "staleDays": 3, "skipTracked": true}
|
|
65
|
+
{"id": "qa-evidence-runs", "platform": "win", "path": "D:\\claude\\*\\.cache\\rightkit-qa\\*", "capBytes": 5368709120, "action": "sweep", "unit": "self", "staleDays": 3, "skipTracked": true},
|
|
66
|
+
{"id": "build-control-targets", "platform": "mac", "path": "/Volumes/D/.rightkit-managed/rightkit-build-control", "capBytes": 429496729600, "action": "report"},
|
|
67
|
+
{"id": "scratch-total", "platform": "mac", "path": "/Volumes/D/scratch", "capBytes": 322122547200, "action": "report"},
|
|
68
|
+
{"id": "claude-tmp", "platform": "mac", "path": "/Volumes/D/claude/tmp", "capBytes": 53687091200, "action": "report"}
|
|
65
69
|
],
|
|
66
70
|
"protectedSegments": ["Application Support", "Keychains", "Keychain", "HeardRightNext", "Credentials", ".ssh", ".rightkit-managed", ".rightkit-swept"]
|
|
67
71
|
}
|
package/disk-stable-root.mjs
CHANGED
|
@@ -64,9 +64,10 @@ export function collectDependencies(pkgRoot) {
|
|
|
64
64
|
return [...found].map(([name, dir]) => ({ name, dir }));
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
/** Copy the installed @rightkit/release package (and its dependencies) into the stable root. The new tree is built
|
|
68
|
-
*
|
|
69
|
-
* `current
|
|
67
|
+
/** Copy the installed @rightkit/release package (and its dependencies) into the stable root. The new tree is built in
|
|
68
|
+
* the fixed `current.staging`, swapped in by rename (the old copy is parked at `current.previous` only until the
|
|
69
|
+
* smoke test at `current` passes, then deleted; a failed swap or smoke run renames it back). No copy is retained.
|
|
70
|
+
* Returns `{ stable, cli, version, replaced, unchanged }`. */
|
|
70
71
|
export function installStableRoot({ cli, platform = process.platform, env = process.env, home = os.homedir(), now = Date.now(), nodePath = process.execPath, smoke = true } = {}) {
|
|
71
72
|
const stable = stableRootPath({ platform, env, home });
|
|
72
73
|
const stableCli = stableCliPath(stable, platform);
|
|
@@ -107,10 +108,10 @@ export function installStableRoot({ cli, platform = process.platform, env = proc
|
|
|
107
108
|
throw error;
|
|
108
109
|
}
|
|
109
110
|
// Swap first, then smoke-run at the fixed `current` path (never from the staging path). A failed run renames
|
|
110
|
-
// `current.previous` back
|
|
111
|
-
|
|
111
|
+
// the transiently parked `current.previous` back; a passed run deletes it (nothing is retained).
|
|
112
|
+
const replaced = existsSync(stable);
|
|
112
113
|
try {
|
|
113
|
-
|
|
114
|
+
swapInPlace({
|
|
114
115
|
staging, destination: stable, previous,
|
|
115
116
|
verify: () => {
|
|
116
117
|
if (!smoke) return;
|
|
@@ -118,7 +119,6 @@ export function installStableRoot({ cli, platform = process.platform, env = proc
|
|
|
118
119
|
if (r.status !== 0) throw new Error(`the installed copy does not run: ${r.stderr || r.stdout || r.error?.message}`);
|
|
119
120
|
},
|
|
120
121
|
});
|
|
121
|
-
replaced = result.previous !== null;
|
|
122
122
|
} catch (error) {
|
|
123
123
|
throw new Error(`disk: could not replace ${stable}: ${error.message}`);
|
|
124
124
|
}
|
|
@@ -130,8 +130,12 @@ export function readScheduleBinding({ platform = process.platform, home = os.hom
|
|
|
130
130
|
if (platform === "darwin") {
|
|
131
131
|
const plist = plistFile ?? file ?? path.join(home, "Library", "LaunchAgents", "dev.rightkit.disk-hygiene.plist");
|
|
132
132
|
if (!existsSync(plist)) return null;
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
// Parse the plist natively: launchd/plutil formatting and XML escaping must not change the binding.
|
|
134
|
+
const result = run("/usr/bin/plutil", ["-extract", "ProgramArguments", "json", "-o", "-", "--", plist], { encoding: "utf8" });
|
|
135
|
+
let args;
|
|
136
|
+
try { if (result.status === 0) args = JSON.parse(String(result.stdout)); } catch { /* malformed binding fails evaluation below */ }
|
|
137
|
+
const valid = Array.isArray(args) && typeof args[0] === "string" && typeof args[1] === "string";
|
|
138
|
+
return valid ? { node: args[0], cli: args[1], source: plist } : { node: null, cli: null, source: plist };
|
|
135
139
|
}
|
|
136
140
|
const r = run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `$t = Get-ScheduledTask -TaskName '${label}' -ErrorAction SilentlyContinue; if (-not $t) { exit 3 }; $a = $t.Actions[0]; Write-Output $a.Execute; Write-Output $a.Arguments`], { encoding: "utf8" });
|
|
137
141
|
if (r.status !== 0) return null;
|