@rightkit/release 0.2.135 → 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 +47 -10
- package/background-process.mjs +18 -0
- package/build-release.mjs +1 -1
- 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-environment.mjs +26 -2
- package/codesign-shim.mjs +2 -1
- package/committed-source.mjs +3 -3
- package/development.mjs +7 -7
- 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-signing-session.mjs +3 -0
- 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/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 +2 -2
- 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/rightkit-drift.mjs +3 -2
- package/rightkit-ownership.json +2028 -139
- package/rightkit-signing-agent.mjs +5 -3
- package/rightkit-sync.mjs +6 -2
- package/rightkit-versions.json +213 -103
- package/runtime-artifact-manifest.mjs +20 -6
- package/sign-updater.mjs +5 -0
- package/signing-agent-client.mjs +2 -1
- package/signing-agent-install.mjs +3 -2
- 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/github-release.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import {
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import { readVerifiedArtifact, assertArtifactUnchanged, digestBytes, verifyArtifactFile } from "./publication-artifacts.mjs";
|
|
5
7
|
import { spawnSync } from "./background-process.mjs";
|
|
6
8
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
9
|
import { verifySealedRelease } from "./release-state.mjs";
|
|
@@ -26,11 +28,20 @@ export function prepareGitHubRelease({ repoRoot, releaseId, platform, repo, stat
|
|
|
26
28
|
mkdirSync(releaseState, { recursive: true });
|
|
27
29
|
const manifestAsset = path.join(releaseState, `${sealed.manifest.app}-${sealed.manifest.version}-${platform}-release-manifest.json`);
|
|
28
30
|
const checksumsAsset = path.join(releaseState, `${sealed.manifest.app}-${sealed.manifest.version}-${platform}-SHA256SUMS.txt`);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
const manifestBytes = Buffer.from(`${JSON.stringify(sealed.manifest, null, 2)}\n`);
|
|
32
|
+
const checksumsBytes = Buffer.from(`${sealed.manifest.files.map((file) => `${file.sha256} ${file.name}`).join("\n")}\n`);
|
|
33
|
+
writeFileSync(manifestAsset, manifestBytes);
|
|
34
|
+
writeFileSync(checksumsAsset, checksumsBytes);
|
|
31
35
|
const installer = path.join(sealed.sealedDir, installers[0].name);
|
|
32
36
|
const assets = [...sealed.manifest.files.map((file) => path.join(sealed.sealedDir, file.name)), manifestAsset, checksumsAsset];
|
|
37
|
+
const publicationArtifacts = new Map(sealed.manifest.files.map((entry) => {
|
|
38
|
+
const file = path.join(sealed.sealedDir, entry.name);
|
|
39
|
+
return [file, verifyArtifactFile(file, entry)];
|
|
40
|
+
}));
|
|
41
|
+
publicationArtifacts.set(manifestAsset, readVerifiedArtifact(manifestAsset, { sha256: digestBytes(manifestBytes) }));
|
|
42
|
+
publicationArtifacts.set(checksumsAsset, readVerifiedArtifact(checksumsAsset, { sha256: digestBytes(checksumsBytes) }));
|
|
33
43
|
return {
|
|
44
|
+
publicationArtifacts,
|
|
34
45
|
sealed,
|
|
35
46
|
installer,
|
|
36
47
|
assets,
|
|
@@ -131,8 +142,7 @@ export function prepareGitHubDirectRelease({
|
|
|
131
142
|
signaturePlatform: effectiveSignaturePlatform,
|
|
132
143
|
};
|
|
133
144
|
Object.defineProperty(plan, "signatureCommandRunner", { value: effectiveSignatureRunner, enumerable: false });
|
|
134
|
-
validateGitHubDirectReleasePlan(plan, { commandRunner: effectiveSignatureRunner, platform: effectiveSignaturePlatform });
|
|
135
|
-
return plan;
|
|
145
|
+
return validateGitHubDirectReleasePlan(plan, { commandRunner: effectiveSignatureRunner, platform: effectiveSignaturePlatform });
|
|
136
146
|
}
|
|
137
147
|
|
|
138
148
|
export const prepareGitHubPortableRelease = prepareGitHubDirectRelease;
|
|
@@ -153,16 +163,30 @@ export function validateGitHubDirectReleasePlan(plan, { commandRunner = plan?.si
|
|
|
153
163
|
if (!existsSync(file) || !statSync(file).isFile()) throw new Error(`direct release payload missing: ${file}`);
|
|
154
164
|
}
|
|
155
165
|
if (statSync(signatureFile).size < 1) throw new Error("detached release manifest signature is missing");
|
|
156
|
-
const
|
|
166
|
+
const manifestArtifact = readVerifiedArtifact(manifestFile, { sha256: plan.publicationArtifacts?.get(manifestFile)?.sha256 ?? sha256(manifestFile) });
|
|
167
|
+
const signatureArtifact = readVerifiedArtifact(signatureFile, { sha256: plan.publicationArtifacts?.get(signatureFile)?.sha256 ?? sha256(signatureFile) });
|
|
168
|
+
const manifest = JSON.parse(manifestArtifact.bytes);
|
|
157
169
|
if (manifest.schemaVersion !== 1 || manifest.kind !== "rightkit-direct-release-manifest" || !STABLE_SEMVER_RE.test(manifest.version ?? "") || manifest.tag !== `v${manifest.version}`) {
|
|
158
170
|
throw new Error("invalid direct release manifest");
|
|
159
171
|
}
|
|
160
172
|
if (plan.tag !== manifest.tag) throw new Error("direct GitHub release tag does not match manifest");
|
|
161
173
|
if (manifest.signingKeyId !== RIGHTRELEASE_MANIFEST_SIGNER.id || manifest.signatureAlgorithm !== RIGHTRELEASE_MANIFEST_SIGNER.algorithm || manifest.signatureProvider !== RIGHTRELEASE_MANIFEST_SIGNER.provider || manifest.signatureProviderVersion !== RIGHTRELEASE_MANIFEST_SIGNER.providerVersion) throw new Error("direct release manifest signer contract is invalid");
|
|
162
|
-
|
|
174
|
+
// Catalog verification operates on snapshots of the exact captured authority bytes.
|
|
175
|
+
const snapshot = mkdtempSync(path.join(os.tmpdir(), "rightkit-direct-authority-"));
|
|
176
|
+
try {
|
|
177
|
+
const manifestSnapshot = path.join(snapshot, path.basename(manifestFile));
|
|
178
|
+
const signatureSnapshot = path.join(snapshot, path.basename(signatureFile));
|
|
179
|
+
writeFileSync(manifestSnapshot, manifestArtifact.bytes);
|
|
180
|
+
writeFileSync(signatureSnapshot, signatureArtifact.bytes);
|
|
181
|
+
verifyManifestCatalogSignature({ manifestPath: manifestSnapshot, signaturePath: signatureSnapshot, expectedSigner: plan.signer, commandRunner, platform });
|
|
182
|
+
readVerifiedArtifact(manifestSnapshot, manifestArtifact);
|
|
183
|
+
readVerifiedArtifact(signatureSnapshot, signatureArtifact);
|
|
184
|
+
} finally { rmSync(snapshot, { recursive: true, force: true }); }
|
|
185
|
+
assertArtifactUnchanged(manifestArtifact);
|
|
186
|
+
assertArtifactUnchanged(signatureArtifact);
|
|
163
187
|
if (!SHA256.test(manifest.checksumsSha256 ?? "")) throw new Error("direct release checksums digest is missing");
|
|
164
|
-
|
|
165
|
-
const checksums = JSON.parse(
|
|
188
|
+
const checksumsArtifact = readVerifiedArtifact(checksumsFile, { sha256: manifest.checksumsSha256 });
|
|
189
|
+
const checksums = JSON.parse(checksumsArtifact.bytes);
|
|
166
190
|
if (checksums.schemaVersion !== 1 || checksums.algorithm !== "sha256" || !checksums.assets || typeof checksums.assets !== "object" || Array.isArray(checksums.assets)) {
|
|
167
191
|
throw new Error("invalid direct release checksums");
|
|
168
192
|
}
|
|
@@ -191,14 +215,18 @@ export function validateGitHubDirectReleasePlan(plan, { commandRunner = plan?.si
|
|
|
191
215
|
addExpected({ name: asset.provenanceName, sha256: asset.provenanceSha256 }, "provenance");
|
|
192
216
|
addExpected({ name: asset.sbomName, sha256: asset.sbomSha256 }, "SBOM");
|
|
193
217
|
}
|
|
218
|
+
const publicationArtifacts = new Map([[manifestFile, manifestArtifact], [signatureFile, signatureArtifact], [checksumsFile, checksumsArtifact]]);
|
|
194
219
|
for (const [name, evidence] of expected) {
|
|
195
220
|
const file = payloadByName.get(name);
|
|
196
221
|
if (!file) throw new Error(`direct release evidence is not attached: ${name}`);
|
|
197
222
|
if (evidence.size !== undefined && (!Number.isSafeInteger(evidence.size) || evidence.size < 1 || statSync(file).size !== evidence.size)) throw new Error(`direct release archive size mismatch: ${name}`);
|
|
198
|
-
|
|
223
|
+
publicationArtifacts.set(file, verifyArtifactFile(file, { sha256: evidence.digest, sizeBytes: evidence.size }));
|
|
199
224
|
if (checksums.assets[name] !== evidence.digest) throw new Error(`direct release checksum mismatch: ${name}`);
|
|
200
225
|
}
|
|
201
|
-
|
|
226
|
+
for (const file of payloadByName.values()) if (!publicationArtifacts.has(file)) throw new Error(`direct release payload lacks signed evidence: ${path.basename(file)}`);
|
|
227
|
+
const result = { ...plan, manifest, checksums, publicationArtifacts };
|
|
228
|
+
Object.defineProperty(result, "signatureCommandRunner", { value: commandRunner, enumerable: false });
|
|
229
|
+
return result;
|
|
202
230
|
}
|
|
203
231
|
|
|
204
232
|
export function repositoryFromRemote(repoRoot) {
|
|
@@ -218,17 +246,19 @@ export function prepareGitHubAddonRelease({ repoRoot, config, configRoot = repoR
|
|
|
218
246
|
if (!existsSync(manifestPath)) throw new Error(`sealed add-on manifest not found: ${manifestPath}`);
|
|
219
247
|
const manifest = validateAddonManifest(JSON.parse(readFileSync(manifestPath, "utf8")));
|
|
220
248
|
if (manifest.addon !== config.addon || manifest.version !== config.version || manifest.platform !== platform || manifest.commit !== commit) throw new Error("sealed add-on identity mismatch");
|
|
249
|
+
const publicationArtifacts = new Map();
|
|
221
250
|
const assets = manifest.files.map((file) => {
|
|
222
251
|
const asset = path.join(sealedDir, file.name);
|
|
223
|
-
|
|
252
|
+
publicationArtifacts.set(asset, verifyArtifactFile(asset, file));
|
|
224
253
|
return asset;
|
|
225
254
|
});
|
|
226
255
|
const digest = addonManifestSha256(manifest);
|
|
256
|
+
publicationArtifacts.set(manifestPath, readVerifiedArtifact(manifestPath, { sha256: digest }));
|
|
227
257
|
const tag = `addon-${manifest.addon}-v${manifest.version}-${platform}-sha256-${digest}`;
|
|
228
258
|
const stateDir = path.join(repoRoot, ".right-release", "state", "addons", manifest.addon, manifest.version, commit.slice(0, 8), platform, "github");
|
|
229
259
|
mkdirSync(stateDir, { recursive: true });
|
|
230
260
|
return {
|
|
231
|
-
kind: "addon", manifest, manifestPath, sealedDir, assets: [...assets, manifestPath], tag,
|
|
261
|
+
publicationArtifacts, kind: "addon", manifest, manifestPath, sealedDir, assets: [...assets, manifestPath], tag,
|
|
232
262
|
title: `${manifest.addon} ${manifest.version} ${platform} add-on`,
|
|
233
263
|
notes: [`Signed ${platform === "mac" ? "macOS" : "Windows"} add-on component set.`, "", `Build commit: \`${manifest.commit}\``, `Manifest SHA-256: \`${digest}\``].join("\n"),
|
|
234
264
|
notesFile: path.join(stateDir, "release-notes.md"),
|
|
@@ -236,8 +266,13 @@ export function prepareGitHubAddonRelease({ repoRoot, config, configRoot = repoR
|
|
|
236
266
|
};
|
|
237
267
|
}
|
|
238
268
|
|
|
239
|
-
export function publishGitHubRelease(plan, { repo, dryRun = false, run = runCommand, signatureCommandRunner = plan?.signatureCommandRunner ?? plan?.cmsCommandRunner, signaturePlatform = plan?.signaturePlatform ?? plan?.cmsPlatform ?? process.platform } = {}) {
|
|
240
|
-
if (plan.kind === "direct-release") validateGitHubDirectReleasePlan(plan, { commandRunner: signatureCommandRunner ?? spawnSync, platform: signaturePlatform });
|
|
269
|
+
export function publishGitHubRelease(plan, { repo, dryRun = false, run = runCommand, signatureCommandRunner = plan?.signatureCommandRunner ?? plan?.cmsCommandRunner, signaturePlatform = plan?.signaturePlatform ?? plan?.cmsPlatform ?? process.platform, trustCommandRunner = spawnSync } = {}) {
|
|
270
|
+
if (plan.kind === "direct-release") plan = validateGitHubDirectReleasePlan(plan, { commandRunner: signatureCommandRunner ?? spawnSync, platform: signaturePlatform });
|
|
271
|
+
if (!(plan.publicationArtifacts instanceof Map) || plan.assets.some((file) => !plan.publicationArtifacts.has(file))) throw new Error("GitHub publication requires sealed or signed hash evidence for every asset");
|
|
272
|
+
for (const artifact of plan.publicationArtifacts.values()) {
|
|
273
|
+
if (artifact.bytes) assertArtifactUnchanged(artifact);
|
|
274
|
+
else verifyArtifactFile(artifact.file, artifact);
|
|
275
|
+
}
|
|
241
276
|
const visibility = JSON.parse(run("gh", ["repo", "view", repo, "--json", "visibility"]));
|
|
242
277
|
if (visibility.visibility !== "PUBLIC" && !(plan.kind !== "addon" && visibility.visibility === "PRIVATE")) {
|
|
243
278
|
throw new Error(`GitHub releases require a public or private repository: ${repo}`);
|
|
@@ -246,16 +281,16 @@ export function publishGitHubRelease(plan, { repo, dryRun = false, run = runComm
|
|
|
246
281
|
// Direct payloads are already sealed by the signed manifest contract. Do
|
|
247
282
|
// not route them through desktop-installer trust checks.
|
|
248
283
|
} else if (!dryRun) {
|
|
249
|
-
if (plan.kind === "addon") verifyAddonTrust(plan);
|
|
250
|
-
else verifyPlatformTrust(plan);
|
|
284
|
+
if (plan.kind === "addon") verifyAddonTrust(plan, trustCommandRunner);
|
|
285
|
+
else verifyPlatformTrust(plan, trustCommandRunner);
|
|
251
286
|
}
|
|
252
287
|
const existing = run("gh", ["release", "view", plan.tag, "--repo", repo, "--json", "tagName"], { allowFailure: true });
|
|
253
288
|
if (dryRun) return { status: existing.ok ? plan.kind === "addon" ? "would-verify" : "would-update" : "would-create", tag: plan.tag, assets: plan.assets };
|
|
254
289
|
if (existing.ok) {
|
|
255
|
-
const missing = plan.assets.filter((asset) => inspectRemoteAsset(plan.tag, repo, asset, run) === "missing");
|
|
290
|
+
const missing = plan.assets.filter((asset) => inspectRemoteAsset(plan.tag, repo, asset, run, plan.publicationArtifacts.get(asset)) === "missing");
|
|
256
291
|
if (missing.length === 0) return { status: "already-verified", tag: plan.tag, assets: plan.assets, manifestUrl: plan.manifestUrl };
|
|
257
292
|
run("gh", ["release", "upload", plan.tag, ...missing, "--repo", repo]);
|
|
258
|
-
for (const asset of plan.assets) verifyRemoteAsset(plan.tag, repo, asset, run);
|
|
293
|
+
for (const asset of plan.assets) verifyRemoteAsset(plan.tag, repo, asset, run, plan.publicationArtifacts.get(asset));
|
|
259
294
|
return { status: "resumed-verified", tag: plan.tag, assets: plan.assets, manifestUrl: plan.manifestUrl };
|
|
260
295
|
}
|
|
261
296
|
if (!existing.ok) {
|
|
@@ -265,35 +300,35 @@ export function publishGitHubRelease(plan, { repo, dryRun = false, run = runComm
|
|
|
265
300
|
rmSync(notesFile, { force: true });
|
|
266
301
|
}
|
|
267
302
|
run("gh", ["release", "upload", plan.tag, ...plan.assets, "--repo", repo]);
|
|
268
|
-
for (const asset of plan.assets) verifyRemoteAsset(plan.tag, repo, asset, run);
|
|
303
|
+
for (const asset of plan.assets) verifyRemoteAsset(plan.tag, repo, asset, run, plan.publicationArtifacts.get(asset));
|
|
269
304
|
return { status: "verified", tag: plan.tag, assets: plan.assets, manifestUrl: plan.manifestUrl };
|
|
270
305
|
}
|
|
271
306
|
|
|
272
|
-
function verifyAddonTrust(plan) {
|
|
307
|
+
function verifyAddonTrust(plan, commandRunner) {
|
|
273
308
|
for (const file of plan.manifest.files.filter((entry) => entry.executable)) {
|
|
274
309
|
const executable = path.join(plan.sealedDir, file.name);
|
|
275
310
|
if (plan.manifest.platform === "mac") {
|
|
276
|
-
const output = runCombined("codesign", ["-dv", "--verbose=4", executable]);
|
|
311
|
+
const output = runCombined("codesign", ["-dv", "--verbose=4", executable], commandRunner);
|
|
277
312
|
if (!/TeamIdentifier=6KLGD3LLKF/.test(output)) throw new Error(`codesign team mismatch: ${file.name}`);
|
|
278
|
-
|
|
279
|
-
} else
|
|
313
|
+
runCombined("codesign", ["--verify", "--strict", "--verbose=2", executable], commandRunner);
|
|
314
|
+
} else runCombined(process.execPath, [path.join(path.dirname(fileURLToPath(import.meta.url)), "sign-windows.mjs"), "--verify-only", executable], commandRunner);
|
|
280
315
|
}
|
|
281
316
|
}
|
|
282
317
|
|
|
283
|
-
function verifyPlatformTrust(plan) {
|
|
318
|
+
export function verifyPlatformTrust(plan, commandRunner = spawnSync) {
|
|
284
319
|
if (plan.sealed.manifest.platform === "win") {
|
|
285
|
-
|
|
320
|
+
runCombined(process.execPath, [path.join(path.dirname(fileURLToPath(import.meta.url)), "sign-windows.mjs"), "--verify-only", plan.installer], commandRunner);
|
|
286
321
|
return;
|
|
287
322
|
}
|
|
288
|
-
|
|
289
|
-
|
|
323
|
+
runCombined("spctl", ["--assess", "--type", "open", "--context", "context:primary-signature", "--verbose=2", plan.installer], commandRunner);
|
|
324
|
+
runCombined("xcrun", ["stapler", "validate", plan.installer], commandRunner);
|
|
290
325
|
}
|
|
291
326
|
|
|
292
|
-
function verifyRemoteAsset(tag, repo, asset, run) {
|
|
293
|
-
if (inspectRemoteAsset(tag, repo, asset, run) !== "match") throw new Error(`GitHub release asset missing
|
|
327
|
+
function verifyRemoteAsset(tag, repo, asset, run, evidence) {
|
|
328
|
+
if (inspectRemoteAsset(tag, repo, asset, run, evidence) !== "match") throw new Error(`GitHub release asset missing; inspect remote asset ${repo}/${tag}/${path.basename(asset)}`);
|
|
294
329
|
}
|
|
295
330
|
|
|
296
|
-
function inspectRemoteAsset(tag, repo, asset, run) {
|
|
331
|
+
function inspectRemoteAsset(tag, repo, asset, run, evidence) {
|
|
297
332
|
const verifyDir = `${path.dirname(asset)}/verify-${path.basename(asset)}`;
|
|
298
333
|
rmSync(verifyDir, { recursive: true, force: true });
|
|
299
334
|
mkdirSync(verifyDir, { recursive: true });
|
|
@@ -301,11 +336,12 @@ function inspectRemoteAsset(tag, repo, asset, run) {
|
|
|
301
336
|
if (!result.ok) {
|
|
302
337
|
rmSync(verifyDir, { recursive: true, force: true });
|
|
303
338
|
if (/no assets match|not found/i.test(`${result.output ?? ""}\n${result.error ?? ""}`)) return "missing";
|
|
304
|
-
throw new Error(`cannot inspect
|
|
339
|
+
throw new Error(`cannot inspect remote asset ${repo}/${tag}/${path.basename(asset)}: ${result.error || result.output}`);
|
|
305
340
|
}
|
|
306
341
|
const downloaded = path.join(verifyDir, path.basename(asset));
|
|
307
|
-
|
|
308
|
-
|
|
342
|
+
try { verifyArtifactFile(downloaded, evidence); }
|
|
343
|
+
catch { throw new Error(`GitHub release asset hash mismatch; remove remote asset ${repo}/${tag}/${path.basename(asset)}`); }
|
|
344
|
+
finally { rmSync(verifyDir, { recursive: true, force: true }); }
|
|
309
345
|
return "match";
|
|
310
346
|
}
|
|
311
347
|
|
|
@@ -323,8 +359,8 @@ function runCommand(command, args, { allowFailure = false, cwd } = {}) {
|
|
|
323
359
|
return allowFailure ? { ok: true, output } : output;
|
|
324
360
|
}
|
|
325
361
|
|
|
326
|
-
function runCombined(command, args) {
|
|
327
|
-
const result =
|
|
362
|
+
function runCombined(command, args, commandRunner = spawnSync) {
|
|
363
|
+
const result = commandRunner(command, args, { encoding: "utf8", windowsHide: true });
|
|
328
364
|
if (result.status !== 0) throw new Error(`${command} ${args.join(" ")} failed: ${result.stderr?.trim() || `exit ${result.status}`}`);
|
|
329
365
|
return `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
330
366
|
}
|
|
@@ -334,7 +370,8 @@ function usage(code) {
|
|
|
334
370
|
process.exit(code);
|
|
335
371
|
}
|
|
336
372
|
|
|
337
|
-
|
|
373
|
+
const realOrSelf = (file) => { try { return realpathSync(file); } catch { return path.resolve(file); } };
|
|
374
|
+
if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
|
|
338
375
|
const args = process.argv.slice(2);
|
|
339
376
|
const options = { platform: process.platform === "win32" ? "win" : "mac", releaseId: "", addon: "", repo: "", dryRun: false };
|
|
340
377
|
for (let index = 0; index < args.length; index += 1) {
|
package/heavy-command.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, realpathSync } from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { spawn, spawnSync } from "./background-process.mjs";
|
|
@@ -355,7 +355,8 @@ function spawnAttached(command, args, env, slot) {
|
|
|
355
355
|
});
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
-
|
|
358
|
+
const realOrSelf = (file) => { try { return realpathSync(file); } catch { return path.resolve(file); } };
|
|
359
|
+
if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
|
|
359
360
|
const args = process.argv.slice(2);
|
|
360
361
|
const task = args[0] === "--watch-owner-base64"
|
|
361
362
|
? watchOwnedProcessTree(JSON.parse(Buffer.from(args[1] || "", "base64url").toString("utf8"))).then(() => 0)
|
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
|
+
}
|