codex-copilot-dx 0.6.11 → 0.6.13

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.
@@ -9,12 +9,15 @@ import {
9
9
  } from "./auth-profile.mjs";
10
10
  import {
11
11
  PM_STUDIO_2_9_7_RECIPE,
12
+ PM_STUDIO_2_9_10_RECIPE,
12
13
  PM_STUDIO_RECIPES,
14
+ createCompatiblePmStudioRecipe,
13
15
  inspectElectronAsarIntegritySlots,
14
16
  inspectAsarBuffer,
15
17
  patchAsarBuffer,
16
18
  sha256Hex,
17
19
  } from "./pm-studio-asar.mjs";
20
+ import { withPmStudioSetupLock } from "./pm-studio-setup-lock.mjs";
18
21
 
19
22
  export const DEFAULT_PM_STUDIO_APP_PATH = "/Applications/PM Studio.app";
20
23
  export const PM_STUDIO_CLAUDE_AUTH_COMMAND = "ccdx auth login claude --reauth --github-login <personal-login>";
@@ -268,9 +271,7 @@ function defaultInspectCodeSign({ appPath, processRunner }) {
268
271
  : "";
269
272
  return {
270
273
  valid: !verify.error && verify.status === 0
271
- && !display.error && display.status === 0
272
- && !entitlementsResult.error && entitlementsResult.status === 0
273
- && hasEntitlements,
274
+ && !display.error && display.status === 0,
274
275
  verifyValid: !verify.error && verify.status === 0,
275
276
  displayValid: !display.error && display.status === 0,
276
277
  entitlementsState: hasEntitlements
@@ -315,11 +316,19 @@ function defaultInspectExecutableIntegrity({ appPath, infoPlistPath, recipe, pro
315
316
  };
316
317
  }
317
318
 
318
- function defaultSignApp({ appPath, processRunner }) {
319
- runChecked(processRunner, "/usr/bin/codesign", [
319
+ function defaultSignApp({ appPath, source, processRunner }) {
320
+ const preserve = [];
321
+ if (source?.codeSign?.displayValid === true) {
322
+ if (source.codeSign.entitlementsState === "xml") preserve.push("entitlements");
323
+ if (source.codeSign.flags) preserve.push("flags");
324
+ if (source.codeSign.runtimeVersion) preserve.push("runtime");
325
+ }
326
+ const args = [
320
327
  "--force", "--deep", "--sign", "-", "--timestamp=none",
321
- "--preserve-metadata=identifier,entitlements,flags,runtime", appPath,
322
- ], "Ad-hoc codesigning PM Studio");
328
+ ];
329
+ if (preserve.length > 0) args.push(`--preserve-metadata=${preserve.join(",")}`);
330
+ args.push(appPath);
331
+ runChecked(processRunner, "/usr/bin/codesign", args, "Ad-hoc codesigning PM Studio");
323
332
  }
324
333
 
325
334
  function defaultListBlockingProcesses({ processRunner }) {
@@ -412,6 +421,7 @@ export function createPmStudioSetupOperations(overrides = {}) {
412
421
  inspectCodeSign: defaultInspectCodeSign,
413
422
  inspectBundleContent: defaultInspectBundleContent,
414
423
  inspectExecutableIntegrity: defaultInspectExecutableIntegrity,
424
+ withSetupLock: ({ appPath }, fn) => withPmStudioSetupLock({ appPath }, fn),
415
425
  signApp: defaultSignApp,
416
426
  listBlockingProcesses: defaultListBlockingProcesses,
417
427
  copyBundle: defaultCopyBundle,
@@ -452,17 +462,6 @@ function bundleContentMatches(actual, expected) {
452
462
  && actual?.xattrCount === expected?.xattrCount;
453
463
  }
454
464
 
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
-
466
465
  export function inspectPmStudioApp({
467
466
  appPath,
468
467
  recipe = PM_STUDIO_2_9_7_RECIPE,
@@ -505,22 +504,14 @@ export function inspectPmStudioApp({
505
504
 
506
505
  const exactSourceBundle = !recipe.sourceBundleContent
507
506
  || 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
507
  const patchedSignatureAccepted = codeSign?.valid === true
519
508
  && codeSign?.adHoc === true
509
+ && codeSign?.identifier === recipe.bundleIdentifier
520
510
  && (!recipe.patchedSigningMetadata
521
511
  || JSON.stringify(signingMetadata(codeSign)) === JSON.stringify(recipe.patchedSigningMetadata));
522
512
  const predecessorSignatureAccepted = codeSign?.valid === true
523
513
  && codeSign?.adHoc === true
514
+ && codeSign?.identifier === recipe.bundleIdentifier
524
515
  && Boolean(recipe.predecessor?.signingMetadata)
525
516
  && JSON.stringify(signingMetadata(codeSign)) === JSON.stringify(recipe.predecessor.signingMetadata);
526
517
  const clean = issues.length === 0
@@ -528,8 +519,7 @@ export function inspectPmStudioApp({
528
519
  && integrityMatches(plistIntegrity, "SHA256", recipe.sourceHeaderSha256)
529
520
  && executableIntegrity.executableSha256 === recipe.sourceExecutableSha256
530
521
  && executableIntegrity.frameworkSha256 === recipe.sourceElectronFrameworkSha256
531
- && exactSourceBundle
532
- && sourceSignatureAccepted;
522
+ && exactSourceBundle;
533
523
  const patched = issues.length === 0
534
524
  && asar.state === "patched"
535
525
  && integrityMatches(plistIntegrity, "SHA256", recipe.patchedHeaderSha256)
@@ -561,15 +551,15 @@ export function inspectPmStudioApp({
561
551
  if (asar.state === "clean" && recipe.sourceBundleContent && !exactSourceBundle) {
562
552
  issues.push("bundle content does not match the exact clean recipe");
563
553
  }
564
- if (!codeSign?.valid && !(asar.state === "clean" && exactInvalidVendorSignature)) {
554
+ if (asar.state !== "clean" && !codeSign?.valid) {
565
555
  issues.push("codesign verification failed");
566
556
  }
567
- else if (asar.state === "clean" && codeSign.adHoc) issues.push("clean bundle has an unexpected ad-hoc signature");
568
557
  else if (["patched", "predecessor", "legacy"].includes(asar.state) && !codeSign.adHoc) {
569
558
  issues.push(`${asar.state} bundle is not ad-hoc signed`);
570
559
  }
571
- else if (asar.state === "clean" && codeSign.teamIdentifier !== recipe.sourceTeamIdentifier) {
572
- issues.push(`clean bundle TeamIdentifier is ${codeSign.teamIdentifier || "missing"}, expected ${recipe.sourceTeamIdentifier}`);
560
+ else if (["patched", "predecessor", "legacy"].includes(asar.state)
561
+ && codeSign.identifier !== recipe.bundleIdentifier) {
562
+ issues.push(`${asar.state} bundle identifier does not match the recipe`);
573
563
  }
574
564
  if (asar.state === "clean"
575
565
  && executableIntegrity.executableSha256 !== recipe.sourceExecutableSha256) {
@@ -605,7 +595,7 @@ export function inspectPmStudioApp({
605
595
  codeSign,
606
596
  bundleContent,
607
597
  sourceVerification: clean
608
- ? exactInvalidVendorSignature ? "exact-bundle-content" : "codesign"
598
+ ? recipe.sourceBundleContent ? "local-content" : "exact-recipe"
609
599
  : "",
610
600
  paths,
611
601
  issues,
@@ -648,6 +638,258 @@ function selectRecipe(metadata, recipes) {
648
638
  && recipe.bundleIdentifier === String(metadata.bundleIdentifier));
649
639
  }
650
640
 
641
+ function compatibleStaticOptions(metadata, executable) {
642
+ return {
643
+ version: String(metadata.version),
644
+ build: String(metadata.build),
645
+ bundleIdentifier: String(metadata.bundleIdentifier),
646
+ sourceTeamIdentifier: "local-app",
647
+ executable,
648
+ electronFrameworkPath: PM_STUDIO_2_9_10_RECIPE.electronFrameworkPath,
649
+ embeddedAsarIntegrity: PM_STUDIO_2_9_10_RECIPE.embeddedAsarIntegrity,
650
+ asarPath: PM_STUDIO_2_9_10_RECIPE.asarPath,
651
+ infoPlistPath: PM_STUDIO_2_9_10_RECIPE.infoPlistPath,
652
+ integrityKey: PM_STUDIO_2_9_10_RECIPE.integrityKey,
653
+ };
654
+ }
655
+
656
+ function compatibleSourceBundleContent({ appPath, operations }) {
657
+ const ignoredXattrs = PM_STUDIO_2_9_10_RECIPE.sourceBundleContent.ignoredXattrs;
658
+ const inspected = operations.inspectBundleContent({
659
+ ...appPaths(appPath, PM_STUDIO_2_9_10_RECIPE),
660
+ recipe: PM_STUDIO_2_9_10_RECIPE,
661
+ processRunner: operations.processRunner,
662
+ });
663
+ return {
664
+ ...inspected,
665
+ ignoredXattrs: [...ignoredXattrs],
666
+ };
667
+ }
668
+
669
+ function compatibleSourceStructure({ appPath, metadata, operations }) {
670
+ const paths = appPaths(appPath, PM_STUDIO_2_9_10_RECIPE);
671
+ const executableIntegrity = operations.inspectExecutableIntegrity({
672
+ ...paths,
673
+ recipe: PM_STUDIO_2_9_10_RECIPE,
674
+ processRunner: operations.processRunner,
675
+ });
676
+ if (!executableIntegrity?.executableName
677
+ || !executableIntegrity?.embeddedIntegrity?.supported
678
+ || executableIntegrity.embeddedIntegrity.state !== PM_STUDIO_2_9_10_RECIPE.embeddedAsarIntegrity) {
679
+ throw setupError("PM_STUDIO_ASAR_INCOMPATIBLE",
680
+ "PM Studio compatible-version discovery found an unsupported executable or embedded ASAR integrity policy");
681
+ }
682
+ const compatibleRecipe = createCompatiblePmStudioRecipe(fs.readFileSync(paths.asarPath), {
683
+ ...compatibleStaticOptions(metadata, executableIntegrity.executableName),
684
+ sourceExecutableSha256: executableIntegrity.executableSha256,
685
+ sourceElectronFrameworkSha256: executableIntegrity.frameworkSha256,
686
+ });
687
+ const plistIntegrity = operations.readAsarIntegrity({
688
+ ...paths,
689
+ recipe: compatibleRecipe,
690
+ processRunner: operations.processRunner,
691
+ });
692
+ if (!integrityMatches(plistIntegrity, "SHA256", compatibleRecipe.sourceHeaderSha256)) {
693
+ throw setupError("PM_STUDIO_ASAR_INCOMPATIBLE",
694
+ "PM Studio compatible-version discovery found mismatched ElectronAsarIntegrity metadata");
695
+ }
696
+ return { paths, executableIntegrity, compatibleRecipe };
697
+ }
698
+
699
+ function frozenBundleContent(value) {
700
+ return Object.freeze({
701
+ ...value,
702
+ ignoredXattrs: Object.freeze([...(value.ignoredXattrs || [])]),
703
+ });
704
+ }
705
+
706
+ function createCompatibleSourceRecipe({ appPath, metadata, operations }) {
707
+ const { compatibleRecipe } = compatibleSourceStructure({ appPath, metadata, operations });
708
+ const sourceBundleContent = compatibleSourceBundleContent({ appPath, operations });
709
+ return Object.freeze({
710
+ ...compatibleRecipe,
711
+ sourceBundleContent: frozenBundleContent(sourceBundleContent),
712
+ });
713
+ }
714
+
715
+ function sameIgnoredXattrs(value) {
716
+ return JSON.stringify(value)
717
+ === JSON.stringify(PM_STUDIO_2_9_10_RECIPE.sourceBundleContent.ignoredXattrs);
718
+ }
719
+
720
+
721
+ function compatibleRecipeFromManifest({ backupDir, manifest, metadata, operations }) {
722
+ if (manifest?.schema_version !== 2
723
+ || manifest?.kind !== "ccdx-pm-studio-backup"
724
+ || !String(manifest?.recipe_id || "").startsWith("pm-studio-compatible-")
725
+ || manifest?.app?.bundle_identifier !== String(metadata.bundleIdentifier)
726
+ || manifest?.app?.version !== String(metadata.version)
727
+ || manifest?.app?.build !== String(metadata.build)
728
+ || !sameIgnoredXattrs(manifest?.source?.bundle_content?.ignoredXattrs)) {
729
+ return null;
730
+ }
731
+ const backupAppPath = path.join(backupDir, "PM Studio.app");
732
+ assertInstalledAppPath(backupAppPath);
733
+ const sourceAsarPath = path.join(backupAppPath, PM_STUDIO_2_9_10_RECIPE.asarPath);
734
+ const backupPaths = appPaths(backupAppPath, PM_STUDIO_2_9_10_RECIPE);
735
+ const executableIntegrity = operations.inspectExecutableIntegrity({
736
+ ...backupPaths,
737
+ recipe: PM_STUDIO_2_9_10_RECIPE,
738
+ processRunner: operations.processRunner,
739
+ });
740
+ const compatibleRecipe = createCompatiblePmStudioRecipe(fs.readFileSync(sourceAsarPath), {
741
+ ...compatibleStaticOptions(metadata, executableIntegrity.executableName),
742
+ sourceExecutableSha256: manifest?.source?.binaries?.main_executable_sha256,
743
+ sourceElectronFrameworkSha256: manifest?.source?.binaries?.electron_framework_sha256,
744
+ });
745
+ const recipe = Object.freeze({
746
+ ...compatibleRecipe,
747
+ sourceBundleContent: frozenBundleContent(manifest.source.bundle_content),
748
+ });
749
+ if (manifest.compatibility !== recipe.compatibility
750
+ || manifest.recipe_id !== recipe.id
751
+ || manifest?.source?.asar_sha256 !== recipe.sourceAsarSha256
752
+ || manifest?.source?.asar_header_sha256 !== recipe.sourceHeaderSha256
753
+ || manifest?.patched?.asar_sha256 !== recipe.patchedAsarSha256
754
+ || manifest?.patched?.asar_header_sha256 !== recipe.patchedHeaderSha256) {
755
+ throw setupError("PM_STUDIO_BACKUP_INVALID",
756
+ `Compatible PM Studio backup does not match its reconstructed recipe: ${backupDir}`);
757
+ }
758
+ const inspection = inspectPmStudioApp({ appPath: backupAppPath, recipe, operations });
759
+ if (inspection.state !== "clean" || inspection.sourceVerification !== "local-content") {
760
+ throw setupError("PM_STUDIO_BACKUP_INVALID",
761
+ `Compatible PM Studio backup source verification failed: ${backupDir}`,
762
+ inspection.issues);
763
+ }
764
+ return recipe;
765
+ }
766
+
767
+ function compatibleRecipeFromBackup({ appPath, backupRoot, metadata, operations }) {
768
+ if (!fs.existsSync(backupRoot)) return null;
769
+ const installedAsar = fs.readFileSync(path.join(appPath, PM_STUDIO_2_9_10_RECIPE.asarPath));
770
+ let candidates = [];
771
+ const invalidCandidates = [];
772
+ for (const entry of fs.readdirSync(backupRoot, { withFileTypes: true })) {
773
+ if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
774
+ const backupDir = path.join(backupRoot, entry.name);
775
+ const manifestPath = path.join(backupDir, "manifest.json");
776
+ if (!fs.existsSync(manifestPath)) continue;
777
+ let manifest;
778
+ try {
779
+ manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
780
+ } catch {
781
+ continue;
782
+ }
783
+ let recipe;
784
+ try {
785
+ recipe = compatibleRecipeFromManifest({ backupDir, manifest, metadata, operations });
786
+ } catch (error) {
787
+ invalidCandidates.push(error);
788
+ continue;
789
+ }
790
+ if (!recipe) continue;
791
+ const asarState = inspectAsarBuffer(installedAsar, recipe).state;
792
+ if (asarState === "drift") continue;
793
+ candidates.push({ recipe, manifest, asarState });
794
+ }
795
+ if (candidates.length > 1) {
796
+ let installedBundleContent;
797
+ try {
798
+ const recipe = candidates[0].recipe;
799
+ installedBundleContent = operations.inspectBundleContent({
800
+ ...appPaths(appPath, recipe),
801
+ recipe,
802
+ processRunner: operations.processRunner,
803
+ });
804
+ } catch (error) {
805
+ throw setupError("PM_STUDIO_BACKUP_INVALID",
806
+ "PM Studio compatible backup candidates could not be distinguished safely",
807
+ [error.message]);
808
+ }
809
+ candidates = candidates.filter(({ recipe, manifest, asarState }) => bundleContentMatches(
810
+ installedBundleContent,
811
+ asarState === "clean" ? recipe.sourceBundleContent : manifest?.patched?.bundle_content,
812
+ ));
813
+ }
814
+ if (candidates.length > 1) {
815
+ throw setupError("PM_STUDIO_UNSUPPORTED_VERSION",
816
+ `PM Studio ${metadata.version} build ${metadata.build} matches multiple compatible backups; no files were changed`);
817
+ }
818
+ if (candidates.length === 1) return candidates[0].recipe;
819
+ if (invalidCandidates.length > 0) {
820
+ throw setupError("PM_STUDIO_BACKUP_INVALID",
821
+ `PM Studio ${metadata.version} build ${metadata.build} has no matching verified compatible backup`,
822
+ invalidCandidates.map((error) => error?.message || String(error)));
823
+ }
824
+ return null;
825
+ }
826
+
827
+ function selectCompatibleRecipe({ appPath, backupRoot, metadata, operations }) {
828
+ let sourceError = null;
829
+ try {
830
+ return createCompatibleSourceRecipe({ appPath, metadata, operations });
831
+ } catch (error) {
832
+ sourceError = error;
833
+ }
834
+ try {
835
+ const backupRecipe = compatibleRecipeFromBackup({ appPath, backupRoot, metadata, operations });
836
+ if (backupRecipe) return backupRecipe;
837
+ } catch (error) {
838
+ if (error?.code === "PM_STUDIO_BACKUP_INVALID") throw error;
839
+ sourceError = error;
840
+ }
841
+ throw setupError("PM_STUDIO_UNSUPPORTED_VERSION",
842
+ `PM Studio ${metadata.version} build ${metadata.build} is not structurally compatible with the safe patch; no files were changed${sourceError?.message ? `: ${sourceError.message}` : ""}`,
843
+ sourceError?.message ? [sourceError.message] : undefined);
844
+ }
845
+
846
+ export function resolvePmStudioCompatibleRecipe({
847
+ appPath = DEFAULT_PM_STUDIO_APP_PATH,
848
+ home = os.homedir(),
849
+ backupRoot = pmStudioBackupRoot(home),
850
+ metadata,
851
+ recipes = PM_STUDIO_RECIPES,
852
+ operations = createPmStudioSetupOperations(),
853
+ inspectApp = inspectPmStudioApp,
854
+ } = {}) {
855
+ if (!metadata || typeof metadata !== "object") {
856
+ throw new TypeError("PM Studio bundle metadata is required");
857
+ }
858
+ const staticRecipe = selectRecipe(metadata, recipes);
859
+ let staticInspection = null;
860
+ if (staticRecipe) {
861
+ try {
862
+ staticInspection = inspectApp({ appPath, recipe: staticRecipe, operations });
863
+ } catch {
864
+ // A metadata match is not an admission gate. Local structure remains authoritative.
865
+ }
866
+ }
867
+ try {
868
+ return selectCompatibleRecipe({
869
+ appPath,
870
+ backupRoot,
871
+ metadata,
872
+ operations,
873
+ });
874
+ } catch (error) {
875
+ if (staticInspection?.state === "drift" && error?.code === "PM_STUDIO_UNSUPPORTED_VERSION") {
876
+ throw setupError("PM_STUDIO_BUNDLE_DRIFT",
877
+ `PM Studio ${metadata.version} build ${metadata.build} differs from its known local structure; no files were changed`,
878
+ staticInspection.issues);
879
+ }
880
+ const staticSourceCanBeSigned = staticInspection?.state !== "clean"
881
+ || !staticRecipe?.patchedSigningMetadata
882
+ || (staticInspection?.codeSign?.displayValid === true
883
+ && staticInspection.codeSign.entitlementsState === "xml");
884
+ if (error?.code === "PM_STUDIO_UNSUPPORTED_VERSION"
885
+ && staticInspection?.state && staticInspection.state !== "drift"
886
+ && staticSourceCanBeSigned) {
887
+ return staticRecipe;
888
+ }
889
+ throw error;
890
+ }
891
+ }
892
+
651
893
  function backupName(recipe) {
652
894
  const sourceHash = recipe.sourceBundleContent?.sha256 || recipe.sourceAsarSha256;
653
895
  return `${recipe.id}-${sourceHash.slice(0, 12)}`;
@@ -671,6 +913,7 @@ function backupManifest(recipe, createdAt) {
671
913
  schema_version: recipe.sourceBundleContent ? 2 : 1,
672
914
  kind: "ccdx-pm-studio-backup",
673
915
  recipe_id: recipe.id,
916
+ ...(recipe.compatibility ? { compatibility: recipe.compatibility } : {}),
674
917
  created_at: createdAt.toISOString(),
675
918
  app: {
676
919
  name: "PM Studio",
@@ -688,7 +931,6 @@ function backupManifest(recipe, createdAt) {
688
931
  embedded_asar_integrity: recipe.embeddedAsarIntegrity,
689
932
  },
690
933
  ...(recipe.sourceBundleContent ? { bundle_content: recipe.sourceBundleContent } : {}),
691
- ...(recipe.sourceArtifact ? { artifact: recipe.sourceArtifact } : {}),
692
934
  },
693
935
  patched: {
694
936
  asar_sha256: recipe.patchedAsarSha256,
@@ -737,6 +979,7 @@ function validateBackup({ backupDir, recipe, operations }) {
737
979
  if (manifest?.schema_version !== expectedSchema
738
980
  || manifest?.kind !== "ccdx-pm-studio-backup"
739
981
  || manifest?.recipe_id !== recipe.id
982
+ || (recipe.compatibility && manifest?.compatibility !== recipe.compatibility)
740
983
  || manifest?.app?.bundle_identifier !== recipe.bundleIdentifier
741
984
  || manifest?.app?.version !== recipe.version
742
985
  || manifest?.app?.build !== recipe.build
@@ -748,8 +991,6 @@ function validateBackup({ backupDir, recipe, operations }) {
748
991
  || manifest?.source?.binaries?.embedded_asar_integrity !== recipe.embeddedAsarIntegrity
749
992
  || (recipe.sourceBundleContent
750
993
  && JSON.stringify(manifest?.source?.bundle_content) !== JSON.stringify(recipe.sourceBundleContent))
751
- || (recipe.sourceArtifact
752
- && JSON.stringify(manifest?.source?.artifact) !== JSON.stringify(recipe.sourceArtifact))
753
994
  || (manifest?.predecessor_patched !== undefined
754
995
  && !patchRecordMatches(manifest.predecessor_patched, recipe, "predecessor"))
755
996
  || (!patchRecordForState(manifest, recipe, "patched")
@@ -851,6 +1092,7 @@ export function assertPatchedBinaryRecord({
851
1092
  if (recipe && (manifest?.schema_version !== expectedSchema
852
1093
  || manifest?.kind !== "ccdx-pm-studio-backup"
853
1094
  || manifest?.recipe_id !== recipe.id
1095
+ || (recipe.compatibility && manifest?.compatibility !== recipe.compatibility)
854
1096
  || manifest?.app?.bundle_identifier !== recipe.bundleIdentifier
855
1097
  || manifest?.app?.version !== recipe.version
856
1098
  || manifest?.app?.build !== recipe.build
@@ -862,9 +1104,7 @@ export function assertPatchedBinaryRecord({
862
1104
  || manifest?.source?.binaries?.electron_framework_sha256 !== recipe.sourceElectronFrameworkSha256
863
1105
  || manifest?.source?.binaries?.embedded_asar_integrity !== recipe.embeddedAsarIntegrity
864
1106
  || JSON.stringify(manifest?.source?.bundle_content) !== JSON.stringify(recipe.sourceBundleContent)
865
- ))
866
- || (recipe.sourceArtifact
867
- && JSON.stringify(manifest?.source?.artifact) !== JSON.stringify(recipe.sourceArtifact)))) {
1107
+ )))) {
868
1108
  throw setupError("PM_STUDIO_PATCH_RECORD_INVALID", "PM Studio patched-binary record does not match the installed patch recipe");
869
1109
  }
870
1110
  const record = patchRecordForState(manifest, recipe, recordState);
@@ -903,21 +1143,29 @@ function flagsWithoutAdHoc(value) {
903
1143
  }
904
1144
 
905
1145
  function assertSigningMetadataPreserved(source, staged, recipe) {
1146
+ const after = signingMetadata(staged.codeSign);
1147
+ if (after.identifier !== recipe.bundleIdentifier) {
1148
+ throw setupError("PM_STUDIO_SIGNING_METADATA_CHANGED",
1149
+ "Ad-hoc signing did not use the PM Studio bundle identifier");
1150
+ }
906
1151
  if (recipe.patchedSigningMetadata) {
907
- if (JSON.stringify(signingMetadata(staged.codeSign))
908
- !== JSON.stringify(recipe.patchedSigningMetadata)) {
1152
+ if (JSON.stringify(after) !== JSON.stringify(recipe.patchedSigningMetadata)) {
909
1153
  throw setupError("PM_STUDIO_SIGNING_METADATA_CHANGED",
910
1154
  "Ad-hoc signing did not produce the exact PM Studio signing metadata required by the recipe");
911
1155
  }
912
1156
  return;
913
1157
  }
914
- const before = signingMetadata(source.codeSign);
915
- const after = signingMetadata(staged.codeSign);
916
- before.flags = flagsWithoutAdHoc(before.flags);
917
- after.flags = flagsWithoutAdHoc(after.flags);
918
- if (JSON.stringify(before) !== JSON.stringify(after)) {
1158
+ const sourceSignature = source.codeSign;
1159
+ if (sourceSignature?.displayValid !== true) return;
1160
+ const flagsChanged = sourceSignature.flags
1161
+ && flagsWithoutAdHoc(sourceSignature.flags) !== flagsWithoutAdHoc(after.flags);
1162
+ const runtimeChanged = sourceSignature.runtimeVersion
1163
+ && sourceSignature.runtimeVersion !== after.runtime_version;
1164
+ const entitlementsChanged = sourceSignature.entitlementsState === "xml"
1165
+ && sourceSignature.entitlementsSha256 !== after.entitlements_sha256;
1166
+ if (flagsChanged || runtimeChanged || entitlementsChanged) {
919
1167
  throw setupError("PM_STUDIO_SIGNING_METADATA_CHANGED",
920
- "Ad-hoc signing did not preserve PM Studio identifier, entitlements, flags, and runtime metadata");
1168
+ "Ad-hoc signing did not preserve available PM Studio entitlements, flags, and runtime metadata");
921
1169
  }
922
1170
  }
923
1171
 
@@ -950,7 +1198,7 @@ function replaceVerifiedStage({ appPath, stagePath, operations }) {
950
1198
  }
951
1199
 
952
1200
  function recoveryMessage(backup) {
953
- 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 the official App.`;
1201
+ 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.`;
954
1202
  }
955
1203
 
956
1204
  export async function runPmStudioSetup({
@@ -963,6 +1211,7 @@ export async function runPmStudioSetup({
963
1211
  logger = (line) => console.log(line),
964
1212
  } = {}) {
965
1213
  const operations = createPmStudioSetupOperations(operationOverrides);
1214
+ return operations.withSetupLock({ appPath }, async () => {
966
1215
  const messages = [];
967
1216
  const emit = (message) => {
968
1217
  messages.push(message);
@@ -980,11 +1229,13 @@ export async function runPmStudioSetup({
980
1229
  processRunner: operations.processRunner,
981
1230
  };
982
1231
  const metadata = operations.readBundleMetadata(genericPaths);
983
- const recipe = selectRecipe(metadata, recipes);
984
- if (!recipe) {
985
- throw setupError("PM_STUDIO_UNSUPPORTED_VERSION",
986
- `PM Studio ${metadata.version} build ${metadata.build} is not supported; no files were changed`);
987
- }
1232
+ const recipe = resolvePmStudioCompatibleRecipe({
1233
+ appPath,
1234
+ backupRoot,
1235
+ metadata,
1236
+ recipes,
1237
+ operations,
1238
+ });
988
1239
 
989
1240
  const source = inspectPmStudioApp({ appPath, recipe, operations });
990
1241
  const finalBackupDir = path.join(backupRoot, backupName(recipe));
@@ -1008,10 +1259,6 @@ export async function runPmStudioSetup({
1008
1259
  `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);
1009
1260
  }
1010
1261
 
1011
- if (source.sourceVerification === "exact-bundle-content") {
1012
- emit(`[WARN] macOS rejected the vendor signature; setup accepted only the exact PM Studio ${recipe.version} official bundle content fingerprint.`);
1013
- }
1014
-
1015
1262
  const backupExists = fs.existsSync(finalBackupDir);
1016
1263
  let verifiedBackup = null;
1017
1264
  if (migratingExistingPatch) {
@@ -1032,7 +1279,7 @@ export async function runPmStudioSetup({
1032
1279
  }
1033
1280
  assertSpace({ appPath, backupRoot, backupExists, operations });
1034
1281
 
1035
- emit("[WARN] PM Studio's vendor signature will be replaced with an ad-hoc signature; an official update or reinstall may overwrite this patch.");
1282
+ emit("[WARN] PM Studio will be ad-hoc signed after patching; an update or reinstall may overwrite this patch.");
1036
1283
  const backup = verifiedBackup || ensureBackup({ appPath, backupRoot, recipe, operations });
1037
1284
  const stagePath = path.join(path.dirname(appPath), `.${path.basename(appPath)}.ccdx-stage-${operations.uuid()}`);
1038
1285
  let preserveStage = false;
@@ -1059,7 +1306,12 @@ export async function runPmStudioSetup({
1059
1306
  hash: recipe.patchedHeaderSha256,
1060
1307
  processRunner: operations.processRunner,
1061
1308
  });
1062
- operations.signApp({ appPath: stagePath, recipe, processRunner: operations.processRunner });
1309
+ operations.signApp({
1310
+ appPath: stagePath,
1311
+ recipe,
1312
+ source: stagedSource,
1313
+ processRunner: operations.processRunner,
1314
+ });
1063
1315
 
1064
1316
  const staged = inspectPmStudioApp({ appPath: stagePath, recipe, operations });
1065
1317
  if (staged.state !== "patched") {
@@ -1140,4 +1392,5 @@ export async function runPmStudioSetup({
1140
1392
  } finally {
1141
1393
  if (!preserveStage && fs.existsSync(stagePath)) operations.removePath({ target: stagePath });
1142
1394
  }
1395
+ });
1143
1396
  }
@@ -14,6 +14,7 @@ import {
14
14
  PM_STUDIO_CLAUDE_AUTH_COMMAND,
15
15
  pmStudioBackupRoot,
16
16
  pmStudioPatchManifestPath,
17
+ resolvePmStudioCompatibleRecipe,
17
18
  } from "./pm-studio-setup.mjs";
18
19
  import { readAdapterStatus } from "./cli-status.mjs";
19
20
  import {
@@ -39,12 +40,6 @@ const PM_RELAY_ROUTE_NAMES = Object.freeze([
39
40
  "pm_chat_completions",
40
41
  ]);
41
42
 
42
- function selectRecipe(metadata, recipes) {
43
- return recipes.find((recipe) => recipe.version === String(metadata.version)
44
- && recipe.build === String(metadata.build)
45
- && recipe.bundleIdentifier === String(metadata.bundleIdentifier));
46
- }
47
-
48
43
  function safeMessage(error) {
49
44
  return String(error?.message || "inspection failed").replace(/\s+/g, " ").trim();
50
45
  }
@@ -146,6 +141,7 @@ export async function inspectPmStudioStatus({
146
141
  existsSync = fs.existsSync,
147
142
  inspectApp = inspectPmStudioApp,
148
143
  verifyPatchRecord = assertPatchedBinaryRecord,
144
+ resolveCompatibleRecipe = resolvePmStudioCompatibleRecipe,
149
145
  readClaudeCredentials = readAuthProfileCredentials,
150
146
  checkRunningAdapterFn = checkRunningAdapter,
151
147
  readAdapterStatusFn = readAdapterStatus,
@@ -167,7 +163,21 @@ export async function inspectPmStudioStatus({
167
163
  build: String(metadata.build),
168
164
  bundleIdentifier: String(metadata.bundleIdentifier),
169
165
  };
170
- const recipe = selectRecipe(normalized, recipes);
166
+ let recipe = null;
167
+ let compatibilityError = null;
168
+ try {
169
+ recipe = resolveCompatibleRecipe({
170
+ appPath,
171
+ home,
172
+ backupRoot,
173
+ metadata: normalized,
174
+ recipes,
175
+ operations,
176
+ inspectApp,
177
+ });
178
+ } catch (error) {
179
+ compatibilityError = error;
180
+ }
171
181
  if (recipe) {
172
182
  app = { ...inspectApp({ appPath, recipe, operations }), recipe: recipe.id };
173
183
  if (["patched", "predecessor"].includes(app.state)) {
@@ -189,7 +199,12 @@ export async function inspectPmStudioStatus({
189
199
  }
190
200
  }
191
201
  } else {
192
- app = { state: "unsupported", metadata: normalized, issues: [], recipe: "" };
202
+ app = {
203
+ state: "unsupported",
204
+ metadata: normalized,
205
+ issues: compatibilityError ? [safeMessage(compatibilityError)] : [],
206
+ recipe: "",
207
+ };
193
208
  }
194
209
  } catch (error) {
195
210
  app = { state: "error", metadata: null, issues: [safeMessage(error)], recipe: "" };
@@ -250,8 +265,8 @@ function appStatusItem(result, commandName) {
250
265
  if (app.state === "legacy") {
251
266
  return { kind: "err", component: "App patch", detail: `${version}; legacy global-origin patch`, message: `PM Studio ${version} has a legacy global-origin patch; run ${commandName} pms setup to migrate` };
252
267
  }
253
- if (app.state === "clean") return { kind: "warn", component: "App patch", detail: `${version}; supported, not patched`, message: `PM Studio ${version} is supported but not patched; run ${commandName} pms setup` };
254
- if (app.state === "unsupported") return { kind: "err", component: "App patch", detail: `${version}; unsupported version`, message: `PM Studio ${version} has no exact patch recipe; no files will be changed` };
268
+ if (app.state === "clean") return { kind: "warn", component: "App patch", detail: `${version}; compatible structure, not patched`, message: `PM Studio ${version} has a compatible local patch structure but is not patched; run ${commandName} pms setup` };
269
+ if (app.state === "unsupported") return { kind: "err", component: "App patch", detail: `${version}; structure incompatible`, message: `PM Studio ${version} does not expose one uniquely compatible patch structure; no files will be changed` };
255
270
  if (app.state === "drift" && app.patchRecipeMatched) {
256
271
  return { kind: "err", component: "App patch", detail: `${version}; patch record drift`, message: `PM Studio ${version} matches the patch recipe, but its installed patch record is not verified: ${app.issues.join("; ")}` };
257
272
  }