@extension.dev/mcp 5.6.0 → 5.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +79 -0
- package/README.md +11 -1
- package/dist/module.js +339 -67
- package/dist/src/lib/carrier.d.ts +71 -0
- package/dist/src/tools/dev.d.ts +6 -0
- package/dist/src/tools/stop.d.ts +2 -0
- package/extensions/live-preview/chromium/action/index.css +2 -0
- package/extensions/live-preview/chromium/action/index.html +16 -0
- package/extensions/live-preview/chromium/action/index.js +1 -0
- package/extensions/live-preview/chromium/background/service_worker.js +169 -0
- package/extensions/live-preview/chromium/images/icon.png +0 -0
- package/extensions/live-preview/chromium/manifest.json +66 -0
- package/extensions/live-preview/chromium/sandbox/page-0.html +28 -0
- package/package.json +6 -5
- package/server.json +2 -2
package/dist/module.js
CHANGED
|
@@ -7,7 +7,8 @@ import node_path, { join } from "node:path";
|
|
|
7
7
|
import { extensionCreate } from "extension-create";
|
|
8
8
|
import node_os from "node:os";
|
|
9
9
|
import cross_spawn from "cross-spawn";
|
|
10
|
-
import node_crypto from "node:crypto";
|
|
10
|
+
import node_crypto, { createHash } from "node:crypto";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
11
12
|
import { execFile, execFileSync } from "node:child_process";
|
|
12
13
|
import { filterKeysForThisBrowser } from "browser-extension-manifest-fields";
|
|
13
14
|
import ws_0 from "ws";
|
|
@@ -220,7 +221,7 @@ __webpack_require__.d(whoami_namespaceObject, {
|
|
|
220
221
|
handler: ()=>whoami_handler,
|
|
221
222
|
schema: ()=>whoami_schema
|
|
222
223
|
});
|
|
223
|
-
var package_namespaceObject = JSON.parse('{"rE":"5.
|
|
224
|
+
var package_namespaceObject = JSON.parse('{"rE":"5.7.0","El":{"OP":"4.0.16-canary.1784889479.74e12044"}}');
|
|
224
225
|
function scaffoldEnginePin(projectPath) {
|
|
225
226
|
try {
|
|
226
227
|
const pkg = JSON.parse(node_fs.readFileSync(node_path.join(projectPath, "package.json"), "utf8"));
|
|
@@ -854,6 +855,178 @@ function resolveSessionBrowser(projectPath, explicit, fallback = "chrome") {
|
|
|
854
855
|
source: "fallback"
|
|
855
856
|
};
|
|
856
857
|
}
|
|
858
|
+
const CHROMIUM_FAMILY = new Set([
|
|
859
|
+
"chrome",
|
|
860
|
+
"chromium",
|
|
861
|
+
"edge",
|
|
862
|
+
"brave",
|
|
863
|
+
"opera",
|
|
864
|
+
"vivaldi",
|
|
865
|
+
"yandex",
|
|
866
|
+
"chromium-based"
|
|
867
|
+
]);
|
|
868
|
+
const GECKO_FAMILY = new Set([
|
|
869
|
+
"firefox",
|
|
870
|
+
"waterfox",
|
|
871
|
+
"librewolf",
|
|
872
|
+
"gecko-based",
|
|
873
|
+
"firefox-based"
|
|
874
|
+
]);
|
|
875
|
+
const WEBKIT_FAMILY = new Set([
|
|
876
|
+
"safari",
|
|
877
|
+
"webkit-based"
|
|
878
|
+
]);
|
|
879
|
+
function isChromiumFamily(browser) {
|
|
880
|
+
return CHROMIUM_FAMILY.has(browser);
|
|
881
|
+
}
|
|
882
|
+
function isGeckoFamily(browser) {
|
|
883
|
+
return GECKO_FAMILY.has(browser);
|
|
884
|
+
}
|
|
885
|
+
const CARRIER_DIR_NAME = "extension-dev-live-preview";
|
|
886
|
+
const MARKER_FILE = "managed-by-extension-dev-mcp.json";
|
|
887
|
+
function deriveCarrierId(source) {
|
|
888
|
+
try {
|
|
889
|
+
const manifest = JSON.parse(node_fs.readFileSync(node_path.join(source, "manifest.json"), "utf-8"));
|
|
890
|
+
if (!manifest.key) return null;
|
|
891
|
+
const hash = createHash("sha256").update(Buffer.from(manifest.key, "base64")).digest();
|
|
892
|
+
return [
|
|
893
|
+
...hash.subarray(0, 16)
|
|
894
|
+
].map((byte)=>String.fromCharCode(97 + (byte >> 4)) + String.fromCharCode(97 + (15 & byte))).join("");
|
|
895
|
+
} catch {
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
function carrierPath(projectPath) {
|
|
900
|
+
return node_path.join(projectPath, "extensions", CARRIER_DIR_NAME);
|
|
901
|
+
}
|
|
902
|
+
function removeCarrier(projectPath) {
|
|
903
|
+
const target = carrierPath(projectPath);
|
|
904
|
+
if (!node_fs.existsSync(target)) return {
|
|
905
|
+
removed: false,
|
|
906
|
+
path: target
|
|
907
|
+
};
|
|
908
|
+
if (!node_fs.existsSync(node_path.join(target, MARKER_FILE))) return {
|
|
909
|
+
removed: false,
|
|
910
|
+
path: target,
|
|
911
|
+
note: `extensions/${CARRIER_DIR_NAME} has no ${MARKER_FILE} marker, so it is not the carrier this tool placed and was left untouched.`
|
|
912
|
+
};
|
|
913
|
+
try {
|
|
914
|
+
node_fs.rmSync(target, {
|
|
915
|
+
recursive: true,
|
|
916
|
+
force: true
|
|
917
|
+
});
|
|
918
|
+
} catch (error) {
|
|
919
|
+
return {
|
|
920
|
+
removed: false,
|
|
921
|
+
path: target,
|
|
922
|
+
note: `Could not remove the carrier: ${error instanceof Error ? error.message : String(error)}`
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
const parent = node_path.join(projectPath, "extensions");
|
|
926
|
+
try {
|
|
927
|
+
if (0 === node_fs.readdirSync(parent).length) node_fs.rmdirSync(parent);
|
|
928
|
+
} catch {}
|
|
929
|
+
return {
|
|
930
|
+
removed: true,
|
|
931
|
+
path: target
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
function ensureCarrierIgnored(projectPath) {
|
|
935
|
+
if (!node_fs.existsSync(node_path.join(projectPath, ".git"))) return null;
|
|
936
|
+
const entry = `extensions/${CARRIER_DIR_NAME}/`;
|
|
937
|
+
const file = node_path.join(projectPath, ".gitignore");
|
|
938
|
+
let current = "";
|
|
939
|
+
try {
|
|
940
|
+
current = node_fs.readFileSync(file, "utf-8");
|
|
941
|
+
} catch {}
|
|
942
|
+
const ignored = current.split("\n").map((line)=>line.trim()).some((line)=>line === entry || line === `extensions/${CARRIER_DIR_NAME}` || "extensions/" === line || "extensions" === line);
|
|
943
|
+
if (ignored) return null;
|
|
944
|
+
try {
|
|
945
|
+
const prefix = "" === current || current.endsWith("\n") ? "" : "\n";
|
|
946
|
+
node_fs.appendFileSync(file, `${prefix}\n# Extension.dev live-preview carrier: a local debug companion, not part of your extension.\n${entry}\n`);
|
|
947
|
+
return entry;
|
|
948
|
+
} catch {
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
function findBundledCarrier(engine) {
|
|
953
|
+
let dir = node_path.dirname(fileURLToPath(import.meta.url));
|
|
954
|
+
for(let depth = 0; depth < 6; depth++){
|
|
955
|
+
const candidate = node_path.join(dir, "extensions", "live-preview", engine);
|
|
956
|
+
if (node_fs.existsSync(node_path.join(candidate, "manifest.json"))) return candidate;
|
|
957
|
+
const parent = node_path.dirname(dir);
|
|
958
|
+
if (parent === dir) break;
|
|
959
|
+
dir = parent;
|
|
960
|
+
}
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
function materializeCarrier(projectPath, browser) {
|
|
964
|
+
if (!isChromiumFamily(browser)) return {
|
|
965
|
+
loaded: false,
|
|
966
|
+
note: `The live-preview carrier is Chromium-family only for now (requested: ${browser}). Firefox has no externally_connectable channel for web pages, so the carrier pairing cannot work there.`
|
|
967
|
+
};
|
|
968
|
+
const source = findBundledCarrier("chromium");
|
|
969
|
+
if (!source) return {
|
|
970
|
+
loaded: false,
|
|
971
|
+
note: "This install ships no bundled carrier payload (extensions/live-preview/chromium missing from the package)."
|
|
972
|
+
};
|
|
973
|
+
const target = carrierPath(projectPath);
|
|
974
|
+
const marker = node_path.join(target, MARKER_FILE);
|
|
975
|
+
if (node_fs.existsSync(target) && !node_fs.existsSync(marker)) return {
|
|
976
|
+
loaded: false,
|
|
977
|
+
path: target,
|
|
978
|
+
note: `A directory already exists at extensions/${CARRIER_DIR_NAME} without the ${MARKER_FILE} marker, so it is not managed by this tool and was left untouched. Remove or rename it to let extension_dev place the carrier there.`
|
|
979
|
+
};
|
|
980
|
+
const carrierId = deriveCarrierId(source);
|
|
981
|
+
try {
|
|
982
|
+
node_fs.rmSync(target, {
|
|
983
|
+
recursive: true,
|
|
984
|
+
force: true
|
|
985
|
+
});
|
|
986
|
+
node_fs.cpSync(source, target, {
|
|
987
|
+
recursive: true
|
|
988
|
+
});
|
|
989
|
+
const manifest = JSON.parse(node_fs.readFileSync(node_path.join(source, "manifest.json"), "utf-8"));
|
|
990
|
+
node_fs.writeFileSync(marker, `${JSON.stringify({
|
|
991
|
+
managedBy: "@extension.dev/mcp",
|
|
992
|
+
carrierVersion: manifest.version ?? "unknown",
|
|
993
|
+
note: "Safe to delete; extension_dev recreates it when carrier: true. extension_stop and extension_build remove it for you."
|
|
994
|
+
}, null, 2)}\n`);
|
|
995
|
+
const ignored = ensureCarrierIgnored(projectPath);
|
|
996
|
+
return {
|
|
997
|
+
loaded: true,
|
|
998
|
+
path: target,
|
|
999
|
+
...ignored ? {
|
|
1000
|
+
gitignored: ignored
|
|
1001
|
+
} : {},
|
|
1002
|
+
note: "Live-preview carrier placed in ./extensions; Extension.js loads it as a companion beside your extension. Open https://inspect.extension.dev/?session=live in the dev browser (any http://localhost origin works too) to watch the session's real-lane chrome.* trace on the Trace tab. It is a debug companion, never part of a release: extension_stop and extension_build remove it again" + (ignored ? `, and ${ignored} was added to .gitignore.` : "."),
|
|
1003
|
+
limitations: [
|
|
1004
|
+
"The trace shows calls a PAGE bridges to the carrier. Your extension's own chrome.* calls run directly in its contexts and never cross the carrier, so they do not appear.",
|
|
1005
|
+
"Bridged calls run under the CARRIER's identity, not your extension's. The preview assumes a single active guest and does not namespace per-extension state, so storage, action/badge state, messaging delivery, offscreen documents and relative script paths belong to the carrier. Rows affected are badged carrier-scoped in the Trace tab.",
|
|
1006
|
+
"Chromium-family only: Firefox has no externally_connectable channel for web pages."
|
|
1007
|
+
],
|
|
1008
|
+
...carrierId ? {
|
|
1009
|
+
bridgeProtocol: {
|
|
1010
|
+
carrierExtensionId: carrierId,
|
|
1011
|
+
allowedOrigins: "https://inspect.extension.dev, https://intelligence.extension.dev, https://themes.extension.dev, http://localhost/*, http://127.0.0.1/*",
|
|
1012
|
+
howTo: "From a page on an allowed origin, register your guest once with a 'session' message (it declares the permissions the carrier enforces), then send 'bridge' messages to run chrome.* for real; each one streams into the Trace tab. Use the EXACT dotted wire names the bridge dispatcher accepts: storage is storage.get/set/remove/clear with the AREA AS AN ARGUMENT, NOT storage.local.get.",
|
|
1013
|
+
example: [
|
|
1014
|
+
`const id = '${carrierId}'`,
|
|
1015
|
+
"const send = (msg) => new Promise((r) => chrome.runtime.sendMessage(id, msg, r))",
|
|
1016
|
+
"await send({ type: 'extensiondev:session', extensionId: 'my-guest', permissions: ['storage'] })",
|
|
1017
|
+
"await send({ type: 'extensiondev:bridge', request: { type: 'EXTENSION_BRIDGE_REQUEST', extensionId: 'my-guest', requestId: 'r1', api: 'storage.get', args: [null, 'local'] } })"
|
|
1018
|
+
].join("\n")
|
|
1019
|
+
}
|
|
1020
|
+
} : {}
|
|
1021
|
+
};
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
return {
|
|
1024
|
+
loaded: false,
|
|
1025
|
+
path: target,
|
|
1026
|
+
note: `Could not place the carrier: ${error instanceof Error ? error.message : String(error)}`
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
857
1030
|
function readBuildSummary(projectPath, browser, since) {
|
|
858
1031
|
const file = node_path.resolve(projectPath, "dist", "extension-js", browser, "build-summary.json");
|
|
859
1032
|
try {
|
|
@@ -1052,6 +1225,27 @@ function manifestDivergence(projectPath, browser) {
|
|
|
1052
1225
|
if (Array.isArray(sourceWar) && sourceWar.length && !Array.isArray(builtWar)) notes.push("The built manifest has no web_accessible_resources although the source declares them. Anything injected into a page (scripting.insertCSS targets, injected scripts, images) will be blocked at runtime.");
|
|
1053
1226
|
return notes;
|
|
1054
1227
|
}
|
|
1228
|
+
const MARKER_FILE_NAME = "managed-by-extension-dev-mcp.json";
|
|
1229
|
+
function carrierEntriesInDist(distDir, depth = 0) {
|
|
1230
|
+
if (depth > 3) return [];
|
|
1231
|
+
let entries;
|
|
1232
|
+
try {
|
|
1233
|
+
entries = node_fs.readdirSync(distDir, {
|
|
1234
|
+
withFileTypes: true
|
|
1235
|
+
});
|
|
1236
|
+
} catch {
|
|
1237
|
+
return [];
|
|
1238
|
+
}
|
|
1239
|
+
const found = [];
|
|
1240
|
+
for (const entry of entries){
|
|
1241
|
+
if (entry.name === CARRIER_DIR_NAME || entry.name === MARKER_FILE_NAME) {
|
|
1242
|
+
found.push(node_path.join(distDir, entry.name));
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
if (entry.isDirectory()) found.push(...carrierEntriesInDist(node_path.join(distDir, entry.name), depth + 1));
|
|
1246
|
+
}
|
|
1247
|
+
return found;
|
|
1248
|
+
}
|
|
1055
1249
|
async function validationPreflight(projectPath, browser) {
|
|
1056
1250
|
try {
|
|
1057
1251
|
const manifestValidate = await Promise.resolve().then(()=>({
|
|
@@ -1076,6 +1270,10 @@ async function validationPreflight(projectPath, browser) {
|
|
|
1076
1270
|
async function build_handler(args) {
|
|
1077
1271
|
const start = Date.now();
|
|
1078
1272
|
const browser = args.browser ?? "chrome";
|
|
1273
|
+
const carrierCleanup = removeCarrier(args.projectPath);
|
|
1274
|
+
const carrierNotes = [];
|
|
1275
|
+
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.");
|
|
1276
|
+
else if (carrierCleanup.note) carrierNotes.push(carrierCleanup.note);
|
|
1079
1277
|
const preflight = args.skipValidation ? null : await validationPreflight(args.projectPath, browser);
|
|
1080
1278
|
if (preflight?.buildBlocking) return JSON.stringify({
|
|
1081
1279
|
success: false,
|
|
@@ -1083,12 +1281,15 @@ async function build_handler(args) {
|
|
|
1083
1281
|
browser,
|
|
1084
1282
|
error: "Build refused: the manifest has errors that produce a broken extension even when the bundler succeeds.",
|
|
1085
1283
|
errors: preflight.errors,
|
|
1086
|
-
warnings:
|
|
1284
|
+
warnings: [
|
|
1285
|
+
...carrierNotes,
|
|
1286
|
+
...preflight.warnings
|
|
1287
|
+
],
|
|
1087
1288
|
duration: Date.now() - start,
|
|
1088
1289
|
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."
|
|
1089
1290
|
});
|
|
1090
1291
|
const clobberedSessions = liveProjectSessions(args.projectPath).filter((session)=>session.browser === browser);
|
|
1091
|
-
const warnings = clobberedSessions.map((session)=>`A live dev session (pid ${session.pid}) is running on this project for ${browser}, and this build wrote over its dist/${browser} output. The dev browser may now serve the production artifact instead of the dev build until the next recompile. Run extension_stop, or let dev recompile on the next source change, to resolve it.`);
|
|
1292
|
+
const warnings = carrierNotes.concat(clobberedSessions.map((session)=>`A live dev session (pid ${session.pid}) is running on this project for ${browser}, and this build wrote over its dist/${browser} output. The dev browser may now serve the production artifact instead of the dev build until the next recompile. Run extension_stop, or let dev recompile on the next source change, to resolve it.`));
|
|
1092
1293
|
const cliArgs = [
|
|
1093
1294
|
"build",
|
|
1094
1295
|
args.projectPath,
|
|
@@ -1118,7 +1319,21 @@ async function build_handler(args) {
|
|
|
1118
1319
|
buildWarningsTruncated: engineSummary.warnings_count
|
|
1119
1320
|
} : {}
|
|
1120
1321
|
} : {};
|
|
1121
|
-
const
|
|
1322
|
+
const distDir = node_path.resolve(args.projectPath, "dist", browser);
|
|
1323
|
+
const entrypoints = builtEntrypoints(distDir);
|
|
1324
|
+
const contamination = carrierEntriesInDist(distDir);
|
|
1325
|
+
if (contamination.length) return JSON.stringify({
|
|
1326
|
+
success: false,
|
|
1327
|
+
status: "contaminated",
|
|
1328
|
+
browser,
|
|
1329
|
+
buildExitCode: 0,
|
|
1330
|
+
error: `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.`,
|
|
1331
|
+
...warnings.length ? {
|
|
1332
|
+
warnings
|
|
1333
|
+
} : {},
|
|
1334
|
+
duration,
|
|
1335
|
+
hint: "Delete the listed paths from dist and build again. The carrier normally lives in ./extensions and is removed before every build."
|
|
1336
|
+
});
|
|
1122
1337
|
const missing = entrypoints.filter((e)=>!e.present);
|
|
1123
1338
|
if (missing.length) return JSON.stringify({
|
|
1124
1339
|
success: false,
|
|
@@ -1198,7 +1413,7 @@ async function build_handler(args) {
|
|
|
1198
1413
|
}
|
|
1199
1414
|
const stop_schema = {
|
|
1200
1415
|
name: "extension_stop",
|
|
1201
|
-
description: "Stop a running dev, start, or preview session: terminates the dev server and the browser it launched. Counterpart to extension_dev/extension_start. Call it when you are done verifying so sessions do not accumulate.",
|
|
1416
|
+
description: "Stop a running dev, start, or preview session: terminates the dev server and the browser it launched, and removes the live-preview carrier if extension_dev placed one. Counterpart to extension_dev/extension_start. Call it when you are done verifying so sessions do not accumulate.",
|
|
1202
1417
|
inputSchema: {
|
|
1203
1418
|
type: "object",
|
|
1204
1419
|
properties: {
|
|
@@ -1219,6 +1434,12 @@ const stop_schema = {
|
|
|
1219
1434
|
required: []
|
|
1220
1435
|
}
|
|
1221
1436
|
};
|
|
1437
|
+
function cleanCarrier(projectPath) {
|
|
1438
|
+
const removal = removeCarrier(projectPath);
|
|
1439
|
+
return removal.removed ? {
|
|
1440
|
+
carrierRemoved: removal.path
|
|
1441
|
+
} : {};
|
|
1442
|
+
}
|
|
1222
1443
|
function pgrepPids(pattern) {
|
|
1223
1444
|
try {
|
|
1224
1445
|
const out = execFileSync("pgrep", [
|
|
@@ -1295,6 +1516,7 @@ async function stopOne(projectPath, browser) {
|
|
|
1295
1516
|
pid: null,
|
|
1296
1517
|
stopped: 0 !== reaped.length,
|
|
1297
1518
|
reaped,
|
|
1519
|
+
...cleanCarrier(projectPath),
|
|
1298
1520
|
detail: 0 === reaped.length ? "No known session for this project/browser (nothing registered in this server and no ready.json contract found)." : `No dev pid on record, but reaped ${reaped.length} orphaned browser process(es) from the profile dir.`
|
|
1299
1521
|
};
|
|
1300
1522
|
}
|
|
@@ -1326,7 +1548,8 @@ async function stopOne(projectPath, browser) {
|
|
|
1326
1548
|
pid,
|
|
1327
1549
|
stopped,
|
|
1328
1550
|
reaped,
|
|
1329
|
-
detail
|
|
1551
|
+
detail,
|
|
1552
|
+
...cleanCarrier(projectPath)
|
|
1330
1553
|
};
|
|
1331
1554
|
}
|
|
1332
1555
|
async function stop_handler(args) {
|
|
@@ -1458,6 +1681,11 @@ const dev_schema = {
|
|
|
1458
1681
|
type: "boolean",
|
|
1459
1682
|
default: false,
|
|
1460
1683
|
description: "Enable extension_eval (runs code in a context; writes a 0600 session token). Implies allowControl, so a single allowEval: true also unlocks storage/reload/open/dom_inspect. You do not need to pass both."
|
|
1684
|
+
},
|
|
1685
|
+
carrier: {
|
|
1686
|
+
type: "boolean",
|
|
1687
|
+
default: false,
|
|
1688
|
+
description: "Load the bundled Extension.dev Live Preview carrier beside your extension (Chromium-family browsers only). It is placed in the project's ./extensions folder, which Extension.js auto-loads; allowlisted pages (inspect.extension.dev, localhost) can then pair with the session and stream its real-lane chrome.* trace. Writes extensions/extension-dev-live-preview/ into the project, gitignores it, and takes it back out on extension_stop or extension_build: it is a debug companion, never part of a release."
|
|
1461
1689
|
}
|
|
1462
1690
|
},
|
|
1463
1691
|
required: [
|
|
@@ -1492,6 +1720,7 @@ async function dev_handler(args) {
|
|
|
1492
1720
|
});
|
|
1493
1721
|
}
|
|
1494
1722
|
}
|
|
1723
|
+
const carrier = args.carrier ? materializeCarrier(args.projectPath, browser) : null;
|
|
1495
1724
|
const allowControl = Boolean(args.allowControl || args.allowEval);
|
|
1496
1725
|
const cliArgs = [
|
|
1497
1726
|
"dev",
|
|
@@ -1600,6 +1829,9 @@ async function dev_handler(args) {
|
|
|
1600
1829
|
...portReport,
|
|
1601
1830
|
projectPath: args.projectPath,
|
|
1602
1831
|
status: "started",
|
|
1832
|
+
...carrier ? {
|
|
1833
|
+
carrier
|
|
1834
|
+
} : {},
|
|
1603
1835
|
...replaced.length > 0 ? {
|
|
1604
1836
|
replacedSession: replaced[0],
|
|
1605
1837
|
...replaced.length > 1 ? {
|
|
@@ -1925,33 +2157,6 @@ async function get_template_source_handler(args) {
|
|
|
1925
2157
|
} : {}
|
|
1926
2158
|
});
|
|
1927
2159
|
}
|
|
1928
|
-
const CHROMIUM_FAMILY = new Set([
|
|
1929
|
-
"chrome",
|
|
1930
|
-
"chromium",
|
|
1931
|
-
"edge",
|
|
1932
|
-
"brave",
|
|
1933
|
-
"opera",
|
|
1934
|
-
"vivaldi",
|
|
1935
|
-
"yandex",
|
|
1936
|
-
"chromium-based"
|
|
1937
|
-
]);
|
|
1938
|
-
const GECKO_FAMILY = new Set([
|
|
1939
|
-
"firefox",
|
|
1940
|
-
"waterfox",
|
|
1941
|
-
"librewolf",
|
|
1942
|
-
"gecko-based",
|
|
1943
|
-
"firefox-based"
|
|
1944
|
-
]);
|
|
1945
|
-
const WEBKIT_FAMILY = new Set([
|
|
1946
|
-
"safari",
|
|
1947
|
-
"webkit-based"
|
|
1948
|
-
]);
|
|
1949
|
-
function isChromiumFamily(browser) {
|
|
1950
|
-
return CHROMIUM_FAMILY.has(browser);
|
|
1951
|
-
}
|
|
1952
|
-
function isGeckoFamily(browser) {
|
|
1953
|
-
return GECKO_FAMILY.has(browser);
|
|
1954
|
-
}
|
|
1955
2160
|
const CHROME_DESKTOP_ONLY_KEYS = [
|
|
1956
2161
|
"file_browser_handlers",
|
|
1957
2162
|
"file_system_provider_capabilities",
|
|
@@ -2186,11 +2391,13 @@ const HARD_APIS = new Set([
|
|
|
2186
2391
|
"pageCapture",
|
|
2187
2392
|
"desktopCapture"
|
|
2188
2393
|
]);
|
|
2189
|
-
function scanApiUsage(roots) {
|
|
2394
|
+
function scanApiUsage(roots, excluded = []) {
|
|
2190
2395
|
const used = new Set();
|
|
2396
|
+
const skip = new Set(excluded.map((d)=>node_path.resolve(d)));
|
|
2191
2397
|
let filesRead = 0;
|
|
2192
2398
|
const walk = (dir, depth)=>{
|
|
2193
2399
|
if (depth > 6 || filesRead > 300) return;
|
|
2400
|
+
if (skip.has(node_path.resolve(dir))) return;
|
|
2194
2401
|
let entries;
|
|
2195
2402
|
try {
|
|
2196
2403
|
entries = node_fs.readdirSync(dir, {
|
|
@@ -2296,7 +2503,7 @@ async function manifest_validate_handler(args) {
|
|
|
2296
2503
|
...view.permissions ?? [],
|
|
2297
2504
|
...view.optional_permissions ?? []
|
|
2298
2505
|
])if ("string" == typeof p) declaredPermSet.add(p);
|
|
2299
|
-
const usedApis = scanApiUsage(roots);
|
|
2506
|
+
const usedApis = scanApiUsage(roots, roots.map((r)=>node_path.join(r, "extensions")));
|
|
2300
2507
|
for (const api of usedApis){
|
|
2301
2508
|
const perm = API_PERMISSION[api];
|
|
2302
2509
|
if (declaredPermSet.has(perm)) continue;
|
|
@@ -4669,27 +4876,40 @@ async function reload_handler(args) {
|
|
|
4669
4876
|
})
|
|
4670
4877
|
], args.projectPath, args.timeout);
|
|
4671
4878
|
}
|
|
4672
|
-
async function pollForTarget(port, url, budgetMs) {
|
|
4879
|
+
async function pollForTarget(port, url, budgetMs, navigatedTargetId) {
|
|
4673
4880
|
const deadline = Date.now() + budgetMs;
|
|
4674
4881
|
const wanted = url.replace(/#.*$/, "");
|
|
4882
|
+
let redirected = null;
|
|
4675
4883
|
for(;;){
|
|
4676
4884
|
try {
|
|
4677
4885
|
const targets = await CDPClient.discoverTargets(port);
|
|
4678
4886
|
for (const t of targets){
|
|
4679
4887
|
const tUrl = String(t.url ?? "");
|
|
4680
|
-
if ("page"
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
}
|
|
4888
|
+
if ("page" !== t.type) continue;
|
|
4889
|
+
const title = "string" == typeof t.title ? t.title : void 0;
|
|
4890
|
+
if (tUrl === wanted || tUrl.startsWith(wanted)) return {
|
|
4891
|
+
id: String(t.id),
|
|
4892
|
+
url: tUrl,
|
|
4893
|
+
title
|
|
4894
|
+
};
|
|
4895
|
+
if (navigatedTargetId && String(t.id) === navigatedTargetId && tUrl && "about:blank" !== tUrl && !tUrl.startsWith("chrome-error://")) redirected = {
|
|
4896
|
+
id: String(t.id),
|
|
4897
|
+
url: tUrl,
|
|
4898
|
+
title,
|
|
4899
|
+
redirectedFrom: url
|
|
4900
|
+
};
|
|
4687
4901
|
}
|
|
4688
4902
|
} catch {}
|
|
4689
|
-
if (Date.now() >= deadline) return
|
|
4903
|
+
if (Date.now() >= deadline) return redirected;
|
|
4690
4904
|
await new Promise((r)=>setTimeout(r, 250));
|
|
4691
4905
|
}
|
|
4692
4906
|
}
|
|
4907
|
+
function isDisposableTab(tabUrl, destination) {
|
|
4908
|
+
if (!tabUrl || "about:blank" === tabUrl) return true;
|
|
4909
|
+
if (/^chrome:\/\/(newtab|new-tab-page)/.test(tabUrl)) return true;
|
|
4910
|
+
const origin = destination.match(/^chrome-extension:\/\/[a-p]{32}\//)?.[0];
|
|
4911
|
+
return Boolean(origin && tabUrl.startsWith(origin));
|
|
4912
|
+
}
|
|
4693
4913
|
async function navigateToUrl(projectPath, browser, url, timeout) {
|
|
4694
4914
|
if (!isChromiumFamily(browser)) return navigateToUrlViaBridge(projectPath, browser, url, timeout);
|
|
4695
4915
|
const resolved = await resolveCdpPort(projectPath, browser);
|
|
@@ -4704,30 +4924,49 @@ async function navigateToUrl(projectPath, browser, url, timeout) {
|
|
|
4704
4924
|
try {
|
|
4705
4925
|
const targets = await CDPClient.discoverTargets(resolved.port);
|
|
4706
4926
|
const pageTargets = targets.filter((t)=>"page" === t.type && !String(t.url || "").startsWith("devtools://"));
|
|
4707
|
-
if (0 === pageTargets.length) return JSON.stringify({
|
|
4708
|
-
ok: false,
|
|
4709
|
-
error: {
|
|
4710
|
-
name: "NoTab",
|
|
4711
|
-
message: "The dev browser has no open page tab to navigate. Trigger one (e.g. extension_open surface, or open the extension) first."
|
|
4712
|
-
}
|
|
4713
|
-
});
|
|
4714
|
-
const target = pageTargets[0];
|
|
4715
4927
|
const browserWsUrl = await CDPClient.discoverBrowserWsUrl(resolved.port);
|
|
4716
4928
|
await cdp.connect(browserWsUrl);
|
|
4717
|
-
const
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
if (
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4929
|
+
const reusable = pageTargets.find((t)=>isDisposableTab(String(t.url ?? ""), url));
|
|
4930
|
+
let navigatedTargetId;
|
|
4931
|
+
let openedNewTab = false;
|
|
4932
|
+
if (reusable) {
|
|
4933
|
+
navigatedTargetId = String(reusable.id);
|
|
4934
|
+
const sessionId = await cdp.attachToTarget(navigatedTargetId);
|
|
4935
|
+
await cdp.navigate(sessionId, url);
|
|
4936
|
+
} else {
|
|
4937
|
+
const created = await cdp.sendCommand("Target.createTarget", {
|
|
4938
|
+
url,
|
|
4939
|
+
background: true
|
|
4940
|
+
}).catch(()=>cdp.sendCommand("Target.createTarget", {
|
|
4941
|
+
url
|
|
4942
|
+
}));
|
|
4943
|
+
navigatedTargetId = "string" == typeof created?.targetId ? created.targetId : void 0;
|
|
4944
|
+
openedNewTab = true;
|
|
4945
|
+
}
|
|
4946
|
+
const settled = await pollForTarget(resolved.port, url, 6000, navigatedTargetId);
|
|
4947
|
+
if (!settled) {
|
|
4948
|
+
const isExtensionPage = url.startsWith("chrome-extension://");
|
|
4949
|
+
return JSON.stringify({
|
|
4950
|
+
ok: false,
|
|
4951
|
+
error: {
|
|
4952
|
+
name: "NavigateFailed",
|
|
4953
|
+
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."}`
|
|
4954
|
+
},
|
|
4955
|
+
hint: isExtensionPage ? "Confirm the path exists in the built dist (extension_build / extension_inspect 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."
|
|
4956
|
+
});
|
|
4957
|
+
}
|
|
4728
4958
|
return JSON.stringify({
|
|
4729
4959
|
ok: true,
|
|
4730
4960
|
navigated: url,
|
|
4961
|
+
...openedNewTab ? {
|
|
4962
|
+
openedNewTab: true
|
|
4963
|
+
} : {},
|
|
4964
|
+
...settled.redirectedFrom ? {
|
|
4965
|
+
redirected: {
|
|
4966
|
+
from: settled.redirectedFrom,
|
|
4967
|
+
to: settled.url
|
|
4968
|
+
}
|
|
4969
|
+
} : {},
|
|
4731
4970
|
target: {
|
|
4732
4971
|
targetId: settled.id,
|
|
4733
4972
|
title: settled.title,
|
|
@@ -4980,6 +5219,39 @@ async function openSurfaceAsTab(projectPath, browser, surface) {
|
|
|
4980
5219
|
} catch {}
|
|
4981
5220
|
return raw;
|
|
4982
5221
|
}
|
|
5222
|
+
async function confirmSurfaceTarget(projectPath, browser, surface, raw) {
|
|
5223
|
+
if (!isChromiumFamily(browser)) return raw;
|
|
5224
|
+
const doc = surfaceDocument(projectPath, browser, surface);
|
|
5225
|
+
if (!doc) return raw;
|
|
5226
|
+
let parsed;
|
|
5227
|
+
try {
|
|
5228
|
+
parsed = JSON.parse(raw);
|
|
5229
|
+
} catch {
|
|
5230
|
+
return raw;
|
|
5231
|
+
}
|
|
5232
|
+
if (parsed?.ok === false) return raw;
|
|
5233
|
+
const resolved = await resolveCdpPort(projectPath, browser);
|
|
5234
|
+
const extensionId = resolved ? await resolveExtensionId(projectPath, browser) : null;
|
|
5235
|
+
if (!resolved || !extensionId) return raw;
|
|
5236
|
+
const wanted = `chrome-extension://${extensionId}/${doc}`;
|
|
5237
|
+
const settled = await pollForTarget(resolved.port, wanted, 3000);
|
|
5238
|
+
if (settled) {
|
|
5239
|
+
parsed.surfaceTarget = {
|
|
5240
|
+
targetId: settled.id,
|
|
5241
|
+
url: settled.url
|
|
5242
|
+
};
|
|
5243
|
+
return JSON.stringify(parsed);
|
|
5244
|
+
}
|
|
5245
|
+
return JSON.stringify({
|
|
5246
|
+
ok: false,
|
|
5247
|
+
error: {
|
|
5248
|
+
name: "SurfaceDidNotOpen",
|
|
5249
|
+
message: `The engine reported the ${surface} as opened, but no page target for ${wanted} appeared within 3s, so nothing is there to inspect.`
|
|
5250
|
+
},
|
|
5251
|
+
engineResult: parsed,
|
|
5252
|
+
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.`
|
|
5253
|
+
});
|
|
5254
|
+
}
|
|
4983
5255
|
const open_schema = {
|
|
4984
5256
|
name: "extension_open",
|
|
4985
5257
|
description: "Open an extension surface or replay an event in a running session. 'popup'/'options'/'sidebar' open UI surfaces; 'newtab'/'history'/'bookmarks' open the extension's chrome_url_overrides page in a tab. 'action' triggers the toolbar action: opens the action's popup, or (no popup) replays chrome.action.onClicked. 'command' replays a chrome.commands.onCommand keyboard shortcut (pass `name`). NOTE: action/command replay invokes your listener WITHOUT a user gesture, so the gesture-derived activeTab grant does not apply (the result includes gesture:false and a warning when activeTab is declared). Requires the dev session to be started with allowControl: true (extension_dev). Wraps `extension open`.",
|
|
@@ -5098,7 +5370,7 @@ async function open_handler(args) {
|
|
|
5098
5370
|
return JSON.stringify(parsed);
|
|
5099
5371
|
}
|
|
5100
5372
|
} catch {}
|
|
5101
|
-
return raw;
|
|
5373
|
+
return AS_TAB_SURFACES.includes(args.surface) ? confirmSurfaceTarget(args.projectPath, browser, args.surface, raw) : raw;
|
|
5102
5374
|
}
|
|
5103
5375
|
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_inspect with listTabs: true.";
|
|
5104
5376
|
function filterPageTargets(raw) {
|
|
@@ -6056,7 +6328,7 @@ function storeMdWarnings(browsers, cwd) {
|
|
|
6056
6328
|
}
|
|
6057
6329
|
const deploy_schema = {
|
|
6058
6330
|
name: "extension_deploy",
|
|
6059
|
-
description: "Submit a built extension to the Chrome Web Store, Firefox AMO,
|
|
6331
|
+
description: "Submit a built extension to the Chrome Web Store, Firefox AMO, Edge Add-ons, and/or the App Store (Safari) THROUGH extension.dev, which holds your store credentials and dispatches the release from your project's mirror CI. DEFAULTS TO A DRY RUN (preflight - dispatches nothing): the platform side verifies auth, the project, that the build exists, and the store workflow, and this tool then adds the per-store verdict from each store's public credential-health record; trust the per-store rows in the result 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 target project is identified by your token (extension_login or a release token in 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 tool arguments and local files are not uploaded. Pass browsers + buildSha (extension_release_list lists valid shas); after a real submission, extension_store_status reads the recorded outcome and review state. Posts to the platform's CLI store-submission endpoint.",
|
|
6060
6332
|
inputSchema: {
|
|
6061
6333
|
type: "object",
|
|
6062
6334
|
properties: {
|
|
@@ -6114,7 +6386,7 @@ async function deploy_handler(args) {
|
|
|
6114
6386
|
const token = resolveToken();
|
|
6115
6387
|
if (!token) return deploy_fail("DeployAuthError", "No token. Run extension_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).");
|
|
6116
6388
|
const browsers = (Array.isArray(args.browsers) ? args.browsers : []).map((b)=>String(b).trim().toLowerCase()).filter(Boolean);
|
|
6117
|
-
if (0 === browsers.length) return deploy_fail("DeployInputError", 'browsers is required (e.g. ["chrome","firefox","edge"]).');
|
|
6389
|
+
if (0 === browsers.length) return deploy_fail("DeployInputError", 'browsers is required (e.g. ["chrome","firefox","edge","safari"]).');
|
|
6118
6390
|
const buildSha = String(args.buildSha || "").trim();
|
|
6119
6391
|
if (!buildSha) return deploy_fail("DeployInputError", "buildSha is required (the built commit to submit).");
|
|
6120
6392
|
const apiCheck = safeApiBase(String(args.api || process.env.EXTENSION_DEV_API_URL || deploy_DEFAULT_API));
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Extension.dev Live Preview carrier, bundled prebuilt in this package
|
|
3
|
+
* under extensions/live-preview/<engine>. extension_dev materializes it into
|
|
4
|
+
* the project's ./extensions folder (which Extension.js auto-scans and loads
|
|
5
|
+
* as a companion next to the user's extension), so the dev browser comes up
|
|
6
|
+
* carrier-equipped: web pages the carrier allowlists (inspect.extension.dev,
|
|
7
|
+
* localhost) can then watch the session's real-lane trace and pair with it.
|
|
8
|
+
*/
|
|
9
|
+
export declare const CARRIER_DIR_NAME = "extension-dev-live-preview";
|
|
10
|
+
/** Where the carrier lands inside a project. */
|
|
11
|
+
export declare function carrierPath(projectPath: string): string;
|
|
12
|
+
/** True only for a directory carrying our marker, i.e. ours to delete. */
|
|
13
|
+
export declare function isManagedCarrier(projectPath: string): boolean;
|
|
14
|
+
export type CarrierRemoval = {
|
|
15
|
+
removed: boolean;
|
|
16
|
+
path: string;
|
|
17
|
+
/** Present when something was there but was NOT ours to touch. */
|
|
18
|
+
note?: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Remove the carrier from a project, marker-guarded.
|
|
22
|
+
*
|
|
23
|
+
* The trace swarm's most release-dangerous finding was that a debugging flag
|
|
24
|
+
* leaves a permanent companion extension in the project: it survived
|
|
25
|
+
* extension_stop, Extension.js auto-scans ./extensions, and the first
|
|
26
|
+
* `git add -A` vendors it. Nothing removed it, ever. Stop and build both call
|
|
27
|
+
* this now, and `extension_dev carrier: true` puts it back on demand.
|
|
28
|
+
*/
|
|
29
|
+
export declare function removeCarrier(projectPath: string): CarrierRemoval;
|
|
30
|
+
/**
|
|
31
|
+
* Keep the carrier out of the user's commits. The scaffolder runs `git init`
|
|
32
|
+
* with no initial commit, so the first `git add -A` in a project that ever ran
|
|
33
|
+
* with carrier: true vendors 460K of somebody else's extension.
|
|
34
|
+
* Returns the line added, or null when nothing needed doing.
|
|
35
|
+
*/
|
|
36
|
+
export declare function ensureCarrierIgnored(projectPath: string): string | null;
|
|
37
|
+
export type CarrierMaterialization = {
|
|
38
|
+
loaded: boolean;
|
|
39
|
+
path?: string;
|
|
40
|
+
note: string;
|
|
41
|
+
/** The .gitignore entry this run added, when it added one. */
|
|
42
|
+
gitignored?: string;
|
|
43
|
+
/**
|
|
44
|
+
* What the carrier lane CANNOT do, stated at the moment it is handed over.
|
|
45
|
+
* The trace swarm's top finding was that the real lane's boundaries were
|
|
46
|
+
* discoverable only by experiment: 14 personas independently filed the
|
|
47
|
+
* carrier-identity scoping as a severe bug, and 10 more expected the trace
|
|
48
|
+
* to show their own extension's calls. Both are honest constraints; neither
|
|
49
|
+
* was written down anywhere the caller would look.
|
|
50
|
+
*/
|
|
51
|
+
limitations?: string[];
|
|
52
|
+
/**
|
|
53
|
+
* How to actually DRIVE the real lane. Seven personas reached a
|
|
54
|
+
* permanently empty trace and concluded the feature was broken, because
|
|
55
|
+
* nothing in the 33 tool schemas, this note, or the rendered page named the
|
|
56
|
+
* carrier's extension id or its message envelopes. Every persona that did
|
|
57
|
+
* reach the real lane used out-of-band knowledge of the emulator source.
|
|
58
|
+
*/
|
|
59
|
+
bridgeProtocol?: {
|
|
60
|
+
carrierExtensionId: string;
|
|
61
|
+
allowedOrigins: string;
|
|
62
|
+
howTo: string;
|
|
63
|
+
example: string;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Copy the bundled carrier into <projectPath>/extensions/<CARRIER_DIR_NAME>.
|
|
68
|
+
* Refuses to touch an existing directory that lacks our marker file (it is
|
|
69
|
+
* the user's, not ours); otherwise replaces it so version updates propagate.
|
|
70
|
+
*/
|
|
71
|
+
export declare function materializeCarrier(projectPath: string, browser: string): CarrierMaterialization;
|