codex-copilot-dx 0.6.14 → 0.6.18
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 +30 -16
- package/bin/cli.mjs +5 -0
- package/package.json +1 -1
- package/src/adapter.mjs +34 -9
- package/src/anthropic-handler.mjs +54 -4
- package/src/anthropic.mjs +64 -26
- 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 +28 -71
- package/src/cli-options.mjs +8 -5
- package/src/copilot-client.mjs +5 -1
- package/src/copilot-responses-compat.mjs +80 -0
- package/src/copilot-responses-policy.mjs +78 -0
- package/src/copilot.mjs +2 -1
- package/src/http-transport.mjs +91 -0
- package/src/lock.mjs +179 -17
- package/src/model-cache.mjs +36 -6
- package/src/models.mjs +17 -0
- package/src/observability.mjs +13 -1
- package/src/pm-studio-asar.mjs +399 -19
- package/src/pm-studio-relay.mjs +6 -3
- package/src/pm-studio-setup.mjs +319 -14
- package/src/pm-studio-status.mjs +5 -1
- package/src/profile-model-runtime.mjs +87 -13
- package/src/profile-routing.mjs +45 -7
- package/src/response-history.mjs +142 -16
- package/src/responses-bridge.mjs +22 -4
- package/src/responses-handler.mjs +154 -39
- package/src/responses-image-pressure.mjs +30 -0
- package/src/responses-proxy.mjs +10 -2
- package/src/responses-request.mjs +109 -103
- package/src/route-plan.mjs +51 -0
- package/src/startup.mjs +8 -2
- 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,55 @@ 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
|
+
export function createPmStudioStatusOperationOverrides() {
|
|
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
|
+
|
|
319
373
|
function defaultSignApp({ appPath, source, processRunner }) {
|
|
320
374
|
const preserve = [];
|
|
321
375
|
if (source?.codeSign?.displayValid === true) {
|
|
@@ -411,6 +465,17 @@ function defaultAssertPermissions({ appPath, backupRoot }) {
|
|
|
411
465
|
}
|
|
412
466
|
}
|
|
413
467
|
|
|
468
|
+
function defaultAssertRestorePermissions({ appPath, backupAppPath }) {
|
|
469
|
+
try {
|
|
470
|
+
fs.accessSync(appPath, fs.constants.R_OK | fs.constants.X_OK);
|
|
471
|
+
fs.accessSync(path.dirname(appPath), fs.constants.W_OK | fs.constants.X_OK);
|
|
472
|
+
fs.accessSync(backupAppPath, fs.constants.R_OK | fs.constants.X_OK);
|
|
473
|
+
} catch {
|
|
474
|
+
throw setupError("PM_STUDIO_PERMISSION_DENIED",
|
|
475
|
+
"PM Studio or its verified backup is not accessible to the current user; restore will not use sudo");
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
414
479
|
export function createPmStudioSetupOperations(overrides = {}) {
|
|
415
480
|
const processRunner = overrides.processRunner || defaultProcessRunner;
|
|
416
481
|
return {
|
|
@@ -432,6 +497,7 @@ export function createPmStudioSetupOperations(overrides = {}) {
|
|
|
432
497
|
availableBytes: defaultAvailableBytes,
|
|
433
498
|
volumeId: defaultVolumeId,
|
|
434
499
|
assertPermissions: defaultAssertPermissions,
|
|
500
|
+
assertRestorePermissions: defaultAssertRestorePermissions,
|
|
435
501
|
readClaudeCredentials: ({ home }) => readAuthProfileCredentials(AUTH_PROFILE_CLAUDE, { home }),
|
|
436
502
|
now: () => new Date(),
|
|
437
503
|
uuid: () => randomUUID(),
|
|
@@ -470,7 +536,9 @@ export function inspectPmStudioApp({
|
|
|
470
536
|
const paths = appPaths(appPath, recipe);
|
|
471
537
|
const issues = [];
|
|
472
538
|
const metadata = operations.readBundleMetadata({ ...paths, recipe, processRunner: operations.processRunner });
|
|
473
|
-
const asar =
|
|
539
|
+
const asar = operations.inspectAsarFile
|
|
540
|
+
? operations.inspectAsarFile({ ...paths, recipe, processRunner: operations.processRunner })
|
|
541
|
+
: inspectAsarBuffer(fs.readFileSync(paths.asarPath), recipe);
|
|
474
542
|
const plistIntegrity = operations.readAsarIntegrity({ ...paths, recipe, processRunner: operations.processRunner });
|
|
475
543
|
const executableIntegrity = operations.inspectExecutableIntegrity({
|
|
476
544
|
...paths, recipe, processRunner: operations.processRunner,
|
|
@@ -679,11 +747,14 @@ function compatibleSourceStructure({ appPath, metadata, operations }) {
|
|
|
679
747
|
throw setupError("PM_STUDIO_ASAR_INCOMPATIBLE",
|
|
680
748
|
"PM Studio compatible-version discovery found an unsupported executable or embedded ASAR integrity policy");
|
|
681
749
|
}
|
|
682
|
-
const
|
|
750
|
+
const recipeOptions = {
|
|
683
751
|
...compatibleStaticOptions(metadata, executableIntegrity.executableName),
|
|
684
752
|
sourceExecutableSha256: executableIntegrity.executableSha256,
|
|
685
753
|
sourceElectronFrameworkSha256: executableIntegrity.frameworkSha256,
|
|
686
|
-
}
|
|
754
|
+
};
|
|
755
|
+
const compatibleRecipe = operations.createCompatibleRecipeFromFile
|
|
756
|
+
? operations.createCompatibleRecipeFromFile({ asarPath: paths.asarPath, options: recipeOptions })
|
|
757
|
+
: createCompatiblePmStudioRecipe(fs.readFileSync(paths.asarPath), recipeOptions);
|
|
687
758
|
const plistIntegrity = operations.readAsarIntegrity({
|
|
688
759
|
...paths,
|
|
689
760
|
recipe: compatibleRecipe,
|
|
@@ -737,11 +808,14 @@ function compatibleRecipeFromManifest({ backupDir, manifest, metadata, operation
|
|
|
737
808
|
recipe: PM_STUDIO_2_9_10_RECIPE,
|
|
738
809
|
processRunner: operations.processRunner,
|
|
739
810
|
});
|
|
740
|
-
const
|
|
811
|
+
const recipeOptions = {
|
|
741
812
|
...compatibleStaticOptions(metadata, executableIntegrity.executableName),
|
|
742
813
|
sourceExecutableSha256: manifest?.source?.binaries?.main_executable_sha256,
|
|
743
814
|
sourceElectronFrameworkSha256: manifest?.source?.binaries?.electron_framework_sha256,
|
|
744
|
-
}
|
|
815
|
+
};
|
|
816
|
+
const compatibleRecipe = operations.createCompatibleRecipeFromFile
|
|
817
|
+
? operations.createCompatibleRecipeFromFile({ asarPath: sourceAsarPath, options: recipeOptions })
|
|
818
|
+
: createCompatiblePmStudioRecipe(fs.readFileSync(sourceAsarPath), recipeOptions);
|
|
745
819
|
const recipe = Object.freeze({
|
|
746
820
|
...compatibleRecipe,
|
|
747
821
|
sourceBundleContent: frozenBundleContent(manifest.source.bundle_content),
|
|
@@ -766,7 +840,8 @@ function compatibleRecipeFromManifest({ backupDir, manifest, metadata, operation
|
|
|
766
840
|
|
|
767
841
|
function compatibleRecipeFromBackup({ appPath, backupRoot, metadata, operations }) {
|
|
768
842
|
if (!fs.existsSync(backupRoot)) return null;
|
|
769
|
-
const
|
|
843
|
+
const installedAsarPath = path.join(appPath, PM_STUDIO_2_9_10_RECIPE.asarPath);
|
|
844
|
+
const installedAsar = operations.inspectAsarFile ? null : fs.readFileSync(installedAsarPath);
|
|
770
845
|
let candidates = [];
|
|
771
846
|
const invalidCandidates = [];
|
|
772
847
|
for (const entry of fs.readdirSync(backupRoot, { withFileTypes: true })) {
|
|
@@ -788,7 +863,9 @@ function compatibleRecipeFromBackup({ appPath, backupRoot, metadata, operations
|
|
|
788
863
|
continue;
|
|
789
864
|
}
|
|
790
865
|
if (!recipe) continue;
|
|
791
|
-
const asarState =
|
|
866
|
+
const asarState = operations.inspectAsarFile
|
|
867
|
+
? operations.inspectAsarFile({ asarPath: installedAsarPath, recipe }).state
|
|
868
|
+
: inspectAsarBuffer(installedAsar, recipe).state;
|
|
792
869
|
if (asarState === "drift") continue;
|
|
793
870
|
candidates.push({ recipe, manifest, asarState });
|
|
794
871
|
}
|
|
@@ -1188,6 +1265,74 @@ function assertSpace({ appPath, backupRoot, backupExists, operations }) {
|
|
|
1188
1265
|
}
|
|
1189
1266
|
}
|
|
1190
1267
|
|
|
1268
|
+
function assertRestoreSpace({ appPath, backupAppPath, operations }) {
|
|
1269
|
+
const bundleBytes = operations.bundleSize(backupAppPath);
|
|
1270
|
+
if (!Number.isFinite(bundleBytes) || bundleBytes <= 0) {
|
|
1271
|
+
throw setupError("PM_STUDIO_SIZE_INVALID", "Could not determine verified PM Studio backup size");
|
|
1272
|
+
}
|
|
1273
|
+
if (operations.availableBytes(path.dirname(appPath)) < bundleBytes + MIN_FREE_MARGIN_BYTES) {
|
|
1274
|
+
throw setupError("PM_STUDIO_SPACE_INSUFFICIENT", "Not enough free space for PM Studio restore staging");
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function codeSignFingerprint(codeSign) {
|
|
1279
|
+
return {
|
|
1280
|
+
valid: codeSign?.valid === true,
|
|
1281
|
+
verifyValid: codeSign?.verifyValid === true,
|
|
1282
|
+
displayValid: codeSign?.displayValid === true,
|
|
1283
|
+
entitlementsState: codeSign?.entitlementsState || "",
|
|
1284
|
+
adHoc: codeSign?.adHoc === true,
|
|
1285
|
+
identifier: codeSign?.identifier || "",
|
|
1286
|
+
teamIdentifier: codeSign?.teamIdentifier || "",
|
|
1287
|
+
flags: codeSign?.flags || "",
|
|
1288
|
+
runtimeVersion: codeSign?.runtimeVersion || "",
|
|
1289
|
+
cdHashFull: codeSign?.cdHashFull || "",
|
|
1290
|
+
notarizationTicket: codeSign?.notarizationTicket || "",
|
|
1291
|
+
entitlementsSha256: codeSign?.entitlementsSha256 || "",
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
function restoreStateFingerprint(inspection, bundleContent) {
|
|
1296
|
+
return JSON.stringify({
|
|
1297
|
+
state: inspection.state,
|
|
1298
|
+
metadata: inspection.metadata,
|
|
1299
|
+
asar: {
|
|
1300
|
+
state: inspection.asar?.state,
|
|
1301
|
+
asarSha256: inspection.asar?.asarSha256,
|
|
1302
|
+
headerSha256: inspection.asar?.headerSha256,
|
|
1303
|
+
},
|
|
1304
|
+
plistIntegrity: inspection.plistIntegrity,
|
|
1305
|
+
executableIntegrity: {
|
|
1306
|
+
executableName: inspection.executableIntegrity?.executableName,
|
|
1307
|
+
executableSha256: inspection.executableIntegrity?.executableSha256,
|
|
1308
|
+
frameworkSha256: inspection.executableIntegrity?.frameworkSha256,
|
|
1309
|
+
embeddedIntegrity: inspection.executableIntegrity?.embeddedIntegrity,
|
|
1310
|
+
matchesRecipeName: inspection.executableIntegrity?.matchesRecipeName,
|
|
1311
|
+
},
|
|
1312
|
+
codeSign: codeSignFingerprint(inspection.codeSign),
|
|
1313
|
+
bundleContent,
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function inspectRestoreState({ appPath, recipe, operations }) {
|
|
1318
|
+
const inspection = inspectPmStudioApp({ appPath, recipe, operations });
|
|
1319
|
+
const bundleContent = inspection.bundleContent || operations.inspectBundleContent({
|
|
1320
|
+
...appPaths(appPath, recipe),
|
|
1321
|
+
recipe,
|
|
1322
|
+
processRunner: operations.processRunner,
|
|
1323
|
+
});
|
|
1324
|
+
return {
|
|
1325
|
+
inspection,
|
|
1326
|
+
fingerprint: restoreStateFingerprint(inspection, bundleContent),
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
function assertRestoreStateMatches(actual, expected, code, message) {
|
|
1331
|
+
if (actual.fingerprint !== expected.fingerprint) {
|
|
1332
|
+
throw setupError(code, message, actual.inspection.issues);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1191
1336
|
function replaceVerifiedStage({ appPath, stagePath, operations }) {
|
|
1192
1337
|
try {
|
|
1193
1338
|
operations.replaceApp({ appPath, stagePath, processRunner: operations.processRunner });
|
|
@@ -1376,9 +1521,8 @@ export async function runPmStudioSetup({
|
|
|
1376
1521
|
emit(`[OK] Verified backup: ${backup.backupAppPath}${backup.reused ? " (reused)" : ""}`);
|
|
1377
1522
|
emit(`[OK] Backup manifest: ${backup.manifestPath}`);
|
|
1378
1523
|
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.`);
|
|
1524
|
+
emit(`[INFO] To restore, quit PM Studio and its updater, then run ${commandName} pms restore.`);
|
|
1525
|
+
emit("[INFO] Restore uses exact source-content verification; the original source signature is inspected and reported but is not an admission gate.");
|
|
1382
1526
|
emit(`[INFO] Start ${commandName} before using Claude in PM Studio; GPT remains on PM Studio's native GitHub Copilot path.`);
|
|
1383
1527
|
return {
|
|
1384
1528
|
status: migratingExistingPatch ? "migrated" : "patched",
|
|
@@ -1394,3 +1538,164 @@ export async function runPmStudioSetup({
|
|
|
1394
1538
|
}
|
|
1395
1539
|
});
|
|
1396
1540
|
}
|
|
1541
|
+
|
|
1542
|
+
export async function runPmStudioRestore({
|
|
1543
|
+
commandName = "ccdx",
|
|
1544
|
+
appPath = DEFAULT_PM_STUDIO_APP_PATH,
|
|
1545
|
+
home = os.homedir(),
|
|
1546
|
+
backupRoot = pmStudioBackupRoot(home),
|
|
1547
|
+
recipes = PM_STUDIO_RECIPES,
|
|
1548
|
+
operations: operationOverrides = {},
|
|
1549
|
+
logger = (line) => console.log(line),
|
|
1550
|
+
} = {}) {
|
|
1551
|
+
const operations = createPmStudioSetupOperations(operationOverrides);
|
|
1552
|
+
return operations.withSetupLock({ appPath }, async () => {
|
|
1553
|
+
const messages = [];
|
|
1554
|
+
const emit = (message) => {
|
|
1555
|
+
messages.push(message);
|
|
1556
|
+
logger(message);
|
|
1557
|
+
};
|
|
1558
|
+
|
|
1559
|
+
assertInstalledAppPath(appPath);
|
|
1560
|
+
assertNoBlockingProcesses(operations, appPath);
|
|
1561
|
+
const metadata = operations.readBundleMetadata({
|
|
1562
|
+
appPath,
|
|
1563
|
+
infoPlistPath: path.join(appPath, "Contents/Info.plist"),
|
|
1564
|
+
processRunner: operations.processRunner,
|
|
1565
|
+
});
|
|
1566
|
+
const recipe = resolvePmStudioCompatibleRecipe({
|
|
1567
|
+
appPath,
|
|
1568
|
+
home,
|
|
1569
|
+
backupRoot,
|
|
1570
|
+
metadata,
|
|
1571
|
+
recipes,
|
|
1572
|
+
operations,
|
|
1573
|
+
});
|
|
1574
|
+
const source = inspectRestoreState({ appPath, recipe, operations });
|
|
1575
|
+
if (source.inspection.state === "clean") {
|
|
1576
|
+
emit(`[OK] PM Studio ${recipe.version} build ${recipe.build} is already the exact clean local bundle; no files were changed.`);
|
|
1577
|
+
return {
|
|
1578
|
+
status: "already_clean",
|
|
1579
|
+
changed: false,
|
|
1580
|
+
recipe: recipe.id,
|
|
1581
|
+
inspection: source.inspection,
|
|
1582
|
+
messages,
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
if (!["patched", "predecessor", "legacy"].includes(source.inspection.state)) {
|
|
1586
|
+
throw setupError("PM_STUDIO_BUNDLE_DRIFT",
|
|
1587
|
+
`PM Studio ${recipe.version} build ${recipe.build} does not match a recorded patch state; no files were changed`,
|
|
1588
|
+
source.inspection.issues);
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
const manifestPath = pmStudioPatchManifestPath({ home, backupRoot, recipe });
|
|
1592
|
+
const backupDir = path.dirname(manifestPath);
|
|
1593
|
+
const backup = validateBackup({ backupDir, recipe, operations });
|
|
1594
|
+
assertPatchedBinaryRecord({
|
|
1595
|
+
inspection: source.inspection,
|
|
1596
|
+
manifestPath: backup.manifestPath,
|
|
1597
|
+
recipe,
|
|
1598
|
+
recordState: source.inspection.state,
|
|
1599
|
+
});
|
|
1600
|
+
operations.assertRestorePermissions({
|
|
1601
|
+
appPath,
|
|
1602
|
+
backupAppPath: backup.backupAppPath,
|
|
1603
|
+
processRunner: operations.processRunner,
|
|
1604
|
+
});
|
|
1605
|
+
assertRestoreSpace({ appPath, backupAppPath: backup.backupAppPath, operations });
|
|
1606
|
+
const manifestSnapshot = fs.readFileSync(backup.manifestPath);
|
|
1607
|
+
const backupSource = inspectRestoreState({ appPath: backup.backupAppPath, recipe, operations });
|
|
1608
|
+
if (backupSource.inspection.state !== "clean") {
|
|
1609
|
+
throw setupError("PM_STUDIO_BACKUP_INVALID", "Verified PM Studio backup is not an exact clean bundle",
|
|
1610
|
+
backupSource.inspection.issues);
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
const stagePath = path.join(path.dirname(appPath),
|
|
1614
|
+
`.${path.basename(appPath)}.ccdx-restore-stage-${operations.uuid()}`);
|
|
1615
|
+
let preserveStage = false;
|
|
1616
|
+
try {
|
|
1617
|
+
operations.copyBundle({
|
|
1618
|
+
source: backup.backupAppPath,
|
|
1619
|
+
destination: stagePath,
|
|
1620
|
+
processRunner: operations.processRunner,
|
|
1621
|
+
});
|
|
1622
|
+
if (!fs.readFileSync(backup.manifestPath).equals(manifestSnapshot)) {
|
|
1623
|
+
throw setupError("PM_STUDIO_BACKUP_CHANGED",
|
|
1624
|
+
"PM Studio backup manifest changed during restore; the verified staging bundle was not installed");
|
|
1625
|
+
}
|
|
1626
|
+
const currentBackup = validateBackup({ backupDir, recipe, operations });
|
|
1627
|
+
const unchangedBackup = inspectRestoreState({
|
|
1628
|
+
appPath: currentBackup.backupAppPath,
|
|
1629
|
+
recipe,
|
|
1630
|
+
operations,
|
|
1631
|
+
});
|
|
1632
|
+
assertRestoreStateMatches(unchangedBackup, backupSource, "PM_STUDIO_BACKUP_CHANGED",
|
|
1633
|
+
"PM Studio backup changed during restore; the verified staging bundle was not installed");
|
|
1634
|
+
const unchangedSource = inspectRestoreState({ appPath, recipe, operations });
|
|
1635
|
+
assertRestoreStateMatches(unchangedSource, source, "PM_STUDIO_SOURCE_CHANGED",
|
|
1636
|
+
"PM Studio changed during restore; the verified staging bundle was not installed");
|
|
1637
|
+
assertPatchedBinaryRecord({
|
|
1638
|
+
inspection: unchangedSource.inspection,
|
|
1639
|
+
manifestPath: backup.manifestPath,
|
|
1640
|
+
recipe,
|
|
1641
|
+
recordState: source.inspection.state,
|
|
1642
|
+
});
|
|
1643
|
+
|
|
1644
|
+
const staged = inspectRestoreState({ appPath: stagePath, recipe, operations });
|
|
1645
|
+
if (staged.inspection.state !== "clean") {
|
|
1646
|
+
throw setupError("PM_STUDIO_RESTORE_STAGE_INVALID",
|
|
1647
|
+
"PM Studio restore staging copy is not an exact clean bundle", staged.inspection.issues);
|
|
1648
|
+
}
|
|
1649
|
+
assertRestoreStateMatches(staged, backupSource, "PM_STUDIO_RESTORE_STAGE_INVALID",
|
|
1650
|
+
"PM Studio restore staging copy differs from the verified clean backup");
|
|
1651
|
+
assertNoBlockingProcesses(operations, appPath);
|
|
1652
|
+
|
|
1653
|
+
const replacementError = replaceVerifiedStage({ appPath, stagePath, operations });
|
|
1654
|
+
let installed;
|
|
1655
|
+
try {
|
|
1656
|
+
installed = inspectRestoreState({ appPath, recipe, operations });
|
|
1657
|
+
} catch {
|
|
1658
|
+
preserveStage = fs.existsSync(stagePath);
|
|
1659
|
+
throw setupError("PM_STUDIO_RESTORE_STATE_UNKNOWN",
|
|
1660
|
+
`PM Studio restore final state could not be verified. Do not launch it. ${recoveryMessage(backup)}${preserveStage ? ` Verified staging retained: ${stagePath}.` : ""}`);
|
|
1661
|
+
}
|
|
1662
|
+
if (installed.inspection.state !== "clean" || installed.fingerprint !== staged.fingerprint) {
|
|
1663
|
+
const originalRetained = installed.fingerprint === source.fingerprint;
|
|
1664
|
+
preserveStage = !originalRetained && fs.existsSync(stagePath);
|
|
1665
|
+
const code = originalRetained
|
|
1666
|
+
? "PM_STUDIO_RESTORE_REPLACE_FAILED"
|
|
1667
|
+
: "PM_STUDIO_RESTORE_INSTALL_VERIFY_FAILED";
|
|
1668
|
+
const stateMessage = originalRetained
|
|
1669
|
+
? `The installed PM Studio remains the verified ${source.inspection.state} bundle`
|
|
1670
|
+
: "The installed PM Studio does not match the verified clean backup; do not launch it";
|
|
1671
|
+
throw setupError(code,
|
|
1672
|
+
`${stateMessage}. ${recoveryMessage(backup)}${preserveStage ? ` Verified staging retained: ${stagePath}.` : ""}`,
|
|
1673
|
+
installed.inspection.issues);
|
|
1674
|
+
}
|
|
1675
|
+
if (replacementError) {
|
|
1676
|
+
emit("[WARN] Atomic replacement reported an error after the exact clean bundle became installed; the final bundle was independently verified.");
|
|
1677
|
+
}
|
|
1678
|
+
emit(`[OK] Restored PM Studio ${recipe.version} build ${recipe.build} from the exact verified clean backup.`);
|
|
1679
|
+
emit(`[OK] Verified backup retained: ${backup.backupAppPath}`);
|
|
1680
|
+
emit(`[OK] Backup manifest retained: ${backup.manifestPath}`);
|
|
1681
|
+
emit("[INFO] The verified clean staging bundle was installed with Foundation's atomic item replacement.");
|
|
1682
|
+
if (installed.inspection.codeSign?.valid === true) {
|
|
1683
|
+
emit("[OK] The restored source bundle's original signature verifies successfully.");
|
|
1684
|
+
} else {
|
|
1685
|
+
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.");
|
|
1686
|
+
}
|
|
1687
|
+
emit(`[INFO] Run ${commandName} pms setup to apply the split-origin patch again.`);
|
|
1688
|
+
return {
|
|
1689
|
+
status: "restored",
|
|
1690
|
+
changed: true,
|
|
1691
|
+
recipe: recipe.id,
|
|
1692
|
+
backup,
|
|
1693
|
+
inspection: installed.inspection,
|
|
1694
|
+
replacementMode: "foundation-atomic-replace",
|
|
1695
|
+
messages,
|
|
1696
|
+
};
|
|
1697
|
+
} finally {
|
|
1698
|
+
if (!preserveStage && fs.existsSync(stagePath)) operations.removePath({ target: stagePath });
|
|
1699
|
+
}
|
|
1700
|
+
});
|
|
1701
|
+
}
|
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: [] };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isValidModelList,
|
|
3
|
-
|
|
3
|
+
loadModelCacheEntry,
|
|
4
4
|
saveModelCache,
|
|
5
5
|
} from "./model-cache.mjs";
|
|
6
6
|
import {
|
|
@@ -22,13 +22,48 @@ function parsedCatalog(result) {
|
|
|
22
22
|
if (result?.status < 200 || result?.status >= 300) {
|
|
23
23
|
throw new Error(`Copilot models returned HTTP ${result?.status}`);
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
let models;
|
|
26
|
+
try {
|
|
27
|
+
models = JSON.parse(result.body);
|
|
28
|
+
} catch {
|
|
29
|
+
throw new Error("Copilot models response was not valid JSON");
|
|
30
|
+
}
|
|
26
31
|
if (!isValidModelList(models)) {
|
|
27
32
|
throw new Error("Copilot models response contained no valid models");
|
|
28
33
|
}
|
|
29
34
|
return models;
|
|
30
35
|
}
|
|
31
36
|
|
|
37
|
+
function createModelRegistry() {
|
|
38
|
+
return {
|
|
39
|
+
modelDefs: undefined,
|
|
40
|
+
models: undefined,
|
|
41
|
+
source: "built-in",
|
|
42
|
+
cacheState: "none",
|
|
43
|
+
cacheSavedAtMs: null,
|
|
44
|
+
lastError: null,
|
|
45
|
+
lastErrorAtMs: null,
|
|
46
|
+
refreshInFlight: false,
|
|
47
|
+
generation: 0,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function sameModelDefs(left, right) {
|
|
52
|
+
if (left === right) return true;
|
|
53
|
+
if (!Array.isArray(left) || !Array.isArray(right)) return false;
|
|
54
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function safeRefreshError(error, { timedOut = false, timeoutMs } = {}) {
|
|
58
|
+
if (timedOut) return `Model refresh timed out after ${timeoutMs}ms`;
|
|
59
|
+
const raw = String(error?.message || error?.name || "Model refresh failed")
|
|
60
|
+
.replace(/[\u0000-\u001f\u007f]+/g, " ")
|
|
61
|
+
.replace(/\b(?:github_pat_|gh[pousr]_|copilot_)[A-Za-z0-9._-]+\b/gi, "<redacted>")
|
|
62
|
+
.replace(/\bBearer\s+\S+/gi, "Bearer <redacted>")
|
|
63
|
+
.trim();
|
|
64
|
+
return (raw || "Model refresh failed").slice(0, 240);
|
|
65
|
+
}
|
|
66
|
+
|
|
32
67
|
export function createProfileModelRuntime({
|
|
33
68
|
codexClient,
|
|
34
69
|
claudeClient = codexClient,
|
|
@@ -51,11 +86,13 @@ export function createProfileModelRuntime({
|
|
|
51
86
|
throw new Error("Isolated Claude models require an isolated Claude client");
|
|
52
87
|
}
|
|
53
88
|
|
|
54
|
-
const codexRegistry =
|
|
89
|
+
const codexRegistry = createModelRegistry();
|
|
55
90
|
const claudeRegistry = claudeMode === "inherited"
|
|
56
91
|
? codexRegistry
|
|
57
|
-
:
|
|
92
|
+
: createModelRegistry();
|
|
58
93
|
const customClaudeDefs = parseModelAliasEnv(env.CCDX_CLAUDE_MODEL_ALIASES);
|
|
94
|
+
const refreshFlights = new Map();
|
|
95
|
+
const refreshGenerations = new Map();
|
|
59
96
|
const refreshTimeoutMs = Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
60
97
|
? timeoutMs
|
|
61
98
|
: DEFAULT_TIMEOUT_MS;
|
|
@@ -85,8 +122,9 @@ export function createProfileModelRuntime({
|
|
|
85
122
|
? customClaudeDefs.map((model) => ({ ...model }))
|
|
86
123
|
: claudeDesktopModelDefsFromCopilotModels(models);
|
|
87
124
|
if (discovered.length) {
|
|
125
|
+
const changed = !sameModelDefs(registry.modelDefs, discovered);
|
|
88
126
|
registry.modelDefs = discovered;
|
|
89
|
-
notifyClaudeModels(discovered);
|
|
127
|
+
if (changed) notifyClaudeModels(discovered);
|
|
90
128
|
}
|
|
91
129
|
return registry.modelDefs;
|
|
92
130
|
}
|
|
@@ -96,11 +134,13 @@ export function createProfileModelRuntime({
|
|
|
96
134
|
? String(claudeCredentialFingerprint || "").trim()
|
|
97
135
|
: String(codexCredentialFingerprint || "").trim();
|
|
98
136
|
if (profile === "claude" && !credentialFingerprint) return false;
|
|
99
|
-
const cached =
|
|
137
|
+
const cached = loadModelCacheEntry({ home, profile, credentialFingerprint });
|
|
100
138
|
if (!cached) return false;
|
|
101
139
|
try {
|
|
102
|
-
applyCatalog({ client, models: cached, registry, source: "cache", ownsClaudeModels });
|
|
103
|
-
|
|
140
|
+
applyCatalog({ client, models: cached.models, registry, source: "cache", ownsClaudeModels });
|
|
141
|
+
registry.cacheState = cached.state;
|
|
142
|
+
registry.cacheSavedAtMs = cached.savedAtMs;
|
|
143
|
+
return { loaded: true, stale: cached.state === "stale" };
|
|
104
144
|
} catch {
|
|
105
145
|
return false;
|
|
106
146
|
}
|
|
@@ -131,13 +171,22 @@ export function createProfileModelRuntime({
|
|
|
131
171
|
}
|
|
132
172
|
}
|
|
133
173
|
|
|
134
|
-
function persist(models, profile) {
|
|
174
|
+
function persist(models, profile, registry, generation) {
|
|
135
175
|
const credentialFingerprint = profile === "claude"
|
|
136
176
|
? String(claudeCredentialFingerprint || "").trim()
|
|
137
177
|
: String(codexCredentialFingerprint || "").trim();
|
|
138
178
|
if (profile === "claude" && !credentialFingerprint) return;
|
|
139
179
|
try {
|
|
140
|
-
saveModelCache(models, {
|
|
180
|
+
const saved = saveModelCache(models, {
|
|
181
|
+
home,
|
|
182
|
+
profile,
|
|
183
|
+
credentialFingerprint,
|
|
184
|
+
isCurrent: () => refreshGenerations.get(profile) === generation,
|
|
185
|
+
});
|
|
186
|
+
if (saved) {
|
|
187
|
+
registry.cacheState = "fresh";
|
|
188
|
+
registry.cacheSavedAtMs = Date.now();
|
|
189
|
+
}
|
|
141
190
|
} catch (error) {
|
|
142
191
|
const label = profile === "codex" ? "Copilot" : "Claude Copilot";
|
|
143
192
|
emit("warn", `Could not persist the ${label} model cache (${error.message})`);
|
|
@@ -162,12 +211,13 @@ export function createProfileModelRuntime({
|
|
|
162
211
|
return profile === "claude" ? "built-in model list" : "no cached model list";
|
|
163
212
|
}
|
|
164
213
|
|
|
165
|
-
async function
|
|
214
|
+
async function performRefresh({ client, profile, registry, ownsClaudeModels, checksCodex }, generation) {
|
|
166
215
|
const controllerLabel = claudeMode === "inherited"
|
|
167
216
|
? ""
|
|
168
217
|
: `${profile === "codex" ? "Codex" : "Claude"} `;
|
|
169
218
|
try {
|
|
170
219
|
const models = await requestCatalog(client);
|
|
220
|
+
if (refreshGenerations.get(profile) !== generation) return registry.modelDefs;
|
|
171
221
|
if (checksCodex) checkCodexCatalog(models);
|
|
172
222
|
const modelDefs = applyCatalog({
|
|
173
223
|
client,
|
|
@@ -176,20 +226,44 @@ export function createProfileModelRuntime({
|
|
|
176
226
|
source: "live",
|
|
177
227
|
ownsClaudeModels,
|
|
178
228
|
});
|
|
179
|
-
persist(models, profile);
|
|
229
|
+
persist(models, profile, registry, generation);
|
|
230
|
+
registry.lastError = null;
|
|
231
|
+
registry.lastErrorAtMs = null;
|
|
180
232
|
if (ownsClaudeModels) logClaudeCatalog(modelDefs);
|
|
181
233
|
return modelDefs;
|
|
182
234
|
} catch (error) {
|
|
235
|
+
if (refreshGenerations.get(profile) !== generation) return registry.modelDefs;
|
|
183
236
|
const fallback = fallbackLabel(registry, profile);
|
|
184
237
|
const timedOut = error?.name === "AbortError" || error?.code === "ABORT_ERR";
|
|
238
|
+
const diagnostic = safeRefreshError(error, { timedOut, timeoutMs: refreshTimeoutMs });
|
|
239
|
+
registry.lastError = diagnostic;
|
|
240
|
+
registry.lastErrorAtMs = Date.now();
|
|
185
241
|
const message = timedOut
|
|
186
242
|
? `${controllerLabel}model refresh timed out after ${refreshTimeoutMs}ms; using ${fallback}`
|
|
187
|
-
: `Could not refresh ${controllerLabel.toLowerCase()}model list; using ${fallback} (${
|
|
243
|
+
: `Could not refresh ${controllerLabel.toLowerCase()}model list; using ${fallback} (${diagnostic})`;
|
|
188
244
|
emit("warn", message[0].toUpperCase() + message.slice(1));
|
|
189
245
|
return registry.modelDefs;
|
|
190
246
|
}
|
|
191
247
|
}
|
|
192
248
|
|
|
249
|
+
function refreshProfile(options) {
|
|
250
|
+
const { profile, registry } = options;
|
|
251
|
+
const existing = refreshFlights.get(profile);
|
|
252
|
+
if (existing) return existing.promise;
|
|
253
|
+
|
|
254
|
+
const generation = (refreshGenerations.get(profile) || 0) + 1;
|
|
255
|
+
refreshGenerations.set(profile, generation);
|
|
256
|
+
registry.generation = generation;
|
|
257
|
+
registry.refreshInFlight = true;
|
|
258
|
+
const flight = { generation, promise: null };
|
|
259
|
+
refreshFlights.set(profile, flight);
|
|
260
|
+
flight.promise = performRefresh(options, generation).finally(() => {
|
|
261
|
+
if (refreshFlights.get(profile) === flight) refreshFlights.delete(profile);
|
|
262
|
+
if (refreshGenerations.get(profile) === generation) registry.refreshInFlight = false;
|
|
263
|
+
});
|
|
264
|
+
return flight.promise;
|
|
265
|
+
}
|
|
266
|
+
|
|
193
267
|
function refreshCodex() {
|
|
194
268
|
return refreshProfile({
|
|
195
269
|
client: codexClient,
|