@extension.dev/mcp 10.10.10 → 10.10.12
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 +44 -0
- package/README.md +2 -2
- package/claude/rules/mcp-tools.md +9 -9
- package/dist/module.js +470 -87
- 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-identity.d.ts +6 -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 +7 -3
- 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.12","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));
|
|
@@ -4752,7 +4805,53 @@ function listSessions() {
|
|
|
4752
4805
|
return Array.from(process_manager_sessions.values());
|
|
4753
4806
|
}
|
|
4754
4807
|
const PLAUSIBLE_SESSION_BINARY = /chrom|edge|brave|opera|vivaldi|yandex|firefox|waterfox|librewolf|zen|floorp|safari|node|electron|extension/i;
|
|
4808
|
+
function readWindowsProcessTable() {
|
|
4809
|
+
try {
|
|
4810
|
+
const out = execFileSync("powershell", [
|
|
4811
|
+
"-NoProfile",
|
|
4812
|
+
"-NonInteractive",
|
|
4813
|
+
"-Command",
|
|
4814
|
+
"[Console]::OutputEncoding = [Text.Encoding]::UTF8; Get-CimInstance Win32_Process | ForEach-Object { \"$($_.ProcessId)`t$($_.Name)`t$($_.CommandLine)\" }"
|
|
4815
|
+
], {
|
|
4816
|
+
encoding: "utf8",
|
|
4817
|
+
windowsHide: true,
|
|
4818
|
+
maxBuffer: 67108864
|
|
4819
|
+
});
|
|
4820
|
+
const rows = [];
|
|
4821
|
+
for (const line of out.split(/\r?\n/)){
|
|
4822
|
+
const [pidText, name = "", ...rest] = line.split("\t");
|
|
4823
|
+
const pid = Number.parseInt(pidText ?? "", 10);
|
|
4824
|
+
if (Number.isInteger(pid) && pid > 0) rows.push({
|
|
4825
|
+
pid,
|
|
4826
|
+
name,
|
|
4827
|
+
commandLine: rest.join("\t")
|
|
4828
|
+
});
|
|
4829
|
+
}
|
|
4830
|
+
return rows.length ? rows : null;
|
|
4831
|
+
} catch {
|
|
4832
|
+
return null;
|
|
4833
|
+
}
|
|
4834
|
+
}
|
|
4835
|
+
function windowsImageName(pid) {
|
|
4836
|
+
try {
|
|
4837
|
+
const out = execFileSync("tasklist", [
|
|
4838
|
+
"/FI",
|
|
4839
|
+
`PID eq ${pid}`,
|
|
4840
|
+
"/FO",
|
|
4841
|
+
"CSV",
|
|
4842
|
+
"/NH"
|
|
4843
|
+
], {
|
|
4844
|
+
encoding: "utf8",
|
|
4845
|
+
windowsHide: true
|
|
4846
|
+
});
|
|
4847
|
+
const match = /^"([^"]+)","(\d+)"/m.exec(out);
|
|
4848
|
+
return match && Number(match[2]) === pid ? match[1] : "";
|
|
4849
|
+
} catch {
|
|
4850
|
+
return "";
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4755
4853
|
function processCommand(pid) {
|
|
4854
|
+
if ("win32" === process.platform) return windowsImageName(pid);
|
|
4756
4855
|
try {
|
|
4757
4856
|
const cmdline = node_fs.readFileSync(`/proc/${pid}/cmdline`, "utf8");
|
|
4758
4857
|
const argv0 = cmdline.split("\0")[0];
|
|
@@ -6200,8 +6299,8 @@ async function build_handler(args) {
|
|
|
6200
6299
|
const engineRefusedJsonOutput = !engineKnownTooOld && 0 !== attempt.code && refusedTheOutputFlag(attempt.stderr ?? "");
|
|
6201
6300
|
if (engineRefusedJsonOutput) attempt = await runExtensionCli(cliArgs, spawn);
|
|
6202
6301
|
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
|
|
6302
|
+
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.");
|
|
6303
|
+
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
6304
|
const duration = Date.now() - start;
|
|
6206
6305
|
const engine = readEngineOutput(stdout ?? "", stderr ?? "");
|
|
6207
6306
|
const out = engine.narration;
|
|
@@ -6710,7 +6809,13 @@ function cleanCarrier(projectPath) {
|
|
|
6710
6809
|
carrierNote: `${removal.note} The carrier is still at ${removal.path}; the engine loads ./extensions, so remove it by hand before the next run.`
|
|
6711
6810
|
} : {};
|
|
6712
6811
|
}
|
|
6713
|
-
function pgrepPids(pattern) {
|
|
6812
|
+
function pgrepPids(pattern, windowsTable) {
|
|
6813
|
+
if ("win32" === process.platform) {
|
|
6814
|
+
const table = windowsTable ? windowsTable() : readWindowsProcessTable();
|
|
6815
|
+
if (!table) return null;
|
|
6816
|
+
const matcher = new RegExp(pattern, "i");
|
|
6817
|
+
return table.filter((row)=>row.pid !== process.pid && matcher.test(row.commandLine)).map((row)=>row.pid);
|
|
6818
|
+
}
|
|
6714
6819
|
try {
|
|
6715
6820
|
const out = execFileSync("pgrep", [
|
|
6716
6821
|
"-f",
|
|
@@ -6763,8 +6868,13 @@ function sessionProcessPids(projectPath, hints = {
|
|
|
6763
6868
|
}) {
|
|
6764
6869
|
const pids = new Set();
|
|
6765
6870
|
let verified = true;
|
|
6871
|
+
let table;
|
|
6872
|
+
const windowsTable = ()=>{
|
|
6873
|
+
if (void 0 === table) table = readWindowsProcessTable();
|
|
6874
|
+
return table;
|
|
6875
|
+
};
|
|
6766
6876
|
const found = (pattern)=>{
|
|
6767
|
-
const hits = pgrepPids(pattern);
|
|
6877
|
+
const hits = pgrepPids(pattern, windowsTable);
|
|
6768
6878
|
if (null === hits) {
|
|
6769
6879
|
verified = false;
|
|
6770
6880
|
return [];
|
|
@@ -6788,14 +6898,31 @@ function sessionProcessPids(projectPath, hints = {
|
|
|
6788
6898
|
verified
|
|
6789
6899
|
};
|
|
6790
6900
|
}
|
|
6791
|
-
function reapSessionProcesses(projectPath, hints = {
|
|
6901
|
+
async function reapSessionProcesses(projectPath, hints = {
|
|
6792
6902
|
pids: []
|
|
6793
6903
|
}) {
|
|
6794
6904
|
const { pids } = sessionProcessPids(projectPath, hints);
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6905
|
+
if (0 === pids.length) return {
|
|
6906
|
+
reaped: [],
|
|
6907
|
+
unconfirmed: []
|
|
6908
|
+
};
|
|
6909
|
+
for (const pid of pids){
|
|
6910
|
+
if ("win32" === process.platform) {
|
|
6911
|
+
killWindowsTree(pid);
|
|
6912
|
+
continue;
|
|
6913
|
+
}
|
|
6914
|
+
try {
|
|
6915
|
+
process.kill(pid, "SIGKILL");
|
|
6916
|
+
} catch {}
|
|
6917
|
+
}
|
|
6918
|
+
await new Promise((resolve)=>setTimeout(resolve, 250));
|
|
6919
|
+
const reaped = [];
|
|
6920
|
+
const unconfirmed = [];
|
|
6921
|
+
for (const pid of pids)("dead" === pidState(pid) ? reaped : unconfirmed).push(pid);
|
|
6922
|
+
return {
|
|
6923
|
+
reaped,
|
|
6924
|
+
unconfirmed
|
|
6925
|
+
};
|
|
6799
6926
|
}
|
|
6800
6927
|
function isAlive(pid) {
|
|
6801
6928
|
try {
|
|
@@ -6806,6 +6933,7 @@ function isAlive(pid) {
|
|
|
6806
6933
|
}
|
|
6807
6934
|
}
|
|
6808
6935
|
function stop_signal(pid, sig) {
|
|
6936
|
+
if ("win32" === process.platform) return killWindowsTree(pid);
|
|
6809
6937
|
try {
|
|
6810
6938
|
process.kill(-pid, sig);
|
|
6811
6939
|
return true;
|
|
@@ -6832,16 +6960,21 @@ async function stopOne(projectPath, browser) {
|
|
|
6832
6960
|
const pid = session?.pid ?? pidFromReadyContract(projectPath, browser);
|
|
6833
6961
|
const hints = contractProcessHints(projectPath, browser);
|
|
6834
6962
|
if (null == pid) {
|
|
6835
|
-
const reaped = reapSessionProcesses(projectPath, hints);
|
|
6963
|
+
const { reaped, unconfirmed } = await reapSessionProcesses(projectPath, hints);
|
|
6836
6964
|
removeSessionMarker(projectPath, browser);
|
|
6965
|
+
let detail;
|
|
6966
|
+
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
6967
|
return {
|
|
6838
6968
|
projectPath,
|
|
6839
6969
|
browser,
|
|
6840
6970
|
pid: null,
|
|
6841
|
-
stopped: 0
|
|
6971
|
+
stopped: reaped.length > 0 && 0 === unconfirmed.length,
|
|
6842
6972
|
reaped,
|
|
6973
|
+
...unconfirmed.length ? {
|
|
6974
|
+
reapUnconfirmed: unconfirmed
|
|
6975
|
+
} : {},
|
|
6843
6976
|
...cleanCarrier(projectPath),
|
|
6844
|
-
detail
|
|
6977
|
+
detail
|
|
6845
6978
|
};
|
|
6846
6979
|
}
|
|
6847
6980
|
let detail;
|
|
@@ -6875,7 +7008,7 @@ async function stopOne(projectPath, browser) {
|
|
|
6875
7008
|
}
|
|
6876
7009
|
detail = isAlive(pid) ? "Sent SIGTERM and SIGKILL but the process still reports alive; it may be exiting." : "Terminated.";
|
|
6877
7010
|
}
|
|
6878
|
-
const reaped = reapSessionProcesses(projectPath, hints);
|
|
7011
|
+
const { reaped } = await reapSessionProcesses(projectPath, hints);
|
|
6879
7012
|
const { pids: survivors, verified } = sessionProcessPids(projectPath, hints);
|
|
6880
7013
|
const dead = "dead" === pidState(pid);
|
|
6881
7014
|
const stopped = dead && verified && 0 === survivors.length;
|
|
@@ -7157,7 +7290,7 @@ async function dev_handler(args) {
|
|
|
7157
7290
|
return spawnFailedEnvelope(dev_schema.name, spawned);
|
|
7158
7291
|
}
|
|
7159
7292
|
const pid = child.pid;
|
|
7160
|
-
registerSession({
|
|
7293
|
+
let markerWarning = registerSession({
|
|
7161
7294
|
pid,
|
|
7162
7295
|
browser,
|
|
7163
7296
|
port: args.port,
|
|
@@ -7351,7 +7484,7 @@ async function dev_handler(args) {
|
|
|
7351
7484
|
unlocked: allowControl ? args.allowEval ? `${controlVerbs}, eval` : controlVerbs : "none (read-only: logs, inspect, wait, doctor)"
|
|
7352
7485
|
};
|
|
7353
7486
|
const boundPort = contractBoundPort(args.projectPath, browser, spawnedAt);
|
|
7354
|
-
if (null !== boundPort && boundPort !== args.port) registerSession({
|
|
7487
|
+
if (null !== boundPort && boundPort !== args.port) markerWarning = registerSession({
|
|
7355
7488
|
pid,
|
|
7356
7489
|
browser,
|
|
7357
7490
|
port: boundPort,
|
|
@@ -7395,7 +7528,8 @@ async function dev_handler(args) {
|
|
|
7395
7528
|
warnings: [
|
|
7396
7529
|
portNote,
|
|
7397
7530
|
profileReused && !args.noBrowser ? REUSED_PROFILE_NOTE : null,
|
|
7398
|
-
...boot.warnings
|
|
7531
|
+
...boot.warnings,
|
|
7532
|
+
markerWarning
|
|
7399
7533
|
],
|
|
7400
7534
|
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
7535
|
});
|
|
@@ -7420,12 +7554,12 @@ const start_schema = {
|
|
|
7420
7554
|
},
|
|
7421
7555
|
port: {
|
|
7422
7556
|
type: "number",
|
|
7423
|
-
description: "
|
|
7557
|
+
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
7558
|
},
|
|
7425
7559
|
noBrowser: {
|
|
7426
7560
|
type: "boolean",
|
|
7427
7561
|
default: false,
|
|
7428
|
-
description: "
|
|
7562
|
+
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
7563
|
},
|
|
7430
7564
|
outputPath: {
|
|
7431
7565
|
type: "string",
|
|
@@ -7457,6 +7591,26 @@ async function start_handler(args) {
|
|
|
7457
7591
|
}
|
|
7458
7592
|
const building = false !== args.build && !outputPath;
|
|
7459
7593
|
const command = building ? "start" : "preview";
|
|
7594
|
+
if ("safari" === browser || "webkit-based" === browser) return envelope_envelope({
|
|
7595
|
+
ok: false,
|
|
7596
|
+
command: start_schema.name,
|
|
7597
|
+
status: "unsupported-browser",
|
|
7598
|
+
error: {
|
|
7599
|
+
code: "E_UNSUPPORTED_BROWSER",
|
|
7600
|
+
message: `The engine's ${command} verb does not run Safari; extension_dev builds, packages and opens the Safari app.`
|
|
7601
|
+
},
|
|
7602
|
+
hint: "Use extension_dev with browser: \"safari\", or extension_build with browser: \"safari\" for the packaged app."
|
|
7603
|
+
});
|
|
7604
|
+
if (!building && (args.host || args.publicHost)) return envelope_envelope({
|
|
7605
|
+
ok: false,
|
|
7606
|
+
command: start_schema.name,
|
|
7607
|
+
status: "bad-request",
|
|
7608
|
+
error: {
|
|
7609
|
+
code: "E_BAD_REQUEST",
|
|
7610
|
+
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."
|
|
7611
|
+
},
|
|
7612
|
+
hint: "Drop host and publicHost, or let extension_start build (build: true, no outputPath)."
|
|
7613
|
+
});
|
|
7460
7614
|
const cliArgs = [
|
|
7461
7615
|
command,
|
|
7462
7616
|
args.projectPath,
|
|
@@ -7477,7 +7631,7 @@ async function start_handler(args) {
|
|
|
7477
7631
|
const { child, logPath } = spawned;
|
|
7478
7632
|
if (void 0 === child.pid) return spawnFailedEnvelope(start_schema.name, spawned);
|
|
7479
7633
|
const pid = child.pid;
|
|
7480
|
-
registerSession({
|
|
7634
|
+
const markerWarning = registerSession({
|
|
7481
7635
|
pid,
|
|
7482
7636
|
browser,
|
|
7483
7637
|
projectPath: args.projectPath,
|
|
@@ -7607,16 +7761,19 @@ async function start_handler(args) {
|
|
|
7607
7761
|
return envelope_envelope({
|
|
7608
7762
|
ok: true,
|
|
7609
7763
|
command: start_schema.name,
|
|
7610
|
-
status:
|
|
7764
|
+
status: "started",
|
|
7611
7765
|
value: {
|
|
7612
7766
|
pid,
|
|
7613
7767
|
browser,
|
|
7614
7768
|
projectPath: args.projectPath,
|
|
7615
|
-
logPath
|
|
7769
|
+
logPath,
|
|
7770
|
+
verb: command,
|
|
7771
|
+
observed: "the engine process was alive 5 s after it was spawned"
|
|
7616
7772
|
},
|
|
7617
|
-
hint: building ? "
|
|
7773
|
+
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
7774
|
warnings: [
|
|
7619
7775
|
...boot.warnings,
|
|
7776
|
+
markerWarning,
|
|
7620
7777
|
stale.removed && "Removed a Live Preview carrier left behind by an earlier dev session, so it was not loaded beside your extension here.",
|
|
7621
7778
|
!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
7779
|
]
|
|
@@ -7719,11 +7876,21 @@ async function runActVerb(args, projectPath, timeoutMs, tool) {
|
|
|
7719
7876
|
"json",
|
|
7720
7877
|
...args.slice(separatorAt)
|
|
7721
7878
|
];
|
|
7722
|
-
const { code, stdout, stderr } = await runExtensionCli(cliArgs, {
|
|
7879
|
+
const { code, stdout, stderr, timedOut, signal } = await runExtensionCli(cliArgs, {
|
|
7723
7880
|
cwd: projectPath,
|
|
7724
7881
|
timeoutMs
|
|
7725
7882
|
});
|
|
7726
7883
|
const out = stdout.trim();
|
|
7884
|
+
if (timedOut) return envelope_envelope({
|
|
7885
|
+
ok: false,
|
|
7886
|
+
command,
|
|
7887
|
+
status: "cli-timeout",
|
|
7888
|
+
error: {
|
|
7889
|
+
code: "E_CLI",
|
|
7890
|
+
name: "CliTimeout",
|
|
7891
|
+
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.`
|
|
7892
|
+
}
|
|
7893
|
+
});
|
|
7727
7894
|
if (out) try {
|
|
7728
7895
|
const frame = translateFrame(JSON.parse(out), projectPath, browserFlag(args));
|
|
7729
7896
|
if (isEnvelope(frame)) {
|
|
@@ -7744,6 +7911,16 @@ async function runActVerb(args, projectPath, timeoutMs, tool) {
|
|
|
7744
7911
|
},
|
|
7745
7912
|
hint: "extension_doctor reports the project's engine version next to the one this server pins."
|
|
7746
7913
|
});
|
|
7914
|
+
if (0 === code && out) return envelope_envelope({
|
|
7915
|
+
ok: false,
|
|
7916
|
+
command,
|
|
7917
|
+
status: "cli-unreadable",
|
|
7918
|
+
error: {
|
|
7919
|
+
code: "E_CLI",
|
|
7920
|
+
name: "CliUnreadable",
|
|
7921
|
+
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.`
|
|
7922
|
+
}
|
|
7923
|
+
});
|
|
7747
7924
|
const message = stderr.trim() || `extension exited with code ${code}`;
|
|
7748
7925
|
return envelope_envelope({
|
|
7749
7926
|
ok: false,
|
|
@@ -9003,6 +9180,10 @@ const WORKER_TARGET_TYPES = new Set([
|
|
|
9003
9180
|
"background_page",
|
|
9004
9181
|
"worker"
|
|
9005
9182
|
]);
|
|
9183
|
+
const BACKGROUND_TARGET_TYPES = new Set([
|
|
9184
|
+
"service_worker",
|
|
9185
|
+
"background_page"
|
|
9186
|
+
]);
|
|
9006
9187
|
async function readExtensionWorkerTargets(port, extensionId) {
|
|
9007
9188
|
try {
|
|
9008
9189
|
const origin = `chrome-extension://${extensionId}/`;
|
|
@@ -9079,6 +9260,11 @@ async function findExtensionPageTargets(port, wantedUrl) {
|
|
|
9079
9260
|
const read = await readExtensionPageTargets(port, wantedUrl);
|
|
9080
9261
|
return "targets" in read ? read.targets : [];
|
|
9081
9262
|
}
|
|
9263
|
+
function remoteValueNote(result) {
|
|
9264
|
+
if (!result) return;
|
|
9265
|
+
if ("undefined" === result.type) return "the expression returned undefined, which is answered as null";
|
|
9266
|
+
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}"`;
|
|
9267
|
+
}
|
|
9082
9268
|
function readRemoteValue(result) {
|
|
9083
9269
|
if (!result) return null;
|
|
9084
9270
|
if ("value" in result) return result.value;
|
|
@@ -9122,9 +9308,13 @@ async function evaluateOnExtensionPage(port, targetId, expression, timeoutMs) {
|
|
|
9122
9308
|
thrown: true,
|
|
9123
9309
|
message: describeException(response.exceptionDetails)
|
|
9124
9310
|
};
|
|
9311
|
+
const note = remoteValueNote(response?.result);
|
|
9125
9312
|
return {
|
|
9126
9313
|
ok: true,
|
|
9127
|
-
value: readRemoteValue(response?.result)
|
|
9314
|
+
value: readRemoteValue(response?.result),
|
|
9315
|
+
...note ? {
|
|
9316
|
+
note
|
|
9317
|
+
} : {}
|
|
9128
9318
|
};
|
|
9129
9319
|
} catch (error) {
|
|
9130
9320
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -9361,11 +9551,18 @@ async function openDevToolsPanel(port, options) {
|
|
|
9361
9551
|
reloadedInspected = false;
|
|
9362
9552
|
}
|
|
9363
9553
|
}
|
|
9554
|
+
let lastEvaluateError = null;
|
|
9364
9555
|
const evaluate = async (expression)=>await cdp.sendCommand("Runtime.evaluate", {
|
|
9365
9556
|
expression,
|
|
9366
9557
|
returnByValue: true,
|
|
9367
9558
|
awaitPromise: true
|
|
9368
|
-
}, sessionId).catch(()=>
|
|
9559
|
+
}, sessionId).catch((err)=>{
|
|
9560
|
+
lastEvaluateError = err instanceof Error ? err.message : String(err);
|
|
9561
|
+
return null;
|
|
9562
|
+
});
|
|
9563
|
+
let registryAnswered = false;
|
|
9564
|
+
let registryError = null;
|
|
9565
|
+
let devtoolsPageLoaded = false;
|
|
9369
9566
|
const wantedId = "string" == typeof options.panelTitle && options.panelTitle ? `${panelIdPrefix(options.extensionId)}${options.panelTitle}` : null;
|
|
9370
9567
|
const prefix = panelIdPrefix(options.extensionId);
|
|
9371
9568
|
let panels = [];
|
|
@@ -9375,7 +9572,11 @@ async function openDevToolsPanel(port, options) {
|
|
|
9375
9572
|
while(!panelId && Date.now() < deadline){
|
|
9376
9573
|
const response = await evaluate(PANEL_IDS_EXPRESSION);
|
|
9377
9574
|
const ids = response?.result?.value;
|
|
9575
|
+
if (response?.exceptionDetails) registryError = response.exceptionDetails.exception?.description ?? response.exceptionDetails.text ?? "the registry read threw";
|
|
9576
|
+
else if (!response) registryError = lastEvaluateError ?? "the registry read did not answer";
|
|
9577
|
+
if (!devtoolsPageLoaded) devtoolsPageLoaded = (await listTargets().catch(()=>[])).some((t)=>"iframe" === t.type && String(t.url ?? "").startsWith(options.devtoolsPageUrl));
|
|
9378
9578
|
if (Array.isArray(ids)) {
|
|
9579
|
+
registryAnswered = true;
|
|
9379
9580
|
panels = ids.map(String);
|
|
9380
9581
|
panelId = panels.find((id)=>wantedId ? id === wantedId : id.startsWith(prefix)) ?? null;
|
|
9381
9582
|
}
|
|
@@ -9398,7 +9599,11 @@ async function openDevToolsPanel(port, options) {
|
|
|
9398
9599
|
stage: "panel",
|
|
9399
9600
|
devtoolsTargetId,
|
|
9400
9601
|
panels,
|
|
9401
|
-
|
|
9602
|
+
devtoolsPageLoaded,
|
|
9603
|
+
...!registryAnswered && registryError ? {
|
|
9604
|
+
registryUnreadable: registryError
|
|
9605
|
+
} : {},
|
|
9606
|
+
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
9607
|
};
|
|
9403
9608
|
reached = "panel";
|
|
9404
9609
|
const framesBefore = new Set((await listTargets().catch(()=>[])).filter((t)=>"iframe" === t.type && String(t.url ?? "").startsWith(`${prefix}/`)).map((t)=>t.id));
|
|
@@ -9412,15 +9617,31 @@ async function openDevToolsPanel(port, options) {
|
|
|
9412
9617
|
};
|
|
9413
9618
|
reached = "show";
|
|
9414
9619
|
let panelTarget = null;
|
|
9620
|
+
let panelTargetInferred = false;
|
|
9621
|
+
let panelFrameCandidates = 0;
|
|
9415
9622
|
const showDeadline = Date.now() + 3000;
|
|
9416
|
-
|
|
9623
|
+
for(;;){
|
|
9417
9624
|
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
|
-
|
|
9625
|
+
panelFrameCandidates = frames.length;
|
|
9626
|
+
const fresh = frames.find((t)=>!framesBefore.has(t.id));
|
|
9627
|
+
if (fresh) {
|
|
9628
|
+
panelTarget = {
|
|
9629
|
+
targetId: fresh.id,
|
|
9630
|
+
url: String(fresh.url)
|
|
9631
|
+
};
|
|
9632
|
+
break;
|
|
9633
|
+
}
|
|
9634
|
+
if (Date.now() >= showDeadline) {
|
|
9635
|
+
if (1 === frames.length) {
|
|
9636
|
+
panelTarget = {
|
|
9637
|
+
targetId: frames[0].id,
|
|
9638
|
+
url: String(frames[0].url)
|
|
9639
|
+
};
|
|
9640
|
+
panelTargetInferred = true;
|
|
9641
|
+
}
|
|
9642
|
+
break;
|
|
9643
|
+
}
|
|
9644
|
+
await sleep(200);
|
|
9424
9645
|
}
|
|
9425
9646
|
return {
|
|
9426
9647
|
opened: true,
|
|
@@ -9428,6 +9649,10 @@ async function openDevToolsPanel(port, options) {
|
|
|
9428
9649
|
panelId,
|
|
9429
9650
|
panelTitle: panelTitleFromId(panelId, options.extensionId),
|
|
9430
9651
|
panelTarget,
|
|
9652
|
+
...panelTargetInferred ? {
|
|
9653
|
+
panelTargetInferred
|
|
9654
|
+
} : {},
|
|
9655
|
+
panelFrameCandidates,
|
|
9431
9656
|
panels,
|
|
9432
9657
|
reloadedInspected
|
|
9433
9658
|
};
|
|
@@ -10354,7 +10579,7 @@ async function openDevToolsSurface(args, browser) {
|
|
|
10354
10579
|
error: {
|
|
10355
10580
|
code: "E_SURFACE_DID_NOT_OPEN",
|
|
10356
10581
|
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}.`
|
|
10582
|
+
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
10583
|
},
|
|
10359
10584
|
value: {
|
|
10360
10585
|
inspected: {
|
|
@@ -10364,7 +10589,7 @@ async function openDevToolsSurface(args, browser) {
|
|
|
10364
10589
|
devtoolsTargetId: outcome.devtoolsTargetId ?? null,
|
|
10365
10590
|
panels: outcome.panels ?? []
|
|
10366
10591
|
},
|
|
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.`
|
|
10592
|
+
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
10593
|
});
|
|
10369
10594
|
}
|
|
10370
10595
|
const panelUrl = outcome.panelTarget?.url ?? null;
|
|
@@ -10394,8 +10619,10 @@ async function openDevToolsSurface(args, browser) {
|
|
|
10394
10619
|
...true !== args.reload || outcome.reloadedInspected ? [] : [
|
|
10395
10620
|
"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
10621
|
],
|
|
10397
|
-
...outcome.panelTarget ?
|
|
10398
|
-
"
|
|
10622
|
+
...outcome.panelTarget ? outcome.panelTargetInferred ? [
|
|
10623
|
+
"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."
|
|
10624
|
+
] : [] : [
|
|
10625
|
+
(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
10626
|
]
|
|
10400
10627
|
],
|
|
10401
10628
|
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 +10961,29 @@ async function listArtifacts(options = {}) {
|
|
|
10734
10961
|
message: `Listing shares failed (${res.status}): ${data?.message || "unknown error"}`
|
|
10735
10962
|
}
|
|
10736
10963
|
};
|
|
10737
|
-
|
|
10964
|
+
if (!Array.isArray(data.artifacts)) return {
|
|
10965
|
+
ok: false,
|
|
10966
|
+
error: {
|
|
10967
|
+
name: "SharesListError",
|
|
10968
|
+
status: res.status,
|
|
10969
|
+
message: `Listing shares answered ${res.status} without an artifacts list, so which shares exist is unknown.`
|
|
10970
|
+
}
|
|
10971
|
+
};
|
|
10972
|
+
const rows = data.artifacts;
|
|
10973
|
+
const valid = rows.filter((artifact)=>artifact && "string" == typeof artifact.artifactId);
|
|
10974
|
+
const malformedRows = rows.length - valid.length;
|
|
10975
|
+
const artifacts = valid.map((artifact)=>"string" == typeof artifact.revokeUrl ? {
|
|
10738
10976
|
...artifact,
|
|
10739
10977
|
revokeUrl: wwwRevokeUrl(artifact.revokeUrl)
|
|
10740
|
-
} : artifact)
|
|
10978
|
+
} : artifact);
|
|
10741
10979
|
const num = (value, fallback)=>"number" == typeof value && Number.isFinite(value) ? value : fallback;
|
|
10742
10980
|
return {
|
|
10743
10981
|
ok: true,
|
|
10744
10982
|
data: {
|
|
10745
10983
|
artifacts,
|
|
10984
|
+
...malformedRows ? {
|
|
10985
|
+
malformedRows
|
|
10986
|
+
} : {},
|
|
10746
10987
|
count: num(data.count, artifacts.length),
|
|
10747
10988
|
matched: num(data.matched, artifacts.length),
|
|
10748
10989
|
limit: num(data.limit, artifacts.length),
|
|
@@ -12312,7 +12553,8 @@ async function listShares(args) {
|
|
|
12312
12553
|
"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
12554
|
"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
12555
|
handsOutAZip ? ZIP_URL_REDIRECT_NOTE : null,
|
|
12315
|
-
truncatedNote
|
|
12556
|
+
truncatedNote,
|
|
12557
|
+
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
12558
|
]
|
|
12317
12559
|
});
|
|
12318
12560
|
}
|
|
@@ -12382,7 +12624,7 @@ async function revokeShare(args) {
|
|
|
12382
12624
|
});
|
|
12383
12625
|
const local = args.projectPath ? readSharedPreviews(args.projectPath) : void 0;
|
|
12384
12626
|
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;
|
|
12627
|
+
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
12628
|
if (!result.ok) {
|
|
12387
12629
|
if (result.held) return platformHoldEnvelope({
|
|
12388
12630
|
command: "extension_shares",
|
|
@@ -12727,7 +12969,13 @@ function verdictSentence(verdict) {
|
|
|
12727
12969
|
return `${parts.join(", ")}.`;
|
|
12728
12970
|
}
|
|
12729
12971
|
function logs_filter_readLogEvents(projectPath, browser, query) {
|
|
12730
|
-
return readLogEvents(engineProjectRoot(projectPath), browser, query);
|
|
12972
|
+
return readLogEvents(engineProjectRoot(projectPath), browser, query).filter((event)=>"string" != typeof event.type);
|
|
12973
|
+
}
|
|
12974
|
+
function readLogDropped(projectPath, browser) {
|
|
12975
|
+
return readLogEvents(engineProjectRoot(projectPath), browser, {}).reduce((sum, event)=>{
|
|
12976
|
+
const record = event;
|
|
12977
|
+
return "gap" === record.type && "number" == typeof record.dropped ? sum + record.dropped : sum;
|
|
12978
|
+
}, 0);
|
|
12731
12979
|
}
|
|
12732
12980
|
function makeFilter(args) {
|
|
12733
12981
|
const level = String(args.level || "all").toLowerCase();
|
|
@@ -13046,6 +13294,11 @@ async function readFromStream(args, browser, limit) {
|
|
|
13046
13294
|
let connectedAt = Date.now();
|
|
13047
13295
|
let replayed = 0;
|
|
13048
13296
|
let liveCount = 0;
|
|
13297
|
+
const startedAt = Date.now();
|
|
13298
|
+
let opened = false;
|
|
13299
|
+
let readySeen = false;
|
|
13300
|
+
let bufferedFrom;
|
|
13301
|
+
let evicted;
|
|
13049
13302
|
return await new Promise((resolve)=>{
|
|
13050
13303
|
let settled = false;
|
|
13051
13304
|
const url = `ws://127.0.0.1:${ready.controlPort}${CONTROL_WS_PATH}`;
|
|
@@ -13072,14 +13325,27 @@ async function readFromStream(args, browser, limit) {
|
|
|
13072
13325
|
try {
|
|
13073
13326
|
socket.close();
|
|
13074
13327
|
} catch {}
|
|
13075
|
-
resolve(summarize(events, "stream", browser, runId, limit, dropped, args.projectPath, void 0,
|
|
13328
|
+
resolve(summarize(events, "stream", browser, runId, limit, dropped, args.projectPath, void 0, [
|
|
13329
|
+
streamNote,
|
|
13330
|
+
opened && !readySeen ? "The control channel opened but the broker never sent its ready frame, so whether the replay covered the run is unknown." : null,
|
|
13331
|
+
"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
|
|
13332
|
+
].filter((note)=>Boolean(note)).join(" ") || void 0, {
|
|
13076
13333
|
replayed,
|
|
13077
13334
|
live: liveCount,
|
|
13078
|
-
followMs
|
|
13335
|
+
followMs,
|
|
13336
|
+
opened,
|
|
13337
|
+
readySeen,
|
|
13338
|
+
..."number" == typeof bufferedFrom ? {
|
|
13339
|
+
bufferedFrom
|
|
13340
|
+
} : {},
|
|
13341
|
+
..."number" == typeof evicted ? {
|
|
13342
|
+
evicted
|
|
13343
|
+
} : {}
|
|
13079
13344
|
}));
|
|
13080
13345
|
};
|
|
13081
13346
|
const timer = setTimeout(finish, followMs);
|
|
13082
13347
|
socket.on("open", ()=>{
|
|
13348
|
+
opened = true;
|
|
13083
13349
|
connectedAt = Date.now();
|
|
13084
13350
|
try {
|
|
13085
13351
|
socket.send(JSON.stringify({
|
|
@@ -13097,8 +13363,12 @@ async function readFromStream(args, browser, limit) {
|
|
|
13097
13363
|
} catch {
|
|
13098
13364
|
return;
|
|
13099
13365
|
}
|
|
13100
|
-
if ("ready" === frame.type
|
|
13101
|
-
|
|
13366
|
+
if ("ready" === frame.type) {
|
|
13367
|
+
readySeen = true;
|
|
13368
|
+
if (frame.runId) runId = String(frame.runId);
|
|
13369
|
+
if ("number" == typeof frame.bufferedFrom) bufferedFrom = frame.bufferedFrom;
|
|
13370
|
+
if ("number" == typeof frame.evicted) evicted = frame.evicted;
|
|
13371
|
+
} else if ("log" === frame.type && frame.event) {
|
|
13102
13372
|
if (matches(frame.event)) {
|
|
13103
13373
|
events.push(frame.event);
|
|
13104
13374
|
const at = Number(frame.event.timestamp);
|
|
@@ -13133,7 +13403,12 @@ async function readFromStream(args, browser, limit) {
|
|
|
13133
13403
|
socket.on("close", (code, reason)=>{
|
|
13134
13404
|
if (settled) return;
|
|
13135
13405
|
const refusal = controlRefusal(code, reason?.toString() ?? "", url, CONTROL_ENVELOPE_VERSION);
|
|
13136
|
-
if (!refusal)
|
|
13406
|
+
if (!refusal) {
|
|
13407
|
+
const elapsed = Date.now() - startedAt;
|
|
13408
|
+
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.`;
|
|
13409
|
+
finish();
|
|
13410
|
+
return;
|
|
13411
|
+
}
|
|
13137
13412
|
if (events.length > 0 || dropped > 0) {
|
|
13138
13413
|
streamNote = refusal.message;
|
|
13139
13414
|
finish();
|
|
@@ -13310,6 +13585,19 @@ function parseClauses(raw) {
|
|
|
13310
13585
|
const context = void 0 === clause.context ? void 0 : Array.isArray(clause.context) ? clause.context.map(String) : [
|
|
13311
13586
|
String(clause.context)
|
|
13312
13587
|
];
|
|
13588
|
+
if (void 0 !== context) try {
|
|
13589
|
+
makeFilter({
|
|
13590
|
+
context
|
|
13591
|
+
})({
|
|
13592
|
+
context: "background",
|
|
13593
|
+
level: "info",
|
|
13594
|
+
seq: 0,
|
|
13595
|
+
ts: 0
|
|
13596
|
+
});
|
|
13597
|
+
} catch (error) {
|
|
13598
|
+
issues.push(`${at}.context: ${error instanceof Error ? error.message : String(error)}`);
|
|
13599
|
+
return;
|
|
13600
|
+
}
|
|
13313
13601
|
const since = void 0 === clause.since ? void 0 : Number(clause.since);
|
|
13314
13602
|
if (void 0 !== since && !Number.isFinite(since)) return void issues.push(`${at}.since must be a number, the seq cursor to read from`);
|
|
13315
13603
|
const ignore = void 0 === clause.ignore ? void 0 : Array.isArray(clause.ignore) ? clause.ignore.map(String) : [
|
|
@@ -13916,6 +14204,12 @@ function assertConsoleErrorsEmpty(clause, stage) {
|
|
|
13916
14204
|
errors: kept.slice(0, 20),
|
|
13917
14205
|
runId
|
|
13918
14206
|
});
|
|
14207
|
+
const droppedLines = readLogDropped(stage.projectPath, stage.browser);
|
|
14208
|
+
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.", {
|
|
14209
|
+
events: all.length,
|
|
14210
|
+
dropped: droppedLines,
|
|
14211
|
+
runId
|
|
14212
|
+
});
|
|
13919
14213
|
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
14214
|
events: all.length,
|
|
13921
14215
|
ignored: errors.length - kept.length,
|
|
@@ -16367,8 +16661,17 @@ function closedShadowWalkerCode(cap) {
|
|
|
16367
16661
|
})();
|
|
16368
16662
|
`;
|
|
16369
16663
|
}
|
|
16664
|
+
function matchPatternRegexSource(pattern) {
|
|
16665
|
+
if ("<all_urls>" === pattern) return "^(https?|file|ftp):";
|
|
16666
|
+
const m = /^(\*|https?|file|ftp|wss?):\/\/([^/]*)(\/.*)?$/.exec(pattern);
|
|
16667
|
+
if (!m) return "^" + pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$";
|
|
16668
|
+
const scheme = "*" === m[1] ? "https?" : m[1];
|
|
16669
|
+
const host = "*" === m[2] ? "[^/]*" : m[2].startsWith("*.") ? "([^/]*\\.)?" + m[2].slice(2).replace(/[.]/g, "\\.") : m[2].replace(/[.]/g, "\\.");
|
|
16670
|
+
const pathPart = (m[3] ?? "/").replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
16671
|
+
return "^" + scheme + "://" + host + pathPart + "$";
|
|
16672
|
+
}
|
|
16370
16673
|
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])";
|
|
16674
|
+
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
16675
|
return `browser.tabs.query({}).then(function (tabs) {
|
|
16373
16676
|
var tab = ${pick};
|
|
16374
16677
|
if (!tab) return { error: "no matching tab" };
|
|
@@ -17043,6 +17346,7 @@ async function listGeckoExtensions(projectPath, browser) {
|
|
|
17043
17346
|
const temporary = extensions.filter((e)=>e.temporarilyInstalled);
|
|
17044
17347
|
if (1 === temporary.length) {
|
|
17045
17348
|
temporary[0].ownExtension = true;
|
|
17349
|
+
temporary[0].ownExtensionInferred = true;
|
|
17046
17350
|
if (void 0 === temporary[0].name && own?.name !== void 0) {
|
|
17047
17351
|
temporary[0].name = own.name;
|
|
17048
17352
|
if (void 0 !== own.version) temporary[0].version = own.version;
|
|
@@ -17064,9 +17368,13 @@ async function listGeckoExtensions(projectPath, browser) {
|
|
|
17064
17368
|
browser,
|
|
17065
17369
|
count: extensions.length,
|
|
17066
17370
|
ownExtensionId: ownEntry?.id ?? null,
|
|
17371
|
+
...ownEntry?.ownExtensionInferred ? {
|
|
17372
|
+
ownExtensionInferred: true
|
|
17373
|
+
} : {},
|
|
17067
17374
|
extensions
|
|
17068
17375
|
},
|
|
17069
17376
|
warnings: [
|
|
17377
|
+
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
17378
|
"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
17379
|
]
|
|
17072
17380
|
});
|
|
@@ -17147,7 +17455,7 @@ function readRelayFrame(value) {
|
|
|
17147
17455
|
}
|
|
17148
17456
|
const eval_schema = {
|
|
17149
17457
|
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
|
|
17458
|
+
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
17459
|
inputSchema: {
|
|
17152
17460
|
type: "object",
|
|
17153
17461
|
properties: {
|
|
@@ -17257,23 +17565,24 @@ async function evaluateOnChromiumExtensionPage(args, browser, context, resolved)
|
|
|
17257
17565
|
});
|
|
17258
17566
|
const workersRead = await readExtensionWorkerTargets(resolved.port, extensionId);
|
|
17259
17567
|
if ("unreadable" in workersRead) return eval_targetsUnreadable(workersRead.unreadable);
|
|
17260
|
-
let workers = workersRead.targets;
|
|
17568
|
+
let workers = workersRead.targets.filter((t)=>BACKGROUND_TARGET_TYPES.has(t.type));
|
|
17261
17569
|
const wakeWarnings = [];
|
|
17262
17570
|
if (0 === workers.length) {
|
|
17263
17571
|
const wake = await wakeExtensionWorker(resolved.port, extensionId);
|
|
17264
|
-
|
|
17572
|
+
const woken = wake.woken ? wake.targets.filter((t)=>BACKGROUND_TARGET_TYPES.has(t.type)) : [];
|
|
17573
|
+
if (!wake.woken || !(woken.length > 0)) return envelope_envelope({
|
|
17265
17574
|
ok: false,
|
|
17266
17575
|
command: eval_schema.name,
|
|
17267
17576
|
status: "no-target",
|
|
17268
17577
|
error: {
|
|
17269
17578
|
code: "E_NO_TARGET",
|
|
17270
17579
|
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}.`
|
|
17580
|
+
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
17581
|
},
|
|
17273
17582
|
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
17583
|
});
|
|
17275
|
-
workers =
|
|
17276
|
-
wakeWarnings.push("
|
|
17584
|
+
workers = woken;
|
|
17585
|
+
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
17586
|
}
|
|
17278
17587
|
const target = workers[0];
|
|
17279
17588
|
const outcome = await evaluateOnExtensionPage(resolved.port, target.targetId, args.expression, args.timeout);
|
|
@@ -17294,6 +17603,7 @@ async function evaluateOnChromiumExtensionPage(args, browser, context, resolved)
|
|
|
17294
17603
|
status: "evaluated",
|
|
17295
17604
|
value: outcome.value,
|
|
17296
17605
|
warnings: [
|
|
17606
|
+
outcome.note ?? null,
|
|
17297
17607
|
...wakeWarnings,
|
|
17298
17608
|
...workers.length > 1 ? [
|
|
17299
17609
|
`${workers.length} background targets match the extension; evaluated in ${target.type} ${target.targetId}.`
|
|
@@ -17376,7 +17686,10 @@ async function evaluateOnChromiumExtensionPage(args, browser, context, resolved)
|
|
|
17376
17686
|
command: eval_schema.name,
|
|
17377
17687
|
status: "evaluated",
|
|
17378
17688
|
value: outcome.value,
|
|
17379
|
-
warnings
|
|
17689
|
+
warnings: [
|
|
17690
|
+
...warnings,
|
|
17691
|
+
outcome.note ?? null
|
|
17692
|
+
],
|
|
17380
17693
|
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
17694
|
});
|
|
17382
17695
|
}
|
|
@@ -17451,9 +17764,12 @@ async function evaluateOnWebTarget(args, matches, url, browser) {
|
|
|
17451
17764
|
command: eval_schema.name,
|
|
17452
17765
|
status: "evaluated",
|
|
17453
17766
|
value: outcome.value,
|
|
17454
|
-
warnings:
|
|
17455
|
-
|
|
17456
|
-
|
|
17767
|
+
warnings: [
|
|
17768
|
+
...live.length > 1 ? [
|
|
17769
|
+
`${live.length} tabs match ${url}; evaluated in ${target.url} (target ${target.targetId}). Narrow the url to pick another.`
|
|
17770
|
+
] : [],
|
|
17771
|
+
outcome.note ?? null
|
|
17772
|
+
],
|
|
17457
17773
|
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
17774
|
});
|
|
17459
17775
|
}
|
|
@@ -17589,13 +17905,14 @@ async function evaluatePastSurfaceCsp(args, context, run) {
|
|
|
17589
17905
|
return direct;
|
|
17590
17906
|
}
|
|
17591
17907
|
const sameDocumentUrl = (a, b)=>String(a ?? "").replace(/[?#].*$/, "") === b.replace(/[?#].*$/, "");
|
|
17592
|
-
function tabByUrl(tabs, url) {
|
|
17908
|
+
function tabByUrl(tabs, url, seen) {
|
|
17593
17909
|
const exact = tabs.filter((tab)=>sameDocumentUrl(tab.url, url));
|
|
17594
17910
|
const covered = exact.length ? exact : tabs.filter((tab)=>matchPatternCovers(url, String(tab.url ?? "")));
|
|
17595
17911
|
const candidates = covered.length ? covered : tabs.filter((tab)=>String(tab.url ?? "").includes(url));
|
|
17912
|
+
if (seen) seen.matched = candidates.length;
|
|
17596
17913
|
return candidates.find((tab)=>true === tab.selected) ?? candidates[0];
|
|
17597
17914
|
}
|
|
17598
|
-
async function evaluateInGeckoTab(args, browser, select, missing) {
|
|
17915
|
+
async function evaluateInGeckoTab(args, browser, select, missing, seen) {
|
|
17599
17916
|
const resolved = await resolveRdpPort(args.projectPath, browser, {
|
|
17600
17917
|
waitMs: 3000,
|
|
17601
17918
|
graceMs: 1000
|
|
@@ -17625,7 +17942,11 @@ async function evaluateInGeckoTab(args, browser, select, missing) {
|
|
|
17625
17942
|
ok: true,
|
|
17626
17943
|
command: eval_schema.name,
|
|
17627
17944
|
status: "ok",
|
|
17628
|
-
value: void 0 === outcome.value ? null : outcome.value
|
|
17945
|
+
value: void 0 === outcome.value ? null : outcome.value,
|
|
17946
|
+
warnings: [
|
|
17947
|
+
`Evaluated in the tab showing ${outcome.tab.url || "an unknown url"}${outcome.tab.title ? ` ("${outcome.tab.title}")` : ""}.`,
|
|
17948
|
+
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
|
|
17949
|
+
]
|
|
17629
17950
|
});
|
|
17630
17951
|
if ("TargetNotFound" === outcome.name) return envelope_envelope({
|
|
17631
17952
|
ok: false,
|
|
@@ -17649,16 +17970,18 @@ async function evaluateInGeckoTab(args, browser, select, missing) {
|
|
|
17649
17970
|
},
|
|
17650
17971
|
hint: "The expression returned a promise that is still pending; pass a larger timeout to wait for it."
|
|
17651
17972
|
});
|
|
17973
|
+
const lost = "EvalLost" === outcome.name;
|
|
17974
|
+
const unsupported = "Unsupported" === outcome.name;
|
|
17652
17975
|
return envelope_envelope({
|
|
17653
17976
|
ok: false,
|
|
17654
17977
|
command: eval_schema.name,
|
|
17655
|
-
status: "eval-failed",
|
|
17978
|
+
status: lost ? "eval-lost" : unsupported ? "eval-unsupported" : "eval-failed",
|
|
17656
17979
|
error: {
|
|
17657
17980
|
code: "E_EVAL",
|
|
17658
17981
|
name: outcome.name,
|
|
17659
17982
|
message: outcome.message
|
|
17660
17983
|
},
|
|
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."
|
|
17984
|
+
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
17985
|
});
|
|
17663
17986
|
}
|
|
17664
17987
|
function backgroundRouteRefusedStatements(args, browser, context, raw) {
|
|
@@ -17675,7 +17998,10 @@ async function evaluatePagePastSiteCsp(args, browser, context, raw) {
|
|
|
17675
17998
|
if (!args.url && null != args.tab) return null;
|
|
17676
17999
|
if (!isSingleExpression(args.expression)) return notOneExpression();
|
|
17677
18000
|
const url = args.url;
|
|
17678
|
-
|
|
18001
|
+
const seen = {
|
|
18002
|
+
matched: 0
|
|
18003
|
+
};
|
|
18004
|
+
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
18005
|
}
|
|
17680
18006
|
async function evaluateThroughExecuteScript(args, browser, context, raw) {
|
|
17681
18007
|
if ("page" !== context && "content" !== context) return null;
|
|
@@ -17835,13 +18161,10 @@ async function eval_handler(args) {
|
|
|
17835
18161
|
if (defaulted) try {
|
|
17836
18162
|
const parsed = JSON.parse(raw);
|
|
17837
18163
|
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).');
|
|
18164
|
+
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
18165
|
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
|
|
18166
|
+
const unreachable = "E_TARGET_NOT_FOUND" === code || /cannot access|chrome-extension:\/\/|chrome:\/\/|no active tab|missing host permission/i.test(JSON.stringify(parsed.error ?? ""));
|
|
18167
|
+
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
18168
|
return actFrameJson(parsed);
|
|
17846
18169
|
}
|
|
17847
18170
|
} catch {}
|
|
@@ -18549,7 +18872,8 @@ async function publish_handler(args) {
|
|
|
18549
18872
|
ref,
|
|
18550
18873
|
api: args.api
|
|
18551
18874
|
});
|
|
18552
|
-
if (buildsRes.ok) {
|
|
18875
|
+
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.`;
|
|
18876
|
+
else if (buildsRes.ok) {
|
|
18553
18877
|
const items = parseBuildIndex(buildsRes.json);
|
|
18554
18878
|
if (args.buildSha) {
|
|
18555
18879
|
const pin = String(args.buildSha).toLowerCase();
|
|
@@ -18571,13 +18895,18 @@ async function publish_handler(args) {
|
|
|
18571
18895
|
}
|
|
18572
18896
|
} else {
|
|
18573
18897
|
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.
|
|
18898
|
+
const platformSha = "string" == typeof data.buildSha && data.buildSha ? String(data.buildSha).toLowerCase() : null;
|
|
18899
|
+
const served = platformSha ? items.find((item)=>item.sha.toLowerCase().startsWith(platformSha) || platformSha.startsWith(item.sha.toLowerCase())) : newestSuccess;
|
|
18900
|
+
if (served) {
|
|
18901
|
+
if (null == data.buildSha) data.buildSha = served.sha;
|
|
18902
|
+
if (null == data.builtAt && served.timestamp) data.builtAt = served.timestamp;
|
|
18903
|
+
if (null == data.version && served.version) data.version = served.version;
|
|
18904
|
+
if (null == data.channel && served.channel) data.channel = served.channel;
|
|
18905
|
+
data.registryUrl = buildsUrl;
|
|
18906
|
+
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.";
|
|
18907
|
+
} else if (platformSha) {
|
|
18579
18908
|
data.registryUrl = buildsUrl;
|
|
18580
|
-
buildNote =
|
|
18909
|
+
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
18910
|
}
|
|
18582
18911
|
}
|
|
18583
18912
|
}
|
|
@@ -18845,7 +19174,7 @@ async function release_promote_handler(args) {
|
|
|
18845
19174
|
api: args.api
|
|
18846
19175
|
});
|
|
18847
19176
|
if (channelsRes.ok) {
|
|
18848
|
-
const rows = parseChannels(channelsRes.json).filter((c)=>c.sha);
|
|
19177
|
+
const rows = channelsShapeProblem(channelsRes.json) ? [] : parseChannels(channelsRes.json).filter((c)=>c.sha);
|
|
18849
19178
|
enrich.validChannelShas = Object.fromEntries(rows.map((c)=>[
|
|
18850
19179
|
c.channel,
|
|
18851
19180
|
c.sha
|
|
@@ -19192,7 +19521,7 @@ async function readReleases(args) {
|
|
|
19192
19521
|
const channelsUrl = registryFileUrl(ref, "channels.json");
|
|
19193
19522
|
const metaUrl = registryFileUrl(ref, "meta.json");
|
|
19194
19523
|
const buildsUrl = registryFileUrl(ref, "builds/index.json");
|
|
19195
|
-
const [
|
|
19524
|
+
const [channelsRaw, metaRes, buildsRaw] = await Promise.all([
|
|
19196
19525
|
fetchRegistryJson(channelsUrl, fetch, {
|
|
19197
19526
|
ref,
|
|
19198
19527
|
api: args.api
|
|
@@ -19206,6 +19535,8 @@ async function readReleases(args) {
|
|
|
19206
19535
|
api: args.api
|
|
19207
19536
|
})
|
|
19208
19537
|
]);
|
|
19538
|
+
const channelsRes = requireShape(channelsRaw, channelsShapeProblem);
|
|
19539
|
+
const buildsRes = requireShape(buildsRaw, buildIndexShapeProblem);
|
|
19209
19540
|
const buildsPageUrl = consoleProjectUrl(ref, "builds", args.api);
|
|
19210
19541
|
const heldRead = [
|
|
19211
19542
|
channelsRes,
|
|
@@ -19730,7 +20061,7 @@ async function submit_handler(args) {
|
|
|
19730
20061
|
health = stores && "object" == typeof stores ? stores : null;
|
|
19731
20062
|
if (!health) healthUnreadable = "stores/health.json had no stores map";
|
|
19732
20063
|
} else healthUnreadable = healthRes.message;
|
|
19733
|
-
if (channelsRes.ok) channelRows = parseChannels(channelsRes.json);
|
|
20064
|
+
if (channelsRes.ok && !channelsShapeProblem(channelsRes.json)) channelRows = parseChannels(channelsRes.json);
|
|
19734
20065
|
} else healthUnreadable = "no stored workspace/project to look up (run extension_auth)";
|
|
19735
20066
|
const preflight = browsers.map((browser)=>{
|
|
19736
20067
|
if (!health) return {
|
|
@@ -20360,6 +20691,36 @@ async function wait_handler(args) {
|
|
|
20360
20691
|
continue;
|
|
20361
20692
|
}
|
|
20362
20693
|
lastContractStatus = contract.status;
|
|
20694
|
+
if ("build" === contract.command) return envelope_envelope({
|
|
20695
|
+
ok: false,
|
|
20696
|
+
command: wait_schema.name,
|
|
20697
|
+
status: "no-session",
|
|
20698
|
+
error: {
|
|
20699
|
+
code: "E_NO_SESSION",
|
|
20700
|
+
message: `${readyPath} is the contract a finished extension_build left (command: build), not a running session, so there is nothing to wait for.`
|
|
20701
|
+
},
|
|
20702
|
+
value: {
|
|
20703
|
+
readyPath,
|
|
20704
|
+
budgetMs,
|
|
20705
|
+
elapsedMs: Date.now() - start
|
|
20706
|
+
},
|
|
20707
|
+
hint: "Start a session with extension_dev (or extension_start), then call extension_wait."
|
|
20708
|
+
});
|
|
20709
|
+
if ("stopped" === contract.status) return envelope_envelope({
|
|
20710
|
+
ok: false,
|
|
20711
|
+
command: wait_schema.name,
|
|
20712
|
+
status: "stopped",
|
|
20713
|
+
error: {
|
|
20714
|
+
code: "E_SESSION_EXITED",
|
|
20715
|
+
message: `${readyPath} records status: stopped; that session was stopped and nothing will become ready.`
|
|
20716
|
+
},
|
|
20717
|
+
value: {
|
|
20718
|
+
readyPath,
|
|
20719
|
+
budgetMs,
|
|
20720
|
+
elapsedMs: Date.now() - start
|
|
20721
|
+
},
|
|
20722
|
+
hint: "Start a session with extension_dev, then call extension_wait."
|
|
20723
|
+
});
|
|
20363
20724
|
if ("ready" === contract.status) {
|
|
20364
20725
|
if ("number" == typeof contract.pid && !wait_isAlive(contract.pid)) return envelope_envelope({
|
|
20365
20726
|
ok: false,
|
|
@@ -20431,7 +20792,7 @@ async function wait_handler(args) {
|
|
|
20431
20792
|
if (!attached && ("start" === contract.command || "preview" === contract.command)) return envelope_envelope({
|
|
20432
20793
|
ok: true,
|
|
20433
20794
|
command: wait_schema.name,
|
|
20434
|
-
status: "
|
|
20795
|
+
status: "build-ready",
|
|
20435
20796
|
value: {
|
|
20436
20797
|
compiled: true,
|
|
20437
20798
|
browserAttached: false,
|
|
@@ -20537,6 +20898,26 @@ async function wait_handler(args) {
|
|
|
20537
20898
|
],
|
|
20538
20899
|
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
20900
|
});
|
|
20901
|
+
if (null === lastContractStatus && !contractUnreadable && !staleContractNote && !findSessionInfo(args.projectPath, browser)) return envelope_envelope({
|
|
20902
|
+
ok: false,
|
|
20903
|
+
command: wait_schema.name,
|
|
20904
|
+
status: "no-session",
|
|
20905
|
+
error: {
|
|
20906
|
+
code: "E_NO_SESSION",
|
|
20907
|
+
message: `No ${browser} session is known for ${args.projectPath}: this server registered none and no contract appeared at ${readyPath} in ${budgetMs} ms.`
|
|
20908
|
+
},
|
|
20909
|
+
value: {
|
|
20910
|
+
compiled: false,
|
|
20911
|
+
browserAttached: false,
|
|
20912
|
+
readyPath,
|
|
20913
|
+
budgetMs,
|
|
20914
|
+
elapsedMs: Date.now() - start
|
|
20915
|
+
},
|
|
20916
|
+
warnings: [
|
|
20917
|
+
clampNote
|
|
20918
|
+
],
|
|
20919
|
+
hint: "Check projectPath and browser, or start a session with extension_dev, then call extension_wait."
|
|
20920
|
+
});
|
|
20540
20921
|
return envelope_envelope({
|
|
20541
20922
|
ok: false,
|
|
20542
20923
|
command: wait_schema.name,
|
|
@@ -23011,7 +23392,9 @@ async function runCli(cmd, args) {
|
|
|
23011
23392
|
} catch {
|
|
23012
23393
|
parsed = null;
|
|
23013
23394
|
}
|
|
23014
|
-
|
|
23395
|
+
if (parsed?.ok === true) return 0;
|
|
23396
|
+
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.");
|
|
23397
|
+
return 1;
|
|
23015
23398
|
}
|
|
23016
23399
|
log("Usage: extension-mcp release promote --build <sha> --channel <channel>");
|
|
23017
23400
|
return 1;
|