@extension.dev/mcp 10.9.0 → 10.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +79 -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 +1002 -57
- 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/preview-upload.d.ts +1 -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 +2 -0
- package/dist/src/lib/webdriver.d.ts +28 -0
- package/dist/src/tools/eval.d.ts +3 -0
- package/dist/src/tools/open.d.ts +2 -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,
|
|
@@ -222,7 +227,7 @@ __webpack_require__.d(wait_namespaceObject, {
|
|
|
222
227
|
handler: ()=>wait_handler,
|
|
223
228
|
schema: ()=>wait_schema
|
|
224
229
|
});
|
|
225
|
-
var package_namespaceObject = JSON.parse('{"rE":"10.
|
|
230
|
+
var package_namespaceObject = JSON.parse('{"rE":"10.10.0","El":{"OP":"4.1.29"}}');
|
|
226
231
|
const ENVELOPE_SCHEMA = 1;
|
|
227
232
|
const collectWarnings = (warnings)=>{
|
|
228
233
|
if (!warnings) return [];
|
|
@@ -1078,9 +1083,52 @@ function parseBuildIndex(json) {
|
|
|
1078
1083
|
}
|
|
1079
1084
|
return out;
|
|
1080
1085
|
}
|
|
1086
|
+
const ADDRESS_KEYS = /url|referrer/i;
|
|
1087
|
+
const KEEP_QUERY = /^(utm_|ref$)/;
|
|
1088
|
+
const SCRUB_ORIGIN = "https://www.extension.dev";
|
|
1089
|
+
const REPOSITORY_REF = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+@/;
|
|
1090
|
+
const DEPTH = 4;
|
|
1091
|
+
function maskRepositoryRef(value) {
|
|
1092
|
+
if (!REPOSITORY_REF.test(value)) return value;
|
|
1093
|
+
return `[owner]/[repo]@${value.slice(value.indexOf("@") + 1)}`;
|
|
1094
|
+
}
|
|
1095
|
+
function scrubUrlProperty(value) {
|
|
1096
|
+
if ("string" != typeof value || !value) return value;
|
|
1097
|
+
let parsed;
|
|
1098
|
+
try {
|
|
1099
|
+
parsed = new URL(value, SCRUB_ORIGIN);
|
|
1100
|
+
} catch {
|
|
1101
|
+
return value;
|
|
1102
|
+
}
|
|
1103
|
+
let changed = false;
|
|
1104
|
+
for (const key of [
|
|
1105
|
+
...parsed.searchParams.keys()
|
|
1106
|
+
])if (!KEEP_QUERY.test(key)) {
|
|
1107
|
+
parsed.searchParams.delete(key);
|
|
1108
|
+
changed = true;
|
|
1109
|
+
}
|
|
1110
|
+
if (!changed) return value;
|
|
1111
|
+
return value.startsWith("/") ? `${parsed.pathname}${parsed.search}${parsed.hash}` : parsed.toString();
|
|
1112
|
+
}
|
|
1113
|
+
function maskDeep(value, remaining) {
|
|
1114
|
+
if ("string" == typeof value) return maskRepositoryRef(value);
|
|
1115
|
+
if (remaining <= 0 || !value || "object" != typeof value) return value;
|
|
1116
|
+
if (Array.isArray(value)) return value.map((item)=>maskDeep(item, remaining - 1));
|
|
1117
|
+
const masked = {};
|
|
1118
|
+
for (const [key, nested] of Object.entries(value))masked[key] = maskDeep(nested, remaining - 1);
|
|
1119
|
+
return masked;
|
|
1120
|
+
}
|
|
1121
|
+
function sanitizeMcpProperties(properties) {
|
|
1122
|
+
const sanitized = {
|
|
1123
|
+
...properties
|
|
1124
|
+
};
|
|
1125
|
+
for (const [key, value] of Object.entries(sanitized))if (ADDRESS_KEYS.test(key)) sanitized[key] = scrubUrlProperty(value);
|
|
1126
|
+
for (const [key, value] of Object.entries(sanitized))sanitized[key] = maskDeep(value, DEPTH);
|
|
1127
|
+
return sanitized;
|
|
1128
|
+
}
|
|
1081
1129
|
const DEFAULT_MEDIA_ORIGIN = "https://media.extension.land";
|
|
1082
1130
|
const DEFAULT_CHANNEL = "latest";
|
|
1083
|
-
const PINNED_COMMIT = "
|
|
1131
|
+
const PINNED_COMMIT = "8e2336852b51c0fb71fd03abeca1f5e93a44700e";
|
|
1084
1132
|
const CHANNEL_CACHE_TTL_MS = 300000;
|
|
1085
1133
|
function mediaOrigin() {
|
|
1086
1134
|
return (process.env.EXTENSION_MEDIA_ORIGIN || "").trim() || DEFAULT_MEDIA_ORIGIN;
|
|
@@ -1212,7 +1260,7 @@ function creationFunnelPayload(event, properties, now = new Date()) {
|
|
|
1212
1260
|
timestamp: now.toISOString(),
|
|
1213
1261
|
properties: {
|
|
1214
1262
|
draft_id: null,
|
|
1215
|
-
...properties,
|
|
1263
|
+
...sanitizeMcpProperties(properties),
|
|
1216
1264
|
source: FUNNEL_SOURCE,
|
|
1217
1265
|
entry: FUNNEL_ENTRY,
|
|
1218
1266
|
session_id: session,
|
|
@@ -1646,6 +1694,42 @@ async function pollDeviceToken(args) {
|
|
|
1646
1694
|
};
|
|
1647
1695
|
}
|
|
1648
1696
|
}
|
|
1697
|
+
const FREE_ALLOWANCE_PHRASE = "counts against your free allowance";
|
|
1698
|
+
function allowanceWallUrl(apiHint) {
|
|
1699
|
+
return `${mcpOrigins(apiHint).www.replace(/\/+$/, "")}/pricing`;
|
|
1700
|
+
}
|
|
1701
|
+
function allowance_asRecord(value) {
|
|
1702
|
+
return value && "object" == typeof value && !Array.isArray(value) ? value : null;
|
|
1703
|
+
}
|
|
1704
|
+
function asCount(value) {
|
|
1705
|
+
return "number" == typeof value && Number.isFinite(value) && value >= 0 ? value : null;
|
|
1706
|
+
}
|
|
1707
|
+
function readPlatformAllowance(body) {
|
|
1708
|
+
const record = allowance_asRecord(body);
|
|
1709
|
+
if (!record) return null;
|
|
1710
|
+
for (const key of [
|
|
1711
|
+
"allowance",
|
|
1712
|
+
"quota"
|
|
1713
|
+
]){
|
|
1714
|
+
const nested = allowance_asRecord(record[key]);
|
|
1715
|
+
if (!nested) continue;
|
|
1716
|
+
const used = asCount(nested.used);
|
|
1717
|
+
const limit = asCount(nested.limit);
|
|
1718
|
+
if (null !== used && null !== limit) return {
|
|
1719
|
+
used,
|
|
1720
|
+
limit
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
return null;
|
|
1724
|
+
}
|
|
1725
|
+
function spendNarration(options) {
|
|
1726
|
+
const counted = readPlatformAllowance(options.body);
|
|
1727
|
+
return {
|
|
1728
|
+
spent: `${options.what} ran on extension.dev's machines and ${FREE_ALLOWANCE_PHRASE}.`,
|
|
1729
|
+
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.",
|
|
1730
|
+
wall: `What the free allowance covers and when the paid plan starts are published at ${allowanceWallUrl(options.api)}.`
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1649
1733
|
const project_create_COMMAND = "extension_project_create";
|
|
1650
1734
|
const FIRST_CALL_BUDGET_MS = 8000;
|
|
1651
1735
|
const RESUME_BUDGET_MS = 22000;
|
|
@@ -1979,6 +2063,11 @@ async function finishFromPoll(poll, ctx) {
|
|
|
1979
2063
|
projectId: data.projectId ?? null,
|
|
1980
2064
|
consoleUrl,
|
|
1981
2065
|
sourceRepo: ctx.args.repo,
|
|
2066
|
+
allowance: spendNarration({
|
|
2067
|
+
what: "This project creation, including its first build,",
|
|
2068
|
+
body: data,
|
|
2069
|
+
api: ctx.apiBase
|
|
2070
|
+
}),
|
|
1982
2071
|
nextSteps: [
|
|
1983
2072
|
`extension_auth (action: login, project: '${finalWorkspace}/${finalProject}')`,
|
|
1984
2073
|
"extension_publish"
|
|
@@ -2122,6 +2211,20 @@ async function searchTemplates(args) {
|
|
|
2122
2211
|
});
|
|
2123
2212
|
}
|
|
2124
2213
|
const get_template_source_COMMAND = "extension_templates";
|
|
2214
|
+
const TEXTUAL = /\.(json|js|mjs|cjs|ts|tsx|jsx|html|htm|css|scss|sass|less|svg|txt|md|map|vue|svelte|ya?ml)$/i;
|
|
2215
|
+
function encodeTemplateFile(filePath, bytes) {
|
|
2216
|
+
if (TEXTUAL.test(filePath)) {
|
|
2217
|
+
const text = bytes.toString("utf8");
|
|
2218
|
+
if (Buffer.from(text, "utf8").equals(bytes)) return {
|
|
2219
|
+
content: text,
|
|
2220
|
+
encoding: "utf8"
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
return {
|
|
2224
|
+
content: bytes.toString("base64"),
|
|
2225
|
+
encoding: "base64"
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2125
2228
|
async function readTemplateSource(args) {
|
|
2126
2229
|
const template = await getTemplateBySlug(args.slug);
|
|
2127
2230
|
if (!template) return envelope_envelope({
|
|
@@ -2156,6 +2259,7 @@ async function readTemplateSource(args) {
|
|
|
2156
2259
|
hint: "Pass specific file paths in the files parameter to read their contents."
|
|
2157
2260
|
});
|
|
2158
2261
|
const fileContents = {};
|
|
2262
|
+
const fileEncodings = {};
|
|
2159
2263
|
const errors = [];
|
|
2160
2264
|
await Promise.all(args.files.map(async (filePath)=>{
|
|
2161
2265
|
const urls = await templateFileUrls(args.slug, filePath);
|
|
@@ -2163,13 +2267,16 @@ async function readTemplateSource(args) {
|
|
|
2163
2267
|
for (const url of urls)try {
|
|
2164
2268
|
const response = await fetch(url);
|
|
2165
2269
|
if (response.ok) {
|
|
2166
|
-
|
|
2270
|
+
const encoded = encodeTemplateFile(filePath, Buffer.from(await response.arrayBuffer()));
|
|
2271
|
+
fileContents[filePath] = encoded.content;
|
|
2272
|
+
fileEncodings[filePath] = encoded.encoding;
|
|
2167
2273
|
return;
|
|
2168
2274
|
}
|
|
2169
2275
|
lastStatus = response.status;
|
|
2170
2276
|
} catch {}
|
|
2171
2277
|
errors.push(`${filePath}: ${lastStatus || "fetch failed"}`);
|
|
2172
2278
|
}));
|
|
2279
|
+
const encodedFiles = Object.keys(fileEncodings).filter((filePath)=>"base64" === fileEncodings[filePath]).sort();
|
|
2173
2280
|
return envelope_envelope({
|
|
2174
2281
|
ok: 0 === errors.length,
|
|
2175
2282
|
command: get_template_source_COMMAND,
|
|
@@ -2180,8 +2287,10 @@ async function readTemplateSource(args) {
|
|
|
2180
2287
|
} : null,
|
|
2181
2288
|
value: {
|
|
2182
2289
|
...meta,
|
|
2183
|
-
fileContents
|
|
2290
|
+
fileContents,
|
|
2291
|
+
fileEncodings
|
|
2184
2292
|
},
|
|
2293
|
+
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
2294
|
warnings: errors
|
|
2186
2295
|
});
|
|
2187
2296
|
}
|
|
@@ -5291,6 +5400,127 @@ function isCdpEndpoint(port) {
|
|
|
5291
5400
|
});
|
|
5292
5401
|
});
|
|
5293
5402
|
}
|
|
5403
|
+
function webdriver_define_property(obj, key, value) {
|
|
5404
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
5405
|
+
value: value,
|
|
5406
|
+
enumerable: true,
|
|
5407
|
+
configurable: true,
|
|
5408
|
+
writable: true
|
|
5409
|
+
});
|
|
5410
|
+
else obj[key] = value;
|
|
5411
|
+
return obj;
|
|
5412
|
+
}
|
|
5413
|
+
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.";
|
|
5414
|
+
function readWebDriverSession(projectPath, browser) {
|
|
5415
|
+
try {
|
|
5416
|
+
const contract = JSON.parse(node_fs.readFileSync(bridge_readyContractPath(projectPath, browser), "utf8"));
|
|
5417
|
+
if ("number" == typeof contract.webdriverPort && Number.isFinite(contract.webdriverPort) && "string" == typeof contract.webdriverSessionId && contract.webdriverSessionId.length > 0) return {
|
|
5418
|
+
port: contract.webdriverPort,
|
|
5419
|
+
sessionId: contract.webdriverSessionId
|
|
5420
|
+
};
|
|
5421
|
+
} catch {}
|
|
5422
|
+
return null;
|
|
5423
|
+
}
|
|
5424
|
+
function readyExtensionId(projectPath, browser) {
|
|
5425
|
+
try {
|
|
5426
|
+
const contract = JSON.parse(node_fs.readFileSync(bridge_readyContractPath(projectPath, browser), "utf8"));
|
|
5427
|
+
const id = String(contract.extensionId || "").trim();
|
|
5428
|
+
return id.length > 0 ? id : null;
|
|
5429
|
+
} catch {
|
|
5430
|
+
return null;
|
|
5431
|
+
}
|
|
5432
|
+
}
|
|
5433
|
+
class WebDriverClient {
|
|
5434
|
+
async call(method, route, body, timeoutMs = 15000) {
|
|
5435
|
+
const controller = new AbortController();
|
|
5436
|
+
const timer = setTimeout(()=>controller.abort(), timeoutMs);
|
|
5437
|
+
try {
|
|
5438
|
+
const response = await this.fetchFn(`${this.base}${route}`, {
|
|
5439
|
+
method,
|
|
5440
|
+
headers: {
|
|
5441
|
+
"content-type": "application/json"
|
|
5442
|
+
},
|
|
5443
|
+
body: "GET" === method ? void 0 : JSON.stringify(body ?? {}),
|
|
5444
|
+
signal: controller.signal
|
|
5445
|
+
});
|
|
5446
|
+
const text = await response.text();
|
|
5447
|
+
let parsed = {};
|
|
5448
|
+
try {
|
|
5449
|
+
parsed = text ? JSON.parse(text) : {};
|
|
5450
|
+
} catch {
|
|
5451
|
+
parsed = {
|
|
5452
|
+
value: {
|
|
5453
|
+
message: text
|
|
5454
|
+
}
|
|
5455
|
+
};
|
|
5456
|
+
}
|
|
5457
|
+
if (!response.ok) {
|
|
5458
|
+
const value = parsed.value || {};
|
|
5459
|
+
throw new Error(String(value.message || value.error || `${response.status} on ${route}`));
|
|
5460
|
+
}
|
|
5461
|
+
return parsed.value;
|
|
5462
|
+
} finally{
|
|
5463
|
+
clearTimeout(timer);
|
|
5464
|
+
}
|
|
5465
|
+
}
|
|
5466
|
+
async alive() {
|
|
5467
|
+
try {
|
|
5468
|
+
await this.call("GET", "/url");
|
|
5469
|
+
return true;
|
|
5470
|
+
} catch {
|
|
5471
|
+
return false;
|
|
5472
|
+
}
|
|
5473
|
+
}
|
|
5474
|
+
async currentUrl() {
|
|
5475
|
+
const value = await this.call("GET", "/url");
|
|
5476
|
+
return "string" == typeof value ? value : null;
|
|
5477
|
+
}
|
|
5478
|
+
async navigate(url, timeoutMs = 60000) {
|
|
5479
|
+
await this.call("POST", "/url", {
|
|
5480
|
+
url
|
|
5481
|
+
}, timeoutMs);
|
|
5482
|
+
}
|
|
5483
|
+
async refresh(timeoutMs = 60000) {
|
|
5484
|
+
await this.call("POST", "/refresh", {}, timeoutMs);
|
|
5485
|
+
}
|
|
5486
|
+
async execute(script, args = [], timeoutMs = 30000) {
|
|
5487
|
+
return this.call("POST", "/execute/sync", {
|
|
5488
|
+
script,
|
|
5489
|
+
args
|
|
5490
|
+
}, timeoutMs);
|
|
5491
|
+
}
|
|
5492
|
+
constructor(info, fetchFn = fetch){
|
|
5493
|
+
webdriver_define_property(this, "info", void 0);
|
|
5494
|
+
webdriver_define_property(this, "fetchFn", void 0);
|
|
5495
|
+
webdriver_define_property(this, "base", void 0);
|
|
5496
|
+
this.info = info;
|
|
5497
|
+
this.fetchFn = fetchFn;
|
|
5498
|
+
this.base = `http://127.0.0.1:${info.port}/session/${info.sessionId}`;
|
|
5499
|
+
}
|
|
5500
|
+
}
|
|
5501
|
+
const EXTENSION_ROOT_READING_SCRIPT = `
|
|
5502
|
+
const nodes = Array.from(document.querySelectorAll('#extension-root,[data-extension-root]'));
|
|
5503
|
+
return {
|
|
5504
|
+
roots: nodes.length,
|
|
5505
|
+
owners: nodes.map((n) => String(n.getAttribute('data-extjs-reinject-owner') || '')),
|
|
5506
|
+
url: String(location.href),
|
|
5507
|
+
title: String(document.title || '')
|
|
5508
|
+
};
|
|
5509
|
+
`;
|
|
5510
|
+
async function readExtensionRoots(client) {
|
|
5511
|
+
const value = await client.execute(EXTENSION_ROOT_READING_SCRIPT);
|
|
5512
|
+
return {
|
|
5513
|
+
roots: "number" == typeof value?.roots ? value.roots : 0,
|
|
5514
|
+
owners: Array.isArray(value?.owners) ? value.owners.map((o)=>String(o)) : [],
|
|
5515
|
+
url: String(value?.url || ""),
|
|
5516
|
+
title: String(value?.title || "")
|
|
5517
|
+
};
|
|
5518
|
+
}
|
|
5519
|
+
function sameDocument(current, wanted) {
|
|
5520
|
+
if (!current) return false;
|
|
5521
|
+
const strip = (u)=>u.replace(/[#?].*$/, "").replace(/\/+$/, "");
|
|
5522
|
+
return strip(current) === strip(wanted);
|
|
5523
|
+
}
|
|
5294
5524
|
const ENGINE_COMPANION_IDS = new Set([
|
|
5295
5525
|
"kgdaecdpfkikjncaalnmmnjjfpofkcbl",
|
|
5296
5526
|
CARRIER_EXTENSION_ID
|
|
@@ -5462,7 +5692,48 @@ async function pollForBridgeTab(projectPath, browser, url, budgetMs) {
|
|
|
5462
5692
|
await new Promise((r)=>setTimeout(r, 250));
|
|
5463
5693
|
}
|
|
5464
5694
|
}
|
|
5695
|
+
const UNKNOWN_VERB = /unknown command/i;
|
|
5465
5696
|
async function navigateToUrlViaBridge(projectPath, browser, url, timeout, tool = "extension_open") {
|
|
5697
|
+
const viaVerb = await runActVerb([
|
|
5698
|
+
"navigate",
|
|
5699
|
+
url,
|
|
5700
|
+
projectPath,
|
|
5701
|
+
"--browser",
|
|
5702
|
+
browser,
|
|
5703
|
+
...null != timeout ? [
|
|
5704
|
+
"--timeout",
|
|
5705
|
+
String(timeout)
|
|
5706
|
+
] : []
|
|
5707
|
+
], projectPath, timeout, tool);
|
|
5708
|
+
let verbFrame;
|
|
5709
|
+
try {
|
|
5710
|
+
verbFrame = JSON.parse(viaVerb);
|
|
5711
|
+
} catch {
|
|
5712
|
+
verbFrame = null;
|
|
5713
|
+
}
|
|
5714
|
+
const verbUnknown = verbFrame?.ok === false && UNKNOWN_VERB.test(String(verbFrame?.error?.message ?? ""));
|
|
5715
|
+
if (verbFrame && !verbUnknown) {
|
|
5716
|
+
if (false === verbFrame.ok) return actFrameJson(verbFrame.hint ? verbFrame : {
|
|
5717
|
+
...verbFrame,
|
|
5718
|
+
hint: "URL navigation rides the agent bridge, so the dev session must be started with allowControl: true (extension_dev)."
|
|
5719
|
+
});
|
|
5720
|
+
const value = verbFrame.value && "object" == typeof verbFrame.value ? verbFrame.value : {};
|
|
5721
|
+
return envelope_envelope({
|
|
5722
|
+
ok: true,
|
|
5723
|
+
command: tool,
|
|
5724
|
+
status: "navigated",
|
|
5725
|
+
value: {
|
|
5726
|
+
navigated: url,
|
|
5727
|
+
tabId: "number" == typeof value.tabId ? value.tabId : null,
|
|
5728
|
+
created: true === value.created,
|
|
5729
|
+
via: "navigate"
|
|
5730
|
+
},
|
|
5731
|
+
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."
|
|
5732
|
+
});
|
|
5733
|
+
}
|
|
5734
|
+
return navigateToUrlViaBackgroundEval(projectPath, browser, url, timeout, tool);
|
|
5735
|
+
}
|
|
5736
|
+
async function navigateToUrlViaBackgroundEval(projectPath, browser, url, timeout, tool = "extension_open") {
|
|
5466
5737
|
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
5738
|
const raw = await runActVerb([
|
|
5468
5739
|
"eval",
|
|
@@ -5481,7 +5752,7 @@ async function navigateToUrlViaBridge(projectPath, browser, url, timeout, tool =
|
|
|
5481
5752
|
const parsed = JSON.parse(raw);
|
|
5482
5753
|
if (parsed?.ok === false) return actFrameJson(parsed.hint ? parsed : {
|
|
5483
5754
|
...parsed,
|
|
5484
|
-
hint: "
|
|
5755
|
+
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
5756
|
});
|
|
5486
5757
|
} catch {
|
|
5487
5758
|
return raw;
|
|
@@ -5533,6 +5804,244 @@ async function resolveBridgeBaseUrl(projectPath, browser, timeout) {
|
|
|
5533
5804
|
} catch {}
|
|
5534
5805
|
return null;
|
|
5535
5806
|
}
|
|
5807
|
+
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.";
|
|
5808
|
+
function filterPageTargets(raw) {
|
|
5809
|
+
return raw.filter((t)=>"page" === t.type && !String(t.url ?? "").startsWith("devtools://")).map((t)=>({
|
|
5810
|
+
targetId: String(t.id),
|
|
5811
|
+
type: String(t.type),
|
|
5812
|
+
url: String(t.url ?? ""),
|
|
5813
|
+
title: String(t.title ?? "")
|
|
5814
|
+
}));
|
|
5815
|
+
}
|
|
5816
|
+
async function listPageTargets(port) {
|
|
5817
|
+
return filterPageTargets(await CDPClient.discoverTargets(port));
|
|
5818
|
+
}
|
|
5819
|
+
function matchTargetsByUrl(targets, needle) {
|
|
5820
|
+
const wanted = needle.toLowerCase();
|
|
5821
|
+
const byUrl = targets.filter((t)=>t.url.toLowerCase().includes(wanted));
|
|
5822
|
+
if (byUrl.length > 0) return byUrl;
|
|
5823
|
+
return targets.filter((t)=>t.title.toLowerCase().includes(wanted));
|
|
5824
|
+
}
|
|
5825
|
+
const RUNTIME_EVALUATE_DEFAULTS = {
|
|
5826
|
+
returnByValue: true,
|
|
5827
|
+
awaitPromise: true,
|
|
5828
|
+
userGesture: true,
|
|
5829
|
+
replMode: true
|
|
5830
|
+
};
|
|
5831
|
+
function stripHash(url) {
|
|
5832
|
+
return url.replace(/#.*$/, "");
|
|
5833
|
+
}
|
|
5834
|
+
function matchExtensionPageTargets(targets, wantedUrl) {
|
|
5835
|
+
const wanted = stripHash(wantedUrl);
|
|
5836
|
+
return targets.filter((t)=>{
|
|
5837
|
+
const url = stripHash(t.url);
|
|
5838
|
+
return url === wanted || url.startsWith(wanted);
|
|
5839
|
+
});
|
|
5840
|
+
}
|
|
5841
|
+
async function findExtensionPageTargets(port, wantedUrl) {
|
|
5842
|
+
try {
|
|
5843
|
+
return matchExtensionPageTargets(await listPageTargets(port), wantedUrl);
|
|
5844
|
+
} catch {
|
|
5845
|
+
return [];
|
|
5846
|
+
}
|
|
5847
|
+
}
|
|
5848
|
+
function readRemoteValue(result) {
|
|
5849
|
+
if (!result) return null;
|
|
5850
|
+
if ("value" in result) return result.value;
|
|
5851
|
+
if ("undefined" === result.type) return null;
|
|
5852
|
+
if ("string" == typeof result.unserializableValue) return result.unserializableValue;
|
|
5853
|
+
return result.description ?? null;
|
|
5854
|
+
}
|
|
5855
|
+
function describeException(details) {
|
|
5856
|
+
const exception = details?.exception;
|
|
5857
|
+
if ("string" == typeof exception?.description && exception.description) return exception.description.split("\n")[0];
|
|
5858
|
+
if (exception && "value" in exception) return String(exception.value);
|
|
5859
|
+
return details?.text || "the expression threw";
|
|
5860
|
+
}
|
|
5861
|
+
async function evaluateOnExtensionPage(port, targetId, expression) {
|
|
5862
|
+
const cdp = new CDPClient();
|
|
5863
|
+
try {
|
|
5864
|
+
await cdp.connect(await CDPClient.discoverBrowserWsUrl(port));
|
|
5865
|
+
const sessionId = await cdp.attachToTarget(targetId);
|
|
5866
|
+
const response = await cdp.sendCommand("Runtime.evaluate", {
|
|
5867
|
+
expression,
|
|
5868
|
+
...RUNTIME_EVALUATE_DEFAULTS
|
|
5869
|
+
}, sessionId);
|
|
5870
|
+
if (response?.exceptionDetails) return {
|
|
5871
|
+
ok: false,
|
|
5872
|
+
thrown: true,
|
|
5873
|
+
message: describeException(response.exceptionDetails)
|
|
5874
|
+
};
|
|
5875
|
+
return {
|
|
5876
|
+
ok: true,
|
|
5877
|
+
value: readRemoteValue(response?.result)
|
|
5878
|
+
};
|
|
5879
|
+
} catch (error) {
|
|
5880
|
+
return {
|
|
5881
|
+
ok: false,
|
|
5882
|
+
thrown: false,
|
|
5883
|
+
message: error instanceof Error ? error.message : String(error)
|
|
5884
|
+
};
|
|
5885
|
+
} finally{
|
|
5886
|
+
try {
|
|
5887
|
+
cdp.disconnect();
|
|
5888
|
+
} catch {}
|
|
5889
|
+
}
|
|
5890
|
+
}
|
|
5891
|
+
const SIDE_PANEL_API_READY = 'typeof chrome !== "undefined" && typeof chrome.sidePanel !== "undefined" && typeof chrome.windows !== "undefined" && document.readyState !== "loading"';
|
|
5892
|
+
const SIDE_PANEL_STATE = "window.__extensionDevSidePanel";
|
|
5893
|
+
const ARM_SIDE_PANEL_SCRIPT = `(async () => {
|
|
5894
|
+
const win = await chrome.windows.getCurrent();
|
|
5895
|
+
const state = { phase: "armed", windowId: win.id };
|
|
5896
|
+
${SIDE_PANEL_STATE} = state;
|
|
5897
|
+
const overlay = document.createElement("div");
|
|
5898
|
+
overlay.setAttribute("data-extension-dev-gesture", "");
|
|
5899
|
+
overlay.style.cssText = "position:fixed;inset:0;z-index:2147483647;background:transparent;cursor:default;";
|
|
5900
|
+
overlay.addEventListener("click", (event) => {
|
|
5901
|
+
event.stopPropagation();
|
|
5902
|
+
event.preventDefault();
|
|
5903
|
+
state.phase = "clicked";
|
|
5904
|
+
const settle = (phase, message) => {
|
|
5905
|
+
state.phase = phase;
|
|
5906
|
+
if (message) state.message = message;
|
|
5907
|
+
overlay.remove();
|
|
5908
|
+
};
|
|
5909
|
+
try {
|
|
5910
|
+
Promise.resolve(chrome.sidePanel.open({ windowId: win.id }))
|
|
5911
|
+
.then(() => settle("opened"))
|
|
5912
|
+
.catch((error) => settle("failed", String((error && error.message) || error)));
|
|
5913
|
+
} catch (error) {
|
|
5914
|
+
settle("failed", String((error && error.message) || error));
|
|
5915
|
+
}
|
|
5916
|
+
}, { once: true });
|
|
5917
|
+
document.documentElement.appendChild(overlay);
|
|
5918
|
+
return win.id;
|
|
5919
|
+
})()`;
|
|
5920
|
+
const READ_SIDE_PANEL_STATE = `(() => {
|
|
5921
|
+
const state = ${SIDE_PANEL_STATE};
|
|
5922
|
+
return state ? { phase: state.phase, message: state.message } : null;
|
|
5923
|
+
})()`;
|
|
5924
|
+
async function pollUntil(read, budgetMs, everyMs) {
|
|
5925
|
+
const deadline = Date.now() + budgetMs;
|
|
5926
|
+
for(;;){
|
|
5927
|
+
const value = await read();
|
|
5928
|
+
if (null !== value) return value;
|
|
5929
|
+
if (Date.now() >= deadline) return null;
|
|
5930
|
+
await new Promise((r)=>setTimeout(r, everyMs));
|
|
5931
|
+
}
|
|
5932
|
+
}
|
|
5933
|
+
async function openSidePanelWithSyntheticGesture(port, hostUrl) {
|
|
5934
|
+
const cdp = new CDPClient();
|
|
5935
|
+
let hostId = null;
|
|
5936
|
+
const closeHost = async ()=>{
|
|
5937
|
+
if (!hostId) return;
|
|
5938
|
+
try {
|
|
5939
|
+
await cdp.sendCommand("Target.closeTarget", {
|
|
5940
|
+
targetId: hostId
|
|
5941
|
+
});
|
|
5942
|
+
} catch {}
|
|
5943
|
+
};
|
|
5944
|
+
try {
|
|
5945
|
+
await cdp.connect(await CDPClient.discoverBrowserWsUrl(port));
|
|
5946
|
+
const created = await cdp.sendCommand("Target.createTarget", {
|
|
5947
|
+
url: hostUrl
|
|
5948
|
+
});
|
|
5949
|
+
if ("string" != typeof created?.targetId) return {
|
|
5950
|
+
opened: false,
|
|
5951
|
+
reason: `the browser did not open a host tab for ${hostUrl}`
|
|
5952
|
+
};
|
|
5953
|
+
hostId = created.targetId;
|
|
5954
|
+
const sessionId = await cdp.attachToTarget(hostId);
|
|
5955
|
+
await cdp.sendCommand("Runtime.enable", {}, sessionId);
|
|
5956
|
+
const evaluate = async (expression)=>await cdp.sendCommand("Runtime.evaluate", {
|
|
5957
|
+
expression,
|
|
5958
|
+
...RUNTIME_EVALUATE_DEFAULTS
|
|
5959
|
+
}, sessionId);
|
|
5960
|
+
const apiReady = await pollUntil(async ()=>{
|
|
5961
|
+
const response = await evaluate(SIDE_PANEL_API_READY).catch(()=>null);
|
|
5962
|
+
return response?.result?.value === true ? true : null;
|
|
5963
|
+
}, 5000, 200);
|
|
5964
|
+
if (!apiReady) {
|
|
5965
|
+
await closeHost();
|
|
5966
|
+
return {
|
|
5967
|
+
opened: false,
|
|
5968
|
+
reason: "chrome.sidePanel is not available in the extension's own page (the manifest needs the sidePanel permission and the page must load)"
|
|
5969
|
+
};
|
|
5970
|
+
}
|
|
5971
|
+
const armed = await evaluate(ARM_SIDE_PANEL_SCRIPT);
|
|
5972
|
+
if (armed.exceptionDetails) {
|
|
5973
|
+
await closeHost();
|
|
5974
|
+
return {
|
|
5975
|
+
opened: false,
|
|
5976
|
+
reason: `arming the gesture listener threw: ${describeException(armed.exceptionDetails)}`
|
|
5977
|
+
};
|
|
5978
|
+
}
|
|
5979
|
+
const point = {
|
|
5980
|
+
x: 10,
|
|
5981
|
+
y: 10
|
|
5982
|
+
};
|
|
5983
|
+
await cdp.sendCommand("Input.dispatchMouseEvent", {
|
|
5984
|
+
type: "mouseMoved",
|
|
5985
|
+
...point
|
|
5986
|
+
}, sessionId);
|
|
5987
|
+
await cdp.sendCommand("Input.dispatchMouseEvent", {
|
|
5988
|
+
type: "mousePressed",
|
|
5989
|
+
button: "left",
|
|
5990
|
+
clickCount: 1,
|
|
5991
|
+
...point
|
|
5992
|
+
}, sessionId);
|
|
5993
|
+
await cdp.sendCommand("Input.dispatchMouseEvent", {
|
|
5994
|
+
type: "mouseReleased",
|
|
5995
|
+
button: "left",
|
|
5996
|
+
clickCount: 1,
|
|
5997
|
+
...point
|
|
5998
|
+
}, sessionId);
|
|
5999
|
+
const settled = await pollUntil(async ()=>{
|
|
6000
|
+
const response = await evaluate(READ_SIDE_PANEL_STATE).catch(()=>null);
|
|
6001
|
+
const state = response?.result?.value;
|
|
6002
|
+
if (state?.phase === "opened" || state?.phase === "failed") return state;
|
|
6003
|
+
return null;
|
|
6004
|
+
}, 3000, 100);
|
|
6005
|
+
await closeHost();
|
|
6006
|
+
if (!settled) return {
|
|
6007
|
+
opened: false,
|
|
6008
|
+
reason: "the synthetic click never reached chrome.sidePanel.open (the page did not report a result within 3s)"
|
|
6009
|
+
};
|
|
6010
|
+
if ("failed" === settled.phase) return {
|
|
6011
|
+
opened: false,
|
|
6012
|
+
reason: `chrome.sidePanel.open rejected: ${settled.message ?? "no message"}`
|
|
6013
|
+
};
|
|
6014
|
+
const excluded = hostId;
|
|
6015
|
+
const panel = await pollUntil(async ()=>{
|
|
6016
|
+
const matches = (await findExtensionPageTargets(port, hostUrl)).filter((t)=>t.targetId !== excluded);
|
|
6017
|
+
return matches.length ? matches[0] : null;
|
|
6018
|
+
}, 3000, 250);
|
|
6019
|
+
if (!panel) return {
|
|
6020
|
+
opened: false,
|
|
6021
|
+
reason: `chrome.sidePanel.open resolved but no page target for ${hostUrl} appeared within 3s`
|
|
6022
|
+
};
|
|
6023
|
+
return {
|
|
6024
|
+
opened: true,
|
|
6025
|
+
targetId: panel.targetId,
|
|
6026
|
+
url: panel.url
|
|
6027
|
+
};
|
|
6028
|
+
} catch (error) {
|
|
6029
|
+
await closeHost();
|
|
6030
|
+
return {
|
|
6031
|
+
opened: false,
|
|
6032
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
6033
|
+
};
|
|
6034
|
+
} finally{
|
|
6035
|
+
try {
|
|
6036
|
+
cdp.disconnect();
|
|
6037
|
+
} catch {}
|
|
6038
|
+
}
|
|
6039
|
+
}
|
|
6040
|
+
const OVERRIDE_SURFACES = [
|
|
6041
|
+
"newtab",
|
|
6042
|
+
"history",
|
|
6043
|
+
"bookmarks"
|
|
6044
|
+
];
|
|
5536
6045
|
async function pollForTarget(port, url, budgetMs, navigatedTargetId) {
|
|
5537
6046
|
const deadline = Date.now() + budgetMs;
|
|
5538
6047
|
const wanted = url.replace(/#.*$/, "");
|
|
@@ -5567,7 +6076,38 @@ function isDisposableTab(tabUrl, destination) {
|
|
|
5567
6076
|
const origin = destination.match(/^chrome-extension:\/\/[a-p]{32}\//)?.[0];
|
|
5568
6077
|
return Boolean(origin && tabUrl.startsWith(origin));
|
|
5569
6078
|
}
|
|
6079
|
+
async function navigateToUrlViaWebDriver(projectPath, browser, url) {
|
|
6080
|
+
const info = readWebDriverSession(projectPath, browser);
|
|
6081
|
+
if (!info) return navigateToUrlViaBridge(projectPath, browser, url);
|
|
6082
|
+
const client = new WebDriverClient(info);
|
|
6083
|
+
try {
|
|
6084
|
+
await client.navigate(url);
|
|
6085
|
+
return envelope_envelope({
|
|
6086
|
+
ok: true,
|
|
6087
|
+
command: open_schema.name,
|
|
6088
|
+
status: "navigated",
|
|
6089
|
+
value: {
|
|
6090
|
+
browser,
|
|
6091
|
+
url: await client.currentUrl().catch(()=>url)
|
|
6092
|
+
},
|
|
6093
|
+
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."
|
|
6094
|
+
});
|
|
6095
|
+
} catch (error) {
|
|
6096
|
+
return envelope_envelope({
|
|
6097
|
+
ok: false,
|
|
6098
|
+
command: open_schema.name,
|
|
6099
|
+
status: "navigate-failed",
|
|
6100
|
+
error: {
|
|
6101
|
+
code: "E_NAVIGATE_FAILED",
|
|
6102
|
+
name: "NavigateError",
|
|
6103
|
+
message: error instanceof Error ? error.message : String(error)
|
|
6104
|
+
},
|
|
6105
|
+
hint: "The automation window refused the URL or is gone. extension_doctor names which."
|
|
6106
|
+
});
|
|
6107
|
+
}
|
|
6108
|
+
}
|
|
5570
6109
|
async function navigateToUrl(projectPath, browser, url, timeout) {
|
|
6110
|
+
if (WEBKIT_FAMILY.has(browser) && readWebDriverSession(projectPath, browser)) return navigateToUrlViaWebDriver(projectPath, browser, url);
|
|
5571
6111
|
if (!isChromiumFamily(browser)) return navigateToUrlViaBridge(projectPath, browser, url, timeout);
|
|
5572
6112
|
const resolved = await resolveCdpPort(projectPath, browser);
|
|
5573
6113
|
if (!resolved) return envelope_envelope({
|
|
@@ -5892,7 +6432,8 @@ async function openSurfaceAsTab(projectPath, browser, surface) {
|
|
|
5892
6432
|
popupBounds = await applyPopupBounds(projectPath, browser, target.targetId);
|
|
5893
6433
|
if (popupBounds) renderedAsTab.popupBounds = popupBounds;
|
|
5894
6434
|
}
|
|
5895
|
-
|
|
6435
|
+
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.`;
|
|
6436
|
+
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
6437
|
return actFrameJson(parsed);
|
|
5897
6438
|
}
|
|
5898
6439
|
} catch {}
|
|
@@ -5940,7 +6481,7 @@ async function confirmSurfaceTarget(projectPath, browser, surface, raw) {
|
|
|
5940
6481
|
}
|
|
5941
6482
|
const open_schema = {
|
|
5942
6483
|
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).",
|
|
6484
|
+
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
6485
|
inputSchema: {
|
|
5945
6486
|
type: "object",
|
|
5946
6487
|
properties: {
|
|
@@ -5992,10 +6533,9 @@ async function open_handler(args) {
|
|
|
5992
6533
|
"popup",
|
|
5993
6534
|
"options",
|
|
5994
6535
|
"sidebar",
|
|
5995
|
-
|
|
5996
|
-
"history",
|
|
5997
|
-
"bookmarks"
|
|
6536
|
+
...OVERRIDE_SURFACES
|
|
5998
6537
|
];
|
|
6538
|
+
if (args.surface && OVERRIDE_SURFACES.includes(args.surface)) return openSurfaceAsTab(args.projectPath, browser, args.surface);
|
|
5999
6539
|
if (args.asTab && args.surface && AS_TAB_SURFACES.includes(args.surface)) return openSurfaceAsTab(args.projectPath, browser, args.surface);
|
|
6000
6540
|
if (!args.surface) return envelope_envelope({
|
|
6001
6541
|
ok: false,
|
|
@@ -6062,8 +6602,64 @@ async function open_handler(args) {
|
|
|
6062
6602
|
return actFrameJson(parsed);
|
|
6063
6603
|
}
|
|
6064
6604
|
} catch {}
|
|
6605
|
+
if (!headless && "sidebar" === args.surface && isChromiumFamily(browser)) {
|
|
6606
|
+
const refusal = readGestureRefusal(raw);
|
|
6607
|
+
if (refusal) return openSidebarThroughGesture(args.projectPath, browser, refusal);
|
|
6608
|
+
}
|
|
6065
6609
|
return AS_TAB_SURFACES.includes(args.surface) ? confirmSurfaceTarget(args.projectPath, browser, args.surface, raw) : raw;
|
|
6066
6610
|
}
|
|
6611
|
+
const E_USER_GESTURE_REQUIRED = "E_USER_GESTURE_REQUIRED";
|
|
6612
|
+
function readGestureRefusal(raw) {
|
|
6613
|
+
let parsed;
|
|
6614
|
+
try {
|
|
6615
|
+
parsed = JSON.parse(raw);
|
|
6616
|
+
} catch {
|
|
6617
|
+
return null;
|
|
6618
|
+
}
|
|
6619
|
+
if (parsed?.ok !== false) return null;
|
|
6620
|
+
const code = "string" == typeof parsed.error?.code ? parsed.error.code : "";
|
|
6621
|
+
const message = String(parsed.error?.message ?? "");
|
|
6622
|
+
return code === E_USER_GESTURE_REQUIRED || /user gesture/i.test(message) ? parsed : null;
|
|
6623
|
+
}
|
|
6624
|
+
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.";
|
|
6625
|
+
async function openSidebarThroughGesture(projectPath, browser, refusal) {
|
|
6626
|
+
const doc = surfaceDocument(projectPath, browser, "sidebar");
|
|
6627
|
+
const resolved = doc ? await resolveCdpPort(projectPath, browser) : null;
|
|
6628
|
+
const extensionId = resolved ? await resolveExtensionId(projectPath, browser) : null;
|
|
6629
|
+
let reason = "the sidebar document or the session's CDP port could not be resolved";
|
|
6630
|
+
if (doc && resolved && extensionId) {
|
|
6631
|
+
const hostUrl = `chrome-extension://${extensionId}/${doc}`;
|
|
6632
|
+
const outcome = await openSidePanelWithSyntheticGesture(resolved.port, hostUrl);
|
|
6633
|
+
if (outcome.opened) return envelope_envelope({
|
|
6634
|
+
ok: true,
|
|
6635
|
+
command: open_schema.name,
|
|
6636
|
+
status: "opened",
|
|
6637
|
+
value: {
|
|
6638
|
+
surface: "sidebar",
|
|
6639
|
+
gesture: "synthetic-click",
|
|
6640
|
+
surfaceTarget: {
|
|
6641
|
+
targetId: outcome.targetId,
|
|
6642
|
+
url: outcome.url
|
|
6643
|
+
}
|
|
6644
|
+
},
|
|
6645
|
+
warnings: [
|
|
6646
|
+
SIDEBAR_GESTURE_WARNING
|
|
6647
|
+
],
|
|
6648
|
+
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."
|
|
6649
|
+
});
|
|
6650
|
+
reason = outcome.reason;
|
|
6651
|
+
}
|
|
6652
|
+
const fallback = await openSurfaceAsTab(projectPath, browser, "sidebar");
|
|
6653
|
+
try {
|
|
6654
|
+
const parsedFallback = JSON.parse(fallback);
|
|
6655
|
+
if (parsedFallback?.ok) {
|
|
6656
|
+
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.`);
|
|
6657
|
+
return actFrameJson(parsedFallback);
|
|
6658
|
+
}
|
|
6659
|
+
} catch {}
|
|
6660
|
+
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.";
|
|
6661
|
+
return actFrameJson(refusal);
|
|
6662
|
+
}
|
|
6067
6663
|
function resolveToken() {
|
|
6068
6664
|
const fromEnv = String(process.env.EXTENSION_DEV_TOKEN || "").trim();
|
|
6069
6665
|
if (fromEnv) return fromEnv;
|
|
@@ -6348,11 +6944,11 @@ const IGNORED_SEGMENTS = new Set([
|
|
|
6348
6944
|
"__MACOSX"
|
|
6349
6945
|
]);
|
|
6350
6946
|
const IGNORED_FILES = /^\.env(\..*)?$|\.(pem|key|p12|pfx|keystore)$/i;
|
|
6351
|
-
const
|
|
6947
|
+
const preview_upload_TEXTUAL = /\.(json|js|mjs|cjs|ts|tsx|jsx|html|htm|css|svg|txt|md|map)$/i;
|
|
6352
6948
|
const MAX_FILES = 2000;
|
|
6353
6949
|
const MAX_CONTENT_CHARS = 67108864;
|
|
6354
6950
|
function encodeFile(relativePath, bytes) {
|
|
6355
|
-
if (
|
|
6951
|
+
if (preview_upload_TEXTUAL.test(relativePath)) {
|
|
6356
6952
|
const text = bytes.toString("utf8");
|
|
6357
6953
|
if (Buffer.from(text, "utf8").equals(bytes)) return {
|
|
6358
6954
|
content: text,
|
|
@@ -6518,6 +7114,7 @@ async function uploadPreview(options) {
|
|
|
6518
7114
|
};
|
|
6519
7115
|
return {
|
|
6520
7116
|
ok: true,
|
|
7117
|
+
body: data,
|
|
6521
7118
|
data: {
|
|
6522
7119
|
artifactId,
|
|
6523
7120
|
previewUrl,
|
|
@@ -6806,6 +7403,21 @@ function localLaneRemedy(checkout) {
|
|
|
6806
7403
|
const shareOut = "Pass share:true for a link that needs no local server and opens for anyone.";
|
|
6807
7404
|
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
7405
|
}
|
|
7406
|
+
function unrequestedShare(args) {
|
|
7407
|
+
const call = {
|
|
7408
|
+
projectPath: args.projectPath
|
|
7409
|
+
};
|
|
7410
|
+
if (args.browser) call.browser = args.browser;
|
|
7411
|
+
if (false === args.build) call.build = false;
|
|
7412
|
+
if (args.distPath) call.distPath = args.distPath;
|
|
7413
|
+
call.share = true;
|
|
7414
|
+
return {
|
|
7415
|
+
requested: false,
|
|
7416
|
+
localLinkNeeds: `deepLink renders only against a ${SURFACE.label} dev server running on this machine, so to anyone else it is a dead link.`,
|
|
7417
|
+
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.",
|
|
7418
|
+
shareCall: `extension_preview_web ${JSON.stringify(call)}`
|
|
7419
|
+
};
|
|
7420
|
+
}
|
|
6809
7421
|
function previewOriginOf(previewUrl) {
|
|
6810
7422
|
try {
|
|
6811
7423
|
return new URL(previewUrl).origin;
|
|
@@ -6883,6 +7495,10 @@ async function buildShare(projectPath, distDir, manifest, browser, verifyInBrows
|
|
|
6883
7495
|
} : {},
|
|
6884
7496
|
serves: "uploaded-local-build",
|
|
6885
7497
|
localBuildUploaded: true,
|
|
7498
|
+
allowance: spendNarration({
|
|
7499
|
+
what: "This share upload",
|
|
7500
|
+
body: result.body
|
|
7501
|
+
}),
|
|
6886
7502
|
...browserCheck ? browserCheck.held ? {
|
|
6887
7503
|
browserLoadable: null,
|
|
6888
7504
|
heldFromPublic: true,
|
|
@@ -6959,7 +7575,7 @@ const preview_web_schema = {
|
|
|
6959
7575
|
share: {
|
|
6960
7576
|
type: "boolean",
|
|
6961
7577
|
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."
|
|
7578
|
+
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
7579
|
}
|
|
6964
7580
|
},
|
|
6965
7581
|
required: [
|
|
@@ -7064,6 +7680,7 @@ async function preview_web_handler(args) {
|
|
|
7064
7680
|
built: false
|
|
7065
7681
|
}
|
|
7066
7682
|
};
|
|
7683
|
+
if (!args.share) result.share = unrequestedShare(args);
|
|
7067
7684
|
const checkout = previewDevCheckout([
|
|
7068
7685
|
args.projectPath,
|
|
7069
7686
|
process.cwd()
|
|
@@ -8112,6 +8729,81 @@ function verdictSentence(verdict) {
|
|
|
8112
8729
|
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
8730
|
return `${parts.join(", ")}.`;
|
|
8114
8731
|
}
|
|
8732
|
+
const execFileAsync = promisify(execFile);
|
|
8733
|
+
const SAFARI_MCP_SETTING = 'Safari > Settings > Developer > "Allow remote automation and external agents"';
|
|
8734
|
+
const SAFARI_MCP_ADD_COMMAND = 'claude mcp add safari-mcp -- "/usr/bin/safaridriver" --mcp';
|
|
8735
|
+
function parseSafariDriverHelp(helpText) {
|
|
8736
|
+
const flags = new Set(Array.from(helpText.matchAll(/(?:^|\s)--([a-z][a-z-]*)/g)).map((m)=>m[1]));
|
|
8737
|
+
return {
|
|
8738
|
+
mcp: flags.has("mcp"),
|
|
8739
|
+
bidi: flags.has("bidi")
|
|
8740
|
+
};
|
|
8741
|
+
}
|
|
8742
|
+
function safariDriverCandidates(safariBinary) {
|
|
8743
|
+
const candidates = [];
|
|
8744
|
+
if (safariBinary) candidates.push(node_path.join(node_path.dirname(safariBinary), "safaridriver"));
|
|
8745
|
+
candidates.push("/usr/bin/safaridriver");
|
|
8746
|
+
return Array.from(new Set(candidates));
|
|
8747
|
+
}
|
|
8748
|
+
async function readSafariDriverHelp(driverPath) {
|
|
8749
|
+
try {
|
|
8750
|
+
const { stdout, stderr } = await execFileAsync(driverPath, [
|
|
8751
|
+
"--help"
|
|
8752
|
+
], {
|
|
8753
|
+
timeout: 5000,
|
|
8754
|
+
env: {
|
|
8755
|
+
...process.env
|
|
8756
|
+
}
|
|
8757
|
+
});
|
|
8758
|
+
return `${stdout}\n${stderr}`;
|
|
8759
|
+
} catch (error) {
|
|
8760
|
+
const failed = error;
|
|
8761
|
+
const text = `${failed.stdout ?? ""}\n${failed.stderr ?? ""}`.trim();
|
|
8762
|
+
return text.length > 0 ? text : null;
|
|
8763
|
+
}
|
|
8764
|
+
}
|
|
8765
|
+
async function detectSafariAutomation(safariBinary) {
|
|
8766
|
+
if ("darwin" !== process.platform) return {
|
|
8767
|
+
safaridriver: null,
|
|
8768
|
+
mcp: false,
|
|
8769
|
+
bidi: false
|
|
8770
|
+
};
|
|
8771
|
+
for (const candidate of safariDriverCandidates(safariBinary)){
|
|
8772
|
+
if (!node_fs.existsSync(candidate)) continue;
|
|
8773
|
+
const help = await readSafariDriverHelp(candidate);
|
|
8774
|
+
if (null !== help) return {
|
|
8775
|
+
safaridriver: candidate,
|
|
8776
|
+
...parseSafariDriverHelp(help)
|
|
8777
|
+
};
|
|
8778
|
+
}
|
|
8779
|
+
return {
|
|
8780
|
+
safaridriver: null,
|
|
8781
|
+
mcp: false,
|
|
8782
|
+
bidi: false
|
|
8783
|
+
};
|
|
8784
|
+
}
|
|
8785
|
+
async function readSafariVersion(safariBinary) {
|
|
8786
|
+
const plist = node_path.resolve(safariBinary, "..", "..", "Info.plist");
|
|
8787
|
+
if (!node_fs.existsSync(plist)) return null;
|
|
8788
|
+
try {
|
|
8789
|
+
const { stdout } = await execFileAsync("/usr/bin/defaults", [
|
|
8790
|
+
"read",
|
|
8791
|
+
plist,
|
|
8792
|
+
"CFBundleShortVersionString"
|
|
8793
|
+
], {
|
|
8794
|
+
timeout: 5000
|
|
8795
|
+
});
|
|
8796
|
+
const version = stdout.trim();
|
|
8797
|
+
return /^\d+(\.\d+)*$/.test(version) ? version : null;
|
|
8798
|
+
} catch {
|
|
8799
|
+
return null;
|
|
8800
|
+
}
|
|
8801
|
+
}
|
|
8802
|
+
function safariAutomationHint(automation) {
|
|
8803
|
+
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.`;
|
|
8804
|
+
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.`;
|
|
8805
|
+
return "No safaridriver found beside Safari, so no automation reading is possible for it from this machine.";
|
|
8806
|
+
}
|
|
8115
8807
|
function makeFilter(args) {
|
|
8116
8808
|
const level = String(args.level || "all").toLowerCase();
|
|
8117
8809
|
const loggingOff = "off" === level;
|
|
@@ -8195,6 +8887,16 @@ async function environmentPreflight() {
|
|
|
8195
8887
|
status: cacheExists ? "pass" : "warn",
|
|
8196
8888
|
detail: cacheExists ? `Template catalog cached at ${cacheFile}` : "Template catalog not cached yet (extension_templates will fetch it)"
|
|
8197
8889
|
});
|
|
8890
|
+
if ("darwin" === process.platform) {
|
|
8891
|
+
const safariBinary = "/Applications/Safari.app/Contents/MacOS/Safari";
|
|
8892
|
+
const automation = await detectSafariAutomation(node_fs.existsSync(safariBinary) ? safariBinary : null);
|
|
8893
|
+
checks.push({
|
|
8894
|
+
check: "safari-agent",
|
|
8895
|
+
status: automation.mcp ? "pass" : "warn",
|
|
8896
|
+
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",
|
|
8897
|
+
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."
|
|
8898
|
+
});
|
|
8899
|
+
}
|
|
8198
8900
|
const healthy = checks.every((c)=>"fail" !== c.status);
|
|
8199
8901
|
return envelope_envelope({
|
|
8200
8902
|
ok: healthy,
|
|
@@ -8324,6 +9026,19 @@ async function doctor_handler(args) {
|
|
|
8324
9026
|
} : {}
|
|
8325
9027
|
});
|
|
8326
9028
|
}
|
|
9029
|
+
if (WEBKIT_FAMILY.has(browser)) {
|
|
9030
|
+
const info = readWebDriverSession(projectPath, browser);
|
|
9031
|
+
const alive = info ? await new WebDriverClient(info).alive() : false;
|
|
9032
|
+
if (info && !alive) healthy = false;
|
|
9033
|
+
checks.push({
|
|
9034
|
+
check: "safari-window",
|
|
9035
|
+
status: info ? alive ? "pass" : "fail" : "skip",
|
|
9036
|
+
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",
|
|
9037
|
+
...info && !alive ? {
|
|
9038
|
+
remediation: "Stop and restart extension_dev --browser=safari; the session that recorded the window opens it again on the first package."
|
|
9039
|
+
} : {}
|
|
9040
|
+
});
|
|
9041
|
+
}
|
|
8327
9042
|
const failures = checks.filter((leg)=>"fail" === leg.status);
|
|
8328
9043
|
const readOnly = null !== readOnlyLeg && 1 === failures.length && failures[0] === readOnlyLeg;
|
|
8329
9044
|
if (readOnly && readOnlyLeg) {
|
|
@@ -8740,6 +9455,16 @@ async function readFromStream(args, browser, limit) {
|
|
|
8740
9455
|
async function logs_handler(args) {
|
|
8741
9456
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
8742
9457
|
const limit = args.limit && args.limit > 0 ? args.limit : DEFAULT_LIMIT;
|
|
9458
|
+
if (WEBKIT_FAMILY.has(browser) && !node_fs.existsSync(logsPath(args.projectPath, browser))) return envelope_envelope({
|
|
9459
|
+
ok: false,
|
|
9460
|
+
command: TOOL,
|
|
9461
|
+
status: "no-log-file",
|
|
9462
|
+
error: {
|
|
9463
|
+
code: "E_LOGS_MISSING",
|
|
9464
|
+
message: `No logs found at ${logsPath(args.projectPath, browser)}, and ${browser} has no CDP or RDP to read instead.`
|
|
9465
|
+
},
|
|
9466
|
+
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."
|
|
9467
|
+
});
|
|
8743
9468
|
if (args.follow) return readFromStream(args, browser, limit);
|
|
8744
9469
|
return readFromFile(args, browser, limit);
|
|
8745
9470
|
}
|
|
@@ -8916,6 +9641,10 @@ function parseClauses(raw) {
|
|
|
8916
9641
|
}
|
|
8917
9642
|
const NO_SESSION_SETTLED_BY = "Start the session with extension_dev, confirm it with extension_wait, then assert again.";
|
|
8918
9643
|
class Stage {
|
|
9644
|
+
webdriver() {
|
|
9645
|
+
const info = readWebDriverSession(this.projectPath, this.browser);
|
|
9646
|
+
return info ? new WebDriverClient(info) : null;
|
|
9647
|
+
}
|
|
8919
9648
|
async port() {
|
|
8920
9649
|
if (void 0 === this.cdpPort) {
|
|
8921
9650
|
const resolved = await resolveCdpPort(this.projectPath, this.browser);
|
|
@@ -8983,6 +9712,7 @@ class Stage {
|
|
|
8983
9712
|
assert_define_property(this, "browser", void 0);
|
|
8984
9713
|
assert_define_property(this, "timeout", void 0);
|
|
8985
9714
|
assert_define_property(this, "chromium", void 0);
|
|
9715
|
+
assert_define_property(this, "webkit", void 0);
|
|
8986
9716
|
assert_define_property(this, "cdpPort", void 0);
|
|
8987
9717
|
assert_define_property(this, "discovered", void 0);
|
|
8988
9718
|
assert_define_property(this, "manifestRead", void 0);
|
|
@@ -8994,6 +9724,7 @@ class Stage {
|
|
|
8994
9724
|
this.discovered = null;
|
|
8995
9725
|
this.client = null;
|
|
8996
9726
|
this.chromium = isChromiumFamily(browser);
|
|
9727
|
+
this.webkit = WEBKIT_FAMILY.has(browser);
|
|
8997
9728
|
}
|
|
8998
9729
|
}
|
|
8999
9730
|
function truncate(value, max = 200) {
|
|
@@ -9125,12 +9856,77 @@ async function assertContentScriptInjected(clause, stage) {
|
|
|
9125
9856
|
runId,
|
|
9126
9857
|
coveringMatches: covering
|
|
9127
9858
|
});
|
|
9859
|
+
if (stage.webkit) return assertContentScriptInjectedOnWebKit(clause, stage, {
|
|
9860
|
+
covering,
|
|
9861
|
+
patterns
|
|
9862
|
+
});
|
|
9128
9863
|
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
9864
|
coveringMatches: covering,
|
|
9130
9865
|
declaredMatches: patterns,
|
|
9131
9866
|
runId
|
|
9132
9867
|
});
|
|
9133
9868
|
}
|
|
9869
|
+
async function assertBackgroundOnWebKit(clause, stage) {
|
|
9870
|
+
const id = BACKGROUND;
|
|
9871
|
+
const subject = clause.subject;
|
|
9872
|
+
const runId = readLogRunId(stage.projectPath, stage.browser);
|
|
9873
|
+
const stale = staleFileNote(stage.projectPath, stage.browser, runId);
|
|
9874
|
+
const lines = readLogEvents(stage.projectPath, stage.browser, {
|
|
9875
|
+
context: [
|
|
9876
|
+
"background"
|
|
9877
|
+
]
|
|
9878
|
+
});
|
|
9879
|
+
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.`, {
|
|
9880
|
+
lines: lines.length,
|
|
9881
|
+
runId
|
|
9882
|
+
});
|
|
9883
|
+
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.", {
|
|
9884
|
+
runId
|
|
9885
|
+
});
|
|
9886
|
+
}
|
|
9887
|
+
async function assertContentScriptInjectedOnWebKit(clause, stage, declared) {
|
|
9888
|
+
const id = CONTENT_SCRIPT;
|
|
9889
|
+
const subject = clause.subject;
|
|
9890
|
+
const client = stage.webdriver();
|
|
9891
|
+
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}`, {
|
|
9892
|
+
coveringMatches: declared.covering,
|
|
9893
|
+
declaredMatches: declared.patterns
|
|
9894
|
+
});
|
|
9895
|
+
let reading;
|
|
9896
|
+
try {
|
|
9897
|
+
const current = await client.currentUrl();
|
|
9898
|
+
if (!sameDocument(current, clause.url)) {
|
|
9899
|
+
await client.navigate(clause.url);
|
|
9900
|
+
await new Promise((resolve)=>setTimeout(resolve, 1200));
|
|
9901
|
+
}
|
|
9902
|
+
reading = await readExtensionRoots(client);
|
|
9903
|
+
} catch (error) {
|
|
9904
|
+
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.", {
|
|
9905
|
+
coveringMatches: declared.covering,
|
|
9906
|
+
declaredMatches: declared.patterns
|
|
9907
|
+
});
|
|
9908
|
+
}
|
|
9909
|
+
const extensionId = readyExtensionId(stage.projectPath, stage.browser) ?? await stage.extensionId();
|
|
9910
|
+
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.", {
|
|
9911
|
+
roots: reading.roots,
|
|
9912
|
+
owners: reading.owners,
|
|
9913
|
+
url: reading.url
|
|
9914
|
+
});
|
|
9915
|
+
const owned = reading.owners.filter((owner)=>owner.length > 0 && owner.includes(extensionId));
|
|
9916
|
+
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.`, {
|
|
9917
|
+
roots: reading.roots,
|
|
9918
|
+
owners: owned,
|
|
9919
|
+
url: reading.url,
|
|
9920
|
+
coveringMatches: declared.covering
|
|
9921
|
+
});
|
|
9922
|
+
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.", {
|
|
9923
|
+
roots: reading.roots,
|
|
9924
|
+
owners: reading.owners,
|
|
9925
|
+
url: reading.url,
|
|
9926
|
+
coveringMatches: declared.covering,
|
|
9927
|
+
declaredMatches: declared.patterns
|
|
9928
|
+
});
|
|
9929
|
+
}
|
|
9134
9930
|
function readStorageValue(value, key) {
|
|
9135
9931
|
if (null == value) return {
|
|
9136
9932
|
shape: "absent"
|
|
@@ -9244,6 +10040,7 @@ function assertConsoleErrorsEmpty(clause, stage) {
|
|
|
9244
10040
|
});
|
|
9245
10041
|
}
|
|
9246
10042
|
async function evaluateClause(clause, stage) {
|
|
10043
|
+
if (stage.webkit && clause.assert === BACKGROUND) return assertBackgroundOnWebKit(clause, stage);
|
|
9247
10044
|
switch(clause.assert){
|
|
9248
10045
|
case BACKGROUND:
|
|
9249
10046
|
return assertBackgroundWorker(clause, stage);
|
|
@@ -11847,7 +12644,7 @@ async function listGeckoExtensions(projectPath, browser) {
|
|
|
11847
12644
|
}
|
|
11848
12645
|
const eval_schema = {
|
|
11849
12646
|
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
|
|
12647
|
+
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
12648
|
inputSchema: {
|
|
11852
12649
|
type: "object",
|
|
11853
12650
|
properties: {
|
|
@@ -11889,15 +12686,8 @@ const eval_schema = {
|
|
|
11889
12686
|
]
|
|
11890
12687
|
}
|
|
11891
12688
|
};
|
|
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){
|
|
12689
|
+
function chromiumManifestVersion(projectPath, browser) {
|
|
12690
|
+
for (const file of manifestCandidates(projectPath, browser)){
|
|
11901
12691
|
let manifest;
|
|
11902
12692
|
try {
|
|
11903
12693
|
manifest = JSON.parse(node_fs.readFileSync(file, "utf8"));
|
|
@@ -11905,15 +12695,154 @@ function resolveDefaultEvalContext(projectPath, browser) {
|
|
|
11905
12695
|
continue;
|
|
11906
12696
|
}
|
|
11907
12697
|
const version = manifest["chromium:manifest_version"] ?? manifest.manifest_version;
|
|
11908
|
-
if (3 === version) return
|
|
11909
|
-
if (2 === version)
|
|
12698
|
+
if (3 === version) return 3;
|
|
12699
|
+
if (2 === version) return 2;
|
|
12700
|
+
}
|
|
12701
|
+
return null;
|
|
12702
|
+
}
|
|
12703
|
+
function resolveDefaultEvalContext(projectPath, browser) {
|
|
12704
|
+
if (!isChromiumFamily(browser)) return "background";
|
|
12705
|
+
return 3 === chromiumManifestVersion(projectPath, browser) ? "page" : "background";
|
|
12706
|
+
}
|
|
12707
|
+
const EXTENSION_PAGE_CONTEXTS = [
|
|
12708
|
+
"popup",
|
|
12709
|
+
"options",
|
|
12710
|
+
"sidebar",
|
|
12711
|
+
"newtab",
|
|
12712
|
+
"history",
|
|
12713
|
+
"bookmarks"
|
|
12714
|
+
];
|
|
12715
|
+
function wantsExtensionPageOverCdp(projectPath, browser, context, url) {
|
|
12716
|
+
if (!isChromiumFamily(browser) || !context) return false;
|
|
12717
|
+
if ("page" === context) return "string" == typeof url && /^chrome-extension:\/\//.test(url);
|
|
12718
|
+
return EXTENSION_PAGE_CONTEXTS.includes(context) && 3 === chromiumManifestVersion(projectPath, browser);
|
|
12719
|
+
}
|
|
12720
|
+
async function evaluateOnChromiumExtensionPage(args, browser, context) {
|
|
12721
|
+
const resolved = await resolveCdpPort(args.projectPath, browser);
|
|
12722
|
+
if (!resolved) return envelope_envelope({
|
|
12723
|
+
ok: false,
|
|
12724
|
+
command: eval_schema.name,
|
|
12725
|
+
status: "no-session",
|
|
12726
|
+
error: {
|
|
12727
|
+
code: "E_NO_SESSION",
|
|
12728
|
+
name: "NoSession",
|
|
12729
|
+
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}`
|
|
12730
|
+
}
|
|
12731
|
+
});
|
|
12732
|
+
let wanted;
|
|
12733
|
+
if ("page" === context) wanted = args.url;
|
|
12734
|
+
else {
|
|
12735
|
+
const doc = surfaceDocument(args.projectPath, browser, context);
|
|
12736
|
+
if (!doc) {
|
|
12737
|
+
const key = SURFACE_MANIFEST_KEYS[context] ?? context;
|
|
12738
|
+
return envelope_envelope({
|
|
12739
|
+
ok: false,
|
|
12740
|
+
command: eval_schema.name,
|
|
12741
|
+
status: "no-surface",
|
|
12742
|
+
error: {
|
|
12743
|
+
code: "E_NO_SURFACE_DOCUMENT",
|
|
12744
|
+
name: "NoSurfaceDocument",
|
|
12745
|
+
message: `This extension declares no ${context}: nothing in its manifest sets ${key}, so there is no ${context} page to evaluate in.`
|
|
12746
|
+
},
|
|
12747
|
+
hint: `To add one, set ${key} in the manifest and rebuild.`
|
|
12748
|
+
});
|
|
12749
|
+
}
|
|
12750
|
+
const extensionId = await resolveExtensionId(args.projectPath, browser);
|
|
12751
|
+
if (!extensionId) return envelope_envelope({
|
|
12752
|
+
ok: false,
|
|
12753
|
+
command: eval_schema.name,
|
|
12754
|
+
status: "no-extension-id",
|
|
12755
|
+
error: {
|
|
12756
|
+
code: "E_NO_EXTENSION_ID",
|
|
12757
|
+
name: "NoExtensionId",
|
|
12758
|
+
message: "Could not resolve the extension id from the live session's CDP targets."
|
|
12759
|
+
},
|
|
12760
|
+
hint: `Confirm the session is ready (extension_wait). ${CDP_PORT_MISSING_HINT}`
|
|
12761
|
+
});
|
|
12762
|
+
wanted = `chrome-extension://${extensionId}/${doc}`;
|
|
12763
|
+
}
|
|
12764
|
+
const targets = await findExtensionPageTargets(resolved.port, wanted);
|
|
12765
|
+
if (0 === targets.length) return envelope_envelope({
|
|
12766
|
+
ok: false,
|
|
12767
|
+
command: eval_schema.name,
|
|
12768
|
+
status: "no-target",
|
|
12769
|
+
error: {
|
|
12770
|
+
code: "E_NO_TARGET",
|
|
12771
|
+
name: "NoTarget",
|
|
12772
|
+
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.`
|
|
12773
|
+
},
|
|
12774
|
+
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.`
|
|
12775
|
+
});
|
|
12776
|
+
const target = targets[0];
|
|
12777
|
+
const outcome = await evaluateOnExtensionPage(resolved.port, target.targetId, args.expression);
|
|
12778
|
+
const others = targets.slice(1);
|
|
12779
|
+
const warnings = others.length ? [
|
|
12780
|
+
`${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.`
|
|
12781
|
+
] : [];
|
|
12782
|
+
if (!outcome.ok) return envelope_envelope({
|
|
12783
|
+
ok: false,
|
|
12784
|
+
command: eval_schema.name,
|
|
12785
|
+
status: outcome.thrown ? "eval-failed" : "cdp-failed",
|
|
12786
|
+
error: {
|
|
12787
|
+
code: outcome.thrown ? "E_EVAL" : "E_CDP",
|
|
12788
|
+
name: outcome.thrown ? "EvalError" : "CdpError",
|
|
12789
|
+
message: outcome.message
|
|
12790
|
+
},
|
|
12791
|
+
warnings,
|
|
12792
|
+
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."
|
|
12793
|
+
});
|
|
12794
|
+
return envelope_envelope({
|
|
12795
|
+
ok: true,
|
|
12796
|
+
command: eval_schema.name,
|
|
12797
|
+
status: "evaluated",
|
|
12798
|
+
value: outcome.value,
|
|
12799
|
+
warnings,
|
|
12800
|
+
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.`
|
|
12801
|
+
});
|
|
12802
|
+
}
|
|
12803
|
+
async function evaluateOnWebKitPage(args, browser, info) {
|
|
12804
|
+
const client = new WebDriverClient(info);
|
|
12805
|
+
if (args.url) {
|
|
12806
|
+
const current = await client.currentUrl().catch(()=>null);
|
|
12807
|
+
if (!current || !current.startsWith(args.url)) await client.navigate(args.url);
|
|
12808
|
+
}
|
|
12809
|
+
try {
|
|
12810
|
+
const value = await client.execute(`return (${args.expression});`);
|
|
12811
|
+
return envelope_envelope({
|
|
12812
|
+
ok: true,
|
|
12813
|
+
command: eval_schema.name,
|
|
12814
|
+
status: "evaluated",
|
|
12815
|
+
value: {
|
|
12816
|
+
context: "page",
|
|
12817
|
+
browser,
|
|
12818
|
+
url: await client.currentUrl().catch(()=>null),
|
|
12819
|
+
result: value
|
|
12820
|
+
},
|
|
12821
|
+
hint: "Evaluated in the Safari automation window's main world over the dev session's WebDriver connection."
|
|
12822
|
+
});
|
|
12823
|
+
} catch (error) {
|
|
12824
|
+
return envelope_envelope({
|
|
12825
|
+
ok: false,
|
|
12826
|
+
command: eval_schema.name,
|
|
12827
|
+
status: "eval-failed",
|
|
12828
|
+
error: {
|
|
12829
|
+
code: "E_EVAL",
|
|
12830
|
+
name: "EvalError",
|
|
12831
|
+
message: error instanceof Error ? error.message : String(error)
|
|
12832
|
+
},
|
|
12833
|
+
hint: "The expression threw, or the automation window is gone. extension_doctor names which; a session that ended needs extension_dev again."
|
|
12834
|
+
});
|
|
11910
12835
|
}
|
|
11911
|
-
return "background";
|
|
11912
12836
|
}
|
|
11913
12837
|
async function eval_handler(args) {
|
|
11914
12838
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
12839
|
+
if (WEBKIT_FAMILY.has(browser) && "page" === args.context) {
|
|
12840
|
+
const info = readWebDriverSession(args.projectPath, browser);
|
|
12841
|
+
if (info) return evaluateOnWebKitPage(args, browser, info);
|
|
12842
|
+
}
|
|
11915
12843
|
const defaulted = !args.context && "page" === resolveDefaultEvalContext(args.projectPath, browser);
|
|
11916
12844
|
const context = defaulted ? "page" : args.context;
|
|
12845
|
+
if (wantsExtensionPageOverCdp(args.projectPath, browser, context, args.url)) return evaluateOnChromiumExtensionPage(args, browser, context);
|
|
11917
12846
|
const raw = await runActVerb([
|
|
11918
12847
|
"eval",
|
|
11919
12848
|
...commonFlags({
|
|
@@ -12075,24 +13004,6 @@ async function reload_handler(args) {
|
|
|
12075
13004
|
})
|
|
12076
13005
|
], args.projectPath, args.timeout, reload_schema.name);
|
|
12077
13006
|
}
|
|
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
13007
|
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
13008
|
const dom_snapshot_schema = {
|
|
12098
13009
|
name: "extension_dom_snapshot",
|
|
@@ -12542,6 +13453,11 @@ async function publish_handler(args) {
|
|
|
12542
13453
|
}
|
|
12543
13454
|
}
|
|
12544
13455
|
}
|
|
13456
|
+
data.allowance = spendNarration({
|
|
13457
|
+
what: "This publish",
|
|
13458
|
+
body: data,
|
|
13459
|
+
api: args.api
|
|
13460
|
+
});
|
|
12545
13461
|
return envelope_envelope({
|
|
12546
13462
|
ok: true,
|
|
12547
13463
|
command: "extension_publish",
|
|
@@ -12726,6 +13642,11 @@ async function release_promote_handler(args) {
|
|
|
12726
13642
|
const promotedRef = resolveProjectRef();
|
|
12727
13643
|
const publicChannelUrl = userlandProjectUrl(promotedRef, UserlandProjectPage.channel(channel), args.api);
|
|
12728
13644
|
const publicBuildUrl = userlandProjectUrl(promotedRef, UserlandProjectPage.build(buildId), args.api);
|
|
13645
|
+
const allowance = spendNarration({
|
|
13646
|
+
what: "This promote",
|
|
13647
|
+
body: data,
|
|
13648
|
+
api: args.api
|
|
13649
|
+
});
|
|
12729
13650
|
const enriched = data && "object" == typeof data && !Array.isArray(data) ? {
|
|
12730
13651
|
...data,
|
|
12731
13652
|
...publicChannelUrl ? {
|
|
@@ -12733,8 +13654,12 @@ async function release_promote_handler(args) {
|
|
|
12733
13654
|
} : {},
|
|
12734
13655
|
...publicBuildUrl ? {
|
|
12735
13656
|
publicBuildUrl
|
|
12736
|
-
} : {}
|
|
12737
|
-
|
|
13657
|
+
} : {},
|
|
13658
|
+
allowance
|
|
13659
|
+
} : {
|
|
13660
|
+
platform: data,
|
|
13661
|
+
allowance
|
|
13662
|
+
};
|
|
12738
13663
|
return envelope_envelope({
|
|
12739
13664
|
ok: true,
|
|
12740
13665
|
command: "extension_release_promote",
|
|
@@ -13457,7 +14382,14 @@ async function submit_handler(args) {
|
|
|
13457
14382
|
if ("string" == typeof data?.message) result.platformMessage = data.message;
|
|
13458
14383
|
message = summaryParts.join(" ");
|
|
13459
14384
|
}
|
|
13460
|
-
if (!dryRun)
|
|
14385
|
+
if (!dryRun) {
|
|
14386
|
+
statusNote = "Track this submission with extension_release_status: it reads the recorded outcome, per-store credential health, and review state from the public registry.";
|
|
14387
|
+
if (platformOk) result.allowance = spendNarration({
|
|
14388
|
+
what: "This submission",
|
|
14389
|
+
body: data,
|
|
14390
|
+
api: args.api
|
|
14391
|
+
});
|
|
14392
|
+
}
|
|
13461
14393
|
return envelope_envelope({
|
|
13462
14394
|
ok,
|
|
13463
14395
|
command: "extension_submit",
|
|
@@ -14277,7 +15209,7 @@ async function auth_handler(args) {
|
|
|
14277
15209
|
});
|
|
14278
15210
|
return readIdentity();
|
|
14279
15211
|
}
|
|
14280
|
-
const
|
|
15212
|
+
const detect_browsers_execFileAsync = promisify(execFile);
|
|
14281
15213
|
const ALL_BROWSERS = [
|
|
14282
15214
|
"chrome",
|
|
14283
15215
|
"chromium",
|
|
@@ -14511,7 +15443,7 @@ function findSystemBinary(browser) {
|
|
|
14511
15443
|
async function getVersion(binaryPath, browser) {
|
|
14512
15444
|
try {
|
|
14513
15445
|
const flag = "firefox" === browser ? "--version" : "--version";
|
|
14514
|
-
const { stdout } = await
|
|
15446
|
+
const { stdout } = await detect_browsers_execFileAsync(binaryPath, [
|
|
14515
15447
|
flag
|
|
14516
15448
|
], {
|
|
14517
15449
|
timeout: 5000,
|
|
@@ -14549,6 +15481,8 @@ async function detectBrowsers(browsers) {
|
|
|
14549
15481
|
}
|
|
14550
15482
|
let version = null;
|
|
14551
15483
|
if (binaryPath && !isWebkit) version = await getVersion(binaryPath, browser);
|
|
15484
|
+
else if (binaryPath && isWebkit) version = await readSafariVersion(binaryPath);
|
|
15485
|
+
const automation = isWebkit && binaryPath ? await detectSafariAutomation(binaryPath) : null;
|
|
14552
15486
|
detected.push({
|
|
14553
15487
|
browser,
|
|
14554
15488
|
binaryPath,
|
|
@@ -14556,11 +15490,16 @@ async function detectBrowsers(browsers) {
|
|
|
14556
15490
|
engine: isWebkit ? "webkit" : isGecko ? "gecko" : "chromium",
|
|
14557
15491
|
version,
|
|
14558
15492
|
cdpSupport: !isGecko && !isWebkit,
|
|
14559
|
-
rdpSupport: isGecko
|
|
15493
|
+
rdpSupport: isGecko,
|
|
15494
|
+
...automation ? {
|
|
15495
|
+
automation
|
|
15496
|
+
} : {}
|
|
14560
15497
|
});
|
|
14561
15498
|
}
|
|
14562
15499
|
const available = detected.filter((d)=>"not_found" !== d.source);
|
|
14563
15500
|
const missing = detected.filter((d)=>"not_found" === d.source);
|
|
15501
|
+
const safari = detected.find((d)=>d.automation);
|
|
15502
|
+
const safariHint = safari?.automation ? ` ${safariAutomationHint(safari.automation)}` : "";
|
|
14564
15503
|
return envelope_envelope({
|
|
14565
15504
|
ok: true,
|
|
14566
15505
|
command: "extension_browsers",
|
|
@@ -14573,7 +15512,7 @@ async function detectBrowsers(browsers) {
|
|
|
14573
15512
|
missing: missing.map((d)=>d.browser)
|
|
14574
15513
|
}
|
|
14575
15514
|
},
|
|
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(", ")}.` : ""}` :
|
|
15515
|
+
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
15516
|
});
|
|
14578
15517
|
}
|
|
14579
15518
|
const BROWSER_NAMES = [
|
|
@@ -14988,15 +15927,16 @@ function toolResultFrame(result) {
|
|
|
14988
15927
|
}
|
|
14989
15928
|
const toolMap = new Map();
|
|
14990
15929
|
for (const tool of tools)toolMap.set(tool.schema.name, tool);
|
|
14991
|
-
|
|
14992
|
-
|
|
15930
|
+
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.";
|
|
15931
|
+
function createServer() {
|
|
14993
15932
|
const server = new Server({
|
|
14994
15933
|
name: "extension-dev",
|
|
14995
15934
|
version: package_namespaceObject.rE
|
|
14996
15935
|
}, {
|
|
14997
15936
|
capabilities: {
|
|
14998
15937
|
tools: {}
|
|
14999
|
-
}
|
|
15938
|
+
},
|
|
15939
|
+
instructions: SERVER_INSTRUCTIONS
|
|
15000
15940
|
});
|
|
15001
15941
|
server.setRequestHandler(ListToolsRequestSchema, async ()=>({
|
|
15002
15942
|
tools: tools.map((t)=>({
|
|
@@ -15063,6 +16003,11 @@ async function startServer() {
|
|
|
15063
16003
|
};
|
|
15064
16004
|
}
|
|
15065
16005
|
});
|
|
16006
|
+
return server;
|
|
16007
|
+
}
|
|
16008
|
+
async function startServer() {
|
|
16009
|
+
installCarrierExitCleanup();
|
|
16010
|
+
const server = createServer();
|
|
15066
16011
|
const transport = new StdioServerTransport();
|
|
15067
16012
|
await server.connect(transport);
|
|
15068
16013
|
}
|