@rightkit/release 0.2.140 → 0.2.141
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/install-app.mjs +201 -21
- package/package.json +1 -1
- package/rightkit-versions.json +4 -3
package/install-app.mjs
CHANGED
|
@@ -20,10 +20,10 @@ export class InstallAppError extends Error {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
const VALUE_FLAGS = new Set(["--dmg", "--zip", "--app", "--app-name", "--bundle-id", "--dest"]);
|
|
23
|
-
const BOOL_FLAGS = new Set(["--relaunch", "--no-relaunch", "--json", "--dev"]);
|
|
23
|
+
const BOOL_FLAGS = new Set(["--relaunch", "--no-relaunch", "--json", "--dev", "--recover"]);
|
|
24
24
|
|
|
25
25
|
export function parseArgs(argv) {
|
|
26
|
-
const opts = { dmg: null, zip: null, app: null, dev: false, appName: null, bundleId: null, dest: "/Applications", relaunch: null, json: false };
|
|
26
|
+
const opts = { dmg: null, zip: null, app: null, dev: false, appName: null, bundleId: null, dest: "/Applications", relaunch: null, json: false, recover: false };
|
|
27
27
|
for (let i = 0; i < argv.length; i += 1) {
|
|
28
28
|
const flag = argv[i];
|
|
29
29
|
if (VALUE_FLAGS.has(flag)) {
|
|
@@ -39,6 +39,7 @@ export function parseArgs(argv) {
|
|
|
39
39
|
} else if (BOOL_FLAGS.has(flag)) {
|
|
40
40
|
if (flag === "--json") opts.json = true;
|
|
41
41
|
else if (flag === "--dev") opts.dev = true;
|
|
42
|
+
else if (flag === "--recover") opts.recover = true;
|
|
42
43
|
else opts.relaunch = flag === "--relaunch";
|
|
43
44
|
} else {
|
|
44
45
|
throw new InstallAppError("usage", `unknown argument: ${flag}`);
|
|
@@ -46,6 +47,14 @@ export function parseArgs(argv) {
|
|
|
46
47
|
}
|
|
47
48
|
if (argv.includes("--relaunch") && argv.includes("--no-relaunch")) throw new InstallAppError("usage", "--relaunch and --no-relaunch are mutually exclusive");
|
|
48
49
|
const inputs = [opts.dmg, opts.zip, opts.app].filter((v) => v !== null).length;
|
|
50
|
+
if (opts.recover) {
|
|
51
|
+
if (inputs > 0 || opts.dev || opts.relaunch !== null || opts.bundleId !== null) throw new InstallAppError("usage", "--recover only accepts --dest, --app-name, and --json");
|
|
52
|
+
if (opts.appName !== null && (!/^[^/\0]+\.app$/.test(opts.appName) || opts.appName === ".app" || opts.appName.includes(".."))) {
|
|
53
|
+
throw new InstallAppError("usage", "--app-name must be a bare bundle name ending in .app");
|
|
54
|
+
}
|
|
55
|
+
if (!path.isAbsolute(opts.dest)) opts.dest = path.resolve(opts.dest);
|
|
56
|
+
return opts;
|
|
57
|
+
}
|
|
49
58
|
if (inputs === 0) throw new InstallAppError("usage", opts.dev ? "one of --dmg <path.dmg>, --zip <path.zip>, or --app <path.app> is required" : "--dmg <path.dmg> is required");
|
|
50
59
|
if (inputs > 1) throw new InstallAppError("usage", "--dmg, --zip, and --app are mutually exclusive");
|
|
51
60
|
if ((opts.zip !== null || opts.app !== null) && !opts.dev) throw new InstallAppError("usage", "--zip and --app require --dev (only DMGs are installable as release builds)");
|
|
@@ -164,6 +173,98 @@ function errText(error) {
|
|
|
164
173
|
return `${error?.stderr || ""}${error?.stdout || ""}`.trim() || error?.message || String(error);
|
|
165
174
|
}
|
|
166
175
|
|
|
176
|
+
// --- crash-safe swap: journal + recovery -----------------------------------
|
|
177
|
+
|
|
178
|
+
const JOURNAL_RE = /^\.(.+)\.rightkit-install\.json$/;
|
|
179
|
+
|
|
180
|
+
export function journalPathFor(dest, appName) {
|
|
181
|
+
return path.join(dest, `.${appName.replace(/\.app$/, "")}.rightkit-install.json`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Atomic write: temp file, fsync, rename, then a best-effort fsync of the directory.
|
|
185
|
+
export function writeJournal(fsImpl, file, data) {
|
|
186
|
+
const tmp = `${file}.tmp`;
|
|
187
|
+
const fd = fsImpl.openSync(tmp, "w");
|
|
188
|
+
try { fsImpl.writeSync(fd, `${JSON.stringify(data)}\n`); fsImpl.fsyncSync(fd); }
|
|
189
|
+
finally { fsImpl.closeSync(fd); }
|
|
190
|
+
fsImpl.renameSync(tmp, file);
|
|
191
|
+
try {
|
|
192
|
+
const dfd = fsImpl.openSync(path.dirname(file), "r");
|
|
193
|
+
try { fsImpl.fsyncSync(dfd); } finally { fsImpl.closeSync(dfd); }
|
|
194
|
+
} catch { /* directory fsync is best effort */ }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const defaultIsAlive = (pid) => {
|
|
198
|
+
try { process.kill(pid, 0); return true; } catch (e) { return e.code === "EPERM"; }
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
async function trashBundle(run, fsImpl, home, now, bundle, appName) {
|
|
202
|
+
const trashDir = path.join(home, ".Trash");
|
|
203
|
+
fsImpl.mkdirSync(trashDir, { recursive: true });
|
|
204
|
+
const trashed = path.join(trashDir, trashName(appName, now()));
|
|
205
|
+
await moveAside(run, fsImpl, bundle, trashed);
|
|
206
|
+
return trashed;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Deterministic roll-forward/back of any interrupted install journal in `dest`. Idempotent.
|
|
210
|
+
export async function recoverInstalls({ dest, appName = null, run, fsImpl, home, now, event, sleep, isAlive = defaultIsAlive, selfPid = process.pid }) {
|
|
211
|
+
let names = [];
|
|
212
|
+
try { names = fsImpl.readdirSync(dest).filter((n) => JOURNAL_RE.test(n)); } catch { return []; }
|
|
213
|
+
if (appName) names = names.filter((n) => n === path.basename(journalPathFor(dest, appName)));
|
|
214
|
+
const results = [];
|
|
215
|
+
for (const name of names.sort()) {
|
|
216
|
+
const file = path.join(dest, name);
|
|
217
|
+
let j;
|
|
218
|
+
try { j = JSON.parse(fsImpl.readFileSync(file, "utf8")); } catch {
|
|
219
|
+
event("recovery-journal-unreadable", { journal: file });
|
|
220
|
+
try { fsImpl.rmSync(file, { force: true }); } catch { /* best effort */ }
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (j.pid && j.pid !== selfPid && isAlive(j.pid)) { event("recovery-skipped", { journal: file, pid: j.pid, reason: "install still running" }); continue; }
|
|
224
|
+
const has = (p) => Boolean(p) && fsImpl.existsSync(p);
|
|
225
|
+
const rm = (p) => { if (p) { try { fsImpl.rmSync(p, { recursive: true, force: true }); } catch { /* best effort */ } } };
|
|
226
|
+
let action;
|
|
227
|
+
let keepJournal = false;
|
|
228
|
+
try {
|
|
229
|
+
if (!has(j.installed)) {
|
|
230
|
+
if (has(j.backup)) { fsImpl.renameSync(j.backup, j.installed); action = "restored-backup"; }
|
|
231
|
+
else action = "cleared";
|
|
232
|
+
} else if (has(j.backup)) {
|
|
233
|
+
let ok = j.phase === "verified";
|
|
234
|
+
if (!ok) { try { await verifyBundle(run, j.installed, { assess: !j.dev, staple: false }); ok = true; } catch { ok = false; } }
|
|
235
|
+
if (ok) {
|
|
236
|
+
try { await trashBundle(run, fsImpl, home, now, j.backup, path.basename(j.installed)); action = "rolled-forward"; }
|
|
237
|
+
catch { action = "rolled-forward-trash-pending"; keepJournal = true; }
|
|
238
|
+
} else {
|
|
239
|
+
rm(j.installed);
|
|
240
|
+
fsImpl.renameSync(j.backup, j.installed);
|
|
241
|
+
action = "rolled-back";
|
|
242
|
+
}
|
|
243
|
+
} else action = "cleared";
|
|
244
|
+
} catch (error) {
|
|
245
|
+
event("recovery-failed", { journal: file, reason: errText(error) });
|
|
246
|
+
continue; // leave the journal for the next run
|
|
247
|
+
}
|
|
248
|
+
rm(j.staged);
|
|
249
|
+
if (j.mount && has(j.mount)) {
|
|
250
|
+
const out = await detachWithRetry({ mount: j.mount, log: (e) => event(e.event, e), sleep, detach: (args) => run("/usr/bin/hdiutil", args) });
|
|
251
|
+
if (out.ok) rm(j.mountRoot);
|
|
252
|
+
} else rm(j.mountRoot);
|
|
253
|
+
if (!keepJournal) rm(file);
|
|
254
|
+
event("recovered", { journal: file, action, installed: j.installed });
|
|
255
|
+
results.push({ journal: file, action, installed: j.installed });
|
|
256
|
+
}
|
|
257
|
+
return results;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export async function recoverApp(opts, deps = {}) {
|
|
261
|
+
const { execFile = realExecFile, fsImpl = fs, home = os.homedir(), now = () => new Date(), sleep = (ms) => new Promise((r) => setTimeout(r, ms)), emit = () => {}, isAlive, platform = process.platform } = deps;
|
|
262
|
+
if (platform !== "darwin") throw new InstallAppError("unsupported", `install-app is not implemented on ${platform}`);
|
|
263
|
+
const results = await recoverInstalls({ dest: opts.dest, appName: opts.appName, run: (c, a) => execFile(c, a), fsImpl, home, now, event: (name, data = {}) => emit({ event: name, ...data }), sleep, isAlive });
|
|
264
|
+
emit({ event: "recover-done", dest: opts.dest, recovered: results.length });
|
|
265
|
+
return { ok: true, recovered: results };
|
|
266
|
+
}
|
|
267
|
+
|
|
167
268
|
export async function installApp(opts, deps = {}) {
|
|
168
269
|
const {
|
|
169
270
|
execFile = realExecFile,
|
|
@@ -175,6 +276,10 @@ export async function installApp(opts, deps = {}) {
|
|
|
175
276
|
kill = (pid, sig) => process.kill(pid, sig),
|
|
176
277
|
emit = () => {},
|
|
177
278
|
tmpdir = os.tmpdir(),
|
|
279
|
+
pid = process.pid,
|
|
280
|
+
isAlive = defaultIsAlive,
|
|
281
|
+
signalTarget = process,
|
|
282
|
+
exit = (code) => process.exit(code),
|
|
178
283
|
} = deps;
|
|
179
284
|
const dev = opts.dev === true;
|
|
180
285
|
const event = (name, data = {}) => emit({ event: name, ...(dev ? { dev: true } : {}), ...data });
|
|
@@ -184,6 +289,40 @@ export async function installApp(opts, deps = {}) {
|
|
|
184
289
|
let mounted = false;
|
|
185
290
|
let mountPoint = null;
|
|
186
291
|
let failedStage = "start";
|
|
292
|
+
let swap = null; // set once the journal exists; tracks which renames happened
|
|
293
|
+
|
|
294
|
+
const removeQuiet = (p) => { if (p) { try { fsImpl.rmSync(p, { recursive: true, force: true }); } catch { /* best effort */ } } };
|
|
295
|
+
// Undo an unfinished swap using renames only. Sync on purpose so a signal handler can run it.
|
|
296
|
+
const rollbackSwap = () => {
|
|
297
|
+
if (!swap) return;
|
|
298
|
+
const s = swap;
|
|
299
|
+
swap = null;
|
|
300
|
+
try {
|
|
301
|
+
if (!s.final) {
|
|
302
|
+
if (s.stagedIn) removeQuiet(s.installed);
|
|
303
|
+
if (s.backedUp && !fsImpl.existsSync(s.installed)) fsImpl.renameSync(s.backup, s.installed);
|
|
304
|
+
removeQuiet(s.staged);
|
|
305
|
+
removeQuiet(s.journal);
|
|
306
|
+
} else {
|
|
307
|
+
removeQuiet(s.staged);
|
|
308
|
+
}
|
|
309
|
+
} catch { /* recovery on the next run finishes the job */ }
|
|
310
|
+
};
|
|
311
|
+
let signaled = false;
|
|
312
|
+
const onSignal = (sig) => {
|
|
313
|
+
if (signaled) return;
|
|
314
|
+
signaled = true;
|
|
315
|
+
(async () => {
|
|
316
|
+
try {
|
|
317
|
+
rollbackSwap();
|
|
318
|
+
event("interrupted", { signal: sig });
|
|
319
|
+
await detach();
|
|
320
|
+
cleanupExtract();
|
|
321
|
+
} catch { /* exiting anyway */ }
|
|
322
|
+
exit(128 + (os.constants.signals[sig] || 0));
|
|
323
|
+
})();
|
|
324
|
+
};
|
|
325
|
+
const handlers = { SIGINT: () => onSignal("SIGINT"), SIGTERM: () => onSignal("SIGTERM") };
|
|
187
326
|
|
|
188
327
|
const detach = async () => {
|
|
189
328
|
if (!mounted) return;
|
|
@@ -199,6 +338,7 @@ export async function installApp(opts, deps = {}) {
|
|
|
199
338
|
|
|
200
339
|
try {
|
|
201
340
|
if (platform !== "darwin") throw new InstallAppError("unsupported", `install-app is not implemented on ${platform}`);
|
|
341
|
+
for (const [sig, fn] of Object.entries(handlers)) signalTarget.on(sig, fn);
|
|
202
342
|
const inputPath = opts.dmg ?? opts.zip ?? opts.app;
|
|
203
343
|
const inputKind = opts.dmg ? "dmg" : opts.zip ? "zip" : "app";
|
|
204
344
|
event("start", { [inputKind]: inputPath, dest: opts.dest });
|
|
@@ -206,6 +346,8 @@ export async function installApp(opts, deps = {}) {
|
|
|
206
346
|
if (!fsImpl.existsSync(inputPath)) throw new InstallAppError(`${inputKind}-missing`, `${inputKind.toUpperCase()} not found: ${inputPath}`);
|
|
207
347
|
if (!fsImpl.existsSync(opts.dest)) throw new InstallAppError("dest-missing", `destination not found: ${opts.dest}`);
|
|
208
348
|
|
|
349
|
+
await recoverInstalls({ dest: opts.dest, appName: opts.appName, run, fsImpl, home, now, event, sleep, isAlive, selfPid: pid });
|
|
350
|
+
|
|
209
351
|
let sourceApp = null;
|
|
210
352
|
const pickApp = (dir, label) => {
|
|
211
353
|
const apps = fsImpl.readdirSync(dir).filter((n) => n.endsWith(".app"));
|
|
@@ -257,30 +399,39 @@ export async function installApp(opts, deps = {}) {
|
|
|
257
399
|
const bundleId = opts.bundleId || newBundleId;
|
|
258
400
|
event("verified", { app: appName, bundleId, teamId: newTeam, replacing: hadInstalled, ...(ticket ? { ticket } : {}) });
|
|
259
401
|
|
|
402
|
+
// Stage a verified copy on the destination volume so the swap is rename(2) only.
|
|
403
|
+
failedStage = "stage";
|
|
404
|
+
const base = appName.replace(/\.app$/, "");
|
|
405
|
+
const staged = path.join(opts.dest, `.${base}.rightkit-install-${pid}.app`);
|
|
406
|
+
const backup = hadInstalled ? path.join(opts.dest, `.${base}.rightkit-backup-${pid}.app`) : null;
|
|
407
|
+
const journal = journalPathFor(opts.dest, appName);
|
|
408
|
+
const journalData = { schema: 1, appName, pid, phase: "staging", dev, installed, backup, staged, mount: mountPoint, mountRoot };
|
|
409
|
+
removeQuiet(staged);
|
|
410
|
+
writeJournal(fsImpl, journal, journalData);
|
|
411
|
+
swap = { installed, backup, staged, journal, backedUp: false, stagedIn: false, final: false };
|
|
412
|
+
try { await run("/usr/bin/ditto", [sourceApp, staged]); }
|
|
413
|
+
catch (error) {
|
|
414
|
+
rollbackSwap();
|
|
415
|
+
throw new InstallAppError("ditto-failed", `ditto failed (${errText(error)}); previous app untouched`);
|
|
416
|
+
}
|
|
417
|
+
{
|
|
418
|
+
const { team: stagedTeam } = await verifyBundle(run, staged, { assess: !dev, staple: false });
|
|
419
|
+
const stagedId = await readBundleId(run, staged);
|
|
420
|
+
if (stagedTeam !== newTeam || stagedId !== newBundleId) throw new InstallAppError("staged-mismatch", `staged copy ${staged} does not match the verified source (team ${stagedTeam}, bundle id ${stagedId})`);
|
|
421
|
+
}
|
|
422
|
+
|
|
260
423
|
// Quit running instance without UI.
|
|
261
424
|
failedStage = "quit";
|
|
262
425
|
const wasRunning = hadInstalled ? (await findPids(run, installed)).length > 0 : false;
|
|
263
426
|
const quit = hadInstalled ? await quitApp(run, kill, sleep, installed) : { pids: [], killed: [] };
|
|
264
427
|
event("quit", { wasRunning, pids: quit.pids, forced: quit.killed });
|
|
265
428
|
|
|
266
|
-
//
|
|
429
|
+
// Swap: old -> backup, staged -> installed. The old bundle is never trashed before the new one is verified.
|
|
267
430
|
failedStage = "replace";
|
|
268
|
-
|
|
269
|
-
if (hadInstalled) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
trashed = path.join(trashDir, trashName(appName, now()));
|
|
273
|
-
await moveAside(run, fsImpl, installed, trashed);
|
|
274
|
-
}
|
|
275
|
-
try {
|
|
276
|
-
await run("/usr/bin/ditto", [sourceApp, installed]);
|
|
277
|
-
} catch (error) {
|
|
278
|
-
try { fsImpl.rmSync(installed, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
279
|
-
let restored = false;
|
|
280
|
-
if (trashed) { try { await moveAside(run, fsImpl, trashed, installed); restored = true; } catch { /* reported below */ } }
|
|
281
|
-
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"}`);
|
|
282
|
-
}
|
|
283
|
-
event("replaced", { installed, previous: trashed });
|
|
431
|
+
writeJournal(fsImpl, journal, { ...journalData, phase: "swapping" });
|
|
432
|
+
if (hadInstalled) { fsImpl.renameSync(installed, backup); swap.backedUp = true; }
|
|
433
|
+
fsImpl.renameSync(staged, installed);
|
|
434
|
+
swap.stagedIn = true;
|
|
284
435
|
// Dev builds are not notarized: clear quarantine on the NEW bundle only so Gatekeeper does not block it.
|
|
285
436
|
if (dev) {
|
|
286
437
|
try { await run("/usr/bin/xattr", ["-dr", "com.apple.quarantine", installed]); }
|
|
@@ -289,9 +440,21 @@ export async function installApp(opts, deps = {}) {
|
|
|
289
440
|
await detach();
|
|
290
441
|
cleanupExtract();
|
|
291
442
|
|
|
292
|
-
// Re-verify installed copy.
|
|
443
|
+
// Re-verify installed copy; on failure the catch below rolls back to the backup.
|
|
293
444
|
failedStage = "reverify";
|
|
294
445
|
await verifyBundle(run, installed, { assess: !dev, staple: false });
|
|
446
|
+
writeJournal(fsImpl, journal, { ...journalData, phase: "verified", mount: null, mountRoot: null });
|
|
447
|
+
swap.final = true;
|
|
448
|
+
|
|
449
|
+
// Only now does the old bundle go to the Trash.
|
|
450
|
+
let trashed = null;
|
|
451
|
+
if (backup) {
|
|
452
|
+
try { trashed = await trashBundle(run, fsImpl, home, now, backup, appName); }
|
|
453
|
+
catch (error) { event("backup-trash-failed", { backup, reason: errText(error) }); trashed = backup; }
|
|
454
|
+
}
|
|
455
|
+
if (!backup || trashed !== backup) removeQuiet(journal);
|
|
456
|
+
swap = null;
|
|
457
|
+
event("replaced", { installed, previous: trashed });
|
|
295
458
|
|
|
296
459
|
// Relaunch hidden and in the background only.
|
|
297
460
|
const relaunch = opts.relaunch === null ? wasRunning : opts.relaunch;
|
|
@@ -304,11 +467,14 @@ export async function installApp(opts, deps = {}) {
|
|
|
304
467
|
event("done", result);
|
|
305
468
|
return result;
|
|
306
469
|
} catch (error) {
|
|
470
|
+
rollbackSwap();
|
|
307
471
|
const reason = error instanceof InstallAppError ? error.message : errText(error);
|
|
308
472
|
event("failed", { stage: failedStage, code: error.code || "error", reason });
|
|
309
473
|
if (!(error instanceof InstallAppError)) throw new InstallAppError("error", reason);
|
|
310
474
|
throw error;
|
|
311
475
|
} finally {
|
|
476
|
+
for (const [sig, fn] of Object.entries(handlers)) signalTarget.off(sig, fn);
|
|
477
|
+
rollbackSwap();
|
|
312
478
|
await detach();
|
|
313
479
|
cleanupExtract();
|
|
314
480
|
}
|
|
@@ -325,7 +491,19 @@ async function verifyBundle(run, app, { assess, staple, dmg = null }) {
|
|
|
325
491
|
await step("codesign-requirement", "/usr/bin/codesign", ["-dr", "-", app]);
|
|
326
492
|
if (assess) await step("spctl-assess", "/usr/sbin/spctl", ["--assess", "--type", "execute", app]);
|
|
327
493
|
let ticket = null;
|
|
494
|
+
let haveStapler = true;
|
|
328
495
|
if (staple) {
|
|
496
|
+
// Clean macOS hosts have no Xcode/CLT, so `xcrun stapler` can be missing.
|
|
497
|
+
try { await run("/usr/bin/xcrun", ["--find", "stapler"]); } catch { haveStapler = false; }
|
|
498
|
+
}
|
|
499
|
+
if (staple && !haveStapler) {
|
|
500
|
+
if (dmg) await step("spctl-dmg", "/usr/sbin/spctl", ["-a", "-t", "open", "--context", "context:primary-signature", dmg]);
|
|
501
|
+
const assessed = await step("spctl-notarized", "/usr/sbin/spctl", ["-a", "-t", "exec", "-vv", app]);
|
|
502
|
+
if (!/Notarized Developer ID/.test(`${assessed?.stderr || ""}\n${assessed?.stdout || ""}`)) {
|
|
503
|
+
throw new InstallAppError("spctl-notarized", `spctl-notarized failed for ${app}: not accepted as Notarized Developer ID`);
|
|
504
|
+
}
|
|
505
|
+
ticket = "spctl-only";
|
|
506
|
+
} else if (staple) {
|
|
329
507
|
try { await run("/usr/bin/xcrun", ["stapler", "validate", app]); ticket = "app-ticket"; }
|
|
330
508
|
catch (error) {
|
|
331
509
|
if (!dmg) throw new InstallAppError("stapler-validate", `stapler-validate failed for ${app}: ${errText(error)}`);
|
|
@@ -396,6 +574,7 @@ export async function main(argv = process.argv.slice(2), io = {}) {
|
|
|
396
574
|
catch (error) {
|
|
397
575
|
stderr(`right-release install-app: ${error.message}`);
|
|
398
576
|
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]");
|
|
577
|
+
stderr(" right-release install-app --recover [--dest /Applications] [--app-name Pulse.app] [--json]");
|
|
399
578
|
stderr(" --dev installs a Developer ID-signed, NOT notarized build (skips spctl/stapler, clears quarantine on the new bundle).");
|
|
400
579
|
return 2;
|
|
401
580
|
}
|
|
@@ -407,7 +586,8 @@ export async function main(argv = process.argv.slice(2), io = {}) {
|
|
|
407
586
|
else stdout(`install-app: ${event}${Object.keys(rest).length ? ` ${JSON.stringify(rest)}` : ""}`);
|
|
408
587
|
};
|
|
409
588
|
try {
|
|
410
|
-
await
|
|
589
|
+
if (opts.recover) await recoverApp(opts, { ...io.deps, emit });
|
|
590
|
+
else await installApp(opts, { ...io.deps, emit });
|
|
411
591
|
return 0;
|
|
412
592
|
} catch (error) {
|
|
413
593
|
return error.code === "unsupported" ? 3 : 1;
|
package/package.json
CHANGED
package/rightkit-versions.json
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"@rightkit/platform-ui": "0.2.0",
|
|
30
30
|
"@rightkit/qa": "0.4.0",
|
|
31
31
|
"@rightkit/qa-chrome": "0.1.3",
|
|
32
|
-
"@rightkit/release": "0.2.
|
|
32
|
+
"@rightkit/release": "0.2.141",
|
|
33
33
|
"@rightkit/settings-ui": "0.3.0",
|
|
34
34
|
"@rightkit/shell": "0.1.0",
|
|
35
35
|
"@rightkit/tauri": "0.2.0",
|
|
@@ -171,7 +171,8 @@
|
|
|
171
171
|
"0.2.136",
|
|
172
172
|
"0.2.137",
|
|
173
173
|
"0.2.138",
|
|
174
|
-
"0.2.139"
|
|
174
|
+
"0.2.139",
|
|
175
|
+
"0.2.140"
|
|
175
176
|
],
|
|
176
177
|
"@rightkit/qa": [
|
|
177
178
|
"0.1.0",
|
|
@@ -383,7 +384,7 @@
|
|
|
383
384
|
"@rightkit/platform-ui": "0.2.0",
|
|
384
385
|
"@rightkit/qa": "0.4.0",
|
|
385
386
|
"@rightkit/qa-chrome": "0.1.3",
|
|
386
|
-
"@rightkit/release": "0.2.
|
|
387
|
+
"@rightkit/release": "0.2.141",
|
|
387
388
|
"@rightkit/settings-ui": "0.3.0",
|
|
388
389
|
"@rightkit/shell": "0.1.0",
|
|
389
390
|
"@rightkit/tauri": "0.2.0",
|