codex-copilot-dx 0.6.11 → 0.6.12
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 +44 -33
- package/package.json +1 -1
- package/src/cli-options.mjs +2 -2
- package/src/doctor.mjs +1 -1
- package/src/pm-studio-asar.mjs +156 -0
- package/src/pm-studio-provenance.mjs +701 -0
- package/src/pm-studio-setup-lock.mjs +336 -0
- package/src/pm-studio-setup.mjs +456 -5
- package/src/pm-studio-status.mjs +25 -3
package/src/pm-studio-setup.mjs
CHANGED
|
@@ -9,12 +9,19 @@ 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 {
|
|
21
|
+
PM_STUDIO_OFFICIAL_REPOSITORY,
|
|
22
|
+
verifyOfficialPmStudioProvenance,
|
|
23
|
+
} from "./pm-studio-provenance.mjs";
|
|
24
|
+
import { withPmStudioSetupLock } from "./pm-studio-setup-lock.mjs";
|
|
18
25
|
|
|
19
26
|
export const DEFAULT_PM_STUDIO_APP_PATH = "/Applications/PM Studio.app";
|
|
20
27
|
export const PM_STUDIO_CLAUDE_AUTH_COMMAND = "ccdx auth login claude --reauth --github-login <personal-login>";
|
|
@@ -315,6 +322,27 @@ function defaultInspectExecutableIntegrity({ appPath, infoPlistPath, recipe, pro
|
|
|
315
322
|
};
|
|
316
323
|
}
|
|
317
324
|
|
|
325
|
+
function defaultReadAppArchitecture({ executablePath, processRunner }) {
|
|
326
|
+
const output = runChecked(processRunner, "/usr/bin/lipo", ["-archs", executablePath],
|
|
327
|
+
"Reading PM Studio executable architecture").stdout.trim();
|
|
328
|
+
const architectures = output.split(/\s+/).filter(Boolean);
|
|
329
|
+
if (architectures.length !== 1 || !["arm64", "x86_64"].includes(architectures[0])) {
|
|
330
|
+
throw setupError("PM_STUDIO_ARCHITECTURE_UNSUPPORTED",
|
|
331
|
+
"PM Studio must contain exactly one supported macOS architecture");
|
|
332
|
+
}
|
|
333
|
+
return architectures[0] === "x86_64" ? "x64" : architectures[0];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function defaultVerifyOfficialProvenance(options) {
|
|
337
|
+
return verifyOfficialPmStudioProvenance({
|
|
338
|
+
...options,
|
|
339
|
+
extractor: async ({ archivePath, destination }) => {
|
|
340
|
+
runChecked(options.processRunner, "/usr/bin/ditto", ["-x", "-k", archivePath, destination],
|
|
341
|
+
"Extracting official PM Studio release");
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
318
346
|
function defaultSignApp({ appPath, processRunner }) {
|
|
319
347
|
runChecked(processRunner, "/usr/bin/codesign", [
|
|
320
348
|
"--force", "--deep", "--sign", "-", "--timestamp=none",
|
|
@@ -412,6 +440,9 @@ export function createPmStudioSetupOperations(overrides = {}) {
|
|
|
412
440
|
inspectCodeSign: defaultInspectCodeSign,
|
|
413
441
|
inspectBundleContent: defaultInspectBundleContent,
|
|
414
442
|
inspectExecutableIntegrity: defaultInspectExecutableIntegrity,
|
|
443
|
+
readAppArchitecture: defaultReadAppArchitecture,
|
|
444
|
+
verifyOfficialProvenance: defaultVerifyOfficialProvenance,
|
|
445
|
+
withSetupLock: ({ appPath }, fn) => withPmStudioSetupLock({ appPath }, fn),
|
|
415
446
|
signApp: defaultSignApp,
|
|
416
447
|
listBlockingProcesses: defaultListBlockingProcesses,
|
|
417
448
|
copyBundle: defaultCopyBundle,
|
|
@@ -507,6 +538,7 @@ export function inspectPmStudioApp({
|
|
|
507
538
|
|| bundleContentMatches(bundleContent, recipe.sourceBundleContent);
|
|
508
539
|
const strictVendorSignature = codeSign?.valid === true
|
|
509
540
|
&& codeSign?.adHoc === false
|
|
541
|
+
&& codeSign?.identifier === recipe.bundleIdentifier
|
|
510
542
|
&& codeSign?.teamIdentifier === recipe.sourceTeamIdentifier;
|
|
511
543
|
const exactInvalidVendorSignature = recipe.sourceBundleContent
|
|
512
544
|
&& exactSourceBundle
|
|
@@ -648,6 +680,385 @@ function selectRecipe(metadata, recipes) {
|
|
|
648
680
|
&& recipe.bundleIdentifier === String(metadata.bundleIdentifier));
|
|
649
681
|
}
|
|
650
682
|
|
|
683
|
+
function compatibleStaticOptions(metadata) {
|
|
684
|
+
return {
|
|
685
|
+
version: String(metadata.version),
|
|
686
|
+
build: String(metadata.build),
|
|
687
|
+
bundleIdentifier: String(metadata.bundleIdentifier),
|
|
688
|
+
sourceTeamIdentifier: PM_STUDIO_2_9_10_RECIPE.sourceTeamIdentifier,
|
|
689
|
+
executable: PM_STUDIO_2_9_10_RECIPE.executable,
|
|
690
|
+
electronFrameworkPath: PM_STUDIO_2_9_10_RECIPE.electronFrameworkPath,
|
|
691
|
+
embeddedAsarIntegrity: PM_STUDIO_2_9_10_RECIPE.embeddedAsarIntegrity,
|
|
692
|
+
asarPath: PM_STUDIO_2_9_10_RECIPE.asarPath,
|
|
693
|
+
infoPlistPath: PM_STUDIO_2_9_10_RECIPE.infoPlistPath,
|
|
694
|
+
integrityKey: PM_STUDIO_2_9_10_RECIPE.integrityKey,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function compatibleSourceBundleContent({ appPath, operations }) {
|
|
699
|
+
const ignoredXattrs = PM_STUDIO_2_9_10_RECIPE.sourceBundleContent.ignoredXattrs;
|
|
700
|
+
const inspected = operations.inspectBundleContent({
|
|
701
|
+
...appPaths(appPath, PM_STUDIO_2_9_10_RECIPE),
|
|
702
|
+
recipe: PM_STUDIO_2_9_10_RECIPE,
|
|
703
|
+
processRunner: operations.processRunner,
|
|
704
|
+
});
|
|
705
|
+
return {
|
|
706
|
+
...inspected,
|
|
707
|
+
ignoredXattrs: [...ignoredXattrs],
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function compatibleSourceStructure({ appPath, metadata, operations }) {
|
|
712
|
+
const paths = appPaths(appPath, PM_STUDIO_2_9_10_RECIPE);
|
|
713
|
+
if (String(metadata.bundleIdentifier) !== PM_STUDIO_2_9_10_RECIPE.bundleIdentifier) {
|
|
714
|
+
throw setupError("PM_STUDIO_COMPATIBLE_SOURCE_UNTRUSTED",
|
|
715
|
+
"PM Studio compatible-version discovery found an unexpected bundle identifier");
|
|
716
|
+
}
|
|
717
|
+
const executableIntegrity = operations.inspectExecutableIntegrity({
|
|
718
|
+
...paths,
|
|
719
|
+
recipe: PM_STUDIO_2_9_10_RECIPE,
|
|
720
|
+
processRunner: operations.processRunner,
|
|
721
|
+
});
|
|
722
|
+
if (!executableIntegrity?.matchesRecipeName
|
|
723
|
+
|| !executableIntegrity?.embeddedIntegrity?.supported
|
|
724
|
+
|| executableIntegrity.embeddedIntegrity.state !== PM_STUDIO_2_9_10_RECIPE.embeddedAsarIntegrity) {
|
|
725
|
+
throw setupError("PM_STUDIO_COMPATIBLE_SOURCE_UNTRUSTED",
|
|
726
|
+
"PM Studio compatible-version discovery found an unsupported executable or embedded ASAR integrity policy");
|
|
727
|
+
}
|
|
728
|
+
const compatibleRecipe = createCompatiblePmStudioRecipe(fs.readFileSync(paths.asarPath), {
|
|
729
|
+
...compatibleStaticOptions(metadata),
|
|
730
|
+
sourceExecutableSha256: executableIntegrity.executableSha256,
|
|
731
|
+
sourceElectronFrameworkSha256: executableIntegrity.frameworkSha256,
|
|
732
|
+
});
|
|
733
|
+
const plistIntegrity = operations.readAsarIntegrity({
|
|
734
|
+
...paths,
|
|
735
|
+
recipe: compatibleRecipe,
|
|
736
|
+
processRunner: operations.processRunner,
|
|
737
|
+
});
|
|
738
|
+
if (!integrityMatches(plistIntegrity, "SHA256", compatibleRecipe.sourceHeaderSha256)) {
|
|
739
|
+
throw setupError("PM_STUDIO_COMPATIBLE_SOURCE_UNTRUSTED",
|
|
740
|
+
"PM Studio compatible-version discovery found mismatched ElectronAsarIntegrity metadata");
|
|
741
|
+
}
|
|
742
|
+
return { paths, executableIntegrity, compatibleRecipe };
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function frozenBundleContent(value) {
|
|
746
|
+
return Object.freeze({
|
|
747
|
+
...value,
|
|
748
|
+
ignoredXattrs: Object.freeze([...(value.ignoredXattrs || [])]),
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function sourceCodeSignature(codeSign) {
|
|
753
|
+
return Object.freeze({
|
|
754
|
+
identifier: codeSign.identifier,
|
|
755
|
+
teamIdentifier: codeSign.teamIdentifier,
|
|
756
|
+
flags: codeSign.flags,
|
|
757
|
+
runtimeVersion: codeSign.runtimeVersion,
|
|
758
|
+
cdHashFull: codeSign.cdHashFull,
|
|
759
|
+
notarizationTicket: codeSign.notarizationTicket,
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function createCompatibleSourceRecipe({ appPath, metadata, operations }) {
|
|
764
|
+
const { paths, compatibleRecipe } = compatibleSourceStructure({ appPath, metadata, operations });
|
|
765
|
+
const codeSign = operations.inspectCodeSign({
|
|
766
|
+
...paths,
|
|
767
|
+
recipe: PM_STUDIO_2_9_10_RECIPE,
|
|
768
|
+
processRunner: operations.processRunner,
|
|
769
|
+
});
|
|
770
|
+
if (codeSign?.valid !== true
|
|
771
|
+
|| codeSign?.adHoc !== false
|
|
772
|
+
|| codeSign?.identifier !== PM_STUDIO_2_9_10_RECIPE.bundleIdentifier
|
|
773
|
+
|| codeSign?.teamIdentifier !== PM_STUDIO_2_9_10_RECIPE.sourceTeamIdentifier) {
|
|
774
|
+
throw setupError("PM_STUDIO_COMPATIBLE_SOURCE_UNTRUSTED",
|
|
775
|
+
"PM Studio compatible-version discovery requires an intact vendor signature");
|
|
776
|
+
}
|
|
777
|
+
const sourceBundleContent = compatibleSourceBundleContent({ appPath, operations });
|
|
778
|
+
return Object.freeze({
|
|
779
|
+
...compatibleRecipe,
|
|
780
|
+
sourceBundleContent: frozenBundleContent(sourceBundleContent),
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function exactInvalidVendorSignature(codeSign) {
|
|
785
|
+
return codeSign?.valid === false
|
|
786
|
+
&& codeSign?.verifyValid === false
|
|
787
|
+
&& codeSign?.displayValid === true
|
|
788
|
+
&& codeSign?.entitlementsState === "invalid"
|
|
789
|
+
&& codeSign?.adHoc === false
|
|
790
|
+
&& codeSign?.identifier === PM_STUDIO_2_9_10_RECIPE.bundleIdentifier
|
|
791
|
+
&& codeSign?.teamIdentifier === PM_STUDIO_2_9_10_RECIPE.sourceTeamIdentifier
|
|
792
|
+
&& typeof codeSign?.flags === "string" && codeSign.flags.length > 0
|
|
793
|
+
&& typeof codeSign?.runtimeVersion === "string" && codeSign.runtimeVersion.length > 0
|
|
794
|
+
&& /^[0-9a-f]{64}$/.test(codeSign?.cdHashFull || "")
|
|
795
|
+
&& codeSign?.notarizationTicket === "stapled";
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function officialArtifact(provenance, metadata, architecture) {
|
|
799
|
+
if (provenance?.verified !== true
|
|
800
|
+
|| provenance.repository !== PM_STUDIO_OFFICIAL_REPOSITORY
|
|
801
|
+
|| provenance.version !== String(metadata.version)
|
|
802
|
+
|| provenance.tag !== `v${metadata.version}`
|
|
803
|
+
|| provenance.arch !== architecture
|
|
804
|
+
|| provenance.comparison?.matched !== true
|
|
805
|
+
|| typeof provenance.releaseUrl !== "string"
|
|
806
|
+
|| typeof provenance.asset?.name !== "string"
|
|
807
|
+
|| !Number.isSafeInteger(provenance.asset?.size)
|
|
808
|
+
|| provenance.asset.size <= 0
|
|
809
|
+
|| !/^[0-9a-f]{64}$/.test(provenance.asset?.sha256 || "")) {
|
|
810
|
+
throw setupError("PM_STUDIO_COMPATIBLE_SOURCE_UNTRUSTED",
|
|
811
|
+
"Official PM Studio release verification returned incomplete evidence");
|
|
812
|
+
}
|
|
813
|
+
return Object.freeze({
|
|
814
|
+
repository: provenance.repository,
|
|
815
|
+
tag: provenance.tag,
|
|
816
|
+
arch: provenance.arch,
|
|
817
|
+
releaseUrl: provenance.releaseUrl,
|
|
818
|
+
asset: Object.freeze({
|
|
819
|
+
name: provenance.asset.name,
|
|
820
|
+
size: provenance.asset.size,
|
|
821
|
+
sha256: provenance.asset.sha256,
|
|
822
|
+
}),
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
async function createOfficialCompatibleSourceRecipe({ appPath, metadata, operations }) {
|
|
827
|
+
const { paths, executableIntegrity, compatibleRecipe } = compatibleSourceStructure({
|
|
828
|
+
appPath, metadata, operations,
|
|
829
|
+
});
|
|
830
|
+
const codeSign = operations.inspectCodeSign({
|
|
831
|
+
...paths,
|
|
832
|
+
recipe: PM_STUDIO_2_9_10_RECIPE,
|
|
833
|
+
processRunner: operations.processRunner,
|
|
834
|
+
});
|
|
835
|
+
if (!exactInvalidVendorSignature(codeSign)) {
|
|
836
|
+
throw setupError("PM_STUDIO_COMPATIBLE_SOURCE_UNTRUSTED",
|
|
837
|
+
"PM Studio has neither an intact vendor signature nor the exact invalid-signature shape accepted for official artifact verification");
|
|
838
|
+
}
|
|
839
|
+
const architecture = operations.readAppArchitecture({
|
|
840
|
+
...paths,
|
|
841
|
+
executablePath: executableIntegrity.executablePath,
|
|
842
|
+
processRunner: operations.processRunner,
|
|
843
|
+
});
|
|
844
|
+
const sourceBundleContent = compatibleSourceBundleContent({ appPath, operations });
|
|
845
|
+
const provenance = await operations.verifyOfficialProvenance({
|
|
846
|
+
installedAppPath: appPath,
|
|
847
|
+
version: String(metadata.version),
|
|
848
|
+
arch: architecture,
|
|
849
|
+
processRunner: operations.processRunner,
|
|
850
|
+
});
|
|
851
|
+
return Object.freeze({
|
|
852
|
+
...compatibleRecipe,
|
|
853
|
+
sourceBundleContent: frozenBundleContent(sourceBundleContent),
|
|
854
|
+
sourceArtifact: officialArtifact(provenance, metadata, architecture),
|
|
855
|
+
sourceCodeSignature: sourceCodeSignature(codeSign),
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function sameIgnoredXattrs(value) {
|
|
860
|
+
return JSON.stringify(value)
|
|
861
|
+
=== JSON.stringify(PM_STUDIO_2_9_10_RECIPE.sourceBundleContent.ignoredXattrs);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function storedOfficialArtifact(value, metadata) {
|
|
865
|
+
const version = String(metadata.version);
|
|
866
|
+
const arch = value?.arch;
|
|
867
|
+
const assetName = `PM-Studio-${version}-mac-${arch}.zip`;
|
|
868
|
+
if (!value || Array.isArray(value)
|
|
869
|
+
|| JSON.stringify(Object.keys(value).sort())
|
|
870
|
+
!== JSON.stringify(["arch", "asset", "releaseUrl", "repository", "tag"])
|
|
871
|
+
|| value.repository !== PM_STUDIO_OFFICIAL_REPOSITORY
|
|
872
|
+
|| value.tag !== `v${version}`
|
|
873
|
+
|| !["arm64", "x64"].includes(arch)
|
|
874
|
+
|| value.releaseUrl !== `https://github.com/${PM_STUDIO_OFFICIAL_REPOSITORY}/releases/tag/v${version}`
|
|
875
|
+
|| !value.asset || Array.isArray(value.asset)
|
|
876
|
+
|| JSON.stringify(Object.keys(value.asset).sort())
|
|
877
|
+
!== JSON.stringify(["name", "sha256", "size"])
|
|
878
|
+
|| value.asset.name !== assetName
|
|
879
|
+
|| !Number.isSafeInteger(value.asset.size)
|
|
880
|
+
|| value.asset.size <= 0
|
|
881
|
+
|| !/^[0-9a-f]{64}$/.test(value.asset.sha256 || "")) {
|
|
882
|
+
throw setupError("PM_STUDIO_BACKUP_INVALID",
|
|
883
|
+
"Compatible PM Studio backup has invalid official release evidence");
|
|
884
|
+
}
|
|
885
|
+
return Object.freeze({
|
|
886
|
+
...value,
|
|
887
|
+
asset: Object.freeze({ ...value.asset }),
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function storedSourceCodeSignature(value) {
|
|
892
|
+
if (!value || Array.isArray(value)
|
|
893
|
+
|| JSON.stringify(Object.keys(value).sort()) !== JSON.stringify([
|
|
894
|
+
"cdHashFull",
|
|
895
|
+
"flags",
|
|
896
|
+
"identifier",
|
|
897
|
+
"notarizationTicket",
|
|
898
|
+
"runtimeVersion",
|
|
899
|
+
"teamIdentifier",
|
|
900
|
+
])
|
|
901
|
+
|| value.identifier !== PM_STUDIO_2_9_10_RECIPE.bundleIdentifier
|
|
902
|
+
|| value.teamIdentifier !== PM_STUDIO_2_9_10_RECIPE.sourceTeamIdentifier
|
|
903
|
+
|| typeof value.flags !== "string" || value.flags.length === 0
|
|
904
|
+
|| typeof value.runtimeVersion !== "string" || value.runtimeVersion.length === 0
|
|
905
|
+
|| !/^[0-9a-f]{64}$/.test(value.cdHashFull || "")
|
|
906
|
+
|| value.notarizationTicket !== "stapled") {
|
|
907
|
+
throw setupError("PM_STUDIO_BACKUP_INVALID",
|
|
908
|
+
"Compatible PM Studio backup has invalid source signing evidence");
|
|
909
|
+
}
|
|
910
|
+
return Object.freeze({ ...value });
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function compatibleRecipeFromManifest({ backupDir, manifest, metadata, operations }) {
|
|
914
|
+
if (manifest?.schema_version !== 2
|
|
915
|
+
|| manifest?.kind !== "ccdx-pm-studio-backup"
|
|
916
|
+
|| !String(manifest?.recipe_id || "").startsWith("pm-studio-compatible-")
|
|
917
|
+
|| manifest?.app?.bundle_identifier !== String(metadata.bundleIdentifier)
|
|
918
|
+
|| manifest?.app?.version !== String(metadata.version)
|
|
919
|
+
|| manifest?.app?.build !== String(metadata.build)
|
|
920
|
+
|| !sameIgnoredXattrs(manifest?.source?.bundle_content?.ignoredXattrs)) {
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
923
|
+
const backupAppPath = path.join(backupDir, "PM Studio.app");
|
|
924
|
+
assertInstalledAppPath(backupAppPath);
|
|
925
|
+
const sourceAsarPath = path.join(backupAppPath, PM_STUDIO_2_9_10_RECIPE.asarPath);
|
|
926
|
+
const compatibleRecipe = createCompatiblePmStudioRecipe(fs.readFileSync(sourceAsarPath), {
|
|
927
|
+
...compatibleStaticOptions(metadata),
|
|
928
|
+
sourceExecutableSha256: manifest?.source?.binaries?.main_executable_sha256,
|
|
929
|
+
sourceElectronFrameworkSha256: manifest?.source?.binaries?.electron_framework_sha256,
|
|
930
|
+
});
|
|
931
|
+
const hasOfficialArtifact = manifest?.source?.artifact !== undefined;
|
|
932
|
+
const hasSourceCodeSignature = manifest?.source?.code_signature !== undefined;
|
|
933
|
+
if (hasOfficialArtifact !== hasSourceCodeSignature) {
|
|
934
|
+
throw setupError("PM_STUDIO_BACKUP_INVALID",
|
|
935
|
+
"Compatible PM Studio backup has incomplete official source evidence");
|
|
936
|
+
}
|
|
937
|
+
const recipe = Object.freeze({
|
|
938
|
+
...compatibleRecipe,
|
|
939
|
+
sourceBundleContent: frozenBundleContent(manifest.source.bundle_content),
|
|
940
|
+
...(hasOfficialArtifact ? {
|
|
941
|
+
sourceArtifact: storedOfficialArtifact(manifest.source.artifact, metadata),
|
|
942
|
+
sourceCodeSignature: storedSourceCodeSignature(manifest.source.code_signature),
|
|
943
|
+
} : {}),
|
|
944
|
+
});
|
|
945
|
+
if (manifest.compatibility !== recipe.compatibility
|
|
946
|
+
|| manifest.recipe_id !== recipe.id
|
|
947
|
+
|| manifest?.source?.asar_sha256 !== recipe.sourceAsarSha256
|
|
948
|
+
|| manifest?.source?.asar_header_sha256 !== recipe.sourceHeaderSha256
|
|
949
|
+
|| manifest?.patched?.asar_sha256 !== recipe.patchedAsarSha256
|
|
950
|
+
|| manifest?.patched?.asar_header_sha256 !== recipe.patchedHeaderSha256) {
|
|
951
|
+
throw setupError("PM_STUDIO_BACKUP_INVALID",
|
|
952
|
+
`Compatible PM Studio backup does not match its reconstructed recipe: ${backupDir}`);
|
|
953
|
+
}
|
|
954
|
+
const inspection = inspectPmStudioApp({ appPath: backupAppPath, recipe, operations });
|
|
955
|
+
const expectedVerification = hasOfficialArtifact ? "exact-bundle-content" : "codesign";
|
|
956
|
+
if (inspection.state !== "clean" || inspection.sourceVerification !== expectedVerification) {
|
|
957
|
+
throw setupError("PM_STUDIO_BACKUP_INVALID",
|
|
958
|
+
`Compatible PM Studio backup source verification failed: ${backupDir}`,
|
|
959
|
+
inspection.issues);
|
|
960
|
+
}
|
|
961
|
+
return recipe;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function compatibleRecipeFromBackup({ appPath, backupRoot, metadata, operations }) {
|
|
965
|
+
if (!fs.existsSync(backupRoot)) return null;
|
|
966
|
+
const installedAsar = fs.readFileSync(path.join(appPath, PM_STUDIO_2_9_10_RECIPE.asarPath));
|
|
967
|
+
let candidates = [];
|
|
968
|
+
const invalidCandidates = [];
|
|
969
|
+
for (const entry of fs.readdirSync(backupRoot, { withFileTypes: true })) {
|
|
970
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
|
|
971
|
+
const backupDir = path.join(backupRoot, entry.name);
|
|
972
|
+
const manifestPath = path.join(backupDir, "manifest.json");
|
|
973
|
+
if (!fs.existsSync(manifestPath)) continue;
|
|
974
|
+
let manifest;
|
|
975
|
+
try {
|
|
976
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
977
|
+
} catch {
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
let recipe;
|
|
981
|
+
try {
|
|
982
|
+
recipe = compatibleRecipeFromManifest({ backupDir, manifest, metadata, operations });
|
|
983
|
+
} catch (error) {
|
|
984
|
+
invalidCandidates.push(error);
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
if (!recipe) continue;
|
|
988
|
+
const asarState = inspectAsarBuffer(installedAsar, recipe).state;
|
|
989
|
+
if (asarState === "drift") continue;
|
|
990
|
+
candidates.push({ recipe, manifest, asarState });
|
|
991
|
+
}
|
|
992
|
+
if (candidates.length > 1) {
|
|
993
|
+
let installedBundleContent;
|
|
994
|
+
try {
|
|
995
|
+
const recipe = candidates[0].recipe;
|
|
996
|
+
installedBundleContent = operations.inspectBundleContent({
|
|
997
|
+
...appPaths(appPath, recipe),
|
|
998
|
+
recipe,
|
|
999
|
+
processRunner: operations.processRunner,
|
|
1000
|
+
});
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
throw setupError("PM_STUDIO_BACKUP_INVALID",
|
|
1003
|
+
"PM Studio compatible backup candidates could not be distinguished safely",
|
|
1004
|
+
[error.message]);
|
|
1005
|
+
}
|
|
1006
|
+
candidates = candidates.filter(({ recipe, manifest, asarState }) => bundleContentMatches(
|
|
1007
|
+
installedBundleContent,
|
|
1008
|
+
asarState === "clean" ? recipe.sourceBundleContent : manifest?.patched?.bundle_content,
|
|
1009
|
+
));
|
|
1010
|
+
}
|
|
1011
|
+
if (candidates.length > 1) {
|
|
1012
|
+
throw setupError("PM_STUDIO_UNSUPPORTED_VERSION",
|
|
1013
|
+
`PM Studio ${metadata.version} build ${metadata.build} matches multiple compatible backups; no files were changed`);
|
|
1014
|
+
}
|
|
1015
|
+
if (candidates.length === 1) return candidates[0].recipe;
|
|
1016
|
+
if (invalidCandidates.length > 0) {
|
|
1017
|
+
throw setupError("PM_STUDIO_BACKUP_INVALID",
|
|
1018
|
+
`PM Studio ${metadata.version} build ${metadata.build} has no matching verified compatible backup`,
|
|
1019
|
+
invalidCandidates.map((error) => error?.message || String(error)));
|
|
1020
|
+
}
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function selectCompatibleRecipe({ appPath, backupRoot, metadata, operations }) {
|
|
1025
|
+
let sourceError = null;
|
|
1026
|
+
try {
|
|
1027
|
+
return createCompatibleSourceRecipe({ appPath, metadata, operations });
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
sourceError = error;
|
|
1030
|
+
}
|
|
1031
|
+
try {
|
|
1032
|
+
const backupRecipe = compatibleRecipeFromBackup({ appPath, backupRoot, metadata, operations });
|
|
1033
|
+
if (backupRecipe) return backupRecipe;
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
if (error?.code === "PM_STUDIO_BACKUP_INVALID") throw error;
|
|
1036
|
+
sourceError = error;
|
|
1037
|
+
}
|
|
1038
|
+
throw setupError("PM_STUDIO_UNSUPPORTED_VERSION",
|
|
1039
|
+
`PM Studio ${metadata.version} build ${metadata.build} is not structurally compatible with the safe patch; no files were changed`,
|
|
1040
|
+
sourceError?.message ? [sourceError.message] : undefined);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
export function resolvePmStudioCompatibleRecipe({
|
|
1044
|
+
appPath = DEFAULT_PM_STUDIO_APP_PATH,
|
|
1045
|
+
home = os.homedir(),
|
|
1046
|
+
backupRoot = pmStudioBackupRoot(home),
|
|
1047
|
+
metadata,
|
|
1048
|
+
recipes = PM_STUDIO_RECIPES,
|
|
1049
|
+
operations = createPmStudioSetupOperations(),
|
|
1050
|
+
} = {}) {
|
|
1051
|
+
if (!metadata || typeof metadata !== "object") {
|
|
1052
|
+
throw new TypeError("PM Studio bundle metadata is required");
|
|
1053
|
+
}
|
|
1054
|
+
return selectRecipe(metadata, recipes) || selectCompatibleRecipe({
|
|
1055
|
+
appPath,
|
|
1056
|
+
backupRoot,
|
|
1057
|
+
metadata,
|
|
1058
|
+
operations,
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
|
|
651
1062
|
function backupName(recipe) {
|
|
652
1063
|
const sourceHash = recipe.sourceBundleContent?.sha256 || recipe.sourceAsarSha256;
|
|
653
1064
|
return `${recipe.id}-${sourceHash.slice(0, 12)}`;
|
|
@@ -671,6 +1082,7 @@ function backupManifest(recipe, createdAt) {
|
|
|
671
1082
|
schema_version: recipe.sourceBundleContent ? 2 : 1,
|
|
672
1083
|
kind: "ccdx-pm-studio-backup",
|
|
673
1084
|
recipe_id: recipe.id,
|
|
1085
|
+
...(recipe.compatibility ? { compatibility: recipe.compatibility } : {}),
|
|
674
1086
|
created_at: createdAt.toISOString(),
|
|
675
1087
|
app: {
|
|
676
1088
|
name: "PM Studio",
|
|
@@ -689,6 +1101,7 @@ function backupManifest(recipe, createdAt) {
|
|
|
689
1101
|
},
|
|
690
1102
|
...(recipe.sourceBundleContent ? { bundle_content: recipe.sourceBundleContent } : {}),
|
|
691
1103
|
...(recipe.sourceArtifact ? { artifact: recipe.sourceArtifact } : {}),
|
|
1104
|
+
...(recipe.sourceCodeSignature ? { code_signature: recipe.sourceCodeSignature } : {}),
|
|
692
1105
|
},
|
|
693
1106
|
patched: {
|
|
694
1107
|
asar_sha256: recipe.patchedAsarSha256,
|
|
@@ -737,6 +1150,7 @@ function validateBackup({ backupDir, recipe, operations }) {
|
|
|
737
1150
|
if (manifest?.schema_version !== expectedSchema
|
|
738
1151
|
|| manifest?.kind !== "ccdx-pm-studio-backup"
|
|
739
1152
|
|| manifest?.recipe_id !== recipe.id
|
|
1153
|
+
|| (recipe.compatibility && manifest?.compatibility !== recipe.compatibility)
|
|
740
1154
|
|| manifest?.app?.bundle_identifier !== recipe.bundleIdentifier
|
|
741
1155
|
|| manifest?.app?.version !== recipe.version
|
|
742
1156
|
|| manifest?.app?.build !== recipe.build
|
|
@@ -750,6 +1164,8 @@ function validateBackup({ backupDir, recipe, operations }) {
|
|
|
750
1164
|
&& JSON.stringify(manifest?.source?.bundle_content) !== JSON.stringify(recipe.sourceBundleContent))
|
|
751
1165
|
|| (recipe.sourceArtifact
|
|
752
1166
|
&& JSON.stringify(manifest?.source?.artifact) !== JSON.stringify(recipe.sourceArtifact))
|
|
1167
|
+
|| (recipe.compatibility && recipe.sourceCodeSignature
|
|
1168
|
+
&& JSON.stringify(manifest?.source?.code_signature) !== JSON.stringify(recipe.sourceCodeSignature))
|
|
753
1169
|
|| (manifest?.predecessor_patched !== undefined
|
|
754
1170
|
&& !patchRecordMatches(manifest.predecessor_patched, recipe, "predecessor"))
|
|
755
1171
|
|| (!patchRecordForState(manifest, recipe, "patched")
|
|
@@ -851,6 +1267,7 @@ export function assertPatchedBinaryRecord({
|
|
|
851
1267
|
if (recipe && (manifest?.schema_version !== expectedSchema
|
|
852
1268
|
|| manifest?.kind !== "ccdx-pm-studio-backup"
|
|
853
1269
|
|| manifest?.recipe_id !== recipe.id
|
|
1270
|
+
|| (recipe.compatibility && manifest?.compatibility !== recipe.compatibility)
|
|
854
1271
|
|| manifest?.app?.bundle_identifier !== recipe.bundleIdentifier
|
|
855
1272
|
|| manifest?.app?.version !== recipe.version
|
|
856
1273
|
|| manifest?.app?.build !== recipe.build
|
|
@@ -864,7 +1281,9 @@ export function assertPatchedBinaryRecord({
|
|
|
864
1281
|
|| JSON.stringify(manifest?.source?.bundle_content) !== JSON.stringify(recipe.sourceBundleContent)
|
|
865
1282
|
))
|
|
866
1283
|
|| (recipe.sourceArtifact
|
|
867
|
-
&& JSON.stringify(manifest?.source?.artifact) !== JSON.stringify(recipe.sourceArtifact))
|
|
1284
|
+
&& JSON.stringify(manifest?.source?.artifact) !== JSON.stringify(recipe.sourceArtifact))
|
|
1285
|
+
|| (recipe.compatibility && recipe.sourceCodeSignature
|
|
1286
|
+
&& JSON.stringify(manifest?.source?.code_signature) !== JSON.stringify(recipe.sourceCodeSignature)))) {
|
|
868
1287
|
throw setupError("PM_STUDIO_PATCH_RECORD_INVALID", "PM Studio patched-binary record does not match the installed patch recipe");
|
|
869
1288
|
}
|
|
870
1289
|
const record = patchRecordForState(manifest, recipe, recordState);
|
|
@@ -915,6 +1334,17 @@ function assertSigningMetadataPreserved(source, staged, recipe) {
|
|
|
915
1334
|
const after = signingMetadata(staged.codeSign);
|
|
916
1335
|
before.flags = flagsWithoutAdHoc(before.flags);
|
|
917
1336
|
after.flags = flagsWithoutAdHoc(after.flags);
|
|
1337
|
+
if (recipe.compatibility
|
|
1338
|
+
&& recipe.sourceArtifact
|
|
1339
|
+
&& source.sourceVerification === "exact-bundle-content"
|
|
1340
|
+
&& source.codeSign?.entitlementsState === "invalid") {
|
|
1341
|
+
if (staged.codeSign?.entitlementsState !== "xml"
|
|
1342
|
+
|| !/^[0-9a-f]{64}$/.test(after.entitlements_sha256)) {
|
|
1343
|
+
throw setupError("PM_STUDIO_SIGNING_METADATA_CHANGED",
|
|
1344
|
+
"Ad-hoc signing did not produce verifiable PM Studio entitlements from the official source bundle");
|
|
1345
|
+
}
|
|
1346
|
+
before.entitlements_sha256 = after.entitlements_sha256;
|
|
1347
|
+
}
|
|
918
1348
|
if (JSON.stringify(before) !== JSON.stringify(after)) {
|
|
919
1349
|
throw setupError("PM_STUDIO_SIGNING_METADATA_CHANGED",
|
|
920
1350
|
"Ad-hoc signing did not preserve PM Studio identifier, entitlements, flags, and runtime metadata");
|
|
@@ -963,6 +1393,7 @@ export async function runPmStudioSetup({
|
|
|
963
1393
|
logger = (line) => console.log(line),
|
|
964
1394
|
} = {}) {
|
|
965
1395
|
const operations = createPmStudioSetupOperations(operationOverrides);
|
|
1396
|
+
return operations.withSetupLock({ appPath }, async () => {
|
|
966
1397
|
const messages = [];
|
|
967
1398
|
const emit = (message) => {
|
|
968
1399
|
messages.push(message);
|
|
@@ -980,10 +1411,27 @@ export async function runPmStudioSetup({
|
|
|
980
1411
|
processRunner: operations.processRunner,
|
|
981
1412
|
};
|
|
982
1413
|
const metadata = operations.readBundleMetadata(genericPaths);
|
|
983
|
-
|
|
1414
|
+
let recipe = selectRecipe(metadata, recipes);
|
|
984
1415
|
if (!recipe) {
|
|
985
|
-
|
|
986
|
-
|
|
1416
|
+
try {
|
|
1417
|
+
recipe = resolvePmStudioCompatibleRecipe({
|
|
1418
|
+
appPath,
|
|
1419
|
+
backupRoot,
|
|
1420
|
+
metadata,
|
|
1421
|
+
recipes,
|
|
1422
|
+
operations,
|
|
1423
|
+
});
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
if (error?.code !== "PM_STUDIO_UNSUPPORTED_VERSION") throw error;
|
|
1426
|
+
emit(`[INFO] Verifying PM Studio ${metadata.version} build ${metadata.build} against its official GitHub release before patching.`);
|
|
1427
|
+
try {
|
|
1428
|
+
recipe = await createOfficialCompatibleSourceRecipe({ appPath, metadata, operations });
|
|
1429
|
+
} catch (sourceError) {
|
|
1430
|
+
throw setupError("PM_STUDIO_UNSUPPORTED_VERSION",
|
|
1431
|
+
`PM Studio ${metadata.version} build ${metadata.build} could not be safely verified as a structurally compatible official release; no files were changed`,
|
|
1432
|
+
sourceError?.message ? [sourceError.message] : undefined);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
987
1435
|
}
|
|
988
1436
|
|
|
989
1437
|
const source = inspectPmStudioApp({ appPath, recipe, operations });
|
|
@@ -1009,7 +1457,9 @@ export async function runPmStudioSetup({
|
|
|
1009
1457
|
}
|
|
1010
1458
|
|
|
1011
1459
|
if (source.sourceVerification === "exact-bundle-content") {
|
|
1012
|
-
emit(
|
|
1460
|
+
emit(recipe.sourceArtifact
|
|
1461
|
+
? `[WARN] macOS rejected the vendor signature; setup accepted PM Studio ${recipe.version} only after its files matched the SHA-256-verified official GitHub release.`
|
|
1462
|
+
: `[WARN] macOS rejected the vendor signature; setup accepted only the exact PM Studio ${recipe.version} official bundle content fingerprint.`);
|
|
1013
1463
|
}
|
|
1014
1464
|
|
|
1015
1465
|
const backupExists = fs.existsSync(finalBackupDir);
|
|
@@ -1140,4 +1590,5 @@ export async function runPmStudioSetup({
|
|
|
1140
1590
|
} finally {
|
|
1141
1591
|
if (!preserveStage && fs.existsSync(stagePath)) operations.removePath({ target: stagePath });
|
|
1142
1592
|
}
|
|
1593
|
+
});
|
|
1143
1594
|
}
|
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 {
|
|
@@ -146,6 +147,7 @@ export async function inspectPmStudioStatus({
|
|
|
146
147
|
existsSync = fs.existsSync,
|
|
147
148
|
inspectApp = inspectPmStudioApp,
|
|
148
149
|
verifyPatchRecord = assertPatchedBinaryRecord,
|
|
150
|
+
resolveCompatibleRecipe = resolvePmStudioCompatibleRecipe,
|
|
149
151
|
readClaudeCredentials = readAuthProfileCredentials,
|
|
150
152
|
checkRunningAdapterFn = checkRunningAdapter,
|
|
151
153
|
readAdapterStatusFn = readAdapterStatus,
|
|
@@ -167,7 +169,22 @@ export async function inspectPmStudioStatus({
|
|
|
167
169
|
build: String(metadata.build),
|
|
168
170
|
bundleIdentifier: String(metadata.bundleIdentifier),
|
|
169
171
|
};
|
|
170
|
-
|
|
172
|
+
let recipe = selectRecipe(normalized, recipes);
|
|
173
|
+
let compatibilityError = null;
|
|
174
|
+
if (!recipe) {
|
|
175
|
+
try {
|
|
176
|
+
recipe = resolveCompatibleRecipe({
|
|
177
|
+
appPath,
|
|
178
|
+
home,
|
|
179
|
+
backupRoot,
|
|
180
|
+
metadata: normalized,
|
|
181
|
+
recipes,
|
|
182
|
+
operations,
|
|
183
|
+
});
|
|
184
|
+
} catch (error) {
|
|
185
|
+
compatibilityError = error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
171
188
|
if (recipe) {
|
|
172
189
|
app = { ...inspectApp({ appPath, recipe, operations }), recipe: recipe.id };
|
|
173
190
|
if (["patched", "predecessor"].includes(app.state)) {
|
|
@@ -189,7 +206,12 @@ export async function inspectPmStudioStatus({
|
|
|
189
206
|
}
|
|
190
207
|
}
|
|
191
208
|
} else {
|
|
192
|
-
app = {
|
|
209
|
+
app = {
|
|
210
|
+
state: "unsupported",
|
|
211
|
+
metadata: normalized,
|
|
212
|
+
issues: compatibilityError ? [safeMessage(compatibilityError)] : [],
|
|
213
|
+
recipe: "",
|
|
214
|
+
};
|
|
193
215
|
}
|
|
194
216
|
} catch (error) {
|
|
195
217
|
app = { state: "error", metadata: null, issues: [safeMessage(error)], recipe: "" };
|
|
@@ -251,7 +273,7 @@ function appStatusItem(result, commandName) {
|
|
|
251
273
|
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
274
|
}
|
|
253
275
|
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};
|
|
276
|
+
if (app.state === "unsupported") return { kind: "err", component: "App patch", detail: `${version}; compatibility unverified`, message: `PM Studio ${version} compatibility cannot be safely verified; no files will be changed` };
|
|
255
277
|
if (app.state === "drift" && app.patchRecipeMatched) {
|
|
256
278
|
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
279
|
}
|