codex-copilot-dx 0.6.20 → 0.7.0
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 +123 -326
- package/bin/cli.mjs +13 -129
- package/package.json +2 -8
- package/scripts/benchmark.mjs +0 -114
- package/src/adapter.mjs +19 -106
- package/src/auth-profile.mjs +18 -130
- package/src/cli-auth.mjs +44 -398
- package/src/cli-models.mjs +21 -49
- package/src/cli-options.mjs +45 -75
- package/src/cli-status.mjs +6 -23
- package/src/config.mjs +2 -8
- package/src/copilot.mjs +2 -2
- package/src/doctor.mjs +44 -291
- package/src/model-cache.mjs +4 -11
- package/src/models.mjs +0 -184
- package/src/observability.mjs +1 -38
- package/src/profile-model-runtime.mjs +61 -189
- package/src/profile-routing.mjs +2 -102
- package/src/profile-runtime.mjs +15 -68
- package/src/responses-handler.mjs +3 -1
- package/src/responses-proxy.mjs +1 -1
- package/src/running-adapter.mjs +2 -4
- package/src/stream-errors.mjs +3 -6
- package/src/stream-performance.mjs +1 -1
- package/src/usage.mjs +0 -31
- package/src/anthropic-handler.mjs +0 -195
- package/src/anthropic.mjs +0 -387
- package/src/claude-config.mjs +0 -57
- package/src/claude-desktop-config.mjs +0 -317
- package/src/cli-error.mjs +0 -50
- package/src/pm-studio-asar.mjs +0 -1244
- package/src/pm-studio-bundle-content.mjs +0 -165
- package/src/pm-studio-relay.mjs +0 -563
- package/src/pm-studio-setup-lock.mjs +0 -336
- package/src/pm-studio-setup.mjs +0 -1581
- package/src/pm-studio-status.mjs +0 -369
- package/src/token-counter-service.mjs +0 -211
- package/src/token-counter-worker.mjs +0 -16
package/src/pm-studio-setup.mjs
DELETED
|
@@ -1,1581 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { randomUUID } from "node:crypto";
|
|
5
|
-
import { spawnSync } from "node:child_process";
|
|
6
|
-
import {
|
|
7
|
-
AUTH_PROFILE_CLAUDE,
|
|
8
|
-
readAuthProfileCredentials,
|
|
9
|
-
} from "./auth-profile.mjs";
|
|
10
|
-
import {
|
|
11
|
-
PM_STUDIO_2_9_7_RECIPE,
|
|
12
|
-
PM_STUDIO_2_9_10_RECIPE,
|
|
13
|
-
PM_STUDIO_RECIPES,
|
|
14
|
-
createCompatiblePmStudioRecipe,
|
|
15
|
-
createCompatiblePmStudioRecipeFromFile,
|
|
16
|
-
inspectAsarFile,
|
|
17
|
-
inspectElectronAsarIntegritySlots,
|
|
18
|
-
inspectElectronAsarIntegrityFile,
|
|
19
|
-
inspectAsarBuffer,
|
|
20
|
-
patchAsarBuffer,
|
|
21
|
-
sha256Hex,
|
|
22
|
-
} from "./pm-studio-asar.mjs";
|
|
23
|
-
import { inspectBundleContent } from "./pm-studio-bundle-content.mjs";
|
|
24
|
-
import { withPmStudioSetupLock } from "./pm-studio-setup-lock.mjs";
|
|
25
|
-
|
|
26
|
-
export { inspectBundleContent } from "./pm-studio-bundle-content.mjs";
|
|
27
|
-
|
|
28
|
-
export const DEFAULT_PM_STUDIO_APP_PATH = "/Applications/PM Studio.app";
|
|
29
|
-
export const PM_STUDIO_CLAUDE_AUTH_COMMAND = "ccdx auth login claude --reauth --github-login <personal-login>";
|
|
30
|
-
const MIN_FREE_MARGIN_BYTES = 16 * 1024 * 1024;
|
|
31
|
-
|
|
32
|
-
function setupError(code, message, details) {
|
|
33
|
-
const error = new Error(message);
|
|
34
|
-
error.code = code;
|
|
35
|
-
if (details !== undefined) error.details = details;
|
|
36
|
-
return error;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function defaultProcessRunner(command, args) {
|
|
40
|
-
return spawnSync(command, args, {
|
|
41
|
-
encoding: "utf8",
|
|
42
|
-
windowsHide: true,
|
|
43
|
-
maxBuffer: 32 * 1024 * 1024,
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function normalizedProcessResult(result) {
|
|
48
|
-
if (typeof result === "string") return { status: 0, stdout: result, stderr: "" };
|
|
49
|
-
return {
|
|
50
|
-
status: result?.status ?? (result?.error ? 1 : 0),
|
|
51
|
-
stdout: String(result?.stdout || ""),
|
|
52
|
-
stderr: String(result?.stderr || ""),
|
|
53
|
-
error: result?.error,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function runChecked(processRunner, command, args, label) {
|
|
58
|
-
const result = normalizedProcessResult(processRunner(command, args));
|
|
59
|
-
if (result.error || result.status !== 0) {
|
|
60
|
-
throw setupError("PM_STUDIO_PROCESS_FAILED", `${label} failed`);
|
|
61
|
-
}
|
|
62
|
-
return result;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function defaultInspectBundleContent({ appPath, recipe, processRunner, readBuffer }) {
|
|
66
|
-
const xattrs = runChecked(processRunner, "/usr/bin/xattr", ["-r", "-s", "-x", "-l", appPath],
|
|
67
|
-
"Reading PM Studio extended attributes");
|
|
68
|
-
return inspectBundleContent(appPath, {
|
|
69
|
-
xattrOutput: xattrs.stdout,
|
|
70
|
-
ignoredXattrs: recipe.sourceBundleContent?.ignoredXattrs || [],
|
|
71
|
-
readBuffer,
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function plistRead(processRunner, plistPath, key) {
|
|
76
|
-
return runChecked(processRunner, "/usr/bin/plutil", ["-extract", key, "raw", "-o", "-", plistPath],
|
|
77
|
-
`Reading ${path.basename(plistPath)}`).stdout.trim();
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function defaultReadBundleMetadata({ infoPlistPath, processRunner }) {
|
|
81
|
-
return {
|
|
82
|
-
version: plistRead(processRunner, infoPlistPath, "CFBundleShortVersionString"),
|
|
83
|
-
build: plistRead(processRunner, infoPlistPath, "CFBundleVersion"),
|
|
84
|
-
bundleIdentifier: plistRead(processRunner, infoPlistPath, "CFBundleIdentifier"),
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function defaultReadAsarIntegrity({ infoPlistPath, recipe, processRunner }) {
|
|
89
|
-
const key = recipe.integrityKey.split(".")[0];
|
|
90
|
-
const result = runChecked(processRunner, "/usr/bin/plutil", [
|
|
91
|
-
"-extract", key, "json", "-o", "-", infoPlistPath,
|
|
92
|
-
], "Reading ElectronAsarIntegrity");
|
|
93
|
-
const dictionary = JSON.parse(result.stdout);
|
|
94
|
-
return dictionary["Resources/app.asar"] || null;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function defaultWriteAsarIntegrity({ infoPlistPath, recipe, hash, processRunner }) {
|
|
98
|
-
const key = recipe.integrityKey.split(".")[0];
|
|
99
|
-
const result = runChecked(processRunner, "/usr/bin/plutil", [
|
|
100
|
-
"-extract", key, "json", "-o", "-", infoPlistPath,
|
|
101
|
-
], "Reading ElectronAsarIntegrity");
|
|
102
|
-
const dictionary = JSON.parse(result.stdout);
|
|
103
|
-
dictionary["Resources/app.asar"] = { algorithm: "SHA256", hash };
|
|
104
|
-
runChecked(processRunner, "/usr/bin/plutil", [
|
|
105
|
-
"-replace", key, "-json", JSON.stringify(dictionary), infoPlistPath,
|
|
106
|
-
], "Updating ElectronAsarIntegrity hash");
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function defaultInspectCodeSign({ appPath, processRunner }) {
|
|
110
|
-
const verify = normalizedProcessResult(processRunner("/usr/bin/codesign", [
|
|
111
|
-
"--verify", "--deep", "--strict", "--verbose=2", appPath,
|
|
112
|
-
]));
|
|
113
|
-
const display = normalizedProcessResult(processRunner("/usr/bin/codesign", [
|
|
114
|
-
"--display", "--verbose=4", appPath,
|
|
115
|
-
]));
|
|
116
|
-
const details = `${display.stdout}\n${display.stderr}`;
|
|
117
|
-
const entitlementsResult = normalizedProcessResult(processRunner("/usr/bin/codesign", [
|
|
118
|
-
"--display", "--entitlements", "-", "--xml", appPath,
|
|
119
|
-
]));
|
|
120
|
-
const entitlementOutput = `${entitlementsResult.stdout}\n${entitlementsResult.stderr}`;
|
|
121
|
-
const plistStart = entitlementOutput.indexOf("<?xml");
|
|
122
|
-
const plistEnd = entitlementOutput.lastIndexOf("</plist>");
|
|
123
|
-
const hasEntitlements = plistStart >= 0 && plistEnd >= plistStart;
|
|
124
|
-
const entitlements = hasEntitlements
|
|
125
|
-
? entitlementOutput.slice(plistStart, plistEnd + "</plist>".length)
|
|
126
|
-
: "";
|
|
127
|
-
return {
|
|
128
|
-
valid: !verify.error && verify.status === 0
|
|
129
|
-
&& !display.error && display.status === 0,
|
|
130
|
-
verifyValid: !verify.error && verify.status === 0,
|
|
131
|
-
displayValid: !display.error && display.status === 0,
|
|
132
|
-
entitlementsState: hasEntitlements
|
|
133
|
-
? "xml"
|
|
134
|
-
: /invalid entitlements blob/i.test(entitlementOutput) ? "invalid" : "unavailable",
|
|
135
|
-
adHoc: /(?:^|\n)Signature=adhoc(?:\n|$)/i.test(details)
|
|
136
|
-
|| /(?:^|\n)TeamIdentifier=not set(?:\n|$)/i.test(details),
|
|
137
|
-
identifier: details.match(/(?:^|\n)Identifier=([^\n]+)/)?.[1]?.trim() || "",
|
|
138
|
-
teamIdentifier: details.match(/(?:^|\n)TeamIdentifier=([^\n]+)/)?.[1]?.trim() || "",
|
|
139
|
-
flags: details.match(/\bflags=([^\s]+)/)?.[1] || "",
|
|
140
|
-
runtimeVersion: details.match(/(?:^|\n)Runtime Version=([^\n]+)/)?.[1]?.trim() || "",
|
|
141
|
-
cdHashFull: details.match(/(?:^|\n)CandidateCDHashFull sha256=([0-9a-f]+)/i)?.[1]?.toLowerCase() || "",
|
|
142
|
-
notarizationTicket: details.match(/(?:^|\n)Notarization Ticket=([^\n]+)/)?.[1]?.trim() || "",
|
|
143
|
-
entitlementsSha256: hasEntitlements ? sha256Hex(entitlements) : "",
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function defaultInspectExecutableIntegrity({ appPath, infoPlistPath, recipe, processRunner }) {
|
|
148
|
-
const executableName = plistRead(processRunner, infoPlistPath, "CFBundleExecutable");
|
|
149
|
-
if (!executableName || path.basename(executableName) !== executableName) {
|
|
150
|
-
throw setupError("PM_STUDIO_EXECUTABLE_INVALID", "PM Studio CFBundleExecutable is invalid");
|
|
151
|
-
}
|
|
152
|
-
const executablePath = path.join(appPath, "Contents", "MacOS", executableName);
|
|
153
|
-
const frameworkPath = path.join(appPath, recipe.electronFrameworkPath);
|
|
154
|
-
const executableBytes = fs.readFileSync(executablePath);
|
|
155
|
-
const frameworkBytes = fs.readFileSync(frameworkPath);
|
|
156
|
-
const executableSlots = inspectElectronAsarIntegritySlots(executableBytes);
|
|
157
|
-
const frameworkSlots = inspectElectronAsarIntegritySlots(frameworkBytes);
|
|
158
|
-
return {
|
|
159
|
-
executableName,
|
|
160
|
-
executablePath,
|
|
161
|
-
executableSha256: sha256Hex(executableBytes),
|
|
162
|
-
frameworkPath,
|
|
163
|
-
frameworkSha256: sha256Hex(frameworkBytes),
|
|
164
|
-
embeddedIntegrity: {
|
|
165
|
-
state: executableSlots.slots.length === 0 && frameworkSlots.slots.length === 0 ? "absent" : "present",
|
|
166
|
-
supported: executableSlots.slots.length === 0 && frameworkSlots.slots.length === 0,
|
|
167
|
-
executable: executableSlots,
|
|
168
|
-
framework: frameworkSlots,
|
|
169
|
-
},
|
|
170
|
-
matchesRecipeName: executableName === recipe.executable,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function statusInspectExecutableIntegrity({ appPath, infoPlistPath, recipe, processRunner, evidenceCache }) {
|
|
175
|
-
const executableName = plistRead(processRunner, infoPlistPath, "CFBundleExecutable");
|
|
176
|
-
if (!executableName || path.basename(executableName) !== executableName) {
|
|
177
|
-
throw setupError("PM_STUDIO_EXECUTABLE_INVALID", "PM Studio CFBundleExecutable is invalid");
|
|
178
|
-
}
|
|
179
|
-
const executablePath = path.join(appPath, "Contents", "MacOS", executableName);
|
|
180
|
-
const frameworkPath = path.join(appPath, recipe.electronFrameworkPath);
|
|
181
|
-
const executable = inspectElectronAsarIntegrityFile(executablePath, { cache: evidenceCache });
|
|
182
|
-
const framework = inspectElectronAsarIntegrityFile(frameworkPath, { cache: evidenceCache });
|
|
183
|
-
return {
|
|
184
|
-
executableName,
|
|
185
|
-
executablePath,
|
|
186
|
-
executableSha256: executable.sha256,
|
|
187
|
-
frameworkPath,
|
|
188
|
-
frameworkSha256: framework.sha256,
|
|
189
|
-
embeddedIntegrity: {
|
|
190
|
-
state: executable.integrity.slots.length === 0 && framework.integrity.slots.length === 0 ? "absent" : "present",
|
|
191
|
-
supported: executable.integrity.slots.length === 0 && framework.integrity.slots.length === 0,
|
|
192
|
-
executable: executable.integrity,
|
|
193
|
-
framework: framework.integrity,
|
|
194
|
-
},
|
|
195
|
-
matchesRecipeName: executableName === recipe.executable,
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function createPmStudioFileInspectionOperationOverrides() {
|
|
200
|
-
const evidenceCache = new Map();
|
|
201
|
-
const bundleReadBuffer = Buffer.allocUnsafe(1024 * 1024);
|
|
202
|
-
return {
|
|
203
|
-
inspectExecutableIntegrity: (args) => statusInspectExecutableIntegrity({
|
|
204
|
-
...args,
|
|
205
|
-
evidenceCache,
|
|
206
|
-
}),
|
|
207
|
-
inspectAsarFile: ({ asarPath, recipe }) => inspectAsarFile(asarPath, recipe, {
|
|
208
|
-
cache: evidenceCache,
|
|
209
|
-
releaseTargets: true,
|
|
210
|
-
}),
|
|
211
|
-
createCompatibleRecipeFromFile: ({ asarPath, options }) => createCompatiblePmStudioRecipeFromFile(
|
|
212
|
-
asarPath,
|
|
213
|
-
options,
|
|
214
|
-
{ cache: evidenceCache, releaseTargets: true },
|
|
215
|
-
),
|
|
216
|
-
inspectBundleContent: (args) => defaultInspectBundleContent({
|
|
217
|
-
...args,
|
|
218
|
-
readBuffer: bundleReadBuffer,
|
|
219
|
-
}),
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export function createPmStudioStatusOperationOverrides() {
|
|
224
|
-
return createPmStudioFileInspectionOperationOverrides();
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function defaultSignApp({ appPath, source, processRunner }) {
|
|
228
|
-
const preserve = [];
|
|
229
|
-
if (source?.codeSign?.displayValid === true) {
|
|
230
|
-
if (source.codeSign.entitlementsState === "xml") preserve.push("entitlements");
|
|
231
|
-
if (source.codeSign.flags) preserve.push("flags");
|
|
232
|
-
if (source.codeSign.runtimeVersion) preserve.push("runtime");
|
|
233
|
-
}
|
|
234
|
-
const args = [
|
|
235
|
-
"--force", "--deep", "--sign", "-", "--timestamp=none",
|
|
236
|
-
];
|
|
237
|
-
if (preserve.length > 0) args.push(`--preserve-metadata=${preserve.join(",")}`);
|
|
238
|
-
args.push(appPath);
|
|
239
|
-
runChecked(processRunner, "/usr/bin/codesign", args, "Ad-hoc codesigning PM Studio");
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function defaultListBlockingProcesses({ processRunner }) {
|
|
243
|
-
const output = runChecked(processRunner, "/bin/ps", ["-axo", "pid=,command="],
|
|
244
|
-
"Checking PM Studio processes").stdout;
|
|
245
|
-
return output.split(/\r?\n/).filter((line) => {
|
|
246
|
-
const value = line.toLowerCase();
|
|
247
|
-
return value.includes("/pm studio.app/")
|
|
248
|
-
|| value.includes("pm studio shipit")
|
|
249
|
-
|| value.includes("pm studio updater")
|
|
250
|
-
|| value.includes("com.pm-studio.app")
|
|
251
|
-
|| value.includes("com.electron.pm-studio.shipit");
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function defaultCopyBundle({ source, destination, processRunner }) {
|
|
256
|
-
if (fs.existsSync(destination)) throw setupError("PM_STUDIO_COPY_TARGET_EXISTS", `Copy target already exists: ${destination}`);
|
|
257
|
-
runChecked(processRunner, "/usr/bin/ditto", ["--rsrc", "--extattr", "--acl", source, destination],
|
|
258
|
-
"Copying PM Studio bundle");
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const ATOMIC_REPLACE_JXA = `function run(argv) {
|
|
262
|
-
ObjC.import("Foundation");
|
|
263
|
-
const manager = $.NSFileManager.defaultManager;
|
|
264
|
-
const oldUrl = $.NSURL.fileURLWithPath(argv[0]);
|
|
265
|
-
const newUrl = $.NSURL.fileURLWithPath(argv[1]);
|
|
266
|
-
const result = Ref();
|
|
267
|
-
const error = Ref();
|
|
268
|
-
const ok = manager.replaceItemAtURLWithItemAtURLBackupItemNameOptionsResultingItemURLError(
|
|
269
|
-
oldUrl, newUrl, undefined, 1, result, error
|
|
270
|
-
);
|
|
271
|
-
if (!ok) throw new Error(ObjC.unwrap(error[0].localizedDescription));
|
|
272
|
-
}`;
|
|
273
|
-
|
|
274
|
-
function defaultReplaceApp({ appPath, stagePath, processRunner }) {
|
|
275
|
-
runChecked(processRunner, "/usr/bin/osascript", [
|
|
276
|
-
"-l", "JavaScript", "-e", ATOMIC_REPLACE_JXA, "--", appPath, stagePath,
|
|
277
|
-
], "Atomically replacing PM Studio");
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function directorySize(directory) {
|
|
281
|
-
let total = 0;
|
|
282
|
-
const visit = (current) => {
|
|
283
|
-
const stat = fs.lstatSync(current);
|
|
284
|
-
total += stat.size;
|
|
285
|
-
if (!stat.isDirectory() || stat.isSymbolicLink()) return;
|
|
286
|
-
for (const name of fs.readdirSync(current)) visit(path.join(current, name));
|
|
287
|
-
};
|
|
288
|
-
visit(directory);
|
|
289
|
-
return total;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function nearestExistingPath(targetPath) {
|
|
293
|
-
let current = path.resolve(targetPath);
|
|
294
|
-
while (!fs.existsSync(current)) {
|
|
295
|
-
const parent = path.dirname(current);
|
|
296
|
-
if (parent === current) throw setupError("PM_STUDIO_PATH_UNAVAILABLE", `No existing parent for ${targetPath}`);
|
|
297
|
-
current = parent;
|
|
298
|
-
}
|
|
299
|
-
return current;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function defaultAvailableBytes(targetPath) {
|
|
303
|
-
const stat = fs.statfsSync(nearestExistingPath(targetPath), { bigint: true });
|
|
304
|
-
return Number(stat.bavail * stat.bsize);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function defaultVolumeId(targetPath) {
|
|
308
|
-
return String(fs.statSync(nearestExistingPath(targetPath)).dev);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function defaultAssertPermissions({ appPath, backupRoot }) {
|
|
312
|
-
try {
|
|
313
|
-
fs.accessSync(appPath, fs.constants.R_OK);
|
|
314
|
-
fs.accessSync(path.dirname(appPath), fs.constants.W_OK);
|
|
315
|
-
fs.accessSync(nearestExistingPath(backupRoot), fs.constants.W_OK);
|
|
316
|
-
} catch {
|
|
317
|
-
throw setupError("PM_STUDIO_PERMISSION_DENIED",
|
|
318
|
-
"PM Studio or its backup/staging location is not accessible to the current user; setup will not use sudo");
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function defaultAssertRestorePermissions({ appPath, backupAppPath }) {
|
|
323
|
-
try {
|
|
324
|
-
fs.accessSync(appPath, fs.constants.R_OK | fs.constants.X_OK);
|
|
325
|
-
fs.accessSync(path.dirname(appPath), fs.constants.W_OK | fs.constants.X_OK);
|
|
326
|
-
fs.accessSync(backupAppPath, fs.constants.R_OK | fs.constants.X_OK);
|
|
327
|
-
} catch {
|
|
328
|
-
throw setupError("PM_STUDIO_PERMISSION_DENIED",
|
|
329
|
-
"PM Studio or its verified backup is not accessible to the current user; restore will not use sudo");
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
export function createPmStudioSetupOperations(overrides = {}) {
|
|
334
|
-
const processRunner = overrides.processRunner || defaultProcessRunner;
|
|
335
|
-
return {
|
|
336
|
-
processRunner,
|
|
337
|
-
readBundleMetadata: defaultReadBundleMetadata,
|
|
338
|
-
readAsarIntegrity: defaultReadAsarIntegrity,
|
|
339
|
-
writeAsarIntegrity: defaultWriteAsarIntegrity,
|
|
340
|
-
inspectCodeSign: defaultInspectCodeSign,
|
|
341
|
-
inspectBundleContent: defaultInspectBundleContent,
|
|
342
|
-
inspectExecutableIntegrity: defaultInspectExecutableIntegrity,
|
|
343
|
-
withSetupLock: ({ appPath }, fn) => withPmStudioSetupLock({ appPath }, fn),
|
|
344
|
-
signApp: defaultSignApp,
|
|
345
|
-
listBlockingProcesses: defaultListBlockingProcesses,
|
|
346
|
-
copyBundle: defaultCopyBundle,
|
|
347
|
-
renamePath: ({ source, destination }) => fs.renameSync(source, destination),
|
|
348
|
-
replaceApp: defaultReplaceApp,
|
|
349
|
-
removePath: ({ target }) => fs.rmSync(target, { recursive: true, force: true }),
|
|
350
|
-
bundleSize: directorySize,
|
|
351
|
-
availableBytes: defaultAvailableBytes,
|
|
352
|
-
volumeId: defaultVolumeId,
|
|
353
|
-
assertPermissions: defaultAssertPermissions,
|
|
354
|
-
assertRestorePermissions: defaultAssertRestorePermissions,
|
|
355
|
-
readClaudeCredentials: ({ home }) => readAuthProfileCredentials(AUTH_PROFILE_CLAUDE, { home }),
|
|
356
|
-
now: () => new Date(),
|
|
357
|
-
uuid: () => randomUUID(),
|
|
358
|
-
...overrides,
|
|
359
|
-
processRunner,
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function appPaths(appPath, recipe) {
|
|
364
|
-
return {
|
|
365
|
-
appPath,
|
|
366
|
-
infoPlistPath: path.join(appPath, recipe.infoPlistPath),
|
|
367
|
-
asarPath: path.join(appPath, recipe.asarPath),
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
function integrityMatches(actual, algorithm, hash) {
|
|
372
|
-
return actual?.algorithm === algorithm && actual?.hash === hash;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function bundleContentMatches(actual, expected) {
|
|
376
|
-
return actual?.scheme === expected?.scheme
|
|
377
|
-
&& actual?.sha256 === expected?.sha256
|
|
378
|
-
&& actual?.entryCount === expected?.entryCount
|
|
379
|
-
&& actual?.regularFileCount === expected?.regularFileCount
|
|
380
|
-
&& actual?.regularBytes === expected?.regularBytes
|
|
381
|
-
&& actual?.symlinkCount === expected?.symlinkCount
|
|
382
|
-
&& actual?.xattrCount === expected?.xattrCount;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
export function inspectPmStudioApp({
|
|
386
|
-
appPath,
|
|
387
|
-
recipe = PM_STUDIO_2_9_7_RECIPE,
|
|
388
|
-
operations = createPmStudioSetupOperations(),
|
|
389
|
-
} = {}) {
|
|
390
|
-
const paths = appPaths(appPath, recipe);
|
|
391
|
-
const issues = [];
|
|
392
|
-
const metadata = operations.readBundleMetadata({ ...paths, recipe, processRunner: operations.processRunner });
|
|
393
|
-
const asar = operations.inspectAsarFile
|
|
394
|
-
? operations.inspectAsarFile({ ...paths, recipe, processRunner: operations.processRunner })
|
|
395
|
-
: inspectAsarBuffer(fs.readFileSync(paths.asarPath), recipe);
|
|
396
|
-
const plistIntegrity = operations.readAsarIntegrity({ ...paths, recipe, processRunner: operations.processRunner });
|
|
397
|
-
const executableIntegrity = operations.inspectExecutableIntegrity({
|
|
398
|
-
...paths, recipe, processRunner: operations.processRunner,
|
|
399
|
-
});
|
|
400
|
-
const codeSign = operations.inspectCodeSign({ ...paths, recipe, processRunner: operations.processRunner });
|
|
401
|
-
let bundleContent = null;
|
|
402
|
-
if (recipe.sourceBundleContent) {
|
|
403
|
-
try {
|
|
404
|
-
bundleContent = operations.inspectBundleContent({
|
|
405
|
-
...paths, recipe, processRunner: operations.processRunner,
|
|
406
|
-
});
|
|
407
|
-
} catch (error) {
|
|
408
|
-
issues.push(`bundle content inspection failed: ${error.message}`);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
if (String(metadata.version) !== recipe.version) {
|
|
413
|
-
issues.push(`version is ${metadata.version}, expected ${recipe.version}`);
|
|
414
|
-
}
|
|
415
|
-
if (String(metadata.build) !== recipe.build) {
|
|
416
|
-
issues.push(`build is ${metadata.build}, expected ${recipe.build}`);
|
|
417
|
-
}
|
|
418
|
-
if (String(metadata.bundleIdentifier) !== recipe.bundleIdentifier) {
|
|
419
|
-
issues.push(`bundle identifier is ${metadata.bundleIdentifier}, expected ${recipe.bundleIdentifier}`);
|
|
420
|
-
}
|
|
421
|
-
if (!executableIntegrity?.matchesRecipeName) issues.push("CFBundleExecutable does not match the recipe");
|
|
422
|
-
if (!executableIntegrity?.embeddedIntegrity?.supported
|
|
423
|
-
|| executableIntegrity.embeddedIntegrity.state !== recipe.embeddedAsarIntegrity) {
|
|
424
|
-
issues.push("Electron Framework has an unsupported embedded ASAR integrity slot");
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const exactSourceBundle = !recipe.sourceBundleContent
|
|
428
|
-
|| bundleContentMatches(bundleContent, recipe.sourceBundleContent);
|
|
429
|
-
const patchedSignatureAccepted = codeSign?.valid === true
|
|
430
|
-
&& codeSign?.adHoc === true
|
|
431
|
-
&& codeSign?.identifier === recipe.bundleIdentifier
|
|
432
|
-
&& (!recipe.patchedSigningMetadata
|
|
433
|
-
|| JSON.stringify(signingMetadata(codeSign)) === JSON.stringify(recipe.patchedSigningMetadata));
|
|
434
|
-
const predecessorSignatureAccepted = codeSign?.valid === true
|
|
435
|
-
&& codeSign?.adHoc === true
|
|
436
|
-
&& codeSign?.identifier === recipe.bundleIdentifier
|
|
437
|
-
&& Boolean(recipe.predecessor?.signingMetadata)
|
|
438
|
-
&& JSON.stringify(signingMetadata(codeSign)) === JSON.stringify(recipe.predecessor.signingMetadata);
|
|
439
|
-
const clean = issues.length === 0
|
|
440
|
-
&& asar.state === "clean"
|
|
441
|
-
&& integrityMatches(plistIntegrity, "SHA256", recipe.sourceHeaderSha256)
|
|
442
|
-
&& executableIntegrity.executableSha256 === recipe.sourceExecutableSha256
|
|
443
|
-
&& executableIntegrity.frameworkSha256 === recipe.sourceElectronFrameworkSha256
|
|
444
|
-
&& exactSourceBundle;
|
|
445
|
-
const patched = issues.length === 0
|
|
446
|
-
&& asar.state === "patched"
|
|
447
|
-
&& integrityMatches(plistIntegrity, "SHA256", recipe.patchedHeaderSha256)
|
|
448
|
-
&& patchedSignatureAccepted;
|
|
449
|
-
const predecessor = issues.length === 0
|
|
450
|
-
&& asar.state === "predecessor"
|
|
451
|
-
&& integrityMatches(plistIntegrity, "SHA256", recipe.predecessor?.headerSha256)
|
|
452
|
-
&& predecessorSignatureAccepted;
|
|
453
|
-
const legacy = issues.length === 0
|
|
454
|
-
&& asar.state === "legacy"
|
|
455
|
-
&& integrityMatches(plistIntegrity, "SHA256", recipe.legacy?.headerSha256)
|
|
456
|
-
&& patchedSignatureAccepted;
|
|
457
|
-
|
|
458
|
-
if (!clean && !patched && !predecessor && !legacy) {
|
|
459
|
-
if (asar.state === "drift") issues.push("app.asar is unknown drift");
|
|
460
|
-
else if (asar.state === "clean"
|
|
461
|
-
&& !integrityMatches(plistIntegrity, "SHA256", recipe.sourceHeaderSha256)) {
|
|
462
|
-
issues.push("clean ASAR does not match ElectronAsarIntegrity");
|
|
463
|
-
} else if (asar.state === "patched"
|
|
464
|
-
&& !integrityMatches(plistIntegrity, "SHA256", recipe.patchedHeaderSha256)) {
|
|
465
|
-
issues.push("patched ASAR does not match ElectronAsarIntegrity");
|
|
466
|
-
} else if (asar.state === "predecessor"
|
|
467
|
-
&& !integrityMatches(plistIntegrity, "SHA256", recipe.predecessor?.headerSha256)) {
|
|
468
|
-
issues.push("predecessor ASAR does not match ElectronAsarIntegrity");
|
|
469
|
-
} else if (asar.state === "legacy"
|
|
470
|
-
&& !integrityMatches(plistIntegrity, "SHA256", recipe.legacy?.headerSha256)) {
|
|
471
|
-
issues.push("legacy ASAR does not match ElectronAsarIntegrity");
|
|
472
|
-
}
|
|
473
|
-
if (asar.state === "clean" && recipe.sourceBundleContent && !exactSourceBundle) {
|
|
474
|
-
issues.push("bundle content does not match the exact clean recipe");
|
|
475
|
-
}
|
|
476
|
-
if (asar.state !== "clean" && !codeSign?.valid) {
|
|
477
|
-
issues.push("codesign verification failed");
|
|
478
|
-
}
|
|
479
|
-
else if (["patched", "predecessor", "legacy"].includes(asar.state) && !codeSign.adHoc) {
|
|
480
|
-
issues.push(`${asar.state} bundle is not ad-hoc signed`);
|
|
481
|
-
}
|
|
482
|
-
else if (["patched", "predecessor", "legacy"].includes(asar.state)
|
|
483
|
-
&& codeSign.identifier !== recipe.bundleIdentifier) {
|
|
484
|
-
issues.push(`${asar.state} bundle identifier does not match the recipe`);
|
|
485
|
-
}
|
|
486
|
-
if (asar.state === "clean"
|
|
487
|
-
&& executableIntegrity.executableSha256 !== recipe.sourceExecutableSha256) {
|
|
488
|
-
issues.push("main executable hash does not match the clean recipe");
|
|
489
|
-
}
|
|
490
|
-
if (asar.state === "clean"
|
|
491
|
-
&& executableIntegrity.frameworkSha256 !== recipe.sourceElectronFrameworkSha256) {
|
|
492
|
-
issues.push("Electron Framework hash does not match the clean recipe");
|
|
493
|
-
}
|
|
494
|
-
if (["patched", "legacy"].includes(asar.state) && codeSign?.valid && codeSign?.adHoc
|
|
495
|
-
&& recipe.patchedSigningMetadata
|
|
496
|
-
&& !patchedSignatureAccepted) {
|
|
497
|
-
issues.push(`${asar.state} bundle signing metadata does not match the recipe`);
|
|
498
|
-
}
|
|
499
|
-
if (asar.state === "predecessor" && codeSign?.valid && codeSign?.adHoc
|
|
500
|
-
&& !predecessorSignatureAccepted) {
|
|
501
|
-
issues.push("predecessor bundle signing metadata does not match the recipe");
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
return {
|
|
506
|
-
state: clean
|
|
507
|
-
? "clean"
|
|
508
|
-
: patched ? "patched" : predecessor ? "predecessor" : legacy ? "legacy" : "drift",
|
|
509
|
-
metadata: {
|
|
510
|
-
version: String(metadata.version),
|
|
511
|
-
build: String(metadata.build),
|
|
512
|
-
bundleIdentifier: String(metadata.bundleIdentifier),
|
|
513
|
-
},
|
|
514
|
-
asar,
|
|
515
|
-
plistIntegrity,
|
|
516
|
-
executableIntegrity,
|
|
517
|
-
codeSign,
|
|
518
|
-
bundleContent,
|
|
519
|
-
sourceVerification: clean
|
|
520
|
-
? recipe.sourceBundleContent ? "local-content" : "exact-recipe"
|
|
521
|
-
: "",
|
|
522
|
-
paths,
|
|
523
|
-
issues,
|
|
524
|
-
};
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
function assertInstalledAppPath(appPath) {
|
|
528
|
-
const stat = fs.lstatSync(appPath);
|
|
529
|
-
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
530
|
-
throw setupError("PM_STUDIO_APP_INVALID", `PM Studio path is not a regular app bundle: ${appPath}`);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
function assertNoBlockingProcesses(operations, appPath) {
|
|
535
|
-
const processes = operations.listBlockingProcesses({ appPath, processRunner: operations.processRunner });
|
|
536
|
-
if (!Array.isArray(processes)) throw setupError("PM_STUDIO_PROCESS_CHECK_FAILED", "PM Studio process check returned invalid data");
|
|
537
|
-
if (processes.length > 0) {
|
|
538
|
-
throw setupError("PM_STUDIO_RUNNING", `PM Studio or its updater is running (${processes.length} matching process(es)); quit it and retry`);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function assertValidClaudeProfile(operations, home) {
|
|
543
|
-
let credentials;
|
|
544
|
-
try {
|
|
545
|
-
credentials = operations.readClaudeCredentials({ home });
|
|
546
|
-
} catch {
|
|
547
|
-
throw setupError("PM_STUDIO_CLAUDE_AUTH_INVALID",
|
|
548
|
-
`Claude isolated authentication could not be read. Run \`${PM_STUDIO_CLAUDE_AUTH_COMMAND}\` first.`);
|
|
549
|
-
}
|
|
550
|
-
if (!credentials?.configured || !credentials?.valid) {
|
|
551
|
-
const reason = credentials?.reason ? ` (${credentials.reason})` : "";
|
|
552
|
-
throw setupError("PM_STUDIO_CLAUDE_AUTH_INVALID",
|
|
553
|
-
`Claude isolated authentication is not valid${reason}. Run \`${PM_STUDIO_CLAUDE_AUTH_COMMAND}\` first.`);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
function selectRecipe(metadata, recipes) {
|
|
558
|
-
return recipes.find((recipe) => recipe.version === String(metadata.version)
|
|
559
|
-
&& recipe.build === String(metadata.build)
|
|
560
|
-
&& recipe.bundleIdentifier === String(metadata.bundleIdentifier));
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
function compatibleStaticOptions(metadata, executable) {
|
|
564
|
-
return {
|
|
565
|
-
version: String(metadata.version),
|
|
566
|
-
build: String(metadata.build),
|
|
567
|
-
bundleIdentifier: String(metadata.bundleIdentifier),
|
|
568
|
-
sourceTeamIdentifier: "local-app",
|
|
569
|
-
executable,
|
|
570
|
-
electronFrameworkPath: PM_STUDIO_2_9_10_RECIPE.electronFrameworkPath,
|
|
571
|
-
embeddedAsarIntegrity: PM_STUDIO_2_9_10_RECIPE.embeddedAsarIntegrity,
|
|
572
|
-
asarPath: PM_STUDIO_2_9_10_RECIPE.asarPath,
|
|
573
|
-
infoPlistPath: PM_STUDIO_2_9_10_RECIPE.infoPlistPath,
|
|
574
|
-
integrityKey: PM_STUDIO_2_9_10_RECIPE.integrityKey,
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function compatibleSourceBundleContent({ appPath, operations }) {
|
|
579
|
-
const ignoredXattrs = PM_STUDIO_2_9_10_RECIPE.sourceBundleContent.ignoredXattrs;
|
|
580
|
-
const inspected = operations.inspectBundleContent({
|
|
581
|
-
...appPaths(appPath, PM_STUDIO_2_9_10_RECIPE),
|
|
582
|
-
recipe: PM_STUDIO_2_9_10_RECIPE,
|
|
583
|
-
processRunner: operations.processRunner,
|
|
584
|
-
});
|
|
585
|
-
return {
|
|
586
|
-
...inspected,
|
|
587
|
-
ignoredXattrs: [...ignoredXattrs],
|
|
588
|
-
};
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
function compatibleSourceStructure({ appPath, metadata, operations }) {
|
|
592
|
-
const paths = appPaths(appPath, PM_STUDIO_2_9_10_RECIPE);
|
|
593
|
-
const executableIntegrity = operations.inspectExecutableIntegrity({
|
|
594
|
-
...paths,
|
|
595
|
-
recipe: PM_STUDIO_2_9_10_RECIPE,
|
|
596
|
-
processRunner: operations.processRunner,
|
|
597
|
-
});
|
|
598
|
-
if (!executableIntegrity?.executableName
|
|
599
|
-
|| !executableIntegrity?.embeddedIntegrity?.supported
|
|
600
|
-
|| executableIntegrity.embeddedIntegrity.state !== PM_STUDIO_2_9_10_RECIPE.embeddedAsarIntegrity) {
|
|
601
|
-
throw setupError("PM_STUDIO_ASAR_INCOMPATIBLE",
|
|
602
|
-
"PM Studio compatible-version discovery found an unsupported executable or embedded ASAR integrity policy");
|
|
603
|
-
}
|
|
604
|
-
const recipeOptions = {
|
|
605
|
-
...compatibleStaticOptions(metadata, executableIntegrity.executableName),
|
|
606
|
-
sourceExecutableSha256: executableIntegrity.executableSha256,
|
|
607
|
-
sourceElectronFrameworkSha256: executableIntegrity.frameworkSha256,
|
|
608
|
-
};
|
|
609
|
-
const compatibleRecipe = operations.createCompatibleRecipeFromFile
|
|
610
|
-
? operations.createCompatibleRecipeFromFile({ asarPath: paths.asarPath, options: recipeOptions })
|
|
611
|
-
: createCompatiblePmStudioRecipe(fs.readFileSync(paths.asarPath), recipeOptions);
|
|
612
|
-
const plistIntegrity = operations.readAsarIntegrity({
|
|
613
|
-
...paths,
|
|
614
|
-
recipe: compatibleRecipe,
|
|
615
|
-
processRunner: operations.processRunner,
|
|
616
|
-
});
|
|
617
|
-
if (!integrityMatches(plistIntegrity, "SHA256", compatibleRecipe.sourceHeaderSha256)) {
|
|
618
|
-
throw setupError("PM_STUDIO_ASAR_INCOMPATIBLE",
|
|
619
|
-
"PM Studio compatible-version discovery found mismatched ElectronAsarIntegrity metadata");
|
|
620
|
-
}
|
|
621
|
-
return { paths, executableIntegrity, compatibleRecipe };
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
function frozenBundleContent(value) {
|
|
625
|
-
return Object.freeze({
|
|
626
|
-
...value,
|
|
627
|
-
ignoredXattrs: Object.freeze([...(value.ignoredXattrs || [])]),
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
function createCompatibleSourceRecipe({ appPath, metadata, operations }) {
|
|
632
|
-
const { compatibleRecipe } = compatibleSourceStructure({ appPath, metadata, operations });
|
|
633
|
-
const sourceBundleContent = compatibleSourceBundleContent({ appPath, operations });
|
|
634
|
-
return Object.freeze({
|
|
635
|
-
...compatibleRecipe,
|
|
636
|
-
sourceBundleContent: frozenBundleContent(sourceBundleContent),
|
|
637
|
-
});
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
function sameIgnoredXattrs(value) {
|
|
641
|
-
return JSON.stringify(value)
|
|
642
|
-
=== JSON.stringify(PM_STUDIO_2_9_10_RECIPE.sourceBundleContent.ignoredXattrs);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
function compatibleRecipeFromManifest({ backupDir, manifest, metadata, operations }) {
|
|
647
|
-
if (manifest?.schema_version !== 2
|
|
648
|
-
|| manifest?.kind !== "ccdx-pm-studio-backup"
|
|
649
|
-
|| !String(manifest?.recipe_id || "").startsWith("pm-studio-compatible-")
|
|
650
|
-
|| manifest?.app?.bundle_identifier !== String(metadata.bundleIdentifier)
|
|
651
|
-
|| manifest?.app?.version !== String(metadata.version)
|
|
652
|
-
|| manifest?.app?.build !== String(metadata.build)
|
|
653
|
-
|| !sameIgnoredXattrs(manifest?.source?.bundle_content?.ignoredXattrs)) {
|
|
654
|
-
return null;
|
|
655
|
-
}
|
|
656
|
-
const backupAppPath = path.join(backupDir, "PM Studio.app");
|
|
657
|
-
assertInstalledAppPath(backupAppPath);
|
|
658
|
-
const sourceAsarPath = path.join(backupAppPath, PM_STUDIO_2_9_10_RECIPE.asarPath);
|
|
659
|
-
const backupPaths = appPaths(backupAppPath, PM_STUDIO_2_9_10_RECIPE);
|
|
660
|
-
const executableIntegrity = operations.inspectExecutableIntegrity({
|
|
661
|
-
...backupPaths,
|
|
662
|
-
recipe: PM_STUDIO_2_9_10_RECIPE,
|
|
663
|
-
processRunner: operations.processRunner,
|
|
664
|
-
});
|
|
665
|
-
const recipeOptions = {
|
|
666
|
-
...compatibleStaticOptions(metadata, executableIntegrity.executableName),
|
|
667
|
-
sourceExecutableSha256: manifest?.source?.binaries?.main_executable_sha256,
|
|
668
|
-
sourceElectronFrameworkSha256: manifest?.source?.binaries?.electron_framework_sha256,
|
|
669
|
-
};
|
|
670
|
-
const compatibleRecipe = operations.createCompatibleRecipeFromFile
|
|
671
|
-
? operations.createCompatibleRecipeFromFile({ asarPath: sourceAsarPath, options: recipeOptions })
|
|
672
|
-
: createCompatiblePmStudioRecipe(fs.readFileSync(sourceAsarPath), recipeOptions);
|
|
673
|
-
const recipe = Object.freeze({
|
|
674
|
-
...compatibleRecipe,
|
|
675
|
-
sourceBundleContent: frozenBundleContent(manifest.source.bundle_content),
|
|
676
|
-
});
|
|
677
|
-
if (manifest.compatibility !== recipe.compatibility
|
|
678
|
-
|| manifest.recipe_id !== recipe.id
|
|
679
|
-
|| manifest?.source?.asar_sha256 !== recipe.sourceAsarSha256
|
|
680
|
-
|| manifest?.source?.asar_header_sha256 !== recipe.sourceHeaderSha256
|
|
681
|
-
|| manifest?.patched?.asar_sha256 !== recipe.patchedAsarSha256
|
|
682
|
-
|| manifest?.patched?.asar_header_sha256 !== recipe.patchedHeaderSha256) {
|
|
683
|
-
throw setupError("PM_STUDIO_BACKUP_INVALID",
|
|
684
|
-
`Compatible PM Studio backup does not match its reconstructed recipe: ${backupDir}`);
|
|
685
|
-
}
|
|
686
|
-
const inspection = inspectPmStudioApp({ appPath: backupAppPath, recipe, operations });
|
|
687
|
-
if (inspection.state !== "clean" || inspection.sourceVerification !== "local-content") {
|
|
688
|
-
throw setupError("PM_STUDIO_BACKUP_INVALID",
|
|
689
|
-
`Compatible PM Studio backup source verification failed: ${backupDir}`,
|
|
690
|
-
inspection.issues);
|
|
691
|
-
}
|
|
692
|
-
return recipe;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
function compatibleRecipeFromBackup({ appPath, backupRoot, metadata, operations }) {
|
|
696
|
-
if (!fs.existsSync(backupRoot)) return null;
|
|
697
|
-
const installedAsarPath = path.join(appPath, PM_STUDIO_2_9_10_RECIPE.asarPath);
|
|
698
|
-
const installedAsar = operations.inspectAsarFile ? null : fs.readFileSync(installedAsarPath);
|
|
699
|
-
let candidates = [];
|
|
700
|
-
const invalidCandidates = [];
|
|
701
|
-
for (const entry of fs.readdirSync(backupRoot, { withFileTypes: true })) {
|
|
702
|
-
if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
|
|
703
|
-
const backupDir = path.join(backupRoot, entry.name);
|
|
704
|
-
const manifestPath = path.join(backupDir, "manifest.json");
|
|
705
|
-
if (!fs.existsSync(manifestPath)) continue;
|
|
706
|
-
let manifest;
|
|
707
|
-
try {
|
|
708
|
-
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
709
|
-
} catch {
|
|
710
|
-
continue;
|
|
711
|
-
}
|
|
712
|
-
let recipe;
|
|
713
|
-
try {
|
|
714
|
-
recipe = compatibleRecipeFromManifest({ backupDir, manifest, metadata, operations });
|
|
715
|
-
} catch (error) {
|
|
716
|
-
invalidCandidates.push(error);
|
|
717
|
-
continue;
|
|
718
|
-
}
|
|
719
|
-
if (!recipe) continue;
|
|
720
|
-
const asarState = operations.inspectAsarFile
|
|
721
|
-
? operations.inspectAsarFile({ asarPath: installedAsarPath, recipe }).state
|
|
722
|
-
: inspectAsarBuffer(installedAsar, recipe).state;
|
|
723
|
-
if (asarState === "drift") continue;
|
|
724
|
-
candidates.push({ recipe, manifest, asarState });
|
|
725
|
-
}
|
|
726
|
-
if (candidates.length > 1) {
|
|
727
|
-
let installedBundleContent;
|
|
728
|
-
try {
|
|
729
|
-
const recipe = candidates[0].recipe;
|
|
730
|
-
installedBundleContent = operations.inspectBundleContent({
|
|
731
|
-
...appPaths(appPath, recipe),
|
|
732
|
-
recipe,
|
|
733
|
-
processRunner: operations.processRunner,
|
|
734
|
-
});
|
|
735
|
-
} catch (error) {
|
|
736
|
-
throw setupError("PM_STUDIO_BACKUP_INVALID",
|
|
737
|
-
"PM Studio compatible backup candidates could not be distinguished safely",
|
|
738
|
-
[error.message]);
|
|
739
|
-
}
|
|
740
|
-
candidates = candidates.filter(({ recipe, manifest, asarState }) => bundleContentMatches(
|
|
741
|
-
installedBundleContent,
|
|
742
|
-
asarState === "clean" ? recipe.sourceBundleContent : manifest?.patched?.bundle_content,
|
|
743
|
-
));
|
|
744
|
-
}
|
|
745
|
-
if (candidates.length > 1) {
|
|
746
|
-
throw setupError("PM_STUDIO_UNSUPPORTED_VERSION",
|
|
747
|
-
`PM Studio ${metadata.version} build ${metadata.build} matches multiple compatible backups; no files were changed`);
|
|
748
|
-
}
|
|
749
|
-
if (candidates.length === 1) return candidates[0].recipe;
|
|
750
|
-
if (invalidCandidates.length > 0) {
|
|
751
|
-
throw setupError("PM_STUDIO_BACKUP_INVALID",
|
|
752
|
-
`PM Studio ${metadata.version} build ${metadata.build} has no matching verified compatible backup`,
|
|
753
|
-
invalidCandidates.map((error) => error?.message || String(error)));
|
|
754
|
-
}
|
|
755
|
-
return null;
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
function selectCompatibleRecipe({ appPath, backupRoot, metadata, operations }) {
|
|
759
|
-
let sourceError = null;
|
|
760
|
-
try {
|
|
761
|
-
return createCompatibleSourceRecipe({ appPath, metadata, operations });
|
|
762
|
-
} catch (error) {
|
|
763
|
-
sourceError = error;
|
|
764
|
-
}
|
|
765
|
-
try {
|
|
766
|
-
const backupRecipe = compatibleRecipeFromBackup({ appPath, backupRoot, metadata, operations });
|
|
767
|
-
if (backupRecipe) return backupRecipe;
|
|
768
|
-
} catch (error) {
|
|
769
|
-
if (error?.code === "PM_STUDIO_BACKUP_INVALID") throw error;
|
|
770
|
-
sourceError = error;
|
|
771
|
-
}
|
|
772
|
-
throw setupError("PM_STUDIO_UNSUPPORTED_VERSION",
|
|
773
|
-
`PM Studio ${metadata.version} build ${metadata.build} is not structurally compatible with the safe patch; no files were changed${sourceError?.message ? `: ${sourceError.message}` : ""}`,
|
|
774
|
-
sourceError?.message ? [sourceError.message] : undefined);
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
export function resolvePmStudioCompatibleRecipe({
|
|
778
|
-
appPath = DEFAULT_PM_STUDIO_APP_PATH,
|
|
779
|
-
home = os.homedir(),
|
|
780
|
-
backupRoot = pmStudioBackupRoot(home),
|
|
781
|
-
metadata,
|
|
782
|
-
recipes = PM_STUDIO_RECIPES,
|
|
783
|
-
operations = createPmStudioSetupOperations(),
|
|
784
|
-
inspectApp = inspectPmStudioApp,
|
|
785
|
-
} = {}) {
|
|
786
|
-
if (!metadata || typeof metadata !== "object") {
|
|
787
|
-
throw new TypeError("PM Studio bundle metadata is required");
|
|
788
|
-
}
|
|
789
|
-
const staticRecipe = selectRecipe(metadata, recipes);
|
|
790
|
-
let staticInspection = null;
|
|
791
|
-
if (staticRecipe) {
|
|
792
|
-
try {
|
|
793
|
-
staticInspection = inspectApp({ appPath, recipe: staticRecipe, operations });
|
|
794
|
-
} catch {
|
|
795
|
-
// A metadata match is not an admission gate. Local structure remains authoritative.
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
try {
|
|
799
|
-
return selectCompatibleRecipe({
|
|
800
|
-
appPath,
|
|
801
|
-
backupRoot,
|
|
802
|
-
metadata,
|
|
803
|
-
operations,
|
|
804
|
-
});
|
|
805
|
-
} catch (error) {
|
|
806
|
-
if (staticInspection?.state === "drift" && error?.code === "PM_STUDIO_UNSUPPORTED_VERSION") {
|
|
807
|
-
throw setupError("PM_STUDIO_BUNDLE_DRIFT",
|
|
808
|
-
`PM Studio ${metadata.version} build ${metadata.build} differs from its known local structure; no files were changed`,
|
|
809
|
-
staticInspection.issues);
|
|
810
|
-
}
|
|
811
|
-
const staticSourceCanBeSigned = staticInspection?.state !== "clean"
|
|
812
|
-
|| !staticRecipe?.patchedSigningMetadata
|
|
813
|
-
|| (staticInspection?.codeSign?.displayValid === true
|
|
814
|
-
&& staticInspection.codeSign.entitlementsState === "xml");
|
|
815
|
-
if (error?.code === "PM_STUDIO_UNSUPPORTED_VERSION"
|
|
816
|
-
&& staticInspection?.state && staticInspection.state !== "drift"
|
|
817
|
-
&& staticSourceCanBeSigned) {
|
|
818
|
-
return staticRecipe;
|
|
819
|
-
}
|
|
820
|
-
throw error;
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
function backupName(recipe) {
|
|
825
|
-
const sourceHash = recipe.sourceBundleContent?.sha256 || recipe.sourceAsarSha256;
|
|
826
|
-
return `${recipe.id}-${sourceHash.slice(0, 12)}`;
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
export function pmStudioBackupRoot(home = os.homedir()) {
|
|
830
|
-
return path.join(home, ".local", "share", "codex-copilot-dx", "pm-studio-backups");
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
export function pmStudioPatchManifestPath({
|
|
834
|
-
home = os.homedir(),
|
|
835
|
-
backupRoot = pmStudioBackupRoot(home),
|
|
836
|
-
recipe,
|
|
837
|
-
} = {}) {
|
|
838
|
-
if (!recipe) throw new TypeError("PM Studio patch recipe is required");
|
|
839
|
-
return path.join(backupRoot, backupName(recipe), "manifest.json");
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
function backupManifest(recipe, createdAt) {
|
|
843
|
-
return {
|
|
844
|
-
schema_version: recipe.sourceBundleContent ? 2 : 1,
|
|
845
|
-
kind: "ccdx-pm-studio-backup",
|
|
846
|
-
recipe_id: recipe.id,
|
|
847
|
-
...(recipe.compatibility ? { compatibility: recipe.compatibility } : {}),
|
|
848
|
-
created_at: createdAt.toISOString(),
|
|
849
|
-
app: {
|
|
850
|
-
name: "PM Studio",
|
|
851
|
-
bundle_identifier: recipe.bundleIdentifier,
|
|
852
|
-
version: recipe.version,
|
|
853
|
-
build: recipe.build,
|
|
854
|
-
},
|
|
855
|
-
source: {
|
|
856
|
-
asar_sha256: recipe.sourceAsarSha256,
|
|
857
|
-
asar_header_sha256: recipe.sourceHeaderSha256,
|
|
858
|
-
electron_asar_integrity: { algorithm: "SHA256", hash: recipe.sourceHeaderSha256 },
|
|
859
|
-
binaries: {
|
|
860
|
-
main_executable_sha256: recipe.sourceExecutableSha256,
|
|
861
|
-
electron_framework_sha256: recipe.sourceElectronFrameworkSha256,
|
|
862
|
-
embedded_asar_integrity: recipe.embeddedAsarIntegrity,
|
|
863
|
-
},
|
|
864
|
-
...(recipe.sourceBundleContent ? { bundle_content: recipe.sourceBundleContent } : {}),
|
|
865
|
-
},
|
|
866
|
-
patched: {
|
|
867
|
-
asar_sha256: recipe.patchedAsarSha256,
|
|
868
|
-
asar_header_sha256: recipe.patchedHeaderSha256,
|
|
869
|
-
electron_asar_integrity: { algorithm: "SHA256", hash: recipe.patchedHeaderSha256 },
|
|
870
|
-
binaries: null,
|
|
871
|
-
...(recipe.sourceBundleContent ? { bundle_content: null } : {}),
|
|
872
|
-
},
|
|
873
|
-
backup_bundle: "PM Studio.app",
|
|
874
|
-
restore_rule: `Restore only while the installed app is PM Studio ${recipe.version} build ${recipe.build}.`,
|
|
875
|
-
};
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
function patchRecordMatches(record, recipe, state) {
|
|
879
|
-
const expected = state === "legacy"
|
|
880
|
-
? recipe.legacy
|
|
881
|
-
: state === "predecessor"
|
|
882
|
-
? recipe.predecessor
|
|
883
|
-
: { asarSha256: recipe.patchedAsarSha256, headerSha256: recipe.patchedHeaderSha256 };
|
|
884
|
-
return Boolean(expected)
|
|
885
|
-
&& record?.asar_sha256 === expected.asarSha256
|
|
886
|
-
&& record?.asar_header_sha256 === expected.headerSha256
|
|
887
|
-
&& integrityMatches(record?.electron_asar_integrity, "SHA256", expected.headerSha256);
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
function patchRecordForState(manifest, recipe, state) {
|
|
891
|
-
const historicalKey = state === "legacy"
|
|
892
|
-
? "legacy_patched"
|
|
893
|
-
: state === "predecessor" ? "predecessor_patched" : "";
|
|
894
|
-
if (historicalKey && patchRecordMatches(manifest?.[historicalKey], recipe, state)) {
|
|
895
|
-
return manifest[historicalKey];
|
|
896
|
-
}
|
|
897
|
-
return patchRecordMatches(manifest?.patched, recipe, state) ? manifest.patched : null;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
function validateBackup({ backupDir, recipe, operations, evidence }) {
|
|
901
|
-
const manifestPath = path.join(backupDir, "manifest.json");
|
|
902
|
-
const backupAppPath = path.join(backupDir, "PM Studio.app");
|
|
903
|
-
let manifest;
|
|
904
|
-
try {
|
|
905
|
-
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
906
|
-
} catch {
|
|
907
|
-
throw setupError("PM_STUDIO_BACKUP_INVALID", `Existing PM Studio backup manifest is invalid: ${manifestPath}`);
|
|
908
|
-
}
|
|
909
|
-
const expectedSchema = recipe.sourceBundleContent ? 2 : 1;
|
|
910
|
-
if (manifest?.schema_version !== expectedSchema
|
|
911
|
-
|| manifest?.kind !== "ccdx-pm-studio-backup"
|
|
912
|
-
|| manifest?.recipe_id !== recipe.id
|
|
913
|
-
|| (recipe.compatibility && manifest?.compatibility !== recipe.compatibility)
|
|
914
|
-
|| manifest?.app?.bundle_identifier !== recipe.bundleIdentifier
|
|
915
|
-
|| manifest?.app?.version !== recipe.version
|
|
916
|
-
|| manifest?.app?.build !== recipe.build
|
|
917
|
-
|| manifest?.source?.asar_sha256 !== recipe.sourceAsarSha256
|
|
918
|
-
|| manifest?.source?.asar_header_sha256 !== recipe.sourceHeaderSha256
|
|
919
|
-
|| !integrityMatches(manifest?.source?.electron_asar_integrity, "SHA256", recipe.sourceHeaderSha256)
|
|
920
|
-
|| manifest?.source?.binaries?.main_executable_sha256 !== recipe.sourceExecutableSha256
|
|
921
|
-
|| manifest?.source?.binaries?.electron_framework_sha256 !== recipe.sourceElectronFrameworkSha256
|
|
922
|
-
|| manifest?.source?.binaries?.embedded_asar_integrity !== recipe.embeddedAsarIntegrity
|
|
923
|
-
|| (recipe.sourceBundleContent
|
|
924
|
-
&& JSON.stringify(manifest?.source?.bundle_content) !== JSON.stringify(recipe.sourceBundleContent))
|
|
925
|
-
|| (manifest?.predecessor_patched !== undefined
|
|
926
|
-
&& !patchRecordMatches(manifest.predecessor_patched, recipe, "predecessor"))
|
|
927
|
-
|| (!patchRecordForState(manifest, recipe, "patched")
|
|
928
|
-
&& !patchRecordForState(manifest, recipe, "legacy")
|
|
929
|
-
&& !patchRecordForState(manifest, recipe, "predecessor"))) {
|
|
930
|
-
throw setupError("PM_STUDIO_BACKUP_INVALID", `Existing PM Studio backup does not match recipe ${recipe.id}`);
|
|
931
|
-
}
|
|
932
|
-
const inspection = inspectPmStudioApp({ appPath: backupAppPath, recipe, operations });
|
|
933
|
-
if (inspection.state !== "clean") {
|
|
934
|
-
throw setupError("PM_STUDIO_BACKUP_INVALID", "Existing PM Studio backup is not an exact clean bundle", inspection.issues);
|
|
935
|
-
}
|
|
936
|
-
if (evidence) {
|
|
937
|
-
evidence.inspection = inspection;
|
|
938
|
-
}
|
|
939
|
-
return { backupDir, backupAppPath, manifestPath, manifest, reused: true };
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
function ensureBackup({ appPath, backupRoot, recipe, operations }) {
|
|
943
|
-
const finalDir = path.join(backupRoot, backupName(recipe));
|
|
944
|
-
if (fs.existsSync(finalDir)) return validateBackup({ backupDir: finalDir, recipe, operations });
|
|
945
|
-
|
|
946
|
-
fs.mkdirSync(backupRoot, { recursive: true, mode: 0o700 });
|
|
947
|
-
const tempDir = path.join(backupRoot, `.${backupName(recipe)}.${operations.uuid()}.tmp`);
|
|
948
|
-
const backupAppPath = path.join(tempDir, "PM Studio.app");
|
|
949
|
-
const manifestPath = path.join(tempDir, "manifest.json");
|
|
950
|
-
fs.mkdirSync(tempDir, { mode: 0o700 });
|
|
951
|
-
try {
|
|
952
|
-
operations.copyBundle({ source: appPath, destination: backupAppPath, processRunner: operations.processRunner });
|
|
953
|
-
fs.writeFileSync(manifestPath, `${JSON.stringify(backupManifest(recipe, operations.now()), null, 2)}\n`, { mode: 0o600 });
|
|
954
|
-
const inspection = inspectPmStudioApp({ appPath: backupAppPath, recipe, operations });
|
|
955
|
-
if (inspection.state !== "clean") {
|
|
956
|
-
throw setupError("PM_STUDIO_BACKUP_VERIFY_FAILED", "New PM Studio backup failed verification", inspection.issues);
|
|
957
|
-
}
|
|
958
|
-
try {
|
|
959
|
-
operations.renamePath({ source: tempDir, destination: finalDir });
|
|
960
|
-
} catch (error) {
|
|
961
|
-
if (!fs.existsSync(finalDir)) throw error;
|
|
962
|
-
return validateBackup({ backupDir: finalDir, recipe, operations });
|
|
963
|
-
}
|
|
964
|
-
return {
|
|
965
|
-
backupDir: finalDir,
|
|
966
|
-
backupAppPath: path.join(finalDir, "PM Studio.app"),
|
|
967
|
-
manifestPath: path.join(finalDir, "manifest.json"),
|
|
968
|
-
manifest: JSON.parse(fs.readFileSync(path.join(finalDir, "manifest.json"), "utf8")),
|
|
969
|
-
reused: false,
|
|
970
|
-
};
|
|
971
|
-
} finally {
|
|
972
|
-
if (fs.existsSync(tempDir)) operations.removePath({ target: tempDir });
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
function writePatchedBinaryRecord({ backup, inspection, operations, recipe }) {
|
|
977
|
-
const manifest = JSON.parse(fs.readFileSync(backup.manifestPath, "utf8"));
|
|
978
|
-
if (manifest.predecessor_patched !== undefined
|
|
979
|
-
&& !patchRecordMatches(manifest.predecessor_patched, recipe, "predecessor")) {
|
|
980
|
-
throw setupError("PM_STUDIO_BACKUP_INVALID", "Existing PM Studio predecessor record does not match the patch recipe");
|
|
981
|
-
}
|
|
982
|
-
if (manifest.predecessor_patched === undefined
|
|
983
|
-
&& patchRecordMatches(manifest.patched, recipe, "predecessor")) {
|
|
984
|
-
manifest.predecessor_patched = manifest.patched;
|
|
985
|
-
}
|
|
986
|
-
if (!patchRecordMatches(manifest.legacy_patched, recipe, "legacy")
|
|
987
|
-
&& patchRecordMatches(manifest.patched, recipe, "legacy")) {
|
|
988
|
-
manifest.legacy_patched = manifest.patched;
|
|
989
|
-
}
|
|
990
|
-
manifest.patched = {
|
|
991
|
-
asar_sha256: recipe.patchedAsarSha256,
|
|
992
|
-
asar_header_sha256: recipe.patchedHeaderSha256,
|
|
993
|
-
electron_asar_integrity: { algorithm: "SHA256", hash: recipe.patchedHeaderSha256 },
|
|
994
|
-
binaries: {
|
|
995
|
-
main_executable_sha256: inspection.executableIntegrity.executableSha256,
|
|
996
|
-
electron_framework_sha256: inspection.executableIntegrity.frameworkSha256,
|
|
997
|
-
},
|
|
998
|
-
signing_metadata: signingMetadata(inspection.codeSign),
|
|
999
|
-
...(recipe.sourceBundleContent ? { bundle_content: inspection.bundleContent } : {}),
|
|
1000
|
-
};
|
|
1001
|
-
const temporaryPath = `${backup.manifestPath}.${operations.uuid()}.tmp`;
|
|
1002
|
-
try {
|
|
1003
|
-
fs.writeFileSync(temporaryPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
1004
|
-
operations.renamePath({ source: temporaryPath, destination: backup.manifestPath });
|
|
1005
|
-
} finally {
|
|
1006
|
-
if (fs.existsSync(temporaryPath)) operations.removePath({ target: temporaryPath });
|
|
1007
|
-
}
|
|
1008
|
-
backup.manifest = manifest;
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
export function assertPatchedBinaryRecord({
|
|
1012
|
-
inspection,
|
|
1013
|
-
manifestPath,
|
|
1014
|
-
recipe,
|
|
1015
|
-
recordState = ["legacy", "predecessor"].includes(inspection?.state)
|
|
1016
|
-
? inspection.state
|
|
1017
|
-
: "patched",
|
|
1018
|
-
}) {
|
|
1019
|
-
let manifest;
|
|
1020
|
-
try {
|
|
1021
|
-
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
1022
|
-
} catch {
|
|
1023
|
-
throw setupError("PM_STUDIO_PATCH_RECORD_INVALID", "PM Studio patched-binary record is missing or invalid");
|
|
1024
|
-
}
|
|
1025
|
-
const expectedSchema = recipe?.sourceBundleContent ? 2 : 1;
|
|
1026
|
-
if (recipe && (manifest?.schema_version !== expectedSchema
|
|
1027
|
-
|| manifest?.kind !== "ccdx-pm-studio-backup"
|
|
1028
|
-
|| manifest?.recipe_id !== recipe.id
|
|
1029
|
-
|| (recipe.compatibility && manifest?.compatibility !== recipe.compatibility)
|
|
1030
|
-
|| manifest?.app?.bundle_identifier !== recipe.bundleIdentifier
|
|
1031
|
-
|| manifest?.app?.version !== recipe.version
|
|
1032
|
-
|| manifest?.app?.build !== recipe.build
|
|
1033
|
-
|| (recipe.sourceBundleContent && (
|
|
1034
|
-
manifest?.source?.asar_sha256 !== recipe.sourceAsarSha256
|
|
1035
|
-
|| manifest?.source?.asar_header_sha256 !== recipe.sourceHeaderSha256
|
|
1036
|
-
|| !integrityMatches(manifest?.source?.electron_asar_integrity, "SHA256", recipe.sourceHeaderSha256)
|
|
1037
|
-
|| manifest?.source?.binaries?.main_executable_sha256 !== recipe.sourceExecutableSha256
|
|
1038
|
-
|| manifest?.source?.binaries?.electron_framework_sha256 !== recipe.sourceElectronFrameworkSha256
|
|
1039
|
-
|| manifest?.source?.binaries?.embedded_asar_integrity !== recipe.embeddedAsarIntegrity
|
|
1040
|
-
|| JSON.stringify(manifest?.source?.bundle_content) !== JSON.stringify(recipe.sourceBundleContent)
|
|
1041
|
-
)))) {
|
|
1042
|
-
throw setupError("PM_STUDIO_PATCH_RECORD_INVALID", "PM Studio patched-binary record does not match the installed patch recipe");
|
|
1043
|
-
}
|
|
1044
|
-
const record = patchRecordForState(manifest, recipe, recordState);
|
|
1045
|
-
if (!record) {
|
|
1046
|
-
throw setupError("PM_STUDIO_PATCH_RECORD_INVALID", `PM Studio ${recordState} record does not match the installed patch recipe`);
|
|
1047
|
-
}
|
|
1048
|
-
const binaries = record.binaries;
|
|
1049
|
-
if (!binaries
|
|
1050
|
-
|| binaries.main_executable_sha256 !== inspection.executableIntegrity.executableSha256
|
|
1051
|
-
|| binaries.electron_framework_sha256 !== inspection.executableIntegrity.frameworkSha256) {
|
|
1052
|
-
throw setupError("PM_STUDIO_BUNDLE_DRIFT", "PM Studio executable or Electron Framework differs from the installed patch record");
|
|
1053
|
-
}
|
|
1054
|
-
if (JSON.stringify(record.signing_metadata) !== JSON.stringify(signingMetadata(inspection.codeSign))) {
|
|
1055
|
-
throw setupError("PM_STUDIO_BUNDLE_DRIFT", "PM Studio signing metadata differs from the installed patch record");
|
|
1056
|
-
}
|
|
1057
|
-
if (recipe?.sourceBundleContent
|
|
1058
|
-
&& JSON.stringify(record.bundle_content) !== JSON.stringify(inspection.bundleContent)) {
|
|
1059
|
-
throw setupError("PM_STUDIO_BUNDLE_DRIFT", "PM Studio bundle content differs from the installed patch record");
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
function signingMetadata(codeSign) {
|
|
1064
|
-
return {
|
|
1065
|
-
identifier: codeSign?.identifier || "",
|
|
1066
|
-
flags: codeSign?.flags || "",
|
|
1067
|
-
runtime_version: codeSign?.runtimeVersion || "",
|
|
1068
|
-
entitlements_sha256: codeSign?.entitlementsSha256 || "",
|
|
1069
|
-
};
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
function flagsWithoutAdHoc(value) {
|
|
1073
|
-
const flags = String(value || "");
|
|
1074
|
-
const match = flags.match(/^0x([0-9a-f]+)/i);
|
|
1075
|
-
if (!match) return flags.replace(/\badhoc\b,?/gi, "").replace(/\(,/, "(");
|
|
1076
|
-
return `0x${(BigInt(`0x${match[1]}`) & ~2n).toString(16)}`;
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
function assertSigningMetadataPreserved(source, staged, recipe) {
|
|
1080
|
-
const after = signingMetadata(staged.codeSign);
|
|
1081
|
-
if (after.identifier !== recipe.bundleIdentifier) {
|
|
1082
|
-
throw setupError("PM_STUDIO_SIGNING_METADATA_CHANGED",
|
|
1083
|
-
"Ad-hoc signing did not use the PM Studio bundle identifier");
|
|
1084
|
-
}
|
|
1085
|
-
if (recipe.patchedSigningMetadata) {
|
|
1086
|
-
if (JSON.stringify(after) !== JSON.stringify(recipe.patchedSigningMetadata)) {
|
|
1087
|
-
throw setupError("PM_STUDIO_SIGNING_METADATA_CHANGED",
|
|
1088
|
-
"Ad-hoc signing did not produce the exact PM Studio signing metadata required by the recipe");
|
|
1089
|
-
}
|
|
1090
|
-
return;
|
|
1091
|
-
}
|
|
1092
|
-
const sourceSignature = source.codeSign;
|
|
1093
|
-
if (sourceSignature?.displayValid !== true) return;
|
|
1094
|
-
const flagsChanged = sourceSignature.flags
|
|
1095
|
-
&& flagsWithoutAdHoc(sourceSignature.flags) !== flagsWithoutAdHoc(after.flags);
|
|
1096
|
-
const runtimeChanged = sourceSignature.runtimeVersion
|
|
1097
|
-
&& sourceSignature.runtimeVersion !== after.runtime_version;
|
|
1098
|
-
const entitlementsChanged = sourceSignature.entitlementsState === "xml"
|
|
1099
|
-
&& sourceSignature.entitlementsSha256 !== after.entitlements_sha256;
|
|
1100
|
-
if (flagsChanged || runtimeChanged || entitlementsChanged) {
|
|
1101
|
-
throw setupError("PM_STUDIO_SIGNING_METADATA_CHANGED",
|
|
1102
|
-
"Ad-hoc signing did not preserve available PM Studio entitlements, flags, and runtime metadata");
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
function assertSpace({ appPath, backupRoot, backupExists, operations }) {
|
|
1107
|
-
const bundleBytes = operations.bundleSize(appPath);
|
|
1108
|
-
if (!Number.isFinite(bundleBytes) || bundleBytes <= 0) throw setupError("PM_STUDIO_SIZE_INVALID", "Could not determine PM Studio bundle size");
|
|
1109
|
-
const appParent = path.dirname(appPath);
|
|
1110
|
-
const stageRequired = bundleBytes + MIN_FREE_MARGIN_BYTES;
|
|
1111
|
-
const backupRequired = backupExists ? 0 : bundleBytes + MIN_FREE_MARGIN_BYTES;
|
|
1112
|
-
const sameVolume = operations.volumeId(appParent) === operations.volumeId(backupRoot);
|
|
1113
|
-
if (sameVolume) {
|
|
1114
|
-
if (operations.availableBytes(appParent) < stageRequired + backupRequired) {
|
|
1115
|
-
throw setupError("PM_STUDIO_SPACE_INSUFFICIENT", "Not enough free space for PM Studio staging and backup");
|
|
1116
|
-
}
|
|
1117
|
-
} else {
|
|
1118
|
-
if (operations.availableBytes(appParent) < stageRequired
|
|
1119
|
-
|| (!backupExists && operations.availableBytes(backupRoot) < backupRequired)) {
|
|
1120
|
-
throw setupError("PM_STUDIO_SPACE_INSUFFICIENT", "Not enough free space for PM Studio staging or backup");
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
function assertRestoreSpace({ appPath, backupAppPath, operations }) {
|
|
1126
|
-
const bundleBytes = operations.bundleSize(backupAppPath);
|
|
1127
|
-
if (!Number.isFinite(bundleBytes) || bundleBytes <= 0) {
|
|
1128
|
-
throw setupError("PM_STUDIO_SIZE_INVALID", "Could not determine verified PM Studio backup size");
|
|
1129
|
-
}
|
|
1130
|
-
if (operations.availableBytes(path.dirname(appPath)) < bundleBytes + MIN_FREE_MARGIN_BYTES) {
|
|
1131
|
-
throw setupError("PM_STUDIO_SPACE_INSUFFICIENT", "Not enough free space for PM Studio restore staging");
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
function codeSignFingerprint(codeSign) {
|
|
1136
|
-
return {
|
|
1137
|
-
valid: codeSign?.valid === true,
|
|
1138
|
-
verifyValid: codeSign?.verifyValid === true,
|
|
1139
|
-
displayValid: codeSign?.displayValid === true,
|
|
1140
|
-
entitlementsState: codeSign?.entitlementsState || "",
|
|
1141
|
-
adHoc: codeSign?.adHoc === true,
|
|
1142
|
-
identifier: codeSign?.identifier || "",
|
|
1143
|
-
teamIdentifier: codeSign?.teamIdentifier || "",
|
|
1144
|
-
flags: codeSign?.flags || "",
|
|
1145
|
-
runtimeVersion: codeSign?.runtimeVersion || "",
|
|
1146
|
-
cdHashFull: codeSign?.cdHashFull || "",
|
|
1147
|
-
notarizationTicket: codeSign?.notarizationTicket || "",
|
|
1148
|
-
entitlementsSha256: codeSign?.entitlementsSha256 || "",
|
|
1149
|
-
};
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
function restoreStateFingerprint(inspection, bundleContent) {
|
|
1153
|
-
return JSON.stringify({
|
|
1154
|
-
state: inspection.state,
|
|
1155
|
-
metadata: inspection.metadata,
|
|
1156
|
-
asar: {
|
|
1157
|
-
state: inspection.asar?.state,
|
|
1158
|
-
asarSha256: inspection.asar?.asarSha256,
|
|
1159
|
-
headerSha256: inspection.asar?.headerSha256,
|
|
1160
|
-
},
|
|
1161
|
-
plistIntegrity: inspection.plistIntegrity,
|
|
1162
|
-
executableIntegrity: {
|
|
1163
|
-
executableName: inspection.executableIntegrity?.executableName,
|
|
1164
|
-
executableSha256: inspection.executableIntegrity?.executableSha256,
|
|
1165
|
-
frameworkSha256: inspection.executableIntegrity?.frameworkSha256,
|
|
1166
|
-
embeddedIntegrity: inspection.executableIntegrity?.embeddedIntegrity,
|
|
1167
|
-
matchesRecipeName: inspection.executableIntegrity?.matchesRecipeName,
|
|
1168
|
-
},
|
|
1169
|
-
codeSign: codeSignFingerprint(inspection.codeSign),
|
|
1170
|
-
bundleContent,
|
|
1171
|
-
});
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
function inspectRestoreState({ appPath, recipe, operations }) {
|
|
1175
|
-
const inspection = inspectPmStudioApp({ appPath, recipe, operations });
|
|
1176
|
-
return restoreStateFromInspection({ appPath, inspection, recipe, operations });
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
function restoreStateFromInspection({ appPath, inspection, recipe, operations }) {
|
|
1180
|
-
const bundleContent = inspection.bundleContent || operations.inspectBundleContent({
|
|
1181
|
-
...appPaths(appPath, recipe),
|
|
1182
|
-
recipe,
|
|
1183
|
-
processRunner: operations.processRunner,
|
|
1184
|
-
});
|
|
1185
|
-
return {
|
|
1186
|
-
inspection,
|
|
1187
|
-
fingerprint: restoreStateFingerprint(inspection, bundleContent),
|
|
1188
|
-
};
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
function assertRestoreStateMatches(actual, expected, code, message) {
|
|
1192
|
-
if (actual.fingerprint !== expected.fingerprint) {
|
|
1193
|
-
throw setupError(code, message, actual.inspection.issues);
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
function replaceVerifiedStage({ appPath, stagePath, operations }) {
|
|
1198
|
-
try {
|
|
1199
|
-
operations.replaceApp({ appPath, stagePath, processRunner: operations.processRunner });
|
|
1200
|
-
return null;
|
|
1201
|
-
} catch (error) {
|
|
1202
|
-
return error;
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
function recoveryMessage(backup) {
|
|
1207
|
-
return `Verified backup: ${backup.backupAppPath}; manifest: ${backup.manifestPath}. Quit PM Studio, confirm the installed version/build still match, then restore only this version-matched backup or reinstall PM Studio.`;
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
export async function runPmStudioSetup({
|
|
1211
|
-
commandName = "ccdx",
|
|
1212
|
-
appPath = DEFAULT_PM_STUDIO_APP_PATH,
|
|
1213
|
-
home = os.homedir(),
|
|
1214
|
-
backupRoot = pmStudioBackupRoot(home),
|
|
1215
|
-
recipes = PM_STUDIO_RECIPES,
|
|
1216
|
-
operations: operationOverrides = {},
|
|
1217
|
-
logger = (line) => console.log(line),
|
|
1218
|
-
} = {}) {
|
|
1219
|
-
const operations = createPmStudioSetupOperations({
|
|
1220
|
-
...createPmStudioFileInspectionOperationOverrides(),
|
|
1221
|
-
...operationOverrides,
|
|
1222
|
-
});
|
|
1223
|
-
return operations.withSetupLock({ appPath }, async () => {
|
|
1224
|
-
const messages = [];
|
|
1225
|
-
const emit = (message) => {
|
|
1226
|
-
messages.push(message);
|
|
1227
|
-
logger(message);
|
|
1228
|
-
};
|
|
1229
|
-
|
|
1230
|
-
assertValidClaudeProfile(operations, home);
|
|
1231
|
-
assertInstalledAppPath(appPath);
|
|
1232
|
-
operations.assertPermissions({ appPath, backupRoot });
|
|
1233
|
-
assertNoBlockingProcesses(operations, appPath);
|
|
1234
|
-
|
|
1235
|
-
const genericPaths = {
|
|
1236
|
-
appPath,
|
|
1237
|
-
infoPlistPath: path.join(appPath, "Contents/Info.plist"),
|
|
1238
|
-
processRunner: operations.processRunner,
|
|
1239
|
-
};
|
|
1240
|
-
const metadata = operations.readBundleMetadata(genericPaths);
|
|
1241
|
-
const recipe = resolvePmStudioCompatibleRecipe({
|
|
1242
|
-
appPath,
|
|
1243
|
-
backupRoot,
|
|
1244
|
-
metadata,
|
|
1245
|
-
recipes,
|
|
1246
|
-
operations,
|
|
1247
|
-
});
|
|
1248
|
-
|
|
1249
|
-
const source = inspectPmStudioApp({ appPath, recipe, operations });
|
|
1250
|
-
const finalBackupDir = path.join(backupRoot, backupName(recipe));
|
|
1251
|
-
if (source.state === "patched") {
|
|
1252
|
-
if (!fs.existsSync(finalBackupDir)) {
|
|
1253
|
-
throw setupError("PM_STUDIO_BACKUP_INVALID", "Patched PM Studio has no matching verified backup; no files were changed");
|
|
1254
|
-
}
|
|
1255
|
-
const backup = validateBackup({ backupDir: finalBackupDir, recipe, operations });
|
|
1256
|
-
assertPatchedBinaryRecord({ inspection: source, manifestPath: backup.manifestPath, recipe });
|
|
1257
|
-
emit(`[OK] PM Studio ${recipe.version} build ${recipe.build} is already patched; no backup or signing was repeated.`);
|
|
1258
|
-
emit(`[OK] Verified backup: ${backup.backupAppPath}`);
|
|
1259
|
-
emit(`[OK] Backup manifest: ${backup.manifestPath}`);
|
|
1260
|
-
emit(`[INFO] Start ${commandName} before using Claude in PM Studio; GPT remains on PM Studio's native GitHub Copilot path.`);
|
|
1261
|
-
return { status: "already_patched", changed: false, recipe: recipe.id, backup, inspection: source, messages };
|
|
1262
|
-
}
|
|
1263
|
-
const migratingLegacy = source.state === "legacy";
|
|
1264
|
-
const migratingPredecessor = source.state === "predecessor";
|
|
1265
|
-
const migratingExistingPatch = migratingLegacy || migratingPredecessor;
|
|
1266
|
-
if (source.state !== "clean" && !migratingExistingPatch) {
|
|
1267
|
-
throw setupError("PM_STUDIO_BUNDLE_DRIFT",
|
|
1268
|
-
`PM Studio ${recipe.version} build ${recipe.build} does not match the clean, current split-origin, predecessor, or legacy recipe; no files were changed`, source.issues);
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
const backupExists = fs.existsSync(finalBackupDir);
|
|
1272
|
-
let verifiedBackup = null;
|
|
1273
|
-
if (migratingExistingPatch) {
|
|
1274
|
-
if (!backupExists) {
|
|
1275
|
-
throw setupError("PM_STUDIO_BACKUP_INVALID",
|
|
1276
|
-
`${migratingLegacy ? "Legacy global-origin" : "Predecessor split-origin"} PM Studio patch has no matching verified clean backup; no files were changed`);
|
|
1277
|
-
}
|
|
1278
|
-
verifiedBackup = validateBackup({ backupDir: finalBackupDir, recipe, operations });
|
|
1279
|
-
assertPatchedBinaryRecord({
|
|
1280
|
-
inspection: source,
|
|
1281
|
-
manifestPath: verifiedBackup.manifestPath,
|
|
1282
|
-
recipe,
|
|
1283
|
-
recordState: source.state,
|
|
1284
|
-
});
|
|
1285
|
-
emit(migratingLegacy
|
|
1286
|
-
? "[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."
|
|
1287
|
-
: "[WARN] Predecessor split-origin patch detected; setup will rebuild from the verified clean backup so delayed Claude model discovery no longer falls back to the native catalog.");
|
|
1288
|
-
}
|
|
1289
|
-
assertSpace({ appPath, backupRoot, backupExists, operations });
|
|
1290
|
-
|
|
1291
|
-
emit("[WARN] PM Studio will be ad-hoc signed after patching; an update or reinstall may overwrite this patch.");
|
|
1292
|
-
const backup = verifiedBackup || ensureBackup({ appPath, backupRoot, recipe, operations });
|
|
1293
|
-
const stagePath = path.join(path.dirname(appPath), `.${path.basename(appPath)}.ccdx-stage-${operations.uuid()}`);
|
|
1294
|
-
let preserveStage = false;
|
|
1295
|
-
try {
|
|
1296
|
-
operations.copyBundle({
|
|
1297
|
-
source: migratingExistingPatch ? backup.backupAppPath : appPath,
|
|
1298
|
-
destination: stagePath,
|
|
1299
|
-
processRunner: operations.processRunner,
|
|
1300
|
-
});
|
|
1301
|
-
const stagedSource = inspectPmStudioApp({ appPath: stagePath, recipe, operations });
|
|
1302
|
-
if (stagedSource.state !== "clean"
|
|
1303
|
-
|| (!migratingExistingPatch && recipe.sourceBundleContent
|
|
1304
|
-
&& stagedSource.bundleContent?.sha256 !== source.bundleContent?.sha256)) {
|
|
1305
|
-
throw setupError("PM_STUDIO_STAGE_INVALID", "PM Studio staging copy does not match the exact clean source bundle", stagedSource.issues);
|
|
1306
|
-
}
|
|
1307
|
-
const stageAsarPath = path.join(stagePath, recipe.asarPath);
|
|
1308
|
-
const patched = patchAsarBuffer(fs.readFileSync(stageAsarPath), recipe);
|
|
1309
|
-
if (!patched.changed) throw setupError("PM_STUDIO_STAGE_INVALID", "Staging copy was unexpectedly already patched");
|
|
1310
|
-
fs.writeFileSync(stageAsarPath, patched.buffer);
|
|
1311
|
-
patched.buffer = null;
|
|
1312
|
-
operations.writeAsarIntegrity({
|
|
1313
|
-
...appPaths(stagePath, recipe),
|
|
1314
|
-
recipe,
|
|
1315
|
-
hash: recipe.patchedHeaderSha256,
|
|
1316
|
-
processRunner: operations.processRunner,
|
|
1317
|
-
});
|
|
1318
|
-
operations.signApp({
|
|
1319
|
-
appPath: stagePath,
|
|
1320
|
-
recipe,
|
|
1321
|
-
source: stagedSource,
|
|
1322
|
-
processRunner: operations.processRunner,
|
|
1323
|
-
});
|
|
1324
|
-
|
|
1325
|
-
const staged = inspectPmStudioApp({ appPath: stagePath, recipe, operations });
|
|
1326
|
-
if (staged.state !== "patched") {
|
|
1327
|
-
throw setupError("PM_STUDIO_STAGE_VERIFY_FAILED", "Patched PM Studio staging bundle failed verification", staged.issues);
|
|
1328
|
-
}
|
|
1329
|
-
assertSigningMetadataPreserved(stagedSource, staged, recipe);
|
|
1330
|
-
writePatchedBinaryRecord({ backup, inspection: staged, operations, recipe });
|
|
1331
|
-
|
|
1332
|
-
assertNoBlockingProcesses(operations, appPath);
|
|
1333
|
-
const unchangedSource = inspectPmStudioApp({ appPath, recipe, operations });
|
|
1334
|
-
if (unchangedSource.state !== source.state
|
|
1335
|
-
|| unchangedSource.asar.asarSha256 !== source.asar.asarSha256
|
|
1336
|
-
|| unchangedSource.asar.headerSha256 !== source.asar.headerSha256
|
|
1337
|
-
|| (recipe.sourceBundleContent
|
|
1338
|
-
&& unchangedSource.bundleContent?.sha256 !== source.bundleContent?.sha256)) {
|
|
1339
|
-
throw setupError("PM_STUDIO_SOURCE_CHANGED", "PM Studio changed during setup; the verified staging bundle was not installed");
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
const replacementError = replaceVerifiedStage({ appPath, stagePath, operations });
|
|
1343
|
-
let installed;
|
|
1344
|
-
try {
|
|
1345
|
-
installed = inspectPmStudioApp({ appPath, recipe, operations });
|
|
1346
|
-
} catch {
|
|
1347
|
-
preserveStage = fs.existsSync(stagePath);
|
|
1348
|
-
throw setupError("PM_STUDIO_REPLACE_STATE_UNKNOWN",
|
|
1349
|
-
`PM Studio replacement final state could not be verified. Do not launch it. ${recoveryMessage(backup)}${preserveStage ? ` Verified staging retained: ${stagePath}.` : ""}`);
|
|
1350
|
-
}
|
|
1351
|
-
if (installed.state !== "patched") {
|
|
1352
|
-
const originalRetained = installed.state === source.state
|
|
1353
|
-
&& installed.asar.asarSha256 === source.asar.asarSha256
|
|
1354
|
-
&& installed.asar.headerSha256 === source.asar.headerSha256
|
|
1355
|
-
&& (!recipe.sourceBundleContent
|
|
1356
|
-
|| installed.bundleContent?.sha256 === source.bundleContent?.sha256);
|
|
1357
|
-
preserveStage = !originalRetained && fs.existsSync(stagePath);
|
|
1358
|
-
const code = originalRetained
|
|
1359
|
-
? "PM_STUDIO_REPLACE_FAILED"
|
|
1360
|
-
: "PM_STUDIO_INSTALL_VERIFY_FAILED";
|
|
1361
|
-
const stateMessage = originalRetained
|
|
1362
|
-
? `The installed PM Studio remains the verified ${source.state} bundle`
|
|
1363
|
-
: "The installed PM Studio did not retain the verified patched state; do not launch it";
|
|
1364
|
-
throw setupError(code,
|
|
1365
|
-
`${stateMessage}. ${recoveryMessage(backup)}${preserveStage ? ` Verified staging retained: ${stagePath}.` : ""}`,
|
|
1366
|
-
installed.issues);
|
|
1367
|
-
}
|
|
1368
|
-
try {
|
|
1369
|
-
assertPatchedBinaryRecord({ inspection: installed, manifestPath: backup.manifestPath, recipe });
|
|
1370
|
-
} catch (error) {
|
|
1371
|
-
throw setupError("PM_STUDIO_INSTALL_VERIFY_FAILED",
|
|
1372
|
-
`Installed PM Studio differs from the verified staging record; do not launch it. ${recoveryMessage(backup)}`,
|
|
1373
|
-
[error.message]);
|
|
1374
|
-
}
|
|
1375
|
-
if (replacementError) {
|
|
1376
|
-
emit("[WARN] Atomic replacement reported an error after the exact patched bundle became installed; the final bundle was independently verified.");
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
emit(migratingLegacy
|
|
1380
|
-
? `[OK] Migrated PM Studio ${recipe.version} build ${recipe.build} from the legacy global-origin patch to the split-origin patch.`
|
|
1381
|
-
: migratingPredecessor
|
|
1382
|
-
? `[OK] Migrated PM Studio ${recipe.version} build ${recipe.build} from the predecessor split-origin patch to the current split-origin patch.`
|
|
1383
|
-
: `[OK] Patched PM Studio ${recipe.version} build ${recipe.build}.`);
|
|
1384
|
-
emit(`[OK] app.asar SHA-256: ${installed.asar.asarSha256}`);
|
|
1385
|
-
emit(`[OK] Verified backup: ${backup.backupAppPath}${backup.reused ? " (reused)" : ""}`);
|
|
1386
|
-
emit(`[OK] Backup manifest: ${backup.manifestPath}`);
|
|
1387
|
-
emit("[INFO] The verified staging bundle was installed with Foundation's atomic item replacement.");
|
|
1388
|
-
emit(`[INFO] To restore, quit PM Studio and its updater, then run ${commandName} pms restore.`);
|
|
1389
|
-
emit("[INFO] Restore uses exact source-content verification; the original source signature is inspected and reported but is not an admission gate.");
|
|
1390
|
-
emit(`[INFO] Start ${commandName} before using Claude in PM Studio; GPT remains on PM Studio's native GitHub Copilot path.`);
|
|
1391
|
-
return {
|
|
1392
|
-
status: migratingExistingPatch ? "migrated" : "patched",
|
|
1393
|
-
changed: true,
|
|
1394
|
-
recipe: recipe.id,
|
|
1395
|
-
backup,
|
|
1396
|
-
inspection: installed,
|
|
1397
|
-
replacementMode: "foundation-atomic-replace",
|
|
1398
|
-
messages,
|
|
1399
|
-
};
|
|
1400
|
-
} finally {
|
|
1401
|
-
if (!preserveStage && fs.existsSync(stagePath)) operations.removePath({ target: stagePath });
|
|
1402
|
-
}
|
|
1403
|
-
});
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
export async function runPmStudioRestore({
|
|
1407
|
-
commandName = "ccdx",
|
|
1408
|
-
appPath = DEFAULT_PM_STUDIO_APP_PATH,
|
|
1409
|
-
home = os.homedir(),
|
|
1410
|
-
backupRoot = pmStudioBackupRoot(home),
|
|
1411
|
-
recipes = PM_STUDIO_RECIPES,
|
|
1412
|
-
operations: operationOverrides = {},
|
|
1413
|
-
logger = (line) => console.log(line),
|
|
1414
|
-
} = {}) {
|
|
1415
|
-
const operations = createPmStudioSetupOperations({
|
|
1416
|
-
...createPmStudioFileInspectionOperationOverrides(),
|
|
1417
|
-
...operationOverrides,
|
|
1418
|
-
});
|
|
1419
|
-
return operations.withSetupLock({ appPath }, async () => {
|
|
1420
|
-
const messages = [];
|
|
1421
|
-
const emit = (message) => {
|
|
1422
|
-
messages.push(message);
|
|
1423
|
-
logger(message);
|
|
1424
|
-
};
|
|
1425
|
-
|
|
1426
|
-
assertInstalledAppPath(appPath);
|
|
1427
|
-
assertNoBlockingProcesses(operations, appPath);
|
|
1428
|
-
const metadata = operations.readBundleMetadata({
|
|
1429
|
-
appPath,
|
|
1430
|
-
infoPlistPath: path.join(appPath, "Contents/Info.plist"),
|
|
1431
|
-
processRunner: operations.processRunner,
|
|
1432
|
-
});
|
|
1433
|
-
const recipe = resolvePmStudioCompatibleRecipe({
|
|
1434
|
-
appPath,
|
|
1435
|
-
home,
|
|
1436
|
-
backupRoot,
|
|
1437
|
-
metadata,
|
|
1438
|
-
recipes,
|
|
1439
|
-
operations,
|
|
1440
|
-
});
|
|
1441
|
-
const source = inspectRestoreState({ appPath, recipe, operations });
|
|
1442
|
-
if (source.inspection.state === "clean") {
|
|
1443
|
-
emit(`[OK] PM Studio ${recipe.version} build ${recipe.build} is already the exact clean local bundle; no files were changed.`);
|
|
1444
|
-
return {
|
|
1445
|
-
status: "already_clean",
|
|
1446
|
-
changed: false,
|
|
1447
|
-
recipe: recipe.id,
|
|
1448
|
-
inspection: source.inspection,
|
|
1449
|
-
messages,
|
|
1450
|
-
};
|
|
1451
|
-
}
|
|
1452
|
-
if (!["patched", "predecessor", "legacy"].includes(source.inspection.state)) {
|
|
1453
|
-
throw setupError("PM_STUDIO_BUNDLE_DRIFT",
|
|
1454
|
-
`PM Studio ${recipe.version} build ${recipe.build} does not match a recorded patch state; no files were changed`,
|
|
1455
|
-
source.inspection.issues);
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
const manifestPath = pmStudioPatchManifestPath({ home, backupRoot, recipe });
|
|
1459
|
-
const backupDir = path.dirname(manifestPath);
|
|
1460
|
-
const backupEvidence = {};
|
|
1461
|
-
const backup = validateBackup({ backupDir, recipe, operations, evidence: backupEvidence });
|
|
1462
|
-
assertPatchedBinaryRecord({
|
|
1463
|
-
inspection: source.inspection,
|
|
1464
|
-
manifestPath: backup.manifestPath,
|
|
1465
|
-
recipe,
|
|
1466
|
-
recordState: source.inspection.state,
|
|
1467
|
-
});
|
|
1468
|
-
operations.assertRestorePermissions({
|
|
1469
|
-
appPath,
|
|
1470
|
-
backupAppPath: backup.backupAppPath,
|
|
1471
|
-
processRunner: operations.processRunner,
|
|
1472
|
-
});
|
|
1473
|
-
assertRestoreSpace({ appPath, backupAppPath: backup.backupAppPath, operations });
|
|
1474
|
-
const manifestSnapshot = fs.readFileSync(backup.manifestPath);
|
|
1475
|
-
const backupSource = restoreStateFromInspection({
|
|
1476
|
-
appPath: backup.backupAppPath,
|
|
1477
|
-
inspection: backupEvidence.inspection,
|
|
1478
|
-
recipe,
|
|
1479
|
-
operations,
|
|
1480
|
-
});
|
|
1481
|
-
if (backupSource.inspection.state !== "clean") {
|
|
1482
|
-
throw setupError("PM_STUDIO_BACKUP_INVALID", "Verified PM Studio backup is not an exact clean bundle",
|
|
1483
|
-
backupSource.inspection.issues);
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
const stagePath = path.join(path.dirname(appPath),
|
|
1487
|
-
`.${path.basename(appPath)}.ccdx-restore-stage-${operations.uuid()}`);
|
|
1488
|
-
let preserveStage = false;
|
|
1489
|
-
try {
|
|
1490
|
-
operations.copyBundle({
|
|
1491
|
-
source: backup.backupAppPath,
|
|
1492
|
-
destination: stagePath,
|
|
1493
|
-
processRunner: operations.processRunner,
|
|
1494
|
-
});
|
|
1495
|
-
if (!fs.readFileSync(backup.manifestPath).equals(manifestSnapshot)) {
|
|
1496
|
-
throw setupError("PM_STUDIO_BACKUP_CHANGED",
|
|
1497
|
-
"PM Studio backup manifest changed during restore; the verified staging bundle was not installed");
|
|
1498
|
-
}
|
|
1499
|
-
const currentBackupEvidence = {};
|
|
1500
|
-
const currentBackup = validateBackup({
|
|
1501
|
-
backupDir,
|
|
1502
|
-
recipe,
|
|
1503
|
-
operations,
|
|
1504
|
-
evidence: currentBackupEvidence,
|
|
1505
|
-
});
|
|
1506
|
-
const unchangedBackup = restoreStateFromInspection({
|
|
1507
|
-
appPath: currentBackup.backupAppPath,
|
|
1508
|
-
inspection: currentBackupEvidence.inspection,
|
|
1509
|
-
recipe,
|
|
1510
|
-
operations,
|
|
1511
|
-
});
|
|
1512
|
-
assertRestoreStateMatches(unchangedBackup, backupSource, "PM_STUDIO_BACKUP_CHANGED",
|
|
1513
|
-
"PM Studio backup changed during restore; the verified staging bundle was not installed");
|
|
1514
|
-
const unchangedSource = inspectRestoreState({ appPath, recipe, operations });
|
|
1515
|
-
assertRestoreStateMatches(unchangedSource, source, "PM_STUDIO_SOURCE_CHANGED",
|
|
1516
|
-
"PM Studio changed during restore; the verified staging bundle was not installed");
|
|
1517
|
-
assertPatchedBinaryRecord({
|
|
1518
|
-
inspection: unchangedSource.inspection,
|
|
1519
|
-
manifestPath: backup.manifestPath,
|
|
1520
|
-
recipe,
|
|
1521
|
-
recordState: source.inspection.state,
|
|
1522
|
-
});
|
|
1523
|
-
|
|
1524
|
-
const staged = inspectRestoreState({ appPath: stagePath, recipe, operations });
|
|
1525
|
-
if (staged.inspection.state !== "clean") {
|
|
1526
|
-
throw setupError("PM_STUDIO_RESTORE_STAGE_INVALID",
|
|
1527
|
-
"PM Studio restore staging copy is not an exact clean bundle", staged.inspection.issues);
|
|
1528
|
-
}
|
|
1529
|
-
assertRestoreStateMatches(staged, backupSource, "PM_STUDIO_RESTORE_STAGE_INVALID",
|
|
1530
|
-
"PM Studio restore staging copy differs from the verified clean backup");
|
|
1531
|
-
assertNoBlockingProcesses(operations, appPath);
|
|
1532
|
-
|
|
1533
|
-
const replacementError = replaceVerifiedStage({ appPath, stagePath, operations });
|
|
1534
|
-
let installed;
|
|
1535
|
-
try {
|
|
1536
|
-
installed = inspectRestoreState({ appPath, recipe, operations });
|
|
1537
|
-
} catch {
|
|
1538
|
-
preserveStage = fs.existsSync(stagePath);
|
|
1539
|
-
throw setupError("PM_STUDIO_RESTORE_STATE_UNKNOWN",
|
|
1540
|
-
`PM Studio restore final state could not be verified. Do not launch it. ${recoveryMessage(backup)}${preserveStage ? ` Verified staging retained: ${stagePath}.` : ""}`);
|
|
1541
|
-
}
|
|
1542
|
-
if (installed.inspection.state !== "clean" || installed.fingerprint !== staged.fingerprint) {
|
|
1543
|
-
const originalRetained = installed.fingerprint === source.fingerprint;
|
|
1544
|
-
preserveStage = !originalRetained && fs.existsSync(stagePath);
|
|
1545
|
-
const code = originalRetained
|
|
1546
|
-
? "PM_STUDIO_RESTORE_REPLACE_FAILED"
|
|
1547
|
-
: "PM_STUDIO_RESTORE_INSTALL_VERIFY_FAILED";
|
|
1548
|
-
const stateMessage = originalRetained
|
|
1549
|
-
? `The installed PM Studio remains the verified ${source.inspection.state} bundle`
|
|
1550
|
-
: "The installed PM Studio does not match the verified clean backup; do not launch it";
|
|
1551
|
-
throw setupError(code,
|
|
1552
|
-
`${stateMessage}. ${recoveryMessage(backup)}${preserveStage ? ` Verified staging retained: ${stagePath}.` : ""}`,
|
|
1553
|
-
installed.inspection.issues);
|
|
1554
|
-
}
|
|
1555
|
-
if (replacementError) {
|
|
1556
|
-
emit("[WARN] Atomic replacement reported an error after the exact clean bundle became installed; the final bundle was independently verified.");
|
|
1557
|
-
}
|
|
1558
|
-
emit(`[OK] Restored PM Studio ${recipe.version} build ${recipe.build} from the exact verified clean backup.`);
|
|
1559
|
-
emit(`[OK] Verified backup retained: ${backup.backupAppPath}`);
|
|
1560
|
-
emit(`[OK] Backup manifest retained: ${backup.manifestPath}`);
|
|
1561
|
-
emit("[INFO] The verified clean staging bundle was installed with Foundation's atomic item replacement.");
|
|
1562
|
-
if (installed.inspection.codeSign?.valid === true) {
|
|
1563
|
-
emit("[OK] The restored source bundle's original signature verifies successfully.");
|
|
1564
|
-
} else {
|
|
1565
|
-
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.");
|
|
1566
|
-
}
|
|
1567
|
-
emit(`[INFO] Run ${commandName} pms setup to apply the split-origin patch again.`);
|
|
1568
|
-
return {
|
|
1569
|
-
status: "restored",
|
|
1570
|
-
changed: true,
|
|
1571
|
-
recipe: recipe.id,
|
|
1572
|
-
backup,
|
|
1573
|
-
inspection: installed.inspection,
|
|
1574
|
-
replacementMode: "foundation-atomic-replace",
|
|
1575
|
-
messages,
|
|
1576
|
-
};
|
|
1577
|
-
} finally {
|
|
1578
|
-
if (!preserveStage && fs.existsSync(stagePath)) operations.removePath({ target: stagePath });
|
|
1579
|
-
}
|
|
1580
|
-
});
|
|
1581
|
-
}
|