codex-copilot-dx 0.6.7 → 0.6.9
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 +47 -21
- package/package.json +1 -1
- package/src/cli-status.mjs +2 -2
- package/src/config.mjs +18 -0
- package/src/observability.mjs +0 -4
- package/src/pm-studio-asar.mjs +193 -50
- package/src/pm-studio-relay.mjs +40 -30
- package/src/pm-studio-setup.mjs +398 -54
- package/src/pm-studio-status.mjs +67 -7
- package/src/responses-request.mjs +16 -2
- package/src/running-adapter.mjs +3 -1
package/src/pm-studio-setup.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
import {
|
|
7
7
|
AUTH_PROFILE_CLAUDE,
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
export const DEFAULT_PM_STUDIO_APP_PATH = "/Applications/PM Studio.app";
|
|
20
20
|
export const PM_STUDIO_CLAUDE_AUTH_COMMAND = "ccdx auth login claude --reauth --github-login <personal-login>";
|
|
21
21
|
const MIN_FREE_MARGIN_BYTES = 16 * 1024 * 1024;
|
|
22
|
+
const BUNDLE_CONTENT_SCHEME = "ccdx-bundle-content-v2";
|
|
22
23
|
|
|
23
24
|
function setupError(code, message, details) {
|
|
24
25
|
const error = new Error(message);
|
|
@@ -31,7 +32,7 @@ function defaultProcessRunner(command, args) {
|
|
|
31
32
|
return spawnSync(command, args, {
|
|
32
33
|
encoding: "utf8",
|
|
33
34
|
windowsHide: true,
|
|
34
|
-
maxBuffer:
|
|
35
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
35
36
|
});
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -53,6 +54,166 @@ function runChecked(processRunner, command, args, label) {
|
|
|
53
54
|
return result;
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
function hashRegularFile(filePath, expected) {
|
|
58
|
+
const hash = createHash("sha256");
|
|
59
|
+
const file = fs.openSync(filePath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
60
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
61
|
+
try {
|
|
62
|
+
const before = fs.fstatSync(file, { bigint: true });
|
|
63
|
+
if (before.dev !== BigInt(expected.dev)
|
|
64
|
+
|| before.ino !== BigInt(expected.ino)
|
|
65
|
+
|| before.size !== BigInt(expected.size)) {
|
|
66
|
+
throw setupError("PM_STUDIO_BUNDLE_CHANGED", `PM Studio bundle changed while reading ${filePath}`);
|
|
67
|
+
}
|
|
68
|
+
for (;;) {
|
|
69
|
+
const length = fs.readSync(file, buffer, 0, buffer.length, null);
|
|
70
|
+
if (length === 0) break;
|
|
71
|
+
hash.update(buffer.subarray(0, length));
|
|
72
|
+
}
|
|
73
|
+
const after = fs.fstatSync(file, { bigint: true });
|
|
74
|
+
if (before.dev !== after.dev
|
|
75
|
+
|| before.ino !== after.ino
|
|
76
|
+
|| before.size !== after.size
|
|
77
|
+
|| before.mtimeNs !== after.mtimeNs
|
|
78
|
+
|| before.ctimeNs !== after.ctimeNs) {
|
|
79
|
+
throw setupError("PM_STUDIO_BUNDLE_CHANGED", `PM Studio bundle changed while hashing ${filePath}`);
|
|
80
|
+
}
|
|
81
|
+
} finally {
|
|
82
|
+
fs.closeSync(file);
|
|
83
|
+
}
|
|
84
|
+
return hash.digest("hex");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseXattrHex(output, root, ignoredXattrs) {
|
|
88
|
+
const records = [];
|
|
89
|
+
const ignored = new Set(ignoredXattrs);
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
let current = null;
|
|
92
|
+
const flush = () => {
|
|
93
|
+
if (!current) return;
|
|
94
|
+
if (!ignored.has(current.name)) {
|
|
95
|
+
const key = `${current.path}\0${current.name}`;
|
|
96
|
+
if (seen.has(key)) throw setupError("PM_STUDIO_XATTR_INVALID", `Duplicate extended attribute ${current.name}`);
|
|
97
|
+
seen.add(key);
|
|
98
|
+
const value = Buffer.from(current.hex.join(""), "hex");
|
|
99
|
+
records.push(["xattr", current.path, current.name, value.length, sha256Hex(value)]);
|
|
100
|
+
}
|
|
101
|
+
current = null;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
for (const line of String(output || "").split(/\r?\n/)) {
|
|
105
|
+
if (!line) continue;
|
|
106
|
+
const separator = line.lastIndexOf(": ");
|
|
107
|
+
if ((line === `${root}:` || line.startsWith(`${root}: `) || line.startsWith(`${root}${path.sep}`))
|
|
108
|
+
&& separator >= root.length && line.endsWith(":")) {
|
|
109
|
+
flush();
|
|
110
|
+
const target = line.slice(0, separator);
|
|
111
|
+
const relative = target === root ? "." : path.relative(root, target).split(path.sep).join("/");
|
|
112
|
+
if (!relative || relative === ".." || relative.startsWith("../") || path.isAbsolute(relative)) {
|
|
113
|
+
throw setupError("PM_STUDIO_XATTR_INVALID", `Extended attribute path escapes PM Studio: ${target}`);
|
|
114
|
+
}
|
|
115
|
+
current = { path: relative, name: line.slice(separator + 2, -1), hex: [] };
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (!current) throw setupError("PM_STUDIO_XATTR_INVALID", "Could not parse PM Studio extended attributes");
|
|
119
|
+
const data = line.includes(" |") ? line.slice(0, line.indexOf(" |")) : line;
|
|
120
|
+
const tokens = data.trim().split(/\s+/);
|
|
121
|
+
if (!/^[0-9a-f]{8}$/i.test(tokens[0]) || tokens.slice(1).some((token) => !/^[0-9a-f]{2}$/i.test(token))) {
|
|
122
|
+
throw setupError("PM_STUDIO_XATTR_INVALID", "Could not parse PM Studio extended attribute bytes");
|
|
123
|
+
}
|
|
124
|
+
current.hex.push(...tokens.slice(1));
|
|
125
|
+
}
|
|
126
|
+
flush();
|
|
127
|
+
return records.sort((left, right) => Buffer.compare(
|
|
128
|
+
Buffer.from(`${left[1]}\0${left[2]}`),
|
|
129
|
+
Buffer.from(`${right[1]}\0${right[2]}`),
|
|
130
|
+
));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function inspectBundleContent(appPath, {
|
|
134
|
+
xattrOutput = "",
|
|
135
|
+
ignoredXattrs = [],
|
|
136
|
+
} = {}) {
|
|
137
|
+
const root = path.resolve(appPath);
|
|
138
|
+
const rootStat = fs.lstatSync(root);
|
|
139
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
|
|
140
|
+
throw setupError("PM_STUDIO_APP_INVALID", `PM Studio path is not a regular app bundle: ${appPath}`);
|
|
141
|
+
}
|
|
142
|
+
const records = [];
|
|
143
|
+
const normalizedPaths = new Map();
|
|
144
|
+
let regularFileCount = 0;
|
|
145
|
+
let regularBytes = 0;
|
|
146
|
+
let symlinkCount = 0;
|
|
147
|
+
|
|
148
|
+
const rememberPath = (relative) => {
|
|
149
|
+
const normalized = relative.normalize("NFC");
|
|
150
|
+
const existing = normalizedPaths.get(normalized);
|
|
151
|
+
if (existing && existing !== relative) {
|
|
152
|
+
throw setupError("PM_STUDIO_BUNDLE_PATH_COLLISION", `PM Studio contains colliding paths: ${existing} and ${relative}`);
|
|
153
|
+
}
|
|
154
|
+
normalizedPaths.set(normalized, relative);
|
|
155
|
+
};
|
|
156
|
+
rememberPath(".");
|
|
157
|
+
records.push(["directory", ".", rootStat.mode & 0o7777]);
|
|
158
|
+
const visit = (directory) => {
|
|
159
|
+
const names = fs.readdirSync(directory).sort((left, right) =>
|
|
160
|
+
Buffer.compare(Buffer.from(left), Buffer.from(right)));
|
|
161
|
+
for (const name of names) {
|
|
162
|
+
const fullPath = path.join(directory, name);
|
|
163
|
+
const stat = fs.lstatSync(fullPath);
|
|
164
|
+
const relative = path.relative(root, fullPath).split(path.sep).join("/");
|
|
165
|
+
const mode = stat.mode & 0o7777;
|
|
166
|
+
rememberPath(relative);
|
|
167
|
+
if (stat.isDirectory() && !stat.isSymbolicLink()) {
|
|
168
|
+
records.push(["directory", relative, mode]);
|
|
169
|
+
visit(fullPath);
|
|
170
|
+
} else if (stat.isSymbolicLink()) {
|
|
171
|
+
const target = fs.readlinkSync(fullPath);
|
|
172
|
+
const resolved = path.resolve(path.dirname(fullPath), target);
|
|
173
|
+
if (path.isAbsolute(target)
|
|
174
|
+
|| (resolved !== root && !resolved.startsWith(`${root}${path.sep}`))
|
|
175
|
+
|| !fs.existsSync(resolved)) {
|
|
176
|
+
throw setupError("PM_STUDIO_BUNDLE_SYMLINK_INVALID", `PM Studio contains an unsafe symlink: ${relative}`);
|
|
177
|
+
}
|
|
178
|
+
symlinkCount += 1;
|
|
179
|
+
records.push(["symlink", relative, mode, target]);
|
|
180
|
+
} else if (stat.isFile()) {
|
|
181
|
+
if (stat.nlink !== 1) {
|
|
182
|
+
throw setupError("PM_STUDIO_BUNDLE_HARDLINK_INVALID", `PM Studio contains a hard-linked file: ${relative}`);
|
|
183
|
+
}
|
|
184
|
+
regularFileCount += 1;
|
|
185
|
+
regularBytes += stat.size;
|
|
186
|
+
records.push(["file", relative, mode, stat.size, hashRegularFile(fullPath, stat)]);
|
|
187
|
+
} else {
|
|
188
|
+
throw setupError("PM_STUDIO_BUNDLE_ENTRY_INVALID", `PM Studio contains an unsupported entry: ${relative}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
visit(root);
|
|
193
|
+
|
|
194
|
+
const xattrRecords = parseXattrHex(xattrOutput, root, ignoredXattrs);
|
|
195
|
+
const hash = createHash("sha256");
|
|
196
|
+
for (const record of [...records, ...xattrRecords]) hash.update(`${JSON.stringify(record)}\n`);
|
|
197
|
+
return {
|
|
198
|
+
scheme: BUNDLE_CONTENT_SCHEME,
|
|
199
|
+
sha256: hash.digest("hex"),
|
|
200
|
+
entryCount: records.length,
|
|
201
|
+
regularFileCount,
|
|
202
|
+
regularBytes,
|
|
203
|
+
symlinkCount,
|
|
204
|
+
xattrCount: xattrRecords.length,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function defaultInspectBundleContent({ appPath, recipe, processRunner }) {
|
|
209
|
+
const xattrs = runChecked(processRunner, "/usr/bin/xattr", ["-r", "-s", "-x", "-l", appPath],
|
|
210
|
+
"Reading PM Studio extended attributes");
|
|
211
|
+
return inspectBundleContent(appPath, {
|
|
212
|
+
xattrOutput: xattrs.stdout,
|
|
213
|
+
ignoredXattrs: recipe.sourceBundleContent?.ignoredXattrs || [],
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
56
217
|
function plistRead(processRunner, plistPath, key) {
|
|
57
218
|
return runChecked(processRunner, "/usr/bin/plutil", ["-extract", key, "raw", "-o", "-", plistPath],
|
|
58
219
|
`Reading ${path.basename(plistPath)}`).stdout.trim();
|
|
@@ -91,30 +252,39 @@ function defaultInspectCodeSign({ appPath, processRunner }) {
|
|
|
91
252
|
const verify = normalizedProcessResult(processRunner("/usr/bin/codesign", [
|
|
92
253
|
"--verify", "--deep", "--strict", "--verbose=2", appPath,
|
|
93
254
|
]));
|
|
94
|
-
if (verify.error || verify.status !== 0) return { valid: false, adHoc: false };
|
|
95
255
|
const display = normalizedProcessResult(processRunner("/usr/bin/codesign", [
|
|
96
256
|
"--display", "--verbose=4", appPath,
|
|
97
257
|
]));
|
|
98
|
-
if (display.error || display.status !== 0) return { valid: false, adHoc: false };
|
|
99
258
|
const details = `${display.stdout}\n${display.stderr}`;
|
|
100
259
|
const entitlementsResult = normalizedProcessResult(processRunner("/usr/bin/codesign", [
|
|
101
260
|
"--display", "--entitlements", "-", "--xml", appPath,
|
|
102
261
|
]));
|
|
103
|
-
if (entitlementsResult.error || entitlementsResult.status !== 0) return { valid: false, adHoc: false };
|
|
104
262
|
const entitlementOutput = `${entitlementsResult.stdout}\n${entitlementsResult.stderr}`;
|
|
105
263
|
const plistStart = entitlementOutput.indexOf("<?xml");
|
|
106
264
|
const plistEnd = entitlementOutput.lastIndexOf("</plist>");
|
|
107
|
-
|
|
108
|
-
const entitlements =
|
|
265
|
+
const hasEntitlements = plistStart >= 0 && plistEnd >= plistStart;
|
|
266
|
+
const entitlements = hasEntitlements
|
|
267
|
+
? entitlementOutput.slice(plistStart, plistEnd + "</plist>".length)
|
|
268
|
+
: "";
|
|
109
269
|
return {
|
|
110
|
-
valid:
|
|
270
|
+
valid: !verify.error && verify.status === 0
|
|
271
|
+
&& !display.error && display.status === 0
|
|
272
|
+
&& !entitlementsResult.error && entitlementsResult.status === 0
|
|
273
|
+
&& hasEntitlements,
|
|
274
|
+
verifyValid: !verify.error && verify.status === 0,
|
|
275
|
+
displayValid: !display.error && display.status === 0,
|
|
276
|
+
entitlementsState: hasEntitlements
|
|
277
|
+
? "xml"
|
|
278
|
+
: /invalid entitlements blob/i.test(entitlementOutput) ? "invalid" : "unavailable",
|
|
111
279
|
adHoc: /(?:^|\n)Signature=adhoc(?:\n|$)/i.test(details)
|
|
112
280
|
|| /(?:^|\n)TeamIdentifier=not set(?:\n|$)/i.test(details),
|
|
113
281
|
identifier: details.match(/(?:^|\n)Identifier=([^\n]+)/)?.[1]?.trim() || "",
|
|
114
282
|
teamIdentifier: details.match(/(?:^|\n)TeamIdentifier=([^\n]+)/)?.[1]?.trim() || "",
|
|
115
283
|
flags: details.match(/\bflags=([^\s]+)/)?.[1] || "",
|
|
116
284
|
runtimeVersion: details.match(/(?:^|\n)Runtime Version=([^\n]+)/)?.[1]?.trim() || "",
|
|
117
|
-
|
|
285
|
+
cdHashFull: details.match(/(?:^|\n)CandidateCDHashFull sha256=([0-9a-f]+)/i)?.[1]?.toLowerCase() || "",
|
|
286
|
+
notarizationTicket: details.match(/(?:^|\n)Notarization Ticket=([^\n]+)/)?.[1]?.trim() || "",
|
|
287
|
+
entitlementsSha256: hasEntitlements ? sha256Hex(entitlements) : "",
|
|
118
288
|
};
|
|
119
289
|
}
|
|
120
290
|
|
|
@@ -240,6 +410,7 @@ export function createPmStudioSetupOperations(overrides = {}) {
|
|
|
240
410
|
readAsarIntegrity: defaultReadAsarIntegrity,
|
|
241
411
|
writeAsarIntegrity: defaultWriteAsarIntegrity,
|
|
242
412
|
inspectCodeSign: defaultInspectCodeSign,
|
|
413
|
+
inspectBundleContent: defaultInspectBundleContent,
|
|
243
414
|
inspectExecutableIntegrity: defaultInspectExecutableIntegrity,
|
|
244
415
|
signApp: defaultSignApp,
|
|
245
416
|
listBlockingProcesses: defaultListBlockingProcesses,
|
|
@@ -271,12 +442,34 @@ function integrityMatches(actual, algorithm, hash) {
|
|
|
271
442
|
return actual?.algorithm === algorithm && actual?.hash === hash;
|
|
272
443
|
}
|
|
273
444
|
|
|
445
|
+
function bundleContentMatches(actual, expected) {
|
|
446
|
+
return actual?.scheme === expected?.scheme
|
|
447
|
+
&& actual?.sha256 === expected?.sha256
|
|
448
|
+
&& actual?.entryCount === expected?.entryCount
|
|
449
|
+
&& actual?.regularFileCount === expected?.regularFileCount
|
|
450
|
+
&& actual?.regularBytes === expected?.regularBytes
|
|
451
|
+
&& actual?.symlinkCount === expected?.symlinkCount
|
|
452
|
+
&& actual?.xattrCount === expected?.xattrCount;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function sourceCodeSignatureMatches(actual, expected) {
|
|
456
|
+
return actual?.adHoc === false
|
|
457
|
+
&& actual?.displayValid === true
|
|
458
|
+
&& actual?.identifier === expected?.identifier
|
|
459
|
+
&& actual?.teamIdentifier === expected?.teamIdentifier
|
|
460
|
+
&& actual?.flags === expected?.flags
|
|
461
|
+
&& actual?.runtimeVersion === expected?.runtimeVersion
|
|
462
|
+
&& actual?.cdHashFull === expected?.cdHashFull
|
|
463
|
+
&& actual?.notarizationTicket === expected?.notarizationTicket;
|
|
464
|
+
}
|
|
465
|
+
|
|
274
466
|
export function inspectPmStudioApp({
|
|
275
467
|
appPath,
|
|
276
468
|
recipe = PM_STUDIO_2_9_7_RECIPE,
|
|
277
469
|
operations = createPmStudioSetupOperations(),
|
|
278
470
|
} = {}) {
|
|
279
471
|
const paths = appPaths(appPath, recipe);
|
|
472
|
+
const issues = [];
|
|
280
473
|
const metadata = operations.readBundleMetadata({ ...paths, recipe, processRunner: operations.processRunner });
|
|
281
474
|
const asar = inspectAsarBuffer(fs.readFileSync(paths.asarPath), recipe);
|
|
282
475
|
const plistIntegrity = operations.readAsarIntegrity({ ...paths, recipe, processRunner: operations.processRunner });
|
|
@@ -284,7 +477,16 @@ export function inspectPmStudioApp({
|
|
|
284
477
|
...paths, recipe, processRunner: operations.processRunner,
|
|
285
478
|
});
|
|
286
479
|
const codeSign = operations.inspectCodeSign({ ...paths, recipe, processRunner: operations.processRunner });
|
|
287
|
-
|
|
480
|
+
let bundleContent = null;
|
|
481
|
+
if (recipe.sourceBundleContent) {
|
|
482
|
+
try {
|
|
483
|
+
bundleContent = operations.inspectBundleContent({
|
|
484
|
+
...paths, recipe, processRunner: operations.processRunner,
|
|
485
|
+
});
|
|
486
|
+
} catch (error) {
|
|
487
|
+
issues.push(`bundle content inspection failed: ${error.message}`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
288
490
|
|
|
289
491
|
if (String(metadata.version) !== recipe.version) {
|
|
290
492
|
issues.push(`version is ${metadata.version}, expected ${recipe.version}`);
|
|
@@ -301,21 +503,39 @@ export function inspectPmStudioApp({
|
|
|
301
503
|
issues.push("Electron Framework has an unsupported embedded ASAR integrity slot");
|
|
302
504
|
}
|
|
303
505
|
|
|
506
|
+
const exactSourceBundle = !recipe.sourceBundleContent
|
|
507
|
+
|| bundleContentMatches(bundleContent, recipe.sourceBundleContent);
|
|
508
|
+
const strictVendorSignature = codeSign?.valid === true
|
|
509
|
+
&& codeSign?.adHoc === false
|
|
510
|
+
&& codeSign?.teamIdentifier === recipe.sourceTeamIdentifier;
|
|
511
|
+
const exactInvalidVendorSignature = recipe.sourceBundleContent
|
|
512
|
+
&& exactSourceBundle
|
|
513
|
+
&& codeSign?.valid === false
|
|
514
|
+
&& codeSign?.verifyValid === false
|
|
515
|
+
&& codeSign?.entitlementsState === "invalid"
|
|
516
|
+
&& sourceCodeSignatureMatches(codeSign, recipe.sourceCodeSignature);
|
|
517
|
+
const sourceSignatureAccepted = strictVendorSignature || exactInvalidVendorSignature;
|
|
518
|
+
const patchedSignatureAccepted = codeSign?.valid === true
|
|
519
|
+
&& codeSign?.adHoc === true
|
|
520
|
+
&& (!recipe.patchedSigningMetadata
|
|
521
|
+
|| JSON.stringify(signingMetadata(codeSign)) === JSON.stringify(recipe.patchedSigningMetadata));
|
|
304
522
|
const clean = issues.length === 0
|
|
305
523
|
&& asar.state === "clean"
|
|
306
524
|
&& integrityMatches(plistIntegrity, "SHA256", recipe.sourceHeaderSha256)
|
|
307
525
|
&& executableIntegrity.executableSha256 === recipe.sourceExecutableSha256
|
|
308
526
|
&& executableIntegrity.frameworkSha256 === recipe.sourceElectronFrameworkSha256
|
|
309
|
-
&&
|
|
310
|
-
&&
|
|
311
|
-
&& codeSign?.teamIdentifier === recipe.sourceTeamIdentifier;
|
|
527
|
+
&& exactSourceBundle
|
|
528
|
+
&& sourceSignatureAccepted;
|
|
312
529
|
const patched = issues.length === 0
|
|
313
530
|
&& asar.state === "patched"
|
|
314
531
|
&& integrityMatches(plistIntegrity, "SHA256", recipe.patchedHeaderSha256)
|
|
315
|
-
&&
|
|
316
|
-
|
|
532
|
+
&& patchedSignatureAccepted;
|
|
533
|
+
const legacy = issues.length === 0
|
|
534
|
+
&& asar.state === "legacy"
|
|
535
|
+
&& integrityMatches(plistIntegrity, "SHA256", recipe.legacy?.headerSha256)
|
|
536
|
+
&& patchedSignatureAccepted;
|
|
317
537
|
|
|
318
|
-
if (!clean && !patched) {
|
|
538
|
+
if (!clean && !patched && !legacy) {
|
|
319
539
|
if (asar.state === "drift") issues.push("app.asar is unknown drift");
|
|
320
540
|
else if (asar.state === "clean"
|
|
321
541
|
&& !integrityMatches(plistIntegrity, "SHA256", recipe.sourceHeaderSha256)) {
|
|
@@ -323,10 +543,20 @@ export function inspectPmStudioApp({
|
|
|
323
543
|
} else if (asar.state === "patched"
|
|
324
544
|
&& !integrityMatches(plistIntegrity, "SHA256", recipe.patchedHeaderSha256)) {
|
|
325
545
|
issues.push("patched ASAR does not match ElectronAsarIntegrity");
|
|
546
|
+
} else if (asar.state === "legacy"
|
|
547
|
+
&& !integrityMatches(plistIntegrity, "SHA256", recipe.legacy?.headerSha256)) {
|
|
548
|
+
issues.push("legacy ASAR does not match ElectronAsarIntegrity");
|
|
549
|
+
}
|
|
550
|
+
if (asar.state === "clean" && recipe.sourceBundleContent && !exactSourceBundle) {
|
|
551
|
+
issues.push("bundle content does not match the exact clean recipe");
|
|
552
|
+
}
|
|
553
|
+
if (!codeSign?.valid && !(asar.state === "clean" && exactInvalidVendorSignature)) {
|
|
554
|
+
issues.push("codesign verification failed");
|
|
326
555
|
}
|
|
327
|
-
if (!codeSign?.valid) issues.push("codesign verification failed");
|
|
328
556
|
else if (asar.state === "clean" && codeSign.adHoc) issues.push("clean bundle has an unexpected ad-hoc signature");
|
|
329
|
-
else if (asar.state
|
|
557
|
+
else if (["patched", "legacy"].includes(asar.state) && !codeSign.adHoc) {
|
|
558
|
+
issues.push(`${asar.state} bundle is not ad-hoc signed`);
|
|
559
|
+
}
|
|
330
560
|
else if (asar.state === "clean" && codeSign.teamIdentifier !== recipe.sourceTeamIdentifier) {
|
|
331
561
|
issues.push(`clean bundle TeamIdentifier is ${codeSign.teamIdentifier || "missing"}, expected ${recipe.sourceTeamIdentifier}`);
|
|
332
562
|
}
|
|
@@ -338,10 +568,15 @@ export function inspectPmStudioApp({
|
|
|
338
568
|
&& executableIntegrity.frameworkSha256 !== recipe.sourceElectronFrameworkSha256) {
|
|
339
569
|
issues.push("Electron Framework hash does not match the clean recipe");
|
|
340
570
|
}
|
|
571
|
+
if (["patched", "legacy"].includes(asar.state) && codeSign?.valid && codeSign?.adHoc
|
|
572
|
+
&& recipe.patchedSigningMetadata
|
|
573
|
+
&& !patchedSignatureAccepted) {
|
|
574
|
+
issues.push(`${asar.state} bundle signing metadata does not match the recipe`);
|
|
575
|
+
}
|
|
341
576
|
}
|
|
342
577
|
|
|
343
578
|
return {
|
|
344
|
-
state: clean ? "clean" : patched ? "patched" : "drift",
|
|
579
|
+
state: clean ? "clean" : patched ? "patched" : legacy ? "legacy" : "drift",
|
|
345
580
|
metadata: {
|
|
346
581
|
version: String(metadata.version),
|
|
347
582
|
build: String(metadata.build),
|
|
@@ -351,6 +586,10 @@ export function inspectPmStudioApp({
|
|
|
351
586
|
plistIntegrity,
|
|
352
587
|
executableIntegrity,
|
|
353
588
|
codeSign,
|
|
589
|
+
bundleContent,
|
|
590
|
+
sourceVerification: clean
|
|
591
|
+
? exactInvalidVendorSignature ? "exact-bundle-content" : "codesign"
|
|
592
|
+
: "",
|
|
354
593
|
paths,
|
|
355
594
|
issues,
|
|
356
595
|
};
|
|
@@ -393,7 +632,8 @@ function selectRecipe(metadata, recipes) {
|
|
|
393
632
|
}
|
|
394
633
|
|
|
395
634
|
function backupName(recipe) {
|
|
396
|
-
|
|
635
|
+
const sourceHash = recipe.sourceBundleContent?.sha256 || recipe.sourceAsarSha256;
|
|
636
|
+
return `${recipe.id}-${sourceHash.slice(0, 12)}`;
|
|
397
637
|
}
|
|
398
638
|
|
|
399
639
|
export function pmStudioBackupRoot(home = os.homedir()) {
|
|
@@ -411,7 +651,7 @@ export function pmStudioPatchManifestPath({
|
|
|
411
651
|
|
|
412
652
|
function backupManifest(recipe, createdAt) {
|
|
413
653
|
return {
|
|
414
|
-
schema_version: 1,
|
|
654
|
+
schema_version: recipe.sourceBundleContent ? 2 : 1,
|
|
415
655
|
kind: "ccdx-pm-studio-backup",
|
|
416
656
|
recipe_id: recipe.id,
|
|
417
657
|
created_at: createdAt.toISOString(),
|
|
@@ -430,18 +670,38 @@ function backupManifest(recipe, createdAt) {
|
|
|
430
670
|
electron_framework_sha256: recipe.sourceElectronFrameworkSha256,
|
|
431
671
|
embedded_asar_integrity: recipe.embeddedAsarIntegrity,
|
|
432
672
|
},
|
|
673
|
+
...(recipe.sourceBundleContent ? { bundle_content: recipe.sourceBundleContent } : {}),
|
|
674
|
+
...(recipe.sourceArtifact ? { artifact: recipe.sourceArtifact } : {}),
|
|
433
675
|
},
|
|
434
676
|
patched: {
|
|
435
677
|
asar_sha256: recipe.patchedAsarSha256,
|
|
436
678
|
asar_header_sha256: recipe.patchedHeaderSha256,
|
|
437
679
|
electron_asar_integrity: { algorithm: "SHA256", hash: recipe.patchedHeaderSha256 },
|
|
438
680
|
binaries: null,
|
|
681
|
+
...(recipe.sourceBundleContent ? { bundle_content: null } : {}),
|
|
439
682
|
},
|
|
440
683
|
backup_bundle: "PM Studio.app",
|
|
441
684
|
restore_rule: `Restore only while the installed app is PM Studio ${recipe.version} build ${recipe.build}.`,
|
|
442
685
|
};
|
|
443
686
|
}
|
|
444
687
|
|
|
688
|
+
function patchRecordMatches(record, recipe, state) {
|
|
689
|
+
const expected = state === "legacy"
|
|
690
|
+
? recipe.legacy
|
|
691
|
+
: { asarSha256: recipe.patchedAsarSha256, headerSha256: recipe.patchedHeaderSha256 };
|
|
692
|
+
return Boolean(expected)
|
|
693
|
+
&& record?.asar_sha256 === expected.asarSha256
|
|
694
|
+
&& record?.asar_header_sha256 === expected.headerSha256
|
|
695
|
+
&& integrityMatches(record?.electron_asar_integrity, "SHA256", expected.headerSha256);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function patchRecordForState(manifest, recipe, state) {
|
|
699
|
+
if (state === "legacy" && patchRecordMatches(manifest?.legacy_patched, recipe, state)) {
|
|
700
|
+
return manifest.legacy_patched;
|
|
701
|
+
}
|
|
702
|
+
return patchRecordMatches(manifest?.patched, recipe, state) ? manifest.patched : null;
|
|
703
|
+
}
|
|
704
|
+
|
|
445
705
|
function validateBackup({ backupDir, recipe, operations }) {
|
|
446
706
|
const manifestPath = path.join(backupDir, "manifest.json");
|
|
447
707
|
const backupAppPath = path.join(backupDir, "PM Studio.app");
|
|
@@ -451,7 +711,9 @@ function validateBackup({ backupDir, recipe, operations }) {
|
|
|
451
711
|
} catch {
|
|
452
712
|
throw setupError("PM_STUDIO_BACKUP_INVALID", `Existing PM Studio backup manifest is invalid: ${manifestPath}`);
|
|
453
713
|
}
|
|
454
|
-
|
|
714
|
+
const expectedSchema = recipe.sourceBundleContent ? 2 : 1;
|
|
715
|
+
if (manifest?.schema_version !== expectedSchema
|
|
716
|
+
|| manifest?.kind !== "ccdx-pm-studio-backup"
|
|
455
717
|
|| manifest?.recipe_id !== recipe.id
|
|
456
718
|
|| manifest?.app?.bundle_identifier !== recipe.bundleIdentifier
|
|
457
719
|
|| manifest?.app?.version !== recipe.version
|
|
@@ -462,9 +724,12 @@ function validateBackup({ backupDir, recipe, operations }) {
|
|
|
462
724
|
|| manifest?.source?.binaries?.main_executable_sha256 !== recipe.sourceExecutableSha256
|
|
463
725
|
|| manifest?.source?.binaries?.electron_framework_sha256 !== recipe.sourceElectronFrameworkSha256
|
|
464
726
|
|| manifest?.source?.binaries?.embedded_asar_integrity !== recipe.embeddedAsarIntegrity
|
|
465
|
-
||
|
|
466
|
-
|
|
467
|
-
||
|
|
727
|
+
|| (recipe.sourceBundleContent
|
|
728
|
+
&& JSON.stringify(manifest?.source?.bundle_content) !== JSON.stringify(recipe.sourceBundleContent))
|
|
729
|
+
|| (recipe.sourceArtifact
|
|
730
|
+
&& JSON.stringify(manifest?.source?.artifact) !== JSON.stringify(recipe.sourceArtifact))
|
|
731
|
+
|| (!patchRecordForState(manifest, recipe, "patched")
|
|
732
|
+
&& !patchRecordForState(manifest, recipe, "legacy"))) {
|
|
468
733
|
throw setupError("PM_STUDIO_BACKUP_INVALID", `Existing PM Studio backup does not match recipe ${recipe.id}`);
|
|
469
734
|
}
|
|
470
735
|
const inspection = inspectPmStudioApp({ appPath: backupAppPath, recipe, operations });
|
|
@@ -508,13 +773,23 @@ function ensureBackup({ appPath, backupRoot, recipe, operations }) {
|
|
|
508
773
|
}
|
|
509
774
|
}
|
|
510
775
|
|
|
511
|
-
function writePatchedBinaryRecord({ backup, inspection, operations }) {
|
|
776
|
+
function writePatchedBinaryRecord({ backup, inspection, operations, recipe }) {
|
|
512
777
|
const manifest = JSON.parse(fs.readFileSync(backup.manifestPath, "utf8"));
|
|
513
|
-
manifest.
|
|
514
|
-
|
|
515
|
-
|
|
778
|
+
if (!patchRecordMatches(manifest.legacy_patched, recipe, "legacy")
|
|
779
|
+
&& patchRecordMatches(manifest.patched, recipe, "legacy")) {
|
|
780
|
+
manifest.legacy_patched = manifest.patched;
|
|
781
|
+
}
|
|
782
|
+
manifest.patched = {
|
|
783
|
+
asar_sha256: recipe.patchedAsarSha256,
|
|
784
|
+
asar_header_sha256: recipe.patchedHeaderSha256,
|
|
785
|
+
electron_asar_integrity: { algorithm: "SHA256", hash: recipe.patchedHeaderSha256 },
|
|
786
|
+
binaries: {
|
|
787
|
+
main_executable_sha256: inspection.executableIntegrity.executableSha256,
|
|
788
|
+
electron_framework_sha256: inspection.executableIntegrity.frameworkSha256,
|
|
789
|
+
},
|
|
790
|
+
signing_metadata: signingMetadata(inspection.codeSign),
|
|
791
|
+
...(recipe.sourceBundleContent ? { bundle_content: inspection.bundleContent } : {}),
|
|
516
792
|
};
|
|
517
|
-
manifest.patched.signing_metadata = signingMetadata(inspection.codeSign);
|
|
518
793
|
const temporaryPath = `${backup.manifestPath}.${operations.uuid()}.tmp`;
|
|
519
794
|
try {
|
|
520
795
|
fs.writeFileSync(temporaryPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
@@ -525,33 +800,55 @@ function writePatchedBinaryRecord({ backup, inspection, operations }) {
|
|
|
525
800
|
backup.manifest = manifest;
|
|
526
801
|
}
|
|
527
802
|
|
|
528
|
-
export function assertPatchedBinaryRecord({
|
|
803
|
+
export function assertPatchedBinaryRecord({
|
|
804
|
+
inspection,
|
|
805
|
+
manifestPath,
|
|
806
|
+
recipe,
|
|
807
|
+
recordState = inspection?.state === "legacy" ? "legacy" : "patched",
|
|
808
|
+
}) {
|
|
529
809
|
let manifest;
|
|
530
810
|
try {
|
|
531
811
|
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
532
812
|
} catch {
|
|
533
813
|
throw setupError("PM_STUDIO_PATCH_RECORD_INVALID", "PM Studio patched-binary record is missing or invalid");
|
|
534
814
|
}
|
|
535
|
-
|
|
815
|
+
const expectedSchema = recipe?.sourceBundleContent ? 2 : 1;
|
|
816
|
+
if (recipe && (manifest?.schema_version !== expectedSchema
|
|
536
817
|
|| manifest?.kind !== "ccdx-pm-studio-backup"
|
|
537
818
|
|| manifest?.recipe_id !== recipe.id
|
|
538
819
|
|| manifest?.app?.bundle_identifier !== recipe.bundleIdentifier
|
|
539
820
|
|| manifest?.app?.version !== recipe.version
|
|
540
821
|
|| manifest?.app?.build !== recipe.build
|
|
541
|
-
||
|
|
542
|
-
|
|
543
|
-
|
|
822
|
+
|| (recipe.sourceBundleContent && (
|
|
823
|
+
manifest?.source?.asar_sha256 !== recipe.sourceAsarSha256
|
|
824
|
+
|| manifest?.source?.asar_header_sha256 !== recipe.sourceHeaderSha256
|
|
825
|
+
|| !integrityMatches(manifest?.source?.electron_asar_integrity, "SHA256", recipe.sourceHeaderSha256)
|
|
826
|
+
|| manifest?.source?.binaries?.main_executable_sha256 !== recipe.sourceExecutableSha256
|
|
827
|
+
|| manifest?.source?.binaries?.electron_framework_sha256 !== recipe.sourceElectronFrameworkSha256
|
|
828
|
+
|| manifest?.source?.binaries?.embedded_asar_integrity !== recipe.embeddedAsarIntegrity
|
|
829
|
+
|| JSON.stringify(manifest?.source?.bundle_content) !== JSON.stringify(recipe.sourceBundleContent)
|
|
830
|
+
))
|
|
831
|
+
|| (recipe.sourceArtifact
|
|
832
|
+
&& JSON.stringify(manifest?.source?.artifact) !== JSON.stringify(recipe.sourceArtifact)))) {
|
|
544
833
|
throw setupError("PM_STUDIO_PATCH_RECORD_INVALID", "PM Studio patched-binary record does not match the installed patch recipe");
|
|
545
834
|
}
|
|
546
|
-
const
|
|
835
|
+
const record = patchRecordForState(manifest, recipe, recordState);
|
|
836
|
+
if (!record) {
|
|
837
|
+
throw setupError("PM_STUDIO_PATCH_RECORD_INVALID", `PM Studio ${recordState} record does not match the installed patch recipe`);
|
|
838
|
+
}
|
|
839
|
+
const binaries = record.binaries;
|
|
547
840
|
if (!binaries
|
|
548
841
|
|| binaries.main_executable_sha256 !== inspection.executableIntegrity.executableSha256
|
|
549
842
|
|| binaries.electron_framework_sha256 !== inspection.executableIntegrity.frameworkSha256) {
|
|
550
843
|
throw setupError("PM_STUDIO_BUNDLE_DRIFT", "PM Studio executable or Electron Framework differs from the installed patch record");
|
|
551
844
|
}
|
|
552
|
-
if (JSON.stringify(
|
|
845
|
+
if (JSON.stringify(record.signing_metadata) !== JSON.stringify(signingMetadata(inspection.codeSign))) {
|
|
553
846
|
throw setupError("PM_STUDIO_BUNDLE_DRIFT", "PM Studio signing metadata differs from the installed patch record");
|
|
554
847
|
}
|
|
848
|
+
if (recipe?.sourceBundleContent
|
|
849
|
+
&& JSON.stringify(record.bundle_content) !== JSON.stringify(inspection.bundleContent)) {
|
|
850
|
+
throw setupError("PM_STUDIO_BUNDLE_DRIFT", "PM Studio bundle content differs from the installed patch record");
|
|
851
|
+
}
|
|
555
852
|
}
|
|
556
853
|
|
|
557
854
|
function signingMetadata(codeSign) {
|
|
@@ -570,7 +867,15 @@ function flagsWithoutAdHoc(value) {
|
|
|
570
867
|
return `0x${(BigInt(`0x${match[1]}`) & ~2n).toString(16)}`;
|
|
571
868
|
}
|
|
572
869
|
|
|
573
|
-
function assertSigningMetadataPreserved(source, staged) {
|
|
870
|
+
function assertSigningMetadataPreserved(source, staged, recipe) {
|
|
871
|
+
if (recipe.patchedSigningMetadata) {
|
|
872
|
+
if (JSON.stringify(signingMetadata(staged.codeSign))
|
|
873
|
+
!== JSON.stringify(recipe.patchedSigningMetadata)) {
|
|
874
|
+
throw setupError("PM_STUDIO_SIGNING_METADATA_CHANGED",
|
|
875
|
+
"Ad-hoc signing did not produce the exact PM Studio signing metadata required by the recipe");
|
|
876
|
+
}
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
574
879
|
const before = signingMetadata(source.codeSign);
|
|
575
880
|
const after = signingMetadata(staged.codeSign);
|
|
576
881
|
before.flags = flagsWithoutAdHoc(before.flags);
|
|
@@ -657,23 +962,53 @@ export async function runPmStudioSetup({
|
|
|
657
962
|
emit(`[OK] PM Studio ${recipe.version} build ${recipe.build} is already patched; no backup or signing was repeated.`);
|
|
658
963
|
emit(`[OK] Verified backup: ${backup.backupAppPath}`);
|
|
659
964
|
emit(`[OK] Backup manifest: ${backup.manifestPath}`);
|
|
660
|
-
emit(`[INFO] Start ${commandName} before
|
|
965
|
+
emit(`[INFO] Start ${commandName} before using Claude in PM Studio; GPT remains on PM Studio's native GitHub Copilot path.`);
|
|
661
966
|
return { status: "already_patched", changed: false, recipe: recipe.id, backup, inspection: source, messages };
|
|
662
967
|
}
|
|
663
|
-
|
|
968
|
+
const migratingLegacy = source.state === "legacy";
|
|
969
|
+
if (source.state !== "clean" && !migratingLegacy) {
|
|
664
970
|
throw setupError("PM_STUDIO_BUNDLE_DRIFT",
|
|
665
|
-
`PM Studio ${recipe.version} build ${recipe.build} does not match the clean or
|
|
971
|
+
`PM Studio ${recipe.version} build ${recipe.build} does not match the clean, split-origin, or legacy recipe; no files were changed`, source.issues);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
if (source.sourceVerification === "exact-bundle-content") {
|
|
975
|
+
emit(`[WARN] macOS rejected the vendor signature; setup accepted only the exact PM Studio ${recipe.version} official bundle content fingerprint.`);
|
|
666
976
|
}
|
|
667
977
|
|
|
668
978
|
const backupExists = fs.existsSync(finalBackupDir);
|
|
979
|
+
let verifiedBackup = null;
|
|
980
|
+
if (migratingLegacy) {
|
|
981
|
+
if (!backupExists) {
|
|
982
|
+
throw setupError("PM_STUDIO_BACKUP_INVALID",
|
|
983
|
+
"Legacy PM Studio global-origin patch has no matching verified clean backup; no files were changed");
|
|
984
|
+
}
|
|
985
|
+
verifiedBackup = validateBackup({ backupDir: finalBackupDir, recipe, operations });
|
|
986
|
+
assertPatchedBinaryRecord({
|
|
987
|
+
inspection: source,
|
|
988
|
+
manifestPath: verifiedBackup.manifestPath,
|
|
989
|
+
recipe,
|
|
990
|
+
recordState: "legacy",
|
|
991
|
+
});
|
|
992
|
+
emit("[WARN] Legacy global-origin patch detected; setup will rebuild from the verified clean backup so GPT and enterprise authentication return to PM Studio's native origin.");
|
|
993
|
+
}
|
|
669
994
|
assertSpace({ appPath, backupRoot, backupExists, operations });
|
|
670
995
|
|
|
671
996
|
emit("[WARN] PM Studio's vendor signature will be replaced with an ad-hoc signature; an official update or reinstall may overwrite this patch.");
|
|
672
|
-
const backup = ensureBackup({ appPath, backupRoot, recipe, operations });
|
|
997
|
+
const backup = verifiedBackup || ensureBackup({ appPath, backupRoot, recipe, operations });
|
|
673
998
|
const stagePath = path.join(path.dirname(appPath), `.${path.basename(appPath)}.ccdx-stage-${operations.uuid()}`);
|
|
674
999
|
let preserveStage = false;
|
|
675
1000
|
try {
|
|
676
|
-
operations.copyBundle({
|
|
1001
|
+
operations.copyBundle({
|
|
1002
|
+
source: migratingLegacy ? backup.backupAppPath : appPath,
|
|
1003
|
+
destination: stagePath,
|
|
1004
|
+
processRunner: operations.processRunner,
|
|
1005
|
+
});
|
|
1006
|
+
const stagedSource = inspectPmStudioApp({ appPath: stagePath, recipe, operations });
|
|
1007
|
+
if (stagedSource.state !== "clean"
|
|
1008
|
+
|| (!migratingLegacy && recipe.sourceBundleContent
|
|
1009
|
+
&& stagedSource.bundleContent?.sha256 !== source.bundleContent?.sha256)) {
|
|
1010
|
+
throw setupError("PM_STUDIO_STAGE_INVALID", "PM Studio staging copy does not match the exact clean source bundle", stagedSource.issues);
|
|
1011
|
+
}
|
|
677
1012
|
const stageAsarPath = path.join(stagePath, recipe.asarPath);
|
|
678
1013
|
const patched = patchAsarBuffer(fs.readFileSync(stageAsarPath), recipe);
|
|
679
1014
|
if (!patched.changed) throw setupError("PM_STUDIO_STAGE_INVALID", "Staging copy was unexpectedly already patched");
|
|
@@ -691,14 +1026,16 @@ export async function runPmStudioSetup({
|
|
|
691
1026
|
if (staged.state !== "patched") {
|
|
692
1027
|
throw setupError("PM_STUDIO_STAGE_VERIFY_FAILED", "Patched PM Studio staging bundle failed verification", staged.issues);
|
|
693
1028
|
}
|
|
694
|
-
assertSigningMetadataPreserved(
|
|
695
|
-
writePatchedBinaryRecord({ backup, inspection: staged, operations });
|
|
1029
|
+
assertSigningMetadataPreserved(stagedSource, staged, recipe);
|
|
1030
|
+
writePatchedBinaryRecord({ backup, inspection: staged, operations, recipe });
|
|
696
1031
|
|
|
697
1032
|
assertNoBlockingProcesses(operations, appPath);
|
|
698
1033
|
const unchangedSource = inspectPmStudioApp({ appPath, recipe, operations });
|
|
699
|
-
if (unchangedSource.state !==
|
|
1034
|
+
if (unchangedSource.state !== source.state
|
|
700
1035
|
|| unchangedSource.asar.asarSha256 !== source.asar.asarSha256
|
|
701
|
-
|| unchangedSource.asar.headerSha256 !== source.asar.headerSha256
|
|
1036
|
+
|| unchangedSource.asar.headerSha256 !== source.asar.headerSha256
|
|
1037
|
+
|| (recipe.sourceBundleContent
|
|
1038
|
+
&& unchangedSource.bundleContent?.sha256 !== source.bundleContent?.sha256)) {
|
|
702
1039
|
throw setupError("PM_STUDIO_SOURCE_CHANGED", "PM Studio changed during setup; the verified staging bundle was not installed");
|
|
703
1040
|
}
|
|
704
1041
|
|
|
@@ -712,12 +1049,17 @@ export async function runPmStudioSetup({
|
|
|
712
1049
|
`PM Studio replacement final state could not be verified. Do not launch it. ${recoveryMessage(backup)}${preserveStage ? ` Verified staging retained: ${stagePath}.` : ""}`);
|
|
713
1050
|
}
|
|
714
1051
|
if (installed.state !== "patched") {
|
|
715
|
-
|
|
716
|
-
|
|
1052
|
+
const originalRetained = installed.state === source.state
|
|
1053
|
+
&& installed.asar.asarSha256 === source.asar.asarSha256
|
|
1054
|
+
&& installed.asar.headerSha256 === source.asar.headerSha256
|
|
1055
|
+
&& (!recipe.sourceBundleContent
|
|
1056
|
+
|| installed.bundleContent?.sha256 === source.bundleContent?.sha256);
|
|
1057
|
+
preserveStage = !originalRetained && fs.existsSync(stagePath);
|
|
1058
|
+
const code = originalRetained
|
|
717
1059
|
? "PM_STUDIO_REPLACE_FAILED"
|
|
718
1060
|
: "PM_STUDIO_INSTALL_VERIFY_FAILED";
|
|
719
|
-
const stateMessage =
|
|
720
|
-
?
|
|
1061
|
+
const stateMessage = originalRetained
|
|
1062
|
+
? `The installed PM Studio remains the verified ${source.state} bundle`
|
|
721
1063
|
: "The installed PM Studio did not retain the verified patched state; do not launch it";
|
|
722
1064
|
throw setupError(code,
|
|
723
1065
|
`${stateMessage}. ${recoveryMessage(backup)}${preserveStage ? ` Verified staging retained: ${stagePath}.` : ""}`,
|
|
@@ -734,7 +1076,9 @@ export async function runPmStudioSetup({
|
|
|
734
1076
|
emit("[WARN] Atomic replacement reported an error after the exact patched bundle became installed; the final bundle was independently verified.");
|
|
735
1077
|
}
|
|
736
1078
|
|
|
737
|
-
emit(
|
|
1079
|
+
emit(migratingLegacy
|
|
1080
|
+
? `[OK] Migrated PM Studio ${recipe.version} build ${recipe.build} from the legacy global-origin patch to the split-origin patch.`
|
|
1081
|
+
: `[OK] Patched PM Studio ${recipe.version} build ${recipe.build}.`);
|
|
738
1082
|
emit(`[OK] app.asar SHA-256: ${installed.asar.asarSha256}`);
|
|
739
1083
|
emit(`[OK] Verified backup: ${backup.backupAppPath}${backup.reused ? " (reused)" : ""}`);
|
|
740
1084
|
emit(`[OK] Backup manifest: ${backup.manifestPath}`);
|
|
@@ -742,9 +1086,9 @@ export async function runPmStudioSetup({
|
|
|
742
1086
|
emit(`[INFO] Restore step 1/3: quit PM Studio and its updater.`);
|
|
743
1087
|
emit(`[INFO] Restore step 2/3: confirm the installed Info.plist still reports version/build ${recipe.version}/${recipe.build}; never restore this backup over another version.`);
|
|
744
1088
|
emit(`[INFO] Restore step 3/3: move ${appPath} aside, copy ${backup.backupAppPath} to that exact path, and verify its signature before launch.`);
|
|
745
|
-
emit(`[INFO] Start ${commandName} before
|
|
1089
|
+
emit(`[INFO] Start ${commandName} before using Claude in PM Studio; GPT remains on PM Studio's native GitHub Copilot path.`);
|
|
746
1090
|
return {
|
|
747
|
-
status: "patched",
|
|
1091
|
+
status: migratingLegacy ? "migrated" : "patched",
|
|
748
1092
|
changed: true,
|
|
749
1093
|
recipe: recipe.id,
|
|
750
1094
|
backup,
|