codex-copilot-dx 0.6.20 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1581 +0,0 @@
1
- import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { randomUUID } from "node:crypto";
5
- import { spawnSync } from "node:child_process";
6
- import {
7
- AUTH_PROFILE_CLAUDE,
8
- readAuthProfileCredentials,
9
- } from "./auth-profile.mjs";
10
- import {
11
- PM_STUDIO_2_9_7_RECIPE,
12
- PM_STUDIO_2_9_10_RECIPE,
13
- PM_STUDIO_RECIPES,
14
- createCompatiblePmStudioRecipe,
15
- createCompatiblePmStudioRecipeFromFile,
16
- inspectAsarFile,
17
- inspectElectronAsarIntegritySlots,
18
- inspectElectronAsarIntegrityFile,
19
- inspectAsarBuffer,
20
- patchAsarBuffer,
21
- sha256Hex,
22
- } from "./pm-studio-asar.mjs";
23
- import { inspectBundleContent } from "./pm-studio-bundle-content.mjs";
24
- import { withPmStudioSetupLock } from "./pm-studio-setup-lock.mjs";
25
-
26
- export { inspectBundleContent } from "./pm-studio-bundle-content.mjs";
27
-
28
- export const DEFAULT_PM_STUDIO_APP_PATH = "/Applications/PM Studio.app";
29
- export const PM_STUDIO_CLAUDE_AUTH_COMMAND = "ccdx auth login claude --reauth --github-login <personal-login>";
30
- const MIN_FREE_MARGIN_BYTES = 16 * 1024 * 1024;
31
-
32
- function setupError(code, message, details) {
33
- const error = new Error(message);
34
- error.code = code;
35
- if (details !== undefined) error.details = details;
36
- return error;
37
- }
38
-
39
- function defaultProcessRunner(command, args) {
40
- return spawnSync(command, args, {
41
- encoding: "utf8",
42
- windowsHide: true,
43
- maxBuffer: 32 * 1024 * 1024,
44
- });
45
- }
46
-
47
- function normalizedProcessResult(result) {
48
- if (typeof result === "string") return { status: 0, stdout: result, stderr: "" };
49
- return {
50
- status: result?.status ?? (result?.error ? 1 : 0),
51
- stdout: String(result?.stdout || ""),
52
- stderr: String(result?.stderr || ""),
53
- error: result?.error,
54
- };
55
- }
56
-
57
- function runChecked(processRunner, command, args, label) {
58
- const result = normalizedProcessResult(processRunner(command, args));
59
- if (result.error || result.status !== 0) {
60
- throw setupError("PM_STUDIO_PROCESS_FAILED", `${label} failed`);
61
- }
62
- return result;
63
- }
64
-
65
- function defaultInspectBundleContent({ appPath, recipe, processRunner, readBuffer }) {
66
- const xattrs = runChecked(processRunner, "/usr/bin/xattr", ["-r", "-s", "-x", "-l", appPath],
67
- "Reading PM Studio extended attributes");
68
- return inspectBundleContent(appPath, {
69
- xattrOutput: xattrs.stdout,
70
- ignoredXattrs: recipe.sourceBundleContent?.ignoredXattrs || [],
71
- readBuffer,
72
- });
73
- }
74
-
75
- function plistRead(processRunner, plistPath, key) {
76
- return runChecked(processRunner, "/usr/bin/plutil", ["-extract", key, "raw", "-o", "-", plistPath],
77
- `Reading ${path.basename(plistPath)}`).stdout.trim();
78
- }
79
-
80
- function defaultReadBundleMetadata({ infoPlistPath, processRunner }) {
81
- return {
82
- version: plistRead(processRunner, infoPlistPath, "CFBundleShortVersionString"),
83
- build: plistRead(processRunner, infoPlistPath, "CFBundleVersion"),
84
- bundleIdentifier: plistRead(processRunner, infoPlistPath, "CFBundleIdentifier"),
85
- };
86
- }
87
-
88
- function defaultReadAsarIntegrity({ infoPlistPath, recipe, processRunner }) {
89
- const key = recipe.integrityKey.split(".")[0];
90
- const result = runChecked(processRunner, "/usr/bin/plutil", [
91
- "-extract", key, "json", "-o", "-", infoPlistPath,
92
- ], "Reading ElectronAsarIntegrity");
93
- const dictionary = JSON.parse(result.stdout);
94
- return dictionary["Resources/app.asar"] || null;
95
- }
96
-
97
- function defaultWriteAsarIntegrity({ infoPlistPath, recipe, hash, processRunner }) {
98
- const key = recipe.integrityKey.split(".")[0];
99
- const result = runChecked(processRunner, "/usr/bin/plutil", [
100
- "-extract", key, "json", "-o", "-", infoPlistPath,
101
- ], "Reading ElectronAsarIntegrity");
102
- const dictionary = JSON.parse(result.stdout);
103
- dictionary["Resources/app.asar"] = { algorithm: "SHA256", hash };
104
- runChecked(processRunner, "/usr/bin/plutil", [
105
- "-replace", key, "-json", JSON.stringify(dictionary), infoPlistPath,
106
- ], "Updating ElectronAsarIntegrity hash");
107
- }
108
-
109
- function defaultInspectCodeSign({ appPath, processRunner }) {
110
- const verify = normalizedProcessResult(processRunner("/usr/bin/codesign", [
111
- "--verify", "--deep", "--strict", "--verbose=2", appPath,
112
- ]));
113
- const display = normalizedProcessResult(processRunner("/usr/bin/codesign", [
114
- "--display", "--verbose=4", appPath,
115
- ]));
116
- const details = `${display.stdout}\n${display.stderr}`;
117
- const entitlementsResult = normalizedProcessResult(processRunner("/usr/bin/codesign", [
118
- "--display", "--entitlements", "-", "--xml", appPath,
119
- ]));
120
- const entitlementOutput = `${entitlementsResult.stdout}\n${entitlementsResult.stderr}`;
121
- const plistStart = entitlementOutput.indexOf("<?xml");
122
- const plistEnd = entitlementOutput.lastIndexOf("</plist>");
123
- const hasEntitlements = plistStart >= 0 && plistEnd >= plistStart;
124
- const entitlements = hasEntitlements
125
- ? entitlementOutput.slice(plistStart, plistEnd + "</plist>".length)
126
- : "";
127
- return {
128
- valid: !verify.error && verify.status === 0
129
- && !display.error && display.status === 0,
130
- verifyValid: !verify.error && verify.status === 0,
131
- displayValid: !display.error && display.status === 0,
132
- entitlementsState: hasEntitlements
133
- ? "xml"
134
- : /invalid entitlements blob/i.test(entitlementOutput) ? "invalid" : "unavailable",
135
- adHoc: /(?:^|\n)Signature=adhoc(?:\n|$)/i.test(details)
136
- || /(?:^|\n)TeamIdentifier=not set(?:\n|$)/i.test(details),
137
- identifier: details.match(/(?:^|\n)Identifier=([^\n]+)/)?.[1]?.trim() || "",
138
- teamIdentifier: details.match(/(?:^|\n)TeamIdentifier=([^\n]+)/)?.[1]?.trim() || "",
139
- flags: details.match(/\bflags=([^\s]+)/)?.[1] || "",
140
- runtimeVersion: details.match(/(?:^|\n)Runtime Version=([^\n]+)/)?.[1]?.trim() || "",
141
- cdHashFull: details.match(/(?:^|\n)CandidateCDHashFull sha256=([0-9a-f]+)/i)?.[1]?.toLowerCase() || "",
142
- notarizationTicket: details.match(/(?:^|\n)Notarization Ticket=([^\n]+)/)?.[1]?.trim() || "",
143
- entitlementsSha256: hasEntitlements ? sha256Hex(entitlements) : "",
144
- };
145
- }
146
-
147
- function defaultInspectExecutableIntegrity({ appPath, infoPlistPath, recipe, processRunner }) {
148
- const executableName = plistRead(processRunner, infoPlistPath, "CFBundleExecutable");
149
- if (!executableName || path.basename(executableName) !== executableName) {
150
- throw setupError("PM_STUDIO_EXECUTABLE_INVALID", "PM Studio CFBundleExecutable is invalid");
151
- }
152
- const executablePath = path.join(appPath, "Contents", "MacOS", executableName);
153
- const frameworkPath = path.join(appPath, recipe.electronFrameworkPath);
154
- const executableBytes = fs.readFileSync(executablePath);
155
- const frameworkBytes = fs.readFileSync(frameworkPath);
156
- const executableSlots = inspectElectronAsarIntegritySlots(executableBytes);
157
- const frameworkSlots = inspectElectronAsarIntegritySlots(frameworkBytes);
158
- return {
159
- executableName,
160
- executablePath,
161
- executableSha256: sha256Hex(executableBytes),
162
- frameworkPath,
163
- frameworkSha256: sha256Hex(frameworkBytes),
164
- embeddedIntegrity: {
165
- state: executableSlots.slots.length === 0 && frameworkSlots.slots.length === 0 ? "absent" : "present",
166
- supported: executableSlots.slots.length === 0 && frameworkSlots.slots.length === 0,
167
- executable: executableSlots,
168
- framework: frameworkSlots,
169
- },
170
- matchesRecipeName: executableName === recipe.executable,
171
- };
172
- }
173
-
174
- function statusInspectExecutableIntegrity({ appPath, infoPlistPath, recipe, processRunner, evidenceCache }) {
175
- const executableName = plistRead(processRunner, infoPlistPath, "CFBundleExecutable");
176
- if (!executableName || path.basename(executableName) !== executableName) {
177
- throw setupError("PM_STUDIO_EXECUTABLE_INVALID", "PM Studio CFBundleExecutable is invalid");
178
- }
179
- const executablePath = path.join(appPath, "Contents", "MacOS", executableName);
180
- const frameworkPath = path.join(appPath, recipe.electronFrameworkPath);
181
- const executable = inspectElectronAsarIntegrityFile(executablePath, { cache: evidenceCache });
182
- const framework = inspectElectronAsarIntegrityFile(frameworkPath, { cache: evidenceCache });
183
- return {
184
- executableName,
185
- executablePath,
186
- executableSha256: executable.sha256,
187
- frameworkPath,
188
- frameworkSha256: framework.sha256,
189
- embeddedIntegrity: {
190
- state: executable.integrity.slots.length === 0 && framework.integrity.slots.length === 0 ? "absent" : "present",
191
- supported: executable.integrity.slots.length === 0 && framework.integrity.slots.length === 0,
192
- executable: executable.integrity,
193
- framework: framework.integrity,
194
- },
195
- matchesRecipeName: executableName === recipe.executable,
196
- };
197
- }
198
-
199
- function createPmStudioFileInspectionOperationOverrides() {
200
- const evidenceCache = new Map();
201
- const bundleReadBuffer = Buffer.allocUnsafe(1024 * 1024);
202
- return {
203
- inspectExecutableIntegrity: (args) => statusInspectExecutableIntegrity({
204
- ...args,
205
- evidenceCache,
206
- }),
207
- inspectAsarFile: ({ asarPath, recipe }) => inspectAsarFile(asarPath, recipe, {
208
- cache: evidenceCache,
209
- releaseTargets: true,
210
- }),
211
- createCompatibleRecipeFromFile: ({ asarPath, options }) => createCompatiblePmStudioRecipeFromFile(
212
- asarPath,
213
- options,
214
- { cache: evidenceCache, releaseTargets: true },
215
- ),
216
- inspectBundleContent: (args) => defaultInspectBundleContent({
217
- ...args,
218
- readBuffer: bundleReadBuffer,
219
- }),
220
- };
221
- }
222
-
223
- export function createPmStudioStatusOperationOverrides() {
224
- return createPmStudioFileInspectionOperationOverrides();
225
- }
226
-
227
- function defaultSignApp({ appPath, source, processRunner }) {
228
- const preserve = [];
229
- if (source?.codeSign?.displayValid === true) {
230
- if (source.codeSign.entitlementsState === "xml") preserve.push("entitlements");
231
- if (source.codeSign.flags) preserve.push("flags");
232
- if (source.codeSign.runtimeVersion) preserve.push("runtime");
233
- }
234
- const args = [
235
- "--force", "--deep", "--sign", "-", "--timestamp=none",
236
- ];
237
- if (preserve.length > 0) args.push(`--preserve-metadata=${preserve.join(",")}`);
238
- args.push(appPath);
239
- runChecked(processRunner, "/usr/bin/codesign", args, "Ad-hoc codesigning PM Studio");
240
- }
241
-
242
- function defaultListBlockingProcesses({ processRunner }) {
243
- const output = runChecked(processRunner, "/bin/ps", ["-axo", "pid=,command="],
244
- "Checking PM Studio processes").stdout;
245
- return output.split(/\r?\n/).filter((line) => {
246
- const value = line.toLowerCase();
247
- return value.includes("/pm studio.app/")
248
- || value.includes("pm studio shipit")
249
- || value.includes("pm studio updater")
250
- || value.includes("com.pm-studio.app")
251
- || value.includes("com.electron.pm-studio.shipit");
252
- });
253
- }
254
-
255
- function defaultCopyBundle({ source, destination, processRunner }) {
256
- if (fs.existsSync(destination)) throw setupError("PM_STUDIO_COPY_TARGET_EXISTS", `Copy target already exists: ${destination}`);
257
- runChecked(processRunner, "/usr/bin/ditto", ["--rsrc", "--extattr", "--acl", source, destination],
258
- "Copying PM Studio bundle");
259
- }
260
-
261
- const ATOMIC_REPLACE_JXA = `function run(argv) {
262
- ObjC.import("Foundation");
263
- const manager = $.NSFileManager.defaultManager;
264
- const oldUrl = $.NSURL.fileURLWithPath(argv[0]);
265
- const newUrl = $.NSURL.fileURLWithPath(argv[1]);
266
- const result = Ref();
267
- const error = Ref();
268
- const ok = manager.replaceItemAtURLWithItemAtURLBackupItemNameOptionsResultingItemURLError(
269
- oldUrl, newUrl, undefined, 1, result, error
270
- );
271
- if (!ok) throw new Error(ObjC.unwrap(error[0].localizedDescription));
272
- }`;
273
-
274
- function defaultReplaceApp({ appPath, stagePath, processRunner }) {
275
- runChecked(processRunner, "/usr/bin/osascript", [
276
- "-l", "JavaScript", "-e", ATOMIC_REPLACE_JXA, "--", appPath, stagePath,
277
- ], "Atomically replacing PM Studio");
278
- }
279
-
280
- function directorySize(directory) {
281
- let total = 0;
282
- const visit = (current) => {
283
- const stat = fs.lstatSync(current);
284
- total += stat.size;
285
- if (!stat.isDirectory() || stat.isSymbolicLink()) return;
286
- for (const name of fs.readdirSync(current)) visit(path.join(current, name));
287
- };
288
- visit(directory);
289
- return total;
290
- }
291
-
292
- function nearestExistingPath(targetPath) {
293
- let current = path.resolve(targetPath);
294
- while (!fs.existsSync(current)) {
295
- const parent = path.dirname(current);
296
- if (parent === current) throw setupError("PM_STUDIO_PATH_UNAVAILABLE", `No existing parent for ${targetPath}`);
297
- current = parent;
298
- }
299
- return current;
300
- }
301
-
302
- function defaultAvailableBytes(targetPath) {
303
- const stat = fs.statfsSync(nearestExistingPath(targetPath), { bigint: true });
304
- return Number(stat.bavail * stat.bsize);
305
- }
306
-
307
- function defaultVolumeId(targetPath) {
308
- return String(fs.statSync(nearestExistingPath(targetPath)).dev);
309
- }
310
-
311
- function defaultAssertPermissions({ appPath, backupRoot }) {
312
- try {
313
- fs.accessSync(appPath, fs.constants.R_OK);
314
- fs.accessSync(path.dirname(appPath), fs.constants.W_OK);
315
- fs.accessSync(nearestExistingPath(backupRoot), fs.constants.W_OK);
316
- } catch {
317
- throw setupError("PM_STUDIO_PERMISSION_DENIED",
318
- "PM Studio or its backup/staging location is not accessible to the current user; setup will not use sudo");
319
- }
320
- }
321
-
322
- function defaultAssertRestorePermissions({ appPath, backupAppPath }) {
323
- try {
324
- fs.accessSync(appPath, fs.constants.R_OK | fs.constants.X_OK);
325
- fs.accessSync(path.dirname(appPath), fs.constants.W_OK | fs.constants.X_OK);
326
- fs.accessSync(backupAppPath, fs.constants.R_OK | fs.constants.X_OK);
327
- } catch {
328
- throw setupError("PM_STUDIO_PERMISSION_DENIED",
329
- "PM Studio or its verified backup is not accessible to the current user; restore will not use sudo");
330
- }
331
- }
332
-
333
- export function createPmStudioSetupOperations(overrides = {}) {
334
- const processRunner = overrides.processRunner || defaultProcessRunner;
335
- return {
336
- processRunner,
337
- readBundleMetadata: defaultReadBundleMetadata,
338
- readAsarIntegrity: defaultReadAsarIntegrity,
339
- writeAsarIntegrity: defaultWriteAsarIntegrity,
340
- inspectCodeSign: defaultInspectCodeSign,
341
- inspectBundleContent: defaultInspectBundleContent,
342
- inspectExecutableIntegrity: defaultInspectExecutableIntegrity,
343
- withSetupLock: ({ appPath }, fn) => withPmStudioSetupLock({ appPath }, fn),
344
- signApp: defaultSignApp,
345
- listBlockingProcesses: defaultListBlockingProcesses,
346
- copyBundle: defaultCopyBundle,
347
- renamePath: ({ source, destination }) => fs.renameSync(source, destination),
348
- replaceApp: defaultReplaceApp,
349
- removePath: ({ target }) => fs.rmSync(target, { recursive: true, force: true }),
350
- bundleSize: directorySize,
351
- availableBytes: defaultAvailableBytes,
352
- volumeId: defaultVolumeId,
353
- assertPermissions: defaultAssertPermissions,
354
- assertRestorePermissions: defaultAssertRestorePermissions,
355
- readClaudeCredentials: ({ home }) => readAuthProfileCredentials(AUTH_PROFILE_CLAUDE, { home }),
356
- now: () => new Date(),
357
- uuid: () => randomUUID(),
358
- ...overrides,
359
- processRunner,
360
- };
361
- }
362
-
363
- function appPaths(appPath, recipe) {
364
- return {
365
- appPath,
366
- infoPlistPath: path.join(appPath, recipe.infoPlistPath),
367
- asarPath: path.join(appPath, recipe.asarPath),
368
- };
369
- }
370
-
371
- function integrityMatches(actual, algorithm, hash) {
372
- return actual?.algorithm === algorithm && actual?.hash === hash;
373
- }
374
-
375
- function bundleContentMatches(actual, expected) {
376
- return actual?.scheme === expected?.scheme
377
- && actual?.sha256 === expected?.sha256
378
- && actual?.entryCount === expected?.entryCount
379
- && actual?.regularFileCount === expected?.regularFileCount
380
- && actual?.regularBytes === expected?.regularBytes
381
- && actual?.symlinkCount === expected?.symlinkCount
382
- && actual?.xattrCount === expected?.xattrCount;
383
- }
384
-
385
- export function inspectPmStudioApp({
386
- appPath,
387
- recipe = PM_STUDIO_2_9_7_RECIPE,
388
- operations = createPmStudioSetupOperations(),
389
- } = {}) {
390
- const paths = appPaths(appPath, recipe);
391
- const issues = [];
392
- const metadata = operations.readBundleMetadata({ ...paths, recipe, processRunner: operations.processRunner });
393
- const asar = operations.inspectAsarFile
394
- ? operations.inspectAsarFile({ ...paths, recipe, processRunner: operations.processRunner })
395
- : inspectAsarBuffer(fs.readFileSync(paths.asarPath), recipe);
396
- const plistIntegrity = operations.readAsarIntegrity({ ...paths, recipe, processRunner: operations.processRunner });
397
- const executableIntegrity = operations.inspectExecutableIntegrity({
398
- ...paths, recipe, processRunner: operations.processRunner,
399
- });
400
- const codeSign = operations.inspectCodeSign({ ...paths, recipe, processRunner: operations.processRunner });
401
- let bundleContent = null;
402
- if (recipe.sourceBundleContent) {
403
- try {
404
- bundleContent = operations.inspectBundleContent({
405
- ...paths, recipe, processRunner: operations.processRunner,
406
- });
407
- } catch (error) {
408
- issues.push(`bundle content inspection failed: ${error.message}`);
409
- }
410
- }
411
-
412
- if (String(metadata.version) !== recipe.version) {
413
- issues.push(`version is ${metadata.version}, expected ${recipe.version}`);
414
- }
415
- if (String(metadata.build) !== recipe.build) {
416
- issues.push(`build is ${metadata.build}, expected ${recipe.build}`);
417
- }
418
- if (String(metadata.bundleIdentifier) !== recipe.bundleIdentifier) {
419
- issues.push(`bundle identifier is ${metadata.bundleIdentifier}, expected ${recipe.bundleIdentifier}`);
420
- }
421
- if (!executableIntegrity?.matchesRecipeName) issues.push("CFBundleExecutable does not match the recipe");
422
- if (!executableIntegrity?.embeddedIntegrity?.supported
423
- || executableIntegrity.embeddedIntegrity.state !== recipe.embeddedAsarIntegrity) {
424
- issues.push("Electron Framework has an unsupported embedded ASAR integrity slot");
425
- }
426
-
427
- const exactSourceBundle = !recipe.sourceBundleContent
428
- || bundleContentMatches(bundleContent, recipe.sourceBundleContent);
429
- const patchedSignatureAccepted = codeSign?.valid === true
430
- && codeSign?.adHoc === true
431
- && codeSign?.identifier === recipe.bundleIdentifier
432
- && (!recipe.patchedSigningMetadata
433
- || JSON.stringify(signingMetadata(codeSign)) === JSON.stringify(recipe.patchedSigningMetadata));
434
- const predecessorSignatureAccepted = codeSign?.valid === true
435
- && codeSign?.adHoc === true
436
- && codeSign?.identifier === recipe.bundleIdentifier
437
- && Boolean(recipe.predecessor?.signingMetadata)
438
- && JSON.stringify(signingMetadata(codeSign)) === JSON.stringify(recipe.predecessor.signingMetadata);
439
- const clean = issues.length === 0
440
- && asar.state === "clean"
441
- && integrityMatches(plistIntegrity, "SHA256", recipe.sourceHeaderSha256)
442
- && executableIntegrity.executableSha256 === recipe.sourceExecutableSha256
443
- && executableIntegrity.frameworkSha256 === recipe.sourceElectronFrameworkSha256
444
- && exactSourceBundle;
445
- const patched = issues.length === 0
446
- && asar.state === "patched"
447
- && integrityMatches(plistIntegrity, "SHA256", recipe.patchedHeaderSha256)
448
- && patchedSignatureAccepted;
449
- const predecessor = issues.length === 0
450
- && asar.state === "predecessor"
451
- && integrityMatches(plistIntegrity, "SHA256", recipe.predecessor?.headerSha256)
452
- && predecessorSignatureAccepted;
453
- const legacy = issues.length === 0
454
- && asar.state === "legacy"
455
- && integrityMatches(plistIntegrity, "SHA256", recipe.legacy?.headerSha256)
456
- && patchedSignatureAccepted;
457
-
458
- if (!clean && !patched && !predecessor && !legacy) {
459
- if (asar.state === "drift") issues.push("app.asar is unknown drift");
460
- else if (asar.state === "clean"
461
- && !integrityMatches(plistIntegrity, "SHA256", recipe.sourceHeaderSha256)) {
462
- issues.push("clean ASAR does not match ElectronAsarIntegrity");
463
- } else if (asar.state === "patched"
464
- && !integrityMatches(plistIntegrity, "SHA256", recipe.patchedHeaderSha256)) {
465
- issues.push("patched ASAR does not match ElectronAsarIntegrity");
466
- } else if (asar.state === "predecessor"
467
- && !integrityMatches(plistIntegrity, "SHA256", recipe.predecessor?.headerSha256)) {
468
- issues.push("predecessor ASAR does not match ElectronAsarIntegrity");
469
- } else if (asar.state === "legacy"
470
- && !integrityMatches(plistIntegrity, "SHA256", recipe.legacy?.headerSha256)) {
471
- issues.push("legacy ASAR does not match ElectronAsarIntegrity");
472
- }
473
- if (asar.state === "clean" && recipe.sourceBundleContent && !exactSourceBundle) {
474
- issues.push("bundle content does not match the exact clean recipe");
475
- }
476
- if (asar.state !== "clean" && !codeSign?.valid) {
477
- issues.push("codesign verification failed");
478
- }
479
- else if (["patched", "predecessor", "legacy"].includes(asar.state) && !codeSign.adHoc) {
480
- issues.push(`${asar.state} bundle is not ad-hoc signed`);
481
- }
482
- else if (["patched", "predecessor", "legacy"].includes(asar.state)
483
- && codeSign.identifier !== recipe.bundleIdentifier) {
484
- issues.push(`${asar.state} bundle identifier does not match the recipe`);
485
- }
486
- if (asar.state === "clean"
487
- && executableIntegrity.executableSha256 !== recipe.sourceExecutableSha256) {
488
- issues.push("main executable hash does not match the clean recipe");
489
- }
490
- if (asar.state === "clean"
491
- && executableIntegrity.frameworkSha256 !== recipe.sourceElectronFrameworkSha256) {
492
- issues.push("Electron Framework hash does not match the clean recipe");
493
- }
494
- if (["patched", "legacy"].includes(asar.state) && codeSign?.valid && codeSign?.adHoc
495
- && recipe.patchedSigningMetadata
496
- && !patchedSignatureAccepted) {
497
- issues.push(`${asar.state} bundle signing metadata does not match the recipe`);
498
- }
499
- if (asar.state === "predecessor" && codeSign?.valid && codeSign?.adHoc
500
- && !predecessorSignatureAccepted) {
501
- issues.push("predecessor bundle signing metadata does not match the recipe");
502
- }
503
- }
504
-
505
- return {
506
- state: clean
507
- ? "clean"
508
- : patched ? "patched" : predecessor ? "predecessor" : legacy ? "legacy" : "drift",
509
- metadata: {
510
- version: String(metadata.version),
511
- build: String(metadata.build),
512
- bundleIdentifier: String(metadata.bundleIdentifier),
513
- },
514
- asar,
515
- plistIntegrity,
516
- executableIntegrity,
517
- codeSign,
518
- bundleContent,
519
- sourceVerification: clean
520
- ? recipe.sourceBundleContent ? "local-content" : "exact-recipe"
521
- : "",
522
- paths,
523
- issues,
524
- };
525
- }
526
-
527
- function assertInstalledAppPath(appPath) {
528
- const stat = fs.lstatSync(appPath);
529
- if (!stat.isDirectory() || stat.isSymbolicLink()) {
530
- throw setupError("PM_STUDIO_APP_INVALID", `PM Studio path is not a regular app bundle: ${appPath}`);
531
- }
532
- }
533
-
534
- function assertNoBlockingProcesses(operations, appPath) {
535
- const processes = operations.listBlockingProcesses({ appPath, processRunner: operations.processRunner });
536
- if (!Array.isArray(processes)) throw setupError("PM_STUDIO_PROCESS_CHECK_FAILED", "PM Studio process check returned invalid data");
537
- if (processes.length > 0) {
538
- throw setupError("PM_STUDIO_RUNNING", `PM Studio or its updater is running (${processes.length} matching process(es)); quit it and retry`);
539
- }
540
- }
541
-
542
- function assertValidClaudeProfile(operations, home) {
543
- let credentials;
544
- try {
545
- credentials = operations.readClaudeCredentials({ home });
546
- } catch {
547
- throw setupError("PM_STUDIO_CLAUDE_AUTH_INVALID",
548
- `Claude isolated authentication could not be read. Run \`${PM_STUDIO_CLAUDE_AUTH_COMMAND}\` first.`);
549
- }
550
- if (!credentials?.configured || !credentials?.valid) {
551
- const reason = credentials?.reason ? ` (${credentials.reason})` : "";
552
- throw setupError("PM_STUDIO_CLAUDE_AUTH_INVALID",
553
- `Claude isolated authentication is not valid${reason}. Run \`${PM_STUDIO_CLAUDE_AUTH_COMMAND}\` first.`);
554
- }
555
- }
556
-
557
- function selectRecipe(metadata, recipes) {
558
- return recipes.find((recipe) => recipe.version === String(metadata.version)
559
- && recipe.build === String(metadata.build)
560
- && recipe.bundleIdentifier === String(metadata.bundleIdentifier));
561
- }
562
-
563
- function compatibleStaticOptions(metadata, executable) {
564
- return {
565
- version: String(metadata.version),
566
- build: String(metadata.build),
567
- bundleIdentifier: String(metadata.bundleIdentifier),
568
- sourceTeamIdentifier: "local-app",
569
- executable,
570
- electronFrameworkPath: PM_STUDIO_2_9_10_RECIPE.electronFrameworkPath,
571
- embeddedAsarIntegrity: PM_STUDIO_2_9_10_RECIPE.embeddedAsarIntegrity,
572
- asarPath: PM_STUDIO_2_9_10_RECIPE.asarPath,
573
- infoPlistPath: PM_STUDIO_2_9_10_RECIPE.infoPlistPath,
574
- integrityKey: PM_STUDIO_2_9_10_RECIPE.integrityKey,
575
- };
576
- }
577
-
578
- function compatibleSourceBundleContent({ appPath, operations }) {
579
- const ignoredXattrs = PM_STUDIO_2_9_10_RECIPE.sourceBundleContent.ignoredXattrs;
580
- const inspected = operations.inspectBundleContent({
581
- ...appPaths(appPath, PM_STUDIO_2_9_10_RECIPE),
582
- recipe: PM_STUDIO_2_9_10_RECIPE,
583
- processRunner: operations.processRunner,
584
- });
585
- return {
586
- ...inspected,
587
- ignoredXattrs: [...ignoredXattrs],
588
- };
589
- }
590
-
591
- function compatibleSourceStructure({ appPath, metadata, operations }) {
592
- const paths = appPaths(appPath, PM_STUDIO_2_9_10_RECIPE);
593
- const executableIntegrity = operations.inspectExecutableIntegrity({
594
- ...paths,
595
- recipe: PM_STUDIO_2_9_10_RECIPE,
596
- processRunner: operations.processRunner,
597
- });
598
- if (!executableIntegrity?.executableName
599
- || !executableIntegrity?.embeddedIntegrity?.supported
600
- || executableIntegrity.embeddedIntegrity.state !== PM_STUDIO_2_9_10_RECIPE.embeddedAsarIntegrity) {
601
- throw setupError("PM_STUDIO_ASAR_INCOMPATIBLE",
602
- "PM Studio compatible-version discovery found an unsupported executable or embedded ASAR integrity policy");
603
- }
604
- const recipeOptions = {
605
- ...compatibleStaticOptions(metadata, executableIntegrity.executableName),
606
- sourceExecutableSha256: executableIntegrity.executableSha256,
607
- sourceElectronFrameworkSha256: executableIntegrity.frameworkSha256,
608
- };
609
- const compatibleRecipe = operations.createCompatibleRecipeFromFile
610
- ? operations.createCompatibleRecipeFromFile({ asarPath: paths.asarPath, options: recipeOptions })
611
- : createCompatiblePmStudioRecipe(fs.readFileSync(paths.asarPath), recipeOptions);
612
- const plistIntegrity = operations.readAsarIntegrity({
613
- ...paths,
614
- recipe: compatibleRecipe,
615
- processRunner: operations.processRunner,
616
- });
617
- if (!integrityMatches(plistIntegrity, "SHA256", compatibleRecipe.sourceHeaderSha256)) {
618
- throw setupError("PM_STUDIO_ASAR_INCOMPATIBLE",
619
- "PM Studio compatible-version discovery found mismatched ElectronAsarIntegrity metadata");
620
- }
621
- return { paths, executableIntegrity, compatibleRecipe };
622
- }
623
-
624
- function frozenBundleContent(value) {
625
- return Object.freeze({
626
- ...value,
627
- ignoredXattrs: Object.freeze([...(value.ignoredXattrs || [])]),
628
- });
629
- }
630
-
631
- function createCompatibleSourceRecipe({ appPath, metadata, operations }) {
632
- const { compatibleRecipe } = compatibleSourceStructure({ appPath, metadata, operations });
633
- const sourceBundleContent = compatibleSourceBundleContent({ appPath, operations });
634
- return Object.freeze({
635
- ...compatibleRecipe,
636
- sourceBundleContent: frozenBundleContent(sourceBundleContent),
637
- });
638
- }
639
-
640
- function sameIgnoredXattrs(value) {
641
- return JSON.stringify(value)
642
- === JSON.stringify(PM_STUDIO_2_9_10_RECIPE.sourceBundleContent.ignoredXattrs);
643
- }
644
-
645
-
646
- function compatibleRecipeFromManifest({ backupDir, manifest, metadata, operations }) {
647
- if (manifest?.schema_version !== 2
648
- || manifest?.kind !== "ccdx-pm-studio-backup"
649
- || !String(manifest?.recipe_id || "").startsWith("pm-studio-compatible-")
650
- || manifest?.app?.bundle_identifier !== String(metadata.bundleIdentifier)
651
- || manifest?.app?.version !== String(metadata.version)
652
- || manifest?.app?.build !== String(metadata.build)
653
- || !sameIgnoredXattrs(manifest?.source?.bundle_content?.ignoredXattrs)) {
654
- return null;
655
- }
656
- const backupAppPath = path.join(backupDir, "PM Studio.app");
657
- assertInstalledAppPath(backupAppPath);
658
- const sourceAsarPath = path.join(backupAppPath, PM_STUDIO_2_9_10_RECIPE.asarPath);
659
- const backupPaths = appPaths(backupAppPath, PM_STUDIO_2_9_10_RECIPE);
660
- const executableIntegrity = operations.inspectExecutableIntegrity({
661
- ...backupPaths,
662
- recipe: PM_STUDIO_2_9_10_RECIPE,
663
- processRunner: operations.processRunner,
664
- });
665
- const recipeOptions = {
666
- ...compatibleStaticOptions(metadata, executableIntegrity.executableName),
667
- sourceExecutableSha256: manifest?.source?.binaries?.main_executable_sha256,
668
- sourceElectronFrameworkSha256: manifest?.source?.binaries?.electron_framework_sha256,
669
- };
670
- const compatibleRecipe = operations.createCompatibleRecipeFromFile
671
- ? operations.createCompatibleRecipeFromFile({ asarPath: sourceAsarPath, options: recipeOptions })
672
- : createCompatiblePmStudioRecipe(fs.readFileSync(sourceAsarPath), recipeOptions);
673
- const recipe = Object.freeze({
674
- ...compatibleRecipe,
675
- sourceBundleContent: frozenBundleContent(manifest.source.bundle_content),
676
- });
677
- if (manifest.compatibility !== recipe.compatibility
678
- || manifest.recipe_id !== recipe.id
679
- || manifest?.source?.asar_sha256 !== recipe.sourceAsarSha256
680
- || manifest?.source?.asar_header_sha256 !== recipe.sourceHeaderSha256
681
- || manifest?.patched?.asar_sha256 !== recipe.patchedAsarSha256
682
- || manifest?.patched?.asar_header_sha256 !== recipe.patchedHeaderSha256) {
683
- throw setupError("PM_STUDIO_BACKUP_INVALID",
684
- `Compatible PM Studio backup does not match its reconstructed recipe: ${backupDir}`);
685
- }
686
- const inspection = inspectPmStudioApp({ appPath: backupAppPath, recipe, operations });
687
- if (inspection.state !== "clean" || inspection.sourceVerification !== "local-content") {
688
- throw setupError("PM_STUDIO_BACKUP_INVALID",
689
- `Compatible PM Studio backup source verification failed: ${backupDir}`,
690
- inspection.issues);
691
- }
692
- return recipe;
693
- }
694
-
695
- function compatibleRecipeFromBackup({ appPath, backupRoot, metadata, operations }) {
696
- if (!fs.existsSync(backupRoot)) return null;
697
- const installedAsarPath = path.join(appPath, PM_STUDIO_2_9_10_RECIPE.asarPath);
698
- const installedAsar = operations.inspectAsarFile ? null : fs.readFileSync(installedAsarPath);
699
- let candidates = [];
700
- const invalidCandidates = [];
701
- for (const entry of fs.readdirSync(backupRoot, { withFileTypes: true })) {
702
- if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
703
- const backupDir = path.join(backupRoot, entry.name);
704
- const manifestPath = path.join(backupDir, "manifest.json");
705
- if (!fs.existsSync(manifestPath)) continue;
706
- let manifest;
707
- try {
708
- manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
709
- } catch {
710
- continue;
711
- }
712
- let recipe;
713
- try {
714
- recipe = compatibleRecipeFromManifest({ backupDir, manifest, metadata, operations });
715
- } catch (error) {
716
- invalidCandidates.push(error);
717
- continue;
718
- }
719
- if (!recipe) continue;
720
- const asarState = operations.inspectAsarFile
721
- ? operations.inspectAsarFile({ asarPath: installedAsarPath, recipe }).state
722
- : inspectAsarBuffer(installedAsar, recipe).state;
723
- if (asarState === "drift") continue;
724
- candidates.push({ recipe, manifest, asarState });
725
- }
726
- if (candidates.length > 1) {
727
- let installedBundleContent;
728
- try {
729
- const recipe = candidates[0].recipe;
730
- installedBundleContent = operations.inspectBundleContent({
731
- ...appPaths(appPath, recipe),
732
- recipe,
733
- processRunner: operations.processRunner,
734
- });
735
- } catch (error) {
736
- throw setupError("PM_STUDIO_BACKUP_INVALID",
737
- "PM Studio compatible backup candidates could not be distinguished safely",
738
- [error.message]);
739
- }
740
- candidates = candidates.filter(({ recipe, manifest, asarState }) => bundleContentMatches(
741
- installedBundleContent,
742
- asarState === "clean" ? recipe.sourceBundleContent : manifest?.patched?.bundle_content,
743
- ));
744
- }
745
- if (candidates.length > 1) {
746
- throw setupError("PM_STUDIO_UNSUPPORTED_VERSION",
747
- `PM Studio ${metadata.version} build ${metadata.build} matches multiple compatible backups; no files were changed`);
748
- }
749
- if (candidates.length === 1) return candidates[0].recipe;
750
- if (invalidCandidates.length > 0) {
751
- throw setupError("PM_STUDIO_BACKUP_INVALID",
752
- `PM Studio ${metadata.version} build ${metadata.build} has no matching verified compatible backup`,
753
- invalidCandidates.map((error) => error?.message || String(error)));
754
- }
755
- return null;
756
- }
757
-
758
- function selectCompatibleRecipe({ appPath, backupRoot, metadata, operations }) {
759
- let sourceError = null;
760
- try {
761
- return createCompatibleSourceRecipe({ appPath, metadata, operations });
762
- } catch (error) {
763
- sourceError = error;
764
- }
765
- try {
766
- const backupRecipe = compatibleRecipeFromBackup({ appPath, backupRoot, metadata, operations });
767
- if (backupRecipe) return backupRecipe;
768
- } catch (error) {
769
- if (error?.code === "PM_STUDIO_BACKUP_INVALID") throw error;
770
- sourceError = error;
771
- }
772
- throw setupError("PM_STUDIO_UNSUPPORTED_VERSION",
773
- `PM Studio ${metadata.version} build ${metadata.build} is not structurally compatible with the safe patch; no files were changed${sourceError?.message ? `: ${sourceError.message}` : ""}`,
774
- sourceError?.message ? [sourceError.message] : undefined);
775
- }
776
-
777
- export function resolvePmStudioCompatibleRecipe({
778
- appPath = DEFAULT_PM_STUDIO_APP_PATH,
779
- home = os.homedir(),
780
- backupRoot = pmStudioBackupRoot(home),
781
- metadata,
782
- recipes = PM_STUDIO_RECIPES,
783
- operations = createPmStudioSetupOperations(),
784
- inspectApp = inspectPmStudioApp,
785
- } = {}) {
786
- if (!metadata || typeof metadata !== "object") {
787
- throw new TypeError("PM Studio bundle metadata is required");
788
- }
789
- const staticRecipe = selectRecipe(metadata, recipes);
790
- let staticInspection = null;
791
- if (staticRecipe) {
792
- try {
793
- staticInspection = inspectApp({ appPath, recipe: staticRecipe, operations });
794
- } catch {
795
- // A metadata match is not an admission gate. Local structure remains authoritative.
796
- }
797
- }
798
- try {
799
- return selectCompatibleRecipe({
800
- appPath,
801
- backupRoot,
802
- metadata,
803
- operations,
804
- });
805
- } catch (error) {
806
- if (staticInspection?.state === "drift" && error?.code === "PM_STUDIO_UNSUPPORTED_VERSION") {
807
- throw setupError("PM_STUDIO_BUNDLE_DRIFT",
808
- `PM Studio ${metadata.version} build ${metadata.build} differs from its known local structure; no files were changed`,
809
- staticInspection.issues);
810
- }
811
- const staticSourceCanBeSigned = staticInspection?.state !== "clean"
812
- || !staticRecipe?.patchedSigningMetadata
813
- || (staticInspection?.codeSign?.displayValid === true
814
- && staticInspection.codeSign.entitlementsState === "xml");
815
- if (error?.code === "PM_STUDIO_UNSUPPORTED_VERSION"
816
- && staticInspection?.state && staticInspection.state !== "drift"
817
- && staticSourceCanBeSigned) {
818
- return staticRecipe;
819
- }
820
- throw error;
821
- }
822
- }
823
-
824
- function backupName(recipe) {
825
- const sourceHash = recipe.sourceBundleContent?.sha256 || recipe.sourceAsarSha256;
826
- return `${recipe.id}-${sourceHash.slice(0, 12)}`;
827
- }
828
-
829
- export function pmStudioBackupRoot(home = os.homedir()) {
830
- return path.join(home, ".local", "share", "codex-copilot-dx", "pm-studio-backups");
831
- }
832
-
833
- export function pmStudioPatchManifestPath({
834
- home = os.homedir(),
835
- backupRoot = pmStudioBackupRoot(home),
836
- recipe,
837
- } = {}) {
838
- if (!recipe) throw new TypeError("PM Studio patch recipe is required");
839
- return path.join(backupRoot, backupName(recipe), "manifest.json");
840
- }
841
-
842
- function backupManifest(recipe, createdAt) {
843
- return {
844
- schema_version: recipe.sourceBundleContent ? 2 : 1,
845
- kind: "ccdx-pm-studio-backup",
846
- recipe_id: recipe.id,
847
- ...(recipe.compatibility ? { compatibility: recipe.compatibility } : {}),
848
- created_at: createdAt.toISOString(),
849
- app: {
850
- name: "PM Studio",
851
- bundle_identifier: recipe.bundleIdentifier,
852
- version: recipe.version,
853
- build: recipe.build,
854
- },
855
- source: {
856
- asar_sha256: recipe.sourceAsarSha256,
857
- asar_header_sha256: recipe.sourceHeaderSha256,
858
- electron_asar_integrity: { algorithm: "SHA256", hash: recipe.sourceHeaderSha256 },
859
- binaries: {
860
- main_executable_sha256: recipe.sourceExecutableSha256,
861
- electron_framework_sha256: recipe.sourceElectronFrameworkSha256,
862
- embedded_asar_integrity: recipe.embeddedAsarIntegrity,
863
- },
864
- ...(recipe.sourceBundleContent ? { bundle_content: recipe.sourceBundleContent } : {}),
865
- },
866
- patched: {
867
- asar_sha256: recipe.patchedAsarSha256,
868
- asar_header_sha256: recipe.patchedHeaderSha256,
869
- electron_asar_integrity: { algorithm: "SHA256", hash: recipe.patchedHeaderSha256 },
870
- binaries: null,
871
- ...(recipe.sourceBundleContent ? { bundle_content: null } : {}),
872
- },
873
- backup_bundle: "PM Studio.app",
874
- restore_rule: `Restore only while the installed app is PM Studio ${recipe.version} build ${recipe.build}.`,
875
- };
876
- }
877
-
878
- function patchRecordMatches(record, recipe, state) {
879
- const expected = state === "legacy"
880
- ? recipe.legacy
881
- : state === "predecessor"
882
- ? recipe.predecessor
883
- : { asarSha256: recipe.patchedAsarSha256, headerSha256: recipe.patchedHeaderSha256 };
884
- return Boolean(expected)
885
- && record?.asar_sha256 === expected.asarSha256
886
- && record?.asar_header_sha256 === expected.headerSha256
887
- && integrityMatches(record?.electron_asar_integrity, "SHA256", expected.headerSha256);
888
- }
889
-
890
- function patchRecordForState(manifest, recipe, state) {
891
- const historicalKey = state === "legacy"
892
- ? "legacy_patched"
893
- : state === "predecessor" ? "predecessor_patched" : "";
894
- if (historicalKey && patchRecordMatches(manifest?.[historicalKey], recipe, state)) {
895
- return manifest[historicalKey];
896
- }
897
- return patchRecordMatches(manifest?.patched, recipe, state) ? manifest.patched : null;
898
- }
899
-
900
- function validateBackup({ backupDir, recipe, operations, evidence }) {
901
- const manifestPath = path.join(backupDir, "manifest.json");
902
- const backupAppPath = path.join(backupDir, "PM Studio.app");
903
- let manifest;
904
- try {
905
- manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
906
- } catch {
907
- throw setupError("PM_STUDIO_BACKUP_INVALID", `Existing PM Studio backup manifest is invalid: ${manifestPath}`);
908
- }
909
- const expectedSchema = recipe.sourceBundleContent ? 2 : 1;
910
- if (manifest?.schema_version !== expectedSchema
911
- || manifest?.kind !== "ccdx-pm-studio-backup"
912
- || manifest?.recipe_id !== recipe.id
913
- || (recipe.compatibility && manifest?.compatibility !== recipe.compatibility)
914
- || manifest?.app?.bundle_identifier !== recipe.bundleIdentifier
915
- || manifest?.app?.version !== recipe.version
916
- || manifest?.app?.build !== recipe.build
917
- || manifest?.source?.asar_sha256 !== recipe.sourceAsarSha256
918
- || manifest?.source?.asar_header_sha256 !== recipe.sourceHeaderSha256
919
- || !integrityMatches(manifest?.source?.electron_asar_integrity, "SHA256", recipe.sourceHeaderSha256)
920
- || manifest?.source?.binaries?.main_executable_sha256 !== recipe.sourceExecutableSha256
921
- || manifest?.source?.binaries?.electron_framework_sha256 !== recipe.sourceElectronFrameworkSha256
922
- || manifest?.source?.binaries?.embedded_asar_integrity !== recipe.embeddedAsarIntegrity
923
- || (recipe.sourceBundleContent
924
- && JSON.stringify(manifest?.source?.bundle_content) !== JSON.stringify(recipe.sourceBundleContent))
925
- || (manifest?.predecessor_patched !== undefined
926
- && !patchRecordMatches(manifest.predecessor_patched, recipe, "predecessor"))
927
- || (!patchRecordForState(manifest, recipe, "patched")
928
- && !patchRecordForState(manifest, recipe, "legacy")
929
- && !patchRecordForState(manifest, recipe, "predecessor"))) {
930
- throw setupError("PM_STUDIO_BACKUP_INVALID", `Existing PM Studio backup does not match recipe ${recipe.id}`);
931
- }
932
- const inspection = inspectPmStudioApp({ appPath: backupAppPath, recipe, operations });
933
- if (inspection.state !== "clean") {
934
- throw setupError("PM_STUDIO_BACKUP_INVALID", "Existing PM Studio backup is not an exact clean bundle", inspection.issues);
935
- }
936
- if (evidence) {
937
- evidence.inspection = inspection;
938
- }
939
- return { backupDir, backupAppPath, manifestPath, manifest, reused: true };
940
- }
941
-
942
- function ensureBackup({ appPath, backupRoot, recipe, operations }) {
943
- const finalDir = path.join(backupRoot, backupName(recipe));
944
- if (fs.existsSync(finalDir)) return validateBackup({ backupDir: finalDir, recipe, operations });
945
-
946
- fs.mkdirSync(backupRoot, { recursive: true, mode: 0o700 });
947
- const tempDir = path.join(backupRoot, `.${backupName(recipe)}.${operations.uuid()}.tmp`);
948
- const backupAppPath = path.join(tempDir, "PM Studio.app");
949
- const manifestPath = path.join(tempDir, "manifest.json");
950
- fs.mkdirSync(tempDir, { mode: 0o700 });
951
- try {
952
- operations.copyBundle({ source: appPath, destination: backupAppPath, processRunner: operations.processRunner });
953
- fs.writeFileSync(manifestPath, `${JSON.stringify(backupManifest(recipe, operations.now()), null, 2)}\n`, { mode: 0o600 });
954
- const inspection = inspectPmStudioApp({ appPath: backupAppPath, recipe, operations });
955
- if (inspection.state !== "clean") {
956
- throw setupError("PM_STUDIO_BACKUP_VERIFY_FAILED", "New PM Studio backup failed verification", inspection.issues);
957
- }
958
- try {
959
- operations.renamePath({ source: tempDir, destination: finalDir });
960
- } catch (error) {
961
- if (!fs.existsSync(finalDir)) throw error;
962
- return validateBackup({ backupDir: finalDir, recipe, operations });
963
- }
964
- return {
965
- backupDir: finalDir,
966
- backupAppPath: path.join(finalDir, "PM Studio.app"),
967
- manifestPath: path.join(finalDir, "manifest.json"),
968
- manifest: JSON.parse(fs.readFileSync(path.join(finalDir, "manifest.json"), "utf8")),
969
- reused: false,
970
- };
971
- } finally {
972
- if (fs.existsSync(tempDir)) operations.removePath({ target: tempDir });
973
- }
974
- }
975
-
976
- function writePatchedBinaryRecord({ backup, inspection, operations, recipe }) {
977
- const manifest = JSON.parse(fs.readFileSync(backup.manifestPath, "utf8"));
978
- if (manifest.predecessor_patched !== undefined
979
- && !patchRecordMatches(manifest.predecessor_patched, recipe, "predecessor")) {
980
- throw setupError("PM_STUDIO_BACKUP_INVALID", "Existing PM Studio predecessor record does not match the patch recipe");
981
- }
982
- if (manifest.predecessor_patched === undefined
983
- && patchRecordMatches(manifest.patched, recipe, "predecessor")) {
984
- manifest.predecessor_patched = manifest.patched;
985
- }
986
- if (!patchRecordMatches(manifest.legacy_patched, recipe, "legacy")
987
- && patchRecordMatches(manifest.patched, recipe, "legacy")) {
988
- manifest.legacy_patched = manifest.patched;
989
- }
990
- manifest.patched = {
991
- asar_sha256: recipe.patchedAsarSha256,
992
- asar_header_sha256: recipe.patchedHeaderSha256,
993
- electron_asar_integrity: { algorithm: "SHA256", hash: recipe.patchedHeaderSha256 },
994
- binaries: {
995
- main_executable_sha256: inspection.executableIntegrity.executableSha256,
996
- electron_framework_sha256: inspection.executableIntegrity.frameworkSha256,
997
- },
998
- signing_metadata: signingMetadata(inspection.codeSign),
999
- ...(recipe.sourceBundleContent ? { bundle_content: inspection.bundleContent } : {}),
1000
- };
1001
- const temporaryPath = `${backup.manifestPath}.${operations.uuid()}.tmp`;
1002
- try {
1003
- fs.writeFileSync(temporaryPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
1004
- operations.renamePath({ source: temporaryPath, destination: backup.manifestPath });
1005
- } finally {
1006
- if (fs.existsSync(temporaryPath)) operations.removePath({ target: temporaryPath });
1007
- }
1008
- backup.manifest = manifest;
1009
- }
1010
-
1011
- export function assertPatchedBinaryRecord({
1012
- inspection,
1013
- manifestPath,
1014
- recipe,
1015
- recordState = ["legacy", "predecessor"].includes(inspection?.state)
1016
- ? inspection.state
1017
- : "patched",
1018
- }) {
1019
- let manifest;
1020
- try {
1021
- manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
1022
- } catch {
1023
- throw setupError("PM_STUDIO_PATCH_RECORD_INVALID", "PM Studio patched-binary record is missing or invalid");
1024
- }
1025
- const expectedSchema = recipe?.sourceBundleContent ? 2 : 1;
1026
- if (recipe && (manifest?.schema_version !== expectedSchema
1027
- || manifest?.kind !== "ccdx-pm-studio-backup"
1028
- || manifest?.recipe_id !== recipe.id
1029
- || (recipe.compatibility && manifest?.compatibility !== recipe.compatibility)
1030
- || manifest?.app?.bundle_identifier !== recipe.bundleIdentifier
1031
- || manifest?.app?.version !== recipe.version
1032
- || manifest?.app?.build !== recipe.build
1033
- || (recipe.sourceBundleContent && (
1034
- manifest?.source?.asar_sha256 !== recipe.sourceAsarSha256
1035
- || manifest?.source?.asar_header_sha256 !== recipe.sourceHeaderSha256
1036
- || !integrityMatches(manifest?.source?.electron_asar_integrity, "SHA256", recipe.sourceHeaderSha256)
1037
- || manifest?.source?.binaries?.main_executable_sha256 !== recipe.sourceExecutableSha256
1038
- || manifest?.source?.binaries?.electron_framework_sha256 !== recipe.sourceElectronFrameworkSha256
1039
- || manifest?.source?.binaries?.embedded_asar_integrity !== recipe.embeddedAsarIntegrity
1040
- || JSON.stringify(manifest?.source?.bundle_content) !== JSON.stringify(recipe.sourceBundleContent)
1041
- )))) {
1042
- throw setupError("PM_STUDIO_PATCH_RECORD_INVALID", "PM Studio patched-binary record does not match the installed patch recipe");
1043
- }
1044
- const record = patchRecordForState(manifest, recipe, recordState);
1045
- if (!record) {
1046
- throw setupError("PM_STUDIO_PATCH_RECORD_INVALID", `PM Studio ${recordState} record does not match the installed patch recipe`);
1047
- }
1048
- const binaries = record.binaries;
1049
- if (!binaries
1050
- || binaries.main_executable_sha256 !== inspection.executableIntegrity.executableSha256
1051
- || binaries.electron_framework_sha256 !== inspection.executableIntegrity.frameworkSha256) {
1052
- throw setupError("PM_STUDIO_BUNDLE_DRIFT", "PM Studio executable or Electron Framework differs from the installed patch record");
1053
- }
1054
- if (JSON.stringify(record.signing_metadata) !== JSON.stringify(signingMetadata(inspection.codeSign))) {
1055
- throw setupError("PM_STUDIO_BUNDLE_DRIFT", "PM Studio signing metadata differs from the installed patch record");
1056
- }
1057
- if (recipe?.sourceBundleContent
1058
- && JSON.stringify(record.bundle_content) !== JSON.stringify(inspection.bundleContent)) {
1059
- throw setupError("PM_STUDIO_BUNDLE_DRIFT", "PM Studio bundle content differs from the installed patch record");
1060
- }
1061
- }
1062
-
1063
- function signingMetadata(codeSign) {
1064
- return {
1065
- identifier: codeSign?.identifier || "",
1066
- flags: codeSign?.flags || "",
1067
- runtime_version: codeSign?.runtimeVersion || "",
1068
- entitlements_sha256: codeSign?.entitlementsSha256 || "",
1069
- };
1070
- }
1071
-
1072
- function flagsWithoutAdHoc(value) {
1073
- const flags = String(value || "");
1074
- const match = flags.match(/^0x([0-9a-f]+)/i);
1075
- if (!match) return flags.replace(/\badhoc\b,?/gi, "").replace(/\(,/, "(");
1076
- return `0x${(BigInt(`0x${match[1]}`) & ~2n).toString(16)}`;
1077
- }
1078
-
1079
- function assertSigningMetadataPreserved(source, staged, recipe) {
1080
- const after = signingMetadata(staged.codeSign);
1081
- if (after.identifier !== recipe.bundleIdentifier) {
1082
- throw setupError("PM_STUDIO_SIGNING_METADATA_CHANGED",
1083
- "Ad-hoc signing did not use the PM Studio bundle identifier");
1084
- }
1085
- if (recipe.patchedSigningMetadata) {
1086
- if (JSON.stringify(after) !== JSON.stringify(recipe.patchedSigningMetadata)) {
1087
- throw setupError("PM_STUDIO_SIGNING_METADATA_CHANGED",
1088
- "Ad-hoc signing did not produce the exact PM Studio signing metadata required by the recipe");
1089
- }
1090
- return;
1091
- }
1092
- const sourceSignature = source.codeSign;
1093
- if (sourceSignature?.displayValid !== true) return;
1094
- const flagsChanged = sourceSignature.flags
1095
- && flagsWithoutAdHoc(sourceSignature.flags) !== flagsWithoutAdHoc(after.flags);
1096
- const runtimeChanged = sourceSignature.runtimeVersion
1097
- && sourceSignature.runtimeVersion !== after.runtime_version;
1098
- const entitlementsChanged = sourceSignature.entitlementsState === "xml"
1099
- && sourceSignature.entitlementsSha256 !== after.entitlements_sha256;
1100
- if (flagsChanged || runtimeChanged || entitlementsChanged) {
1101
- throw setupError("PM_STUDIO_SIGNING_METADATA_CHANGED",
1102
- "Ad-hoc signing did not preserve available PM Studio entitlements, flags, and runtime metadata");
1103
- }
1104
- }
1105
-
1106
- function assertSpace({ appPath, backupRoot, backupExists, operations }) {
1107
- const bundleBytes = operations.bundleSize(appPath);
1108
- if (!Number.isFinite(bundleBytes) || bundleBytes <= 0) throw setupError("PM_STUDIO_SIZE_INVALID", "Could not determine PM Studio bundle size");
1109
- const appParent = path.dirname(appPath);
1110
- const stageRequired = bundleBytes + MIN_FREE_MARGIN_BYTES;
1111
- const backupRequired = backupExists ? 0 : bundleBytes + MIN_FREE_MARGIN_BYTES;
1112
- const sameVolume = operations.volumeId(appParent) === operations.volumeId(backupRoot);
1113
- if (sameVolume) {
1114
- if (operations.availableBytes(appParent) < stageRequired + backupRequired) {
1115
- throw setupError("PM_STUDIO_SPACE_INSUFFICIENT", "Not enough free space for PM Studio staging and backup");
1116
- }
1117
- } else {
1118
- if (operations.availableBytes(appParent) < stageRequired
1119
- || (!backupExists && operations.availableBytes(backupRoot) < backupRequired)) {
1120
- throw setupError("PM_STUDIO_SPACE_INSUFFICIENT", "Not enough free space for PM Studio staging or backup");
1121
- }
1122
- }
1123
- }
1124
-
1125
- function assertRestoreSpace({ appPath, backupAppPath, operations }) {
1126
- const bundleBytes = operations.bundleSize(backupAppPath);
1127
- if (!Number.isFinite(bundleBytes) || bundleBytes <= 0) {
1128
- throw setupError("PM_STUDIO_SIZE_INVALID", "Could not determine verified PM Studio backup size");
1129
- }
1130
- if (operations.availableBytes(path.dirname(appPath)) < bundleBytes + MIN_FREE_MARGIN_BYTES) {
1131
- throw setupError("PM_STUDIO_SPACE_INSUFFICIENT", "Not enough free space for PM Studio restore staging");
1132
- }
1133
- }
1134
-
1135
- function codeSignFingerprint(codeSign) {
1136
- return {
1137
- valid: codeSign?.valid === true,
1138
- verifyValid: codeSign?.verifyValid === true,
1139
- displayValid: codeSign?.displayValid === true,
1140
- entitlementsState: codeSign?.entitlementsState || "",
1141
- adHoc: codeSign?.adHoc === true,
1142
- identifier: codeSign?.identifier || "",
1143
- teamIdentifier: codeSign?.teamIdentifier || "",
1144
- flags: codeSign?.flags || "",
1145
- runtimeVersion: codeSign?.runtimeVersion || "",
1146
- cdHashFull: codeSign?.cdHashFull || "",
1147
- notarizationTicket: codeSign?.notarizationTicket || "",
1148
- entitlementsSha256: codeSign?.entitlementsSha256 || "",
1149
- };
1150
- }
1151
-
1152
- function restoreStateFingerprint(inspection, bundleContent) {
1153
- return JSON.stringify({
1154
- state: inspection.state,
1155
- metadata: inspection.metadata,
1156
- asar: {
1157
- state: inspection.asar?.state,
1158
- asarSha256: inspection.asar?.asarSha256,
1159
- headerSha256: inspection.asar?.headerSha256,
1160
- },
1161
- plistIntegrity: inspection.plistIntegrity,
1162
- executableIntegrity: {
1163
- executableName: inspection.executableIntegrity?.executableName,
1164
- executableSha256: inspection.executableIntegrity?.executableSha256,
1165
- frameworkSha256: inspection.executableIntegrity?.frameworkSha256,
1166
- embeddedIntegrity: inspection.executableIntegrity?.embeddedIntegrity,
1167
- matchesRecipeName: inspection.executableIntegrity?.matchesRecipeName,
1168
- },
1169
- codeSign: codeSignFingerprint(inspection.codeSign),
1170
- bundleContent,
1171
- });
1172
- }
1173
-
1174
- function inspectRestoreState({ appPath, recipe, operations }) {
1175
- const inspection = inspectPmStudioApp({ appPath, recipe, operations });
1176
- return restoreStateFromInspection({ appPath, inspection, recipe, operations });
1177
- }
1178
-
1179
- function restoreStateFromInspection({ appPath, inspection, recipe, operations }) {
1180
- const bundleContent = inspection.bundleContent || operations.inspectBundleContent({
1181
- ...appPaths(appPath, recipe),
1182
- recipe,
1183
- processRunner: operations.processRunner,
1184
- });
1185
- return {
1186
- inspection,
1187
- fingerprint: restoreStateFingerprint(inspection, bundleContent),
1188
- };
1189
- }
1190
-
1191
- function assertRestoreStateMatches(actual, expected, code, message) {
1192
- if (actual.fingerprint !== expected.fingerprint) {
1193
- throw setupError(code, message, actual.inspection.issues);
1194
- }
1195
- }
1196
-
1197
- function replaceVerifiedStage({ appPath, stagePath, operations }) {
1198
- try {
1199
- operations.replaceApp({ appPath, stagePath, processRunner: operations.processRunner });
1200
- return null;
1201
- } catch (error) {
1202
- return error;
1203
- }
1204
- }
1205
-
1206
- function recoveryMessage(backup) {
1207
- return `Verified backup: ${backup.backupAppPath}; manifest: ${backup.manifestPath}. Quit PM Studio, confirm the installed version/build still match, then restore only this version-matched backup or reinstall PM Studio.`;
1208
- }
1209
-
1210
- export async function runPmStudioSetup({
1211
- commandName = "ccdx",
1212
- appPath = DEFAULT_PM_STUDIO_APP_PATH,
1213
- home = os.homedir(),
1214
- backupRoot = pmStudioBackupRoot(home),
1215
- recipes = PM_STUDIO_RECIPES,
1216
- operations: operationOverrides = {},
1217
- logger = (line) => console.log(line),
1218
- } = {}) {
1219
- const operations = createPmStudioSetupOperations({
1220
- ...createPmStudioFileInspectionOperationOverrides(),
1221
- ...operationOverrides,
1222
- });
1223
- return operations.withSetupLock({ appPath }, async () => {
1224
- const messages = [];
1225
- const emit = (message) => {
1226
- messages.push(message);
1227
- logger(message);
1228
- };
1229
-
1230
- assertValidClaudeProfile(operations, home);
1231
- assertInstalledAppPath(appPath);
1232
- operations.assertPermissions({ appPath, backupRoot });
1233
- assertNoBlockingProcesses(operations, appPath);
1234
-
1235
- const genericPaths = {
1236
- appPath,
1237
- infoPlistPath: path.join(appPath, "Contents/Info.plist"),
1238
- processRunner: operations.processRunner,
1239
- };
1240
- const metadata = operations.readBundleMetadata(genericPaths);
1241
- const recipe = resolvePmStudioCompatibleRecipe({
1242
- appPath,
1243
- backupRoot,
1244
- metadata,
1245
- recipes,
1246
- operations,
1247
- });
1248
-
1249
- const source = inspectPmStudioApp({ appPath, recipe, operations });
1250
- const finalBackupDir = path.join(backupRoot, backupName(recipe));
1251
- if (source.state === "patched") {
1252
- if (!fs.existsSync(finalBackupDir)) {
1253
- throw setupError("PM_STUDIO_BACKUP_INVALID", "Patched PM Studio has no matching verified backup; no files were changed");
1254
- }
1255
- const backup = validateBackup({ backupDir: finalBackupDir, recipe, operations });
1256
- assertPatchedBinaryRecord({ inspection: source, manifestPath: backup.manifestPath, recipe });
1257
- emit(`[OK] PM Studio ${recipe.version} build ${recipe.build} is already patched; no backup or signing was repeated.`);
1258
- emit(`[OK] Verified backup: ${backup.backupAppPath}`);
1259
- emit(`[OK] Backup manifest: ${backup.manifestPath}`);
1260
- emit(`[INFO] Start ${commandName} before using Claude in PM Studio; GPT remains on PM Studio's native GitHub Copilot path.`);
1261
- return { status: "already_patched", changed: false, recipe: recipe.id, backup, inspection: source, messages };
1262
- }
1263
- const migratingLegacy = source.state === "legacy";
1264
- const migratingPredecessor = source.state === "predecessor";
1265
- const migratingExistingPatch = migratingLegacy || migratingPredecessor;
1266
- if (source.state !== "clean" && !migratingExistingPatch) {
1267
- throw setupError("PM_STUDIO_BUNDLE_DRIFT",
1268
- `PM Studio ${recipe.version} build ${recipe.build} does not match the clean, current split-origin, predecessor, or legacy recipe; no files were changed`, source.issues);
1269
- }
1270
-
1271
- const backupExists = fs.existsSync(finalBackupDir);
1272
- let verifiedBackup = null;
1273
- if (migratingExistingPatch) {
1274
- if (!backupExists) {
1275
- throw setupError("PM_STUDIO_BACKUP_INVALID",
1276
- `${migratingLegacy ? "Legacy global-origin" : "Predecessor split-origin"} PM Studio patch has no matching verified clean backup; no files were changed`);
1277
- }
1278
- verifiedBackup = validateBackup({ backupDir: finalBackupDir, recipe, operations });
1279
- assertPatchedBinaryRecord({
1280
- inspection: source,
1281
- manifestPath: verifiedBackup.manifestPath,
1282
- recipe,
1283
- recordState: source.state,
1284
- });
1285
- emit(migratingLegacy
1286
- ? "[WARN] Legacy global-origin patch detected; setup will rebuild from the verified clean backup so GPT and enterprise authentication return to PM Studio's native origin."
1287
- : "[WARN] Predecessor split-origin patch detected; setup will rebuild from the verified clean backup so delayed Claude model discovery no longer falls back to the native catalog.");
1288
- }
1289
- assertSpace({ appPath, backupRoot, backupExists, operations });
1290
-
1291
- emit("[WARN] PM Studio will be ad-hoc signed after patching; an update or reinstall may overwrite this patch.");
1292
- const backup = verifiedBackup || ensureBackup({ appPath, backupRoot, recipe, operations });
1293
- const stagePath = path.join(path.dirname(appPath), `.${path.basename(appPath)}.ccdx-stage-${operations.uuid()}`);
1294
- let preserveStage = false;
1295
- try {
1296
- operations.copyBundle({
1297
- source: migratingExistingPatch ? backup.backupAppPath : appPath,
1298
- destination: stagePath,
1299
- processRunner: operations.processRunner,
1300
- });
1301
- const stagedSource = inspectPmStudioApp({ appPath: stagePath, recipe, operations });
1302
- if (stagedSource.state !== "clean"
1303
- || (!migratingExistingPatch && recipe.sourceBundleContent
1304
- && stagedSource.bundleContent?.sha256 !== source.bundleContent?.sha256)) {
1305
- throw setupError("PM_STUDIO_STAGE_INVALID", "PM Studio staging copy does not match the exact clean source bundle", stagedSource.issues);
1306
- }
1307
- const stageAsarPath = path.join(stagePath, recipe.asarPath);
1308
- const patched = patchAsarBuffer(fs.readFileSync(stageAsarPath), recipe);
1309
- if (!patched.changed) throw setupError("PM_STUDIO_STAGE_INVALID", "Staging copy was unexpectedly already patched");
1310
- fs.writeFileSync(stageAsarPath, patched.buffer);
1311
- patched.buffer = null;
1312
- operations.writeAsarIntegrity({
1313
- ...appPaths(stagePath, recipe),
1314
- recipe,
1315
- hash: recipe.patchedHeaderSha256,
1316
- processRunner: operations.processRunner,
1317
- });
1318
- operations.signApp({
1319
- appPath: stagePath,
1320
- recipe,
1321
- source: stagedSource,
1322
- processRunner: operations.processRunner,
1323
- });
1324
-
1325
- const staged = inspectPmStudioApp({ appPath: stagePath, recipe, operations });
1326
- if (staged.state !== "patched") {
1327
- throw setupError("PM_STUDIO_STAGE_VERIFY_FAILED", "Patched PM Studio staging bundle failed verification", staged.issues);
1328
- }
1329
- assertSigningMetadataPreserved(stagedSource, staged, recipe);
1330
- writePatchedBinaryRecord({ backup, inspection: staged, operations, recipe });
1331
-
1332
- assertNoBlockingProcesses(operations, appPath);
1333
- const unchangedSource = inspectPmStudioApp({ appPath, recipe, operations });
1334
- if (unchangedSource.state !== source.state
1335
- || unchangedSource.asar.asarSha256 !== source.asar.asarSha256
1336
- || unchangedSource.asar.headerSha256 !== source.asar.headerSha256
1337
- || (recipe.sourceBundleContent
1338
- && unchangedSource.bundleContent?.sha256 !== source.bundleContent?.sha256)) {
1339
- throw setupError("PM_STUDIO_SOURCE_CHANGED", "PM Studio changed during setup; the verified staging bundle was not installed");
1340
- }
1341
-
1342
- const replacementError = replaceVerifiedStage({ appPath, stagePath, operations });
1343
- let installed;
1344
- try {
1345
- installed = inspectPmStudioApp({ appPath, recipe, operations });
1346
- } catch {
1347
- preserveStage = fs.existsSync(stagePath);
1348
- throw setupError("PM_STUDIO_REPLACE_STATE_UNKNOWN",
1349
- `PM Studio replacement final state could not be verified. Do not launch it. ${recoveryMessage(backup)}${preserveStage ? ` Verified staging retained: ${stagePath}.` : ""}`);
1350
- }
1351
- if (installed.state !== "patched") {
1352
- const originalRetained = installed.state === source.state
1353
- && installed.asar.asarSha256 === source.asar.asarSha256
1354
- && installed.asar.headerSha256 === source.asar.headerSha256
1355
- && (!recipe.sourceBundleContent
1356
- || installed.bundleContent?.sha256 === source.bundleContent?.sha256);
1357
- preserveStage = !originalRetained && fs.existsSync(stagePath);
1358
- const code = originalRetained
1359
- ? "PM_STUDIO_REPLACE_FAILED"
1360
- : "PM_STUDIO_INSTALL_VERIFY_FAILED";
1361
- const stateMessage = originalRetained
1362
- ? `The installed PM Studio remains the verified ${source.state} bundle`
1363
- : "The installed PM Studio did not retain the verified patched state; do not launch it";
1364
- throw setupError(code,
1365
- `${stateMessage}. ${recoveryMessage(backup)}${preserveStage ? ` Verified staging retained: ${stagePath}.` : ""}`,
1366
- installed.issues);
1367
- }
1368
- try {
1369
- assertPatchedBinaryRecord({ inspection: installed, manifestPath: backup.manifestPath, recipe });
1370
- } catch (error) {
1371
- throw setupError("PM_STUDIO_INSTALL_VERIFY_FAILED",
1372
- `Installed PM Studio differs from the verified staging record; do not launch it. ${recoveryMessage(backup)}`,
1373
- [error.message]);
1374
- }
1375
- if (replacementError) {
1376
- emit("[WARN] Atomic replacement reported an error after the exact patched bundle became installed; the final bundle was independently verified.");
1377
- }
1378
-
1379
- emit(migratingLegacy
1380
- ? `[OK] Migrated PM Studio ${recipe.version} build ${recipe.build} from the legacy global-origin patch to the split-origin patch.`
1381
- : migratingPredecessor
1382
- ? `[OK] Migrated PM Studio ${recipe.version} build ${recipe.build} from the predecessor split-origin patch to the current split-origin patch.`
1383
- : `[OK] Patched PM Studio ${recipe.version} build ${recipe.build}.`);
1384
- emit(`[OK] app.asar SHA-256: ${installed.asar.asarSha256}`);
1385
- emit(`[OK] Verified backup: ${backup.backupAppPath}${backup.reused ? " (reused)" : ""}`);
1386
- emit(`[OK] Backup manifest: ${backup.manifestPath}`);
1387
- emit("[INFO] The verified staging bundle was installed with Foundation's atomic item replacement.");
1388
- emit(`[INFO] To restore, quit PM Studio and its updater, then run ${commandName} pms restore.`);
1389
- emit("[INFO] Restore uses exact source-content verification; the original source signature is inspected and reported but is not an admission gate.");
1390
- emit(`[INFO] Start ${commandName} before using Claude in PM Studio; GPT remains on PM Studio's native GitHub Copilot path.`);
1391
- return {
1392
- status: migratingExistingPatch ? "migrated" : "patched",
1393
- changed: true,
1394
- recipe: recipe.id,
1395
- backup,
1396
- inspection: installed,
1397
- replacementMode: "foundation-atomic-replace",
1398
- messages,
1399
- };
1400
- } finally {
1401
- if (!preserveStage && fs.existsSync(stagePath)) operations.removePath({ target: stagePath });
1402
- }
1403
- });
1404
- }
1405
-
1406
- export async function runPmStudioRestore({
1407
- commandName = "ccdx",
1408
- appPath = DEFAULT_PM_STUDIO_APP_PATH,
1409
- home = os.homedir(),
1410
- backupRoot = pmStudioBackupRoot(home),
1411
- recipes = PM_STUDIO_RECIPES,
1412
- operations: operationOverrides = {},
1413
- logger = (line) => console.log(line),
1414
- } = {}) {
1415
- const operations = createPmStudioSetupOperations({
1416
- ...createPmStudioFileInspectionOperationOverrides(),
1417
- ...operationOverrides,
1418
- });
1419
- return operations.withSetupLock({ appPath }, async () => {
1420
- const messages = [];
1421
- const emit = (message) => {
1422
- messages.push(message);
1423
- logger(message);
1424
- };
1425
-
1426
- assertInstalledAppPath(appPath);
1427
- assertNoBlockingProcesses(operations, appPath);
1428
- const metadata = operations.readBundleMetadata({
1429
- appPath,
1430
- infoPlistPath: path.join(appPath, "Contents/Info.plist"),
1431
- processRunner: operations.processRunner,
1432
- });
1433
- const recipe = resolvePmStudioCompatibleRecipe({
1434
- appPath,
1435
- home,
1436
- backupRoot,
1437
- metadata,
1438
- recipes,
1439
- operations,
1440
- });
1441
- const source = inspectRestoreState({ appPath, recipe, operations });
1442
- if (source.inspection.state === "clean") {
1443
- emit(`[OK] PM Studio ${recipe.version} build ${recipe.build} is already the exact clean local bundle; no files were changed.`);
1444
- return {
1445
- status: "already_clean",
1446
- changed: false,
1447
- recipe: recipe.id,
1448
- inspection: source.inspection,
1449
- messages,
1450
- };
1451
- }
1452
- if (!["patched", "predecessor", "legacy"].includes(source.inspection.state)) {
1453
- throw setupError("PM_STUDIO_BUNDLE_DRIFT",
1454
- `PM Studio ${recipe.version} build ${recipe.build} does not match a recorded patch state; no files were changed`,
1455
- source.inspection.issues);
1456
- }
1457
-
1458
- const manifestPath = pmStudioPatchManifestPath({ home, backupRoot, recipe });
1459
- const backupDir = path.dirname(manifestPath);
1460
- const backupEvidence = {};
1461
- const backup = validateBackup({ backupDir, recipe, operations, evidence: backupEvidence });
1462
- assertPatchedBinaryRecord({
1463
- inspection: source.inspection,
1464
- manifestPath: backup.manifestPath,
1465
- recipe,
1466
- recordState: source.inspection.state,
1467
- });
1468
- operations.assertRestorePermissions({
1469
- appPath,
1470
- backupAppPath: backup.backupAppPath,
1471
- processRunner: operations.processRunner,
1472
- });
1473
- assertRestoreSpace({ appPath, backupAppPath: backup.backupAppPath, operations });
1474
- const manifestSnapshot = fs.readFileSync(backup.manifestPath);
1475
- const backupSource = restoreStateFromInspection({
1476
- appPath: backup.backupAppPath,
1477
- inspection: backupEvidence.inspection,
1478
- recipe,
1479
- operations,
1480
- });
1481
- if (backupSource.inspection.state !== "clean") {
1482
- throw setupError("PM_STUDIO_BACKUP_INVALID", "Verified PM Studio backup is not an exact clean bundle",
1483
- backupSource.inspection.issues);
1484
- }
1485
-
1486
- const stagePath = path.join(path.dirname(appPath),
1487
- `.${path.basename(appPath)}.ccdx-restore-stage-${operations.uuid()}`);
1488
- let preserveStage = false;
1489
- try {
1490
- operations.copyBundle({
1491
- source: backup.backupAppPath,
1492
- destination: stagePath,
1493
- processRunner: operations.processRunner,
1494
- });
1495
- if (!fs.readFileSync(backup.manifestPath).equals(manifestSnapshot)) {
1496
- throw setupError("PM_STUDIO_BACKUP_CHANGED",
1497
- "PM Studio backup manifest changed during restore; the verified staging bundle was not installed");
1498
- }
1499
- const currentBackupEvidence = {};
1500
- const currentBackup = validateBackup({
1501
- backupDir,
1502
- recipe,
1503
- operations,
1504
- evidence: currentBackupEvidence,
1505
- });
1506
- const unchangedBackup = restoreStateFromInspection({
1507
- appPath: currentBackup.backupAppPath,
1508
- inspection: currentBackupEvidence.inspection,
1509
- recipe,
1510
- operations,
1511
- });
1512
- assertRestoreStateMatches(unchangedBackup, backupSource, "PM_STUDIO_BACKUP_CHANGED",
1513
- "PM Studio backup changed during restore; the verified staging bundle was not installed");
1514
- const unchangedSource = inspectRestoreState({ appPath, recipe, operations });
1515
- assertRestoreStateMatches(unchangedSource, source, "PM_STUDIO_SOURCE_CHANGED",
1516
- "PM Studio changed during restore; the verified staging bundle was not installed");
1517
- assertPatchedBinaryRecord({
1518
- inspection: unchangedSource.inspection,
1519
- manifestPath: backup.manifestPath,
1520
- recipe,
1521
- recordState: source.inspection.state,
1522
- });
1523
-
1524
- const staged = inspectRestoreState({ appPath: stagePath, recipe, operations });
1525
- if (staged.inspection.state !== "clean") {
1526
- throw setupError("PM_STUDIO_RESTORE_STAGE_INVALID",
1527
- "PM Studio restore staging copy is not an exact clean bundle", staged.inspection.issues);
1528
- }
1529
- assertRestoreStateMatches(staged, backupSource, "PM_STUDIO_RESTORE_STAGE_INVALID",
1530
- "PM Studio restore staging copy differs from the verified clean backup");
1531
- assertNoBlockingProcesses(operations, appPath);
1532
-
1533
- const replacementError = replaceVerifiedStage({ appPath, stagePath, operations });
1534
- let installed;
1535
- try {
1536
- installed = inspectRestoreState({ appPath, recipe, operations });
1537
- } catch {
1538
- preserveStage = fs.existsSync(stagePath);
1539
- throw setupError("PM_STUDIO_RESTORE_STATE_UNKNOWN",
1540
- `PM Studio restore final state could not be verified. Do not launch it. ${recoveryMessage(backup)}${preserveStage ? ` Verified staging retained: ${stagePath}.` : ""}`);
1541
- }
1542
- if (installed.inspection.state !== "clean" || installed.fingerprint !== staged.fingerprint) {
1543
- const originalRetained = installed.fingerprint === source.fingerprint;
1544
- preserveStage = !originalRetained && fs.existsSync(stagePath);
1545
- const code = originalRetained
1546
- ? "PM_STUDIO_RESTORE_REPLACE_FAILED"
1547
- : "PM_STUDIO_RESTORE_INSTALL_VERIFY_FAILED";
1548
- const stateMessage = originalRetained
1549
- ? `The installed PM Studio remains the verified ${source.inspection.state} bundle`
1550
- : "The installed PM Studio does not match the verified clean backup; do not launch it";
1551
- throw setupError(code,
1552
- `${stateMessage}. ${recoveryMessage(backup)}${preserveStage ? ` Verified staging retained: ${stagePath}.` : ""}`,
1553
- installed.inspection.issues);
1554
- }
1555
- if (replacementError) {
1556
- emit("[WARN] Atomic replacement reported an error after the exact clean bundle became installed; the final bundle was independently verified.");
1557
- }
1558
- emit(`[OK] Restored PM Studio ${recipe.version} build ${recipe.build} from the exact verified clean backup.`);
1559
- emit(`[OK] Verified backup retained: ${backup.backupAppPath}`);
1560
- emit(`[OK] Backup manifest retained: ${backup.manifestPath}`);
1561
- emit("[INFO] The verified clean staging bundle was installed with Foundation's atomic item replacement.");
1562
- if (installed.inspection.codeSign?.valid === true) {
1563
- emit("[OK] The restored source bundle's original signature verifies successfully.");
1564
- } else {
1565
- emit("[WARN] The restored bundle exactly matches its recorded local source, whose original signature does not currently verify; reinstall PM Studio if a vendor-valid signature is required.");
1566
- }
1567
- emit(`[INFO] Run ${commandName} pms setup to apply the split-origin patch again.`);
1568
- return {
1569
- status: "restored",
1570
- changed: true,
1571
- recipe: recipe.id,
1572
- backup,
1573
- inspection: installed.inspection,
1574
- replacementMode: "foundation-atomic-replace",
1575
- messages,
1576
- };
1577
- } finally {
1578
- if (!preserveStage && fs.existsSync(stagePath)) operations.removePath({ target: stagePath });
1579
- }
1580
- });
1581
- }