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.
- package/README.md +37 -36
- package/package.json +1 -1
- package/src/cli-options.mjs +2 -2
- package/src/doctor.mjs +2 -2
- package/src/pm-studio-asar.mjs +274 -4
- package/src/pm-studio-setup-lock.mjs +336 -0
- package/src/pm-studio-setup.mjs +314 -61
- package/src/pm-studio-status.mjs +25 -10
package/src/pm-studio-setup.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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 (
|
|
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 (
|
|
572
|
-
|
|
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
|
-
?
|
|
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(
|
|
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
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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
|
|
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
|
|
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 =
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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
|
|
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({
|
|
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
|
}
|
package/src/pm-studio-status.mjs
CHANGED
|
@@ -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
|
-
|
|
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 = {
|
|
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};
|
|
254
|
-
if (app.state === "unsupported") return { kind: "err", component: "App patch", detail: `${version};
|
|
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
|
}
|