codex-copilot-dx 0.6.15 → 0.6.19
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 +28 -15
- package/bin/cli.mjs +7 -1
- package/package.json +1 -1
- package/src/adapter.mjs +47 -10
- package/src/anthropic-handler.mjs +25 -6
- package/src/anthropic.mjs +19 -7
- package/src/atomic-file.mjs +76 -2
- package/src/auth-profile.mjs +11 -34
- package/src/auth.mjs +265 -52
- package/src/cli-auth.mjs +124 -94
- package/src/cli-error.mjs +44 -0
- package/src/cli-options.mjs +8 -5
- package/src/copilot-client.mjs +5 -1
- package/src/copilot-responses-compat.mjs +9 -2
- package/src/copilot.mjs +2 -1
- package/src/http-transport.mjs +138 -0
- package/src/lock.mjs +199 -17
- package/src/models.mjs +17 -0
- package/src/pm-studio-asar.mjs +399 -19
- package/src/pm-studio-relay.mjs +26 -5
- package/src/pm-studio-setup.mjs +351 -16
- package/src/pm-studio-status.mjs +5 -1
- package/src/response-history.mjs +122 -13
- package/src/responses-bridge.mjs +22 -4
- package/src/responses-handler.mjs +123 -36
- package/src/responses-image-pressure.mjs +30 -0
- package/src/responses-proxy.mjs +13 -1
- package/src/responses-request.mjs +17 -5
- package/src/runtime-config.mjs +10 -0
- package/src/token-counter-service.mjs +188 -0
- package/src/token-counter-worker.mjs +10 -0
- package/src/usage.mjs +25 -7
package/src/pm-studio-setup.mjs
CHANGED
|
@@ -12,7 +12,10 @@ import {
|
|
|
12
12
|
PM_STUDIO_2_9_10_RECIPE,
|
|
13
13
|
PM_STUDIO_RECIPES,
|
|
14
14
|
createCompatiblePmStudioRecipe,
|
|
15
|
+
createCompatiblePmStudioRecipeFromFile,
|
|
16
|
+
inspectAsarFile,
|
|
15
17
|
inspectElectronAsarIntegritySlots,
|
|
18
|
+
inspectElectronAsarIntegrityFile,
|
|
16
19
|
inspectAsarBuffer,
|
|
17
20
|
patchAsarBuffer,
|
|
18
21
|
sha256Hex,
|
|
@@ -57,10 +60,10 @@ function runChecked(processRunner, command, args, label) {
|
|
|
57
60
|
return result;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
function hashRegularFile(filePath, expected) {
|
|
63
|
+
function hashRegularFile(filePath, expected, readBuffer) {
|
|
61
64
|
const hash = createHash("sha256");
|
|
62
65
|
const file = fs.openSync(filePath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
63
|
-
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
66
|
+
const buffer = readBuffer || Buffer.allocUnsafe(1024 * 1024);
|
|
64
67
|
try {
|
|
65
68
|
const before = fs.fstatSync(file, { bigint: true });
|
|
66
69
|
if (before.dev !== BigInt(expected.dev)
|
|
@@ -136,6 +139,7 @@ function parseXattrHex(output, root, ignoredXattrs) {
|
|
|
136
139
|
export function inspectBundleContent(appPath, {
|
|
137
140
|
xattrOutput = "",
|
|
138
141
|
ignoredXattrs = [],
|
|
142
|
+
readBuffer,
|
|
139
143
|
} = {}) {
|
|
140
144
|
const root = path.resolve(appPath);
|
|
141
145
|
const rootStat = fs.lstatSync(root);
|
|
@@ -186,7 +190,7 @@ export function inspectBundleContent(appPath, {
|
|
|
186
190
|
}
|
|
187
191
|
regularFileCount += 1;
|
|
188
192
|
regularBytes += stat.size;
|
|
189
|
-
records.push(["file", relative, mode, stat.size, hashRegularFile(fullPath, stat)]);
|
|
193
|
+
records.push(["file", relative, mode, stat.size, hashRegularFile(fullPath, stat, readBuffer)]);
|
|
190
194
|
} else {
|
|
191
195
|
throw setupError("PM_STUDIO_BUNDLE_ENTRY_INVALID", `PM Studio contains an unsupported entry: ${relative}`);
|
|
192
196
|
}
|
|
@@ -208,12 +212,13 @@ export function inspectBundleContent(appPath, {
|
|
|
208
212
|
};
|
|
209
213
|
}
|
|
210
214
|
|
|
211
|
-
function defaultInspectBundleContent({ appPath, recipe, processRunner }) {
|
|
215
|
+
function defaultInspectBundleContent({ appPath, recipe, processRunner, readBuffer }) {
|
|
212
216
|
const xattrs = runChecked(processRunner, "/usr/bin/xattr", ["-r", "-s", "-x", "-l", appPath],
|
|
213
217
|
"Reading PM Studio extended attributes");
|
|
214
218
|
return inspectBundleContent(appPath, {
|
|
215
219
|
xattrOutput: xattrs.stdout,
|
|
216
220
|
ignoredXattrs: recipe.sourceBundleContent?.ignoredXattrs || [],
|
|
221
|
+
readBuffer,
|
|
217
222
|
});
|
|
218
223
|
}
|
|
219
224
|
|
|
@@ -316,6 +321,59 @@ function defaultInspectExecutableIntegrity({ appPath, infoPlistPath, recipe, pro
|
|
|
316
321
|
};
|
|
317
322
|
}
|
|
318
323
|
|
|
324
|
+
function statusInspectExecutableIntegrity({ appPath, infoPlistPath, recipe, processRunner, evidenceCache }) {
|
|
325
|
+
const executableName = plistRead(processRunner, infoPlistPath, "CFBundleExecutable");
|
|
326
|
+
if (!executableName || path.basename(executableName) !== executableName) {
|
|
327
|
+
throw setupError("PM_STUDIO_EXECUTABLE_INVALID", "PM Studio CFBundleExecutable is invalid");
|
|
328
|
+
}
|
|
329
|
+
const executablePath = path.join(appPath, "Contents", "MacOS", executableName);
|
|
330
|
+
const frameworkPath = path.join(appPath, recipe.electronFrameworkPath);
|
|
331
|
+
const executable = inspectElectronAsarIntegrityFile(executablePath, { cache: evidenceCache });
|
|
332
|
+
const framework = inspectElectronAsarIntegrityFile(frameworkPath, { cache: evidenceCache });
|
|
333
|
+
return {
|
|
334
|
+
executableName,
|
|
335
|
+
executablePath,
|
|
336
|
+
executableSha256: executable.sha256,
|
|
337
|
+
frameworkPath,
|
|
338
|
+
frameworkSha256: framework.sha256,
|
|
339
|
+
embeddedIntegrity: {
|
|
340
|
+
state: executable.integrity.slots.length === 0 && framework.integrity.slots.length === 0 ? "absent" : "present",
|
|
341
|
+
supported: executable.integrity.slots.length === 0 && framework.integrity.slots.length === 0,
|
|
342
|
+
executable: executable.integrity,
|
|
343
|
+
framework: framework.integrity,
|
|
344
|
+
},
|
|
345
|
+
matchesRecipeName: executableName === recipe.executable,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function createPmStudioFileInspectionOperationOverrides() {
|
|
350
|
+
const evidenceCache = new Map();
|
|
351
|
+
const bundleReadBuffer = Buffer.allocUnsafe(1024 * 1024);
|
|
352
|
+
return {
|
|
353
|
+
inspectExecutableIntegrity: (args) => statusInspectExecutableIntegrity({
|
|
354
|
+
...args,
|
|
355
|
+
evidenceCache,
|
|
356
|
+
}),
|
|
357
|
+
inspectAsarFile: ({ asarPath, recipe }) => inspectAsarFile(asarPath, recipe, {
|
|
358
|
+
cache: evidenceCache,
|
|
359
|
+
releaseTargets: true,
|
|
360
|
+
}),
|
|
361
|
+
createCompatibleRecipeFromFile: ({ asarPath, options }) => createCompatiblePmStudioRecipeFromFile(
|
|
362
|
+
asarPath,
|
|
363
|
+
options,
|
|
364
|
+
{ cache: evidenceCache, releaseTargets: true },
|
|
365
|
+
),
|
|
366
|
+
inspectBundleContent: (args) => defaultInspectBundleContent({
|
|
367
|
+
...args,
|
|
368
|
+
readBuffer: bundleReadBuffer,
|
|
369
|
+
}),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function createPmStudioStatusOperationOverrides() {
|
|
374
|
+
return createPmStudioFileInspectionOperationOverrides();
|
|
375
|
+
}
|
|
376
|
+
|
|
319
377
|
function defaultSignApp({ appPath, source, processRunner }) {
|
|
320
378
|
const preserve = [];
|
|
321
379
|
if (source?.codeSign?.displayValid === true) {
|
|
@@ -411,6 +469,17 @@ function defaultAssertPermissions({ appPath, backupRoot }) {
|
|
|
411
469
|
}
|
|
412
470
|
}
|
|
413
471
|
|
|
472
|
+
function defaultAssertRestorePermissions({ appPath, backupAppPath }) {
|
|
473
|
+
try {
|
|
474
|
+
fs.accessSync(appPath, fs.constants.R_OK | fs.constants.X_OK);
|
|
475
|
+
fs.accessSync(path.dirname(appPath), fs.constants.W_OK | fs.constants.X_OK);
|
|
476
|
+
fs.accessSync(backupAppPath, fs.constants.R_OK | fs.constants.X_OK);
|
|
477
|
+
} catch {
|
|
478
|
+
throw setupError("PM_STUDIO_PERMISSION_DENIED",
|
|
479
|
+
"PM Studio or its verified backup is not accessible to the current user; restore will not use sudo");
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
414
483
|
export function createPmStudioSetupOperations(overrides = {}) {
|
|
415
484
|
const processRunner = overrides.processRunner || defaultProcessRunner;
|
|
416
485
|
return {
|
|
@@ -432,6 +501,7 @@ export function createPmStudioSetupOperations(overrides = {}) {
|
|
|
432
501
|
availableBytes: defaultAvailableBytes,
|
|
433
502
|
volumeId: defaultVolumeId,
|
|
434
503
|
assertPermissions: defaultAssertPermissions,
|
|
504
|
+
assertRestorePermissions: defaultAssertRestorePermissions,
|
|
435
505
|
readClaudeCredentials: ({ home }) => readAuthProfileCredentials(AUTH_PROFILE_CLAUDE, { home }),
|
|
436
506
|
now: () => new Date(),
|
|
437
507
|
uuid: () => randomUUID(),
|
|
@@ -470,7 +540,9 @@ export function inspectPmStudioApp({
|
|
|
470
540
|
const paths = appPaths(appPath, recipe);
|
|
471
541
|
const issues = [];
|
|
472
542
|
const metadata = operations.readBundleMetadata({ ...paths, recipe, processRunner: operations.processRunner });
|
|
473
|
-
const asar =
|
|
543
|
+
const asar = operations.inspectAsarFile
|
|
544
|
+
? operations.inspectAsarFile({ ...paths, recipe, processRunner: operations.processRunner })
|
|
545
|
+
: inspectAsarBuffer(fs.readFileSync(paths.asarPath), recipe);
|
|
474
546
|
const plistIntegrity = operations.readAsarIntegrity({ ...paths, recipe, processRunner: operations.processRunner });
|
|
475
547
|
const executableIntegrity = operations.inspectExecutableIntegrity({
|
|
476
548
|
...paths, recipe, processRunner: operations.processRunner,
|
|
@@ -679,11 +751,14 @@ function compatibleSourceStructure({ appPath, metadata, operations }) {
|
|
|
679
751
|
throw setupError("PM_STUDIO_ASAR_INCOMPATIBLE",
|
|
680
752
|
"PM Studio compatible-version discovery found an unsupported executable or embedded ASAR integrity policy");
|
|
681
753
|
}
|
|
682
|
-
const
|
|
754
|
+
const recipeOptions = {
|
|
683
755
|
...compatibleStaticOptions(metadata, executableIntegrity.executableName),
|
|
684
756
|
sourceExecutableSha256: executableIntegrity.executableSha256,
|
|
685
757
|
sourceElectronFrameworkSha256: executableIntegrity.frameworkSha256,
|
|
686
|
-
}
|
|
758
|
+
};
|
|
759
|
+
const compatibleRecipe = operations.createCompatibleRecipeFromFile
|
|
760
|
+
? operations.createCompatibleRecipeFromFile({ asarPath: paths.asarPath, options: recipeOptions })
|
|
761
|
+
: createCompatiblePmStudioRecipe(fs.readFileSync(paths.asarPath), recipeOptions);
|
|
687
762
|
const plistIntegrity = operations.readAsarIntegrity({
|
|
688
763
|
...paths,
|
|
689
764
|
recipe: compatibleRecipe,
|
|
@@ -737,11 +812,14 @@ function compatibleRecipeFromManifest({ backupDir, manifest, metadata, operation
|
|
|
737
812
|
recipe: PM_STUDIO_2_9_10_RECIPE,
|
|
738
813
|
processRunner: operations.processRunner,
|
|
739
814
|
});
|
|
740
|
-
const
|
|
815
|
+
const recipeOptions = {
|
|
741
816
|
...compatibleStaticOptions(metadata, executableIntegrity.executableName),
|
|
742
817
|
sourceExecutableSha256: manifest?.source?.binaries?.main_executable_sha256,
|
|
743
818
|
sourceElectronFrameworkSha256: manifest?.source?.binaries?.electron_framework_sha256,
|
|
744
|
-
}
|
|
819
|
+
};
|
|
820
|
+
const compatibleRecipe = operations.createCompatibleRecipeFromFile
|
|
821
|
+
? operations.createCompatibleRecipeFromFile({ asarPath: sourceAsarPath, options: recipeOptions })
|
|
822
|
+
: createCompatiblePmStudioRecipe(fs.readFileSync(sourceAsarPath), recipeOptions);
|
|
745
823
|
const recipe = Object.freeze({
|
|
746
824
|
...compatibleRecipe,
|
|
747
825
|
sourceBundleContent: frozenBundleContent(manifest.source.bundle_content),
|
|
@@ -766,7 +844,8 @@ function compatibleRecipeFromManifest({ backupDir, manifest, metadata, operation
|
|
|
766
844
|
|
|
767
845
|
function compatibleRecipeFromBackup({ appPath, backupRoot, metadata, operations }) {
|
|
768
846
|
if (!fs.existsSync(backupRoot)) return null;
|
|
769
|
-
const
|
|
847
|
+
const installedAsarPath = path.join(appPath, PM_STUDIO_2_9_10_RECIPE.asarPath);
|
|
848
|
+
const installedAsar = operations.inspectAsarFile ? null : fs.readFileSync(installedAsarPath);
|
|
770
849
|
let candidates = [];
|
|
771
850
|
const invalidCandidates = [];
|
|
772
851
|
for (const entry of fs.readdirSync(backupRoot, { withFileTypes: true })) {
|
|
@@ -788,7 +867,9 @@ function compatibleRecipeFromBackup({ appPath, backupRoot, metadata, operations
|
|
|
788
867
|
continue;
|
|
789
868
|
}
|
|
790
869
|
if (!recipe) continue;
|
|
791
|
-
const asarState =
|
|
870
|
+
const asarState = operations.inspectAsarFile
|
|
871
|
+
? operations.inspectAsarFile({ asarPath: installedAsarPath, recipe }).state
|
|
872
|
+
: inspectAsarBuffer(installedAsar, recipe).state;
|
|
792
873
|
if (asarState === "drift") continue;
|
|
793
874
|
candidates.push({ recipe, manifest, asarState });
|
|
794
875
|
}
|
|
@@ -966,7 +1047,7 @@ function patchRecordForState(manifest, recipe, state) {
|
|
|
966
1047
|
return patchRecordMatches(manifest?.patched, recipe, state) ? manifest.patched : null;
|
|
967
1048
|
}
|
|
968
1049
|
|
|
969
|
-
function validateBackup({ backupDir, recipe, operations }) {
|
|
1050
|
+
function validateBackup({ backupDir, recipe, operations, evidence }) {
|
|
970
1051
|
const manifestPath = path.join(backupDir, "manifest.json");
|
|
971
1052
|
const backupAppPath = path.join(backupDir, "PM Studio.app");
|
|
972
1053
|
let manifest;
|
|
@@ -1002,6 +1083,9 @@ function validateBackup({ backupDir, recipe, operations }) {
|
|
|
1002
1083
|
if (inspection.state !== "clean") {
|
|
1003
1084
|
throw setupError("PM_STUDIO_BACKUP_INVALID", "Existing PM Studio backup is not an exact clean bundle", inspection.issues);
|
|
1004
1085
|
}
|
|
1086
|
+
if (evidence) {
|
|
1087
|
+
evidence.inspection = inspection;
|
|
1088
|
+
}
|
|
1005
1089
|
return { backupDir, backupAppPath, manifestPath, manifest, reused: true };
|
|
1006
1090
|
}
|
|
1007
1091
|
|
|
@@ -1188,6 +1272,78 @@ function assertSpace({ appPath, backupRoot, backupExists, operations }) {
|
|
|
1188
1272
|
}
|
|
1189
1273
|
}
|
|
1190
1274
|
|
|
1275
|
+
function assertRestoreSpace({ appPath, backupAppPath, operations }) {
|
|
1276
|
+
const bundleBytes = operations.bundleSize(backupAppPath);
|
|
1277
|
+
if (!Number.isFinite(bundleBytes) || bundleBytes <= 0) {
|
|
1278
|
+
throw setupError("PM_STUDIO_SIZE_INVALID", "Could not determine verified PM Studio backup size");
|
|
1279
|
+
}
|
|
1280
|
+
if (operations.availableBytes(path.dirname(appPath)) < bundleBytes + MIN_FREE_MARGIN_BYTES) {
|
|
1281
|
+
throw setupError("PM_STUDIO_SPACE_INSUFFICIENT", "Not enough free space for PM Studio restore staging");
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function codeSignFingerprint(codeSign) {
|
|
1286
|
+
return {
|
|
1287
|
+
valid: codeSign?.valid === true,
|
|
1288
|
+
verifyValid: codeSign?.verifyValid === true,
|
|
1289
|
+
displayValid: codeSign?.displayValid === true,
|
|
1290
|
+
entitlementsState: codeSign?.entitlementsState || "",
|
|
1291
|
+
adHoc: codeSign?.adHoc === true,
|
|
1292
|
+
identifier: codeSign?.identifier || "",
|
|
1293
|
+
teamIdentifier: codeSign?.teamIdentifier || "",
|
|
1294
|
+
flags: codeSign?.flags || "",
|
|
1295
|
+
runtimeVersion: codeSign?.runtimeVersion || "",
|
|
1296
|
+
cdHashFull: codeSign?.cdHashFull || "",
|
|
1297
|
+
notarizationTicket: codeSign?.notarizationTicket || "",
|
|
1298
|
+
entitlementsSha256: codeSign?.entitlementsSha256 || "",
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
function restoreStateFingerprint(inspection, bundleContent) {
|
|
1303
|
+
return JSON.stringify({
|
|
1304
|
+
state: inspection.state,
|
|
1305
|
+
metadata: inspection.metadata,
|
|
1306
|
+
asar: {
|
|
1307
|
+
state: inspection.asar?.state,
|
|
1308
|
+
asarSha256: inspection.asar?.asarSha256,
|
|
1309
|
+
headerSha256: inspection.asar?.headerSha256,
|
|
1310
|
+
},
|
|
1311
|
+
plistIntegrity: inspection.plistIntegrity,
|
|
1312
|
+
executableIntegrity: {
|
|
1313
|
+
executableName: inspection.executableIntegrity?.executableName,
|
|
1314
|
+
executableSha256: inspection.executableIntegrity?.executableSha256,
|
|
1315
|
+
frameworkSha256: inspection.executableIntegrity?.frameworkSha256,
|
|
1316
|
+
embeddedIntegrity: inspection.executableIntegrity?.embeddedIntegrity,
|
|
1317
|
+
matchesRecipeName: inspection.executableIntegrity?.matchesRecipeName,
|
|
1318
|
+
},
|
|
1319
|
+
codeSign: codeSignFingerprint(inspection.codeSign),
|
|
1320
|
+
bundleContent,
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function inspectRestoreState({ appPath, recipe, operations }) {
|
|
1325
|
+
const inspection = inspectPmStudioApp({ appPath, recipe, operations });
|
|
1326
|
+
return restoreStateFromInspection({ appPath, inspection, recipe, operations });
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
function restoreStateFromInspection({ appPath, inspection, recipe, operations }) {
|
|
1330
|
+
const bundleContent = inspection.bundleContent || operations.inspectBundleContent({
|
|
1331
|
+
...appPaths(appPath, recipe),
|
|
1332
|
+
recipe,
|
|
1333
|
+
processRunner: operations.processRunner,
|
|
1334
|
+
});
|
|
1335
|
+
return {
|
|
1336
|
+
inspection,
|
|
1337
|
+
fingerprint: restoreStateFingerprint(inspection, bundleContent),
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
function assertRestoreStateMatches(actual, expected, code, message) {
|
|
1342
|
+
if (actual.fingerprint !== expected.fingerprint) {
|
|
1343
|
+
throw setupError(code, message, actual.inspection.issues);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1191
1347
|
function replaceVerifiedStage({ appPath, stagePath, operations }) {
|
|
1192
1348
|
try {
|
|
1193
1349
|
operations.replaceApp({ appPath, stagePath, processRunner: operations.processRunner });
|
|
@@ -1210,7 +1366,10 @@ export async function runPmStudioSetup({
|
|
|
1210
1366
|
operations: operationOverrides = {},
|
|
1211
1367
|
logger = (line) => console.log(line),
|
|
1212
1368
|
} = {}) {
|
|
1213
|
-
const operations = createPmStudioSetupOperations(
|
|
1369
|
+
const operations = createPmStudioSetupOperations({
|
|
1370
|
+
...createPmStudioFileInspectionOperationOverrides(),
|
|
1371
|
+
...operationOverrides,
|
|
1372
|
+
});
|
|
1214
1373
|
return operations.withSetupLock({ appPath }, async () => {
|
|
1215
1374
|
const messages = [];
|
|
1216
1375
|
const emit = (message) => {
|
|
@@ -1376,9 +1535,8 @@ export async function runPmStudioSetup({
|
|
|
1376
1535
|
emit(`[OK] Verified backup: ${backup.backupAppPath}${backup.reused ? " (reused)" : ""}`);
|
|
1377
1536
|
emit(`[OK] Backup manifest: ${backup.manifestPath}`);
|
|
1378
1537
|
emit("[INFO] The verified staging bundle was installed with Foundation's atomic item replacement.");
|
|
1379
|
-
emit(`[INFO]
|
|
1380
|
-
emit(
|
|
1381
|
-
emit(`[INFO] Restore step 3/3: move ${appPath} aside, copy ${backup.backupAppPath} to that exact path, and verify its signature before launch.`);
|
|
1538
|
+
emit(`[INFO] To restore, quit PM Studio and its updater, then run ${commandName} pms restore.`);
|
|
1539
|
+
emit("[INFO] Restore uses exact source-content verification; the original source signature is inspected and reported but is not an admission gate.");
|
|
1382
1540
|
emit(`[INFO] Start ${commandName} before using Claude in PM Studio; GPT remains on PM Studio's native GitHub Copilot path.`);
|
|
1383
1541
|
return {
|
|
1384
1542
|
status: migratingExistingPatch ? "migrated" : "patched",
|
|
@@ -1394,3 +1552,180 @@ export async function runPmStudioSetup({
|
|
|
1394
1552
|
}
|
|
1395
1553
|
});
|
|
1396
1554
|
}
|
|
1555
|
+
|
|
1556
|
+
export async function runPmStudioRestore({
|
|
1557
|
+
commandName = "ccdx",
|
|
1558
|
+
appPath = DEFAULT_PM_STUDIO_APP_PATH,
|
|
1559
|
+
home = os.homedir(),
|
|
1560
|
+
backupRoot = pmStudioBackupRoot(home),
|
|
1561
|
+
recipes = PM_STUDIO_RECIPES,
|
|
1562
|
+
operations: operationOverrides = {},
|
|
1563
|
+
logger = (line) => console.log(line),
|
|
1564
|
+
} = {}) {
|
|
1565
|
+
const operations = createPmStudioSetupOperations({
|
|
1566
|
+
...createPmStudioFileInspectionOperationOverrides(),
|
|
1567
|
+
...operationOverrides,
|
|
1568
|
+
});
|
|
1569
|
+
return operations.withSetupLock({ appPath }, async () => {
|
|
1570
|
+
const messages = [];
|
|
1571
|
+
const emit = (message) => {
|
|
1572
|
+
messages.push(message);
|
|
1573
|
+
logger(message);
|
|
1574
|
+
};
|
|
1575
|
+
|
|
1576
|
+
assertInstalledAppPath(appPath);
|
|
1577
|
+
assertNoBlockingProcesses(operations, appPath);
|
|
1578
|
+
const metadata = operations.readBundleMetadata({
|
|
1579
|
+
appPath,
|
|
1580
|
+
infoPlistPath: path.join(appPath, "Contents/Info.plist"),
|
|
1581
|
+
processRunner: operations.processRunner,
|
|
1582
|
+
});
|
|
1583
|
+
const recipe = resolvePmStudioCompatibleRecipe({
|
|
1584
|
+
appPath,
|
|
1585
|
+
home,
|
|
1586
|
+
backupRoot,
|
|
1587
|
+
metadata,
|
|
1588
|
+
recipes,
|
|
1589
|
+
operations,
|
|
1590
|
+
});
|
|
1591
|
+
const source = inspectRestoreState({ appPath, recipe, operations });
|
|
1592
|
+
if (source.inspection.state === "clean") {
|
|
1593
|
+
emit(`[OK] PM Studio ${recipe.version} build ${recipe.build} is already the exact clean local bundle; no files were changed.`);
|
|
1594
|
+
return {
|
|
1595
|
+
status: "already_clean",
|
|
1596
|
+
changed: false,
|
|
1597
|
+
recipe: recipe.id,
|
|
1598
|
+
inspection: source.inspection,
|
|
1599
|
+
messages,
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
if (!["patched", "predecessor", "legacy"].includes(source.inspection.state)) {
|
|
1603
|
+
throw setupError("PM_STUDIO_BUNDLE_DRIFT",
|
|
1604
|
+
`PM Studio ${recipe.version} build ${recipe.build} does not match a recorded patch state; no files were changed`,
|
|
1605
|
+
source.inspection.issues);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
const manifestPath = pmStudioPatchManifestPath({ home, backupRoot, recipe });
|
|
1609
|
+
const backupDir = path.dirname(manifestPath);
|
|
1610
|
+
const backupEvidence = {};
|
|
1611
|
+
const backup = validateBackup({ backupDir, recipe, operations, evidence: backupEvidence });
|
|
1612
|
+
assertPatchedBinaryRecord({
|
|
1613
|
+
inspection: source.inspection,
|
|
1614
|
+
manifestPath: backup.manifestPath,
|
|
1615
|
+
recipe,
|
|
1616
|
+
recordState: source.inspection.state,
|
|
1617
|
+
});
|
|
1618
|
+
operations.assertRestorePermissions({
|
|
1619
|
+
appPath,
|
|
1620
|
+
backupAppPath: backup.backupAppPath,
|
|
1621
|
+
processRunner: operations.processRunner,
|
|
1622
|
+
});
|
|
1623
|
+
assertRestoreSpace({ appPath, backupAppPath: backup.backupAppPath, operations });
|
|
1624
|
+
const manifestSnapshot = fs.readFileSync(backup.manifestPath);
|
|
1625
|
+
const backupSource = restoreStateFromInspection({
|
|
1626
|
+
appPath: backup.backupAppPath,
|
|
1627
|
+
inspection: backupEvidence.inspection,
|
|
1628
|
+
recipe,
|
|
1629
|
+
operations,
|
|
1630
|
+
});
|
|
1631
|
+
if (backupSource.inspection.state !== "clean") {
|
|
1632
|
+
throw setupError("PM_STUDIO_BACKUP_INVALID", "Verified PM Studio backup is not an exact clean bundle",
|
|
1633
|
+
backupSource.inspection.issues);
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
const stagePath = path.join(path.dirname(appPath),
|
|
1637
|
+
`.${path.basename(appPath)}.ccdx-restore-stage-${operations.uuid()}`);
|
|
1638
|
+
let preserveStage = false;
|
|
1639
|
+
try {
|
|
1640
|
+
operations.copyBundle({
|
|
1641
|
+
source: backup.backupAppPath,
|
|
1642
|
+
destination: stagePath,
|
|
1643
|
+
processRunner: operations.processRunner,
|
|
1644
|
+
});
|
|
1645
|
+
if (!fs.readFileSync(backup.manifestPath).equals(manifestSnapshot)) {
|
|
1646
|
+
throw setupError("PM_STUDIO_BACKUP_CHANGED",
|
|
1647
|
+
"PM Studio backup manifest changed during restore; the verified staging bundle was not installed");
|
|
1648
|
+
}
|
|
1649
|
+
const currentBackupEvidence = {};
|
|
1650
|
+
const currentBackup = validateBackup({
|
|
1651
|
+
backupDir,
|
|
1652
|
+
recipe,
|
|
1653
|
+
operations,
|
|
1654
|
+
evidence: currentBackupEvidence,
|
|
1655
|
+
});
|
|
1656
|
+
const unchangedBackup = restoreStateFromInspection({
|
|
1657
|
+
appPath: currentBackup.backupAppPath,
|
|
1658
|
+
inspection: currentBackupEvidence.inspection,
|
|
1659
|
+
recipe,
|
|
1660
|
+
operations,
|
|
1661
|
+
});
|
|
1662
|
+
assertRestoreStateMatches(unchangedBackup, backupSource, "PM_STUDIO_BACKUP_CHANGED",
|
|
1663
|
+
"PM Studio backup changed during restore; the verified staging bundle was not installed");
|
|
1664
|
+
const unchangedSource = inspectRestoreState({ appPath, recipe, operations });
|
|
1665
|
+
assertRestoreStateMatches(unchangedSource, source, "PM_STUDIO_SOURCE_CHANGED",
|
|
1666
|
+
"PM Studio changed during restore; the verified staging bundle was not installed");
|
|
1667
|
+
assertPatchedBinaryRecord({
|
|
1668
|
+
inspection: unchangedSource.inspection,
|
|
1669
|
+
manifestPath: backup.manifestPath,
|
|
1670
|
+
recipe,
|
|
1671
|
+
recordState: source.inspection.state,
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
const staged = inspectRestoreState({ appPath: stagePath, recipe, operations });
|
|
1675
|
+
if (staged.inspection.state !== "clean") {
|
|
1676
|
+
throw setupError("PM_STUDIO_RESTORE_STAGE_INVALID",
|
|
1677
|
+
"PM Studio restore staging copy is not an exact clean bundle", staged.inspection.issues);
|
|
1678
|
+
}
|
|
1679
|
+
assertRestoreStateMatches(staged, backupSource, "PM_STUDIO_RESTORE_STAGE_INVALID",
|
|
1680
|
+
"PM Studio restore staging copy differs from the verified clean backup");
|
|
1681
|
+
assertNoBlockingProcesses(operations, appPath);
|
|
1682
|
+
|
|
1683
|
+
const replacementError = replaceVerifiedStage({ appPath, stagePath, operations });
|
|
1684
|
+
let installed;
|
|
1685
|
+
try {
|
|
1686
|
+
installed = inspectRestoreState({ appPath, recipe, operations });
|
|
1687
|
+
} catch {
|
|
1688
|
+
preserveStage = fs.existsSync(stagePath);
|
|
1689
|
+
throw setupError("PM_STUDIO_RESTORE_STATE_UNKNOWN",
|
|
1690
|
+
`PM Studio restore final state could not be verified. Do not launch it. ${recoveryMessage(backup)}${preserveStage ? ` Verified staging retained: ${stagePath}.` : ""}`);
|
|
1691
|
+
}
|
|
1692
|
+
if (installed.inspection.state !== "clean" || installed.fingerprint !== staged.fingerprint) {
|
|
1693
|
+
const originalRetained = installed.fingerprint === source.fingerprint;
|
|
1694
|
+
preserveStage = !originalRetained && fs.existsSync(stagePath);
|
|
1695
|
+
const code = originalRetained
|
|
1696
|
+
? "PM_STUDIO_RESTORE_REPLACE_FAILED"
|
|
1697
|
+
: "PM_STUDIO_RESTORE_INSTALL_VERIFY_FAILED";
|
|
1698
|
+
const stateMessage = originalRetained
|
|
1699
|
+
? `The installed PM Studio remains the verified ${source.inspection.state} bundle`
|
|
1700
|
+
: "The installed PM Studio does not match the verified clean backup; do not launch it";
|
|
1701
|
+
throw setupError(code,
|
|
1702
|
+
`${stateMessage}. ${recoveryMessage(backup)}${preserveStage ? ` Verified staging retained: ${stagePath}.` : ""}`,
|
|
1703
|
+
installed.inspection.issues);
|
|
1704
|
+
}
|
|
1705
|
+
if (replacementError) {
|
|
1706
|
+
emit("[WARN] Atomic replacement reported an error after the exact clean bundle became installed; the final bundle was independently verified.");
|
|
1707
|
+
}
|
|
1708
|
+
emit(`[OK] Restored PM Studio ${recipe.version} build ${recipe.build} from the exact verified clean backup.`);
|
|
1709
|
+
emit(`[OK] Verified backup retained: ${backup.backupAppPath}`);
|
|
1710
|
+
emit(`[OK] Backup manifest retained: ${backup.manifestPath}`);
|
|
1711
|
+
emit("[INFO] The verified clean staging bundle was installed with Foundation's atomic item replacement.");
|
|
1712
|
+
if (installed.inspection.codeSign?.valid === true) {
|
|
1713
|
+
emit("[OK] The restored source bundle's original signature verifies successfully.");
|
|
1714
|
+
} else {
|
|
1715
|
+
emit("[WARN] The restored bundle exactly matches its recorded local source, whose original signature does not currently verify; reinstall PM Studio if a vendor-valid signature is required.");
|
|
1716
|
+
}
|
|
1717
|
+
emit(`[INFO] Run ${commandName} pms setup to apply the split-origin patch again.`);
|
|
1718
|
+
return {
|
|
1719
|
+
status: "restored",
|
|
1720
|
+
changed: true,
|
|
1721
|
+
recipe: recipe.id,
|
|
1722
|
+
backup,
|
|
1723
|
+
inspection: installed.inspection,
|
|
1724
|
+
replacementMode: "foundation-atomic-replace",
|
|
1725
|
+
messages,
|
|
1726
|
+
};
|
|
1727
|
+
} finally {
|
|
1728
|
+
if (!preserveStage && fs.existsSync(stagePath)) operations.removePath({ target: stagePath });
|
|
1729
|
+
}
|
|
1730
|
+
});
|
|
1731
|
+
}
|
package/src/pm-studio-status.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import { PM_STUDIO_RECIPES } from "./pm-studio-asar.mjs";
|
|
9
9
|
import {
|
|
10
10
|
assertPatchedBinaryRecord,
|
|
11
|
+
createPmStudioStatusOperationOverrides,
|
|
11
12
|
createPmStudioSetupOperations,
|
|
12
13
|
DEFAULT_PM_STUDIO_APP_PATH,
|
|
13
14
|
inspectPmStudioApp,
|
|
@@ -147,7 +148,10 @@ export async function inspectPmStudioStatus({
|
|
|
147
148
|
readAdapterStatusFn = readAdapterStatus,
|
|
148
149
|
probePmStudioRelayFn = probePmStudioRelay,
|
|
149
150
|
} = {}) {
|
|
150
|
-
const operations = createPmStudioSetupOperations(
|
|
151
|
+
const operations = createPmStudioSetupOperations({
|
|
152
|
+
...createPmStudioStatusOperationOverrides(),
|
|
153
|
+
...operationOverrides,
|
|
154
|
+
});
|
|
151
155
|
let app;
|
|
152
156
|
if (!existsSync(appPath)) {
|
|
153
157
|
app = { state: "not_installed", metadata: null, issues: [] };
|