@extension.dev/mcp 10.1.0 → 10.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +100 -0
- package/README.md +5 -4
- package/dist/module.d.ts +1 -1
- package/dist/module.js +1863 -593
- package/dist/src/index.d.ts +9 -0
- package/dist/src/lib/artifacts-api.d.ts +2 -0
- package/dist/src/lib/boot-verdict.d.ts +0 -1
- package/dist/src/lib/carrier-exit.d.ts +8 -0
- package/dist/src/lib/carrier-registry.d.ts +4 -0
- package/dist/src/lib/carrier.d.ts +8 -0
- package/dist/src/lib/engine-version.d.ts +24 -0
- package/dist/src/lib/envelope.d.ts +1 -1
- package/dist/src/lib/guest-load-oracle.d.ts +1 -0
- package/dist/src/lib/login-flow.d.ts +1 -1
- package/dist/src/lib/process-manager.d.ts +1 -0
- package/dist/src/lib/server-identity.d.ts +16 -0
- package/dist/src/lib/session-paths.d.ts +12 -0
- package/dist/src/lib/share-cors-probe.d.ts +17 -0
- package/dist/src/lib/template-artifact-source.d.ts +1 -1
- package/dist/src/lib/zip-entries.d.ts +5 -0
- package/dist/src/tools/build.d.ts +23 -0
- package/dist/src/tools/logs-constants.d.ts +2 -2
- package/dist/src/tools/logs-filter.d.ts +3 -1
- package/dist/src/tools/logs.d.ts +8 -0
- package/dist/src/tools/whoami.d.ts +3 -1
- package/package.json +20 -4
- package/server.json +2 -2
package/dist/module.js
CHANGED
|
@@ -8,6 +8,7 @@ import { extensionCreate } from "extension-create";
|
|
|
8
8
|
import node_os from "node:os";
|
|
9
9
|
import cross_spawn from "cross-spawn";
|
|
10
10
|
import node_crypto, { createHash } from "node:crypto";
|
|
11
|
+
import { CLOSE_BAD_HELLO, CLOSE_BAD_INSTANCE, CLOSE_CONTROL_UNAVAILABLE, CLOSE_SLOW_CONSUMER, CONTROL_ENVELOPE_VERSION, CONTROL_WS_PATH, buildSummaryPath, logsPath, matchesLogQuery, readLogEvents, readReadyContract, readyContractPath as bridge_readyContractPath, sessionArtifactsRootDir } from "extension-develop/bridge";
|
|
11
12
|
import { fileURLToPath } from "node:url";
|
|
12
13
|
import { execFile, execFileSync } from "node:child_process";
|
|
13
14
|
import ws_0 from "ws";
|
|
@@ -431,6 +432,7 @@ __webpack_require__.d(browsers_namespaceObject, {
|
|
|
431
432
|
var build_namespaceObject = {};
|
|
432
433
|
__webpack_require__.r(build_namespaceObject);
|
|
433
434
|
__webpack_require__.d(build_namespaceObject, {
|
|
435
|
+
BUNDLE_ID_PATTERN: ()=>BUNDLE_ID_PATTERN,
|
|
434
436
|
handler: ()=>build_handler,
|
|
435
437
|
schema: ()=>build_schema
|
|
436
438
|
});
|
|
@@ -481,6 +483,7 @@ __webpack_require__.d(list_extensions_namespaceObject, {
|
|
|
481
483
|
var logs_namespaceObject = {};
|
|
482
484
|
__webpack_require__.r(logs_namespaceObject);
|
|
483
485
|
__webpack_require__.d(logs_namespaceObject, {
|
|
486
|
+
controlRefusal: ()=>controlRefusal,
|
|
484
487
|
handler: ()=>logs_handler,
|
|
485
488
|
schema: ()=>logs_schema_schema
|
|
486
489
|
});
|
|
@@ -579,7 +582,7 @@ __webpack_require__.d(wait_namespaceObject, {
|
|
|
579
582
|
handler: ()=>wait_handler,
|
|
580
583
|
schema: ()=>wait_schema
|
|
581
584
|
});
|
|
582
|
-
var package_namespaceObject = JSON.parse('{"rE":"10.
|
|
585
|
+
var package_namespaceObject = JSON.parse('{"rE":"10.3.0","El":{"OP":"4.0.20"}}');
|
|
583
586
|
const ENVELOPE_SCHEMA = 1;
|
|
584
587
|
const collectWarnings = (warnings)=>{
|
|
585
588
|
if (!warnings) return [];
|
|
@@ -605,7 +608,7 @@ function envelopeObject(init) {
|
|
|
605
608
|
if ("string" == typeof init.hint && init.hint) frame.hint = init.hint;
|
|
606
609
|
return frame;
|
|
607
610
|
}
|
|
608
|
-
function
|
|
611
|
+
function envelope_envelope(init) {
|
|
609
612
|
return JSON.stringify(envelopeObject(init));
|
|
610
613
|
}
|
|
611
614
|
function sessionCommandSinceEnvelopeOwnsCommand(contract) {
|
|
@@ -769,7 +772,7 @@ async function spawnFailedEnvelope(command, spawned) {
|
|
|
769
772
|
await new Promise((resolve)=>setTimeout(resolve, 25));
|
|
770
773
|
cause = spawned.spawnError?.() ?? null;
|
|
771
774
|
}
|
|
772
|
-
return
|
|
775
|
+
return envelope_envelope({
|
|
773
776
|
ok: false,
|
|
774
777
|
command,
|
|
775
778
|
status: "spawn-failed",
|
|
@@ -865,7 +868,11 @@ function tokenTtlNote(workspaceSlug, projectSlug) {
|
|
|
865
868
|
function resolveApiBase(api) {
|
|
866
869
|
return String(api || process.env.EXTENSION_DEV_API_URL || DEFAULT_API).replace(/\/+$/, "");
|
|
867
870
|
}
|
|
868
|
-
function
|
|
871
|
+
function isExtensionDevHost(hostname) {
|
|
872
|
+
const host = hostname.toLowerCase().replace(/\.+$/, "");
|
|
873
|
+
return "extension.dev" === host || host.endsWith(".extension.dev");
|
|
874
|
+
}
|
|
875
|
+
function safeApiBase(raw, callerSupplied) {
|
|
869
876
|
let parsed;
|
|
870
877
|
try {
|
|
871
878
|
parsed = new URL(raw);
|
|
@@ -880,6 +887,11 @@ function safeApiBase(raw) {
|
|
|
880
887
|
ok: false,
|
|
881
888
|
message: `Refusing to send the access token to ${raw}: use https (http is allowed only for localhost).`
|
|
882
889
|
};
|
|
890
|
+
const fromCaller = String(callerSupplied || "").trim();
|
|
891
|
+
if (fromCaller && !isLocalhost && !isExtensionDevHost(parsed.hostname)) return {
|
|
892
|
+
ok: false,
|
|
893
|
+
message: `Refusing to send the access token to ${raw}: an api argument may only name an extension.dev host or a local dev server. To use a self-hosted platform, set EXTENSION_DEV_API_URL where the server is launched instead of passing it per call.`
|
|
894
|
+
};
|
|
883
895
|
return {
|
|
884
896
|
ok: true,
|
|
885
897
|
base: `${parsed.origin}${parsed.pathname}`.replace(/\/+$/, "")
|
|
@@ -1081,7 +1093,7 @@ class RegistryAccessTokens {
|
|
|
1081
1093
|
status: "no-credential"
|
|
1082
1094
|
};
|
|
1083
1095
|
}
|
|
1084
|
-
const check = safeApiBase(resolveApiBase(apiHint || creds?.api));
|
|
1096
|
+
const check = safeApiBase(resolveApiBase(apiHint || creds?.api), apiHint);
|
|
1085
1097
|
if (!check.ok) return {
|
|
1086
1098
|
status: "denied",
|
|
1087
1099
|
message: check.message
|
|
@@ -1415,7 +1427,7 @@ async function create_handler(args) {
|
|
|
1415
1427
|
error: capture("error")
|
|
1416
1428
|
}
|
|
1417
1429
|
});
|
|
1418
|
-
const failure = (err, transient)=>transient ?
|
|
1430
|
+
const failure = (err, transient)=>transient ? envelope_envelope({
|
|
1419
1431
|
ok: false,
|
|
1420
1432
|
command: COMMAND,
|
|
1421
1433
|
status: "template-fetch-failed",
|
|
@@ -1428,7 +1440,7 @@ async function create_handler(args) {
|
|
|
1428
1440
|
duration: Date.now() - start,
|
|
1429
1441
|
log: logTail()
|
|
1430
1442
|
}
|
|
1431
|
-
}) :
|
|
1443
|
+
}) : envelope_envelope({
|
|
1432
1444
|
ok: false,
|
|
1433
1445
|
command: COMMAND,
|
|
1434
1446
|
status: "scaffold-failed",
|
|
@@ -1455,7 +1467,7 @@ async function create_handler(args) {
|
|
|
1455
1467
|
}
|
|
1456
1468
|
}
|
|
1457
1469
|
const hasManifest = node_fs.existsSync(node_path.join(result.projectPath, "manifest.json")) || node_fs.existsSync(node_path.join(result.projectPath, "src", "manifest.json"));
|
|
1458
|
-
if (!hasManifest) return
|
|
1470
|
+
if (!hasManifest) return envelope_envelope({
|
|
1459
1471
|
ok: false,
|
|
1460
1472
|
command: COMMAND,
|
|
1461
1473
|
status: "scaffold-incomplete",
|
|
@@ -1483,7 +1495,7 @@ async function create_handler(args) {
|
|
|
1483
1495
|
const deployUrl = `${wwwOrigin}${(0, paths_0.wwwNewPath)({
|
|
1484
1496
|
template: result.template
|
|
1485
1497
|
})}`;
|
|
1486
|
-
return
|
|
1498
|
+
return envelope_envelope({
|
|
1487
1499
|
ok: true,
|
|
1488
1500
|
command: COMMAND,
|
|
1489
1501
|
status: "created",
|
|
@@ -1525,7 +1537,7 @@ async function create_handler(args) {
|
|
|
1525
1537
|
}
|
|
1526
1538
|
const DEFAULT_MEDIA_ORIGIN = "https://media.extension.land";
|
|
1527
1539
|
const DEFAULT_CHANNEL = "latest";
|
|
1528
|
-
const PINNED_COMMIT = "
|
|
1540
|
+
const PINNED_COMMIT = "520a8ede767fe1937c7184f4857408caf9c5934f";
|
|
1529
1541
|
const CHANNEL_CACHE_TTL_MS = 300000;
|
|
1530
1542
|
function mediaOrigin() {
|
|
1531
1543
|
return (process.env.EXTENSION_MEDIA_ORIGIN || "").trim() || DEFAULT_MEDIA_ORIGIN;
|
|
@@ -1733,7 +1745,7 @@ async function searchTemplates(args) {
|
|
|
1733
1745
|
repositoryUrl: t.repositoryUrl,
|
|
1734
1746
|
downloads: t.downloads
|
|
1735
1747
|
}));
|
|
1736
|
-
return
|
|
1748
|
+
return envelope_envelope({
|
|
1737
1749
|
ok: true,
|
|
1738
1750
|
command: "extension_templates",
|
|
1739
1751
|
status: "listed",
|
|
@@ -1746,7 +1758,7 @@ async function searchTemplates(args) {
|
|
|
1746
1758
|
const get_template_source_COMMAND = "extension_templates";
|
|
1747
1759
|
async function readTemplateSource(args) {
|
|
1748
1760
|
const template = await getTemplateBySlug(args.slug);
|
|
1749
|
-
if (!template) return
|
|
1761
|
+
if (!template) return envelope_envelope({
|
|
1750
1762
|
ok: false,
|
|
1751
1763
|
command: get_template_source_COMMAND,
|
|
1752
1764
|
status: "template-not-found",
|
|
@@ -1766,7 +1778,7 @@ async function readTemplateSource(args) {
|
|
|
1766
1778
|
keyFiles: template.keyFiles,
|
|
1767
1779
|
repositoryUrl: template.repositoryUrl
|
|
1768
1780
|
};
|
|
1769
|
-
if (!args.files?.length) return
|
|
1781
|
+
if (!args.files?.length) return envelope_envelope({
|
|
1770
1782
|
ok: true,
|
|
1771
1783
|
command: get_template_source_COMMAND,
|
|
1772
1784
|
status: "file-list",
|
|
@@ -1791,7 +1803,7 @@ async function readTemplateSource(args) {
|
|
|
1791
1803
|
} catch {}
|
|
1792
1804
|
errors.push(`${filePath}: ${lastStatus || "fetch failed"}`);
|
|
1793
1805
|
}));
|
|
1794
|
-
return
|
|
1806
|
+
return envelope_envelope({
|
|
1795
1807
|
ok: 0 === errors.length,
|
|
1796
1808
|
command: get_template_source_COMMAND,
|
|
1797
1809
|
status: errors.length ? "partial" : "read",
|
|
@@ -1873,7 +1885,7 @@ const templates_schema = {
|
|
|
1873
1885
|
};
|
|
1874
1886
|
async function templates_handler(args) {
|
|
1875
1887
|
if ((args.action ?? "list") === "source" || !args.action && args.slug) {
|
|
1876
|
-
if (!args.slug) return
|
|
1888
|
+
if (!args.slug) return envelope_envelope({
|
|
1877
1889
|
ok: false,
|
|
1878
1890
|
command: "extension_templates",
|
|
1879
1891
|
status: "bad-request",
|
|
@@ -1957,14 +1969,239 @@ const LAUNCH_BROWSER = {
|
|
|
1957
1969
|
enum: LAUNCHABLE_BROWSERS,
|
|
1958
1970
|
default: "chrome"
|
|
1959
1971
|
};
|
|
1972
|
+
const OUTPUT_JSON_FLOOR = {
|
|
1973
|
+
act: "3.18.1",
|
|
1974
|
+
doctor: "4.0.11",
|
|
1975
|
+
dev: "4.0.17",
|
|
1976
|
+
build: "4.0.17"
|
|
1977
|
+
};
|
|
1978
|
+
const VERDICT_TTL_MS = 60000;
|
|
1979
|
+
const PROBE_TIMEOUT_MS = 20000;
|
|
1980
|
+
const SEMVER = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
1981
|
+
function decompose(version) {
|
|
1982
|
+
const match = SEMVER.exec(version.trim());
|
|
1983
|
+
if (!match) return null;
|
|
1984
|
+
return {
|
|
1985
|
+
release: [
|
|
1986
|
+
Number(match[1]),
|
|
1987
|
+
Number(match[2]),
|
|
1988
|
+
Number(match[3])
|
|
1989
|
+
],
|
|
1990
|
+
prerelease: match[4] ? match[4].split(".") : []
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
function parseVersion(text) {
|
|
1994
|
+
for (const line of text.split("\n")){
|
|
1995
|
+
const candidate = line.trim();
|
|
1996
|
+
if (candidate) {
|
|
1997
|
+
if (decompose(candidate)) return candidate.replace(/^v/, "");
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
return null;
|
|
2001
|
+
}
|
|
2002
|
+
function compareNumericIdentifiers(a, b) {
|
|
2003
|
+
const left = a.replace(/^0+(?=\d)/, "");
|
|
2004
|
+
const right = b.replace(/^0+(?=\d)/, "");
|
|
2005
|
+
if (left.length !== right.length) return left.length < right.length ? -1 : 1;
|
|
2006
|
+
if (left === right) return 0;
|
|
2007
|
+
return left < right ? -1 : 1;
|
|
2008
|
+
}
|
|
2009
|
+
function compareVersions(a, b) {
|
|
2010
|
+
const left = decompose(a);
|
|
2011
|
+
const right = decompose(b);
|
|
2012
|
+
if (!left || !right) return null;
|
|
2013
|
+
for(let i = 0; i < 3; i += 1)if (left.release[i] !== right.release[i]) return left.release[i] < right.release[i] ? -1 : 1;
|
|
2014
|
+
if (!left.prerelease.length && !right.prerelease.length) return 0;
|
|
2015
|
+
if (!left.prerelease.length) return 1;
|
|
2016
|
+
if (!right.prerelease.length) return -1;
|
|
2017
|
+
const length = Math.max(left.prerelease.length, right.prerelease.length);
|
|
2018
|
+
for(let i = 0; i < length; i += 1){
|
|
2019
|
+
const one = left.prerelease[i];
|
|
2020
|
+
const other = right.prerelease[i];
|
|
2021
|
+
if (void 0 === one) return -1;
|
|
2022
|
+
if (void 0 === other) return 1;
|
|
2023
|
+
const oneIsNumeric = /^\d+$/.test(one);
|
|
2024
|
+
const otherIsNumeric = /^\d+$/.test(other);
|
|
2025
|
+
if (oneIsNumeric && otherIsNumeric) {
|
|
2026
|
+
const ordering = compareNumericIdentifiers(one, other);
|
|
2027
|
+
if (0 !== ordering) return ordering;
|
|
2028
|
+
continue;
|
|
2029
|
+
}
|
|
2030
|
+
if (oneIsNumeric !== otherIsNumeric) return oneIsNumeric ? -1 : 1;
|
|
2031
|
+
if (one !== other) return one < other ? -1 : 1;
|
|
2032
|
+
}
|
|
2033
|
+
return 0;
|
|
2034
|
+
}
|
|
2035
|
+
const verdicts = new Map();
|
|
2036
|
+
function invocationKey(command, prefixArgs) {
|
|
2037
|
+
return [
|
|
2038
|
+
command,
|
|
2039
|
+
...prefixArgs
|
|
2040
|
+
].join("\0");
|
|
2041
|
+
}
|
|
2042
|
+
const NPX_PIN = /^extension@(.+)$/;
|
|
2043
|
+
function parseCapabilities(stdout) {
|
|
2044
|
+
for (const line of stdout.split("\n")){
|
|
2045
|
+
const candidate = line.trim();
|
|
2046
|
+
if (!candidate.startsWith("{")) continue;
|
|
2047
|
+
let frame;
|
|
2048
|
+
try {
|
|
2049
|
+
frame = JSON.parse(candidate);
|
|
2050
|
+
} catch {
|
|
2051
|
+
continue;
|
|
2052
|
+
}
|
|
2053
|
+
if (!frame || "object" != typeof frame) continue;
|
|
2054
|
+
const envelope = frame;
|
|
2055
|
+
if (1 !== envelope.schema) continue;
|
|
2056
|
+
if (true !== envelope.ok) continue;
|
|
2057
|
+
if ("capabilities" !== envelope.command) continue;
|
|
2058
|
+
const value = envelope.value;
|
|
2059
|
+
if (!value || "object" != typeof value) continue;
|
|
2060
|
+
const version = "string" == typeof value.version ? parseVersion(value.version) : null;
|
|
2061
|
+
const roster = value.outputJsonCommands;
|
|
2062
|
+
if (version && Array.isArray(roster)) {
|
|
2063
|
+
if (roster.every((name)=>"string" == typeof name)) return {
|
|
2064
|
+
version,
|
|
2065
|
+
outputJsonCommands: roster
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
return null;
|
|
2070
|
+
}
|
|
2071
|
+
async function resolvedEngineFacts(projectPath) {
|
|
2072
|
+
const { command, prefixArgs } = resolveExtensionInvocation(projectPath);
|
|
2073
|
+
const key = invocationKey(command, prefixArgs);
|
|
2074
|
+
const cached = verdicts.get(key);
|
|
2075
|
+
if (cached && cached.expiresAt > Date.now()) return {
|
|
2076
|
+
version: cached.version,
|
|
2077
|
+
outputJsonCommands: cached.outputJsonCommands
|
|
2078
|
+
};
|
|
2079
|
+
const remember = (found)=>{
|
|
2080
|
+
verdicts.set(key, {
|
|
2081
|
+
...found,
|
|
2082
|
+
expiresAt: Date.now() + VERDICT_TTL_MS
|
|
2083
|
+
});
|
|
2084
|
+
return found;
|
|
2085
|
+
};
|
|
2086
|
+
for (const arg of prefixArgs){
|
|
2087
|
+
const pin = NPX_PIN.exec(arg);
|
|
2088
|
+
const pinned = pin ? parseVersion(pin[1]) : null;
|
|
2089
|
+
if (pinned) return remember({
|
|
2090
|
+
version: pinned,
|
|
2091
|
+
outputJsonCommands: null
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
try {
|
|
2095
|
+
const probe = await runExtensionCli([
|
|
2096
|
+
"capabilities"
|
|
2097
|
+
], {
|
|
2098
|
+
cwd: projectPath,
|
|
2099
|
+
timeoutMs: PROBE_TIMEOUT_MS
|
|
2100
|
+
});
|
|
2101
|
+
if (0 === probe.code) {
|
|
2102
|
+
const answered = parseCapabilities(probe.stdout ?? "");
|
|
2103
|
+
if (answered) return remember(answered);
|
|
2104
|
+
}
|
|
2105
|
+
} catch {}
|
|
2106
|
+
try {
|
|
2107
|
+
const probe = await runExtensionCli([
|
|
2108
|
+
"--version"
|
|
2109
|
+
], {
|
|
2110
|
+
cwd: projectPath,
|
|
2111
|
+
timeoutMs: PROBE_TIMEOUT_MS
|
|
2112
|
+
});
|
|
2113
|
+
if (0 !== probe.code) return remember({
|
|
2114
|
+
version: null,
|
|
2115
|
+
outputJsonCommands: null
|
|
2116
|
+
});
|
|
2117
|
+
return remember({
|
|
2118
|
+
version: parseVersion(probe.stdout ?? "") ?? parseVersion(probe.stderr ?? ""),
|
|
2119
|
+
outputJsonCommands: null
|
|
2120
|
+
});
|
|
2121
|
+
} catch {
|
|
2122
|
+
return remember({
|
|
2123
|
+
version: null,
|
|
2124
|
+
outputJsonCommands: null
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
const ROSTER_NAMES = {
|
|
2129
|
+
act: [
|
|
2130
|
+
"eval",
|
|
2131
|
+
"inspect",
|
|
2132
|
+
"open",
|
|
2133
|
+
"reload",
|
|
2134
|
+
"storage"
|
|
2135
|
+
],
|
|
2136
|
+
doctor: [
|
|
2137
|
+
"doctor"
|
|
2138
|
+
],
|
|
2139
|
+
dev: [
|
|
2140
|
+
"dev"
|
|
2141
|
+
],
|
|
2142
|
+
build: [
|
|
2143
|
+
"build"
|
|
2144
|
+
]
|
|
2145
|
+
};
|
|
2146
|
+
async function outputJsonVerdict(command, projectPath) {
|
|
2147
|
+
const floor = OUTPUT_JSON_FLOOR[command];
|
|
2148
|
+
let engine;
|
|
2149
|
+
try {
|
|
2150
|
+
engine = await resolvedEngineFacts(projectPath);
|
|
2151
|
+
} catch {
|
|
2152
|
+
return {
|
|
2153
|
+
supported: null,
|
|
2154
|
+
version: null,
|
|
2155
|
+
floor
|
|
2156
|
+
};
|
|
2157
|
+
}
|
|
2158
|
+
const roster = engine.outputJsonCommands;
|
|
2159
|
+
if (roster) return {
|
|
2160
|
+
supported: ROSTER_NAMES[command].every((name)=>roster.includes(name)),
|
|
2161
|
+
version: engine.version,
|
|
2162
|
+
floor
|
|
2163
|
+
};
|
|
2164
|
+
const version = engine.version;
|
|
2165
|
+
if (null === version) return {
|
|
2166
|
+
supported: null,
|
|
2167
|
+
version: null,
|
|
2168
|
+
floor
|
|
2169
|
+
};
|
|
2170
|
+
const ordering = compareVersions(version, floor);
|
|
2171
|
+
if (null === ordering) return {
|
|
2172
|
+
supported: null,
|
|
2173
|
+
version,
|
|
2174
|
+
floor
|
|
2175
|
+
};
|
|
2176
|
+
return {
|
|
2177
|
+
supported: ordering >= 0,
|
|
2178
|
+
version,
|
|
2179
|
+
floor
|
|
2180
|
+
};
|
|
2181
|
+
}
|
|
2182
|
+
const UNKNOWN_OUTPUT_FLAG = /unknown option[^\n]*--output\b/i;
|
|
2183
|
+
function refusedTheOutputFlag(stderr) {
|
|
2184
|
+
return UNKNOWN_OUTPUT_FLAG.test(stderr);
|
|
2185
|
+
}
|
|
2186
|
+
async function outputFlagRefusalMessage(command, cliName, projectPath) {
|
|
2187
|
+
const verdict = await outputJsonVerdict(command, projectPath);
|
|
2188
|
+
const preamble = `The Extension.js resolved for this project refused \`--output json\` on \`extension ${cliName}\`.`;
|
|
2189
|
+
const why = "You did not pass that flag: this server adds it so it can read a structured result instead of parsing a report written for a human, and unlike `build` there is no second source it can fall back to here, so it cannot simply drop it.";
|
|
2190
|
+
if (false === verdict.supported) return `${preamble} It reports ${verdict.version}, and that flag only reached \`extension ${cliName}\` in ${verdict.floor}. ${why} Upgrade the project's Extension.js to ${verdict.floor} or newer and run this again.`;
|
|
2191
|
+
if (true === verdict.supported) return `${preamble} It reports ${verdict.version}, which is at or above ${verdict.floor}, the release where that flag reached \`extension ${cliName}\`, so the binary being run is not the version it claims to be. ${why} Check the project's node_modules/.bin/extension and reinstall it, rather than upgrading a version that already looks new enough.`;
|
|
2192
|
+
return `${preamble} Its version could not be read, so the cause cannot be confirmed, but that flag only reached \`extension ${cliName}\` in ${verdict.floor} and a refusal is what an engine below that does. ${why} Check the project's Extension.js install and bring it to ${verdict.floor} or newer.`;
|
|
2193
|
+
}
|
|
1960
2194
|
const sessions = new Map();
|
|
1961
2195
|
const registrationStamps = new Map();
|
|
1962
2196
|
function sessionKey(projectPath, browser) {
|
|
1963
2197
|
return `${node_path.resolve(projectPath)}::${browser}`;
|
|
1964
2198
|
}
|
|
1965
|
-
function
|
|
2199
|
+
function sessionStateDir() {
|
|
1966
2200
|
return process.env.EXTENSION_MCP_SESSION_DIR || node_path.join(node_os.tmpdir(), "extension-dev-mcp-sessions");
|
|
1967
2201
|
}
|
|
2202
|
+
function markerDir() {
|
|
2203
|
+
return sessionStateDir();
|
|
2204
|
+
}
|
|
1968
2205
|
function markerPath(projectPath, browser) {
|
|
1969
2206
|
const digest = node_crypto.createHash("sha1").update(sessionKey(projectPath, browser)).digest("hex").slice(0, 16);
|
|
1970
2207
|
return node_path.join(markerDir(), `${digest}.json`);
|
|
@@ -2047,8 +2284,29 @@ function removeSession(projectPath, browser, pid) {
|
|
|
2047
2284
|
function listSessions() {
|
|
2048
2285
|
return Array.from(sessions.values());
|
|
2049
2286
|
}
|
|
2287
|
+
function sessionPathHint(file) {
|
|
2288
|
+
return `Looked at ${file} (the session-state layout this MCP's pinned engine publishes). If the project runs an older Extension.js, its layout may differ and this path will never appear.`;
|
|
2289
|
+
}
|
|
2290
|
+
function profilesRootDir(projectPath) {
|
|
2291
|
+
return node_path.join(sessionArtifactsRootDir(projectPath), "profiles");
|
|
2292
|
+
}
|
|
2293
|
+
function browserProfileRootDir(projectPath, browser) {
|
|
2294
|
+
return node_path.join(profilesRootDir(projectPath), `${browser}-profile`);
|
|
2295
|
+
}
|
|
2296
|
+
const PERSISTED_PROFILE_DIR_NAME = "dev";
|
|
2297
|
+
function profileRemediation(input) {
|
|
2298
|
+
const { projectPath, browser, profile } = input;
|
|
2299
|
+
const raw = "string" == typeof profile ? profile.trim() : "";
|
|
2300
|
+
if ("false" === raw.toLowerCase()) return `This session was launched with profile:"false", so it runs your real ${browser} profile and there is no Extension.js profile directory to remove. Quit the ${browser} window that holds it and start again.`;
|
|
2301
|
+
if (raw.length > 0) {
|
|
2302
|
+
const explicit = node_path.isAbsolute(raw) ? raw : node_path.resolve(projectPath, raw);
|
|
2303
|
+
return `This session was launched against the profile you passed, ${explicit}. Close whatever still holds it, or remove that directory, before retrying.`;
|
|
2304
|
+
}
|
|
2305
|
+
const root = browserProfileRootDir(projectPath, browser);
|
|
2306
|
+
return `The engine keeps this session's profile in a directory inside ${root}, one per run: "${PERSISTED_PROFILE_DIR_NAME}" when the profile is persisted, otherwise three random words drawn fresh on every start, which no caller can predict. List ${root} to see which run directories exist and remove the one the stuck browser holds, or remove ${root} entirely once no session is running.`;
|
|
2307
|
+
}
|
|
2050
2308
|
function contractSightings(projectPath) {
|
|
2051
|
-
const root =
|
|
2309
|
+
const root = sessionArtifactsRootDir(projectPath);
|
|
2052
2310
|
let dirs;
|
|
2053
2311
|
try {
|
|
2054
2312
|
dirs = node_fs.readdirSync(root);
|
|
@@ -2057,7 +2315,7 @@ function contractSightings(projectPath) {
|
|
|
2057
2315
|
}
|
|
2058
2316
|
const sightings = [];
|
|
2059
2317
|
for (const dir of dirs){
|
|
2060
|
-
const readyPath =
|
|
2318
|
+
const readyPath = bridge_readyContractPath(projectPath, dir);
|
|
2061
2319
|
try {
|
|
2062
2320
|
const stat = node_fs.statSync(readyPath);
|
|
2063
2321
|
const contract = JSON.parse(node_fs.readFileSync(readyPath, "utf8"));
|
|
@@ -2126,7 +2384,7 @@ function deadReadySession(projectPath) {
|
|
|
2126
2384
|
return null;
|
|
2127
2385
|
}
|
|
2128
2386
|
function contractBoundPort(projectPath, browser, since) {
|
|
2129
|
-
const readyPath =
|
|
2387
|
+
const readyPath = bridge_readyContractPath(projectPath, browser);
|
|
2130
2388
|
try {
|
|
2131
2389
|
const stat = node_fs.statSync(readyPath);
|
|
2132
2390
|
if (stat.mtimeMs < since) return null;
|
|
@@ -2189,9 +2447,18 @@ function isChromiumFamily(browser) {
|
|
|
2189
2447
|
function isGeckoFamily(browser) {
|
|
2190
2448
|
return GECKO_FAMILY.has(browser);
|
|
2191
2449
|
}
|
|
2450
|
+
function insideRepository(projectPath) {
|
|
2451
|
+
let current = node_path.resolve(projectPath);
|
|
2452
|
+
for(;;){
|
|
2453
|
+
if (node_fs.existsSync(node_path.join(current, ".git"))) return true;
|
|
2454
|
+
const parent = node_path.dirname(current);
|
|
2455
|
+
if (parent === current) return false;
|
|
2456
|
+
current = parent;
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2192
2459
|
function ensureProjectIgnored(projectPath, options) {
|
|
2193
2460
|
const entry = options.entry;
|
|
2194
|
-
if (!
|
|
2461
|
+
if (!insideRepository(projectPath)) return {
|
|
2195
2462
|
state: "not-a-repo",
|
|
2196
2463
|
entry
|
|
2197
2464
|
};
|
|
@@ -2223,6 +2490,56 @@ function ensureProjectIgnored(projectPath, options) {
|
|
|
2223
2490
|
};
|
|
2224
2491
|
}
|
|
2225
2492
|
}
|
|
2493
|
+
const placedHere = new Set();
|
|
2494
|
+
function recordDir() {
|
|
2495
|
+
return node_path.join(sessionStateDir(), "carriers");
|
|
2496
|
+
}
|
|
2497
|
+
function recordPath(resolved) {
|
|
2498
|
+
const digest = node_crypto.createHash("sha1").update(resolved).digest("hex").slice(0, 16);
|
|
2499
|
+
return node_path.join(recordDir(), `${digest}.json`);
|
|
2500
|
+
}
|
|
2501
|
+
function rememberCarrier(projectPath) {
|
|
2502
|
+
const resolved = node_path.resolve(projectPath);
|
|
2503
|
+
placedHere.add(resolved);
|
|
2504
|
+
try {
|
|
2505
|
+
node_fs.mkdirSync(recordDir(), {
|
|
2506
|
+
recursive: true
|
|
2507
|
+
});
|
|
2508
|
+
node_fs.writeFileSync(recordPath(resolved), `${JSON.stringify({
|
|
2509
|
+
projectPath: resolved,
|
|
2510
|
+
pid: process.pid,
|
|
2511
|
+
placedAt: new Date().toISOString()
|
|
2512
|
+
})}\n`);
|
|
2513
|
+
} catch {}
|
|
2514
|
+
}
|
|
2515
|
+
function forgetCarrier(projectPath) {
|
|
2516
|
+
const resolved = node_path.resolve(projectPath);
|
|
2517
|
+
placedHere.delete(resolved);
|
|
2518
|
+
try {
|
|
2519
|
+
node_fs.rmSync(recordPath(resolved), {
|
|
2520
|
+
force: true
|
|
2521
|
+
});
|
|
2522
|
+
} catch {}
|
|
2523
|
+
}
|
|
2524
|
+
function carriersPlacedHere() {
|
|
2525
|
+
return [
|
|
2526
|
+
...placedHere
|
|
2527
|
+
];
|
|
2528
|
+
}
|
|
2529
|
+
function rememberedCarriers() {
|
|
2530
|
+
let files;
|
|
2531
|
+
try {
|
|
2532
|
+
files = node_fs.readdirSync(recordDir());
|
|
2533
|
+
} catch {
|
|
2534
|
+
return [];
|
|
2535
|
+
}
|
|
2536
|
+
const out = [];
|
|
2537
|
+
for (const file of files)if (file.endsWith(".json")) try {
|
|
2538
|
+
const parsed = JSON.parse(node_fs.readFileSync(node_path.join(recordDir(), file), "utf8"));
|
|
2539
|
+
if ("string" == typeof parsed.projectPath && parsed.projectPath) out.push(parsed.projectPath);
|
|
2540
|
+
} catch {}
|
|
2541
|
+
return out;
|
|
2542
|
+
}
|
|
2226
2543
|
const CARRIER_DIR_NAME = "extension-dev-live-preview";
|
|
2227
2544
|
const CARRIER_EXTENSION_ID = "ibppeifnekhjjjmpjfiobccjlicbmgcb";
|
|
2228
2545
|
const MARKER_FILE = "managed-by-extension-dev-mcp.json";
|
|
@@ -2241,16 +2558,78 @@ function deriveCarrierId(source) {
|
|
|
2241
2558
|
function carrierPath(projectPath) {
|
|
2242
2559
|
return node_path.join(projectPath, "extensions", CARRIER_DIR_NAME);
|
|
2243
2560
|
}
|
|
2561
|
+
function relativeFiles(dir, base = dir, depth = 0) {
|
|
2562
|
+
if (depth > 4) return null;
|
|
2563
|
+
let entries;
|
|
2564
|
+
try {
|
|
2565
|
+
entries = node_fs.readdirSync(dir, {
|
|
2566
|
+
withFileTypes: true
|
|
2567
|
+
});
|
|
2568
|
+
} catch {
|
|
2569
|
+
return null;
|
|
2570
|
+
}
|
|
2571
|
+
const out = [];
|
|
2572
|
+
for (const entry of entries){
|
|
2573
|
+
const full = node_path.join(dir, entry.name);
|
|
2574
|
+
if (entry.isDirectory()) {
|
|
2575
|
+
const nested = relativeFiles(full, base, depth + 1);
|
|
2576
|
+
if (null === nested) return null;
|
|
2577
|
+
out.push(...nested);
|
|
2578
|
+
continue;
|
|
2579
|
+
}
|
|
2580
|
+
out.push(node_path.relative(base, full));
|
|
2581
|
+
if (out.length > 500) return null;
|
|
2582
|
+
}
|
|
2583
|
+
return out;
|
|
2584
|
+
}
|
|
2585
|
+
function claimCarrier(target) {
|
|
2586
|
+
if (node_fs.existsSync(node_path.join(target, MARKER_FILE))) return {
|
|
2587
|
+
ours: true,
|
|
2588
|
+
how: "marker"
|
|
2589
|
+
};
|
|
2590
|
+
if (deriveCarrierId(target) === CARRIER_EXTENSION_ID) return {
|
|
2591
|
+
ours: true,
|
|
2592
|
+
how: "payload"
|
|
2593
|
+
};
|
|
2594
|
+
if (node_fs.existsSync(node_path.join(target, "manifest.json"))) return {
|
|
2595
|
+
ours: false,
|
|
2596
|
+
how: "foreign"
|
|
2597
|
+
};
|
|
2598
|
+
const source = findBundledCarrier("chromium");
|
|
2599
|
+
const bundled = source ? relativeFiles(source) : null;
|
|
2600
|
+
const present = relativeFiles(target);
|
|
2601
|
+
if (!bundled || !present) return {
|
|
2602
|
+
ours: false,
|
|
2603
|
+
how: "foreign"
|
|
2604
|
+
};
|
|
2605
|
+
const known = new Set(bundled);
|
|
2606
|
+
return present.every((file)=>known.has(file)) ? {
|
|
2607
|
+
ours: true,
|
|
2608
|
+
how: "partial"
|
|
2609
|
+
} : {
|
|
2610
|
+
ours: false,
|
|
2611
|
+
how: "foreign"
|
|
2612
|
+
};
|
|
2613
|
+
}
|
|
2614
|
+
const RECOVERY_NOTE = {
|
|
2615
|
+
payload: `Its ${MARKER_FILE} marker was missing, but its manifest key derives the carrier's own extension id ${CARRIER_EXTENSION_ID}, which only this package's payload has, so it was recognised as ours and taken back.`,
|
|
2616
|
+
partial: `Its ${MARKER_FILE} marker was missing and it holds no manifest, but every file in it belongs to the bundled carrier payload, so it was recognised as a half-written copy of ours and taken back.`
|
|
2617
|
+
};
|
|
2618
|
+
const FOREIGN_NOTE = `extensions/${CARRIER_DIR_NAME} has no ${MARKER_FILE} marker, does not carry the carrier's own manifest key, and holds files this package never ships, so it is not the carrier this tool placed and was left untouched. Nothing here deletes a directory this tool did not write: rename it or move it out of ./extensions yourself if you want the carrier to live at that path.`;
|
|
2244
2619
|
function removeCarrier(projectPath) {
|
|
2245
2620
|
const target = carrierPath(projectPath);
|
|
2246
|
-
if (!node_fs.existsSync(target))
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2621
|
+
if (!node_fs.existsSync(target)) {
|
|
2622
|
+
forgetCarrier(projectPath);
|
|
2623
|
+
return {
|
|
2624
|
+
removed: false,
|
|
2625
|
+
path: target
|
|
2626
|
+
};
|
|
2627
|
+
}
|
|
2628
|
+
const claim = claimCarrier(target);
|
|
2629
|
+
if (!claim.ours) return {
|
|
2251
2630
|
removed: false,
|
|
2252
2631
|
path: target,
|
|
2253
|
-
note:
|
|
2632
|
+
note: FOREIGN_NOTE
|
|
2254
2633
|
};
|
|
2255
2634
|
try {
|
|
2256
2635
|
node_fs.rmSync(target, {
|
|
@@ -2264,13 +2643,17 @@ function removeCarrier(projectPath) {
|
|
|
2264
2643
|
note: `Could not remove the carrier: ${error instanceof Error ? error.message : String(error)}`
|
|
2265
2644
|
};
|
|
2266
2645
|
}
|
|
2646
|
+
forgetCarrier(projectPath);
|
|
2267
2647
|
const parent = node_path.join(projectPath, "extensions");
|
|
2268
2648
|
try {
|
|
2269
2649
|
if (0 === node_fs.readdirSync(parent).length) node_fs.rmdirSync(parent);
|
|
2270
2650
|
} catch {}
|
|
2271
2651
|
return {
|
|
2272
2652
|
removed: true,
|
|
2273
|
-
path: target
|
|
2653
|
+
path: target,
|
|
2654
|
+
..."marker" === claim.how ? {} : {
|
|
2655
|
+
note: RECOVERY_NOTE[claim.how]
|
|
2656
|
+
}
|
|
2274
2657
|
};
|
|
2275
2658
|
}
|
|
2276
2659
|
function ensureCarrierIgnored(projectPath) {
|
|
@@ -2308,10 +2691,14 @@ function materializeCarrier(projectPath, browser) {
|
|
|
2308
2691
|
};
|
|
2309
2692
|
const target = carrierPath(projectPath);
|
|
2310
2693
|
const marker = node_path.join(target, MARKER_FILE);
|
|
2311
|
-
|
|
2694
|
+
const claim = node_fs.existsSync(target) ? claimCarrier(target) : {
|
|
2695
|
+
ours: true,
|
|
2696
|
+
how: "marker"
|
|
2697
|
+
};
|
|
2698
|
+
if (!claim.ours) return {
|
|
2312
2699
|
loaded: false,
|
|
2313
2700
|
path: target,
|
|
2314
|
-
note: `A directory already exists at extensions/${CARRIER_DIR_NAME}
|
|
2701
|
+
note: `A directory already exists at extensions/${CARRIER_DIR_NAME} that this tool did not place: no ${MARKER_FILE} marker, no carrier manifest key, and files this package never ships. It was left untouched. Rename it or move it out of ./extensions to let extension_dev place the carrier there.`
|
|
2315
2702
|
};
|
|
2316
2703
|
const carrierId = deriveCarrierId(source);
|
|
2317
2704
|
try {
|
|
@@ -2328,6 +2715,7 @@ function materializeCarrier(projectPath, browser) {
|
|
|
2328
2715
|
carrierVersion: manifest.version ?? "unknown",
|
|
2329
2716
|
note: "Safe to delete; extension_dev recreates it when carrier: true. extension_stop and extension_build remove it for you."
|
|
2330
2717
|
}, null, 2)}\n`);
|
|
2718
|
+
rememberCarrier(projectPath);
|
|
2331
2719
|
const ignored = ensureCarrierIgnored(projectPath);
|
|
2332
2720
|
return {
|
|
2333
2721
|
loaded: true,
|
|
@@ -2335,7 +2723,7 @@ function materializeCarrier(projectPath, browser) {
|
|
|
2335
2723
|
...ignored ? {
|
|
2336
2724
|
gitignored: ignored
|
|
2337
2725
|
} : {},
|
|
2338
|
-
note: "Live-preview carrier placed in ./extensions; Extension.js loads it as a companion beside your extension. Open https://preview.extension.dev
|
|
2726
|
+
note: "Live-preview carrier placed in ./extensions; Extension.js loads it as a companion beside your extension. Open https://preview.extension.dev/ in the dev browser, load a build from this machine, and switch the lane toggle to Real to watch the session's real-lane chrome.* trace on the Trace tab. It is a debug companion, never part of a release: extension_stop and extension_build remove it again" + (ignored ? `, and ${ignored} was added to .gitignore.` : "."),
|
|
2339
2727
|
limitations: [
|
|
2340
2728
|
"The trace shows calls a PAGE bridges to the carrier. Your extension's own chrome.* calls run directly in its contexts and never cross the carrier, so they do not appear.",
|
|
2341
2729
|
"Bridged calls run under the CARRIER's identity, not your extension's. The preview assumes a single active guest and does not namespace per-extension state, so storage, action/badge state, messaging delivery, offscreen documents and relative script paths belong to the carrier. Rows affected are badged carrier-scoped in the Trace tab.",
|
|
@@ -2345,7 +2733,7 @@ function materializeCarrier(projectPath, browser) {
|
|
|
2345
2733
|
...carrierId ? {
|
|
2346
2734
|
bridgeProtocol: {
|
|
2347
2735
|
carrierExtensionId: carrierId,
|
|
2348
|
-
allowedOrigins: "https://preview.extension.dev, https://code.extension.dev, https://themes.extension.dev, http://localhost
|
|
2736
|
+
allowedOrigins: "https://preview.extension.dev, https://code.extension.dev, https://themes.extension.dev, and those same apps' dev servers on http://localhost and http://127.0.0.1 (ports 3103, 3104, 3110, 3111). The carrier checks the sender's origin, so a page on any other localhost port is refused.",
|
|
2349
2737
|
howTo: "From a page on an allowed origin, register your guest once with a 'session' message (it declares the permissions the carrier enforces), then send 'bridge' messages to run chrome.* for real; each one streams into the Trace tab. Use the EXACT dotted wire names the bridge dispatcher accepts: storage is storage.get/set/remove/clear with the AREA AS AN ARGUMENT, NOT storage.local.get.",
|
|
2350
2738
|
example: [
|
|
2351
2739
|
`const id = '${carrierId}'`,
|
|
@@ -2364,16 +2752,115 @@ function materializeCarrier(projectPath, browser) {
|
|
|
2364
2752
|
};
|
|
2365
2753
|
}
|
|
2366
2754
|
}
|
|
2755
|
+
const EOCD_SIGNATURE = 0x06054b50;
|
|
2756
|
+
const CENTRAL_SIGNATURE = 0x02014b50;
|
|
2757
|
+
const EOCD_MIN_LENGTH = 22;
|
|
2758
|
+
const MAX_COMMENT_LENGTH = 0xffff;
|
|
2759
|
+
const MAX_DIRECTORY_BYTES = 33554432;
|
|
2760
|
+
const UNREADABLE = {
|
|
2761
|
+
names: [],
|
|
2762
|
+
readable: false
|
|
2763
|
+
};
|
|
2764
|
+
function readZipEntryNames(zipPath) {
|
|
2765
|
+
let handle = null;
|
|
2766
|
+
try {
|
|
2767
|
+
const size = node_fs.statSync(zipPath).size;
|
|
2768
|
+
if (size < EOCD_MIN_LENGTH) return UNREADABLE;
|
|
2769
|
+
handle = node_fs.openSync(zipPath, "r");
|
|
2770
|
+
const tailLength = Math.min(size, MAX_COMMENT_LENGTH + EOCD_MIN_LENGTH);
|
|
2771
|
+
const tail = Buffer.alloc(tailLength);
|
|
2772
|
+
node_fs.readSync(handle, tail, 0, tailLength, size - tailLength);
|
|
2773
|
+
let eocd = -1;
|
|
2774
|
+
for(let at = tail.length - EOCD_MIN_LENGTH; at >= 0; at -= 1)if (tail.readUInt32LE(at) === EOCD_SIGNATURE) {
|
|
2775
|
+
eocd = at;
|
|
2776
|
+
break;
|
|
2777
|
+
}
|
|
2778
|
+
if (eocd < 0) return UNREADABLE;
|
|
2779
|
+
const expected = tail.readUInt16LE(eocd + 10);
|
|
2780
|
+
const directoryBytes = tail.readUInt32LE(eocd + 12);
|
|
2781
|
+
const directoryOffset = tail.readUInt32LE(eocd + 16);
|
|
2782
|
+
if (0xffff === expected || 0xffffffff === directoryBytes || 0xffffffff === directoryOffset || directoryBytes > MAX_DIRECTORY_BYTES || directoryOffset + directoryBytes > size) return UNREADABLE;
|
|
2783
|
+
if (0 === directoryBytes) return {
|
|
2784
|
+
names: [],
|
|
2785
|
+
readable: 0 === expected
|
|
2786
|
+
};
|
|
2787
|
+
const directory = Buffer.alloc(directoryBytes);
|
|
2788
|
+
node_fs.readSync(handle, directory, 0, directoryBytes, directoryOffset);
|
|
2789
|
+
const names = [];
|
|
2790
|
+
let at = 0;
|
|
2791
|
+
while(at + 46 <= directory.length){
|
|
2792
|
+
if (directory.readUInt32LE(at) !== CENTRAL_SIGNATURE) return UNREADABLE;
|
|
2793
|
+
const nameLength = directory.readUInt16LE(at + 28);
|
|
2794
|
+
const extraLength = directory.readUInt16LE(at + 30);
|
|
2795
|
+
const commentLength = directory.readUInt16LE(at + 32);
|
|
2796
|
+
const nameStart = at + 46;
|
|
2797
|
+
if (nameStart + nameLength > directory.length) return UNREADABLE;
|
|
2798
|
+
names.push(directory.subarray(nameStart, nameStart + nameLength).toString("utf8"));
|
|
2799
|
+
at = nameStart + nameLength + extraLength + commentLength;
|
|
2800
|
+
}
|
|
2801
|
+
return {
|
|
2802
|
+
names,
|
|
2803
|
+
readable: names.length === expected
|
|
2804
|
+
};
|
|
2805
|
+
} catch {
|
|
2806
|
+
return UNREADABLE;
|
|
2807
|
+
} finally{
|
|
2808
|
+
if (null !== handle) try {
|
|
2809
|
+
node_fs.closeSync(handle);
|
|
2810
|
+
} catch {}
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2367
2813
|
const build_COMMAND = "extension_build";
|
|
2368
2814
|
function readBuildSummary(projectPath, browser, since) {
|
|
2369
|
-
const file =
|
|
2815
|
+
const file = buildSummaryPath(projectPath, browser);
|
|
2370
2816
|
try {
|
|
2371
2817
|
const stat = node_fs.statSync(file);
|
|
2372
|
-
if (stat.mtimeMs
|
|
2373
|
-
|
|
2374
|
-
|
|
2818
|
+
if (stat.mtimeMs >= since) {
|
|
2819
|
+
const summary = JSON.parse(node_fs.readFileSync(file, "utf8"));
|
|
2820
|
+
if (summary && "object" == typeof summary) return {
|
|
2821
|
+
file,
|
|
2822
|
+
summary
|
|
2823
|
+
};
|
|
2824
|
+
}
|
|
2375
2825
|
} catch {}
|
|
2376
|
-
return
|
|
2826
|
+
return {
|
|
2827
|
+
file,
|
|
2828
|
+
summary: null
|
|
2829
|
+
};
|
|
2830
|
+
}
|
|
2831
|
+
function readEngineOutput(stdout, stderr) {
|
|
2832
|
+
let frame = null;
|
|
2833
|
+
const rest = [];
|
|
2834
|
+
for (const line of stdout.split("\n")){
|
|
2835
|
+
const text = line.trim();
|
|
2836
|
+
if (!frame && text.startsWith("{")) {
|
|
2837
|
+
let parsed = null;
|
|
2838
|
+
try {
|
|
2839
|
+
parsed = JSON.parse(text);
|
|
2840
|
+
} catch {}
|
|
2841
|
+
if (isEnvelope(parsed)) {
|
|
2842
|
+
frame = parsed;
|
|
2843
|
+
continue;
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
rest.push(line);
|
|
2847
|
+
}
|
|
2848
|
+
const narration = [
|
|
2849
|
+
rest.join("\n").trim(),
|
|
2850
|
+
stderr.trim()
|
|
2851
|
+
].filter(Boolean).join("\n");
|
|
2852
|
+
return {
|
|
2853
|
+
frame,
|
|
2854
|
+
narration
|
|
2855
|
+
};
|
|
2856
|
+
}
|
|
2857
|
+
function engineSummaries(frame) {
|
|
2858
|
+
const value = frame?.value;
|
|
2859
|
+
if (!value || !Array.isArray(value.summaries)) return [];
|
|
2860
|
+
return value.summaries.filter((entry)=>Boolean(entry) && "object" == typeof entry);
|
|
2861
|
+
}
|
|
2862
|
+
function summaryForBrowser(summaries, browser) {
|
|
2863
|
+
return summaries.find((entry)=>entry.browser === browser) ?? summaries[0] ?? null;
|
|
2377
2864
|
}
|
|
2378
2865
|
function builtEntrypoints(distDir) {
|
|
2379
2866
|
let manifest;
|
|
@@ -2462,7 +2949,7 @@ function locateSourceZip(projectPath, browser, since) {
|
|
|
2462
2949
|
}
|
|
2463
2950
|
const build_schema = {
|
|
2464
2951
|
name: "extension_build",
|
|
2465
|
-
description: "Build a browser extension for production. The output lands in dist/<browser>/. Pass zip:true to also package a .zip for store submission. The build refuses a manifest with build-blocking errors unless you pass skipValidation:true, because such a manifest yields a broken bundle the bundler itself never flags.",
|
|
2952
|
+
description: "Build a browser extension for production. The output lands in dist/<browser>/. Pass zip:true to also package a .zip for store submission. With browser:'safari' the build converts the extension into a macOS app through Xcode, and bundleId sets the identifier it ships under. The build refuses a manifest with build-blocking errors unless you pass skipValidation:true, because such a manifest yields a broken bundle the bundler itself never flags.",
|
|
2466
2953
|
inputSchema: {
|
|
2467
2954
|
type: "object",
|
|
2468
2955
|
properties: {
|
|
@@ -2506,6 +2993,24 @@ const build_schema = {
|
|
|
2506
2993
|
type: "boolean",
|
|
2507
2994
|
default: false,
|
|
2508
2995
|
description: "Build even when extension_manifest_validate reports build-blocking errors. The build normally refuses: a manifest error yields a broken bundle the bundler itself never flags."
|
|
2996
|
+
},
|
|
2997
|
+
appName: {
|
|
2998
|
+
type: "string",
|
|
2999
|
+
description: "Safari targets only: name of the generated macOS app, which also names the Xcode scheme and the .app on disk. Defaults to the manifest name."
|
|
3000
|
+
},
|
|
3001
|
+
bundleId: {
|
|
3002
|
+
type: "string",
|
|
3003
|
+
description: "Safari targets only: a reverse-DNS bundle identifier you own, such as com.acme.readinglist. Without one the app is packaged under a generated dev.extensionjs.* identifier derived from the app name, which every project built from the same template shares, and the first team to register it takes it."
|
|
3004
|
+
},
|
|
3005
|
+
macOsOnly: {
|
|
3006
|
+
type: "boolean",
|
|
3007
|
+
default: true,
|
|
3008
|
+
description: "Safari targets only: generate a macOS-only Xcode project. Pass false for a universal project that also targets iOS and iPadOS, which is what you want if the extension ships on iPhone or iPad."
|
|
3009
|
+
},
|
|
3010
|
+
forceRegenerate: {
|
|
3011
|
+
type: "boolean",
|
|
3012
|
+
default: false,
|
|
3013
|
+
description: "Safari targets only: regenerate the Xcode project even when the engine considers it up to date. Use it when an earlier packaging run left the project broken."
|
|
2509
3014
|
}
|
|
2510
3015
|
},
|
|
2511
3016
|
required: [
|
|
@@ -2513,6 +3018,11 @@ const build_schema = {
|
|
|
2513
3018
|
]
|
|
2514
3019
|
}
|
|
2515
3020
|
};
|
|
3021
|
+
const SAFARI_VENDORS = new Set([
|
|
3022
|
+
"safari",
|
|
3023
|
+
"webkit-based"
|
|
3024
|
+
]);
|
|
3025
|
+
const BUNDLE_ID_PATTERN = /^[A-Za-z][A-Za-z0-9-]*(\.[A-Za-z][A-Za-z0-9-]*)+$/;
|
|
2516
3026
|
function manifestDivergence(projectPath, browser) {
|
|
2517
3027
|
const read = (p)=>{
|
|
2518
3028
|
try {
|
|
@@ -2540,23 +3050,58 @@ function manifestDivergence(projectPath, browser) {
|
|
|
2540
3050
|
return notes;
|
|
2541
3051
|
}
|
|
2542
3052
|
const MARKER_FILE_NAME = "managed-by-extension-dev-mcp.json";
|
|
2543
|
-
function
|
|
2544
|
-
|
|
3053
|
+
function namesCarrier(entryName) {
|
|
3054
|
+
return entryName === CARRIER_DIR_NAME || entryName === MARKER_FILE_NAME;
|
|
3055
|
+
}
|
|
3056
|
+
function carrierEntriesInZip(zipPath) {
|
|
3057
|
+
const listing = readZipEntryNames(zipPath);
|
|
3058
|
+
if (!listing.readable) return {
|
|
3059
|
+
paths: [],
|
|
3060
|
+
unchecked: [
|
|
3061
|
+
zipPath
|
|
3062
|
+
]
|
|
3063
|
+
};
|
|
3064
|
+
const hits = listing.names.filter((name)=>name.split("/").some(namesCarrier)).map((name)=>`${zipPath} -> ${name}`);
|
|
3065
|
+
return {
|
|
3066
|
+
paths: hits,
|
|
3067
|
+
unchecked: []
|
|
3068
|
+
};
|
|
3069
|
+
}
|
|
3070
|
+
function carrierContamination(dir, depth = 0) {
|
|
3071
|
+
if (depth > 4) return {
|
|
3072
|
+
paths: [],
|
|
3073
|
+
unchecked: []
|
|
3074
|
+
};
|
|
2545
3075
|
let entries;
|
|
2546
3076
|
try {
|
|
2547
|
-
entries = node_fs.readdirSync(
|
|
3077
|
+
entries = node_fs.readdirSync(dir, {
|
|
2548
3078
|
withFileTypes: true
|
|
2549
3079
|
});
|
|
2550
3080
|
} catch {
|
|
2551
|
-
return
|
|
3081
|
+
return {
|
|
3082
|
+
paths: [],
|
|
3083
|
+
unchecked: []
|
|
3084
|
+
};
|
|
2552
3085
|
}
|
|
2553
|
-
const found =
|
|
3086
|
+
const found = {
|
|
3087
|
+
paths: [],
|
|
3088
|
+
unchecked: []
|
|
3089
|
+
};
|
|
3090
|
+
const absorb = (other)=>{
|
|
3091
|
+
found.paths.push(...other.paths);
|
|
3092
|
+
found.unchecked.push(...other.unchecked);
|
|
3093
|
+
};
|
|
2554
3094
|
for (const entry of entries){
|
|
2555
|
-
|
|
2556
|
-
|
|
3095
|
+
const full = node_path.join(dir, entry.name);
|
|
3096
|
+
if (namesCarrier(entry.name)) {
|
|
3097
|
+
found.paths.push(full);
|
|
2557
3098
|
continue;
|
|
2558
3099
|
}
|
|
2559
|
-
if (entry.isDirectory())
|
|
3100
|
+
if (entry.isDirectory()) {
|
|
3101
|
+
absorb(carrierContamination(full, depth + 1));
|
|
3102
|
+
continue;
|
|
3103
|
+
}
|
|
3104
|
+
if (entry.isFile() && entry.name.endsWith(".zip")) absorb(carrierEntriesInZip(full));
|
|
2560
3105
|
}
|
|
2561
3106
|
return found;
|
|
2562
3107
|
}
|
|
@@ -2585,12 +3130,61 @@ async function validationPreflight(projectPath, browser) {
|
|
|
2585
3130
|
async function build_handler(args) {
|
|
2586
3131
|
const start = Date.now();
|
|
2587
3132
|
const browser = args.browser ?? "chrome";
|
|
3133
|
+
const safari = SAFARI_VENDORS.has(browser);
|
|
3134
|
+
const safariOnly = [
|
|
3135
|
+
[
|
|
3136
|
+
"appName",
|
|
3137
|
+
args.appName
|
|
3138
|
+
],
|
|
3139
|
+
[
|
|
3140
|
+
"bundleId",
|
|
3141
|
+
args.bundleId
|
|
3142
|
+
],
|
|
3143
|
+
[
|
|
3144
|
+
"macOsOnly",
|
|
3145
|
+
args.macOsOnly
|
|
3146
|
+
],
|
|
3147
|
+
[
|
|
3148
|
+
"forceRegenerate",
|
|
3149
|
+
true === args.forceRegenerate ? true : void 0
|
|
3150
|
+
]
|
|
3151
|
+
].filter(([, value])=>void 0 !== value).map(([name])=>name);
|
|
3152
|
+
if (safariOnly.length > 0 && !safari) return envelope_envelope({
|
|
3153
|
+
ok: false,
|
|
3154
|
+
command: build_COMMAND,
|
|
3155
|
+
status: "safari-only-option",
|
|
3156
|
+
error: {
|
|
3157
|
+
code: "E_SAFARI_ONLY_OPTION",
|
|
3158
|
+
message: `${safariOnly.join(", ")} configure the Safari web-extension conversion and mean nothing for a ${browser} build. Nothing was built, so the options were not silently ignored.`
|
|
3159
|
+
},
|
|
3160
|
+
value: {
|
|
3161
|
+
browser,
|
|
3162
|
+
options: safariOnly,
|
|
3163
|
+
duration: Date.now() - start
|
|
3164
|
+
},
|
|
3165
|
+
hint: `Pass browser: "safari" to package a Safari app, or drop these options to build for ${browser}.`
|
|
3166
|
+
});
|
|
3167
|
+
if (void 0 !== args.bundleId && !BUNDLE_ID_PATTERN.test(args.bundleId)) return envelope_envelope({
|
|
3168
|
+
ok: false,
|
|
3169
|
+
command: build_COMMAND,
|
|
3170
|
+
status: "invalid-bundle-id",
|
|
3171
|
+
error: {
|
|
3172
|
+
code: "E_INVALID_BUNDLE_ID",
|
|
3173
|
+
message: `bundleId ${JSON.stringify(args.bundleId)} is not a reverse-DNS identifier, so Xcode would reject it. Expected two or more dot-separated segments of letters, digits and hyphens, each starting with a letter.`
|
|
3174
|
+
},
|
|
3175
|
+
value: {
|
|
3176
|
+
browser,
|
|
3177
|
+
bundleId: args.bundleId,
|
|
3178
|
+
duration: Date.now() - start
|
|
3179
|
+
},
|
|
3180
|
+
hint: 'Use an identifier under a domain you own, for example "com.acme.readinglist".'
|
|
3181
|
+
});
|
|
2588
3182
|
const carrierCleanup = removeCarrier(args.projectPath);
|
|
2589
3183
|
const carrierNotes = [];
|
|
2590
3184
|
if (carrierCleanup.removed) carrierNotes.push("Removed the Extension.dev live-preview carrier from ./extensions before building. It is a debug companion, not part of your extension; run extension_dev with carrier: true to get it back.");
|
|
2591
3185
|
else if (carrierCleanup.note) carrierNotes.push(carrierCleanup.note);
|
|
2592
3186
|
const preflight = args.skipValidation ? null : await validationPreflight(args.projectPath, browser);
|
|
2593
|
-
if (preflight?.buildBlocking) return
|
|
3187
|
+
if (preflight?.buildBlocking) return envelope_envelope({
|
|
2594
3188
|
ok: false,
|
|
2595
3189
|
command: build_COMMAND,
|
|
2596
3190
|
status: "manifest-blocked",
|
|
@@ -2623,40 +3217,63 @@ async function build_handler(args) {
|
|
|
2623
3217
|
if (args.polyfill) cliArgs.push("--polyfill");
|
|
2624
3218
|
if (args.silent) cliArgs.push("--silent");
|
|
2625
3219
|
if (args.mode) cliArgs.push("--mode", args.mode);
|
|
2626
|
-
|
|
3220
|
+
if (args.appName) cliArgs.push("--app-name", args.appName);
|
|
3221
|
+
if (args.bundleId) cliArgs.push("--bundle-id", args.bundleId);
|
|
3222
|
+
if (void 0 !== args.macOsOnly) cliArgs.push("--macos-only", String(args.macOsOnly));
|
|
3223
|
+
if (args.forceRegenerate) cliArgs.push("--force-regenerate");
|
|
3224
|
+
const spawn = {
|
|
2627
3225
|
cwd: args.projectPath,
|
|
2628
3226
|
timeoutMs: 180000
|
|
2629
|
-
}
|
|
3227
|
+
};
|
|
3228
|
+
const verdict = await outputJsonVerdict("build", args.projectPath);
|
|
3229
|
+
const engineKnownTooOld = false === verdict.supported;
|
|
3230
|
+
let attempt = engineKnownTooOld ? await runExtensionCli(cliArgs, spawn) : await runExtensionCli([
|
|
3231
|
+
...cliArgs,
|
|
3232
|
+
"--output",
|
|
3233
|
+
"json"
|
|
3234
|
+
], spawn);
|
|
3235
|
+
const engineRefusedJsonOutput = !engineKnownTooOld && 0 !== attempt.code && refusedTheOutputFlag(attempt.stderr ?? "");
|
|
3236
|
+
if (engineRefusedJsonOutput) attempt = await runExtensionCli(cliArgs, spawn);
|
|
3237
|
+
const { code, stdout, stderr } = attempt;
|
|
3238
|
+
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.");
|
|
3239
|
+
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.`);
|
|
2630
3240
|
const duration = Date.now() - start;
|
|
2631
|
-
const
|
|
3241
|
+
const engine = readEngineOutput(stdout ?? "", stderr ?? "");
|
|
3242
|
+
const out = engine.narration;
|
|
2632
3243
|
const lastLines = (text, n)=>text.split("\n").slice(-n).join("\n");
|
|
2633
3244
|
if (0 === code) {
|
|
2634
|
-
const
|
|
2635
|
-
const
|
|
2636
|
-
const engineSummary =
|
|
3245
|
+
const inlineSummary = summaryForBrowser(engineSummaries(engine.frame), browser);
|
|
3246
|
+
const persisted = inlineSummary ? null : readBuildSummary(args.projectPath, browser, start);
|
|
3247
|
+
const engineSummary = inlineSummary ?? persisted?.summary ?? null;
|
|
3248
|
+
const summaryPathNote = persisted && !persisted.summary ? `This build reported no summary of its own, and no summary from this run was found on disk either, so the byte totals and the engine's structured warnings are missing from the result below. The extension that was built is unaffected. ${sessionPathHint(persisted.file)}` : null;
|
|
3249
|
+
const status = engine.frame?.status;
|
|
2637
3250
|
const buildWarnings = engineSummary?.warnings?.length ? engineSummary.warnings : [];
|
|
2638
3251
|
const buildWarningsTruncated = buildWarnings.length && "number" == typeof engineSummary?.warnings_count && engineSummary.warnings_count > buildWarnings.length ? engineSummary.warnings_count : void 0;
|
|
2639
3252
|
const distDir = node_path.resolve(args.projectPath, "dist", browser);
|
|
2640
3253
|
const entrypoints = builtEntrypoints(distDir);
|
|
2641
|
-
const contamination =
|
|
2642
|
-
|
|
3254
|
+
const contamination = carrierContamination(node_path.resolve(args.projectPath, "dist"));
|
|
3255
|
+
const uncheckedNote = contamination.unchecked.length ? `Could not read the entry table of ${contamination.unchecked.join(", ")}, so those archives were not checked for the live-preview carrier. Unpack and check them yourself before submitting.` : null;
|
|
3256
|
+
if (contamination.paths.length) return envelope_envelope({
|
|
2643
3257
|
ok: false,
|
|
2644
3258
|
command: build_COMMAND,
|
|
2645
3259
|
status: "carrier-in-dist",
|
|
2646
3260
|
error: {
|
|
2647
3261
|
code: "E_CARRIER_IN_DIST",
|
|
2648
|
-
message: `The build output contains the Extension.dev live-preview carrier: ${contamination.join(", ")}. That is a local debug companion and must never ship. This artifact is not safe to submit.`
|
|
3262
|
+
message: `The build output contains the Extension.dev live-preview carrier: ${contamination.paths.join(", ")}. That is a local debug companion and must never ship. This artifact is not safe to submit.`
|
|
2649
3263
|
},
|
|
2650
3264
|
value: {
|
|
2651
3265
|
browser,
|
|
2652
3266
|
buildExitCode: 0,
|
|
2653
3267
|
duration
|
|
2654
3268
|
},
|
|
2655
|
-
warnings
|
|
2656
|
-
|
|
3269
|
+
warnings: [
|
|
3270
|
+
...warnings,
|
|
3271
|
+
uncheckedNote
|
|
3272
|
+
],
|
|
3273
|
+
hint: "Delete the listed paths from dist and build again. The carrier lives in ./extensions and is taken back before every build run through this tool, so an entry inside a zip means that archive was packed by something else, usually 'extension build --zip-source' driven straight at the engine while a dev session had the carrier in place."
|
|
2657
3274
|
});
|
|
2658
3275
|
const missing = entrypoints.filter((e)=>!e.present);
|
|
2659
|
-
if (missing.length) return
|
|
3276
|
+
if (missing.length) return envelope_envelope({
|
|
2660
3277
|
ok: false,
|
|
2661
3278
|
command: build_COMMAND,
|
|
2662
3279
|
status: "entrypoint-missing",
|
|
@@ -2684,18 +3301,36 @@ async function build_handler(args) {
|
|
|
2684
3301
|
const zipSourcePath = args.zipSource ? locateSourceZip(args.projectPath, browser, start) : null;
|
|
2685
3302
|
if (args.zipSource && !zipSourcePath) zipNotes.push("zipSource: true was requested and the build succeeded, but no *-source.zip file could be located in dist/. The engine may not have packaged it; check the build output below.");
|
|
2686
3303
|
const divergence = manifestDivergence(args.projectPath, browser);
|
|
2687
|
-
|
|
3304
|
+
const safariIdentity = safari ? engineSummary?.safari ?? null : null;
|
|
3305
|
+
const derivedBundleIdNote = safariIdentity?.bundleIdDerived === true ? `The Safari app was packaged under the generated bundle identifier ${safariIdentity.bundleId ?? "the engine derived for you"}, which the engine derived from your app name rather than one you chose. It is fine for running the app locally. Every project built from the same template derives the same identifier, and Apple binds one permanently to the first team that registers it, so whoever submits first takes it and everyone after is locked out. Rebuild with bundleId set to a reverse-DNS identifier under a domain you own, which regenerates the Xcode project, and do it before your first submission: afterwards a new identifier is a new extension carrying none of your users.` : null;
|
|
3306
|
+
const safariIdentityMissingNote = safari && !safariIdentity ? `The build succeeded but reported no Safari app identity, so this run cannot tell you which bundle identifier the app carries. Either the packager did not run (a non-macOS host, or Xcode missing, skips packaging and leaves a plain bundle in dist/${browser}), or the engine installed in this project predates the reporting contract. Check the build output below, and run extension_doctor if you expected an app.` : null;
|
|
3307
|
+
return envelope_envelope({
|
|
2688
3308
|
ok: true,
|
|
2689
3309
|
command: build_COMMAND,
|
|
2690
3310
|
status: "built",
|
|
2691
3311
|
value: {
|
|
2692
3312
|
browser,
|
|
2693
|
-
...
|
|
2694
|
-
|
|
3313
|
+
...safariIdentity ? {
|
|
3314
|
+
safariApp: safariIdentity
|
|
3315
|
+
} : {},
|
|
3316
|
+
..."string" == typeof engineSummary?.output_path ? {
|
|
3317
|
+
outputPath: engineSummary.output_path
|
|
3318
|
+
} : {},
|
|
3319
|
+
..."number" == typeof engineSummary?.total_bytes ? {
|
|
3320
|
+
totalBytes: engineSummary.total_bytes
|
|
3321
|
+
} : {},
|
|
3322
|
+
..."number" == typeof engineSummary?.total_assets ? {
|
|
3323
|
+
totalAssets: engineSummary.total_assets
|
|
3324
|
+
} : {},
|
|
3325
|
+
..."number" == typeof engineSummary?.largest_asset_bytes ? {
|
|
3326
|
+
largestAssetBytes: engineSummary.largest_asset_bytes
|
|
2695
3327
|
} : {},
|
|
2696
3328
|
...status ? {
|
|
2697
3329
|
engineBuildStatus: status
|
|
2698
3330
|
} : {},
|
|
3331
|
+
...engineRefusedJsonOutput ? {
|
|
3332
|
+
engineRejectedJsonOutput: true
|
|
3333
|
+
} : {},
|
|
2699
3334
|
...entrypoints.length ? {
|
|
2700
3335
|
entrypoints
|
|
2701
3336
|
} : {},
|
|
@@ -2719,12 +3354,17 @@ async function build_handler(args) {
|
|
|
2719
3354
|
...warnings,
|
|
2720
3355
|
...preflight?.warnings ?? [],
|
|
2721
3356
|
...buildWarnings,
|
|
2722
|
-
...zipNotes
|
|
3357
|
+
...zipNotes,
|
|
3358
|
+
uncheckedNote,
|
|
3359
|
+
derivedBundleIdNote,
|
|
3360
|
+
safariIdentityMissingNote,
|
|
3361
|
+
summaryPathNote
|
|
2723
3362
|
]
|
|
2724
3363
|
});
|
|
2725
3364
|
}
|
|
2726
|
-
const
|
|
2727
|
-
|
|
3365
|
+
const engineFailure = "string" == typeof engine.frame?.error?.message ? engine.frame.error.message.trim() : "";
|
|
3366
|
+
const message = engineFailure || stderr.trim() || out || `extension build exited with code ${code}`;
|
|
3367
|
+
return envelope_envelope({
|
|
2728
3368
|
ok: false,
|
|
2729
3369
|
command: build_COMMAND,
|
|
2730
3370
|
status: "build-failed",
|
|
@@ -2737,7 +3377,7 @@ async function build_handler(args) {
|
|
|
2737
3377
|
duration
|
|
2738
3378
|
},
|
|
2739
3379
|
warnings,
|
|
2740
|
-
hint: "Check that the project has a valid src/manifest.json
|
|
3380
|
+
hint: "Check that the project has a valid src/manifest.json. Missing dependencies are installed by the build itself, so a failure here is usually the manifest, a compile error, or a Safari toolchain the host does not have."
|
|
2741
3381
|
});
|
|
2742
3382
|
}
|
|
2743
3383
|
const MERGED_FD_CHATTER = [
|
|
@@ -2764,12 +3404,9 @@ function legacyProfileLockScrape(cleanOutput) {
|
|
|
2764
3404
|
}
|
|
2765
3405
|
const LEGACY_FIDELITY_WARNING = "This session ran a CLI that does not stamp the machine contract, so the boot verdict was read from the dev server's output instead of its ready contract. Diagnostics are less precise: compile errors come back as a text tail rather than a list. Upgrade the project's extension dependency to get the precise verdict.";
|
|
2766
3406
|
const READY_STAMP_PROFILE_LOCKED_CODE = "profile_locked";
|
|
2767
|
-
function readyContractPath(projectPath, browser) {
|
|
2768
|
-
return node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json");
|
|
2769
|
-
}
|
|
2770
3407
|
function readContract(projectPath, browser, since) {
|
|
2771
3408
|
try {
|
|
2772
|
-
const file =
|
|
3409
|
+
const file = bridge_readyContractPath(projectPath, browser);
|
|
2773
3410
|
const stat = node_fs.statSync(file);
|
|
2774
3411
|
const contract = JSON.parse(node_fs.readFileSync(file, "utf8"));
|
|
2775
3412
|
if (!contract || "object" != typeof contract) return null;
|
|
@@ -2893,6 +3530,71 @@ async function pollBootVerdict(projectPath, browser, options) {
|
|
|
2893
3530
|
warnings: []
|
|
2894
3531
|
};
|
|
2895
3532
|
}
|
|
3533
|
+
function sweepCarriers(projectPaths) {
|
|
3534
|
+
const out = [];
|
|
3535
|
+
const seen = new Set();
|
|
3536
|
+
for (const projectPath of projectPaths){
|
|
3537
|
+
const resolved = node_path.resolve(projectPath);
|
|
3538
|
+
if (!seen.has(resolved)) {
|
|
3539
|
+
seen.add(resolved);
|
|
3540
|
+
try {
|
|
3541
|
+
const removal = removeCarrier(resolved);
|
|
3542
|
+
if (!removal.removed && !removal.note) continue;
|
|
3543
|
+
out.push({
|
|
3544
|
+
projectPath: resolved,
|
|
3545
|
+
...removal
|
|
3546
|
+
});
|
|
3547
|
+
} catch (error) {
|
|
3548
|
+
out.push({
|
|
3549
|
+
projectPath: resolved,
|
|
3550
|
+
removed: false,
|
|
3551
|
+
path: carrierPath(resolved),
|
|
3552
|
+
note: `Could not remove the carrier: ${error instanceof Error ? error.message : String(error)}`
|
|
3553
|
+
});
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
return out;
|
|
3558
|
+
}
|
|
3559
|
+
function sweepCarriersPlacedHere() {
|
|
3560
|
+
try {
|
|
3561
|
+
return sweepCarriers(carriersPlacedHere());
|
|
3562
|
+
} catch {
|
|
3563
|
+
return [];
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
const EXIT_SIGNALS = [
|
|
3567
|
+
"SIGINT",
|
|
3568
|
+
"SIGTERM",
|
|
3569
|
+
"SIGHUP"
|
|
3570
|
+
];
|
|
3571
|
+
let carrier_exit_installed = [];
|
|
3572
|
+
function installCarrierExitCleanup() {
|
|
3573
|
+
if (carrier_exit_installed.length) return;
|
|
3574
|
+
const onExit = ()=>{
|
|
3575
|
+
sweepCarriersPlacedHere();
|
|
3576
|
+
};
|
|
3577
|
+
process.on("exit", onExit);
|
|
3578
|
+
carrier_exit_installed.push({
|
|
3579
|
+
event: "exit",
|
|
3580
|
+
handler: onExit
|
|
3581
|
+
});
|
|
3582
|
+
for (const signal of EXIT_SIGNALS){
|
|
3583
|
+
const onSignal = ()=>{
|
|
3584
|
+
sweepCarriersPlacedHere();
|
|
3585
|
+
process.removeListener(signal, onSignal);
|
|
3586
|
+
carrier_exit_installed = carrier_exit_installed.filter((entry)=>entry.handler !== onSignal);
|
|
3587
|
+
if (0 === process.listenerCount(signal)) try {
|
|
3588
|
+
process.kill(process.pid, signal);
|
|
3589
|
+
} catch {}
|
|
3590
|
+
};
|
|
3591
|
+
process.on(signal, onSignal);
|
|
3592
|
+
carrier_exit_installed.push({
|
|
3593
|
+
event: signal,
|
|
3594
|
+
handler: onSignal
|
|
3595
|
+
});
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
2896
3598
|
const stop_schema = {
|
|
2897
3599
|
name: "extension_stop",
|
|
2898
3600
|
description: "Stop a session that extension_dev or extension_start is running: terminate the server and the browser it launched, and remove the live-preview carrier if extension_dev placed one. This covers extension_start build:false too, which the registry records as a preview session. Call it when you are done verifying, so sessions do not accumulate.",
|
|
@@ -2907,7 +3609,7 @@ const stop_schema = {
|
|
|
2907
3609
|
all: {
|
|
2908
3610
|
type: "boolean",
|
|
2909
3611
|
default: false,
|
|
2910
|
-
description: "Stop every known session across projects and browsers, found from this server's registry AND the on-disk markers earlier runs left, so it still works after an MCP restart. projectPath/browser are then ignored."
|
|
3612
|
+
description: "Stop every known session across projects and browsers, found from this server's registry AND the on-disk markers earlier runs left, so it still works after an MCP restart. It also takes back every live-preview carrier still recorded on this machine, including one in a project whose session was never stopped. projectPath/browser are then ignored."
|
|
2911
3613
|
}
|
|
2912
3614
|
},
|
|
2913
3615
|
required: []
|
|
@@ -2973,7 +3675,7 @@ function sessionProcessPids(projectPath) {
|
|
|
2973
3675
|
const escaped = escapeRegex(form);
|
|
2974
3676
|
const patterns = [
|
|
2975
3677
|
`extension[^ ]* (dev|start|preview) ${escaped}`,
|
|
2976
|
-
`${
|
|
3678
|
+
`${escapeRegex(profilesRootDir(form))}${escapeRegex(node_path.sep)}`
|
|
2977
3679
|
];
|
|
2978
3680
|
for (const pattern of patterns)for (const pid of pgrepPids(pattern))pids.add(pid);
|
|
2979
3681
|
}
|
|
@@ -3009,12 +3711,9 @@ function stop_signal(pid, sig) {
|
|
|
3009
3711
|
}
|
|
3010
3712
|
}
|
|
3011
3713
|
}
|
|
3012
|
-
function readyJsonPath(projectPath, browser) {
|
|
3013
|
-
return node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json");
|
|
3014
|
-
}
|
|
3015
3714
|
function pidFromReadyContract(projectPath, browser) {
|
|
3016
3715
|
try {
|
|
3017
|
-
const raw = node_fs.readFileSync(
|
|
3716
|
+
const raw = node_fs.readFileSync(bridge_readyContractPath(projectPath, browser), "utf8");
|
|
3018
3717
|
const contract = JSON.parse(raw);
|
|
3019
3718
|
return "number" == typeof contract.pid ? contract.pid : null;
|
|
3020
3719
|
} catch {
|
|
@@ -3051,7 +3750,7 @@ async function stopOne(projectPath, browser) {
|
|
|
3051
3750
|
removeSession(projectPath, browser);
|
|
3052
3751
|
removeSessionMarker(projectPath, browser);
|
|
3053
3752
|
try {
|
|
3054
|
-
node_fs.rmSync(
|
|
3753
|
+
node_fs.rmSync(bridge_readyContractPath(projectPath, browser), {
|
|
3055
3754
|
force: true
|
|
3056
3755
|
});
|
|
3057
3756
|
} catch {}
|
|
@@ -3077,28 +3776,36 @@ async function stop_handler(args) {
|
|
|
3077
3776
|
const key = `${node_path.resolve(m.projectPath)}::${m.browser}`;
|
|
3078
3777
|
if (!candidates.has(key)) candidates.set(key, m);
|
|
3079
3778
|
}
|
|
3080
|
-
|
|
3779
|
+
const outcomes = [];
|
|
3780
|
+
for (const c of candidates.values())outcomes.push(await stopOne(c.projectPath, c.browser));
|
|
3781
|
+
const visited = new Set(outcomes.map((outcome)=>node_path.resolve(outcome.projectPath)));
|
|
3782
|
+
const carriers = sweepCarriers(rememberedCarriers().filter((p)=>!visited.has(node_path.resolve(p))));
|
|
3783
|
+
if (0 === candidates.size && 0 === carriers.length) return envelope_envelope({
|
|
3081
3784
|
ok: true,
|
|
3082
3785
|
command: stop_schema.name,
|
|
3083
3786
|
status: "nothing-to-stop",
|
|
3084
3787
|
value: {
|
|
3085
3788
|
stopped: []
|
|
3086
3789
|
},
|
|
3087
|
-
hint: "No sessions registered in this server
|
|
3790
|
+
hint: "No sessions registered in this server, no session markers on disk, and no carrier left in any project this machine recorded. Nothing to stop."
|
|
3088
3791
|
});
|
|
3089
|
-
|
|
3090
|
-
for (const c of candidates.values())outcomes.push(await stopOne(c.projectPath, c.browser));
|
|
3091
|
-
return envelope({
|
|
3792
|
+
return envelope_envelope({
|
|
3092
3793
|
ok: outcomes.every((o)=>o.stopped),
|
|
3093
3794
|
command: stop_schema.name,
|
|
3094
3795
|
status: "stopped-all",
|
|
3095
3796
|
value: {
|
|
3096
|
-
stopped: outcomes
|
|
3797
|
+
stopped: outcomes,
|
|
3798
|
+
...carriers.length ? {
|
|
3799
|
+
carriersSwept: carriers
|
|
3800
|
+
} : {}
|
|
3097
3801
|
},
|
|
3098
|
-
warnings:
|
|
3802
|
+
warnings: [
|
|
3803
|
+
...outcomes.map((o)=>o.stopped ? null : o.detail),
|
|
3804
|
+
...carriers.map((c)=>c.removed ? `Took the live-preview carrier back out of ${c.projectPath}, which had no session left to stop it.` : `Left the carrier in ${c.projectPath} alone: ${c.note ?? "unknown reason"}`)
|
|
3805
|
+
]
|
|
3099
3806
|
});
|
|
3100
3807
|
}
|
|
3101
|
-
if (!args.projectPath) return
|
|
3808
|
+
if (!args.projectPath) return envelope_envelope({
|
|
3102
3809
|
ok: false,
|
|
3103
3810
|
command: stop_schema.name,
|
|
3104
3811
|
status: "bad-request",
|
|
@@ -3109,7 +3816,7 @@ async function stop_handler(args) {
|
|
|
3109
3816
|
});
|
|
3110
3817
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
3111
3818
|
const outcome = await stopOne(args.projectPath, browser);
|
|
3112
|
-
return
|
|
3819
|
+
return envelope_envelope({
|
|
3113
3820
|
ok: outcome.stopped,
|
|
3114
3821
|
command: stop_schema.name,
|
|
3115
3822
|
status: null === outcome.pid ? "not-found" : outcome.stopped ? "stopped" : "still-alive",
|
|
@@ -3219,7 +3926,7 @@ async function dev_handler(args) {
|
|
|
3219
3926
|
if (existing.length > 0) {
|
|
3220
3927
|
if (!args.replace) {
|
|
3221
3928
|
const listed = existing.map((s)=>`pid ${s.pid} (${s.browser})`).join(", ");
|
|
3222
|
-
return
|
|
3929
|
+
return envelope_envelope({
|
|
3223
3930
|
ok: false,
|
|
3224
3931
|
command: dev_schema.name,
|
|
3225
3932
|
status: "session-exists",
|
|
@@ -3277,6 +3984,9 @@ async function dev_handler(args) {
|
|
|
3277
3984
|
child.on("exit", ()=>{
|
|
3278
3985
|
removeSession(args.projectPath, browser, pid);
|
|
3279
3986
|
removeSessionMarker(args.projectPath, browser, pid);
|
|
3987
|
+
if (carrier) try {
|
|
3988
|
+
removeCarrier(args.projectPath);
|
|
3989
|
+
} catch {}
|
|
3280
3990
|
});
|
|
3281
3991
|
const boot = await pollBootVerdict(args.projectPath, browser, {
|
|
3282
3992
|
child,
|
|
@@ -3294,7 +4004,7 @@ async function dev_handler(args) {
|
|
|
3294
4004
|
};
|
|
3295
4005
|
if ("exited" === boot.verdict.kind) {
|
|
3296
4006
|
const { exitCode: code, signal } = boot.verdict;
|
|
3297
|
-
return
|
|
4007
|
+
return envelope_envelope({
|
|
3298
4008
|
ok: false,
|
|
3299
4009
|
command: dev_schema.name,
|
|
3300
4010
|
status: "exited",
|
|
@@ -3314,7 +4024,7 @@ async function dev_handler(args) {
|
|
|
3314
4024
|
}
|
|
3315
4025
|
if ("compile-failed" === boot.verdict.kind) {
|
|
3316
4026
|
const { compileErrors } = boot.verdict;
|
|
3317
|
-
return
|
|
4027
|
+
return envelope_envelope({
|
|
3318
4028
|
ok: false,
|
|
3319
4029
|
command: dev_schema.name,
|
|
3320
4030
|
status: "compile-failed",
|
|
@@ -3333,10 +4043,16 @@ async function dev_handler(args) {
|
|
|
3333
4043
|
warnings: boot.warnings
|
|
3334
4044
|
});
|
|
3335
4045
|
}
|
|
3336
|
-
const
|
|
4046
|
+
const usesManagedProfile = "string" != typeof args.profile || 0 === args.profile.trim().length;
|
|
4047
|
+
const profileRootDir = usesManagedProfile ? browserProfileRootDir(args.projectPath, browser) : null;
|
|
4048
|
+
const profileAdvice = profileRemediation({
|
|
4049
|
+
projectPath: args.projectPath,
|
|
4050
|
+
browser,
|
|
4051
|
+
profile: args.profile
|
|
4052
|
+
});
|
|
3337
4053
|
if ("profile-locked" === boot.verdict.kind) {
|
|
3338
4054
|
const { owner, lockedAt } = boot.verdict;
|
|
3339
|
-
return
|
|
4055
|
+
return envelope_envelope({
|
|
3340
4056
|
ok: false,
|
|
3341
4057
|
command: dev_schema.name,
|
|
3342
4058
|
status: "profile-locked",
|
|
@@ -3346,18 +4062,20 @@ async function dev_handler(args) {
|
|
|
3346
4062
|
},
|
|
3347
4063
|
value: {
|
|
3348
4064
|
...session,
|
|
3349
|
-
|
|
4065
|
+
...profileRootDir ? {
|
|
4066
|
+
profileRootDir
|
|
4067
|
+
} : {},
|
|
3350
4068
|
owner,
|
|
3351
4069
|
...lockedAt ? {
|
|
3352
4070
|
lockedAt
|
|
3353
4071
|
} : {},
|
|
3354
4072
|
output: cleanOutput.slice(0, 2000)
|
|
3355
4073
|
},
|
|
3356
|
-
hint: `A locked profile means another session's browser still holds it: call extension_stop with this projectPath to kill that session, then start extension_dev again. If the lock survives a crash,
|
|
4074
|
+
hint: `A locked profile means another session's browser still holds it: call extension_stop with this projectPath to kill that session, then start extension_dev again. If the lock survives a crash, clear it by hand before retrying. ${profileAdvice}`,
|
|
3357
4075
|
warnings: boot.warnings
|
|
3358
4076
|
});
|
|
3359
4077
|
}
|
|
3360
|
-
if ("browser-exited" === boot.verdict.kind) return
|
|
4078
|
+
if ("browser-exited" === boot.verdict.kind) return envelope_envelope({
|
|
3361
4079
|
ok: false,
|
|
3362
4080
|
command: dev_schema.name,
|
|
3363
4081
|
status: "browser-exited",
|
|
@@ -3370,7 +4088,7 @@ async function dev_handler(args) {
|
|
|
3370
4088
|
...boot.verdict.stamp,
|
|
3371
4089
|
output: cleanOutput.slice(0, 2000)
|
|
3372
4090
|
},
|
|
3373
|
-
hint: `A locked profile means another session's browser still holds it: call extension_stop with this projectPath to kill that session, then start extension_dev again. If the lock survives a crash,
|
|
4091
|
+
hint: `A locked profile means another session's browser still holds it: call extension_stop with this projectPath to kill that session, then start extension_dev again. If the lock survives a crash, clear it by hand before retrying. ${profileAdvice}`,
|
|
3374
4092
|
warnings: boot.warnings
|
|
3375
4093
|
});
|
|
3376
4094
|
const controlVerbs = "storage, reload, open, dom_snapshot";
|
|
@@ -3397,7 +4115,7 @@ async function dev_handler(args) {
|
|
|
3397
4115
|
requestedPort: args.port ?? 8080
|
|
3398
4116
|
};
|
|
3399
4117
|
const portNote = null !== boundPort ? void 0 !== args.port && args.port !== boundPort ? `Requested port ${args.port} was not available; the dev server bound ${boundPort} (read from the engine's ready.json contract, the same source extension_wait reports).` : null : "The engine has not stamped its ready.json contract yet, so the bound port is not known at response time (a taken port makes the server bind the next free one). extension_wait reports the bound port from that contract once it lands; requestedPort above is only what was asked for.";
|
|
3400
|
-
return
|
|
4118
|
+
return envelope_envelope({
|
|
3401
4119
|
ok: true,
|
|
3402
4120
|
command: dev_schema.name,
|
|
3403
4121
|
status: "started",
|
|
@@ -3473,6 +4191,7 @@ async function start_handler(args) {
|
|
|
3473
4191
|
if (void 0 !== args.port) cliArgs.push("--port", String(args.port));
|
|
3474
4192
|
if (args.noBrowser) cliArgs.push("--no-browser");
|
|
3475
4193
|
cliArgs.push(...launchFlagArgs(args));
|
|
4194
|
+
const stale = removeCarrier(args.projectPath);
|
|
3476
4195
|
const spawnedAt = Date.now();
|
|
3477
4196
|
const spawned = spawnExtensionCli(cliArgs, {
|
|
3478
4197
|
projectDir: args.projectPath
|
|
@@ -3506,7 +4225,7 @@ async function start_handler(args) {
|
|
|
3506
4225
|
};
|
|
3507
4226
|
if ("exited" === boot.verdict.kind) {
|
|
3508
4227
|
const { exitCode: code, signal } = boot.verdict;
|
|
3509
|
-
return
|
|
4228
|
+
return envelope_envelope({
|
|
3510
4229
|
ok: false,
|
|
3511
4230
|
command: start_schema.name,
|
|
3512
4231
|
status: "exited",
|
|
@@ -3526,7 +4245,7 @@ async function start_handler(args) {
|
|
|
3526
4245
|
}
|
|
3527
4246
|
if ("compile-failed" === boot.verdict.kind) {
|
|
3528
4247
|
const { compileErrors } = boot.verdict;
|
|
3529
|
-
return
|
|
4248
|
+
return envelope_envelope({
|
|
3530
4249
|
ok: false,
|
|
3531
4250
|
command: start_schema.name,
|
|
3532
4251
|
status: "compile-failed",
|
|
@@ -3547,7 +4266,7 @@ async function start_handler(args) {
|
|
|
3547
4266
|
}
|
|
3548
4267
|
if ("browser-exited" === boot.verdict.kind || "profile-locked" === boot.verdict.kind) {
|
|
3549
4268
|
const stamp = "browser-exited" === boot.verdict.kind ? boot.verdict.stamp : {};
|
|
3550
|
-
return
|
|
4269
|
+
return envelope_envelope({
|
|
3551
4270
|
ok: false,
|
|
3552
4271
|
command: start_schema.name,
|
|
3553
4272
|
status: boot.verdict.kind,
|
|
@@ -3564,7 +4283,7 @@ async function start_handler(args) {
|
|
|
3564
4283
|
warnings: boot.warnings
|
|
3565
4284
|
});
|
|
3566
4285
|
}
|
|
3567
|
-
return
|
|
4286
|
+
return envelope_envelope({
|
|
3568
4287
|
ok: true,
|
|
3569
4288
|
command: start_schema.name,
|
|
3570
4289
|
status: building ? "started" : "launched",
|
|
@@ -3575,7 +4294,10 @@ async function start_handler(args) {
|
|
|
3575
4294
|
logPath
|
|
3576
4295
|
},
|
|
3577
4296
|
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.",
|
|
3578
|
-
warnings:
|
|
4297
|
+
warnings: [
|
|
4298
|
+
...boot.warnings,
|
|
4299
|
+
stale.removed && "Removed a Live Preview carrier left behind by an earlier dev session, so it was not loaded beside your extension here."
|
|
4300
|
+
]
|
|
3579
4301
|
});
|
|
3580
4302
|
}
|
|
3581
4303
|
function toMcpSpeak(text) {
|
|
@@ -3685,8 +4407,19 @@ async function runActVerb(args, projectPath, timeoutMs, tool) {
|
|
|
3685
4407
|
}
|
|
3686
4408
|
if (frame && "object" == typeof frame) return JSON.stringify(wrapLegacyFrame(frame, command));
|
|
3687
4409
|
} catch {}
|
|
4410
|
+
if (refusedTheOutputFlag(stderr ?? "")) return envelope_envelope({
|
|
4411
|
+
ok: false,
|
|
4412
|
+
command,
|
|
4413
|
+
status: "engine-too-old",
|
|
4414
|
+
error: {
|
|
4415
|
+
code: "E_ENGINE_TOO_OLD",
|
|
4416
|
+
name: "CliError",
|
|
4417
|
+
message: await outputFlagRefusalMessage("act", args[0], projectPath)
|
|
4418
|
+
},
|
|
4419
|
+
hint: "extension_doctor reports the project's engine version next to the one this server pins."
|
|
4420
|
+
});
|
|
3688
4421
|
const message = stderr.trim() || `extension exited with code ${code}`;
|
|
3689
|
-
return
|
|
4422
|
+
return envelope_envelope({
|
|
3690
4423
|
ok: false,
|
|
3691
4424
|
command,
|
|
3692
4425
|
status: "cli-failed",
|
|
@@ -4140,7 +4873,7 @@ class CDPClient extends CDPConnection {
|
|
|
4140
4873
|
async function resolveContractPort(projectPath, browser, field, options) {
|
|
4141
4874
|
const waitMs = options?.waitMs ?? 20000;
|
|
4142
4875
|
const graceMs = options?.graceMs ?? 2500;
|
|
4143
|
-
const readyPath =
|
|
4876
|
+
const readyPath = bridge_readyContractPath(projectPath, browser);
|
|
4144
4877
|
const deadline = Date.now() + waitMs;
|
|
4145
4878
|
let contractSeen = false;
|
|
4146
4879
|
let contractSeenAt = null;
|
|
@@ -4209,6 +4942,10 @@ const ENGINE_COMPANION_IDS = new Set([
|
|
|
4209
4942
|
CARRIER_EXTENSION_ID
|
|
4210
4943
|
]);
|
|
4211
4944
|
const EXTENSION_URL = /^chrome-extension:\/\/([a-p]{32})\//i;
|
|
4945
|
+
function isEngineCompanionUrl(url) {
|
|
4946
|
+
const match = EXTENSION_URL.exec(String(url ?? ""));
|
|
4947
|
+
return match ? ENGINE_COMPANION_IDS.has(match[1].toLowerCase()) : false;
|
|
4948
|
+
}
|
|
4212
4949
|
async function verifyGuestLoaded(projectPath, browser, options) {
|
|
4213
4950
|
let cdpPort;
|
|
4214
4951
|
try {
|
|
@@ -4339,7 +5076,7 @@ async function navigateToUrlViaBridge(projectPath, browser, url, timeout, tool =
|
|
|
4339
5076
|
return raw;
|
|
4340
5077
|
}
|
|
4341
5078
|
const settled = await pollForBridgeTab(projectPath, browser, url, null != timeout ? Math.min(timeout, 6000) : 6000);
|
|
4342
|
-
if (!settled) return
|
|
5079
|
+
if (!settled) return envelope_envelope({
|
|
4343
5080
|
ok: false,
|
|
4344
5081
|
command: tool,
|
|
4345
5082
|
status: "navigate-failed",
|
|
@@ -4350,7 +5087,7 @@ async function navigateToUrlViaBridge(projectPath, browser, url, timeout, tool =
|
|
|
4350
5087
|
},
|
|
4351
5088
|
hint: "Confirm the URL, or discover open tabs with extension_dom_snapshot listTabs: true. For an extension page, the path must match the BUILT manifest."
|
|
4352
5089
|
});
|
|
4353
|
-
return
|
|
5090
|
+
return envelope_envelope({
|
|
4354
5091
|
ok: true,
|
|
4355
5092
|
command: tool,
|
|
4356
5093
|
status: "navigated",
|
|
@@ -4422,7 +5159,7 @@ function isDisposableTab(tabUrl, destination) {
|
|
|
4422
5159
|
async function navigateToUrl(projectPath, browser, url, timeout) {
|
|
4423
5160
|
if (!isChromiumFamily(browser)) return navigateToUrlViaBridge(projectPath, browser, url, timeout);
|
|
4424
5161
|
const resolved = await resolveCdpPort(projectPath, browser);
|
|
4425
|
-
if (!resolved) return
|
|
5162
|
+
if (!resolved) return envelope_envelope({
|
|
4426
5163
|
ok: false,
|
|
4427
5164
|
command: open_schema.name,
|
|
4428
5165
|
status: "no-session",
|
|
@@ -4458,7 +5195,7 @@ async function navigateToUrl(projectPath, browser, url, timeout) {
|
|
|
4458
5195
|
const settled = await pollForTarget(resolved.port, url, 6000, navigatedTargetId);
|
|
4459
5196
|
if (!settled) {
|
|
4460
5197
|
const isExtensionPage = url.startsWith("chrome-extension://");
|
|
4461
|
-
return
|
|
5198
|
+
return envelope_envelope({
|
|
4462
5199
|
ok: false,
|
|
4463
5200
|
command: open_schema.name,
|
|
4464
5201
|
status: "navigate-failed",
|
|
@@ -4470,7 +5207,7 @@ async function navigateToUrl(projectPath, browser, url, timeout) {
|
|
|
4470
5207
|
hint: isExtensionPage ? "Confirm the path exists in the built dist (extension_build / extension_analyze list entrypoints). For an extension page, the path must match the BUILT manifest, which may differ from your source layout." : "Confirm the URL loads in a normal browser and that the dev session's browser has network access. Nothing about your extension bundle is implicated in a failed http(s) navigation."
|
|
4471
5208
|
});
|
|
4472
5209
|
}
|
|
4473
|
-
return
|
|
5210
|
+
return envelope_envelope({
|
|
4474
5211
|
ok: true,
|
|
4475
5212
|
command: open_schema.name,
|
|
4476
5213
|
status: "navigated",
|
|
@@ -4494,7 +5231,7 @@ async function navigateToUrl(projectPath, browser, url, timeout) {
|
|
|
4494
5231
|
hint: "Inspect it with extension_dom_snapshot or extension_inspect using url (context: 'page'), they resolve the tab themselves. `target.targetId` is a CDP target id, NOT a chrome.tabs id: do not pass it as `tab`. If you need a numeric tab id, call extension_dom_snapshot with listTabs: true."
|
|
4495
5232
|
});
|
|
4496
5233
|
} catch (e) {
|
|
4497
|
-
return
|
|
5234
|
+
return envelope_envelope({
|
|
4498
5235
|
ok: false,
|
|
4499
5236
|
command: open_schema.name,
|
|
4500
5237
|
status: "navigate-failed",
|
|
@@ -4569,7 +5306,7 @@ function declaredCommands(projectPath, browser) {
|
|
|
4569
5306
|
}
|
|
4570
5307
|
function readDistPath(projectPath, browser) {
|
|
4571
5308
|
try {
|
|
4572
|
-
const file =
|
|
5309
|
+
const file = bridge_readyContractPath(projectPath, browser);
|
|
4573
5310
|
const contract = JSON.parse(node_fs.readFileSync(file, "utf8"));
|
|
4574
5311
|
return "string" == typeof contract?.distPath ? contract.distPath : null;
|
|
4575
5312
|
} catch {
|
|
@@ -4624,7 +5361,7 @@ function declaredSurfaces(projectPath, browser) {
|
|
|
4624
5361
|
}
|
|
4625
5362
|
function missingSurfaceError(projectPath, browser, surface, consequence) {
|
|
4626
5363
|
const declared = declaredSurfaces(projectPath, browser);
|
|
4627
|
-
if (null === declared) return
|
|
5364
|
+
if (null === declared) return envelope_envelope({
|
|
4628
5365
|
ok: false,
|
|
4629
5366
|
command: open_schema.name,
|
|
4630
5367
|
status: "no-manifest",
|
|
@@ -4638,7 +5375,7 @@ function missingSurfaceError(projectPath, browser, surface, consequence) {
|
|
|
4638
5375
|
const key = SURFACE_MANIFEST_KEYS[surface] ?? surface;
|
|
4639
5376
|
const others = declared.filter((s)=>s !== surface);
|
|
4640
5377
|
const nextVerb = "popup" === surface ? 'To exercise the toolbar button of a popup-less extension, call extension_open with surface: "action", which replays chrome.action.onClicked. To give the extension a popup, set action.default_popup in the manifest and rebuild.' : `To add one, set ${key} in the manifest and rebuild.`;
|
|
4641
|
-
return
|
|
5378
|
+
return envelope_envelope({
|
|
4642
5379
|
ok: false,
|
|
4643
5380
|
command: open_schema.name,
|
|
4644
5381
|
status: "no-surface",
|
|
@@ -4716,7 +5453,7 @@ async function openSurfaceAsTab(projectPath, browser, surface) {
|
|
|
4716
5453
|
let extensionId = null;
|
|
4717
5454
|
if (isChromiumFamily(browser)) {
|
|
4718
5455
|
extensionId = await resolveExtensionId(projectPath, browser);
|
|
4719
|
-
if (!extensionId) return
|
|
5456
|
+
if (!extensionId) return envelope_envelope({
|
|
4720
5457
|
ok: false,
|
|
4721
5458
|
command: open_schema.name,
|
|
4722
5459
|
status: "no-extension-id",
|
|
@@ -4730,7 +5467,7 @@ async function openSurfaceAsTab(projectPath, browser, surface) {
|
|
|
4730
5467
|
url = `chrome-extension://${extensionId}/${doc}`;
|
|
4731
5468
|
} else {
|
|
4732
5469
|
const base = await resolveBridgeBaseUrl(projectPath, browser);
|
|
4733
|
-
if (!base) return
|
|
5470
|
+
if (!base) return envelope_envelope({
|
|
4734
5471
|
ok: false,
|
|
4735
5472
|
command: open_schema.name,
|
|
4736
5473
|
status: "no-extension-id",
|
|
@@ -4793,7 +5530,7 @@ async function confirmSurfaceTarget(projectPath, browser, surface, raw) {
|
|
|
4793
5530
|
});
|
|
4794
5531
|
return actFrameJson(parsed);
|
|
4795
5532
|
}
|
|
4796
|
-
return
|
|
5533
|
+
return envelope_envelope({
|
|
4797
5534
|
ok: false,
|
|
4798
5535
|
command: open_schema.name,
|
|
4799
5536
|
status: "surface-did-not-open",
|
|
@@ -4867,7 +5604,7 @@ async function open_handler(args) {
|
|
|
4867
5604
|
"bookmarks"
|
|
4868
5605
|
];
|
|
4869
5606
|
if (args.asTab && args.surface && AS_TAB_SURFACES.includes(args.surface)) return openSurfaceAsTab(args.projectPath, browser, args.surface);
|
|
4870
|
-
if (!args.surface) return
|
|
5607
|
+
if (!args.surface) return envelope_envelope({
|
|
4871
5608
|
ok: false,
|
|
4872
5609
|
command: open_schema.name,
|
|
4873
5610
|
status: "bad-request",
|
|
@@ -4879,7 +5616,7 @@ async function open_handler(args) {
|
|
|
4879
5616
|
});
|
|
4880
5617
|
if ("command" === args.surface) {
|
|
4881
5618
|
const declared = declaredCommands(args.projectPath, browser);
|
|
4882
|
-
if (declared && args.name && !declared.includes(args.name)) return
|
|
5619
|
+
if (declared && args.name && !declared.includes(args.name)) return envelope_envelope({
|
|
4883
5620
|
ok: false,
|
|
4884
5621
|
command: open_schema.name,
|
|
4885
5622
|
status: "unknown-command",
|
|
@@ -4916,7 +5653,7 @@ async function open_handler(args) {
|
|
|
4916
5653
|
const parsed = JSON.parse(raw);
|
|
4917
5654
|
const msg = String(parsed?.error?.message ?? "");
|
|
4918
5655
|
const code = "string" == typeof parsed?.error?.code ? parsed.error.code : "";
|
|
4919
|
-
const refusedWindow = "
|
|
5656
|
+
const refusedWindow = "E_TARGET_NOT_FOUND" === code || /active browser window|no active|headless|user gesture/i.test(msg);
|
|
4920
5657
|
if (parsed?.ok === false && refusedWindow) {
|
|
4921
5658
|
if (AS_TAB_SURFACES.includes(args.surface)) {
|
|
4922
5659
|
const fallback = await openSurfaceAsTab(args.projectPath, browser, args.surface);
|
|
@@ -4950,7 +5687,7 @@ async function publish(options = {}) {
|
|
|
4950
5687
|
}
|
|
4951
5688
|
};
|
|
4952
5689
|
const doFetch = options.fetchImpl ?? fetch;
|
|
4953
|
-
const apiCheck = safeApiBase(resolveApiBase(options.api));
|
|
5690
|
+
const apiCheck = safeApiBase(resolveApiBase(options.api), options.api);
|
|
4954
5691
|
if (!apiCheck.ok) return {
|
|
4955
5692
|
ok: false,
|
|
4956
5693
|
error: {
|
|
@@ -5003,15 +5740,207 @@ async function publish(options = {}) {
|
|
|
5003
5740
|
data
|
|
5004
5741
|
};
|
|
5005
5742
|
}
|
|
5743
|
+
const ARTIFACT_ID = /^gen_(?:[0-9a-f]{32}|[0-9a-f]{64})$/;
|
|
5744
|
+
const ARTIFACT_ID_CANDIDATE = /gen_[0-9a-f]+/;
|
|
5745
|
+
function parseArtifactRef(input) {
|
|
5746
|
+
const raw = String(input ?? "").trim();
|
|
5747
|
+
if (!raw) return null;
|
|
5748
|
+
if (ARTIFACT_ID.test(raw)) return raw;
|
|
5749
|
+
let parsed = null;
|
|
5750
|
+
try {
|
|
5751
|
+
parsed = new URL(raw);
|
|
5752
|
+
} catch {
|
|
5753
|
+
parsed = null;
|
|
5754
|
+
}
|
|
5755
|
+
if (parsed) {
|
|
5756
|
+
const fromQuery = parsed.searchParams.get("preview");
|
|
5757
|
+
if (fromQuery && ARTIFACT_ID.test(fromQuery.trim())) return fromQuery.trim();
|
|
5758
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
5759
|
+
for(let i = segments.length - 1; i >= 0; i -= 1){
|
|
5760
|
+
const raw = segments[i];
|
|
5761
|
+
if (!raw) continue;
|
|
5762
|
+
const segment = decodeURIComponent(raw);
|
|
5763
|
+
if (ARTIFACT_ID.test(segment)) return segment;
|
|
5764
|
+
}
|
|
5765
|
+
}
|
|
5766
|
+
const loose = ARTIFACT_ID_CANDIDATE.exec(raw);
|
|
5767
|
+
return loose && ARTIFACT_ID.test(loose[0]) ? loose[0] : null;
|
|
5768
|
+
}
|
|
5769
|
+
function wwwRevokeUrl(value) {
|
|
5770
|
+
let parsed;
|
|
5771
|
+
try {
|
|
5772
|
+
parsed = new URL(value);
|
|
5773
|
+
} catch {
|
|
5774
|
+
return value;
|
|
5775
|
+
}
|
|
5776
|
+
if ("extension.dev" !== parsed.hostname.toLowerCase()) return value;
|
|
5777
|
+
parsed.hostname = "www.extension.dev";
|
|
5778
|
+
return parsed.toString();
|
|
5779
|
+
}
|
|
5780
|
+
function authError(name) {
|
|
5781
|
+
return {
|
|
5782
|
+
ok: false,
|
|
5783
|
+
error: {
|
|
5784
|
+
name,
|
|
5785
|
+
message: "No token. Run extension_auth (action: login), or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard)."
|
|
5786
|
+
}
|
|
5787
|
+
};
|
|
5788
|
+
}
|
|
5789
|
+
async function readBody(res) {
|
|
5790
|
+
const text = await res.text();
|
|
5791
|
+
try {
|
|
5792
|
+
return JSON.parse(text);
|
|
5793
|
+
} catch {
|
|
5794
|
+
return {
|
|
5795
|
+
message: text
|
|
5796
|
+
};
|
|
5797
|
+
}
|
|
5798
|
+
}
|
|
5799
|
+
async function listArtifacts(options = {}) {
|
|
5800
|
+
const token = options.token ?? resolveToken();
|
|
5801
|
+
if (!token) return authError("SharesAuthError");
|
|
5802
|
+
const apiCheck = safeApiBase(resolveApiBase(options.api), options.api);
|
|
5803
|
+
if (!apiCheck.ok) return {
|
|
5804
|
+
ok: false,
|
|
5805
|
+
error: {
|
|
5806
|
+
name: "SharesConfigError",
|
|
5807
|
+
message: apiCheck.message
|
|
5808
|
+
}
|
|
5809
|
+
};
|
|
5810
|
+
const url = new URL(`${apiCheck.base}/api/artifacts`);
|
|
5811
|
+
if (null != options.limit) url.searchParams.set("limit", String(options.limit));
|
|
5812
|
+
if (options.liveOnly) url.searchParams.set("status", "live");
|
|
5813
|
+
const doFetch = options.fetchImpl ?? fetch;
|
|
5814
|
+
let res;
|
|
5815
|
+
try {
|
|
5816
|
+
res = await doFetch(url.toString(), {
|
|
5817
|
+
headers: {
|
|
5818
|
+
authorization: `Bearer ${token}`,
|
|
5819
|
+
accept: "application/json",
|
|
5820
|
+
...identityHeaders("extension_shares")
|
|
5821
|
+
}
|
|
5822
|
+
});
|
|
5823
|
+
} catch (err) {
|
|
5824
|
+
return {
|
|
5825
|
+
ok: false,
|
|
5826
|
+
error: {
|
|
5827
|
+
name: "SharesNetworkError",
|
|
5828
|
+
message: `Could not reach ${url.toString()}: ${err?.message || err}`
|
|
5829
|
+
}
|
|
5830
|
+
};
|
|
5831
|
+
}
|
|
5832
|
+
const data = await readBody(res);
|
|
5833
|
+
if (401 === res.status) return authError("SharesAuthError");
|
|
5834
|
+
if (!res.ok) return {
|
|
5835
|
+
ok: false,
|
|
5836
|
+
error: {
|
|
5837
|
+
name: "SharesListError",
|
|
5838
|
+
status: res.status,
|
|
5839
|
+
message: `Listing shares failed (${res.status}): ${data?.message || "unknown error"}`
|
|
5840
|
+
}
|
|
5841
|
+
};
|
|
5842
|
+
const artifacts = Array.isArray(data.artifacts) ? data.artifacts.map((artifact)=>artifact && "string" == typeof artifact.revokeUrl ? {
|
|
5843
|
+
...artifact,
|
|
5844
|
+
revokeUrl: wwwRevokeUrl(artifact.revokeUrl)
|
|
5845
|
+
} : artifact) : [];
|
|
5846
|
+
const num = (value, fallback)=>"number" == typeof value && Number.isFinite(value) ? value : fallback;
|
|
5847
|
+
return {
|
|
5848
|
+
ok: true,
|
|
5849
|
+
data: {
|
|
5850
|
+
artifacts,
|
|
5851
|
+
count: num(data.count, artifacts.length),
|
|
5852
|
+
matched: num(data.matched, artifacts.length),
|
|
5853
|
+
limit: num(data.limit, artifacts.length),
|
|
5854
|
+
truncated: true === data.truncated,
|
|
5855
|
+
truncatedReported: "boolean" == typeof data.truncated,
|
|
5856
|
+
scanned: num(data.scanned, 0)
|
|
5857
|
+
}
|
|
5858
|
+
};
|
|
5859
|
+
}
|
|
5860
|
+
async function revokeArtifact(options) {
|
|
5861
|
+
const token = options.token ?? resolveToken();
|
|
5862
|
+
if (!token) return authError("SharesAuthError");
|
|
5863
|
+
const apiCheck = safeApiBase(resolveApiBase(options.api), options.api);
|
|
5864
|
+
if (!apiCheck.ok) return {
|
|
5865
|
+
ok: false,
|
|
5866
|
+
error: {
|
|
5867
|
+
name: "SharesConfigError",
|
|
5868
|
+
message: apiCheck.message
|
|
5869
|
+
}
|
|
5870
|
+
};
|
|
5871
|
+
const url = `${apiCheck.base}/api/artifacts/${encodeURIComponent(options.artifactId)}`;
|
|
5872
|
+
const doFetch = options.fetchImpl ?? fetch;
|
|
5873
|
+
let res;
|
|
5874
|
+
try {
|
|
5875
|
+
res = await doFetch(url, {
|
|
5876
|
+
method: "DELETE",
|
|
5877
|
+
headers: {
|
|
5878
|
+
authorization: `Bearer ${token}`,
|
|
5879
|
+
accept: "application/json",
|
|
5880
|
+
...identityHeaders("extension_shares")
|
|
5881
|
+
}
|
|
5882
|
+
});
|
|
5883
|
+
} catch (err) {
|
|
5884
|
+
return {
|
|
5885
|
+
ok: false,
|
|
5886
|
+
error: {
|
|
5887
|
+
name: "SharesNetworkError",
|
|
5888
|
+
message: `Could not reach ${url}: ${err?.message || err}`
|
|
5889
|
+
}
|
|
5890
|
+
};
|
|
5891
|
+
}
|
|
5892
|
+
const data = await readBody(res);
|
|
5893
|
+
if (401 === res.status) return authError("SharesAuthError");
|
|
5894
|
+
if (404 === res.status) return {
|
|
5895
|
+
ok: false,
|
|
5896
|
+
error: {
|
|
5897
|
+
name: "SharesNotFoundError",
|
|
5898
|
+
status: 404,
|
|
5899
|
+
message: `The platform has no live share ${options.artifactId} for this token. It may already be revoked, already expired, owned by a different project than the one this token is scoped to, or a teammate's personal share, which belongs to that person alone and no project token can revoke.`
|
|
5900
|
+
}
|
|
5901
|
+
};
|
|
5902
|
+
if (!res.ok) return {
|
|
5903
|
+
ok: false,
|
|
5904
|
+
error: {
|
|
5905
|
+
name: "SharesRevokeError",
|
|
5906
|
+
status: res.status,
|
|
5907
|
+
message: `Revoking ${options.artifactId} failed (${res.status}): ${data?.message || "unknown error"}`
|
|
5908
|
+
}
|
|
5909
|
+
};
|
|
5910
|
+
return {
|
|
5911
|
+
ok: true,
|
|
5912
|
+
data: {
|
|
5913
|
+
artifactId: options.artifactId,
|
|
5914
|
+
revoked: true === data.revoked,
|
|
5915
|
+
..."string" == typeof data.revokedAt ? {
|
|
5916
|
+
revokedAt: data.revokedAt
|
|
5917
|
+
} : {}
|
|
5918
|
+
}
|
|
5919
|
+
};
|
|
5920
|
+
}
|
|
5006
5921
|
const IGNORED_SEGMENTS = new Set([
|
|
5007
5922
|
"node_modules",
|
|
5008
5923
|
".git",
|
|
5009
5924
|
".DS_Store",
|
|
5010
5925
|
"__MACOSX"
|
|
5011
5926
|
]);
|
|
5927
|
+
const IGNORED_FILES = /^\.env(\..*)?$|\.(pem|key|p12|pfx|keystore)$/i;
|
|
5012
5928
|
const TEXTUAL = /\.(json|js|mjs|cjs|ts|tsx|jsx|html|htm|css|svg|txt|md|map)$/i;
|
|
5013
5929
|
const MAX_FILES = 2000;
|
|
5014
5930
|
const MAX_CONTENT_CHARS = 67108864;
|
|
5931
|
+
function encodeFile(relativePath, bytes) {
|
|
5932
|
+
if (TEXTUAL.test(relativePath)) {
|
|
5933
|
+
const text = bytes.toString("utf8");
|
|
5934
|
+
if (Buffer.from(text, "utf8").equals(bytes)) return {
|
|
5935
|
+
content: text,
|
|
5936
|
+
encoding: "utf8"
|
|
5937
|
+
};
|
|
5938
|
+
}
|
|
5939
|
+
return {
|
|
5940
|
+
content: bytes.toString("base64"),
|
|
5941
|
+
encoding: "base64"
|
|
5942
|
+
};
|
|
5943
|
+
}
|
|
5015
5944
|
function collectDistFiles(distDir) {
|
|
5016
5945
|
const files = [];
|
|
5017
5946
|
const walk = (dir)=>{
|
|
@@ -5026,12 +5955,13 @@ function collectDistFiles(distDir) {
|
|
|
5026
5955
|
continue;
|
|
5027
5956
|
}
|
|
5028
5957
|
if (!entry.isFile()) continue;
|
|
5958
|
+
if (IGNORED_FILES.test(entry.name)) continue;
|
|
5029
5959
|
const relative = node_path.relative(distDir, absolute).split(node_path.sep).join("/");
|
|
5030
5960
|
const bytes = node_fs.readFileSync(absolute);
|
|
5031
|
-
const encoding =
|
|
5961
|
+
const { content, encoding } = encodeFile(relative, bytes);
|
|
5032
5962
|
files.push({
|
|
5033
5963
|
path: relative,
|
|
5034
|
-
content
|
|
5964
|
+
content,
|
|
5035
5965
|
encoding
|
|
5036
5966
|
});
|
|
5037
5967
|
}
|
|
@@ -5048,7 +5978,7 @@ async function uploadPreview(options) {
|
|
|
5048
5978
|
message: "No token. Run extension_auth (action: login), or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard)."
|
|
5049
5979
|
}
|
|
5050
5980
|
};
|
|
5051
|
-
const apiCheck = safeApiBase(resolveApiBase(options.api));
|
|
5981
|
+
const apiCheck = safeApiBase(resolveApiBase(options.api), options.api);
|
|
5052
5982
|
if (!apiCheck.ok) return {
|
|
5053
5983
|
ok: false,
|
|
5054
5984
|
error: {
|
|
@@ -5083,13 +6013,17 @@ async function uploadPreview(options) {
|
|
|
5083
6013
|
}
|
|
5084
6014
|
};
|
|
5085
6015
|
const totalChars = files.reduce((sum, file)=>sum + file.content.length, 0);
|
|
5086
|
-
if (totalChars > MAX_CONTENT_CHARS)
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
6016
|
+
if (totalChars > MAX_CONTENT_CHARS) {
|
|
6017
|
+
const encodedBytes = files.reduce((sum, file)=>sum + Buffer.byteLength(file.content, file.encoding), 0);
|
|
6018
|
+
const mb = (value)=>(value / 1048576).toFixed(1);
|
|
6019
|
+
return {
|
|
6020
|
+
ok: false,
|
|
6021
|
+
error: {
|
|
6022
|
+
name: "PreviewTooLargeError",
|
|
6023
|
+
message: `${options.distDir} is too large to share: ${mb(encodedBytes)}MB of files. A shared preview holds about ${mb(MAX_CONTENT_CHARS)}MB of text, and roughly ${mb(3 * MAX_CONTENT_CHARS / 4)}MB when the build is mostly images, fonts or wasm, which travel base64-encoded. Trim the bundled assets, or build without source maps if yours are large.`
|
|
6024
|
+
}
|
|
6025
|
+
};
|
|
6026
|
+
}
|
|
5093
6027
|
const name = "string" == typeof options.manifest.name && options.manifest.name.trim() ? options.manifest.name : node_path.basename(options.distDir);
|
|
5094
6028
|
const version = "string" == typeof options.manifest.version ? options.manifest.version : void 0;
|
|
5095
6029
|
const doFetch = options.fetchImpl ?? fetch;
|
|
@@ -5100,6 +6034,7 @@ async function uploadPreview(options) {
|
|
|
5100
6034
|
headers: {
|
|
5101
6035
|
authorization: `Bearer ${token}`,
|
|
5102
6036
|
"content-type": "application/json",
|
|
6037
|
+
"x-extensiondev-origin": "mcp",
|
|
5103
6038
|
...identityHeaders("extension_preview_web")
|
|
5104
6039
|
},
|
|
5105
6040
|
body: JSON.stringify({
|
|
@@ -5153,14 +6088,14 @@ async function uploadPreview(options) {
|
|
|
5153
6088
|
artifactId,
|
|
5154
6089
|
previewUrl,
|
|
5155
6090
|
zipUrl: "string" == typeof data.zipUrl ? data.zipUrl : void 0,
|
|
5156
|
-
revokeUrl: "string" == typeof data.revokeUrl ? data.revokeUrl : void 0,
|
|
6091
|
+
revokeUrl: "string" == typeof data.revokeUrl ? wwwRevokeUrl(data.revokeUrl) : void 0,
|
|
5157
6092
|
expiresAt: "string" == typeof data.expiresAt ? data.expiresAt : void 0
|
|
5158
6093
|
}
|
|
5159
6094
|
};
|
|
5160
6095
|
}
|
|
5161
6096
|
const SHARE_STATE_DIR = ".extension.dev";
|
|
5162
6097
|
const SHARE_RECORD_FILE = "shared-previews.json";
|
|
5163
|
-
const FILE_NOTE = "Every preview link shared from this project. revokeUrl is the
|
|
6098
|
+
const FILE_NOTE = "Every preview link shared from this project. revokeUrl is the handle that kills a link before expiresAt. Losing it is recoverable: run extension_shares to list every share this token owns, each with its revokeUrl. Re-sharing an unchanged build returns the same link rather than a new one, and only a revoked link is replaced. Safe to delete once every link below has expired.";
|
|
5164
6099
|
const IGNORE_COMMENT = "# Extension.dev local state: revoke handles for shared previews, not part of your extension.";
|
|
5165
6100
|
function sharedPreviewsPath(projectPath) {
|
|
5166
6101
|
return node_path.join(projectPath, SHARE_STATE_DIR, SHARE_RECORD_FILE);
|
|
@@ -5233,7 +6168,7 @@ function recordSharedPreview(projectPath, entry) {
|
|
|
5233
6168
|
return {
|
|
5234
6169
|
recorded: false,
|
|
5235
6170
|
path: file,
|
|
5236
|
-
note: `Could not write the share record to ${file}: ${error instanceof Error ? error.message : String(error)}.
|
|
6171
|
+
note: `Could not write the share record to ${file}: ${error instanceof Error ? error.message : String(error)}. Keep the revokeUrl in this response, or run extension_shares later to list every share this token owns with its revokeUrl.`
|
|
5237
6172
|
};
|
|
5238
6173
|
}
|
|
5239
6174
|
const ignore = ensureProjectIgnored(projectPath, {
|
|
@@ -5259,8 +6194,125 @@ function recordSharedPreview(projectPath, entry) {
|
|
|
5259
6194
|
} : {}
|
|
5260
6195
|
};
|
|
5261
6196
|
}
|
|
6197
|
+
const MAX_HOPS = 4;
|
|
6198
|
+
function allows(allowOrigin, origin) {
|
|
6199
|
+
if (!allowOrigin) return false;
|
|
6200
|
+
const value = allowOrigin.trim();
|
|
6201
|
+
return "*" === value || value.toLowerCase() === origin.toLowerCase();
|
|
6202
|
+
}
|
|
6203
|
+
async function probeShareCors(options) {
|
|
6204
|
+
const doFetch = options.fetchImpl ?? fetch;
|
|
6205
|
+
const origin = options.origin;
|
|
6206
|
+
let url = options.zipUrl;
|
|
6207
|
+
let redirects = 0;
|
|
6208
|
+
const verdict = (extra)=>({
|
|
6209
|
+
checkedUrl: options.zipUrl,
|
|
6210
|
+
origin,
|
|
6211
|
+
finalUrl: url,
|
|
6212
|
+
finalStatus: 0,
|
|
6213
|
+
redirects,
|
|
6214
|
+
allowOrigin: null,
|
|
6215
|
+
...extra
|
|
6216
|
+
});
|
|
6217
|
+
for(let hop = 0; hop < MAX_HOPS; hop++){
|
|
6218
|
+
const controller = new AbortController();
|
|
6219
|
+
let res;
|
|
6220
|
+
try {
|
|
6221
|
+
res = await doFetch(url, {
|
|
6222
|
+
method: "GET",
|
|
6223
|
+
redirect: "manual",
|
|
6224
|
+
signal: controller.signal,
|
|
6225
|
+
headers: {
|
|
6226
|
+
origin,
|
|
6227
|
+
"sec-fetch-mode": "cors"
|
|
6228
|
+
}
|
|
6229
|
+
});
|
|
6230
|
+
} catch (err) {
|
|
6231
|
+
return verdict({
|
|
6232
|
+
ok: false,
|
|
6233
|
+
reason: `Could not reach ${url}: ${err instanceof Error ? err.message : String(err)}`
|
|
6234
|
+
});
|
|
6235
|
+
}
|
|
6236
|
+
try {
|
|
6237
|
+
await res.body?.cancel();
|
|
6238
|
+
} catch {
|
|
6239
|
+
controller.abort();
|
|
6240
|
+
}
|
|
6241
|
+
const status = res.status;
|
|
6242
|
+
const allowOrigin = res.headers.get("access-control-allow-origin");
|
|
6243
|
+
if (status >= 300 && status < 400) {
|
|
6244
|
+
const location = res.headers.get("location");
|
|
6245
|
+
if (!location) return verdict({
|
|
6246
|
+
ok: false,
|
|
6247
|
+
finalStatus: status,
|
|
6248
|
+
allowOrigin,
|
|
6249
|
+
reason: `${url} answered ${status} with no Location, so the download goes nowhere.`
|
|
6250
|
+
});
|
|
6251
|
+
url = new URL(location, url).toString();
|
|
6252
|
+
redirects += 1;
|
|
6253
|
+
continue;
|
|
6254
|
+
}
|
|
6255
|
+
if (status >= 400) return verdict({
|
|
6256
|
+
ok: false,
|
|
6257
|
+
finalStatus: status,
|
|
6258
|
+
allowOrigin,
|
|
6259
|
+
reason: `The build's zip answered ${status}, so the link has nothing to render.`
|
|
6260
|
+
});
|
|
6261
|
+
if (!allows(allowOrigin, origin)) return verdict({
|
|
6262
|
+
ok: false,
|
|
6263
|
+
finalStatus: status,
|
|
6264
|
+
allowOrigin,
|
|
6265
|
+
reason: `${url} answered ${status} but with ` + (allowOrigin ? `access-control-allow-origin: ${allowOrigin}, which does not cover ${origin}` : "no access-control-allow-origin header") + `. A browser at ${origin} will refuse to read it, so the link opens to an error even though this fetch succeeded.` + (redirects > 0 ? " The header has to be on this response, not on the redirect that led here." : "")
|
|
6266
|
+
});
|
|
6267
|
+
return verdict({
|
|
6268
|
+
ok: true,
|
|
6269
|
+
finalStatus: status,
|
|
6270
|
+
allowOrigin,
|
|
6271
|
+
reason: `A browser at ${origin} can read the build's zip: the final response after ${redirects} redirect(s) answered ${status} with access-control-allow-origin: ${allowOrigin}.`
|
|
6272
|
+
});
|
|
6273
|
+
}
|
|
6274
|
+
return verdict({
|
|
6275
|
+
ok: false,
|
|
6276
|
+
reason: `The build's zip redirected more than ${MAX_HOPS} times, so nothing could be read from it.`
|
|
6277
|
+
});
|
|
6278
|
+
}
|
|
5262
6279
|
const preview_web_COMMAND = "extension_preview_web";
|
|
5263
6280
|
const DEFAULT_PREVIEW_DEV_URL = "http://localhost:3110";
|
|
6281
|
+
const LOCAL_HOSTS = new Set([
|
|
6282
|
+
"localhost",
|
|
6283
|
+
"127.0.0.1",
|
|
6284
|
+
"[::1]",
|
|
6285
|
+
"::1"
|
|
6286
|
+
]);
|
|
6287
|
+
function safeHostBase(raw) {
|
|
6288
|
+
const trimmed = String(raw || "").replace(/\/+$/, "");
|
|
6289
|
+
let parsed;
|
|
6290
|
+
try {
|
|
6291
|
+
parsed = new URL(trimmed);
|
|
6292
|
+
} catch {
|
|
6293
|
+
return {
|
|
6294
|
+
ok: false,
|
|
6295
|
+
message: `hostUrl is not a URL: ${raw}. Leave it unset to use ${DEFAULT_PREVIEW_DEV_URL}.`
|
|
6296
|
+
};
|
|
6297
|
+
}
|
|
6298
|
+
const isLocal = LOCAL_HOSTS.has(parsed.hostname) || parsed.hostname.endsWith(".localhost") || "preview.extension.dev" === parsed.hostname;
|
|
6299
|
+
if (!isLocal) return {
|
|
6300
|
+
ok: false,
|
|
6301
|
+
message: `Refusing to use ${raw} as the preview host: hostUrl may only name a local preview dev server. Leave it unset to use ${DEFAULT_PREVIEW_DEV_URL}, or pass share:true for a link that needs no local server.`
|
|
6302
|
+
};
|
|
6303
|
+
return {
|
|
6304
|
+
ok: true,
|
|
6305
|
+
base: trimmed
|
|
6306
|
+
};
|
|
6307
|
+
}
|
|
6308
|
+
const HOST_CLAIM_MAX_CHARS = 120;
|
|
6309
|
+
function clipHostClaim(value) {
|
|
6310
|
+
return value.length > HOST_CLAIM_MAX_CHARS ? value.slice(0, HOST_CLAIM_MAX_CHARS) : value;
|
|
6311
|
+
}
|
|
6312
|
+
function expectedPreviewIdentifier(name, distDir) {
|
|
6313
|
+
const base = (name ?? node_path.basename(distDir)).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "extension";
|
|
6314
|
+
return base.startsWith("local-") ? base : `local-${base}`;
|
|
6315
|
+
}
|
|
5264
6316
|
const SURFACE = {
|
|
5265
6317
|
defaultOrigin: DEFAULT_PREVIEW_DEV_URL,
|
|
5266
6318
|
scheme: (encoded)=>`preview://build/${encoded}`,
|
|
@@ -5268,7 +6320,58 @@ const SURFACE = {
|
|
|
5268
6320
|
devCommand: "pnpm --filter preview.extension.dev dev",
|
|
5269
6321
|
label: "preview.extension.dev"
|
|
5270
6322
|
};
|
|
5271
|
-
|
|
6323
|
+
const PREVIEW_APP_LOCATIONS = [
|
|
6324
|
+
[
|
|
6325
|
+
"apps",
|
|
6326
|
+
"web",
|
|
6327
|
+
"preview.extension.dev"
|
|
6328
|
+
],
|
|
6329
|
+
[
|
|
6330
|
+
"apps",
|
|
6331
|
+
"preview.extension.dev"
|
|
6332
|
+
],
|
|
6333
|
+
[
|
|
6334
|
+
"preview.extension.dev"
|
|
6335
|
+
]
|
|
6336
|
+
];
|
|
6337
|
+
function previewDevCheckout(startPaths) {
|
|
6338
|
+
const seen = new Set();
|
|
6339
|
+
for (const start of startPaths){
|
|
6340
|
+
let dir;
|
|
6341
|
+
try {
|
|
6342
|
+
dir = node_path.resolve(start);
|
|
6343
|
+
} catch {
|
|
6344
|
+
continue;
|
|
6345
|
+
}
|
|
6346
|
+
for(let depth = 0; depth < 8; depth++){
|
|
6347
|
+
if (seen.has(dir)) break;
|
|
6348
|
+
seen.add(dir);
|
|
6349
|
+
for (const location of PREVIEW_APP_LOCATIONS){
|
|
6350
|
+
const manifest = node_path.join(dir, ...location, "package.json");
|
|
6351
|
+
try {
|
|
6352
|
+
const parsed = JSON.parse(node_fs.readFileSync(manifest, "utf8"));
|
|
6353
|
+
if (parsed.name === SURFACE.label) return dir;
|
|
6354
|
+
} catch {}
|
|
6355
|
+
}
|
|
6356
|
+
const parent = node_path.dirname(dir);
|
|
6357
|
+
if (parent === dir) break;
|
|
6358
|
+
dir = parent;
|
|
6359
|
+
}
|
|
6360
|
+
}
|
|
6361
|
+
return null;
|
|
6362
|
+
}
|
|
6363
|
+
function localLaneRemedy(checkout) {
|
|
6364
|
+
const shareOut = "Pass share:true for a link that needs no local server and opens for anyone.";
|
|
6365
|
+
return checkout ? `Start it with '${SURFACE.devCommand}' in ${checkout}. ${shareOut}` : `${SURFACE.label} is a private app of the extension.dev monorepo and no npm install of this server can start it, so the default lane cannot resolve on this machine. ${shareOut}`;
|
|
6366
|
+
}
|
|
6367
|
+
function previewOriginOf(previewUrl) {
|
|
6368
|
+
try {
|
|
6369
|
+
return new URL(previewUrl).origin;
|
|
6370
|
+
} catch {
|
|
6371
|
+
return null;
|
|
6372
|
+
}
|
|
6373
|
+
}
|
|
6374
|
+
async function buildShare(projectPath, distDir, manifest, browser, verifyInBrowserTerms) {
|
|
5272
6375
|
const result = await uploadPreview({
|
|
5273
6376
|
distDir,
|
|
5274
6377
|
manifest,
|
|
@@ -5287,6 +6390,11 @@ async function buildShare(projectPath, distDir, manifest, browser) {
|
|
|
5287
6390
|
} : {}
|
|
5288
6391
|
};
|
|
5289
6392
|
}
|
|
6393
|
+
const origin = previewOriginOf(result.data.previewUrl);
|
|
6394
|
+
const browserCheck = verifyInBrowserTerms && result.data.zipUrl && origin ? await probeShareCors({
|
|
6395
|
+
zipUrl: result.data.zipUrl,
|
|
6396
|
+
origin
|
|
6397
|
+
}) : null;
|
|
5290
6398
|
const sharedAt = new Date().toISOString();
|
|
5291
6399
|
const record = recordSharedPreview(projectPath, {
|
|
5292
6400
|
sharedAt,
|
|
@@ -5327,8 +6435,14 @@ async function buildShare(projectPath, distDir, manifest, browser) {
|
|
|
5327
6435
|
} : {},
|
|
5328
6436
|
serves: "uploaded-local-build",
|
|
5329
6437
|
localBuildUploaded: true,
|
|
6438
|
+
...browserCheck ? {
|
|
6439
|
+
browserLoadable: browserCheck.ok,
|
|
6440
|
+
browserCheck
|
|
6441
|
+
} : {
|
|
6442
|
+
browserLoadable: null
|
|
6443
|
+
},
|
|
5330
6444
|
record,
|
|
5331
|
-
note: "Anyone with this link can open the build you just made, running in the emulator. No install, no sign-in, no dev server. They can also download the whole build as a zip from zipUrl, so the link hands over the built code. It stays live until expiresAt; DELETE revokeUrl with the same token to kill it sooner, and a revoked link stays dead. revokeUrl is the
|
|
6445
|
+
note: "Anyone with this link can open the build you just made, running in the emulator. No install, no sign-in, no dev server. They can also download the whole build as a zip from zipUrl, so the link hands over the built code. It stays live until expiresAt; DELETE revokeUrl with the same token to kill it sooner, and a revoked link stays dead. revokeUrl is the handle that pulls this link early. Re-sharing an unchanged build returns this same link rather than a second one, and only a revoked link is replaced by a different one, so " + (record.recorded ? `it was also written to ${record.path} (record.path), which lists every share from this project.` : `keep it: ${record.note}`) + " To find this link again later, or to pull it back once it has left this conversation, run extension_shares: it lists every link this token has shared with its live or dead state, and revokes one by artifactId or by pasting any of its URLs." + (record.warning ? ` ${record.warning}` : "")
|
|
5332
6446
|
};
|
|
5333
6447
|
}
|
|
5334
6448
|
function detectSurfaces(manifest) {
|
|
@@ -5351,7 +6465,7 @@ function detectSurfaces(manifest) {
|
|
|
5351
6465
|
}
|
|
5352
6466
|
const preview_web_schema = {
|
|
5353
6467
|
name: "extension_preview_web",
|
|
5354
|
-
description: "Preview an in-progress extension in the web emulator, with no real browser. This builds the project (unless build:false)
|
|
6468
|
+
description: "Preview an in-progress extension in the web emulator, with no real browser. This builds the project (unless build:false) and previews dist/<browser>. Pass share:true unless you are working inside the extension.dev monorepo: it uploads the build and returns a link anyone can open, with no install, sign-in or dev server, and it is the only lane that works from an npm install of this server. Sharing also serves the build as a zip, so it hands over the built code; read the share property before using it. The default lane instead returns a deep link over the dev-only preview://build scheme, which resolves only against a preview.extension.dev dev server on this machine, so it is for people developing extension.dev itself. Call extension_shares to list and revoke every link shared this way, so one never vanishes with this response.",
|
|
5355
6469
|
inputSchema: {
|
|
5356
6470
|
type: "object",
|
|
5357
6471
|
properties: {
|
|
@@ -5378,7 +6492,7 @@ const preview_web_schema = {
|
|
|
5378
6492
|
probe: {
|
|
5379
6493
|
type: "boolean",
|
|
5380
6494
|
default: true,
|
|
5381
|
-
description: "Fetch the surface's dev middleware first to confirm the artifact loads."
|
|
6495
|
+
description: "Fetch the surface's dev middleware first to confirm the artifact loads on the local host. With share:true it also checks the shared link the way a browser would, following the zip's redirects and asserting the final response allows the preview origin, and reports that as share.browserLoadable."
|
|
5382
6496
|
},
|
|
5383
6497
|
open: {
|
|
5384
6498
|
type: "boolean",
|
|
@@ -5393,7 +6507,7 @@ const preview_web_schema = {
|
|
|
5393
6507
|
share: {
|
|
5394
6508
|
type: "boolean",
|
|
5395
6509
|
default: false,
|
|
5396
|
-
description: "Upload the built dist and return a public link (share.previewUrl) that renders those exact bytes for anyone: no install, sign-in or dev server. It also serves the build as a zip (share.zipUrl), so sharing hands over the code. Needs a token scoped to an extension.dev project (extension_auth or EXTENSION_DEV_TOKEN); without one you get a login hint and the local preview still succeeds. Live until share.expiresAt; DELETE share.revokeUrl to kill it sooner. Revocation is permanent and re-sharing
|
|
6510
|
+
description: "Upload the built dist and return a public link (share.previewUrl) that renders those exact bytes for anyone: no install, sign-in or dev server. It also serves the build as a zip (share.zipUrl), so sharing hands over the code. Needs a token scoped to an extension.dev project (extension_auth or EXTENSION_DEV_TOKEN); without one you get a login hint and the local preview still succeeds. Live until share.expiresAt; DELETE share.revokeUrl to kill it sooner. Revocation is permanent, and re-sharing an unchanged build returns the same link unless it was revoked, so each share is also appended to the project's gitignored .extension.dev/shared-previews.json."
|
|
5397
6511
|
}
|
|
5398
6512
|
},
|
|
5399
6513
|
required: [
|
|
@@ -5403,7 +6517,21 @@ const preview_web_schema = {
|
|
|
5403
6517
|
};
|
|
5404
6518
|
async function preview_web_handler(args) {
|
|
5405
6519
|
const browser = args.browser ?? "chrome";
|
|
5406
|
-
const
|
|
6520
|
+
const host = safeHostBase(args.hostUrl ?? SURFACE.defaultOrigin);
|
|
6521
|
+
if (!host.ok) return envelope_envelope({
|
|
6522
|
+
ok: false,
|
|
6523
|
+
command: preview_web_COMMAND,
|
|
6524
|
+
status: "bad-host-url",
|
|
6525
|
+
error: {
|
|
6526
|
+
code: "E_BAD_HOST_URL",
|
|
6527
|
+
message: host.message
|
|
6528
|
+
},
|
|
6529
|
+
value: {
|
|
6530
|
+
stage: "resolve-host",
|
|
6531
|
+
hostUrl: args.hostUrl
|
|
6532
|
+
}
|
|
6533
|
+
});
|
|
6534
|
+
const hostBase = host.base;
|
|
5407
6535
|
const shouldBuild = args.distPath ? false : false !== args.build;
|
|
5408
6536
|
let buildResult = null;
|
|
5409
6537
|
if (shouldBuild) {
|
|
@@ -5419,7 +6547,7 @@ async function preview_web_handler(args) {
|
|
|
5419
6547
|
raw
|
|
5420
6548
|
};
|
|
5421
6549
|
}
|
|
5422
|
-
if (!buildResult || true !== buildResult.ok) return
|
|
6550
|
+
if (!buildResult || true !== buildResult.ok) return envelope_envelope({
|
|
5423
6551
|
ok: false,
|
|
5424
6552
|
command: preview_web_COMMAND,
|
|
5425
6553
|
status: "build-failed",
|
|
@@ -5435,7 +6563,7 @@ async function preview_web_handler(args) {
|
|
|
5435
6563
|
}
|
|
5436
6564
|
const distDir = args.distPath ? node_path.resolve(args.distPath) : node_path.resolve(args.projectPath, "dist", browser);
|
|
5437
6565
|
const manifestPath = node_path.join(distDir, "manifest.json");
|
|
5438
|
-
if (!node_fs.existsSync(manifestPath)) return
|
|
6566
|
+
if (!node_fs.existsSync(manifestPath)) return envelope_envelope({
|
|
5439
6567
|
ok: false,
|
|
5440
6568
|
command: preview_web_COMMAND,
|
|
5441
6569
|
status: "no-dist",
|
|
@@ -5452,7 +6580,7 @@ async function preview_web_handler(args) {
|
|
|
5452
6580
|
try {
|
|
5453
6581
|
manifest = JSON.parse(node_fs.readFileSync(manifestPath, "utf8"));
|
|
5454
6582
|
} catch (err) {
|
|
5455
|
-
return
|
|
6583
|
+
return envelope_envelope({
|
|
5456
6584
|
ok: false,
|
|
5457
6585
|
command: preview_web_COMMAND,
|
|
5458
6586
|
status: "bad-dist-manifest",
|
|
@@ -5484,7 +6612,12 @@ async function preview_web_handler(args) {
|
|
|
5484
6612
|
built: false
|
|
5485
6613
|
}
|
|
5486
6614
|
};
|
|
5487
|
-
const
|
|
6615
|
+
const checkout = previewDevCheckout([
|
|
6616
|
+
args.projectPath,
|
|
6617
|
+
process.cwd()
|
|
6618
|
+
]);
|
|
6619
|
+
const remedy = localLaneRemedy(checkout);
|
|
6620
|
+
const hint = `Open deepLink in a browser to see the extension render in ${SURFACE.label}'s emulator, where the Trace tab shows every chrome.* call it makes and the lane toggle switches between the emulated backend and a real carrier-equipped browser. It needs that surface's dev server running on this machine. ${remedy}`;
|
|
5488
6621
|
const previewWarnings = [];
|
|
5489
6622
|
if (args.open) {
|
|
5490
6623
|
const sessionBrowser = args.openIn ?? browser;
|
|
@@ -5502,8 +6635,15 @@ async function preview_web_handler(args) {
|
|
|
5502
6635
|
result.openedIn = sessionBrowser;
|
|
5503
6636
|
if (true !== opened.ok) previewWarnings.push("Could not open the preview in a browser. This needs a live dev session (run extension_dev, then extension_wait for ready). The deepLink above still works if you open it yourself.");
|
|
5504
6637
|
}
|
|
5505
|
-
if (args.share)
|
|
5506
|
-
|
|
6638
|
+
if (args.share) {
|
|
6639
|
+
const share = await buildShare(args.projectPath, distDir, manifest, browser, false !== args.probe);
|
|
6640
|
+
result.share = share;
|
|
6641
|
+
if (true === share.ok && false === share.browserLoadable) {
|
|
6642
|
+
const check = share.browserCheck;
|
|
6643
|
+
previewWarnings.push(`The link uploaded, but it will not render for anyone: ${check?.reason ?? "its zip is not readable from the preview origin."} Do not hand this link out as working until that is fixed.`);
|
|
6644
|
+
}
|
|
6645
|
+
}
|
|
6646
|
+
if (false === args.probe) return envelope_envelope({
|
|
5507
6647
|
ok: true,
|
|
5508
6648
|
command: preview_web_COMMAND,
|
|
5509
6649
|
status: "previewed",
|
|
@@ -5515,229 +6655,166 @@ async function preview_web_handler(args) {
|
|
|
5515
6655
|
try {
|
|
5516
6656
|
const res = await fetch(probeUrl, {
|
|
5517
6657
|
headers: {
|
|
5518
|
-
accept: "application/json"
|
|
5519
|
-
}
|
|
5520
|
-
});
|
|
5521
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
5522
|
-
if (!res.ok || !contentType.includes("application/json")) return envelope({
|
|
5523
|
-
ok: true,
|
|
5524
|
-
command: preview_web_COMMAND,
|
|
5525
|
-
status: "host-not-serving-preview",
|
|
5526
|
-
value: {
|
|
5527
|
-
...result,
|
|
5528
|
-
hostReachable: true,
|
|
5529
|
-
previewLoadable: false,
|
|
5530
|
-
probe: {
|
|
5531
|
-
status: res.status,
|
|
5532
|
-
contentType
|
|
5533
|
-
}
|
|
5534
|
-
},
|
|
5535
|
-
hint,
|
|
5536
|
-
warnings: [
|
|
5537
|
-
...previewWarnings,
|
|
5538
|
-
`${SURFACE.label} answered but not with a preview payload. On the deployed host ${SURFACE.fetchPath} does not exist (dev-only); run a local dev server (${SURFACE.devCommand}) to use web preview.`
|
|
5539
|
-
]
|
|
5540
|
-
});
|
|
5541
|
-
const payload = await res.json();
|
|
5542
|
-
return envelope({
|
|
5543
|
-
ok: true,
|
|
5544
|
-
command: preview_web_COMMAND,
|
|
5545
|
-
status: "previewed",
|
|
5546
|
-
value: {
|
|
5547
|
-
...result,
|
|
5548
|
-
hostReachable: true,
|
|
5549
|
-
previewLoadable: true,
|
|
5550
|
-
probe: {
|
|
5551
|
-
identifier: payload.identifier,
|
|
5552
|
-
loadedName: payload.manifest?.name,
|
|
5553
|
-
loadedVersion: payload.version,
|
|
5554
|
-
fileCount: Array.isArray(payload.files) ? payload.files.length : 0
|
|
5555
|
-
}
|
|
5556
|
-
},
|
|
5557
|
-
hint,
|
|
5558
|
-
warnings: previewWarnings
|
|
5559
|
-
});
|
|
5560
|
-
} catch (err) {
|
|
5561
|
-
return envelope({
|
|
5562
|
-
ok: true,
|
|
5563
|
-
command: preview_web_COMMAND,
|
|
5564
|
-
status: "host-unreachable",
|
|
5565
|
-
value: {
|
|
5566
|
-
...result,
|
|
5567
|
-
hostReachable: false,
|
|
5568
|
-
previewLoadable: false,
|
|
5569
|
-
probe: {
|
|
5570
|
-
error: err instanceof Error ? err.message : String(err)
|
|
5571
|
-
}
|
|
5572
|
-
},
|
|
5573
|
-
hint,
|
|
5574
|
-
warnings: [
|
|
5575
|
-
...previewWarnings,
|
|
5576
|
-
`Could not reach ${SURFACE.label} at ${hostBase}. Start it with '${SURFACE.devCommand}', then open deepLink.`
|
|
5577
|
-
]
|
|
5578
|
-
});
|
|
5579
|
-
}
|
|
5580
|
-
}
|
|
5581
|
-
const ARTIFACT_ID = /^gen_[0-9a-f]{32}$/;
|
|
5582
|
-
const ARTIFACT_ID_ANYWHERE = /gen_[0-9a-f]{32}/;
|
|
5583
|
-
function parseArtifactRef(input) {
|
|
5584
|
-
const raw = String(input ?? "").trim();
|
|
5585
|
-
if (!raw) return null;
|
|
5586
|
-
if (ARTIFACT_ID.test(raw)) return raw;
|
|
5587
|
-
let parsed = null;
|
|
5588
|
-
try {
|
|
5589
|
-
parsed = new URL(raw);
|
|
5590
|
-
} catch {
|
|
5591
|
-
parsed = null;
|
|
5592
|
-
}
|
|
5593
|
-
if (parsed) {
|
|
5594
|
-
const fromQuery = parsed.searchParams.get("preview");
|
|
5595
|
-
if (fromQuery && ARTIFACT_ID.test(fromQuery.trim())) return fromQuery.trim();
|
|
5596
|
-
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
5597
|
-
for(let i = segments.length - 1; i >= 0; i -= 1){
|
|
5598
|
-
const segment = decodeURIComponent(segments[i]);
|
|
5599
|
-
if (ARTIFACT_ID.test(segment)) return segment;
|
|
5600
|
-
}
|
|
5601
|
-
}
|
|
5602
|
-
const loose = ARTIFACT_ID_ANYWHERE.exec(raw);
|
|
5603
|
-
return loose ? loose[0] : null;
|
|
5604
|
-
}
|
|
5605
|
-
function authError(name) {
|
|
5606
|
-
return {
|
|
5607
|
-
ok: false,
|
|
5608
|
-
error: {
|
|
5609
|
-
name,
|
|
5610
|
-
message: "No token. Run extension_auth (action: login), or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard)."
|
|
5611
|
-
}
|
|
5612
|
-
};
|
|
5613
|
-
}
|
|
5614
|
-
async function readBody(res) {
|
|
5615
|
-
const text = await res.text();
|
|
5616
|
-
try {
|
|
5617
|
-
return JSON.parse(text);
|
|
5618
|
-
} catch {
|
|
5619
|
-
return {
|
|
5620
|
-
message: text
|
|
5621
|
-
};
|
|
5622
|
-
}
|
|
5623
|
-
}
|
|
5624
|
-
async function listArtifacts(options = {}) {
|
|
5625
|
-
const token = options.token ?? resolveToken();
|
|
5626
|
-
if (!token) return authError("SharesAuthError");
|
|
5627
|
-
const apiCheck = safeApiBase(resolveApiBase(options.api));
|
|
5628
|
-
if (!apiCheck.ok) return {
|
|
5629
|
-
ok: false,
|
|
5630
|
-
error: {
|
|
5631
|
-
name: "SharesConfigError",
|
|
5632
|
-
message: apiCheck.message
|
|
5633
|
-
}
|
|
5634
|
-
};
|
|
5635
|
-
const url = new URL(`${apiCheck.base}/api/artifacts`);
|
|
5636
|
-
if (null != options.limit) url.searchParams.set("limit", String(options.limit));
|
|
5637
|
-
if (options.liveOnly) url.searchParams.set("status", "live");
|
|
5638
|
-
const doFetch = options.fetchImpl ?? fetch;
|
|
5639
|
-
let res;
|
|
5640
|
-
try {
|
|
5641
|
-
res = await doFetch(url.toString(), {
|
|
5642
|
-
headers: {
|
|
5643
|
-
authorization: `Bearer ${token}`,
|
|
5644
|
-
accept: "application/json",
|
|
5645
|
-
...identityHeaders("extension_shares")
|
|
5646
|
-
}
|
|
5647
|
-
});
|
|
5648
|
-
} catch (err) {
|
|
5649
|
-
return {
|
|
5650
|
-
ok: false,
|
|
5651
|
-
error: {
|
|
5652
|
-
name: "SharesNetworkError",
|
|
5653
|
-
message: `Could not reach ${url.toString()}: ${err?.message || err}`
|
|
5654
|
-
}
|
|
5655
|
-
};
|
|
5656
|
-
}
|
|
5657
|
-
const data = await readBody(res);
|
|
5658
|
-
if (401 === res.status) return authError("SharesAuthError");
|
|
5659
|
-
if (!res.ok) return {
|
|
5660
|
-
ok: false,
|
|
5661
|
-
error: {
|
|
5662
|
-
name: "SharesListError",
|
|
5663
|
-
status: res.status,
|
|
5664
|
-
message: `Listing shares failed (${res.status}): ${data?.message || "unknown error"}`
|
|
5665
|
-
}
|
|
5666
|
-
};
|
|
5667
|
-
const artifacts = Array.isArray(data.artifacts) ? data.artifacts : [];
|
|
5668
|
-
const num = (value, fallback)=>"number" == typeof value && Number.isFinite(value) ? value : fallback;
|
|
5669
|
-
return {
|
|
5670
|
-
ok: true,
|
|
5671
|
-
data: {
|
|
5672
|
-
artifacts,
|
|
5673
|
-
count: num(data.count, artifacts.length),
|
|
5674
|
-
matched: num(data.matched, artifacts.length),
|
|
5675
|
-
limit: num(data.limit, artifacts.length),
|
|
5676
|
-
truncated: true === data.truncated,
|
|
5677
|
-
scanned: num(data.scanned, 0)
|
|
5678
|
-
}
|
|
5679
|
-
};
|
|
5680
|
-
}
|
|
5681
|
-
async function revokeArtifact(options) {
|
|
5682
|
-
const token = options.token ?? resolveToken();
|
|
5683
|
-
if (!token) return authError("SharesAuthError");
|
|
5684
|
-
const apiCheck = safeApiBase(resolveApiBase(options.api));
|
|
5685
|
-
if (!apiCheck.ok) return {
|
|
5686
|
-
ok: false,
|
|
5687
|
-
error: {
|
|
5688
|
-
name: "SharesConfigError",
|
|
5689
|
-
message: apiCheck.message
|
|
5690
|
-
}
|
|
5691
|
-
};
|
|
5692
|
-
const url = `${apiCheck.base}/api/artifacts/${encodeURIComponent(options.artifactId)}`;
|
|
5693
|
-
const doFetch = options.fetchImpl ?? fetch;
|
|
5694
|
-
let res;
|
|
5695
|
-
try {
|
|
5696
|
-
res = await doFetch(url, {
|
|
5697
|
-
method: "DELETE",
|
|
5698
|
-
headers: {
|
|
5699
|
-
authorization: `Bearer ${token}`,
|
|
5700
|
-
accept: "application/json",
|
|
5701
|
-
...identityHeaders("extension_shares")
|
|
6658
|
+
accept: "application/json"
|
|
5702
6659
|
}
|
|
5703
6660
|
});
|
|
6661
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
6662
|
+
let raw = null;
|
|
6663
|
+
if (res.ok && contentType.includes("application/json")) try {
|
|
6664
|
+
raw = await res.json();
|
|
6665
|
+
} catch {
|
|
6666
|
+
raw = null;
|
|
6667
|
+
}
|
|
6668
|
+
const payload = raw && "object" == typeof raw && !Array.isArray(raw) ? raw : null;
|
|
6669
|
+
if (!payload) return envelope_envelope({
|
|
6670
|
+
ok: true,
|
|
6671
|
+
command: preview_web_COMMAND,
|
|
6672
|
+
status: "host-not-serving-preview",
|
|
6673
|
+
value: {
|
|
6674
|
+
...result,
|
|
6675
|
+
hostReachable: true,
|
|
6676
|
+
previewLoadable: false,
|
|
6677
|
+
probe: {
|
|
6678
|
+
status: res.status,
|
|
6679
|
+
contentType
|
|
6680
|
+
}
|
|
6681
|
+
},
|
|
6682
|
+
hint,
|
|
6683
|
+
warnings: [
|
|
6684
|
+
...previewWarnings,
|
|
6685
|
+
`${SURFACE.label} answered but not with a preview payload. On the deployed host ${SURFACE.fetchPath} does not exist, because it is dev-only. ${remedy}`
|
|
6686
|
+
]
|
|
6687
|
+
});
|
|
6688
|
+
const localName = "string" == typeof manifest.name ? manifest.name : null;
|
|
6689
|
+
const localVersion = "string" == typeof manifest.version ? manifest.version : null;
|
|
6690
|
+
const remoteName = "string" == typeof payload.manifest?.name ? payload.manifest.name : null;
|
|
6691
|
+
const remoteVersion = "string" == typeof payload.version ? payload.version : null;
|
|
6692
|
+
const remoteIdentifier = "string" == typeof payload.identifier ? payload.identifier : null;
|
|
6693
|
+
const fileCount = Array.isArray(payload.files) ? payload.files.length : 0;
|
|
6694
|
+
const matchesDist = (null === localName || remoteName === localName) && (null === localVersion || remoteVersion === localVersion);
|
|
6695
|
+
if (!matchesDist) {
|
|
6696
|
+
const hostReported = {
|
|
6697
|
+
...null !== remoteIdentifier ? {
|
|
6698
|
+
identifier: clipHostClaim(remoteIdentifier)
|
|
6699
|
+
} : {},
|
|
6700
|
+
...null !== remoteName ? {
|
|
6701
|
+
name: clipHostClaim(remoteName)
|
|
6702
|
+
} : {},
|
|
6703
|
+
...null !== remoteVersion ? {
|
|
6704
|
+
version: clipHostClaim(remoteVersion)
|
|
6705
|
+
} : {}
|
|
6706
|
+
};
|
|
6707
|
+
return envelope_envelope({
|
|
6708
|
+
ok: true,
|
|
6709
|
+
command: preview_web_COMMAND,
|
|
6710
|
+
status: "host-serving-different-artifact",
|
|
6711
|
+
value: {
|
|
6712
|
+
...result,
|
|
6713
|
+
hostReachable: true,
|
|
6714
|
+
previewLoadable: false,
|
|
6715
|
+
probe: {
|
|
6716
|
+
method: "server-fetch",
|
|
6717
|
+
provesBrowserLoad: false,
|
|
6718
|
+
matchesDist: false,
|
|
6719
|
+
fileCount,
|
|
6720
|
+
...Object.keys(hostReported).length ? {
|
|
6721
|
+
hostReported
|
|
6722
|
+
} : {}
|
|
6723
|
+
}
|
|
6724
|
+
},
|
|
6725
|
+
hint,
|
|
6726
|
+
warnings: [
|
|
6727
|
+
...previewWarnings,
|
|
6728
|
+
`Something answered at ${hostBase}${SURFACE.fetchPath} but described a different artifact than the build this call minted, so previewLoadable stays false. probe.hostReported carries that host's own claims, clipped and unverified, not facts about your build; another local server on this port is the usual cause. ${remedy}`
|
|
6729
|
+
]
|
|
6730
|
+
});
|
|
6731
|
+
}
|
|
6732
|
+
const identifierConfirmed = null !== remoteIdentifier && remoteIdentifier === expectedPreviewIdentifier(localName, distDir);
|
|
6733
|
+
const unconfirmed = {
|
|
6734
|
+
...!identifierConfirmed && null !== remoteIdentifier ? {
|
|
6735
|
+
identifier: clipHostClaim(remoteIdentifier)
|
|
6736
|
+
} : {},
|
|
6737
|
+
...null === localName && null !== remoteName ? {
|
|
6738
|
+
name: clipHostClaim(remoteName)
|
|
6739
|
+
} : {},
|
|
6740
|
+
...null === localVersion && null !== remoteVersion ? {
|
|
6741
|
+
version: clipHostClaim(remoteVersion)
|
|
6742
|
+
} : {}
|
|
6743
|
+
};
|
|
6744
|
+
return envelope_envelope({
|
|
6745
|
+
ok: true,
|
|
6746
|
+
command: preview_web_COMMAND,
|
|
6747
|
+
status: "previewed",
|
|
6748
|
+
value: {
|
|
6749
|
+
...result,
|
|
6750
|
+
hostReachable: true,
|
|
6751
|
+
previewLoadable: true,
|
|
6752
|
+
previewLoadableLane: "local-dev-host",
|
|
6753
|
+
probe: {
|
|
6754
|
+
...identifierConfirmed ? {
|
|
6755
|
+
identifier: remoteIdentifier
|
|
6756
|
+
} : {},
|
|
6757
|
+
...null !== localName ? {
|
|
6758
|
+
loadedName: localName
|
|
6759
|
+
} : {},
|
|
6760
|
+
...null !== localVersion ? {
|
|
6761
|
+
loadedVersion: localVersion
|
|
6762
|
+
} : {},
|
|
6763
|
+
fileCount,
|
|
6764
|
+
method: "server-fetch",
|
|
6765
|
+
provesBrowserLoad: false,
|
|
6766
|
+
matchesDist: true,
|
|
6767
|
+
...Object.keys(unconfirmed).length ? {
|
|
6768
|
+
hostReported: unconfirmed
|
|
6769
|
+
} : {}
|
|
6770
|
+
}
|
|
6771
|
+
},
|
|
6772
|
+
hint,
|
|
6773
|
+
warnings: previewWarnings
|
|
6774
|
+
});
|
|
5704
6775
|
} catch (err) {
|
|
5705
|
-
|
|
6776
|
+
const shared = result.share;
|
|
6777
|
+
if (args.share && shared?.ok === true) return envelope_envelope({
|
|
6778
|
+
ok: true,
|
|
6779
|
+
command: preview_web_COMMAND,
|
|
6780
|
+
status: "shared",
|
|
6781
|
+
value: {
|
|
6782
|
+
...result,
|
|
6783
|
+
hostReachable: false,
|
|
6784
|
+
previewLoadable: false,
|
|
6785
|
+
probe: {
|
|
6786
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6787
|
+
}
|
|
6788
|
+
},
|
|
6789
|
+
hint: "share.previewUrl is live and needs no local server. The deepLink lane is separate: it only resolves against a preview.extension.dev dev server on this machine, and none answered.",
|
|
6790
|
+
warnings: [
|
|
6791
|
+
...previewWarnings,
|
|
6792
|
+
`The share link works; only the local ${SURFACE.label} dev lane at ${hostBase} is unreachable, which is expected outside the extension.dev monorepo.`
|
|
6793
|
+
]
|
|
6794
|
+
});
|
|
6795
|
+
return envelope_envelope({
|
|
5706
6796
|
ok: false,
|
|
6797
|
+
command: preview_web_COMMAND,
|
|
6798
|
+
status: "host-unreachable",
|
|
5707
6799
|
error: {
|
|
5708
|
-
|
|
5709
|
-
message: `
|
|
5710
|
-
}
|
|
5711
|
-
|
|
6800
|
+
code: "E_PREVIEW_HOST_UNREACHABLE",
|
|
6801
|
+
message: `Nothing is serving ${SURFACE.label} at ${hostBase}, so deepLink has nothing to open. ${remedy}`
|
|
6802
|
+
},
|
|
6803
|
+
value: {
|
|
6804
|
+
...result,
|
|
6805
|
+
hostReachable: false,
|
|
6806
|
+
previewLoadable: false,
|
|
6807
|
+
probe: {
|
|
6808
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6809
|
+
}
|
|
6810
|
+
},
|
|
6811
|
+
hint,
|
|
6812
|
+
warnings: [
|
|
6813
|
+
...previewWarnings,
|
|
6814
|
+
`Could not reach ${SURFACE.label} at ${hostBase}. ${remedy}`
|
|
6815
|
+
]
|
|
6816
|
+
});
|
|
5712
6817
|
}
|
|
5713
|
-
const data = await readBody(res);
|
|
5714
|
-
if (401 === res.status) return authError("SharesAuthError");
|
|
5715
|
-
if (404 === res.status) return {
|
|
5716
|
-
ok: false,
|
|
5717
|
-
error: {
|
|
5718
|
-
name: "SharesNotFoundError",
|
|
5719
|
-
status: 404,
|
|
5720
|
-
message: `The platform has no live share ${options.artifactId} for this token. It may already be revoked, already expired, owned by a different project than the one this token is scoped to, or a teammate's personal share, which belongs to that person alone and no project token can revoke.`
|
|
5721
|
-
}
|
|
5722
|
-
};
|
|
5723
|
-
if (!res.ok) return {
|
|
5724
|
-
ok: false,
|
|
5725
|
-
error: {
|
|
5726
|
-
name: "SharesRevokeError",
|
|
5727
|
-
status: res.status,
|
|
5728
|
-
message: `Revoking ${options.artifactId} failed (${res.status}): ${data?.message || "unknown error"}`
|
|
5729
|
-
}
|
|
5730
|
-
};
|
|
5731
|
-
return {
|
|
5732
|
-
ok: true,
|
|
5733
|
-
data: {
|
|
5734
|
-
artifactId: options.artifactId,
|
|
5735
|
-
revoked: true === data.revoked,
|
|
5736
|
-
..."string" == typeof data.revokedAt ? {
|
|
5737
|
-
revokedAt: data.revokedAt
|
|
5738
|
-
} : {}
|
|
5739
|
-
}
|
|
5740
|
-
};
|
|
5741
6818
|
}
|
|
5742
6819
|
const LOGIN_HINT = "Run extension_auth (action: login), or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard).";
|
|
5743
6820
|
const shares_schema = {
|
|
@@ -5839,8 +6916,9 @@ function localIndex(entries) {
|
|
|
5839
6916
|
}
|
|
5840
6917
|
return index;
|
|
5841
6918
|
}
|
|
5842
|
-
function localOnlyStatus(entry,
|
|
5843
|
-
if (
|
|
6919
|
+
function localOnlyStatus(entry, completeness, liveFiltered, now) {
|
|
6920
|
+
if ("cut" === completeness) return "unknown: the platform list was cut short, so this share may simply be past the returned window.";
|
|
6921
|
+
if ("unsaid" === completeness) return "unknown: the platform did not say whether that list was whole, so this share may simply be past the returned window.";
|
|
5844
6922
|
const expiresAt = entry.expiresAt ? Date.parse(entry.expiresAt) : NaN;
|
|
5845
6923
|
if (Number.isFinite(expiresAt) && expiresAt <= now) return "expired: its own expiresAt has passed, and the platform no longer lists it.";
|
|
5846
6924
|
if (liveFiltered) return 'not in this live-only listing: the platform was asked for live shares only, so this one may be expired or revoked rather than not owned. Rerun with status:"all" to tell.';
|
|
@@ -5871,7 +6949,7 @@ async function listShares(args) {
|
|
|
5871
6949
|
});
|
|
5872
6950
|
if (!listing.ok) {
|
|
5873
6951
|
const isAuth = "SharesAuthError" === listing.error.name;
|
|
5874
|
-
return
|
|
6952
|
+
return envelope_envelope({
|
|
5875
6953
|
ok: true,
|
|
5876
6954
|
command: "extension_shares",
|
|
5877
6955
|
status: "listed-local-only",
|
|
@@ -5887,7 +6965,10 @@ async function listShares(args) {
|
|
|
5887
6965
|
},
|
|
5888
6966
|
shares: [],
|
|
5889
6967
|
localOnly: (local?.entries ?? []).map((entry)=>({
|
|
5890
|
-
...entry
|
|
6968
|
+
...entry,
|
|
6969
|
+
...entry.revokeUrl ? {
|
|
6970
|
+
revokeUrl: wwwRevokeUrl(entry.revokeUrl)
|
|
6971
|
+
} : {}
|
|
5891
6972
|
})),
|
|
5892
6973
|
localRecord
|
|
5893
6974
|
},
|
|
@@ -5921,9 +7002,13 @@ async function listShares(args) {
|
|
|
5921
7002
|
} : {}
|
|
5922
7003
|
};
|
|
5923
7004
|
});
|
|
7005
|
+
const completeness = listing.data.truncatedReported ? listing.data.truncated ? "cut" : "whole" : "unsaid";
|
|
5924
7006
|
const localOnly = (local?.entries ?? []).filter((entry)=>!seen.has(entry.artifactId)).map((entry)=>({
|
|
5925
7007
|
...entry,
|
|
5926
|
-
|
|
7008
|
+
...entry.revokeUrl ? {
|
|
7009
|
+
revokeUrl: wwwRevokeUrl(entry.revokeUrl)
|
|
7010
|
+
} : {},
|
|
7011
|
+
status: localOnlyStatus(entry, completeness, liveFiltered, now)
|
|
5927
7012
|
}));
|
|
5928
7013
|
const liveCount = shares.filter((share)=>share.live).length;
|
|
5929
7014
|
const ownership = {
|
|
@@ -5931,8 +7016,8 @@ async function listShares(args) {
|
|
|
5931
7016
|
personal: shares.filter((s)=>"personal" === s.attribution.ownership).length,
|
|
5932
7017
|
unknown: shares.filter((s)=>"unknown" === s.attribution.ownership).length
|
|
5933
7018
|
};
|
|
5934
|
-
const truncatedNote =
|
|
5935
|
-
return
|
|
7019
|
+
const truncatedNote = "cut" === completeness ? `This is not the whole set: ${listing.data.count} of ${listing.data.matched} matched shares came back at limit ${listing.data.limit}. truncated also goes true when the server spent its budget working out which shares you are entitled to see, so matched is a floor and not a total. Raise limit (max 200) or pass status:"live" to narrow it, and do not read a missing share as revoked.` : "unsaid" === completeness ? `This listing cannot be called whole: the platform returned no truncated field, so whether ${listing.data.count} shares are all of them is unstated. A share missing from it is unaccounted for rather than dead, and no missing share is reported as revoked or unowned here.` : null;
|
|
7020
|
+
return envelope_envelope({
|
|
5936
7021
|
ok: true,
|
|
5937
7022
|
command: "extension_shares",
|
|
5938
7023
|
status: "listed",
|
|
@@ -5944,6 +7029,7 @@ async function listShares(args) {
|
|
|
5944
7029
|
matched: listing.data.matched,
|
|
5945
7030
|
limit: listing.data.limit,
|
|
5946
7031
|
truncated: listing.data.truncated,
|
|
7032
|
+
truncatedReported: listing.data.truncatedReported,
|
|
5947
7033
|
scanned: listing.data.scanned,
|
|
5948
7034
|
ownership,
|
|
5949
7035
|
...truncatedNote ? {
|
|
@@ -5958,25 +7044,29 @@ async function listShares(args) {
|
|
|
5958
7044
|
},
|
|
5959
7045
|
hint: `${liveCount} of ${shares.length} listed shares still resolve${local ? `; ${localOnly.length} local record ${1 === localOnly.length ? "entry is" : "entries are"} ${liveFiltered ? 'not in this live-only listing (possibly dead rather than not owned; rerun with status:"all" to tell)' : "without an artifact behind them"}` : ""}. Revoke one with action:"revoke" and its artifactId or any of its URLs.`,
|
|
5960
7046
|
warnings: [
|
|
5961
|
-
"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
|
|
7047
|
+
"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.",
|
|
5962
7048
|
"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.",
|
|
5963
7049
|
truncatedNote
|
|
5964
7050
|
]
|
|
5965
7051
|
});
|
|
5966
7052
|
}
|
|
5967
7053
|
async function revokeShare(args) {
|
|
5968
|
-
const
|
|
5969
|
-
|
|
7054
|
+
const supplied = String(args.artifactId || args.url || "").trim();
|
|
7055
|
+
const ref = parseArtifactRef(supplied);
|
|
7056
|
+
if (!ref) return envelope_envelope({
|
|
5970
7057
|
ok: false,
|
|
5971
7058
|
command: "extension_shares",
|
|
5972
7059
|
status: "bad-request",
|
|
5973
7060
|
value: {
|
|
5974
|
-
action: "revoke"
|
|
7061
|
+
action: "revoke",
|
|
7062
|
+
...supplied ? {
|
|
7063
|
+
ref: supplied
|
|
7064
|
+
} : {}
|
|
5975
7065
|
},
|
|
5976
7066
|
error: {
|
|
5977
7067
|
code: "E_BAD_REQUEST",
|
|
5978
7068
|
name: "SharesInputError",
|
|
5979
|
-
message: "Nothing to revoke. Pass artifactId (a gen_... id) or url (the previewUrl, zipUrl, viewUrl, or revokeUrl of the share). Run action
|
|
7069
|
+
message: supplied ? `The reference is malformed and nothing was sent to the platform: no share id could be read out of ${JSON.stringify(supplied.length > 200 ? `${supplied.slice(0, 200)}...` : supplied)}. A share id is gen_ followed by exactly 64 lowercase hex characters (32 on shares minted before 2026-07-30), and a url revokes only when it contains one whole. Check the ref for a copy-paste cut, or run action:"list" and copy the exact artifactId.` : 'Nothing to revoke. Pass artifactId (a gen_... id) or url (the previewUrl, zipUrl, viewUrl, or revokeUrl of the share). Run action:"list" to see both.'
|
|
5980
7070
|
}
|
|
5981
7071
|
});
|
|
5982
7072
|
const result = await revokeArtifact({
|
|
@@ -5990,7 +7080,7 @@ async function revokeShare(args) {
|
|
|
5990
7080
|
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;
|
|
5991
7081
|
if (!result.ok) {
|
|
5992
7082
|
const isAuth = "SharesAuthError" === result.error.name;
|
|
5993
|
-
return
|
|
7083
|
+
return envelope_envelope({
|
|
5994
7084
|
ok: false,
|
|
5995
7085
|
command: "extension_shares",
|
|
5996
7086
|
status: "revoke-failed",
|
|
@@ -6011,7 +7101,7 @@ async function revokeShare(args) {
|
|
|
6011
7101
|
]
|
|
6012
7102
|
});
|
|
6013
7103
|
}
|
|
6014
|
-
if (true !== result.data.revoked) return
|
|
7104
|
+
if (true !== result.data.revoked) return envelope_envelope({
|
|
6015
7105
|
ok: true,
|
|
6016
7106
|
command: "extension_shares",
|
|
6017
7107
|
status: "revoke-unconfirmed",
|
|
@@ -6028,7 +7118,7 @@ async function revokeShare(args) {
|
|
|
6028
7118
|
recordNote
|
|
6029
7119
|
]
|
|
6030
7120
|
});
|
|
6031
|
-
return
|
|
7121
|
+
return envelope_envelope({
|
|
6032
7122
|
ok: true,
|
|
6033
7123
|
command: "extension_shares",
|
|
6034
7124
|
status: "revoked",
|
|
@@ -6374,7 +7464,7 @@ async function manifest_validate_handler(args) {
|
|
|
6374
7464
|
const errors = [
|
|
6375
7465
|
args.projectPath ? `No manifest.json found under ${args.projectPath} (looked in the root and src/).` : "Pass manifestPath (path to manifest.json) or projectPath (project root)."
|
|
6376
7466
|
];
|
|
6377
|
-
return
|
|
7467
|
+
return envelope_envelope({
|
|
6378
7468
|
ok: false,
|
|
6379
7469
|
command: manifest_validate_COMMAND,
|
|
6380
7470
|
status: "manifest-not-found",
|
|
@@ -6400,7 +7490,7 @@ async function manifest_validate_handler(args) {
|
|
|
6400
7490
|
const errors = [
|
|
6401
7491
|
`Cannot read manifest: ${err instanceof Error ? err.message : err}`
|
|
6402
7492
|
];
|
|
6403
|
-
return
|
|
7493
|
+
return envelope_envelope({
|
|
6404
7494
|
ok: false,
|
|
6405
7495
|
command: manifest_validate_COMMAND,
|
|
6406
7496
|
status: "manifest-unreadable",
|
|
@@ -6535,7 +7625,7 @@ async function manifest_validate_handler(args) {
|
|
|
6535
7625
|
else result.warnings.push(`${browser} (not requested, checked by default): ${issues}`);
|
|
6536
7626
|
}
|
|
6537
7627
|
result.valid = 0 === result.errors.length;
|
|
6538
|
-
return
|
|
7628
|
+
return envelope_envelope({
|
|
6539
7629
|
ok: result.valid,
|
|
6540
7630
|
command: manifest_validate_COMMAND,
|
|
6541
7631
|
status: result.valid ? "valid" : "invalid",
|
|
@@ -7217,7 +8307,7 @@ async function theme_verify_handler(args) {
|
|
|
7217
8307
|
try {
|
|
7218
8308
|
text = await promises.readFile(abs, "utf8");
|
|
7219
8309
|
} catch {
|
|
7220
|
-
return
|
|
8310
|
+
return envelope_envelope({
|
|
7221
8311
|
ok: false,
|
|
7222
8312
|
command: theme_verify_COMMAND,
|
|
7223
8313
|
status: "bad-input",
|
|
@@ -7231,7 +8321,7 @@ async function theme_verify_handler(args) {
|
|
|
7231
8321
|
try {
|
|
7232
8322
|
raw = JSON.parse(text);
|
|
7233
8323
|
} catch (err) {
|
|
7234
|
-
return
|
|
8324
|
+
return envelope_envelope({
|
|
7235
8325
|
ok: false,
|
|
7236
8326
|
command: theme_verify_COMMAND,
|
|
7237
8327
|
status: "bad-input",
|
|
@@ -7243,7 +8333,7 @@ async function theme_verify_handler(args) {
|
|
|
7243
8333
|
});
|
|
7244
8334
|
}
|
|
7245
8335
|
} else {
|
|
7246
|
-
if (!args.manifest) return
|
|
8336
|
+
if (!args.manifest) return envelope_envelope({
|
|
7247
8337
|
ok: false,
|
|
7248
8338
|
command: theme_verify_COMMAND,
|
|
7249
8339
|
status: "bad-request",
|
|
@@ -7260,7 +8350,7 @@ async function theme_verify_handler(args) {
|
|
|
7260
8350
|
try {
|
|
7261
8351
|
({ manifest, theme } = coerceInput(raw));
|
|
7262
8352
|
} catch (err) {
|
|
7263
|
-
return
|
|
8353
|
+
return envelope_envelope({
|
|
7264
8354
|
ok: false,
|
|
7265
8355
|
command: theme_verify_COMMAND,
|
|
7266
8356
|
status: "bad-input",
|
|
@@ -7364,7 +8454,7 @@ async function theme_verify_handler(args) {
|
|
|
7364
8454
|
how: "a Chrome theme is an unpacked extension: extension_dev then extension_logs on the exported theme dir (headless, focus-safe)"
|
|
7365
8455
|
}
|
|
7366
8456
|
];
|
|
7367
|
-
return
|
|
8457
|
+
return envelope_envelope({
|
|
7368
8458
|
ok: true,
|
|
7369
8459
|
command: theme_verify_COMMAND,
|
|
7370
8460
|
status: verdict,
|
|
@@ -7526,7 +8616,7 @@ function formatBytes(bytes) {
|
|
|
7526
8616
|
async function analyze_handler(args) {
|
|
7527
8617
|
const browser = args.browser ?? "chrome";
|
|
7528
8618
|
const distPath = node_path.resolve(args.projectPath, "dist", browser);
|
|
7529
|
-
if (!node_fs.existsSync(distPath)) return
|
|
8619
|
+
if (!node_fs.existsSync(distPath)) return envelope_envelope({
|
|
7530
8620
|
ok: false,
|
|
7531
8621
|
command: analyze_COMMAND,
|
|
7532
8622
|
status: "no-dist",
|
|
@@ -7636,7 +8726,7 @@ async function analyze_handler(args) {
|
|
|
7636
8726
|
under10MB: totalSize - archiveSize < 10485760
|
|
7637
8727
|
}
|
|
7638
8728
|
};
|
|
7639
|
-
return
|
|
8729
|
+
return envelope_envelope({
|
|
7640
8730
|
ok: true,
|
|
7641
8731
|
command: analyze_COMMAND,
|
|
7642
8732
|
status: "analyzed",
|
|
@@ -8124,7 +9214,7 @@ async function inspectViaBridge(args, browser, include, maxBytes) {
|
|
|
8124
9214
|
}
|
|
8125
9215
|
const frame = Array.isArray(parsed?.value?.frames) ? parsed.value.frames[0] : null;
|
|
8126
9216
|
if (parsed?.ok === true && frame && "object" == typeof frame) value = frame;
|
|
8127
|
-
else if (parsed?.ok === true) return
|
|
9217
|
+
else if (parsed?.ok === true) return envelope_envelope({
|
|
8128
9218
|
ok: false,
|
|
8129
9219
|
command: TOOL,
|
|
8130
9220
|
status: "inspect-failed",
|
|
@@ -8167,7 +9257,7 @@ async function inspectViaBridge(args, browser, include, maxBytes) {
|
|
|
8167
9257
|
const cap = maxBytes > 0 ? maxBytes : 65536;
|
|
8168
9258
|
await collectGeckoDeepDom(args, browser, urlFilter, cap, result, notes);
|
|
8169
9259
|
}
|
|
8170
|
-
return
|
|
9260
|
+
return envelope_envelope({
|
|
8171
9261
|
ok: true,
|
|
8172
9262
|
command: TOOL,
|
|
8173
9263
|
status: "inspected",
|
|
@@ -8188,7 +9278,7 @@ async function inspect_handler(args) {
|
|
|
8188
9278
|
const maxBytes = args.maxBytes ?? 262144;
|
|
8189
9279
|
if (!isChromiumFamily(browser)) return inspectViaBridge(args, browser, include, maxBytes);
|
|
8190
9280
|
const resolved = await resolveCdpPort(args.projectPath, browser);
|
|
8191
|
-
if (!resolved) return
|
|
9281
|
+
if (!resolved) return envelope_envelope({
|
|
8192
9282
|
ok: false,
|
|
8193
9283
|
command: inspect_schema_schema.name,
|
|
8194
9284
|
status: "no-session",
|
|
@@ -8212,7 +9302,7 @@ async function inspect_handler(args) {
|
|
|
8212
9302
|
const pageTargets = allTargets.filter((t)=>"page" === t.type && !t.url.startsWith("devtools://") && (!t.url.startsWith("chrome://") || isOverridePage(t.url)));
|
|
8213
9303
|
if (0 === pageTargets.length) {
|
|
8214
9304
|
const chromeOnly = allTargets.some((t)=>"page" === t.type && t.url.startsWith("chrome://"));
|
|
8215
|
-
return
|
|
9305
|
+
return envelope_envelope({
|
|
8216
9306
|
ok: false,
|
|
8217
9307
|
command: inspect_schema_schema.name,
|
|
8218
9308
|
status: "no-inspectable-target",
|
|
@@ -8231,7 +9321,17 @@ async function inspect_handler(args) {
|
|
|
8231
9321
|
});
|
|
8232
9322
|
}
|
|
8233
9323
|
const isExtensionSurface = (u)=>u.startsWith("chrome-extension://") || u.startsWith("moz-extension://");
|
|
8234
|
-
const
|
|
9324
|
+
const defaultRank = (u)=>{
|
|
9325
|
+
if (isEngineCompanionUrl(u)) return 3;
|
|
9326
|
+
if (isOverridePage(u)) return 2;
|
|
9327
|
+
if (isExtensionSurface(u)) return 0;
|
|
9328
|
+
return 1;
|
|
9329
|
+
};
|
|
9330
|
+
const target = args.url ? pageTargets.find((t)=>t.url.includes(args.url)) ?? pageTargets.find((t)=>!isExtensionSurface(t.url)) ?? pageTargets[0] : pageTargets.map((t, index)=>({
|
|
9331
|
+
t,
|
|
9332
|
+
index,
|
|
9333
|
+
rank: defaultRank(t.url)
|
|
9334
|
+
})).sort((a, b)=>a.rank - b.rank || a.index - b.index)[0].t;
|
|
8235
9335
|
const browserWsUrl = await CDPClient.discoverBrowserWsUrl(cdpPort);
|
|
8236
9336
|
await cdp.connect(browserWsUrl);
|
|
8237
9337
|
const sessionId = await cdp.attachToTarget(target.id);
|
|
@@ -8240,13 +9340,27 @@ async function inspect_handler(args) {
|
|
|
8240
9340
|
await cdp.navigate(sessionId, args.url);
|
|
8241
9341
|
await new Promise((r)=>setTimeout(r, 1500));
|
|
8242
9342
|
} else await new Promise((r)=>setTimeout(r, 500));
|
|
9343
|
+
let documentUrl = "";
|
|
9344
|
+
let toolchainWarning = null;
|
|
9345
|
+
if (!args.url && defaultRank(target.url) >= 2) {
|
|
9346
|
+
try {
|
|
9347
|
+
const href = await cdp.evaluate(sessionId, "location.href");
|
|
9348
|
+
documentUrl = "string" == typeof href ? href : "";
|
|
9349
|
+
} catch {
|
|
9350
|
+
documentUrl = "";
|
|
9351
|
+
}
|
|
9352
|
+
toolchainWarning = isEngineCompanionUrl(documentUrl) || isEngineCompanionUrl(target.url) ? `Inspected ${target.url}, which is rendered by the Extension.js toolchain's own companion extension, not by this project. Pass url, or open one of the extension's surfaces first (extension_open), then inspect again.` : `No url was given and only override pages were open, so this inspected ${target.url}. Unless this extension provides that override itself, this is the toolchain's welcome surface, not your extension: pass url or open a surface with extension_open.`;
|
|
9353
|
+
}
|
|
8243
9354
|
const result = {
|
|
8244
9355
|
cdpPort,
|
|
8245
9356
|
browser,
|
|
8246
9357
|
target: {
|
|
8247
9358
|
id: target.id,
|
|
8248
9359
|
url: target.url,
|
|
8249
|
-
title: target.title
|
|
9360
|
+
title: target.title,
|
|
9361
|
+
...documentUrl && documentUrl !== target.url ? {
|
|
9362
|
+
documentUrl
|
|
9363
|
+
} : {}
|
|
8250
9364
|
},
|
|
8251
9365
|
targets: pageTargets.map((t)=>({
|
|
8252
9366
|
id: t.id,
|
|
@@ -8294,17 +9408,18 @@ async function inspect_handler(args) {
|
|
|
8294
9408
|
}
|
|
8295
9409
|
const probeWarning = result.probeWarning;
|
|
8296
9410
|
delete result.probeWarning;
|
|
8297
|
-
return
|
|
9411
|
+
return envelope_envelope({
|
|
8298
9412
|
ok: true,
|
|
8299
9413
|
command: inspect_schema_schema.name,
|
|
8300
9414
|
status: "inspected",
|
|
8301
9415
|
value: result,
|
|
8302
9416
|
warnings: [
|
|
8303
|
-
"string" == typeof probeWarning ? probeWarning : null
|
|
9417
|
+
"string" == typeof probeWarning ? probeWarning : null,
|
|
9418
|
+
toolchainWarning
|
|
8304
9419
|
]
|
|
8305
9420
|
});
|
|
8306
9421
|
} catch (err) {
|
|
8307
|
-
return
|
|
9422
|
+
return envelope_envelope({
|
|
8308
9423
|
ok: false,
|
|
8309
9424
|
command: inspect_schema_schema.name,
|
|
8310
9425
|
status: "cdp-failed",
|
|
@@ -8347,8 +9462,7 @@ function list_extensions_unpackedExtensionId(distPath) {
|
|
|
8347
9462
|
function readOwnIdentity(projectPath, browser) {
|
|
8348
9463
|
let contract;
|
|
8349
9464
|
try {
|
|
8350
|
-
|
|
8351
|
-
contract = JSON.parse(node_fs.readFileSync(file, "utf8"));
|
|
9465
|
+
contract = JSON.parse(node_fs.readFileSync(bridge_readyContractPath(projectPath, browser), "utf8"));
|
|
8352
9466
|
} catch {
|
|
8353
9467
|
return null;
|
|
8354
9468
|
}
|
|
@@ -8379,7 +9493,7 @@ const UNRESOLVED_NOTE = "Identity unresolved: the browser's Extensions CDP domai
|
|
|
8379
9493
|
async function list_extensions_handler(args) {
|
|
8380
9494
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser, "chrome");
|
|
8381
9495
|
if (isGeckoFamily(browser)) return listGeckoExtensions(args.projectPath, browser);
|
|
8382
|
-
if (!isChromiumFamily(browser)) return
|
|
9496
|
+
if (!isChromiumFamily(browser)) return envelope_envelope({
|
|
8383
9497
|
ok: false,
|
|
8384
9498
|
command: list_extensions_schema.name,
|
|
8385
9499
|
status: "unsupported-browser",
|
|
@@ -8390,7 +9504,7 @@ async function list_extensions_handler(args) {
|
|
|
8390
9504
|
hint: "Target a Chromium-family (CDP) or Firefox-family (RDP) dev session."
|
|
8391
9505
|
});
|
|
8392
9506
|
const resolved = await resolveCdpPort(args.projectPath, browser);
|
|
8393
|
-
if (!resolved) return
|
|
9507
|
+
if (!resolved) return envelope_envelope({
|
|
8394
9508
|
ok: false,
|
|
8395
9509
|
command: list_extensions_schema.name,
|
|
8396
9510
|
status: "no-session",
|
|
@@ -8470,7 +9584,7 @@ async function list_extensions_handler(args) {
|
|
|
8470
9584
|
return (a.name ?? a.id).localeCompare(b.name ?? b.id);
|
|
8471
9585
|
});
|
|
8472
9586
|
const ownEntry = extensions.find((e)=>e.ownExtension);
|
|
8473
|
-
return
|
|
9587
|
+
return envelope_envelope({
|
|
8474
9588
|
ok: true,
|
|
8475
9589
|
command: list_extensions_schema.name,
|
|
8476
9590
|
status: "listed",
|
|
@@ -8486,7 +9600,7 @@ async function list_extensions_handler(args) {
|
|
|
8486
9600
|
]
|
|
8487
9601
|
});
|
|
8488
9602
|
} catch (error) {
|
|
8489
|
-
return
|
|
9603
|
+
return envelope_envelope({
|
|
8490
9604
|
ok: false,
|
|
8491
9605
|
command: list_extensions_schema.name,
|
|
8492
9606
|
status: "cdp-failed",
|
|
@@ -8501,7 +9615,7 @@ async function list_extensions_handler(args) {
|
|
|
8501
9615
|
}
|
|
8502
9616
|
async function listGeckoExtensions(projectPath, browser) {
|
|
8503
9617
|
const resolved = await resolveRdpPort(projectPath, browser);
|
|
8504
|
-
if (!resolved) return
|
|
9618
|
+
if (!resolved) return envelope_envelope({
|
|
8505
9619
|
ok: false,
|
|
8506
9620
|
command: list_extensions_schema.name,
|
|
8507
9621
|
status: "no-session",
|
|
@@ -8555,7 +9669,7 @@ async function listGeckoExtensions(projectPath, browser) {
|
|
|
8555
9669
|
return (a.name ?? a.id).localeCompare(b.name ?? b.id);
|
|
8556
9670
|
});
|
|
8557
9671
|
const ownEntry = extensions.find((e)=>e.ownExtension);
|
|
8558
|
-
return
|
|
9672
|
+
return envelope_envelope({
|
|
8559
9673
|
ok: true,
|
|
8560
9674
|
command: list_extensions_schema.name,
|
|
8561
9675
|
status: "listed",
|
|
@@ -8571,7 +9685,7 @@ async function listGeckoExtensions(projectPath, browser) {
|
|
|
8571
9685
|
]
|
|
8572
9686
|
});
|
|
8573
9687
|
} catch (error) {
|
|
8574
|
-
return
|
|
9688
|
+
return envelope_envelope({
|
|
8575
9689
|
ok: false,
|
|
8576
9690
|
command: list_extensions_schema.name,
|
|
8577
9691
|
status: "rdp-failed",
|
|
@@ -8582,57 +9696,23 @@ async function listGeckoExtensions(projectPath, browser) {
|
|
|
8582
9696
|
});
|
|
8583
9697
|
}
|
|
8584
9698
|
}
|
|
8585
|
-
const
|
|
8586
|
-
const
|
|
8587
|
-
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
"debug",
|
|
8591
|
-
"trace"
|
|
8592
|
-
];
|
|
8593
|
-
function levelRank(level) {
|
|
8594
|
-
const l = "log" === level ? "info" : level;
|
|
8595
|
-
const i = LEVEL_ORDER.indexOf(l);
|
|
8596
|
-
return -1 === i ? LEVEL_ORDER.length : i;
|
|
8597
|
-
}
|
|
8598
|
-
function makeUrlMatcher(pattern) {
|
|
8599
|
-
const hasGlob = pattern.includes("*");
|
|
8600
|
-
let re = null;
|
|
8601
|
-
if (hasGlob) {
|
|
8602
|
-
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
8603
|
-
re = new RegExp(escaped);
|
|
8604
|
-
}
|
|
8605
|
-
return (event)=>{
|
|
8606
|
-
const candidates = [
|
|
8607
|
-
event.url,
|
|
8608
|
-
event.hostname
|
|
8609
|
-
].filter((v)=>"string" == typeof v);
|
|
8610
|
-
if (0 === candidates.length) return false;
|
|
8611
|
-
return candidates.some((c)=>re ? re.test(c) : c.includes(pattern));
|
|
8612
|
-
};
|
|
8613
|
-
}
|
|
9699
|
+
const CLOSE_REFUSAL_FLOOR = 4000;
|
|
9700
|
+
const DEFAULT_LIMIT = 200;
|
|
9701
|
+
const DEFAULT_FOLLOW_MS = 4000;
|
|
9702
|
+
const MIN_FOLLOW_MS = 500;
|
|
9703
|
+
const MAX_FOLLOW_MS = 15000;
|
|
8614
9704
|
function makeFilter(args) {
|
|
8615
|
-
const
|
|
8616
|
-
const
|
|
8617
|
-
const
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
if ("off" === minLevel) {
|
|
8627
|
-
if ("dx.signal" !== event.eventType) return false;
|
|
8628
|
-
} else if ("all" !== minLevel) {
|
|
8629
|
-
if (levelRank(event.level) > levelRank(minLevel)) return false;
|
|
8630
|
-
}
|
|
8631
|
-
if (null != sinceSeq && Number.isFinite(sinceSeq) && "number" == typeof event.seq && event.seq <= sinceSeq) return false;
|
|
8632
|
-
if (urlMatches && !urlMatches(event)) return false;
|
|
8633
|
-
if (null != tabId && Number.isFinite(tabId) && event.tabId !== tabId) return false;
|
|
8634
|
-
return true;
|
|
8635
|
-
};
|
|
9705
|
+
const level = String(args.level || "all").toLowerCase();
|
|
9706
|
+
const loggingOff = "off" === level;
|
|
9707
|
+
const query = {
|
|
9708
|
+
context: args.context,
|
|
9709
|
+
level: loggingOff ? "all" : level,
|
|
9710
|
+
signalsOnly: Boolean(args.signalsOnly) || loggingOff,
|
|
9711
|
+
since: args.since,
|
|
9712
|
+
url: args.url,
|
|
9713
|
+
tab: args.tab
|
|
9714
|
+
};
|
|
9715
|
+
return (event)=>matchesLogQuery(event, query);
|
|
8636
9716
|
}
|
|
8637
9717
|
const logs_schema_schema = {
|
|
8638
9718
|
name: "extension_logs",
|
|
@@ -8702,12 +9782,12 @@ const logs_schema_schema = {
|
|
|
8702
9782
|
},
|
|
8703
9783
|
followMs: {
|
|
8704
9784
|
type: "number",
|
|
8705
|
-
default:
|
|
8706
|
-
description:
|
|
9785
|
+
default: DEFAULT_FOLLOW_MS,
|
|
9786
|
+
description: `How long to collect live frames when follow=true (clamped ${MIN_FOLLOW_MS}–${MAX_FOLLOW_MS}ms).`
|
|
8707
9787
|
},
|
|
8708
9788
|
limit: {
|
|
8709
9789
|
type: "number",
|
|
8710
|
-
default:
|
|
9790
|
+
default: DEFAULT_LIMIT,
|
|
8711
9791
|
description: "How many of the most recent events to return."
|
|
8712
9792
|
}
|
|
8713
9793
|
},
|
|
@@ -8717,23 +9797,6 @@ const logs_schema_schema = {
|
|
|
8717
9797
|
}
|
|
8718
9798
|
};
|
|
8719
9799
|
const logs_TOOL = "extension_logs";
|
|
8720
|
-
function logsFilePath(projectPath, browser) {
|
|
8721
|
-
return node_path.resolve(projectPath, "dist", "extension-js", browser, "logs.ndjson");
|
|
8722
|
-
}
|
|
8723
|
-
function readReadyContract(projectPath, browser) {
|
|
8724
|
-
const readyPath = node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json");
|
|
8725
|
-
try {
|
|
8726
|
-
const c = JSON.parse(node_fs.readFileSync(readyPath, "utf8"));
|
|
8727
|
-
if ("number" != typeof c.controlPort || !c.instanceId) return null;
|
|
8728
|
-
return {
|
|
8729
|
-
controlPort: c.controlPort,
|
|
8730
|
-
instanceId: String(c.instanceId),
|
|
8731
|
-
runId: String(c.runId || "")
|
|
8732
|
-
};
|
|
8733
|
-
} catch {
|
|
8734
|
-
return null;
|
|
8735
|
-
}
|
|
8736
|
-
}
|
|
8737
9800
|
function capRecent(events, limit) {
|
|
8738
9801
|
if (events.length <= limit) return {
|
|
8739
9802
|
events,
|
|
@@ -8747,9 +9810,9 @@ function capRecent(events, limit) {
|
|
|
8747
9810
|
function emptyReason(projectPath, browser) {
|
|
8748
9811
|
let contract;
|
|
8749
9812
|
try {
|
|
8750
|
-
contract = JSON.parse(node_fs.readFileSync(
|
|
9813
|
+
contract = JSON.parse(node_fs.readFileSync(bridge_readyContractPath(projectPath, browser), "utf8"));
|
|
8751
9814
|
} catch {
|
|
8752
|
-
return
|
|
9815
|
+
return `No ready.json for this project/browser: no dev session has produced a build here, so there is nothing to log. Start one with extension_dev. ${sessionPathHint(bridge_readyContractPath(projectPath, browser))}`;
|
|
8753
9816
|
}
|
|
8754
9817
|
if ("error" === contract.status) {
|
|
8755
9818
|
const errs = contract.errors;
|
|
@@ -8767,7 +9830,7 @@ function summarize(events, source, browser, runId, limit, dropped, projectPath,
|
|
|
8767
9830
|
const lastSeq = out.length ? out.reduce((m, e)=>"number" == typeof e.seq && e.seq > m ? e.seq : m, -1) : -1;
|
|
8768
9831
|
const reason = 0 === matched && projectPath ? emptyReason(projectPath, browser) : void 0;
|
|
8769
9832
|
const stale = Boolean(staleNote) && matched > 0;
|
|
8770
|
-
return
|
|
9833
|
+
return envelope_envelope({
|
|
8771
9834
|
ok: true,
|
|
8772
9835
|
command: logs_TOOL,
|
|
8773
9836
|
status: 0 === matched ? "empty" : stale ? "stale" : "read",
|
|
@@ -8792,7 +9855,7 @@ function summarize(events, source, browser, runId, limit, dropped, projectPath,
|
|
|
8792
9855
|
function staleFileNote(projectPath, browser, eventsRunId) {
|
|
8793
9856
|
let contract;
|
|
8794
9857
|
try {
|
|
8795
|
-
contract = JSON.parse(node_fs.readFileSync(
|
|
9858
|
+
contract = JSON.parse(node_fs.readFileSync(bridge_readyContractPath(projectPath, browser), "utf8"));
|
|
8796
9859
|
} catch {
|
|
8797
9860
|
return "These events survive from a previous session: no ready.json exists for this project/browser now, so nothing current is producing logs.";
|
|
8798
9861
|
}
|
|
@@ -8808,8 +9871,8 @@ function staleFileNote(projectPath, browser, eventsRunId) {
|
|
|
8808
9871
|
if (eventsRunId && liveIds.length > 0 && !liveIds.includes(eventsRunId)) return `These events carry runId ${eventsRunId} but the current session is run ${liveIds.join(" / ")}, which has written nothing yet. Do not read these as the current run's output.`;
|
|
8809
9872
|
}
|
|
8810
9873
|
async function readFromFile(args, browser, limit) {
|
|
8811
|
-
const file =
|
|
8812
|
-
if (!node_fs.existsSync(file)) return
|
|
9874
|
+
const file = logsPath(args.projectPath, browser);
|
|
9875
|
+
if (!node_fs.existsSync(file)) return envelope_envelope({
|
|
8813
9876
|
ok: false,
|
|
8814
9877
|
command: logs_TOOL,
|
|
8815
9878
|
status: "no-log-file",
|
|
@@ -8817,7 +9880,7 @@ async function readFromFile(args, browser, limit) {
|
|
|
8817
9880
|
code: "E_LOGS_MISSING",
|
|
8818
9881
|
message: `No logs found at ${file}.`
|
|
8819
9882
|
},
|
|
8820
|
-
hint:
|
|
9883
|
+
hint: `Start a dev session first (extension_dev), or pass browser to match it. For live frames before any line is written, use follow:true. ${sessionPathHint(file)}`
|
|
8821
9884
|
});
|
|
8822
9885
|
const matches = makeFilter(args);
|
|
8823
9886
|
const lines = node_fs.readFileSync(file, "utf8").split("\n").filter(Boolean);
|
|
@@ -8838,12 +9901,47 @@ async function readFromFile(args, browser, limit) {
|
|
|
8838
9901
|
}
|
|
8839
9902
|
return summarize(events, "file", browser, runId, limit, 0, args.projectPath, staleFileNote(args.projectPath, browser, runId));
|
|
8840
9903
|
}
|
|
9904
|
+
function controlRefusal(closeCode, reason, url, sentVersion) {
|
|
9905
|
+
if (!Number.isFinite(closeCode) || closeCode < CLOSE_REFUSAL_FLOOR) return;
|
|
9906
|
+
const said = reason.trim();
|
|
9907
|
+
const preamble = `The dev server refused the control channel at ${url}: close code ${closeCode}${said ? ` ("${said}")` : ""}.`;
|
|
9908
|
+
if (closeCode === CLOSE_BAD_HELLO) return {
|
|
9909
|
+
code: "E_CONTROL_ENVELOPE",
|
|
9910
|
+
status: "control-channel-refused",
|
|
9911
|
+
message: `${preamble} That code is the broker rejecting the hello frame itself. This MCP dialed as a consumer with control envelope version ${sentVersion}, the version its pinned Extension.js speaks, and the role is one the broker always accepts, so the envelope version is the part that differs: the engine installed in this project is older or newer and speaks a different one. The session may be logging normally even though this read returned nothing.`,
|
|
9912
|
+
hint: "Update the project's Extension.js so its control envelope matches this MCP's pinned engine, or read the file instead by calling extension_logs without follow. extension_doctor reports both versions."
|
|
9913
|
+
};
|
|
9914
|
+
if (closeCode === CLOSE_BAD_INSTANCE) return {
|
|
9915
|
+
code: "E_STALE_CONTRACT",
|
|
9916
|
+
status: "control-channel-stale",
|
|
9917
|
+
message: `${preamble} That code means the instanceId this MCP sent belongs to a PREVIOUS dev session: the ready.json it read is stale, so the dev server was replaced after that file was written. Nothing here is a version problem, and the engine is not at fault; the session that is running now has an instance this read never named.`,
|
|
9918
|
+
hint: "Wait for the current session to publish its contract with extension_wait and retry, or start one with extension_dev if none is running. extension_stop clears a contract left behind by a session that has gone away."
|
|
9919
|
+
};
|
|
9920
|
+
if (closeCode === CLOSE_CONTROL_UNAVAILABLE) return {
|
|
9921
|
+
code: "E_NO_CONTROL_CHANNEL",
|
|
9922
|
+
status: "control-channel-unavailable",
|
|
9923
|
+
message: `${preamble} That code means the broker has no control channel to hand out: the session was started without allowControl, so it turns controlling clients away. Following logs is a read, not a control operation, so a session that refuses this connection cannot be streamed at all until it is relaunched.`,
|
|
9924
|
+
hint: "Relaunch the session with extension_dev and allowControl: true, or read the file instead by calling extension_logs without follow."
|
|
9925
|
+
};
|
|
9926
|
+
if (closeCode === CLOSE_SLOW_CONSUMER) return {
|
|
9927
|
+
code: "E_CONTROL_CHANNEL",
|
|
9928
|
+
status: "control-channel-dropped",
|
|
9929
|
+
message: `${preamble} That code means this reader fell far enough behind that the broker dropped it to protect itself. The session, the engine version and the control envelope are all fine: the log volume simply outran this follow window, and whatever came back stops at the moment of the drop rather than at the end of the window.`,
|
|
9930
|
+
hint: "Narrow the query before following again (level, context, url or tab), lower followMs, or read the completed file by calling extension_logs without follow."
|
|
9931
|
+
};
|
|
9932
|
+
return {
|
|
9933
|
+
code: "E_CONTROL_CHANNEL",
|
|
9934
|
+
status: "control-channel-refused",
|
|
9935
|
+
message: `${preamble} That code is an application-level refusal this MCP does not recognise, so the broker's own reason above is the whole diagnosis. It dialed as a consumer with control envelope version ${sentVersion}, the version its pinned Extension.js speaks; an engine newer than the pin can refuse for reasons this release has never seen.`,
|
|
9936
|
+
hint: "Read the file instead by calling extension_logs without follow, and compare the engine versions with extension_doctor."
|
|
9937
|
+
};
|
|
9938
|
+
}
|
|
8841
9939
|
async function readFromStream(args, browser, limit) {
|
|
8842
9940
|
const ready = readReadyContract(args.projectPath, browser);
|
|
8843
9941
|
if (!ready) {
|
|
8844
9942
|
const running = knownSessionBrowsers(args.projectPath).filter((b)=>b !== browser);
|
|
8845
9943
|
const retarget = running.length ? `An active session exists for browser(s): ${running.join(", ")}, pass that as \`browser\`. Otherwise run` : "Run";
|
|
8846
|
-
return
|
|
9944
|
+
return envelope_envelope({
|
|
8847
9945
|
ok: false,
|
|
8848
9946
|
command: logs_TOOL,
|
|
8849
9947
|
status: "no-control-channel",
|
|
@@ -8854,7 +9952,7 @@ async function readFromStream(args, browser, limit) {
|
|
|
8854
9952
|
hint: `${retarget} extension_dev (browser: ${browser}) and wait for it to be ready, then retry. For past logs without a live channel, call without follow.`
|
|
8855
9953
|
});
|
|
8856
9954
|
}
|
|
8857
|
-
const followMs = Math.min(Math.max(args.followMs ??
|
|
9955
|
+
const followMs = Math.min(Math.max(args.followMs ?? DEFAULT_FOLLOW_MS, MIN_FOLLOW_MS), MAX_FOLLOW_MS);
|
|
8858
9956
|
const matches = makeFilter(args);
|
|
8859
9957
|
const events = [];
|
|
8860
9958
|
let dropped = 0;
|
|
@@ -8866,7 +9964,7 @@ async function readFromStream(args, browser, limit) {
|
|
|
8866
9964
|
try {
|
|
8867
9965
|
socket = new ws_0(url);
|
|
8868
9966
|
} catch (err) {
|
|
8869
|
-
resolve(
|
|
9967
|
+
resolve(envelope_envelope({
|
|
8870
9968
|
ok: false,
|
|
8871
9969
|
command: logs_TOOL,
|
|
8872
9970
|
status: "control-channel-failed",
|
|
@@ -8892,7 +9990,7 @@ async function readFromStream(args, browser, limit) {
|
|
|
8892
9990
|
try {
|
|
8893
9991
|
socket.send(JSON.stringify({
|
|
8894
9992
|
type: "hello",
|
|
8895
|
-
v:
|
|
9993
|
+
v: CONTROL_ENVELOPE_VERSION,
|
|
8896
9994
|
role: "consumer",
|
|
8897
9995
|
instanceId: ready.instanceId
|
|
8898
9996
|
}));
|
|
@@ -8922,7 +10020,7 @@ async function readFromStream(args, browser, limit) {
|
|
|
8922
10020
|
try {
|
|
8923
10021
|
socket.close();
|
|
8924
10022
|
} catch {}
|
|
8925
|
-
resolve(
|
|
10023
|
+
resolve(envelope_envelope({
|
|
8926
10024
|
ok: false,
|
|
8927
10025
|
command: logs_TOOL,
|
|
8928
10026
|
status: "control-channel-failed",
|
|
@@ -8933,12 +10031,33 @@ async function readFromStream(args, browser, limit) {
|
|
|
8933
10031
|
hint: "The dev session may have stopped or the control port changed. Re-check with extension_wait."
|
|
8934
10032
|
}));
|
|
8935
10033
|
});
|
|
8936
|
-
socket.on("close",
|
|
10034
|
+
socket.on("close", (code, reason)=>{
|
|
10035
|
+
if (settled) return;
|
|
10036
|
+
const refusal = controlRefusal(code, reason?.toString() ?? "", url, CONTROL_ENVELOPE_VERSION);
|
|
10037
|
+
if (!refusal) return void finish();
|
|
10038
|
+
if (events.length > 0 || dropped > 0) {
|
|
10039
|
+
streamNote = refusal.message;
|
|
10040
|
+
finish();
|
|
10041
|
+
return;
|
|
10042
|
+
}
|
|
10043
|
+
settled = true;
|
|
10044
|
+
clearTimeout(timer);
|
|
10045
|
+
resolve(envelope_envelope({
|
|
10046
|
+
ok: false,
|
|
10047
|
+
command: logs_TOOL,
|
|
10048
|
+
status: refusal.status,
|
|
10049
|
+
error: {
|
|
10050
|
+
code: refusal.code,
|
|
10051
|
+
message: refusal.message
|
|
10052
|
+
},
|
|
10053
|
+
hint: refusal.hint
|
|
10054
|
+
}));
|
|
10055
|
+
});
|
|
8937
10056
|
});
|
|
8938
10057
|
}
|
|
8939
10058
|
async function logs_handler(args) {
|
|
8940
10059
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
8941
|
-
const limit = args.limit && args.limit > 0 ? args.limit :
|
|
10060
|
+
const limit = args.limit && args.limit > 0 ? args.limit : DEFAULT_LIMIT;
|
|
8942
10061
|
if (args.follow) return readFromStream(args, browser, limit);
|
|
8943
10062
|
return readFromFile(args, browser, limit);
|
|
8944
10063
|
}
|
|
@@ -9106,7 +10225,7 @@ async function storage_handler(args) {
|
|
|
9106
10225
|
if (args.area) cli.push("--area", args.area);
|
|
9107
10226
|
if (args.key) cli.push("--key", args.key);
|
|
9108
10227
|
if ("set" === args.action) {
|
|
9109
|
-
if (void 0 === args.value) return
|
|
10228
|
+
if (void 0 === args.value) return envelope_envelope({
|
|
9110
10229
|
ok: false,
|
|
9111
10230
|
command: storage_schema.name,
|
|
9112
10231
|
status: "bad-request",
|
|
@@ -9116,7 +10235,7 @@ async function storage_handler(args) {
|
|
|
9116
10235
|
message: "storage set requires a value"
|
|
9117
10236
|
}
|
|
9118
10237
|
});
|
|
9119
|
-
if (void 0 === args.key) return
|
|
10238
|
+
if (void 0 === args.key) return envelope_envelope({
|
|
9120
10239
|
ok: false,
|
|
9121
10240
|
command: storage_schema.name,
|
|
9122
10241
|
status: "bad-request",
|
|
@@ -9271,7 +10390,7 @@ const dom_snapshot_schema = {
|
|
|
9271
10390
|
};
|
|
9272
10391
|
async function cdpPortOrError(projectPath, browser, feature) {
|
|
9273
10392
|
if (!isChromiumFamily(browser)) return {
|
|
9274
|
-
error:
|
|
10393
|
+
error: envelope_envelope({
|
|
9275
10394
|
ok: false,
|
|
9276
10395
|
command: dom_snapshot_schema.name,
|
|
9277
10396
|
status: "unsupported-browser",
|
|
@@ -9284,7 +10403,7 @@ async function cdpPortOrError(projectPath, browser, feature) {
|
|
|
9284
10403
|
};
|
|
9285
10404
|
const resolved = await resolveCdpPort(projectPath, browser);
|
|
9286
10405
|
if (!resolved) return {
|
|
9287
|
-
error:
|
|
10406
|
+
error: envelope_envelope({
|
|
9288
10407
|
ok: false,
|
|
9289
10408
|
command: dom_snapshot_schema.name,
|
|
9290
10409
|
status: "no-session",
|
|
@@ -9305,7 +10424,7 @@ async function dom_snapshot_handler(args) {
|
|
|
9305
10424
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
9306
10425
|
if (isGeckoFamily(browser)) {
|
|
9307
10426
|
const resolved = await resolveRdpPort(args.projectPath, browser);
|
|
9308
|
-
if (!resolved) return
|
|
10427
|
+
if (!resolved) return envelope_envelope({
|
|
9309
10428
|
ok: false,
|
|
9310
10429
|
command: dom_snapshot_schema.name,
|
|
9311
10430
|
status: "no-session",
|
|
@@ -9317,7 +10436,7 @@ async function dom_snapshot_handler(args) {
|
|
|
9317
10436
|
});
|
|
9318
10437
|
try {
|
|
9319
10438
|
const tabs = await rdpListTabs(resolved.port);
|
|
9320
|
-
return
|
|
10439
|
+
return envelope_envelope({
|
|
9321
10440
|
ok: true,
|
|
9322
10441
|
command: dom_snapshot_schema.name,
|
|
9323
10442
|
status: "listed-targets",
|
|
@@ -9339,7 +10458,7 @@ async function dom_snapshot_handler(args) {
|
|
|
9339
10458
|
]
|
|
9340
10459
|
});
|
|
9341
10460
|
} catch (e) {
|
|
9342
|
-
return
|
|
10461
|
+
return envelope_envelope({
|
|
9343
10462
|
ok: false,
|
|
9344
10463
|
command: dom_snapshot_schema.name,
|
|
9345
10464
|
status: "rdp-failed",
|
|
@@ -9356,7 +10475,7 @@ async function dom_snapshot_handler(args) {
|
|
|
9356
10475
|
if ("error" in cdp) return cdp.error;
|
|
9357
10476
|
try {
|
|
9358
10477
|
const targets = await listPageTargets(cdp.port);
|
|
9359
|
-
return
|
|
10478
|
+
return envelope_envelope({
|
|
9360
10479
|
ok: true,
|
|
9361
10480
|
command: dom_snapshot_schema.name,
|
|
9362
10481
|
status: "listed-targets",
|
|
@@ -9369,7 +10488,7 @@ async function dom_snapshot_handler(args) {
|
|
|
9369
10488
|
]
|
|
9370
10489
|
});
|
|
9371
10490
|
} catch (e) {
|
|
9372
|
-
return
|
|
10491
|
+
return envelope_envelope({
|
|
9373
10492
|
ok: false,
|
|
9374
10493
|
command: dom_snapshot_schema.name,
|
|
9375
10494
|
status: "cdp-failed",
|
|
@@ -9397,7 +10516,7 @@ async function dom_snapshot_handler(args) {
|
|
|
9397
10516
|
let targetTab = args.tab;
|
|
9398
10517
|
let resolvedTarget = null;
|
|
9399
10518
|
if (args.tabUrl) {
|
|
9400
|
-
if (null != args.tab || args.url) return
|
|
10519
|
+
if (null != args.tab || args.url) return envelope_envelope({
|
|
9401
10520
|
ok: false,
|
|
9402
10521
|
command: dom_snapshot_schema.name,
|
|
9403
10522
|
status: "bad-request",
|
|
@@ -9415,7 +10534,7 @@ async function dom_snapshot_handler(args) {
|
|
|
9415
10534
|
try {
|
|
9416
10535
|
targets = await listPageTargets(cdp.port);
|
|
9417
10536
|
} catch (e) {
|
|
9418
|
-
return
|
|
10537
|
+
return envelope_envelope({
|
|
9419
10538
|
ok: false,
|
|
9420
10539
|
command: dom_snapshot_schema.name,
|
|
9421
10540
|
status: "cdp-failed",
|
|
@@ -9428,7 +10547,7 @@ async function dom_snapshot_handler(args) {
|
|
|
9428
10547
|
});
|
|
9429
10548
|
}
|
|
9430
10549
|
const matches = matchTargetsByUrl(targets, args.tabUrl);
|
|
9431
|
-
if (0 === matches.length) return
|
|
10550
|
+
if (0 === matches.length) return envelope_envelope({
|
|
9432
10551
|
ok: false,
|
|
9433
10552
|
command: dom_snapshot_schema.name,
|
|
9434
10553
|
status: "no-matching-target",
|
|
@@ -9442,7 +10561,7 @@ async function dom_snapshot_handler(args) {
|
|
|
9442
10561
|
},
|
|
9443
10562
|
hint: `Pick one from availableTargets and retry with a \`tabUrl\` substring of its url, or open the page first (extension_open with \`url\`). ${TARGET_ID_NOTE}`
|
|
9444
10563
|
});
|
|
9445
|
-
if (matches.length > 1) return
|
|
10564
|
+
if (matches.length > 1) return envelope_envelope({
|
|
9446
10565
|
ok: false,
|
|
9447
10566
|
command: dom_snapshot_schema.name,
|
|
9448
10567
|
status: "ambiguous-target",
|
|
@@ -9464,7 +10583,7 @@ async function dom_snapshot_handler(args) {
|
|
|
9464
10583
|
const listed = await listBridgeTabs(args.projectPath, browser, args.timeout);
|
|
9465
10584
|
if ("error" in listed) return listed.error;
|
|
9466
10585
|
const matches = matchTabsByUrl(listed.tabs, args.tabUrl);
|
|
9467
|
-
if (0 === matches.length) return
|
|
10586
|
+
if (0 === matches.length) return envelope_envelope({
|
|
9468
10587
|
ok: false,
|
|
9469
10588
|
command: dom_snapshot_schema.name,
|
|
9470
10589
|
status: "no-matching-target",
|
|
@@ -9478,7 +10597,7 @@ async function dom_snapshot_handler(args) {
|
|
|
9478
10597
|
},
|
|
9479
10598
|
hint: "Pick one from availableTabs and retry with a `tabUrl` substring of its url, or open the page first (extension_open with `url`)."
|
|
9480
10599
|
});
|
|
9481
|
-
if (matches.length > 1) return
|
|
10600
|
+
if (matches.length > 1) return envelope_envelope({
|
|
9482
10601
|
ok: false,
|
|
9483
10602
|
command: dom_snapshot_schema.name,
|
|
9484
10603
|
status: "ambiguous-target",
|
|
@@ -9546,7 +10665,7 @@ const publish_schema = {
|
|
|
9546
10665
|
}
|
|
9547
10666
|
};
|
|
9548
10667
|
function fail(name, message, status, code) {
|
|
9549
|
-
return
|
|
10668
|
+
return envelope_envelope({
|
|
9550
10669
|
ok: false,
|
|
9551
10670
|
command: "extension_publish",
|
|
9552
10671
|
status,
|
|
@@ -9573,16 +10692,22 @@ async function publish_handler(args) {
|
|
|
9573
10692
|
api: args.api,
|
|
9574
10693
|
token
|
|
9575
10694
|
});
|
|
9576
|
-
if (!result.ok)
|
|
9577
|
-
|
|
9578
|
-
|
|
9579
|
-
|
|
9580
|
-
|
|
9581
|
-
|
|
9582
|
-
|
|
9583
|
-
|
|
9584
|
-
|
|
9585
|
-
|
|
10695
|
+
if (!result.ok) {
|
|
10696
|
+
const projectMissing = /\(404\)/.test(result.error.message);
|
|
10697
|
+
return envelope_envelope({
|
|
10698
|
+
ok: false,
|
|
10699
|
+
command: "extension_publish",
|
|
10700
|
+
status: "publish-failed",
|
|
10701
|
+
error: {
|
|
10702
|
+
code: "E_PLATFORM",
|
|
10703
|
+
name: result.error.name,
|
|
10704
|
+
message: result.error.message
|
|
10705
|
+
},
|
|
10706
|
+
...projectMissing ? {
|
|
10707
|
+
hint: "The token's project does not exist on the host this call targeted. Run extension_auth (action: status) to see which workspace/project the token is scoped to, create that project first (import a repo or a template at extension.dev/new), or run extension_auth (action: login) against a project that exists there."
|
|
10708
|
+
} : {}
|
|
10709
|
+
});
|
|
10710
|
+
}
|
|
9586
10711
|
const data = result.data;
|
|
9587
10712
|
let note = null;
|
|
9588
10713
|
if (null != args.ttlHours && "public" === data.visibility) note = "ttlHours was ignored: this is a public project, whose share URL is its canonical public page.";
|
|
@@ -9627,7 +10752,7 @@ async function publish_handler(args) {
|
|
|
9627
10752
|
}
|
|
9628
10753
|
}
|
|
9629
10754
|
}
|
|
9630
|
-
return
|
|
10755
|
+
return envelope_envelope({
|
|
9631
10756
|
ok: true,
|
|
9632
10757
|
command: "extension_publish",
|
|
9633
10758
|
status: "published",
|
|
@@ -9680,7 +10805,7 @@ const release_promote_schema = {
|
|
|
9680
10805
|
}
|
|
9681
10806
|
};
|
|
9682
10807
|
function release_promote_fail(name, message, status, code) {
|
|
9683
|
-
return
|
|
10808
|
+
return envelope_envelope({
|
|
9684
10809
|
ok: false,
|
|
9685
10810
|
command: "extension_release_promote",
|
|
9686
10811
|
status,
|
|
@@ -9697,7 +10822,7 @@ async function release_promote_handler(args) {
|
|
|
9697
10822
|
const buildId = String(args.buildId || "").trim();
|
|
9698
10823
|
const channel = String(args.channel || "").trim();
|
|
9699
10824
|
if (!buildId || !channel) return release_promote_fail("ReleaseInputError", "buildId and channel are required.", "bad-request", "E_BAD_REQUEST");
|
|
9700
|
-
const apiCheck = safeApiBase(resolveApiBase(args.api));
|
|
10825
|
+
const apiCheck = safeApiBase(resolveApiBase(args.api), args.api);
|
|
9701
10826
|
if (!apiCheck.ok) return release_promote_fail("ReleaseConfigError", apiCheck.message, "bad-config", "E_CONFIG");
|
|
9702
10827
|
const url = `${apiCheck.base}/api/cli/release/promote`;
|
|
9703
10828
|
const body = {
|
|
@@ -9754,7 +10879,7 @@ async function release_promote_handler(args) {
|
|
|
9754
10879
|
}
|
|
9755
10880
|
}
|
|
9756
10881
|
}
|
|
9757
|
-
return
|
|
10882
|
+
return envelope_envelope({
|
|
9758
10883
|
ok: false,
|
|
9759
10884
|
command: "extension_release_promote",
|
|
9760
10885
|
status: "promote-failed",
|
|
@@ -9782,7 +10907,7 @@ async function release_promote_handler(args) {
|
|
|
9782
10907
|
publicBuildUrl
|
|
9783
10908
|
} : {}
|
|
9784
10909
|
} : data;
|
|
9785
|
-
return
|
|
10910
|
+
return envelope_envelope({
|
|
9786
10911
|
ok: true,
|
|
9787
10912
|
command: "extension_release_promote",
|
|
9788
10913
|
status: "promoted",
|
|
@@ -9790,7 +10915,7 @@ async function release_promote_handler(args) {
|
|
|
9790
10915
|
});
|
|
9791
10916
|
}
|
|
9792
10917
|
function release_list_fail(name, message, status, code, extra) {
|
|
9793
|
-
return
|
|
10918
|
+
return envelope_envelope({
|
|
9794
10919
|
ok: false,
|
|
9795
10920
|
command: "extension_release_status",
|
|
9796
10921
|
status,
|
|
@@ -9847,7 +10972,7 @@ async function readReleases(args) {
|
|
|
9847
10972
|
const publicUrlNote = publicProjectUrl ? isPrivate ? "publicUrl links open only for workspace members. This project is private, so an outside recipient needs a share link from extension_publish." : "publicUrl links are the public build pages: no login needed, and they carry the per-browser downloads and the run locally instructions." : null;
|
|
9848
10973
|
const channelsUnavailable = channelsRes.ok ? null : `channels.json unreadable: ${channelsRes.message}`;
|
|
9849
10974
|
const buildsUnavailable = buildsRes.ok ? null : `builds/index.json unreadable: ${buildsRes.message}`;
|
|
9850
|
-
return
|
|
10975
|
+
return envelope_envelope({
|
|
9851
10976
|
ok: true,
|
|
9852
10977
|
command: "extension_release_status",
|
|
9853
10978
|
status: "read",
|
|
@@ -9961,7 +11086,7 @@ function normalizeStoresStatus(json) {
|
|
|
9961
11086
|
return out;
|
|
9962
11087
|
}
|
|
9963
11088
|
function store_status_fail(name, message, status, code, extra) {
|
|
9964
|
-
return
|
|
11089
|
+
return envelope_envelope({
|
|
9965
11090
|
ok: false,
|
|
9966
11091
|
command: "extension_release_status",
|
|
9967
11092
|
status,
|
|
@@ -10053,7 +11178,7 @@ async function readStores(args) {
|
|
|
10053
11178
|
const healthUnavailable = healthRes.ok ? null : `stores/health.json unreadable: ${healthRes.message}`;
|
|
10054
11179
|
const statusUnavailable = statusRes.ok ? null : `stores/status.json unreadable: ${statusRes.message}`;
|
|
10055
11180
|
const submissionsUnavailable = submissionsRes.ok || 404 === submissionsRes.status ? null : `stores/submissions.json unreadable: ${submissionsRes.message}`;
|
|
10056
|
-
return
|
|
11181
|
+
return envelope_envelope({
|
|
10057
11182
|
ok: true,
|
|
10058
11183
|
command: "extension_release_status",
|
|
10059
11184
|
status: "read",
|
|
@@ -10151,7 +11276,7 @@ async function release_status_handler(args) {
|
|
|
10151
11276
|
stores
|
|
10152
11277
|
].filter(Boolean);
|
|
10153
11278
|
const ok = sections.some((section)=>true === section.ok);
|
|
10154
|
-
return
|
|
11279
|
+
return envelope_envelope({
|
|
10155
11280
|
ok,
|
|
10156
11281
|
command: "extension_release_status",
|
|
10157
11282
|
status: ok ? "read" : "unavailable",
|
|
@@ -10240,7 +11365,7 @@ const submit_schema = {
|
|
|
10240
11365
|
}
|
|
10241
11366
|
};
|
|
10242
11367
|
function submit_fail(name, message, status, code) {
|
|
10243
|
-
return
|
|
11368
|
+
return envelope_envelope({
|
|
10244
11369
|
ok: false,
|
|
10245
11370
|
command: "extension_submit",
|
|
10246
11371
|
status,
|
|
@@ -10258,7 +11383,7 @@ async function submit_handler(args) {
|
|
|
10258
11383
|
if (0 === browsers.length) return submit_fail("SubmitInputError", 'browsers is required (e.g. ["chrome","firefox","edge","safari"]).', "bad-request", "E_BAD_REQUEST");
|
|
10259
11384
|
const buildSha = String(args.buildSha || "").trim();
|
|
10260
11385
|
if (!buildSha) return submit_fail("SubmitInputError", "buildSha is required (the built commit to submit).", "bad-request", "E_BAD_REQUEST");
|
|
10261
|
-
const apiCheck = safeApiBase(resolveApiBase(args.api));
|
|
11386
|
+
const apiCheck = safeApiBase(resolveApiBase(args.api), args.api);
|
|
10262
11387
|
if (!apiCheck.ok) return submit_fail("SubmitConfigError", apiCheck.message, "bad-config", "E_CONFIG");
|
|
10263
11388
|
const url = `${apiCheck.base}/api/cli/stores/submit`;
|
|
10264
11389
|
const dryRun = false !== args.dryRun;
|
|
@@ -10390,7 +11515,7 @@ async function submit_handler(args) {
|
|
|
10390
11515
|
message = summaryParts.join(" ");
|
|
10391
11516
|
}
|
|
10392
11517
|
if (!dryRun) statusNote = "Track this submission with extension_release_status: it reads the recorded outcome, per-store credential health, and review state from the public registry.";
|
|
10393
|
-
return
|
|
11518
|
+
return envelope_envelope({
|
|
10394
11519
|
ok,
|
|
10395
11520
|
command: "extension_submit",
|
|
10396
11521
|
status: dryRun ? "preflight" : "submitted",
|
|
@@ -10403,14 +11528,31 @@ async function submit_handler(args) {
|
|
|
10403
11528
|
]
|
|
10404
11529
|
});
|
|
10405
11530
|
}
|
|
10406
|
-
function
|
|
11531
|
+
function readContractForDiagnosis(projectPath, browser) {
|
|
10407
11532
|
try {
|
|
10408
|
-
const raw = node_fs.readFileSync(
|
|
11533
|
+
const raw = node_fs.readFileSync(bridge_readyContractPath(projectPath, browser), "utf8");
|
|
10409
11534
|
return JSON.parse(raw);
|
|
10410
11535
|
} catch {
|
|
10411
11536
|
return null;
|
|
10412
11537
|
}
|
|
10413
11538
|
}
|
|
11539
|
+
function sightedContractBrowser(projectPath) {
|
|
11540
|
+
let dirs;
|
|
11541
|
+
try {
|
|
11542
|
+
dirs = node_fs.readdirSync(sessionArtifactsRootDir(projectPath));
|
|
11543
|
+
} catch {
|
|
11544
|
+
return null;
|
|
11545
|
+
}
|
|
11546
|
+
let best = null;
|
|
11547
|
+
for (const dir of dirs)try {
|
|
11548
|
+
const stat = node_fs.statSync(bridge_readyContractPath(projectPath, dir));
|
|
11549
|
+
if (!best || stat.mtimeMs > best.mtimeMs) best = {
|
|
11550
|
+
browser: dir,
|
|
11551
|
+
mtimeMs: stat.mtimeMs
|
|
11552
|
+
};
|
|
11553
|
+
} catch {}
|
|
11554
|
+
return best ? best.browser : null;
|
|
11555
|
+
}
|
|
10414
11556
|
const doctor_schema = {
|
|
10415
11557
|
name: "extension_doctor",
|
|
10416
11558
|
description: "Diagnose a dev session end to end: ready contract, dev-server process, control-port agreement, control channel, eval token, executor, browser liveness. This returns one {check, status, detail, remediation?} per leg, in dependency order. Read a 'skip' as blocked, not as a pass: it names the check that blocked it. Run this first when any act tool (storage, reload, eval, open) errors unexpectedly. Call it with no projectPath for a pre-flight environment check (node, the Extension.js CLI, the template cache) before any project exists.",
|
|
@@ -10457,7 +11599,7 @@ async function environmentPreflight() {
|
|
|
10457
11599
|
detail: cacheExists ? `Template catalog cached at ${cacheFile}` : "Template catalog not cached yet (extension_templates will fetch it)"
|
|
10458
11600
|
});
|
|
10459
11601
|
const healthy = checks.every((c)=>"fail" !== c.status);
|
|
10460
|
-
return
|
|
11602
|
+
return envelope_envelope({
|
|
10461
11603
|
ok: healthy,
|
|
10462
11604
|
command: doctor_schema.name,
|
|
10463
11605
|
status: healthy ? "healthy" : "unhealthy",
|
|
@@ -10477,22 +11619,11 @@ function safeStringify(value) {
|
|
|
10477
11619
|
}
|
|
10478
11620
|
}
|
|
10479
11621
|
function recentErrorLogs(projectPath, browser, max = 5) {
|
|
10480
|
-
const file = node_path.resolve(projectPath, "dist", "extension-js", browser, "logs.ndjson");
|
|
10481
|
-
let lines;
|
|
10482
|
-
try {
|
|
10483
|
-
lines = node_fs.readFileSync(file, "utf8").split("\n").filter(Boolean);
|
|
10484
|
-
} catch {
|
|
10485
|
-
return [];
|
|
10486
|
-
}
|
|
10487
11622
|
const errs = [];
|
|
10488
|
-
for (const
|
|
10489
|
-
|
|
10490
|
-
|
|
10491
|
-
|
|
10492
|
-
} catch {
|
|
10493
|
-
continue;
|
|
10494
|
-
}
|
|
10495
|
-
if (!ev || "error" !== ev.level) continue;
|
|
11623
|
+
for (const event of readLogEvents(projectPath, browser, {
|
|
11624
|
+
level: "error"
|
|
11625
|
+
})){
|
|
11626
|
+
const ev = event;
|
|
10496
11627
|
const parts = Array.isArray(ev.messageParts) ? ev.messageParts : Array.isArray(ev.args) ? ev.args : null;
|
|
10497
11628
|
let msg = parts ? parts.map((p)=>"string" == typeof p ? p : safeStringify(p)).join(" ") : ev.message || ev.text || "";
|
|
10498
11629
|
if (!msg && ev.errorName) msg = ev.stack ? `${ev.errorName}: ${ev.stack}` : ev.errorName;
|
|
@@ -10517,7 +11648,8 @@ function capabilityProbeChecks(parsed) {
|
|
|
10517
11648
|
async function doctor_handler(args) {
|
|
10518
11649
|
if (!args.projectPath) return environmentPreflight();
|
|
10519
11650
|
const projectPath = args.projectPath;
|
|
10520
|
-
const
|
|
11651
|
+
const resolved = resolveSessionBrowser(projectPath, args.browser);
|
|
11652
|
+
const browser = "fallback" === resolved.source ? sightedContractBrowser(projectPath) ?? resolved.browser : resolved.browser;
|
|
10521
11653
|
const { code, stdout, stderr } = await runExtensionCli([
|
|
10522
11654
|
"doctor",
|
|
10523
11655
|
projectPath,
|
|
@@ -10528,6 +11660,17 @@ async function doctor_handler(args) {
|
|
|
10528
11660
|
], {
|
|
10529
11661
|
cwd: projectPath
|
|
10530
11662
|
});
|
|
11663
|
+
if (refusedTheOutputFlag(stderr ?? "")) return envelope_envelope({
|
|
11664
|
+
ok: false,
|
|
11665
|
+
command: doctor_schema.name,
|
|
11666
|
+
status: "engine-too-old",
|
|
11667
|
+
error: {
|
|
11668
|
+
code: "E_ENGINE_TOO_OLD",
|
|
11669
|
+
name: "CliError",
|
|
11670
|
+
message: await outputFlagRefusalMessage("doctor", "doctor", projectPath)
|
|
11671
|
+
},
|
|
11672
|
+
hint: "Until then, extension_logs without follow still reads this project's log file, and the session's ready.json still records how the last build ended."
|
|
11673
|
+
});
|
|
10531
11674
|
const out = stdout.trim();
|
|
10532
11675
|
try {
|
|
10533
11676
|
const parsed = JSON.parse(out);
|
|
@@ -10538,7 +11681,7 @@ async function doctor_handler(args) {
|
|
|
10538
11681
|
if ("string" == typeof check.remediation) check.remediation = toMcpSpeak(check.remediation);
|
|
10539
11682
|
}
|
|
10540
11683
|
let healthy = 0 === code;
|
|
10541
|
-
const contract =
|
|
11684
|
+
const contract = readContractForDiagnosis(projectPath, browser);
|
|
10542
11685
|
if (contract?.status === "error") {
|
|
10543
11686
|
healthy = false;
|
|
10544
11687
|
const browserExited = "browser_exited" === contract.code || void 0 !== contract.browserExitCode;
|
|
@@ -10574,7 +11717,7 @@ async function doctor_handler(args) {
|
|
|
10574
11717
|
} : {}
|
|
10575
11718
|
});
|
|
10576
11719
|
}
|
|
10577
|
-
return
|
|
11720
|
+
return envelope_envelope({
|
|
10578
11721
|
ok: healthy,
|
|
10579
11722
|
command: doctor_schema.name,
|
|
10580
11723
|
status: healthy ? "healthy" : "unhealthy",
|
|
@@ -10588,7 +11731,8 @@ async function doctor_handler(args) {
|
|
|
10588
11731
|
});
|
|
10589
11732
|
} catch {
|
|
10590
11733
|
const message = stderr.trim() || `extension exited with code ${code}`;
|
|
10591
|
-
|
|
11734
|
+
const cliReport = toMcpSpeak(out).trim().slice(0, 4000);
|
|
11735
|
+
return envelope_envelope({
|
|
10592
11736
|
ok: false,
|
|
10593
11737
|
command: doctor_schema.name,
|
|
10594
11738
|
status: "cli-failed",
|
|
@@ -10597,7 +11741,13 @@ async function doctor_handler(args) {
|
|
|
10597
11741
|
name: "CliError",
|
|
10598
11742
|
message: toMcpSpeak(message)
|
|
10599
11743
|
},
|
|
10600
|
-
|
|
11744
|
+
value: {
|
|
11745
|
+
browser,
|
|
11746
|
+
...cliReport ? {
|
|
11747
|
+
cliReport
|
|
11748
|
+
} : {}
|
|
11749
|
+
},
|
|
11750
|
+
hint: cliReport ? "cliReport is the doctor's own output, unparsed: read the failing checks and remediations there." : "extension doctor requires a recent extension CLI, the project's local install may predate it."
|
|
10601
11751
|
});
|
|
10602
11752
|
}
|
|
10603
11753
|
}
|
|
@@ -10641,7 +11791,7 @@ async function wait_handler(args) {
|
|
|
10641
11791
|
const requested = args.timeoutMs ?? args.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
10642
11792
|
const budgetMs = Math.min(Math.max(requested, MIN_TIMEOUT_MS), SAFE_CEILING_MS);
|
|
10643
11793
|
const clamped = requested > SAFE_CEILING_MS;
|
|
10644
|
-
const readyPath =
|
|
11794
|
+
const readyPath = bridge_readyContractPath(args.projectPath, browser);
|
|
10645
11795
|
const buildOnly = findSessionInfo(args.projectPath, browser)?.noBrowser === true;
|
|
10646
11796
|
const since = sessionSinceMs(args.projectPath, browser);
|
|
10647
11797
|
const start = Date.now();
|
|
@@ -10663,7 +11813,7 @@ async function wait_handler(args) {
|
|
|
10663
11813
|
}
|
|
10664
11814
|
lastContractStatus = contract.status;
|
|
10665
11815
|
if ("ready" === contract.status) {
|
|
10666
|
-
if ("number" == typeof contract.pid && !wait_isAlive(contract.pid)) return
|
|
11816
|
+
if ("number" == typeof contract.pid && !wait_isAlive(contract.pid)) return envelope_envelope({
|
|
10667
11817
|
ok: false,
|
|
10668
11818
|
command: wait_schema.name,
|
|
10669
11819
|
status: "stale",
|
|
@@ -10679,7 +11829,7 @@ async function wait_handler(args) {
|
|
|
10679
11829
|
}
|
|
10680
11830
|
});
|
|
10681
11831
|
const attached = "attached" === contract.runtime || "string" == typeof contract.executorAttachedAt;
|
|
10682
|
-
if (!attached && buildOnly) return
|
|
11832
|
+
if (!attached && buildOnly) return envelope_envelope({
|
|
10683
11833
|
ok: true,
|
|
10684
11834
|
command: wait_schema.name,
|
|
10685
11835
|
status: "ready",
|
|
@@ -10710,7 +11860,7 @@ async function wait_handler(args) {
|
|
|
10710
11860
|
const warnings = [];
|
|
10711
11861
|
if (runtimeErrors.length) warnings.push(`Compiled and attached, but the extension is throwing at runtime (${runtimeErrors.length} recent error event${1 === runtimeErrors.length ? "" : "s"} above). Check extension_logs (level: error) or extension_doctor before trusting this session.`);
|
|
10712
11862
|
if (guestCheck.checked && !guestCheck.loaded) warnings.push("The engine reports the runtime attached, but the browser's own target list shows no chrome-extension:// target for your extension, only the engine companion. This is the signature of a silently rejected --load-extension (extension.js BUGS_TO_FIX §83): the CLI and ready.json cannot see it, and the control verbs will fail against a guest that is not there. Check the manifest and extension_logs.");
|
|
10713
|
-
return
|
|
11863
|
+
return envelope_envelope({
|
|
10714
11864
|
ok: true,
|
|
10715
11865
|
command: wait_schema.name,
|
|
10716
11866
|
status: "ready",
|
|
@@ -10740,7 +11890,7 @@ async function wait_handler(args) {
|
|
|
10740
11890
|
warnings
|
|
10741
11891
|
});
|
|
10742
11892
|
}
|
|
10743
|
-
if ("error" === contract.status) return
|
|
11893
|
+
if ("error" === contract.status) return envelope_envelope({
|
|
10744
11894
|
ok: false,
|
|
10745
11895
|
command: wait_schema.name,
|
|
10746
11896
|
status: "contract-error",
|
|
@@ -10759,7 +11909,7 @@ async function wait_handler(args) {
|
|
|
10759
11909
|
} catch {}
|
|
10760
11910
|
await new Promise((resolve)=>setTimeout(resolve, pollInterval));
|
|
10761
11911
|
}
|
|
10762
|
-
if (sawCompiledButUnattached) return
|
|
11912
|
+
if (sawCompiledButUnattached) return envelope_envelope({
|
|
10763
11913
|
ok: false,
|
|
10764
11914
|
command: wait_schema.name,
|
|
10765
11915
|
status: "compiled-not-attached",
|
|
@@ -10776,7 +11926,7 @@ async function wait_handler(args) {
|
|
|
10776
11926
|
},
|
|
10777
11927
|
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."
|
|
10778
11928
|
});
|
|
10779
|
-
return
|
|
11929
|
+
return envelope_envelope({
|
|
10780
11930
|
ok: false,
|
|
10781
11931
|
command: wait_schema.name,
|
|
10782
11932
|
status: "timeout",
|
|
@@ -10954,7 +12104,7 @@ async function add_feature_handler(args) {
|
|
|
10954
12104
|
const projectPath = node_path.resolve(args.projectPath);
|
|
10955
12105
|
const srcDir = node_path.join(projectPath, "src");
|
|
10956
12106
|
const manifestPath = node_path.join(srcDir, "manifest.json");
|
|
10957
|
-
if (!node_fs.existsSync(manifestPath)) return
|
|
12107
|
+
if (!node_fs.existsSync(manifestPath)) return envelope_envelope({
|
|
10958
12108
|
ok: false,
|
|
10959
12109
|
command: add_feature_COMMAND,
|
|
10960
12110
|
status: "manifest-not-found",
|
|
@@ -10965,7 +12115,7 @@ async function add_feature_handler(args) {
|
|
|
10965
12115
|
hint: "Ensure projectPath points to an extension project root with src/manifest.json"
|
|
10966
12116
|
});
|
|
10967
12117
|
const templateSlug = FEATURE_TEMPLATE_MAP[args.feature]?.[framework];
|
|
10968
|
-
if (!templateSlug) return
|
|
12118
|
+
if (!templateSlug) return envelope_envelope({
|
|
10969
12119
|
ok: false,
|
|
10970
12120
|
command: add_feature_COMMAND,
|
|
10971
12121
|
status: "no-reference-template",
|
|
@@ -11048,7 +12198,7 @@ async function add_feature_handler(args) {
|
|
|
11048
12198
|
});
|
|
11049
12199
|
const conflicts = filesToCreate.filter((f)=>node_fs.existsSync(node_path.join(projectPath, f.path)));
|
|
11050
12200
|
const conflictHint = `Warning: ${conflicts.length} file(s) already exist and would be overwritten.`;
|
|
11051
|
-
return
|
|
12201
|
+
return envelope_envelope({
|
|
11052
12202
|
ok: true,
|
|
11053
12203
|
command: add_feature_COMMAND,
|
|
11054
12204
|
status: conflicts.length ? "planned-with-conflicts" : "planned",
|
|
@@ -11191,7 +12341,7 @@ const FIRST_CALL_BUDGET_MS = 8000;
|
|
|
11191
12341
|
const RESUME_BUDGET_MS = 22000;
|
|
11192
12342
|
const PENDING_TTL_NOTE = "Once authorized, the minted token lives at most 7 days (server-enforced); CI must re-mint before expiry (console: project settings -> Access tokens).";
|
|
11193
12343
|
function login_fail(name, message, status, code) {
|
|
11194
|
-
return
|
|
12344
|
+
return envelope_envelope({
|
|
11195
12345
|
ok: false,
|
|
11196
12346
|
command: "extension_auth",
|
|
11197
12347
|
status,
|
|
@@ -11204,7 +12354,7 @@ function login_fail(name, message, status, code) {
|
|
|
11204
12354
|
}
|
|
11205
12355
|
function success(creds) {
|
|
11206
12356
|
const expiresAt = creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null;
|
|
11207
|
-
return
|
|
12357
|
+
return envelope_envelope({
|
|
11208
12358
|
ok: true,
|
|
11209
12359
|
command: "extension_auth",
|
|
11210
12360
|
status: "logged-in",
|
|
@@ -11223,7 +12373,7 @@ function login_pending(start) {
|
|
|
11223
12373
|
const complete = String(start.verificationUriComplete || "").trim();
|
|
11224
12374
|
const hasCompleteLink = complete.length > 0 && complete !== start.verificationUri;
|
|
11225
12375
|
const message = hasCompleteLink ? `Open ${complete} and approve (code ${start.userCode} is pre-filled), then call extension_auth (action: login) again with this deviceCode and the same project. If the page asks for a code, enter ${start.userCode} at ${start.verificationUri}.` : `Open ${start.verificationUri} and enter code ${start.userCode}, then call extension_auth (action: login) again with this deviceCode and the same project.`;
|
|
11226
|
-
return
|
|
12376
|
+
return envelope_envelope({
|
|
11227
12377
|
ok: false,
|
|
11228
12378
|
command: "extension_auth",
|
|
11229
12379
|
status: "authorization-pending",
|
|
@@ -11248,7 +12398,7 @@ function login_pending(start) {
|
|
|
11248
12398
|
}
|
|
11249
12399
|
function resumePending(deviceCode, verificationUri) {
|
|
11250
12400
|
const message = `Still waiting for authorization. The one-click link and code from the previous response are still valid: open that link (or enter the code at ${verificationUri}), then call extension_auth (action: login) again with this same deviceCode and the same project.`;
|
|
11251
|
-
return
|
|
12401
|
+
return envelope_envelope({
|
|
11252
12402
|
ok: false,
|
|
11253
12403
|
command: "extension_auth",
|
|
11254
12404
|
status: "authorization-pending",
|
|
@@ -11269,8 +12419,8 @@ function resumePending(deviceCode, verificationUri) {
|
|
|
11269
12419
|
}
|
|
11270
12420
|
async function loginToProject(args) {
|
|
11271
12421
|
const project = String(args.project || "").trim();
|
|
11272
|
-
if (!/^[^/]+\/[^/]+$/.test(project)) return login_fail("BadRequest", "project must be in the form '<workspace>/<project>'.", "bad-request", "E_BAD_REQUEST");
|
|
11273
|
-
const apiCheck = safeApiBase(resolveApiBase(args.api));
|
|
12422
|
+
if (!/^[^/]+\/[^/]+$/.test(project)) return login_fail("BadRequest", "project must be in the form '<workspace>/<project>'. The slug pair is the console address bar: an existing project's page is console.extension.dev/<workspace>/<project>. If the project does not exist yet, create it at extension.dev/new, then log in with the slugs the console shows.", "bad-request", "E_BAD_REQUEST");
|
|
12423
|
+
const apiCheck = safeApiBase(resolveApiBase(args.api), args.api);
|
|
11274
12424
|
if (!apiCheck.ok) return login_fail("LoginConfigError", apiCheck.message, "login-failed", "E_AUTH_FAILED");
|
|
11275
12425
|
const apiBase = apiCheck.base;
|
|
11276
12426
|
let config;
|
|
@@ -11323,9 +12473,92 @@ async function loginToProject(args) {
|
|
|
11323
12473
|
verificationUriComplete: start.verificationUriComplete
|
|
11324
12474
|
});
|
|
11325
12475
|
}
|
|
11326
|
-
|
|
12476
|
+
const server_identity_DEFAULT_TIMEOUT_MS = 5000;
|
|
12477
|
+
async function askServerIdentity(args) {
|
|
12478
|
+
const fetchImpl = args.fetchImpl ?? fetch;
|
|
12479
|
+
const controller = new AbortController();
|
|
12480
|
+
const timer = setTimeout(()=>controller.abort(), args.timeoutMs ?? server_identity_DEFAULT_TIMEOUT_MS);
|
|
12481
|
+
try {
|
|
12482
|
+
const res = await fetchImpl(`${args.apiBase.replace(/\/+$/, "")}/api/cli/whoami`, {
|
|
12483
|
+
headers: {
|
|
12484
|
+
authorization: `Bearer ${args.token}`,
|
|
12485
|
+
accept: "application/json"
|
|
12486
|
+
},
|
|
12487
|
+
signal: controller.signal
|
|
12488
|
+
});
|
|
12489
|
+
if (200 === res.status) {
|
|
12490
|
+
const data = await res.json().catch(()=>null);
|
|
12491
|
+
const login = String(data?.login || "").trim();
|
|
12492
|
+
if (!login) return {
|
|
12493
|
+
kind: "unavailable",
|
|
12494
|
+
detail: "the server answered 200 without a login"
|
|
12495
|
+
};
|
|
12496
|
+
return {
|
|
12497
|
+
kind: "confirmed",
|
|
12498
|
+
login,
|
|
12499
|
+
live: data?.live === true
|
|
12500
|
+
};
|
|
12501
|
+
}
|
|
12502
|
+
if (401 === res.status) return {
|
|
12503
|
+
kind: "refused"
|
|
12504
|
+
};
|
|
12505
|
+
return {
|
|
12506
|
+
kind: "unavailable",
|
|
12507
|
+
detail: `the server answered ${res.status}, which is not a verdict on this credential`
|
|
12508
|
+
};
|
|
12509
|
+
} catch (error) {
|
|
12510
|
+
return {
|
|
12511
|
+
kind: "unavailable",
|
|
12512
|
+
detail: error?.message || "network error"
|
|
12513
|
+
};
|
|
12514
|
+
} finally{
|
|
12515
|
+
clearTimeout(timer);
|
|
12516
|
+
}
|
|
12517
|
+
}
|
|
12518
|
+
function describeServer(check, api) {
|
|
12519
|
+
if ("confirmed" === check.kind) return {
|
|
12520
|
+
status: "logged-in",
|
|
12521
|
+
note: `The server at ${api} confirms this token: it resolves to ${check.login} and is live there.`,
|
|
12522
|
+
warning: null,
|
|
12523
|
+
value: {
|
|
12524
|
+
verdict: "confirmed",
|
|
12525
|
+
api,
|
|
12526
|
+
login: check.login,
|
|
12527
|
+
live: check.live
|
|
12528
|
+
}
|
|
12529
|
+
};
|
|
12530
|
+
if ("refused" === check.kind) return {
|
|
12531
|
+
status: "refused-by-server",
|
|
12532
|
+
note: `The server at ${api} refused this credential: it does not resolve to an identity there (expired, revoked, or minted for another environment). The workspace/project above is only what the local file claims. Run extension_auth (action: login) to re-authenticate.`,
|
|
12533
|
+
warning: null,
|
|
12534
|
+
value: {
|
|
12535
|
+
verdict: "refused",
|
|
12536
|
+
api
|
|
12537
|
+
}
|
|
12538
|
+
};
|
|
12539
|
+
if ("unavailable" === check.kind) return {
|
|
12540
|
+
status: "logged-in",
|
|
12541
|
+
note: null,
|
|
12542
|
+
warning: `Could not verify this login with the server at ${api} (${check.detail}). The identity above is the local file's claim only, not server-confirmed.`,
|
|
12543
|
+
value: {
|
|
12544
|
+
verdict: "unavailable",
|
|
12545
|
+
api,
|
|
12546
|
+
detail: check.detail
|
|
12547
|
+
}
|
|
12548
|
+
};
|
|
12549
|
+
return {
|
|
12550
|
+
status: "expired",
|
|
12551
|
+
note: null,
|
|
12552
|
+
warning: null,
|
|
12553
|
+
value: {
|
|
12554
|
+
verdict: "not-asked",
|
|
12555
|
+
detail: check.detail
|
|
12556
|
+
}
|
|
12557
|
+
};
|
|
12558
|
+
}
|
|
12559
|
+
async function readIdentity(deps) {
|
|
11327
12560
|
const creds = readCredentials();
|
|
11328
|
-
if (!creds) return
|
|
12561
|
+
if (!creds) return envelope_envelope({
|
|
11329
12562
|
ok: true,
|
|
11330
12563
|
command: "extension_auth",
|
|
11331
12564
|
status: "logged-out",
|
|
@@ -11337,19 +12570,39 @@ async function readIdentity() {
|
|
|
11337
12570
|
const recordedApi = String(creds.api || "").trim();
|
|
11338
12571
|
const effectiveDefaultApi = resolveApiBase();
|
|
11339
12572
|
const apiDiverges = Boolean(recordedApi) && recordedApi !== effectiveDefaultApi;
|
|
12573
|
+
const askApi = recordedApi || effectiveDefaultApi;
|
|
11340
12574
|
const envTokenSet = Boolean(String(process.env.EXTENSION_DEV_TOKEN || "").trim());
|
|
12575
|
+
let check;
|
|
12576
|
+
if (expired) check = {
|
|
12577
|
+
kind: "not-asked",
|
|
12578
|
+
detail: "the stored token has already expired locally"
|
|
12579
|
+
};
|
|
12580
|
+
else {
|
|
12581
|
+
const safe = safeApiBase(askApi);
|
|
12582
|
+
check = safe.ok ? await askServerIdentity({
|
|
12583
|
+
apiBase: safe.base,
|
|
12584
|
+
token: creds.token,
|
|
12585
|
+
fetchImpl: deps?.fetchImpl
|
|
12586
|
+
}) : {
|
|
12587
|
+
kind: "unavailable",
|
|
12588
|
+
detail: safe.message
|
|
12589
|
+
};
|
|
12590
|
+
}
|
|
12591
|
+
const server = describeServer(check, askApi);
|
|
11341
12592
|
const identityNote = expired ? "The stored token has expired. Run extension_auth (action: login) to refresh it." : `Logged in as ${creds.workspaceSlug}/${creds.projectSlug}, per the token extension_auth stored on this machine. That token is what scopes the identity: it does not follow the current working directory or project folder.`;
|
|
11342
12593
|
const apiDivergesNote = apiDiverges ? `This login was minted via ${recordedApi}: access grants for private registry reads use that recorded base when no api argument is given, while other authenticated tools target ${effectiveDefaultApi} unless given one.` : null;
|
|
11343
12594
|
const envTokenNote = envTokenSet ? "EXTENSION_DEV_TOKEN is set and takes precedence over this stored login for authenticated tools; this report describes only the stored login." : null;
|
|
11344
12595
|
const message = [
|
|
11345
12596
|
identityNote,
|
|
12597
|
+
server.note,
|
|
12598
|
+
server.warning,
|
|
11346
12599
|
apiDivergesNote,
|
|
11347
12600
|
envTokenNote
|
|
11348
12601
|
].filter(Boolean).join(" ");
|
|
11349
|
-
return
|
|
12602
|
+
return envelope_envelope({
|
|
11350
12603
|
ok: true,
|
|
11351
12604
|
command: "extension_auth",
|
|
11352
|
-
status: expired ? "expired" :
|
|
12605
|
+
status: expired ? "expired" : server.status,
|
|
11353
12606
|
value: {
|
|
11354
12607
|
workspaceSlug: creds.workspaceSlug,
|
|
11355
12608
|
projectSlug: creds.projectSlug,
|
|
@@ -11360,11 +12613,13 @@ async function readIdentity() {
|
|
|
11360
12613
|
provider: creds.provider ?? "extensiondev",
|
|
11361
12614
|
expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
|
|
11362
12615
|
expiresInSeconds: creds.expiresAt ? creds.expiresAt - now : null,
|
|
11363
|
-
expired
|
|
12616
|
+
expired,
|
|
12617
|
+
server: server.value
|
|
11364
12618
|
},
|
|
11365
12619
|
hint: message,
|
|
11366
12620
|
warnings: [
|
|
11367
12621
|
tokenTtlNote(creds.workspaceSlug, creds.projectSlug),
|
|
12622
|
+
server.warning,
|
|
11368
12623
|
apiDivergesNote,
|
|
11369
12624
|
envTokenNote
|
|
11370
12625
|
]
|
|
@@ -11377,7 +12632,7 @@ async function clearLocalCredentials() {
|
|
|
11377
12632
|
project: creds.projectSlug
|
|
11378
12633
|
}, "settings/access-tokens") : null;
|
|
11379
12634
|
const result = clearCredentials();
|
|
11380
|
-
return
|
|
12635
|
+
return envelope_envelope({
|
|
11381
12636
|
ok: true,
|
|
11382
12637
|
command: "extension_auth",
|
|
11383
12638
|
status: result.cleared ? "logged-out" : "nothing-to-clear",
|
|
@@ -11390,7 +12645,7 @@ async function clearLocalCredentials() {
|
|
|
11390
12645
|
}
|
|
11391
12646
|
const auth_schema = {
|
|
11392
12647
|
name: "extension_auth",
|
|
11393
|
-
description: "Sign this machine in to extension.dev, report that login, or clear it. Pass action:'status' (the default) to name the workspace and project the stored token is scoped to and when it expires, never the token itself; that identity comes from the stored token alone, and does not change with the current working directory or whichever project folder you are in. Pass action:'login' for a two-phase flow: call with `project` to get a code plus a URL the user authorizes at extension.dev/device, then call again with the returned `deviceCode`. GitHub federation happens server-side, so no GitHub token lands on this machine. Minted tokens live at most 7 days, server-enforced, so CI must re-mint before expiry on the console's project settings, Access tokens page. Pass action:'logout' to delete the local credentials only; the token stays valid server-side until it is revoked at the URL the response returns.",
|
|
12648
|
+
description: "Sign this machine in to extension.dev, report that login, or clear it. Pass action:'status' (the default) to name the workspace and project the stored token is scoped to and when it expires, never the token itself; that identity comes from the stored token alone, and does not change with the current working directory or whichever project folder you are in. Status also asks the platform's whoami endpoint whether that credential actually resolves there: the answer is reported as confirmed, refused-by-server, or unverified when the server cannot be reached, so a local file claiming a login the server would refuse is never reported as simply logged in. Pass action:'login' for a two-phase flow: call with `project` to get a code plus a URL the user authorizes at extension.dev/device, then call again with the returned `deviceCode`. GitHub federation happens server-side, so no GitHub token lands on this machine. Minted tokens live at most 7 days, server-enforced, so CI must re-mint before expiry on the console's project settings, Access tokens page. Pass action:'logout' to delete the local credentials only; the token stays valid server-side until it is revoked at the URL the response returns.",
|
|
11394
12649
|
inputSchema: {
|
|
11395
12650
|
type: "object",
|
|
11396
12651
|
properties: {
|
|
@@ -11405,7 +12660,7 @@ const auth_schema = {
|
|
|
11405
12660
|
},
|
|
11406
12661
|
project: {
|
|
11407
12662
|
type: "string",
|
|
11408
|
-
description: "login: target project as '<workspace>/<project>'; the token is scoped to it."
|
|
12663
|
+
description: "login: target project as '<workspace>/<project>'; the token is scoped to it. The slug pair is the console address bar: an existing project's page is console.extension.dev/<workspace>/<project>. Create one at extension.dev/new if none exists yet."
|
|
11409
12664
|
},
|
|
11410
12665
|
deviceCode: {
|
|
11411
12666
|
type: "string",
|
|
@@ -11710,7 +12965,7 @@ async function detectBrowsers(browsers) {
|
|
|
11710
12965
|
}
|
|
11711
12966
|
const available = detected.filter((d)=>"not_found" !== d.source);
|
|
11712
12967
|
const missing = detected.filter((d)=>"not_found" === d.source);
|
|
11713
|
-
return
|
|
12968
|
+
return envelope_envelope({
|
|
11714
12969
|
ok: true,
|
|
11715
12970
|
command: "extension_browsers",
|
|
11716
12971
|
status: "detected",
|
|
@@ -11767,7 +13022,7 @@ async function listManagedBrowsers() {
|
|
|
11767
13022
|
});
|
|
11768
13023
|
}
|
|
11769
13024
|
}
|
|
11770
|
-
return
|
|
13025
|
+
return envelope_envelope({
|
|
11771
13026
|
ok: true,
|
|
11772
13027
|
command: "extension_browsers",
|
|
11773
13028
|
status: "listed",
|
|
@@ -11786,7 +13041,7 @@ async function installManagedBrowser(browser) {
|
|
|
11786
13041
|
await extensionInstall({
|
|
11787
13042
|
browser
|
|
11788
13043
|
});
|
|
11789
|
-
return
|
|
13044
|
+
return envelope_envelope({
|
|
11790
13045
|
ok: true,
|
|
11791
13046
|
command: "extension_browsers",
|
|
11792
13047
|
status: "installed",
|
|
@@ -11797,7 +13052,7 @@ async function installManagedBrowser(browser) {
|
|
|
11797
13052
|
hint: `Browser "${browser}" is now available. Use extension_dev or extension_start with browser: "${browser}".`
|
|
11798
13053
|
});
|
|
11799
13054
|
} catch (err) {
|
|
11800
|
-
return
|
|
13055
|
+
return envelope_envelope({
|
|
11801
13056
|
ok: false,
|
|
11802
13057
|
command: "extension_browsers",
|
|
11803
13058
|
status: "install-failed",
|
|
@@ -11815,7 +13070,7 @@ async function installManagedBrowser(browser) {
|
|
|
11815
13070
|
}
|
|
11816
13071
|
async function uninstallManagedBrowser(args) {
|
|
11817
13072
|
const start = Date.now();
|
|
11818
|
-
if (!args.browser && !args.all) return
|
|
13073
|
+
if (!args.browser && !args.all) return envelope_envelope({
|
|
11819
13074
|
ok: false,
|
|
11820
13075
|
command: "extension_browsers",
|
|
11821
13076
|
status: "bad-request",
|
|
@@ -11829,7 +13084,7 @@ async function uninstallManagedBrowser(args) {
|
|
|
11829
13084
|
browser: args.browser,
|
|
11830
13085
|
all: args.all
|
|
11831
13086
|
});
|
|
11832
|
-
return
|
|
13087
|
+
return envelope_envelope({
|
|
11833
13088
|
ok: true,
|
|
11834
13089
|
command: "extension_browsers",
|
|
11835
13090
|
status: "uninstalled",
|
|
@@ -11840,7 +13095,7 @@ async function uninstallManagedBrowser(args) {
|
|
|
11840
13095
|
hint: 'Use extension_browsers with action: "list" to confirm what remains in the managed cache.'
|
|
11841
13096
|
});
|
|
11842
13097
|
} catch (err) {
|
|
11843
|
-
return
|
|
13098
|
+
return envelope_envelope({
|
|
11844
13099
|
ok: false,
|
|
11845
13100
|
command: "extension_browsers",
|
|
11846
13101
|
status: "uninstall-failed",
|
|
@@ -11897,7 +13152,7 @@ async function browsers_handler(args) {
|
|
|
11897
13152
|
const action = args.action ?? "detect";
|
|
11898
13153
|
if ("list" === action) return listManagedBrowsers();
|
|
11899
13154
|
if ("install" === action) {
|
|
11900
|
-
if (!args.browser) return
|
|
13155
|
+
if (!args.browser) return envelope_envelope({
|
|
11901
13156
|
ok: false,
|
|
11902
13157
|
command: "extension_browsers",
|
|
11903
13158
|
status: "bad-request",
|
|
@@ -12066,7 +13321,7 @@ function describeToolArgs(inputSchema) {
|
|
|
12066
13321
|
};
|
|
12067
13322
|
}
|
|
12068
13323
|
function inputValidationError(toolName, issues, inputSchema) {
|
|
12069
|
-
return
|
|
13324
|
+
return envelope_envelope({
|
|
12070
13325
|
ok: false,
|
|
12071
13326
|
command: toolName,
|
|
12072
13327
|
status: "invalid-arguments",
|
|
@@ -12113,9 +13368,30 @@ const tools = [
|
|
|
12113
13368
|
browsers_namespaceObject,
|
|
12114
13369
|
doctor_namespaceObject
|
|
12115
13370
|
];
|
|
13371
|
+
function toolResultFrame(result) {
|
|
13372
|
+
let refused = false;
|
|
13373
|
+
try {
|
|
13374
|
+
const parsed = JSON.parse(result);
|
|
13375
|
+
refused = isEnvelope(parsed) && false === parsed.ok;
|
|
13376
|
+
} catch {
|
|
13377
|
+
refused = false;
|
|
13378
|
+
}
|
|
13379
|
+
return {
|
|
13380
|
+
content: [
|
|
13381
|
+
{
|
|
13382
|
+
type: "text",
|
|
13383
|
+
text: result
|
|
13384
|
+
}
|
|
13385
|
+
],
|
|
13386
|
+
...refused ? {
|
|
13387
|
+
isError: true
|
|
13388
|
+
} : {}
|
|
13389
|
+
};
|
|
13390
|
+
}
|
|
12116
13391
|
const toolMap = new Map();
|
|
12117
13392
|
for (const tool of tools)toolMap.set(tool.schema.name, tool);
|
|
12118
13393
|
async function startServer() {
|
|
13394
|
+
installCarrierExitCleanup();
|
|
12119
13395
|
const server = new Server({
|
|
12120
13396
|
name: "extension-dev",
|
|
12121
13397
|
version: package_namespaceObject.rE
|
|
@@ -12138,7 +13414,7 @@ async function startServer() {
|
|
|
12138
13414
|
content: [
|
|
12139
13415
|
{
|
|
12140
13416
|
type: "text",
|
|
12141
|
-
text:
|
|
13417
|
+
text: envelope_envelope({
|
|
12142
13418
|
ok: false,
|
|
12143
13419
|
command: name,
|
|
12144
13420
|
status: "unknown-tool",
|
|
@@ -12167,20 +13443,13 @@ async function startServer() {
|
|
|
12167
13443
|
};
|
|
12168
13444
|
try {
|
|
12169
13445
|
const result = await tool.handler(normalizedArgs);
|
|
12170
|
-
return
|
|
12171
|
-
content: [
|
|
12172
|
-
{
|
|
12173
|
-
type: "text",
|
|
12174
|
-
text: result
|
|
12175
|
-
}
|
|
12176
|
-
]
|
|
12177
|
-
};
|
|
13446
|
+
return toolResultFrame(result);
|
|
12178
13447
|
} catch (err) {
|
|
12179
13448
|
return {
|
|
12180
13449
|
content: [
|
|
12181
13450
|
{
|
|
12182
13451
|
type: "text",
|
|
12183
|
-
text:
|
|
13452
|
+
text: envelope_envelope({
|
|
12184
13453
|
ok: false,
|
|
12185
13454
|
command: name,
|
|
12186
13455
|
status: "internal-error",
|
|
@@ -12290,4 +13559,5 @@ async function runCli(cmd, args) {
|
|
|
12290
13559
|
log(`Unknown command: ${cmd}. Expected one of: login, logout, whoami, release.`);
|
|
12291
13560
|
return 1;
|
|
12292
13561
|
}
|
|
12293
|
-
|
|
13562
|
+
var version_0 = package_namespaceObject.rE;
|
|
13563
|
+
export { runCli, startServer, version_0 as version };
|