@rightkit/release 0.2.172 → 0.2.174
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 +10 -4
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +1 -1
- package/cli/right-release.mjs +7 -3
- package/development-signing.mjs +135 -4
- package/development.mjs +27 -12
- package/in-place-install.mjs +32 -7
- package/install-app-windows.mjs +108 -17
- package/install-app.mjs +32 -8
- package/install-tauri-devid.mjs +231 -0
- package/inventory-check.mjs +1 -1
- package/mac-bundle-signing.mjs +143 -1
- package/mac-package-recipe.mjs +304 -0
- package/mac-signing-matrix.mjs +73 -6
- package/macos-signing-session.mjs +12 -2
- package/media-sidecar.mjs +1 -1
- package/package-recipe-targets.mjs +245 -0
- package/package.json +1 -1
- package/portable-development.mjs +9 -9
- package/project-type.mjs +1 -1
- package/qa-build-features.mjs +7 -1
- package/recipe-build.mjs +346 -0
- package/release.mjs +27 -9
- package/rightkit-signing-agent.mjs +10 -4
- package/rightkit-sync.mjs +1 -1
- package/rightkit-versions.json +11 -6
- package/runtime-budgets.mjs +14 -5
- package/stage-binary.mjs +1 -1
- package/stage-sidecar-command.mjs +73 -0
- package/stage-sidecar.mjs +839 -0
- package/swift-mac-dmg.mjs +2 -1
- package/win-package-recipe.mjs +321 -0
package/install-app-windows.mjs
CHANGED
|
@@ -5,15 +5,23 @@ import { spawnSync } from "./background-process.mjs";
|
|
|
5
5
|
import { previousPathOf } from "./in-place-install.mjs";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* `right-release install-app --installer <Setup.exe> --dest <install folder> --exe <App.exe
|
|
8
|
+
* `right-release install-app --installer <Setup.exe> --dest <install folder> --exe <App.exe> [--payload <file>]... [--payload-manifest <json>]` on Windows (Orthic-Labs/rightkit#258).
|
|
9
9
|
*
|
|
10
10
|
* The same rule as the macOS install: the app is installed and updated IN PLACE at one fixed folder (`--dest`, the
|
|
11
|
-
* folder the NSIS installer already owns, for example `%LOCALAPPDATA%\Programs\Pulse`).
|
|
12
|
-
*
|
|
13
|
-
* versioned, digest or per-run folder. The silent installer is pinned to the fixed folder with `/D=` so it cannot choose another one.
|
|
11
|
+
* folder the NSIS installer already owns, for example `%LOCALAPPDATA%\Programs\Pulse`). The silent installer is pinned to the
|
|
12
|
+
* fixed folder with `/D=` so it cannot choose another one.
|
|
14
13
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* DATA SAFETY (Orthic-Labs/rightkit#348): the install folder can also hold USER DATA (CodeRight keeps coderight.db, its WAL,
|
|
15
|
+
* state and models in `%LOCALAPPDATA%\CodeRight`). So this module NEVER copies, restores, removes or overwrites the whole
|
|
16
|
+
* install folder. Rollback is PAYLOAD-ONLY, from an explicit allowlist of the files the installer itself writes:
|
|
17
|
+
* - the allowlist is `--payload <relative file>` (repeatable) and/or `--payload-manifest <json file>` (`{ "files": [...] }` or a bare array)
|
|
18
|
+
* plus the `--exe` file; entries are relative regular files inside `--dest`, no directories, wildcards, `..` or drive paths;
|
|
19
|
+
* - with NO declared payload the allowlist is just the `--exe` file (always backed up and restored; nothing else is);
|
|
20
|
+
* - no path segment from `--dest` down to an allowlisted file may be a symlink or junction (reparse point); one is refused before backup and before restore or removal;
|
|
21
|
+
* - the backup is `<dest>.previous`, holding only the allowlisted files that existed before (replaced on each install; nothing runs from it);
|
|
22
|
+
* - a failed install restores only allowlisted files that existed before and removes only allowlisted files the failed run newly wrote;
|
|
23
|
+
* it never touches any other file in `--dest`.
|
|
24
|
+
* Dev installs do not pass through this module: they keep no backup at all (see development.mjs, install-app.mjs --dev).
|
|
17
25
|
*/
|
|
18
26
|
export class InstallAppWindowsError extends Error {
|
|
19
27
|
constructor(code, message) { super(message); this.name = "InstallAppWindowsError"; this.code = code; }
|
|
@@ -26,6 +34,40 @@ export function windowsInstallArguments(dest) {
|
|
|
26
34
|
|
|
27
35
|
const sha256 = (fs, file) => createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
28
36
|
|
|
37
|
+
const SEGMENT_BAD = /[<>:"|?*\0]/;
|
|
38
|
+
|
|
39
|
+
/** One allowlist entry as a normalized `a/b/c` relative file path, or a usage error. */
|
|
40
|
+
function normalizePayloadEntry(entry) {
|
|
41
|
+
if (typeof entry !== "string" || !entry.trim()) throw new InstallAppWindowsError("usage", "a payload entry must be a non-empty relative file path");
|
|
42
|
+
if (path.win32.isAbsolute(entry) || path.posix.isAbsolute(entry) || /^[A-Za-z]:/.test(entry)) throw new InstallAppWindowsError("usage", `payload entry ${JSON.stringify(entry)} must be relative to --dest, not absolute`);
|
|
43
|
+
const segments = entry.split(/[\\/]/);
|
|
44
|
+
if (segments.some((segment) => segment === "" || segment === "." || segment === ".." || SEGMENT_BAD.test(segment))) {
|
|
45
|
+
throw new InstallAppWindowsError("usage", `payload entry ${JSON.stringify(entry)} must be a plain relative file path (no directories, wildcards, "..", or empty segments)`);
|
|
46
|
+
}
|
|
47
|
+
return segments.join("/");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The payload allowlist: `--exe`, `--payload` entries and a `--payload-manifest` file, normalized and de-duplicated. Empty when nothing is declared. */
|
|
51
|
+
export function resolvePayloadAllowlist({ exe, payload = [], payloadManifest = null }, { fs = nodeFs } = {}) {
|
|
52
|
+
const declared = [...(Array.isArray(payload) ? payload : [payload])];
|
|
53
|
+
if (payloadManifest) {
|
|
54
|
+
let parsed;
|
|
55
|
+
try { parsed = JSON.parse(String(fs.readFileSync(payloadManifest, "utf8"))); }
|
|
56
|
+
catch (error) { throw new InstallAppWindowsError("usage", `--payload-manifest ${payloadManifest} cannot be read as JSON: ${error.message}`); }
|
|
57
|
+
const files = Array.isArray(parsed) ? parsed : parsed?.files;
|
|
58
|
+
if (!Array.isArray(files)) throw new InstallAppWindowsError("usage", `--payload-manifest ${payloadManifest} must be a JSON array of relative files, or { "files": [...] }`);
|
|
59
|
+
declared.push(...files);
|
|
60
|
+
}
|
|
61
|
+
// `--exe` alone is the minimal allowlist: the executable is always backed up and restored; data files never are.
|
|
62
|
+
const normalized = [...new Set([exe, ...declared].map(normalizePayloadEntry))];
|
|
63
|
+
const lower = new Set();
|
|
64
|
+
for (const entry of normalized) {
|
|
65
|
+
if (lower.has(entry.toLowerCase())) throw new InstallAppWindowsError("usage", `payload entry ${JSON.stringify(entry)} is listed twice (Windows paths are case-insensitive)`);
|
|
66
|
+
lower.add(entry.toLowerCase());
|
|
67
|
+
}
|
|
68
|
+
return normalized.sort();
|
|
69
|
+
}
|
|
70
|
+
|
|
29
71
|
export function installAppWindows(opts, { platform = process.platform, fs = nodeFs, run = spawnSync, event = () => {} } = {}) {
|
|
30
72
|
if (platform !== "win32") throw new InstallAppWindowsError("unsupported", `the Windows installer mode runs on win32, not ${platform}`);
|
|
31
73
|
const { installer, dest, exe } = opts;
|
|
@@ -33,31 +75,80 @@ export function installAppWindows(opts, { platform = process.platform, fs = node
|
|
|
33
75
|
if (!dest || !path.win32.isAbsolute(dest) || path.win32.parse(dest).root === dest) throw new InstallAppWindowsError("usage", "--dest must be the fixed absolute install folder, not a drive root");
|
|
34
76
|
if (!exe || exe !== path.win32.basename(exe)) throw new InstallAppWindowsError("usage", "--exe must be the bare executable name inside --dest");
|
|
35
77
|
if (!fs.existsSync(installer)) throw new InstallAppWindowsError("installer-missing", `installer not found: ${installer}`);
|
|
78
|
+
const allowlist = resolvePayloadAllowlist({ exe, payload: opts.payload, payloadManifest: opts.payloadManifest }, { fs });
|
|
36
79
|
const previous = previousPathOf(dest);
|
|
37
80
|
const installed = path.win32.join(dest, exe);
|
|
38
81
|
const hadInstall = fs.existsSync(dest);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
82
|
+
const inDest = (relative) => path.win32.join(dest, ...relative.split("/"));
|
|
83
|
+
const inPrevious = (relative) => path.win32.join(previous, ...relative.split("/"));
|
|
84
|
+
const isFile = (file) => { try { return (fs.lstatSync ?? fs.statSync).call(fs, file).isFile(); } catch { return false; } };
|
|
85
|
+
// lstat every segment from dest down to the file; a symlink or junction anywhere in the chain is refused. A missing segment ends the walk.
|
|
86
|
+
const assertNoLinks = (relative) => {
|
|
87
|
+
const lstat = fs.lstatSync ?? fs.statSync;
|
|
88
|
+
let current = dest;
|
|
89
|
+
for (const segment of [null, ...relative.split("/")]) {
|
|
90
|
+
if (segment !== null) current = path.win32.join(current, segment);
|
|
91
|
+
let stat;
|
|
92
|
+
try { stat = lstat.call(fs, current); } catch { return; }
|
|
93
|
+
if (stat.isSymbolicLink?.() || stat.isReparsePoint?.()) throw new InstallAppWindowsError("usage", `${current} is a symlink or junction; refusing to back up, restore or remove payload entry ${relative} through it`);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
event("start", { installer, dest, replacing: hadInstall, payloadFiles: allowlist.length });
|
|
97
|
+
|
|
98
|
+
// Backup: ONLY the allowlisted files that exist now, into `<dest>.previous`. The rest of --dest (user data) is never copied.
|
|
99
|
+
const backedUp = [];
|
|
100
|
+
const absentBefore = [];
|
|
101
|
+
let backupMade = false;
|
|
102
|
+
if (hadInstall && allowlist.length > 0) {
|
|
103
|
+
for (const relative of allowlist) {
|
|
104
|
+
const file = inDest(relative);
|
|
105
|
+
assertNoLinks(relative);
|
|
106
|
+
if (!fs.existsSync(file)) { absentBefore.push(relative); continue; }
|
|
107
|
+
if (!isFile(file)) throw new InstallAppWindowsError("usage", `payload entry ${relative} is not a regular file in ${dest}; refusing to back it up or roll it back`);
|
|
108
|
+
backedUp.push(relative);
|
|
109
|
+
}
|
|
110
|
+
// One payload-only rollback copy, replaced on every install (this also clears a whole-folder copy an older RightKit left).
|
|
42
111
|
fs.rmSync(previous, { recursive: true, force: true });
|
|
43
|
-
|
|
44
|
-
|
|
112
|
+
for (const relative of backedUp) {
|
|
113
|
+
fs.mkdirSync(path.win32.dirname(inPrevious(relative)), { recursive: true });
|
|
114
|
+
fs.copyFileSync(inDest(relative), inPrevious(relative));
|
|
115
|
+
}
|
|
116
|
+
backupMade = true;
|
|
117
|
+
event("previous-kept", { previous, files: backedUp.length, payloadOnly: true });
|
|
45
118
|
}
|
|
119
|
+
// Roll back the payload and nothing else: restore allowlisted files that existed, remove allowlisted files this run newly wrote.
|
|
120
|
+
// Each file is restored independently; failures are collected and reported together, never masking the installer error.
|
|
121
|
+
const restoreFailures = [];
|
|
46
122
|
const restore = () => {
|
|
47
|
-
if (!
|
|
48
|
-
|
|
123
|
+
if (!backupMade) return false;
|
|
124
|
+
for (const relative of backedUp) {
|
|
125
|
+
try {
|
|
126
|
+
assertNoLinks(relative);
|
|
127
|
+
fs.mkdirSync(path.win32.dirname(inDest(relative)), { recursive: true });
|
|
128
|
+
fs.copyFileSync(inPrevious(relative), inDest(relative));
|
|
129
|
+
} catch (error) { restoreFailures.push(`${relative}: ${error.message}`); }
|
|
130
|
+
}
|
|
131
|
+
for (const relative of absentBefore) {
|
|
132
|
+
try {
|
|
133
|
+
assertNoLinks(relative);
|
|
134
|
+
const file = inDest(relative);
|
|
135
|
+
if (fs.existsSync(file) && isFile(file)) fs.rmSync(file, { force: true });
|
|
136
|
+
} catch (error) { restoreFailures.push(`${relative}: ${error.message}`); }
|
|
137
|
+
}
|
|
49
138
|
return true;
|
|
50
139
|
};
|
|
140
|
+
const failureNote = () => (restoreFailures.length ? `; ROLLBACK INCOMPLETE, ${restoreFailures.length} file(s) failed: ${restoreFailures.join(" | ")}` : "");
|
|
141
|
+
const noRollback = "nothing was installed before, so nothing was rolled back and no other file in the install folder was touched";
|
|
51
142
|
const result = run(installer, windowsInstallArguments(dest), { stdio: "inherit", windowsHide: true, shell: false });
|
|
52
143
|
if (result.status !== 0) {
|
|
53
144
|
const restored = restore();
|
|
54
|
-
throw new InstallAppWindowsError("installer-failed", `installer exited ${result.status ?? "abnormally"}${result.error ? `: ${result.error.message}` : ""}; ${restored ? `restored ${dest} from ${previous}
|
|
145
|
+
throw new InstallAppWindowsError("installer-failed", `installer exited ${result.status ?? "abnormally"}${result.error ? `: ${result.error.message}` : ""}; ${restored ? `restored the ${backedUp.length} payload file(s) of ${dest} from ${previous}; no other file was touched` : noRollback}${failureNote()}`);
|
|
55
146
|
}
|
|
56
147
|
// The installer must have updated the fixed folder: the executable is at the same path as before, not in a new folder.
|
|
57
148
|
if (!fs.existsSync(installed) || fs.statSync(installed).size === 0) {
|
|
58
149
|
const restored = restore();
|
|
59
|
-
throw new InstallAppWindowsError("path-changed", `the installer did not place ${exe} at the fixed path ${installed}; ${restored ? `restored from ${previous}
|
|
150
|
+
throw new InstallAppWindowsError("path-changed", `the installer did not place ${exe} at the fixed path ${installed}; ${restored ? `restored the ${backedUp.length} payload file(s) from ${previous}; no other file was touched` : noRollback}${failureNote()}`);
|
|
60
151
|
}
|
|
61
|
-
event("replaced", { installed, previous:
|
|
62
|
-
return { ok: true, installed, previous:
|
|
152
|
+
event("replaced", { installed, previous: backupMade ? previous : null, sha256: sha256(fs, installed) });
|
|
153
|
+
return { ok: true, installed, previous: backupMade ? previous : null };
|
|
63
154
|
}
|
package/install-app.mjs
CHANGED
|
@@ -22,11 +22,11 @@ export class InstallAppError extends Error {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
const VALUE_FLAGS = new Set(["--dmg", "--zip", "--app", "--app-name", "--bundle-id", "--dest", "--installer", "--exe"]);
|
|
25
|
+
const VALUE_FLAGS = new Set(["--dmg", "--zip", "--app", "--app-name", "--bundle-id", "--dest", "--installer", "--exe", "--payload", "--payload-manifest"]);
|
|
26
26
|
const BOOL_FLAGS = new Set(["--relaunch", "--no-relaunch", "--json", "--dev", "--recover"]);
|
|
27
27
|
|
|
28
28
|
export function parseArgs(argv) {
|
|
29
|
-
const opts = { dmg: null, zip: null, app: null, installer: null, exe: null, dev: false, appName: null, bundleId: null, dest: "/Applications", relaunch: null, json: false, recover: false };
|
|
29
|
+
const opts = { dmg: null, zip: null, app: null, installer: null, exe: null, payload: [], payloadManifest: null, dev: false, appName: null, bundleId: null, dest: "/Applications", relaunch: null, json: false, recover: false };
|
|
30
30
|
for (let i = 0; i < argv.length; i += 1) {
|
|
31
31
|
const flag = argv[i];
|
|
32
32
|
if (VALUE_FLAGS.has(flag)) {
|
|
@@ -40,6 +40,8 @@ export function parseArgs(argv) {
|
|
|
40
40
|
else if (flag === "--bundle-id") opts.bundleId = value;
|
|
41
41
|
else if (flag === "--installer") opts.installer = value;
|
|
42
42
|
else if (flag === "--exe") opts.exe = value;
|
|
43
|
+
else if (flag === "--payload") opts.payload.push(value);
|
|
44
|
+
else if (flag === "--payload-manifest") opts.payloadManifest = value;
|
|
43
45
|
else opts.dest = value;
|
|
44
46
|
} else if (BOOL_FLAGS.has(flag)) {
|
|
45
47
|
if (flag === "--json") opts.json = true;
|
|
@@ -51,11 +53,13 @@ export function parseArgs(argv) {
|
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
55
|
if (opts.installer !== null) {
|
|
54
|
-
// Windows: run the sealed NSIS installer in place at one fixed folder,
|
|
56
|
+
// Windows: run the sealed NSIS installer in place at one fixed folder; rollback is payload-only (--payload / --payload-manifest), never the whole folder.
|
|
55
57
|
if (opts.dmg !== null || opts.zip !== null || opts.app !== null || opts.recover || opts.dev) throw new InstallAppError("usage", "--installer cannot be combined with --dmg, --zip, --app, --dev or --recover");
|
|
56
58
|
if (!argv.includes("--dest") || opts.exe === null) throw new InstallAppError("usage", "--installer requires --dest <fixed install folder> and --exe <App.exe>");
|
|
59
|
+
if (opts.payloadManifest !== null) opts.payloadManifest = path.resolve(opts.payloadManifest);
|
|
57
60
|
return opts;
|
|
58
61
|
}
|
|
62
|
+
if (opts.payload.length > 0 || opts.payloadManifest !== null) throw new InstallAppError("usage", "--payload and --payload-manifest only apply to --installer (the Windows install)");
|
|
59
63
|
if (argv.includes("--relaunch") && argv.includes("--no-relaunch")) throw new InstallAppError("usage", "--relaunch and --no-relaunch are mutually exclusive");
|
|
60
64
|
const inputs = [opts.dmg, opts.zip, opts.app].filter((v) => v !== null).length;
|
|
61
65
|
if (opts.recover) {
|
|
@@ -311,7 +315,8 @@ export async function recoverInstalls({ dest, appName = null, run, fsImpl, home,
|
|
|
311
315
|
let ok = j.phase === "verified";
|
|
312
316
|
if (!ok) { try { await verifyBundle(run, j.installed, { assess: !j.dev, staple: false }); ok = true; } catch { ok = false; } }
|
|
313
317
|
if (ok) {
|
|
314
|
-
|
|
318
|
+
// A dev journal (from an older RightKit) never keeps the old bundle: the staged path is cleared below.
|
|
319
|
+
try { if (!j.dev) await keepPreviousBundle(run, fsImpl, j.staged, j.installed); action = "rolled-forward"; }
|
|
315
320
|
catch { action = "rolled-forward-previous-pending"; keepJournal = true; keepStaged = true; }
|
|
316
321
|
} else {
|
|
317
322
|
swapBack(swapPaths, fsImpl, j.staged, j.installed); // staged now holds the bad new bundle
|
|
@@ -325,7 +330,7 @@ export async function recoverInstalls({ dest, appName = null, run, fsImpl, home,
|
|
|
325
330
|
let ok = j.phase === "verified";
|
|
326
331
|
if (!ok) { try { await verifyBundle(run, j.installed, { assess: !j.dev, staple: false }); ok = true; } catch { ok = false; } }
|
|
327
332
|
if (ok) {
|
|
328
|
-
try { await keepPreviousBundle(run, fsImpl, j.backup, j.installed); action = "rolled-forward"; }
|
|
333
|
+
try { if (j.dev) rm(j.backup); else await keepPreviousBundle(run, fsImpl, j.backup, j.installed); action = "rolled-forward"; }
|
|
329
334
|
catch { action = "rolled-forward-previous-pending"; keepJournal = true; }
|
|
330
335
|
} else {
|
|
331
336
|
rm(j.installed);
|
|
@@ -384,6 +389,7 @@ export async function installApp(opts, deps = {}) {
|
|
|
384
389
|
let mountPoint = null;
|
|
385
390
|
let failedStage = "start";
|
|
386
391
|
let swap = null; // set once the journal exists; tracks which renames happened
|
|
392
|
+
let devOldRemoved = false; // a dev install removed the old bundle; nothing can put it back
|
|
387
393
|
|
|
388
394
|
const removeQuiet = (p) => { if (p) { try { fsImpl.rmSync(p, { recursive: true, force: true }); } catch { /* best effort */ } } };
|
|
389
395
|
// Undo an unfinished swap using renames only. Sync on purpose so a signal handler can run it.
|
|
@@ -507,7 +513,8 @@ export async function installApp(opts, deps = {}) {
|
|
|
507
513
|
const base = appName.replace(/\.app$/, "");
|
|
508
514
|
// Staging is never run from: it is verified, then renamed into place. The rollback copy is the fixed `<installed>.previous`.
|
|
509
515
|
const staged = path.join(opts.dest, `.${base}.rightkit-install-${pid}.app`);
|
|
510
|
-
|
|
516
|
+
// Dev installs keep NO old copy of any kind (Orthic-Labs/rightkit#348): the old bundle is removed, never moved to `.previous`.
|
|
517
|
+
const backup = hadInstalled && !dev ? previousPathOf(installed) : null;
|
|
511
518
|
const journal = journalPathFor(opts.dest, appName);
|
|
512
519
|
const journalData = { schema: 1, appName, pid, phase: "staging", dev, installed, backup, staged, mount: mountPoint, mountRoot };
|
|
513
520
|
removeQuiet(staged);
|
|
@@ -543,6 +550,17 @@ export async function installApp(opts, deps = {}) {
|
|
|
543
550
|
writeJournal(fsImpl, journal, { ...jd, phase: "swapping", swap: swapMode });
|
|
544
551
|
fsImpl.renameSync(staged, installed);
|
|
545
552
|
swap.stagedIn = true;
|
|
553
|
+
} else if (dev) {
|
|
554
|
+
// Dev: remove the old bundle, then rename the verified staged copy over the fixed path. No backup, no exchange (an exchange
|
|
555
|
+
// would park the old bundle at the staged path). A failure from here leaves the path absent or partial: reinstall from an older DMG.
|
|
556
|
+
swapMode = "replace";
|
|
557
|
+
swap.mode = "two-rename";
|
|
558
|
+
jd = { ...journalData, swap: "two-rename", backup: null };
|
|
559
|
+
writeJournal(fsImpl, journal, { ...jd, phase: "swapping" });
|
|
560
|
+
devOldRemoved = true;
|
|
561
|
+
fsImpl.rmSync(installed, { recursive: true, force: true });
|
|
562
|
+
fsImpl.renameSync(staged, installed);
|
|
563
|
+
swap.stagedIn = true;
|
|
546
564
|
} else {
|
|
547
565
|
jd = { ...journalData, swap: "atomic", backup: null, stagedIno: fsImpl.statSync(staged).ino };
|
|
548
566
|
writeJournal(fsImpl, journal, { ...jd, phase: "swapping" });
|
|
@@ -586,6 +604,8 @@ export async function installApp(opts, deps = {}) {
|
|
|
586
604
|
}
|
|
587
605
|
if (!oldBundle || kept !== previousPathOf(installed)) { if (!oldBundle) removeQuiet(journal); }
|
|
588
606
|
else removeQuiet(journal);
|
|
607
|
+
// A stale `.previous` from an older RightKit must not survive a dev install either.
|
|
608
|
+
if (dev) removeQuiet(previousPathOf(installed));
|
|
589
609
|
swap = null;
|
|
590
610
|
event("replaced", { installed, previous: kept, swap: swapMode });
|
|
591
611
|
|
|
@@ -601,7 +621,11 @@ export async function installApp(opts, deps = {}) {
|
|
|
601
621
|
return result;
|
|
602
622
|
} catch (error) {
|
|
603
623
|
rollbackSwap();
|
|
604
|
-
|
|
624
|
+
let reason = error instanceof InstallAppError ? error.message : errText(error);
|
|
625
|
+
if (dev && devOldRemoved) {
|
|
626
|
+
reason = `${reason}; the dev install keeps no old copy, so the failed install was removed: reinstall from an older DMG`;
|
|
627
|
+
if (error instanceof InstallAppError) error.message = reason;
|
|
628
|
+
}
|
|
605
629
|
event("failed", { stage: failedStage, code: error.code || "error", reason });
|
|
606
630
|
if (!(error instanceof InstallAppError)) throw new InstallAppError("error", reason);
|
|
607
631
|
throw error;
|
|
@@ -708,7 +732,7 @@ export async function main(argv = process.argv.slice(2), io = {}) {
|
|
|
708
732
|
stderr(`right-release install-app: ${error.message}`);
|
|
709
733
|
stderr("usage: right-release install-app (--dmg <path.dmg> | --dev (--zip <path.zip> | --app <path.app> | --dmg <path.dmg>)) [--app-name Pulse.app] [--bundle-id dev.orthic.pulse] [--dest /Applications] [--relaunch|--no-relaunch] [--json]");
|
|
710
734
|
stderr(" right-release install-app --recover [--dest /Applications] [--app-name Pulse.app] [--json]");
|
|
711
|
-
stderr(" right-release install-app --installer <Setup.exe> --dest <fixed install folder> --exe <App.exe> [--json] (Windows: in place,
|
|
735
|
+
stderr(" right-release install-app --installer <Setup.exe> --dest <fixed install folder> --exe <App.exe> [--payload <file>]... [--payload-manifest <json>] [--json] (Windows: in place; payload-only rollback, never the whole folder)");
|
|
712
736
|
stderr(" --dev installs a Developer ID-signed, NOT notarized build (skips spctl/stapler, clears quarantine on the new bundle).");
|
|
713
737
|
return 2;
|
|
714
738
|
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// `install.kind: "tauri-devid"`: install a freshly built Tauri .app at ONE fixed path, signed with the stable
|
|
2
|
+
// Developer ID, so macOS privacy grants (TCC) and Keychain code requirements survive every rebuild.
|
|
3
|
+
//
|
|
4
|
+
// Pipeline (installTauriDevId): copy the built .app to a private staging copy -> sign nested code, then the app,
|
|
5
|
+
// with the stable Developer ID (signMacosDevelopmentBundle) -> assert the designated requirement equals the
|
|
6
|
+
// installed copy's (assertStableDesignatedRequirement) -> install at the fixed path with NO backup of the old copy
|
|
7
|
+
// (installApp, dev mode; Orthic-Labs/rightkit#348) -> verify the installed copy; any verify failure removes the new
|
|
8
|
+
// copy and leaves the path absent (reinstall from an older DMG). Nothing ever creates `<dest>.previous`.
|
|
9
|
+
// resetTauriDevId removes the install, only when asked (mode "remove").
|
|
10
|
+
//
|
|
11
|
+
// Every external call (copy, sign, run, install, file system) is injectable through `deps`; the defaults are the
|
|
12
|
+
// real RightKit pieces. macOS paths are POSIX, so this module uses path.posix throughout.
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { cpSync, existsSync, mkdtempSync, readFileSync, renameSync, rmSync } from "node:fs";
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { spawnSync } from "./background-process.mjs";
|
|
18
|
+
import { assertSignedIdentities, assertStableDesignatedRequirement, readDevSigningSettings, signMacosDevelopmentBundle } from "./development-signing.mjs";
|
|
19
|
+
import { installApp } from "./install-app.mjs";
|
|
20
|
+
import { previousPathOf } from "./in-place-install.mjs"; // only to clear a stale `.previous` an older RightKit left; never to create one
|
|
21
|
+
|
|
22
|
+
export const TAURI_DEVID_KIND = "tauri-devid";
|
|
23
|
+
|
|
24
|
+
export class TauriDevIdError extends Error {
|
|
25
|
+
constructor(code, message, details = {}) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "TauriDevIdError";
|
|
28
|
+
this.code = code;
|
|
29
|
+
Object.assign(this, details);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const P = path.posix;
|
|
34
|
+
const absolute = (appRoot, value) => (P.isAbsolute(value) ? P.normalize(value) : P.join(appRoot, value));
|
|
35
|
+
const text = (result) => `${result?.stdout ?? ""}\n${result?.stderr ?? ""}`.trim();
|
|
36
|
+
|
|
37
|
+
/** Real implementations of every external call. Tests replace any of them. */
|
|
38
|
+
export function defaultDeps() {
|
|
39
|
+
return {
|
|
40
|
+
run: spawnSync,
|
|
41
|
+
sign: signMacosDevelopmentBundle,
|
|
42
|
+
assertRequirement: assertStableDesignatedRequirement,
|
|
43
|
+
assertIdentities: assertSignedIdentities,
|
|
44
|
+
installApp,
|
|
45
|
+
exists: existsSync,
|
|
46
|
+
makeTemp: () => mkdtempSync(P.join(os.tmpdir(), "rightkit-tauri-devid-")),
|
|
47
|
+
copyApp: (from, to) => cpSync(from, to, { recursive: true, verbatimSymlinks: true }),
|
|
48
|
+
rename: renameSync,
|
|
49
|
+
remove: (target) => rmSync(target, { recursive: true, force: true }),
|
|
50
|
+
readFile: readFileSync,
|
|
51
|
+
log: (line) => console.log(line),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const invalid = (message) => new TauriDevIdError("TAURI_DEVID_CONFIG_INVALID", message);
|
|
56
|
+
const SAME_IDENTITY_RULE = "dev builds use the SAME install path and bundle id as production (else permission issues), replaced in place with no backup copy; no separate .dev identity";
|
|
57
|
+
|
|
58
|
+
/** The tauri config file for a descriptor: `install.tauriConfig` (contained, relative to the app root) or `tauri.conf.json` beside the manifest. */
|
|
59
|
+
function tauriConfigPath(descriptor, appRoot) {
|
|
60
|
+
if (descriptor.tauriConfig !== undefined) return absolute(appRoot, descriptor.tauriConfig);
|
|
61
|
+
return absolute(appRoot, P.join(P.dirname(descriptor.manifestPath ?? "src-tauri/Cargo.toml"), "tauri.conf.json"));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** `{ productName?, identifier? }` from the tauri config, or null when it cannot be read or parsed. */
|
|
65
|
+
function readTauriConfig(descriptor, appRoot, readFile) {
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(String(readFile(tauriConfigPath(descriptor, appRoot))));
|
|
68
|
+
const pick = (value) => (typeof value === "string" && value ? value : undefined);
|
|
69
|
+
return { productName: pick(parsed?.productName), identifier: pick(parsed?.identifier) };
|
|
70
|
+
} catch { return null; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The `tauri-devid` descriptor, validated. Accepts the config root or the descriptor itself.
|
|
75
|
+
* Fail-closed on identity: the destination must be exactly `/Applications/<productName>.app`, where productName is the
|
|
76
|
+
* tauri.conf.json productName (install.productName must equal it) and, when the release config declares
|
|
77
|
+
* `targets.mac.notarize.app`, the basename of that app must agree. There is no other destination.
|
|
78
|
+
* With `readFile` (the install and reset paths) the tauri config must be readable (`tauri.conf.json` beside the manifest, or
|
|
79
|
+
* `install.tauriConfig`) or `install.bundleId` must be set explicitly; without it only the descriptor's shape is checked.
|
|
80
|
+
* `releaseConfig` is the release config root when `config` is the bare descriptor.
|
|
81
|
+
*/
|
|
82
|
+
export function resolveTauriDevIdDescriptor(config, { appRoot = "", readFile, releaseConfig } = {}) {
|
|
83
|
+
const descriptor = config?.development?.targets?.mac?.install ?? config;
|
|
84
|
+
if (!descriptor || descriptor.kind !== TAURI_DEVID_KIND) throw new TauriDevIdError("TAURI_DEVID_CONFIG_INVALID", `development.targets.mac.install.kind must be "${TAURI_DEVID_KIND}"`);
|
|
85
|
+
if (typeof descriptor.productName !== "string" || !descriptor.productName) throw new TauriDevIdError("TAURI_DEVID_CONFIG_INVALID", "install.productName is required");
|
|
86
|
+
if (typeof descriptor.destination !== "string" || !P.isAbsolute(descriptor.destination) || !descriptor.destination.endsWith(".app") || descriptor.destination.split("/").includes("..")) {
|
|
87
|
+
throw new TauriDevIdError("TAURI_DEVID_CONFIG_INVALID", "install.destination must be an absolute /…/<Product>.app path (the one fixed install path, for example /Applications/<Product>.app)");
|
|
88
|
+
}
|
|
89
|
+
if (descriptor.tauriConfig !== undefined && (typeof descriptor.tauriConfig !== "string" || !descriptor.tauriConfig || P.isAbsolute(descriptor.tauriConfig) || /^[A-Za-z]:/.test(descriptor.tauriConfig) || descriptor.tauriConfig.split(/[\/]/).includes("..") || !/\.json$/i.test(descriptor.tauriConfig))) {
|
|
90
|
+
throw invalid("install.tauriConfig must be a relative path to a .json file inside the app");
|
|
91
|
+
}
|
|
92
|
+
const destination = P.normalize(descriptor.destination);
|
|
93
|
+
const release = releaseConfig ?? (config?.development ? config : undefined);
|
|
94
|
+
const notarizeApp = release?.targets?.mac?.notarize?.app;
|
|
95
|
+
const notarizeName = typeof notarizeApp === "string" && notarizeApp.replaceAll("\\", "/").replace(/\/+$/, "").endsWith(".app") ? P.basename(notarizeApp.replaceAll("\\", "/").replace(/\/+$/, ""), ".app") : undefined;
|
|
96
|
+
if (readFile) {
|
|
97
|
+
const tauri = readTauriConfig(descriptor, appRoot, readFile);
|
|
98
|
+
if (!tauri && descriptor.tauriConfig !== undefined) throw invalid(`install.tauriConfig ${descriptor.tauriConfig} cannot be read or is not JSON; the production identity is read from it`);
|
|
99
|
+
if (!tauri && !descriptor.bundleId) throw invalid("cannot establish the production identity: tauri.conf.json (beside the manifest, or install.tauriConfig) is unreadable and install.bundleId is not set; set either so the install can be checked against it");
|
|
100
|
+
if (tauri?.productName !== undefined && tauri.productName !== descriptor.productName) throw invalid(`install.productName ${JSON.stringify(descriptor.productName)} differs from the tauri.conf.json productName ${JSON.stringify(tauri.productName)}: ${SAME_IDENTITY_RULE}`);
|
|
101
|
+
}
|
|
102
|
+
if (notarizeName !== undefined && notarizeName !== descriptor.productName) throw invalid(`install.productName ${JSON.stringify(descriptor.productName)} disagrees with targets.mac.notarize.app (${notarizeName}.app): ${SAME_IDENTITY_RULE}`);
|
|
103
|
+
if (destination !== `/Applications/${descriptor.productName}.app`) {
|
|
104
|
+
throw invalid(`install.destination ${destination} is not the production install path /Applications/${descriptor.productName}.app: ${SAME_IDENTITY_RULE}`);
|
|
105
|
+
}
|
|
106
|
+
for (const key of ["devIdentity", "refuseSystemApplications"]) {
|
|
107
|
+
if (descriptor[key] !== undefined) throw invalid(`install.${key} is not supported: ${SAME_IDENTITY_RULE}`);
|
|
108
|
+
}
|
|
109
|
+
if (descriptor.signing !== undefined) throw new TauriDevIdError("TAURI_DEVID_CONFIG_INVALID", `install.signing is not allowed for ${TAURI_DEVID_KIND}: ad hoc signing (signing: "-") changes the code identity every build and resets privacy grants; dev builds sign with the same Developer ID as release`);
|
|
110
|
+
const binaries = descriptor.verifyBinaries ?? [];
|
|
111
|
+
if (!Array.isArray(binaries) || binaries.some((item) => typeof item !== "string" || !item || P.isAbsolute(item) || item.split("/").includes(".."))) {
|
|
112
|
+
throw new TauriDevIdError("TAURI_DEVID_CONFIG_INVALID", "install.verifyBinaries must be relative paths inside the app, for example Contents/MacOS/<name>");
|
|
113
|
+
}
|
|
114
|
+
// Shape of the per-binary signing keys (identifiers, identifierTemplate, entitlements, timestamp, assert, assertTeamSignedNested).
|
|
115
|
+
try { readDevSigningSettings(descriptor, config?.development ? config : {}); }
|
|
116
|
+
catch (error) { throw new TauriDevIdError("TAURI_DEVID_CONFIG_INVALID", error.message); }
|
|
117
|
+
return { ...descriptor, destination, verifyBinaries: binaries, appRoot };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The built .app's CFBundleIdentifier must equal the production identifier (tauri.conf.json, and install.bundleId when set). Fail-closed. */
|
|
121
|
+
function assertBuiltIdentity({ source, descriptor, appRoot, env, deps }) {
|
|
122
|
+
const production = readTauriConfig(descriptor, appRoot, deps.readFile)?.identifier ?? null;
|
|
123
|
+
if (descriptor.bundleId && production && descriptor.bundleId !== production) {
|
|
124
|
+
throw invalid(`install.bundleId ${descriptor.bundleId} differs from the tauri.conf.json identifier ${production}: ${SAME_IDENTITY_RULE}`);
|
|
125
|
+
}
|
|
126
|
+
const expected = production ?? descriptor.bundleId;
|
|
127
|
+
if (!expected) throw invalid(`no production bundle identifier: the tauri.conf.json identifier is unreadable and install.bundleId is not set, so the built app cannot be checked: ${SAME_IDENTITY_RULE}`);
|
|
128
|
+
const read = deps.run("/usr/bin/plutil", ["-extract", "CFBundleIdentifier", "raw", "-o", "-", P.join(source, "Contents", "Info.plist")], { cwd: appRoot, env, encoding: "utf8", windowsHide: true, shell: false });
|
|
129
|
+
const actual = read.error || read.status !== 0 ? null : String(read.stdout ?? "").trim();
|
|
130
|
+
if (actual !== expected) {
|
|
131
|
+
throw new TauriDevIdError("TAURI_DEVID_BUNDLE_ID_MISMATCH", `built ${source} CFBundleIdentifier ${actual === null ? "(unreadable)" : JSON.stringify(actual)} is not the production identifier ${JSON.stringify(expected)}: ${SAME_IDENTITY_RULE}`, { builtBundleId: actual, productionBundleId: expected });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const builtPathOf = (built, appRoot) => {
|
|
136
|
+
const value = typeof built === "string" ? built : built?.app;
|
|
137
|
+
if (typeof value !== "string" || !value.replace(/\/+$/, "").endsWith(".app")) throw new TauriDevIdError("TAURI_DEVID_BUILT_INVALID", "built must be the path of the built .app (a string, or { app })");
|
|
138
|
+
return absolute(appRoot, value.replace(/\/+$/, ""));
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const digestOf = (deps, file) => createHash("sha256").update(deps.readFile(file)).digest("hex");
|
|
142
|
+
|
|
143
|
+
function verifyInstalled({ appRoot, destination, staged, expectedRequirement, binaries, env, deps, descriptor, config, signing }) {
|
|
144
|
+
const strict = deps.run("codesign", ["--verify", "--strict", "--deep", destination], { cwd: appRoot, env, encoding: "utf8", windowsHide: true, shell: false });
|
|
145
|
+
if (strict.error || strict.status !== 0) throw new TauriDevIdError("TAURI_DEVID_VERIFY_FAILED", `installed ${destination} failed codesign --verify --strict --deep: ${strict.error?.message ?? text(strict)}`);
|
|
146
|
+
const display = deps.run("codesign", ["--display", "-r-", destination], { cwd: appRoot, env, encoding: "utf8", windowsHide: true, shell: false });
|
|
147
|
+
const actual = /^designated => (.+)$/m.exec(text(display))?.[1]?.trim();
|
|
148
|
+
if (display.status !== 0 || actual !== expectedRequirement) {
|
|
149
|
+
throw new TauriDevIdError("TAURI_DEVID_VERIFY_FAILED", `installed ${destination} designated requirement ${JSON.stringify(actual)} is not the signed ${JSON.stringify(expectedRequirement)}`, { installedRequirement: actual, designatedRequirement: expectedRequirement });
|
|
150
|
+
}
|
|
151
|
+
// Declared identity assertions (install.assert, install.assertTeamSignedNested) against the installed copy; a failure rolls back.
|
|
152
|
+
const settings = readDevSigningSettings(descriptor, config?.development ? config : {});
|
|
153
|
+
if (settings.assertions.length || settings.assertTeamSignedNested) {
|
|
154
|
+
try {
|
|
155
|
+
deps.assertIdentities({ root: appRoot, bundle: destination, settings, teamIdentifier: signing.teamIdentifier, identity: signing.identity, env, run: deps.run, join: P.join, label: "installed copy",
|
|
156
|
+
...(deps.listFiles ? { listFiles: deps.listFiles } : {}), ...(deps.isMachO ? { isMachO: deps.isMachO } : {}) });
|
|
157
|
+
} catch (error) { throw new TauriDevIdError("TAURI_DEVID_VERIFY_FAILED", `installed ${destination} failed its identity assertions: ${error.message}`, { problems: error.problems }); }
|
|
158
|
+
}
|
|
159
|
+
for (const binary of binaries) {
|
|
160
|
+
let want; let got;
|
|
161
|
+
try { want = digestOf(deps, P.join(staged, binary)); got = digestOf(deps, P.join(destination, binary)); }
|
|
162
|
+
catch (error) { throw new TauriDevIdError("TAURI_DEVID_VERIFY_FAILED", `cannot read ${binary} to verify the install: ${error.message}`); }
|
|
163
|
+
if (want !== got) throw new TauriDevIdError("TAURI_DEVID_VERIFY_FAILED", `installed ${binary} does not match the signed staged copy`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @param {{appRoot: string, config: object, built: string|{app: string}, deps?: object, env?: object}} options
|
|
169
|
+
* `config` is the app's release config (development.targets.mac.install is the `tauri-devid` descriptor).
|
|
170
|
+
* @returns {Promise<{installed: string, designatedRequirement: string, identity: string, teamIdentifier: string, signing: object}>}
|
|
171
|
+
*/
|
|
172
|
+
export async function installTauriDevId({ appRoot, config, built, deps = {}, env = {}, releaseConfig }) {
|
|
173
|
+
const d = { ...defaultDeps(), ...deps };
|
|
174
|
+
const descriptor = resolveTauriDevIdDescriptor(config, { appRoot, readFile: d.readFile, releaseConfig });
|
|
175
|
+
const destination = descriptor.destination;
|
|
176
|
+
const source = builtPathOf(built, appRoot);
|
|
177
|
+
if (!d.exists(source)) throw new TauriDevIdError("TAURI_DEVID_BUILT_INVALID", `built app not found: ${source}`);
|
|
178
|
+
// Same identity as production: the built bundle id must be the production identifier, or nothing is copied or signed.
|
|
179
|
+
assertBuiltIdentity({ source, descriptor, appRoot, env, deps: d });
|
|
180
|
+
const name = P.basename(destination);
|
|
181
|
+
const tempRoot = d.makeTemp();
|
|
182
|
+
// The staging copy carries the final bundle name so installApp installs <dest dir>/<name> without renaming.
|
|
183
|
+
const staged = P.join(tempRoot, name);
|
|
184
|
+
try {
|
|
185
|
+
d.copyApp(source, staged);
|
|
186
|
+
// 1. Sign: nested code first, then the app, with the stable Developer ID (development-signing orders it deepest first).
|
|
187
|
+
const signing = await d.sign({ root: appRoot, bundle: staged, descriptor, config, env, ...(d.listFiles ? { listFiles: d.listFiles } : {}), ...(d.isMachO ? { isMachO: d.isMachO } : {}) });
|
|
188
|
+
if (signing.adhoc || !signing.designatedRequirement) throw new TauriDevIdError("TAURI_DEVID_SIGNING_UNSTABLE", `signing produced no stable Developer ID designated requirement for ${staged}; refusing to install`);
|
|
189
|
+
// 2. Same identity as the installed copy, or fail: a changed designated requirement resets every privacy grant.
|
|
190
|
+
if (d.exists(destination)) await d.assertRequirement({ root: appRoot, installed: destination, expected: signing.designatedRequirement, env, run: d.run });
|
|
191
|
+
// 3. Install at the fixed path. The replaced copy is NOT kept anywhere (no <dest>.previous, no backup of any kind).
|
|
192
|
+
await d.installApp({ app: staged, dev: true, dest: P.dirname(destination), appName: name, bundleId: descriptor.bundleId ?? null, relaunch: null }, { env });
|
|
193
|
+
// A stale <dest>.previous from an older RightKit must not survive a dev install either.
|
|
194
|
+
d.remove(previousPathOf(destination));
|
|
195
|
+
// 4. Verify the installed copy; a failure removes the bad copy and leaves the path absent: reinstall from an older DMG.
|
|
196
|
+
try {
|
|
197
|
+
verifyInstalled({ appRoot, destination, staged, expectedRequirement: signing.designatedRequirement, binaries: descriptor.verifyBinaries, env, deps: d, descriptor, config, signing });
|
|
198
|
+
} catch (error) {
|
|
199
|
+
d.remove(destination);
|
|
200
|
+
error.rolledBack = "removed-failed-install";
|
|
201
|
+
error.message = `${error.message}; the failed install was removed and no old copy is kept: reinstall from an older DMG`;
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
d.log(`right-release dev: installed ${destination}; identity=${signing.identity}; TeamIdentifier=${signing.teamIdentifier}; designated=${signing.designatedRequirement}`);
|
|
205
|
+
return { installed: destination, designatedRequirement: signing.designatedRequirement, identity: signing.identity, teamIdentifier: signing.teamIdentifier, signing };
|
|
206
|
+
} finally { d.remove(tempRoot); }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Deliberate reset of a fixed install: mode "remove" (required, there is no default) deletes the install and any stale
|
|
211
|
+
* `.previous` (and, with `resetPrivacy`, runs `tccutil reset All <bundleId>`). There is no restore mode: dev installs keep
|
|
212
|
+
* no old copy (Orthic-Labs/rightkit#348), reinstall from an older DMG instead.
|
|
213
|
+
* Quit the app first; nothing here kills processes.
|
|
214
|
+
*/
|
|
215
|
+
export async function resetTauriDevId({ appRoot, config, mode, resetPrivacy = false, deps = {}, env = {}, releaseConfig }) {
|
|
216
|
+
const d = { ...defaultDeps(), ...deps };
|
|
217
|
+
const descriptor = resolveTauriDevIdDescriptor(config, { appRoot, readFile: d.readFile, releaseConfig });
|
|
218
|
+
const destination = descriptor.destination;
|
|
219
|
+
if (mode !== "remove") throw new TauriDevIdError("TAURI_DEVID_RESET_MODE_INVALID", `mode must be "remove" (restore was removed: dev installs keep no old copy), got ${JSON.stringify(mode)}`);
|
|
220
|
+
d.remove(destination);
|
|
221
|
+
d.remove(previousPathOf(destination));
|
|
222
|
+
let privacyReset = false;
|
|
223
|
+
if (resetPrivacy) {
|
|
224
|
+
if (!descriptor.bundleId) throw new TauriDevIdError("TAURI_DEVID_CONFIG_INVALID", "resetPrivacy requires install.bundleId");
|
|
225
|
+
const reset = d.run("tccutil", ["reset", "All", descriptor.bundleId], { cwd: appRoot, env, encoding: "utf8", windowsHide: true, shell: false });
|
|
226
|
+
if (reset.error || reset.status !== 0) throw new TauriDevIdError("TAURI_DEVID_RESET_FAILED", `tccutil reset failed: ${reset.error?.message ?? text(reset)}`);
|
|
227
|
+
privacyReset = true;
|
|
228
|
+
}
|
|
229
|
+
d.log(`right-release dev: removed ${destination}${privacyReset ? " and reset its privacy grants" : ""}`);
|
|
230
|
+
return { mode, destination, removed: true, privacyReset };
|
|
231
|
+
}
|
package/inventory-check.mjs
CHANGED
|
@@ -24,7 +24,7 @@ const CACHE_TTL_MS = 60 * 60 * 1000;
|
|
|
24
24
|
const CONCURRENCY = 8;
|
|
25
25
|
const REQUEST_TIMEOUT_MS = 15_000;
|
|
26
26
|
const CRATES_API_INTERVAL_MS = 1000;
|
|
27
|
-
const USER_AGENT = "rightkit-inventory-check/1 (+https://github.com/
|
|
27
|
+
const USER_AGENT = "rightkit-inventory-check/1 (+https://github.com/bogusyogi/rightkit)";
|
|
28
28
|
const EXCLUDED_PREFIX = "build-control-";
|
|
29
29
|
const OWNER_PROBE_CRATE = "rightkit-fs";
|
|
30
30
|
const EXTRA_ACCOUNT_CRATES = Object.freeze([]);
|