codex-copilot-dx 0.6.7 → 0.6.9

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,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import { randomUUID } from "node:crypto";
4
+ import { createHash, randomUUID } from "node:crypto";
5
5
  import { spawnSync } from "node:child_process";
6
6
  import {
7
7
  AUTH_PROFILE_CLAUDE,
@@ -19,6 +19,7 @@ import {
19
19
  export const DEFAULT_PM_STUDIO_APP_PATH = "/Applications/PM Studio.app";
20
20
  export const PM_STUDIO_CLAUDE_AUTH_COMMAND = "ccdx auth login claude --reauth --github-login <personal-login>";
21
21
  const MIN_FREE_MARGIN_BYTES = 16 * 1024 * 1024;
22
+ const BUNDLE_CONTENT_SCHEME = "ccdx-bundle-content-v2";
22
23
 
23
24
  function setupError(code, message, details) {
24
25
  const error = new Error(message);
@@ -31,7 +32,7 @@ function defaultProcessRunner(command, args) {
31
32
  return spawnSync(command, args, {
32
33
  encoding: "utf8",
33
34
  windowsHide: true,
34
- maxBuffer: 8 * 1024 * 1024,
35
+ maxBuffer: 32 * 1024 * 1024,
35
36
  });
36
37
  }
37
38
 
@@ -53,6 +54,166 @@ function runChecked(processRunner, command, args, label) {
53
54
  return result;
54
55
  }
55
56
 
57
+ function hashRegularFile(filePath, expected) {
58
+ const hash = createHash("sha256");
59
+ const file = fs.openSync(filePath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
60
+ const buffer = Buffer.allocUnsafe(1024 * 1024);
61
+ try {
62
+ const before = fs.fstatSync(file, { bigint: true });
63
+ if (before.dev !== BigInt(expected.dev)
64
+ || before.ino !== BigInt(expected.ino)
65
+ || before.size !== BigInt(expected.size)) {
66
+ throw setupError("PM_STUDIO_BUNDLE_CHANGED", `PM Studio bundle changed while reading ${filePath}`);
67
+ }
68
+ for (;;) {
69
+ const length = fs.readSync(file, buffer, 0, buffer.length, null);
70
+ if (length === 0) break;
71
+ hash.update(buffer.subarray(0, length));
72
+ }
73
+ const after = fs.fstatSync(file, { bigint: true });
74
+ if (before.dev !== after.dev
75
+ || before.ino !== after.ino
76
+ || before.size !== after.size
77
+ || before.mtimeNs !== after.mtimeNs
78
+ || before.ctimeNs !== after.ctimeNs) {
79
+ throw setupError("PM_STUDIO_BUNDLE_CHANGED", `PM Studio bundle changed while hashing ${filePath}`);
80
+ }
81
+ } finally {
82
+ fs.closeSync(file);
83
+ }
84
+ return hash.digest("hex");
85
+ }
86
+
87
+ function parseXattrHex(output, root, ignoredXattrs) {
88
+ const records = [];
89
+ const ignored = new Set(ignoredXattrs);
90
+ const seen = new Set();
91
+ let current = null;
92
+ const flush = () => {
93
+ if (!current) return;
94
+ if (!ignored.has(current.name)) {
95
+ const key = `${current.path}\0${current.name}`;
96
+ if (seen.has(key)) throw setupError("PM_STUDIO_XATTR_INVALID", `Duplicate extended attribute ${current.name}`);
97
+ seen.add(key);
98
+ const value = Buffer.from(current.hex.join(""), "hex");
99
+ records.push(["xattr", current.path, current.name, value.length, sha256Hex(value)]);
100
+ }
101
+ current = null;
102
+ };
103
+
104
+ for (const line of String(output || "").split(/\r?\n/)) {
105
+ if (!line) continue;
106
+ const separator = line.lastIndexOf(": ");
107
+ if ((line === `${root}:` || line.startsWith(`${root}: `) || line.startsWith(`${root}${path.sep}`))
108
+ && separator >= root.length && line.endsWith(":")) {
109
+ flush();
110
+ const target = line.slice(0, separator);
111
+ const relative = target === root ? "." : path.relative(root, target).split(path.sep).join("/");
112
+ if (!relative || relative === ".." || relative.startsWith("../") || path.isAbsolute(relative)) {
113
+ throw setupError("PM_STUDIO_XATTR_INVALID", `Extended attribute path escapes PM Studio: ${target}`);
114
+ }
115
+ current = { path: relative, name: line.slice(separator + 2, -1), hex: [] };
116
+ continue;
117
+ }
118
+ if (!current) throw setupError("PM_STUDIO_XATTR_INVALID", "Could not parse PM Studio extended attributes");
119
+ const data = line.includes(" |") ? line.slice(0, line.indexOf(" |")) : line;
120
+ const tokens = data.trim().split(/\s+/);
121
+ if (!/^[0-9a-f]{8}$/i.test(tokens[0]) || tokens.slice(1).some((token) => !/^[0-9a-f]{2}$/i.test(token))) {
122
+ throw setupError("PM_STUDIO_XATTR_INVALID", "Could not parse PM Studio extended attribute bytes");
123
+ }
124
+ current.hex.push(...tokens.slice(1));
125
+ }
126
+ flush();
127
+ return records.sort((left, right) => Buffer.compare(
128
+ Buffer.from(`${left[1]}\0${left[2]}`),
129
+ Buffer.from(`${right[1]}\0${right[2]}`),
130
+ ));
131
+ }
132
+
133
+ export function inspectBundleContent(appPath, {
134
+ xattrOutput = "",
135
+ ignoredXattrs = [],
136
+ } = {}) {
137
+ const root = path.resolve(appPath);
138
+ const rootStat = fs.lstatSync(root);
139
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
140
+ throw setupError("PM_STUDIO_APP_INVALID", `PM Studio path is not a regular app bundle: ${appPath}`);
141
+ }
142
+ const records = [];
143
+ const normalizedPaths = new Map();
144
+ let regularFileCount = 0;
145
+ let regularBytes = 0;
146
+ let symlinkCount = 0;
147
+
148
+ const rememberPath = (relative) => {
149
+ const normalized = relative.normalize("NFC");
150
+ const existing = normalizedPaths.get(normalized);
151
+ if (existing && existing !== relative) {
152
+ throw setupError("PM_STUDIO_BUNDLE_PATH_COLLISION", `PM Studio contains colliding paths: ${existing} and ${relative}`);
153
+ }
154
+ normalizedPaths.set(normalized, relative);
155
+ };
156
+ rememberPath(".");
157
+ records.push(["directory", ".", rootStat.mode & 0o7777]);
158
+ const visit = (directory) => {
159
+ const names = fs.readdirSync(directory).sort((left, right) =>
160
+ Buffer.compare(Buffer.from(left), Buffer.from(right)));
161
+ for (const name of names) {
162
+ const fullPath = path.join(directory, name);
163
+ const stat = fs.lstatSync(fullPath);
164
+ const relative = path.relative(root, fullPath).split(path.sep).join("/");
165
+ const mode = stat.mode & 0o7777;
166
+ rememberPath(relative);
167
+ if (stat.isDirectory() && !stat.isSymbolicLink()) {
168
+ records.push(["directory", relative, mode]);
169
+ visit(fullPath);
170
+ } else if (stat.isSymbolicLink()) {
171
+ const target = fs.readlinkSync(fullPath);
172
+ const resolved = path.resolve(path.dirname(fullPath), target);
173
+ if (path.isAbsolute(target)
174
+ || (resolved !== root && !resolved.startsWith(`${root}${path.sep}`))
175
+ || !fs.existsSync(resolved)) {
176
+ throw setupError("PM_STUDIO_BUNDLE_SYMLINK_INVALID", `PM Studio contains an unsafe symlink: ${relative}`);
177
+ }
178
+ symlinkCount += 1;
179
+ records.push(["symlink", relative, mode, target]);
180
+ } else if (stat.isFile()) {
181
+ if (stat.nlink !== 1) {
182
+ throw setupError("PM_STUDIO_BUNDLE_HARDLINK_INVALID", `PM Studio contains a hard-linked file: ${relative}`);
183
+ }
184
+ regularFileCount += 1;
185
+ regularBytes += stat.size;
186
+ records.push(["file", relative, mode, stat.size, hashRegularFile(fullPath, stat)]);
187
+ } else {
188
+ throw setupError("PM_STUDIO_BUNDLE_ENTRY_INVALID", `PM Studio contains an unsupported entry: ${relative}`);
189
+ }
190
+ }
191
+ };
192
+ visit(root);
193
+
194
+ const xattrRecords = parseXattrHex(xattrOutput, root, ignoredXattrs);
195
+ const hash = createHash("sha256");
196
+ for (const record of [...records, ...xattrRecords]) hash.update(`${JSON.stringify(record)}\n`);
197
+ return {
198
+ scheme: BUNDLE_CONTENT_SCHEME,
199
+ sha256: hash.digest("hex"),
200
+ entryCount: records.length,
201
+ regularFileCount,
202
+ regularBytes,
203
+ symlinkCount,
204
+ xattrCount: xattrRecords.length,
205
+ };
206
+ }
207
+
208
+ function defaultInspectBundleContent({ appPath, recipe, processRunner }) {
209
+ const xattrs = runChecked(processRunner, "/usr/bin/xattr", ["-r", "-s", "-x", "-l", appPath],
210
+ "Reading PM Studio extended attributes");
211
+ return inspectBundleContent(appPath, {
212
+ xattrOutput: xattrs.stdout,
213
+ ignoredXattrs: recipe.sourceBundleContent?.ignoredXattrs || [],
214
+ });
215
+ }
216
+
56
217
  function plistRead(processRunner, plistPath, key) {
57
218
  return runChecked(processRunner, "/usr/bin/plutil", ["-extract", key, "raw", "-o", "-", plistPath],
58
219
  `Reading ${path.basename(plistPath)}`).stdout.trim();
@@ -91,30 +252,39 @@ function defaultInspectCodeSign({ appPath, processRunner }) {
91
252
  const verify = normalizedProcessResult(processRunner("/usr/bin/codesign", [
92
253
  "--verify", "--deep", "--strict", "--verbose=2", appPath,
93
254
  ]));
94
- if (verify.error || verify.status !== 0) return { valid: false, adHoc: false };
95
255
  const display = normalizedProcessResult(processRunner("/usr/bin/codesign", [
96
256
  "--display", "--verbose=4", appPath,
97
257
  ]));
98
- if (display.error || display.status !== 0) return { valid: false, adHoc: false };
99
258
  const details = `${display.stdout}\n${display.stderr}`;
100
259
  const entitlementsResult = normalizedProcessResult(processRunner("/usr/bin/codesign", [
101
260
  "--display", "--entitlements", "-", "--xml", appPath,
102
261
  ]));
103
- if (entitlementsResult.error || entitlementsResult.status !== 0) return { valid: false, adHoc: false };
104
262
  const entitlementOutput = `${entitlementsResult.stdout}\n${entitlementsResult.stderr}`;
105
263
  const plistStart = entitlementOutput.indexOf("<?xml");
106
264
  const plistEnd = entitlementOutput.lastIndexOf("</plist>");
107
- if (plistStart < 0 || plistEnd < plistStart) return { valid: false, adHoc: false };
108
- const entitlements = entitlementOutput.slice(plistStart, plistEnd + "</plist>".length);
265
+ const hasEntitlements = plistStart >= 0 && plistEnd >= plistStart;
266
+ const entitlements = hasEntitlements
267
+ ? entitlementOutput.slice(plistStart, plistEnd + "</plist>".length)
268
+ : "";
109
269
  return {
110
- valid: true,
270
+ valid: !verify.error && verify.status === 0
271
+ && !display.error && display.status === 0
272
+ && !entitlementsResult.error && entitlementsResult.status === 0
273
+ && hasEntitlements,
274
+ verifyValid: !verify.error && verify.status === 0,
275
+ displayValid: !display.error && display.status === 0,
276
+ entitlementsState: hasEntitlements
277
+ ? "xml"
278
+ : /invalid entitlements blob/i.test(entitlementOutput) ? "invalid" : "unavailable",
111
279
  adHoc: /(?:^|\n)Signature=adhoc(?:\n|$)/i.test(details)
112
280
  || /(?:^|\n)TeamIdentifier=not set(?:\n|$)/i.test(details),
113
281
  identifier: details.match(/(?:^|\n)Identifier=([^\n]+)/)?.[1]?.trim() || "",
114
282
  teamIdentifier: details.match(/(?:^|\n)TeamIdentifier=([^\n]+)/)?.[1]?.trim() || "",
115
283
  flags: details.match(/\bflags=([^\s]+)/)?.[1] || "",
116
284
  runtimeVersion: details.match(/(?:^|\n)Runtime Version=([^\n]+)/)?.[1]?.trim() || "",
117
- entitlementsSha256: sha256Hex(entitlements),
285
+ cdHashFull: details.match(/(?:^|\n)CandidateCDHashFull sha256=([0-9a-f]+)/i)?.[1]?.toLowerCase() || "",
286
+ notarizationTicket: details.match(/(?:^|\n)Notarization Ticket=([^\n]+)/)?.[1]?.trim() || "",
287
+ entitlementsSha256: hasEntitlements ? sha256Hex(entitlements) : "",
118
288
  };
119
289
  }
120
290
 
@@ -240,6 +410,7 @@ export function createPmStudioSetupOperations(overrides = {}) {
240
410
  readAsarIntegrity: defaultReadAsarIntegrity,
241
411
  writeAsarIntegrity: defaultWriteAsarIntegrity,
242
412
  inspectCodeSign: defaultInspectCodeSign,
413
+ inspectBundleContent: defaultInspectBundleContent,
243
414
  inspectExecutableIntegrity: defaultInspectExecutableIntegrity,
244
415
  signApp: defaultSignApp,
245
416
  listBlockingProcesses: defaultListBlockingProcesses,
@@ -271,12 +442,34 @@ function integrityMatches(actual, algorithm, hash) {
271
442
  return actual?.algorithm === algorithm && actual?.hash === hash;
272
443
  }
273
444
 
445
+ function bundleContentMatches(actual, expected) {
446
+ return actual?.scheme === expected?.scheme
447
+ && actual?.sha256 === expected?.sha256
448
+ && actual?.entryCount === expected?.entryCount
449
+ && actual?.regularFileCount === expected?.regularFileCount
450
+ && actual?.regularBytes === expected?.regularBytes
451
+ && actual?.symlinkCount === expected?.symlinkCount
452
+ && actual?.xattrCount === expected?.xattrCount;
453
+ }
454
+
455
+ function sourceCodeSignatureMatches(actual, expected) {
456
+ return actual?.adHoc === false
457
+ && actual?.displayValid === true
458
+ && actual?.identifier === expected?.identifier
459
+ && actual?.teamIdentifier === expected?.teamIdentifier
460
+ && actual?.flags === expected?.flags
461
+ && actual?.runtimeVersion === expected?.runtimeVersion
462
+ && actual?.cdHashFull === expected?.cdHashFull
463
+ && actual?.notarizationTicket === expected?.notarizationTicket;
464
+ }
465
+
274
466
  export function inspectPmStudioApp({
275
467
  appPath,
276
468
  recipe = PM_STUDIO_2_9_7_RECIPE,
277
469
  operations = createPmStudioSetupOperations(),
278
470
  } = {}) {
279
471
  const paths = appPaths(appPath, recipe);
472
+ const issues = [];
280
473
  const metadata = operations.readBundleMetadata({ ...paths, recipe, processRunner: operations.processRunner });
281
474
  const asar = inspectAsarBuffer(fs.readFileSync(paths.asarPath), recipe);
282
475
  const plistIntegrity = operations.readAsarIntegrity({ ...paths, recipe, processRunner: operations.processRunner });
@@ -284,7 +477,16 @@ export function inspectPmStudioApp({
284
477
  ...paths, recipe, processRunner: operations.processRunner,
285
478
  });
286
479
  const codeSign = operations.inspectCodeSign({ ...paths, recipe, processRunner: operations.processRunner });
287
- const issues = [];
480
+ let bundleContent = null;
481
+ if (recipe.sourceBundleContent) {
482
+ try {
483
+ bundleContent = operations.inspectBundleContent({
484
+ ...paths, recipe, processRunner: operations.processRunner,
485
+ });
486
+ } catch (error) {
487
+ issues.push(`bundle content inspection failed: ${error.message}`);
488
+ }
489
+ }
288
490
 
289
491
  if (String(metadata.version) !== recipe.version) {
290
492
  issues.push(`version is ${metadata.version}, expected ${recipe.version}`);
@@ -301,21 +503,39 @@ export function inspectPmStudioApp({
301
503
  issues.push("Electron Framework has an unsupported embedded ASAR integrity slot");
302
504
  }
303
505
 
506
+ const exactSourceBundle = !recipe.sourceBundleContent
507
+ || bundleContentMatches(bundleContent, recipe.sourceBundleContent);
508
+ const strictVendorSignature = codeSign?.valid === true
509
+ && codeSign?.adHoc === false
510
+ && codeSign?.teamIdentifier === recipe.sourceTeamIdentifier;
511
+ const exactInvalidVendorSignature = recipe.sourceBundleContent
512
+ && exactSourceBundle
513
+ && codeSign?.valid === false
514
+ && codeSign?.verifyValid === false
515
+ && codeSign?.entitlementsState === "invalid"
516
+ && sourceCodeSignatureMatches(codeSign, recipe.sourceCodeSignature);
517
+ const sourceSignatureAccepted = strictVendorSignature || exactInvalidVendorSignature;
518
+ const patchedSignatureAccepted = codeSign?.valid === true
519
+ && codeSign?.adHoc === true
520
+ && (!recipe.patchedSigningMetadata
521
+ || JSON.stringify(signingMetadata(codeSign)) === JSON.stringify(recipe.patchedSigningMetadata));
304
522
  const clean = issues.length === 0
305
523
  && asar.state === "clean"
306
524
  && integrityMatches(plistIntegrity, "SHA256", recipe.sourceHeaderSha256)
307
525
  && executableIntegrity.executableSha256 === recipe.sourceExecutableSha256
308
526
  && executableIntegrity.frameworkSha256 === recipe.sourceElectronFrameworkSha256
309
- && codeSign?.valid === true
310
- && codeSign?.adHoc === false
311
- && codeSign?.teamIdentifier === recipe.sourceTeamIdentifier;
527
+ && exactSourceBundle
528
+ && sourceSignatureAccepted;
312
529
  const patched = issues.length === 0
313
530
  && asar.state === "patched"
314
531
  && integrityMatches(plistIntegrity, "SHA256", recipe.patchedHeaderSha256)
315
- && codeSign?.valid === true
316
- && codeSign?.adHoc === true;
532
+ && patchedSignatureAccepted;
533
+ const legacy = issues.length === 0
534
+ && asar.state === "legacy"
535
+ && integrityMatches(plistIntegrity, "SHA256", recipe.legacy?.headerSha256)
536
+ && patchedSignatureAccepted;
317
537
 
318
- if (!clean && !patched) {
538
+ if (!clean && !patched && !legacy) {
319
539
  if (asar.state === "drift") issues.push("app.asar is unknown drift");
320
540
  else if (asar.state === "clean"
321
541
  && !integrityMatches(plistIntegrity, "SHA256", recipe.sourceHeaderSha256)) {
@@ -323,10 +543,20 @@ export function inspectPmStudioApp({
323
543
  } else if (asar.state === "patched"
324
544
  && !integrityMatches(plistIntegrity, "SHA256", recipe.patchedHeaderSha256)) {
325
545
  issues.push("patched ASAR does not match ElectronAsarIntegrity");
546
+ } else if (asar.state === "legacy"
547
+ && !integrityMatches(plistIntegrity, "SHA256", recipe.legacy?.headerSha256)) {
548
+ issues.push("legacy ASAR does not match ElectronAsarIntegrity");
549
+ }
550
+ if (asar.state === "clean" && recipe.sourceBundleContent && !exactSourceBundle) {
551
+ issues.push("bundle content does not match the exact clean recipe");
552
+ }
553
+ if (!codeSign?.valid && !(asar.state === "clean" && exactInvalidVendorSignature)) {
554
+ issues.push("codesign verification failed");
326
555
  }
327
- if (!codeSign?.valid) issues.push("codesign verification failed");
328
556
  else if (asar.state === "clean" && codeSign.adHoc) issues.push("clean bundle has an unexpected ad-hoc signature");
329
- else if (asar.state === "patched" && !codeSign.adHoc) issues.push("patched bundle is not ad-hoc signed");
557
+ else if (["patched", "legacy"].includes(asar.state) && !codeSign.adHoc) {
558
+ issues.push(`${asar.state} bundle is not ad-hoc signed`);
559
+ }
330
560
  else if (asar.state === "clean" && codeSign.teamIdentifier !== recipe.sourceTeamIdentifier) {
331
561
  issues.push(`clean bundle TeamIdentifier is ${codeSign.teamIdentifier || "missing"}, expected ${recipe.sourceTeamIdentifier}`);
332
562
  }
@@ -338,10 +568,15 @@ export function inspectPmStudioApp({
338
568
  && executableIntegrity.frameworkSha256 !== recipe.sourceElectronFrameworkSha256) {
339
569
  issues.push("Electron Framework hash does not match the clean recipe");
340
570
  }
571
+ if (["patched", "legacy"].includes(asar.state) && codeSign?.valid && codeSign?.adHoc
572
+ && recipe.patchedSigningMetadata
573
+ && !patchedSignatureAccepted) {
574
+ issues.push(`${asar.state} bundle signing metadata does not match the recipe`);
575
+ }
341
576
  }
342
577
 
343
578
  return {
344
- state: clean ? "clean" : patched ? "patched" : "drift",
579
+ state: clean ? "clean" : patched ? "patched" : legacy ? "legacy" : "drift",
345
580
  metadata: {
346
581
  version: String(metadata.version),
347
582
  build: String(metadata.build),
@@ -351,6 +586,10 @@ export function inspectPmStudioApp({
351
586
  plistIntegrity,
352
587
  executableIntegrity,
353
588
  codeSign,
589
+ bundleContent,
590
+ sourceVerification: clean
591
+ ? exactInvalidVendorSignature ? "exact-bundle-content" : "codesign"
592
+ : "",
354
593
  paths,
355
594
  issues,
356
595
  };
@@ -393,7 +632,8 @@ function selectRecipe(metadata, recipes) {
393
632
  }
394
633
 
395
634
  function backupName(recipe) {
396
- return `${recipe.id}-${recipe.sourceAsarSha256.slice(0, 12)}`;
635
+ const sourceHash = recipe.sourceBundleContent?.sha256 || recipe.sourceAsarSha256;
636
+ return `${recipe.id}-${sourceHash.slice(0, 12)}`;
397
637
  }
398
638
 
399
639
  export function pmStudioBackupRoot(home = os.homedir()) {
@@ -411,7 +651,7 @@ export function pmStudioPatchManifestPath({
411
651
 
412
652
  function backupManifest(recipe, createdAt) {
413
653
  return {
414
- schema_version: 1,
654
+ schema_version: recipe.sourceBundleContent ? 2 : 1,
415
655
  kind: "ccdx-pm-studio-backup",
416
656
  recipe_id: recipe.id,
417
657
  created_at: createdAt.toISOString(),
@@ -430,18 +670,38 @@ function backupManifest(recipe, createdAt) {
430
670
  electron_framework_sha256: recipe.sourceElectronFrameworkSha256,
431
671
  embedded_asar_integrity: recipe.embeddedAsarIntegrity,
432
672
  },
673
+ ...(recipe.sourceBundleContent ? { bundle_content: recipe.sourceBundleContent } : {}),
674
+ ...(recipe.sourceArtifact ? { artifact: recipe.sourceArtifact } : {}),
433
675
  },
434
676
  patched: {
435
677
  asar_sha256: recipe.patchedAsarSha256,
436
678
  asar_header_sha256: recipe.patchedHeaderSha256,
437
679
  electron_asar_integrity: { algorithm: "SHA256", hash: recipe.patchedHeaderSha256 },
438
680
  binaries: null,
681
+ ...(recipe.sourceBundleContent ? { bundle_content: null } : {}),
439
682
  },
440
683
  backup_bundle: "PM Studio.app",
441
684
  restore_rule: `Restore only while the installed app is PM Studio ${recipe.version} build ${recipe.build}.`,
442
685
  };
443
686
  }
444
687
 
688
+ function patchRecordMatches(record, recipe, state) {
689
+ const expected = state === "legacy"
690
+ ? recipe.legacy
691
+ : { asarSha256: recipe.patchedAsarSha256, headerSha256: recipe.patchedHeaderSha256 };
692
+ return Boolean(expected)
693
+ && record?.asar_sha256 === expected.asarSha256
694
+ && record?.asar_header_sha256 === expected.headerSha256
695
+ && integrityMatches(record?.electron_asar_integrity, "SHA256", expected.headerSha256);
696
+ }
697
+
698
+ function patchRecordForState(manifest, recipe, state) {
699
+ if (state === "legacy" && patchRecordMatches(manifest?.legacy_patched, recipe, state)) {
700
+ return manifest.legacy_patched;
701
+ }
702
+ return patchRecordMatches(manifest?.patched, recipe, state) ? manifest.patched : null;
703
+ }
704
+
445
705
  function validateBackup({ backupDir, recipe, operations }) {
446
706
  const manifestPath = path.join(backupDir, "manifest.json");
447
707
  const backupAppPath = path.join(backupDir, "PM Studio.app");
@@ -451,7 +711,9 @@ function validateBackup({ backupDir, recipe, operations }) {
451
711
  } catch {
452
712
  throw setupError("PM_STUDIO_BACKUP_INVALID", `Existing PM Studio backup manifest is invalid: ${manifestPath}`);
453
713
  }
454
- if (manifest?.kind !== "ccdx-pm-studio-backup"
714
+ const expectedSchema = recipe.sourceBundleContent ? 2 : 1;
715
+ if (manifest?.schema_version !== expectedSchema
716
+ || manifest?.kind !== "ccdx-pm-studio-backup"
455
717
  || manifest?.recipe_id !== recipe.id
456
718
  || manifest?.app?.bundle_identifier !== recipe.bundleIdentifier
457
719
  || manifest?.app?.version !== recipe.version
@@ -462,9 +724,12 @@ function validateBackup({ backupDir, recipe, operations }) {
462
724
  || manifest?.source?.binaries?.main_executable_sha256 !== recipe.sourceExecutableSha256
463
725
  || manifest?.source?.binaries?.electron_framework_sha256 !== recipe.sourceElectronFrameworkSha256
464
726
  || manifest?.source?.binaries?.embedded_asar_integrity !== recipe.embeddedAsarIntegrity
465
- || manifest?.patched?.asar_sha256 !== recipe.patchedAsarSha256
466
- || manifest?.patched?.asar_header_sha256 !== recipe.patchedHeaderSha256
467
- || !integrityMatches(manifest?.patched?.electron_asar_integrity, "SHA256", recipe.patchedHeaderSha256)) {
727
+ || (recipe.sourceBundleContent
728
+ && JSON.stringify(manifest?.source?.bundle_content) !== JSON.stringify(recipe.sourceBundleContent))
729
+ || (recipe.sourceArtifact
730
+ && JSON.stringify(manifest?.source?.artifact) !== JSON.stringify(recipe.sourceArtifact))
731
+ || (!patchRecordForState(manifest, recipe, "patched")
732
+ && !patchRecordForState(manifest, recipe, "legacy"))) {
468
733
  throw setupError("PM_STUDIO_BACKUP_INVALID", `Existing PM Studio backup does not match recipe ${recipe.id}`);
469
734
  }
470
735
  const inspection = inspectPmStudioApp({ appPath: backupAppPath, recipe, operations });
@@ -508,13 +773,23 @@ function ensureBackup({ appPath, backupRoot, recipe, operations }) {
508
773
  }
509
774
  }
510
775
 
511
- function writePatchedBinaryRecord({ backup, inspection, operations }) {
776
+ function writePatchedBinaryRecord({ backup, inspection, operations, recipe }) {
512
777
  const manifest = JSON.parse(fs.readFileSync(backup.manifestPath, "utf8"));
513
- manifest.patched.binaries = {
514
- main_executable_sha256: inspection.executableIntegrity.executableSha256,
515
- electron_framework_sha256: inspection.executableIntegrity.frameworkSha256,
778
+ if (!patchRecordMatches(manifest.legacy_patched, recipe, "legacy")
779
+ && patchRecordMatches(manifest.patched, recipe, "legacy")) {
780
+ manifest.legacy_patched = manifest.patched;
781
+ }
782
+ manifest.patched = {
783
+ asar_sha256: recipe.patchedAsarSha256,
784
+ asar_header_sha256: recipe.patchedHeaderSha256,
785
+ electron_asar_integrity: { algorithm: "SHA256", hash: recipe.patchedHeaderSha256 },
786
+ binaries: {
787
+ main_executable_sha256: inspection.executableIntegrity.executableSha256,
788
+ electron_framework_sha256: inspection.executableIntegrity.frameworkSha256,
789
+ },
790
+ signing_metadata: signingMetadata(inspection.codeSign),
791
+ ...(recipe.sourceBundleContent ? { bundle_content: inspection.bundleContent } : {}),
516
792
  };
517
- manifest.patched.signing_metadata = signingMetadata(inspection.codeSign);
518
793
  const temporaryPath = `${backup.manifestPath}.${operations.uuid()}.tmp`;
519
794
  try {
520
795
  fs.writeFileSync(temporaryPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
@@ -525,33 +800,55 @@ function writePatchedBinaryRecord({ backup, inspection, operations }) {
525
800
  backup.manifest = manifest;
526
801
  }
527
802
 
528
- export function assertPatchedBinaryRecord({ inspection, manifestPath, recipe }) {
803
+ export function assertPatchedBinaryRecord({
804
+ inspection,
805
+ manifestPath,
806
+ recipe,
807
+ recordState = inspection?.state === "legacy" ? "legacy" : "patched",
808
+ }) {
529
809
  let manifest;
530
810
  try {
531
811
  manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
532
812
  } catch {
533
813
  throw setupError("PM_STUDIO_PATCH_RECORD_INVALID", "PM Studio patched-binary record is missing or invalid");
534
814
  }
535
- if (recipe && (manifest?.schema_version !== 1
815
+ const expectedSchema = recipe?.sourceBundleContent ? 2 : 1;
816
+ if (recipe && (manifest?.schema_version !== expectedSchema
536
817
  || manifest?.kind !== "ccdx-pm-studio-backup"
537
818
  || manifest?.recipe_id !== recipe.id
538
819
  || manifest?.app?.bundle_identifier !== recipe.bundleIdentifier
539
820
  || manifest?.app?.version !== recipe.version
540
821
  || manifest?.app?.build !== recipe.build
541
- || manifest?.patched?.asar_sha256 !== recipe.patchedAsarSha256
542
- || manifest?.patched?.asar_header_sha256 !== recipe.patchedHeaderSha256
543
- || !integrityMatches(manifest?.patched?.electron_asar_integrity, "SHA256", recipe.patchedHeaderSha256))) {
822
+ || (recipe.sourceBundleContent && (
823
+ manifest?.source?.asar_sha256 !== recipe.sourceAsarSha256
824
+ || manifest?.source?.asar_header_sha256 !== recipe.sourceHeaderSha256
825
+ || !integrityMatches(manifest?.source?.electron_asar_integrity, "SHA256", recipe.sourceHeaderSha256)
826
+ || manifest?.source?.binaries?.main_executable_sha256 !== recipe.sourceExecutableSha256
827
+ || manifest?.source?.binaries?.electron_framework_sha256 !== recipe.sourceElectronFrameworkSha256
828
+ || manifest?.source?.binaries?.embedded_asar_integrity !== recipe.embeddedAsarIntegrity
829
+ || JSON.stringify(manifest?.source?.bundle_content) !== JSON.stringify(recipe.sourceBundleContent)
830
+ ))
831
+ || (recipe.sourceArtifact
832
+ && JSON.stringify(manifest?.source?.artifact) !== JSON.stringify(recipe.sourceArtifact)))) {
544
833
  throw setupError("PM_STUDIO_PATCH_RECORD_INVALID", "PM Studio patched-binary record does not match the installed patch recipe");
545
834
  }
546
- const binaries = manifest?.patched?.binaries;
835
+ const record = patchRecordForState(manifest, recipe, recordState);
836
+ if (!record) {
837
+ throw setupError("PM_STUDIO_PATCH_RECORD_INVALID", `PM Studio ${recordState} record does not match the installed patch recipe`);
838
+ }
839
+ const binaries = record.binaries;
547
840
  if (!binaries
548
841
  || binaries.main_executable_sha256 !== inspection.executableIntegrity.executableSha256
549
842
  || binaries.electron_framework_sha256 !== inspection.executableIntegrity.frameworkSha256) {
550
843
  throw setupError("PM_STUDIO_BUNDLE_DRIFT", "PM Studio executable or Electron Framework differs from the installed patch record");
551
844
  }
552
- if (JSON.stringify(manifest?.patched?.signing_metadata) !== JSON.stringify(signingMetadata(inspection.codeSign))) {
845
+ if (JSON.stringify(record.signing_metadata) !== JSON.stringify(signingMetadata(inspection.codeSign))) {
553
846
  throw setupError("PM_STUDIO_BUNDLE_DRIFT", "PM Studio signing metadata differs from the installed patch record");
554
847
  }
848
+ if (recipe?.sourceBundleContent
849
+ && JSON.stringify(record.bundle_content) !== JSON.stringify(inspection.bundleContent)) {
850
+ throw setupError("PM_STUDIO_BUNDLE_DRIFT", "PM Studio bundle content differs from the installed patch record");
851
+ }
555
852
  }
556
853
 
557
854
  function signingMetadata(codeSign) {
@@ -570,7 +867,15 @@ function flagsWithoutAdHoc(value) {
570
867
  return `0x${(BigInt(`0x${match[1]}`) & ~2n).toString(16)}`;
571
868
  }
572
869
 
573
- function assertSigningMetadataPreserved(source, staged) {
870
+ function assertSigningMetadataPreserved(source, staged, recipe) {
871
+ if (recipe.patchedSigningMetadata) {
872
+ if (JSON.stringify(signingMetadata(staged.codeSign))
873
+ !== JSON.stringify(recipe.patchedSigningMetadata)) {
874
+ throw setupError("PM_STUDIO_SIGNING_METADATA_CHANGED",
875
+ "Ad-hoc signing did not produce the exact PM Studio signing metadata required by the recipe");
876
+ }
877
+ return;
878
+ }
574
879
  const before = signingMetadata(source.codeSign);
575
880
  const after = signingMetadata(staged.codeSign);
576
881
  before.flags = flagsWithoutAdHoc(before.flags);
@@ -657,23 +962,53 @@ export async function runPmStudioSetup({
657
962
  emit(`[OK] PM Studio ${recipe.version} build ${recipe.build} is already patched; no backup or signing was repeated.`);
658
963
  emit(`[OK] Verified backup: ${backup.backupAppPath}`);
659
964
  emit(`[OK] Backup manifest: ${backup.manifestPath}`);
660
- emit(`[INFO] Start ${commandName} before opening PM Studio.`);
965
+ emit(`[INFO] Start ${commandName} before using Claude in PM Studio; GPT remains on PM Studio's native GitHub Copilot path.`);
661
966
  return { status: "already_patched", changed: false, recipe: recipe.id, backup, inspection: source, messages };
662
967
  }
663
- if (source.state !== "clean") {
968
+ const migratingLegacy = source.state === "legacy";
969
+ if (source.state !== "clean" && !migratingLegacy) {
664
970
  throw setupError("PM_STUDIO_BUNDLE_DRIFT",
665
- `PM Studio ${recipe.version} build ${recipe.build} does not match the clean or patched recipe; no files were changed`, source.issues);
971
+ `PM Studio ${recipe.version} build ${recipe.build} does not match the clean, split-origin, or legacy recipe; no files were changed`, source.issues);
972
+ }
973
+
974
+ if (source.sourceVerification === "exact-bundle-content") {
975
+ emit(`[WARN] macOS rejected the vendor signature; setup accepted only the exact PM Studio ${recipe.version} official bundle content fingerprint.`);
666
976
  }
667
977
 
668
978
  const backupExists = fs.existsSync(finalBackupDir);
979
+ let verifiedBackup = null;
980
+ if (migratingLegacy) {
981
+ if (!backupExists) {
982
+ throw setupError("PM_STUDIO_BACKUP_INVALID",
983
+ "Legacy PM Studio global-origin patch has no matching verified clean backup; no files were changed");
984
+ }
985
+ verifiedBackup = validateBackup({ backupDir: finalBackupDir, recipe, operations });
986
+ assertPatchedBinaryRecord({
987
+ inspection: source,
988
+ manifestPath: verifiedBackup.manifestPath,
989
+ recipe,
990
+ recordState: "legacy",
991
+ });
992
+ emit("[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.");
993
+ }
669
994
  assertSpace({ appPath, backupRoot, backupExists, operations });
670
995
 
671
996
  emit("[WARN] PM Studio's vendor signature will be replaced with an ad-hoc signature; an official update or reinstall may overwrite this patch.");
672
- const backup = ensureBackup({ appPath, backupRoot, recipe, operations });
997
+ const backup = verifiedBackup || ensureBackup({ appPath, backupRoot, recipe, operations });
673
998
  const stagePath = path.join(path.dirname(appPath), `.${path.basename(appPath)}.ccdx-stage-${operations.uuid()}`);
674
999
  let preserveStage = false;
675
1000
  try {
676
- operations.copyBundle({ source: appPath, destination: stagePath, processRunner: operations.processRunner });
1001
+ operations.copyBundle({
1002
+ source: migratingLegacy ? backup.backupAppPath : appPath,
1003
+ destination: stagePath,
1004
+ processRunner: operations.processRunner,
1005
+ });
1006
+ const stagedSource = inspectPmStudioApp({ appPath: stagePath, recipe, operations });
1007
+ if (stagedSource.state !== "clean"
1008
+ || (!migratingLegacy && recipe.sourceBundleContent
1009
+ && stagedSource.bundleContent?.sha256 !== source.bundleContent?.sha256)) {
1010
+ throw setupError("PM_STUDIO_STAGE_INVALID", "PM Studio staging copy does not match the exact clean source bundle", stagedSource.issues);
1011
+ }
677
1012
  const stageAsarPath = path.join(stagePath, recipe.asarPath);
678
1013
  const patched = patchAsarBuffer(fs.readFileSync(stageAsarPath), recipe);
679
1014
  if (!patched.changed) throw setupError("PM_STUDIO_STAGE_INVALID", "Staging copy was unexpectedly already patched");
@@ -691,14 +1026,16 @@ export async function runPmStudioSetup({
691
1026
  if (staged.state !== "patched") {
692
1027
  throw setupError("PM_STUDIO_STAGE_VERIFY_FAILED", "Patched PM Studio staging bundle failed verification", staged.issues);
693
1028
  }
694
- assertSigningMetadataPreserved(source, staged);
695
- writePatchedBinaryRecord({ backup, inspection: staged, operations });
1029
+ assertSigningMetadataPreserved(stagedSource, staged, recipe);
1030
+ writePatchedBinaryRecord({ backup, inspection: staged, operations, recipe });
696
1031
 
697
1032
  assertNoBlockingProcesses(operations, appPath);
698
1033
  const unchangedSource = inspectPmStudioApp({ appPath, recipe, operations });
699
- if (unchangedSource.state !== "clean"
1034
+ if (unchangedSource.state !== source.state
700
1035
  || unchangedSource.asar.asarSha256 !== source.asar.asarSha256
701
- || unchangedSource.asar.headerSha256 !== source.asar.headerSha256) {
1036
+ || unchangedSource.asar.headerSha256 !== source.asar.headerSha256
1037
+ || (recipe.sourceBundleContent
1038
+ && unchangedSource.bundleContent?.sha256 !== source.bundleContent?.sha256)) {
702
1039
  throw setupError("PM_STUDIO_SOURCE_CHANGED", "PM Studio changed during setup; the verified staging bundle was not installed");
703
1040
  }
704
1041
 
@@ -712,12 +1049,17 @@ export async function runPmStudioSetup({
712
1049
  `PM Studio replacement final state could not be verified. Do not launch it. ${recoveryMessage(backup)}${preserveStage ? ` Verified staging retained: ${stagePath}.` : ""}`);
713
1050
  }
714
1051
  if (installed.state !== "patched") {
715
- preserveStage = installed.state !== "clean" && fs.existsSync(stagePath);
716
- const code = installed.state === "clean"
1052
+ const originalRetained = installed.state === source.state
1053
+ && installed.asar.asarSha256 === source.asar.asarSha256
1054
+ && installed.asar.headerSha256 === source.asar.headerSha256
1055
+ && (!recipe.sourceBundleContent
1056
+ || installed.bundleContent?.sha256 === source.bundleContent?.sha256);
1057
+ preserveStage = !originalRetained && fs.existsSync(stagePath);
1058
+ const code = originalRetained
717
1059
  ? "PM_STUDIO_REPLACE_FAILED"
718
1060
  : "PM_STUDIO_INSTALL_VERIFY_FAILED";
719
- const stateMessage = installed.state === "clean"
720
- ? "The installed PM Studio remains the verified clean bundle"
1061
+ const stateMessage = originalRetained
1062
+ ? `The installed PM Studio remains the verified ${source.state} bundle`
721
1063
  : "The installed PM Studio did not retain the verified patched state; do not launch it";
722
1064
  throw setupError(code,
723
1065
  `${stateMessage}. ${recoveryMessage(backup)}${preserveStage ? ` Verified staging retained: ${stagePath}.` : ""}`,
@@ -734,7 +1076,9 @@ export async function runPmStudioSetup({
734
1076
  emit("[WARN] Atomic replacement reported an error after the exact patched bundle became installed; the final bundle was independently verified.");
735
1077
  }
736
1078
 
737
- emit(`[OK] Patched PM Studio ${recipe.version} build ${recipe.build}.`);
1079
+ emit(migratingLegacy
1080
+ ? `[OK] Migrated PM Studio ${recipe.version} build ${recipe.build} from the legacy global-origin patch to the split-origin patch.`
1081
+ : `[OK] Patched PM Studio ${recipe.version} build ${recipe.build}.`);
738
1082
  emit(`[OK] app.asar SHA-256: ${installed.asar.asarSha256}`);
739
1083
  emit(`[OK] Verified backup: ${backup.backupAppPath}${backup.reused ? " (reused)" : ""}`);
740
1084
  emit(`[OK] Backup manifest: ${backup.manifestPath}`);
@@ -742,9 +1086,9 @@ export async function runPmStudioSetup({
742
1086
  emit(`[INFO] Restore step 1/3: quit PM Studio and its updater.`);
743
1087
  emit(`[INFO] Restore step 2/3: confirm the installed Info.plist still reports version/build ${recipe.version}/${recipe.build}; never restore this backup over another version.`);
744
1088
  emit(`[INFO] Restore step 3/3: move ${appPath} aside, copy ${backup.backupAppPath} to that exact path, and verify its signature before launch.`);
745
- emit(`[INFO] Start ${commandName} before opening PM Studio.`);
1089
+ emit(`[INFO] Start ${commandName} before using Claude in PM Studio; GPT remains on PM Studio's native GitHub Copilot path.`);
746
1090
  return {
747
- status: "patched",
1091
+ status: migratingLegacy ? "migrated" : "patched",
748
1092
  changed: true,
749
1093
  recipe: recipe.id,
750
1094
  backup,