@extension.dev/mcp 9.0.0 → 10.0.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 +47 -0
- package/claude/rules/mcp-tools.md +22 -10
- package/dist/module.js +2083 -1020
- package/dist/src/lib/act.d.ts +4 -1
- package/dist/src/lib/boot-verdict.d.ts +53 -0
- package/dist/src/lib/bridge-tabs.d.ts +2 -2
- package/dist/src/lib/envelope.d.ts +35 -0
- package/dist/src/lib/legacy-stdout.d.ts +7 -0
- package/package.json +4 -4
- package/server.json +2 -2
package/dist/module.js
CHANGED
|
@@ -579,7 +579,7 @@ __webpack_require__.d(wait_namespaceObject, {
|
|
|
579
579
|
handler: ()=>wait_handler,
|
|
580
580
|
schema: ()=>wait_schema
|
|
581
581
|
});
|
|
582
|
-
var package_namespaceObject = JSON.parse('{"rE":"
|
|
582
|
+
var package_namespaceObject = JSON.parse('{"rE":"10.0.0","El":{"OP":"4.0.17"}}');
|
|
583
583
|
function credentialsPath() {
|
|
584
584
|
if ("win32" === process.platform) {
|
|
585
585
|
const base = process.env.APPDATA || process.env.LOCALAPPDATA || node_path.join(node_os.homedir(), "AppData", "Roaming");
|
|
@@ -1109,6 +1109,38 @@ function parseBuildIndex(json) {
|
|
|
1109
1109
|
}
|
|
1110
1110
|
return out;
|
|
1111
1111
|
}
|
|
1112
|
+
const ENVELOPE_SCHEMA = 1;
|
|
1113
|
+
const collectWarnings = (warnings)=>{
|
|
1114
|
+
if (!warnings) return [];
|
|
1115
|
+
const kept = [];
|
|
1116
|
+
for (const warning of warnings){
|
|
1117
|
+
if ("string" != typeof warning) continue;
|
|
1118
|
+
const text = warning.trim();
|
|
1119
|
+
if (text && !kept.includes(text)) kept.push(text);
|
|
1120
|
+
}
|
|
1121
|
+
return kept;
|
|
1122
|
+
};
|
|
1123
|
+
function envelopeObject(init) {
|
|
1124
|
+
const frame = {
|
|
1125
|
+
schema: ENVELOPE_SCHEMA,
|
|
1126
|
+
ok: init.ok,
|
|
1127
|
+
command: init.command,
|
|
1128
|
+
status: init.status,
|
|
1129
|
+
value: void 0 === init.value ? null : init.value,
|
|
1130
|
+
error: init.error ?? null,
|
|
1131
|
+
warnings: collectWarnings(init.warnings)
|
|
1132
|
+
};
|
|
1133
|
+
if (void 0 !== init.truncated) frame.truncated = init.truncated;
|
|
1134
|
+
if ("string" == typeof init.hint && init.hint) frame.hint = init.hint;
|
|
1135
|
+
return frame;
|
|
1136
|
+
}
|
|
1137
|
+
function envelope(init) {
|
|
1138
|
+
return JSON.stringify(envelopeObject(init));
|
|
1139
|
+
}
|
|
1140
|
+
function isEnvelope(frame) {
|
|
1141
|
+
return !!frame && "object" == typeof frame && frame.schema === ENVELOPE_SCHEMA;
|
|
1142
|
+
}
|
|
1143
|
+
const COMMAND = "extension_create";
|
|
1112
1144
|
function scaffoldEnginePin(projectPath) {
|
|
1113
1145
|
try {
|
|
1114
1146
|
const pkg = JSON.parse(node_fs.readFileSync(node_path.join(projectPath, "package.json"), "utf8"));
|
|
@@ -1146,7 +1178,7 @@ function detectPackageManager(projectPath) {
|
|
|
1146
1178
|
}
|
|
1147
1179
|
const create_schema = {
|
|
1148
1180
|
name: "extension_create",
|
|
1149
|
-
description: "Create a
|
|
1181
|
+
description: "Create a browser extension project from a template in the extension.dev catalog. Call extension_templates first to see what is available. The scaffolder may initialize a git repository in the new project. Read the result's defaultsApplied block for that, and for every other decision made without being asked.",
|
|
1150
1182
|
inputSchema: {
|
|
1151
1183
|
type: "object",
|
|
1152
1184
|
properties: {
|
|
@@ -1205,13 +1237,31 @@ async function create_handler(args) {
|
|
|
1205
1237
|
error: capture("error")
|
|
1206
1238
|
}
|
|
1207
1239
|
});
|
|
1208
|
-
const failure = (err, transient)=>
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1240
|
+
const failure = (err, transient)=>transient ? envelope({
|
|
1241
|
+
ok: false,
|
|
1242
|
+
command: COMMAND,
|
|
1243
|
+
status: "template-fetch-failed",
|
|
1244
|
+
error: {
|
|
1245
|
+
code: "E_TEMPLATE_FETCH",
|
|
1246
|
+
message: "Template download failed (network/timeout/rate-limit). This is not a bad template name. Retry, or check connectivity/GitHub rate limits."
|
|
1247
|
+
},
|
|
1248
|
+
value: {
|
|
1249
|
+
cause: err instanceof Error ? err.message : String(err),
|
|
1250
|
+
duration: Date.now() - start,
|
|
1251
|
+
log: logTail()
|
|
1252
|
+
}
|
|
1253
|
+
}) : envelope({
|
|
1254
|
+
ok: false,
|
|
1255
|
+
command: COMMAND,
|
|
1256
|
+
status: "scaffold-failed",
|
|
1257
|
+
error: {
|
|
1258
|
+
code: "E_SCAFFOLD_FAILED",
|
|
1259
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1260
|
+
},
|
|
1261
|
+
value: {
|
|
1262
|
+
duration: Date.now() - start,
|
|
1263
|
+
log: logTail()
|
|
1264
|
+
}
|
|
1215
1265
|
});
|
|
1216
1266
|
let result;
|
|
1217
1267
|
try {
|
|
@@ -1227,13 +1277,19 @@ async function create_handler(args) {
|
|
|
1227
1277
|
}
|
|
1228
1278
|
}
|
|
1229
1279
|
const hasManifest = node_fs.existsSync(node_path.join(result.projectPath, "manifest.json")) || node_fs.existsSync(node_path.join(result.projectPath, "src", "manifest.json"));
|
|
1230
|
-
if (!hasManifest) return
|
|
1280
|
+
if (!hasManifest) return envelope({
|
|
1231
1281
|
ok: false,
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
error:
|
|
1235
|
-
|
|
1236
|
-
|
|
1282
|
+
command: COMMAND,
|
|
1283
|
+
status: "scaffold-incomplete",
|
|
1284
|
+
error: {
|
|
1285
|
+
code: "E_SCAFFOLD_INCOMPLETE",
|
|
1286
|
+
message: `The scaffold is incomplete: no manifest.json exists under ${result.projectPath} (checked the root and src/). Do not run extension_dev against it.`
|
|
1287
|
+
},
|
|
1288
|
+
value: {
|
|
1289
|
+
projectPath: result.projectPath,
|
|
1290
|
+
duration: Date.now() - start,
|
|
1291
|
+
log: logTail()
|
|
1292
|
+
},
|
|
1237
1293
|
hint: "Delete the directory and retry extension_create; a template download interrupted mid-way can leave a partial tree."
|
|
1238
1294
|
});
|
|
1239
1295
|
const packageManager = result.packageManager || (result.depsInstalled ? detectPackageManager(result.projectPath) : "npm");
|
|
@@ -1249,41 +1305,44 @@ async function create_handler(args) {
|
|
|
1249
1305
|
const deployUrl = `${wwwOrigin}${(0, paths_0.wwwNewPath)({
|
|
1250
1306
|
template: result.template
|
|
1251
1307
|
})}`;
|
|
1252
|
-
return
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1308
|
+
return envelope({
|
|
1309
|
+
ok: true,
|
|
1310
|
+
command: COMMAND,
|
|
1311
|
+
status: "created",
|
|
1312
|
+
value: {
|
|
1313
|
+
resolvedPath: result.projectPath,
|
|
1314
|
+
projectPath: result.projectPath,
|
|
1315
|
+
projectName: result.projectName,
|
|
1316
|
+
template: result.template,
|
|
1317
|
+
depsInstalled: result.depsInstalled,
|
|
1318
|
+
packageManager: result.depsInstalled ? packageManager : null,
|
|
1319
|
+
deployUrl,
|
|
1320
|
+
defaultsApplied: {
|
|
1321
|
+
parentDir: args.parentDir ? `${resolvedParent} (explicit)` : `${resolvedParent} (default: the MCP server process cwd, not yours; pass parentDir to choose)`,
|
|
1322
|
+
...void 0 === args.template ? {
|
|
1323
|
+
template: "typescript (default; call extension_templates to pick another, e.g. javascript for plain JS)"
|
|
1324
|
+
} : {},
|
|
1325
|
+
packageManager: `${packageManager} (auto-detected by the scaffolder, not asked)`,
|
|
1326
|
+
browser: "chrome (default: extension_dev and extension_build target chrome unless you pass browser)",
|
|
1327
|
+
gitInit
|
|
1328
|
+
},
|
|
1329
|
+
duration: Date.now() - start,
|
|
1330
|
+
nextSteps: [
|
|
1331
|
+
...result.depsInstalled ? [
|
|
1332
|
+
`cd ${result.projectPath}`,
|
|
1333
|
+
runDev
|
|
1334
|
+
] : [
|
|
1335
|
+
`cd ${result.projectPath}`,
|
|
1336
|
+
`${packageManager} install`,
|
|
1337
|
+
runDev
|
|
1338
|
+
],
|
|
1339
|
+
`To ship: extension_create scaffolds and runs locally, it does not host. Open ${deployUrl} to deploy this template to the web.`
|
|
1340
|
+
]
|
|
1268
1341
|
},
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
...result.depsInstalled ? [
|
|
1272
|
-
|
|
1273
|
-
runDev
|
|
1274
|
-
] : [
|
|
1275
|
-
`cd ${result.projectPath}`,
|
|
1276
|
-
`${packageManager} install`,
|
|
1277
|
-
runDev
|
|
1278
|
-
],
|
|
1279
|
-
`To ship: extension_create scaffolds and runs locally, it does not host. Open ${deployUrl} to deploy this template to the web.`
|
|
1280
|
-
],
|
|
1281
|
-
...engineWarning ? {
|
|
1282
|
-
engineWarning
|
|
1283
|
-
} : {},
|
|
1284
|
-
...result.depsInstalled ? {} : {
|
|
1285
|
-
warnings: logTail()
|
|
1286
|
-
}
|
|
1342
|
+
warnings: [
|
|
1343
|
+
engineWarning,
|
|
1344
|
+
...result.depsInstalled ? [] : logTail()
|
|
1345
|
+
]
|
|
1287
1346
|
});
|
|
1288
1347
|
}
|
|
1289
1348
|
const DEFAULT_MEDIA_ORIGIN = "https://media.extension.land";
|
|
@@ -1479,15 +1538,27 @@ async function searchTemplates(args) {
|
|
|
1479
1538
|
repositoryUrl: t.repositoryUrl,
|
|
1480
1539
|
downloads: t.downloads
|
|
1481
1540
|
}));
|
|
1482
|
-
return
|
|
1483
|
-
|
|
1484
|
-
|
|
1541
|
+
return envelope({
|
|
1542
|
+
ok: true,
|
|
1543
|
+
command: "extension_templates",
|
|
1544
|
+
status: "listed",
|
|
1545
|
+
value: {
|
|
1546
|
+
count: results.length,
|
|
1547
|
+
templates: results
|
|
1548
|
+
}
|
|
1485
1549
|
});
|
|
1486
1550
|
}
|
|
1551
|
+
const get_template_source_COMMAND = "extension_templates";
|
|
1487
1552
|
async function readTemplateSource(args) {
|
|
1488
1553
|
const template = await getTemplateBySlug(args.slug);
|
|
1489
|
-
if (!template) return
|
|
1490
|
-
|
|
1554
|
+
if (!template) return envelope({
|
|
1555
|
+
ok: false,
|
|
1556
|
+
command: get_template_source_COMMAND,
|
|
1557
|
+
status: "template-not-found",
|
|
1558
|
+
error: {
|
|
1559
|
+
code: "E_TEMPLATE_NOT_FOUND",
|
|
1560
|
+
message: `Template '${args.slug}' not found in the catalog`
|
|
1561
|
+
},
|
|
1491
1562
|
hint: 'Use extension_templates with action: "list" to see available templates.'
|
|
1492
1563
|
});
|
|
1493
1564
|
const meta = {
|
|
@@ -1500,9 +1571,14 @@ async function readTemplateSource(args) {
|
|
|
1500
1571
|
keyFiles: template.keyFiles,
|
|
1501
1572
|
repositoryUrl: template.repositoryUrl
|
|
1502
1573
|
};
|
|
1503
|
-
if (!args.files?.length) return
|
|
1504
|
-
|
|
1505
|
-
|
|
1574
|
+
if (!args.files?.length) return envelope({
|
|
1575
|
+
ok: true,
|
|
1576
|
+
command: get_template_source_COMMAND,
|
|
1577
|
+
status: "file-list",
|
|
1578
|
+
value: {
|
|
1579
|
+
...meta,
|
|
1580
|
+
files: template.files.map((f)=>stripTemplatePathPrefix(template.slug, f))
|
|
1581
|
+
},
|
|
1506
1582
|
hint: "Pass specific file paths in the files parameter to read their contents."
|
|
1507
1583
|
});
|
|
1508
1584
|
const fileContents = {};
|
|
@@ -1520,17 +1596,24 @@ async function readTemplateSource(args) {
|
|
|
1520
1596
|
} catch {}
|
|
1521
1597
|
errors.push(`${filePath}: ${lastStatus || "fetch failed"}`);
|
|
1522
1598
|
}));
|
|
1523
|
-
return
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1599
|
+
return envelope({
|
|
1600
|
+
ok: 0 === errors.length,
|
|
1601
|
+
command: get_template_source_COMMAND,
|
|
1602
|
+
status: errors.length ? "partial" : "read",
|
|
1603
|
+
error: errors.length ? {
|
|
1604
|
+
code: "E_TEMPLATE_FETCH",
|
|
1605
|
+
message: `${errors.length} of ${errors.length + Object.keys(fileContents).length} file(s) could not be read from the template.`
|
|
1606
|
+
} : null,
|
|
1607
|
+
value: {
|
|
1608
|
+
...meta,
|
|
1609
|
+
fileContents
|
|
1610
|
+
},
|
|
1611
|
+
warnings: errors
|
|
1529
1612
|
});
|
|
1530
1613
|
}
|
|
1531
1614
|
const templates_schema = {
|
|
1532
1615
|
name: "extension_templates",
|
|
1533
|
-
description: "Browse the extension.dev template catalog. action:'list' (default)
|
|
1616
|
+
description: "Browse the extension.dev template catalog. Pass action:'list' (the default) to search and filter it and get metadata per template. Pass action:'source' with a `slug` to read one template's files, for learning a pattern before building something similar. Read `framework` as the UI framework only, never the language: TypeScript and JavaScript templates live under slugs ('typescript', 'content-typescript'), shadcn is a React variant ('sidebar-shadcn'), and provider AIs carry the 'ai' tag ('ai-chatgpt', 'ai-claude'). Reach those through query, tags or slug.",
|
|
1534
1617
|
inputSchema: {
|
|
1535
1618
|
type: "object",
|
|
1536
1619
|
properties: {
|
|
@@ -1595,9 +1678,14 @@ const templates_schema = {
|
|
|
1595
1678
|
};
|
|
1596
1679
|
async function templates_handler(args) {
|
|
1597
1680
|
if ((args.action ?? "list") === "source" || !args.action && args.slug) {
|
|
1598
|
-
if (!args.slug) return
|
|
1681
|
+
if (!args.slug) return envelope({
|
|
1599
1682
|
ok: false,
|
|
1600
|
-
|
|
1683
|
+
command: "extension_templates",
|
|
1684
|
+
status: "bad-request",
|
|
1685
|
+
error: {
|
|
1686
|
+
code: "E_BAD_REQUEST",
|
|
1687
|
+
message: "action 'source' needs a slug."
|
|
1688
|
+
},
|
|
1601
1689
|
hint: 'Call extension_templates with action: "list" to find one.'
|
|
1602
1690
|
});
|
|
1603
1691
|
return readTemplateSource({
|
|
@@ -1938,21 +2026,6 @@ function deadReadySession(projectPath) {
|
|
|
1938
2026
|
};
|
|
1939
2027
|
return null;
|
|
1940
2028
|
}
|
|
1941
|
-
function browserExitStamp(projectPath, browser, since) {
|
|
1942
|
-
const readyPath = node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json");
|
|
1943
|
-
try {
|
|
1944
|
-
const stat = node_fs.statSync(readyPath);
|
|
1945
|
-
if (stat.mtimeMs < since) return null;
|
|
1946
|
-
const contract = JSON.parse(node_fs.readFileSync(readyPath, "utf8"));
|
|
1947
|
-
const exited = contract?.code === "browser_exited" || contract?.browserExitCode !== void 0 || contract?.browserExitedAt !== void 0;
|
|
1948
|
-
if (contract?.status === "error" && exited) return {
|
|
1949
|
-
code: contract.code,
|
|
1950
|
-
browserExitCode: contract.browserExitCode ?? null,
|
|
1951
|
-
browserExitedAt: contract.browserExitedAt
|
|
1952
|
-
};
|
|
1953
|
-
} catch {}
|
|
1954
|
-
return null;
|
|
1955
|
-
}
|
|
1956
2029
|
function contractBoundPort(projectPath, browser, since) {
|
|
1957
2030
|
const readyPath = node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json");
|
|
1958
2031
|
try {
|
|
@@ -2192,6 +2265,7 @@ function materializeCarrier(projectPath, browser) {
|
|
|
2192
2265
|
};
|
|
2193
2266
|
}
|
|
2194
2267
|
}
|
|
2268
|
+
const build_COMMAND = "extension_build";
|
|
2195
2269
|
function readBuildSummary(projectPath, browser, since) {
|
|
2196
2270
|
const file = node_path.resolve(projectPath, "dist", "extension-js", browser, "build-summary.json");
|
|
2197
2271
|
try {
|
|
@@ -2289,7 +2363,7 @@ function locateSourceZip(projectPath, browser, since) {
|
|
|
2289
2363
|
}
|
|
2290
2364
|
const build_schema = {
|
|
2291
2365
|
name: "extension_build",
|
|
2292
|
-
description: "Build a browser extension for production.
|
|
2366
|
+
description: "Build a browser extension for production. The output lands in dist/<browser>/. Pass zip:true to also package a .zip for store submission. The build refuses a manifest with build-blocking errors unless you pass skipValidation:true, because such a manifest yields a broken bundle the bundler itself never flags.",
|
|
2293
2367
|
inputSchema: {
|
|
2294
2368
|
type: "object",
|
|
2295
2369
|
properties: {
|
|
@@ -2398,11 +2472,12 @@ async function validationPreflight(projectPath, browser) {
|
|
|
2398
2472
|
browser
|
|
2399
2473
|
]
|
|
2400
2474
|
}));
|
|
2475
|
+
const value = parsed?.value ?? {};
|
|
2401
2476
|
return {
|
|
2402
|
-
valid: Boolean(
|
|
2403
|
-
buildBlocking: Boolean(
|
|
2404
|
-
errors: Array.isArray(
|
|
2405
|
-
warnings: Array.isArray(
|
|
2477
|
+
valid: Boolean(value.valid),
|
|
2478
|
+
buildBlocking: Boolean(value.buildBlocking),
|
|
2479
|
+
errors: Array.isArray(value.errors) ? value.errors : [],
|
|
2480
|
+
warnings: Array.isArray(value.warnings) ? value.warnings : []
|
|
2406
2481
|
};
|
|
2407
2482
|
} catch {
|
|
2408
2483
|
return null;
|
|
@@ -2416,17 +2491,23 @@ async function build_handler(args) {
|
|
|
2416
2491
|
if (carrierCleanup.removed) carrierNotes.push("Removed the Extension.dev live-preview carrier from ./extensions before building. It is a debug companion, not part of your extension; run extension_dev with carrier: true to get it back.");
|
|
2417
2492
|
else if (carrierCleanup.note) carrierNotes.push(carrierCleanup.note);
|
|
2418
2493
|
const preflight = args.skipValidation ? null : await validationPreflight(args.projectPath, browser);
|
|
2419
|
-
if (preflight?.buildBlocking) return
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
error:
|
|
2424
|
-
|
|
2494
|
+
if (preflight?.buildBlocking) return envelope({
|
|
2495
|
+
ok: false,
|
|
2496
|
+
command: build_COMMAND,
|
|
2497
|
+
status: "manifest-blocked",
|
|
2498
|
+
error: {
|
|
2499
|
+
code: "E_MANIFEST_BLOCKING",
|
|
2500
|
+
message: "Build refused: the manifest has errors that produce a broken extension even when the bundler succeeds."
|
|
2501
|
+
},
|
|
2502
|
+
value: {
|
|
2503
|
+
browser,
|
|
2504
|
+
errors: preflight.errors,
|
|
2505
|
+
duration: Date.now() - start
|
|
2506
|
+
},
|
|
2425
2507
|
warnings: [
|
|
2426
2508
|
...carrierNotes,
|
|
2427
2509
|
...preflight.warnings
|
|
2428
2510
|
],
|
|
2429
|
-
duration: Date.now() - start,
|
|
2430
2511
|
hint: "Fix the errors above, then build again. Run extension_manifest_validate for the full report. To build anyway (for example to inspect the broken output), pass skipValidation: true."
|
|
2431
2512
|
});
|
|
2432
2513
|
const clobberedSessions = liveProjectSessions(args.projectPath).filter((session)=>session.browser === browser);
|
|
@@ -2454,107 +2535,268 @@ async function build_handler(args) {
|
|
|
2454
2535
|
const size = out.match(/Size:\s*([\d.]+\s*[kKmMgG]?B)/)?.[1];
|
|
2455
2536
|
const status = out.match(/Build Status:\s*(\w+)/)?.[1];
|
|
2456
2537
|
const engineSummary = readBuildSummary(args.projectPath, browser, start);
|
|
2457
|
-
const buildWarnings = engineSummary?.warnings?.length ?
|
|
2458
|
-
|
|
2459
|
-
..."number" == typeof engineSummary.warnings_count && engineSummary.warnings_count > engineSummary.warnings.length ? {
|
|
2460
|
-
buildWarningsTruncated: engineSummary.warnings_count
|
|
2461
|
-
} : {}
|
|
2462
|
-
} : {};
|
|
2538
|
+
const buildWarnings = engineSummary?.warnings?.length ? engineSummary.warnings : [];
|
|
2539
|
+
const buildWarningsTruncated = buildWarnings.length && "number" == typeof engineSummary?.warnings_count && engineSummary.warnings_count > buildWarnings.length ? engineSummary.warnings_count : void 0;
|
|
2463
2540
|
const distDir = node_path.resolve(args.projectPath, "dist", browser);
|
|
2464
2541
|
const entrypoints = builtEntrypoints(distDir);
|
|
2465
2542
|
const contamination = carrierEntriesInDist(distDir);
|
|
2466
|
-
if (contamination.length) return
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2543
|
+
if (contamination.length) return envelope({
|
|
2544
|
+
ok: false,
|
|
2545
|
+
command: build_COMMAND,
|
|
2546
|
+
status: "carrier-in-dist",
|
|
2547
|
+
error: {
|
|
2548
|
+
code: "E_CARRIER_IN_DIST",
|
|
2549
|
+
message: `The build output contains the Extension.dev live-preview carrier: ${contamination.join(", ")}. That is a local debug companion and must never ship. This artifact is not safe to submit.`
|
|
2550
|
+
},
|
|
2551
|
+
value: {
|
|
2552
|
+
browser,
|
|
2553
|
+
buildExitCode: 0,
|
|
2554
|
+
duration
|
|
2555
|
+
},
|
|
2556
|
+
warnings,
|
|
2476
2557
|
hint: "Delete the listed paths from dist and build again. The carrier normally lives in ./extensions and is removed before every build."
|
|
2477
2558
|
});
|
|
2478
2559
|
const missing = entrypoints.filter((e)=>!e.present);
|
|
2479
|
-
if (missing.length) return
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2560
|
+
if (missing.length) return envelope({
|
|
2561
|
+
ok: false,
|
|
2562
|
+
command: build_COMMAND,
|
|
2563
|
+
status: "entrypoint-missing",
|
|
2564
|
+
error: {
|
|
2565
|
+
code: "E_ENTRYPOINT_MISSING",
|
|
2566
|
+
message: `The build reported success but ${missing.length} declared entrypoint(s) are missing from dist/${browser}: ` + missing.map((m)=>`${m.role} -> ${m.path}`).join(", ") + ". The browser will refuse to load this build."
|
|
2567
|
+
},
|
|
2568
|
+
value: {
|
|
2569
|
+
browser,
|
|
2570
|
+
buildExitCode: 0,
|
|
2571
|
+
entrypoints,
|
|
2572
|
+
duration,
|
|
2573
|
+
output: lastLines(out, 12)
|
|
2574
|
+
},
|
|
2575
|
+
warnings: [
|
|
2576
|
+
...warnings,
|
|
2577
|
+
...preflight?.warnings ?? [],
|
|
2578
|
+
...buildWarnings
|
|
2579
|
+
],
|
|
2495
2580
|
hint: "The bundler exited 0 but did not emit these files. Check that the manifest paths match what the build produces, and that nothing references a file outside the source tree."
|
|
2496
2581
|
});
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2582
|
+
const zipNotes = [];
|
|
2583
|
+
const zipPath = args.zip ? locateDistZip(args.projectPath, browser, args.zipFilename, start) : null;
|
|
2584
|
+
if (args.zip && !zipPath) zipNotes.push(`zip: true was requested and the build succeeded, but no .zip file could be located in dist/${browser}. The engine may not have packaged it; check the build output below.`);
|
|
2585
|
+
const zipSourcePath = args.zipSource ? locateSourceZip(args.projectPath, browser, start) : null;
|
|
2586
|
+
if (args.zipSource && !zipSourcePath) zipNotes.push("zipSource: true was requested and the build succeeded, but no *-source.zip file could be located in dist/. The engine may not have packaged it; check the build output below.");
|
|
2587
|
+
const divergence = manifestDivergence(args.projectPath, browser);
|
|
2588
|
+
return envelope({
|
|
2589
|
+
ok: true,
|
|
2590
|
+
command: build_COMMAND,
|
|
2591
|
+
status: "built",
|
|
2592
|
+
value: {
|
|
2593
|
+
browser,
|
|
2594
|
+
...size ? {
|
|
2595
|
+
size
|
|
2596
|
+
} : {},
|
|
2597
|
+
...status ? {
|
|
2598
|
+
engineBuildStatus: status
|
|
2599
|
+
} : {},
|
|
2600
|
+
...entrypoints.length ? {
|
|
2601
|
+
entrypoints
|
|
2602
|
+
} : {},
|
|
2603
|
+
...void 0 !== buildWarningsTruncated ? {
|
|
2604
|
+
buildWarningsTruncated
|
|
2605
|
+
} : {},
|
|
2606
|
+
...divergence.length ? {
|
|
2516
2607
|
productionDivergence: divergence
|
|
2517
|
-
} : {}
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
warnings
|
|
2521
|
-
} : {},
|
|
2522
|
-
zip: args.zip ?? false,
|
|
2523
|
-
...args.zip ? (()=>{
|
|
2524
|
-
const zipPath = locateDistZip(args.projectPath, browser, args.zipFilename, start);
|
|
2525
|
-
return zipPath ? {
|
|
2608
|
+
} : {},
|
|
2609
|
+
zip: args.zip ?? false,
|
|
2610
|
+
...zipPath ? {
|
|
2526
2611
|
zipPath
|
|
2527
|
-
} : {
|
|
2528
|
-
|
|
2529
|
-
};
|
|
2530
|
-
})() : {},
|
|
2531
|
-
...args.zipSource ? (()=>{
|
|
2532
|
-
const zipSourcePath = locateSourceZip(args.projectPath, browser, start);
|
|
2533
|
-
return zipSourcePath ? {
|
|
2612
|
+
} : {},
|
|
2613
|
+
...zipSourcePath ? {
|
|
2534
2614
|
zipSourcePath
|
|
2535
|
-
} : {
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
}
|
|
2539
|
-
|
|
2540
|
-
|
|
2615
|
+
} : {},
|
|
2616
|
+
duration,
|
|
2617
|
+
output: lastLines(out, 12)
|
|
2618
|
+
},
|
|
2619
|
+
warnings: [
|
|
2620
|
+
...warnings,
|
|
2621
|
+
...preflight?.warnings ?? [],
|
|
2622
|
+
...buildWarnings,
|
|
2623
|
+
...zipNotes
|
|
2624
|
+
]
|
|
2541
2625
|
});
|
|
2542
2626
|
}
|
|
2543
2627
|
const message = stderr.trim() || out || `extension build exited with code ${code}`;
|
|
2544
|
-
return
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2628
|
+
return envelope({
|
|
2629
|
+
ok: false,
|
|
2630
|
+
command: build_COMMAND,
|
|
2631
|
+
status: "build-failed",
|
|
2632
|
+
error: {
|
|
2633
|
+
code: "E_BUILD_FAILED",
|
|
2634
|
+
message: message.slice(0, 1200)
|
|
2635
|
+
},
|
|
2636
|
+
value: {
|
|
2637
|
+
browser,
|
|
2638
|
+
duration
|
|
2639
|
+
},
|
|
2640
|
+
warnings,
|
|
2552
2641
|
hint: "Check that the project has a valid src/manifest.json and its dependencies are installed (extension_dev auto-installs; build does not)."
|
|
2553
2642
|
});
|
|
2554
2643
|
}
|
|
2644
|
+
const NOISE = [
|
|
2645
|
+
/^npm warn Unknown project config/i,
|
|
2646
|
+
/This will stop working in the next major version of npm/i,
|
|
2647
|
+
/^npm warn config/i,
|
|
2648
|
+
/^npm warn exec/i,
|
|
2649
|
+
/The following package(s)? (was|were) not found and will be installed/i,
|
|
2650
|
+
/V8: .*Invalid asm\.js/i,
|
|
2651
|
+
/^\(node:\d+\) V8:/i,
|
|
2652
|
+
/Use `node --trace-warnings/i,
|
|
2653
|
+
/Invalid asm\.js:/i,
|
|
2654
|
+
/Linking failure in asm\.js/i,
|
|
2655
|
+
/Successfully compiled asm\.js/i
|
|
2656
|
+
];
|
|
2657
|
+
function denoiseCliLog(raw) {
|
|
2658
|
+
return raw.split("\n").filter((line)=>!NOISE.some((re)=>re.test(line.trim()))).join("\n").replace(/\n{2,}/g, "\n").trimStart();
|
|
2659
|
+
}
|
|
2660
|
+
function legacyCompileScrape(cleanOutput) {
|
|
2661
|
+
return /compiled with errors|✖✖✖|ERROR in |Module not found|NOT FOUND/i.test(cleanOutput);
|
|
2662
|
+
}
|
|
2663
|
+
function legacyProfileLockScrape(cleanOutput) {
|
|
2664
|
+
return /SingletonLock|ProcessSingleton|profile[^\n]*(in use|locked)|already (open|running)/i.test(cleanOutput);
|
|
2665
|
+
}
|
|
2666
|
+
const LEGACY_FIDELITY_WARNING = "This session ran a CLI that does not stamp the machine contract, so the boot verdict was read from the dev server's output instead of its ready contract. Diagnostics are less precise: compile errors come back as a text tail rather than a list. Upgrade the project's extension dependency to get the precise verdict.";
|
|
2667
|
+
const PROFILE_LOCKED_CODE = "profile_locked";
|
|
2668
|
+
function readyContractPath(projectPath, browser) {
|
|
2669
|
+
return node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json");
|
|
2670
|
+
}
|
|
2671
|
+
function readContract(projectPath, browser, since) {
|
|
2672
|
+
try {
|
|
2673
|
+
const file = readyContractPath(projectPath, browser);
|
|
2674
|
+
const stat = node_fs.statSync(file);
|
|
2675
|
+
const contract = JSON.parse(node_fs.readFileSync(file, "utf8"));
|
|
2676
|
+
if (!contract || "object" != typeof contract) return null;
|
|
2677
|
+
return {
|
|
2678
|
+
contract,
|
|
2679
|
+
fresh: stat.mtimeMs >= since
|
|
2680
|
+
};
|
|
2681
|
+
} catch {
|
|
2682
|
+
return null;
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
function speaksMachineContract(contract) {
|
|
2686
|
+
return !!contract && "object" == typeof contract && 1 === contract.schema;
|
|
2687
|
+
}
|
|
2688
|
+
function profileLockOwner(contract) {
|
|
2689
|
+
const owner = contract.profileLockOwner;
|
|
2690
|
+
if (!owner || "object" != typeof owner) return null;
|
|
2691
|
+
const { host, pid } = owner;
|
|
2692
|
+
return {
|
|
2693
|
+
...host ? {
|
|
2694
|
+
host
|
|
2695
|
+
} : {},
|
|
2696
|
+
...null != pid ? {
|
|
2697
|
+
pid
|
|
2698
|
+
} : {}
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
function contractVerdict(reading, noBrowser) {
|
|
2702
|
+
const { contract, fresh } = reading;
|
|
2703
|
+
if (!fresh) return null;
|
|
2704
|
+
if ("error" !== contract.status) return null;
|
|
2705
|
+
if (!noBrowser && contract.code === PROFILE_LOCKED_CODE) return {
|
|
2706
|
+
kind: "profile-locked",
|
|
2707
|
+
owner: profileLockOwner(contract),
|
|
2708
|
+
message: "string" == typeof contract.message ? contract.message : void 0,
|
|
2709
|
+
lockedAt: "string" == typeof contract.profileLockedAt ? contract.profileLockedAt : void 0
|
|
2710
|
+
};
|
|
2711
|
+
const browserExited = "browser_exited" === contract.code || void 0 !== contract.browserExitCode || void 0 !== contract.browserExitedAt;
|
|
2712
|
+
if (!noBrowser && browserExited) return {
|
|
2713
|
+
kind: "browser-exited",
|
|
2714
|
+
stamp: {
|
|
2715
|
+
code: contract.code,
|
|
2716
|
+
browserExitCode: contract.browserExitCode ?? null,
|
|
2717
|
+
browserExitedAt: contract.browserExitedAt
|
|
2718
|
+
}
|
|
2719
|
+
};
|
|
2720
|
+
return {
|
|
2721
|
+
kind: "compile-failed",
|
|
2722
|
+
message: contract.message,
|
|
2723
|
+
compileErrors: Array.isArray(contract.errors) ? contract.errors : []
|
|
2724
|
+
};
|
|
2725
|
+
}
|
|
2726
|
+
async function pollBootVerdict(projectPath, browser, options) {
|
|
2727
|
+
const { child, readOutput, budgetMs, since } = options;
|
|
2728
|
+
const noBrowser = Boolean(options.noBrowser);
|
|
2729
|
+
const intervalMs = options.intervalMs ?? 250;
|
|
2730
|
+
const deadline = Date.now() + budgetMs;
|
|
2731
|
+
let contractSeen = null;
|
|
2732
|
+
for(;;){
|
|
2733
|
+
if (null !== child.exitCode || null !== child.signalCode) return {
|
|
2734
|
+
verdict: {
|
|
2735
|
+
kind: "exited",
|
|
2736
|
+
exitCode: child.exitCode,
|
|
2737
|
+
signal: child.signalCode
|
|
2738
|
+
},
|
|
2739
|
+
machineContract: speaksMachineContract(contractSeen?.contract),
|
|
2740
|
+
output: denoiseCliLog(readOutput()),
|
|
2741
|
+
warnings: []
|
|
2742
|
+
};
|
|
2743
|
+
contractSeen = readContract(projectPath, browser, since) ?? contractSeen;
|
|
2744
|
+
if (contractSeen) {
|
|
2745
|
+
const verdict = contractVerdict(contractSeen, noBrowser);
|
|
2746
|
+
if (verdict) return {
|
|
2747
|
+
verdict,
|
|
2748
|
+
machineContract: speaksMachineContract(contractSeen.contract),
|
|
2749
|
+
output: denoiseCliLog(readOutput()),
|
|
2750
|
+
warnings: []
|
|
2751
|
+
};
|
|
2752
|
+
}
|
|
2753
|
+
if (Date.now() >= deadline) break;
|
|
2754
|
+
await new Promise((resolve)=>setTimeout(resolve, Math.min(intervalMs, Math.max(deadline - Date.now(), 1))));
|
|
2755
|
+
}
|
|
2756
|
+
const machineContract = speaksMachineContract(contractSeen?.contract);
|
|
2757
|
+
const output = denoiseCliLog(readOutput());
|
|
2758
|
+
if (machineContract) return {
|
|
2759
|
+
verdict: {
|
|
2760
|
+
kind: "alive"
|
|
2761
|
+
},
|
|
2762
|
+
machineContract,
|
|
2763
|
+
output,
|
|
2764
|
+
warnings: []
|
|
2765
|
+
};
|
|
2766
|
+
if (legacyCompileScrape(output)) return {
|
|
2767
|
+
verdict: {
|
|
2768
|
+
kind: "compile-failed",
|
|
2769
|
+
compileErrors: []
|
|
2770
|
+
},
|
|
2771
|
+
machineContract,
|
|
2772
|
+
output,
|
|
2773
|
+
warnings: [
|
|
2774
|
+
LEGACY_FIDELITY_WARNING
|
|
2775
|
+
]
|
|
2776
|
+
};
|
|
2777
|
+
if (!noBrowser && legacyProfileLockScrape(output)) return {
|
|
2778
|
+
verdict: {
|
|
2779
|
+
kind: "profile-locked",
|
|
2780
|
+
owner: null
|
|
2781
|
+
},
|
|
2782
|
+
machineContract,
|
|
2783
|
+
output,
|
|
2784
|
+
warnings: [
|
|
2785
|
+
LEGACY_FIDELITY_WARNING
|
|
2786
|
+
]
|
|
2787
|
+
};
|
|
2788
|
+
return {
|
|
2789
|
+
verdict: {
|
|
2790
|
+
kind: "alive"
|
|
2791
|
+
},
|
|
2792
|
+
machineContract,
|
|
2793
|
+
output,
|
|
2794
|
+
warnings: []
|
|
2795
|
+
};
|
|
2796
|
+
}
|
|
2555
2797
|
const stop_schema = {
|
|
2556
2798
|
name: "extension_stop",
|
|
2557
|
-
description: "Stop a session that extension_dev or extension_start is running:
|
|
2799
|
+
description: "Stop a session that extension_dev or extension_start is running: terminate the server and the browser it launched, and remove the live-preview carrier if extension_dev placed one. This covers extension_start build:false too, which the registry records as a preview session. Call it when you are done verifying, so sessions do not accumulate.",
|
|
2558
2800
|
inputSchema: {
|
|
2559
2801
|
type: "object",
|
|
2560
2802
|
properties: {
|
|
@@ -2698,22 +2940,47 @@ async function stop_handler(args) {
|
|
|
2698
2940
|
const key = `${node_path.resolve(m.projectPath)}::${m.browser}`;
|
|
2699
2941
|
if (!candidates.has(key)) candidates.set(key, m);
|
|
2700
2942
|
}
|
|
2701
|
-
if (0 === candidates.size) return
|
|
2702
|
-
|
|
2703
|
-
|
|
2943
|
+
if (0 === candidates.size) return envelope({
|
|
2944
|
+
ok: true,
|
|
2945
|
+
command: stop_schema.name,
|
|
2946
|
+
status: "nothing-to-stop",
|
|
2947
|
+
value: {
|
|
2948
|
+
stopped: []
|
|
2949
|
+
},
|
|
2950
|
+
hint: "No sessions registered in this server and no session markers on disk. Nothing to stop."
|
|
2704
2951
|
});
|
|
2705
2952
|
const outcomes = [];
|
|
2706
2953
|
for (const c of candidates.values())outcomes.push(await stopOne(c.projectPath, c.browser));
|
|
2707
|
-
return
|
|
2708
|
-
|
|
2954
|
+
return envelope({
|
|
2955
|
+
ok: outcomes.every((o)=>o.stopped),
|
|
2956
|
+
command: stop_schema.name,
|
|
2957
|
+
status: "stopped-all",
|
|
2958
|
+
value: {
|
|
2959
|
+
stopped: outcomes
|
|
2960
|
+
},
|
|
2961
|
+
warnings: outcomes.map((o)=>o.stopped ? null : o.detail)
|
|
2709
2962
|
});
|
|
2710
2963
|
}
|
|
2711
|
-
if (!args.projectPath) return
|
|
2712
|
-
|
|
2964
|
+
if (!args.projectPath) return envelope({
|
|
2965
|
+
ok: false,
|
|
2966
|
+
command: stop_schema.name,
|
|
2967
|
+
status: "bad-request",
|
|
2968
|
+
error: {
|
|
2969
|
+
code: "E_BAD_REQUEST",
|
|
2970
|
+
message: "projectPath is required unless all=true. Pass the same projectPath used with extension_dev/extension_start."
|
|
2971
|
+
}
|
|
2713
2972
|
});
|
|
2714
2973
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
2715
2974
|
const outcome = await stopOne(args.projectPath, browser);
|
|
2716
|
-
return
|
|
2975
|
+
return envelope({
|
|
2976
|
+
ok: outcome.stopped,
|
|
2977
|
+
command: stop_schema.name,
|
|
2978
|
+
status: null === outcome.pid ? "not-found" : outcome.stopped ? "stopped" : "still-alive",
|
|
2979
|
+
value: outcome,
|
|
2980
|
+
warnings: [
|
|
2981
|
+
outcome.stopped ? null : outcome.detail
|
|
2982
|
+
]
|
|
2983
|
+
});
|
|
2717
2984
|
}
|
|
2718
2985
|
const LAUNCH_FLAG_SCHEMA = {
|
|
2719
2986
|
profile: {
|
|
@@ -2761,7 +3028,7 @@ function launchFlagArgs(args) {
|
|
|
2761
3028
|
}
|
|
2762
3029
|
const dev_schema = {
|
|
2763
3030
|
name: "extension_dev",
|
|
2764
|
-
description: "Run the extension
|
|
3031
|
+
description: "Run the extension while you edit it: dev build, hot module replacement, and a browser with the extension loaded. Reach for this first when the ask is \"run my extension\". ONLY this tool unlocks the control channel that extension_storage, extension_reload, extension_open and extension_dom_snapshot need (allowControl:true) and the eval channel that extension_eval needs (allowEval:true, which implies allowControl, so you never need to pass both). Use extension_start instead to run the production build in a browser. The result carries the process info that extension_wait and extension_inspect need.",
|
|
2765
3032
|
inputSchema: {
|
|
2766
3033
|
type: "object",
|
|
2767
3034
|
properties: {
|
|
@@ -2815,15 +3082,21 @@ async function dev_handler(args) {
|
|
|
2815
3082
|
if (existing.length > 0) {
|
|
2816
3083
|
if (!args.replace) {
|
|
2817
3084
|
const listed = existing.map((s)=>`pid ${s.pid} (${s.browser})`).join(", ");
|
|
2818
|
-
return
|
|
3085
|
+
return envelope({
|
|
2819
3086
|
ok: false,
|
|
3087
|
+
command: dev_schema.name,
|
|
2820
3088
|
status: "session-exists",
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
3089
|
+
error: {
|
|
3090
|
+
code: "E_SESSION_EXISTS",
|
|
3091
|
+
message: `A dev session is already running for this project (${listed}). Starting another would fork the session: both browsers contend for the same profile and the new one dies on the profile lock.`
|
|
3092
|
+
},
|
|
3093
|
+
value: {
|
|
3094
|
+
projectPath: args.projectPath,
|
|
3095
|
+
sessions: existing.map((s)=>({
|
|
3096
|
+
pid: s.pid,
|
|
3097
|
+
browser: s.browser
|
|
3098
|
+
}))
|
|
3099
|
+
},
|
|
2827
3100
|
hint: "Call extension_stop with this projectPath first, or pass replace: true to have extension_dev stop the old session before starting the new one."
|
|
2828
3101
|
});
|
|
2829
3102
|
}
|
|
@@ -2864,55 +3137,101 @@ async function dev_handler(args) {
|
|
|
2864
3137
|
noBrowser: Boolean(args.noBrowser)
|
|
2865
3138
|
});
|
|
2866
3139
|
child.on("exit", ()=>removeSession(args.projectPath, browser));
|
|
2867
|
-
await
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
3140
|
+
const boot = await pollBootVerdict(args.projectPath, browser, {
|
|
3141
|
+
child,
|
|
3142
|
+
readOutput: spawned.readOutput,
|
|
3143
|
+
budgetMs: 3000,
|
|
3144
|
+
since: spawnedAt,
|
|
3145
|
+
noBrowser: Boolean(args.noBrowser)
|
|
3146
|
+
});
|
|
3147
|
+
const cleanOutput = boot.output;
|
|
3148
|
+
const session = {
|
|
3149
|
+
projectPath: args.projectPath,
|
|
3150
|
+
browser,
|
|
3151
|
+
pid,
|
|
3152
|
+
logPath
|
|
3153
|
+
};
|
|
3154
|
+
if ("exited" === boot.verdict.kind) {
|
|
3155
|
+
const { exitCode: code, signal } = boot.verdict;
|
|
3156
|
+
return envelope({
|
|
2874
3157
|
ok: false,
|
|
3158
|
+
command: dev_schema.name,
|
|
2875
3159
|
status: "exited",
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
3160
|
+
error: {
|
|
3161
|
+
code: "E_SESSION_EXITED",
|
|
3162
|
+
message: `The dev server exited during startup (${signal ? `signal ${signal}` : `exit code ${code}`}). No session is running, so extension_logs/wait/eval and the control verbs have nothing to attach to.`
|
|
3163
|
+
},
|
|
3164
|
+
value: {
|
|
3165
|
+
...session,
|
|
3166
|
+
exitCode: code,
|
|
3167
|
+
signal,
|
|
3168
|
+
output: cleanOutput.slice(0, 2000)
|
|
3169
|
+
},
|
|
3170
|
+
hint: "Read `value.output` above for the cause: a port already in use, a manifest the build rejects, or a missing browser binary are the common ones. Fix it and call extension_dev again; extension_doctor with this projectPath will also report what the last session recorded.",
|
|
3171
|
+
warnings: boot.warnings
|
|
2885
3172
|
});
|
|
2886
3173
|
}
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
status: "compile-failed",
|
|
2891
|
-
projectPath: args.projectPath,
|
|
2892
|
-
browser,
|
|
2893
|
-
pid,
|
|
2894
|
-
error: "The dev server started but the FIRST COMPILE FAILED, so the browser has nothing usable to load. The session is running; the extension is not.",
|
|
2895
|
-
output: cleanOutput.slice(0, 2000),
|
|
2896
|
-
logPath,
|
|
2897
|
-
hint: "Fix the compile error in `output` above and save: the dev server is still running and will recompile. Do not call extension_wait yet, it will report ready for a build that failed."
|
|
2898
|
-
});
|
|
2899
|
-
const exitStamp = args.noBrowser ? null : browserExitStamp(args.projectPath, browser, spawnedAt);
|
|
2900
|
-
const profileLockHit = !args.noBrowser && /SingletonLock|ProcessSingleton|profile[^\n]*(in use|locked)|already (open|running)/i.test(cleanOutput);
|
|
2901
|
-
if (exitStamp || profileLockHit) {
|
|
2902
|
-
const profileDir = node_path.join(args.projectPath, "dist", `extension-profile-${browser}`);
|
|
2903
|
-
return JSON.stringify({
|
|
3174
|
+
if ("compile-failed" === boot.verdict.kind) {
|
|
3175
|
+
const { compileErrors } = boot.verdict;
|
|
3176
|
+
return envelope({
|
|
2904
3177
|
ok: false,
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
3178
|
+
command: dev_schema.name,
|
|
3179
|
+
status: "compile-failed",
|
|
3180
|
+
error: {
|
|
3181
|
+
code: "E_FIRST_COMPILE",
|
|
3182
|
+
message: boot.verdict.message ?? "The dev server started but the first compile failed, so the browser has nothing usable to load. The session is running; the extension is not."
|
|
3183
|
+
},
|
|
3184
|
+
value: {
|
|
3185
|
+
...session,
|
|
3186
|
+
compileErrors,
|
|
3187
|
+
...compileErrors.length ? {} : {
|
|
3188
|
+
output: cleanOutput.slice(0, 2000)
|
|
3189
|
+
}
|
|
3190
|
+
},
|
|
3191
|
+
hint: "Fix the compile error listed in `value.compileErrors` and save: the dev server is still running and will recompile. Do not call extension_wait yet, it will report ready for a build that failed.",
|
|
3192
|
+
warnings: boot.warnings
|
|
2914
3193
|
});
|
|
2915
3194
|
}
|
|
3195
|
+
const profileDir = node_path.join(args.projectPath, "dist", `extension-profile-${browser}`);
|
|
3196
|
+
if ("profile-locked" === boot.verdict.kind) {
|
|
3197
|
+
const { owner, lockedAt } = boot.verdict;
|
|
3198
|
+
return envelope({
|
|
3199
|
+
ok: false,
|
|
3200
|
+
command: dev_schema.name,
|
|
3201
|
+
status: "profile-locked",
|
|
3202
|
+
error: {
|
|
3203
|
+
code: "E_PROFILE_LOCKED",
|
|
3204
|
+
message: boot.verdict.message ?? `The dev server is running but the ${browser} browser it launched died during startup because its profile is locked by another browser instance` + (owner?.pid ? ` (pid ${owner.pid}${owner.host ? ` on ${owner.host}` : ""})` : "") + "."
|
|
3205
|
+
},
|
|
3206
|
+
value: {
|
|
3207
|
+
...session,
|
|
3208
|
+
profileDir,
|
|
3209
|
+
owner,
|
|
3210
|
+
...lockedAt ? {
|
|
3211
|
+
lockedAt
|
|
3212
|
+
} : {},
|
|
3213
|
+
output: cleanOutput.slice(0, 2000)
|
|
3214
|
+
},
|
|
3215
|
+
hint: `A locked profile means another session's browser still holds it: call extension_stop with this projectPath to kill that session, then start extension_dev again. If the lock survives a crash, remove ${profileDir} manually before retrying.`,
|
|
3216
|
+
warnings: boot.warnings
|
|
3217
|
+
});
|
|
3218
|
+
}
|
|
3219
|
+
if ("browser-exited" === boot.verdict.kind) return envelope({
|
|
3220
|
+
ok: false,
|
|
3221
|
+
command: dev_schema.name,
|
|
3222
|
+
status: "browser-exited",
|
|
3223
|
+
error: {
|
|
3224
|
+
code: "E_BROWSER_EXITED",
|
|
3225
|
+
message: `The dev server is running but the ${browser} browser it launched died during startup.`
|
|
3226
|
+
},
|
|
3227
|
+
value: {
|
|
3228
|
+
...session,
|
|
3229
|
+
...boot.verdict.stamp,
|
|
3230
|
+
output: cleanOutput.slice(0, 2000)
|
|
3231
|
+
},
|
|
3232
|
+
hint: `A locked profile means another session's browser still holds it: call extension_stop with this projectPath to kill that session, then start extension_dev again. If the lock survives a crash, remove ${profileDir} manually before retrying.`,
|
|
3233
|
+
warnings: boot.warnings
|
|
3234
|
+
});
|
|
2916
3235
|
const controlVerbs = "storage, reload, open, dom_snapshot";
|
|
2917
3236
|
const capabilities = {
|
|
2918
3237
|
allowControl,
|
|
@@ -2931,54 +3250,43 @@ async function dev_handler(args) {
|
|
|
2931
3250
|
const portReport = null !== boundPort ? {
|
|
2932
3251
|
port: boundPort,
|
|
2933
3252
|
...void 0 !== args.port && args.port !== boundPort ? {
|
|
2934
|
-
requestedPort: args.port
|
|
2935
|
-
portNote: `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).`
|
|
3253
|
+
requestedPort: args.port
|
|
2936
3254
|
} : {}
|
|
2937
3255
|
} : {
|
|
2938
|
-
requestedPort: args.port ?? 8080
|
|
2939
|
-
portNote: "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."
|
|
3256
|
+
requestedPort: args.port ?? 8080
|
|
2940
3257
|
};
|
|
2941
|
-
|
|
3258
|
+
const portNote = null !== boundPort ? void 0 !== args.port && args.port !== boundPort ? `Requested port ${args.port} was not available; the dev server bound ${boundPort} (read from the engine's ready.json contract, the same source extension_wait reports).` : null : "The engine has not stamped its ready.json contract yet, so the bound port is not known at response time (a taken port makes the server bind the next free one). extension_wait reports the bound port from that contract once it lands; requestedPort above is only what was asked for.";
|
|
3259
|
+
return envelope({
|
|
2942
3260
|
ok: true,
|
|
2943
|
-
|
|
2944
|
-
browser,
|
|
2945
|
-
...portReport,
|
|
2946
|
-
projectPath: args.projectPath,
|
|
3261
|
+
command: dev_schema.name,
|
|
2947
3262
|
status: "started",
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
...
|
|
2954
|
-
|
|
2955
|
-
} : {}
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
3263
|
+
value: {
|
|
3264
|
+
pid,
|
|
3265
|
+
browser,
|
|
3266
|
+
...portReport,
|
|
3267
|
+
projectPath: args.projectPath,
|
|
3268
|
+
...carrier ? {
|
|
3269
|
+
carrier
|
|
3270
|
+
} : {},
|
|
3271
|
+
...replaced.length > 0 ? {
|
|
3272
|
+
replacedSession: replaced[0],
|
|
3273
|
+
...replaced.length > 1 ? {
|
|
3274
|
+
replacedSessions: replaced
|
|
3275
|
+
} : {}
|
|
3276
|
+
} : {},
|
|
3277
|
+
capabilities,
|
|
3278
|
+
logPath
|
|
3279
|
+
},
|
|
3280
|
+
warnings: [
|
|
3281
|
+
portNote,
|
|
3282
|
+
...boot.warnings
|
|
3283
|
+
],
|
|
3284
|
+
hint: args.noBrowser ? "Build-only session (noBrowser: true): no browser will launch, so no runtime will ever attach. extension_wait returns as soon as the first compile lands (compiled: true, browserAttached: false) instead of waiting out its budget; do not wait for a browser. The control verbs (storage/reload/open/dom_snapshot/eval) need a live browser and will not work against this session. When you are done, call extension_stop to shut down the dev server." : "Use extension_wait to check when the extension is fully loaded, then extension_inspect to inspect the live state. " + (allowControl ? `Control channel is ON: extension_${controlVerbs.split(", ").join("/extension_")}${args.allowEval ? "/extension_eval" : ""} will work against this session.` : "Control channel is OFF: extension_storage/reload/open/dom_snapshot need allowControl: true, and extension_eval needs allowEval: true (which also implies allowControl). To unlock them, call extension_dev again with the flag you need plus replace: true (it stops this session first); a plain second call is refused so the session does not fork.") + " When you are done, call extension_stop to shut down the dev server and browser."
|
|
2961
3285
|
});
|
|
2962
3286
|
}
|
|
2963
|
-
function denoiseEarlyOutput(raw) {
|
|
2964
|
-
const NOISE = [
|
|
2965
|
-
/^npm warn Unknown project config/i,
|
|
2966
|
-
/This will stop working in the next major version of npm/i,
|
|
2967
|
-
/^npm warn config/i,
|
|
2968
|
-
/^npm warn exec/i,
|
|
2969
|
-
/The following package(s)? (was|were) not found and will be installed/i,
|
|
2970
|
-
/V8: .*Invalid asm\.js/i,
|
|
2971
|
-
/^\(node:\d+\) V8:/i,
|
|
2972
|
-
/Use `node --trace-warnings/i,
|
|
2973
|
-
/Invalid asm\.js:/i,
|
|
2974
|
-
/Linking failure in asm\.js/i,
|
|
2975
|
-
/Successfully compiled asm\.js/i
|
|
2976
|
-
];
|
|
2977
|
-
return raw.split("\n").filter((line)=>!NOISE.some((re)=>re.test(line.trim()))).join("\n").replace(/\n{2,}/g, "\n").trimStart();
|
|
2978
|
-
}
|
|
2979
3287
|
const start_schema = {
|
|
2980
3288
|
name: "extension_start",
|
|
2981
|
-
description: "Run the PRODUCTION build in a browser:
|
|
3289
|
+
description: "Run the PRODUCTION build in a browser: build the project, serve it, and launch. There is no hot module replacement and no control channel, so your edits are not picked up and extension_eval, extension_storage, extension_reload, extension_open and extension_dom_snapshot cannot attach to this session. Use extension_dev while writing code, and this to check what actually ships. Pass build:false to launch an existing dist/<browser> without rebuilding.",
|
|
2982
3290
|
inputSchema: {
|
|
2983
3291
|
type: "object",
|
|
2984
3292
|
properties: {
|
|
@@ -3037,54 +3345,106 @@ async function start_handler(args) {
|
|
|
3037
3345
|
command
|
|
3038
3346
|
});
|
|
3039
3347
|
child.on("exit", ()=>removeSession(args.projectPath, browser));
|
|
3040
|
-
await
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3348
|
+
const boot = await pollBootVerdict(args.projectPath, browser, {
|
|
3349
|
+
child,
|
|
3350
|
+
readOutput: spawned.readOutput,
|
|
3351
|
+
budgetMs: 5000,
|
|
3352
|
+
since: spawnedAt,
|
|
3353
|
+
noBrowser: Boolean(args.noBrowser)
|
|
3354
|
+
});
|
|
3355
|
+
const cleanOutput = boot.output;
|
|
3356
|
+
const session = {
|
|
3357
|
+
projectPath: args.projectPath,
|
|
3358
|
+
browser,
|
|
3359
|
+
pid,
|
|
3360
|
+
logPath
|
|
3361
|
+
};
|
|
3362
|
+
if ("exited" === boot.verdict.kind) {
|
|
3363
|
+
const { exitCode: code, signal } = boot.verdict;
|
|
3364
|
+
return envelope({
|
|
3046
3365
|
ok: false,
|
|
3366
|
+
command: start_schema.name,
|
|
3047
3367
|
status: "exited",
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3368
|
+
error: {
|
|
3369
|
+
code: "E_SESSION_EXITED",
|
|
3370
|
+
message: `The ${command} process exited during startup (${signal ? `signal ${signal}` : `exit code ${code}`}). No session is running.`
|
|
3371
|
+
},
|
|
3372
|
+
value: {
|
|
3373
|
+
...session,
|
|
3374
|
+
exitCode: code,
|
|
3375
|
+
signal,
|
|
3376
|
+
output: cleanOutput.slice(0, 2000)
|
|
3377
|
+
},
|
|
3378
|
+
hint: building ? "Read `value.output` above for the cause: a failed production build, a port already in use, or a missing browser binary are the common ones. extension_build will surface a build error on its own." : "Read `value.output` above for the cause: a missing or broken dist/ (run extension_build first, or drop build:false), or a missing browser binary are the common ones.",
|
|
3379
|
+
warnings: boot.warnings
|
|
3057
3380
|
});
|
|
3058
3381
|
}
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3382
|
+
if ("compile-failed" === boot.verdict.kind) {
|
|
3383
|
+
const { compileErrors } = boot.verdict;
|
|
3384
|
+
return envelope({
|
|
3385
|
+
ok: false,
|
|
3386
|
+
command: start_schema.name,
|
|
3387
|
+
status: "compile-failed",
|
|
3388
|
+
error: {
|
|
3389
|
+
code: "E_FIRST_COMPILE",
|
|
3390
|
+
message: boot.verdict.message ?? `The ${command} process is running but the build failed, so the browser has nothing usable to load.`
|
|
3391
|
+
},
|
|
3392
|
+
value: {
|
|
3393
|
+
...session,
|
|
3394
|
+
buildErrors: compileErrors,
|
|
3395
|
+
...compileErrors.length ? {} : {
|
|
3396
|
+
output: cleanOutput.slice(0, 2000)
|
|
3397
|
+
}
|
|
3398
|
+
},
|
|
3399
|
+
hint: "Fix the build error listed in `value.buildErrors`, then call extension_start again. extension_build reports the same failure on its own.",
|
|
3400
|
+
warnings: boot.warnings
|
|
3401
|
+
});
|
|
3402
|
+
}
|
|
3403
|
+
if ("browser-exited" === boot.verdict.kind || "profile-locked" === boot.verdict.kind) {
|
|
3404
|
+
const stamp = "browser-exited" === boot.verdict.kind ? boot.verdict.stamp : {};
|
|
3405
|
+
return envelope({
|
|
3406
|
+
ok: false,
|
|
3407
|
+
command: start_schema.name,
|
|
3408
|
+
status: boot.verdict.kind,
|
|
3409
|
+
error: {
|
|
3410
|
+
code: "profile-locked" === boot.verdict.kind ? "E_PROFILE_LOCKED" : "E_BROWSER_EXITED",
|
|
3411
|
+
message: `The ${command} process is running but the browser it launched has exited (the extension may have been rejected or the browser crashed). The session cannot be driven.`
|
|
3412
|
+
},
|
|
3413
|
+
value: {
|
|
3414
|
+
...session,
|
|
3415
|
+
...stamp,
|
|
3416
|
+
output: cleanOutput.slice(0, 2000)
|
|
3417
|
+
},
|
|
3418
|
+
hint: "Read `value.output` above and extension_logs for the cause, then call extension_stop to clean up before retrying.",
|
|
3419
|
+
warnings: boot.warnings
|
|
3420
|
+
});
|
|
3421
|
+
}
|
|
3422
|
+
return envelope({
|
|
3073
3423
|
ok: true,
|
|
3074
|
-
|
|
3075
|
-
browser,
|
|
3076
|
-
projectPath: args.projectPath,
|
|
3424
|
+
command: start_schema.name,
|
|
3077
3425
|
status: building ? "started" : "launched",
|
|
3426
|
+
value: {
|
|
3427
|
+
pid,
|
|
3428
|
+
browser,
|
|
3429
|
+
projectPath: args.projectPath,
|
|
3430
|
+
logPath
|
|
3431
|
+
},
|
|
3078
3432
|
hint: building ? "Use extension_wait to check when the build and browser launch are complete. When you are done, call extension_stop to shut down the session." : "Call extension_stop when you are done to close the preview browser.",
|
|
3079
|
-
|
|
3080
|
-
logPath
|
|
3433
|
+
warnings: boot.warnings
|
|
3081
3434
|
});
|
|
3082
3435
|
}
|
|
3083
3436
|
function toMcpSpeak(text) {
|
|
3084
3437
|
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`");
|
|
3085
3438
|
}
|
|
3086
|
-
|
|
3087
|
-
|
|
3439
|
+
const CONTROL_CODES = new Set([
|
|
3440
|
+
"E_CONTROL_DENIED",
|
|
3441
|
+
"E_CONTROL_UNAVAILABLE",
|
|
3442
|
+
"E_NO_CONTROL_CHANNEL",
|
|
3443
|
+
"E_NOT_ATTACHED",
|
|
3444
|
+
"E_SESSION_NOT_FOUND"
|
|
3445
|
+
]);
|
|
3446
|
+
function withSessionContext(message, projectPath, code) {
|
|
3447
|
+
const isControlError = code ? CONTROL_CODES.has(code) : /no active control channel|control channel refused|\b1006\b|no executor connected|is the session started with allowControl/i.test(message);
|
|
3088
3448
|
if (!isControlError) return message;
|
|
3089
3449
|
const dead = deadReadySession(projectPath);
|
|
3090
3450
|
if (dead) return `${message}\nLikely cause: the dev server has exited, ${dead.browser} ready.json still says ready but its pid ${dead.pid} is dead. Restart with extension_dev (this is not an allowControl problem); extension_doctor confirms.`;
|
|
@@ -3094,12 +3454,61 @@ function withSessionContext(message, projectPath) {
|
|
|
3094
3454
|
}
|
|
3095
3455
|
function translateFrame(frame, projectPath) {
|
|
3096
3456
|
if (!frame || false !== frame.ok) return frame;
|
|
3097
|
-
if (frame.error && "string" == typeof frame.error.message) frame.error.message = withSessionContext(toMcpSpeak(frame.error.message), projectPath);
|
|
3457
|
+
if (frame.error && "string" == typeof frame.error.message) frame.error.message = withSessionContext(toMcpSpeak(frame.error.message), projectPath, "string" == typeof frame.error.code ? frame.error.code : void 0);
|
|
3098
3458
|
if ("string" == typeof frame.error?.hint) frame.error.hint = toMcpSpeak(frame.error.hint);
|
|
3099
3459
|
if ("string" == typeof frame.hint) frame.hint = toMcpSpeak(frame.hint);
|
|
3100
3460
|
return frame;
|
|
3101
3461
|
}
|
|
3102
|
-
|
|
3462
|
+
const VERB_TOOL = {
|
|
3463
|
+
eval: "extension_eval",
|
|
3464
|
+
inspect: "extension_dom_snapshot",
|
|
3465
|
+
open: "extension_open",
|
|
3466
|
+
reload: "extension_reload",
|
|
3467
|
+
storage: "extension_storage"
|
|
3468
|
+
};
|
|
3469
|
+
const LEGACY_CODES = {
|
|
3470
|
+
BadRequest: "E_BAD_REQUEST",
|
|
3471
|
+
CliError: "E_CLI",
|
|
3472
|
+
NoSession: "E_NO_SESSION"
|
|
3473
|
+
};
|
|
3474
|
+
function wrapLegacyFrame(frame, command) {
|
|
3475
|
+
const { ok, value, truncated, error, hint, note, notes, warning, ...extras } = frame;
|
|
3476
|
+
const raw = error ?? {};
|
|
3477
|
+
const name = "string" == typeof raw.name ? raw.name : void 0;
|
|
3478
|
+
const wrapped = envelopeObject({
|
|
3479
|
+
ok: true === ok,
|
|
3480
|
+
command,
|
|
3481
|
+
status: true === ok ? "ok" : "failed",
|
|
3482
|
+
value: void 0 === value ? null : value,
|
|
3483
|
+
error: true === ok ? null : {
|
|
3484
|
+
code: "string" == typeof raw.code && raw.code ? raw.code : name && LEGACY_CODES[name] || "E_CLI",
|
|
3485
|
+
message: "string" == typeof raw.message ? raw.message : "command failed",
|
|
3486
|
+
...name ? {
|
|
3487
|
+
name
|
|
3488
|
+
} : {},
|
|
3489
|
+
...void 0 !== raw.engine ? {
|
|
3490
|
+
engine: raw.engine
|
|
3491
|
+
} : {}
|
|
3492
|
+
},
|
|
3493
|
+
...true === truncated ? {
|
|
3494
|
+
truncated: true
|
|
3495
|
+
} : {},
|
|
3496
|
+
..."string" == typeof hint ? {
|
|
3497
|
+
hint
|
|
3498
|
+
} : {},
|
|
3499
|
+
warnings: [
|
|
3500
|
+
"string" == typeof note ? note : null,
|
|
3501
|
+
"string" == typeof warning ? warning : null,
|
|
3502
|
+
...Array.isArray(notes) ? notes.map((n)=>String(n)) : []
|
|
3503
|
+
]
|
|
3504
|
+
});
|
|
3505
|
+
return {
|
|
3506
|
+
...extras,
|
|
3507
|
+
...wrapped
|
|
3508
|
+
};
|
|
3509
|
+
}
|
|
3510
|
+
async function runActVerb(args, projectPath, timeoutMs, tool) {
|
|
3511
|
+
const command = tool ?? VERB_TOOL[args[0]] ?? "extension_act";
|
|
3103
3512
|
const { code, stdout, stderr } = await runExtensionCli([
|
|
3104
3513
|
...args,
|
|
3105
3514
|
"--output",
|
|
@@ -3110,19 +3519,44 @@ async function runActVerb(args, projectPath, timeoutMs) {
|
|
|
3110
3519
|
});
|
|
3111
3520
|
const out = stdout.trim();
|
|
3112
3521
|
if (out) try {
|
|
3113
|
-
const frame = JSON.parse(out);
|
|
3114
|
-
if (
|
|
3115
|
-
|
|
3522
|
+
const frame = translateFrame(JSON.parse(out), projectPath);
|
|
3523
|
+
if (isEnvelope(frame)) {
|
|
3524
|
+
frame.command = command;
|
|
3525
|
+
if (!Array.isArray(frame.warnings)) frame.warnings = [];
|
|
3526
|
+
return JSON.stringify(frame);
|
|
3527
|
+
}
|
|
3528
|
+
if (frame && "object" == typeof frame) return JSON.stringify(wrapLegacyFrame(frame, command));
|
|
3116
3529
|
} catch {}
|
|
3117
3530
|
const message = stderr.trim() || `extension exited with code ${code}`;
|
|
3118
|
-
return
|
|
3531
|
+
return envelope({
|
|
3119
3532
|
ok: false,
|
|
3533
|
+
command,
|
|
3534
|
+
status: "cli-failed",
|
|
3120
3535
|
error: {
|
|
3536
|
+
code: "E_CLI",
|
|
3121
3537
|
name: "CliError",
|
|
3122
3538
|
message: withSessionContext(toMcpSpeak(message), projectPath)
|
|
3123
3539
|
}
|
|
3124
3540
|
});
|
|
3125
3541
|
}
|
|
3542
|
+
function addWarning(frame, text) {
|
|
3543
|
+
if ("string" != typeof text || !text.trim()) return;
|
|
3544
|
+
if (!Array.isArray(frame.warnings)) frame.warnings = [];
|
|
3545
|
+
if (!frame.warnings.includes(text)) frame.warnings.push(text);
|
|
3546
|
+
}
|
|
3547
|
+
function actFrameJson(frame) {
|
|
3548
|
+
return JSON.stringify(frame);
|
|
3549
|
+
}
|
|
3550
|
+
function patchValue(frame, patch) {
|
|
3551
|
+
const current = frame.value;
|
|
3552
|
+
if (current && "object" == typeof current && !Array.isArray(current)) return void Object.assign(current, patch);
|
|
3553
|
+
frame.value = null == current ? {
|
|
3554
|
+
...patch
|
|
3555
|
+
} : {
|
|
3556
|
+
result: current,
|
|
3557
|
+
...patch
|
|
3558
|
+
};
|
|
3559
|
+
}
|
|
3126
3560
|
function commonFlags(args) {
|
|
3127
3561
|
const flags = [];
|
|
3128
3562
|
if (args.context) flags.push("--context", args.context);
|
|
@@ -3609,7 +4043,7 @@ function isCdpEndpoint(port) {
|
|
|
3609
4043
|
});
|
|
3610
4044
|
});
|
|
3611
4045
|
}
|
|
3612
|
-
async function listBridgeTabs(projectPath, browser, timeout) {
|
|
4046
|
+
async function listBridgeTabs(projectPath, browser, timeout, tool = "extension_dom_snapshot") {
|
|
3613
4047
|
const raw = await runActVerb([
|
|
3614
4048
|
"inspect",
|
|
3615
4049
|
projectPath,
|
|
@@ -3620,7 +4054,7 @@ async function listBridgeTabs(projectPath, browser, timeout) {
|
|
|
3620
4054
|
"--timeout",
|
|
3621
4055
|
String(timeout)
|
|
3622
4056
|
] : []
|
|
3623
|
-
], projectPath, timeout);
|
|
4057
|
+
], projectPath, timeout, tool);
|
|
3624
4058
|
let parsed;
|
|
3625
4059
|
try {
|
|
3626
4060
|
parsed = JSON.parse(raw);
|
|
@@ -3662,7 +4096,7 @@ async function pollForBridgeTab(projectPath, browser, url, budgetMs) {
|
|
|
3662
4096
|
await new Promise((r)=>setTimeout(r, 250));
|
|
3663
4097
|
}
|
|
3664
4098
|
}
|
|
3665
|
-
async function navigateToUrlViaBridge(projectPath, browser, url, timeout) {
|
|
4099
|
+
async function navigateToUrlViaBridge(projectPath, browser, url, timeout, tool = "extension_open") {
|
|
3666
4100
|
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 }; })()`;
|
|
3667
4101
|
const raw = await runActVerb([
|
|
3668
4102
|
"eval",
|
|
@@ -3676,32 +4110,39 @@ async function navigateToUrlViaBridge(projectPath, browser, url, timeout) {
|
|
|
3676
4110
|
"--timeout",
|
|
3677
4111
|
String(timeout)
|
|
3678
4112
|
] : []
|
|
3679
|
-
], projectPath, timeout);
|
|
4113
|
+
], projectPath, timeout, tool);
|
|
3680
4114
|
try {
|
|
3681
4115
|
const parsed = JSON.parse(raw);
|
|
3682
|
-
if (parsed?.ok === false) {
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
}
|
|
4116
|
+
if (parsed?.ok === false) return actFrameJson(parsed.hint ? parsed : {
|
|
4117
|
+
...parsed,
|
|
4118
|
+
hint: "On this browser family URL navigation rides the agent bridge (a background eval of tabs.update), so the dev session must be started with allowEval: true (extension_dev)."
|
|
4119
|
+
});
|
|
3686
4120
|
} catch {
|
|
3687
4121
|
return raw;
|
|
3688
4122
|
}
|
|
3689
4123
|
const settled = await pollForBridgeTab(projectPath, browser, url, null != timeout ? Math.min(timeout, 6000) : 6000);
|
|
3690
|
-
if (!settled) return
|
|
4124
|
+
if (!settled) return envelope({
|
|
3691
4125
|
ok: false,
|
|
4126
|
+
command: tool,
|
|
4127
|
+
status: "navigate-failed",
|
|
3692
4128
|
error: {
|
|
4129
|
+
code: "E_NAVIGATE_FAILED",
|
|
3693
4130
|
name: "NavigateFailed",
|
|
3694
4131
|
message: `Navigation to ${url} did not produce a tab reporting that URL. The URL may not exist, or the browser refused the navigation (Firefox rejects privileged about:/chrome: URLs and other extensions' moz-extension: pages).`
|
|
3695
4132
|
},
|
|
3696
4133
|
hint: "Confirm the URL, or discover open tabs with extension_dom_snapshot listTabs: true. For an extension page, the path must match the BUILT manifest."
|
|
3697
4134
|
});
|
|
3698
|
-
return
|
|
4135
|
+
return envelope({
|
|
3699
4136
|
ok: true,
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
4137
|
+
command: tool,
|
|
4138
|
+
status: "navigated",
|
|
4139
|
+
value: {
|
|
4140
|
+
navigated: url,
|
|
4141
|
+
tab: {
|
|
4142
|
+
tabId: settled.tabId,
|
|
4143
|
+
url: settled.url,
|
|
4144
|
+
title: settled.title
|
|
4145
|
+
}
|
|
3705
4146
|
},
|
|
3706
4147
|
hint: "Inspect it with extension_dom_snapshot or extension_eval using url or this numeric tab id (context: 'page'/'content')."
|
|
3707
4148
|
});
|
|
@@ -3763,9 +4204,12 @@ function isDisposableTab(tabUrl, destination) {
|
|
|
3763
4204
|
async function navigateToUrl(projectPath, browser, url, timeout) {
|
|
3764
4205
|
if (!isChromiumFamily(browser)) return navigateToUrlViaBridge(projectPath, browser, url, timeout);
|
|
3765
4206
|
const resolved = await resolveCdpPort(projectPath, browser);
|
|
3766
|
-
if (!resolved) return
|
|
4207
|
+
if (!resolved) return envelope({
|
|
3767
4208
|
ok: false,
|
|
4209
|
+
command: open_schema.name,
|
|
4210
|
+
status: "no-session",
|
|
3768
4211
|
error: {
|
|
4212
|
+
code: "E_NO_SESSION",
|
|
3769
4213
|
name: "NoSession",
|
|
3770
4214
|
message: `No active dev session / CDP port for ${browser}. Start extension_dev and extension_wait for ready. ${CDP_PORT_MISSING_HINT}`
|
|
3771
4215
|
}
|
|
@@ -3796,38 +4240,48 @@ async function navigateToUrl(projectPath, browser, url, timeout) {
|
|
|
3796
4240
|
const settled = await pollForTarget(resolved.port, url, 6000, navigatedTargetId);
|
|
3797
4241
|
if (!settled) {
|
|
3798
4242
|
const isExtensionPage = url.startsWith("chrome-extension://");
|
|
3799
|
-
return
|
|
4243
|
+
return envelope({
|
|
3800
4244
|
ok: false,
|
|
4245
|
+
command: open_schema.name,
|
|
4246
|
+
status: "navigate-failed",
|
|
3801
4247
|
error: {
|
|
4248
|
+
code: "E_NAVIGATE_FAILED",
|
|
3802
4249
|
name: "NavigateFailed",
|
|
3803
4250
|
message: `Navigation to ${url} did not produce a live page target. ${isExtensionPage ? "The URL may not exist in the extension bundle, or Chrome refused the navigation." : "The page may have failed to load, or the browser refused the navigation."}`
|
|
3804
4251
|
},
|
|
3805
4252
|
hint: isExtensionPage ? "Confirm the path exists in the built dist (extension_build / extension_analyze list entrypoints). For an extension page, the path must match the BUILT manifest, which may differ from your source layout." : "Confirm the URL loads in a normal browser and that the dev session's browser has network access. Nothing about your extension bundle is implicated in a failed http(s) navigation."
|
|
3806
4253
|
});
|
|
3807
4254
|
}
|
|
3808
|
-
return
|
|
4255
|
+
return envelope({
|
|
3809
4256
|
ok: true,
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
4257
|
+
command: open_schema.name,
|
|
4258
|
+
status: "navigated",
|
|
4259
|
+
value: {
|
|
4260
|
+
navigated: url,
|
|
4261
|
+
...openedNewTab ? {
|
|
4262
|
+
openedNewTab: true
|
|
4263
|
+
} : {},
|
|
4264
|
+
...settled.redirectedFrom ? {
|
|
4265
|
+
redirected: {
|
|
4266
|
+
from: settled.redirectedFrom,
|
|
4267
|
+
to: settled.url
|
|
4268
|
+
}
|
|
4269
|
+
} : {},
|
|
4270
|
+
target: {
|
|
4271
|
+
targetId: settled.id,
|
|
4272
|
+
title: settled.title,
|
|
4273
|
+
url: settled.url
|
|
3818
4274
|
}
|
|
3819
|
-
} : {},
|
|
3820
|
-
target: {
|
|
3821
|
-
targetId: settled.id,
|
|
3822
|
-
title: settled.title,
|
|
3823
|
-
url: settled.url
|
|
3824
4275
|
},
|
|
3825
4276
|
hint: "Inspect it with extension_dom_snapshot or extension_inspect using url (context: 'page'), they resolve the tab themselves. `target.targetId` is a CDP target id, NOT a chrome.tabs id: do not pass it as `tab`. If you need a numeric tab id, call extension_dom_snapshot with listTabs: true."
|
|
3826
4277
|
});
|
|
3827
4278
|
} catch (e) {
|
|
3828
|
-
return
|
|
4279
|
+
return envelope({
|
|
3829
4280
|
ok: false,
|
|
4281
|
+
command: open_schema.name,
|
|
4282
|
+
status: "navigate-failed",
|
|
3830
4283
|
error: {
|
|
4284
|
+
code: "E_NAVIGATE_ERROR",
|
|
3831
4285
|
name: "NavigateError",
|
|
3832
4286
|
message: e instanceof Error ? e.message : String(e)
|
|
3833
4287
|
}
|
|
@@ -3941,9 +4395,12 @@ function declaredSurfaces(projectPath, browser) {
|
|
|
3941
4395
|
}
|
|
3942
4396
|
function missingSurfaceError(projectPath, browser, surface, consequence) {
|
|
3943
4397
|
const declared = declaredSurfaces(projectPath, browser);
|
|
3944
|
-
if (null === declared) return
|
|
4398
|
+
if (null === declared) return envelope({
|
|
3945
4399
|
ok: false,
|
|
4400
|
+
command: open_schema.name,
|
|
4401
|
+
status: "no-manifest",
|
|
3946
4402
|
error: {
|
|
4403
|
+
code: "E_MANIFEST_NOT_FOUND",
|
|
3947
4404
|
name: "NoSurfaceDocument",
|
|
3948
4405
|
message: `No readable manifest was found for this project (checked dist/${browser}, dist, src, and the project root), so the ${surface} document cannot be resolved.`
|
|
3949
4406
|
},
|
|
@@ -3952,15 +4409,18 @@ function missingSurfaceError(projectPath, browser, surface, consequence) {
|
|
|
3952
4409
|
const key = SURFACE_MANIFEST_KEYS[surface] ?? surface;
|
|
3953
4410
|
const others = declared.filter((s)=>s !== surface);
|
|
3954
4411
|
const nextVerb = "popup" === surface ? 'To exercise the toolbar button of a popup-less extension, call extension_open with surface: "action", which replays chrome.action.onClicked. To give the extension a popup, set action.default_popup in the manifest and rebuild.' : `To add one, set ${key} in the manifest and rebuild.`;
|
|
3955
|
-
return
|
|
4412
|
+
return envelope({
|
|
3956
4413
|
ok: false,
|
|
4414
|
+
command: open_schema.name,
|
|
4415
|
+
status: "no-surface",
|
|
3957
4416
|
error: {
|
|
4417
|
+
code: "E_NO_SURFACE_DOCUMENT",
|
|
3958
4418
|
name: "NoSurfaceDocument",
|
|
3959
4419
|
message: `This extension declares no ${surface}: nothing in its manifest sets ${key}, ${consequence}. That is how the extension is built, not a failure of the session or the tooling.`
|
|
3960
4420
|
},
|
|
3961
|
-
|
|
4421
|
+
value: others.length ? {
|
|
3962
4422
|
declaredSurfaces: others
|
|
3963
|
-
} :
|
|
4423
|
+
} : null,
|
|
3964
4424
|
hint: (others.length ? `Surfaces this extension does declare: ${others.join(", ")}; extension_open can target those. ` : "The manifest declares no other UI surface documents either. ") + nextVerb
|
|
3965
4425
|
});
|
|
3966
4426
|
}
|
|
@@ -4027,9 +4487,12 @@ async function openSurfaceAsTab(projectPath, browser, surface) {
|
|
|
4027
4487
|
let extensionId = null;
|
|
4028
4488
|
if (isChromiumFamily(browser)) {
|
|
4029
4489
|
extensionId = await resolveExtensionId(projectPath, browser);
|
|
4030
|
-
if (!extensionId) return
|
|
4490
|
+
if (!extensionId) return envelope({
|
|
4031
4491
|
ok: false,
|
|
4492
|
+
command: open_schema.name,
|
|
4493
|
+
status: "no-extension-id",
|
|
4032
4494
|
error: {
|
|
4495
|
+
code: "E_NO_EXTENSION_ID",
|
|
4033
4496
|
name: "NoExtensionId",
|
|
4034
4497
|
message: "Could not resolve the extension id from the live session's CDP targets."
|
|
4035
4498
|
},
|
|
@@ -4038,9 +4501,12 @@ async function openSurfaceAsTab(projectPath, browser, surface) {
|
|
|
4038
4501
|
url = `chrome-extension://${extensionId}/${doc}`;
|
|
4039
4502
|
} else {
|
|
4040
4503
|
const base = await resolveBridgeBaseUrl(projectPath, browser);
|
|
4041
|
-
if (!base) return
|
|
4504
|
+
if (!base) return envelope({
|
|
4042
4505
|
ok: false,
|
|
4506
|
+
command: open_schema.name,
|
|
4507
|
+
status: "no-extension-id",
|
|
4043
4508
|
error: {
|
|
4509
|
+
code: "E_NO_EXTENSION_ID",
|
|
4044
4510
|
name: "NoExtensionId",
|
|
4045
4511
|
message: "Could not resolve the extension's moz-extension:// base URL from the live session (a background eval of runtime.getURL)."
|
|
4046
4512
|
},
|
|
@@ -4053,18 +4519,22 @@ async function openSurfaceAsTab(projectPath, browser, surface) {
|
|
|
4053
4519
|
try {
|
|
4054
4520
|
const parsed = JSON.parse(raw);
|
|
4055
4521
|
if (parsed?.ok) {
|
|
4056
|
-
|
|
4522
|
+
const renderedAsTab = {
|
|
4057
4523
|
surface,
|
|
4058
4524
|
document: doc,
|
|
4059
4525
|
extensionId
|
|
4060
4526
|
};
|
|
4527
|
+
patchValue(parsed, {
|
|
4528
|
+
renderedAsTab
|
|
4529
|
+
});
|
|
4530
|
+
const target = parsed.value?.target ?? parsed.target;
|
|
4061
4531
|
let popupBounds = null;
|
|
4062
|
-
if (("popup" === surface || "action" === surface) && "string" == typeof
|
|
4063
|
-
popupBounds = await applyPopupBounds(projectPath, browser,
|
|
4064
|
-
if (popupBounds)
|
|
4532
|
+
if (("popup" === surface || "action" === surface) && "string" == typeof target?.targetId) {
|
|
4533
|
+
popupBounds = await applyPopupBounds(projectPath, browser, target.targetId);
|
|
4534
|
+
if (popupBounds) renderedAsTab.popupBounds = popupBounds;
|
|
4065
4535
|
}
|
|
4066
4536
|
parsed.hint = `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. ") + `Inspect it with extension_dom_snapshot context: '${surface}' (include: ['html']), or extension_inspect with this url. ` + "Do NOT pass this extension-page url to extension_dom_snapshot or extension_eval as a tab target: script injection cannot reach extension pages, only the surface context or CDP can.";
|
|
4067
|
-
return
|
|
4537
|
+
return actFrameJson(parsed);
|
|
4068
4538
|
}
|
|
4069
4539
|
} catch {}
|
|
4070
4540
|
return raw;
|
|
@@ -4086,25 +4556,32 @@ async function confirmSurfaceTarget(projectPath, browser, surface, raw) {
|
|
|
4086
4556
|
const wanted = `chrome-extension://${extensionId}/${doc}`;
|
|
4087
4557
|
const settled = await pollForTarget(resolved.port, wanted, 3000);
|
|
4088
4558
|
if (settled) {
|
|
4089
|
-
parsed
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4559
|
+
patchValue(parsed, {
|
|
4560
|
+
surfaceTarget: {
|
|
4561
|
+
targetId: settled.id,
|
|
4562
|
+
url: settled.url
|
|
4563
|
+
}
|
|
4564
|
+
});
|
|
4565
|
+
return actFrameJson(parsed);
|
|
4094
4566
|
}
|
|
4095
|
-
return
|
|
4567
|
+
return envelope({
|
|
4096
4568
|
ok: false,
|
|
4569
|
+
command: open_schema.name,
|
|
4570
|
+
status: "surface-did-not-open",
|
|
4097
4571
|
error: {
|
|
4572
|
+
code: "E_SURFACE_DID_NOT_OPEN",
|
|
4098
4573
|
name: "SurfaceDidNotOpen",
|
|
4099
4574
|
message: `The engine reported the ${surface} as opened, but no page target for ${wanted} appeared within 3s, so nothing is there to inspect.`
|
|
4100
4575
|
},
|
|
4101
|
-
|
|
4576
|
+
value: {
|
|
4577
|
+
engineResult: parsed
|
|
4578
|
+
},
|
|
4102
4579
|
hint: `Retry with asTab: true to render ${doc} in a real tab, which works headed or headless. If you expected a window, check that the session is headed and that the surface is declared in the BUILT manifest.`
|
|
4103
4580
|
});
|
|
4104
4581
|
}
|
|
4105
4582
|
const open_schema = {
|
|
4106
4583
|
name: "extension_open",
|
|
4107
|
-
description: "Open an extension surface or replay an event in a running session. popup
|
|
4584
|
+
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).",
|
|
4108
4585
|
inputSchema: {
|
|
4109
4586
|
type: "object",
|
|
4110
4587
|
properties: {
|
|
@@ -4161,22 +4638,30 @@ async function open_handler(args) {
|
|
|
4161
4638
|
"bookmarks"
|
|
4162
4639
|
];
|
|
4163
4640
|
if (args.asTab && args.surface && AS_TAB_SURFACES.includes(args.surface)) return openSurfaceAsTab(args.projectPath, browser, args.surface);
|
|
4164
|
-
if (!args.surface) return
|
|
4641
|
+
if (!args.surface) return envelope({
|
|
4165
4642
|
ok: false,
|
|
4643
|
+
command: open_schema.name,
|
|
4644
|
+
status: "bad-request",
|
|
4166
4645
|
error: {
|
|
4646
|
+
code: "E_BAD_REQUEST",
|
|
4167
4647
|
name: "BadRequest",
|
|
4168
4648
|
message: "Pass `surface` (popup/options/sidebar/action/command) to open a surface, or `url` to navigate a tab."
|
|
4169
4649
|
}
|
|
4170
4650
|
});
|
|
4171
4651
|
if ("command" === args.surface) {
|
|
4172
4652
|
const declared = declaredCommands(args.projectPath, browser);
|
|
4173
|
-
if (declared && args.name && !declared.includes(args.name)) return
|
|
4653
|
+
if (declared && args.name && !declared.includes(args.name)) return envelope({
|
|
4174
4654
|
ok: false,
|
|
4655
|
+
command: open_schema.name,
|
|
4656
|
+
status: "unknown-command",
|
|
4175
4657
|
error: {
|
|
4658
|
+
code: "E_UNKNOWN_COMMAND",
|
|
4176
4659
|
name: "UnknownCommand",
|
|
4177
4660
|
message: `"${args.name}" is not declared in the manifest's \`commands\`, so triggering it can only ever be a no-op.`
|
|
4178
4661
|
},
|
|
4179
|
-
|
|
4662
|
+
value: {
|
|
4663
|
+
declaredCommands: declared
|
|
4664
|
+
},
|
|
4180
4665
|
hint: declared.length ? `Declared commands are: ${declared.join(", ")}. Check for a typo, or add "${args.name}" to the manifest.` : "This manifest declares no commands at all. Add a `commands` block, rebuild, then retry."
|
|
4181
4666
|
});
|
|
4182
4667
|
}
|
|
@@ -4192,7 +4677,7 @@ async function open_handler(args) {
|
|
|
4192
4677
|
if ("command" === args.surface && args.name) cli.push("--name", args.name);
|
|
4193
4678
|
cli.push("--browser", browser);
|
|
4194
4679
|
if (null != args.timeout) cli.push("--timeout", String(args.timeout));
|
|
4195
|
-
const raw = await runActVerb(cli, args.projectPath, args.timeout);
|
|
4680
|
+
const raw = await runActVerb(cli, args.projectPath, args.timeout, open_schema.name);
|
|
4196
4681
|
const headless = sessionIsHeadless();
|
|
4197
4682
|
if (headless && [
|
|
4198
4683
|
"popup",
|
|
@@ -4201,19 +4686,21 @@ async function open_handler(args) {
|
|
|
4201
4686
|
].includes(args.surface)) try {
|
|
4202
4687
|
const parsed = JSON.parse(raw);
|
|
4203
4688
|
const msg = String(parsed?.error?.message ?? "");
|
|
4204
|
-
|
|
4689
|
+
const code = "string" == typeof parsed?.error?.code ? parsed.error.code : "";
|
|
4690
|
+
const refusedWindow = "E_NO_TARGET" === code || /active browser window|no active|headless|user gesture/i.test(msg);
|
|
4691
|
+
if (parsed?.ok === false && refusedWindow) {
|
|
4205
4692
|
if (AS_TAB_SURFACES.includes(args.surface)) {
|
|
4206
4693
|
const fallback = await openSurfaceAsTab(args.projectPath, browser, args.surface);
|
|
4207
4694
|
try {
|
|
4208
4695
|
const parsedFallback = JSON.parse(fallback);
|
|
4209
4696
|
if (parsedFallback?.ok) {
|
|
4210
|
-
parsedFallback
|
|
4211
|
-
return
|
|
4697
|
+
addWarning(parsedFallback, `The dev browser is headless, and a real popup/sidebar window can only open in a headed session, so the surface was rendered as a tab instead. For the real window, ${HEADED_RELAUNCH}, then open the surface again without asTab.`);
|
|
4698
|
+
return actFrameJson(parsedFallback);
|
|
4212
4699
|
}
|
|
4213
4700
|
} catch {}
|
|
4214
4701
|
}
|
|
4215
4702
|
if (!parsed.hint) parsed.hint = /user gesture/i.test(msg) ? "This surface can only open from a real user gesture, which headless automation cannot produce. Retry with asTab: true to render the surface document in a tab instead." : `The dev browser is running headless, and a popup/sidebar window needs a headed session. Retry with asTab: true to render the surface document in a tab, or for the real window, ${HEADED_RELAUNCH}.`;
|
|
4216
|
-
return
|
|
4703
|
+
return actFrameJson(parsed);
|
|
4217
4704
|
}
|
|
4218
4705
|
} catch {}
|
|
4219
4706
|
return AS_TAB_SURFACES.includes(args.surface) ? confirmSurfaceTarget(args.projectPath, browser, args.surface, raw) : raw;
|
|
@@ -4543,6 +5030,7 @@ function recordSharedPreview(projectPath, entry) {
|
|
|
4543
5030
|
} : {}
|
|
4544
5031
|
};
|
|
4545
5032
|
}
|
|
5033
|
+
const preview_web_COMMAND = "extension_preview_web";
|
|
4546
5034
|
const DEFAULT_PREVIEW_DEV_URL = "http://localhost:3110";
|
|
4547
5035
|
const SURFACE = {
|
|
4548
5036
|
defaultOrigin: DEFAULT_PREVIEW_DEV_URL,
|
|
@@ -4634,7 +5122,7 @@ function detectSurfaces(manifest) {
|
|
|
4634
5122
|
}
|
|
4635
5123
|
const preview_web_schema = {
|
|
4636
5124
|
name: "extension_preview_web",
|
|
4637
|
-
description: "Preview an in-progress extension in the web emulator, with no real browser.
|
|
5125
|
+
description: "Preview an in-progress extension in the web emulator, with no real browser. This builds the project (unless build:false), points preview.extension.dev at dist/<browser> over the dev-only preview://build scheme, and returns a deep link plus a loadability check. Use it as the author's door for a local build: it renders your build and carries the Emulated/Real lane toggle and the Trace tab, but the deep link resolves only on this machine. Pass share:true to get a public link that reaches anyone. Call extension_shares to list and revoke every link shared this way, so one never vanishes with this response.",
|
|
4638
5126
|
inputSchema: {
|
|
4639
5127
|
type: "object",
|
|
4640
5128
|
properties: {
|
|
@@ -4698,41 +5186,61 @@ async function preview_web_handler(args) {
|
|
|
4698
5186
|
buildResult = JSON.parse(raw);
|
|
4699
5187
|
} catch {
|
|
4700
5188
|
buildResult = {
|
|
4701
|
-
|
|
5189
|
+
ok: false,
|
|
4702
5190
|
raw
|
|
4703
5191
|
};
|
|
4704
5192
|
}
|
|
4705
|
-
if (!buildResult || true !== buildResult.
|
|
5193
|
+
if (!buildResult || true !== buildResult.ok) return envelope({
|
|
4706
5194
|
ok: false,
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
5195
|
+
command: preview_web_COMMAND,
|
|
5196
|
+
status: "build-failed",
|
|
5197
|
+
error: {
|
|
5198
|
+
code: "E_BUILD_FAILED",
|
|
5199
|
+
message: "The build failed, so there is nothing to preview. See buildResult for the cause."
|
|
5200
|
+
},
|
|
5201
|
+
value: {
|
|
5202
|
+
stage: "build",
|
|
5203
|
+
buildResult
|
|
5204
|
+
}
|
|
4710
5205
|
});
|
|
4711
5206
|
}
|
|
4712
5207
|
const distDir = args.distPath ? node_path.resolve(args.distPath) : node_path.resolve(args.projectPath, "dist", browser);
|
|
4713
5208
|
const manifestPath = node_path.join(distDir, "manifest.json");
|
|
4714
|
-
if (!node_fs.existsSync(manifestPath)) return
|
|
5209
|
+
if (!node_fs.existsSync(manifestPath)) return envelope({
|
|
4715
5210
|
ok: false,
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
error:
|
|
5211
|
+
command: preview_web_COMMAND,
|
|
5212
|
+
status: "no-dist",
|
|
5213
|
+
error: {
|
|
5214
|
+
code: "E_NO_DIST",
|
|
5215
|
+
message: `No manifest.json in ${distDir}. Build the project first (build:true), or pass distPath to an already-built directory.`
|
|
5216
|
+
},
|
|
5217
|
+
value: {
|
|
5218
|
+
stage: "resolve-dist",
|
|
5219
|
+
distDir
|
|
5220
|
+
}
|
|
4719
5221
|
});
|
|
4720
5222
|
let manifest = {};
|
|
4721
5223
|
try {
|
|
4722
5224
|
manifest = JSON.parse(node_fs.readFileSync(manifestPath, "utf8"));
|
|
4723
5225
|
} catch (err) {
|
|
4724
|
-
return
|
|
5226
|
+
return envelope({
|
|
4725
5227
|
ok: false,
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
error:
|
|
5228
|
+
command: preview_web_COMMAND,
|
|
5229
|
+
status: "bad-dist-manifest",
|
|
5230
|
+
error: {
|
|
5231
|
+
code: "E_BAD_MANIFEST",
|
|
5232
|
+
message: `manifest.json in ${distDir} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
5233
|
+
},
|
|
5234
|
+
value: {
|
|
5235
|
+
stage: "resolve-dist",
|
|
5236
|
+
distDir
|
|
5237
|
+
}
|
|
4729
5238
|
});
|
|
4730
5239
|
}
|
|
4731
5240
|
const encoded = Buffer.from(distDir).toString("base64url");
|
|
4732
5241
|
const internalUrl = SURFACE.scheme(encoded);
|
|
4733
5242
|
const deepLink = `${hostBase}/?url=${encodeURIComponent(internalUrl)}`;
|
|
4734
5243
|
const result = {
|
|
4735
|
-
ok: true,
|
|
4736
5244
|
deepLink,
|
|
4737
5245
|
distDir,
|
|
4738
5246
|
manifest: {
|
|
@@ -4745,9 +5253,10 @@ async function preview_web_handler(args) {
|
|
|
4745
5253
|
built: true
|
|
4746
5254
|
} : {
|
|
4747
5255
|
built: false
|
|
4748
|
-
}
|
|
4749
|
-
hint: `Open deepLink in a browser to see the extension render in ${SURFACE.label}'s emulator. It must be running (${SURFACE.devCommand}). Once it renders, the Trace tab shows every chrome.* call it makes, and the lane toggle switches between the emulated backend and a real carrier-equipped browser.`
|
|
5256
|
+
}
|
|
4750
5257
|
};
|
|
5258
|
+
const hint = `Open deepLink in a browser to see the extension render in ${SURFACE.label}'s emulator. It must be running (${SURFACE.devCommand}). Once it renders, the Trace tab shows every chrome.* call it makes, and the lane toggle switches between the emulated backend and a real carrier-equipped browser.`;
|
|
5259
|
+
const previewWarnings = [];
|
|
4751
5260
|
if (args.open) {
|
|
4752
5261
|
const sessionBrowser = args.openIn ?? browser;
|
|
4753
5262
|
const navRaw = await navigateToUrl(args.projectPath, sessionBrowser, deepLink);
|
|
@@ -4762,10 +5271,17 @@ async function preview_web_handler(args) {
|
|
|
4762
5271
|
}
|
|
4763
5272
|
result.opened = opened;
|
|
4764
5273
|
result.openedIn = sessionBrowser;
|
|
4765
|
-
if (true !== opened.ok)
|
|
5274
|
+
if (true !== opened.ok) previewWarnings.push("Could not open the preview in a browser. This needs a live dev session (run extension_dev, then extension_wait for ready). The deepLink above still works if you open it yourself.");
|
|
4766
5275
|
}
|
|
4767
5276
|
if (args.share) result.share = await buildShare(args.projectPath, distDir, manifest, browser);
|
|
4768
|
-
if (false === args.probe) return
|
|
5277
|
+
if (false === args.probe) return envelope({
|
|
5278
|
+
ok: true,
|
|
5279
|
+
command: preview_web_COMMAND,
|
|
5280
|
+
status: "previewed",
|
|
5281
|
+
value: result,
|
|
5282
|
+
hint,
|
|
5283
|
+
warnings: previewWarnings
|
|
5284
|
+
});
|
|
4769
5285
|
const probeUrl = `${hostBase}${SURFACE.fetchPath}?url=${encodeURIComponent(internalUrl)}`;
|
|
4770
5286
|
try {
|
|
4771
5287
|
const res = await fetch(probeUrl, {
|
|
@@ -4774,37 +5290,62 @@ async function preview_web_handler(args) {
|
|
|
4774
5290
|
}
|
|
4775
5291
|
});
|
|
4776
5292
|
const contentType = res.headers.get("content-type") ?? "";
|
|
4777
|
-
if (!res.ok || !contentType.includes("application/json")) return
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
5293
|
+
if (!res.ok || !contentType.includes("application/json")) return envelope({
|
|
5294
|
+
ok: true,
|
|
5295
|
+
command: preview_web_COMMAND,
|
|
5296
|
+
status: "host-not-serving-preview",
|
|
5297
|
+
value: {
|
|
5298
|
+
...result,
|
|
5299
|
+
hostReachable: true,
|
|
5300
|
+
previewLoadable: false,
|
|
5301
|
+
probe: {
|
|
5302
|
+
status: res.status,
|
|
5303
|
+
contentType
|
|
5304
|
+
}
|
|
5305
|
+
},
|
|
5306
|
+
hint,
|
|
5307
|
+
warnings: [
|
|
5308
|
+
...previewWarnings,
|
|
5309
|
+
`${SURFACE.label} answered but not with a preview payload. On the deployed host ${SURFACE.fetchPath} does not exist (dev-only); run a local dev server (${SURFACE.devCommand}) to use web preview.`
|
|
5310
|
+
]
|
|
4786
5311
|
});
|
|
4787
5312
|
const payload = await res.json();
|
|
4788
|
-
return
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
5313
|
+
return envelope({
|
|
5314
|
+
ok: true,
|
|
5315
|
+
command: preview_web_COMMAND,
|
|
5316
|
+
status: "previewed",
|
|
5317
|
+
value: {
|
|
5318
|
+
...result,
|
|
5319
|
+
hostReachable: true,
|
|
5320
|
+
previewLoadable: true,
|
|
5321
|
+
probe: {
|
|
5322
|
+
identifier: payload.identifier,
|
|
5323
|
+
loadedName: payload.manifest?.name,
|
|
5324
|
+
loadedVersion: payload.version,
|
|
5325
|
+
fileCount: Array.isArray(payload.files) ? payload.files.length : 0
|
|
5326
|
+
}
|
|
5327
|
+
},
|
|
5328
|
+
hint,
|
|
5329
|
+
warnings: previewWarnings
|
|
4798
5330
|
});
|
|
4799
5331
|
} catch (err) {
|
|
4800
|
-
return
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
5332
|
+
return envelope({
|
|
5333
|
+
ok: true,
|
|
5334
|
+
command: preview_web_COMMAND,
|
|
5335
|
+
status: "host-unreachable",
|
|
5336
|
+
value: {
|
|
5337
|
+
...result,
|
|
5338
|
+
hostReachable: false,
|
|
5339
|
+
previewLoadable: false,
|
|
5340
|
+
probe: {
|
|
5341
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5342
|
+
}
|
|
5343
|
+
},
|
|
5344
|
+
hint,
|
|
5345
|
+
warnings: [
|
|
5346
|
+
...previewWarnings,
|
|
5347
|
+
`Could not reach ${SURFACE.label} at ${hostBase}. Start it with '${SURFACE.devCommand}', then open deepLink.`
|
|
5348
|
+
]
|
|
4808
5349
|
});
|
|
4809
5350
|
}
|
|
4810
5351
|
}
|
|
@@ -4972,7 +5513,7 @@ async function revokeArtifact(options) {
|
|
|
4972
5513
|
const LOGIN_HINT = "Run extension_auth (action: login), or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard).";
|
|
4973
5514
|
const shares_schema = {
|
|
4974
5515
|
name: "extension_shares",
|
|
4975
|
-
description: "List and revoke the public preview links this token has shared
|
|
5516
|
+
description: "List and revoke the public preview links this token has shared, which is what extension_preview_web share:true hands out. Pass action:'list' (the default) for every artifact the logged-in project owns, with its artifactId, name, version, live or dead state, createdAt, expiresAt, revokedAt, size, previewUrl, zipUrl and revokeUrl, so a link whose response you lost is findable again. Each row carries owner and sharedBy as the platform returned them. Read attribution.ownership for who may revoke a share: 'project' means the workspace holds it and any member can pull it back, 'personal' means one person holds it alone, 'unknown' means no owner was disclosed. Read attribution.credit as credit only, never access; it names the publisher, and reads 'CLI token <id>' or 'not recorded' when no person can be named. Pass action:'revoke' with an artifactId, or with any URL of the share, to kill one permanently. Pass projectPath to reconcile against the project's own append-only .extension.dev/shared-previews.json, which is read and never rewritten: a share made on another machine shows as remoteOnly, a record with no live artifact as localOnly. This needs the same token as sharing (extension_auth or EXTENSION_DEV_TOKEN); without one, listing still returns the local record with a login hint.",
|
|
4976
5517
|
inputSchema: {
|
|
4977
5518
|
type: "object",
|
|
4978
5519
|
properties: {
|
|
@@ -5099,23 +5640,29 @@ async function listShares(args) {
|
|
|
5099
5640
|
});
|
|
5100
5641
|
if (!listing.ok) {
|
|
5101
5642
|
const isAuth = "SharesAuthError" === listing.error.name;
|
|
5102
|
-
return
|
|
5643
|
+
return envelope({
|
|
5103
5644
|
ok: true,
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5645
|
+
command: "extension_shares",
|
|
5646
|
+
status: "listed-local-only",
|
|
5647
|
+
value: {
|
|
5648
|
+
action: "list",
|
|
5649
|
+
server: {
|
|
5650
|
+
listed: false,
|
|
5651
|
+
errorName: listing.error.name,
|
|
5652
|
+
reason: listing.error.message,
|
|
5653
|
+
...isAuth ? {
|
|
5654
|
+
loginHint: LOGIN_HINT
|
|
5655
|
+
} : {}
|
|
5656
|
+
},
|
|
5657
|
+
shares: [],
|
|
5658
|
+
localOnly: (local?.entries ?? []).map((entry)=>({
|
|
5659
|
+
...entry
|
|
5660
|
+
})),
|
|
5661
|
+
localRecord
|
|
5112
5662
|
},
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
})),
|
|
5117
|
-
localRecord,
|
|
5118
|
-
note: isAuth ? `The platform was not asked, so live and dead cannot be told apart here. ${LOGIN_HINT} localOnly is this project's own record of every link it ever shared, revoke handles included.` : "The platform could not be reached, so localOnly below is this project's own record and not a statement about what is still live."
|
|
5663
|
+
warnings: [
|
|
5664
|
+
isAuth ? `The platform was not asked, so live and dead cannot be told apart here. ${LOGIN_HINT} localOnly is this project's own record of every link it ever shared, revoke handles included.` : "The platform could not be reached, so localOnly below is this project's own record and not a statement about what is still live."
|
|
5665
|
+
]
|
|
5119
5666
|
});
|
|
5120
5667
|
}
|
|
5121
5668
|
const now = Date.now();
|
|
@@ -5153,37 +5700,50 @@ async function listShares(args) {
|
|
|
5153
5700
|
personal: shares.filter((s)=>"personal" === s.attribution.ownership).length,
|
|
5154
5701
|
unknown: shares.filter((s)=>"unknown" === s.attribution.ownership).length
|
|
5155
5702
|
};
|
|
5156
|
-
|
|
5703
|
+
const truncatedNote = listing.data.truncated ? `This is not the whole set: ${listing.data.count} of ${listing.data.matched} matched shares came back at limit ${listing.data.limit}. truncated also goes true when the server spent its budget working out which shares you are entitled to see, so matched is a floor and not a total. Raise limit (max 200) or pass status:"live" to narrow it, and do not read a missing share as revoked.` : null;
|
|
5704
|
+
return envelope({
|
|
5157
5705
|
ok: true,
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5706
|
+
command: "extension_shares",
|
|
5707
|
+
status: "listed",
|
|
5708
|
+
value: {
|
|
5709
|
+
action: "list",
|
|
5710
|
+
server: {
|
|
5711
|
+
listed: true,
|
|
5712
|
+
count: listing.data.count,
|
|
5713
|
+
matched: listing.data.matched,
|
|
5714
|
+
limit: listing.data.limit,
|
|
5715
|
+
truncated: listing.data.truncated,
|
|
5716
|
+
scanned: listing.data.scanned,
|
|
5717
|
+
ownership,
|
|
5718
|
+
...truncatedNote ? {
|
|
5719
|
+
truncatedNote
|
|
5720
|
+
} : {}
|
|
5721
|
+
},
|
|
5722
|
+
shares,
|
|
5723
|
+
...local ? {
|
|
5724
|
+
localOnly
|
|
5725
|
+
} : {},
|
|
5726
|
+
localRecord
|
|
5170
5727
|
},
|
|
5171
|
-
shares
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
note: "previewUrl and zipUrl are null for a share that is no longer live, because a revoked or expired link cannot resolve for anyone. revokeUrl stays on every row. Revocation is permanent: a revoked id is burned and re-sharing the same build mints a different link.",
|
|
5178
|
-
attributionNote: "attribution.ownership says who the share belongs to and therefore who may revoke it: project means the owning workspace holds it and any member can pull it back, personal means one person holds it alone. attribution.credit names the publisher and is attribution only, granting and restricting nothing. A credit of \"CLI token ...\" means the platform could not resolve which human minted that token, and a credit of \"not recorded\" means it never knew; neither is a name, and neither should be reported as one."
|
|
5728
|
+
hint: `${liveCount} of ${shares.length} listed shares still resolve${local ? `; ${localOnly.length} local record ${1 === localOnly.length ? "entry has" : "entries have"} no artifact behind them` : ""}. Revoke one with action:"revoke" and its artifactId or any of its URLs.`,
|
|
5729
|
+
warnings: [
|
|
5730
|
+
"previewUrl and zipUrl are null for a share that is no longer live, because a revoked or expired link cannot resolve for anyone. revokeUrl stays on every row. Revocation is permanent: a revoked id is burned and re-sharing the same build mints a different link.",
|
|
5731
|
+
"attribution.ownership says who the share belongs to and therefore who may revoke it: project means the owning workspace holds it and any member can pull it back, personal means one person holds it alone. attribution.credit names the publisher and is attribution only, granting and restricting nothing. A credit of \"CLI token ...\" means the platform could not resolve which human minted that token, and a credit of \"not recorded\" means it never knew; neither is a name, and neither should be reported as one.",
|
|
5732
|
+
truncatedNote
|
|
5733
|
+
]
|
|
5179
5734
|
});
|
|
5180
5735
|
}
|
|
5181
5736
|
async function revokeShare(args) {
|
|
5182
5737
|
const ref = parseArtifactRef(args.artifactId || args.url || "");
|
|
5183
|
-
if (!ref) return
|
|
5738
|
+
if (!ref) return envelope({
|
|
5184
5739
|
ok: false,
|
|
5185
|
-
|
|
5740
|
+
command: "extension_shares",
|
|
5741
|
+
status: "bad-request",
|
|
5742
|
+
value: {
|
|
5743
|
+
action: "revoke"
|
|
5744
|
+
},
|
|
5186
5745
|
error: {
|
|
5746
|
+
code: "E_BAD_REQUEST",
|
|
5187
5747
|
name: "SharesInputError",
|
|
5188
5748
|
message: "Nothing to revoke. Pass artifactId (a gen_... id) or url (the previewUrl, zipUrl, viewUrl, or revokeUrl of the share). Run action:\"list\" to see both."
|
|
5189
5749
|
}
|
|
@@ -5199,34 +5759,43 @@ async function revokeShare(args) {
|
|
|
5199
5759
|
const recordNote = local ? entry ? `${local.path} still lists this share as its own append-only history and was not rewritten, so the entry stays with its original sharedAt. The platform is the truth for whether a link resolves.` : `${local.path} has no entry for this share, so it was made from another machine or another checkout.` : void 0;
|
|
5200
5760
|
if (!result.ok) {
|
|
5201
5761
|
const isAuth = "SharesAuthError" === result.error.name;
|
|
5202
|
-
return
|
|
5762
|
+
return envelope({
|
|
5203
5763
|
ok: false,
|
|
5204
|
-
|
|
5205
|
-
|
|
5764
|
+
command: "extension_shares",
|
|
5765
|
+
status: "revoke-failed",
|
|
5766
|
+
value: {
|
|
5767
|
+
action: "revoke",
|
|
5768
|
+
artifactId: ref
|
|
5769
|
+
},
|
|
5206
5770
|
error: {
|
|
5771
|
+
code: isAuth ? "E_AUTH_REQUIRED" : "E_PLATFORM",
|
|
5207
5772
|
name: result.error.name,
|
|
5208
5773
|
message: result.error.message
|
|
5209
5774
|
},
|
|
5210
5775
|
...isAuth ? {
|
|
5211
|
-
|
|
5776
|
+
hint: LOGIN_HINT
|
|
5212
5777
|
} : {},
|
|
5213
|
-
|
|
5778
|
+
warnings: [
|
|
5214
5779
|
recordNote
|
|
5215
|
-
|
|
5780
|
+
]
|
|
5216
5781
|
});
|
|
5217
5782
|
}
|
|
5218
|
-
return
|
|
5783
|
+
return envelope({
|
|
5219
5784
|
ok: true,
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5785
|
+
command: "extension_shares",
|
|
5786
|
+
status: "revoked",
|
|
5787
|
+
value: {
|
|
5788
|
+
action: "revoke",
|
|
5789
|
+
artifactId: ref,
|
|
5790
|
+
revoked: result.data.revoked,
|
|
5791
|
+
...result.data.revokedAt ? {
|
|
5792
|
+
revokedAt: result.data.revokedAt
|
|
5793
|
+
} : {}
|
|
5794
|
+
},
|
|
5795
|
+
warnings: [
|
|
5796
|
+
"The link is dead for everyone, permanently: the zip is deleted and the id is burned, so it can never resolve again. Sharing the same build later returns a different link, and anyone holding the old one gets nothing.",
|
|
5227
5797
|
recordNote
|
|
5228
|
-
|
|
5229
|
-
note: "The link is dead for everyone, permanently: the zip is deleted and the id is burned, so it can never resolve again. Sharing the same build later returns a different link, and anyone holding the old one gets nothing."
|
|
5798
|
+
]
|
|
5230
5799
|
});
|
|
5231
5800
|
}
|
|
5232
5801
|
async function shares_handler(args) {
|
|
@@ -5259,6 +5828,7 @@ async function shares_handler(args) {
|
|
|
5259
5828
|
} : {}
|
|
5260
5829
|
});
|
|
5261
5830
|
}
|
|
5831
|
+
const manifest_validate_COMMAND = "extension_manifest_validate";
|
|
5262
5832
|
const CHROME_DESKTOP_ONLY_KEYS = [
|
|
5263
5833
|
"file_browser_handlers",
|
|
5264
5834
|
"file_system_provider_capabilities",
|
|
@@ -5352,7 +5922,7 @@ const KNOWN_PERMISSIONS = new Set([
|
|
|
5352
5922
|
]);
|
|
5353
5923
|
const manifest_validate_schema = {
|
|
5354
5924
|
name: "extension_manifest_validate",
|
|
5355
|
-
description: "Validate a manifest.json
|
|
5925
|
+
description: "Validate a manifest.json across browsers. This reports missing fields, invalid permissions, dangling file references, and cross-browser compatibility issues. Read buildBlocking for the errors that make extension_build refuse.",
|
|
5356
5926
|
inputSchema: {
|
|
5357
5927
|
type: "object",
|
|
5358
5928
|
properties: {
|
|
@@ -5552,29 +6122,51 @@ async function manifest_validate_handler(args) {
|
|
|
5552
6122
|
similarTemplates: []
|
|
5553
6123
|
};
|
|
5554
6124
|
const manifestPath = args.manifestPath ?? (args.projectPath ? findManifest(args.projectPath) : null);
|
|
5555
|
-
if (!manifestPath)
|
|
5556
|
-
|
|
5557
|
-
errors: [
|
|
6125
|
+
if (!manifestPath) {
|
|
6126
|
+
const errors = [
|
|
5558
6127
|
args.projectPath ? `No manifest.json found under ${args.projectPath} (looked in the root and src/).` : "Pass manifestPath (path to manifest.json) or projectPath (project root)."
|
|
5559
|
-
]
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
6128
|
+
];
|
|
6129
|
+
return envelope({
|
|
6130
|
+
ok: false,
|
|
6131
|
+
command: manifest_validate_COMMAND,
|
|
6132
|
+
status: "manifest-not-found",
|
|
6133
|
+
error: {
|
|
6134
|
+
code: "E_MANIFEST_NOT_FOUND",
|
|
6135
|
+
message: errors[0]
|
|
6136
|
+
},
|
|
6137
|
+
value: {
|
|
6138
|
+
valid: false,
|
|
6139
|
+
errors,
|
|
6140
|
+
browserSupport: {},
|
|
6141
|
+
similarTemplates: []
|
|
6142
|
+
},
|
|
6143
|
+
warnings: []
|
|
6144
|
+
});
|
|
6145
|
+
}
|
|
5564
6146
|
const manifestDir = node_path.dirname(node_path.resolve(manifestPath));
|
|
5565
6147
|
let manifest;
|
|
5566
6148
|
try {
|
|
5567
6149
|
const raw = node_fs.readFileSync(node_path.resolve(manifestPath), "utf8");
|
|
5568
6150
|
manifest = JSON.parse(raw);
|
|
5569
6151
|
} catch (err) {
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
6152
|
+
const errors = [
|
|
6153
|
+
`Cannot read manifest: ${err instanceof Error ? err.message : err}`
|
|
6154
|
+
];
|
|
6155
|
+
return envelope({
|
|
6156
|
+
ok: false,
|
|
6157
|
+
command: manifest_validate_COMMAND,
|
|
6158
|
+
status: "manifest-unreadable",
|
|
6159
|
+
error: {
|
|
6160
|
+
code: "E_BAD_MANIFEST",
|
|
6161
|
+
message: errors[0]
|
|
6162
|
+
},
|
|
6163
|
+
value: {
|
|
6164
|
+
valid: false,
|
|
6165
|
+
errors,
|
|
6166
|
+
browserSupport: {},
|
|
6167
|
+
similarTemplates: []
|
|
6168
|
+
},
|
|
6169
|
+
warnings: []
|
|
5578
6170
|
});
|
|
5579
6171
|
}
|
|
5580
6172
|
if (!manifest.name) result.errors.push("Missing required field: name");
|
|
@@ -5694,9 +6286,15 @@ async function manifest_validate_handler(args) {
|
|
|
5694
6286
|
else result.warnings.push(`${browser} (not requested, checked by default): ${issues}`);
|
|
5695
6287
|
}
|
|
5696
6288
|
result.valid = 0 === result.errors.length;
|
|
5697
|
-
return
|
|
5698
|
-
|
|
5699
|
-
|
|
6289
|
+
return envelope({
|
|
6290
|
+
ok: result.valid,
|
|
6291
|
+
command: manifest_validate_COMMAND,
|
|
6292
|
+
status: result.valid ? "valid" : "invalid",
|
|
6293
|
+
value: {
|
|
6294
|
+
...result,
|
|
6295
|
+
buildBlocking: result.errors.length > 0
|
|
6296
|
+
},
|
|
6297
|
+
warnings: result.warnings
|
|
5700
6298
|
});
|
|
5701
6299
|
}
|
|
5702
6300
|
const CHROME_THEME_COLOR_KEYS = {
|
|
@@ -6304,9 +6902,10 @@ function resolveChromeTheme(theme) {
|
|
|
6304
6902
|
caveats
|
|
6305
6903
|
};
|
|
6306
6904
|
}
|
|
6905
|
+
const theme_verify_COMMAND = "extension_theme_verify";
|
|
6307
6906
|
const theme_verify_schema = {
|
|
6308
6907
|
name: "extension_theme_verify",
|
|
6309
|
-
description: "Verify a Chrome theme manifest before it ships.
|
|
6908
|
+
description: "Verify a Chrome theme manifest before it ships. This settles the four-leg WYSIWYG contract (app-shows == manifest-says == chrome-paints, plus chrome-accepts) as far as is possible headless: it derives every color current Chrome would paint from the manifest through the transcribed Chromium resolver, and classifies each problem as D1 fabrication, D3 parity gap, or D4 acceptance gap (keys Chrome silently discards: dead legacy, incognito, unknown, out-of-range). It verifies only, and never authors or mutates a theme. The app-rendered and real-pixel legs need a browser, so they come back as needsAttended pointing at the assert:theme and install-parity harnesses, never as passed.",
|
|
6310
6909
|
inputSchema: {
|
|
6311
6910
|
type: "object",
|
|
6312
6911
|
properties: {
|
|
@@ -6369,9 +6968,12 @@ async function theme_verify_handler(args) {
|
|
|
6369
6968
|
try {
|
|
6370
6969
|
text = await promises.readFile(abs, "utf8");
|
|
6371
6970
|
} catch {
|
|
6372
|
-
return
|
|
6971
|
+
return envelope({
|
|
6373
6972
|
ok: false,
|
|
6973
|
+
command: theme_verify_COMMAND,
|
|
6974
|
+
status: "bad-input",
|
|
6374
6975
|
error: {
|
|
6976
|
+
code: "E_BAD_REQUEST",
|
|
6375
6977
|
name: "InputError",
|
|
6376
6978
|
message: `Cannot read ${abs}`
|
|
6377
6979
|
}
|
|
@@ -6380,18 +6982,24 @@ async function theme_verify_handler(args) {
|
|
|
6380
6982
|
try {
|
|
6381
6983
|
raw = JSON.parse(text);
|
|
6382
6984
|
} catch (err) {
|
|
6383
|
-
return
|
|
6985
|
+
return envelope({
|
|
6384
6986
|
ok: false,
|
|
6987
|
+
command: theme_verify_COMMAND,
|
|
6988
|
+
status: "bad-input",
|
|
6385
6989
|
error: {
|
|
6990
|
+
code: "E_BAD_REQUEST",
|
|
6386
6991
|
name: "InputError",
|
|
6387
6992
|
message: `${abs} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
6388
6993
|
}
|
|
6389
6994
|
});
|
|
6390
6995
|
}
|
|
6391
6996
|
} else {
|
|
6392
|
-
if (!args.manifest) return
|
|
6997
|
+
if (!args.manifest) return envelope({
|
|
6393
6998
|
ok: false,
|
|
6999
|
+
command: theme_verify_COMMAND,
|
|
7000
|
+
status: "bad-request",
|
|
6394
7001
|
error: {
|
|
7002
|
+
code: "E_BAD_REQUEST",
|
|
6395
7003
|
name: "InputError",
|
|
6396
7004
|
message: "Pass `manifest` (an object) or `manifestPath` (a file path)."
|
|
6397
7005
|
}
|
|
@@ -6403,9 +7011,12 @@ async function theme_verify_handler(args) {
|
|
|
6403
7011
|
try {
|
|
6404
7012
|
({ manifest, theme } = coerceInput(raw));
|
|
6405
7013
|
} catch (err) {
|
|
6406
|
-
return
|
|
7014
|
+
return envelope({
|
|
6407
7015
|
ok: false,
|
|
7016
|
+
command: theme_verify_COMMAND,
|
|
7017
|
+
status: "bad-input",
|
|
6408
7018
|
error: {
|
|
7019
|
+
code: "E_BAD_REQUEST",
|
|
6409
7020
|
name: "InputError",
|
|
6410
7021
|
message: err instanceof Error ? err.message : String(err)
|
|
6411
7022
|
}
|
|
@@ -6504,74 +7115,77 @@ async function theme_verify_handler(args) {
|
|
|
6504
7115
|
how: "a Chrome theme is an unpacked extension: extension_dev then extension_logs on the exported theme dir (headless, focus-safe)"
|
|
6505
7116
|
}
|
|
6506
7117
|
];
|
|
6507
|
-
return
|
|
7118
|
+
return envelope({
|
|
6508
7119
|
ok: true,
|
|
6509
|
-
|
|
6510
|
-
verdict,
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
},
|
|
6522
|
-
legs: {
|
|
6523
|
-
appShows: {
|
|
6524
|
-
status: "needs-attended",
|
|
6525
|
-
detail: "Not run here (needs a browser). The app self-verifies app == resolve(manifest) via the seed door.",
|
|
6526
|
-
how: attended[0].how
|
|
6527
|
-
},
|
|
6528
|
-
manifestSays: {
|
|
6529
|
-
status: hasError ? "invalid" : "verified",
|
|
6530
|
-
name,
|
|
6531
|
-
version,
|
|
6532
|
-
grammar: {
|
|
6533
|
-
nameValid,
|
|
6534
|
-
versionValid,
|
|
6535
|
-
errors: grammarErrors
|
|
6536
|
-
},
|
|
6537
|
-
declared: {
|
|
6538
|
-
colors: Object.keys(declaredColors),
|
|
6539
|
-
tints: Object.keys(declaredTints),
|
|
6540
|
-
images: Object.keys(declaredImages),
|
|
6541
|
-
properties: Object.keys(declaredProps)
|
|
7120
|
+
command: theme_verify_COMMAND,
|
|
7121
|
+
status: verdict,
|
|
7122
|
+
value: {
|
|
7123
|
+
needsAttended: true,
|
|
7124
|
+
summary: {
|
|
7125
|
+
errors: grammarErrors.length,
|
|
7126
|
+
warnings: findings.filter((f)=>"warn" === f.severity).length,
|
|
7127
|
+
advisories: findings.filter((f)=>"info" === f.severity).length,
|
|
7128
|
+
byClass: {
|
|
7129
|
+
D1: findings.filter((f)=>"D1" === f.class).length,
|
|
7130
|
+
D3: findings.filter((f)=>"D3" === f.class).length,
|
|
7131
|
+
D4: findings.filter((f)=>"D4" === f.class).length
|
|
6542
7132
|
}
|
|
6543
7133
|
},
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
status: "reported",
|
|
6547
|
-
detail: "Headless proxy: every color current stable Chrome derives from this manifest.",
|
|
6548
|
-
resolved
|
|
6549
|
-
},
|
|
6550
|
-
realPaint: {
|
|
7134
|
+
legs: {
|
|
7135
|
+
appShows: {
|
|
6551
7136
|
status: "needs-attended",
|
|
6552
|
-
|
|
7137
|
+
detail: "Not run here (needs a browser). The app self-verifies app == resolve(manifest) via the seed door.",
|
|
7138
|
+
how: attended[0].how
|
|
7139
|
+
},
|
|
7140
|
+
manifestSays: {
|
|
7141
|
+
status: hasError ? "invalid" : "verified",
|
|
7142
|
+
name,
|
|
7143
|
+
version,
|
|
7144
|
+
grammar: {
|
|
7145
|
+
nameValid,
|
|
7146
|
+
versionValid,
|
|
7147
|
+
errors: grammarErrors
|
|
7148
|
+
},
|
|
7149
|
+
declared: {
|
|
7150
|
+
colors: Object.keys(declaredColors),
|
|
7151
|
+
tints: Object.keys(declaredTints),
|
|
7152
|
+
images: Object.keys(declaredImages),
|
|
7153
|
+
properties: Object.keys(declaredProps)
|
|
7154
|
+
}
|
|
7155
|
+
},
|
|
7156
|
+
chromePaints: {
|
|
7157
|
+
resolver: {
|
|
7158
|
+
status: "reported",
|
|
7159
|
+
detail: "Headless proxy: every color current stable Chrome derives from this manifest.",
|
|
7160
|
+
resolved
|
|
7161
|
+
},
|
|
7162
|
+
realPaint: {
|
|
7163
|
+
status: "needs-attended",
|
|
7164
|
+
how: attended[1].how
|
|
7165
|
+
}
|
|
7166
|
+
},
|
|
7167
|
+
chromeAccepts: {
|
|
7168
|
+
status: "reported",
|
|
7169
|
+
detail: "Static analysis of what Chrome parses but discards (the D4 acceptance gap).",
|
|
7170
|
+
discarded: findings.filter((f)=>"chrome-accepts" === f.leg).map((f)=>({
|
|
7171
|
+
key: f.key,
|
|
7172
|
+
detail: f.detail
|
|
7173
|
+
})),
|
|
7174
|
+
live: {
|
|
7175
|
+
status: "needs-attended",
|
|
7176
|
+
how: attended[2].how
|
|
7177
|
+
}
|
|
6553
7178
|
}
|
|
6554
7179
|
},
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
discarded: findings.filter((f)=>"chrome-accepts" === f.leg).map((f)=>({
|
|
6559
|
-
key: f.key,
|
|
6560
|
-
detail: f.detail
|
|
6561
|
-
})),
|
|
6562
|
-
live: {
|
|
6563
|
-
status: "needs-attended",
|
|
6564
|
-
how: attended[2].how
|
|
6565
|
-
}
|
|
6566
|
-
}
|
|
6567
|
-
},
|
|
6568
|
-
findings,
|
|
6569
|
-
attended
|
|
7180
|
+
findings,
|
|
7181
|
+
attended
|
|
7182
|
+
}
|
|
6570
7183
|
});
|
|
6571
7184
|
}
|
|
7185
|
+
const analyze_COMMAND = "extension_analyze";
|
|
6572
7186
|
const analyze_schema = {
|
|
6573
7187
|
name: "extension_analyze",
|
|
6574
|
-
description: "Analyze a BUILT extension on disk: file sizes, declared entry points, permissions, bundle composition, and store-readiness checks.
|
|
7188
|
+
description: "Analyze a BUILT extension on disk: file sizes, declared entry points, permissions, bundle composition, and store-readiness checks. This is static only: it reads dist/<browser> from the filesystem and never touches a browser, so build first with extension_build. Use extension_inspect for a running extension's live DOM and console.",
|
|
6575
7189
|
inputSchema: {
|
|
6576
7190
|
type: "object",
|
|
6577
7191
|
properties: {
|
|
@@ -6663,8 +7277,14 @@ function formatBytes(bytes) {
|
|
|
6663
7277
|
async function analyze_handler(args) {
|
|
6664
7278
|
const browser = args.browser ?? "chrome";
|
|
6665
7279
|
const distPath = node_path.resolve(args.projectPath, "dist", browser);
|
|
6666
|
-
if (!node_fs.existsSync(distPath)) return
|
|
6667
|
-
|
|
7280
|
+
if (!node_fs.existsSync(distPath)) return envelope({
|
|
7281
|
+
ok: false,
|
|
7282
|
+
command: analyze_COMMAND,
|
|
7283
|
+
status: "no-dist",
|
|
7284
|
+
error: {
|
|
7285
|
+
code: "E_NO_DIST",
|
|
7286
|
+
message: `Build output not found at ${distPath}. Run extension_build first.`
|
|
7287
|
+
},
|
|
6668
7288
|
hint: `Use extension_build with browser: "${browser}" to build the extension.`
|
|
6669
7289
|
});
|
|
6670
7290
|
const files = walkDir(distPath);
|
|
@@ -6720,25 +7340,18 @@ async function analyze_handler(args) {
|
|
|
6720
7340
|
if (PROMO_RE.test(f.path)) sizeWarnings.push(`${f.path} (${formatBytes(f.size)}) looks like a store-listing promo image shipped inside the extension package, move it out of the bundled sources so it does not inflate the store zip.`);
|
|
6721
7341
|
else if ("image" === f.type && !f.path.includes("icon") && f.size > 51200 && shippableSize > 0 && f.size / shippableSize > 0.25) sizeWarnings.push(`${f.path} (${formatBytes(f.size)}) is ${Math.round(f.size / shippableSize * 100)}% of the shipped bundle, unusually large for a shipped asset.`);
|
|
6722
7342
|
}
|
|
7343
|
+
const note = "development" === buildType ? `This dist contains ${formatBytes(sourcemapSize)} of sourcemaps and looks like a dev build; run extension_build for production sizes. shippableSize excludes sourcemaps.` : void 0;
|
|
7344
|
+
const archiveNote = archiveSize > 0 ? `This dist contains ${formatBytes(archiveSize)} of .zip archive(s) (store packaging output, written into dist by zip builds). shippableSize excludes them so the packaged copy does not double-count the files it contains.` : void 0;
|
|
6723
7345
|
const result = {
|
|
6724
7346
|
browser,
|
|
6725
7347
|
distPath,
|
|
6726
7348
|
entrypoints,
|
|
6727
|
-
...sizeWarnings.length ? {
|
|
6728
|
-
sizeWarnings
|
|
6729
|
-
} : {},
|
|
6730
7349
|
buildType,
|
|
6731
7350
|
totalSize,
|
|
6732
7351
|
totalSizeFormatted: formatBytes(totalSize),
|
|
6733
7352
|
shippableSize,
|
|
6734
7353
|
shippableSizeFormatted: formatBytes(shippableSize),
|
|
6735
7354
|
fileCount: files.length,
|
|
6736
|
-
..."development" === buildType ? {
|
|
6737
|
-
note: `This dist contains ${formatBytes(sourcemapSize)} of sourcemaps and looks like a dev build; run extension_build for production sizes. shippableSize excludes sourcemaps.`
|
|
6738
|
-
} : {},
|
|
6739
|
-
...archiveSize > 0 ? {
|
|
6740
|
-
archiveNote: `This dist contains ${formatBytes(archiveSize)} of .zip archive(s) (store packaging output, written into dist by zip builds). shippableSize excludes them so the packaged copy does not double-count the files it contains.`
|
|
6741
|
-
} : {},
|
|
6742
7355
|
manifest: {
|
|
6743
7356
|
name: manifest.name,
|
|
6744
7357
|
version: manifest.version,
|
|
@@ -6774,7 +7387,17 @@ async function analyze_handler(args) {
|
|
|
6774
7387
|
under10MB: totalSize - archiveSize < 10485760
|
|
6775
7388
|
}
|
|
6776
7389
|
};
|
|
6777
|
-
return
|
|
7390
|
+
return envelope({
|
|
7391
|
+
ok: true,
|
|
7392
|
+
command: analyze_COMMAND,
|
|
7393
|
+
status: "analyzed",
|
|
7394
|
+
value: result,
|
|
7395
|
+
warnings: [
|
|
7396
|
+
...sizeWarnings,
|
|
7397
|
+
note,
|
|
7398
|
+
archiveNote
|
|
7399
|
+
]
|
|
7400
|
+
});
|
|
6778
7401
|
}
|
|
6779
7402
|
function rdp_define_property(obj, key, value) {
|
|
6780
7403
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
@@ -7013,6 +7636,7 @@ async function rdpCollectConsoleMessages(port, options) {
|
|
|
7013
7636
|
return messages;
|
|
7014
7637
|
});
|
|
7015
7638
|
}
|
|
7639
|
+
const TOOL = "extension_inspect";
|
|
7016
7640
|
function buildBridgeInspectExpression(opts) {
|
|
7017
7641
|
const parts = [
|
|
7018
7642
|
"const out = {};"
|
|
@@ -7090,7 +7714,7 @@ async function collectGeckoDeepDom(args, browser, urlFilter, cap, result, notes)
|
|
|
7090
7714
|
"--timeout",
|
|
7091
7715
|
String(args.timeout)
|
|
7092
7716
|
] : []
|
|
7093
|
-
], args.projectPath, args.timeout);
|
|
7717
|
+
], args.projectPath, args.timeout, TOOL);
|
|
7094
7718
|
let parsed;
|
|
7095
7719
|
try {
|
|
7096
7720
|
parsed = JSON.parse(raw);
|
|
@@ -7130,11 +7754,11 @@ async function collectGeckoConsole(args, browser, urlFilter, result, notes) {
|
|
|
7130
7754
|
async function inspectViaBridge(args, browser, include, maxBytes) {
|
|
7131
7755
|
const notes = [];
|
|
7132
7756
|
if (args.url) {
|
|
7133
|
-
const listed = await listBridgeTabs(args.projectPath, browser, args.timeout);
|
|
7757
|
+
const listed = await listBridgeTabs(args.projectPath, browser, args.timeout, "extension_inspect");
|
|
7134
7758
|
if ("error" in listed) return listed.error;
|
|
7135
7759
|
const already = listed.tabs.some((t)=>t.url.includes(args.url));
|
|
7136
7760
|
if (!already) {
|
|
7137
|
-
const nav = await navigateToUrlViaBridge(args.projectPath, browser, args.url, args.timeout);
|
|
7761
|
+
const nav = await navigateToUrlViaBridge(args.projectPath, browser, args.url, args.timeout, "extension_inspect");
|
|
7138
7762
|
try {
|
|
7139
7763
|
if (JSON.parse(nav)?.ok !== true) return nav;
|
|
7140
7764
|
} catch {
|
|
@@ -7167,7 +7791,7 @@ async function inspectViaBridge(args, browser, include, maxBytes) {
|
|
|
7167
7791
|
"--timeout",
|
|
7168
7792
|
String(args.timeout)
|
|
7169
7793
|
] : []
|
|
7170
|
-
], args.projectPath, args.timeout);
|
|
7794
|
+
], args.projectPath, args.timeout, TOOL);
|
|
7171
7795
|
let parsed;
|
|
7172
7796
|
try {
|
|
7173
7797
|
parsed = JSON.parse(raw);
|
|
@@ -7188,7 +7812,7 @@ async function inspectViaBridge(args, browser, include, maxBytes) {
|
|
|
7188
7812
|
"--timeout",
|
|
7189
7813
|
String(args.timeout)
|
|
7190
7814
|
] : []
|
|
7191
|
-
], args.projectPath, args.timeout);
|
|
7815
|
+
], args.projectPath, args.timeout, TOOL);
|
|
7192
7816
|
try {
|
|
7193
7817
|
parsed = JSON.parse(raw);
|
|
7194
7818
|
} catch {
|
|
@@ -7196,9 +7820,12 @@ async function inspectViaBridge(args, browser, include, maxBytes) {
|
|
|
7196
7820
|
}
|
|
7197
7821
|
const frame = Array.isArray(parsed?.value?.frames) ? parsed.value.frames[0] : null;
|
|
7198
7822
|
if (parsed?.ok === true && frame && "object" == typeof frame) value = frame;
|
|
7199
|
-
else if (parsed?.ok === true) return
|
|
7823
|
+
else if (parsed?.ok === true) return envelope({
|
|
7200
7824
|
ok: false,
|
|
7825
|
+
command: TOOL,
|
|
7826
|
+
status: "inspect-failed",
|
|
7201
7827
|
error: {
|
|
7828
|
+
code: "E_BRIDGE",
|
|
7202
7829
|
name: "InspectFailed",
|
|
7203
7830
|
message: String(parsed?.value?.error ?? "the content-script inspect returned nothing")
|
|
7204
7831
|
},
|
|
@@ -7224,10 +7851,11 @@ async function inspectViaBridge(args, browser, include, maxBytes) {
|
|
|
7224
7851
|
}
|
|
7225
7852
|
if (include.has("dom_snapshot") && value.domSnapshot) result.domSnapshot = value.domSnapshot;
|
|
7226
7853
|
if (include.has("extension_roots") && void 0 !== value.extensionRoots) result.extensionRoots = value.extensionRoots;
|
|
7854
|
+
let probeWarning = null;
|
|
7227
7855
|
if (value.probes) {
|
|
7228
7856
|
result.probes = value.probes;
|
|
7229
7857
|
const jsLooking = (args.probe ?? []).filter((p)=>/^typeof\s|^(chrome|browser|window|document)\.|\(\)|=>|===/.test(p));
|
|
7230
|
-
if (jsLooking.length)
|
|
7858
|
+
if (jsLooking.length) probeWarning = `Probes are CSS selectors run through querySelectorAll against the live page, NOT JavaScript expressions. ${jsLooking.map((s)=>`"${s}"`).join(", ")} parsed as selectors and will match nothing. To evaluate JS, use extension_eval.`;
|
|
7231
7859
|
}
|
|
7232
7860
|
const urlFilter = args.url ?? ("string" == typeof value.meta?.url ? value.meta.url : void 0);
|
|
7233
7861
|
if (include.has("console")) await collectGeckoConsole(args, browser, urlFilter, result, notes);
|
|
@@ -7235,12 +7863,20 @@ async function inspectViaBridge(args, browser, include, maxBytes) {
|
|
|
7235
7863
|
const cap = maxBytes > 0 ? maxBytes : 65536;
|
|
7236
7864
|
await collectGeckoDeepDom(args, browser, urlFilter, cap, result, notes);
|
|
7237
7865
|
}
|
|
7238
|
-
|
|
7239
|
-
|
|
7866
|
+
return envelope({
|
|
7867
|
+
ok: true,
|
|
7868
|
+
command: TOOL,
|
|
7869
|
+
status: "inspected",
|
|
7870
|
+
value: result,
|
|
7871
|
+
warnings: [
|
|
7872
|
+
...notes,
|
|
7873
|
+
probeWarning
|
|
7874
|
+
]
|
|
7875
|
+
});
|
|
7240
7876
|
}
|
|
7241
7877
|
const inspect_schema = {
|
|
7242
7878
|
name: "extension_inspect",
|
|
7243
|
-
description: "
|
|
7879
|
+
description: "Inspect a running extension deeply over the browser's debugger protocol: full HTML including shadow DOM, DOM structure, content-script injection, console messages, and CSS selector queries through `probe`. This is the ONLY tool that pierces closed shadow roots (deepDom), runs selector probes, and navigates a tab to `url` before reading it. It reads a web or override page and picks the first inspectable target, or the first whose url contains `url`; it cannot address an extension surface by name and takes no chrome.tabs id. Use extension_dom_snapshot to choose which tab or which open surface (popup, options, sidebar, devtools) to read, or to enumerate what is open. Use extension_analyze for a built extension's files and sizes on disk. Chromium rides the Chrome DevTools Protocol and needs the session's debug port, not allowControl. Firefox is fully paired: summary, meta, html, dom_snapshot, extension_roots and probes ride the agent bridge and need allowEval:true, console rides the RDP watcher replay on engine 4.0.15 and later, and deepDom needs an MV2 session with host permissions for the target url, because the Firefox MV3 background CSP blocks bridge evals. This requires an active dev or start session.",
|
|
7244
7880
|
inputSchema: {
|
|
7245
7881
|
type: "object",
|
|
7246
7882
|
properties: {
|
|
@@ -7303,8 +7939,14 @@ async function inspect_handler(args) {
|
|
|
7303
7939
|
const maxBytes = args.maxBytes ?? 262144;
|
|
7304
7940
|
if (!isChromiumFamily(browser)) return inspectViaBridge(args, browser, include, maxBytes);
|
|
7305
7941
|
const resolved = await resolveCdpPort(args.projectPath, browser);
|
|
7306
|
-
if (!resolved) return
|
|
7307
|
-
|
|
7942
|
+
if (!resolved) return envelope({
|
|
7943
|
+
ok: false,
|
|
7944
|
+
command: inspect_schema.name,
|
|
7945
|
+
status: "no-session",
|
|
7946
|
+
error: {
|
|
7947
|
+
code: "E_NO_SESSION",
|
|
7948
|
+
message: "No active dev session found. Cannot connect to Chrome DevTools Protocol."
|
|
7949
|
+
},
|
|
7308
7950
|
hint: `Start a dev session first with extension_dev, then use extension_wait to confirm it is ready. ${CDP_PORT_MISSING_HINT}`
|
|
7309
7951
|
});
|
|
7310
7952
|
const cdpPort = resolved.port;
|
|
@@ -7321,14 +7963,22 @@ async function inspect_handler(args) {
|
|
|
7321
7963
|
const pageTargets = allTargets.filter((t)=>"page" === t.type && !t.url.startsWith("devtools://") && (!t.url.startsWith("chrome://") || isOverridePage(t.url)));
|
|
7322
7964
|
if (0 === pageTargets.length) {
|
|
7323
7965
|
const chromeOnly = allTargets.some((t)=>"page" === t.type && t.url.startsWith("chrome://"));
|
|
7324
|
-
return
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7966
|
+
return envelope({
|
|
7967
|
+
ok: false,
|
|
7968
|
+
command: inspect_schema.name,
|
|
7969
|
+
status: "no-inspectable-target",
|
|
7970
|
+
error: {
|
|
7971
|
+
code: "E_NO_TARGET",
|
|
7972
|
+
message: chromeOnly ? "No inspectable page targets found. Only internal chrome:// pages are open; open the extension's surface (or pass a url to navigate a tab) first." : "No inspectable page targets found. The extension may not have opened a page yet."
|
|
7973
|
+
},
|
|
7974
|
+
value: {
|
|
7975
|
+
cdpPort,
|
|
7976
|
+
browser,
|
|
7977
|
+
allTargets: allTargets.map((t)=>({
|
|
7978
|
+
type: t.type,
|
|
7979
|
+
url: t.url?.slice(0, 100)
|
|
7980
|
+
}))
|
|
7981
|
+
}
|
|
7332
7982
|
});
|
|
7333
7983
|
}
|
|
7334
7984
|
const target = args.url ? pageTargets.find((t)=>t.url.includes(args.url)) ?? pageTargets[0] : pageTargets[0];
|
|
@@ -7392,11 +8042,29 @@ async function inspect_handler(args) {
|
|
|
7392
8042
|
result.closedShadowRoots = closed;
|
|
7393
8043
|
result.deepDom = true;
|
|
7394
8044
|
}
|
|
7395
|
-
|
|
8045
|
+
const probeWarning = result.probeWarning;
|
|
8046
|
+
delete result.probeWarning;
|
|
8047
|
+
return envelope({
|
|
8048
|
+
ok: true,
|
|
8049
|
+
command: inspect_schema.name,
|
|
8050
|
+
status: "inspected",
|
|
8051
|
+
value: result,
|
|
8052
|
+
warnings: [
|
|
8053
|
+
"string" == typeof probeWarning ? probeWarning : null
|
|
8054
|
+
]
|
|
8055
|
+
});
|
|
7396
8056
|
} catch (err) {
|
|
7397
|
-
return
|
|
7398
|
-
|
|
7399
|
-
|
|
8057
|
+
return envelope({
|
|
8058
|
+
ok: false,
|
|
8059
|
+
command: inspect_schema.name,
|
|
8060
|
+
status: "cdp-failed",
|
|
8061
|
+
error: {
|
|
8062
|
+
code: "E_CDP",
|
|
8063
|
+
message: `CDP inspection failed: ${err instanceof Error ? err.message : err}`
|
|
8064
|
+
},
|
|
8065
|
+
value: {
|
|
8066
|
+
cdpPort
|
|
8067
|
+
},
|
|
7400
8068
|
hint: "Ensure a dev session is running. The browser may have closed or the CDP port may have changed."
|
|
7401
8069
|
});
|
|
7402
8070
|
} finally{
|
|
@@ -7405,7 +8073,7 @@ async function inspect_handler(args) {
|
|
|
7405
8073
|
}
|
|
7406
8074
|
const list_extensions_schema = {
|
|
7407
8075
|
name: "extension_list_extensions",
|
|
7408
|
-
description: "List the extensions in the running dev browser: id, name, version, and
|
|
8076
|
+
description: "List the extensions in the running dev browser: id, name, version, and, on Chromium, live contexts. This session's own extension carries ownExtension:true, with name and version from the ready contract even when the browser exposes no identity. Chromium rides the Chrome DevTools Protocol, so an entry needs at least one live context, and a dormant MV3 service worker may be absent until it wakes. Firefox rides the RDP root actor (listAddons, engine 4.0.15 and later), so entries are installed add-ons regardless of contexts, are marked temporarilyInstalled where relevant, and carry no contexts. Other extensions' contexts are never attached to or evaluated in. This requires an active dev or start session.",
|
|
7409
8077
|
inputSchema: {
|
|
7410
8078
|
type: "object",
|
|
7411
8079
|
properties: {
|
|
@@ -7461,13 +8129,25 @@ const UNRESOLVED_NOTE = "Identity unresolved: the browser's Extensions CDP domai
|
|
|
7461
8129
|
async function list_extensions_handler(args) {
|
|
7462
8130
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser, "chrome");
|
|
7463
8131
|
if (isGeckoFamily(browser)) return listGeckoExtensions(args.projectPath, browser);
|
|
7464
|
-
if (!isChromiumFamily(browser)) return
|
|
7465
|
-
|
|
8132
|
+
if (!isChromiumFamily(browser)) return envelope({
|
|
8133
|
+
ok: false,
|
|
8134
|
+
command: list_extensions_schema.name,
|
|
8135
|
+
status: "unsupported-browser",
|
|
8136
|
+
error: {
|
|
8137
|
+
code: "E_UNSUPPORTED_BROWSER",
|
|
8138
|
+
message: `Listing extensions for ${browser} is not supported: no debugging-protocol pairing exists for this browser family.`
|
|
8139
|
+
},
|
|
7466
8140
|
hint: "Target a Chromium-family (CDP) or Firefox-family (RDP) dev session."
|
|
7467
8141
|
});
|
|
7468
8142
|
const resolved = await resolveCdpPort(args.projectPath, browser);
|
|
7469
|
-
if (!resolved) return
|
|
7470
|
-
|
|
8143
|
+
if (!resolved) return envelope({
|
|
8144
|
+
ok: false,
|
|
8145
|
+
command: list_extensions_schema.name,
|
|
8146
|
+
status: "no-session",
|
|
8147
|
+
error: {
|
|
8148
|
+
code: "E_NO_SESSION",
|
|
8149
|
+
message: "No active dev session found. Cannot connect to Chrome DevTools Protocol."
|
|
8150
|
+
},
|
|
7471
8151
|
hint: `Start a dev session first with extension_dev, then use extension_wait to confirm it is ready. ${CDP_PORT_MISSING_HINT}`
|
|
7472
8152
|
});
|
|
7473
8153
|
const cdpPort = resolved.port;
|
|
@@ -7540,17 +8220,30 @@ async function list_extensions_handler(args) {
|
|
|
7540
8220
|
return (a.name ?? a.id).localeCompare(b.name ?? b.id);
|
|
7541
8221
|
});
|
|
7542
8222
|
const ownEntry = extensions.find((e)=>e.ownExtension);
|
|
7543
|
-
return
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
|
|
8223
|
+
return envelope({
|
|
8224
|
+
ok: true,
|
|
8225
|
+
command: list_extensions_schema.name,
|
|
8226
|
+
status: "listed",
|
|
8227
|
+
value: {
|
|
8228
|
+
cdpPort,
|
|
8229
|
+
browser,
|
|
8230
|
+
count: extensions.length,
|
|
8231
|
+
ownExtensionId: ownEntry?.id ?? null,
|
|
8232
|
+
extensions
|
|
8233
|
+
},
|
|
8234
|
+
warnings: [
|
|
8235
|
+
"Lists extensions that currently have at least one live context (service worker or open page). An MV3 service worker that has gone dormant with no open page may be absent until it wakes. ownExtension marks the extension this dev session serves, identified from the session's ready contract. Other identity is read read-only via the Extensions domain; other extensions' contexts are never attached to or evaluated in."
|
|
8236
|
+
]
|
|
7550
8237
|
});
|
|
7551
8238
|
} catch (error) {
|
|
7552
|
-
return
|
|
7553
|
-
|
|
8239
|
+
return envelope({
|
|
8240
|
+
ok: false,
|
|
8241
|
+
command: list_extensions_schema.name,
|
|
8242
|
+
status: "cdp-failed",
|
|
8243
|
+
error: {
|
|
8244
|
+
code: "E_CDP",
|
|
8245
|
+
message: `Failed to list extensions: ${error.message}`
|
|
8246
|
+
}
|
|
7554
8247
|
});
|
|
7555
8248
|
} finally{
|
|
7556
8249
|
cdp.disconnect();
|
|
@@ -7558,8 +8251,14 @@ async function list_extensions_handler(args) {
|
|
|
7558
8251
|
}
|
|
7559
8252
|
async function listGeckoExtensions(projectPath, browser) {
|
|
7560
8253
|
const resolved = await resolveRdpPort(projectPath, browser);
|
|
7561
|
-
if (!resolved) return
|
|
7562
|
-
|
|
8254
|
+
if (!resolved) return envelope({
|
|
8255
|
+
ok: false,
|
|
8256
|
+
command: list_extensions_schema.name,
|
|
8257
|
+
status: "no-session",
|
|
8258
|
+
error: {
|
|
8259
|
+
code: "E_NO_SESSION",
|
|
8260
|
+
message: "No active dev session with a Firefox debugger server (RDP) found."
|
|
8261
|
+
},
|
|
7563
8262
|
hint: `Start a dev session first with extension_dev, then use extension_wait to confirm it is ready. ${RDP_PORT_MISSING_HINT}`
|
|
7564
8263
|
});
|
|
7565
8264
|
const rdpPort = resolved.port;
|
|
@@ -7606,17 +8305,30 @@ async function listGeckoExtensions(projectPath, browser) {
|
|
|
7606
8305
|
return (a.name ?? a.id).localeCompare(b.name ?? b.id);
|
|
7607
8306
|
});
|
|
7608
8307
|
const ownEntry = extensions.find((e)=>e.ownExtension);
|
|
7609
|
-
return
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
8308
|
+
return envelope({
|
|
8309
|
+
ok: true,
|
|
8310
|
+
command: list_extensions_schema.name,
|
|
8311
|
+
status: "listed",
|
|
8312
|
+
value: {
|
|
8313
|
+
rdpPort,
|
|
8314
|
+
browser,
|
|
8315
|
+
count: extensions.length,
|
|
8316
|
+
ownExtensionId: ownEntry?.id ?? null,
|
|
8317
|
+
extensions
|
|
8318
|
+
},
|
|
8319
|
+
warnings: [
|
|
8320
|
+
"Lists INSTALLED add-ons via the RDP root actor (listAddons), regardless of whether a context is currently live, so entries carry no contexts. temporarilyInstalled marks temporary loads; ownExtension marks the extension this dev session serves, matched from the session's ready contract. Add-ons are never attached to or evaluated in."
|
|
8321
|
+
]
|
|
7616
8322
|
});
|
|
7617
8323
|
} catch (error) {
|
|
7618
|
-
return
|
|
7619
|
-
|
|
8324
|
+
return envelope({
|
|
8325
|
+
ok: false,
|
|
8326
|
+
command: list_extensions_schema.name,
|
|
8327
|
+
status: "rdp-failed",
|
|
8328
|
+
error: {
|
|
8329
|
+
code: "E_RDP",
|
|
8330
|
+
message: `Failed to list extensions over RDP: ${error.message}`
|
|
8331
|
+
}
|
|
7620
8332
|
});
|
|
7621
8333
|
}
|
|
7622
8334
|
}
|
|
@@ -7672,7 +8384,7 @@ function makeFilter(args) {
|
|
|
7672
8384
|
}
|
|
7673
8385
|
const logs_schema_schema = {
|
|
7674
8386
|
name: "extension_logs",
|
|
7675
|
-
description: "Read or stream logs from every context of a running dev session (service worker, content scripts, popup, options, sidebar, devtools, pages) in one ordered timeline.
|
|
8387
|
+
description: "Read or stream logs from every context of a running dev session (service worker, content scripts, popup, options, sidebar, devtools, pages) in one ordered timeline. This reads the same agent-bridge plane as the `extension logs` CLI: a one-shot returns the most recent matching lines from logs.ndjson, and follow:true collects from the live control channel for a bounded window. This requires an active extension_dev session.",
|
|
7676
8388
|
inputSchema: {
|
|
7677
8389
|
type: "object",
|
|
7678
8390
|
properties: {
|
|
@@ -7749,6 +8461,7 @@ const logs_schema_schema = {
|
|
|
7749
8461
|
]
|
|
7750
8462
|
}
|
|
7751
8463
|
};
|
|
8464
|
+
const logs_TOOL = "extension_logs";
|
|
7752
8465
|
function logsFilePath(projectPath, browser) {
|
|
7753
8466
|
return node_path.resolve(projectPath, "dist", "extension-js", browser, "logs.ndjson");
|
|
7754
8467
|
}
|
|
@@ -7798,24 +8511,26 @@ function summarize(events, source, browser, runId, limit, dropped, projectPath,
|
|
|
7798
8511
|
const { events: out, truncated } = capRecent(events, limit);
|
|
7799
8512
|
const lastSeq = out.length ? out.reduce((m, e)=>"number" == typeof e.seq && e.seq > m ? e.seq : m, -1) : -1;
|
|
7800
8513
|
const reason = 0 === matched && projectPath ? emptyReason(projectPath, browser) : void 0;
|
|
7801
|
-
|
|
8514
|
+
const stale = Boolean(staleNote) && matched > 0;
|
|
8515
|
+
return envelope({
|
|
7802
8516
|
ok: true,
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
7808
|
-
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
|
|
7812
|
-
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
|
|
7817
|
-
|
|
7818
|
-
|
|
8517
|
+
command: logs_TOOL,
|
|
8518
|
+
status: 0 === matched ? "empty" : stale ? "stale" : "read",
|
|
8519
|
+
value: {
|
|
8520
|
+
source,
|
|
8521
|
+
browser,
|
|
8522
|
+
runId: runId || void 0,
|
|
8523
|
+
matched,
|
|
8524
|
+
count: out.length,
|
|
8525
|
+
truncated,
|
|
8526
|
+
dropped: dropped || void 0,
|
|
8527
|
+
nextSince: lastSeq >= 0 ? lastSeq : void 0,
|
|
8528
|
+
events: out
|
|
8529
|
+
},
|
|
8530
|
+
warnings: [
|
|
8531
|
+
reason ?? null,
|
|
8532
|
+
stale ? staleNote ?? null : null
|
|
8533
|
+
]
|
|
7819
8534
|
});
|
|
7820
8535
|
}
|
|
7821
8536
|
function staleFileNote(projectPath, browser, eventsRunId) {
|
|
@@ -7838,8 +8553,14 @@ function staleFileNote(projectPath, browser, eventsRunId) {
|
|
|
7838
8553
|
}
|
|
7839
8554
|
async function readFromFile(args, browser, limit) {
|
|
7840
8555
|
const file = logsFilePath(args.projectPath, browser);
|
|
7841
|
-
if (!node_fs.existsSync(file)) return
|
|
7842
|
-
|
|
8556
|
+
if (!node_fs.existsSync(file)) return envelope({
|
|
8557
|
+
ok: false,
|
|
8558
|
+
command: logs_TOOL,
|
|
8559
|
+
status: "no-log-file",
|
|
8560
|
+
error: {
|
|
8561
|
+
code: "E_LOGS_MISSING",
|
|
8562
|
+
message: `No logs found at ${file}.`
|
|
8563
|
+
},
|
|
7843
8564
|
hint: "Start a dev session first (extension_dev), or pass browser to match it. For live frames before any line is written, use follow:true."
|
|
7844
8565
|
});
|
|
7845
8566
|
const matches = makeFilter(args);
|
|
@@ -7866,8 +8587,14 @@ async function readFromStream(args, browser, limit) {
|
|
|
7866
8587
|
if (!ready) {
|
|
7867
8588
|
const running = knownSessionBrowsers(args.projectPath).filter((b)=>b !== browser);
|
|
7868
8589
|
const retarget = running.length ? `An active session exists for browser(s): ${running.join(", ")}, pass that as \`browser\`. Otherwise run` : "Run";
|
|
7869
|
-
return
|
|
7870
|
-
|
|
8590
|
+
return envelope({
|
|
8591
|
+
ok: false,
|
|
8592
|
+
command: logs_TOOL,
|
|
8593
|
+
status: "no-control-channel",
|
|
8594
|
+
error: {
|
|
8595
|
+
code: "E_NO_CONTROL_CHANNEL",
|
|
8596
|
+
message: `No active control channel found for ${browser}.`
|
|
8597
|
+
},
|
|
7871
8598
|
hint: `${retarget} extension_dev (browser: ${browser}) and wait for it to be ready, then retry. For past logs without a live channel, call without follow.`
|
|
7872
8599
|
});
|
|
7873
8600
|
}
|
|
@@ -7883,8 +8610,14 @@ async function readFromStream(args, browser, limit) {
|
|
|
7883
8610
|
try {
|
|
7884
8611
|
socket = new ws_0(url);
|
|
7885
8612
|
} catch (err) {
|
|
7886
|
-
resolve(
|
|
7887
|
-
|
|
8613
|
+
resolve(envelope({
|
|
8614
|
+
ok: false,
|
|
8615
|
+
command: logs_TOOL,
|
|
8616
|
+
status: "control-channel-failed",
|
|
8617
|
+
error: {
|
|
8618
|
+
code: "E_CONTROL_CHANNEL",
|
|
8619
|
+
message: `Could not open control channel at ${url}: ${err instanceof Error ? err.message : String(err)}`
|
|
8620
|
+
}
|
|
7888
8621
|
}));
|
|
7889
8622
|
return;
|
|
7890
8623
|
}
|
|
@@ -7924,8 +8657,14 @@ async function readFromStream(args, browser, limit) {
|
|
|
7924
8657
|
if (settled) return;
|
|
7925
8658
|
settled = true;
|
|
7926
8659
|
clearTimeout(timer);
|
|
7927
|
-
resolve(
|
|
7928
|
-
|
|
8660
|
+
resolve(envelope({
|
|
8661
|
+
ok: false,
|
|
8662
|
+
command: logs_TOOL,
|
|
8663
|
+
status: "control-channel-failed",
|
|
8664
|
+
error: {
|
|
8665
|
+
code: "E_CONTROL_CHANNEL",
|
|
8666
|
+
message: `Control channel error at ${url}.`
|
|
8667
|
+
},
|
|
7929
8668
|
hint: "The dev session may have stopped or the control port changed. Re-check with extension_wait."
|
|
7930
8669
|
}));
|
|
7931
8670
|
});
|
|
@@ -7940,7 +8679,7 @@ async function logs_handler(args) {
|
|
|
7940
8679
|
}
|
|
7941
8680
|
const eval_schema = {
|
|
7942
8681
|
name: "extension_eval",
|
|
7943
|
-
description: "Evaluate an expression in a running extension context.
|
|
8682
|
+
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 evaluate over the in-bundle relay and need no tab id, but must already be open: open one with extension_open first, because a closed one returns an explicit error. Call extension_dom_snapshot with listTabs:true to enumerate {tabId, url, title}.",
|
|
7944
8683
|
inputSchema: {
|
|
7945
8684
|
type: "object",
|
|
7946
8685
|
properties: {
|
|
@@ -8016,28 +8755,32 @@ async function eval_handler(args) {
|
|
|
8016
8755
|
context,
|
|
8017
8756
|
browser
|
|
8018
8757
|
})
|
|
8019
|
-
], args.projectPath, args.timeout);
|
|
8758
|
+
], args.projectPath, args.timeout, eval_schema.name);
|
|
8020
8759
|
if ("content" === args.context) try {
|
|
8021
8760
|
const parsed = JSON.parse(raw);
|
|
8022
8761
|
if (parsed?.ok === true && (null === parsed.value || void 0 === parsed.value)) {
|
|
8023
|
-
parsed
|
|
8024
|
-
return
|
|
8762
|
+
addWarning(parsed, "On Extension.js >= 4.0.14 a failed injection errors explicitly, so this null is the expression's real result. On OLDER engines (bug 61) it could mean the injection never ran; if this result looks wrong, check the engine version with extension_doctor, or verify with extension_logs or context:'page'.");
|
|
8763
|
+
return actFrameJson(parsed);
|
|
8025
8764
|
}
|
|
8026
8765
|
} catch {}
|
|
8027
8766
|
if (defaulted) try {
|
|
8028
8767
|
const parsed = JSON.parse(raw);
|
|
8029
8768
|
if (parsed && "object" == typeof parsed) {
|
|
8030
|
-
parsed
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
|
|
8769
|
+
patchValue(parsed, {
|
|
8770
|
+
defaultedContext: "page"
|
|
8771
|
+
});
|
|
8772
|
+
addWarning(parsed, 'No context given: defaulted to "page" (the active tab) because this Chromium session\'s MV3 background is a service worker whose CSP blocks eval. Pass context: "background" explicitly to target the worker (works on Firefox/MV2 builds).');
|
|
8773
|
+
const code = "string" == typeof parsed.error?.code ? parsed.error.code : "";
|
|
8774
|
+
const unreachable = "E_TARGET_NOT_FOUND" === code || /cannot access|chrome-extension:\/\/|chrome:\/\//i.test(JSON.stringify(parsed.error ?? ""));
|
|
8775
|
+
if (false === parsed.ok && unreachable) parsed.hint = "The active tab is a browser or extension page that eval cannot reach. Navigate the dev browser to a regular web page, or pass url (match pattern) or tab to pick one; extension_dom_snapshot with listTabs: true lists open tabs.";
|
|
8776
|
+
return actFrameJson(parsed);
|
|
8034
8777
|
}
|
|
8035
8778
|
} catch {}
|
|
8036
8779
|
return raw;
|
|
8037
8780
|
}
|
|
8038
8781
|
const storage_schema = {
|
|
8039
8782
|
name: "extension_storage",
|
|
8040
|
-
description: "Read or write chrome.storage in a running extension.
|
|
8783
|
+
description: "Read or write chrome.storage in a running extension. Start the session with allowControl:true (extension_dev). Set one key per call: there is no bulk-object set.",
|
|
8041
8784
|
inputSchema: {
|
|
8042
8785
|
type: "object",
|
|
8043
8786
|
properties: {
|
|
@@ -8097,16 +8840,22 @@ async function storage_handler(args) {
|
|
|
8097
8840
|
if (args.area) cli.push("--area", args.area);
|
|
8098
8841
|
if (args.key) cli.push("--key", args.key);
|
|
8099
8842
|
if ("set" === args.action) {
|
|
8100
|
-
if (void 0 === args.value) return
|
|
8843
|
+
if (void 0 === args.value) return envelope({
|
|
8101
8844
|
ok: false,
|
|
8845
|
+
command: storage_schema.name,
|
|
8846
|
+
status: "bad-request",
|
|
8102
8847
|
error: {
|
|
8848
|
+
code: "E_BAD_REQUEST",
|
|
8103
8849
|
name: "BadRequest",
|
|
8104
8850
|
message: "storage set requires a value"
|
|
8105
8851
|
}
|
|
8106
8852
|
});
|
|
8107
|
-
if (void 0 === args.key) return
|
|
8853
|
+
if (void 0 === args.key) return envelope({
|
|
8108
8854
|
ok: false,
|
|
8855
|
+
command: storage_schema.name,
|
|
8856
|
+
status: "bad-request",
|
|
8109
8857
|
error: {
|
|
8858
|
+
code: "E_BAD_REQUEST",
|
|
8110
8859
|
name: "BadRequest",
|
|
8111
8860
|
message: 'storage set requires `key` (string) and `value` args, one key per call. There is no bulk-object set: to seed {a: 1, b: 2}, call once with key: "a" and once with key: "b".'
|
|
8112
8861
|
}
|
|
@@ -8116,11 +8865,11 @@ async function storage_handler(args) {
|
|
|
8116
8865
|
if (args.context) cli.push("--context", args.context);
|
|
8117
8866
|
cli.push("--browser", browser);
|
|
8118
8867
|
if (null != args.timeout) cli.push("--timeout", String(args.timeout));
|
|
8119
|
-
return runActVerb(cli, args.projectPath, args.timeout);
|
|
8868
|
+
return runActVerb(cli, args.projectPath, args.timeout, storage_schema.name);
|
|
8120
8869
|
}
|
|
8121
8870
|
const reload_schema = {
|
|
8122
8871
|
name: "extension_reload",
|
|
8123
|
-
description: "Reload a running extension
|
|
8872
|
+
description: "Reload a running extension's background context, or a tab. Start the session with allowControl:true (extension_dev).",
|
|
8124
8873
|
inputSchema: {
|
|
8125
8874
|
type: "object",
|
|
8126
8875
|
properties: {
|
|
@@ -8155,7 +8904,7 @@ async function reload_handler(args) {
|
|
|
8155
8904
|
...args,
|
|
8156
8905
|
browser
|
|
8157
8906
|
})
|
|
8158
|
-
], args.projectPath, args.timeout);
|
|
8907
|
+
], args.projectPath, args.timeout, reload_schema.name);
|
|
8159
8908
|
}
|
|
8160
8909
|
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.";
|
|
8161
8910
|
function filterPageTargets(raw) {
|
|
@@ -8178,7 +8927,7 @@ function matchTargetsByUrl(targets, needle) {
|
|
|
8178
8927
|
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.";
|
|
8179
8928
|
const dom_snapshot_schema = {
|
|
8180
8929
|
name: "extension_dom_snapshot",
|
|
8181
|
-
description: "Take a
|
|
8930
|
+
description: "Take a shallow structured DOM snapshot of one chosen surface through the agent bridge (no CDP, localhost only): element counts, extension roots, open shadow roots, optional byte-capped HTML, and optional recent console lines. This is the SURFACE PICKER: the only tool that reads an open extension surface by name (`context`: popup, options, sidebar, devtools) or an override page, the only one that takes a numeric chrome.tabs id, and the only one that enumerates what is open (listTargets for CDP targetIds and RDP tab actors, listTabs for numeric tab ids). An ambiguous `tabUrl` returns the candidates instead of guessing. It does not pierce closed shadow roots, run selector probes, or navigate: use extension_inspect for those, and for a deep read of an already-open web page. Start the session with allowControl:true (extension_dev).",
|
|
8182
8931
|
inputSchema: {
|
|
8183
8932
|
type: "object",
|
|
8184
8933
|
properties: {
|
|
@@ -8256,9 +9005,12 @@ const dom_snapshot_schema = {
|
|
|
8256
9005
|
};
|
|
8257
9006
|
async function cdpPortOrError(projectPath, browser, feature) {
|
|
8258
9007
|
if (!isChromiumFamily(browser)) return {
|
|
8259
|
-
error:
|
|
9008
|
+
error: envelope({
|
|
8260
9009
|
ok: false,
|
|
9010
|
+
command: dom_snapshot_schema.name,
|
|
9011
|
+
status: "unsupported-browser",
|
|
8261
9012
|
error: {
|
|
9013
|
+
code: "E_UNSUPPORTED_BROWSER",
|
|
8262
9014
|
name: "Unsupported",
|
|
8263
9015
|
message: `${feature} reads the browser's CDP page targets, which ${browser} (Gecko) does not expose. Target the tab with \`url\` or \`tab\` instead, and discover tabs with listTabs: true (agent bridge, works on every browser).`
|
|
8264
9016
|
}
|
|
@@ -8266,9 +9018,12 @@ async function cdpPortOrError(projectPath, browser, feature) {
|
|
|
8266
9018
|
};
|
|
8267
9019
|
const resolved = await resolveCdpPort(projectPath, browser);
|
|
8268
9020
|
if (!resolved) return {
|
|
8269
|
-
error:
|
|
9021
|
+
error: envelope({
|
|
8270
9022
|
ok: false,
|
|
9023
|
+
command: dom_snapshot_schema.name,
|
|
9024
|
+
status: "no-session",
|
|
8271
9025
|
error: {
|
|
9026
|
+
code: "E_NO_SESSION",
|
|
8272
9027
|
name: "NoSession",
|
|
8273
9028
|
message: `No active dev session / CDP port for ${browser}, so ${feature} has no browser to ask. Start extension_dev and extension_wait for ready. ${CDP_PORT_MISSING_HINT}`
|
|
8274
9029
|
}
|
|
@@ -8284,34 +9039,46 @@ async function dom_snapshot_handler(args) {
|
|
|
8284
9039
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
8285
9040
|
if (isGeckoFamily(browser)) {
|
|
8286
9041
|
const resolved = await resolveRdpPort(args.projectPath, browser);
|
|
8287
|
-
if (!resolved) return
|
|
9042
|
+
if (!resolved) return envelope({
|
|
8288
9043
|
ok: false,
|
|
9044
|
+
command: dom_snapshot_schema.name,
|
|
9045
|
+
status: "no-session",
|
|
8289
9046
|
error: {
|
|
9047
|
+
code: "E_NO_SESSION",
|
|
8290
9048
|
name: "NoSession",
|
|
8291
9049
|
message: `No active dev session with a Firefox debugger server (RDP) for ${browser}, so listTargets has no browser to ask. Start extension_dev and extension_wait for ready. ${RDP_PORT_MISSING_HINT}`
|
|
8292
9050
|
}
|
|
8293
9051
|
});
|
|
8294
9052
|
try {
|
|
8295
9053
|
const tabs = await rdpListTabs(resolved.port);
|
|
8296
|
-
return
|
|
9054
|
+
return envelope({
|
|
8297
9055
|
ok: true,
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
|
|
9056
|
+
command: dom_snapshot_schema.name,
|
|
9057
|
+
status: "listed-targets",
|
|
9058
|
+
value: {
|
|
9059
|
+
browser,
|
|
9060
|
+
transport: "rdp",
|
|
9061
|
+
targets: tabs.map((t)=>({
|
|
9062
|
+
actor: String(t.actor ?? ""),
|
|
9063
|
+
type: "tab",
|
|
9064
|
+
url: String(t.url ?? ""),
|
|
9065
|
+
title: String(t.title ?? ""),
|
|
9066
|
+
...true === t.selected ? {
|
|
9067
|
+
selected: true
|
|
9068
|
+
} : {}
|
|
9069
|
+
}))
|
|
9070
|
+
},
|
|
9071
|
+
warnings: [
|
|
9072
|
+
RDP_ACTOR_NOTE
|
|
9073
|
+
]
|
|
8310
9074
|
});
|
|
8311
9075
|
} catch (e) {
|
|
8312
|
-
return
|
|
9076
|
+
return envelope({
|
|
8313
9077
|
ok: false,
|
|
9078
|
+
command: dom_snapshot_schema.name,
|
|
9079
|
+
status: "rdp-failed",
|
|
8314
9080
|
error: {
|
|
9081
|
+
code: "E_RDP",
|
|
8315
9082
|
name: "RdpError",
|
|
8316
9083
|
message: `Could not list tab targets over RDP: ${e instanceof Error ? e.message : String(e)}`
|
|
8317
9084
|
},
|
|
@@ -8323,16 +9090,25 @@ async function dom_snapshot_handler(args) {
|
|
|
8323
9090
|
if ("error" in cdp) return cdp.error;
|
|
8324
9091
|
try {
|
|
8325
9092
|
const targets = await listPageTargets(cdp.port);
|
|
8326
|
-
return
|
|
9093
|
+
return envelope({
|
|
8327
9094
|
ok: true,
|
|
8328
|
-
|
|
8329
|
-
targets,
|
|
8330
|
-
|
|
9095
|
+
command: dom_snapshot_schema.name,
|
|
9096
|
+
status: "listed-targets",
|
|
9097
|
+
value: {
|
|
9098
|
+
browser,
|
|
9099
|
+
targets
|
|
9100
|
+
},
|
|
9101
|
+
warnings: [
|
|
9102
|
+
TARGET_ID_NOTE
|
|
9103
|
+
]
|
|
8331
9104
|
});
|
|
8332
9105
|
} catch (e) {
|
|
8333
|
-
return
|
|
9106
|
+
return envelope({
|
|
8334
9107
|
ok: false,
|
|
9108
|
+
command: dom_snapshot_schema.name,
|
|
9109
|
+
status: "cdp-failed",
|
|
8335
9110
|
error: {
|
|
9111
|
+
code: "E_CDP",
|
|
8336
9112
|
name: "CdpError",
|
|
8337
9113
|
message: `Could not list page targets: ${e instanceof Error ? e.message : String(e)}`
|
|
8338
9114
|
},
|
|
@@ -8350,14 +9126,17 @@ async function dom_snapshot_handler(args) {
|
|
|
8350
9126
|
"--timeout",
|
|
8351
9127
|
String(args.timeout)
|
|
8352
9128
|
] : []
|
|
8353
|
-
], args.projectPath, args.timeout);
|
|
9129
|
+
], args.projectPath, args.timeout, dom_snapshot_schema.name);
|
|
8354
9130
|
let targetUrl = args.url;
|
|
8355
9131
|
let targetTab = args.tab;
|
|
8356
9132
|
let resolvedTarget = null;
|
|
8357
9133
|
if (args.tabUrl) {
|
|
8358
|
-
if (null != args.tab || args.url) return
|
|
9134
|
+
if (null != args.tab || args.url) return envelope({
|
|
8359
9135
|
ok: false,
|
|
9136
|
+
command: dom_snapshot_schema.name,
|
|
9137
|
+
status: "bad-request",
|
|
8360
9138
|
error: {
|
|
9139
|
+
code: "E_BAD_REQUEST",
|
|
8361
9140
|
name: "BadRequest",
|
|
8362
9141
|
message: "Pass ONE tab selector: `tabUrl` (URL substring, resolved against live targets), `url` (engine-side match), or `tab` (numeric chrome.tabs id), not several."
|
|
8363
9142
|
}
|
|
@@ -8370,9 +9149,12 @@ async function dom_snapshot_handler(args) {
|
|
|
8370
9149
|
try {
|
|
8371
9150
|
targets = await listPageTargets(cdp.port);
|
|
8372
9151
|
} catch (e) {
|
|
8373
|
-
return
|
|
9152
|
+
return envelope({
|
|
8374
9153
|
ok: false,
|
|
9154
|
+
command: dom_snapshot_schema.name,
|
|
9155
|
+
status: "cdp-failed",
|
|
8375
9156
|
error: {
|
|
9157
|
+
code: "E_CDP",
|
|
8376
9158
|
name: "CdpError",
|
|
8377
9159
|
message: `Could not list page targets to resolve tabUrl: ${e instanceof Error ? e.message : String(e)}`
|
|
8378
9160
|
},
|
|
@@ -8380,22 +9162,32 @@ async function dom_snapshot_handler(args) {
|
|
|
8380
9162
|
});
|
|
8381
9163
|
}
|
|
8382
9164
|
const matches = matchTargetsByUrl(targets, args.tabUrl);
|
|
8383
|
-
if (0 === matches.length) return
|
|
9165
|
+
if (0 === matches.length) return envelope({
|
|
8384
9166
|
ok: false,
|
|
9167
|
+
command: dom_snapshot_schema.name,
|
|
9168
|
+
status: "no-matching-target",
|
|
8385
9169
|
error: {
|
|
9170
|
+
code: "E_NO_MATCHING_TARGET",
|
|
8386
9171
|
name: "NoMatchingTarget",
|
|
8387
9172
|
message: `No open page target's url (or title) contains "${args.tabUrl}" (case-insensitive).`
|
|
8388
9173
|
},
|
|
8389
|
-
|
|
9174
|
+
value: {
|
|
9175
|
+
availableTargets: targets
|
|
9176
|
+
},
|
|
8390
9177
|
hint: `Pick one from availableTargets and retry with a \`tabUrl\` substring of its url, or open the page first (extension_open with \`url\`). ${TARGET_ID_NOTE}`
|
|
8391
9178
|
});
|
|
8392
|
-
if (matches.length > 1) return
|
|
9179
|
+
if (matches.length > 1) return envelope({
|
|
8393
9180
|
ok: false,
|
|
9181
|
+
command: dom_snapshot_schema.name,
|
|
9182
|
+
status: "ambiguous-target",
|
|
8394
9183
|
error: {
|
|
9184
|
+
code: "E_AMBIGUOUS_TARGET",
|
|
8395
9185
|
name: "AmbiguousTabUrl",
|
|
8396
9186
|
message: `${matches.length} page targets match "${args.tabUrl}"; refusing to guess which tab you mean.`
|
|
8397
9187
|
},
|
|
8398
|
-
|
|
9188
|
+
value: {
|
|
9189
|
+
matchingTargets: matches
|
|
9190
|
+
},
|
|
8399
9191
|
hint: `Narrow \`tabUrl\` to a longer substring that matches exactly one url in matchingTargets. ${TARGET_ID_NOTE}`
|
|
8400
9192
|
});
|
|
8401
9193
|
resolvedTarget = {
|
|
@@ -8406,22 +9198,32 @@ async function dom_snapshot_handler(args) {
|
|
|
8406
9198
|
const listed = await listBridgeTabs(args.projectPath, browser, args.timeout);
|
|
8407
9199
|
if ("error" in listed) return listed.error;
|
|
8408
9200
|
const matches = matchTabsByUrl(listed.tabs, args.tabUrl);
|
|
8409
|
-
if (0 === matches.length) return
|
|
9201
|
+
if (0 === matches.length) return envelope({
|
|
8410
9202
|
ok: false,
|
|
9203
|
+
command: dom_snapshot_schema.name,
|
|
9204
|
+
status: "no-matching-target",
|
|
8411
9205
|
error: {
|
|
9206
|
+
code: "E_NO_MATCHING_TARGET",
|
|
8412
9207
|
name: "NoMatchingTarget",
|
|
8413
9208
|
message: `No open tab's url (or title) contains "${args.tabUrl}" (case-insensitive).`
|
|
8414
9209
|
},
|
|
8415
|
-
|
|
9210
|
+
value: {
|
|
9211
|
+
availableTabs: listed.tabs
|
|
9212
|
+
},
|
|
8416
9213
|
hint: "Pick one from availableTabs and retry with a `tabUrl` substring of its url, or open the page first (extension_open with `url`)."
|
|
8417
9214
|
});
|
|
8418
|
-
if (matches.length > 1) return
|
|
9215
|
+
if (matches.length > 1) return envelope({
|
|
8419
9216
|
ok: false,
|
|
9217
|
+
command: dom_snapshot_schema.name,
|
|
9218
|
+
status: "ambiguous-target",
|
|
8420
9219
|
error: {
|
|
9220
|
+
code: "E_AMBIGUOUS_TARGET",
|
|
8421
9221
|
name: "AmbiguousTabUrl",
|
|
8422
9222
|
message: `${matches.length} tabs match "${args.tabUrl}"; refusing to guess which tab you mean.`
|
|
8423
9223
|
},
|
|
8424
|
-
|
|
9224
|
+
value: {
|
|
9225
|
+
matchingTabs: matches
|
|
9226
|
+
},
|
|
8425
9227
|
hint: "Narrow `tabUrl` to a longer substring that matches exactly one url in matchingTabs, or pass its numeric tabId as `tab`."
|
|
8426
9228
|
});
|
|
8427
9229
|
resolvedTarget = {
|
|
@@ -8443,22 +9245,24 @@ async function dom_snapshot_handler(args) {
|
|
|
8443
9245
|
if (null != withConsole) cli.push("--with-console", String(withConsole));
|
|
8444
9246
|
cli.push("--browser", resolveSessionBrowser(args.projectPath, args.browser).browser);
|
|
8445
9247
|
if (null != args.timeout) cli.push("--timeout", String(args.timeout));
|
|
8446
|
-
const raw = await runActVerb(cli, args.projectPath, args.timeout);
|
|
9248
|
+
const raw = await runActVerb(cli, args.projectPath, args.timeout, dom_snapshot_schema.name);
|
|
8447
9249
|
if (!resolvedTarget) return raw;
|
|
8448
9250
|
try {
|
|
8449
9251
|
const parsed = JSON.parse(raw);
|
|
8450
|
-
parsed
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
9252
|
+
patchValue(parsed, {
|
|
9253
|
+
resolvedTarget: {
|
|
9254
|
+
...resolvedTarget,
|
|
9255
|
+
matchedBy: "tabUrl"
|
|
9256
|
+
}
|
|
9257
|
+
});
|
|
9258
|
+
return actFrameJson(parsed);
|
|
8455
9259
|
} catch {
|
|
8456
9260
|
return raw;
|
|
8457
9261
|
}
|
|
8458
9262
|
}
|
|
8459
9263
|
const publish_schema = {
|
|
8460
9264
|
name: "extension_publish",
|
|
8461
|
-
description: "Publish the project your stored token is scoped to (extension_auth, or EXTENSION_DEV_TOKEN) to extension.dev and return its shareable URL. This is what \"deploy\" or \"ship\" an extension usually means; extension_submit is the separate store-review path. The target is the token's project: there is no projectPath and no local file is uploaded. For a
|
|
9265
|
+
description: "Publish the project your stored token is scoped to (extension_auth, or EXTENSION_DEV_TOKEN) to extension.dev, and return its shareable URL. This is what \"deploy\" or \"ship\" an extension usually means; extension_submit is the separate store-review path. The target is the token's project: there is no projectPath, and no local file is uploaded. For a public project the URL is the canonical public page and ttlHours does not apply. For a private one it is a fresh time-limited share link (?share=) whose lifetime is ttlHours.",
|
|
8462
9266
|
inputSchema: {
|
|
8463
9267
|
type: "object",
|
|
8464
9268
|
properties: {
|
|
@@ -8475,10 +9279,13 @@ const publish_schema = {
|
|
|
8475
9279
|
required: []
|
|
8476
9280
|
}
|
|
8477
9281
|
};
|
|
8478
|
-
function fail(name, message) {
|
|
8479
|
-
return
|
|
9282
|
+
function fail(name, message, status, code) {
|
|
9283
|
+
return envelope({
|
|
8480
9284
|
ok: false,
|
|
9285
|
+
command: "extension_publish",
|
|
9286
|
+
status,
|
|
8481
9287
|
error: {
|
|
9288
|
+
code,
|
|
8482
9289
|
name,
|
|
8483
9290
|
message
|
|
8484
9291
|
}
|
|
@@ -8486,13 +9293,13 @@ function fail(name, message) {
|
|
|
8486
9293
|
}
|
|
8487
9294
|
async function publish_handler(args) {
|
|
8488
9295
|
const token = resolveToken();
|
|
8489
|
-
if (!token) return fail("PublishAuthError", "No token. Run extension_auth (action: login), or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard).");
|
|
9296
|
+
if (!token) return fail("PublishAuthError", "No token. Run extension_auth (action: login), or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard).", "auth-required", "E_AUTH_REQUIRED");
|
|
8490
9297
|
if (null != args.ttlHours) {
|
|
8491
9298
|
const t = Number(args.ttlHours);
|
|
8492
|
-
if (!Number.isInteger(t) || t < 1 || t > 168) return fail("PublishBadRequest", "ttlHours must be an integer between 1 and 168.");
|
|
9299
|
+
if (!Number.isInteger(t) || t < 1 || t > 168) return fail("PublishBadRequest", "ttlHours must be an integer between 1 and 168.", "bad-request", "E_BAD_REQUEST");
|
|
8493
9300
|
}
|
|
8494
9301
|
if (null != args.buildSha && "" !== args.buildSha) {
|
|
8495
|
-
if (!/^[0-9a-f]{7,40}$/i.test(args.buildSha)) return fail("PublishBadRequest", "buildSha must be a 7-40 character hex git sha.");
|
|
9302
|
+
if (!/^[0-9a-f]{7,40}$/i.test(args.buildSha)) return fail("PublishBadRequest", "buildSha must be a 7-40 character hex git sha.", "bad-request", "E_BAD_REQUEST");
|
|
8496
9303
|
}
|
|
8497
9304
|
const result = await publish({
|
|
8498
9305
|
ttlHours: args.ttlHours,
|
|
@@ -8500,9 +9307,20 @@ async function publish_handler(args) {
|
|
|
8500
9307
|
api: args.api,
|
|
8501
9308
|
token
|
|
8502
9309
|
});
|
|
8503
|
-
if (!result.ok) return
|
|
9310
|
+
if (!result.ok) return envelope({
|
|
9311
|
+
ok: false,
|
|
9312
|
+
command: "extension_publish",
|
|
9313
|
+
status: "publish-failed",
|
|
9314
|
+
error: {
|
|
9315
|
+
code: "E_PLATFORM",
|
|
9316
|
+
name: result.error.name,
|
|
9317
|
+
message: result.error.message
|
|
9318
|
+
}
|
|
9319
|
+
});
|
|
8504
9320
|
const data = result.data;
|
|
8505
|
-
|
|
9321
|
+
let note = null;
|
|
9322
|
+
if (null != args.ttlHours && "public" === data.visibility) note = "ttlHours was ignored: this is a public project, whose share URL is its canonical public page.";
|
|
9323
|
+
let buildNote = null;
|
|
8506
9324
|
const ref = resolveProjectRef();
|
|
8507
9325
|
if (ref) {
|
|
8508
9326
|
const buildsUrl = registryFileUrl(ref, "builds/index.json");
|
|
@@ -8524,15 +9342,24 @@ async function publish_handler(args) {
|
|
|
8524
9342
|
if (null == data.version && served.version) data.version = served.version;
|
|
8525
9343
|
if (null == data.channel && served.channel) data.channel = served.channel;
|
|
8526
9344
|
data.registryUrl = buildsUrl;
|
|
8527
|
-
if (!pinned && null == args.buildSha)
|
|
9345
|
+
if (!pinned && null == args.buildSha) buildNote = "buildSha/builtAt/version describe the newest successful build in the project's registry index, which is what the share link serves. Pin buildSha to serve a specific build.";
|
|
8528
9346
|
}
|
|
8529
9347
|
}
|
|
8530
9348
|
}
|
|
8531
|
-
return
|
|
9349
|
+
return envelope({
|
|
9350
|
+
ok: true,
|
|
9351
|
+
command: "extension_publish",
|
|
9352
|
+
status: "published",
|
|
9353
|
+
value: data,
|
|
9354
|
+
warnings: [
|
|
9355
|
+
note,
|
|
9356
|
+
buildNote
|
|
9357
|
+
]
|
|
9358
|
+
});
|
|
8532
9359
|
}
|
|
8533
9360
|
const release_promote_schema = {
|
|
8534
9361
|
name: "extension_release_promote",
|
|
8535
|
-
description: "Promote a built extension to a release channel (stable, preview, beta,
|
|
9362
|
+
description: "Promote a built extension to a release channel (stable, preview, beta, …) on extension.dev, headless. This WRITES: it is the only verb that changes what a channel points at. It is auth-gated by your stored login (extension_auth) or a release token in EXTENSION_DEV_TOKEN, minted and revoked under project settings, Access tokens. Tokens live at most 7 days, so CI must re-mint before expiry. The project comes from the token. Call extension_release_status to find a valid buildId. Cutting a version-bump PR is not available headlessly, because it writes to your source repo and needs an interactive login.",
|
|
8536
9363
|
inputSchema: {
|
|
8537
9364
|
type: "object",
|
|
8538
9365
|
properties: {
|
|
@@ -8571,10 +9398,13 @@ const release_promote_schema = {
|
|
|
8571
9398
|
]
|
|
8572
9399
|
}
|
|
8573
9400
|
};
|
|
8574
|
-
function release_promote_fail(name, message) {
|
|
8575
|
-
return
|
|
9401
|
+
function release_promote_fail(name, message, status, code) {
|
|
9402
|
+
return envelope({
|
|
8576
9403
|
ok: false,
|
|
9404
|
+
command: "extension_release_promote",
|
|
9405
|
+
status,
|
|
8577
9406
|
error: {
|
|
9407
|
+
code,
|
|
8578
9408
|
name,
|
|
8579
9409
|
message
|
|
8580
9410
|
}
|
|
@@ -8582,12 +9412,12 @@ function release_promote_fail(name, message) {
|
|
|
8582
9412
|
}
|
|
8583
9413
|
async function release_promote_handler(args) {
|
|
8584
9414
|
const token = resolveToken();
|
|
8585
|
-
if (!token) return release_promote_fail("ReleaseAuthError", "No token. Set EXTENSION_DEV_TOKEN to a release token (create one in the extension.dev dashboard under project settings -> Access tokens; tokens live at most 7 days, so CI must re-mint before expiry), or run extension_auth (action: login).");
|
|
9415
|
+
if (!token) return release_promote_fail("ReleaseAuthError", "No token. Set EXTENSION_DEV_TOKEN to a release token (create one in the extension.dev dashboard under project settings -> Access tokens; tokens live at most 7 days, so CI must re-mint before expiry), or run extension_auth (action: login).", "auth-required", "E_AUTH_REQUIRED");
|
|
8586
9416
|
const buildId = String(args.buildId || "").trim();
|
|
8587
9417
|
const channel = String(args.channel || "").trim();
|
|
8588
|
-
if (!buildId || !channel) return release_promote_fail("ReleaseInputError", "buildId and channel are required.");
|
|
9418
|
+
if (!buildId || !channel) return release_promote_fail("ReleaseInputError", "buildId and channel are required.", "bad-request", "E_BAD_REQUEST");
|
|
8589
9419
|
const apiCheck = safeApiBase(resolveApiBase(args.api));
|
|
8590
|
-
if (!apiCheck.ok) return release_promote_fail("ReleaseConfigError", apiCheck.message);
|
|
9420
|
+
if (!apiCheck.ok) return release_promote_fail("ReleaseConfigError", apiCheck.message, "bad-config", "E_CONFIG");
|
|
8591
9421
|
const url = `${apiCheck.base}/api/cli/release/promote`;
|
|
8592
9422
|
const body = {
|
|
8593
9423
|
buildId,
|
|
@@ -8608,7 +9438,7 @@ async function release_promote_handler(args) {
|
|
|
8608
9438
|
body: JSON.stringify(body)
|
|
8609
9439
|
});
|
|
8610
9440
|
} catch (err) {
|
|
8611
|
-
return release_promote_fail("ReleaseNetworkError", `Could not reach ${url}: ${err?.message || err}
|
|
9441
|
+
return release_promote_fail("ReleaseNetworkError", `Could not reach ${url}: ${err?.message || err}`, "network-failed", "E_NETWORK");
|
|
8612
9442
|
}
|
|
8613
9443
|
const text = await res.text();
|
|
8614
9444
|
let data;
|
|
@@ -8622,10 +9452,11 @@ async function release_promote_handler(args) {
|
|
|
8622
9452
|
if (!res.ok) {
|
|
8623
9453
|
const code = "string" == typeof data?.code ? data.code : void 0;
|
|
8624
9454
|
const enrich = {};
|
|
9455
|
+
let hint = "";
|
|
8625
9456
|
const ref = resolveProjectRef();
|
|
8626
9457
|
if (404 === res.status || "UNKNOWN_BUILD" === code) {
|
|
8627
9458
|
enrich.buildsPageUrl = consoleProjectUrl(ref, "builds", args.api);
|
|
8628
|
-
|
|
9459
|
+
hint = "Run extension_release_status to see this project's channels, their promoted shas, and recent builds.";
|
|
8629
9460
|
if (ref) {
|
|
8630
9461
|
const channelsUrl = registryFileUrl(ref, "channels.json");
|
|
8631
9462
|
const channelsRes = await fetchRegistryJson(channelsUrl, fetch, {
|
|
@@ -8642,16 +9473,20 @@ async function release_promote_handler(args) {
|
|
|
8642
9473
|
}
|
|
8643
9474
|
}
|
|
8644
9475
|
}
|
|
8645
|
-
return
|
|
9476
|
+
return envelope({
|
|
8646
9477
|
ok: false,
|
|
9478
|
+
command: "extension_release_promote",
|
|
9479
|
+
status: "promote-failed",
|
|
8647
9480
|
error: {
|
|
9481
|
+
code: "E_PLATFORM",
|
|
8648
9482
|
name: "ReleaseError",
|
|
8649
9483
|
message: `promote failed (${res.status}): ${data?.message || text || "unknown error"}`,
|
|
8650
9484
|
...code ? {
|
|
8651
|
-
code
|
|
9485
|
+
platformCode: code
|
|
8652
9486
|
} : {}
|
|
8653
9487
|
},
|
|
8654
|
-
|
|
9488
|
+
value: enrich,
|
|
9489
|
+
hint
|
|
8655
9490
|
});
|
|
8656
9491
|
}
|
|
8657
9492
|
const promotedRef = resolveProjectRef();
|
|
@@ -8666,21 +9501,29 @@ async function release_promote_handler(args) {
|
|
|
8666
9501
|
publicBuildUrl
|
|
8667
9502
|
} : {}
|
|
8668
9503
|
} : data;
|
|
8669
|
-
return
|
|
9504
|
+
return envelope({
|
|
9505
|
+
ok: true,
|
|
9506
|
+
command: "extension_release_promote",
|
|
9507
|
+
status: "promoted",
|
|
9508
|
+
value: enriched
|
|
9509
|
+
});
|
|
8670
9510
|
}
|
|
8671
|
-
function release_list_fail(name, message, extra) {
|
|
8672
|
-
return
|
|
9511
|
+
function release_list_fail(name, message, status, code, extra) {
|
|
9512
|
+
return envelope({
|
|
8673
9513
|
ok: false,
|
|
9514
|
+
command: "extension_release_status",
|
|
9515
|
+
status,
|
|
9516
|
+
value: extra ?? {},
|
|
8674
9517
|
error: {
|
|
9518
|
+
code,
|
|
8675
9519
|
name,
|
|
8676
9520
|
message
|
|
8677
|
-
}
|
|
8678
|
-
...extra ?? {}
|
|
9521
|
+
}
|
|
8679
9522
|
});
|
|
8680
9523
|
}
|
|
8681
9524
|
async function readReleases(args) {
|
|
8682
9525
|
const ref = resolveProjectRef(args);
|
|
8683
|
-
if (!ref) return release_list_fail("ReleaseListInputError", "No project to list. Run extension_auth (action: login), which names the project, or pass workspace + project explicitly.");
|
|
9526
|
+
if (!ref) return release_list_fail("ReleaseListInputError", "No project to list. Run extension_auth (action: login), which names the project, or pass workspace + project explicitly.", "auth-required", "E_AUTH_REQUIRED");
|
|
8684
9527
|
const channelsUrl = registryFileUrl(ref, "channels.json");
|
|
8685
9528
|
const metaUrl = registryFileUrl(ref, "meta.json");
|
|
8686
9529
|
const buildsUrl = registryFileUrl(ref, "builds/index.json");
|
|
@@ -8699,7 +9542,7 @@ async function readReleases(args) {
|
|
|
8699
9542
|
})
|
|
8700
9543
|
]);
|
|
8701
9544
|
const buildsPageUrl = consoleProjectUrl(ref, "builds", args.api);
|
|
8702
|
-
if (!channelsRes.ok && !metaRes.ok && !buildsRes.ok) return release_list_fail("ReleaseListNotFound", `No registry data for ${ref.workspace}/${ref.project} (${channelsUrl} returned ${channelsRes.status ?? "no response"}). The project may have no builds yet, or the workspace/project slugs may be wrong. If it is private, make sure extension_auth covers this exact project (a token scoped elsewhere cannot read it). The console Builds page is the authoritative view: ${buildsPageUrl}`, {
|
|
9545
|
+
if (!channelsRes.ok && !metaRes.ok && !buildsRes.ok) return release_list_fail("ReleaseListNotFound", `No registry data for ${ref.workspace}/${ref.project} (${channelsUrl} returned ${channelsRes.status ?? "no response"}). The project may have no builds yet, or the workspace/project slugs may be wrong. If it is private, make sure extension_auth covers this exact project (a token scoped elsewhere cannot read it). The console Builds page is the authoritative view: ${buildsPageUrl}`, "unavailable", "E_PLATFORM", {
|
|
8703
9546
|
workspace: ref.workspace,
|
|
8704
9547
|
project: ref.project,
|
|
8705
9548
|
registryUrl: channelsUrl,
|
|
@@ -8720,31 +9563,37 @@ async function readReleases(args) {
|
|
|
8720
9563
|
...b,
|
|
8721
9564
|
publicUrl: userlandProjectUrl(ref, userland.UserlandProjectPage.build(b.sha), args.api)
|
|
8722
9565
|
}));
|
|
8723
|
-
const
|
|
9566
|
+
const publicUrlNote = publicProjectUrl ? isPrivate ? "publicUrl links open only for workspace members. This project is private, so an outside recipient needs a share link from extension_publish." : "publicUrl links are the public build pages: no login needed, and they carry the per-browser downloads and the run locally instructions." : null;
|
|
9567
|
+
const channelsUnavailable = channelsRes.ok ? null : `channels.json unreadable: ${channelsRes.message}`;
|
|
9568
|
+
const buildsUnavailable = buildsRes.ok ? null : `builds/index.json unreadable: ${buildsRes.message}`;
|
|
9569
|
+
return envelope({
|
|
8724
9570
|
ok: true,
|
|
8725
|
-
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
9571
|
+
command: "extension_release_status",
|
|
9572
|
+
status: "read",
|
|
9573
|
+
value: {
|
|
9574
|
+
workspace: ref.workspace,
|
|
9575
|
+
project: ref.project,
|
|
9576
|
+
...meta?.name ? {
|
|
9577
|
+
name: meta.name
|
|
9578
|
+
} : {},
|
|
9579
|
+
...meta?.visibility ? {
|
|
9580
|
+
visibility: meta.visibility
|
|
9581
|
+
} : {},
|
|
9582
|
+
channels: channelsWithUrls,
|
|
9583
|
+
recentBuilds: buildsWithUrls,
|
|
9584
|
+
registryUrl: channelsUrl,
|
|
9585
|
+
buildsPageUrl,
|
|
9586
|
+
...publicProjectUrl ? {
|
|
9587
|
+
publicProjectUrl
|
|
9588
|
+
} : {}
|
|
9589
|
+
},
|
|
9590
|
+
hint: promotable.length > 0 || recentBuilds.length > 0 ? `Promotable shas: channels currently pin ${promotable.length > 0 ? promotable.join(", ") : "none"}; recent builds add ${recentBuilds.filter((b)=>"success" === b.status).map((b)=>b.sha).join(", ") || "none"}. Use one of these as buildId/buildSha for promote/deploy/publish.` : `No channels or builds are recorded on the registry yet for ${ref.workspace}/${ref.project}. Push a commit to produce a build, then check ${buildsPageUrl}.`,
|
|
9591
|
+
warnings: [
|
|
9592
|
+
publicUrlNote,
|
|
9593
|
+
channelsUnavailable,
|
|
9594
|
+
buildsUnavailable
|
|
9595
|
+
]
|
|
9596
|
+
});
|
|
8748
9597
|
}
|
|
8749
9598
|
const KNOWN_STORES = [
|
|
8750
9599
|
"chrome",
|
|
@@ -8830,19 +9679,22 @@ function normalizeStoresStatus(json) {
|
|
|
8830
9679
|
}
|
|
8831
9680
|
return out;
|
|
8832
9681
|
}
|
|
8833
|
-
function store_status_fail(name, message, extra) {
|
|
8834
|
-
return
|
|
9682
|
+
function store_status_fail(name, message, status, code, extra) {
|
|
9683
|
+
return envelope({
|
|
8835
9684
|
ok: false,
|
|
9685
|
+
command: "extension_release_status",
|
|
9686
|
+
status,
|
|
9687
|
+
value: extra ?? {},
|
|
8836
9688
|
error: {
|
|
9689
|
+
code,
|
|
8837
9690
|
name,
|
|
8838
9691
|
message
|
|
8839
|
-
}
|
|
8840
|
-
...extra ?? {}
|
|
9692
|
+
}
|
|
8841
9693
|
});
|
|
8842
9694
|
}
|
|
8843
9695
|
async function readStores(args) {
|
|
8844
9696
|
const ref = resolveProjectRef(args);
|
|
8845
|
-
if (!ref) return store_status_fail("StoreStatusInputError", "No project to inspect. Run extension_auth (action: login), which names the project, or pass workspace + project explicitly.");
|
|
9697
|
+
if (!ref) return store_status_fail("StoreStatusInputError", "No project to inspect. Run extension_auth (action: login), which names the project, or pass workspace + project explicitly.", "auth-required", "E_AUTH_REQUIRED");
|
|
8846
9698
|
const healthUrl = registryFileUrl(ref, "stores/health.json");
|
|
8847
9699
|
const statusUrl = registryFileUrl(ref, "stores/status.json");
|
|
8848
9700
|
const submissionsUrl = registryFileUrl(ref, "stores/submissions.json");
|
|
@@ -8861,7 +9713,7 @@ async function readStores(args) {
|
|
|
8861
9713
|
api: args.api
|
|
8862
9714
|
})
|
|
8863
9715
|
]);
|
|
8864
|
-
if (!healthRes.ok && !statusRes.ok && !submissionsRes.ok) return store_status_fail("StoreStatusNotFound", `No store data on the registry for ${ref.workspace}/${ref.project} (${healthUrl} returned ${healthRes.status ?? "no response"}). The project may have no stores configured yet, be private (private registry data needs a share token), or the workspace/project slugs may be wrong. Configure stores at ${consoleStoresUrl}/new; the console Stores page is the authoritative view: ${consoleStoresUrl}`, {
|
|
9716
|
+
if (!healthRes.ok && !statusRes.ok && !submissionsRes.ok) return store_status_fail("StoreStatusNotFound", `No store data on the registry for ${ref.workspace}/${ref.project} (${healthUrl} returned ${healthRes.status ?? "no response"}). The project may have no stores configured yet, be private (private registry data needs a share token), or the workspace/project slugs may be wrong. Configure stores at ${consoleStoresUrl}/new; the console Stores page is the authoritative view: ${consoleStoresUrl}`, "unavailable", "E_PLATFORM", {
|
|
8865
9717
|
workspace: ref.workspace,
|
|
8866
9718
|
project: ref.project,
|
|
8867
9719
|
registryUrls: {
|
|
@@ -8917,33 +9769,41 @@ async function readStores(args) {
|
|
|
8917
9769
|
if (review?.status) tail.push(`review ${review.status}${review.checkedAt ? ` (checked ${review.checkedAt})` : ""}`);
|
|
8918
9770
|
return tail.length > 0 ? `${head}; ${tail.join("; ")}` : head;
|
|
8919
9771
|
});
|
|
8920
|
-
const
|
|
9772
|
+
const healthUnavailable = healthRes.ok ? null : `stores/health.json unreadable: ${healthRes.message}`;
|
|
9773
|
+
const statusUnavailable = statusRes.ok ? null : `stores/status.json unreadable: ${statusRes.message}`;
|
|
9774
|
+
const submissionsUnavailable = submissionsRes.ok || 404 === submissionsRes.status ? null : `stores/submissions.json unreadable: ${submissionsRes.message}`;
|
|
9775
|
+
return envelope({
|
|
8921
9776
|
ok: true,
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
9777
|
+
command: "extension_release_status",
|
|
9778
|
+
status: "read",
|
|
9779
|
+
value: {
|
|
9780
|
+
workspace: ref.workspace,
|
|
9781
|
+
project: ref.project,
|
|
9782
|
+
stores: rows,
|
|
9783
|
+
...status.lastSubmission ? {
|
|
9784
|
+
lastSubmission: status.lastSubmission
|
|
9785
|
+
} : {},
|
|
9786
|
+
...status.lastPollAt ? {
|
|
9787
|
+
lastPollAt: status.lastPollAt
|
|
9788
|
+
} : {},
|
|
9789
|
+
registryUrls: {
|
|
9790
|
+
health: healthUrl,
|
|
9791
|
+
status: statusUrl,
|
|
9792
|
+
submissions: submissionsUrl
|
|
9793
|
+
},
|
|
9794
|
+
consoleStoresUrl
|
|
8935
9795
|
},
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
9796
|
+
hint: `${summaryParts.join(". ")}. This is the registry's recorded state (submissions and the review poller write it); the store dashboards are authoritative and may be ahead of it.`,
|
|
9797
|
+
warnings: [
|
|
9798
|
+
healthUnavailable,
|
|
9799
|
+
statusUnavailable,
|
|
9800
|
+
submissionsUnavailable
|
|
9801
|
+
]
|
|
9802
|
+
});
|
|
8943
9803
|
}
|
|
8944
9804
|
const release_status_schema = {
|
|
8945
9805
|
name: "extension_release_status",
|
|
8946
|
-
description: "Read where a project stands on extension.dev, from the public registry (registry.extension.land).
|
|
9806
|
+
description: "Read where a project stands on extension.dev, from the public registry (registry.extension.land). This is read-only: it dispatches nothing and promotes nothing. Pass include:'releases' for the release channels (channel to promoted build sha), recent builds, and a public build-page URL for each, which is how you find a valid sha for extension_release_promote, extension_submit or extension_publish. Pass include:'stores' for the per-store picture after an extension_submit (chrome, firefox, edge, safari): configured or not, the last credential health check, the last recorded submission, and the latest review status, read from stores/health.json, stores/status.json and stores/submissions.json. Both are included by default. This defaults to the logged-in project (extension_auth); pass workspace and project to read another. Private projects work when your stored login covers them. Registry state can lag the store dashboards by up to a polling interval.",
|
|
8947
9807
|
inputSchema: {
|
|
8948
9808
|
type: "object",
|
|
8949
9809
|
properties: {
|
|
@@ -8979,13 +9839,16 @@ function parse(raw) {
|
|
|
8979
9839
|
try {
|
|
8980
9840
|
return JSON.parse(raw);
|
|
8981
9841
|
} catch {
|
|
8982
|
-
return {
|
|
9842
|
+
return envelopeObject({
|
|
8983
9843
|
ok: false,
|
|
9844
|
+
command: "extension_release_status",
|
|
9845
|
+
status: "unavailable",
|
|
8984
9846
|
error: {
|
|
9847
|
+
code: "E_PARSE",
|
|
8985
9848
|
name: "ParseError",
|
|
8986
9849
|
message: raw
|
|
8987
9850
|
}
|
|
8988
|
-
};
|
|
9851
|
+
});
|
|
8989
9852
|
}
|
|
8990
9853
|
}
|
|
8991
9854
|
async function release_status_handler(args) {
|
|
@@ -9006,14 +9869,19 @@ async function release_status_handler(args) {
|
|
|
9006
9869
|
releases,
|
|
9007
9870
|
stores
|
|
9008
9871
|
].filter(Boolean);
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9872
|
+
const ok = sections.some((section)=>true === section.ok);
|
|
9873
|
+
return envelope({
|
|
9874
|
+
ok,
|
|
9875
|
+
command: "extension_release_status",
|
|
9876
|
+
status: ok ? "read" : "unavailable",
|
|
9877
|
+
value: {
|
|
9878
|
+
...releases ? {
|
|
9879
|
+
releases
|
|
9880
|
+
} : {},
|
|
9881
|
+
...stores ? {
|
|
9882
|
+
stores
|
|
9883
|
+
} : {}
|
|
9884
|
+
}
|
|
9017
9885
|
});
|
|
9018
9886
|
}
|
|
9019
9887
|
function storeMdWarnings(browsers, cwd) {
|
|
@@ -9044,7 +9912,7 @@ function storeMdWarnings(browsers, cwd) {
|
|
|
9044
9912
|
}
|
|
9045
9913
|
const submit_schema = {
|
|
9046
9914
|
name: "extension_submit",
|
|
9047
|
-
description: "
|
|
9915
|
+
description: "Submit a built extension for store REVIEW through extension.dev, which holds your store credentials and dispatches from your project's mirror CI: the Chrome Web Store, Firefox AMO, Edge Add-ons and the App Store (Safari). This is store review only. It does not push a build to the extension.dev platform, and it does not make a shareable link: that is extension_publish, which is what \"deploy\" or \"ship\" an extension almost always means. Reach for this only when the ask is explicitly a store submission. It defaults to a dry run that dispatches nothing: the platform verifies auth, project, build and store workflow, and this tool adds each store's credential-health verdict. Trust those per-store rows over the platform's bare preflight line, which does not check store health. Pass dryRun:false to actually submit, which is irreversible and enters store review. The project comes from your token (extension_auth or EXTENSION_DEV_TOKEN; tokens live at most 7 days, so CI must re-mint from the console's Access tokens page). Store credentials are never arguments, and no local file is uploaded. Call extension_release_status for valid shas, and, after a real submission, for the recorded outcome and review state.",
|
|
9048
9916
|
inputSchema: {
|
|
9049
9917
|
type: "object",
|
|
9050
9918
|
properties: {
|
|
@@ -9086,10 +9954,13 @@ const submit_schema = {
|
|
|
9086
9954
|
]
|
|
9087
9955
|
}
|
|
9088
9956
|
};
|
|
9089
|
-
function submit_fail(name, message) {
|
|
9090
|
-
return
|
|
9957
|
+
function submit_fail(name, message, status, code) {
|
|
9958
|
+
return envelope({
|
|
9091
9959
|
ok: false,
|
|
9960
|
+
command: "extension_submit",
|
|
9961
|
+
status,
|
|
9092
9962
|
error: {
|
|
9963
|
+
code,
|
|
9093
9964
|
name,
|
|
9094
9965
|
message
|
|
9095
9966
|
}
|
|
@@ -9097,13 +9968,13 @@ function submit_fail(name, message) {
|
|
|
9097
9968
|
}
|
|
9098
9969
|
async function submit_handler(args) {
|
|
9099
9970
|
const token = resolveToken();
|
|
9100
|
-
if (!token) return submit_fail("SubmitAuthError", "No token. Run extension_auth (action: login), or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard under project settings -> Access tokens; tokens live at most 7 days, so CI must re-mint before expiry).");
|
|
9971
|
+
if (!token) return submit_fail("SubmitAuthError", "No token. Run extension_auth (action: login), or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard under project settings -> Access tokens; tokens live at most 7 days, so CI must re-mint before expiry).", "auth-required", "E_AUTH_REQUIRED");
|
|
9101
9972
|
const browsers = (Array.isArray(args.browsers) ? args.browsers : []).map((b)=>String(b).trim().toLowerCase()).filter(Boolean);
|
|
9102
|
-
if (0 === browsers.length) return submit_fail("SubmitInputError", 'browsers is required (e.g. ["chrome","firefox","edge","safari"]).');
|
|
9973
|
+
if (0 === browsers.length) return submit_fail("SubmitInputError", 'browsers is required (e.g. ["chrome","firefox","edge","safari"]).', "bad-request", "E_BAD_REQUEST");
|
|
9103
9974
|
const buildSha = String(args.buildSha || "").trim();
|
|
9104
|
-
if (!buildSha) return submit_fail("SubmitInputError", "buildSha is required (the built commit to submit).");
|
|
9975
|
+
if (!buildSha) return submit_fail("SubmitInputError", "buildSha is required (the built commit to submit).", "bad-request", "E_BAD_REQUEST");
|
|
9105
9976
|
const apiCheck = safeApiBase(resolveApiBase(args.api));
|
|
9106
|
-
if (!apiCheck.ok) return submit_fail("SubmitConfigError", apiCheck.message);
|
|
9977
|
+
if (!apiCheck.ok) return submit_fail("SubmitConfigError", apiCheck.message, "bad-config", "E_CONFIG");
|
|
9107
9978
|
const url = `${apiCheck.base}/api/cli/stores/submit`;
|
|
9108
9979
|
const dryRun = false !== args.dryRun;
|
|
9109
9980
|
const body = {
|
|
@@ -9125,7 +9996,7 @@ async function submit_handler(args) {
|
|
|
9125
9996
|
body: JSON.stringify(body)
|
|
9126
9997
|
});
|
|
9127
9998
|
} catch (err) {
|
|
9128
|
-
return submit_fail("SubmitNetworkError", `Could not reach ${url}: ${err?.message || err}
|
|
9999
|
+
return submit_fail("SubmitNetworkError", `Could not reach ${url}: ${err?.message || err}`, "network-failed", "E_NETWORK");
|
|
9129
10000
|
}
|
|
9130
10001
|
const text = await res.text();
|
|
9131
10002
|
let data;
|
|
@@ -9136,7 +10007,7 @@ async function submit_handler(args) {
|
|
|
9136
10007
|
message: text
|
|
9137
10008
|
};
|
|
9138
10009
|
}
|
|
9139
|
-
if (!res.ok) return submit_fail("SubmitError", `${dryRun ? "preflight" : "submit"} failed (${res.status}): ${data?.message || text || "unknown error"}
|
|
10010
|
+
if (!res.ok) return submit_fail("SubmitError", `${dryRun ? "preflight" : "submit"} failed (${res.status}): ${data?.message || text || "unknown error"}`, "submit-failed", "E_PLATFORM");
|
|
9140
10011
|
const warnings = Array.isArray(data?.warnings) ? [
|
|
9141
10012
|
...data.warnings
|
|
9142
10013
|
] : [];
|
|
@@ -9146,6 +10017,13 @@ async function submit_handler(args) {
|
|
|
9146
10017
|
dryRun,
|
|
9147
10018
|
...data
|
|
9148
10019
|
};
|
|
10020
|
+
delete result.ok;
|
|
10021
|
+
delete result.warnings;
|
|
10022
|
+
delete result.message;
|
|
10023
|
+
let ok = data?.ok !== false;
|
|
10024
|
+
let message = "string" == typeof data?.message ? data.message : "";
|
|
10025
|
+
let channelNote = null;
|
|
10026
|
+
let statusNote = null;
|
|
9149
10027
|
if (dryRun) {
|
|
9150
10028
|
const ref = resolveProjectRef();
|
|
9151
10029
|
const consoleStoresUrl = consoleProjectUrl(ref, "stores", args.api);
|
|
@@ -9213,18 +10091,28 @@ async function submit_handler(args) {
|
|
|
9213
10091
|
if (actionable.length > 0) summaryParts.push(`Preflight passed for ${actionable.join(", ")}: the platform verified auth, the project, build ${data?.buildId ?? buildSha}, and the store workflow, and the store credentials passed their last health check.`);
|
|
9214
10092
|
for (const p of blocked)summaryParts.push(`${p.browser}: ${"unknown" === p.configured ? "cannot be verified" : "NOT actionable"} - ${p.reason}`);
|
|
9215
10093
|
summaryParts.push(storeModeNote);
|
|
9216
|
-
|
|
10094
|
+
ok = actionable.length > 0;
|
|
9217
10095
|
result.preflight = preflight;
|
|
9218
10096
|
result.channel = resolvedChannel;
|
|
9219
10097
|
result.channelDefaulted = channelDefaulted;
|
|
9220
|
-
if (channelDefaulted)
|
|
10098
|
+
if (channelDefaulted) channelNote = `channel: ${resolvedChannel} (default)`;
|
|
9221
10099
|
result.consoleStoresUrl = consoleStoresUrl;
|
|
9222
10100
|
if ("string" == typeof data?.message) result.platformMessage = data.message;
|
|
9223
|
-
|
|
9224
|
-
}
|
|
9225
|
-
if (!dryRun)
|
|
9226
|
-
|
|
9227
|
-
|
|
10101
|
+
message = summaryParts.join(" ");
|
|
10102
|
+
}
|
|
10103
|
+
if (!dryRun) statusNote = "Track this submission with extension_release_status: it reads the recorded outcome, per-store credential health, and review state from the public registry.";
|
|
10104
|
+
return envelope({
|
|
10105
|
+
ok,
|
|
10106
|
+
command: "extension_submit",
|
|
10107
|
+
status: dryRun ? "preflight" : "submitted",
|
|
10108
|
+
value: result,
|
|
10109
|
+
hint: message,
|
|
10110
|
+
warnings: [
|
|
10111
|
+
...warnings,
|
|
10112
|
+
channelNote,
|
|
10113
|
+
statusNote
|
|
10114
|
+
]
|
|
10115
|
+
});
|
|
9228
10116
|
}
|
|
9229
10117
|
const ENGINE_COMPANION_IDS = new Set([
|
|
9230
10118
|
"kgdaecdpfkikjncaalnmmnjjfpofkcbl",
|
|
@@ -9293,7 +10181,7 @@ function doctor_readReadyContract(projectPath, browser) {
|
|
|
9293
10181
|
}
|
|
9294
10182
|
const doctor_schema = {
|
|
9295
10183
|
name: "extension_doctor",
|
|
9296
|
-
description: "Diagnose a dev session end
|
|
10184
|
+
description: "Diagnose a dev session end to end: ready contract, dev-server process, control-port agreement, control channel, eval token, executor, browser liveness. This returns one {check, status, detail, remediation?} per leg, in dependency order. Read a 'skip' as blocked, not as a pass: it names the check that blocked it. Run this first when any act tool (storage, reload, eval, open) errors unexpectedly. Call it with no projectPath for a pre-flight environment check (node, the Extension.js CLI, the template cache) before any project exists.",
|
|
9297
10185
|
inputSchema: {
|
|
9298
10186
|
type: "object",
|
|
9299
10187
|
properties: {
|
|
@@ -9337,16 +10225,21 @@ async function environmentPreflight() {
|
|
|
9337
10225
|
detail: cacheExists ? `Template catalog cached at ${cacheFile}` : "Template catalog not cached yet (extension_templates will fetch it)"
|
|
9338
10226
|
});
|
|
9339
10227
|
const healthy = checks.every((c)=>"fail" !== c.status);
|
|
9340
|
-
return
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
10228
|
+
return envelope({
|
|
10229
|
+
ok: healthy,
|
|
10230
|
+
command: doctor_schema.name,
|
|
10231
|
+
status: healthy ? "healthy" : "unhealthy",
|
|
10232
|
+
value: {
|
|
10233
|
+
mode: "environment",
|
|
10234
|
+
checks
|
|
10235
|
+
},
|
|
9344
10236
|
hint: "Pass projectPath to diagnose a live dev session end-to-end."
|
|
9345
10237
|
});
|
|
9346
10238
|
}
|
|
9347
10239
|
function safeStringify(value) {
|
|
9348
10240
|
try {
|
|
9349
|
-
|
|
10241
|
+
const text = JSON.stringify(value);
|
|
10242
|
+
return text ?? String(value);
|
|
9350
10243
|
} catch {
|
|
9351
10244
|
return String(value);
|
|
9352
10245
|
}
|
|
@@ -9402,7 +10295,8 @@ async function doctor_handler(args) {
|
|
|
9402
10295
|
});
|
|
9403
10296
|
const out = stdout.trim();
|
|
9404
10297
|
try {
|
|
9405
|
-
const
|
|
10298
|
+
const parsed = JSON.parse(out);
|
|
10299
|
+
const checks = isEnvelope(parsed) ? parsed.value?.checks : parsed;
|
|
9406
10300
|
if (!Array.isArray(checks)) throw new Error("not a check array");
|
|
9407
10301
|
for (const check of checks){
|
|
9408
10302
|
if ("string" == typeof check.detail) check.detail = toMcpSpeak(check.detail);
|
|
@@ -9445,23 +10339,30 @@ async function doctor_handler(args) {
|
|
|
9445
10339
|
} : {}
|
|
9446
10340
|
});
|
|
9447
10341
|
}
|
|
9448
|
-
return
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
10342
|
+
return envelope({
|
|
10343
|
+
ok: healthy,
|
|
10344
|
+
command: doctor_schema.name,
|
|
10345
|
+
status: healthy ? "healthy" : "unhealthy",
|
|
10346
|
+
value: {
|
|
10347
|
+
browser,
|
|
10348
|
+
...engineVersion ? {
|
|
10349
|
+
engineVersion
|
|
10350
|
+
} : {},
|
|
10351
|
+
checks
|
|
10352
|
+
}
|
|
9455
10353
|
});
|
|
9456
10354
|
} catch {
|
|
9457
10355
|
const message = stderr.trim() || `extension exited with code ${code}`;
|
|
9458
|
-
return
|
|
10356
|
+
return envelope({
|
|
9459
10357
|
ok: false,
|
|
10358
|
+
command: doctor_schema.name,
|
|
10359
|
+
status: "cli-failed",
|
|
9460
10360
|
error: {
|
|
10361
|
+
code: "E_CLI",
|
|
9461
10362
|
name: "CliError",
|
|
9462
|
-
message: toMcpSpeak(message)
|
|
9463
|
-
|
|
9464
|
-
|
|
10363
|
+
message: toMcpSpeak(message)
|
|
10364
|
+
},
|
|
10365
|
+
hint: "extension doctor requires a recent extension CLI, the project's local install may predate it."
|
|
9465
10366
|
});
|
|
9466
10367
|
}
|
|
9467
10368
|
}
|
|
@@ -9478,7 +10379,7 @@ function wait_isAlive(pid) {
|
|
|
9478
10379
|
}
|
|
9479
10380
|
const wait_schema = {
|
|
9480
10381
|
name: "extension_wait",
|
|
9481
|
-
description: "Wait for a running dev or start session to be ready.
|
|
10382
|
+
description: "Wait for a running dev or start session to be ready. This polls the ready.json contract and reports compiled (the compiler finished), browserAttached (the runtime executor connected), and guestLoaded (the browser's own target list shows your extension). Read guestLoaded as the trustworthy load signal: it catches a silently rejected --load-extension that leaves ready.json stamped attached with empty logs. It is null when it could not be checked, for example a gecko session with no CDP port. Every result reports budgetMs and elapsedMs; on status 'timeout', call again to keep waiting on the same contract. In a noBrowser session this returns as soon as the compile lands, instead of waiting for a browser that will never attach. Ports come from the contract, so they match what the server actually bound.",
|
|
9482
10383
|
inputSchema: {
|
|
9483
10384
|
type: "object",
|
|
9484
10385
|
properties: {
|
|
@@ -9516,31 +10417,42 @@ async function wait_handler(args) {
|
|
|
9516
10417
|
const contract = JSON.parse(raw);
|
|
9517
10418
|
lastContractStatus = contract.status;
|
|
9518
10419
|
if ("ready" === contract.status) {
|
|
9519
|
-
if ("number" == typeof contract.pid && !wait_isAlive(contract.pid)) return
|
|
10420
|
+
if ("number" == typeof contract.pid && !wait_isAlive(contract.pid)) return envelope({
|
|
10421
|
+
ok: false,
|
|
10422
|
+
command: wait_schema.name,
|
|
9520
10423
|
status: "stale",
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
|
|
9524
|
-
|
|
9525
|
-
|
|
10424
|
+
error: {
|
|
10425
|
+
code: "E_STALE_CONTRACT",
|
|
10426
|
+
message: `ready.json reports ready but its dev-server pid ${contract.pid} is dead, the session exited. Restart with extension_dev; extension_doctor will confirm.`
|
|
10427
|
+
},
|
|
10428
|
+
value: {
|
|
10429
|
+
browser: contract.browser,
|
|
10430
|
+
pid: contract.pid,
|
|
10431
|
+
budgetMs,
|
|
10432
|
+
elapsedMs: Date.now() - start
|
|
10433
|
+
}
|
|
9526
10434
|
});
|
|
9527
10435
|
const attached = "attached" === contract.runtime || "string" == typeof contract.executorAttachedAt;
|
|
9528
|
-
if (!attached && buildOnly) return
|
|
10436
|
+
if (!attached && buildOnly) return envelope({
|
|
10437
|
+
ok: true,
|
|
10438
|
+
command: wait_schema.name,
|
|
9529
10439
|
status: "ready",
|
|
9530
|
-
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
|
|
9536
|
-
|
|
9537
|
-
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
|
|
10440
|
+
value: {
|
|
10441
|
+
buildOnly: true,
|
|
10442
|
+
compiled: true,
|
|
10443
|
+
browserAttached: false,
|
|
10444
|
+
sessionCommand: contract.command,
|
|
10445
|
+
browser: contract.browser,
|
|
10446
|
+
port: contract.port,
|
|
10447
|
+
pid: contract.pid,
|
|
10448
|
+
distPath: contract.distPath,
|
|
10449
|
+
manifestPath: contract.manifestPath,
|
|
10450
|
+
compiledAt: contract.compiledAt,
|
|
10451
|
+
startedAt: contract.startedAt,
|
|
10452
|
+
budgetMs,
|
|
10453
|
+
elapsedMs: Date.now() - start
|
|
10454
|
+
},
|
|
10455
|
+
hint: "Build-only session (noBrowser): the extension compiled and the dev server is live, but no browser was launched, so browserAttached will never become true. Do not call extension_wait again to wait for a browser. The control verbs (storage/reload/open/dom_snapshot/eval) need a live browser and will not work against this session."
|
|
9544
10456
|
});
|
|
9545
10457
|
if (!attached) {
|
|
9546
10458
|
await new Promise((r)=>setTimeout(r, pollInterval));
|
|
@@ -9552,72 +10464,98 @@ async function wait_handler(args) {
|
|
|
9552
10464
|
const warnings = [];
|
|
9553
10465
|
if (runtimeErrors.length) warnings.push(`Compiled and attached, but the extension is throwing at runtime (${runtimeErrors.length} recent error event${1 === runtimeErrors.length ? "" : "s"} above). Check extension_logs (level: error) or extension_doctor before trusting this session.`);
|
|
9554
10466
|
if (guestCheck.checked && !guestCheck.loaded) warnings.push("The engine reports the runtime attached, but the browser's own target list shows no chrome-extension:// target for your extension, only the engine companion. This is the signature of a silently rejected --load-extension (extension.js BUGS_TO_FIX §83): the CLI and ready.json cannot see it, and the control verbs will fail against a guest that is not there. Check the manifest and extension_logs.");
|
|
9555
|
-
return
|
|
10467
|
+
return envelope({
|
|
10468
|
+
ok: true,
|
|
10469
|
+
command: wait_schema.name,
|
|
9556
10470
|
status: "ready",
|
|
9557
|
-
|
|
9558
|
-
|
|
9559
|
-
|
|
9560
|
-
|
|
9561
|
-
|
|
9562
|
-
|
|
9563
|
-
|
|
10471
|
+
value: {
|
|
10472
|
+
compiled: true,
|
|
10473
|
+
browserAttached: true,
|
|
10474
|
+
guestLoaded: guestCheck.checked ? guestCheck.loaded : null,
|
|
10475
|
+
...guestCheck.checked ? {
|
|
10476
|
+
guestIds: guestCheck.guestIds
|
|
10477
|
+
} : {
|
|
10478
|
+
guestLoadNote: guestCheck.reason
|
|
10479
|
+
},
|
|
10480
|
+
sessionCommand: contract.command,
|
|
10481
|
+
browser: contract.browser,
|
|
10482
|
+
port: contract.port,
|
|
10483
|
+
pid: contract.pid,
|
|
10484
|
+
distPath: contract.distPath,
|
|
10485
|
+
manifestPath: contract.manifestPath,
|
|
10486
|
+
compiledAt: contract.compiledAt,
|
|
10487
|
+
startedAt: contract.startedAt,
|
|
10488
|
+
budgetMs,
|
|
10489
|
+
elapsedMs: Date.now() - start,
|
|
10490
|
+
...runtimeErrors.length ? {
|
|
10491
|
+
runtimeErrors
|
|
10492
|
+
} : {}
|
|
9564
10493
|
},
|
|
9565
|
-
|
|
9566
|
-
browser: contract.browser,
|
|
9567
|
-
port: contract.port,
|
|
9568
|
-
pid: contract.pid,
|
|
9569
|
-
distPath: contract.distPath,
|
|
9570
|
-
manifestPath: contract.manifestPath,
|
|
9571
|
-
compiledAt: contract.compiledAt,
|
|
9572
|
-
startedAt: contract.startedAt,
|
|
9573
|
-
budgetMs,
|
|
9574
|
-
elapsedMs: Date.now() - start,
|
|
9575
|
-
...runtimeErrors.length ? {
|
|
9576
|
-
runtimeErrors
|
|
9577
|
-
} : {},
|
|
9578
|
-
...warnings.length ? {
|
|
9579
|
-
warning: warnings.join(" ")
|
|
9580
|
-
} : {}
|
|
10494
|
+
warnings
|
|
9581
10495
|
});
|
|
9582
10496
|
}
|
|
9583
|
-
if ("error" === contract.status) return
|
|
9584
|
-
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
-
|
|
9588
|
-
|
|
9589
|
-
|
|
9590
|
-
|
|
10497
|
+
if ("error" === contract.status) return envelope({
|
|
10498
|
+
ok: false,
|
|
10499
|
+
command: wait_schema.name,
|
|
10500
|
+
status: "contract-error",
|
|
10501
|
+
error: {
|
|
10502
|
+
code: contract.code ? `E_${String(contract.code).toUpperCase()}` : "E_CONTRACT_ERROR",
|
|
10503
|
+
message: contract.message ?? "The dev server stamped its ready contract with an error.",
|
|
10504
|
+
contractCode: contract.code
|
|
10505
|
+
},
|
|
10506
|
+
value: {
|
|
10507
|
+
errors: contract.errors,
|
|
10508
|
+
browser: contract.browser,
|
|
10509
|
+
budgetMs,
|
|
10510
|
+
elapsedMs: Date.now() - start
|
|
10511
|
+
}
|
|
9591
10512
|
});
|
|
9592
10513
|
} catch {}
|
|
9593
10514
|
await new Promise((resolve)=>setTimeout(resolve, pollInterval));
|
|
9594
10515
|
}
|
|
9595
|
-
if (sawCompiledButUnattached) return
|
|
10516
|
+
if (sawCompiledButUnattached) return envelope({
|
|
10517
|
+
ok: false,
|
|
10518
|
+
command: wait_schema.name,
|
|
9596
10519
|
status: "compiled-not-attached",
|
|
9597
|
-
|
|
9598
|
-
|
|
9599
|
-
|
|
9600
|
-
|
|
9601
|
-
|
|
9602
|
-
|
|
10520
|
+
error: {
|
|
10521
|
+
code: "E_NOT_ATTACHED",
|
|
10522
|
+
message: `The extension compiled, but the runtime executor never attached within this call's ${budgetMs}ms budget. The build is fine; the browser side is not connected, so extension_eval/storage/reload/open will fail with "no executor connected".`
|
|
10523
|
+
},
|
|
10524
|
+
value: {
|
|
10525
|
+
compiled: true,
|
|
10526
|
+
browserAttached: false,
|
|
10527
|
+
readyPath,
|
|
10528
|
+
budgetMs,
|
|
10529
|
+
elapsedMs: Date.now() - start
|
|
10530
|
+
},
|
|
9603
10531
|
hint: "This is usually transient: call extension_wait again. If it persists, stop and restart the session with extension_dev (a restart reliably reattaches); extension_doctor reports the executor leg."
|
|
9604
10532
|
});
|
|
9605
|
-
return
|
|
10533
|
+
return envelope({
|
|
10534
|
+
ok: false,
|
|
10535
|
+
command: wait_schema.name,
|
|
9606
10536
|
status: "timeout",
|
|
9607
|
-
|
|
9608
|
-
|
|
9609
|
-
|
|
9610
|
-
|
|
9611
|
-
|
|
9612
|
-
|
|
9613
|
-
|
|
10537
|
+
error: {
|
|
10538
|
+
code: "E_WAIT_TIMEOUT",
|
|
10539
|
+
message: "starting" === lastContractStatus ? `Not ready after ${budgetMs}ms this call: the dev server stamped its contract (status: starting) but the first compile has not landed yet.` : `Not ready after ${budgetMs}ms this call: no ready contract was observed at ${readyPath}, so neither the compile nor a browser attach has been seen.`
|
|
10540
|
+
},
|
|
10541
|
+
value: {
|
|
10542
|
+
compiled: false,
|
|
10543
|
+
browserAttached: false,
|
|
10544
|
+
readyPath,
|
|
10545
|
+
budgetMs,
|
|
10546
|
+
elapsedMs: Date.now() - start
|
|
10547
|
+
},
|
|
10548
|
+
warnings: [
|
|
10549
|
+
clamped ? `requested ${requested}ms was clamped to ${SAFE_CEILING_MS}ms to stay under the MCP client request timeout` : null
|
|
10550
|
+
],
|
|
9614
10551
|
hint: "Still building, call extension_wait again to keep waiting (it resumes polling the same contract). If it never readies, check the dev process with extension_doctor."
|
|
9615
10552
|
});
|
|
9616
10553
|
}
|
|
10554
|
+
const add_feature_COMMAND = "extension_add_feature";
|
|
9617
10555
|
const EXAMPLES_TREE_BASE = `https://github.com/extension-js/examples/tree/${PINNED_COMMIT}/examples`;
|
|
9618
10556
|
const add_feature_schema = {
|
|
9619
10557
|
name: "extension_add_feature",
|
|
9620
|
-
description: "Plan a new feature surface for an existing extension.
|
|
10558
|
+
description: "Plan a new feature surface for an existing extension. This returns step-by-step instructions, the manifest additions to make, and reference templates from the extension.dev catalog. It modifies no files: apply the returned plan yourself.",
|
|
9621
10559
|
inputSchema: {
|
|
9622
10560
|
type: "object",
|
|
9623
10561
|
properties: {
|
|
@@ -9769,13 +10707,25 @@ async function add_feature_handler(args) {
|
|
|
9769
10707
|
const projectPath = node_path.resolve(args.projectPath);
|
|
9770
10708
|
const srcDir = node_path.join(projectPath, "src");
|
|
9771
10709
|
const manifestPath = node_path.join(srcDir, "manifest.json");
|
|
9772
|
-
if (!node_fs.existsSync(manifestPath)) return
|
|
9773
|
-
|
|
10710
|
+
if (!node_fs.existsSync(manifestPath)) return envelope({
|
|
10711
|
+
ok: false,
|
|
10712
|
+
command: add_feature_COMMAND,
|
|
10713
|
+
status: "manifest-not-found",
|
|
10714
|
+
error: {
|
|
10715
|
+
code: "E_MANIFEST_NOT_FOUND",
|
|
10716
|
+
message: `No manifest.json found at ${manifestPath}`
|
|
10717
|
+
},
|
|
9774
10718
|
hint: "Ensure projectPath points to an extension project root with src/manifest.json"
|
|
9775
10719
|
});
|
|
9776
10720
|
const templateSlug = FEATURE_TEMPLATE_MAP[args.feature]?.[framework];
|
|
9777
|
-
if (!templateSlug) return
|
|
9778
|
-
|
|
10721
|
+
if (!templateSlug) return envelope({
|
|
10722
|
+
ok: false,
|
|
10723
|
+
command: add_feature_COMMAND,
|
|
10724
|
+
status: "no-reference-template",
|
|
10725
|
+
error: {
|
|
10726
|
+
code: "E_NO_REFERENCE_TEMPLATE",
|
|
10727
|
+
message: `No reference template for feature "${args.feature}" with framework "${framework}"`
|
|
10728
|
+
}
|
|
9779
10729
|
});
|
|
9780
10730
|
const template = await getTemplateBySlug(templateSlug);
|
|
9781
10731
|
const referenceFiles = template?.keyFiles ?? template?.files ?? [];
|
|
@@ -9850,29 +10800,40 @@ async function add_feature_handler(args) {
|
|
|
9850
10800
|
hint: "Background service worker / script"
|
|
9851
10801
|
});
|
|
9852
10802
|
const conflicts = filesToCreate.filter((f)=>node_fs.existsSync(node_path.join(projectPath, f.path)));
|
|
9853
|
-
|
|
9854
|
-
|
|
9855
|
-
|
|
9856
|
-
|
|
9857
|
-
|
|
9858
|
-
|
|
9859
|
-
|
|
10803
|
+
const conflictHint = `Warning: ${conflicts.length} file(s) already exist and would be overwritten.`;
|
|
10804
|
+
return envelope({
|
|
10805
|
+
ok: true,
|
|
10806
|
+
command: add_feature_COMMAND,
|
|
10807
|
+
status: conflicts.length ? "planned-with-conflicts" : "planned",
|
|
10808
|
+
value: {
|
|
10809
|
+
feature: args.feature,
|
|
10810
|
+
framework,
|
|
10811
|
+
referenceTemplate: {
|
|
10812
|
+
slug: templateSlug,
|
|
10813
|
+
repositoryUrl: `${EXAMPLES_TREE_BASE}/${templateSlug}`,
|
|
10814
|
+
referenceFiles: referenceFiles.filter((f)=>f.includes(featureDir) || f.includes("manifest"))
|
|
10815
|
+
},
|
|
10816
|
+
manifestUpdates,
|
|
10817
|
+
filesToCreate: filesToCreate.map((f)=>({
|
|
10818
|
+
...f,
|
|
10819
|
+
exists: node_fs.existsSync(node_path.join(projectPath, f.path))
|
|
10820
|
+
})),
|
|
10821
|
+
conflicts: conflicts.map((c)=>c.path),
|
|
10822
|
+
instructions: [
|
|
10823
|
+
`1. Add these fields to your src/manifest.json:\n${JSON.stringify(manifestUpdates, null, 2)}`,
|
|
10824
|
+
"2. Create the following files in your project:",
|
|
10825
|
+
...filesToCreate.map((f)=>` - ${f.path} (${f.hint})`),
|
|
10826
|
+
"sidebar" === args.feature ? "3. Add background.ts to handle sidebar open: chromium uses chrome.sidePanel.setPanelBehavior, firefox uses browser.sidebarAction.open()" : "",
|
|
10827
|
+
`4. Reference template source: ${EXAMPLES_TREE_BASE}/${templateSlug}/src`,
|
|
10828
|
+
"5. Run npm run dev to test"
|
|
10829
|
+
].filter(Boolean)
|
|
9860
10830
|
},
|
|
9861
|
-
|
|
9862
|
-
|
|
9863
|
-
|
|
9864
|
-
|
|
9865
|
-
|
|
9866
|
-
|
|
9867
|
-
instructions: [
|
|
9868
|
-
`1. Add these fields to your src/manifest.json:\n${JSON.stringify(manifestUpdates, null, 2)}`,
|
|
9869
|
-
"2. Create the following files in your project:",
|
|
9870
|
-
...filesToCreate.map((f)=>` - ${f.path} (${f.hint})`),
|
|
9871
|
-
"sidebar" === args.feature ? "3. Add background.ts to handle sidebar open: chromium uses chrome.sidePanel.setPanelBehavior, firefox uses browser.sidebarAction.open()" : "",
|
|
9872
|
-
`4. Reference template source: ${EXAMPLES_TREE_BASE}/${templateSlug}/src`,
|
|
9873
|
-
"5. Run npm run dev to test"
|
|
9874
|
-
].filter(Boolean),
|
|
9875
|
-
hint: conflicts.length ? `Warning: ${conflicts.length} file(s) already exist and would be overwritten.` : "No conflicts detected. Safe to create all files."
|
|
10831
|
+
warnings: conflicts.length ? [
|
|
10832
|
+
conflictHint
|
|
10833
|
+
] : [],
|
|
10834
|
+
...conflicts.length ? {} : {
|
|
10835
|
+
hint: "No conflicts detected. Safe to create all files."
|
|
10836
|
+
}
|
|
9876
10837
|
});
|
|
9877
10838
|
}
|
|
9878
10839
|
async function requestDeviceCode(args) {
|
|
@@ -9969,10 +10930,14 @@ async function pollDeviceToken(args) {
|
|
|
9969
10930
|
}
|
|
9970
10931
|
const FIRST_CALL_BUDGET_MS = 8000;
|
|
9971
10932
|
const RESUME_BUDGET_MS = 22000;
|
|
9972
|
-
|
|
9973
|
-
|
|
10933
|
+
const PENDING_TTL_NOTE = "Once authorized, the minted token lives at most 7 days (server-enforced); CI must re-mint before expiry (console: project settings -> Access tokens).";
|
|
10934
|
+
function login_fail(name, message, status, code) {
|
|
10935
|
+
return envelope({
|
|
9974
10936
|
ok: false,
|
|
10937
|
+
command: "extension_auth",
|
|
10938
|
+
status,
|
|
9975
10939
|
error: {
|
|
10940
|
+
code,
|
|
9976
10941
|
name,
|
|
9977
10942
|
message
|
|
9978
10943
|
}
|
|
@@ -9980,52 +10945,78 @@ function login_fail(name, message) {
|
|
|
9980
10945
|
}
|
|
9981
10946
|
function success(creds) {
|
|
9982
10947
|
const expiresAt = creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null;
|
|
9983
|
-
return
|
|
10948
|
+
return envelope({
|
|
9984
10949
|
ok: true,
|
|
10950
|
+
command: "extension_auth",
|
|
9985
10951
|
status: "logged-in",
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
10952
|
+
value: {
|
|
10953
|
+
workspaceSlug: creds.workspaceSlug,
|
|
10954
|
+
projectSlug: creds.projectSlug,
|
|
10955
|
+
expiresAt
|
|
10956
|
+
},
|
|
10957
|
+
hint: `Logged in to ${creds.workspaceSlug}/${creds.projectSlug}. extension_publish can now use the stored token. The token expires ${expiresAt ?? "within 7 days"}: extension.dev CLI tokens live at most 7 days, so CI must re-mint before then (console: project settings -> Access tokens).`,
|
|
10958
|
+
warnings: [
|
|
10959
|
+
tokenTtlNote(creds.workspaceSlug, creds.projectSlug)
|
|
10960
|
+
]
|
|
9991
10961
|
});
|
|
9992
10962
|
}
|
|
9993
10963
|
function login_pending(start) {
|
|
9994
10964
|
const complete = String(start.verificationUriComplete || "").trim();
|
|
9995
10965
|
const hasCompleteLink = complete.length > 0 && complete !== start.verificationUri;
|
|
9996
10966
|
const message = hasCompleteLink ? `Open ${complete} and approve (code ${start.userCode} is pre-filled), then call extension_auth (action: login) again with this deviceCode and the same project. If the page asks for a code, enter ${start.userCode} at ${start.verificationUri}.` : `Open ${start.verificationUri} and enter code ${start.userCode}, then call extension_auth (action: login) again with this deviceCode and the same project.`;
|
|
9997
|
-
return
|
|
10967
|
+
return envelope({
|
|
9998
10968
|
ok: false,
|
|
9999
|
-
|
|
10000
|
-
|
|
10001
|
-
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
}
|
|
10005
|
-
|
|
10006
|
-
|
|
10007
|
-
|
|
10969
|
+
command: "extension_auth",
|
|
10970
|
+
status: "authorization-pending",
|
|
10971
|
+
error: {
|
|
10972
|
+
code: "E_AUTH_PENDING",
|
|
10973
|
+
message
|
|
10974
|
+
},
|
|
10975
|
+
value: {
|
|
10976
|
+
userCode: start.userCode,
|
|
10977
|
+
verificationUri: start.verificationUri,
|
|
10978
|
+
...hasCompleteLink ? {
|
|
10979
|
+
verificationUriComplete: complete
|
|
10980
|
+
} : {},
|
|
10981
|
+
deviceCode: start.deviceCode,
|
|
10982
|
+
legacyStatus: "authorization_pending"
|
|
10983
|
+
},
|
|
10984
|
+
hint: message,
|
|
10985
|
+
warnings: [
|
|
10986
|
+
PENDING_TTL_NOTE
|
|
10987
|
+
]
|
|
10008
10988
|
});
|
|
10009
10989
|
}
|
|
10010
10990
|
function resumePending(deviceCode, verificationUri) {
|
|
10011
|
-
|
|
10991
|
+
const message = `Still waiting for authorization. The one-click link and code from the previous response are still valid: open that link (or enter the code at ${verificationUri}), then call extension_auth (action: login) again with this same deviceCode and the same project.`;
|
|
10992
|
+
return envelope({
|
|
10012
10993
|
ok: false,
|
|
10013
|
-
|
|
10014
|
-
|
|
10015
|
-
|
|
10016
|
-
|
|
10017
|
-
|
|
10994
|
+
command: "extension_auth",
|
|
10995
|
+
status: "authorization-pending",
|
|
10996
|
+
error: {
|
|
10997
|
+
code: "E_AUTH_PENDING",
|
|
10998
|
+
message
|
|
10999
|
+
},
|
|
11000
|
+
value: {
|
|
11001
|
+
verificationUri,
|
|
11002
|
+
deviceCode,
|
|
11003
|
+
legacyStatus: "authorization_pending"
|
|
11004
|
+
},
|
|
11005
|
+
hint: message,
|
|
11006
|
+
warnings: [
|
|
11007
|
+
PENDING_TTL_NOTE
|
|
11008
|
+
]
|
|
10018
11009
|
});
|
|
10019
11010
|
}
|
|
10020
11011
|
async function loginToProject(args) {
|
|
10021
11012
|
const project = String(args.project || "").trim();
|
|
10022
|
-
if (!/^[^/]+\/[^/]+$/.test(project)) return login_fail("BadRequest", "project must be in the form '<workspace>/<project>'.");
|
|
11013
|
+
if (!/^[^/]+\/[^/]+$/.test(project)) return login_fail("BadRequest", "project must be in the form '<workspace>/<project>'.", "bad-request", "E_BAD_REQUEST");
|
|
10023
11014
|
const apiBase = resolveApiBase(args.api);
|
|
10024
11015
|
let config;
|
|
10025
11016
|
try {
|
|
10026
11017
|
config = await fetchLoginConfig(apiBase);
|
|
10027
11018
|
} catch (err) {
|
|
10028
|
-
return login_fail("LoginConfigError", err?.message || "Could not load login config.");
|
|
11019
|
+
return login_fail("LoginConfigError", err?.message || "Could not load login config.", "login-failed", "E_AUTH_FAILED");
|
|
10029
11020
|
}
|
|
10030
11021
|
if (args.deviceCode) {
|
|
10031
11022
|
const poll = await pollDeviceToken({
|
|
@@ -10037,9 +11028,9 @@ async function loginToProject(args) {
|
|
|
10037
11028
|
budgetMs: RESUME_BUDGET_MS
|
|
10038
11029
|
});
|
|
10039
11030
|
if (poll.ok) return success(poll.creds);
|
|
10040
|
-
if ("expired" === poll.reason) return login_fail("LoginExpired", "The device code expired. Run extension_auth (action: login) again to restart.");
|
|
10041
|
-
if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied at extension.dev/device.");
|
|
10042
|
-
if ("error" === poll.reason) return login_fail("LoginError", poll.message || "Device login failed.");
|
|
11031
|
+
if ("expired" === poll.reason) return login_fail("LoginExpired", "The device code expired. Run extension_auth (action: login) again to restart.", "login-expired", "E_AUTH_EXPIRED");
|
|
11032
|
+
if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied at extension.dev/device.", "login-denied", "E_AUTH_DENIED");
|
|
11033
|
+
if ("error" === poll.reason) return login_fail("LoginError", poll.message || "Device login failed.", "login-failed", "E_AUTH_FAILED");
|
|
10043
11034
|
return resumePending(String(args.deviceCode), config.verificationUri);
|
|
10044
11035
|
}
|
|
10045
11036
|
let start;
|
|
@@ -10050,7 +11041,7 @@ async function loginToProject(args) {
|
|
|
10050
11041
|
project
|
|
10051
11042
|
});
|
|
10052
11043
|
} catch (err) {
|
|
10053
|
-
return login_fail("LoginStartError", err?.message || "Could not start the device flow.");
|
|
11044
|
+
return login_fail("LoginStartError", err?.message || "Could not start the device flow.", "login-failed", "E_AUTH_FAILED");
|
|
10054
11045
|
}
|
|
10055
11046
|
const poll = await pollDeviceToken({
|
|
10056
11047
|
apiBase,
|
|
@@ -10061,7 +11052,7 @@ async function loginToProject(args) {
|
|
|
10061
11052
|
budgetMs: FIRST_CALL_BUDGET_MS
|
|
10062
11053
|
});
|
|
10063
11054
|
if (poll.ok) return success(poll.creds);
|
|
10064
|
-
if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied at extension.dev/device.");
|
|
11055
|
+
if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied at extension.dev/device.", "login-denied", "E_AUTH_DENIED");
|
|
10065
11056
|
return login_pending({
|
|
10066
11057
|
deviceCode: start.deviceCode,
|
|
10067
11058
|
userCode: start.userCode,
|
|
@@ -10071,10 +11062,12 @@ async function loginToProject(args) {
|
|
|
10071
11062
|
}
|
|
10072
11063
|
async function readIdentity() {
|
|
10073
11064
|
const creds = readCredentials();
|
|
10074
|
-
if (!creds) return
|
|
11065
|
+
if (!creds) return envelope({
|
|
10075
11066
|
ok: true,
|
|
11067
|
+
command: "extension_auth",
|
|
10076
11068
|
status: "logged-out",
|
|
10077
|
-
|
|
11069
|
+
value: {},
|
|
11070
|
+
hint: "No stored credentials. Run extension_auth (action: login) to authenticate."
|
|
10078
11071
|
});
|
|
10079
11072
|
const now = Math.floor(Date.now() / 1000);
|
|
10080
11073
|
const expired = Boolean(creds.expiresAt && creds.expiresAt <= now);
|
|
@@ -10082,28 +11075,36 @@ async function readIdentity() {
|
|
|
10082
11075
|
const effectiveDefaultApi = resolveApiBase();
|
|
10083
11076
|
const apiDiverges = Boolean(recordedApi) && recordedApi !== effectiveDefaultApi;
|
|
10084
11077
|
const envTokenSet = Boolean(String(process.env.EXTENSION_DEV_TOKEN || "").trim());
|
|
10085
|
-
const
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
|
|
10089
|
-
|
|
11078
|
+
const identityNote = expired ? "The stored token has expired. Run extension_auth (action: login) to refresh it." : `Logged in as ${creds.workspaceSlug}/${creds.projectSlug}, per the token extension_auth stored on this machine. That token is what scopes the identity: it does not follow the current working directory or project folder.`;
|
|
11079
|
+
const apiDivergesNote = apiDiverges ? `This login was minted via ${recordedApi}, but authenticated tools do not read that recorded value: they target ${effectiveDefaultApi} unless given an api argument.` : null;
|
|
11080
|
+
const envTokenNote = envTokenSet ? "EXTENSION_DEV_TOKEN is set and takes precedence over this stored login for authenticated tools; this report describes only the stored login." : null;
|
|
11081
|
+
const message = [
|
|
11082
|
+
identityNote,
|
|
11083
|
+
apiDivergesNote,
|
|
11084
|
+
envTokenNote
|
|
11085
|
+
].filter(Boolean).join(" ");
|
|
11086
|
+
return envelope({
|
|
10090
11087
|
ok: true,
|
|
11088
|
+
command: "extension_auth",
|
|
10091
11089
|
status: expired ? "expired" : "logged-in",
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10103
|
-
|
|
10104
|
-
|
|
10105
|
-
|
|
10106
|
-
|
|
11090
|
+
value: {
|
|
11091
|
+
workspaceSlug: creds.workspaceSlug,
|
|
11092
|
+
projectSlug: creds.projectSlug,
|
|
11093
|
+
...recordedApi ? {
|
|
11094
|
+
apiRecordedAtLogin: recordedApi
|
|
11095
|
+
} : {},
|
|
11096
|
+
apiDefault: effectiveDefaultApi,
|
|
11097
|
+
provider: creds.provider ?? "extensiondev",
|
|
11098
|
+
expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
|
|
11099
|
+
expiresInSeconds: creds.expiresAt ? creds.expiresAt - now : null,
|
|
11100
|
+
expired
|
|
11101
|
+
},
|
|
11102
|
+
hint: message,
|
|
11103
|
+
warnings: [
|
|
11104
|
+
tokenTtlNote(creds.workspaceSlug, creds.projectSlug),
|
|
11105
|
+
apiDivergesNote,
|
|
11106
|
+
envTokenNote
|
|
11107
|
+
]
|
|
10107
11108
|
});
|
|
10108
11109
|
}
|
|
10109
11110
|
async function clearLocalCredentials() {
|
|
@@ -10113,18 +11114,20 @@ async function clearLocalCredentials() {
|
|
|
10113
11114
|
project: creds.projectSlug
|
|
10114
11115
|
}, "settings/access-tokens") : null;
|
|
10115
11116
|
const result = clearCredentials();
|
|
10116
|
-
return
|
|
11117
|
+
return envelope({
|
|
10117
11118
|
ok: true,
|
|
10118
|
-
|
|
10119
|
-
|
|
10120
|
-
|
|
10121
|
-
|
|
10122
|
-
|
|
11119
|
+
command: "extension_auth",
|
|
11120
|
+
status: result.cleared ? "logged-out" : "nothing-to-clear",
|
|
11121
|
+
value: {
|
|
11122
|
+
cleared: result.cleared,
|
|
11123
|
+
revokeUrl: result.cleared && revokeUrl ? revokeUrl : null
|
|
11124
|
+
},
|
|
11125
|
+
hint: result.cleared ? revokeUrl ? `Local credentials removed. The token stays valid server-side until it expires; revoke it now at ${revokeUrl} (takes about a minute to propagate).` : "Local credentials removed. The token stays valid server-side until it expires; revoke it from the project's access-tokens page if needed." : "No stored credentials to remove."
|
|
10123
11126
|
});
|
|
10124
11127
|
}
|
|
10125
11128
|
const auth_schema = {
|
|
10126
11129
|
name: "extension_auth",
|
|
10127
|
-
description: "Sign this machine in to extension.dev, report that login, or clear it. action:'status' (default)
|
|
11130
|
+
description: "Sign this machine in to extension.dev, report that login, or clear it. Pass action:'status' (the default) to name the workspace and project the stored token is scoped to and when it expires, never the token itself; that identity comes from the stored token alone, and does not change with the current working directory or whichever project folder you are in. Pass action:'login' for a two-phase flow: call with `project` to get a code plus a URL the user authorizes at extension.dev/device, then call again with the returned `deviceCode`. GitHub federation happens server-side, so no GitHub token lands on this machine. Minted tokens live at most 7 days, server-enforced, so CI must re-mint before expiry on the console's project settings, Access tokens page. Pass action:'logout' to delete the local credentials only; the token stays valid server-side until it is revoked at the URL the response returns.",
|
|
10128
11131
|
inputSchema: {
|
|
10129
11132
|
type: "object",
|
|
10130
11133
|
properties: {
|
|
@@ -10444,12 +11447,17 @@ async function detectBrowsers(browsers) {
|
|
|
10444
11447
|
}
|
|
10445
11448
|
const available = detected.filter((d)=>"not_found" !== d.source);
|
|
10446
11449
|
const missing = detected.filter((d)=>"not_found" === d.source);
|
|
10447
|
-
return
|
|
10448
|
-
|
|
10449
|
-
|
|
10450
|
-
|
|
10451
|
-
|
|
10452
|
-
|
|
11450
|
+
return envelope({
|
|
11451
|
+
ok: true,
|
|
11452
|
+
command: "extension_browsers",
|
|
11453
|
+
status: "detected",
|
|
11454
|
+
value: {
|
|
11455
|
+
detected,
|
|
11456
|
+
managed,
|
|
11457
|
+
summary: {
|
|
11458
|
+
available: available.map((d)=>d.browser),
|
|
11459
|
+
missing: missing.map((d)=>d.browser)
|
|
11460
|
+
}
|
|
10453
11461
|
},
|
|
10454
11462
|
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(", ")}.` : ""}` : "All requested browsers are available."
|
|
10455
11463
|
});
|
|
@@ -10496,11 +11504,16 @@ async function listManagedBrowsers() {
|
|
|
10496
11504
|
});
|
|
10497
11505
|
}
|
|
10498
11506
|
}
|
|
10499
|
-
return
|
|
10500
|
-
|
|
10501
|
-
|
|
10502
|
-
|
|
10503
|
-
|
|
11507
|
+
return envelope({
|
|
11508
|
+
ok: true,
|
|
11509
|
+
command: "extension_browsers",
|
|
11510
|
+
status: "listed",
|
|
11511
|
+
value: {
|
|
11512
|
+
cacheRoot,
|
|
11513
|
+
cacheExists: existsSync(cacheRoot),
|
|
11514
|
+
installed,
|
|
11515
|
+
availableToInstall: BROWSER_NAMES.filter((b)=>!installed.some((i)=>i.browser === b))
|
|
11516
|
+
},
|
|
10504
11517
|
hint: 0 === installed.length ? "No managed browsers found. Use extension_browsers with action: \"install\" to install one, or use a system-installed browser." : `${installed.length} managed browser(s) found. Use extension_browsers with action: "detect" for a full system scan.`
|
|
10505
11518
|
});
|
|
10506
11519
|
}
|
|
@@ -10510,51 +11523,78 @@ async function installManagedBrowser(browser) {
|
|
|
10510
11523
|
await extensionInstall({
|
|
10511
11524
|
browser
|
|
10512
11525
|
});
|
|
10513
|
-
return
|
|
11526
|
+
return envelope({
|
|
11527
|
+
ok: true,
|
|
11528
|
+
command: "extension_browsers",
|
|
10514
11529
|
status: "installed",
|
|
10515
|
-
|
|
10516
|
-
|
|
11530
|
+
value: {
|
|
11531
|
+
browser,
|
|
11532
|
+
duration: Date.now() - start
|
|
11533
|
+
},
|
|
10517
11534
|
hint: `Browser "${browser}" is now available. Use extension_dev or extension_start with browser: "${browser}".`
|
|
10518
11535
|
});
|
|
10519
11536
|
} catch (err) {
|
|
10520
|
-
return
|
|
10521
|
-
|
|
10522
|
-
|
|
10523
|
-
|
|
10524
|
-
|
|
11537
|
+
return envelope({
|
|
11538
|
+
ok: false,
|
|
11539
|
+
command: "extension_browsers",
|
|
11540
|
+
status: "install-failed",
|
|
11541
|
+
value: {
|
|
11542
|
+
browser,
|
|
11543
|
+
duration: Date.now() - start
|
|
11544
|
+
},
|
|
11545
|
+
error: {
|
|
11546
|
+
code: "E_BROWSER_INSTALL",
|
|
11547
|
+
message: err instanceof Error ? err.message : String(err)
|
|
11548
|
+
},
|
|
10525
11549
|
hint: "edge" === browser ? "Edge installation on Linux may require elevated privileges. Try using Chrome or Chromium instead." : "Check network connectivity and disk space. You can also install browsers manually."
|
|
10526
11550
|
});
|
|
10527
11551
|
}
|
|
10528
11552
|
}
|
|
10529
11553
|
async function uninstallManagedBrowser(args) {
|
|
10530
11554
|
const start = Date.now();
|
|
10531
|
-
if (!args.browser && !args.all) return
|
|
10532
|
-
|
|
10533
|
-
|
|
11555
|
+
if (!args.browser && !args.all) return envelope({
|
|
11556
|
+
ok: false,
|
|
11557
|
+
command: "extension_browsers",
|
|
11558
|
+
status: "bad-request",
|
|
11559
|
+
error: {
|
|
11560
|
+
code: "E_BAD_REQUEST",
|
|
11561
|
+
message: "Provide a browser to remove, or set all: true."
|
|
11562
|
+
}
|
|
10534
11563
|
});
|
|
10535
11564
|
try {
|
|
10536
11565
|
await extensionUninstall({
|
|
10537
11566
|
browser: args.browser,
|
|
10538
11567
|
all: args.all
|
|
10539
11568
|
});
|
|
10540
|
-
return
|
|
11569
|
+
return envelope({
|
|
11570
|
+
ok: true,
|
|
11571
|
+
command: "extension_browsers",
|
|
10541
11572
|
status: "uninstalled",
|
|
10542
|
-
|
|
10543
|
-
|
|
11573
|
+
value: {
|
|
11574
|
+
target: args.all ? "all" : args.browser,
|
|
11575
|
+
duration: Date.now() - start
|
|
11576
|
+
},
|
|
10544
11577
|
hint: 'Use extension_browsers with action: "list" to confirm what remains in the managed cache.'
|
|
10545
11578
|
});
|
|
10546
11579
|
} catch (err) {
|
|
10547
|
-
return
|
|
10548
|
-
|
|
10549
|
-
|
|
10550
|
-
|
|
10551
|
-
|
|
11580
|
+
return envelope({
|
|
11581
|
+
ok: false,
|
|
11582
|
+
command: "extension_browsers",
|
|
11583
|
+
status: "uninstall-failed",
|
|
11584
|
+
value: {
|
|
11585
|
+
target: args.all ? "all" : args.browser,
|
|
11586
|
+
duration: Date.now() - start
|
|
11587
|
+
},
|
|
11588
|
+
error: {
|
|
11589
|
+
code: "E_BROWSER_UNINSTALL",
|
|
11590
|
+
message: err instanceof Error ? err.message : String(err)
|
|
11591
|
+
}
|
|
10552
11592
|
});
|
|
10553
11593
|
}
|
|
10554
11594
|
}
|
|
10555
11595
|
const browsers_schema = {
|
|
10556
11596
|
name: "extension_browsers",
|
|
10557
|
-
description: "Find, install
|
|
11597
|
+
description: "Find, install and remove the browsers Extension.js tooling can launch. Pass action:'detect' (the default) to scan both system-installed and managed browsers, and report each one's binary path, version, engine and debugger support. Pass action:'list' for the managed cache this tool downloads into, with sizes on disk. Pass action:'install' to download a managed binary: 580 to 625 MB in one blocking call, so allow a generous client timeout. Pass action:'uninstall' to remove managed binaries; it never touches a system install.",
|
|
10558
11598
|
inputSchema: {
|
|
10559
11599
|
type: "object",
|
|
10560
11600
|
properties: {
|
|
@@ -10594,10 +11634,14 @@ async function browsers_handler(args) {
|
|
|
10594
11634
|
const action = args.action ?? "detect";
|
|
10595
11635
|
if ("list" === action) return listManagedBrowsers();
|
|
10596
11636
|
if ("install" === action) {
|
|
10597
|
-
if (!args.browser) return
|
|
11637
|
+
if (!args.browser) return envelope({
|
|
10598
11638
|
ok: false,
|
|
10599
|
-
|
|
10600
|
-
|
|
11639
|
+
command: "extension_browsers",
|
|
11640
|
+
status: "bad-request",
|
|
11641
|
+
error: {
|
|
11642
|
+
code: "E_BAD_REQUEST",
|
|
11643
|
+
message: "action 'install' needs a browser: one of chrome, chromium, edge, firefox."
|
|
11644
|
+
}
|
|
10601
11645
|
});
|
|
10602
11646
|
return installManagedBrowser(args.browser);
|
|
10603
11647
|
}
|
|
@@ -10759,11 +11803,16 @@ function describeToolArgs(inputSchema) {
|
|
|
10759
11803
|
};
|
|
10760
11804
|
}
|
|
10761
11805
|
function inputValidationError(toolName, issues, inputSchema) {
|
|
10762
|
-
return
|
|
11806
|
+
return envelope({
|
|
10763
11807
|
ok: false,
|
|
11808
|
+
command: toolName,
|
|
11809
|
+
status: "invalid-arguments",
|
|
10764
11810
|
error: {
|
|
11811
|
+
code: "E_INPUT_VALIDATION",
|
|
10765
11812
|
name: "InputValidationError",
|
|
10766
|
-
message: `Invalid arguments for ${toolName}: ${issues.map((i)=>`${i.path}: ${i.message}`).join("; ")}
|
|
11813
|
+
message: `Invalid arguments for ${toolName}: ${issues.map((i)=>`${i.path}: ${i.message}`).join("; ")}`
|
|
11814
|
+
},
|
|
11815
|
+
value: {
|
|
10767
11816
|
issues,
|
|
10768
11817
|
...inputSchema ? {
|
|
10769
11818
|
args: describeToolArgs(inputSchema)
|
|
@@ -10826,9 +11875,17 @@ async function startServer() {
|
|
|
10826
11875
|
content: [
|
|
10827
11876
|
{
|
|
10828
11877
|
type: "text",
|
|
10829
|
-
text:
|
|
10830
|
-
|
|
10831
|
-
|
|
11878
|
+
text: envelope({
|
|
11879
|
+
ok: false,
|
|
11880
|
+
command: name,
|
|
11881
|
+
status: "unknown-tool",
|
|
11882
|
+
error: {
|
|
11883
|
+
code: "E_UNKNOWN_TOOL",
|
|
11884
|
+
message: `Unknown tool: ${name}`
|
|
11885
|
+
},
|
|
11886
|
+
value: {
|
|
11887
|
+
availableTools: tools.map((t)=>t.schema.name)
|
|
11888
|
+
}
|
|
10832
11889
|
})
|
|
10833
11890
|
}
|
|
10834
11891
|
],
|
|
@@ -10860,9 +11917,15 @@ async function startServer() {
|
|
|
10860
11917
|
content: [
|
|
10861
11918
|
{
|
|
10862
11919
|
type: "text",
|
|
10863
|
-
text:
|
|
10864
|
-
|
|
10865
|
-
|
|
11920
|
+
text: envelope({
|
|
11921
|
+
ok: false,
|
|
11922
|
+
command: name,
|
|
11923
|
+
status: "internal-error",
|
|
11924
|
+
error: {
|
|
11925
|
+
code: "E_INTERNAL",
|
|
11926
|
+
name: err instanceof Error ? err.name : "Error",
|
|
11927
|
+
message: err instanceof Error ? err.message : String(err)
|
|
11928
|
+
}
|
|
10866
11929
|
})
|
|
10867
11930
|
}
|
|
10868
11931
|
],
|