@extension.dev/mcp 10.10.10 → 10.10.11
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +33 -0
- package/README.md +2 -2
- package/claude/rules/mcp-tools.md +9 -9
- package/dist/module.js +411 -85
- package/dist/src/lib/artifacts-api.d.ts +1 -0
- package/dist/src/lib/cdp-devtools.d.ts +4 -0
- package/dist/src/lib/cdp-extension-page.d.ts +12 -0
- package/dist/src/lib/common-schema.d.ts +1 -1
- package/dist/src/lib/exec.d.ts +1 -0
- package/dist/src/lib/process-manager.d.ts +1 -1
- package/dist/src/lib/registry.d.ts +3 -0
- package/dist/src/tools/assert.d.ts +1 -1
- package/dist/src/tools/dom-snapshot.d.ts +1 -1
- package/dist/src/tools/eval.d.ts +1 -1
- package/dist/src/tools/inspect-gecko.d.ts +1 -0
- package/dist/src/tools/logs-filter.d.ts +1 -0
- package/dist/src/tools/open.d.ts +1 -1
- package/dist/src/tools/reload.d.ts +1 -1
- package/dist/src/tools/stop.d.ts +1 -0
- package/dist/src/tools/storage.d.ts +1 -1
- package/package.json +2 -2
- package/server.json +2 -2
package/dist/module.js
CHANGED
|
@@ -247,7 +247,7 @@ __webpack_require__.d(workspace_create_namespaceObject, {
|
|
|
247
247
|
handler: ()=>workspace_create_handler,
|
|
248
248
|
schema: ()=>workspace_create_schema
|
|
249
249
|
});
|
|
250
|
-
var package_namespaceObject = JSON.parse('{"rE":"10.10.
|
|
250
|
+
var package_namespaceObject = JSON.parse('{"rE":"10.10.11","El":{"OP":"4.1.31"}}');
|
|
251
251
|
const ENVELOPE_SCHEMA = 1;
|
|
252
252
|
const collectWarnings = (warnings)=>{
|
|
253
253
|
if (!warnings) return [];
|
|
@@ -301,8 +301,24 @@ function descendantPids(pid) {
|
|
|
301
301
|
return [];
|
|
302
302
|
}
|
|
303
303
|
}
|
|
304
|
+
function killWindowsTree(pid) {
|
|
305
|
+
try {
|
|
306
|
+
execFileSync("taskkill", [
|
|
307
|
+
"/PID",
|
|
308
|
+
String(pid),
|
|
309
|
+
"/T",
|
|
310
|
+
"/F"
|
|
311
|
+
], {
|
|
312
|
+
stdio: "ignore"
|
|
313
|
+
});
|
|
314
|
+
return true;
|
|
315
|
+
} catch {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
304
319
|
function killTree(pid, signal) {
|
|
305
320
|
if (!pid) return;
|
|
321
|
+
if ("win32" === process.platform) return void killWindowsTree(pid);
|
|
306
322
|
for (const target of [
|
|
307
323
|
...descendantPids(pid).reverse(),
|
|
308
324
|
pid
|
|
@@ -1148,6 +1164,21 @@ function safeApiBase(raw, callerSupplied) {
|
|
|
1148
1164
|
base: `${parsed.origin}${parsed.pathname}`.replace(/\/+$/, "")
|
|
1149
1165
|
};
|
|
1150
1166
|
}
|
|
1167
|
+
function readJsonObject(text) {
|
|
1168
|
+
try {
|
|
1169
|
+
const parsed = JSON.parse(text);
|
|
1170
|
+
if (!parsed || "object" != typeof parsed || Array.isArray(parsed)) return {
|
|
1171
|
+
problem: "not a JSON object"
|
|
1172
|
+
};
|
|
1173
|
+
return {
|
|
1174
|
+
value: parsed
|
|
1175
|
+
};
|
|
1176
|
+
} catch (err) {
|
|
1177
|
+
return {
|
|
1178
|
+
problem: err instanceof Error ? err.message : String(err)
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1151
1182
|
async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
|
|
1152
1183
|
const res = await fetchImpl(`${apiBase}/api/cli/login/config`, {
|
|
1153
1184
|
headers: {
|
|
@@ -1155,7 +1186,9 @@ async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
|
|
|
1155
1186
|
}
|
|
1156
1187
|
});
|
|
1157
1188
|
if (!res.ok) throw new Error(`Could not fetch login config from ${apiBase} (${res.status}).`);
|
|
1158
|
-
const
|
|
1189
|
+
const read = readJsonObject(await res.text());
|
|
1190
|
+
if ("problem" in read) throw new Error(`The login config at ${apiBase}/api/cli/login/config answered ${res.status} but could not be read (${read.problem}), so what this platform supports is unknown.`);
|
|
1191
|
+
const data = read.value;
|
|
1159
1192
|
return {
|
|
1160
1193
|
deviceCodeUrl: String(data.deviceCodeUrl || "/api/cli/device/code"),
|
|
1161
1194
|
deviceTokenUrl: String(data.deviceTokenUrl || "/api/cli/device/token"),
|
|
@@ -1625,6 +1658,23 @@ function userlandProjectUrl(ref, page = "", apiHint) {
|
|
|
1625
1658
|
return "";
|
|
1626
1659
|
}
|
|
1627
1660
|
}
|
|
1661
|
+
function channelsShapeProblem(json) {
|
|
1662
|
+
if (!json || "object" != typeof json || Array.isArray(json)) return "the body is not a channels map";
|
|
1663
|
+
const bad = Object.entries(json).find(([, row])=>!row || "object" != typeof row);
|
|
1664
|
+
return bad ? `channel ${bad[0]} is not an object` : null;
|
|
1665
|
+
}
|
|
1666
|
+
function buildIndexShapeProblem(json) {
|
|
1667
|
+
const items = json?.items;
|
|
1668
|
+
return Array.isArray(items) ? null : "the body has no items list";
|
|
1669
|
+
}
|
|
1670
|
+
function requireShape(read, problem) {
|
|
1671
|
+
if (!read.ok) return read;
|
|
1672
|
+
const why = problem(read.json);
|
|
1673
|
+
return why ? {
|
|
1674
|
+
ok: false,
|
|
1675
|
+
message: `answered 200 but ${why}`
|
|
1676
|
+
} : read;
|
|
1677
|
+
}
|
|
1628
1678
|
async function readJson(url, res) {
|
|
1629
1679
|
try {
|
|
1630
1680
|
const text = await res.text();
|
|
@@ -2317,7 +2367,7 @@ const SESSION_BROWSER = {
|
|
|
2317
2367
|
};
|
|
2318
2368
|
const CALL_TIMEOUT = {
|
|
2319
2369
|
type: "number",
|
|
2320
|
-
description: "Command timeout in ms
|
|
2370
|
+
description: "Command timeout in ms. When omitted, the default depends on the route: 30000 through the dev session's control channel, 10000 over the Firefox debugger protocol, and 15000 per command over the Chromium debug port."
|
|
2321
2371
|
};
|
|
2322
2372
|
const API_BASE = {
|
|
2323
2373
|
type: "string",
|
|
@@ -4709,7 +4759,10 @@ function writeMarkerBestEffort(info, registeredAtMs) {
|
|
|
4709
4759
|
serverPid: process.pid,
|
|
4710
4760
|
registeredAt: new Date(registeredAtMs).toISOString()
|
|
4711
4761
|
}));
|
|
4712
|
-
|
|
4762
|
+
return null;
|
|
4763
|
+
} catch (error) {
|
|
4764
|
+
return `The session marker could not be written to ${markerDir()} (${error instanceof Error ? error.message : String(error)}), so if this server restarts, extension_stop (all: true) and the fork guard will not see this session; stop it by projectPath and browser, or by its pid ${info.pid}.`;
|
|
4765
|
+
}
|
|
4713
4766
|
}
|
|
4714
4767
|
function registerSession(info) {
|
|
4715
4768
|
const key = sessionKey(info.projectPath, info.browser);
|
|
@@ -4720,7 +4773,7 @@ function registerSession(info) {
|
|
|
4720
4773
|
at
|
|
4721
4774
|
});
|
|
4722
4775
|
process_manager_sessions.set(key, info);
|
|
4723
|
-
writeMarkerBestEffort(info, at);
|
|
4776
|
+
return writeMarkerBestEffort(info, at);
|
|
4724
4777
|
}
|
|
4725
4778
|
function sessionSinceMs(projectPath, browser) {
|
|
4726
4779
|
const stamp = registrationStamps.get(sessionKey(projectPath, browser));
|
|
@@ -6200,8 +6253,8 @@ async function build_handler(args) {
|
|
|
6200
6253
|
const engineRefusedJsonOutput = !engineKnownTooOld && 0 !== attempt.code && refusedTheOutputFlag(attempt.stderr ?? "");
|
|
6201
6254
|
if (engineRefusedJsonOutput) attempt = await runExtensionCli(cliArgs, spawn);
|
|
6202
6255
|
const { code, stdout, stderr } = attempt;
|
|
6203
|
-
if (engineRefusedJsonOutput) warnings.push("The Extension.js installed in this project is older than the one this server expects: it rejected --output json on build, so the build was run a second time without that flag and the result
|
|
6204
|
-
else if (engineKnownTooOld) warnings.push(`The Extension.js installed in this project is older than the one this server expects: it reports ${verdict.version}, and --output json only reached extension build in ${verdict.floor}, so the build was run without that flag and the result
|
|
6256
|
+
if (engineRefusedJsonOutput) warnings.push("The Extension.js installed in this project is older than the one this server expects: it rejected --output json on build, so the build was run a second time without that flag and the result comes from the build summary the engine writes into dist/extension-js/ when this run left one there (a separate warning names the path when it did not). The extension that came out is exactly the same one. Upgrade the project's Extension.js to get the richer report back, including the Safari app identity and the byte totals from the run that just happened, and to stop paying for the second build.");
|
|
6257
|
+
else if (engineKnownTooOld) warnings.push(`The Extension.js installed in this project is older than the one this server expects: it reports ${verdict.version}, and --output json only reached extension build in ${verdict.floor}, so the build was run without that flag and the result comes from the build summary the engine writes into dist/extension-js/ when this run left one there (a separate warning names the path when it did not). The extension that came out is exactly the same one, and nothing was built twice. Upgrade the project's Extension.js to get the richer report back, including the Safari app identity and the byte totals from the run that just happened.`);
|
|
6205
6258
|
const duration = Date.now() - start;
|
|
6206
6259
|
const engine = readEngineOutput(stdout ?? "", stderr ?? "");
|
|
6207
6260
|
const out = engine.narration;
|
|
@@ -6788,14 +6841,31 @@ function sessionProcessPids(projectPath, hints = {
|
|
|
6788
6841
|
verified
|
|
6789
6842
|
};
|
|
6790
6843
|
}
|
|
6791
|
-
function reapSessionProcesses(projectPath, hints = {
|
|
6844
|
+
async function reapSessionProcesses(projectPath, hints = {
|
|
6792
6845
|
pids: []
|
|
6793
6846
|
}) {
|
|
6794
6847
|
const { pids } = sessionProcessPids(projectPath, hints);
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6848
|
+
if (0 === pids.length) return {
|
|
6849
|
+
reaped: [],
|
|
6850
|
+
unconfirmed: []
|
|
6851
|
+
};
|
|
6852
|
+
for (const pid of pids){
|
|
6853
|
+
if ("win32" === process.platform) {
|
|
6854
|
+
killWindowsTree(pid);
|
|
6855
|
+
continue;
|
|
6856
|
+
}
|
|
6857
|
+
try {
|
|
6858
|
+
process.kill(pid, "SIGKILL");
|
|
6859
|
+
} catch {}
|
|
6860
|
+
}
|
|
6861
|
+
await new Promise((resolve)=>setTimeout(resolve, 250));
|
|
6862
|
+
const reaped = [];
|
|
6863
|
+
const unconfirmed = [];
|
|
6864
|
+
for (const pid of pids)("dead" === pidState(pid) ? reaped : unconfirmed).push(pid);
|
|
6865
|
+
return {
|
|
6866
|
+
reaped,
|
|
6867
|
+
unconfirmed
|
|
6868
|
+
};
|
|
6799
6869
|
}
|
|
6800
6870
|
function isAlive(pid) {
|
|
6801
6871
|
try {
|
|
@@ -6806,6 +6876,7 @@ function isAlive(pid) {
|
|
|
6806
6876
|
}
|
|
6807
6877
|
}
|
|
6808
6878
|
function stop_signal(pid, sig) {
|
|
6879
|
+
if ("win32" === process.platform) return killWindowsTree(pid);
|
|
6809
6880
|
try {
|
|
6810
6881
|
process.kill(-pid, sig);
|
|
6811
6882
|
return true;
|
|
@@ -6832,16 +6903,21 @@ async function stopOne(projectPath, browser) {
|
|
|
6832
6903
|
const pid = session?.pid ?? pidFromReadyContract(projectPath, browser);
|
|
6833
6904
|
const hints = contractProcessHints(projectPath, browser);
|
|
6834
6905
|
if (null == pid) {
|
|
6835
|
-
const reaped = reapSessionProcesses(projectPath, hints);
|
|
6906
|
+
const { reaped, unconfirmed } = await reapSessionProcesses(projectPath, hints);
|
|
6836
6907
|
removeSessionMarker(projectPath, browser);
|
|
6908
|
+
let detail;
|
|
6909
|
+
detail = unconfirmed.length ? `No dev pid on record. Killed orphaned browser process(es) from the profile dir, but ${unconfirmed.length} still report alive 250 ms later (pids ${unconfirmed.join(", ")})${reaped.length ? `; ${reaped.length} confirmed gone` : ""}.` : reaped.length ? `No dev pid on record, but reaped ${reaped.length} orphaned browser process(es) from the profile dir, each confirmed gone.` : "No known session for this project/browser (nothing registered in this server and no ready.json contract found).";
|
|
6837
6910
|
return {
|
|
6838
6911
|
projectPath,
|
|
6839
6912
|
browser,
|
|
6840
6913
|
pid: null,
|
|
6841
|
-
stopped: 0
|
|
6914
|
+
stopped: reaped.length > 0 && 0 === unconfirmed.length,
|
|
6842
6915
|
reaped,
|
|
6916
|
+
...unconfirmed.length ? {
|
|
6917
|
+
reapUnconfirmed: unconfirmed
|
|
6918
|
+
} : {},
|
|
6843
6919
|
...cleanCarrier(projectPath),
|
|
6844
|
-
detail
|
|
6920
|
+
detail
|
|
6845
6921
|
};
|
|
6846
6922
|
}
|
|
6847
6923
|
let detail;
|
|
@@ -6875,7 +6951,7 @@ async function stopOne(projectPath, browser) {
|
|
|
6875
6951
|
}
|
|
6876
6952
|
detail = isAlive(pid) ? "Sent SIGTERM and SIGKILL but the process still reports alive; it may be exiting." : "Terminated.";
|
|
6877
6953
|
}
|
|
6878
|
-
const reaped = reapSessionProcesses(projectPath, hints);
|
|
6954
|
+
const { reaped } = await reapSessionProcesses(projectPath, hints);
|
|
6879
6955
|
const { pids: survivors, verified } = sessionProcessPids(projectPath, hints);
|
|
6880
6956
|
const dead = "dead" === pidState(pid);
|
|
6881
6957
|
const stopped = dead && verified && 0 === survivors.length;
|
|
@@ -7157,7 +7233,7 @@ async function dev_handler(args) {
|
|
|
7157
7233
|
return spawnFailedEnvelope(dev_schema.name, spawned);
|
|
7158
7234
|
}
|
|
7159
7235
|
const pid = child.pid;
|
|
7160
|
-
registerSession({
|
|
7236
|
+
let markerWarning = registerSession({
|
|
7161
7237
|
pid,
|
|
7162
7238
|
browser,
|
|
7163
7239
|
port: args.port,
|
|
@@ -7351,7 +7427,7 @@ async function dev_handler(args) {
|
|
|
7351
7427
|
unlocked: allowControl ? args.allowEval ? `${controlVerbs}, eval` : controlVerbs : "none (read-only: logs, inspect, wait, doctor)"
|
|
7352
7428
|
};
|
|
7353
7429
|
const boundPort = contractBoundPort(args.projectPath, browser, spawnedAt);
|
|
7354
|
-
if (null !== boundPort && boundPort !== args.port) registerSession({
|
|
7430
|
+
if (null !== boundPort && boundPort !== args.port) markerWarning = registerSession({
|
|
7355
7431
|
pid,
|
|
7356
7432
|
browser,
|
|
7357
7433
|
port: boundPort,
|
|
@@ -7395,7 +7471,8 @@ async function dev_handler(args) {
|
|
|
7395
7471
|
warnings: [
|
|
7396
7472
|
portNote,
|
|
7397
7473
|
profileReused && !args.noBrowser ? REUSED_PROFILE_NOTE : null,
|
|
7398
|
-
...boot.warnings
|
|
7474
|
+
...boot.warnings,
|
|
7475
|
+
markerWarning
|
|
7399
7476
|
],
|
|
7400
7477
|
hint: args.noBrowser ? "Build-only session (noBrowser: true): no browser will launch, so no runtime will ever attach. extension_wait returns as soon as the first compile lands (compiled: true, browserAttached: false) instead of waiting out its budget; do not wait for a browser. The control verbs (storage/reload/open/dom_snapshot/eval) need a live browser and will not work against this session. When you are done, call extension_stop to shut down the dev server." : "Use extension_wait to check when the extension is fully loaded, then extension_inspect to inspect the live state. " + (allowControl ? controlBound ? `Control channel is ON (port ${control.port} per ready.json): extension_${controlVerbs.split(", ").join("/extension_")}${args.allowEval ? "/extension_eval" : ""} will work against this session.` : control.unavailableReason ? `Control was requested, but the engine reports no control port (${control.unavailableReason}), so extension_${controlVerbs.split(", ").join("/extension_")} will be refused until the session is relaunched.` : `Control was requested; ready.json has not stamped the control port yet, so extension_wait is what confirms extension_${controlVerbs.split(", ").join("/extension_")} will work.` : "Control channel is OFF: extension_storage/reload/open/dom_snapshot need allowControl: true, and extension_eval needs allowEval: true (which also implies allowControl). To unlock them, call extension_dev again with the flag you need plus replace: true (it stops this session first); a plain second call is refused so the session does not fork.") + " When you are done, call extension_stop to shut down the dev server and browser."
|
|
7401
7478
|
});
|
|
@@ -7420,12 +7497,12 @@ const start_schema = {
|
|
|
7420
7497
|
},
|
|
7421
7498
|
port: {
|
|
7422
7499
|
type: "number",
|
|
7423
|
-
description: "
|
|
7500
|
+
description: "Passed to the engine as --port (0 for auto-assign). A production start serves nothing over it today; it matters only to a toolchain that reads it."
|
|
7424
7501
|
},
|
|
7425
7502
|
noBrowser: {
|
|
7426
7503
|
type: "boolean",
|
|
7427
7504
|
default: false,
|
|
7428
|
-
description: "
|
|
7505
|
+
description: "Build (or, with build:false, check the dist) without launching a browser. A production start serves nothing, so with no browser the engine process ends once the build does; read the result with extension_build rather than a session."
|
|
7429
7506
|
},
|
|
7430
7507
|
outputPath: {
|
|
7431
7508
|
type: "string",
|
|
@@ -7457,6 +7534,26 @@ async function start_handler(args) {
|
|
|
7457
7534
|
}
|
|
7458
7535
|
const building = false !== args.build && !outputPath;
|
|
7459
7536
|
const command = building ? "start" : "preview";
|
|
7537
|
+
if ("safari" === browser || "webkit-based" === browser) return envelope_envelope({
|
|
7538
|
+
ok: false,
|
|
7539
|
+
command: start_schema.name,
|
|
7540
|
+
status: "unsupported-browser",
|
|
7541
|
+
error: {
|
|
7542
|
+
code: "E_UNSUPPORTED_BROWSER",
|
|
7543
|
+
message: `The engine's ${command} verb does not run Safari; extension_dev builds, packages and opens the Safari app.`
|
|
7544
|
+
},
|
|
7545
|
+
hint: "Use extension_dev with browser: \"safari\", or extension_build with browser: \"safari\" for the packaged app."
|
|
7546
|
+
});
|
|
7547
|
+
if (!building && (args.host || args.publicHost)) return envelope_envelope({
|
|
7548
|
+
ok: false,
|
|
7549
|
+
command: start_schema.name,
|
|
7550
|
+
status: "bad-request",
|
|
7551
|
+
error: {
|
|
7552
|
+
code: "E_BAD_REQUEST",
|
|
7553
|
+
message: "host and publicHost are not options of the engine's preview verb, which build: false and outputPath run; they apply only when start builds."
|
|
7554
|
+
},
|
|
7555
|
+
hint: "Drop host and publicHost, or let extension_start build (build: true, no outputPath)."
|
|
7556
|
+
});
|
|
7460
7557
|
const cliArgs = [
|
|
7461
7558
|
command,
|
|
7462
7559
|
args.projectPath,
|
|
@@ -7477,7 +7574,7 @@ async function start_handler(args) {
|
|
|
7477
7574
|
const { child, logPath } = spawned;
|
|
7478
7575
|
if (void 0 === child.pid) return spawnFailedEnvelope(start_schema.name, spawned);
|
|
7479
7576
|
const pid = child.pid;
|
|
7480
|
-
registerSession({
|
|
7577
|
+
const markerWarning = registerSession({
|
|
7481
7578
|
pid,
|
|
7482
7579
|
browser,
|
|
7483
7580
|
projectPath: args.projectPath,
|
|
@@ -7607,16 +7704,19 @@ async function start_handler(args) {
|
|
|
7607
7704
|
return envelope_envelope({
|
|
7608
7705
|
ok: true,
|
|
7609
7706
|
command: start_schema.name,
|
|
7610
|
-
status:
|
|
7707
|
+
status: "started",
|
|
7611
7708
|
value: {
|
|
7612
7709
|
pid,
|
|
7613
7710
|
browser,
|
|
7614
7711
|
projectPath: args.projectPath,
|
|
7615
|
-
logPath
|
|
7712
|
+
logPath,
|
|
7713
|
+
verb: command,
|
|
7714
|
+
observed: "the engine process was alive 5 s after it was spawned"
|
|
7616
7715
|
},
|
|
7617
|
-
hint: building ? "
|
|
7716
|
+
hint: building ? "The engine process was alive 5 s after spawn, which is all this answer knows; extension_wait reads whether the build landed. When you are done, call extension_stop to shut the session down." : "The engine's preview process was alive 5 s after spawn, which is all this answer knows: whether a browser shows the extension is not read here. Call extension_stop when you are done.",
|
|
7618
7717
|
warnings: [
|
|
7619
7718
|
...boot.warnings,
|
|
7719
|
+
markerWarning,
|
|
7620
7720
|
stale.removed && "Removed a Live Preview carrier left behind by an earlier dev session, so it was not loaded beside your extension here.",
|
|
7621
7721
|
!stale.removed && stale.note && /Could not remove/i.test(stale.note) && `${stale.note} A Live Preview carrier is still in ./extensions and the engine loads that folder, so this run has the debug companion beside your extension.`
|
|
7622
7722
|
]
|
|
@@ -7719,11 +7819,21 @@ async function runActVerb(args, projectPath, timeoutMs, tool) {
|
|
|
7719
7819
|
"json",
|
|
7720
7820
|
...args.slice(separatorAt)
|
|
7721
7821
|
];
|
|
7722
|
-
const { code, stdout, stderr } = await runExtensionCli(cliArgs, {
|
|
7822
|
+
const { code, stdout, stderr, timedOut, signal } = await runExtensionCli(cliArgs, {
|
|
7723
7823
|
cwd: projectPath,
|
|
7724
7824
|
timeoutMs
|
|
7725
7825
|
});
|
|
7726
7826
|
const out = stdout.trim();
|
|
7827
|
+
if (timedOut) return envelope_envelope({
|
|
7828
|
+
ok: false,
|
|
7829
|
+
command,
|
|
7830
|
+
status: "cli-timeout",
|
|
7831
|
+
error: {
|
|
7832
|
+
code: "E_CLI",
|
|
7833
|
+
name: "CliTimeout",
|
|
7834
|
+
message: `This server stopped the extension CLI after ${timeoutMs ?? 30000} ms (${signal ?? "a signal"}) before it answered. The ${args[0]} verb may already have acted; read the state it changes before repeating it.`
|
|
7835
|
+
}
|
|
7836
|
+
});
|
|
7727
7837
|
if (out) try {
|
|
7728
7838
|
const frame = translateFrame(JSON.parse(out), projectPath, browserFlag(args));
|
|
7729
7839
|
if (isEnvelope(frame)) {
|
|
@@ -7744,6 +7854,16 @@ async function runActVerb(args, projectPath, timeoutMs, tool) {
|
|
|
7744
7854
|
},
|
|
7745
7855
|
hint: "extension_doctor reports the project's engine version next to the one this server pins."
|
|
7746
7856
|
});
|
|
7857
|
+
if (0 === code && out) return envelope_envelope({
|
|
7858
|
+
ok: false,
|
|
7859
|
+
command,
|
|
7860
|
+
status: "cli-unreadable",
|
|
7861
|
+
error: {
|
|
7862
|
+
code: "E_CLI",
|
|
7863
|
+
name: "CliUnreadable",
|
|
7864
|
+
message: `The extension CLI exited 0 but its answer could not be read as JSON (${out.slice(0, 200)}). The ${args[0]} verb may have acted; read the state it changes before repeating it.`
|
|
7865
|
+
}
|
|
7866
|
+
});
|
|
7747
7867
|
const message = stderr.trim() || `extension exited with code ${code}`;
|
|
7748
7868
|
return envelope_envelope({
|
|
7749
7869
|
ok: false,
|
|
@@ -9003,6 +9123,10 @@ const WORKER_TARGET_TYPES = new Set([
|
|
|
9003
9123
|
"background_page",
|
|
9004
9124
|
"worker"
|
|
9005
9125
|
]);
|
|
9126
|
+
const BACKGROUND_TARGET_TYPES = new Set([
|
|
9127
|
+
"service_worker",
|
|
9128
|
+
"background_page"
|
|
9129
|
+
]);
|
|
9006
9130
|
async function readExtensionWorkerTargets(port, extensionId) {
|
|
9007
9131
|
try {
|
|
9008
9132
|
const origin = `chrome-extension://${extensionId}/`;
|
|
@@ -9079,6 +9203,11 @@ async function findExtensionPageTargets(port, wantedUrl) {
|
|
|
9079
9203
|
const read = await readExtensionPageTargets(port, wantedUrl);
|
|
9080
9204
|
return "targets" in read ? read.targets : [];
|
|
9081
9205
|
}
|
|
9206
|
+
function remoteValueNote(result) {
|
|
9207
|
+
if (!result) return;
|
|
9208
|
+
if ("undefined" === result.type) return "the expression returned undefined, which is answered as null";
|
|
9209
|
+
if ("string" == typeof result.unserializableValue) return `the expression returned ${result.unserializableValue}${"bigint" === result.type ? " (a BigInt)" : ""}, which JSON cannot carry, so value holds it as the string "${result.unserializableValue}"`;
|
|
9210
|
+
}
|
|
9082
9211
|
function readRemoteValue(result) {
|
|
9083
9212
|
if (!result) return null;
|
|
9084
9213
|
if ("value" in result) return result.value;
|
|
@@ -9122,9 +9251,13 @@ async function evaluateOnExtensionPage(port, targetId, expression, timeoutMs) {
|
|
|
9122
9251
|
thrown: true,
|
|
9123
9252
|
message: describeException(response.exceptionDetails)
|
|
9124
9253
|
};
|
|
9254
|
+
const note = remoteValueNote(response?.result);
|
|
9125
9255
|
return {
|
|
9126
9256
|
ok: true,
|
|
9127
|
-
value: readRemoteValue(response?.result)
|
|
9257
|
+
value: readRemoteValue(response?.result),
|
|
9258
|
+
...note ? {
|
|
9259
|
+
note
|
|
9260
|
+
} : {}
|
|
9128
9261
|
};
|
|
9129
9262
|
} catch (error) {
|
|
9130
9263
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -9361,11 +9494,18 @@ async function openDevToolsPanel(port, options) {
|
|
|
9361
9494
|
reloadedInspected = false;
|
|
9362
9495
|
}
|
|
9363
9496
|
}
|
|
9497
|
+
let lastEvaluateError = null;
|
|
9364
9498
|
const evaluate = async (expression)=>await cdp.sendCommand("Runtime.evaluate", {
|
|
9365
9499
|
expression,
|
|
9366
9500
|
returnByValue: true,
|
|
9367
9501
|
awaitPromise: true
|
|
9368
|
-
}, sessionId).catch(()=>
|
|
9502
|
+
}, sessionId).catch((err)=>{
|
|
9503
|
+
lastEvaluateError = err instanceof Error ? err.message : String(err);
|
|
9504
|
+
return null;
|
|
9505
|
+
});
|
|
9506
|
+
let registryAnswered = false;
|
|
9507
|
+
let registryError = null;
|
|
9508
|
+
let devtoolsPageLoaded = false;
|
|
9369
9509
|
const wantedId = "string" == typeof options.panelTitle && options.panelTitle ? `${panelIdPrefix(options.extensionId)}${options.panelTitle}` : null;
|
|
9370
9510
|
const prefix = panelIdPrefix(options.extensionId);
|
|
9371
9511
|
let panels = [];
|
|
@@ -9375,7 +9515,11 @@ async function openDevToolsPanel(port, options) {
|
|
|
9375
9515
|
while(!panelId && Date.now() < deadline){
|
|
9376
9516
|
const response = await evaluate(PANEL_IDS_EXPRESSION);
|
|
9377
9517
|
const ids = response?.result?.value;
|
|
9518
|
+
if (response?.exceptionDetails) registryError = response.exceptionDetails.exception?.description ?? response.exceptionDetails.text ?? "the registry read threw";
|
|
9519
|
+
else if (!response) registryError = lastEvaluateError ?? "the registry read did not answer";
|
|
9520
|
+
if (!devtoolsPageLoaded) devtoolsPageLoaded = (await listTargets().catch(()=>[])).some((t)=>"iframe" === t.type && String(t.url ?? "").startsWith(options.devtoolsPageUrl));
|
|
9378
9521
|
if (Array.isArray(ids)) {
|
|
9522
|
+
registryAnswered = true;
|
|
9379
9523
|
panels = ids.map(String);
|
|
9380
9524
|
panelId = panels.find((id)=>wantedId ? id === wantedId : id.startsWith(prefix)) ?? null;
|
|
9381
9525
|
}
|
|
@@ -9398,7 +9542,11 @@ async function openDevToolsPanel(port, options) {
|
|
|
9398
9542
|
stage: "panel",
|
|
9399
9543
|
devtoolsTargetId,
|
|
9400
9544
|
panels,
|
|
9401
|
-
|
|
9545
|
+
devtoolsPageLoaded,
|
|
9546
|
+
...!registryAnswered && registryError ? {
|
|
9547
|
+
registryUnreadable: registryError
|
|
9548
|
+
} : {},
|
|
9549
|
+
reason: registryAnswered ? devtoolsPageLoaded ? wantedId ? `no panel with id ${wantedId} registered within ${budgetMs}ms` : `no panel from ${prefix} registered within ${budgetMs}ms` : `the devtools page ${options.devtoolsPageUrl} never appeared as a frame within ${budgetMs}ms, so nothing could register a panel` : `the DevTools panel registry could not be read (${registryError ?? "no answer"}), so whether a panel registered is unknown`
|
|
9402
9550
|
};
|
|
9403
9551
|
reached = "panel";
|
|
9404
9552
|
const framesBefore = new Set((await listTargets().catch(()=>[])).filter((t)=>"iframe" === t.type && String(t.url ?? "").startsWith(`${prefix}/`)).map((t)=>t.id));
|
|
@@ -9412,15 +9560,31 @@ async function openDevToolsPanel(port, options) {
|
|
|
9412
9560
|
};
|
|
9413
9561
|
reached = "show";
|
|
9414
9562
|
let panelTarget = null;
|
|
9563
|
+
let panelTargetInferred = false;
|
|
9564
|
+
let panelFrameCandidates = 0;
|
|
9415
9565
|
const showDeadline = Date.now() + 3000;
|
|
9416
|
-
|
|
9566
|
+
for(;;){
|
|
9417
9567
|
const frames = (await listTargets().catch(()=>[])).filter((t)=>"iframe" === t.type && String(t.url ?? "").startsWith(`${prefix}/`) && !String(t.url ?? "").startsWith(options.devtoolsPageUrl));
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
|
|
9423
|
-
|
|
9568
|
+
panelFrameCandidates = frames.length;
|
|
9569
|
+
const fresh = frames.find((t)=>!framesBefore.has(t.id));
|
|
9570
|
+
if (fresh) {
|
|
9571
|
+
panelTarget = {
|
|
9572
|
+
targetId: fresh.id,
|
|
9573
|
+
url: String(fresh.url)
|
|
9574
|
+
};
|
|
9575
|
+
break;
|
|
9576
|
+
}
|
|
9577
|
+
if (Date.now() >= showDeadline) {
|
|
9578
|
+
if (1 === frames.length) {
|
|
9579
|
+
panelTarget = {
|
|
9580
|
+
targetId: frames[0].id,
|
|
9581
|
+
url: String(frames[0].url)
|
|
9582
|
+
};
|
|
9583
|
+
panelTargetInferred = true;
|
|
9584
|
+
}
|
|
9585
|
+
break;
|
|
9586
|
+
}
|
|
9587
|
+
await sleep(200);
|
|
9424
9588
|
}
|
|
9425
9589
|
return {
|
|
9426
9590
|
opened: true,
|
|
@@ -9428,6 +9592,10 @@ async function openDevToolsPanel(port, options) {
|
|
|
9428
9592
|
panelId,
|
|
9429
9593
|
panelTitle: panelTitleFromId(panelId, options.extensionId),
|
|
9430
9594
|
panelTarget,
|
|
9595
|
+
...panelTargetInferred ? {
|
|
9596
|
+
panelTargetInferred
|
|
9597
|
+
} : {},
|
|
9598
|
+
panelFrameCandidates,
|
|
9431
9599
|
panels,
|
|
9432
9600
|
reloadedInspected
|
|
9433
9601
|
};
|
|
@@ -10354,7 +10522,7 @@ async function openDevToolsSurface(args, browser) {
|
|
|
10354
10522
|
error: {
|
|
10355
10523
|
code: "E_SURFACE_DID_NOT_OPEN",
|
|
10356
10524
|
name: "PanelDidNotRegister",
|
|
10357
|
-
message: "panel" === outcome.stage ? `DevTools opened on ${inspected.url} and ${doc} loaded in it, but ${outcome.reason}.${registered.length ? ` Panels this extension did register: ${registered.map((id)=>id.slice(`chrome-extension://${extensionId}`.length)).join(", ")}.` : ""}` : `DevTools opened on ${inspected.url} and the panel registered, but ${outcome.reason}.`
|
|
10525
|
+
message: "panel" === outcome.stage ? `DevTools opened on ${inspected.url}${false === outcome.devtoolsPageLoaded ? "" : ` and ${doc} loaded in it`}, but ${outcome.reason}.${registered.length ? ` Panels this extension did register: ${registered.map((id)=>id.slice(`chrome-extension://${extensionId}`.length)).join(", ")}.` : ""}` : `DevTools opened on ${inspected.url} and the panel registered, but ${outcome.reason}.`
|
|
10358
10526
|
},
|
|
10359
10527
|
value: {
|
|
10360
10528
|
inspected: {
|
|
@@ -10364,7 +10532,7 @@ async function openDevToolsSurface(args, browser) {
|
|
|
10364
10532
|
devtoolsTargetId: outcome.devtoolsTargetId ?? null,
|
|
10365
10533
|
panels: outcome.panels ?? []
|
|
10366
10534
|
},
|
|
10367
|
-
hint: args.panel && registered.length ? `No panel is titled "${args.panel}"; pass one of the registered titles as \`panel\`, or omit it for the first.` : `The devtools page registers panels with chrome.devtools.panels.create; extension_logs (context: ['devtools']) shows what ${doc} wrote or threw. An extension that creates its panel only when the page reports to it (Preact Devtools does, through its content script) needs the page loaded with DevTools already open: retry with reload: true, and waitMs for a longer wait. DevTools stays open on the tab.`
|
|
10535
|
+
hint: "panel" === outcome.stage && outcome.registryUnreadable ? "This browser build's DevTools frontend did not answer the panel-registry read this server uses, so the panel may well exist; open DevTools by hand in a headed session to see it, and extension_logs (context: ['devtools']) reads the devtools page either way." : args.panel && registered.length ? `No panel is titled "${args.panel}"; pass one of the registered titles as \`panel\`, or omit it for the first.` : `The devtools page registers panels with chrome.devtools.panels.create; extension_logs (context: ['devtools']) shows what ${doc} wrote or threw. An extension that creates its panel only when the page reports to it (Preact Devtools does, through its content script) needs the page loaded with DevTools already open: retry with reload: true, and waitMs for a longer wait. DevTools stays open on the tab.`
|
|
10368
10536
|
});
|
|
10369
10537
|
}
|
|
10370
10538
|
const panelUrl = outcome.panelTarget?.url ?? null;
|
|
@@ -10394,8 +10562,10 @@ async function openDevToolsSurface(args, browser) {
|
|
|
10394
10562
|
...true !== args.reload || outcome.reloadedInspected ? [] : [
|
|
10395
10563
|
"reload: true was asked for, but the inspected tab could not be reloaded (the attach or Page.reload failed), so the panel was read without a reload."
|
|
10396
10564
|
],
|
|
10397
|
-
...outcome.panelTarget ?
|
|
10398
|
-
"
|
|
10565
|
+
...outcome.panelTarget ? outcome.panelTargetInferred ? [
|
|
10566
|
+
"No new panel frame appeared after showPanel; the one frame of this extension already inside DevTools was taken as the panel, which is an inference, not a match."
|
|
10567
|
+
] : [] : [
|
|
10568
|
+
(outcome.panelFrameCandidates ?? 0) > 1 ? `The panel is shown but ${outcome.panelFrameCandidates} frames of this extension sit inside DevTools and none appeared after showPanel, so which one is the panel is unknown; its url is not given.` : "The panel is shown but its document target had not appeared within 3s; call extension_open surface: \"devtools\" again once it loads to get its url."
|
|
10399
10569
|
]
|
|
10400
10570
|
],
|
|
10401
10571
|
hint: panelUrl ? `DevTools is open on ${inspected.url} with the "${outcome.panelTitle}" panel shown. Read the panel with extension_eval context: "page", url: "${panelUrl}" (over CDP, with chrome.devtools available); context: "devtools" reads the devtools page itself (${doc}), the hidden document that registered the panel. The panel is an iframe inside DevTools, not a tab, so extension_dom_snapshot and extension_inspect do not reach it.` : `DevTools is open on ${inspected.url} with the "${outcome.panelTitle}" panel shown. context: "devtools" on extension_eval reads the devtools page itself (${doc}).`
|
|
@@ -10734,15 +10904,29 @@ async function listArtifacts(options = {}) {
|
|
|
10734
10904
|
message: `Listing shares failed (${res.status}): ${data?.message || "unknown error"}`
|
|
10735
10905
|
}
|
|
10736
10906
|
};
|
|
10737
|
-
|
|
10907
|
+
if (!Array.isArray(data.artifacts)) return {
|
|
10908
|
+
ok: false,
|
|
10909
|
+
error: {
|
|
10910
|
+
name: "SharesListError",
|
|
10911
|
+
status: res.status,
|
|
10912
|
+
message: `Listing shares answered ${res.status} without an artifacts list, so which shares exist is unknown.`
|
|
10913
|
+
}
|
|
10914
|
+
};
|
|
10915
|
+
const rows = data.artifacts;
|
|
10916
|
+
const valid = rows.filter((artifact)=>artifact && "string" == typeof artifact.artifactId);
|
|
10917
|
+
const malformedRows = rows.length - valid.length;
|
|
10918
|
+
const artifacts = valid.map((artifact)=>"string" == typeof artifact.revokeUrl ? {
|
|
10738
10919
|
...artifact,
|
|
10739
10920
|
revokeUrl: wwwRevokeUrl(artifact.revokeUrl)
|
|
10740
|
-
} : artifact)
|
|
10921
|
+
} : artifact);
|
|
10741
10922
|
const num = (value, fallback)=>"number" == typeof value && Number.isFinite(value) ? value : fallback;
|
|
10742
10923
|
return {
|
|
10743
10924
|
ok: true,
|
|
10744
10925
|
data: {
|
|
10745
10926
|
artifacts,
|
|
10927
|
+
...malformedRows ? {
|
|
10928
|
+
malformedRows
|
|
10929
|
+
} : {},
|
|
10746
10930
|
count: num(data.count, artifacts.length),
|
|
10747
10931
|
matched: num(data.matched, artifacts.length),
|
|
10748
10932
|
limit: num(data.limit, artifacts.length),
|
|
@@ -12312,7 +12496,8 @@ async function listShares(args) {
|
|
|
12312
12496
|
"previewUrl and zipUrl are null for a share that is no longer live, because a revoked or expired link cannot resolve for anyone. revokeUrl stays on every row. Revocation is permanent: a revoked id is burned, and re-sharing that build mints a different link. Re-sharing a build that was NOT revoked returns its existing link instead.",
|
|
12313
12497
|
"attribution.ownership says who the share belongs to and therefore who may revoke it: project means the owning workspace holds it and any member can pull it back, personal means one person holds it alone. attribution.credit names the publisher and is attribution only, granting and restricting nothing. A credit of \"CLI token ...\" means the platform could not resolve which human minted that token, and a credit of \"not recorded\" means it never knew; neither is a name, and neither should be reported as one.",
|
|
12314
12498
|
handsOutAZip ? ZIP_URL_REDIRECT_NOTE : null,
|
|
12315
|
-
truncatedNote
|
|
12499
|
+
truncatedNote,
|
|
12500
|
+
listing.data.malformedRows ? `${listing.data.malformedRows} row(s) of the platform's listing carried no artifact id and were left out, so the list may be short by that many.` : null
|
|
12316
12501
|
]
|
|
12317
12502
|
});
|
|
12318
12503
|
}
|
|
@@ -12382,7 +12567,7 @@ async function revokeShare(args) {
|
|
|
12382
12567
|
});
|
|
12383
12568
|
const local = args.projectPath ? readSharedPreviews(args.projectPath) : void 0;
|
|
12384
12569
|
const entry = local ? localIndex(local.entries).get(ref) : void 0;
|
|
12385
|
-
const recordNote = local ? entry ? `${local.path} still lists this share as its own append-only history and was not rewritten, so the entry stays with its original sharedAt. The platform is the truth for whether a link resolves.` : `${local.path} has no entry for this share, so it was made from another machine or another checkout.` : void 0;
|
|
12570
|
+
const recordNote = local ? local.unreadable ? `${local.path} exists but could not be parsed, so whether this project recorded the share is unknown.` : entry ? `${local.path} still lists this share as its own append-only history and was not rewritten, so the entry stays with its original sharedAt. The platform is the truth for whether a link resolves.` : `${local.path} has no entry for this share, so it was made from another machine or another checkout.` : void 0;
|
|
12386
12571
|
if (!result.ok) {
|
|
12387
12572
|
if (result.held) return platformHoldEnvelope({
|
|
12388
12573
|
command: "extension_shares",
|
|
@@ -12727,7 +12912,13 @@ function verdictSentence(verdict) {
|
|
|
12727
12912
|
return `${parts.join(", ")}.`;
|
|
12728
12913
|
}
|
|
12729
12914
|
function logs_filter_readLogEvents(projectPath, browser, query) {
|
|
12730
|
-
return readLogEvents(engineProjectRoot(projectPath), browser, query);
|
|
12915
|
+
return readLogEvents(engineProjectRoot(projectPath), browser, query).filter((event)=>"string" != typeof event.type);
|
|
12916
|
+
}
|
|
12917
|
+
function readLogDropped(projectPath, browser) {
|
|
12918
|
+
return readLogEvents(engineProjectRoot(projectPath), browser, {}).reduce((sum, event)=>{
|
|
12919
|
+
const record = event;
|
|
12920
|
+
return "gap" === record.type && "number" == typeof record.dropped ? sum + record.dropped : sum;
|
|
12921
|
+
}, 0);
|
|
12731
12922
|
}
|
|
12732
12923
|
function makeFilter(args) {
|
|
12733
12924
|
const level = String(args.level || "all").toLowerCase();
|
|
@@ -13046,6 +13237,11 @@ async function readFromStream(args, browser, limit) {
|
|
|
13046
13237
|
let connectedAt = Date.now();
|
|
13047
13238
|
let replayed = 0;
|
|
13048
13239
|
let liveCount = 0;
|
|
13240
|
+
const startedAt = Date.now();
|
|
13241
|
+
let opened = false;
|
|
13242
|
+
let readySeen = false;
|
|
13243
|
+
let bufferedFrom;
|
|
13244
|
+
let evicted;
|
|
13049
13245
|
return await new Promise((resolve)=>{
|
|
13050
13246
|
let settled = false;
|
|
13051
13247
|
const url = `ws://127.0.0.1:${ready.controlPort}${CONTROL_WS_PATH}`;
|
|
@@ -13072,14 +13268,27 @@ async function readFromStream(args, browser, limit) {
|
|
|
13072
13268
|
try {
|
|
13073
13269
|
socket.close();
|
|
13074
13270
|
} catch {}
|
|
13075
|
-
resolve(summarize(events, "stream", browser, runId, limit, dropped, args.projectPath, void 0,
|
|
13271
|
+
resolve(summarize(events, "stream", browser, runId, limit, dropped, args.projectPath, void 0, [
|
|
13272
|
+
streamNote,
|
|
13273
|
+
opened && !readySeen ? "The control channel opened but the broker never sent its ready frame, so whether the replay covered the run is unknown." : null,
|
|
13274
|
+
"number" == typeof evicted && evicted > 0 ? `The broker's ring had already evicted ${evicted} older event(s) before this read${"number" == typeof bufferedFrom ? ` (it holds from seq ${bufferedFrom})` : ""}; read the file (call without follow) for those.` : null
|
|
13275
|
+
].filter((note)=>Boolean(note)).join(" ") || void 0, {
|
|
13076
13276
|
replayed,
|
|
13077
13277
|
live: liveCount,
|
|
13078
|
-
followMs
|
|
13278
|
+
followMs,
|
|
13279
|
+
opened,
|
|
13280
|
+
readySeen,
|
|
13281
|
+
..."number" == typeof bufferedFrom ? {
|
|
13282
|
+
bufferedFrom
|
|
13283
|
+
} : {},
|
|
13284
|
+
..."number" == typeof evicted ? {
|
|
13285
|
+
evicted
|
|
13286
|
+
} : {}
|
|
13079
13287
|
}));
|
|
13080
13288
|
};
|
|
13081
13289
|
const timer = setTimeout(finish, followMs);
|
|
13082
13290
|
socket.on("open", ()=>{
|
|
13291
|
+
opened = true;
|
|
13083
13292
|
connectedAt = Date.now();
|
|
13084
13293
|
try {
|
|
13085
13294
|
socket.send(JSON.stringify({
|
|
@@ -13097,8 +13306,12 @@ async function readFromStream(args, browser, limit) {
|
|
|
13097
13306
|
} catch {
|
|
13098
13307
|
return;
|
|
13099
13308
|
}
|
|
13100
|
-
if ("ready" === frame.type
|
|
13101
|
-
|
|
13309
|
+
if ("ready" === frame.type) {
|
|
13310
|
+
readySeen = true;
|
|
13311
|
+
if (frame.runId) runId = String(frame.runId);
|
|
13312
|
+
if ("number" == typeof frame.bufferedFrom) bufferedFrom = frame.bufferedFrom;
|
|
13313
|
+
if ("number" == typeof frame.evicted) evicted = frame.evicted;
|
|
13314
|
+
} else if ("log" === frame.type && frame.event) {
|
|
13102
13315
|
if (matches(frame.event)) {
|
|
13103
13316
|
events.push(frame.event);
|
|
13104
13317
|
const at = Number(frame.event.timestamp);
|
|
@@ -13133,7 +13346,12 @@ async function readFromStream(args, browser, limit) {
|
|
|
13133
13346
|
socket.on("close", (code, reason)=>{
|
|
13134
13347
|
if (settled) return;
|
|
13135
13348
|
const refusal = controlRefusal(code, reason?.toString() ?? "", url, CONTROL_ENVELOPE_VERSION);
|
|
13136
|
-
if (!refusal)
|
|
13349
|
+
if (!refusal) {
|
|
13350
|
+
const elapsed = Date.now() - startedAt;
|
|
13351
|
+
if (elapsed < followMs - 250) streamNote = `The control channel closed after ${Math.round(elapsed / 1000)} s of the ${Math.round(followMs / 1000)} s window (close code ${code}${reason?.length ? `, "${reason.toString()}"` : ""}), so this is a partial read; the dev session may have stopped or restarted. Re-check with extension_wait.`;
|
|
13352
|
+
finish();
|
|
13353
|
+
return;
|
|
13354
|
+
}
|
|
13137
13355
|
if (events.length > 0 || dropped > 0) {
|
|
13138
13356
|
streamNote = refusal.message;
|
|
13139
13357
|
finish();
|
|
@@ -13310,6 +13528,19 @@ function parseClauses(raw) {
|
|
|
13310
13528
|
const context = void 0 === clause.context ? void 0 : Array.isArray(clause.context) ? clause.context.map(String) : [
|
|
13311
13529
|
String(clause.context)
|
|
13312
13530
|
];
|
|
13531
|
+
if (void 0 !== context) try {
|
|
13532
|
+
makeFilter({
|
|
13533
|
+
context
|
|
13534
|
+
})({
|
|
13535
|
+
context: "background",
|
|
13536
|
+
level: "info",
|
|
13537
|
+
seq: 0,
|
|
13538
|
+
ts: 0
|
|
13539
|
+
});
|
|
13540
|
+
} catch (error) {
|
|
13541
|
+
issues.push(`${at}.context: ${error instanceof Error ? error.message : String(error)}`);
|
|
13542
|
+
return;
|
|
13543
|
+
}
|
|
13313
13544
|
const since = void 0 === clause.since ? void 0 : Number(clause.since);
|
|
13314
13545
|
if (void 0 !== since && !Number.isFinite(since)) return void issues.push(`${at}.since must be a number, the seq cursor to read from`);
|
|
13315
13546
|
const ignore = void 0 === clause.ignore ? void 0 : Array.isArray(clause.ignore) ? clause.ignore.map(String) : [
|
|
@@ -13916,6 +14147,12 @@ function assertConsoleErrorsEmpty(clause, stage) {
|
|
|
13916
14147
|
errors: kept.slice(0, 20),
|
|
13917
14148
|
runId
|
|
13918
14149
|
});
|
|
14150
|
+
const droppedLines = readLogDropped(stage.projectPath, stage.browser);
|
|
14151
|
+
if (droppedLines > 0) return inconclusiveCheck(id, subject, `No error-level log event${scope} among the ${all.length} event(s) that reached the file in run ${runId || "(unnamed)"}, but the writer dropped ${droppedLines} line(s) when it fell behind, and any of them may have been an error.`, "Read extension_logs with follow: true while driving the extension, which streams lines the file writer dropped, then assert again.", {
|
|
14152
|
+
events: all.length,
|
|
14153
|
+
dropped: droppedLines,
|
|
14154
|
+
runId
|
|
14155
|
+
});
|
|
13919
14156
|
return passCheck(id, subject, `No error-level log event${scope} among ${all.length} event(s) in run ${runId || "(unnamed)"}${ignored.length && errors.length ? `, and ${errors.length} matched an ignore entry and were not counted` : ""}${foreignErrors ? `; ${foreignErrors} browser-relayed error(s) from other origins were not counted` : ""}.`, {
|
|
13920
14157
|
events: all.length,
|
|
13921
14158
|
ignored: errors.length - kept.length,
|
|
@@ -16367,8 +16604,17 @@ function closedShadowWalkerCode(cap) {
|
|
|
16367
16604
|
})();
|
|
16368
16605
|
`;
|
|
16369
16606
|
}
|
|
16607
|
+
function matchPatternRegexSource(pattern) {
|
|
16608
|
+
if ("<all_urls>" === pattern) return "^(https?|file|ftp):";
|
|
16609
|
+
const m = /^(\*|https?|file|ftp|wss?):\/\/([^/]*)(\/.*)?$/.exec(pattern);
|
|
16610
|
+
if (!m) return "^" + pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$";
|
|
16611
|
+
const scheme = "*" === m[1] ? "https?" : m[1];
|
|
16612
|
+
const host = "*" === m[2] ? "[^/]*" : m[2].startsWith("*.") ? "([^/]*\\.)?" + m[2].slice(2).replace(/[.]/g, "\\.") : m[2].replace(/[.]/g, "\\.");
|
|
16613
|
+
const pathPart = (m[3] ?? "/").replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
16614
|
+
return "^" + scheme + "://" + host + pathPart + "$";
|
|
16615
|
+
}
|
|
16370
16616
|
function executeScriptExpression(urlFilter, code, tabId) {
|
|
16371
|
-
const pick = "number" == typeof tabId ? `tabs.find(function (t) { return t.id === ${tabId}; })` : urlFilter ? `tabs.find(function (t) { return String(t.url || "").toLowerCase().indexOf(${JSON.stringify(urlFilter.toLowerCase())}) !== -1; })` : "(tabs.find(function (t) { return t.active; }) || tabs[0])";
|
|
16617
|
+
const pick = "number" == typeof tabId ? `tabs.find(function (t) { return t.id === ${tabId}; })` : urlFilter ? /\*|<all_urls>/.test(urlFilter) ? `tabs.find(function (t) { return new RegExp(${JSON.stringify(matchPatternRegexSource(urlFilter))}).test(String(t.url || "")); })` : `tabs.find(function (t) { return String(t.url || "").toLowerCase().indexOf(${JSON.stringify(urlFilter.toLowerCase())}) !== -1; })` : "(tabs.find(function (t) { return t.active; }) || tabs[0])";
|
|
16372
16618
|
return `browser.tabs.query({}).then(function (tabs) {
|
|
16373
16619
|
var tab = ${pick};
|
|
16374
16620
|
if (!tab) return { error: "no matching tab" };
|
|
@@ -17043,6 +17289,7 @@ async function listGeckoExtensions(projectPath, browser) {
|
|
|
17043
17289
|
const temporary = extensions.filter((e)=>e.temporarilyInstalled);
|
|
17044
17290
|
if (1 === temporary.length) {
|
|
17045
17291
|
temporary[0].ownExtension = true;
|
|
17292
|
+
temporary[0].ownExtensionInferred = true;
|
|
17046
17293
|
if (void 0 === temporary[0].name && own?.name !== void 0) {
|
|
17047
17294
|
temporary[0].name = own.name;
|
|
17048
17295
|
if (void 0 !== own.version) temporary[0].version = own.version;
|
|
@@ -17064,9 +17311,13 @@ async function listGeckoExtensions(projectPath, browser) {
|
|
|
17064
17311
|
browser,
|
|
17065
17312
|
count: extensions.length,
|
|
17066
17313
|
ownExtensionId: ownEntry?.id ?? null,
|
|
17314
|
+
...ownEntry?.ownExtensionInferred ? {
|
|
17315
|
+
ownExtensionInferred: true
|
|
17316
|
+
} : {},
|
|
17067
17317
|
extensions
|
|
17068
17318
|
},
|
|
17069
17319
|
warnings: [
|
|
17320
|
+
ownEntry?.ownExtensionInferred ? `No installed add-on carries this session's name${own?.name ? ` ("${own.name}")` : ""}, so ${ownEntry.id} is marked ownExtension only because it is the one temporary install. That is an inference: if this project's add-on failed to install, it may be another temporary add-on such as the engine's companion. extension_logs shows whether the install succeeded.` : null,
|
|
17070
17321
|
"Lists INSTALLED add-ons via the RDP root actor (listAddons), regardless of whether a context is currently live, so entries carry no contexts. temporarilyInstalled marks temporary loads; ownExtension marks the extension this dev session serves, matched from the session's ready contract. Add-ons are never attached to or evaluated in."
|
|
17071
17322
|
]
|
|
17072
17323
|
});
|
|
@@ -17147,7 +17398,7 @@ function readRelayFrame(value) {
|
|
|
17147
17398
|
}
|
|
17148
17399
|
const eval_schema = {
|
|
17149
17400
|
name: "extension_eval",
|
|
17150
|
-
description: "Evaluate an expression in a running extension context. Start the session with allowEval:true (extension_dev), which writes a 0600 session token; without that token every route of this tool, the debug port included, answers eval-disabled. Context defaults to 'background', except on a Chromium MV3 session (the default template) where it defaults to 'page', the active tab
|
|
17401
|
+
description: "Evaluate an expression in a running extension context. Start the session with allowEval:true (extension_dev), which writes a 0600 session token; without that token every route of this tool, the debug port included, answers eval-disabled. Context defaults to 'background', except on a Chromium MV3 session (the default template) where it defaults to 'page', the active tab; pass context:'background' to evaluate in the service worker, which on Chromium goes over the debug port. Debug-port evaluates run with a user gesture, so gesture-gated APIs (permissions.request, sidePanel.open) can succeed here and still fail when the extension's own code calls them. For content and page, pass `url` to pick the tab, or omit both `url` and `tab` for the active tab; a numeric `tab` only disambiguates. Extension surfaces (popup, options, sidebar, devtools) and override pages (newtab, history, bookmarks) need no tab id but must already be open: open one with extension_open first, because a closed one returns an explicit error. On a Chromium MV3 session those pages, and context:'page' with a chrome-extension:// url, evaluate over CDP, the inspector path the extension page CSP does not govern; elsewhere they evaluate over the in-bundle relay. On Firefox a document whose content security policy forbids eval (the extension's own pages, or a site's) is evaluated over the debugger protocol instead, which takes one expression; a page inside the extension that is no declared surface (pages/*) is reached the same way by context:'page' and its moz-extension:// url once a tab shows it. Call extension_dom_snapshot with listTabs:true to enumerate {tabId, url, title}.",
|
|
17151
17402
|
inputSchema: {
|
|
17152
17403
|
type: "object",
|
|
17153
17404
|
properties: {
|
|
@@ -17257,23 +17508,24 @@ async function evaluateOnChromiumExtensionPage(args, browser, context, resolved)
|
|
|
17257
17508
|
});
|
|
17258
17509
|
const workersRead = await readExtensionWorkerTargets(resolved.port, extensionId);
|
|
17259
17510
|
if ("unreadable" in workersRead) return eval_targetsUnreadable(workersRead.unreadable);
|
|
17260
|
-
let workers = workersRead.targets;
|
|
17511
|
+
let workers = workersRead.targets.filter((t)=>BACKGROUND_TARGET_TYPES.has(t.type));
|
|
17261
17512
|
const wakeWarnings = [];
|
|
17262
17513
|
if (0 === workers.length) {
|
|
17263
17514
|
const wake = await wakeExtensionWorker(resolved.port, extensionId);
|
|
17264
|
-
|
|
17515
|
+
const woken = wake.woken ? wake.targets.filter((t)=>BACKGROUND_TARGET_TYPES.has(t.type)) : [];
|
|
17516
|
+
if (!wake.woken || !(woken.length > 0)) return envelope_envelope({
|
|
17265
17517
|
ok: false,
|
|
17266
17518
|
command: eval_schema.name,
|
|
17267
17519
|
status: "no-target",
|
|
17268
17520
|
error: {
|
|
17269
17521
|
code: "E_NO_TARGET",
|
|
17270
17522
|
name: "NoTarget",
|
|
17271
|
-
message: `No running background for chrome-extension://${extensionId}/: Chrome stops an idle MV3 service worker and lists no target for it, and starting it through ServiceWorker.startWorker did not bring one back: ${wake.reason}.`
|
|
17523
|
+
message: `No running background for chrome-extension://${extensionId}/: Chrome stops an idle MV3 service worker and lists no target for it, and starting it through ServiceWorker.startWorker did not bring one back: ${wake.woken ? "only dedicated workers were listed, which are not the background" : wake.reason}.`
|
|
17272
17524
|
},
|
|
17273
17525
|
hint: "Check the extension declares a background (extension_manifest_validate), then wake it with an event it listens to (extension_reload, open a surface with extension_open, navigate a matching tab) and retry. extension_logs (context: ['background']) holds what it wrote before it idled."
|
|
17274
17526
|
});
|
|
17275
|
-
workers =
|
|
17276
|
-
wakeWarnings.push("
|
|
17527
|
+
workers = woken;
|
|
17528
|
+
wakeWarnings.push("No running background worker was listed, so it was started through ServiceWorker.startWorker before evaluating. Chrome stops an idle MV3 service worker after about 30 s without events, but a worker that never started looks the same here, so this does not say it idled; any in-memory state from an earlier run of it is gone unless it was persisted.");
|
|
17277
17529
|
}
|
|
17278
17530
|
const target = workers[0];
|
|
17279
17531
|
const outcome = await evaluateOnExtensionPage(resolved.port, target.targetId, args.expression, args.timeout);
|
|
@@ -17294,6 +17546,7 @@ async function evaluateOnChromiumExtensionPage(args, browser, context, resolved)
|
|
|
17294
17546
|
status: "evaluated",
|
|
17295
17547
|
value: outcome.value,
|
|
17296
17548
|
warnings: [
|
|
17549
|
+
outcome.note ?? null,
|
|
17297
17550
|
...wakeWarnings,
|
|
17298
17551
|
...workers.length > 1 ? [
|
|
17299
17552
|
`${workers.length} background targets match the extension; evaluated in ${target.type} ${target.targetId}.`
|
|
@@ -17376,7 +17629,10 @@ async function evaluateOnChromiumExtensionPage(args, browser, context, resolved)
|
|
|
17376
17629
|
command: eval_schema.name,
|
|
17377
17630
|
status: "evaluated",
|
|
17378
17631
|
value: outcome.value,
|
|
17379
|
-
warnings
|
|
17632
|
+
warnings: [
|
|
17633
|
+
...warnings,
|
|
17634
|
+
outcome.note ?? null
|
|
17635
|
+
],
|
|
17380
17636
|
hint: `Evaluated over CDP in ${target.url} (target ${target.targetId}), the inspector path the extension page CSP does not govern. The result is serialized by value, so return plain data rather than DOM nodes; a promise is awaited before returning.`
|
|
17381
17637
|
});
|
|
17382
17638
|
}
|
|
@@ -17451,9 +17707,12 @@ async function evaluateOnWebTarget(args, matches, url, browser) {
|
|
|
17451
17707
|
command: eval_schema.name,
|
|
17452
17708
|
status: "evaluated",
|
|
17453
17709
|
value: outcome.value,
|
|
17454
|
-
warnings:
|
|
17455
|
-
|
|
17456
|
-
|
|
17710
|
+
warnings: [
|
|
17711
|
+
...live.length > 1 ? [
|
|
17712
|
+
`${live.length} tabs match ${url}; evaluated in ${target.url} (target ${target.targetId}). Narrow the url to pick another.`
|
|
17713
|
+
] : [],
|
|
17714
|
+
outcome.note ?? null
|
|
17715
|
+
],
|
|
17457
17716
|
hint: `Evaluated over CDP in ${target.url} (target ${target.targetId}), the page's main world through the inspector, which the site's CSP and Trusted Types do not govern. Pass context: "content" for the extension's isolated world instead.`
|
|
17458
17717
|
});
|
|
17459
17718
|
}
|
|
@@ -17589,13 +17848,14 @@ async function evaluatePastSurfaceCsp(args, context, run) {
|
|
|
17589
17848
|
return direct;
|
|
17590
17849
|
}
|
|
17591
17850
|
const sameDocumentUrl = (a, b)=>String(a ?? "").replace(/[?#].*$/, "") === b.replace(/[?#].*$/, "");
|
|
17592
|
-
function tabByUrl(tabs, url) {
|
|
17851
|
+
function tabByUrl(tabs, url, seen) {
|
|
17593
17852
|
const exact = tabs.filter((tab)=>sameDocumentUrl(tab.url, url));
|
|
17594
17853
|
const covered = exact.length ? exact : tabs.filter((tab)=>matchPatternCovers(url, String(tab.url ?? "")));
|
|
17595
17854
|
const candidates = covered.length ? covered : tabs.filter((tab)=>String(tab.url ?? "").includes(url));
|
|
17855
|
+
if (seen) seen.matched = candidates.length;
|
|
17596
17856
|
return candidates.find((tab)=>true === tab.selected) ?? candidates[0];
|
|
17597
17857
|
}
|
|
17598
|
-
async function evaluateInGeckoTab(args, browser, select, missing) {
|
|
17858
|
+
async function evaluateInGeckoTab(args, browser, select, missing, seen) {
|
|
17599
17859
|
const resolved = await resolveRdpPort(args.projectPath, browser, {
|
|
17600
17860
|
waitMs: 3000,
|
|
17601
17861
|
graceMs: 1000
|
|
@@ -17625,7 +17885,11 @@ async function evaluateInGeckoTab(args, browser, select, missing) {
|
|
|
17625
17885
|
ok: true,
|
|
17626
17886
|
command: eval_schema.name,
|
|
17627
17887
|
status: "ok",
|
|
17628
|
-
value: void 0 === outcome.value ? null : outcome.value
|
|
17888
|
+
value: void 0 === outcome.value ? null : outcome.value,
|
|
17889
|
+
warnings: [
|
|
17890
|
+
`Evaluated in the tab showing ${outcome.tab.url || "an unknown url"}${outcome.tab.title ? ` ("${outcome.tab.title}")` : ""}.`,
|
|
17891
|
+
seen && seen.matched > 1 ? `${seen.matched} open tabs matched the url; the selected one (or the first) was used. Narrow the url to pick another.` : null
|
|
17892
|
+
]
|
|
17629
17893
|
});
|
|
17630
17894
|
if ("TargetNotFound" === outcome.name) return envelope_envelope({
|
|
17631
17895
|
ok: false,
|
|
@@ -17649,16 +17913,18 @@ async function evaluateInGeckoTab(args, browser, select, missing) {
|
|
|
17649
17913
|
},
|
|
17650
17914
|
hint: "The expression returned a promise that is still pending; pass a larger timeout to wait for it."
|
|
17651
17915
|
});
|
|
17916
|
+
const lost = "EvalLost" === outcome.name;
|
|
17917
|
+
const unsupported = "Unsupported" === outcome.name;
|
|
17652
17918
|
return envelope_envelope({
|
|
17653
17919
|
ok: false,
|
|
17654
17920
|
command: eval_schema.name,
|
|
17655
|
-
status: "eval-failed",
|
|
17921
|
+
status: lost ? "eval-lost" : unsupported ? "eval-unsupported" : "eval-failed",
|
|
17656
17922
|
error: {
|
|
17657
17923
|
code: "E_EVAL",
|
|
17658
17924
|
name: outcome.name,
|
|
17659
17925
|
message: outcome.message
|
|
17660
17926
|
},
|
|
17661
|
-
hint: "The expression threw inside the document. It ran over the debugger protocol, which the document's content security policy does not govern, so this is the expression's own error."
|
|
17927
|
+
hint: lost ? "The document navigated or closed before the expression answered, so it may have run in part; read the page's state before repeating it." : unsupported ? "The debugger protocol could not evaluate in that tab, so the expression never ran. extension_dom_snapshot reads the page without eval." : "The expression threw inside the document. It ran over the debugger protocol, which the document's content security policy does not govern, so this is the expression's own error."
|
|
17662
17928
|
});
|
|
17663
17929
|
}
|
|
17664
17930
|
function backgroundRouteRefusedStatements(args, browser, context, raw) {
|
|
@@ -17675,7 +17941,10 @@ async function evaluatePagePastSiteCsp(args, browser, context, raw) {
|
|
|
17675
17941
|
if (!args.url && null != args.tab) return null;
|
|
17676
17942
|
if (!isSingleExpression(args.expression)) return notOneExpression();
|
|
17677
17943
|
const url = args.url;
|
|
17678
|
-
|
|
17944
|
+
const seen = {
|
|
17945
|
+
matched: 0
|
|
17946
|
+
};
|
|
17947
|
+
return evaluateInGeckoTab(args, browser, url ? (tabs)=>tabByUrl(tabs, url, seen) : (tabs)=>tabs.find((tab)=>true === tab.selected), url ? `No open tab matches url: ${url}` : "No tab is selected in the dev browser.", seen);
|
|
17679
17948
|
}
|
|
17680
17949
|
async function evaluateThroughExecuteScript(args, browser, context, raw) {
|
|
17681
17950
|
if ("page" !== context && "content" !== context) return null;
|
|
@@ -17835,13 +18104,10 @@ async function eval_handler(args) {
|
|
|
17835
18104
|
if (defaulted) try {
|
|
17836
18105
|
const parsed = JSON.parse(raw);
|
|
17837
18106
|
if (parsed && "object" == typeof parsed) {
|
|
17838
|
-
|
|
17839
|
-
defaultedContext: "page"
|
|
17840
|
-
});
|
|
17841
|
-
addWarning(parsed, 'No context given: defaulted to "page" (the active tab) because this Chromium session\'s MV3 background is a service worker whose CSP blocks eval. Pass context: "background" explicitly to target the worker (works on Firefox/MV2 builds).');
|
|
18107
|
+
addWarning(parsed, 'No context given: defaulted to "page" (the active tab). Pass context: "background" to evaluate in the service worker, which this tool reaches over the debug port.');
|
|
17842
18108
|
const code = "string" == typeof parsed.error?.code ? parsed.error.code : "";
|
|
17843
|
-
const unreachable = "E_TARGET_NOT_FOUND" === code || /cannot access|chrome-extension:\/\/|chrome
|
|
17844
|
-
if (false === parsed.ok && unreachable) parsed.hint = "The active tab is a browser or extension page
|
|
18109
|
+
const unreachable = "E_TARGET_NOT_FOUND" === code || /cannot access|chrome-extension:\/\/|chrome:\/\/|no active tab|missing host permission/i.test(JSON.stringify(parsed.error ?? ""));
|
|
18110
|
+
if (false === parsed.ok && unreachable) parsed.hint = "The active tab could not be reached: it is a browser or extension page, a site outside the extension's host permissions, or there is no active tab. Navigate the dev browser to a page the extension may script, or pass url (match pattern) or tab to pick one; extension_dom_snapshot with listTabs: true lists open tabs.";
|
|
17845
18111
|
return actFrameJson(parsed);
|
|
17846
18112
|
}
|
|
17847
18113
|
} catch {}
|
|
@@ -18549,7 +18815,8 @@ async function publish_handler(args) {
|
|
|
18549
18815
|
ref,
|
|
18550
18816
|
api: args.api
|
|
18551
18817
|
});
|
|
18552
|
-
if (buildsRes.ok) {
|
|
18818
|
+
if (buildsRes.ok && buildIndexShapeProblem(buildsRes.json)) buildNote = `The project's build index at ${buildsUrl} answered but ${buildIndexShapeProblem(buildsRes.json)}, so buildSha, version and builtAt were not filled in from it.`;
|
|
18819
|
+
else if (buildsRes.ok) {
|
|
18553
18820
|
const items = parseBuildIndex(buildsRes.json);
|
|
18554
18821
|
if (args.buildSha) {
|
|
18555
18822
|
const pin = String(args.buildSha).toLowerCase();
|
|
@@ -18571,13 +18838,18 @@ async function publish_handler(args) {
|
|
|
18571
18838
|
}
|
|
18572
18839
|
} else {
|
|
18573
18840
|
const newestSuccess = items.filter(isSuccessfulBuild).sort((a, b)=>String(b.timestamp ?? "").localeCompare(String(a.timestamp ?? "")))[0];
|
|
18574
|
-
|
|
18575
|
-
|
|
18576
|
-
|
|
18577
|
-
if (null == data.
|
|
18578
|
-
if (null == data.
|
|
18841
|
+
const platformSha = "string" == typeof data.buildSha && data.buildSha ? String(data.buildSha).toLowerCase() : null;
|
|
18842
|
+
const served = platformSha ? items.find((item)=>item.sha.toLowerCase().startsWith(platformSha) || platformSha.startsWith(item.sha.toLowerCase())) : newestSuccess;
|
|
18843
|
+
if (served) {
|
|
18844
|
+
if (null == data.buildSha) data.buildSha = served.sha;
|
|
18845
|
+
if (null == data.builtAt && served.timestamp) data.builtAt = served.timestamp;
|
|
18846
|
+
if (null == data.version && served.version) data.version = served.version;
|
|
18847
|
+
if (null == data.channel && served.channel) data.channel = served.channel;
|
|
18848
|
+
data.registryUrl = buildsUrl;
|
|
18849
|
+
buildNote = platformSha ? `builtAt/version are read from the index row of build ${served.sha}, the build the platform says the link serves.` : "The platform named no build for this link; buildSha/builtAt/version describe the newest successful build in the project's registry index, which is what an unpinned link is expected to serve. Pin buildSha to serve a specific build.";
|
|
18850
|
+
} else if (platformSha) {
|
|
18579
18851
|
data.registryUrl = buildsUrl;
|
|
18580
|
-
buildNote =
|
|
18852
|
+
buildNote = `The platform says the link serves build ${data.buildSha}, which the project's registry index does not list, so builtAt and version were not filled in from another build.`;
|
|
18581
18853
|
}
|
|
18582
18854
|
}
|
|
18583
18855
|
}
|
|
@@ -18845,7 +19117,7 @@ async function release_promote_handler(args) {
|
|
|
18845
19117
|
api: args.api
|
|
18846
19118
|
});
|
|
18847
19119
|
if (channelsRes.ok) {
|
|
18848
|
-
const rows = parseChannels(channelsRes.json).filter((c)=>c.sha);
|
|
19120
|
+
const rows = channelsShapeProblem(channelsRes.json) ? [] : parseChannels(channelsRes.json).filter((c)=>c.sha);
|
|
18849
19121
|
enrich.validChannelShas = Object.fromEntries(rows.map((c)=>[
|
|
18850
19122
|
c.channel,
|
|
18851
19123
|
c.sha
|
|
@@ -19192,7 +19464,7 @@ async function readReleases(args) {
|
|
|
19192
19464
|
const channelsUrl = registryFileUrl(ref, "channels.json");
|
|
19193
19465
|
const metaUrl = registryFileUrl(ref, "meta.json");
|
|
19194
19466
|
const buildsUrl = registryFileUrl(ref, "builds/index.json");
|
|
19195
|
-
const [
|
|
19467
|
+
const [channelsRaw, metaRes, buildsRaw] = await Promise.all([
|
|
19196
19468
|
fetchRegistryJson(channelsUrl, fetch, {
|
|
19197
19469
|
ref,
|
|
19198
19470
|
api: args.api
|
|
@@ -19206,6 +19478,8 @@ async function readReleases(args) {
|
|
|
19206
19478
|
api: args.api
|
|
19207
19479
|
})
|
|
19208
19480
|
]);
|
|
19481
|
+
const channelsRes = requireShape(channelsRaw, channelsShapeProblem);
|
|
19482
|
+
const buildsRes = requireShape(buildsRaw, buildIndexShapeProblem);
|
|
19209
19483
|
const buildsPageUrl = consoleProjectUrl(ref, "builds", args.api);
|
|
19210
19484
|
const heldRead = [
|
|
19211
19485
|
channelsRes,
|
|
@@ -19730,7 +20004,7 @@ async function submit_handler(args) {
|
|
|
19730
20004
|
health = stores && "object" == typeof stores ? stores : null;
|
|
19731
20005
|
if (!health) healthUnreadable = "stores/health.json had no stores map";
|
|
19732
20006
|
} else healthUnreadable = healthRes.message;
|
|
19733
|
-
if (channelsRes.ok) channelRows = parseChannels(channelsRes.json);
|
|
20007
|
+
if (channelsRes.ok && !channelsShapeProblem(channelsRes.json)) channelRows = parseChannels(channelsRes.json);
|
|
19734
20008
|
} else healthUnreadable = "no stored workspace/project to look up (run extension_auth)";
|
|
19735
20009
|
const preflight = browsers.map((browser)=>{
|
|
19736
20010
|
if (!health) return {
|
|
@@ -20360,6 +20634,36 @@ async function wait_handler(args) {
|
|
|
20360
20634
|
continue;
|
|
20361
20635
|
}
|
|
20362
20636
|
lastContractStatus = contract.status;
|
|
20637
|
+
if ("build" === contract.command) return envelope_envelope({
|
|
20638
|
+
ok: false,
|
|
20639
|
+
command: wait_schema.name,
|
|
20640
|
+
status: "no-session",
|
|
20641
|
+
error: {
|
|
20642
|
+
code: "E_NO_SESSION",
|
|
20643
|
+
message: `${readyPath} is the contract a finished extension_build left (command: build), not a running session, so there is nothing to wait for.`
|
|
20644
|
+
},
|
|
20645
|
+
value: {
|
|
20646
|
+
readyPath,
|
|
20647
|
+
budgetMs,
|
|
20648
|
+
elapsedMs: Date.now() - start
|
|
20649
|
+
},
|
|
20650
|
+
hint: "Start a session with extension_dev (or extension_start), then call extension_wait."
|
|
20651
|
+
});
|
|
20652
|
+
if ("stopped" === contract.status) return envelope_envelope({
|
|
20653
|
+
ok: false,
|
|
20654
|
+
command: wait_schema.name,
|
|
20655
|
+
status: "stopped",
|
|
20656
|
+
error: {
|
|
20657
|
+
code: "E_SESSION_EXITED",
|
|
20658
|
+
message: `${readyPath} records status: stopped; that session was stopped and nothing will become ready.`
|
|
20659
|
+
},
|
|
20660
|
+
value: {
|
|
20661
|
+
readyPath,
|
|
20662
|
+
budgetMs,
|
|
20663
|
+
elapsedMs: Date.now() - start
|
|
20664
|
+
},
|
|
20665
|
+
hint: "Start a session with extension_dev, then call extension_wait."
|
|
20666
|
+
});
|
|
20363
20667
|
if ("ready" === contract.status) {
|
|
20364
20668
|
if ("number" == typeof contract.pid && !wait_isAlive(contract.pid)) return envelope_envelope({
|
|
20365
20669
|
ok: false,
|
|
@@ -20431,7 +20735,7 @@ async function wait_handler(args) {
|
|
|
20431
20735
|
if (!attached && ("start" === contract.command || "preview" === contract.command)) return envelope_envelope({
|
|
20432
20736
|
ok: true,
|
|
20433
20737
|
command: wait_schema.name,
|
|
20434
|
-
status: "
|
|
20738
|
+
status: "build-ready",
|
|
20435
20739
|
value: {
|
|
20436
20740
|
compiled: true,
|
|
20437
20741
|
browserAttached: false,
|
|
@@ -20537,6 +20841,26 @@ async function wait_handler(args) {
|
|
|
20537
20841
|
],
|
|
20538
20842
|
hint: "This is usually transient: call extension_wait again. If it persists, stop and restart the session with extension_dev (a restart reliably reattaches); extension_doctor reports the executor leg."
|
|
20539
20843
|
});
|
|
20844
|
+
if (null === lastContractStatus && !contractUnreadable && !staleContractNote && !findSessionInfo(args.projectPath, browser)) return envelope_envelope({
|
|
20845
|
+
ok: false,
|
|
20846
|
+
command: wait_schema.name,
|
|
20847
|
+
status: "no-session",
|
|
20848
|
+
error: {
|
|
20849
|
+
code: "E_NO_SESSION",
|
|
20850
|
+
message: `No ${browser} session is known for ${args.projectPath}: this server registered none and no contract appeared at ${readyPath} in ${budgetMs} ms.`
|
|
20851
|
+
},
|
|
20852
|
+
value: {
|
|
20853
|
+
compiled: false,
|
|
20854
|
+
browserAttached: false,
|
|
20855
|
+
readyPath,
|
|
20856
|
+
budgetMs,
|
|
20857
|
+
elapsedMs: Date.now() - start
|
|
20858
|
+
},
|
|
20859
|
+
warnings: [
|
|
20860
|
+
clampNote
|
|
20861
|
+
],
|
|
20862
|
+
hint: "Check projectPath and browser, or start a session with extension_dev, then call extension_wait."
|
|
20863
|
+
});
|
|
20540
20864
|
return envelope_envelope({
|
|
20541
20865
|
ok: false,
|
|
20542
20866
|
command: wait_schema.name,
|
|
@@ -23011,7 +23335,9 @@ async function runCli(cmd, args) {
|
|
|
23011
23335
|
} catch {
|
|
23012
23336
|
parsed = null;
|
|
23013
23337
|
}
|
|
23014
|
-
|
|
23338
|
+
if (parsed?.ok === true) return 0;
|
|
23339
|
+
if (null === parsed || "object" != typeof parsed) log("The promote answer above could not be read as an envelope, so whether the channel moved is unknown; check extension_release_list before retrying.");
|
|
23340
|
+
return 1;
|
|
23015
23341
|
}
|
|
23016
23342
|
log("Usage: extension-mcp release promote --build <sha> --channel <channel>");
|
|
23017
23343
|
return 1;
|