codex-plus-patcher 0.7.0 → 0.7.2
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 +2 -1
- package/package.json +2 -2
- package/src/cli.js +94 -0
- package/src/core/asar.js +6 -4
- package/src/core/dev-mode.js +339 -0
- package/src/core/plugin-audit.js +1605 -0
- package/src/patches/26.623.41415-4505.js +44 -0
- package/src/patches/26.623.42026-4514.js +44 -0
- package/src/patches/index.js +10 -1
- package/src/patches/lib/common-patches.js +621 -195
- package/src/patches/lib/hooks/message-composer.js +1 -1
- package/src/patches/lib/hooks/project-selector.js +2 -2
- package/src/patches/lib/hooks/review.js +4 -2
- package/src/patches/lib/hooks/settings-commands.js +3 -2
- package/src/patches/lib/hooks/sidebar.js +1 -6
- package/src/patches/lib/project-selector-shortcut-patch.js +141 -2
- package/src/runtime/api/index.js +3 -0
- package/src/runtime/assets.js +4 -4
- package/src/runtime/host/projectSelector.js +5 -1
- package/src/runtime/plugins/mermaidFullscreen.js +19 -6
- package/src/runtime/plugins/nestedRepositories.js +72 -11
- package/src/runtime/plugins/projectColors.js +96 -7
- package/src/runtime/plugins/projectSelectorShortcut.js +67 -12
- package/src/runtime/plugins/sidebarNameBlur.js +1 -1
- package/src/runtime/plugins/userBubbleColors.js +4 -0
|
@@ -0,0 +1,1605 @@
|
|
|
1
|
+
const childProcess = require("node:child_process");
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const http = require("node:http");
|
|
4
|
+
const net = require("node:net");
|
|
5
|
+
const os = require("node:os");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
DEFAULT_DEV_HOME,
|
|
10
|
+
DEFAULT_ELECTRON_USER_DATA,
|
|
11
|
+
launchDevApp,
|
|
12
|
+
syncDevHome,
|
|
13
|
+
} = require("./dev-mode");
|
|
14
|
+
const { patchCodexApp } = require("./patch-engine");
|
|
15
|
+
const { patchSets } = require("../patches");
|
|
16
|
+
const packageJson = require("../../package.json");
|
|
17
|
+
|
|
18
|
+
const DEFAULT_SOURCE = "/Applications/Codex.app";
|
|
19
|
+
const DEFAULT_TARGET = path.resolve("work/Codex Plus.app");
|
|
20
|
+
const DEFAULT_PORT = 9234;
|
|
21
|
+
|
|
22
|
+
function expandPath(input) {
|
|
23
|
+
if (input === "~") return os.homedir();
|
|
24
|
+
if (input.startsWith("~/")) return path.join(os.homedir(), input.slice(2));
|
|
25
|
+
return input;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseArgs(argv) {
|
|
29
|
+
const args = {
|
|
30
|
+
source: DEFAULT_SOURCE,
|
|
31
|
+
target: DEFAULT_TARGET,
|
|
32
|
+
sourceHome: path.join(os.homedir(), ".codex"),
|
|
33
|
+
devHome: DEFAULT_DEV_HOME,
|
|
34
|
+
electronUserDataPath: DEFAULT_ELECTRON_USER_DATA,
|
|
35
|
+
remoteDebuggingPort: DEFAULT_PORT,
|
|
36
|
+
apply: true,
|
|
37
|
+
launch: true,
|
|
38
|
+
json: false,
|
|
39
|
+
keepOpen: false,
|
|
40
|
+
includeNativeOpenProbes: false,
|
|
41
|
+
noProgress: false,
|
|
42
|
+
quiet: false,
|
|
43
|
+
devInstanceId: "audit",
|
|
44
|
+
};
|
|
45
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
46
|
+
const arg = argv[index];
|
|
47
|
+
const next = () => {
|
|
48
|
+
index += 1;
|
|
49
|
+
if (index >= argv.length) throw new Error(`Missing value for ${arg}`);
|
|
50
|
+
return argv[index];
|
|
51
|
+
};
|
|
52
|
+
if (arg === "--source") args.source = path.resolve(expandPath(next()));
|
|
53
|
+
else if (arg === "--target") args.target = path.resolve(expandPath(next()));
|
|
54
|
+
else if (arg === "--source-home") args.sourceHome = path.resolve(expandPath(next()));
|
|
55
|
+
else if (arg === "--dev-home") args.devHome = path.resolve(expandPath(next()));
|
|
56
|
+
else if (arg === "--electron-user-data") args.electronUserDataPath = path.resolve(expandPath(next()));
|
|
57
|
+
else if (arg === "--dev-instance-id") args.devInstanceId = next();
|
|
58
|
+
else if (arg === "--remote-debugging-port" || arg === "--port") args.remoteDebuggingPort = Number(next());
|
|
59
|
+
else if (arg === "--no-apply") args.apply = false;
|
|
60
|
+
else if (arg === "--no-launch") args.launch = false;
|
|
61
|
+
else if (arg === "--json" || arg === "--format=json") args.json = true;
|
|
62
|
+
else if (arg === "--quiet") args.quiet = true;
|
|
63
|
+
else if (arg === "--no-progress") args.noProgress = true;
|
|
64
|
+
else if (arg === "--keep-open") args.keepOpen = true;
|
|
65
|
+
else if (arg === "--include-native-open-probes") args.includeNativeOpenProbes = true;
|
|
66
|
+
else throw new Error(`Unknown argument: ${arg}`);
|
|
67
|
+
}
|
|
68
|
+
return args;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function auditIdentity({ cwd = path.resolve(__dirname, "../.."), execFileSync = childProcess.execFileSync } = {}) {
|
|
72
|
+
const identity = {
|
|
73
|
+
packageName: packageJson.name,
|
|
74
|
+
packageVersion: packageJson.version,
|
|
75
|
+
gitSha: "unknown",
|
|
76
|
+
gitDirty: null,
|
|
77
|
+
gitAvailable: false,
|
|
78
|
+
};
|
|
79
|
+
try {
|
|
80
|
+
identity.gitSha = execFileSync("git", ["rev-parse", "--short=12", "HEAD"], {
|
|
81
|
+
cwd,
|
|
82
|
+
encoding: "utf8",
|
|
83
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
84
|
+
}).trim() || "unknown";
|
|
85
|
+
identity.gitAvailable = true;
|
|
86
|
+
identity.gitDirty = execFileSync("git", ["status", "--porcelain"], {
|
|
87
|
+
cwd,
|
|
88
|
+
encoding: "utf8",
|
|
89
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
90
|
+
}).trim().length > 0;
|
|
91
|
+
} catch {
|
|
92
|
+
identity.gitSha = "unknown";
|
|
93
|
+
identity.gitDirty = null;
|
|
94
|
+
identity.gitAvailable = false;
|
|
95
|
+
}
|
|
96
|
+
return identity;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function portIsFree(port) {
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
const server = net.createServer();
|
|
102
|
+
server.once("error", () => resolve(false));
|
|
103
|
+
server.once("listening", () => {
|
|
104
|
+
server.close(() => resolve(true));
|
|
105
|
+
});
|
|
106
|
+
server.listen(port, "127.0.0.1");
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function findFreePort(start) {
|
|
111
|
+
for (let port = start; port < start + 100; port += 1) {
|
|
112
|
+
if (await portIsFree(port)) return port;
|
|
113
|
+
}
|
|
114
|
+
throw new Error(`Could not find a free remote debugging port starting at ${start}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function auditAttachCommand(port) {
|
|
118
|
+
return `codex-plus-patcher audit-plugins --no-apply --no-launch --keep-open --port ${port}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getJson(url) {
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
const request = http.get(url, (response) => {
|
|
124
|
+
const chunks = [];
|
|
125
|
+
response.on("data", (chunk) => chunks.push(chunk));
|
|
126
|
+
response.on("end", () => {
|
|
127
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
128
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
129
|
+
reject(new Error(`${url} returned ${response.statusCode}: ${text}`));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
resolve(JSON.parse(text));
|
|
134
|
+
} catch (error) {
|
|
135
|
+
reject(error);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
request.on("error", reject);
|
|
140
|
+
request.setTimeout(1000, () => request.destroy(new Error(`${url} timed out`)));
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function delay(ms) {
|
|
145
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function waitForRendererTarget(port, timeoutMs = 90000) {
|
|
149
|
+
const deadline = Date.now() + timeoutMs;
|
|
150
|
+
let lastError = null;
|
|
151
|
+
while (Date.now() < deadline) {
|
|
152
|
+
try {
|
|
153
|
+
const targets = await getJson(`http://127.0.0.1:${port}/json/list`);
|
|
154
|
+
const target = targets.find((entry) => entry.url === "app://-/index.html");
|
|
155
|
+
if (target) return target;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
lastError = error;
|
|
158
|
+
}
|
|
159
|
+
await delay(500);
|
|
160
|
+
}
|
|
161
|
+
throw new Error(`Timed out waiting for app://-/index.html on port ${port}${lastError ? `: ${lastError.message}` : ""}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function findRendererTargetOnPort(port) {
|
|
165
|
+
try {
|
|
166
|
+
const targets = await getJson(`http://127.0.0.1:${port}/json/list`);
|
|
167
|
+
return targets.find((entry) => entry.url === "app://-/index.html") || null;
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function waitForMermaidViewerTarget(port, beforeIds = new Set(), timeoutMs = 10000) {
|
|
174
|
+
const deadline = Date.now() + timeoutMs;
|
|
175
|
+
let lastError = null;
|
|
176
|
+
while (Date.now() < deadline) {
|
|
177
|
+
try {
|
|
178
|
+
const targets = await getJson(`http://127.0.0.1:${port}/json/list`);
|
|
179
|
+
const target = targets.find((entry) =>
|
|
180
|
+
!beforeIds.has(entry.id) &&
|
|
181
|
+
entry.url?.startsWith("file://") &&
|
|
182
|
+
entry.url.includes("codex-plus-mermaid-"));
|
|
183
|
+
if (target) return target;
|
|
184
|
+
} catch (error) {
|
|
185
|
+
lastError = error;
|
|
186
|
+
}
|
|
187
|
+
await delay(250);
|
|
188
|
+
}
|
|
189
|
+
throw new Error(`Timed out waiting for Mermaid viewer target on port ${port}${lastError ? `: ${lastError.message}` : ""}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function verifyMermaidViewerRender(appCdp, port, { Session = CdpSession, timeoutMs = 15000 } = {}) {
|
|
193
|
+
const beforeTargets = await getJson(`http://127.0.0.1:${port}/json/list`).catch(() => []);
|
|
194
|
+
const beforeIds = new Set(beforeTargets.map((target) => target.id));
|
|
195
|
+
await appCdp.evaluate(`(() => {
|
|
196
|
+
const host = document.createElement("div");
|
|
197
|
+
host.setAttribute("data-markdown-copy", "code-block");
|
|
198
|
+
const pre = document.createElement("pre");
|
|
199
|
+
pre.className = "sr-only";
|
|
200
|
+
pre.textContent = "graph TD;A-->B";
|
|
201
|
+
const diagram = document.createElement("div");
|
|
202
|
+
diagram.setAttribute("data-codex-plus-mermaid-diagram", "");
|
|
203
|
+
host.append(pre, diagram);
|
|
204
|
+
document.body.appendChild(host);
|
|
205
|
+
window.CodexPlus.plugins.get("mermaidFullscreen").exports.openViewer(diagram);
|
|
206
|
+
setTimeout(() => host.remove(), 1000);
|
|
207
|
+
return true;
|
|
208
|
+
})()`);
|
|
209
|
+
const viewerTarget = await waitForMermaidViewerTarget(port, beforeIds, timeoutMs);
|
|
210
|
+
const viewer = new Session(viewerTarget.webSocketDebuggerUrl);
|
|
211
|
+
try {
|
|
212
|
+
await viewer.connect();
|
|
213
|
+
await viewer.send("Runtime.enable");
|
|
214
|
+
const deadline = Date.now() + timeoutMs;
|
|
215
|
+
let status = null;
|
|
216
|
+
while (Date.now() < deadline) {
|
|
217
|
+
status = await viewer.evaluate(`(() => {
|
|
218
|
+
const svg = document.querySelector("#stage svg");
|
|
219
|
+
const status = document.getElementById("render-status")?.textContent || "";
|
|
220
|
+
return {
|
|
221
|
+
hasSvg: Boolean(svg && svg.outerHTML.length > 1000),
|
|
222
|
+
status,
|
|
223
|
+
statusHidden: document.getElementById("render-status")?.hidden ?? null,
|
|
224
|
+
svgLength: svg?.outerHTML?.length || 0,
|
|
225
|
+
bodyText: document.body?.innerText?.slice(0, 500) || "",
|
|
226
|
+
};
|
|
227
|
+
})()`);
|
|
228
|
+
if (status.hasSvg && !/Mermaid render failed:/i.test(status.status)) {
|
|
229
|
+
return {
|
|
230
|
+
ok: true,
|
|
231
|
+
url: viewerTarget.url,
|
|
232
|
+
status: status.status,
|
|
233
|
+
svgLength: status.svgLength,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
if (/Mermaid render failed:/i.test(status.status) || /Mermaid render failed:/i.test(status.bodyText)) break;
|
|
237
|
+
await delay(250);
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
ok: false,
|
|
241
|
+
url: viewerTarget.url,
|
|
242
|
+
message: status?.status || "Mermaid viewer did not render an SVG",
|
|
243
|
+
status,
|
|
244
|
+
};
|
|
245
|
+
} finally {
|
|
246
|
+
try {
|
|
247
|
+
await viewer.send("Page.close");
|
|
248
|
+
} catch {
|
|
249
|
+
// The viewer may already be closed.
|
|
250
|
+
}
|
|
251
|
+
await viewer.close();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function listRunningAuditApps({
|
|
256
|
+
targetApp = DEFAULT_TARGET,
|
|
257
|
+
electronUserDataPath = DEFAULT_ELECTRON_USER_DATA,
|
|
258
|
+
execFileSync = childProcess.execFileSync,
|
|
259
|
+
} = {}) {
|
|
260
|
+
const targetBinary = path.join(path.resolve(targetApp), "Contents/MacOS/Codex");
|
|
261
|
+
const userDataArg = `--user-data-dir=${path.resolve(electronUserDataPath)}`;
|
|
262
|
+
let text;
|
|
263
|
+
try {
|
|
264
|
+
text = execFileSync("ps", ["-axo", "pid=,command="], {
|
|
265
|
+
encoding: "utf8",
|
|
266
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
267
|
+
});
|
|
268
|
+
} catch {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
return text
|
|
272
|
+
.split("\n")
|
|
273
|
+
.map((line) => {
|
|
274
|
+
const match = line.match(/^\s*(\d+)\s+(.*)$/);
|
|
275
|
+
if (!match) return null;
|
|
276
|
+
const command = match[2];
|
|
277
|
+
if (!command.startsWith(targetBinary) || !command.includes(userDataArg)) return null;
|
|
278
|
+
const portMatch = command.match(/--remote-debugging-port=(\d+)/);
|
|
279
|
+
return {
|
|
280
|
+
pid: Number(match[1]),
|
|
281
|
+
command,
|
|
282
|
+
remoteDebuggingPort: portMatch ? Number(portMatch[1]) : null,
|
|
283
|
+
};
|
|
284
|
+
})
|
|
285
|
+
.filter(Boolean);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
class AuditPreflightError extends Error {
|
|
289
|
+
constructor(message, details = {}) {
|
|
290
|
+
super(message);
|
|
291
|
+
this.name = "AuditPreflightError";
|
|
292
|
+
this.details = details;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function auditPreflight(args, {
|
|
297
|
+
findPort = findFreePort,
|
|
298
|
+
findRendererTarget = findRendererTargetOnPort,
|
|
299
|
+
listRunningApps = listRunningAuditApps,
|
|
300
|
+
} = {}) {
|
|
301
|
+
const requestedPort = args.remoteDebuggingPort;
|
|
302
|
+
const existingTarget = await findRendererTarget(requestedPort);
|
|
303
|
+
const runningApps = listRunningApps({
|
|
304
|
+
targetApp: args.target,
|
|
305
|
+
electronUserDataPath: args.electronUserDataPath,
|
|
306
|
+
});
|
|
307
|
+
const livePorts = Array.from(new Set(runningApps
|
|
308
|
+
.map((app) => app.remoteDebuggingPort)
|
|
309
|
+
.filter((port) => port != null)));
|
|
310
|
+
const livePort = existingTarget ? requestedPort : livePorts[0] ?? null;
|
|
311
|
+
const existingApp = runningApps[0] || null;
|
|
312
|
+
const suggestedCommand = livePort == null ? null : auditAttachCommand(livePort);
|
|
313
|
+
|
|
314
|
+
if (!args.launch) {
|
|
315
|
+
return {
|
|
316
|
+
port: requestedPort,
|
|
317
|
+
launch: false,
|
|
318
|
+
reuseExisting: Boolean(existingTarget || existingApp),
|
|
319
|
+
existingApp,
|
|
320
|
+
existingTarget,
|
|
321
|
+
livePort,
|
|
322
|
+
suggestedCommand,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (existingTarget || existingApp) {
|
|
327
|
+
if (!args.apply) {
|
|
328
|
+
if (livePort == null) {
|
|
329
|
+
throw new AuditPreflightError(
|
|
330
|
+
"Codex Plus is already running for this audit target, but no remote debugging port was detected",
|
|
331
|
+
{ existingApp, livePort, suggestedCommand },
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
port: livePort,
|
|
336
|
+
launch: false,
|
|
337
|
+
reuseExisting: true,
|
|
338
|
+
existingApp,
|
|
339
|
+
existingTarget,
|
|
340
|
+
livePort,
|
|
341
|
+
suggestedCommand,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
throw new AuditPreflightError(
|
|
345
|
+
`Codex Plus audit app is already running${livePort == null ? "" : ` on port ${livePort}`}; close it before applying patches, or rerun ${suggestedCommand}`,
|
|
346
|
+
{ existingApp, livePort, suggestedCommand },
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
port: await findPort(requestedPort),
|
|
352
|
+
launch: true,
|
|
353
|
+
reuseExisting: false,
|
|
354
|
+
existingApp: null,
|
|
355
|
+
existingTarget: null,
|
|
356
|
+
livePort: null,
|
|
357
|
+
suggestedCommand: null,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
class CdpSession {
|
|
362
|
+
constructor(webSocketDebuggerUrl) {
|
|
363
|
+
if (typeof WebSocket !== "function") {
|
|
364
|
+
throw new Error("This audit requires a Node.js runtime with global WebSocket support");
|
|
365
|
+
}
|
|
366
|
+
this.nextId = 1;
|
|
367
|
+
this.pending = new Map();
|
|
368
|
+
this.socket = new WebSocket(webSocketDebuggerUrl);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
connect() {
|
|
372
|
+
return new Promise((resolve, reject) => {
|
|
373
|
+
this.socket.addEventListener("open", resolve, { once: true });
|
|
374
|
+
this.socket.addEventListener("error", reject, { once: true });
|
|
375
|
+
this.socket.addEventListener("message", (event) => {
|
|
376
|
+
const message = JSON.parse(event.data);
|
|
377
|
+
if (!message.id) return;
|
|
378
|
+
const pending = this.pending.get(message.id);
|
|
379
|
+
if (!pending) return;
|
|
380
|
+
this.pending.delete(message.id);
|
|
381
|
+
if (message.error) pending.reject(new Error(message.error.message || JSON.stringify(message.error)));
|
|
382
|
+
else pending.resolve(message.result);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
send(method, params = {}) {
|
|
388
|
+
const id = this.nextId++;
|
|
389
|
+
this.socket.send(JSON.stringify({ id, method, params }));
|
|
390
|
+
return new Promise((resolve, reject) => {
|
|
391
|
+
this.pending.set(id, { resolve, reject });
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async evaluate(expression, { awaitPromise = true } = {}) {
|
|
396
|
+
const result = await this.send("Runtime.evaluate", {
|
|
397
|
+
expression,
|
|
398
|
+
awaitPromise,
|
|
399
|
+
returnByValue: true,
|
|
400
|
+
userGesture: true,
|
|
401
|
+
});
|
|
402
|
+
if (result.exceptionDetails) {
|
|
403
|
+
const details = result.exceptionDetails;
|
|
404
|
+
throw new Error(details.exception?.description || details.text || "Runtime.evaluate failed");
|
|
405
|
+
}
|
|
406
|
+
return result.result?.value;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async close() {
|
|
410
|
+
try {
|
|
411
|
+
this.socket.close();
|
|
412
|
+
} catch {
|
|
413
|
+
// Nothing useful to do while finishing JSON output.
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async function waitForLiveRuntime(cdp, timeoutMs = 90000) {
|
|
419
|
+
const deadline = Date.now() + timeoutMs;
|
|
420
|
+
let lastStatus = null;
|
|
421
|
+
while (Date.now() < deadline) {
|
|
422
|
+
lastStatus = await cdp.evaluate(`(() => {
|
|
423
|
+
const plugins = window.CodexPlus?.plugins;
|
|
424
|
+
const hasList = typeof plugins?.list === "function";
|
|
425
|
+
return {
|
|
426
|
+
readyState: document.readyState,
|
|
427
|
+
hasCodexPlus: Boolean(window.CodexPlus),
|
|
428
|
+
hasPluginList: hasList,
|
|
429
|
+
registered: hasList ? plugins.list().length : null,
|
|
430
|
+
started: window.__CodexPlusRuntime?.core?.startedPlugins?.size ?? null,
|
|
431
|
+
};
|
|
432
|
+
})()`);
|
|
433
|
+
if (lastStatus.hasPluginList && lastStatus.registered >= 10 && lastStatus.started >= 10) return lastStatus;
|
|
434
|
+
if (lastStatus.readyState === "complete" && lastStatus.hasCodexPlus && !lastStatus.hasPluginList) return lastStatus;
|
|
435
|
+
await delay(250);
|
|
436
|
+
}
|
|
437
|
+
throw new Error(`Timed out waiting for Codex Plus runtime plugins: ${JSON.stringify(lastStatus)}`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function waitForAppShellMounted(cdp, timeoutMs = 90000) {
|
|
441
|
+
const deadline = Date.now() + timeoutMs;
|
|
442
|
+
let lastStatus = null;
|
|
443
|
+
while (Date.now() < deadline) {
|
|
444
|
+
lastStatus = await cdp.evaluate(`(() => {
|
|
445
|
+
const root = document.getElementById("root");
|
|
446
|
+
const bodyText = document.body?.innerText?.trim() ?? "";
|
|
447
|
+
const interactiveCount = document.querySelectorAll("button,a,nav,[role=navigation]").length;
|
|
448
|
+
const hasErrorBoundary = /^Oops, an error has occurred\\b/.test(bodyText);
|
|
449
|
+
return {
|
|
450
|
+
readyState: document.readyState,
|
|
451
|
+
hasRoot: Boolean(root),
|
|
452
|
+
hasStartupLoader: Boolean(document.querySelector("#root .startup-loader")),
|
|
453
|
+
hasErrorBoundary,
|
|
454
|
+
bodyTextLength: bodyText.length,
|
|
455
|
+
elementCount: document.querySelectorAll("*").length,
|
|
456
|
+
interactiveCount,
|
|
457
|
+
sampleText: bodyText.slice(0, 120),
|
|
458
|
+
};
|
|
459
|
+
})()`);
|
|
460
|
+
if (lastStatus.hasErrorBoundary) {
|
|
461
|
+
throw new Error(`Codex app shell rendered error boundary: ${JSON.stringify(lastStatus)}`);
|
|
462
|
+
}
|
|
463
|
+
if (
|
|
464
|
+
lastStatus.readyState === "complete" &&
|
|
465
|
+
lastStatus.hasRoot &&
|
|
466
|
+
!lastStatus.hasStartupLoader &&
|
|
467
|
+
lastStatus.bodyTextLength > 0 &&
|
|
468
|
+
lastStatus.interactiveCount > 0
|
|
469
|
+
) {
|
|
470
|
+
return lastStatus;
|
|
471
|
+
}
|
|
472
|
+
await delay(250);
|
|
473
|
+
}
|
|
474
|
+
throw new Error(`Timed out waiting for Codex app shell to mount: ${JSON.stringify(lastStatus)}`);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function failedPlugins(result) {
|
|
478
|
+
return Array.from(new Set((result.failures || [])
|
|
479
|
+
.map((failure) => failure.plugin)
|
|
480
|
+
.filter((plugin) => plugin && plugin !== "audit")));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function failedPatches(result) {
|
|
484
|
+
return Array.from(new Set((result.failures || [])
|
|
485
|
+
.flatMap((failure) => [
|
|
486
|
+
failure.patch,
|
|
487
|
+
failure.patchId,
|
|
488
|
+
failure.details?.patch,
|
|
489
|
+
failure.details?.patchId,
|
|
490
|
+
])
|
|
491
|
+
.filter(Boolean)));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function formatAuditJson(result) {
|
|
495
|
+
return `${JSON.stringify(result, null, 2)}\n`;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function formatAuditResult(result, { quiet = false } = {}) {
|
|
499
|
+
const expectedWarnings = result.expectedWarnings || [];
|
|
500
|
+
if (quiet) {
|
|
501
|
+
if (!result.ok) return `Plugin audit failed: ${result.failures.length} failures\n`;
|
|
502
|
+
return expectedWarnings.length > 0
|
|
503
|
+
? "All plugin probes passed with expected warnings.\n"
|
|
504
|
+
: "All plugin probes passed.\n";
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (!result.ok) {
|
|
508
|
+
const plugins = failedPlugins(result);
|
|
509
|
+
const patches = failedPatches(result);
|
|
510
|
+
const lines = [
|
|
511
|
+
`Plugin audit failed: ${result.failures.length} failures`,
|
|
512
|
+
"",
|
|
513
|
+
];
|
|
514
|
+
if (plugins.length > 0) lines.push(`Failed plugins: ${plugins.join(", ")}`);
|
|
515
|
+
if (patches.length > 0) lines.push(`Failed patches: ${patches.join(", ")}`);
|
|
516
|
+
if (plugins.length > 0 || patches.length > 0) lines.push("");
|
|
517
|
+
for (const failure of result.failures) {
|
|
518
|
+
lines.push(`${failure.plugin || "audit"}`);
|
|
519
|
+
lines.push(` ${failure.message || "probe failed"}`);
|
|
520
|
+
if (failure.patch || failure.patchId || failure.details?.patch || failure.details?.patchId) {
|
|
521
|
+
lines.push(` patch: ${failure.patch || failure.patchId || failure.details?.patch || failure.details?.patchId}`);
|
|
522
|
+
}
|
|
523
|
+
if (Array.isArray(failure.details?.crashDumps) && failure.details.crashDumps.length > 0) {
|
|
524
|
+
lines.push(` crash dumps: ${failure.details.crashDumps.join(", ")}`);
|
|
525
|
+
}
|
|
526
|
+
if (failure.details?.livePort != null) {
|
|
527
|
+
lines.push(` live port: ${failure.details.livePort}`);
|
|
528
|
+
}
|
|
529
|
+
if (failure.details?.suggestedCommand) {
|
|
530
|
+
lines.push(` suggested command: ${failure.details.suggestedCommand}`);
|
|
531
|
+
}
|
|
532
|
+
lines.push("");
|
|
533
|
+
}
|
|
534
|
+
if (expectedWarnings.length > 0) {
|
|
535
|
+
lines.push("Expected warnings:");
|
|
536
|
+
for (const warning of expectedWarnings) {
|
|
537
|
+
lines.push(` ${warning.plugin || "audit"} ${warning.code || "warning"}: ${warning.message || "expected warning"}`);
|
|
538
|
+
}
|
|
539
|
+
lines.push("");
|
|
540
|
+
}
|
|
541
|
+
lines.push("Re-run with --json for full probe details.");
|
|
542
|
+
return `${lines.join("\n").replace(/\n{3,}/g, "\n\n")}\n`;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const probeCount = Object.keys(result.pluginResults || {}).length;
|
|
546
|
+
const runtime = result.runtimeStatus || {};
|
|
547
|
+
const appShell = result.appShellStatus || {};
|
|
548
|
+
const cleanup = result.cleanupResult;
|
|
549
|
+
const cleanupText = cleanup?.keptOpen
|
|
550
|
+
? "kept open"
|
|
551
|
+
: cleanup?.attempted
|
|
552
|
+
? cleanup.ok ? "cleaned up" : `cleanup failed: ${cleanup.message}`
|
|
553
|
+
: "not launched";
|
|
554
|
+
const lines = [
|
|
555
|
+
"Audit Codex Plus plugins",
|
|
556
|
+
`Source: ${result.applyResult?.sourceApp || result.source || DEFAULT_SOURCE}`,
|
|
557
|
+
`Target: ${result.target?.app || DEFAULT_TARGET}`,
|
|
558
|
+
];
|
|
559
|
+
if (result.applyResult?.patchSet) lines.push(`Patch set: ${result.applyResult.patchSet}`);
|
|
560
|
+
lines.push(
|
|
561
|
+
"",
|
|
562
|
+
`Port: ${result.target?.remoteDebuggingPort ?? "unknown"}`,
|
|
563
|
+
result.preflight?.reuseExisting ? "Launch: reused existing app" : "Launch: audit-launched app",
|
|
564
|
+
`Runtime ready: ${runtime.registered ?? result.registeredPlugins?.length ?? 0} registered, ${runtime.started ?? result.startedPlugins?.length ?? 0} started`,
|
|
565
|
+
`App shell: ${appShell.hasStartupLoader === false ? "mounted" : "unknown"}`,
|
|
566
|
+
`Probed ${probeCount} plugins`,
|
|
567
|
+
`Warnings: ${expectedWarnings.length} expected`,
|
|
568
|
+
`Native open probes: ${result.nativeOpenProbes?.included ? "included" : "skipped"}`,
|
|
569
|
+
`Cleanup: ${cleanupText}`,
|
|
570
|
+
"",
|
|
571
|
+
"All plugin probes passed.",
|
|
572
|
+
);
|
|
573
|
+
if (expectedWarnings.length > 0) {
|
|
574
|
+
lines.push("", "Expected warnings:");
|
|
575
|
+
for (const warning of expectedWarnings) {
|
|
576
|
+
lines.push(`${warning.plugin || "audit"} ${warning.code || "warning"}: ${warning.message || "expected warning"}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return `${lines.join("\n")}\n`;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function shouldShowAuditProgress(args, stream = process.stdout) {
|
|
583
|
+
return !args.json && !args.quiet && !args.noProgress && stream != null;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function timestamp(date = new Date()) {
|
|
587
|
+
return date.toISOString();
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async function createAuditProgress(args, {
|
|
591
|
+
stream = process.stdout,
|
|
592
|
+
importOra = (specifier) => import(specifier),
|
|
593
|
+
now = () => new Date(),
|
|
594
|
+
} = {}) {
|
|
595
|
+
if (!shouldShowAuditProgress(args, stream)) return null;
|
|
596
|
+
if (stream.isTTY) {
|
|
597
|
+
const { default: ora } = await importOra("ora");
|
|
598
|
+
const spinner = ora({ color: "cyan", spinner: "dots", stream });
|
|
599
|
+
let active = false;
|
|
600
|
+
return {
|
|
601
|
+
start(text) {
|
|
602
|
+
if (active) spinner.succeed();
|
|
603
|
+
spinner.text = text;
|
|
604
|
+
spinner.start();
|
|
605
|
+
active = true;
|
|
606
|
+
},
|
|
607
|
+
succeed(text) {
|
|
608
|
+
if (!active) return;
|
|
609
|
+
spinner.succeed(text);
|
|
610
|
+
active = false;
|
|
611
|
+
},
|
|
612
|
+
fail(text) {
|
|
613
|
+
if (!active) return;
|
|
614
|
+
spinner.fail(text);
|
|
615
|
+
active = false;
|
|
616
|
+
},
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
start(text) {
|
|
621
|
+
stream.write(`[${timestamp(now())}] ${text}\n`);
|
|
622
|
+
},
|
|
623
|
+
succeed(text) {
|
|
624
|
+
stream.write(`[${timestamp(now())}] OK ${text}\n`);
|
|
625
|
+
},
|
|
626
|
+
fail(text) {
|
|
627
|
+
stream.write(`[${timestamp(now())}] FAIL ${text}\n`);
|
|
628
|
+
},
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function progressStart(progress, text) {
|
|
633
|
+
progress?.start?.(text);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function progressSucceed(progress, text) {
|
|
637
|
+
progress?.succeed?.(text);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function progressFail(progress, text) {
|
|
641
|
+
progress?.fail?.(text);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async function withAuditProgress(progress, startText, doneText, action) {
|
|
645
|
+
progressStart(progress, startText);
|
|
646
|
+
try {
|
|
647
|
+
const result = await action();
|
|
648
|
+
progressSucceed(progress, doneText);
|
|
649
|
+
return result;
|
|
650
|
+
} catch (error) {
|
|
651
|
+
progressFail(progress, startText);
|
|
652
|
+
throw error;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
async function cleanupLaunchedAuditApp(launchResult, {
|
|
657
|
+
keepOpen = false,
|
|
658
|
+
kill = process.kill,
|
|
659
|
+
wait = delay,
|
|
660
|
+
} = {}) {
|
|
661
|
+
const pid = launchResult?.pid;
|
|
662
|
+
if (keepOpen) return { attempted: false, keptOpen: true, ok: true, pid };
|
|
663
|
+
if (pid == null) return { attempted: false, keptOpen: false, ok: true, pid: null };
|
|
664
|
+
const signals = ["SIGTERM", "SIGKILL"];
|
|
665
|
+
for (const signal of signals) {
|
|
666
|
+
try {
|
|
667
|
+
kill(-pid, signal);
|
|
668
|
+
} catch (groupError) {
|
|
669
|
+
try {
|
|
670
|
+
kill(pid, signal);
|
|
671
|
+
} catch (processError) {
|
|
672
|
+
if (processError.code === "ESRCH") return { attempted: true, keptOpen: false, ok: true, pid };
|
|
673
|
+
if (signal === "SIGKILL") {
|
|
674
|
+
return {
|
|
675
|
+
attempted: true,
|
|
676
|
+
keptOpen: false,
|
|
677
|
+
ok: false,
|
|
678
|
+
pid,
|
|
679
|
+
message: processError.message || groupError.message,
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
await wait(signal === "SIGTERM" ? 500 : 0);
|
|
685
|
+
}
|
|
686
|
+
return { attempted: true, keptOpen: false, ok: true, pid };
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function processIsAlive(pid, { kill = process.kill } = {}) {
|
|
690
|
+
if (pid == null) return false;
|
|
691
|
+
try {
|
|
692
|
+
kill(pid, 0);
|
|
693
|
+
return true;
|
|
694
|
+
} catch (error) {
|
|
695
|
+
return error?.code === "EPERM";
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function listCrashpadPendingDumps(electronUserDataPath, { readdirSync = fs.readdirSync } = {}) {
|
|
700
|
+
const pendingDir = path.join(electronUserDataPath, "Crashpad", "pending");
|
|
701
|
+
try {
|
|
702
|
+
return readdirSync(pendingDir, { withFileTypes: true })
|
|
703
|
+
.filter((entry) => entry.isFile() && (entry.name.endsWith(".dmp") || entry.name.endsWith("_sidecar.json")))
|
|
704
|
+
.map((entry) => path.join(pendingDir, entry.name))
|
|
705
|
+
.sort();
|
|
706
|
+
} catch {
|
|
707
|
+
return [];
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async function checkKeepOpenAppStability(launchResult, {
|
|
712
|
+
electronUserDataPath,
|
|
713
|
+
wait = delay,
|
|
714
|
+
isAlive = processIsAlive,
|
|
715
|
+
listCrashDumps = listCrashpadPendingDumps,
|
|
716
|
+
waitMs = 15000,
|
|
717
|
+
intervalMs = 500,
|
|
718
|
+
} = {}) {
|
|
719
|
+
const pid = launchResult?.pid ?? null;
|
|
720
|
+
if (pid == null) {
|
|
721
|
+
return {
|
|
722
|
+
checked: false,
|
|
723
|
+
ok: true,
|
|
724
|
+
pid,
|
|
725
|
+
alive: null,
|
|
726
|
+
crashDumps: [],
|
|
727
|
+
message: "No audit-launched app process to check",
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
const deadline = Date.now() + waitMs;
|
|
731
|
+
let alive = isAlive(pid);
|
|
732
|
+
while (alive && Date.now() < deadline) {
|
|
733
|
+
await wait(Math.min(intervalMs, Math.max(0, deadline - Date.now())));
|
|
734
|
+
alive = isAlive(pid);
|
|
735
|
+
}
|
|
736
|
+
const crashDumps = electronUserDataPath ? listCrashDumps(electronUserDataPath) : [];
|
|
737
|
+
return {
|
|
738
|
+
checked: true,
|
|
739
|
+
ok: alive,
|
|
740
|
+
pid,
|
|
741
|
+
alive,
|
|
742
|
+
crashDumps,
|
|
743
|
+
message: alive ? "Audit-launched app is still running" : "Audit-launched app exited after probes",
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function appendFailure(result, failure) {
|
|
748
|
+
result.failures = [...(result.failures || []), failure];
|
|
749
|
+
result.ok = false;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function pluginAuditExpression({ includeNativeOpenProbes = false } = {}) {
|
|
753
|
+
const options = JSON.stringify({ includeNativeOpenProbes });
|
|
754
|
+
return `(${async function runPluginAudit(options) {
|
|
755
|
+
const requiredPlugins = [
|
|
756
|
+
"aboutMetadata",
|
|
757
|
+
"nestedRepositories",
|
|
758
|
+
"diagnosticErrors",
|
|
759
|
+
"userBubbleColors",
|
|
760
|
+
"projectColors",
|
|
761
|
+
"projectPathHeader",
|
|
762
|
+
"sidebarNameBlur",
|
|
763
|
+
"devTools",
|
|
764
|
+
"projectSelectorShortcut",
|
|
765
|
+
"mermaidFullscreen",
|
|
766
|
+
];
|
|
767
|
+
const pluginResults = {};
|
|
768
|
+
const failures = [];
|
|
769
|
+
const expectedWarnings = [];
|
|
770
|
+
const add = (id, ok, details = {}) => {
|
|
771
|
+
pluginResults[id] = { ok, ...details };
|
|
772
|
+
if (!ok) failures.push({ plugin: id, message: details.message || "probe failed", details });
|
|
773
|
+
};
|
|
774
|
+
const fail = (id, error, details = {}) => add(id, false, { message: error?.message || String(error), ...details });
|
|
775
|
+
const pass = (id, details = {}) => add(id, true, details);
|
|
776
|
+
const warn = (id, code, message, details = {}) => {
|
|
777
|
+
expectedWarnings.push({ plugin: id, code, message, details });
|
|
778
|
+
};
|
|
779
|
+
const pluginIds = () => {
|
|
780
|
+
if (typeof window.CodexPlus?.plugins?.list !== "function") {
|
|
781
|
+
throw new Error("CodexPlus.plugins.list is not available");
|
|
782
|
+
}
|
|
783
|
+
return window.CodexPlus.plugins.list().map((plugin) => plugin.id);
|
|
784
|
+
};
|
|
785
|
+
const started = () => Array.from(window.__CodexPlusRuntime?.core?.startedPlugins || []);
|
|
786
|
+
const common = (id) => ({
|
|
787
|
+
registered: pluginIds().includes(id),
|
|
788
|
+
started: started().includes(id),
|
|
789
|
+
});
|
|
790
|
+
const checkCommon = (id) => {
|
|
791
|
+
const details = common(id);
|
|
792
|
+
if (!details.registered || !details.started) throw new Error(`${id} is not registered and started`);
|
|
793
|
+
return details;
|
|
794
|
+
};
|
|
795
|
+
const rendererAssetUrls = async () => {
|
|
796
|
+
const roots = Array.from(new Set([
|
|
797
|
+
...Array.from(document.scripts).map((script) => script.src),
|
|
798
|
+
...performance.getEntriesByType("resource").map((entry) => entry.name),
|
|
799
|
+
])).filter((url) => typeof url === "string" && url.startsWith("app://-/") && url.endsWith(".js"));
|
|
800
|
+
const urls = new Set(roots);
|
|
801
|
+
for (const url of roots.filter((candidate) => candidate.includes("/assets/index-"))) {
|
|
802
|
+
try {
|
|
803
|
+
const text = await fetch(url).then((response) => response.text());
|
|
804
|
+
for (const match of text.matchAll(/["'](\.\/[^"']+\.js)["']/g)) {
|
|
805
|
+
urls.add(new URL(match[1], url).href);
|
|
806
|
+
}
|
|
807
|
+
} catch {
|
|
808
|
+
// Missing source readback is handled by the caller's evidence checks.
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return Array.from(urls);
|
|
812
|
+
};
|
|
813
|
+
const rendererSourceEvidence = async (needles) => {
|
|
814
|
+
const urls = await rendererAssetUrls();
|
|
815
|
+
const evidence = Object.fromEntries(needles.map((needle) => [needle, null]));
|
|
816
|
+
for (const url of urls) {
|
|
817
|
+
if (!url.includes("/assets/") || url.includes("/assets/codex-plus/")) continue;
|
|
818
|
+
let text = "";
|
|
819
|
+
try {
|
|
820
|
+
text = await fetch(url).then((response) => response.text());
|
|
821
|
+
} catch {
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
for (const needle of needles) {
|
|
825
|
+
if (evidence[needle] == null && text.includes(needle)) evidence[needle] = url;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
return {
|
|
829
|
+
urlsChecked: urls.length,
|
|
830
|
+
evidence,
|
|
831
|
+
};
|
|
832
|
+
};
|
|
833
|
+
const waitForProjectThreadRows = async (timeoutMs = 45000) => {
|
|
834
|
+
const startedAt = Date.now();
|
|
835
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
836
|
+
const rows = document.querySelectorAll("[data-app-action-sidebar-project-list-id] [data-app-action-sidebar-thread-row]");
|
|
837
|
+
if (rows.length > 0) return rows.length;
|
|
838
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
839
|
+
}
|
|
840
|
+
return 0;
|
|
841
|
+
};
|
|
842
|
+
const isTransparentColor = (value) => value === "rgba(0, 0, 0, 0)" || value === "transparent";
|
|
843
|
+
const waitForMountedProjectComposer = async (expectedAccent, timeoutMs = 20000) => {
|
|
844
|
+
const startedAt = Date.now();
|
|
845
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
846
|
+
const editor = document.querySelector("[data-codex-composer]");
|
|
847
|
+
const surface = editor?.closest("[data-codex-plus-user-entry]") || editor?.closest(".composer-surface-chrome");
|
|
848
|
+
if (surface) {
|
|
849
|
+
const computed = getComputedStyle(surface);
|
|
850
|
+
const surfaceAccent = computed.getPropertyValue("--codex-plus-project-accent").trim();
|
|
851
|
+
if (
|
|
852
|
+
surface.hasAttribute("data-codex-plus-user-entry") &&
|
|
853
|
+
surface.hasAttribute("data-codex-plus-project-color") &&
|
|
854
|
+
surfaceAccent === expectedAccent &&
|
|
855
|
+
computed.boxShadow !== "none"
|
|
856
|
+
) {
|
|
857
|
+
return {
|
|
858
|
+
marked: true,
|
|
859
|
+
projectMarked: true,
|
|
860
|
+
accent: surfaceAccent,
|
|
861
|
+
boxShadow: computed.boxShadow,
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
866
|
+
}
|
|
867
|
+
const editor = document.querySelector("[data-codex-composer]");
|
|
868
|
+
const surface = editor?.closest("[data-codex-plus-user-entry]") || editor?.closest(".composer-surface-chrome");
|
|
869
|
+
const computed = surface ? getComputedStyle(surface) : null;
|
|
870
|
+
return {
|
|
871
|
+
marked: surface?.hasAttribute("data-codex-plus-user-entry") || false,
|
|
872
|
+
projectMarked: surface?.hasAttribute("data-codex-plus-project-color") || false,
|
|
873
|
+
accent: computed?.getPropertyValue("--codex-plus-project-accent").trim() || "",
|
|
874
|
+
boxShadow: computed?.boxShadow || "",
|
|
875
|
+
};
|
|
876
|
+
};
|
|
877
|
+
const composerPermissionPickerStatus = () => {
|
|
878
|
+
const editor = document.querySelector("[data-codex-composer]");
|
|
879
|
+
const labels = ["Full access", "Ask for approval", "Approve for me", "Custom"];
|
|
880
|
+
const normalize = (value) => String(value || "").replace(/\s+/g, " ").trim();
|
|
881
|
+
const rgb = (value) => {
|
|
882
|
+
const match = String(value || "").match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
883
|
+
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
|
|
884
|
+
};
|
|
885
|
+
const luminance = (color) => {
|
|
886
|
+
if (!color) return null;
|
|
887
|
+
const channel = (value) => {
|
|
888
|
+
const normalized = value / 255;
|
|
889
|
+
return normalized <= 0.03928 ? normalized / 12.92 : Math.pow((normalized + 0.055) / 1.055, 2.4);
|
|
890
|
+
};
|
|
891
|
+
return 0.2126 * channel(color[0]) + 0.7152 * channel(color[1]) + 0.0722 * channel(color[2]);
|
|
892
|
+
};
|
|
893
|
+
const contrast = (foreground, background) => {
|
|
894
|
+
const fg = luminance(rgb(foreground));
|
|
895
|
+
const bg = luminance(rgb(background));
|
|
896
|
+
if (fg == null || bg == null) return null;
|
|
897
|
+
const lighter = Math.max(fg, bg);
|
|
898
|
+
const darker = Math.min(fg, bg);
|
|
899
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
900
|
+
};
|
|
901
|
+
const isTransparent = (value) => {
|
|
902
|
+
const text = String(value || "").trim();
|
|
903
|
+
return text === "transparent" || text === "rgba(0, 0, 0, 0)" || /rgba\([^)]*,\s*0\)$/.test(text);
|
|
904
|
+
};
|
|
905
|
+
const trigger = Array.from(document.querySelectorAll("button")).find((button) => {
|
|
906
|
+
const text = normalize(button.textContent);
|
|
907
|
+
return labels.some((label) => text === label || text.startsWith(`${label} `));
|
|
908
|
+
});
|
|
909
|
+
const triggerStyle = trigger ? getComputedStyle(trigger) : null;
|
|
910
|
+
const surface = editor?.closest("[data-codex-plus-user-entry]");
|
|
911
|
+
const surfaceStyle = surface ? getComputedStyle(surface) : null;
|
|
912
|
+
let labelStyle = null;
|
|
913
|
+
if (trigger) {
|
|
914
|
+
const walker = document.createTreeWalker(trigger, NodeFilter.SHOW_TEXT);
|
|
915
|
+
while (walker.nextNode()) {
|
|
916
|
+
const node = walker.currentNode;
|
|
917
|
+
if (normalize(node.nodeValue) !== "") {
|
|
918
|
+
labelStyle = getComputedStyle(node.parentElement);
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
const triggerColor = triggerStyle?.color || null;
|
|
924
|
+
const labelColor = labelStyle?.color || triggerColor;
|
|
925
|
+
const labelTextFillColor = labelStyle?.webkitTextFillColor || triggerStyle?.webkitTextFillColor || null;
|
|
926
|
+
const effectiveLabelColor = labelTextFillColor && !isTransparent(labelTextFillColor) ? labelTextFillColor : labelColor;
|
|
927
|
+
const surfaceBackground = surfaceStyle?.backgroundColor || null;
|
|
928
|
+
return {
|
|
929
|
+
editorMounted: Boolean(editor),
|
|
930
|
+
editorEditable: editor?.getAttribute("contenteditable") === "true",
|
|
931
|
+
editorText: normalize(editor?.textContent),
|
|
932
|
+
triggerMounted: Boolean(trigger),
|
|
933
|
+
triggerText: normalize(trigger?.textContent),
|
|
934
|
+
triggerDisabled: Boolean(trigger?.disabled),
|
|
935
|
+
triggerAriaDisabled: trigger?.getAttribute("aria-disabled") || null,
|
|
936
|
+
triggerState: trigger?.getAttribute("data-state") || null,
|
|
937
|
+
triggerOpacity: triggerStyle?.opacity || null,
|
|
938
|
+
triggerColor,
|
|
939
|
+
labelColor,
|
|
940
|
+
labelTextFillColor,
|
|
941
|
+
surfaceBackground,
|
|
942
|
+
triggerContrast: contrast(effectiveLabelColor, surfaceBackground),
|
|
943
|
+
labelTextFillTransparent: isTransparent(labelTextFillColor),
|
|
944
|
+
triggerClassName: String(trigger?.className || ""),
|
|
945
|
+
};
|
|
946
|
+
};
|
|
947
|
+
const jsx = (type, props, key) => ({ type, props: props || {}, key });
|
|
948
|
+
const jsxs = jsx;
|
|
949
|
+
const reviewDeps = {
|
|
950
|
+
jsx,
|
|
951
|
+
jsxs,
|
|
952
|
+
Fragment: "fragment",
|
|
953
|
+
createElement: (type, props, ...children) => ({ type, props: { ...(props || {}), children } }),
|
|
954
|
+
React: {
|
|
955
|
+
createElement: (type, props, ...children) => ({ type, props: { ...(props || {}), children } }),
|
|
956
|
+
useState(initial) { return [typeof initial === "function" ? initial() : initial, () => {}]; },
|
|
957
|
+
useMemo(fn) { return fn(); },
|
|
958
|
+
useEffect() {},
|
|
959
|
+
},
|
|
960
|
+
useStore() { return { value: { routeKind: "local-thread", conversationId: "audit-conversation" } }; },
|
|
961
|
+
useAtom(atom) { return atom?.auditValue; },
|
|
962
|
+
routeAtom: { auditValue: { routeKind: "local-thread", conversationId: "audit-conversation" } },
|
|
963
|
+
cwdAtom: { auditValue: "/tmp/codex-plus-audit" },
|
|
964
|
+
hostIdAtom: { auditValue: "local" },
|
|
965
|
+
hostConfigAtom: { auditValue: { id: "local", label: "Local" } },
|
|
966
|
+
conversationIdAtom: { auditValue: "audit-conversation" },
|
|
967
|
+
gitRequest: { request() { return Promise.resolve({ main: null, repositories: [] }); } },
|
|
968
|
+
pathValue(value) { return value; },
|
|
969
|
+
DefaultReview: "default-review",
|
|
970
|
+
Button: "button",
|
|
971
|
+
Tooltip: "tooltip",
|
|
972
|
+
Icon: "icon",
|
|
973
|
+
Dropdown: "dropdown",
|
|
974
|
+
DropdownMenu: "dropdown-menu",
|
|
975
|
+
BranchPickerDropdownContent: "branch-picker",
|
|
976
|
+
ReviewToolbar: "review-toolbar",
|
|
977
|
+
parseDiff() { return []; },
|
|
978
|
+
DiffCard: "diff-card",
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
try {
|
|
982
|
+
const details = checkCommon("aboutMetadata");
|
|
983
|
+
const providerOutput = window.CodexPlus.ui.about.buildInfo.map((provider) => provider());
|
|
984
|
+
const provenance = providerOutput.some((entry) =>
|
|
985
|
+
entry?.lines?.includes("Codex Plus runtime plugin layer active") &&
|
|
986
|
+
entry?.lines?.includes("Plugin: aboutMetadata"));
|
|
987
|
+
if (!provenance) throw new Error("About build info lacks Codex Plus provenance");
|
|
988
|
+
pass("aboutMetadata", { ...details, provenance });
|
|
989
|
+
} catch (error) {
|
|
990
|
+
fail("aboutMetadata", error);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
try {
|
|
994
|
+
const details = checkCommon("nestedRepositories");
|
|
995
|
+
let nestedStateCalls = 0;
|
|
996
|
+
const nestedReviewDeps = {
|
|
997
|
+
...reviewDeps,
|
|
998
|
+
React: {
|
|
999
|
+
...reviewDeps.React,
|
|
1000
|
+
useState(initial) {
|
|
1001
|
+
nestedStateCalls += 1;
|
|
1002
|
+
if (nestedStateCalls === 1) {
|
|
1003
|
+
return [{
|
|
1004
|
+
main: { id: "main:/tmp/codex-plus-audit", kind: "main", path: ".", label: "Main", cwd: "/tmp/codex-plus-audit" },
|
|
1005
|
+
repositories: [{ id: "repo:pkg", kind: "nested", path: "pkg", label: "pkg", cwd: "/tmp/codex-plus-audit/pkg" }],
|
|
1006
|
+
warnings: [],
|
|
1007
|
+
}, () => {}];
|
|
1008
|
+
}
|
|
1009
|
+
return [typeof initial === "function" ? initial() : initial, () => {}];
|
|
1010
|
+
},
|
|
1011
|
+
},
|
|
1012
|
+
};
|
|
1013
|
+
const wrapped = window.CodexPlus.ui.review.renderBody({ defaultBody: "body", props: {}, deps: nestedReviewDeps });
|
|
1014
|
+
const hostModuleRegistered = window.__CodexPlusRuntime.core.hostModules.has("codex-plus:native:repository-targets");
|
|
1015
|
+
if (wrapped === "body") throw new Error("Review body was not wrapped");
|
|
1016
|
+
if (!hostModuleRegistered) throw new Error("Repository-target host module is not registered");
|
|
1017
|
+
pass("nestedRepositories", { ...details, hostModuleRegistered, reviewWrapped: true });
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
fail("nestedRepositories", error);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
try {
|
|
1023
|
+
const details = checkCommon("diagnosticErrors");
|
|
1024
|
+
const rendered = window.CodexPlus.ui.errors.renderDetails({ jsx, error: new Error("boom") });
|
|
1025
|
+
const renderedDiagnostic = rendered?.type === "pre" && String(rendered?.props?.children || "").includes("boom");
|
|
1026
|
+
if (!renderedDiagnostic) throw new Error("Diagnostic error details did not render");
|
|
1027
|
+
pass("diagnosticErrors", { ...details, renderedDiagnostic });
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
fail("diagnosticErrors", error);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
try {
|
|
1033
|
+
const details = checkCommon("userBubbleColors");
|
|
1034
|
+
const bubbleProps = window.CodexPlus.ui.message.userBubbleProps({});
|
|
1035
|
+
const composerProps = window.CodexPlus.ui.composer.surfaceProps({});
|
|
1036
|
+
const bubbleMarked = Object.prototype.hasOwnProperty.call(bubbleProps || {}, "data-codex-plus-user-bubble");
|
|
1037
|
+
const composerMarked = Object.prototype.hasOwnProperty.call(composerProps || {}, "data-codex-plus-user-entry");
|
|
1038
|
+
if (!bubbleMarked || !composerMarked) throw new Error("User bubble or composer marker is missing");
|
|
1039
|
+
pass("userBubbleColors", { ...details, bubbleMarked, composerMarked });
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
fail("userBubbleColors", error);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
try {
|
|
1045
|
+
const details = checkCommon("projectColors");
|
|
1046
|
+
const sampleProject = {
|
|
1047
|
+
projectId: "hassio-dev",
|
|
1048
|
+
label: "hassio-dev",
|
|
1049
|
+
path: "/tmp/hassio-dev",
|
|
1050
|
+
repositoryData: { rootFolder: "hassio-dev" },
|
|
1051
|
+
};
|
|
1052
|
+
const projectProps = window.CodexPlus.ui.sidebar.projectRowProps({ project: sampleProject });
|
|
1053
|
+
const threadProps = window.CodexPlus.ui.sidebar.threadRowProps({ project: sampleProject });
|
|
1054
|
+
const bubbleProps = window.CodexPlus.ui.message.userBubbleProps({ project: sampleProject });
|
|
1055
|
+
const composerProps = window.CodexPlus.ui.composer.surfaceProps({ project: sampleProject });
|
|
1056
|
+
const accent = projectProps?.style?.["--codex-plus-project-accent"];
|
|
1057
|
+
const matchingProps = [threadProps, bubbleProps, composerProps].every((props) =>
|
|
1058
|
+
props?.style?.["--codex-plus-project-accent"] === accent);
|
|
1059
|
+
const liveRows = Array.from(document.querySelectorAll("[data-codex-plus-project-color]"));
|
|
1060
|
+
const liveAccents = liveRows.map((row) => getComputedStyle(row).getPropertyValue("--codex-plus-project-accent").trim()).filter(Boolean);
|
|
1061
|
+
const projectThreadRowCount = await waitForProjectThreadRows();
|
|
1062
|
+
let selectedProjectAccent = "";
|
|
1063
|
+
let mountedComposer = null;
|
|
1064
|
+
const unstyledProjectThreadLists = Array.from(document.querySelectorAll("[data-app-action-sidebar-project-list-id]"))
|
|
1065
|
+
.map((list) => {
|
|
1066
|
+
const projectId = list.getAttribute("data-app-action-sidebar-project-list-id") || "";
|
|
1067
|
+
const projectRow = document.querySelector(`[data-app-action-sidebar-project-row][data-app-action-sidebar-project-id="${CSS.escape(projectId)}"]`);
|
|
1068
|
+
const threadRows = Array.from(list.querySelectorAll("[data-app-action-sidebar-thread-row]"));
|
|
1069
|
+
if (!projectRow || threadRows.length === 0) return null;
|
|
1070
|
+
const projectAccent = getComputedStyle(projectRow).getPropertyValue("--codex-plus-project-accent").trim();
|
|
1071
|
+
const listComputed = getComputedStyle(list);
|
|
1072
|
+
const listAccent = listComputed.getPropertyValue("--codex-plus-project-accent").trim();
|
|
1073
|
+
const listBackground = listComputed.backgroundColor;
|
|
1074
|
+
if (!selectedProjectAccent) {
|
|
1075
|
+
selectedProjectAccent = projectAccent;
|
|
1076
|
+
threadRows[0].click();
|
|
1077
|
+
}
|
|
1078
|
+
const unstyledRows = threadRows.filter((row) => {
|
|
1079
|
+
const computed = getComputedStyle(row);
|
|
1080
|
+
const rowAccent = computed.getPropertyValue("--codex-plus-project-accent").trim();
|
|
1081
|
+
return rowAccent !== projectAccent || isTransparentColor(computed.backgroundColor);
|
|
1082
|
+
});
|
|
1083
|
+
return list.hasAttribute("data-codex-plus-project-sidebar-color") &&
|
|
1084
|
+
listAccent === projectAccent &&
|
|
1085
|
+
!isTransparentColor(listBackground) &&
|
|
1086
|
+
unstyledRows.length === 0
|
|
1087
|
+
? null
|
|
1088
|
+
: {
|
|
1089
|
+
projectId,
|
|
1090
|
+
projectLabel: projectRow.getAttribute("data-app-action-sidebar-project-label") || projectRow.innerText.trim(),
|
|
1091
|
+
projectAccent,
|
|
1092
|
+
listAccent,
|
|
1093
|
+
listBackground,
|
|
1094
|
+
threadRows: threadRows.length,
|
|
1095
|
+
unstyledRows: unstyledRows.length,
|
|
1096
|
+
listMarked: list.hasAttribute("data-codex-plus-project-sidebar-color"),
|
|
1097
|
+
};
|
|
1098
|
+
})
|
|
1099
|
+
.filter(Boolean);
|
|
1100
|
+
if (selectedProjectAccent) mountedComposer = await waitForMountedProjectComposer(selectedProjectAccent);
|
|
1101
|
+
if (!accent) throw new Error("Project accent was not computed");
|
|
1102
|
+
if (!matchingProps) throw new Error("Project, thread, bubble, and composer props do not share an accent");
|
|
1103
|
+
if (projectThreadRowCount === 0) throw new Error("No visible project child thread rows appeared; project sidebar styling was not proven");
|
|
1104
|
+
if (unstyledProjectThreadLists.length > 0) {
|
|
1105
|
+
throw new Error(`Project sidebar child rows or list containers are not styled like their project rows: ${JSON.stringify(unstyledProjectThreadLists.slice(0, 4))}`);
|
|
1106
|
+
}
|
|
1107
|
+
if (!mountedComposer?.marked || !mountedComposer?.projectMarked || mountedComposer?.accent !== selectedProjectAccent) {
|
|
1108
|
+
throw new Error(`Mounted composer does not carry the selected project accent: ${JSON.stringify(mountedComposer)}`);
|
|
1109
|
+
}
|
|
1110
|
+
pass("projectColors", {
|
|
1111
|
+
...details,
|
|
1112
|
+
accent,
|
|
1113
|
+
matchingProps,
|
|
1114
|
+
liveRows: liveRows.length,
|
|
1115
|
+
liveAccents: Array.from(new Set(liveAccents)).slice(0, 8),
|
|
1116
|
+
styledProjectThreadLists: projectThreadRowCount,
|
|
1117
|
+
mountedComposer,
|
|
1118
|
+
});
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
fail("projectColors", error);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
try {
|
|
1124
|
+
const details = checkCommon("projectPathHeader");
|
|
1125
|
+
const plugin = window.CodexPlus.plugins.get("projectPathHeader");
|
|
1126
|
+
const accessory = plugin?.exports?.ProjectPathAccessory?.({ context: { cwd: "/tmp/example" }, jsx, jsxs });
|
|
1127
|
+
const missing = plugin?.exports?.ProjectPathAccessory?.({ context: {}, jsx, jsxs });
|
|
1128
|
+
if (accessory == null) throw new Error("Project path accessory was not rendered for cwd");
|
|
1129
|
+
if (missing != null) throw new Error("Project path accessory rendered without cwd");
|
|
1130
|
+
pass("projectPathHeader", { ...details, renderedForCwd: true, skippedMissingCwd: true });
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
fail("projectPathHeader", error);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
try {
|
|
1136
|
+
const status = composerPermissionPickerStatus();
|
|
1137
|
+
if (status.editorMounted && status.editorEditable && status.triggerMounted) {
|
|
1138
|
+
const lowOpacity = Number(status.triggerOpacity) < 0.5;
|
|
1139
|
+
const lowContrast = status.triggerContrast != null && status.triggerContrast < 4.5;
|
|
1140
|
+
if (lowOpacity || lowContrast || status.labelTextFillTransparent) {
|
|
1141
|
+
throw new Error(`Composer permissions picker text is unreadable: ${JSON.stringify(status)}`);
|
|
1142
|
+
}
|
|
1143
|
+
const ariaDisabled = status.triggerAriaDisabled === "true";
|
|
1144
|
+
const visuallyDisabled = /\bopacity-40\b/.test(status.triggerClassName);
|
|
1145
|
+
if (status.triggerDisabled || ariaDisabled || visuallyDisabled) {
|
|
1146
|
+
warn(
|
|
1147
|
+
"audit",
|
|
1148
|
+
"composer-permission-picker-disabled",
|
|
1149
|
+
"Composer permissions picker is disabled while the composer is editable",
|
|
1150
|
+
status,
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
if (!status.editorMounted || !status.triggerMounted) {
|
|
1155
|
+
throw new Error(`Composer permissions picker was not found: ${JSON.stringify(status)}`);
|
|
1156
|
+
}
|
|
1157
|
+
pass("audit", { composerPermissionPicker: status });
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
fail("audit", error);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
try {
|
|
1163
|
+
const details = checkCommon("sidebarNameBlur");
|
|
1164
|
+
const metadata = window.CodexPlus.ui.commands.commandMetadata().some((command) => command.id === "codexPlusToggleSidebarNameBlur");
|
|
1165
|
+
if (!metadata) throw new Error("Sidebar blur command metadata is missing");
|
|
1166
|
+
const commandPalette = await rendererSourceEvidence([
|
|
1167
|
+
"globalThis.CodexPlus?.ui?.commands?.commandMetadata",
|
|
1168
|
+
"e.title??e.electron?.menuTitle??",
|
|
1169
|
+
]);
|
|
1170
|
+
if (!commandPalette.evidence["globalThis.CodexPlus?.ui?.commands?.commandMetadata"]) {
|
|
1171
|
+
throw new Error("Sidebar blur command is not wired into the renderer command palette");
|
|
1172
|
+
}
|
|
1173
|
+
if (!commandPalette.evidence["e.title??e.electron?.menuTitle??"]) {
|
|
1174
|
+
throw new Error("Renderer command palette cannot read literal Codex Plus command titles");
|
|
1175
|
+
}
|
|
1176
|
+
const root = document.documentElement;
|
|
1177
|
+
const previous = root.getAttribute("data-codex-plus-sidebar-names-blurred");
|
|
1178
|
+
let toggled = false;
|
|
1179
|
+
let filter = "";
|
|
1180
|
+
try {
|
|
1181
|
+
root.removeAttribute("data-codex-plus-sidebar-names-blurred");
|
|
1182
|
+
window.CodexPlus.commands.run("codexPlusToggleSidebarNameBlur");
|
|
1183
|
+
toggled = root.getAttribute("data-codex-plus-sidebar-names-blurred") === "true";
|
|
1184
|
+
const probe = document.createElement("span");
|
|
1185
|
+
probe.setAttribute("data-codex-plus-sidebar-name", "");
|
|
1186
|
+
probe.textContent = "probe";
|
|
1187
|
+
document.body.appendChild(probe);
|
|
1188
|
+
filter = getComputedStyle(probe).filter;
|
|
1189
|
+
probe.remove();
|
|
1190
|
+
} finally {
|
|
1191
|
+
if (previous == null) root.removeAttribute("data-codex-plus-sidebar-names-blurred");
|
|
1192
|
+
else root.setAttribute("data-codex-plus-sidebar-names-blurred", previous);
|
|
1193
|
+
}
|
|
1194
|
+
if (!toggled) throw new Error("Sidebar blur command did not toggle the root marker");
|
|
1195
|
+
if (!String(filter).includes("blur")) throw new Error("Sidebar blur computed style is not active");
|
|
1196
|
+
const restored = root.getAttribute("data-codex-plus-sidebar-names-blurred") === previous;
|
|
1197
|
+
if (!restored) throw new Error("Sidebar blur probe did not restore its previous state");
|
|
1198
|
+
pass("sidebarNameBlur", { ...details, metadata, commandPalette, toggled, filter, restored });
|
|
1199
|
+
} catch (error) {
|
|
1200
|
+
fail("sidebarNameBlur", error);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
try {
|
|
1204
|
+
const details = checkCommon("devTools");
|
|
1205
|
+
const metadata = window.CodexPlus.ui.commands.commandMetadata().some((command) => command.id === "codexPlusOpenDevTools");
|
|
1206
|
+
if (!metadata) throw new Error("DevTools command metadata is missing");
|
|
1207
|
+
if (options.includeNativeOpenProbes) {
|
|
1208
|
+
const result = await window.CodexPlus.commands.run("codexPlusOpenDevTools");
|
|
1209
|
+
if (!result?.ok) throw new Error(`DevTools command returned ${JSON.stringify(result)}`);
|
|
1210
|
+
pass("devTools", { ...details, metadata, nativeOpenProbe: true, result });
|
|
1211
|
+
} else {
|
|
1212
|
+
pass("devTools", { ...details, metadata, nativeOpenProbe: false });
|
|
1213
|
+
}
|
|
1214
|
+
} catch (error) {
|
|
1215
|
+
fail("devTools", error);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
try {
|
|
1219
|
+
const details = checkCommon("projectSelectorShortcut");
|
|
1220
|
+
const projects = [
|
|
1221
|
+
{ projectId: "codex-plus", label: "codex-plus", repositoryData: { rootFolder: "codex-plus" } },
|
|
1222
|
+
{ projectId: "hassio-dev", label: "hassio-dev", repositoryData: { rootFolder: "hassio-dev" } },
|
|
1223
|
+
{ projectId: "dotfiles", label: "dotfiles", repositoryData: { rootFolder: "dotfiles" } },
|
|
1224
|
+
];
|
|
1225
|
+
const ranked = window.CodexPlus.ui.projectSelector.fuzzyFilter(projects, "hdev").map((project) => project.projectId);
|
|
1226
|
+
const highlight = window.CodexPlus.ui.projectSelector.fuzzyHighlight({ text: "hassio-dev", query: "hdev", jsx });
|
|
1227
|
+
const highlightCount = Array.isArray(highlight) ? highlight.filter((part) => part?.type === "strong").length : 0;
|
|
1228
|
+
const rankedProjects = window.CodexPlus.ui.projectSelector.fuzzyFilter(projects, "hdev");
|
|
1229
|
+
const selected = [];
|
|
1230
|
+
const events = [];
|
|
1231
|
+
window.CodexPlusHost.adapters.projectSelector.acceptFirst(
|
|
1232
|
+
{ key: "Enter", preventDefault() { events.push("preventDefault"); }, stopPropagation() { events.push("stopPropagation"); } },
|
|
1233
|
+
rankedProjects,
|
|
1234
|
+
(projectId) => selected.push(projectId),
|
|
1235
|
+
"hdev",
|
|
1236
|
+
);
|
|
1237
|
+
if (ranked[0] !== "hassio-dev") throw new Error(`Fuzzy ranking returned ${ranked.join(", ")}`);
|
|
1238
|
+
if (highlightCount === 0) throw new Error("Fuzzy match highlight did not render");
|
|
1239
|
+
if (selected[0] !== "hassio-dev" || events.length !== 2) throw new Error("Enter-to-first-result adapter did not select first ranked result");
|
|
1240
|
+
pass("projectSelectorShortcut", { ...details, ranked, highlightCount, selected });
|
|
1241
|
+
} catch (error) {
|
|
1242
|
+
fail("projectSelectorShortcut", error);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
try {
|
|
1246
|
+
const details = checkCommon("mermaidFullscreen");
|
|
1247
|
+
const diagramProps = window.CodexPlus.ui.mermaid.diagramProps({ code: "graph TD;A-->B" });
|
|
1248
|
+
const marker = Object.prototype.hasOwnProperty.call(diagramProps || {}, "data-codex-plus-mermaid-diagram");
|
|
1249
|
+
if (!marker) throw new Error("Mermaid diagram marker is missing");
|
|
1250
|
+
const plugin = window.CodexPlus.plugins.get("mermaidFullscreen");
|
|
1251
|
+
const container = document.createElement("div");
|
|
1252
|
+
container.setAttribute("data-markdown-copy", "code-block");
|
|
1253
|
+
const diagram = document.createElement("div");
|
|
1254
|
+
diagram.setAttribute("data-codex-plus-mermaid-diagram", "");
|
|
1255
|
+
const source = document.createElement("pre");
|
|
1256
|
+
source.className = "sr-only";
|
|
1257
|
+
source.textContent = "graph TD;A-->B";
|
|
1258
|
+
container.appendChild(diagram);
|
|
1259
|
+
container.appendChild(source);
|
|
1260
|
+
document.body.appendChild(container);
|
|
1261
|
+
plugin?.exports?.decorateAll?.(document);
|
|
1262
|
+
const buttonRendered = Boolean(container.querySelector(":scope > .codex-plus-mermaid-expand-button"));
|
|
1263
|
+
container.remove();
|
|
1264
|
+
if (!buttonRendered) throw new Error("Mermaid expand button did not render");
|
|
1265
|
+
plugin?.exports?.decorateAll?.(document);
|
|
1266
|
+
const liveDiagrams = Array.from(document.querySelectorAll('[data-codex-plus-mermaid-diagram], [aria-label="Mermaid diagram"][role="img"]'))
|
|
1267
|
+
.filter((element) => element.querySelector("svg") || element.getAttribute("aria-label") === "Mermaid diagram");
|
|
1268
|
+
const liveMissingButtons = liveDiagrams.filter((element) => {
|
|
1269
|
+
const host = element.closest('[data-markdown-copy="code-block"]') || element;
|
|
1270
|
+
return !host.querySelector(":scope > .codex-plus-mermaid-expand-button");
|
|
1271
|
+
});
|
|
1272
|
+
if (liveMissingButtons.length > 0) throw new Error(`Live Mermaid diagrams missing popout buttons: ${liveMissingButtons.length}`);
|
|
1273
|
+
if (options.includeNativeOpenProbes) {
|
|
1274
|
+
const nativeResult = await window.CodexPlus.native.request("mermaid/openViewer", {
|
|
1275
|
+
html: "<!doctype html><meta charset='utf-8'><title>Codex Plus Mermaid Audit</title><div>ok</div>",
|
|
1276
|
+
});
|
|
1277
|
+
if (!nativeResult?.ok) throw new Error(`Mermaid native viewer returned ${JSON.stringify(nativeResult)}`);
|
|
1278
|
+
pass("mermaidFullscreen", { ...details, marker, buttonRendered, liveDiagramCount: liveDiagrams.length, nativeOpenProbe: true, nativeResult });
|
|
1279
|
+
} else {
|
|
1280
|
+
pass("mermaidFullscreen", { ...details, marker, buttonRendered, liveDiagramCount: liveDiagrams.length, nativeOpenProbe: false });
|
|
1281
|
+
}
|
|
1282
|
+
} catch (error) {
|
|
1283
|
+
fail("mermaidFullscreen", error);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
for (const id of requiredPlugins) {
|
|
1287
|
+
if (!pluginResults[id]) fail(id, new Error("Probe did not run"));
|
|
1288
|
+
}
|
|
1289
|
+
return {
|
|
1290
|
+
ok: failures.length === 0,
|
|
1291
|
+
failures,
|
|
1292
|
+
pluginResults,
|
|
1293
|
+
expectedWarnings,
|
|
1294
|
+
registeredPlugins: typeof window.CodexPlus?.plugins?.list === "function" ? pluginIds() : null,
|
|
1295
|
+
startedPlugins: started(),
|
|
1296
|
+
};
|
|
1297
|
+
}}(${options}))`;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
async function runAudit(args, {
|
|
1301
|
+
progress = null,
|
|
1302
|
+
operations = {},
|
|
1303
|
+
} = {}) {
|
|
1304
|
+
const findPort = operations.findFreePort || findFreePort;
|
|
1305
|
+
const patchApp = operations.patchCodexApp || patchCodexApp;
|
|
1306
|
+
const syncHome = operations.syncDevHome || syncDevHome;
|
|
1307
|
+
const launchApp = operations.launchDevApp || launchDevApp;
|
|
1308
|
+
const waitRenderer = operations.waitForRendererTarget || waitForRendererTarget;
|
|
1309
|
+
const Session = operations.CdpSession || CdpSession;
|
|
1310
|
+
const waitRuntime = operations.waitForLiveRuntime || waitForLiveRuntime;
|
|
1311
|
+
const waitAppShell = operations.waitForAppShellMounted || waitForAppShellMounted;
|
|
1312
|
+
const verifyMermaidViewer = operations.verifyMermaidViewerRender || verifyMermaidViewerRender;
|
|
1313
|
+
const cleanupApp = operations.cleanupLaunchedAuditApp || cleanupLaunchedAuditApp;
|
|
1314
|
+
const checkStability = operations.checkKeepOpenAppStability || checkKeepOpenAppStability;
|
|
1315
|
+
const preflightAudit = operations.auditPreflight || auditPreflight;
|
|
1316
|
+
const readIdentity = operations.auditIdentity || auditIdentity;
|
|
1317
|
+
let preflight = null;
|
|
1318
|
+
let port = args.remoteDebuggingPort;
|
|
1319
|
+
const identity = readIdentity();
|
|
1320
|
+
let applyResult = null;
|
|
1321
|
+
let syncResult = null;
|
|
1322
|
+
let launchResult = null;
|
|
1323
|
+
let target = null;
|
|
1324
|
+
let cdp = null;
|
|
1325
|
+
let runtimeStatus = null;
|
|
1326
|
+
let appShellStatus = null;
|
|
1327
|
+
let cleanupResult = null;
|
|
1328
|
+
let result = null;
|
|
1329
|
+
try {
|
|
1330
|
+
preflight = await preflightAudit(args, { findPort });
|
|
1331
|
+
port = preflight.port;
|
|
1332
|
+
if (args.apply) {
|
|
1333
|
+
applyResult = await withAuditProgress(
|
|
1334
|
+
progress,
|
|
1335
|
+
`Applying patch set to ${args.target}`,
|
|
1336
|
+
"Applied patch set",
|
|
1337
|
+
() => patchApp({
|
|
1338
|
+
sourceApp: args.source,
|
|
1339
|
+
targetApp: args.target,
|
|
1340
|
+
patchSets,
|
|
1341
|
+
progress: undefined,
|
|
1342
|
+
}),
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
if (args.apply || args.launch) {
|
|
1346
|
+
syncResult = await withAuditProgress(
|
|
1347
|
+
progress,
|
|
1348
|
+
"Syncing dev home",
|
|
1349
|
+
"Synced dev home",
|
|
1350
|
+
() => syncHome({
|
|
1351
|
+
sourceHome: args.sourceHome,
|
|
1352
|
+
devHome: args.devHome,
|
|
1353
|
+
}),
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
if (preflight.launch) {
|
|
1357
|
+
launchResult = await withAuditProgress(
|
|
1358
|
+
progress,
|
|
1359
|
+
`Launching Codex Plus on port ${port}`,
|
|
1360
|
+
`Launched app on port ${port}`,
|
|
1361
|
+
() => launchApp({
|
|
1362
|
+
targetApp: args.target,
|
|
1363
|
+
devHome: args.devHome,
|
|
1364
|
+
electronUserDataPath: args.electronUserDataPath,
|
|
1365
|
+
remoteDebuggingPort: port,
|
|
1366
|
+
devInstanceId: args.devInstanceId,
|
|
1367
|
+
}),
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
target = await withAuditProgress(
|
|
1371
|
+
progress,
|
|
1372
|
+
"Waiting for app://-/index.html",
|
|
1373
|
+
"Found app://-/index.html",
|
|
1374
|
+
() => waitRenderer(port),
|
|
1375
|
+
);
|
|
1376
|
+
cdp = new Session(target.webSocketDebuggerUrl);
|
|
1377
|
+
await cdp.connect();
|
|
1378
|
+
await cdp.send("Runtime.enable");
|
|
1379
|
+
runtimeStatus = await withAuditProgress(
|
|
1380
|
+
progress,
|
|
1381
|
+
"Waiting for Codex Plus runtime",
|
|
1382
|
+
"Runtime ready",
|
|
1383
|
+
() => waitRuntime(cdp),
|
|
1384
|
+
);
|
|
1385
|
+
appShellStatus = await withAuditProgress(
|
|
1386
|
+
progress,
|
|
1387
|
+
"Waiting for Codex app shell",
|
|
1388
|
+
"App shell mounted",
|
|
1389
|
+
() => waitAppShell(cdp),
|
|
1390
|
+
);
|
|
1391
|
+
const live = await withAuditProgress(
|
|
1392
|
+
progress,
|
|
1393
|
+
"Running plugin probes",
|
|
1394
|
+
"Probed plugins",
|
|
1395
|
+
() => cdp.evaluate(pluginAuditExpression({ includeNativeOpenProbes: args.includeNativeOpenProbes })),
|
|
1396
|
+
);
|
|
1397
|
+
const shouldProbeMermaidViewer = live.pluginResults?.mermaidFullscreen?.ok;
|
|
1398
|
+
const mermaidViewerRender = shouldProbeMermaidViewer
|
|
1399
|
+
? await withAuditProgress(
|
|
1400
|
+
progress,
|
|
1401
|
+
"Verifying Mermaid viewer render",
|
|
1402
|
+
"Mermaid viewer rendered",
|
|
1403
|
+
() => verifyMermaidViewer(cdp, port, { Session }),
|
|
1404
|
+
)
|
|
1405
|
+
: null;
|
|
1406
|
+
if (mermaidViewerRender != null) {
|
|
1407
|
+
live.pluginResults.mermaidFullscreen.viewerRenderProbe = mermaidViewerRender;
|
|
1408
|
+
if (!mermaidViewerRender.ok) {
|
|
1409
|
+
live.ok = false;
|
|
1410
|
+
live.pluginResults.mermaidFullscreen.ok = false;
|
|
1411
|
+
live.failures.push({
|
|
1412
|
+
plugin: "mermaidFullscreen",
|
|
1413
|
+
message: `Mermaid viewer render failed: ${mermaidViewerRender.message || "no SVG rendered"}`,
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
appShellStatus = await withAuditProgress(
|
|
1418
|
+
progress,
|
|
1419
|
+
"Verifying Codex app shell after probes",
|
|
1420
|
+
"App shell still healthy",
|
|
1421
|
+
() => waitAppShell(cdp),
|
|
1422
|
+
);
|
|
1423
|
+
result = {
|
|
1424
|
+
ok: live.ok,
|
|
1425
|
+
failures: live.failures,
|
|
1426
|
+
expectedWarnings: live.expectedWarnings || [],
|
|
1427
|
+
pluginResults: live.pluginResults,
|
|
1428
|
+
target: {
|
|
1429
|
+
app: path.resolve(args.target),
|
|
1430
|
+
remoteDebuggingPort: port,
|
|
1431
|
+
url: target?.url,
|
|
1432
|
+
webSocketDebuggerUrl: target?.webSocketDebuggerUrl,
|
|
1433
|
+
pid: launchResult?.pid,
|
|
1434
|
+
},
|
|
1435
|
+
devHome: path.resolve(args.devHome),
|
|
1436
|
+
electronUserDataPath: path.resolve(args.electronUserDataPath),
|
|
1437
|
+
applyResult,
|
|
1438
|
+
syncResult: syncResult && {
|
|
1439
|
+
copied: syncResult.copied,
|
|
1440
|
+
scrubbedGlobalState: syncResult.scrubbedGlobalState,
|
|
1441
|
+
sqliteSnapshots: syncResult.sqliteSnapshots,
|
|
1442
|
+
worktrees: syncResult.worktrees,
|
|
1443
|
+
sessions: syncResult.sessions,
|
|
1444
|
+
},
|
|
1445
|
+
launchResult: launchResult && {
|
|
1446
|
+
command: launchResult.command,
|
|
1447
|
+
args: launchResult.args,
|
|
1448
|
+
pid: launchResult.pid,
|
|
1449
|
+
devBundle: launchResult.devBundle,
|
|
1450
|
+
instanceIdentity: launchResult.instanceIdentity,
|
|
1451
|
+
},
|
|
1452
|
+
registeredPlugins: live.registeredPlugins,
|
|
1453
|
+
startedPlugins: live.startedPlugins,
|
|
1454
|
+
runtimeStatus,
|
|
1455
|
+
appShellStatus,
|
|
1456
|
+
audit: identity,
|
|
1457
|
+
nativeOpenProbes: {
|
|
1458
|
+
included: Boolean(args.includeNativeOpenProbes),
|
|
1459
|
+
},
|
|
1460
|
+
mermaidViewerRender,
|
|
1461
|
+
preflight,
|
|
1462
|
+
};
|
|
1463
|
+
return result;
|
|
1464
|
+
} catch (error) {
|
|
1465
|
+
result = {
|
|
1466
|
+
ok: false,
|
|
1467
|
+
failures: [{
|
|
1468
|
+
plugin: "audit",
|
|
1469
|
+
message: error.message,
|
|
1470
|
+
details: error.details,
|
|
1471
|
+
}],
|
|
1472
|
+
expectedWarnings: [],
|
|
1473
|
+
pluginResults: {},
|
|
1474
|
+
target: {
|
|
1475
|
+
app: path.resolve(args.target),
|
|
1476
|
+
remoteDebuggingPort: port,
|
|
1477
|
+
url: target?.url,
|
|
1478
|
+
webSocketDebuggerUrl: target?.webSocketDebuggerUrl,
|
|
1479
|
+
pid: launchResult?.pid,
|
|
1480
|
+
},
|
|
1481
|
+
devHome: path.resolve(args.devHome),
|
|
1482
|
+
electronUserDataPath: path.resolve(args.electronUserDataPath),
|
|
1483
|
+
applyResult,
|
|
1484
|
+
syncResult: syncResult && {
|
|
1485
|
+
copied: syncResult.copied,
|
|
1486
|
+
scrubbedGlobalState: syncResult.scrubbedGlobalState,
|
|
1487
|
+
sqliteSnapshots: syncResult.sqliteSnapshots,
|
|
1488
|
+
worktrees: syncResult.worktrees,
|
|
1489
|
+
sessions: syncResult.sessions,
|
|
1490
|
+
},
|
|
1491
|
+
launchResult: launchResult && {
|
|
1492
|
+
command: launchResult.command,
|
|
1493
|
+
args: launchResult.args,
|
|
1494
|
+
pid: launchResult.pid,
|
|
1495
|
+
devBundle: launchResult.devBundle,
|
|
1496
|
+
instanceIdentity: launchResult.instanceIdentity,
|
|
1497
|
+
},
|
|
1498
|
+
registeredPlugins: null,
|
|
1499
|
+
startedPlugins: null,
|
|
1500
|
+
runtimeStatus,
|
|
1501
|
+
appShellStatus,
|
|
1502
|
+
audit: identity,
|
|
1503
|
+
nativeOpenProbes: {
|
|
1504
|
+
included: Boolean(args.includeNativeOpenProbes),
|
|
1505
|
+
},
|
|
1506
|
+
preflight,
|
|
1507
|
+
};
|
|
1508
|
+
return result;
|
|
1509
|
+
} finally {
|
|
1510
|
+
if (cdp) await cdp.close();
|
|
1511
|
+
if (result && args.keepOpen && launchResult) {
|
|
1512
|
+
let stability;
|
|
1513
|
+
try {
|
|
1514
|
+
stability = await checkStability(launchResult, {
|
|
1515
|
+
electronUserDataPath: args.electronUserDataPath,
|
|
1516
|
+
});
|
|
1517
|
+
} catch (error) {
|
|
1518
|
+
stability = {
|
|
1519
|
+
checked: true,
|
|
1520
|
+
ok: false,
|
|
1521
|
+
pid: launchResult.pid ?? null,
|
|
1522
|
+
alive: null,
|
|
1523
|
+
crashDumps: [],
|
|
1524
|
+
message: `Could not verify keep-open app stability: ${error.message || String(error)}`,
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
result.appStability = stability;
|
|
1528
|
+
if (!stability.ok) {
|
|
1529
|
+
appendFailure(result, {
|
|
1530
|
+
plugin: "audit",
|
|
1531
|
+
message: stability.message,
|
|
1532
|
+
details: {
|
|
1533
|
+
pid: stability.pid,
|
|
1534
|
+
alive: stability.alive,
|
|
1535
|
+
crashDumps: stability.crashDumps,
|
|
1536
|
+
},
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
try {
|
|
1541
|
+
cleanupResult = launchResult
|
|
1542
|
+
? await withAuditProgress(
|
|
1543
|
+
progress,
|
|
1544
|
+
"Cleaning up launched audit app",
|
|
1545
|
+
args.keepOpen ? "Kept audit app open" : "Cleaned up launched audit app",
|
|
1546
|
+
() => cleanupApp(launchResult, { keepOpen: args.keepOpen }),
|
|
1547
|
+
)
|
|
1548
|
+
: await cleanupApp(launchResult, { keepOpen: false });
|
|
1549
|
+
} catch (error) {
|
|
1550
|
+
cleanupResult = {
|
|
1551
|
+
attempted: Boolean(launchResult?.pid),
|
|
1552
|
+
keptOpen: false,
|
|
1553
|
+
ok: false,
|
|
1554
|
+
pid: launchResult?.pid ?? null,
|
|
1555
|
+
message: error.message || String(error),
|
|
1556
|
+
};
|
|
1557
|
+
progressFail(progress, "Cleaning up launched audit app");
|
|
1558
|
+
}
|
|
1559
|
+
if (result) result.cleanupResult = cleanupResult;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
async function main() {
|
|
1564
|
+
const args = parseArgs(process.argv.slice(2));
|
|
1565
|
+
const progress = await createAuditProgress(args);
|
|
1566
|
+
const result = await runAudit(args, { progress });
|
|
1567
|
+
process.stdout.write(args.json ? formatAuditJson(result) : formatAuditResult(result, args));
|
|
1568
|
+
if (!result.ok) process.exitCode = 1;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
if (require.main === module) {
|
|
1572
|
+
main().catch((error) => {
|
|
1573
|
+
console.error(error.stack || error.message || String(error));
|
|
1574
|
+
process.exitCode = 1;
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
module.exports = {
|
|
1579
|
+
auditAttachCommand,
|
|
1580
|
+
auditIdentity,
|
|
1581
|
+
auditPreflight,
|
|
1582
|
+
cleanupLaunchedAuditApp,
|
|
1583
|
+
checkKeepOpenAppStability,
|
|
1584
|
+
createAuditProgress,
|
|
1585
|
+
DEFAULT_PORT,
|
|
1586
|
+
DEFAULT_SOURCE,
|
|
1587
|
+
DEFAULT_TARGET,
|
|
1588
|
+
failedPatches,
|
|
1589
|
+
failedPlugins,
|
|
1590
|
+
findFreePort,
|
|
1591
|
+
findRendererTargetOnPort,
|
|
1592
|
+
formatAuditJson,
|
|
1593
|
+
formatAuditResult,
|
|
1594
|
+
listRunningAuditApps,
|
|
1595
|
+
listCrashpadPendingDumps,
|
|
1596
|
+
parseArgs,
|
|
1597
|
+
pluginAuditExpression,
|
|
1598
|
+
processIsAlive,
|
|
1599
|
+
runAudit,
|
|
1600
|
+
shouldShowAuditProgress,
|
|
1601
|
+
waitForAppShellMounted,
|
|
1602
|
+
waitForLiveRuntime,
|
|
1603
|
+
verifyMermaidViewerRender,
|
|
1604
|
+
waitForRendererTarget,
|
|
1605
|
+
};
|