@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/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.10","El":{"OP":"4.1.31"}}');
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 data = await res.json().catch(()=>({}));
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 (default 5000)"
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
- } catch {}
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 was read from the build summary the engine writes into dist/extension-js/. 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.");
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 was read from the build summary the engine writes into dist/extension-js/. 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.`);
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
- for (const pid of pids)try {
6796
- process.kill(pid, "SIGKILL");
6797
- } catch {}
6798
- return pids;
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 !== reaped.length,
6971
+ stopped: reaped.length > 0 && 0 === unconfirmed.length,
6842
6972
  reaped,
6973
+ ...unconfirmed.length ? {
6974
+ reapUnconfirmed: unconfirmed
6975
+ } : {},
6843
6976
  ...cleanCarrier(projectPath),
6844
- detail: 0 === reaped.length ? "No known session for this project/browser (nothing registered in this server and no ready.json contract found)." : `No dev pid on record, but reaped ${reaped.length} orphaned browser process(es) from the profile dir.`
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: "Server port (0 for auto-assign)"
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: "Serve without launching a browser"
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: building ? "started" : "launched",
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 ? "Use extension_wait to check when the build and browser launch are complete. When you are done, call extension_stop to shut down the session." : "Call extension_stop when you are done to close the preview browser.",
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(()=>null);
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
- reason: wantedId ? `no panel with id ${wantedId} registered within ${budgetMs}ms` : `no panel from ${prefix} registered within ${budgetMs}ms`
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
- while(!panelTarget && Date.now() < showDeadline){
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
- const fresh = frames.find((t)=>!framesBefore.has(t.id)) ?? frames[0];
9419
- if (fresh) panelTarget = {
9420
- targetId: fresh.id,
9421
- url: String(fresh.url)
9422
- };
9423
- else await sleep(200);
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
- "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."
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
- const artifacts = Array.isArray(data.artifacts) ? data.artifacts.map((artifact)=>artifact && "string" == typeof artifact.revokeUrl ? {
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, streamNote, {
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 && frame.runId) runId = String(frame.runId);
13101
- else if ("log" === frame.type && frame.event) {
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) return void finish();
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, because the MV3 service worker CSP blocks eval; pass context:'background' to target the worker anyway and get that explanation back. 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}.",
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
- if (!wake.woken) return envelope_envelope({
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 = wake.targets;
17276
- wakeWarnings.push("The background worker was idle (Chrome stops an MV3 service worker after about 30 s without events) and was started through ServiceWorker.startWorker before evaluating; state it held before idling is gone unless it was persisted.");
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: live.length > 1 ? [
17455
- `${live.length} tabs match ${url}; evaluated in ${target.url} (target ${target.targetId}). Narrow the url to pick another.`
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
- return evaluateInGeckoTab(args, browser, url ? (tabs)=>tabByUrl(tabs, url) : (tabs)=>tabs.find((tab)=>true === tab.selected), url ? `No open tab matches url: ${url}` : "No tab is selected in the dev browser.");
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
- patchValue(parsed, {
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:\/\//i.test(JSON.stringify(parsed.error ?? ""));
17844
- if (false === parsed.ok && unreachable) parsed.hint = "The active tab is a browser or extension page that eval cannot reach. Navigate the dev browser to a regular web page, or pass url (match pattern) or tab to pick one; extension_dom_snapshot with listTabs: true lists open tabs.";
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
- if (newestSuccess) {
18575
- if (null == data.buildSha) data.buildSha = newestSuccess.sha;
18576
- if (null == data.builtAt && newestSuccess.timestamp) data.builtAt = newestSuccess.timestamp;
18577
- if (null == data.version && newestSuccess.version) data.version = newestSuccess.version;
18578
- if (null == data.channel && newestSuccess.channel) data.channel = newestSuccess.channel;
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 = "buildSha/builtAt/version describe the newest successful build in the project's registry index, which is what the share link serves. Pin buildSha to serve a specific build.";
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 [channelsRes, metaRes, buildsRes] = await Promise.all([
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: "launched",
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
- return parsed?.ok === false ? 1 : 0;
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;