@rightkit/release 0.2.134 → 0.2.136
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -0
- package/addon-command.mjs +53 -34
- package/apple-profile-renew.mjs +30 -12
- package/artifact-storage.mjs +53 -12
- package/background-process.mjs +18 -0
- package/build-release.mjs +4 -3
- package/cache-command.mjs +4 -3
- package/cargo-contract.mjs +1 -1
- package/cargo-guard.mjs +4 -3
- package/cargo-registry-readback.mjs +51 -0
- package/cargo-target.mjs +3 -1
- package/cli/right-release.mjs +26 -0
- package/codesign-argv.mjs +64 -0
- package/codesign-environment.mjs +58 -0
- package/codesign-shim.mjs +51 -0
- package/committed-source.mjs +3 -3
- package/development.mjs +11 -9
- package/direct-bootstrap.mjs +19 -9
- package/dmg-build.mjs +117 -0
- package/dmg-layout.mjs +123 -0
- package/github-release.mjs +74 -37
- package/heavy-command.mjs +3 -2
- package/install-app.mjs +358 -0
- package/inventory-check.mjs +980 -0
- package/macos-notarization.mjs +24 -8
- package/macos-signing-session.mjs +50 -2
- package/managed-cargo.mjs +23 -19
- package/managed-host-secrets.mjs +30 -0
- package/managed-roots.mjs +39 -0
- package/model-promote.mjs +31 -37
- package/notarize-macos.mjs +5 -5
- package/notary-auth.mjs +4 -0
- package/osascript-guard.mjs +24 -0
- package/ownership-approval-command.mjs +15 -4
- package/ownership-approval.mjs +114 -49
- package/ownership-availability.mjs +25 -0
- package/ownership-catalog-config.json +18 -0
- package/ownership-catalog.mjs +94 -0
- package/ownership-command.mjs +17 -10
- package/ownership-declaration.json +15 -0
- package/ownership-default-policy.json +1 -11
- package/ownership-evidence.mjs +10 -8
- package/ownership-git-inventory.mjs +29 -1
- package/ownership-helper-paths.json +4 -4
- package/ownership-hooks.mjs +107 -0
- package/ownership-internal-owners.json +44 -0
- package/ownership-key-authority.mjs +112 -7
- package/ownership-key-command.mjs +76 -19
- package/ownership-key-trust.mjs +97 -9
- package/ownership-lockfiles.mjs +311 -0
- package/ownership-report-command.mjs +12 -3
- package/ownership-report.mjs +20 -17
- package/ownership-repositories.json +19 -0
- package/ownership-scan-worker.mjs +96 -11
- package/ownership-source.mjs +39 -0
- package/ownership-staged.mjs +193 -0
- package/ownership-verdict.mjs +74 -0
- package/ownership.mjs +215 -107
- package/package.json +1 -1
- package/protected-credentials.mjs +111 -0
- package/prune-r2.mjs +4 -3
- package/pub-upload.mjs +3 -85
- package/publication-artifacts.mjs +108 -0
- package/publication-gate.mjs +28 -7
- package/publish-cargo.mjs +47 -16
- package/publish-swift.mjs +46 -17
- package/publish-update.mjs +61 -39
- package/r2-verified-upload.mjs +41 -0
- package/registry-parity.mjs +5 -3
- package/release-state.mjs +1 -0
- package/release-token.mjs +2 -0
- package/release.mjs +2 -1
- package/rightkit-drift.mjs +3 -2
- package/rightkit-ownership.json +2028 -139
- package/rightkit-signing-agent.mjs +161 -19
- package/rightkit-sync.mjs +6 -2
- package/rightkit-versions.json +220 -106
- package/runtime-artifact-manifest.mjs +20 -6
- package/sign-updater.mjs +5 -0
- package/signing-agent-client.mjs +78 -22
- package/signing-agent-command.mjs +5 -4
- package/signing-agent-install.mjs +31 -4
- package/standalone-clone-verify.mjs +3 -2
- package/tauri-bundle-marker.mjs +3 -2
- package/upload-large.mjs +36 -82
- package/verifier.mjs +5 -1
package/install-app.mjs
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Sanctioned, non-intrusive local app install from a signed DMG (macOS).
|
|
3
|
+
// Never uses a shell, osascript, AppleEvents, or foreground activation.
|
|
4
|
+
import { execFile as nodeExecFile } from "./background-process.mjs";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { pathToFileURL } from "node:url";
|
|
9
|
+
|
|
10
|
+
const SIGTERM_WAIT_MS = 15_000;
|
|
11
|
+
const POLL_MS = 250;
|
|
12
|
+
|
|
13
|
+
export class InstallAppError extends Error {
|
|
14
|
+
constructor(code, message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.code = code;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const VALUE_FLAGS = new Set(["--dmg", "--app-name", "--bundle-id", "--dest"]);
|
|
21
|
+
const BOOL_FLAGS = new Set(["--relaunch", "--no-relaunch", "--json"]);
|
|
22
|
+
|
|
23
|
+
export function parseArgs(argv) {
|
|
24
|
+
const opts = { dmg: null, appName: null, bundleId: null, dest: "/Applications", relaunch: null, json: false };
|
|
25
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
26
|
+
const flag = argv[i];
|
|
27
|
+
if (VALUE_FLAGS.has(flag)) {
|
|
28
|
+
const value = argv[i + 1];
|
|
29
|
+
if (value === undefined || value.startsWith("--")) throw new InstallAppError("usage", `${flag} requires a value`);
|
|
30
|
+
i += 1;
|
|
31
|
+
if (flag === "--dmg") opts.dmg = value;
|
|
32
|
+
else if (flag === "--app-name") opts.appName = value;
|
|
33
|
+
else if (flag === "--bundle-id") opts.bundleId = value;
|
|
34
|
+
else opts.dest = value;
|
|
35
|
+
} else if (BOOL_FLAGS.has(flag)) {
|
|
36
|
+
if (flag === "--json") opts.json = true;
|
|
37
|
+
else opts.relaunch = flag === "--relaunch";
|
|
38
|
+
} else {
|
|
39
|
+
throw new InstallAppError("usage", `unknown argument: ${flag}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (argv.includes("--relaunch") && argv.includes("--no-relaunch")) throw new InstallAppError("usage", "--relaunch and --no-relaunch are mutually exclusive");
|
|
43
|
+
if (!opts.dmg) throw new InstallAppError("usage", "--dmg <path.dmg> is required");
|
|
44
|
+
if (!opts.dmg.toLowerCase().endsWith(".dmg")) throw new InstallAppError("usage", "--dmg must name a .dmg file");
|
|
45
|
+
if (opts.appName !== null && (!/^[^/\0]+\.app$/.test(opts.appName) || opts.appName === ".app" || opts.appName.includes(".."))) {
|
|
46
|
+
throw new InstallAppError("usage", "--app-name must be a bare bundle name ending in .app");
|
|
47
|
+
}
|
|
48
|
+
if (opts.bundleId !== null && !/^[A-Za-z0-9][A-Za-z0-9.-]*$/.test(opts.bundleId)) throw new InstallAppError("usage", "--bundle-id is not a valid bundle identifier");
|
|
49
|
+
if (!path.isAbsolute(opts.dest)) opts.dest = path.resolve(opts.dest);
|
|
50
|
+
opts.dmg = path.resolve(opts.dmg);
|
|
51
|
+
return opts;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// --- pure parsers ---------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
function xmlUnescape(text) {
|
|
57
|
+
return text.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Minimal plist reader for hdiutil attach -plist output (dict/array/string/data/integer/true/false).
|
|
61
|
+
export function parsePlist(xml) {
|
|
62
|
+
const cleaned = String(xml).replace(/<\?[\s\S]*?\?>/g, "").replace(/<!--[\s\S]*?-->/g, "").replace(/<!DOCTYPE[^>]*>/gi, "");
|
|
63
|
+
const tokens = [...cleaned.matchAll(/<(\/?)([a-zA-Z]+)(?:\s[^>]*?)?(\s*\/)?>|([^<]+)/g)];
|
|
64
|
+
let pos = 0;
|
|
65
|
+
const skipText = () => { while (pos < tokens.length && tokens[pos][4] !== undefined && tokens[pos][4].trim() === "") pos += 1; };
|
|
66
|
+
function readValue() {
|
|
67
|
+
skipText();
|
|
68
|
+
const t = tokens[pos];
|
|
69
|
+
if (!t || t[4] !== undefined || t[1]) throw new InstallAppError("plist", "malformed plist");
|
|
70
|
+
const name = t[2];
|
|
71
|
+
pos += 1;
|
|
72
|
+
if (t[3]) return name === "true" ? true : name === "false" ? false : "";
|
|
73
|
+
if (name === "dict") {
|
|
74
|
+
const out = {};
|
|
75
|
+
for (;;) {
|
|
76
|
+
skipText();
|
|
77
|
+
const n = tokens[pos];
|
|
78
|
+
if (!n) throw new InstallAppError("plist", "unterminated dict");
|
|
79
|
+
if (n[1] && n[2] === "dict") { pos += 1; return out; }
|
|
80
|
+
if (n[2] !== "key") throw new InstallAppError("plist", "expected key");
|
|
81
|
+
pos += 1;
|
|
82
|
+
const key = tokens[pos]?.[4] !== undefined ? xmlUnescape(tokens[pos++][4]) : "";
|
|
83
|
+
pos += 1; // </key>
|
|
84
|
+
out[key] = readValue();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (name === "array") {
|
|
88
|
+
const out = [];
|
|
89
|
+
for (;;) {
|
|
90
|
+
skipText();
|
|
91
|
+
const n = tokens[pos];
|
|
92
|
+
if (!n) throw new InstallAppError("plist", "unterminated array");
|
|
93
|
+
if (n[1] && n[2] === "array") { pos += 1; return out; }
|
|
94
|
+
out.push(readValue());
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
let text = "";
|
|
98
|
+
if (tokens[pos]?.[4] !== undefined) { text = xmlUnescape(tokens[pos][4]); pos += 1; }
|
|
99
|
+
pos += 1; // closing tag
|
|
100
|
+
if (name === "integer") return Number(text);
|
|
101
|
+
if (name === "real") return Number(text);
|
|
102
|
+
return text;
|
|
103
|
+
}
|
|
104
|
+
while (pos < tokens.length && !(tokens[pos][2] === "plist" && !tokens[pos][1])) pos += 1;
|
|
105
|
+
if (pos >= tokens.length) throw new InstallAppError("plist", "no <plist> element");
|
|
106
|
+
pos += 1;
|
|
107
|
+
return readValue();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function mountPointsFromAttach(plistXml) {
|
|
111
|
+
const parsed = parsePlist(plistXml);
|
|
112
|
+
const entities = parsed?.["system-entities"];
|
|
113
|
+
if (!Array.isArray(entities)) throw new InstallAppError("attach", "hdiutil attach output has no system-entities");
|
|
114
|
+
return entities.map((e) => e?.["mount-point"]).filter((m) => typeof m === "string" && m.length > 0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function parseTeamId(codesignDvvOutput) {
|
|
118
|
+
const match = /^TeamIdentifier=(.+)$/m.exec(codesignDvvOutput);
|
|
119
|
+
const value = match?.[1].trim();
|
|
120
|
+
return value && value !== "not set" ? value : null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Parse `ps -axo pid=,comm=` and return pids whose executable lives inside <app>/Contents/MacOS/.
|
|
124
|
+
export function matchPids(psOutput, appBundlePath, { selfPid = process.pid } = {}) {
|
|
125
|
+
const prefix = `${appBundlePath.replace(/\/+$/, "")}/Contents/MacOS/`;
|
|
126
|
+
const pids = [];
|
|
127
|
+
for (const line of String(psOutput).split("\n")) {
|
|
128
|
+
const m = /^\s*(\d+)\s+(.+?)\s*$/.exec(line);
|
|
129
|
+
if (!m) continue;
|
|
130
|
+
const pid = Number(m[1]);
|
|
131
|
+
if (pid === selfPid || pid <= 1) continue;
|
|
132
|
+
if (m[2].startsWith(prefix)) pids.push(pid);
|
|
133
|
+
}
|
|
134
|
+
return pids;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function trashName(appName, now = new Date()) {
|
|
138
|
+
const stamp = now.toISOString().replace(/[:T]/g, "-").replace(/\..*$/, "");
|
|
139
|
+
const base = appName.replace(/\.app$/, "");
|
|
140
|
+
return `${base} ${stamp}.app`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// --- orchestration --------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
const realExecFile = (cmd, args) => new Promise((resolve, reject) => {
|
|
146
|
+
nodeExecFile(cmd, args, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, windowsHide: true }, (error, stdout, stderr) => {
|
|
147
|
+
if (error) { error.stdout = stdout; error.stderr = stderr; reject(error); } else resolve({ stdout, stderr });
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
function errText(error) {
|
|
152
|
+
return `${error?.stderr || ""}${error?.stdout || ""}`.trim() || error?.message || String(error);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function installApp(opts, deps = {}) {
|
|
156
|
+
const {
|
|
157
|
+
execFile = realExecFile,
|
|
158
|
+
platform = process.platform,
|
|
159
|
+
fsImpl = fs,
|
|
160
|
+
home = os.homedir(),
|
|
161
|
+
now = () => new Date(),
|
|
162
|
+
sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
163
|
+
kill = (pid, sig) => process.kill(pid, sig),
|
|
164
|
+
emit = () => {},
|
|
165
|
+
tmpdir = os.tmpdir(),
|
|
166
|
+
} = deps;
|
|
167
|
+
const event = (name, data = {}) => emit({ event: name, ...data });
|
|
168
|
+
const run = (cmd, args) => execFile(cmd, args);
|
|
169
|
+
let mountRoot = null;
|
|
170
|
+
let mounted = false;
|
|
171
|
+
let mountPoint = null;
|
|
172
|
+
let failedStage = "start";
|
|
173
|
+
|
|
174
|
+
const detach = async () => {
|
|
175
|
+
if (!mounted) return;
|
|
176
|
+
mounted = false;
|
|
177
|
+
try { await run("/usr/bin/hdiutil", ["detach", mountPoint]); }
|
|
178
|
+
catch { try { await run("/usr/bin/hdiutil", ["detach", "-force", mountPoint]); } catch { /* best effort */ } }
|
|
179
|
+
try { fsImpl.rmSync(mountRoot, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
if (platform !== "darwin") throw new InstallAppError("unsupported", `install-app is not implemented on ${platform}`);
|
|
184
|
+
event("start", { dmg: opts.dmg, dest: opts.dest });
|
|
185
|
+
if (!fsImpl.existsSync(opts.dmg)) throw new InstallAppError("dmg-missing", `DMG not found: ${opts.dmg}`);
|
|
186
|
+
if (!fsImpl.existsSync(opts.dest)) throw new InstallAppError("dest-missing", `destination not found: ${opts.dest}`);
|
|
187
|
+
|
|
188
|
+
failedStage = "attach";
|
|
189
|
+
mountRoot = fsImpl.mkdtempSync(path.join(tmpdir, "right-install-app-"));
|
|
190
|
+
const attached = await run("/usr/bin/hdiutil", ["attach", "-nobrowse", "-readonly", "-noautoopen", "-mountrandom", mountRoot, "-plist", opts.dmg]);
|
|
191
|
+
const mounts = mountPointsFromAttach(attached.stdout);
|
|
192
|
+
if (mounts.length === 0) throw new InstallAppError("attach", "DMG attached but exposed no mount point");
|
|
193
|
+
mountPoint = mounts[0];
|
|
194
|
+
mounted = true;
|
|
195
|
+
|
|
196
|
+
let sourceApp = null;
|
|
197
|
+
for (const mp of mounts) {
|
|
198
|
+
const apps = fsImpl.readdirSync(mp).filter((n) => n.endsWith(".app"));
|
|
199
|
+
if (opts.appName) { if (apps.includes(opts.appName)) { sourceApp = path.join(mp, opts.appName); break; } }
|
|
200
|
+
else if (apps.length === 1) { sourceApp = path.join(mp, apps[0]); break; }
|
|
201
|
+
else if (apps.length > 1) throw new InstallAppError("ambiguous-app", `DMG contains multiple apps (${apps.join(", ")}); pass --app-name`);
|
|
202
|
+
}
|
|
203
|
+
if (!sourceApp) throw new InstallAppError("app-not-found", opts.appName ? `${opts.appName} not found in DMG` : "no .app found in DMG");
|
|
204
|
+
const appName = path.basename(sourceApp);
|
|
205
|
+
const installed = path.join(opts.dest, appName);
|
|
206
|
+
const hadInstalled = fsImpl.existsSync(installed);
|
|
207
|
+
|
|
208
|
+
// Verify the mounted app before touching anything installed.
|
|
209
|
+
failedStage = "verify";
|
|
210
|
+
const newTeam = await verifyBundle(run, sourceApp, { assess: true, staple: true });
|
|
211
|
+
const newBundleId = await readBundleId(run, sourceApp);
|
|
212
|
+
if (opts.bundleId && newBundleId !== opts.bundleId) throw new InstallAppError("bundle-id-mismatch", `DMG app bundle id ${newBundleId} != --bundle-id ${opts.bundleId}`);
|
|
213
|
+
let installedTeam = null;
|
|
214
|
+
if (hadInstalled) {
|
|
215
|
+
const installedBundleId = await readBundleId(run, installed);
|
|
216
|
+
if (installedBundleId !== newBundleId) throw new InstallAppError("bundle-id-mismatch", `DMG app bundle id ${newBundleId} != installed bundle id ${installedBundleId}`);
|
|
217
|
+
try { installedTeam = await teamIdOf(run, installed); } catch { installedTeam = null; }
|
|
218
|
+
if (installedTeam && installedTeam !== newTeam) throw new InstallAppError("team-id-changed", `refusing team change: installed ${installedTeam}, DMG ${newTeam}`);
|
|
219
|
+
}
|
|
220
|
+
const bundleId = opts.bundleId || newBundleId;
|
|
221
|
+
event("verified", { app: appName, bundleId, teamId: newTeam, replacing: hadInstalled });
|
|
222
|
+
|
|
223
|
+
// Quit running instance without UI.
|
|
224
|
+
failedStage = "quit";
|
|
225
|
+
const wasRunning = hadInstalled ? (await findPids(run, installed)).length > 0 : false;
|
|
226
|
+
const quit = hadInstalled ? await quitApp(run, kill, sleep, installed) : { pids: [], killed: [] };
|
|
227
|
+
event("quit", { wasRunning, pids: quit.pids, forced: quit.killed });
|
|
228
|
+
|
|
229
|
+
// Replace.
|
|
230
|
+
failedStage = "replace";
|
|
231
|
+
let trashed = null;
|
|
232
|
+
if (hadInstalled) {
|
|
233
|
+
const trashDir = path.join(home, ".Trash");
|
|
234
|
+
fsImpl.mkdirSync(trashDir, { recursive: true });
|
|
235
|
+
trashed = path.join(trashDir, trashName(appName, now()));
|
|
236
|
+
await moveAside(run, fsImpl, installed, trashed);
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
await run("/usr/bin/ditto", [sourceApp, installed]);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
try { fsImpl.rmSync(installed, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
242
|
+
let restored = false;
|
|
243
|
+
if (trashed) { try { await moveAside(run, fsImpl, trashed, installed); restored = true; } catch { /* reported below */ } }
|
|
244
|
+
throw new InstallAppError("ditto-failed", `ditto failed (${errText(error)}); ${trashed ? (restored ? "previous app restored" : `RESTORE FAILED, previous app is at ${trashed}`) : "no previous app to restore"}`);
|
|
245
|
+
}
|
|
246
|
+
event("replaced", { installed, previous: trashed });
|
|
247
|
+
await detach();
|
|
248
|
+
|
|
249
|
+
// Re-verify installed copy.
|
|
250
|
+
failedStage = "reverify";
|
|
251
|
+
await verifyBundle(run, installed, { assess: true, staple: false });
|
|
252
|
+
|
|
253
|
+
// Relaunch hidden and in the background only.
|
|
254
|
+
const relaunch = opts.relaunch === null ? wasRunning : opts.relaunch;
|
|
255
|
+
if (relaunch) {
|
|
256
|
+
failedStage = "relaunch";
|
|
257
|
+
await run("/usr/bin/open", ["-g", "-j", "-b", bundleId]);
|
|
258
|
+
event("relaunched", { bundleId });
|
|
259
|
+
}
|
|
260
|
+
const result = { ok: true, app: appName, installed, bundleId, teamId: newTeam, previous: trashed, relaunched: relaunch };
|
|
261
|
+
event("done", result);
|
|
262
|
+
return result;
|
|
263
|
+
} catch (error) {
|
|
264
|
+
const reason = error instanceof InstallAppError ? error.message : errText(error);
|
|
265
|
+
event("failed", { stage: failedStage, code: error.code || "error", reason });
|
|
266
|
+
if (!(error instanceof InstallAppError)) throw new InstallAppError("error", reason);
|
|
267
|
+
throw error;
|
|
268
|
+
} finally {
|
|
269
|
+
await detach();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function verifyBundle(run, app, { assess, staple }) {
|
|
274
|
+
const step = async (code, cmd, args) => {
|
|
275
|
+
try { return await run(cmd, args); }
|
|
276
|
+
catch (error) { throw new InstallAppError(code, `${code} failed for ${app}: ${errText(error)}`); }
|
|
277
|
+
};
|
|
278
|
+
await step("codesign-verify", "/usr/bin/codesign", ["--verify", "--deep", "--strict", app]);
|
|
279
|
+
const team = await teamIdOf(run, app);
|
|
280
|
+
if (!team) throw new InstallAppError("team-id-missing", `${app} has no Team ID (ad-hoc or unsigned)`);
|
|
281
|
+
await step("codesign-requirement", "/usr/bin/codesign", ["-dr", "-", app]);
|
|
282
|
+
if (assess) await step("spctl-assess", "/usr/sbin/spctl", ["--assess", "--type", "execute", app]);
|
|
283
|
+
if (staple) await step("stapler-validate", "/usr/bin/xcrun", ["stapler", "validate", app]);
|
|
284
|
+
return team;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function teamIdOf(run, app) {
|
|
288
|
+
const out = await run("/usr/bin/codesign", ["-dvv", app]);
|
|
289
|
+
return parseTeamId(`${out.stderr || ""}\n${out.stdout || ""}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function readBundleId(run, app) {
|
|
293
|
+
let out;
|
|
294
|
+
try { out = await run("/usr/bin/plutil", ["-convert", "json", "-o", "-", path.join(app, "Contents", "Info.plist")]); }
|
|
295
|
+
catch (error) { throw new InstallAppError("info-plist", `cannot read Info.plist of ${app}: ${errText(error)}`); }
|
|
296
|
+
const id = JSON.parse(out.stdout)?.CFBundleIdentifier;
|
|
297
|
+
if (typeof id !== "string" || !id) throw new InstallAppError("info-plist", `no CFBundleIdentifier in ${app}`);
|
|
298
|
+
return id;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function findPids(run, app) {
|
|
302
|
+
const ps = await run("/bin/ps", ["-axo", "pid=,comm="]);
|
|
303
|
+
return matchPids(ps.stdout, app);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function quitApp(run, kill, sleep, app) {
|
|
307
|
+
const pids = await findPids(run, app);
|
|
308
|
+
const send = (pid, sig) => { try { kill(pid, sig); } catch (e) { if (e.code !== "ESRCH") throw e; } };
|
|
309
|
+
for (const pid of pids) send(pid, "SIGTERM");
|
|
310
|
+
let waited = 0;
|
|
311
|
+
let remaining = pids.length ? await findPids(run, app) : [];
|
|
312
|
+
while (remaining.length && waited < SIGTERM_WAIT_MS) {
|
|
313
|
+
await sleep(POLL_MS);
|
|
314
|
+
waited += POLL_MS;
|
|
315
|
+
remaining = await findPids(run, app);
|
|
316
|
+
}
|
|
317
|
+
for (const pid of remaining) send(pid, "SIGKILL");
|
|
318
|
+
if (remaining.length) await sleep(POLL_MS);
|
|
319
|
+
return { pids, killed: remaining };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function moveAside(run, fsImpl, from, to) {
|
|
323
|
+
try {
|
|
324
|
+
fsImpl.renameSync(from, to);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (error.code !== "EXDEV") throw error;
|
|
327
|
+
await run("/usr/bin/ditto", [from, to]);
|
|
328
|
+
fsImpl.rmSync(from, { recursive: true, force: true });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export async function main(argv = process.argv.slice(2), io = {}) {
|
|
333
|
+
const stdout = io.stdout || ((s) => console.log(s));
|
|
334
|
+
const stderr = io.stderr || ((s) => console.error(s));
|
|
335
|
+
let opts;
|
|
336
|
+
try { opts = parseArgs(argv); }
|
|
337
|
+
catch (error) {
|
|
338
|
+
stderr(`right-release install-app: ${error.message}`);
|
|
339
|
+
stderr("usage: right-release install-app --dmg <path.dmg> [--app-name Pulse.app] [--bundle-id dev.orthic.pulse] [--dest /Applications] [--relaunch|--no-relaunch] [--json]");
|
|
340
|
+
return 2;
|
|
341
|
+
}
|
|
342
|
+
const emit = (e) => {
|
|
343
|
+
if (opts.json) { stdout(JSON.stringify({ schema: 1, ts: new Date().toISOString(), ...e })); return; }
|
|
344
|
+
const { event, ...rest } = e;
|
|
345
|
+
if (event === "failed") stderr(`install-app: failed at ${rest.stage}: ${rest.reason}`);
|
|
346
|
+
else stdout(`install-app: ${event}${Object.keys(rest).length ? ` ${JSON.stringify(rest)}` : ""}`);
|
|
347
|
+
};
|
|
348
|
+
try {
|
|
349
|
+
await installApp(opts, { ...io.deps, emit });
|
|
350
|
+
return 0;
|
|
351
|
+
} catch (error) {
|
|
352
|
+
return error.code === "unsupported" ? 3 : 1;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
357
|
+
main().then((code) => { process.exitCode = code; });
|
|
358
|
+
}
|