@extension.dev/mcp 10.9.0 → 10.10.1
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 +150 -0
- package/README.md +34 -0
- package/claude/ARCHITECTURE.md +1 -4
- package/claude/CLAUDE.md +5 -0
- package/claude/commands/extension-debug.md +2 -0
- package/claude/rules/mcp-tools.md +16 -0
- package/dist/module.js +1409 -117
- package/dist/src/index.d.ts +3 -0
- package/dist/src/lib/allowance.d.ts +16 -0
- package/dist/src/lib/analytics-scrub.d.ts +3 -0
- package/dist/src/lib/cdp-extension-page.d.ts +21 -0
- package/dist/src/lib/envelope.d.ts +1 -1
- package/dist/src/lib/extension-surfaces.d.ts +7 -0
- package/dist/src/lib/preview-upload.d.ts +1 -0
- package/dist/src/lib/relay-eval.d.ts +12 -0
- package/dist/src/lib/safari-automation.d.ts +16 -0
- package/dist/src/lib/template-artifact-source.d.ts +1 -1
- package/dist/src/lib/types.d.ts +5 -0
- package/dist/src/lib/webdriver.d.ts +28 -0
- package/dist/src/tools/detect-browsers.d.ts +1 -0
- package/dist/src/tools/eval.d.ts +4 -0
- package/dist/src/tools/inspect-gecko.d.ts +2 -0
- package/dist/src/tools/open.d.ts +2 -0
- package/dist/src/tools/stop.d.ts +5 -0
- package/package.json +4 -4
- package/server.json +2 -2
package/dist/module.js
CHANGED
|
@@ -16,9 +16,9 @@ import { fileURLToPath } from "node:url";
|
|
|
16
16
|
import { execFile, execFileSync } from "node:child_process";
|
|
17
17
|
import ws_0 from "ws";
|
|
18
18
|
import node_http from "node:http";
|
|
19
|
+
import { promisify } from "node:util";
|
|
19
20
|
import { filterKeysForThisBrowser } from "browser-extension-manifest-fields";
|
|
20
21
|
import node_net from "node:net";
|
|
21
|
-
import { promisify } from "node:util";
|
|
22
22
|
import { extensionInstall, extensionUninstall, getManagedBrowsersCacheRoot } from "extension-install";
|
|
23
23
|
var add_feature_namespaceObject = {};
|
|
24
24
|
__webpack_require__.r(add_feature_namespaceObject);
|
|
@@ -91,9 +91,12 @@ __webpack_require__.d(dom_snapshot_namespaceObject, {
|
|
|
91
91
|
var eval_namespaceObject = {};
|
|
92
92
|
__webpack_require__.r(eval_namespaceObject);
|
|
93
93
|
__webpack_require__.d(eval_namespaceObject, {
|
|
94
|
+
EXTENSION_PAGE_CONTEXTS: ()=>EXTENSION_PAGE_CONTEXTS,
|
|
95
|
+
chromiumManifestVersion: ()=>chromiumManifestVersion,
|
|
94
96
|
handler: ()=>eval_handler,
|
|
95
97
|
resolveDefaultEvalContext: ()=>resolveDefaultEvalContext,
|
|
96
|
-
schema: ()=>eval_schema
|
|
98
|
+
schema: ()=>eval_schema,
|
|
99
|
+
wantsExtensionPageOverCdp: ()=>wantsExtensionPageOverCdp
|
|
97
100
|
});
|
|
98
101
|
var inspect_namespaceObject = {};
|
|
99
102
|
__webpack_require__.r(inspect_namespaceObject);
|
|
@@ -126,6 +129,8 @@ __webpack_require__.d(manifest_validate_namespaceObject, {
|
|
|
126
129
|
var open_namespaceObject = {};
|
|
127
130
|
__webpack_require__.r(open_namespaceObject);
|
|
128
131
|
__webpack_require__.d(open_namespaceObject, {
|
|
132
|
+
E_USER_GESTURE_REQUIRED: ()=>E_USER_GESTURE_REQUIRED,
|
|
133
|
+
OVERRIDE_SURFACES: ()=>OVERRIDE_SURFACES,
|
|
129
134
|
SURFACE_MANIFEST_KEYS: ()=>SURFACE_MANIFEST_KEYS,
|
|
130
135
|
clampPopupBounds: ()=>clampPopupBounds,
|
|
131
136
|
declaredSurfaces: ()=>declaredSurfaces,
|
|
@@ -187,6 +192,7 @@ __webpack_require__.d(start_namespaceObject, {
|
|
|
187
192
|
var stop_namespaceObject = {};
|
|
188
193
|
__webpack_require__.r(stop_namespaceObject);
|
|
189
194
|
__webpack_require__.d(stop_namespaceObject, {
|
|
195
|
+
contractProcessHints: ()=>contractProcessHints,
|
|
190
196
|
handler: ()=>stop_handler,
|
|
191
197
|
schema: ()=>stop_schema,
|
|
192
198
|
stopOne: ()=>stopOne
|
|
@@ -222,7 +228,7 @@ __webpack_require__.d(wait_namespaceObject, {
|
|
|
222
228
|
handler: ()=>wait_handler,
|
|
223
229
|
schema: ()=>wait_schema
|
|
224
230
|
});
|
|
225
|
-
var package_namespaceObject = JSON.parse('{"rE":"10.
|
|
231
|
+
var package_namespaceObject = JSON.parse('{"rE":"10.10.1","El":{"OP":"4.1.30"}}');
|
|
226
232
|
const ENVELOPE_SCHEMA = 1;
|
|
227
233
|
const collectWarnings = (warnings)=>{
|
|
228
234
|
if (!warnings) return [];
|
|
@@ -1078,9 +1084,52 @@ function parseBuildIndex(json) {
|
|
|
1078
1084
|
}
|
|
1079
1085
|
return out;
|
|
1080
1086
|
}
|
|
1087
|
+
const ADDRESS_KEYS = /url|referrer/i;
|
|
1088
|
+
const KEEP_QUERY = /^(utm_|ref$)/;
|
|
1089
|
+
const SCRUB_ORIGIN = "https://www.extension.dev";
|
|
1090
|
+
const REPOSITORY_REF = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+@/;
|
|
1091
|
+
const DEPTH = 4;
|
|
1092
|
+
function maskRepositoryRef(value) {
|
|
1093
|
+
if (!REPOSITORY_REF.test(value)) return value;
|
|
1094
|
+
return `[owner]/[repo]@${value.slice(value.indexOf("@") + 1)}`;
|
|
1095
|
+
}
|
|
1096
|
+
function scrubUrlProperty(value) {
|
|
1097
|
+
if ("string" != typeof value || !value) return value;
|
|
1098
|
+
let parsed;
|
|
1099
|
+
try {
|
|
1100
|
+
parsed = new URL(value, SCRUB_ORIGIN);
|
|
1101
|
+
} catch {
|
|
1102
|
+
return value;
|
|
1103
|
+
}
|
|
1104
|
+
let changed = false;
|
|
1105
|
+
for (const key of [
|
|
1106
|
+
...parsed.searchParams.keys()
|
|
1107
|
+
])if (!KEEP_QUERY.test(key)) {
|
|
1108
|
+
parsed.searchParams.delete(key);
|
|
1109
|
+
changed = true;
|
|
1110
|
+
}
|
|
1111
|
+
if (!changed) return value;
|
|
1112
|
+
return value.startsWith("/") ? `${parsed.pathname}${parsed.search}${parsed.hash}` : parsed.toString();
|
|
1113
|
+
}
|
|
1114
|
+
function maskDeep(value, remaining) {
|
|
1115
|
+
if ("string" == typeof value) return maskRepositoryRef(value);
|
|
1116
|
+
if (remaining <= 0 || !value || "object" != typeof value) return value;
|
|
1117
|
+
if (Array.isArray(value)) return value.map((item)=>maskDeep(item, remaining - 1));
|
|
1118
|
+
const masked = {};
|
|
1119
|
+
for (const [key, nested] of Object.entries(value))masked[key] = maskDeep(nested, remaining - 1);
|
|
1120
|
+
return masked;
|
|
1121
|
+
}
|
|
1122
|
+
function sanitizeMcpProperties(properties) {
|
|
1123
|
+
const sanitized = {
|
|
1124
|
+
...properties
|
|
1125
|
+
};
|
|
1126
|
+
for (const [key, value] of Object.entries(sanitized))if (ADDRESS_KEYS.test(key)) sanitized[key] = scrubUrlProperty(value);
|
|
1127
|
+
for (const [key, value] of Object.entries(sanitized))sanitized[key] = maskDeep(value, DEPTH);
|
|
1128
|
+
return sanitized;
|
|
1129
|
+
}
|
|
1081
1130
|
const DEFAULT_MEDIA_ORIGIN = "https://media.extension.land";
|
|
1082
1131
|
const DEFAULT_CHANNEL = "latest";
|
|
1083
|
-
const PINNED_COMMIT = "
|
|
1132
|
+
const PINNED_COMMIT = "8e2336852b51c0fb71fd03abeca1f5e93a44700e";
|
|
1084
1133
|
const CHANNEL_CACHE_TTL_MS = 300000;
|
|
1085
1134
|
function mediaOrigin() {
|
|
1086
1135
|
return (process.env.EXTENSION_MEDIA_ORIGIN || "").trim() || DEFAULT_MEDIA_ORIGIN;
|
|
@@ -1212,7 +1261,7 @@ function creationFunnelPayload(event, properties, now = new Date()) {
|
|
|
1212
1261
|
timestamp: now.toISOString(),
|
|
1213
1262
|
properties: {
|
|
1214
1263
|
draft_id: null,
|
|
1215
|
-
...properties,
|
|
1264
|
+
...sanitizeMcpProperties(properties),
|
|
1216
1265
|
source: FUNNEL_SOURCE,
|
|
1217
1266
|
entry: FUNNEL_ENTRY,
|
|
1218
1267
|
session_id: session,
|
|
@@ -1646,6 +1695,42 @@ async function pollDeviceToken(args) {
|
|
|
1646
1695
|
};
|
|
1647
1696
|
}
|
|
1648
1697
|
}
|
|
1698
|
+
const FREE_ALLOWANCE_PHRASE = "counts against your free allowance";
|
|
1699
|
+
function allowanceWallUrl(apiHint) {
|
|
1700
|
+
return `${mcpOrigins(apiHint).www.replace(/\/+$/, "")}/pricing`;
|
|
1701
|
+
}
|
|
1702
|
+
function allowance_asRecord(value) {
|
|
1703
|
+
return value && "object" == typeof value && !Array.isArray(value) ? value : null;
|
|
1704
|
+
}
|
|
1705
|
+
function asCount(value) {
|
|
1706
|
+
return "number" == typeof value && Number.isFinite(value) && value >= 0 ? value : null;
|
|
1707
|
+
}
|
|
1708
|
+
function readPlatformAllowance(body) {
|
|
1709
|
+
const record = allowance_asRecord(body);
|
|
1710
|
+
if (!record) return null;
|
|
1711
|
+
for (const key of [
|
|
1712
|
+
"allowance",
|
|
1713
|
+
"quota"
|
|
1714
|
+
]){
|
|
1715
|
+
const nested = allowance_asRecord(record[key]);
|
|
1716
|
+
if (!nested) continue;
|
|
1717
|
+
const used = asCount(nested.used);
|
|
1718
|
+
const limit = asCount(nested.limit);
|
|
1719
|
+
if (null !== used && null !== limit) return {
|
|
1720
|
+
used,
|
|
1721
|
+
limit
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
return null;
|
|
1725
|
+
}
|
|
1726
|
+
function spendNarration(options) {
|
|
1727
|
+
const counted = readPlatformAllowance(options.body);
|
|
1728
|
+
return {
|
|
1729
|
+
spent: `${options.what} ran on extension.dev's machines and ${FREE_ALLOWANCE_PHRASE}.`,
|
|
1730
|
+
remains: counted ? `The platform reports ${counted.used} of ${counted.limit} used.` : "The platform sent no remaining count on this call, and this client never invents one. When the allowance runs out, the platform refuses with its own numbers.",
|
|
1731
|
+
wall: `What the free allowance covers and when the paid plan starts are published at ${allowanceWallUrl(options.api)}.`
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1649
1734
|
const project_create_COMMAND = "extension_project_create";
|
|
1650
1735
|
const FIRST_CALL_BUDGET_MS = 8000;
|
|
1651
1736
|
const RESUME_BUDGET_MS = 22000;
|
|
@@ -1979,6 +2064,11 @@ async function finishFromPoll(poll, ctx) {
|
|
|
1979
2064
|
projectId: data.projectId ?? null,
|
|
1980
2065
|
consoleUrl,
|
|
1981
2066
|
sourceRepo: ctx.args.repo,
|
|
2067
|
+
allowance: spendNarration({
|
|
2068
|
+
what: "This project creation, including its first build,",
|
|
2069
|
+
body: data,
|
|
2070
|
+
api: ctx.apiBase
|
|
2071
|
+
}),
|
|
1982
2072
|
nextSteps: [
|
|
1983
2073
|
`extension_auth (action: login, project: '${finalWorkspace}/${finalProject}')`,
|
|
1984
2074
|
"extension_publish"
|
|
@@ -2122,6 +2212,20 @@ async function searchTemplates(args) {
|
|
|
2122
2212
|
});
|
|
2123
2213
|
}
|
|
2124
2214
|
const get_template_source_COMMAND = "extension_templates";
|
|
2215
|
+
const TEXTUAL = /\.(json|js|mjs|cjs|ts|tsx|jsx|html|htm|css|scss|sass|less|svg|txt|md|map|vue|svelte|ya?ml)$/i;
|
|
2216
|
+
function encodeTemplateFile(filePath, bytes) {
|
|
2217
|
+
if (TEXTUAL.test(filePath)) {
|
|
2218
|
+
const text = bytes.toString("utf8");
|
|
2219
|
+
if (Buffer.from(text, "utf8").equals(bytes)) return {
|
|
2220
|
+
content: text,
|
|
2221
|
+
encoding: "utf8"
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
return {
|
|
2225
|
+
content: bytes.toString("base64"),
|
|
2226
|
+
encoding: "base64"
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2125
2229
|
async function readTemplateSource(args) {
|
|
2126
2230
|
const template = await getTemplateBySlug(args.slug);
|
|
2127
2231
|
if (!template) return envelope_envelope({
|
|
@@ -2156,6 +2260,7 @@ async function readTemplateSource(args) {
|
|
|
2156
2260
|
hint: "Pass specific file paths in the files parameter to read their contents."
|
|
2157
2261
|
});
|
|
2158
2262
|
const fileContents = {};
|
|
2263
|
+
const fileEncodings = {};
|
|
2159
2264
|
const errors = [];
|
|
2160
2265
|
await Promise.all(args.files.map(async (filePath)=>{
|
|
2161
2266
|
const urls = await templateFileUrls(args.slug, filePath);
|
|
@@ -2163,13 +2268,16 @@ async function readTemplateSource(args) {
|
|
|
2163
2268
|
for (const url of urls)try {
|
|
2164
2269
|
const response = await fetch(url);
|
|
2165
2270
|
if (response.ok) {
|
|
2166
|
-
|
|
2271
|
+
const encoded = encodeTemplateFile(filePath, Buffer.from(await response.arrayBuffer()));
|
|
2272
|
+
fileContents[filePath] = encoded.content;
|
|
2273
|
+
fileEncodings[filePath] = encoded.encoding;
|
|
2167
2274
|
return;
|
|
2168
2275
|
}
|
|
2169
2276
|
lastStatus = response.status;
|
|
2170
2277
|
} catch {}
|
|
2171
2278
|
errors.push(`${filePath}: ${lastStatus || "fetch failed"}`);
|
|
2172
2279
|
}));
|
|
2280
|
+
const encodedFiles = Object.keys(fileEncodings).filter((filePath)=>"base64" === fileEncodings[filePath]).sort();
|
|
2173
2281
|
return envelope_envelope({
|
|
2174
2282
|
ok: 0 === errors.length,
|
|
2175
2283
|
command: get_template_source_COMMAND,
|
|
@@ -2180,8 +2288,10 @@ async function readTemplateSource(args) {
|
|
|
2180
2288
|
} : null,
|
|
2181
2289
|
value: {
|
|
2182
2290
|
...meta,
|
|
2183
|
-
fileContents
|
|
2291
|
+
fileContents,
|
|
2292
|
+
fileEncodings
|
|
2184
2293
|
},
|
|
2294
|
+
hint: encodedFiles.length ? `Base64-encoded, not text: ${encodedFiles.join(", ")}. Decode before writing to disk. fileEncodings names the encoding of every file read.` : void 0,
|
|
2185
2295
|
warnings: errors
|
|
2186
2296
|
});
|
|
2187
2297
|
}
|
|
@@ -3995,8 +4105,31 @@ function processCommand(pid) {
|
|
|
3995
4105
|
return "";
|
|
3996
4106
|
}
|
|
3997
4107
|
}
|
|
3998
|
-
function
|
|
4108
|
+
function contractProcessHints(projectPath, browser) {
|
|
4109
|
+
try {
|
|
4110
|
+
const contract = JSON.parse(node_fs.readFileSync(bridge_readyContractPath(projectPath, browser), "utf8"));
|
|
4111
|
+
const pids = [
|
|
4112
|
+
contract.browserPid,
|
|
4113
|
+
contract.launcherPid
|
|
4114
|
+
].filter((pid)=>"number" == typeof pid && Number.isInteger(pid) && pid > 0);
|
|
4115
|
+
return {
|
|
4116
|
+
..."string" == typeof contract.profilePath && contract.profilePath.trim() ? {
|
|
4117
|
+
profilePath: contract.profilePath
|
|
4118
|
+
} : {},
|
|
4119
|
+
pids
|
|
4120
|
+
};
|
|
4121
|
+
} catch {
|
|
4122
|
+
return {
|
|
4123
|
+
pids: []
|
|
4124
|
+
};
|
|
4125
|
+
}
|
|
4126
|
+
}
|
|
4127
|
+
function sessionProcessPids(projectPath, hints = {
|
|
4128
|
+
pids: []
|
|
4129
|
+
}) {
|
|
3999
4130
|
const pids = new Set();
|
|
4131
|
+
if (hints.profilePath) for (const pid of pgrepPids(escapeRegex(hints.profilePath)))pids.add(pid);
|
|
4132
|
+
for (const pid of hints.pids)if (pid !== process.pid && isAlive(pid)) pids.add(pid);
|
|
4000
4133
|
for (const form of projectPathForms(projectPath)){
|
|
4001
4134
|
const escaped = escapeRegex(form);
|
|
4002
4135
|
const patterns = [
|
|
@@ -4009,8 +4142,10 @@ function sessionProcessPids(projectPath) {
|
|
|
4009
4142
|
...pids
|
|
4010
4143
|
].filter((pid)=>PLAUSIBLE_SESSION_BINARY.test(processCommand(pid)));
|
|
4011
4144
|
}
|
|
4012
|
-
function reapSessionProcesses(projectPath
|
|
4013
|
-
|
|
4145
|
+
function reapSessionProcesses(projectPath, hints = {
|
|
4146
|
+
pids: []
|
|
4147
|
+
}) {
|
|
4148
|
+
const pids = sessionProcessPids(projectPath, hints);
|
|
4014
4149
|
for (const pid of pids)try {
|
|
4015
4150
|
process.kill(pid, "SIGKILL");
|
|
4016
4151
|
} catch {}
|
|
@@ -4049,8 +4184,9 @@ function pidFromReadyContract(projectPath, browser) {
|
|
|
4049
4184
|
async function stopOne(projectPath, browser) {
|
|
4050
4185
|
const session = findSessionInfo(projectPath, browser);
|
|
4051
4186
|
const pid = session?.pid ?? pidFromReadyContract(projectPath, browser);
|
|
4187
|
+
const hints = contractProcessHints(projectPath, browser);
|
|
4052
4188
|
if (null == pid) {
|
|
4053
|
-
const reaped = reapSessionProcesses(projectPath);
|
|
4189
|
+
const reaped = reapSessionProcesses(projectPath, hints);
|
|
4054
4190
|
removeSessionMarker(projectPath, browser);
|
|
4055
4191
|
return {
|
|
4056
4192
|
projectPath,
|
|
@@ -4072,7 +4208,7 @@ async function stopOne(projectPath, browser) {
|
|
|
4072
4208
|
}
|
|
4073
4209
|
detail = isAlive(pid) ? "Sent SIGTERM and SIGKILL but the process still reports alive; it may be exiting." : "Terminated.";
|
|
4074
4210
|
} else detail = "Process was already gone; cleaned up session records.";
|
|
4075
|
-
const reaped = reapSessionProcesses(projectPath);
|
|
4211
|
+
const reaped = reapSessionProcesses(projectPath, hints);
|
|
4076
4212
|
removeSession(projectPath, browser);
|
|
4077
4213
|
removeSessionMarker(projectPath, browser);
|
|
4078
4214
|
try {
|
|
@@ -4080,7 +4216,7 @@ async function stopOne(projectPath, browser) {
|
|
|
4080
4216
|
force: true
|
|
4081
4217
|
});
|
|
4082
4218
|
} catch {}
|
|
4083
|
-
const survivors = sessionProcessPids(projectPath);
|
|
4219
|
+
const survivors = sessionProcessPids(projectPath, hints);
|
|
4084
4220
|
const stopped = !isAlive(pid) && 0 === survivors.length;
|
|
4085
4221
|
if (survivors.length) detail += ` Warning: ${survivors.length} browser process(es) still alive after reap (pids ${survivors.join(", ")}).`;
|
|
4086
4222
|
else if (reaped.length) detail += ` Reaped ${reaped.length} browser process(es).`;
|
|
@@ -4444,7 +4580,8 @@ async function dev_handler(args) {
|
|
|
4444
4580
|
} : {
|
|
4445
4581
|
requestedPort: args.port ?? 8080
|
|
4446
4582
|
};
|
|
4447
|
-
const
|
|
4583
|
+
const autoAssigned = 0 === args.port;
|
|
4584
|
+
const portNote = null !== boundPort ? void 0 !== args.port && args.port !== boundPort ? autoAssigned ? `Port 0 asked the engine for any free port and it picked ${boundPort} (read from the engine's ready.json contract, the same source extension_wait reports).` : `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 : autoAssigned ? "Port 0 asked the engine for any free port, and it has not stamped its ready.json contract yet, so the port it picked is not known at response time. extension_wait reports it from that contract once it lands." : "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.";
|
|
4448
4585
|
return envelope_envelope({
|
|
4449
4586
|
ok: true,
|
|
4450
4587
|
command: dev_schema.name,
|
|
@@ -4634,7 +4771,7 @@ async function start_handler(args) {
|
|
|
4634
4771
|
});
|
|
4635
4772
|
}
|
|
4636
4773
|
function toMcpSpeak(text) {
|
|
4637
|
-
return text.replace(/`?extension dev(?: [^\s`]*)? --browser[= ]([\w-]+) --allow-control`?/g, 'extension_dev with { browser: "$1", allowControl: true }').replace(/--allow-control/g, "allowControl: true (extension_dev)").replace(/--allow-eval/g, "allowEval: true (extension_dev)").replace(/Use --context page --tab <id>/g, 'Use context: "page" (targets the active tab; pass url or tab to pick another)').replace(/--context[= ](background|popup|options|sidebar|devtools|newtab|history|bookmarks|content|page)\b/g, 'context: "$1"').replace(/--tab[= ](\d+|<[\w-]+>)/g, "tab: $1").replace(/--url[= ]"([^"]+)"/g, 'url: "$1"').replace(/--url[= ](<[\w-]+>|\S*(?:\/\/|\*)\S*)/g, 'url: "$1"').replace(/--browser[= ]([\w]+-based|chrome|chromium|edge|brave|opera|vivaldi|yandex|firefox|waterfox|librewolf|safari)\b/g, 'browser: "$1"').replace(/--timeout[= ](\d+)/g, "timeout: $1").replace(/`extension dev`/g, "extension_dev").replace(/\bextension dev\b/g, "extension_dev").replace(/--tab\b/g, "`tab`").replace(/--url\b/g, "`url`").replace(/--context\b/g, "`context`").replace(/--browser\b/g, "`browser`").replace(/--timeout\b/g, "`timeout`");
|
|
4774
|
+
return text.replace(/`?extension dev(?: [^\s`]*)? --browser[= ]([\w-]+) --allow-control`?/g, 'extension_dev with { browser: "$1", allowControl: true }').replace(/--allow-control/g, "allowControl: true (extension_dev)").replace(/--allow-eval/g, "allowEval: true (extension_dev)").replace(/Use --context page --tab <id>/g, 'Use context: "page" (targets the active tab; pass url or tab to pick another)').replace(/--context[= ](background|popup|options|sidebar|devtools|newtab|history|bookmarks|content|page)\b/g, 'context: "$1"').replace(/--tab[= ](\d+|<[\w-]+>)/g, "tab: $1").replace(/--url[= ]"([^"]+)"/g, 'url: "$1"').replace(/--url[= ](<[\w-]+>|\S*(?:\/\/|\*)\S*)/g, 'url: "$1"').replace(/--browser[= ]([\w]+-based|chrome|chromium|edge|brave|opera|vivaldi|yandex|firefox|waterfox|librewolf|safari)\b/g, 'browser: "$1"').replace(/--timeout[= ](\d+)/g, "timeout: $1").replace(/\bextension open (popup|options|sidebar|newtab|history|bookmarks)\b/g, 'extension_open with surface: "$1"').replace(/`extension dev`/g, "extension_dev").replace(/\bextension dev\b/g, "extension_dev").replace(/--tab\b/g, "`tab`").replace(/--url\b/g, "`url`").replace(/--context\b/g, "`context`").replace(/--browser\b/g, "`browser`").replace(/--timeout\b/g, "`timeout`");
|
|
4638
4775
|
}
|
|
4639
4776
|
const CONTROL_CHANNEL_DOWN_CODES = new Set([
|
|
4640
4777
|
"E_CONTROL_DENIED",
|
|
@@ -5291,6 +5428,127 @@ function isCdpEndpoint(port) {
|
|
|
5291
5428
|
});
|
|
5292
5429
|
});
|
|
5293
5430
|
}
|
|
5431
|
+
function webdriver_define_property(obj, key, value) {
|
|
5432
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
5433
|
+
value: value,
|
|
5434
|
+
enumerable: true,
|
|
5435
|
+
configurable: true,
|
|
5436
|
+
writable: true
|
|
5437
|
+
});
|
|
5438
|
+
else obj[key] = value;
|
|
5439
|
+
return obj;
|
|
5440
|
+
}
|
|
5441
|
+
const WEBDRIVER_SESSION_MISSING_HINT = "A Safari page is readable only through a safaridriver session the dev session records in ready.json (webdriverPort and webdriverSessionId), and no Extension.js release opens one today: Safari grants one automation session at a time, so this server never opens its own. What Safari does give you is the dev session's log file, which extension_logs and the log-based assertions read once the extension is enabled; the rest is Web Inspector, attended.";
|
|
5442
|
+
function readWebDriverSession(projectPath, browser) {
|
|
5443
|
+
try {
|
|
5444
|
+
const contract = JSON.parse(node_fs.readFileSync(bridge_readyContractPath(projectPath, browser), "utf8"));
|
|
5445
|
+
if ("number" == typeof contract.webdriverPort && Number.isFinite(contract.webdriverPort) && "string" == typeof contract.webdriverSessionId && contract.webdriverSessionId.length > 0) return {
|
|
5446
|
+
port: contract.webdriverPort,
|
|
5447
|
+
sessionId: contract.webdriverSessionId
|
|
5448
|
+
};
|
|
5449
|
+
} catch {}
|
|
5450
|
+
return null;
|
|
5451
|
+
}
|
|
5452
|
+
function readyExtensionId(projectPath, browser) {
|
|
5453
|
+
try {
|
|
5454
|
+
const contract = JSON.parse(node_fs.readFileSync(bridge_readyContractPath(projectPath, browser), "utf8"));
|
|
5455
|
+
const id = String(contract.extensionId || "").trim();
|
|
5456
|
+
return id.length > 0 ? id : null;
|
|
5457
|
+
} catch {
|
|
5458
|
+
return null;
|
|
5459
|
+
}
|
|
5460
|
+
}
|
|
5461
|
+
class WebDriverClient {
|
|
5462
|
+
async call(method, route, body, timeoutMs = 15000) {
|
|
5463
|
+
const controller = new AbortController();
|
|
5464
|
+
const timer = setTimeout(()=>controller.abort(), timeoutMs);
|
|
5465
|
+
try {
|
|
5466
|
+
const response = await this.fetchFn(`${this.base}${route}`, {
|
|
5467
|
+
method,
|
|
5468
|
+
headers: {
|
|
5469
|
+
"content-type": "application/json"
|
|
5470
|
+
},
|
|
5471
|
+
body: "GET" === method ? void 0 : JSON.stringify(body ?? {}),
|
|
5472
|
+
signal: controller.signal
|
|
5473
|
+
});
|
|
5474
|
+
const text = await response.text();
|
|
5475
|
+
let parsed = {};
|
|
5476
|
+
try {
|
|
5477
|
+
parsed = text ? JSON.parse(text) : {};
|
|
5478
|
+
} catch {
|
|
5479
|
+
parsed = {
|
|
5480
|
+
value: {
|
|
5481
|
+
message: text
|
|
5482
|
+
}
|
|
5483
|
+
};
|
|
5484
|
+
}
|
|
5485
|
+
if (!response.ok) {
|
|
5486
|
+
const value = parsed.value || {};
|
|
5487
|
+
throw new Error(String(value.message || value.error || `${response.status} on ${route}`));
|
|
5488
|
+
}
|
|
5489
|
+
return parsed.value;
|
|
5490
|
+
} finally{
|
|
5491
|
+
clearTimeout(timer);
|
|
5492
|
+
}
|
|
5493
|
+
}
|
|
5494
|
+
async alive() {
|
|
5495
|
+
try {
|
|
5496
|
+
await this.call("GET", "/url");
|
|
5497
|
+
return true;
|
|
5498
|
+
} catch {
|
|
5499
|
+
return false;
|
|
5500
|
+
}
|
|
5501
|
+
}
|
|
5502
|
+
async currentUrl() {
|
|
5503
|
+
const value = await this.call("GET", "/url");
|
|
5504
|
+
return "string" == typeof value ? value : null;
|
|
5505
|
+
}
|
|
5506
|
+
async navigate(url, timeoutMs = 60000) {
|
|
5507
|
+
await this.call("POST", "/url", {
|
|
5508
|
+
url
|
|
5509
|
+
}, timeoutMs);
|
|
5510
|
+
}
|
|
5511
|
+
async refresh(timeoutMs = 60000) {
|
|
5512
|
+
await this.call("POST", "/refresh", {}, timeoutMs);
|
|
5513
|
+
}
|
|
5514
|
+
async execute(script, args = [], timeoutMs = 30000) {
|
|
5515
|
+
return this.call("POST", "/execute/sync", {
|
|
5516
|
+
script,
|
|
5517
|
+
args
|
|
5518
|
+
}, timeoutMs);
|
|
5519
|
+
}
|
|
5520
|
+
constructor(info, fetchFn = fetch){
|
|
5521
|
+
webdriver_define_property(this, "info", void 0);
|
|
5522
|
+
webdriver_define_property(this, "fetchFn", void 0);
|
|
5523
|
+
webdriver_define_property(this, "base", void 0);
|
|
5524
|
+
this.info = info;
|
|
5525
|
+
this.fetchFn = fetchFn;
|
|
5526
|
+
this.base = `http://127.0.0.1:${info.port}/session/${info.sessionId}`;
|
|
5527
|
+
}
|
|
5528
|
+
}
|
|
5529
|
+
const EXTENSION_ROOT_READING_SCRIPT = `
|
|
5530
|
+
const nodes = Array.from(document.querySelectorAll('#extension-root,[data-extension-root]'));
|
|
5531
|
+
return {
|
|
5532
|
+
roots: nodes.length,
|
|
5533
|
+
owners: nodes.map((n) => String(n.getAttribute('data-extjs-reinject-owner') || '')),
|
|
5534
|
+
url: String(location.href),
|
|
5535
|
+
title: String(document.title || '')
|
|
5536
|
+
};
|
|
5537
|
+
`;
|
|
5538
|
+
async function readExtensionRoots(client) {
|
|
5539
|
+
const value = await client.execute(EXTENSION_ROOT_READING_SCRIPT);
|
|
5540
|
+
return {
|
|
5541
|
+
roots: "number" == typeof value?.roots ? value.roots : 0,
|
|
5542
|
+
owners: Array.isArray(value?.owners) ? value.owners.map((o)=>String(o)) : [],
|
|
5543
|
+
url: String(value?.url || ""),
|
|
5544
|
+
title: String(value?.title || "")
|
|
5545
|
+
};
|
|
5546
|
+
}
|
|
5547
|
+
function sameDocument(current, wanted) {
|
|
5548
|
+
if (!current) return false;
|
|
5549
|
+
const strip = (u)=>u.replace(/[#?].*$/, "").replace(/\/+$/, "");
|
|
5550
|
+
return strip(current) === strip(wanted);
|
|
5551
|
+
}
|
|
5294
5552
|
const ENGINE_COMPANION_IDS = new Set([
|
|
5295
5553
|
"kgdaecdpfkikjncaalnmmnjjfpofkcbl",
|
|
5296
5554
|
CARRIER_EXTENSION_ID
|
|
@@ -5462,7 +5720,48 @@ async function pollForBridgeTab(projectPath, browser, url, budgetMs) {
|
|
|
5462
5720
|
await new Promise((r)=>setTimeout(r, 250));
|
|
5463
5721
|
}
|
|
5464
5722
|
}
|
|
5723
|
+
const UNKNOWN_VERB = /unknown command/i;
|
|
5465
5724
|
async function navigateToUrlViaBridge(projectPath, browser, url, timeout, tool = "extension_open") {
|
|
5725
|
+
const viaVerb = await runActVerb([
|
|
5726
|
+
"navigate",
|
|
5727
|
+
url,
|
|
5728
|
+
projectPath,
|
|
5729
|
+
"--browser",
|
|
5730
|
+
browser,
|
|
5731
|
+
...null != timeout ? [
|
|
5732
|
+
"--timeout",
|
|
5733
|
+
String(timeout)
|
|
5734
|
+
] : []
|
|
5735
|
+
], projectPath, timeout, tool);
|
|
5736
|
+
let verbFrame;
|
|
5737
|
+
try {
|
|
5738
|
+
verbFrame = JSON.parse(viaVerb);
|
|
5739
|
+
} catch {
|
|
5740
|
+
verbFrame = null;
|
|
5741
|
+
}
|
|
5742
|
+
const verbUnknown = verbFrame?.ok === false && UNKNOWN_VERB.test(String(verbFrame?.error?.message ?? ""));
|
|
5743
|
+
if (verbFrame && !verbUnknown) {
|
|
5744
|
+
if (false === verbFrame.ok) return actFrameJson(verbFrame.hint ? verbFrame : {
|
|
5745
|
+
...verbFrame,
|
|
5746
|
+
hint: "URL navigation rides the agent bridge, so the dev session must be started with allowControl: true (extension_dev)."
|
|
5747
|
+
});
|
|
5748
|
+
const value = verbFrame.value && "object" == typeof verbFrame.value ? verbFrame.value : {};
|
|
5749
|
+
return envelope_envelope({
|
|
5750
|
+
ok: true,
|
|
5751
|
+
command: tool,
|
|
5752
|
+
status: "navigated",
|
|
5753
|
+
value: {
|
|
5754
|
+
navigated: url,
|
|
5755
|
+
tabId: "number" == typeof value.tabId ? value.tabId : null,
|
|
5756
|
+
created: true === value.created,
|
|
5757
|
+
via: "navigate"
|
|
5758
|
+
},
|
|
5759
|
+
hint: "The tab now shows this page. Content scripts that match it ran on load; read them with extension_eval (context: 'content', url) or extension_assert content-script-injected."
|
|
5760
|
+
});
|
|
5761
|
+
}
|
|
5762
|
+
return navigateToUrlViaBackgroundEval(projectPath, browser, url, timeout, tool);
|
|
5763
|
+
}
|
|
5764
|
+
async function navigateToUrlViaBackgroundEval(projectPath, browser, url, timeout, tool = "extension_open") {
|
|
5466
5765
|
const expression = `(async () => { const api = typeof browser !== "undefined" ? browser : chrome; const tabs = await api.tabs.query({ active: true, currentWindow: true }); const active = tabs && tabs[0]; const tab = active && active.id != null ? await api.tabs.update(active.id, { url: ${JSON.stringify(url)} }) : await api.tabs.create({ url: ${JSON.stringify(url)} }); return { tabId: tab && tab.id != null ? tab.id : null }; })()`;
|
|
5467
5766
|
const raw = await runActVerb([
|
|
5468
5767
|
"eval",
|
|
@@ -5481,7 +5780,7 @@ async function navigateToUrlViaBridge(projectPath, browser, url, timeout, tool =
|
|
|
5481
5780
|
const parsed = JSON.parse(raw);
|
|
5482
5781
|
if (parsed?.ok === false) return actFrameJson(parsed.hint ? parsed : {
|
|
5483
5782
|
...parsed,
|
|
5484
|
-
hint: "
|
|
5783
|
+
hint: "This engine predates the navigate verb, so URL navigation rides a background eval of tabs.update: the dev session must be started with allowEval: true (extension_dev), and an MV3 background that refuses eval by CSP (Safari, strict Chromium builds) cannot navigate this way at all; upgrade the project's Extension.js to one with `extension navigate`."
|
|
5485
5784
|
});
|
|
5486
5785
|
} catch {
|
|
5487
5786
|
return raw;
|
|
@@ -5533,6 +5832,244 @@ async function resolveBridgeBaseUrl(projectPath, browser, timeout) {
|
|
|
5533
5832
|
} catch {}
|
|
5534
5833
|
return null;
|
|
5535
5834
|
}
|
|
5835
|
+
const TARGET_ID_NOTE = "targetId is a CDP target id, NOT a chrome.tabs id: do not pass it as `tab`. Target a tab with `tabUrl` (URL substring) or `url`; if you need a numeric tab id, call extension_dom_snapshot with listTabs: true.";
|
|
5836
|
+
function filterPageTargets(raw) {
|
|
5837
|
+
return raw.filter((t)=>"page" === t.type && !String(t.url ?? "").startsWith("devtools://")).map((t)=>({
|
|
5838
|
+
targetId: String(t.id),
|
|
5839
|
+
type: String(t.type),
|
|
5840
|
+
url: String(t.url ?? ""),
|
|
5841
|
+
title: String(t.title ?? "")
|
|
5842
|
+
}));
|
|
5843
|
+
}
|
|
5844
|
+
async function listPageTargets(port) {
|
|
5845
|
+
return filterPageTargets(await CDPClient.discoverTargets(port));
|
|
5846
|
+
}
|
|
5847
|
+
function matchTargetsByUrl(targets, needle) {
|
|
5848
|
+
const wanted = needle.toLowerCase();
|
|
5849
|
+
const byUrl = targets.filter((t)=>t.url.toLowerCase().includes(wanted));
|
|
5850
|
+
if (byUrl.length > 0) return byUrl;
|
|
5851
|
+
return targets.filter((t)=>t.title.toLowerCase().includes(wanted));
|
|
5852
|
+
}
|
|
5853
|
+
const RUNTIME_EVALUATE_DEFAULTS = {
|
|
5854
|
+
returnByValue: true,
|
|
5855
|
+
awaitPromise: true,
|
|
5856
|
+
userGesture: true,
|
|
5857
|
+
replMode: true
|
|
5858
|
+
};
|
|
5859
|
+
function stripHash(url) {
|
|
5860
|
+
return url.replace(/#.*$/, "");
|
|
5861
|
+
}
|
|
5862
|
+
function matchExtensionPageTargets(targets, wantedUrl) {
|
|
5863
|
+
const wanted = stripHash(wantedUrl);
|
|
5864
|
+
return targets.filter((t)=>{
|
|
5865
|
+
const url = stripHash(t.url);
|
|
5866
|
+
return url === wanted || url.startsWith(wanted);
|
|
5867
|
+
});
|
|
5868
|
+
}
|
|
5869
|
+
async function findExtensionPageTargets(port, wantedUrl) {
|
|
5870
|
+
try {
|
|
5871
|
+
return matchExtensionPageTargets(await listPageTargets(port), wantedUrl);
|
|
5872
|
+
} catch {
|
|
5873
|
+
return [];
|
|
5874
|
+
}
|
|
5875
|
+
}
|
|
5876
|
+
function readRemoteValue(result) {
|
|
5877
|
+
if (!result) return null;
|
|
5878
|
+
if ("value" in result) return result.value;
|
|
5879
|
+
if ("undefined" === result.type) return null;
|
|
5880
|
+
if ("string" == typeof result.unserializableValue) return result.unserializableValue;
|
|
5881
|
+
return result.description ?? null;
|
|
5882
|
+
}
|
|
5883
|
+
function describeException(details) {
|
|
5884
|
+
const exception = details?.exception;
|
|
5885
|
+
if ("string" == typeof exception?.description && exception.description) return exception.description.split("\n")[0];
|
|
5886
|
+
if (exception && "value" in exception) return String(exception.value);
|
|
5887
|
+
return details?.text || "the expression threw";
|
|
5888
|
+
}
|
|
5889
|
+
async function evaluateOnExtensionPage(port, targetId, expression) {
|
|
5890
|
+
const cdp = new CDPClient();
|
|
5891
|
+
try {
|
|
5892
|
+
await cdp.connect(await CDPClient.discoverBrowserWsUrl(port));
|
|
5893
|
+
const sessionId = await cdp.attachToTarget(targetId);
|
|
5894
|
+
const response = await cdp.sendCommand("Runtime.evaluate", {
|
|
5895
|
+
expression,
|
|
5896
|
+
...RUNTIME_EVALUATE_DEFAULTS
|
|
5897
|
+
}, sessionId);
|
|
5898
|
+
if (response?.exceptionDetails) return {
|
|
5899
|
+
ok: false,
|
|
5900
|
+
thrown: true,
|
|
5901
|
+
message: describeException(response.exceptionDetails)
|
|
5902
|
+
};
|
|
5903
|
+
return {
|
|
5904
|
+
ok: true,
|
|
5905
|
+
value: readRemoteValue(response?.result)
|
|
5906
|
+
};
|
|
5907
|
+
} catch (error) {
|
|
5908
|
+
return {
|
|
5909
|
+
ok: false,
|
|
5910
|
+
thrown: false,
|
|
5911
|
+
message: error instanceof Error ? error.message : String(error)
|
|
5912
|
+
};
|
|
5913
|
+
} finally{
|
|
5914
|
+
try {
|
|
5915
|
+
cdp.disconnect();
|
|
5916
|
+
} catch {}
|
|
5917
|
+
}
|
|
5918
|
+
}
|
|
5919
|
+
const SIDE_PANEL_API_READY = 'typeof chrome !== "undefined" && typeof chrome.sidePanel !== "undefined" && typeof chrome.windows !== "undefined" && document.readyState !== "loading"';
|
|
5920
|
+
const SIDE_PANEL_STATE = "window.__extensionDevSidePanel";
|
|
5921
|
+
const ARM_SIDE_PANEL_SCRIPT = `(async () => {
|
|
5922
|
+
const win = await chrome.windows.getCurrent();
|
|
5923
|
+
const state = { phase: "armed", windowId: win.id };
|
|
5924
|
+
${SIDE_PANEL_STATE} = state;
|
|
5925
|
+
const overlay = document.createElement("div");
|
|
5926
|
+
overlay.setAttribute("data-extension-dev-gesture", "");
|
|
5927
|
+
overlay.style.cssText = "position:fixed;inset:0;z-index:2147483647;background:transparent;cursor:default;";
|
|
5928
|
+
overlay.addEventListener("click", (event) => {
|
|
5929
|
+
event.stopPropagation();
|
|
5930
|
+
event.preventDefault();
|
|
5931
|
+
state.phase = "clicked";
|
|
5932
|
+
const settle = (phase, message) => {
|
|
5933
|
+
state.phase = phase;
|
|
5934
|
+
if (message) state.message = message;
|
|
5935
|
+
overlay.remove();
|
|
5936
|
+
};
|
|
5937
|
+
try {
|
|
5938
|
+
Promise.resolve(chrome.sidePanel.open({ windowId: win.id }))
|
|
5939
|
+
.then(() => settle("opened"))
|
|
5940
|
+
.catch((error) => settle("failed", String((error && error.message) || error)));
|
|
5941
|
+
} catch (error) {
|
|
5942
|
+
settle("failed", String((error && error.message) || error));
|
|
5943
|
+
}
|
|
5944
|
+
}, { once: true });
|
|
5945
|
+
document.documentElement.appendChild(overlay);
|
|
5946
|
+
return win.id;
|
|
5947
|
+
})()`;
|
|
5948
|
+
const READ_SIDE_PANEL_STATE = `(() => {
|
|
5949
|
+
const state = ${SIDE_PANEL_STATE};
|
|
5950
|
+
return state ? { phase: state.phase, message: state.message } : null;
|
|
5951
|
+
})()`;
|
|
5952
|
+
async function pollUntil(read, budgetMs, everyMs) {
|
|
5953
|
+
const deadline = Date.now() + budgetMs;
|
|
5954
|
+
for(;;){
|
|
5955
|
+
const value = await read();
|
|
5956
|
+
if (null !== value) return value;
|
|
5957
|
+
if (Date.now() >= deadline) return null;
|
|
5958
|
+
await new Promise((r)=>setTimeout(r, everyMs));
|
|
5959
|
+
}
|
|
5960
|
+
}
|
|
5961
|
+
async function openSidePanelWithSyntheticGesture(port, hostUrl) {
|
|
5962
|
+
const cdp = new CDPClient();
|
|
5963
|
+
let hostId = null;
|
|
5964
|
+
const closeHost = async ()=>{
|
|
5965
|
+
if (!hostId) return;
|
|
5966
|
+
try {
|
|
5967
|
+
await cdp.sendCommand("Target.closeTarget", {
|
|
5968
|
+
targetId: hostId
|
|
5969
|
+
});
|
|
5970
|
+
} catch {}
|
|
5971
|
+
};
|
|
5972
|
+
try {
|
|
5973
|
+
await cdp.connect(await CDPClient.discoverBrowserWsUrl(port));
|
|
5974
|
+
const created = await cdp.sendCommand("Target.createTarget", {
|
|
5975
|
+
url: hostUrl
|
|
5976
|
+
});
|
|
5977
|
+
if ("string" != typeof created?.targetId) return {
|
|
5978
|
+
opened: false,
|
|
5979
|
+
reason: `the browser did not open a host tab for ${hostUrl}`
|
|
5980
|
+
};
|
|
5981
|
+
hostId = created.targetId;
|
|
5982
|
+
const sessionId = await cdp.attachToTarget(hostId);
|
|
5983
|
+
await cdp.sendCommand("Runtime.enable", {}, sessionId);
|
|
5984
|
+
const evaluate = async (expression)=>await cdp.sendCommand("Runtime.evaluate", {
|
|
5985
|
+
expression,
|
|
5986
|
+
...RUNTIME_EVALUATE_DEFAULTS
|
|
5987
|
+
}, sessionId);
|
|
5988
|
+
const apiReady = await pollUntil(async ()=>{
|
|
5989
|
+
const response = await evaluate(SIDE_PANEL_API_READY).catch(()=>null);
|
|
5990
|
+
return response?.result?.value === true ? true : null;
|
|
5991
|
+
}, 5000, 200);
|
|
5992
|
+
if (!apiReady) {
|
|
5993
|
+
await closeHost();
|
|
5994
|
+
return {
|
|
5995
|
+
opened: false,
|
|
5996
|
+
reason: "chrome.sidePanel is not available in the extension's own page (the manifest needs the sidePanel permission and the page must load)"
|
|
5997
|
+
};
|
|
5998
|
+
}
|
|
5999
|
+
const armed = await evaluate(ARM_SIDE_PANEL_SCRIPT);
|
|
6000
|
+
if (armed.exceptionDetails) {
|
|
6001
|
+
await closeHost();
|
|
6002
|
+
return {
|
|
6003
|
+
opened: false,
|
|
6004
|
+
reason: `arming the gesture listener threw: ${describeException(armed.exceptionDetails)}`
|
|
6005
|
+
};
|
|
6006
|
+
}
|
|
6007
|
+
const point = {
|
|
6008
|
+
x: 10,
|
|
6009
|
+
y: 10
|
|
6010
|
+
};
|
|
6011
|
+
await cdp.sendCommand("Input.dispatchMouseEvent", {
|
|
6012
|
+
type: "mouseMoved",
|
|
6013
|
+
...point
|
|
6014
|
+
}, sessionId);
|
|
6015
|
+
await cdp.sendCommand("Input.dispatchMouseEvent", {
|
|
6016
|
+
type: "mousePressed",
|
|
6017
|
+
button: "left",
|
|
6018
|
+
clickCount: 1,
|
|
6019
|
+
...point
|
|
6020
|
+
}, sessionId);
|
|
6021
|
+
await cdp.sendCommand("Input.dispatchMouseEvent", {
|
|
6022
|
+
type: "mouseReleased",
|
|
6023
|
+
button: "left",
|
|
6024
|
+
clickCount: 1,
|
|
6025
|
+
...point
|
|
6026
|
+
}, sessionId);
|
|
6027
|
+
const settled = await pollUntil(async ()=>{
|
|
6028
|
+
const response = await evaluate(READ_SIDE_PANEL_STATE).catch(()=>null);
|
|
6029
|
+
const state = response?.result?.value;
|
|
6030
|
+
if (state?.phase === "opened" || state?.phase === "failed") return state;
|
|
6031
|
+
return null;
|
|
6032
|
+
}, 3000, 100);
|
|
6033
|
+
await closeHost();
|
|
6034
|
+
if (!settled) return {
|
|
6035
|
+
opened: false,
|
|
6036
|
+
reason: "the synthetic click never reached chrome.sidePanel.open (the page did not report a result within 3s)"
|
|
6037
|
+
};
|
|
6038
|
+
if ("failed" === settled.phase) return {
|
|
6039
|
+
opened: false,
|
|
6040
|
+
reason: `chrome.sidePanel.open rejected: ${settled.message ?? "no message"}`
|
|
6041
|
+
};
|
|
6042
|
+
const excluded = hostId;
|
|
6043
|
+
const panel = await pollUntil(async ()=>{
|
|
6044
|
+
const matches = (await findExtensionPageTargets(port, hostUrl)).filter((t)=>t.targetId !== excluded);
|
|
6045
|
+
return matches.length ? matches[0] : null;
|
|
6046
|
+
}, 3000, 250);
|
|
6047
|
+
if (!panel) return {
|
|
6048
|
+
opened: false,
|
|
6049
|
+
reason: `chrome.sidePanel.open resolved but no page target for ${hostUrl} appeared within 3s`
|
|
6050
|
+
};
|
|
6051
|
+
return {
|
|
6052
|
+
opened: true,
|
|
6053
|
+
targetId: panel.targetId,
|
|
6054
|
+
url: panel.url
|
|
6055
|
+
};
|
|
6056
|
+
} catch (error) {
|
|
6057
|
+
await closeHost();
|
|
6058
|
+
return {
|
|
6059
|
+
opened: false,
|
|
6060
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
6061
|
+
};
|
|
6062
|
+
} finally{
|
|
6063
|
+
try {
|
|
6064
|
+
cdp.disconnect();
|
|
6065
|
+
} catch {}
|
|
6066
|
+
}
|
|
6067
|
+
}
|
|
6068
|
+
const OVERRIDE_SURFACES = [
|
|
6069
|
+
"newtab",
|
|
6070
|
+
"history",
|
|
6071
|
+
"bookmarks"
|
|
6072
|
+
];
|
|
5536
6073
|
async function pollForTarget(port, url, budgetMs, navigatedTargetId) {
|
|
5537
6074
|
const deadline = Date.now() + budgetMs;
|
|
5538
6075
|
const wanted = url.replace(/#.*$/, "");
|
|
@@ -5567,7 +6104,38 @@ function isDisposableTab(tabUrl, destination) {
|
|
|
5567
6104
|
const origin = destination.match(/^chrome-extension:\/\/[a-p]{32}\//)?.[0];
|
|
5568
6105
|
return Boolean(origin && tabUrl.startsWith(origin));
|
|
5569
6106
|
}
|
|
6107
|
+
async function navigateToUrlViaWebDriver(projectPath, browser, url) {
|
|
6108
|
+
const info = readWebDriverSession(projectPath, browser);
|
|
6109
|
+
if (!info) return navigateToUrlViaBridge(projectPath, browser, url);
|
|
6110
|
+
const client = new WebDriverClient(info);
|
|
6111
|
+
try {
|
|
6112
|
+
await client.navigate(url);
|
|
6113
|
+
return envelope_envelope({
|
|
6114
|
+
ok: true,
|
|
6115
|
+
command: open_schema.name,
|
|
6116
|
+
status: "navigated",
|
|
6117
|
+
value: {
|
|
6118
|
+
browser,
|
|
6119
|
+
url: await client.currentUrl().catch(()=>url)
|
|
6120
|
+
},
|
|
6121
|
+
hint: "The Safari automation window now shows this page. Content scripts that match it ran on load; read their DOM with extension_eval or extension_assert."
|
|
6122
|
+
});
|
|
6123
|
+
} catch (error) {
|
|
6124
|
+
return envelope_envelope({
|
|
6125
|
+
ok: false,
|
|
6126
|
+
command: open_schema.name,
|
|
6127
|
+
status: "navigate-failed",
|
|
6128
|
+
error: {
|
|
6129
|
+
code: "E_NAVIGATE_FAILED",
|
|
6130
|
+
name: "NavigateError",
|
|
6131
|
+
message: error instanceof Error ? error.message : String(error)
|
|
6132
|
+
},
|
|
6133
|
+
hint: "The automation window refused the URL or is gone. extension_doctor names which."
|
|
6134
|
+
});
|
|
6135
|
+
}
|
|
6136
|
+
}
|
|
5570
6137
|
async function navigateToUrl(projectPath, browser, url, timeout) {
|
|
6138
|
+
if (WEBKIT_FAMILY.has(browser) && readWebDriverSession(projectPath, browser)) return navigateToUrlViaWebDriver(projectPath, browser, url);
|
|
5571
6139
|
if (!isChromiumFamily(browser)) return navigateToUrlViaBridge(projectPath, browser, url, timeout);
|
|
5572
6140
|
const resolved = await resolveCdpPort(projectPath, browser);
|
|
5573
6141
|
if (!resolved) return envelope_envelope({
|
|
@@ -5892,7 +6460,8 @@ async function openSurfaceAsTab(projectPath, browser, surface) {
|
|
|
5892
6460
|
popupBounds = await applyPopupBounds(projectPath, browser, target.targetId);
|
|
5893
6461
|
if (popupBounds) renderedAsTab.popupBounds = popupBounds;
|
|
5894
6462
|
}
|
|
5895
|
-
|
|
6463
|
+
const reachIt = `Inspect it with extension_dom_snapshot context: '${surface}' (include: ['html']), or extension_inspect with this url. To run code in it, call extension_eval with context: '${surface}' (on Chromium this goes over CDP, which the extension page CSP does not govern); do NOT pass this extension-page url as a tab target for script injection, which cannot reach extension pages.`;
|
|
6464
|
+
parsed.hint = OVERRIDE_SURFACES.includes(surface) ? `Opened the ${surface} override page in a tab, which is the only place the browser ever renders a chrome_url_overrides page, so this is the real surface and not a stand-in. ` + reachIt : `Rendered the ${surface} document in a real tab, which is how you inspect a surface headlessly. ` + (popupBounds ? `The window was resized to the popup's content size (${popupBounds.width}x${popupBounds.height}${popupBounds.clamped ? ", clamped to Chrome's 25x25-800x600 popup bounds" : ""}), approximating real popup rendering. This resizes the WHOLE browser window for the session. It is the same page with the same extension APIs, but window.close() closes the tab. ` : "It is the same page with the same extension APIs, but it is NOT hosted in a popup window: no popup sizing, and window.close() closes the tab. ") + reachIt;
|
|
5896
6465
|
return actFrameJson(parsed);
|
|
5897
6466
|
}
|
|
5898
6467
|
} catch {}
|
|
@@ -5940,7 +6509,7 @@ async function confirmSurfaceTarget(projectPath, browser, surface, raw) {
|
|
|
5940
6509
|
}
|
|
5941
6510
|
const open_schema = {
|
|
5942
6511
|
name: "extension_open",
|
|
5943
|
-
description: "Open an extension surface, or replay an event, in a running session. Pass surface:'popup', 'options' or 'sidebar' to open a UI surface, or 'newtab', 'history' or 'bookmarks' to open the matching chrome_url_overrides page in a tab. Pass surface:'action' to trigger the toolbar action, which opens its popup or replays chrome.action.onClicked when there is none. Pass surface:'command' with `name` to replay a chrome.commands.onCommand shortcut. Note that action and command replay invoke your listener without a user gesture, so the gesture-derived activeTab grant does not apply; the result reports gesture:false and warns when activeTab is declared. Start the session with allowControl:true (extension_dev).",
|
|
6512
|
+
description: "Open an extension surface, or replay an event, in a running session. Pass surface:'popup', 'options' or 'sidebar' to open a UI surface, or 'newtab', 'history' or 'bookmarks' to open the matching chrome_url_overrides page in a tab (always a tab, resolved by the server, never sent to the engine). On Chromium, when Chrome refuses the sidebar for lack of a user gesture, the server opens the real panel through a synthetic click on the extension's own page and says so in warnings; if that fails too it renders the sidebar document as a tab. Pass surface:'action' to trigger the toolbar action, which opens its popup or replays chrome.action.onClicked when there is none. Pass surface:'command' with `name` to replay a chrome.commands.onCommand shortcut. Note that action and command replay invoke your listener without a user gesture, so the gesture-derived activeTab grant does not apply; the result reports gesture:false and warns when activeTab is declared. Start the session with allowControl:true (extension_dev).",
|
|
5944
6513
|
inputSchema: {
|
|
5945
6514
|
type: "object",
|
|
5946
6515
|
properties: {
|
|
@@ -5992,10 +6561,9 @@ async function open_handler(args) {
|
|
|
5992
6561
|
"popup",
|
|
5993
6562
|
"options",
|
|
5994
6563
|
"sidebar",
|
|
5995
|
-
|
|
5996
|
-
"history",
|
|
5997
|
-
"bookmarks"
|
|
6564
|
+
...OVERRIDE_SURFACES
|
|
5998
6565
|
];
|
|
6566
|
+
if (args.surface && OVERRIDE_SURFACES.includes(args.surface)) return openSurfaceAsTab(args.projectPath, browser, args.surface);
|
|
5999
6567
|
if (args.asTab && args.surface && AS_TAB_SURFACES.includes(args.surface)) return openSurfaceAsTab(args.projectPath, browser, args.surface);
|
|
6000
6568
|
if (!args.surface) return envelope_envelope({
|
|
6001
6569
|
ok: false,
|
|
@@ -6062,8 +6630,135 @@ async function open_handler(args) {
|
|
|
6062
6630
|
return actFrameJson(parsed);
|
|
6063
6631
|
}
|
|
6064
6632
|
} catch {}
|
|
6633
|
+
if (!headless && "sidebar" === args.surface && isChromiumFamily(browser)) {
|
|
6634
|
+
const refusal = readGestureRefusal(raw);
|
|
6635
|
+
if (refusal) return openSidebarThroughGesture(args.projectPath, browser, refusal);
|
|
6636
|
+
}
|
|
6637
|
+
if ("sidebar" === args.surface && isGeckoFamily(browser)) {
|
|
6638
|
+
const refusal = readUnsupportedRefusal(raw);
|
|
6639
|
+
if (refusal) return openGeckoSidebar(args.projectPath, browser, refusal, args.timeout);
|
|
6640
|
+
}
|
|
6065
6641
|
return AS_TAB_SURFACES.includes(args.surface) ? confirmSurfaceTarget(args.projectPath, browser, args.surface, raw) : raw;
|
|
6066
6642
|
}
|
|
6643
|
+
const E_USER_GESTURE_REQUIRED = "E_USER_GESTURE_REQUIRED";
|
|
6644
|
+
function readGestureRefusal(raw) {
|
|
6645
|
+
let parsed;
|
|
6646
|
+
try {
|
|
6647
|
+
parsed = JSON.parse(raw);
|
|
6648
|
+
} catch {
|
|
6649
|
+
return null;
|
|
6650
|
+
}
|
|
6651
|
+
if (parsed?.ok !== false) return null;
|
|
6652
|
+
const code = "string" == typeof parsed.error?.code ? parsed.error.code : "";
|
|
6653
|
+
const message = String(parsed.error?.message ?? "");
|
|
6654
|
+
return code === E_USER_GESTURE_REQUIRED || /user gesture/i.test(message) ? parsed : null;
|
|
6655
|
+
}
|
|
6656
|
+
function readUnsupportedRefusal(raw) {
|
|
6657
|
+
let parsed;
|
|
6658
|
+
try {
|
|
6659
|
+
parsed = JSON.parse(raw);
|
|
6660
|
+
} catch {
|
|
6661
|
+
return null;
|
|
6662
|
+
}
|
|
6663
|
+
if (parsed?.ok !== false) return null;
|
|
6664
|
+
const code = "string" == typeof parsed.error?.code ? parsed.error.code : "";
|
|
6665
|
+
const message = String(parsed.error?.message ?? "");
|
|
6666
|
+
return "E_NOT_IMPLEMENTED" === code || /not available/i.test(message) ? parsed : null;
|
|
6667
|
+
}
|
|
6668
|
+
const GECKO_SIDEBAR_GESTURE = "Firefox opens a sidebar_action panel only from a user gesture (the toolbar button or View > Sidebar; Bugzilla 1392624), and the engine's open verb carries none, so it cannot open the panel";
|
|
6669
|
+
async function openGeckoSidebar(projectPath, browser, refusal, timeout) {
|
|
6670
|
+
const doc = surfaceDocument(projectPath, browser, "sidebar");
|
|
6671
|
+
if (!doc) return missingSurfaceError(projectPath, browser, "sidebar", "so there is no sidebar panel to open");
|
|
6672
|
+
const probe = await runActVerb([
|
|
6673
|
+
"inspect",
|
|
6674
|
+
projectPath,
|
|
6675
|
+
"--context",
|
|
6676
|
+
"sidebar",
|
|
6677
|
+
"--include",
|
|
6678
|
+
"summary",
|
|
6679
|
+
"--browser",
|
|
6680
|
+
browser,
|
|
6681
|
+
...null != timeout ? [
|
|
6682
|
+
"--timeout",
|
|
6683
|
+
String(timeout)
|
|
6684
|
+
] : []
|
|
6685
|
+
], projectPath, timeout, open_schema.name);
|
|
6686
|
+
let open = null;
|
|
6687
|
+
try {
|
|
6688
|
+
const parsed = JSON.parse(probe);
|
|
6689
|
+
if (parsed?.ok === true) open = parsed;
|
|
6690
|
+
} catch {}
|
|
6691
|
+
if (open) {
|
|
6692
|
+
const url = "string" == typeof open.value?.url ? open.value.url : "string" == typeof open.value?.meta?.url ? open.value.meta.url : void 0;
|
|
6693
|
+
return envelope_envelope({
|
|
6694
|
+
ok: true,
|
|
6695
|
+
command: open_schema.name,
|
|
6696
|
+
status: "already-open",
|
|
6697
|
+
value: {
|
|
6698
|
+
surface: "sidebar",
|
|
6699
|
+
document: doc,
|
|
6700
|
+
alreadyOpen: true,
|
|
6701
|
+
...url ? {
|
|
6702
|
+
url
|
|
6703
|
+
} : {}
|
|
6704
|
+
},
|
|
6705
|
+
hint: `The sidebar panel is open in the ${browser} window already: read it with extension_dom_snapshot context: 'sidebar' or run code in it with extension_eval context: 'sidebar'. ${GECKO_SIDEBAR_GESTURE}, and it did not need to.`
|
|
6706
|
+
});
|
|
6707
|
+
}
|
|
6708
|
+
const fallback = await openSurfaceAsTab(projectPath, browser, "sidebar");
|
|
6709
|
+
try {
|
|
6710
|
+
const parsedFallback = JSON.parse(fallback);
|
|
6711
|
+
if (parsedFallback?.ok) {
|
|
6712
|
+
addWarning(parsedFallback, `${GECKO_SIDEBAR_GESTURE}, and the panel is not open now, so the sidebar document was rendered as a tab instead. The DOM is the same document the panel would show; the panel hosting stays unverified. A person opens the real panel from the toolbar button or View > Sidebar.`);
|
|
6713
|
+
return actFrameJson(parsedFallback);
|
|
6714
|
+
}
|
|
6715
|
+
} catch {}
|
|
6716
|
+
refusal.error = {
|
|
6717
|
+
...refusal.error ?? {},
|
|
6718
|
+
message: `${GECKO_SIDEBAR_GESTURE}. Rendering the document ${doc} as a tab failed as well.`
|
|
6719
|
+
};
|
|
6720
|
+
refusal.hint = "Start the session with allowEval: true so the document can be opened by url through the bridge, or open the panel from the toolbar button in the dev browser and read it with extension_dom_snapshot context: 'sidebar'.";
|
|
6721
|
+
return actFrameJson(refusal);
|
|
6722
|
+
}
|
|
6723
|
+
const SIDEBAR_GESTURE_WARNING = "Chrome opens the side panel only from a user gesture and the engine's open verb carries none, so the server opened the extension's own sidebar page in a tab, dispatched a synthetic click on it over CDP and called chrome.sidePanel.open from inside that click, then closed the tab. The panel that opened is the real one; the toolbar wiring (action.onClicked or sidePanel.setPanelBehavior) was not exercised.";
|
|
6724
|
+
async function openSidebarThroughGesture(projectPath, browser, refusal) {
|
|
6725
|
+
const doc = surfaceDocument(projectPath, browser, "sidebar");
|
|
6726
|
+
const resolved = doc ? await resolveCdpPort(projectPath, browser) : null;
|
|
6727
|
+
const extensionId = resolved ? await resolveExtensionId(projectPath, browser) : null;
|
|
6728
|
+
let reason = "the sidebar document or the session's CDP port could not be resolved";
|
|
6729
|
+
if (doc && resolved && extensionId) {
|
|
6730
|
+
const hostUrl = `chrome-extension://${extensionId}/${doc}`;
|
|
6731
|
+
const outcome = await openSidePanelWithSyntheticGesture(resolved.port, hostUrl);
|
|
6732
|
+
if (outcome.opened) return envelope_envelope({
|
|
6733
|
+
ok: true,
|
|
6734
|
+
command: open_schema.name,
|
|
6735
|
+
status: "opened",
|
|
6736
|
+
value: {
|
|
6737
|
+
surface: "sidebar",
|
|
6738
|
+
gesture: "synthetic-click",
|
|
6739
|
+
surfaceTarget: {
|
|
6740
|
+
targetId: outcome.targetId,
|
|
6741
|
+
url: outcome.url
|
|
6742
|
+
}
|
|
6743
|
+
},
|
|
6744
|
+
warnings: [
|
|
6745
|
+
SIDEBAR_GESTURE_WARNING
|
|
6746
|
+
],
|
|
6747
|
+
hint: "Read the panel with extension_dom_snapshot context: 'sidebar' (include: ['html']) or run code in it with extension_eval context: 'sidebar'. To exercise the toolbar path itself, a person must click the toolbar icon in the dev browser."
|
|
6748
|
+
});
|
|
6749
|
+
reason = outcome.reason;
|
|
6750
|
+
}
|
|
6751
|
+
const fallback = await openSurfaceAsTab(projectPath, browser, "sidebar");
|
|
6752
|
+
try {
|
|
6753
|
+
const parsedFallback = JSON.parse(fallback);
|
|
6754
|
+
if (parsedFallback?.ok) {
|
|
6755
|
+
addWarning(parsedFallback, `Chrome opens the side panel only from a user gesture, the engine's open verb carries none, and the server's synthetic click did not open it either (${reason}), so the sidebar document was rendered as a tab instead. The DOM is the same React tree the panel would show; the panel hosting and the toolbar wiring stay unverified. Opening the real panel needs a toolbar click from a person in the dev browser.`);
|
|
6756
|
+
return actFrameJson(parsedFallback);
|
|
6757
|
+
}
|
|
6758
|
+
} catch {}
|
|
6759
|
+
refusal.hint = `Chrome opens the side panel only from a user gesture, which the engine's open verb cannot carry, and the server's synthetic click did not open it either (${reason}). ` + (doc && extensionId ? `To read the panel page anyway, call extension_open with url: "chrome-extension://${extensionId}/${doc}" and then extension_dom_snapshot context: 'sidebar' on it. ` : "To read the panel page anyway, open its document by url with extension_open and read it with extension_dom_snapshot context: 'sidebar'. ") + "Opening the real panel needs a toolbar click from a person in the dev browser.";
|
|
6760
|
+
return actFrameJson(refusal);
|
|
6761
|
+
}
|
|
6067
6762
|
function resolveToken() {
|
|
6068
6763
|
const fromEnv = String(process.env.EXTENSION_DEV_TOKEN || "").trim();
|
|
6069
6764
|
if (fromEnv) return fromEnv;
|
|
@@ -6348,11 +7043,11 @@ const IGNORED_SEGMENTS = new Set([
|
|
|
6348
7043
|
"__MACOSX"
|
|
6349
7044
|
]);
|
|
6350
7045
|
const IGNORED_FILES = /^\.env(\..*)?$|\.(pem|key|p12|pfx|keystore)$/i;
|
|
6351
|
-
const
|
|
7046
|
+
const preview_upload_TEXTUAL = /\.(json|js|mjs|cjs|ts|tsx|jsx|html|htm|css|svg|txt|md|map)$/i;
|
|
6352
7047
|
const MAX_FILES = 2000;
|
|
6353
7048
|
const MAX_CONTENT_CHARS = 67108864;
|
|
6354
7049
|
function encodeFile(relativePath, bytes) {
|
|
6355
|
-
if (
|
|
7050
|
+
if (preview_upload_TEXTUAL.test(relativePath)) {
|
|
6356
7051
|
const text = bytes.toString("utf8");
|
|
6357
7052
|
if (Buffer.from(text, "utf8").equals(bytes)) return {
|
|
6358
7053
|
content: text,
|
|
@@ -6518,6 +7213,7 @@ async function uploadPreview(options) {
|
|
|
6518
7213
|
};
|
|
6519
7214
|
return {
|
|
6520
7215
|
ok: true,
|
|
7216
|
+
body: data,
|
|
6521
7217
|
data: {
|
|
6522
7218
|
artifactId,
|
|
6523
7219
|
previewUrl,
|
|
@@ -6806,6 +7502,21 @@ function localLaneRemedy(checkout) {
|
|
|
6806
7502
|
const shareOut = "Pass share:true for a link that needs no local server and opens for anyone.";
|
|
6807
7503
|
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}`;
|
|
6808
7504
|
}
|
|
7505
|
+
function unrequestedShare(args) {
|
|
7506
|
+
const call = {
|
|
7507
|
+
projectPath: args.projectPath
|
|
7508
|
+
};
|
|
7509
|
+
if (args.browser) call.browser = args.browser;
|
|
7510
|
+
if (false === args.build) call.build = false;
|
|
7511
|
+
if (args.distPath) call.distPath = args.distPath;
|
|
7512
|
+
call.share = true;
|
|
7513
|
+
return {
|
|
7514
|
+
requested: false,
|
|
7515
|
+
localLinkNeeds: `deepLink renders only against a ${SURFACE.label} dev server running on this machine, so to anyone else it is a dead link.`,
|
|
7516
|
+
shareSpends: "share:true uploads this build to extension.dev's machines and counts against your free allowance, which is why it is off by default.",
|
|
7517
|
+
shareCall: `extension_preview_web ${JSON.stringify(call)}`
|
|
7518
|
+
};
|
|
7519
|
+
}
|
|
6809
7520
|
function previewOriginOf(previewUrl) {
|
|
6810
7521
|
try {
|
|
6811
7522
|
return new URL(previewUrl).origin;
|
|
@@ -6883,6 +7594,10 @@ async function buildShare(projectPath, distDir, manifest, browser, verifyInBrows
|
|
|
6883
7594
|
} : {},
|
|
6884
7595
|
serves: "uploaded-local-build",
|
|
6885
7596
|
localBuildUploaded: true,
|
|
7597
|
+
allowance: spendNarration({
|
|
7598
|
+
what: "This share upload",
|
|
7599
|
+
body: result.body
|
|
7600
|
+
}),
|
|
6886
7601
|
...browserCheck ? browserCheck.held ? {
|
|
6887
7602
|
browserLoadable: null,
|
|
6888
7603
|
heldFromPublic: true,
|
|
@@ -6959,7 +7674,7 @@ const preview_web_schema = {
|
|
|
6959
7674
|
share: {
|
|
6960
7675
|
type: "boolean",
|
|
6961
7676
|
default: false,
|
|
6962
|
-
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."
|
|
7677
|
+
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. Uploading runs on extension.dev's machines and counts against your free allowance; left false, the result's share property says what the local deepLink needs, what share:true spends, and the exact call to get a shareable link. 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."
|
|
6963
7678
|
}
|
|
6964
7679
|
},
|
|
6965
7680
|
required: [
|
|
@@ -7064,6 +7779,7 @@ async function preview_web_handler(args) {
|
|
|
7064
7779
|
built: false
|
|
7065
7780
|
}
|
|
7066
7781
|
};
|
|
7782
|
+
if (!args.share) result.share = unrequestedShare(args);
|
|
7067
7783
|
const checkout = previewDevCheckout([
|
|
7068
7784
|
args.projectPath,
|
|
7069
7785
|
process.cwd()
|
|
@@ -8112,6 +8828,81 @@ function verdictSentence(verdict) {
|
|
|
8112
8828
|
if (verdict.inconclusive.length > 0) parts.push(`${verdict.inconclusive.length} inconclusive, meaning this platform cannot cover the question today: not a pass, not a bug in the extension, and each one carries the evidence that would settle it in settledBy`);
|
|
8113
8829
|
return `${parts.join(", ")}.`;
|
|
8114
8830
|
}
|
|
8831
|
+
const execFileAsync = promisify(execFile);
|
|
8832
|
+
const SAFARI_MCP_SETTING = 'Safari > Settings > Developer > "Allow remote automation and external agents"';
|
|
8833
|
+
const SAFARI_MCP_ADD_COMMAND = 'claude mcp add safari-mcp -- "/usr/bin/safaridriver" --mcp';
|
|
8834
|
+
function parseSafariDriverHelp(helpText) {
|
|
8835
|
+
const flags = new Set(Array.from(helpText.matchAll(/(?:^|\s)--([a-z][a-z-]*)/g)).map((m)=>m[1]));
|
|
8836
|
+
return {
|
|
8837
|
+
mcp: flags.has("mcp"),
|
|
8838
|
+
bidi: flags.has("bidi")
|
|
8839
|
+
};
|
|
8840
|
+
}
|
|
8841
|
+
function safariDriverCandidates(safariBinary) {
|
|
8842
|
+
const candidates = [];
|
|
8843
|
+
if (safariBinary) candidates.push(node_path.join(node_path.dirname(safariBinary), "safaridriver"));
|
|
8844
|
+
candidates.push("/usr/bin/safaridriver");
|
|
8845
|
+
return Array.from(new Set(candidates));
|
|
8846
|
+
}
|
|
8847
|
+
async function readSafariDriverHelp(driverPath) {
|
|
8848
|
+
try {
|
|
8849
|
+
const { stdout, stderr } = await execFileAsync(driverPath, [
|
|
8850
|
+
"--help"
|
|
8851
|
+
], {
|
|
8852
|
+
timeout: 5000,
|
|
8853
|
+
env: {
|
|
8854
|
+
...process.env
|
|
8855
|
+
}
|
|
8856
|
+
});
|
|
8857
|
+
return `${stdout}\n${stderr}`;
|
|
8858
|
+
} catch (error) {
|
|
8859
|
+
const failed = error;
|
|
8860
|
+
const text = `${failed.stdout ?? ""}\n${failed.stderr ?? ""}`.trim();
|
|
8861
|
+
return text.length > 0 ? text : null;
|
|
8862
|
+
}
|
|
8863
|
+
}
|
|
8864
|
+
async function detectSafariAutomation(safariBinary) {
|
|
8865
|
+
if ("darwin" !== process.platform) return {
|
|
8866
|
+
safaridriver: null,
|
|
8867
|
+
mcp: false,
|
|
8868
|
+
bidi: false
|
|
8869
|
+
};
|
|
8870
|
+
for (const candidate of safariDriverCandidates(safariBinary)){
|
|
8871
|
+
if (!node_fs.existsSync(candidate)) continue;
|
|
8872
|
+
const help = await readSafariDriverHelp(candidate);
|
|
8873
|
+
if (null !== help) return {
|
|
8874
|
+
safaridriver: candidate,
|
|
8875
|
+
...parseSafariDriverHelp(help)
|
|
8876
|
+
};
|
|
8877
|
+
}
|
|
8878
|
+
return {
|
|
8879
|
+
safaridriver: null,
|
|
8880
|
+
mcp: false,
|
|
8881
|
+
bidi: false
|
|
8882
|
+
};
|
|
8883
|
+
}
|
|
8884
|
+
async function readSafariVersion(safariBinary) {
|
|
8885
|
+
const plist = node_path.resolve(safariBinary, "..", "..", "Info.plist");
|
|
8886
|
+
if (!node_fs.existsSync(plist)) return null;
|
|
8887
|
+
try {
|
|
8888
|
+
const { stdout } = await execFileAsync("/usr/bin/defaults", [
|
|
8889
|
+
"read",
|
|
8890
|
+
plist,
|
|
8891
|
+
"CFBundleShortVersionString"
|
|
8892
|
+
], {
|
|
8893
|
+
timeout: 5000
|
|
8894
|
+
});
|
|
8895
|
+
const version = stdout.trim();
|
|
8896
|
+
return /^\d+(\.\d+)*$/.test(version) ? version : null;
|
|
8897
|
+
} catch {
|
|
8898
|
+
return null;
|
|
8899
|
+
}
|
|
8900
|
+
}
|
|
8901
|
+
function safariAutomationHint(automation) {
|
|
8902
|
+
if (automation.mcp) return `Safari ships Apple's Safari MCP server (${automation.safaridriver} --mcp). Enable ${SAFARI_MCP_SETTING}, then add it beside this server: ${SAFARI_MCP_ADD_COMMAND}. It drives an isolated automation window with page-level tools (tabs, console, network, screenshots, evaluate); it has no extension-aware tool, so use it to read a page your content script touches, not the popup or background.`;
|
|
8903
|
+
if (automation.safaridriver) return `This safaridriver (${automation.safaridriver}) has no --mcp flag: Apple's Safari MCP server needs Safari 27 or Safari Technology Preview 247+. Until then a Safari session is build, open and enable only, with no console or DOM reading from this server.`;
|
|
8904
|
+
return "No safaridriver found beside Safari, so no automation reading is possible for it from this machine.";
|
|
8905
|
+
}
|
|
8115
8906
|
function makeFilter(args) {
|
|
8116
8907
|
const level = String(args.level || "all").toLowerCase();
|
|
8117
8908
|
const loggingOff = "off" === level;
|
|
@@ -8195,6 +8986,16 @@ async function environmentPreflight() {
|
|
|
8195
8986
|
status: cacheExists ? "pass" : "warn",
|
|
8196
8987
|
detail: cacheExists ? `Template catalog cached at ${cacheFile}` : "Template catalog not cached yet (extension_templates will fetch it)"
|
|
8197
8988
|
});
|
|
8989
|
+
if ("darwin" === process.platform) {
|
|
8990
|
+
const safariBinary = "/Applications/Safari.app/Contents/MacOS/Safari";
|
|
8991
|
+
const automation = await detectSafariAutomation(node_fs.existsSync(safariBinary) ? safariBinary : null);
|
|
8992
|
+
checks.push({
|
|
8993
|
+
check: "safari-agent",
|
|
8994
|
+
status: automation.mcp ? "pass" : "warn",
|
|
8995
|
+
detail: automation.mcp ? `Apple's Safari MCP server is available (${automation.safaridriver} --mcp)` : automation.safaridriver ? `${automation.safaridriver} has no --mcp flag, so Safari has no agent-readable window from this machine` : "No safaridriver found, so Safari has no agent-readable window from this machine",
|
|
8996
|
+
remediation: automation.mcp ? `Enable ${SAFARI_MCP_SETTING}, then add it beside this server: ${SAFARI_MCP_ADD_COMMAND}. It reads pages in an isolated automation window and has no extension-aware tool.` : "Install Safari 27 (Software Update) or Safari Technology Preview 247+ for Apple's Safari MCP server. A Safari dev session still builds, opens and guides the enable step without it."
|
|
8997
|
+
});
|
|
8998
|
+
}
|
|
8198
8999
|
const healthy = checks.every((c)=>"fail" !== c.status);
|
|
8199
9000
|
return envelope_envelope({
|
|
8200
9001
|
ok: healthy,
|
|
@@ -8247,6 +9048,25 @@ function capabilityProbeChecks(parsed) {
|
|
|
8247
9048
|
return value?.checks;
|
|
8248
9049
|
}
|
|
8249
9050
|
const CONTROL_OFF_BY_CHOICE = /\bwas not started with --allow-control\b/i;
|
|
9051
|
+
function pidIsAlive(pid) {
|
|
9052
|
+
try {
|
|
9053
|
+
process.kill(pid, 0);
|
|
9054
|
+
return true;
|
|
9055
|
+
} catch {
|
|
9056
|
+
return false;
|
|
9057
|
+
}
|
|
9058
|
+
}
|
|
9059
|
+
function reconcileRelaunchedBrowser(checks, contract) {
|
|
9060
|
+
const exitedLeg = checks.find((leg)=>"browser" === leg.check && "fail" === leg.status && /\bexited\b/i.test(String(leg.detail ?? "")));
|
|
9061
|
+
if (!exitedLeg) return false;
|
|
9062
|
+
const executorAnswered = checks.some((leg)=>("executor" === leg.check || "control-channel" === leg.check) && "pass" === leg.status);
|
|
9063
|
+
const browserAlive = "number" == typeof contract?.browserPid && pidIsAlive(contract.browserPid);
|
|
9064
|
+
if (!executorAnswered && !browserAlive) return false;
|
|
9065
|
+
exitedLeg.status = "warn";
|
|
9066
|
+
exitedLeg.detail = `${exitedLeg.detail ?? "browser exited"}. ${browserAlive ? `The browser pid the launcher recorded (${contract?.browserPid}) is alive` : "The executor answered a probe after that exit"}, so the session is live and the recorded exit was an earlier process: Firefox hands a fresh profile to a relaunched process and the first one exits 0.`;
|
|
9067
|
+
exitedLeg.remediation = "Nothing to do. If a later call finds the session unreachable, extension_stop and extension_dev again.";
|
|
9068
|
+
return true;
|
|
9069
|
+
}
|
|
8250
9070
|
function controlOffByChoiceLeg(checks) {
|
|
8251
9071
|
return checks.find((leg)=>"control-channel" === leg.check && "fail" === leg.status && "string" == typeof leg.detail && CONTROL_OFF_BY_CHOICE.test(leg.detail)) ?? null;
|
|
8252
9072
|
}
|
|
@@ -8283,23 +9103,15 @@ async function doctor_handler(args) {
|
|
|
8283
9103
|
if (!Array.isArray(probed)) throw new Error("not a check array");
|
|
8284
9104
|
const checks = probed;
|
|
8285
9105
|
const readOnlyLeg = controlOffByChoiceLeg(checks);
|
|
8286
|
-
for (const check of checks){
|
|
8287
|
-
if ("string" == typeof check.detail) check.detail = toMcpSpeak(check.detail);
|
|
8288
|
-
if ("string" == typeof check.remediation) check.remediation = toMcpSpeak(check.remediation);
|
|
8289
|
-
}
|
|
8290
|
-
let healthy = 0 === code;
|
|
8291
|
-
const contract = readContractForDiagnosis(projectPath, browser);
|
|
8292
|
-
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
const detail = browserExited ? `The ${browser} browser for this session exited unexpectedly${null != contract.browserExitCode ? ` (exit code ${contract.browserExitCode})` : ""}; the extension may have been rejected or the browser crashed. The session cannot be driven.` : contract.errors && contract.errors.length ? contract.errors.join("; ") : contract.message || "The dev session recorded status: error in ready.json.";
|
|
8296
|
-
checks.push({
|
|
8297
|
-
check: "runtime-errors",
|
|
8298
|
-
status: "fail",
|
|
8299
|
-
detail: toMcpSpeak(detail),
|
|
8300
|
-
remediation: browserExited ? "Read extension_logs and the session log for the rejection cause, call extension_stop to clean up, then relaunch." : "The build or extension load failed. Fix the reported error, let the dev server recompile, then re-run doctor."
|
|
8301
|
-
});
|
|
8302
|
-
} else {
|
|
9106
|
+
for (const check of checks){
|
|
9107
|
+
if ("string" == typeof check.detail) check.detail = toMcpSpeak(check.detail);
|
|
9108
|
+
if ("string" == typeof check.remediation) check.remediation = toMcpSpeak(check.remediation);
|
|
9109
|
+
}
|
|
9110
|
+
let healthy = 0 === code;
|
|
9111
|
+
const contract = readContractForDiagnosis(projectPath, browser);
|
|
9112
|
+
const relaunched = reconcileRelaunchedBrowser(checks, contract);
|
|
9113
|
+
if (relaunched) healthy = !checks.some((leg)=>"fail" === leg.status);
|
|
9114
|
+
if (contract?.status !== "error" || relaunched) {
|
|
8303
9115
|
const errs = recentErrorLogs(projectPath, browser);
|
|
8304
9116
|
if (errs.length) {
|
|
8305
9117
|
healthy = false;
|
|
@@ -8310,6 +9122,16 @@ async function doctor_handler(args) {
|
|
|
8310
9122
|
remediation: "The extension is throwing at runtime. Inspect with extension_logs. A chrome.* API called without its permission is a common cause: extension_manifest_validate catches a permission MISSING FROM permissions[], but it does not model host-permission scope (e.g. webRequest with no matching host_permissions) or gesture requirements (e.g. activeTab without a user gesture), so a valid:true there does not rule those out."
|
|
8311
9123
|
});
|
|
8312
9124
|
}
|
|
9125
|
+
} else {
|
|
9126
|
+
healthy = false;
|
|
9127
|
+
const browserExited = "browser_exited" === contract.code || void 0 !== contract.browserExitCode;
|
|
9128
|
+
const detail = browserExited ? `The ${browser} browser for this session exited unexpectedly${null != contract.browserExitCode ? ` (exit code ${contract.browserExitCode})` : ""}; the extension may have been rejected or the browser crashed. The session cannot be driven.` : contract.errors && contract.errors.length ? contract.errors.join("; ") : contract.message || "The dev session recorded status: error in ready.json.";
|
|
9129
|
+
checks.push({
|
|
9130
|
+
check: "runtime-errors",
|
|
9131
|
+
status: "fail",
|
|
9132
|
+
detail: toMcpSpeak(detail),
|
|
9133
|
+
remediation: browserExited ? "Read extension_logs and the session log for the rejection cause, call extension_stop to clean up, then relaunch." : "The build or extension load failed. Fix the reported error, let the dev server recompile, then re-run doctor."
|
|
9134
|
+
});
|
|
8313
9135
|
}
|
|
8314
9136
|
const engineVersion = projectEngineVersion(projectPath);
|
|
8315
9137
|
if (engineVersion) {
|
|
@@ -8324,6 +9146,19 @@ async function doctor_handler(args) {
|
|
|
8324
9146
|
} : {}
|
|
8325
9147
|
});
|
|
8326
9148
|
}
|
|
9149
|
+
if (WEBKIT_FAMILY.has(browser)) {
|
|
9150
|
+
const info = readWebDriverSession(projectPath, browser);
|
|
9151
|
+
const alive = info ? await new WebDriverClient(info).alive() : false;
|
|
9152
|
+
if (info && !alive) healthy = false;
|
|
9153
|
+
checks.push({
|
|
9154
|
+
check: "safari-window",
|
|
9155
|
+
status: info ? alive ? "pass" : "fail" : "skip",
|
|
9156
|
+
detail: info ? alive ? `Safari automation window recorded by the dev session (safaridriver on port ${info.port}, session ${info.sessionId})` : `ready.json records a Safari automation session on port ${info.port}, but it no longer answers: the window or the driver is gone` : "no safaridriver session recorded; page-world eval and open by url use the bridge, and everything else already does",
|
|
9157
|
+
...info && !alive ? {
|
|
9158
|
+
remediation: "Stop and restart extension_dev --browser=safari; the session that recorded the window opens it again on the first package."
|
|
9159
|
+
} : {}
|
|
9160
|
+
});
|
|
9161
|
+
}
|
|
8327
9162
|
const failures = checks.filter((leg)=>"fail" === leg.status);
|
|
8328
9163
|
const readOnly = null !== readOnlyLeg && 1 === failures.length && failures[0] === readOnlyLeg;
|
|
8329
9164
|
if (readOnly && readOnlyLeg) {
|
|
@@ -8740,6 +9575,16 @@ async function readFromStream(args, browser, limit) {
|
|
|
8740
9575
|
async function logs_handler(args) {
|
|
8741
9576
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
8742
9577
|
const limit = args.limit && args.limit > 0 ? args.limit : DEFAULT_LIMIT;
|
|
9578
|
+
if (WEBKIT_FAMILY.has(browser) && !node_fs.existsSync(logsPath(args.projectPath, browser))) return envelope_envelope({
|
|
9579
|
+
ok: false,
|
|
9580
|
+
command: TOOL,
|
|
9581
|
+
status: "no-log-file",
|
|
9582
|
+
error: {
|
|
9583
|
+
code: "E_LOGS_MISSING",
|
|
9584
|
+
message: `No logs found at ${logsPath(args.projectPath, browser)}, and ${browser} has no CDP or RDP to read instead.`
|
|
9585
|
+
},
|
|
9586
|
+
hint: "Safari logs arrive through the extension's bridge to the dev server, with background and content contexts, once the extension is enabled and a dev session from an Extension.js that streams Safari logs (4.1.28 or newer) is running. Start extension_dev --browser=safari, enable the extension in Safari > Settings > Extensions, then read again. The page a content script changes is also readable with extension_eval (context: 'page') or extension_assert content-script-injected."
|
|
9587
|
+
});
|
|
8743
9588
|
if (args.follow) return readFromStream(args, browser, limit);
|
|
8744
9589
|
return readFromFile(args, browser, limit);
|
|
8745
9590
|
}
|
|
@@ -8916,6 +9761,10 @@ function parseClauses(raw) {
|
|
|
8916
9761
|
}
|
|
8917
9762
|
const NO_SESSION_SETTLED_BY = "Start the session with extension_dev, confirm it with extension_wait, then assert again.";
|
|
8918
9763
|
class Stage {
|
|
9764
|
+
webdriver() {
|
|
9765
|
+
const info = readWebDriverSession(this.projectPath, this.browser);
|
|
9766
|
+
return info ? new WebDriverClient(info) : null;
|
|
9767
|
+
}
|
|
8919
9768
|
async port() {
|
|
8920
9769
|
if (void 0 === this.cdpPort) {
|
|
8921
9770
|
const resolved = await resolveCdpPort(this.projectPath, this.browser);
|
|
@@ -8983,6 +9832,7 @@ class Stage {
|
|
|
8983
9832
|
assert_define_property(this, "browser", void 0);
|
|
8984
9833
|
assert_define_property(this, "timeout", void 0);
|
|
8985
9834
|
assert_define_property(this, "chromium", void 0);
|
|
9835
|
+
assert_define_property(this, "webkit", void 0);
|
|
8986
9836
|
assert_define_property(this, "cdpPort", void 0);
|
|
8987
9837
|
assert_define_property(this, "discovered", void 0);
|
|
8988
9838
|
assert_define_property(this, "manifestRead", void 0);
|
|
@@ -8994,6 +9844,7 @@ class Stage {
|
|
|
8994
9844
|
this.discovered = null;
|
|
8995
9845
|
this.client = null;
|
|
8996
9846
|
this.chromium = isChromiumFamily(browser);
|
|
9847
|
+
this.webkit = WEBKIT_FAMILY.has(browser);
|
|
8997
9848
|
}
|
|
8998
9849
|
}
|
|
8999
9850
|
function truncate(value, max = 200) {
|
|
@@ -9125,12 +9976,77 @@ async function assertContentScriptInjected(clause, stage) {
|
|
|
9125
9976
|
runId,
|
|
9126
9977
|
coveringMatches: covering
|
|
9127
9978
|
});
|
|
9979
|
+
if (stage.webkit) return assertContentScriptInjectedOnWebKit(clause, stage, {
|
|
9980
|
+
covering,
|
|
9981
|
+
patterns
|
|
9982
|
+
});
|
|
9128
9983
|
return inconclusiveCheck(id, subject, covering.length > 0 ? `${covering.length} declared content_scripts match(es) cover ${clause.url} (${covering.join(", ")}), but nothing observable proves the script executed there: this platform cannot see into a content script's isolated world, and the content context logged nothing at that url in run ${runId || "(unnamed)"}.${stale ? ` ${stale}` : ""}` : `The built manifest (${read.file}) declares no content_scripts match covering ${clause.url}${patterns.length ? ` (declared: ${patterns.join(", ")})` : " and declares no content script at all"}. That is not proof of non-injection either: scripts registered at runtime with chrome.scripting.registerContentScripts are invisible to this reader.`, "Have the content script write one line, a console call or a dx.signal, and this check reads it from the log stream. To settle it now, read a marker the script sets with extension_eval (context: 'content', url: the page), which runs in the same isolated world the content script does.", {
|
|
9129
9984
|
coveringMatches: covering,
|
|
9130
9985
|
declaredMatches: patterns,
|
|
9131
9986
|
runId
|
|
9132
9987
|
});
|
|
9133
9988
|
}
|
|
9989
|
+
async function assertBackgroundOnWebKit(clause, stage) {
|
|
9990
|
+
const id = BACKGROUND;
|
|
9991
|
+
const subject = clause.subject;
|
|
9992
|
+
const runId = readLogRunId(stage.projectPath, stage.browser);
|
|
9993
|
+
const stale = staleFileNote(stage.projectPath, stage.browser, runId);
|
|
9994
|
+
const lines = readLogEvents(stage.projectPath, stage.browser, {
|
|
9995
|
+
context: [
|
|
9996
|
+
"background"
|
|
9997
|
+
]
|
|
9998
|
+
});
|
|
9999
|
+
if (lines.length > 0 && !stale) return passCheck(id, subject, `The background context wrote ${lines.length} log line(s) in run ${runId || "(unnamed)"} over the extension's bridge, and only a running background writes from that context.`, {
|
|
10000
|
+
lines: lines.length,
|
|
10001
|
+
runId
|
|
10002
|
+
});
|
|
10003
|
+
return inconclusiveCheck(id, subject, `${stage.browser} lists no background worker to this server, and the background context logged nothing in run ${runId || "(unnamed)"}${stale ? ` (${stale})` : ""}.`, "Have the background write one line on start (a console call), which reaches the dev session's log over the bridge once the extension is enabled; or open Web Inspector (Develop > Web Extension Background Content), attended.", {
|
|
10004
|
+
runId
|
|
10005
|
+
});
|
|
10006
|
+
}
|
|
10007
|
+
async function assertContentScriptInjectedOnWebKit(clause, stage, declared) {
|
|
10008
|
+
const id = CONTENT_SCRIPT;
|
|
10009
|
+
const subject = clause.subject;
|
|
10010
|
+
const client = stage.webdriver();
|
|
10011
|
+
if (!client) return inconclusiveCheck(id, subject, `The content context logged nothing at ${clause.url} in this run, and no safaridriver session is recorded for ${stage.browser}, so no page was read either.${declared.covering.length ? ` ${declared.covering.length} declared match(es) cover the url (${declared.covering.join(", ")}), which is not proof the script ran.` : ""}`, `Open ${clause.url} in Safari with the extension enabled, have the content script write one line (a console call), and assert again: the line reaches the dev session's log over the bridge. ${WEBDRIVER_SESSION_MISSING_HINT}`, {
|
|
10012
|
+
coveringMatches: declared.covering,
|
|
10013
|
+
declaredMatches: declared.patterns
|
|
10014
|
+
});
|
|
10015
|
+
let reading;
|
|
10016
|
+
try {
|
|
10017
|
+
const current = await client.currentUrl();
|
|
10018
|
+
if (!sameDocument(current, clause.url)) {
|
|
10019
|
+
await client.navigate(clause.url);
|
|
10020
|
+
await new Promise((resolve)=>setTimeout(resolve, 1200));
|
|
10021
|
+
}
|
|
10022
|
+
reading = await readExtensionRoots(client);
|
|
10023
|
+
} catch (error) {
|
|
10024
|
+
return inconclusiveCheck(id, subject, `The Safari automation window could not be read: ${error instanceof Error ? error.message : String(error)}.`, "Confirm the dev session still holds its window with extension_doctor, restart extension_dev --browser=safari if it ended, then assert again.", {
|
|
10025
|
+
coveringMatches: declared.covering,
|
|
10026
|
+
declaredMatches: declared.patterns
|
|
10027
|
+
});
|
|
10028
|
+
}
|
|
10029
|
+
const extensionId = readyExtensionId(stage.projectPath, stage.browser) ?? await stage.extensionId();
|
|
10030
|
+
if (!extensionId) return inconclusiveCheck(id, subject, `The Safari automation window shows ${reading.roots} extension root(s) at ${reading.url}, but this session recorded no extension id, so none of them can be attributed to this extension rather than another one.`, "Restart extension_dev --browser=safari on an Extension.js that stamps extensionId into ready.json, then assert again.", {
|
|
10031
|
+
roots: reading.roots,
|
|
10032
|
+
owners: reading.owners,
|
|
10033
|
+
url: reading.url
|
|
10034
|
+
});
|
|
10035
|
+
const owned = reading.owners.filter((owner)=>owner.length > 0 && owner.includes(extensionId));
|
|
10036
|
+
if (owned.length > 0) return passCheck(id, subject, `${owned.length} extension root(s) mounted by this extension's content script are in the DOM at ${reading.url}, read from the Safari automation window; only an injected content script mounts them.`, {
|
|
10037
|
+
roots: reading.roots,
|
|
10038
|
+
owners: owned,
|
|
10039
|
+
url: reading.url,
|
|
10040
|
+
coveringMatches: declared.covering
|
|
10041
|
+
});
|
|
10042
|
+
return inconclusiveCheck(id, subject, declared.covering.length > 0 ? `${declared.covering.length} declared content_scripts match(es) cover ${clause.url} (${declared.covering.join(", ")}), and the Safari automation window shows ${reading.roots} extension root(s) at ${reading.url}${reading.roots > 0 ? " none of which names this extension as owner" : ""}. A script that mounts no root leaves nothing this reader can see, and Safari carries no console feed over WebDriver to read a line instead.` : `The built manifest declares no content_scripts match covering ${clause.url}${declared.patterns.length ? ` (declared: ${declared.patterns.join(", ")})` : " and declares no content script at all"}, and the Safari automation window shows ${reading.roots} extension root(s) there.`, "Have the content script mount an element with data-extension-root (Extension.js stamps the owner on it), or set any DOM marker and read it with extension_eval (context: 'page', url: the page). A console line would need the WebDriver BiDi log domain, which this server does not speak yet.", {
|
|
10043
|
+
roots: reading.roots,
|
|
10044
|
+
owners: reading.owners,
|
|
10045
|
+
url: reading.url,
|
|
10046
|
+
coveringMatches: declared.covering,
|
|
10047
|
+
declaredMatches: declared.patterns
|
|
10048
|
+
});
|
|
10049
|
+
}
|
|
9134
10050
|
function readStorageValue(value, key) {
|
|
9135
10051
|
if (null == value) return {
|
|
9136
10052
|
shape: "absent"
|
|
@@ -9244,6 +10160,7 @@ function assertConsoleErrorsEmpty(clause, stage) {
|
|
|
9244
10160
|
});
|
|
9245
10161
|
}
|
|
9246
10162
|
async function evaluateClause(clause, stage) {
|
|
10163
|
+
if (stage.webkit && clause.assert === BACKGROUND) return assertBackgroundOnWebKit(clause, stage);
|
|
9247
10164
|
switch(clause.assert){
|
|
9248
10165
|
case BACKGROUND:
|
|
9249
10166
|
return assertBackgroundWorker(clause, stage);
|
|
@@ -11170,6 +12087,32 @@ const inspect_schema_schema = {
|
|
|
11170
12087
|
]
|
|
11171
12088
|
}
|
|
11172
12089
|
};
|
|
12090
|
+
const EXTENSION_PAGE_CONTEXTS = [
|
|
12091
|
+
"popup",
|
|
12092
|
+
"options",
|
|
12093
|
+
"sidebar",
|
|
12094
|
+
"newtab",
|
|
12095
|
+
"history",
|
|
12096
|
+
"bookmarks"
|
|
12097
|
+
];
|
|
12098
|
+
const EXTENSION_ORIGIN = /^(moz|chrome|safari-web)-extension:\/\//;
|
|
12099
|
+
function isExtensionUrl(url) {
|
|
12100
|
+
return "string" == typeof url && EXTENSION_ORIGIN.test(url);
|
|
12101
|
+
}
|
|
12102
|
+
function surfaceForExtensionUrl(projectPath, browser, url) {
|
|
12103
|
+
const bare = url.replace(EXTENSION_ORIGIN, "").replace(/^[^/]*\//, (m)=>EXTENSION_ORIGIN.test(url) ? "" : m).replace(/^\.?\//, "").replace(/[?#].*$/, "");
|
|
12104
|
+
if (!bare) return null;
|
|
12105
|
+
for (const context of EXTENSION_PAGE_CONTEXTS){
|
|
12106
|
+
const document = surfaceDocument(projectPath, browser, context);
|
|
12107
|
+
if (document) {
|
|
12108
|
+
if (bare === document || bare.endsWith(`/${document}`) || document.endsWith(`/${bare}`)) return {
|
|
12109
|
+
context,
|
|
12110
|
+
document
|
|
12111
|
+
};
|
|
12112
|
+
}
|
|
12113
|
+
}
|
|
12114
|
+
return null;
|
|
12115
|
+
}
|
|
11173
12116
|
const inspect_gecko_TOOL = inspect_schema_schema.name;
|
|
11174
12117
|
function buildBridgeInspectExpression(opts) {
|
|
11175
12118
|
const parts = [
|
|
@@ -11287,7 +12230,22 @@ async function collectGeckoConsole(args, browser, urlFilter, result, notes) {
|
|
|
11287
12230
|
}
|
|
11288
12231
|
async function inspectViaBridge(args, browser, include, maxBytes) {
|
|
11289
12232
|
const notes = [];
|
|
11290
|
-
|
|
12233
|
+
const surface = args.url ? surfaceForExtensionUrl(args.projectPath, browser, args.url) : null;
|
|
12234
|
+
if (args.url && !surface && EXTENSION_ORIGIN.test(args.url)) {
|
|
12235
|
+
const declared = declaredSurfaces(args.projectPath, browser) ?? [];
|
|
12236
|
+
return envelope_envelope({
|
|
12237
|
+
ok: false,
|
|
12238
|
+
command: inspect_gecko_TOOL,
|
|
12239
|
+
status: "no-surface",
|
|
12240
|
+
error: {
|
|
12241
|
+
code: "E_NO_SURFACE_DOCUMENT",
|
|
12242
|
+
name: "NoSurfaceDocument",
|
|
12243
|
+
message: `${args.url} is a page inside the extension, and on ${browser} a page inside the extension is read through its own surface relay, but that path matches none of the surface documents the manifest declares${declared.length ? ` (${declared.join(", ")})` : ""}. Script injection cannot reach an extension page at all.`
|
|
12244
|
+
},
|
|
12245
|
+
hint: declared.length ? `Pass the declared document path (extension_open surface: "${declared[0]}" opens one), or read it with extension_dom_snapshot context: "${declared[0]}".` : "Declare the page as a surface in the manifest (action.default_popup, options_ui.page, sidebar_action.default_panel or chrome_url_overrides) and rebuild."
|
|
12246
|
+
});
|
|
12247
|
+
}
|
|
12248
|
+
if (args.url && !surface) {
|
|
11291
12249
|
const listed = await listBridgeTabs(args.projectPath, browser, args.timeout, inspect_gecko_TOOL);
|
|
11292
12250
|
if ("error" in listed) return listed.error;
|
|
11293
12251
|
const already = listed.tabs.some((t)=>t.url.includes(args.url));
|
|
@@ -11314,8 +12272,8 @@ async function inspectViaBridge(args, browser, include, maxBytes) {
|
|
|
11314
12272
|
expression,
|
|
11315
12273
|
args.projectPath,
|
|
11316
12274
|
"--context",
|
|
11317
|
-
"page",
|
|
11318
|
-
...args.url ? [
|
|
12275
|
+
surface ? surface.context : "page",
|
|
12276
|
+
...args.url && !surface ? [
|
|
11319
12277
|
"--url",
|
|
11320
12278
|
args.url
|
|
11321
12279
|
] : [],
|
|
@@ -11369,7 +12327,11 @@ async function inspectViaBridge(args, browser, include, maxBytes) {
|
|
|
11369
12327
|
if (null === value) return raw;
|
|
11370
12328
|
const result = {
|
|
11371
12329
|
browser,
|
|
11372
|
-
transport: "bridge"
|
|
12330
|
+
transport: "bridge",
|
|
12331
|
+
...surface ? {
|
|
12332
|
+
surface: surface.context,
|
|
12333
|
+
document: surface.document
|
|
12334
|
+
} : {}
|
|
11373
12335
|
};
|
|
11374
12336
|
if (value.meta) {
|
|
11375
12337
|
result.target = {
|
|
@@ -11845,9 +12807,72 @@ async function listGeckoExtensions(projectPath, browser) {
|
|
|
11845
12807
|
});
|
|
11846
12808
|
}
|
|
11847
12809
|
}
|
|
12810
|
+
const RELAY_MARK = "__extensionDevRelay";
|
|
12811
|
+
function cloneHelper() {
|
|
12812
|
+
return `var clone = function (v) {
|
|
12813
|
+
if (v === undefined) return null;
|
|
12814
|
+
try {
|
|
12815
|
+
if (typeof structuredClone === "function") { structuredClone(v); return v; }
|
|
12816
|
+
return JSON.parse(JSON.stringify(v));
|
|
12817
|
+
} catch (e) { return String(v); }
|
|
12818
|
+
};`;
|
|
12819
|
+
}
|
|
12820
|
+
function relaySafeExpression(expression, token) {
|
|
12821
|
+
const src = JSON.stringify(expression);
|
|
12822
|
+
const key = JSON.stringify(token);
|
|
12823
|
+
return `(function () {
|
|
12824
|
+
${cloneHelper()}
|
|
12825
|
+
var value = (0, eval)(${src});
|
|
12826
|
+
if (value && typeof value.then === "function") {
|
|
12827
|
+
var store = globalThis.${RELAY_MARK} = globalThis.${RELAY_MARK} || {};
|
|
12828
|
+
store[${key}] = { done: false };
|
|
12829
|
+
Promise.resolve(value).then(function (settled) {
|
|
12830
|
+
store[${key}] = { done: true, ok: true, value: clone(settled) };
|
|
12831
|
+
}, function (error) {
|
|
12832
|
+
store[${key}] = { done: true, ok: false, name: (error && error.name) || "EvalError", message: (error && error.message) || String(error) };
|
|
12833
|
+
});
|
|
12834
|
+
return { ${RELAY_MARK}: 1, done: false, token: ${key} };
|
|
12835
|
+
}
|
|
12836
|
+
return { ${RELAY_MARK}: 1, done: true, ok: true, value: clone(value) };
|
|
12837
|
+
})()`;
|
|
12838
|
+
}
|
|
12839
|
+
function relayPollExpression(token) {
|
|
12840
|
+
const key = JSON.stringify(token);
|
|
12841
|
+
return `(function () {
|
|
12842
|
+
var store = globalThis.${RELAY_MARK};
|
|
12843
|
+
var entry = store && store[${key}];
|
|
12844
|
+
if (!entry) return { ${RELAY_MARK}: 1, done: true, ok: false, name: "RelayLost", message: "the page reloaded or navigated before the expression settled, so its result is gone" };
|
|
12845
|
+
if (!entry.done) return { ${RELAY_MARK}: 1, done: false, token: ${key} };
|
|
12846
|
+
delete store[${key}];
|
|
12847
|
+
return { ${RELAY_MARK}: 1, done: true, ok: entry.ok, value: entry.value, name: entry.name, message: entry.message };
|
|
12848
|
+
})()`;
|
|
12849
|
+
}
|
|
12850
|
+
function readRelayFrame(value) {
|
|
12851
|
+
if (!value || "object" != typeof value || Array.isArray(value)) return null;
|
|
12852
|
+
const frame = value;
|
|
12853
|
+
if (1 !== frame[RELAY_MARK] || "boolean" != typeof frame.done) return null;
|
|
12854
|
+
return {
|
|
12855
|
+
done: frame.done,
|
|
12856
|
+
..."boolean" == typeof frame.ok ? {
|
|
12857
|
+
ok: frame.ok
|
|
12858
|
+
} : {},
|
|
12859
|
+
..."value" in frame ? {
|
|
12860
|
+
value: frame.value
|
|
12861
|
+
} : {},
|
|
12862
|
+
..."string" == typeof frame.name ? {
|
|
12863
|
+
name: frame.name
|
|
12864
|
+
} : {},
|
|
12865
|
+
..."string" == typeof frame.message ? {
|
|
12866
|
+
message: frame.message
|
|
12867
|
+
} : {},
|
|
12868
|
+
..."string" == typeof frame.token ? {
|
|
12869
|
+
token: frame.token
|
|
12870
|
+
} : {}
|
|
12871
|
+
};
|
|
12872
|
+
}
|
|
11848
12873
|
const eval_schema = {
|
|
11849
12874
|
name: "extension_eval",
|
|
11850
|
-
description: "Evaluate an expression in a running extension context. Start the session with allowEval:true (extension_dev), which writes a 0600 session token. Context defaults to 'background', except on a Chromium MV3 session (the default template) where it defaults to 'page', the active tab, because the MV3 service worker CSP blocks eval; pass context:'background' to target the worker anyway and get that explanation back. For content and page, pass `url` to pick the tab, or omit both `url` and `tab` for the active tab; a numeric `tab` only disambiguates. Extension surfaces (popup, options, sidebar, devtools) and override pages
|
|
12875
|
+
description: "Evaluate an expression in a running extension context. Start the session with allowEval:true (extension_dev), which writes a 0600 session token. Context defaults to 'background', except on a Chromium MV3 session (the default template) where it defaults to 'page', the active tab, because the MV3 service worker CSP blocks eval; pass context:'background' to target the worker anyway and get that explanation back. For content and page, pass `url` to pick the tab, or omit both `url` and `tab` for the active tab; a numeric `tab` only disambiguates. Extension surfaces (popup, options, sidebar, devtools) and override pages (newtab, history, bookmarks) need no tab id but must already be open: open one with extension_open first, because a closed one returns an explicit error. On a Chromium MV3 session those pages, and context:'page' with a chrome-extension:// url, evaluate over CDP, the inspector path the extension page CSP does not govern; elsewhere they evaluate over the in-bundle relay. Call extension_dom_snapshot with listTabs:true to enumerate {tabId, url, title}.",
|
|
11851
12876
|
inputSchema: {
|
|
11852
12877
|
type: "object",
|
|
11853
12878
|
properties: {
|
|
@@ -11889,15 +12914,8 @@ const eval_schema = {
|
|
|
11889
12914
|
]
|
|
11890
12915
|
}
|
|
11891
12916
|
};
|
|
11892
|
-
function
|
|
11893
|
-
|
|
11894
|
-
const candidates = [
|
|
11895
|
-
node_path.join(projectPath, "dist", browser, "manifest.json"),
|
|
11896
|
-
node_path.join(projectPath, "dist", "manifest.json"),
|
|
11897
|
-
node_path.join(projectPath, "src", "manifest.json"),
|
|
11898
|
-
node_path.join(projectPath, "manifest.json")
|
|
11899
|
-
];
|
|
11900
|
-
for (const file of candidates){
|
|
12917
|
+
function chromiumManifestVersion(projectPath, browser) {
|
|
12918
|
+
for (const file of manifestCandidates(projectPath, browser)){
|
|
11901
12919
|
let manifest;
|
|
11902
12920
|
try {
|
|
11903
12921
|
manifest = JSON.parse(node_fs.readFileSync(file, "utf8"));
|
|
@@ -11905,15 +12923,247 @@ function resolveDefaultEvalContext(projectPath, browser) {
|
|
|
11905
12923
|
continue;
|
|
11906
12924
|
}
|
|
11907
12925
|
const version = manifest["chromium:manifest_version"] ?? manifest.manifest_version;
|
|
11908
|
-
if (3 === version) return
|
|
11909
|
-
if (2 === version)
|
|
12926
|
+
if (3 === version) return 3;
|
|
12927
|
+
if (2 === version) return 2;
|
|
12928
|
+
}
|
|
12929
|
+
return null;
|
|
12930
|
+
}
|
|
12931
|
+
function resolveDefaultEvalContext(projectPath, browser) {
|
|
12932
|
+
if (!isChromiumFamily(browser)) return "background";
|
|
12933
|
+
return 3 === chromiumManifestVersion(projectPath, browser) ? "page" : "background";
|
|
12934
|
+
}
|
|
12935
|
+
function wantsExtensionPageOverCdp(projectPath, browser, context, url) {
|
|
12936
|
+
if (!isChromiumFamily(browser) || !context) return false;
|
|
12937
|
+
if ("page" === context) return "string" == typeof url && /^chrome-extension:\/\//.test(url);
|
|
12938
|
+
return EXTENSION_PAGE_CONTEXTS.includes(context) && 3 === chromiumManifestVersion(projectPath, browser);
|
|
12939
|
+
}
|
|
12940
|
+
async function evaluateOnChromiumExtensionPage(args, browser, context) {
|
|
12941
|
+
const resolved = await resolveCdpPort(args.projectPath, browser);
|
|
12942
|
+
if (!resolved) return envelope_envelope({
|
|
12943
|
+
ok: false,
|
|
12944
|
+
command: eval_schema.name,
|
|
12945
|
+
status: "no-session",
|
|
12946
|
+
error: {
|
|
12947
|
+
code: "E_NO_SESSION",
|
|
12948
|
+
name: "NoSession",
|
|
12949
|
+
message: `No active dev session / CDP port for ${browser}, and an extension page evaluates over CDP. Start extension_dev with allowEval: true and extension_wait for ready. ${CDP_PORT_MISSING_HINT}`
|
|
12950
|
+
}
|
|
12951
|
+
});
|
|
12952
|
+
let wanted;
|
|
12953
|
+
if ("page" === context) wanted = args.url;
|
|
12954
|
+
else {
|
|
12955
|
+
const doc = surfaceDocument(args.projectPath, browser, context);
|
|
12956
|
+
if (!doc) {
|
|
12957
|
+
const key = SURFACE_MANIFEST_KEYS[context] ?? context;
|
|
12958
|
+
return envelope_envelope({
|
|
12959
|
+
ok: false,
|
|
12960
|
+
command: eval_schema.name,
|
|
12961
|
+
status: "no-surface",
|
|
12962
|
+
error: {
|
|
12963
|
+
code: "E_NO_SURFACE_DOCUMENT",
|
|
12964
|
+
name: "NoSurfaceDocument",
|
|
12965
|
+
message: `This extension declares no ${context}: nothing in its manifest sets ${key}, so there is no ${context} page to evaluate in.`
|
|
12966
|
+
},
|
|
12967
|
+
hint: `To add one, set ${key} in the manifest and rebuild.`
|
|
12968
|
+
});
|
|
12969
|
+
}
|
|
12970
|
+
const extensionId = await resolveExtensionId(args.projectPath, browser);
|
|
12971
|
+
if (!extensionId) return envelope_envelope({
|
|
12972
|
+
ok: false,
|
|
12973
|
+
command: eval_schema.name,
|
|
12974
|
+
status: "no-extension-id",
|
|
12975
|
+
error: {
|
|
12976
|
+
code: "E_NO_EXTENSION_ID",
|
|
12977
|
+
name: "NoExtensionId",
|
|
12978
|
+
message: "Could not resolve the extension id from the live session's CDP targets."
|
|
12979
|
+
},
|
|
12980
|
+
hint: `Confirm the session is ready (extension_wait). ${CDP_PORT_MISSING_HINT}`
|
|
12981
|
+
});
|
|
12982
|
+
wanted = `chrome-extension://${extensionId}/${doc}`;
|
|
11910
12983
|
}
|
|
11911
|
-
|
|
12984
|
+
const targets = await findExtensionPageTargets(resolved.port, wanted);
|
|
12985
|
+
if (0 === targets.length) return envelope_envelope({
|
|
12986
|
+
ok: false,
|
|
12987
|
+
command: eval_schema.name,
|
|
12988
|
+
status: "no-target",
|
|
12989
|
+
error: {
|
|
12990
|
+
code: "E_NO_TARGET",
|
|
12991
|
+
name: "NoTarget",
|
|
12992
|
+
message: `No open page at ${wanted}, so there is nothing to evaluate in. An extension page evaluates over CDP on its own target, which exists only while the page is open.`
|
|
12993
|
+
},
|
|
12994
|
+
hint: "page" === context ? "Open it first with extension_open (url: this address), then retry. extension_dom_snapshot with listTargets: true lists what is open." : `Open it first with extension_open surface: "${context}", then retry. extension_dom_snapshot with listTargets: true lists what is open.`
|
|
12995
|
+
});
|
|
12996
|
+
const target = targets[0];
|
|
12997
|
+
const outcome = await evaluateOnExtensionPage(resolved.port, target.targetId, args.expression);
|
|
12998
|
+
const others = targets.slice(1);
|
|
12999
|
+
const warnings = others.length ? [
|
|
13000
|
+
`${targets.length} open pages match ${wanted}; evaluated in target ${target.targetId} (${target.url}). The others: ${others.map((t)=>t.targetId).join(", ")}. Close the copies you do not mean, or navigate away from them.`
|
|
13001
|
+
] : [];
|
|
13002
|
+
if (!outcome.ok) return envelope_envelope({
|
|
13003
|
+
ok: false,
|
|
13004
|
+
command: eval_schema.name,
|
|
13005
|
+
status: outcome.thrown ? "eval-failed" : "cdp-failed",
|
|
13006
|
+
error: {
|
|
13007
|
+
code: outcome.thrown ? "E_EVAL" : "E_CDP",
|
|
13008
|
+
name: outcome.thrown ? "EvalError" : "CdpError",
|
|
13009
|
+
message: outcome.message
|
|
13010
|
+
},
|
|
13011
|
+
warnings,
|
|
13012
|
+
hint: outcome.thrown ? `The expression threw inside ${target.url}. It ran over CDP in the page's main world with extension APIs available; a promise is awaited, so an async expression can be returned directly.` : "The session's debug port refused the call or the target went away. extension_doctor names which; a session that ended needs extension_dev again."
|
|
13013
|
+
});
|
|
13014
|
+
return envelope_envelope({
|
|
13015
|
+
ok: true,
|
|
13016
|
+
command: eval_schema.name,
|
|
13017
|
+
status: "evaluated",
|
|
13018
|
+
value: outcome.value,
|
|
13019
|
+
warnings,
|
|
13020
|
+
hint: `Evaluated over CDP in ${target.url} (target ${target.targetId}), the inspector path the extension page CSP does not govern. The result is serialized by value, so return plain data rather than DOM nodes; a promise is awaited before returning.`
|
|
13021
|
+
});
|
|
13022
|
+
}
|
|
13023
|
+
async function evaluateOnWebKitPage(args, browser, info) {
|
|
13024
|
+
const client = new WebDriverClient(info);
|
|
13025
|
+
if (args.url) {
|
|
13026
|
+
const current = await client.currentUrl().catch(()=>null);
|
|
13027
|
+
if (!current || !current.startsWith(args.url)) await client.navigate(args.url);
|
|
13028
|
+
}
|
|
13029
|
+
try {
|
|
13030
|
+
const value = await client.execute(`return (${args.expression});`);
|
|
13031
|
+
return envelope_envelope({
|
|
13032
|
+
ok: true,
|
|
13033
|
+
command: eval_schema.name,
|
|
13034
|
+
status: "evaluated",
|
|
13035
|
+
value: {
|
|
13036
|
+
context: "page",
|
|
13037
|
+
browser,
|
|
13038
|
+
url: await client.currentUrl().catch(()=>null),
|
|
13039
|
+
result: value
|
|
13040
|
+
},
|
|
13041
|
+
hint: "Evaluated in the Safari automation window's main world over the dev session's WebDriver connection."
|
|
13042
|
+
});
|
|
13043
|
+
} catch (error) {
|
|
13044
|
+
return envelope_envelope({
|
|
13045
|
+
ok: false,
|
|
13046
|
+
command: eval_schema.name,
|
|
13047
|
+
status: "eval-failed",
|
|
13048
|
+
error: {
|
|
13049
|
+
code: "E_EVAL",
|
|
13050
|
+
name: "EvalError",
|
|
13051
|
+
message: error instanceof Error ? error.message : String(error)
|
|
13052
|
+
},
|
|
13053
|
+
hint: "The expression threw, or the automation window is gone. extension_doctor names which; a session that ended needs extension_dev again."
|
|
13054
|
+
});
|
|
13055
|
+
}
|
|
13056
|
+
}
|
|
13057
|
+
const RELAY_POLL_MS = 300;
|
|
13058
|
+
const RELAY_DEFAULT_BUDGET_MS = 30000;
|
|
13059
|
+
function tryParseFrame(raw) {
|
|
13060
|
+
try {
|
|
13061
|
+
const parsed = JSON.parse(raw);
|
|
13062
|
+
return parsed && "object" == typeof parsed ? parsed : null;
|
|
13063
|
+
} catch {
|
|
13064
|
+
return null;
|
|
13065
|
+
}
|
|
13066
|
+
}
|
|
13067
|
+
async function evaluateThroughRelay(args, browser, context) {
|
|
13068
|
+
const token = node_crypto.randomUUID();
|
|
13069
|
+
const budgetMs = args.timeout ?? RELAY_DEFAULT_BUDGET_MS;
|
|
13070
|
+
const deadline = Date.now() + budgetMs;
|
|
13071
|
+
const run = (expression)=>runActVerb([
|
|
13072
|
+
"eval",
|
|
13073
|
+
...commonFlags({
|
|
13074
|
+
...args,
|
|
13075
|
+
context,
|
|
13076
|
+
browser
|
|
13077
|
+
}),
|
|
13078
|
+
"--",
|
|
13079
|
+
expression,
|
|
13080
|
+
args.projectPath
|
|
13081
|
+
], args.projectPath, args.timeout, eval_schema.name);
|
|
13082
|
+
let raw = await run(relaySafeExpression(args.expression, token));
|
|
13083
|
+
let parsed = tryParseFrame(raw);
|
|
13084
|
+
if (!parsed || true !== parsed.ok) return raw;
|
|
13085
|
+
let frame = readRelayFrame(parsed.value);
|
|
13086
|
+
if (!frame) return raw;
|
|
13087
|
+
let polls = 0;
|
|
13088
|
+
while(!frame.done){
|
|
13089
|
+
if (Date.now() >= deadline) return envelope_envelope({
|
|
13090
|
+
ok: false,
|
|
13091
|
+
command: eval_schema.name,
|
|
13092
|
+
status: "eval-pending",
|
|
13093
|
+
error: {
|
|
13094
|
+
code: "E_WAIT_TIMEOUT",
|
|
13095
|
+
name: "EvalPending",
|
|
13096
|
+
message: `The expression returned a promise that had not settled after ${budgetMs} ms; it is still running in the ${context} page.`
|
|
13097
|
+
},
|
|
13098
|
+
value: {
|
|
13099
|
+
context,
|
|
13100
|
+
token,
|
|
13101
|
+
polls
|
|
13102
|
+
},
|
|
13103
|
+
hint: `Its outcome lands in globalThis.${RELAY_MARK}[${JSON.stringify(token)}] inside the ${context} page when it settles ({done, ok, value}): read it with extension_eval in the same context, or pass a larger timeout to wait here.`
|
|
13104
|
+
});
|
|
13105
|
+
await new Promise((r)=>setTimeout(r, RELAY_POLL_MS));
|
|
13106
|
+
polls += 1;
|
|
13107
|
+
raw = await run(relayPollExpression(token));
|
|
13108
|
+
parsed = tryParseFrame(raw);
|
|
13109
|
+
if (!parsed || true !== parsed.ok) return raw;
|
|
13110
|
+
const next = readRelayFrame(parsed.value);
|
|
13111
|
+
if (!next) return raw;
|
|
13112
|
+
frame = next;
|
|
13113
|
+
}
|
|
13114
|
+
if (false === frame.ok) return envelope_envelope({
|
|
13115
|
+
ok: false,
|
|
13116
|
+
command: eval_schema.name,
|
|
13117
|
+
status: "eval-failed",
|
|
13118
|
+
error: {
|
|
13119
|
+
code: "E_EVAL",
|
|
13120
|
+
name: frame.name || "EvalError",
|
|
13121
|
+
message: frame.message || "the expression rejected"
|
|
13122
|
+
},
|
|
13123
|
+
hint: `The expression threw, or the promise it returned rejected, inside the ${context} page.`
|
|
13124
|
+
});
|
|
13125
|
+
parsed.value = void 0 === frame.value ? null : frame.value;
|
|
13126
|
+
if (polls > 0) parsed.hint = `The expression returned a promise; the ${context} page settled it and this call polled ${polls} time${1 === polls ? "" : "s"} for the result. ` + ("string" == typeof parsed.hint ? parsed.hint : "");
|
|
13127
|
+
return actFrameJson(parsed);
|
|
11912
13128
|
}
|
|
11913
13129
|
async function eval_handler(args) {
|
|
11914
13130
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
13131
|
+
if (WEBKIT_FAMILY.has(browser) && "page" === args.context) {
|
|
13132
|
+
const info = readWebDriverSession(args.projectPath, browser);
|
|
13133
|
+
if (info) return evaluateOnWebKitPage(args, browser, info);
|
|
13134
|
+
}
|
|
11915
13135
|
const defaulted = !args.context && "page" === resolveDefaultEvalContext(args.projectPath, browser);
|
|
11916
13136
|
const context = defaulted ? "page" : args.context;
|
|
13137
|
+
if (wantsExtensionPageOverCdp(args.projectPath, browser, context, args.url)) return evaluateOnChromiumExtensionPage(args, browser, context);
|
|
13138
|
+
if ("page" === context && isExtensionUrl(args.url)) {
|
|
13139
|
+
const surface = surfaceForExtensionUrl(args.projectPath, browser, args.url);
|
|
13140
|
+
if (surface) {
|
|
13141
|
+
const raw = await evaluateThroughRelay({
|
|
13142
|
+
...args,
|
|
13143
|
+
url: void 0,
|
|
13144
|
+
tab: void 0
|
|
13145
|
+
}, browser, surface.context);
|
|
13146
|
+
const parsed = tryParseFrame(raw);
|
|
13147
|
+
if (parsed) {
|
|
13148
|
+
addWarning(parsed, `${args.url} is the extension's own ${surface.context} document (${surface.document}), which script injection cannot reach on any engine, so this evaluated through the ${surface.context} surface relay. Pass context: "${surface.context}" directly next time.`);
|
|
13149
|
+
return actFrameJson(parsed);
|
|
13150
|
+
}
|
|
13151
|
+
return raw;
|
|
13152
|
+
}
|
|
13153
|
+
const declared = declaredSurfaces(args.projectPath, browser) ?? [];
|
|
13154
|
+
return envelope_envelope({
|
|
13155
|
+
ok: false,
|
|
13156
|
+
command: eval_schema.name,
|
|
13157
|
+
status: "no-surface",
|
|
13158
|
+
error: {
|
|
13159
|
+
code: "E_NO_SURFACE_DOCUMENT",
|
|
13160
|
+
name: "NoSurfaceDocument",
|
|
13161
|
+
message: `${args.url} is a page inside the extension, which script injection cannot reach, and it matches none of the surface documents the manifest declares${declared.length ? ` (${declared.join(", ")})` : ""}.`
|
|
13162
|
+
},
|
|
13163
|
+
hint: declared.length ? `Evaluate in a declared surface with context: "${declared[0]}" (open it first with extension_open), or read a web page by url.` : "Declare the page as a surface in the manifest (action.default_popup, options_ui.page, sidebar_action.default_panel or chrome_url_overrides) and rebuild."
|
|
13164
|
+
});
|
|
13165
|
+
}
|
|
13166
|
+
if (context && EXTENSION_PAGE_CONTEXTS.includes(context)) return evaluateThroughRelay(args, browser, context);
|
|
11917
13167
|
const raw = await runActVerb([
|
|
11918
13168
|
"eval",
|
|
11919
13169
|
...commonFlags({
|
|
@@ -12075,24 +13325,6 @@ async function reload_handler(args) {
|
|
|
12075
13325
|
})
|
|
12076
13326
|
], args.projectPath, args.timeout, reload_schema.name);
|
|
12077
13327
|
}
|
|
12078
|
-
const TARGET_ID_NOTE = "targetId is a CDP target id, NOT a chrome.tabs id: do not pass it as `tab`. Target a tab with `tabUrl` (URL substring) or `url`; if you need a numeric tab id, call extension_dom_snapshot with listTabs: true.";
|
|
12079
|
-
function filterPageTargets(raw) {
|
|
12080
|
-
return raw.filter((t)=>"page" === t.type && !String(t.url ?? "").startsWith("devtools://")).map((t)=>({
|
|
12081
|
-
targetId: String(t.id),
|
|
12082
|
-
type: String(t.type),
|
|
12083
|
-
url: String(t.url ?? ""),
|
|
12084
|
-
title: String(t.title ?? "")
|
|
12085
|
-
}));
|
|
12086
|
-
}
|
|
12087
|
-
async function listPageTargets(port) {
|
|
12088
|
-
return filterPageTargets(await CDPClient.discoverTargets(port));
|
|
12089
|
-
}
|
|
12090
|
-
function matchTargetsByUrl(targets, needle) {
|
|
12091
|
-
const wanted = needle.toLowerCase();
|
|
12092
|
-
const byUrl = targets.filter((t)=>t.url.toLowerCase().includes(wanted));
|
|
12093
|
-
if (byUrl.length > 0) return byUrl;
|
|
12094
|
-
return targets.filter((t)=>t.title.toLowerCase().includes(wanted));
|
|
12095
|
-
}
|
|
12096
13328
|
const RDP_ACTOR_NOTE = "actor is an RDP tab descriptor actor id, NOT a chrome.tabs id: do not pass it as `tab`. Target a tab with `tabUrl` (URL substring) or `url`; if you need a numeric tab id, call extension_dom_snapshot with listTabs: true.";
|
|
12097
13329
|
const dom_snapshot_schema = {
|
|
12098
13330
|
name: "extension_dom_snapshot",
|
|
@@ -12542,6 +13774,11 @@ async function publish_handler(args) {
|
|
|
12542
13774
|
}
|
|
12543
13775
|
}
|
|
12544
13776
|
}
|
|
13777
|
+
data.allowance = spendNarration({
|
|
13778
|
+
what: "This publish",
|
|
13779
|
+
body: data,
|
|
13780
|
+
api: args.api
|
|
13781
|
+
});
|
|
12545
13782
|
return envelope_envelope({
|
|
12546
13783
|
ok: true,
|
|
12547
13784
|
command: "extension_publish",
|
|
@@ -12726,6 +13963,11 @@ async function release_promote_handler(args) {
|
|
|
12726
13963
|
const promotedRef = resolveProjectRef();
|
|
12727
13964
|
const publicChannelUrl = userlandProjectUrl(promotedRef, UserlandProjectPage.channel(channel), args.api);
|
|
12728
13965
|
const publicBuildUrl = userlandProjectUrl(promotedRef, UserlandProjectPage.build(buildId), args.api);
|
|
13966
|
+
const allowance = spendNarration({
|
|
13967
|
+
what: "This promote",
|
|
13968
|
+
body: data,
|
|
13969
|
+
api: args.api
|
|
13970
|
+
});
|
|
12729
13971
|
const enriched = data && "object" == typeof data && !Array.isArray(data) ? {
|
|
12730
13972
|
...data,
|
|
12731
13973
|
...publicChannelUrl ? {
|
|
@@ -12733,8 +13975,12 @@ async function release_promote_handler(args) {
|
|
|
12733
13975
|
} : {},
|
|
12734
13976
|
...publicBuildUrl ? {
|
|
12735
13977
|
publicBuildUrl
|
|
12736
|
-
} : {}
|
|
12737
|
-
|
|
13978
|
+
} : {},
|
|
13979
|
+
allowance
|
|
13980
|
+
} : {
|
|
13981
|
+
platform: data,
|
|
13982
|
+
allowance
|
|
13983
|
+
};
|
|
12738
13984
|
return envelope_envelope({
|
|
12739
13985
|
ok: true,
|
|
12740
13986
|
command: "extension_release_promote",
|
|
@@ -13457,7 +14703,14 @@ async function submit_handler(args) {
|
|
|
13457
14703
|
if ("string" == typeof data?.message) result.platformMessage = data.message;
|
|
13458
14704
|
message = summaryParts.join(" ");
|
|
13459
14705
|
}
|
|
13460
|
-
if (!dryRun)
|
|
14706
|
+
if (!dryRun) {
|
|
14707
|
+
statusNote = "Track this submission with extension_release_status: it reads the recorded outcome, per-store credential health, and review state from the public registry.";
|
|
14708
|
+
if (platformOk) result.allowance = spendNarration({
|
|
14709
|
+
what: "This submission",
|
|
14710
|
+
body: data,
|
|
14711
|
+
api: args.api
|
|
14712
|
+
});
|
|
14713
|
+
}
|
|
13461
14714
|
return envelope_envelope({
|
|
13462
14715
|
ok,
|
|
13463
14716
|
command: "extension_submit",
|
|
@@ -13599,6 +14852,12 @@ async function wait_handler(args) {
|
|
|
13599
14852
|
pid: contract.pid,
|
|
13600
14853
|
distPath: contract.distPath,
|
|
13601
14854
|
manifestPath: contract.manifestPath,
|
|
14855
|
+
..."number" == typeof contract.browserPid ? {
|
|
14856
|
+
browserPid: contract.browserPid
|
|
14857
|
+
} : {},
|
|
14858
|
+
..."string" == typeof contract.profilePath && contract.profilePath ? {
|
|
14859
|
+
profilePath: contract.profilePath
|
|
14860
|
+
} : {},
|
|
13602
14861
|
compiledAt: contract.compiledAt,
|
|
13603
14862
|
startedAt: contract.startedAt,
|
|
13604
14863
|
budgetMs,
|
|
@@ -14277,7 +15536,7 @@ async function auth_handler(args) {
|
|
|
14277
15536
|
});
|
|
14278
15537
|
return readIdentity();
|
|
14279
15538
|
}
|
|
14280
|
-
const
|
|
15539
|
+
const detect_browsers_execFileAsync = promisify(execFile);
|
|
14281
15540
|
const ALL_BROWSERS = [
|
|
14282
15541
|
"chrome",
|
|
14283
15542
|
"chromium",
|
|
@@ -14455,44 +15714,58 @@ function resolveCacheRoot() {
|
|
|
14455
15714
|
if (home) return node_path.join(home, ".cache", "extension.js", "browsers");
|
|
14456
15715
|
return node_path.resolve(process.cwd(), ".cache", "extension.js", "browsers");
|
|
14457
15716
|
}
|
|
14458
|
-
|
|
14459
|
-
|
|
15717
|
+
const MANAGED_SEARCH_DEPTH = 8;
|
|
15718
|
+
const MANAGED_EXEC_NAMES = {
|
|
15719
|
+
chrome: [
|
|
15720
|
+
"chrome",
|
|
15721
|
+
"chrome.exe",
|
|
15722
|
+
"Google Chrome for Testing"
|
|
15723
|
+
],
|
|
15724
|
+
chromium: [
|
|
15725
|
+
"chrome",
|
|
15726
|
+
"chromium",
|
|
15727
|
+
"chrome.exe",
|
|
15728
|
+
"chromium.exe",
|
|
15729
|
+
"Chromium"
|
|
15730
|
+
],
|
|
15731
|
+
edge: [
|
|
15732
|
+
"msedge",
|
|
15733
|
+
"msedge.exe",
|
|
15734
|
+
"microsoft-edge",
|
|
15735
|
+
"Microsoft Edge"
|
|
15736
|
+
],
|
|
15737
|
+
firefox: [
|
|
15738
|
+
"firefox",
|
|
15739
|
+
"firefox.exe",
|
|
15740
|
+
"firefox-bin"
|
|
15741
|
+
]
|
|
15742
|
+
};
|
|
15743
|
+
function executableInsideBundle(bundle, names) {
|
|
15744
|
+
const macos = node_path.join(bundle, "Contents", "MacOS");
|
|
15745
|
+
try {
|
|
15746
|
+
for (const entry of node_fs.readdirSync(macos, {
|
|
15747
|
+
withFileTypes: true
|
|
15748
|
+
}))if (entry.isFile() && names.includes(entry.name)) return node_path.join(macos, entry.name);
|
|
15749
|
+
} catch {}
|
|
15750
|
+
return null;
|
|
15751
|
+
}
|
|
15752
|
+
function findManagedBinary(browser, cacheRoot = resolveCacheRoot()) {
|
|
15753
|
+
const browserDir = node_path.join(cacheRoot, browser);
|
|
14460
15754
|
if (!node_fs.existsSync(browserDir)) return null;
|
|
14461
|
-
const
|
|
14462
|
-
chrome: [
|
|
14463
|
-
"chrome",
|
|
14464
|
-
"chrome.exe",
|
|
14465
|
-
"Google Chrome for Testing"
|
|
14466
|
-
],
|
|
14467
|
-
chromium: [
|
|
14468
|
-
"chrome",
|
|
14469
|
-
"chromium",
|
|
14470
|
-
"chrome.exe",
|
|
14471
|
-
"chromium.exe",
|
|
14472
|
-
"Chromium.app"
|
|
14473
|
-
],
|
|
14474
|
-
edge: [
|
|
14475
|
-
"msedge",
|
|
14476
|
-
"msedge.exe",
|
|
14477
|
-
"microsoft-edge",
|
|
14478
|
-
"Microsoft Edge"
|
|
14479
|
-
],
|
|
14480
|
-
firefox: [
|
|
14481
|
-
"firefox",
|
|
14482
|
-
"firefox.exe",
|
|
14483
|
-
"Firefox.app"
|
|
14484
|
-
]
|
|
14485
|
-
};
|
|
14486
|
-
const names = execNames[browser] ?? [];
|
|
15755
|
+
const names = MANAGED_EXEC_NAMES[browser] ?? [];
|
|
14487
15756
|
function search(dir, depth) {
|
|
14488
|
-
if (depth >
|
|
15757
|
+
if (depth > MANAGED_SEARCH_DEPTH) return null;
|
|
14489
15758
|
try {
|
|
14490
15759
|
for (const entry of node_fs.readdirSync(dir, {
|
|
14491
15760
|
withFileTypes: true
|
|
14492
15761
|
})){
|
|
14493
15762
|
const full = node_path.join(dir, entry.name);
|
|
14494
15763
|
if (entry.isFile() && names.includes(entry.name)) return full;
|
|
14495
|
-
if (entry.isDirectory() &&
|
|
15764
|
+
if (entry.isDirectory() && entry.name.endsWith(".app")) {
|
|
15765
|
+
const inside = executableInsideBundle(full, names);
|
|
15766
|
+
if (inside) return inside;
|
|
15767
|
+
}
|
|
15768
|
+
if (entry.isDirectory() && depth < MANAGED_SEARCH_DEPTH) {
|
|
14496
15769
|
const found = search(full, depth + 1);
|
|
14497
15770
|
if (found) return found;
|
|
14498
15771
|
}
|
|
@@ -14511,7 +15784,7 @@ function findSystemBinary(browser) {
|
|
|
14511
15784
|
async function getVersion(binaryPath, browser) {
|
|
14512
15785
|
try {
|
|
14513
15786
|
const flag = "firefox" === browser ? "--version" : "--version";
|
|
14514
|
-
const { stdout } = await
|
|
15787
|
+
const { stdout } = await detect_browsers_execFileAsync(binaryPath, [
|
|
14515
15788
|
flag
|
|
14516
15789
|
], {
|
|
14517
15790
|
timeout: 5000,
|
|
@@ -14543,12 +15816,16 @@ async function detectBrowsers(browsers) {
|
|
|
14543
15816
|
const isWebkit = WEBKIT_FAMILY.has(browser);
|
|
14544
15817
|
let binaryPath = findManagedBinary(browser);
|
|
14545
15818
|
let source = "managed";
|
|
15819
|
+
const systemBinaryPath = findSystemBinary(browser);
|
|
14546
15820
|
if (!binaryPath) {
|
|
14547
|
-
binaryPath =
|
|
15821
|
+
binaryPath = systemBinaryPath;
|
|
14548
15822
|
source = binaryPath ? "system" : "not_found";
|
|
14549
15823
|
}
|
|
14550
15824
|
let version = null;
|
|
14551
15825
|
if (binaryPath && !isWebkit) version = await getVersion(binaryPath, browser);
|
|
15826
|
+
else if (binaryPath && isWebkit) version = await readSafariVersion(binaryPath);
|
|
15827
|
+
const automation = isWebkit && binaryPath ? await detectSafariAutomation(binaryPath) : null;
|
|
15828
|
+
const shadowedSystem = "managed" === source && systemBinaryPath && systemBinaryPath !== binaryPath ? systemBinaryPath : null;
|
|
14552
15829
|
detected.push({
|
|
14553
15830
|
browser,
|
|
14554
15831
|
binaryPath,
|
|
@@ -14556,11 +15833,20 @@ async function detectBrowsers(browsers) {
|
|
|
14556
15833
|
engine: isWebkit ? "webkit" : isGecko ? "gecko" : "chromium",
|
|
14557
15834
|
version,
|
|
14558
15835
|
cdpSupport: !isGecko && !isWebkit,
|
|
14559
|
-
rdpSupport: isGecko
|
|
15836
|
+
rdpSupport: isGecko,
|
|
15837
|
+
...automation ? {
|
|
15838
|
+
automation
|
|
15839
|
+
} : {},
|
|
15840
|
+
...shadowedSystem ? {
|
|
15841
|
+
systemBinaryPath: shadowedSystem,
|
|
15842
|
+
devLaunches: `extension_dev launches the managed binary above, not the system install at ${shadowedSystem}; pass ${isGecko ? "geckoBinary" : "chromiumBinary"} to extension_dev to use the system one.`
|
|
15843
|
+
} : {}
|
|
14560
15844
|
});
|
|
14561
15845
|
}
|
|
14562
15846
|
const available = detected.filter((d)=>"not_found" !== d.source);
|
|
14563
15847
|
const missing = detected.filter((d)=>"not_found" === d.source);
|
|
15848
|
+
const safari = detected.find((d)=>d.automation);
|
|
15849
|
+
const safariHint = safari?.automation ? ` ${safariAutomationHint(safari.automation)}` : "";
|
|
14564
15850
|
return envelope_envelope({
|
|
14565
15851
|
ok: true,
|
|
14566
15852
|
command: "extension_browsers",
|
|
@@ -14573,7 +15859,7 @@ async function detectBrowsers(browsers) {
|
|
|
14573
15859
|
missing: missing.map((d)=>d.browser)
|
|
14574
15860
|
}
|
|
14575
15861
|
},
|
|
14576
|
-
hint: missing.length ? `Missing browser(s): ${missing.map((d)=>d.browser).join(", ")}.${missing.some((d)=>MANAGED_INSTALLABLE.has(d.browser)) ? ` Use extension_browsers with action: "install" to install ${missing.filter((d)=>MANAGED_INSTALLABLE.has(d.browser)).map((d)=>d.browser).join(", ")}.` : ""}` :
|
|
15862
|
+
hint: missing.length ? `Missing browser(s): ${missing.map((d)=>d.browser).join(", ")}.${missing.some((d)=>MANAGED_INSTALLABLE.has(d.browser)) ? ` Use extension_browsers with action: "install" to install ${missing.filter((d)=>MANAGED_INSTALLABLE.has(d.browser)).map((d)=>d.browser).join(", ")}.` : ""}${safariHint}` : `All requested browsers are available.${safariHint}`
|
|
14577
15863
|
});
|
|
14578
15864
|
}
|
|
14579
15865
|
const BROWSER_NAMES = [
|
|
@@ -14988,15 +16274,16 @@ function toolResultFrame(result) {
|
|
|
14988
16274
|
}
|
|
14989
16275
|
const toolMap = new Map();
|
|
14990
16276
|
for (const tool of tools)toolMap.set(tool.schema.name, tool);
|
|
14991
|
-
|
|
14992
|
-
|
|
16277
|
+
const SERVER_INSTRUCTIONS = "extension-dev runs, inspects, drives, builds and publishes browser extensions (Chrome, Edge, Firefox, Safari and the other Chromium and Gecko browsers) through Extension.js and extension.dev.\nWhen the ask is to run, start, wait for, watch, inspect, drive, test, debug or build a browser extension, search this server first and use its tools: extension_dev starts the dev session (allowEval: true also turns on control), extension_wait blocks until it is ready, extension_logs streams its console, extension_open opens a surface or a url, extension_dom_snapshot and extension_inspect read a live page, extension_eval runs code in a context, extension_build makes a store-ready bundle, extension_stop ends the session.\nThese replace hand-rolled ps, curl, remote-debugging-port lookups and CDP or Playwright scripts: the server already holds the session's debug port, the extension id and the session token.\nEvery tool answers one JSON envelope {ok, status, value, error, hint, warnings}; read hint and warnings before choosing the next call, and treat ok: false as the answer, not a transport error.";
|
|
16278
|
+
function createServer() {
|
|
14993
16279
|
const server = new Server({
|
|
14994
16280
|
name: "extension-dev",
|
|
14995
16281
|
version: package_namespaceObject.rE
|
|
14996
16282
|
}, {
|
|
14997
16283
|
capabilities: {
|
|
14998
16284
|
tools: {}
|
|
14999
|
-
}
|
|
16285
|
+
},
|
|
16286
|
+
instructions: SERVER_INSTRUCTIONS
|
|
15000
16287
|
});
|
|
15001
16288
|
server.setRequestHandler(ListToolsRequestSchema, async ()=>({
|
|
15002
16289
|
tools: tools.map((t)=>({
|
|
@@ -15063,6 +16350,11 @@ async function startServer() {
|
|
|
15063
16350
|
};
|
|
15064
16351
|
}
|
|
15065
16352
|
});
|
|
16353
|
+
return server;
|
|
16354
|
+
}
|
|
16355
|
+
async function startServer() {
|
|
16356
|
+
installCarrierExitCleanup();
|
|
16357
|
+
const server = createServer();
|
|
15066
16358
|
const transport = new StdioServerTransport();
|
|
15067
16359
|
await server.connect(transport);
|
|
15068
16360
|
}
|