@extension.dev/mcp 5.6.1 → 6.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 +87 -0
- package/LICENSE +201 -21
- package/README.md +12 -2
- package/bin/extension-mcp.js +1 -1
- package/dist/module.js +624 -119
- package/dist/src/lib/carrier.d.ts +83 -0
- package/dist/src/lib/guest-load-oracle.d.ts +27 -0
- package/dist/src/lib/registry.d.ts +22 -4
- package/dist/src/lib/template-artifact-source.d.ts +2 -0
- package/dist/src/lib/urls-origins.d.ts +34 -0
- package/dist/src/lib/urls-paths.d.ts +56 -0
- package/dist/src/tools/dev.d.ts +6 -0
- package/dist/src/tools/open.d.ts +1 -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 +10 -7
- 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";
|
|
@@ -137,7 +138,8 @@ __webpack_require__.r(open_namespaceObject);
|
|
|
137
138
|
__webpack_require__.d(open_namespaceObject, {
|
|
138
139
|
clampPopupBounds: ()=>clampPopupBounds,
|
|
139
140
|
handler: ()=>open_handler,
|
|
140
|
-
schema: ()=>open_schema
|
|
141
|
+
schema: ()=>open_schema,
|
|
142
|
+
sessionIsHeadless: ()=>sessionIsHeadless
|
|
141
143
|
});
|
|
142
144
|
var preview_namespaceObject = {};
|
|
143
145
|
__webpack_require__.r(preview_namespaceObject);
|
|
@@ -220,7 +222,7 @@ __webpack_require__.d(whoami_namespaceObject, {
|
|
|
220
222
|
handler: ()=>whoami_handler,
|
|
221
223
|
schema: ()=>whoami_schema
|
|
222
224
|
});
|
|
223
|
-
var package_namespaceObject = JSON.parse('{"rE":"5.
|
|
225
|
+
var package_namespaceObject = JSON.parse('{"rE":"5.7.0","El":{"OP":"4.0.16-canary.1784889479.74e12044"}}');
|
|
224
226
|
function scaffoldEnginePin(projectPath) {
|
|
225
227
|
try {
|
|
226
228
|
const pkg = JSON.parse(node_fs.readFileSync(node_path.join(projectPath, "package.json"), "utf8"));
|
|
@@ -387,10 +389,85 @@ async function create_handler(args) {
|
|
|
387
389
|
}
|
|
388
390
|
});
|
|
389
391
|
}
|
|
392
|
+
const DEFAULT_MEDIA_ORIGIN = "https://media.extension.land";
|
|
393
|
+
const DEFAULT_CHANNEL = "latest";
|
|
394
|
+
const PINNED_COMMIT = "2d2ed9668cca002148d9eecd953a08b54d0bad9d";
|
|
395
|
+
const CHANNEL_CACHE_TTL_MS = 300000;
|
|
396
|
+
function mediaOrigin() {
|
|
397
|
+
return (process.env.EXTENSION_MEDIA_ORIGIN || "").trim() || DEFAULT_MEDIA_ORIGIN;
|
|
398
|
+
}
|
|
399
|
+
function channelName() {
|
|
400
|
+
return (process.env.EXTENSION_TEMPLATES_CHANNEL || "").trim() || DEFAULT_CHANNEL;
|
|
401
|
+
}
|
|
402
|
+
function pinnedCommitOverride() {
|
|
403
|
+
return (process.env.EXTENSION_TEMPLATES_COMMIT || "").trim();
|
|
404
|
+
}
|
|
405
|
+
function rawBaseForCommit(commit) {
|
|
406
|
+
return `https://raw.githubusercontent.com/extension-js/examples/${commit}`;
|
|
407
|
+
}
|
|
408
|
+
let releaseCache = null;
|
|
409
|
+
let releaseCacheExpiresAt = 0;
|
|
410
|
+
let releaseRequest = null;
|
|
411
|
+
function releaseForCommit(origin, commit) {
|
|
412
|
+
return {
|
|
413
|
+
commit,
|
|
414
|
+
metaUrl: `${origin}/templates/${commit}/templates-meta.json`,
|
|
415
|
+
filesBaseUrl: `${origin}/templates/${commit}/files`
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
async function resolveRelease() {
|
|
419
|
+
const now = Date.now();
|
|
420
|
+
if (releaseCache && now < releaseCacheExpiresAt) return releaseCache;
|
|
421
|
+
if (releaseRequest) return releaseRequest;
|
|
422
|
+
const origin = mediaOrigin();
|
|
423
|
+
const override = pinnedCommitOverride();
|
|
424
|
+
if (override) {
|
|
425
|
+
releaseCache = releaseForCommit(origin, override);
|
|
426
|
+
releaseCacheExpiresAt = now + CHANNEL_CACHE_TTL_MS;
|
|
427
|
+
return releaseCache;
|
|
428
|
+
}
|
|
429
|
+
releaseRequest = (async ()=>{
|
|
430
|
+
try {
|
|
431
|
+
const pointerUrl = `${origin}/templates/${channelName()}.json`;
|
|
432
|
+
const response = await fetch(pointerUrl, {
|
|
433
|
+
headers: {
|
|
434
|
+
Accept: "application/json"
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
if (!response.ok) return null;
|
|
438
|
+
const pointer = await response.json();
|
|
439
|
+
const commit = String(pointer?.commit || "").trim();
|
|
440
|
+
if (!commit) return null;
|
|
441
|
+
releaseCache = releaseForCommit(origin, commit);
|
|
442
|
+
releaseCacheExpiresAt = Date.now() + CHANNEL_CACHE_TTL_MS;
|
|
443
|
+
return releaseCache;
|
|
444
|
+
} catch {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
})();
|
|
448
|
+
try {
|
|
449
|
+
return await releaseRequest;
|
|
450
|
+
} finally{
|
|
451
|
+
releaseRequest = null;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async function templateMetaUrls() {
|
|
455
|
+
const urls = [];
|
|
456
|
+
const release = await resolveRelease();
|
|
457
|
+
if (release) urls.push(release.metaUrl);
|
|
458
|
+
urls.push(`${rawBaseForCommit(PINNED_COMMIT)}/templates-meta.json`);
|
|
459
|
+
return urls;
|
|
460
|
+
}
|
|
461
|
+
async function templateFileUrls(slug, relativePath) {
|
|
462
|
+
const urls = [];
|
|
463
|
+
const release = await resolveRelease();
|
|
464
|
+
if (release) urls.push(`${release.filesBaseUrl}/${slug}/${relativePath}`);
|
|
465
|
+
urls.push(`${rawBaseForCommit(PINNED_COMMIT)}/examples/${slug}/${relativePath}`);
|
|
466
|
+
return urls;
|
|
467
|
+
}
|
|
390
468
|
const CACHE_DIR = node_path.join(node_os.homedir(), ".cache", "extension-js");
|
|
391
469
|
const CACHE_FILE = node_path.join(CACHE_DIR, "templates-meta.json");
|
|
392
470
|
const CACHE_TTL_MS = 3600000;
|
|
393
|
-
const TEMPLATES_META_URL = "https://github.com/extension-js/examples/releases/download/nightly/templates-meta.json";
|
|
394
471
|
function isCacheValid() {
|
|
395
472
|
try {
|
|
396
473
|
const stat = node_fs.statSync(CACHE_FILE);
|
|
@@ -404,17 +481,22 @@ async function fetchTemplatesMeta() {
|
|
|
404
481
|
const cached = JSON.parse(node_fs.readFileSync(CACHE_FILE, "utf8"));
|
|
405
482
|
return cached;
|
|
406
483
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
484
|
+
let lastStatus = 0;
|
|
485
|
+
for (const url of (await templateMetaUrls()))try {
|
|
486
|
+
const response = await fetch(url);
|
|
487
|
+
if (!response.ok) {
|
|
488
|
+
lastStatus = response.status;
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
const data = await response.json();
|
|
492
|
+
node_fs.mkdirSync(CACHE_DIR, {
|
|
493
|
+
recursive: true
|
|
494
|
+
});
|
|
495
|
+
node_fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2));
|
|
496
|
+
return data;
|
|
497
|
+
} catch {}
|
|
498
|
+
if (node_fs.existsSync(CACHE_FILE)) return JSON.parse(node_fs.readFileSync(CACHE_FILE, "utf8"));
|
|
499
|
+
throw new Error(`Failed to fetch templates-meta.json (${lastStatus || "network error"}).`);
|
|
418
500
|
}
|
|
419
501
|
async function listTemplates(filters) {
|
|
420
502
|
const meta = await fetchTemplatesMeta();
|
|
@@ -854,6 +936,180 @@ function resolveSessionBrowser(projectPath, explicit, fallback = "chrome") {
|
|
|
854
936
|
source: "fallback"
|
|
855
937
|
};
|
|
856
938
|
}
|
|
939
|
+
const CHROMIUM_FAMILY = new Set([
|
|
940
|
+
"chrome",
|
|
941
|
+
"chromium",
|
|
942
|
+
"edge",
|
|
943
|
+
"brave",
|
|
944
|
+
"opera",
|
|
945
|
+
"vivaldi",
|
|
946
|
+
"yandex",
|
|
947
|
+
"chromium-based"
|
|
948
|
+
]);
|
|
949
|
+
const GECKO_FAMILY = new Set([
|
|
950
|
+
"firefox",
|
|
951
|
+
"waterfox",
|
|
952
|
+
"librewolf",
|
|
953
|
+
"gecko-based",
|
|
954
|
+
"firefox-based"
|
|
955
|
+
]);
|
|
956
|
+
const WEBKIT_FAMILY = new Set([
|
|
957
|
+
"safari",
|
|
958
|
+
"webkit-based"
|
|
959
|
+
]);
|
|
960
|
+
function isChromiumFamily(browser) {
|
|
961
|
+
return CHROMIUM_FAMILY.has(browser);
|
|
962
|
+
}
|
|
963
|
+
function isGeckoFamily(browser) {
|
|
964
|
+
return GECKO_FAMILY.has(browser);
|
|
965
|
+
}
|
|
966
|
+
const CARRIER_DIR_NAME = "extension-dev-live-preview";
|
|
967
|
+
const CARRIER_EXTENSION_ID = "ibppeifnekhjjjmpjfiobccjlicbmgcb";
|
|
968
|
+
const MARKER_FILE = "managed-by-extension-dev-mcp.json";
|
|
969
|
+
function deriveCarrierId(source) {
|
|
970
|
+
try {
|
|
971
|
+
const manifest = JSON.parse(node_fs.readFileSync(node_path.join(source, "manifest.json"), "utf-8"));
|
|
972
|
+
if (!manifest.key) return null;
|
|
973
|
+
const hash = createHash("sha256").update(Buffer.from(manifest.key, "base64")).digest();
|
|
974
|
+
return [
|
|
975
|
+
...hash.subarray(0, 16)
|
|
976
|
+
].map((byte)=>String.fromCharCode(97 + (byte >> 4)) + String.fromCharCode(97 + (15 & byte))).join("");
|
|
977
|
+
} catch {
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
function carrierPath(projectPath) {
|
|
982
|
+
return node_path.join(projectPath, "extensions", CARRIER_DIR_NAME);
|
|
983
|
+
}
|
|
984
|
+
function removeCarrier(projectPath) {
|
|
985
|
+
const target = carrierPath(projectPath);
|
|
986
|
+
if (!node_fs.existsSync(target)) return {
|
|
987
|
+
removed: false,
|
|
988
|
+
path: target
|
|
989
|
+
};
|
|
990
|
+
if (!node_fs.existsSync(node_path.join(target, MARKER_FILE))) return {
|
|
991
|
+
removed: false,
|
|
992
|
+
path: target,
|
|
993
|
+
note: `extensions/${CARRIER_DIR_NAME} has no ${MARKER_FILE} marker, so it is not the carrier this tool placed and was left untouched.`
|
|
994
|
+
};
|
|
995
|
+
try {
|
|
996
|
+
node_fs.rmSync(target, {
|
|
997
|
+
recursive: true,
|
|
998
|
+
force: true
|
|
999
|
+
});
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
return {
|
|
1002
|
+
removed: false,
|
|
1003
|
+
path: target,
|
|
1004
|
+
note: `Could not remove the carrier: ${error instanceof Error ? error.message : String(error)}`
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
const parent = node_path.join(projectPath, "extensions");
|
|
1008
|
+
try {
|
|
1009
|
+
if (0 === node_fs.readdirSync(parent).length) node_fs.rmdirSync(parent);
|
|
1010
|
+
} catch {}
|
|
1011
|
+
return {
|
|
1012
|
+
removed: true,
|
|
1013
|
+
path: target
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
function ensureCarrierIgnored(projectPath) {
|
|
1017
|
+
if (!node_fs.existsSync(node_path.join(projectPath, ".git"))) return null;
|
|
1018
|
+
const entry = `extensions/${CARRIER_DIR_NAME}/`;
|
|
1019
|
+
const file = node_path.join(projectPath, ".gitignore");
|
|
1020
|
+
let current = "";
|
|
1021
|
+
try {
|
|
1022
|
+
current = node_fs.readFileSync(file, "utf-8");
|
|
1023
|
+
} catch {}
|
|
1024
|
+
const ignored = current.split("\n").map((line)=>line.trim()).some((line)=>line === entry || line === `extensions/${CARRIER_DIR_NAME}` || "extensions/" === line || "extensions" === line);
|
|
1025
|
+
if (ignored) return null;
|
|
1026
|
+
try {
|
|
1027
|
+
const prefix = "" === current || current.endsWith("\n") ? "" : "\n";
|
|
1028
|
+
node_fs.appendFileSync(file, `${prefix}\n# Extension.dev live-preview carrier: a local debug companion, not part of your extension.\n${entry}\n`);
|
|
1029
|
+
return entry;
|
|
1030
|
+
} catch {
|
|
1031
|
+
return null;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
function findBundledCarrier(engine) {
|
|
1035
|
+
let dir = node_path.dirname(fileURLToPath(import.meta.url));
|
|
1036
|
+
for(let depth = 0; depth < 6; depth++){
|
|
1037
|
+
const candidate = node_path.join(dir, "extensions", "live-preview", engine);
|
|
1038
|
+
if (node_fs.existsSync(node_path.join(candidate, "manifest.json"))) return candidate;
|
|
1039
|
+
const parent = node_path.dirname(dir);
|
|
1040
|
+
if (parent === dir) break;
|
|
1041
|
+
dir = parent;
|
|
1042
|
+
}
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
function materializeCarrier(projectPath, browser) {
|
|
1046
|
+
if (!isChromiumFamily(browser)) return {
|
|
1047
|
+
loaded: false,
|
|
1048
|
+
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.`
|
|
1049
|
+
};
|
|
1050
|
+
const source = findBundledCarrier("chromium");
|
|
1051
|
+
if (!source) return {
|
|
1052
|
+
loaded: false,
|
|
1053
|
+
note: "This install ships no bundled carrier payload (extensions/live-preview/chromium missing from the package)."
|
|
1054
|
+
};
|
|
1055
|
+
const target = carrierPath(projectPath);
|
|
1056
|
+
const marker = node_path.join(target, MARKER_FILE);
|
|
1057
|
+
if (node_fs.existsSync(target) && !node_fs.existsSync(marker)) return {
|
|
1058
|
+
loaded: false,
|
|
1059
|
+
path: target,
|
|
1060
|
+
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.`
|
|
1061
|
+
};
|
|
1062
|
+
const carrierId = deriveCarrierId(source);
|
|
1063
|
+
try {
|
|
1064
|
+
node_fs.rmSync(target, {
|
|
1065
|
+
recursive: true,
|
|
1066
|
+
force: true
|
|
1067
|
+
});
|
|
1068
|
+
node_fs.cpSync(source, target, {
|
|
1069
|
+
recursive: true
|
|
1070
|
+
});
|
|
1071
|
+
const manifest = JSON.parse(node_fs.readFileSync(node_path.join(source, "manifest.json"), "utf-8"));
|
|
1072
|
+
node_fs.writeFileSync(marker, `${JSON.stringify({
|
|
1073
|
+
managedBy: "@extension.dev/mcp",
|
|
1074
|
+
carrierVersion: manifest.version ?? "unknown",
|
|
1075
|
+
note: "Safe to delete; extension_dev recreates it when carrier: true. extension_stop and extension_build remove it for you."
|
|
1076
|
+
}, null, 2)}\n`);
|
|
1077
|
+
const ignored = ensureCarrierIgnored(projectPath);
|
|
1078
|
+
return {
|
|
1079
|
+
loaded: true,
|
|
1080
|
+
path: target,
|
|
1081
|
+
...ignored ? {
|
|
1082
|
+
gitignored: ignored
|
|
1083
|
+
} : {},
|
|
1084
|
+
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.` : "."),
|
|
1085
|
+
limitations: [
|
|
1086
|
+
"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.",
|
|
1087
|
+
"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.",
|
|
1088
|
+
"Chromium-family only: Firefox has no externally_connectable channel for web pages."
|
|
1089
|
+
],
|
|
1090
|
+
graduation: "The carrier lane is the SHARED real lane: bridged calls run as the carrier, by design (see limitations). Your guest is already loaded as ITSELF in this same session, so for its own storage, identity, badge and messaging (the isolated real thing), drive the guest directly instead of the carrier bridge: extension_storage, extension_eval and extension_dom_inspect against this projectPath all operate on the guest as itself. Start (or replace) this session with allowControl: true (or allowEval: true) to unlock them. Use the carrier bridge for the shared real-lane TRACE; use the control verbs for the guest's OWN state.",
|
|
1091
|
+
...carrierId ? {
|
|
1092
|
+
bridgeProtocol: {
|
|
1093
|
+
carrierExtensionId: carrierId,
|
|
1094
|
+
allowedOrigins: "https://inspect.extension.dev, https://intelligence.extension.dev, https://themes.extension.dev, http://localhost/*, http://127.0.0.1/*",
|
|
1095
|
+
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.",
|
|
1096
|
+
example: [
|
|
1097
|
+
`const id = '${carrierId}'`,
|
|
1098
|
+
"const send = (msg) => new Promise((r) => chrome.runtime.sendMessage(id, msg, r))",
|
|
1099
|
+
"await send({ type: 'extensiondev:session', extensionId: 'my-guest', permissions: ['storage'] })",
|
|
1100
|
+
"await send({ type: 'extensiondev:bridge', request: { type: 'EXTENSION_BRIDGE_REQUEST', extensionId: 'my-guest', requestId: 'r1', api: 'storage.get', args: [null, 'local'] } })"
|
|
1101
|
+
].join("\n")
|
|
1102
|
+
}
|
|
1103
|
+
} : {}
|
|
1104
|
+
};
|
|
1105
|
+
} catch (error) {
|
|
1106
|
+
return {
|
|
1107
|
+
loaded: false,
|
|
1108
|
+
path: target,
|
|
1109
|
+
note: `Could not place the carrier: ${error instanceof Error ? error.message : String(error)}`
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
857
1113
|
function readBuildSummary(projectPath, browser, since) {
|
|
858
1114
|
const file = node_path.resolve(projectPath, "dist", "extension-js", browser, "build-summary.json");
|
|
859
1115
|
try {
|
|
@@ -1052,6 +1308,27 @@ function manifestDivergence(projectPath, browser) {
|
|
|
1052
1308
|
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
1309
|
return notes;
|
|
1054
1310
|
}
|
|
1311
|
+
const MARKER_FILE_NAME = "managed-by-extension-dev-mcp.json";
|
|
1312
|
+
function carrierEntriesInDist(distDir, depth = 0) {
|
|
1313
|
+
if (depth > 3) return [];
|
|
1314
|
+
let entries;
|
|
1315
|
+
try {
|
|
1316
|
+
entries = node_fs.readdirSync(distDir, {
|
|
1317
|
+
withFileTypes: true
|
|
1318
|
+
});
|
|
1319
|
+
} catch {
|
|
1320
|
+
return [];
|
|
1321
|
+
}
|
|
1322
|
+
const found = [];
|
|
1323
|
+
for (const entry of entries){
|
|
1324
|
+
if (entry.name === CARRIER_DIR_NAME || entry.name === MARKER_FILE_NAME) {
|
|
1325
|
+
found.push(node_path.join(distDir, entry.name));
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
if (entry.isDirectory()) found.push(...carrierEntriesInDist(node_path.join(distDir, entry.name), depth + 1));
|
|
1329
|
+
}
|
|
1330
|
+
return found;
|
|
1331
|
+
}
|
|
1055
1332
|
async function validationPreflight(projectPath, browser) {
|
|
1056
1333
|
try {
|
|
1057
1334
|
const manifestValidate = await Promise.resolve().then(()=>({
|
|
@@ -1076,6 +1353,10 @@ async function validationPreflight(projectPath, browser) {
|
|
|
1076
1353
|
async function build_handler(args) {
|
|
1077
1354
|
const start = Date.now();
|
|
1078
1355
|
const browser = args.browser ?? "chrome";
|
|
1356
|
+
const carrierCleanup = removeCarrier(args.projectPath);
|
|
1357
|
+
const carrierNotes = [];
|
|
1358
|
+
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.");
|
|
1359
|
+
else if (carrierCleanup.note) carrierNotes.push(carrierCleanup.note);
|
|
1079
1360
|
const preflight = args.skipValidation ? null : await validationPreflight(args.projectPath, browser);
|
|
1080
1361
|
if (preflight?.buildBlocking) return JSON.stringify({
|
|
1081
1362
|
success: false,
|
|
@@ -1083,12 +1364,15 @@ async function build_handler(args) {
|
|
|
1083
1364
|
browser,
|
|
1084
1365
|
error: "Build refused: the manifest has errors that produce a broken extension even when the bundler succeeds.",
|
|
1085
1366
|
errors: preflight.errors,
|
|
1086
|
-
warnings:
|
|
1367
|
+
warnings: [
|
|
1368
|
+
...carrierNotes,
|
|
1369
|
+
...preflight.warnings
|
|
1370
|
+
],
|
|
1087
1371
|
duration: Date.now() - start,
|
|
1088
1372
|
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
1373
|
});
|
|
1090
1374
|
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.`);
|
|
1375
|
+
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
1376
|
const cliArgs = [
|
|
1093
1377
|
"build",
|
|
1094
1378
|
args.projectPath,
|
|
@@ -1118,7 +1402,21 @@ async function build_handler(args) {
|
|
|
1118
1402
|
buildWarningsTruncated: engineSummary.warnings_count
|
|
1119
1403
|
} : {}
|
|
1120
1404
|
} : {};
|
|
1121
|
-
const
|
|
1405
|
+
const distDir = node_path.resolve(args.projectPath, "dist", browser);
|
|
1406
|
+
const entrypoints = builtEntrypoints(distDir);
|
|
1407
|
+
const contamination = carrierEntriesInDist(distDir);
|
|
1408
|
+
if (contamination.length) return JSON.stringify({
|
|
1409
|
+
success: false,
|
|
1410
|
+
status: "contaminated",
|
|
1411
|
+
browser,
|
|
1412
|
+
buildExitCode: 0,
|
|
1413
|
+
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.`,
|
|
1414
|
+
...warnings.length ? {
|
|
1415
|
+
warnings
|
|
1416
|
+
} : {},
|
|
1417
|
+
duration,
|
|
1418
|
+
hint: "Delete the listed paths from dist and build again. The carrier normally lives in ./extensions and is removed before every build."
|
|
1419
|
+
});
|
|
1122
1420
|
const missing = entrypoints.filter((e)=>!e.present);
|
|
1123
1421
|
if (missing.length) return JSON.stringify({
|
|
1124
1422
|
success: false,
|
|
@@ -1198,7 +1496,7 @@ async function build_handler(args) {
|
|
|
1198
1496
|
}
|
|
1199
1497
|
const stop_schema = {
|
|
1200
1498
|
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.",
|
|
1499
|
+
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
1500
|
inputSchema: {
|
|
1203
1501
|
type: "object",
|
|
1204
1502
|
properties: {
|
|
@@ -1219,6 +1517,12 @@ const stop_schema = {
|
|
|
1219
1517
|
required: []
|
|
1220
1518
|
}
|
|
1221
1519
|
};
|
|
1520
|
+
function cleanCarrier(projectPath) {
|
|
1521
|
+
const removal = removeCarrier(projectPath);
|
|
1522
|
+
return removal.removed ? {
|
|
1523
|
+
carrierRemoved: removal.path
|
|
1524
|
+
} : {};
|
|
1525
|
+
}
|
|
1222
1526
|
function pgrepPids(pattern) {
|
|
1223
1527
|
try {
|
|
1224
1528
|
const out = execFileSync("pgrep", [
|
|
@@ -1295,6 +1599,7 @@ async function stopOne(projectPath, browser) {
|
|
|
1295
1599
|
pid: null,
|
|
1296
1600
|
stopped: 0 !== reaped.length,
|
|
1297
1601
|
reaped,
|
|
1602
|
+
...cleanCarrier(projectPath),
|
|
1298
1603
|
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
1604
|
};
|
|
1300
1605
|
}
|
|
@@ -1326,7 +1631,8 @@ async function stopOne(projectPath, browser) {
|
|
|
1326
1631
|
pid,
|
|
1327
1632
|
stopped,
|
|
1328
1633
|
reaped,
|
|
1329
|
-
detail
|
|
1634
|
+
detail,
|
|
1635
|
+
...cleanCarrier(projectPath)
|
|
1330
1636
|
};
|
|
1331
1637
|
}
|
|
1332
1638
|
async function stop_handler(args) {
|
|
@@ -1458,6 +1764,11 @@ const dev_schema = {
|
|
|
1458
1764
|
type: "boolean",
|
|
1459
1765
|
default: false,
|
|
1460
1766
|
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."
|
|
1767
|
+
},
|
|
1768
|
+
carrier: {
|
|
1769
|
+
type: "boolean",
|
|
1770
|
+
default: false,
|
|
1771
|
+
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
1772
|
}
|
|
1462
1773
|
},
|
|
1463
1774
|
required: [
|
|
@@ -1492,6 +1803,7 @@ async function dev_handler(args) {
|
|
|
1492
1803
|
});
|
|
1493
1804
|
}
|
|
1494
1805
|
}
|
|
1806
|
+
const carrier = args.carrier ? materializeCarrier(args.projectPath, browser) : null;
|
|
1495
1807
|
const allowControl = Boolean(args.allowControl || args.allowEval);
|
|
1496
1808
|
const cliArgs = [
|
|
1497
1809
|
"dev",
|
|
@@ -1522,6 +1834,7 @@ async function dev_handler(args) {
|
|
|
1522
1834
|
child.on("exit", ()=>removeSession(args.projectPath, browser));
|
|
1523
1835
|
await new Promise((resolve)=>setTimeout(resolve, 3000));
|
|
1524
1836
|
const earlyOutput = spawned.readOutput();
|
|
1837
|
+
const cleanOutput = denoiseEarlyOutput(earlyOutput);
|
|
1525
1838
|
if (null !== child.exitCode || null !== child.signalCode) {
|
|
1526
1839
|
const code = child.exitCode;
|
|
1527
1840
|
const signal = child.signalCode;
|
|
@@ -1534,12 +1847,12 @@ async function dev_handler(args) {
|
|
|
1534
1847
|
exitCode: code,
|
|
1535
1848
|
signal,
|
|
1536
1849
|
error: `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.`,
|
|
1537
|
-
output:
|
|
1850
|
+
output: cleanOutput.slice(0, 2000),
|
|
1538
1851
|
logPath,
|
|
1539
1852
|
hint: "Read `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."
|
|
1540
1853
|
});
|
|
1541
1854
|
}
|
|
1542
|
-
const compileFailed = /compiled with errors|✖✖✖|ERROR in |Module not found|NOT FOUND/i.test(
|
|
1855
|
+
const compileFailed = /compiled with errors|✖✖✖|ERROR in |Module not found|NOT FOUND/i.test(cleanOutput);
|
|
1543
1856
|
if (compileFailed) return JSON.stringify({
|
|
1544
1857
|
ok: false,
|
|
1545
1858
|
status: "compile-failed",
|
|
@@ -1547,12 +1860,12 @@ async function dev_handler(args) {
|
|
|
1547
1860
|
browser,
|
|
1548
1861
|
pid,
|
|
1549
1862
|
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.",
|
|
1550
|
-
output:
|
|
1863
|
+
output: cleanOutput.slice(0, 2000),
|
|
1551
1864
|
logPath,
|
|
1552
1865
|
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."
|
|
1553
1866
|
});
|
|
1554
1867
|
const exitStamp = args.noBrowser ? null : browserExitStamp(args.projectPath, browser, spawnedAt);
|
|
1555
|
-
const profileLockHit = !args.noBrowser && /SingletonLock|ProcessSingleton|profile[^\n]*(in use|locked)|already (open|running)/i.test(
|
|
1868
|
+
const profileLockHit = !args.noBrowser && /SingletonLock|ProcessSingleton|profile[^\n]*(in use|locked)|already (open|running)/i.test(cleanOutput);
|
|
1556
1869
|
if (exitStamp || profileLockHit) {
|
|
1557
1870
|
const profileDir = node_path.join(args.projectPath, "dist", `extension-profile-${browser}`);
|
|
1558
1871
|
return JSON.stringify({
|
|
@@ -1563,7 +1876,7 @@ async function dev_handler(args) {
|
|
|
1563
1876
|
pid,
|
|
1564
1877
|
...exitStamp ?? {},
|
|
1565
1878
|
error: `The dev server is running but the ${browser} browser it launched died during startup` + (profileLockHit ? " because its profile is locked by another browser instance." : "."),
|
|
1566
|
-
output:
|
|
1879
|
+
output: cleanOutput.slice(0, 2000),
|
|
1567
1880
|
logPath,
|
|
1568
1881
|
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.`
|
|
1569
1882
|
});
|
|
@@ -1600,6 +1913,9 @@ async function dev_handler(args) {
|
|
|
1600
1913
|
...portReport,
|
|
1601
1914
|
projectPath: args.projectPath,
|
|
1602
1915
|
status: "started",
|
|
1916
|
+
...carrier ? {
|
|
1917
|
+
carrier
|
|
1918
|
+
} : {},
|
|
1603
1919
|
...replaced.length > 0 ? {
|
|
1604
1920
|
replacedSession: replaced[0],
|
|
1605
1921
|
...replaced.length > 1 ? {
|
|
@@ -1608,7 +1924,7 @@ async function dev_handler(args) {
|
|
|
1608
1924
|
} : {},
|
|
1609
1925
|
capabilities,
|
|
1610
1926
|
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_inspect/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_source_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_inspect 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.",
|
|
1611
|
-
earlyOutput:
|
|
1927
|
+
earlyOutput: cleanOutput.slice(0, 500),
|
|
1612
1928
|
logPath
|
|
1613
1929
|
});
|
|
1614
1930
|
}
|
|
@@ -1617,6 +1933,8 @@ function denoiseEarlyOutput(raw) {
|
|
|
1617
1933
|
/^npm warn Unknown project config/i,
|
|
1618
1934
|
/This will stop working in the next major version of npm/i,
|
|
1619
1935
|
/^npm warn config/i,
|
|
1936
|
+
/^npm warn exec/i,
|
|
1937
|
+
/The following package(s)? (was|were) not found and will be installed/i,
|
|
1620
1938
|
/V8: .*Invalid asm\.js/i,
|
|
1621
1939
|
/^\(node:\d+\) V8:/i,
|
|
1622
1940
|
/Use `node --trace-warnings/i,
|
|
@@ -1860,7 +2178,6 @@ async function preview_handler(args) {
|
|
|
1860
2178
|
logPath
|
|
1861
2179
|
});
|
|
1862
2180
|
}
|
|
1863
|
-
const RAW_BASE = "https://raw.githubusercontent.com/extension-js/examples/main/examples";
|
|
1864
2181
|
const get_template_source_schema = {
|
|
1865
2182
|
name: "extension_get_template_source",
|
|
1866
2183
|
description: "Read source files from a template in the extension.dev template catalog. Use this to learn implementation patterns before building something similar.",
|
|
@@ -1908,14 +2225,17 @@ async function get_template_source_handler(args) {
|
|
|
1908
2225
|
const fileContents = {};
|
|
1909
2226
|
const errors = [];
|
|
1910
2227
|
await Promise.all(args.files.map(async (filePath)=>{
|
|
1911
|
-
const
|
|
1912
|
-
|
|
2228
|
+
const urls = await templateFileUrls(args.slug, filePath);
|
|
2229
|
+
let lastStatus = 0;
|
|
2230
|
+
for (const url of urls)try {
|
|
1913
2231
|
const response = await fetch(url);
|
|
1914
|
-
if (response.ok)
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
2232
|
+
if (response.ok) {
|
|
2233
|
+
fileContents[filePath] = await response.text();
|
|
2234
|
+
return;
|
|
2235
|
+
}
|
|
2236
|
+
lastStatus = response.status;
|
|
2237
|
+
} catch {}
|
|
2238
|
+
errors.push(`${filePath}: ${lastStatus || "fetch failed"}`);
|
|
1919
2239
|
}));
|
|
1920
2240
|
return JSON.stringify({
|
|
1921
2241
|
...meta,
|
|
@@ -1925,33 +2245,6 @@ async function get_template_source_handler(args) {
|
|
|
1925
2245
|
} : {}
|
|
1926
2246
|
});
|
|
1927
2247
|
}
|
|
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
2248
|
const CHROME_DESKTOP_ONLY_KEYS = [
|
|
1956
2249
|
"file_browser_handlers",
|
|
1957
2250
|
"file_system_provider_capabilities",
|
|
@@ -2186,11 +2479,13 @@ const HARD_APIS = new Set([
|
|
|
2186
2479
|
"pageCapture",
|
|
2187
2480
|
"desktopCapture"
|
|
2188
2481
|
]);
|
|
2189
|
-
function scanApiUsage(roots) {
|
|
2482
|
+
function scanApiUsage(roots, excluded = []) {
|
|
2190
2483
|
const used = new Set();
|
|
2484
|
+
const skip = new Set(excluded.map((d)=>node_path.resolve(d)));
|
|
2191
2485
|
let filesRead = 0;
|
|
2192
2486
|
const walk = (dir, depth)=>{
|
|
2193
2487
|
if (depth > 6 || filesRead > 300) return;
|
|
2488
|
+
if (skip.has(node_path.resolve(dir))) return;
|
|
2194
2489
|
let entries;
|
|
2195
2490
|
try {
|
|
2196
2491
|
entries = node_fs.readdirSync(dir, {
|
|
@@ -2296,7 +2591,7 @@ async function manifest_validate_handler(args) {
|
|
|
2296
2591
|
...view.permissions ?? [],
|
|
2297
2592
|
...view.optional_permissions ?? []
|
|
2298
2593
|
])if ("string" == typeof p) declaredPermSet.add(p);
|
|
2299
|
-
const usedApis = scanApiUsage(roots);
|
|
2594
|
+
const usedApis = scanApiUsage(roots, roots.map((r)=>node_path.join(r, "extensions")));
|
|
2300
2595
|
for (const api of usedApis){
|
|
2301
2596
|
const perm = API_PERMISSION[api];
|
|
2302
2597
|
if (declaredPermSet.has(perm)) continue;
|
|
@@ -4669,27 +4964,40 @@ async function reload_handler(args) {
|
|
|
4669
4964
|
})
|
|
4670
4965
|
], args.projectPath, args.timeout);
|
|
4671
4966
|
}
|
|
4672
|
-
async function pollForTarget(port, url, budgetMs) {
|
|
4967
|
+
async function pollForTarget(port, url, budgetMs, navigatedTargetId) {
|
|
4673
4968
|
const deadline = Date.now() + budgetMs;
|
|
4674
4969
|
const wanted = url.replace(/#.*$/, "");
|
|
4970
|
+
let redirected = null;
|
|
4675
4971
|
for(;;){
|
|
4676
4972
|
try {
|
|
4677
4973
|
const targets = await CDPClient.discoverTargets(port);
|
|
4678
4974
|
for (const t of targets){
|
|
4679
4975
|
const tUrl = String(t.url ?? "");
|
|
4680
|
-
if ("page"
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
}
|
|
4976
|
+
if ("page" !== t.type) continue;
|
|
4977
|
+
const title = "string" == typeof t.title ? t.title : void 0;
|
|
4978
|
+
if (tUrl === wanted || tUrl.startsWith(wanted)) return {
|
|
4979
|
+
id: String(t.id),
|
|
4980
|
+
url: tUrl,
|
|
4981
|
+
title
|
|
4982
|
+
};
|
|
4983
|
+
if (navigatedTargetId && String(t.id) === navigatedTargetId && tUrl && "about:blank" !== tUrl && !tUrl.startsWith("chrome-error://")) redirected = {
|
|
4984
|
+
id: String(t.id),
|
|
4985
|
+
url: tUrl,
|
|
4986
|
+
title,
|
|
4987
|
+
redirectedFrom: url
|
|
4988
|
+
};
|
|
4687
4989
|
}
|
|
4688
4990
|
} catch {}
|
|
4689
|
-
if (Date.now() >= deadline) return
|
|
4991
|
+
if (Date.now() >= deadline) return redirected;
|
|
4690
4992
|
await new Promise((r)=>setTimeout(r, 250));
|
|
4691
4993
|
}
|
|
4692
4994
|
}
|
|
4995
|
+
function isDisposableTab(tabUrl, destination) {
|
|
4996
|
+
if (!tabUrl || "about:blank" === tabUrl) return true;
|
|
4997
|
+
if (/^chrome:\/\/(newtab|new-tab-page)/.test(tabUrl)) return true;
|
|
4998
|
+
const origin = destination.match(/^chrome-extension:\/\/[a-p]{32}\//)?.[0];
|
|
4999
|
+
return Boolean(origin && tabUrl.startsWith(origin));
|
|
5000
|
+
}
|
|
4693
5001
|
async function navigateToUrl(projectPath, browser, url, timeout) {
|
|
4694
5002
|
if (!isChromiumFamily(browser)) return navigateToUrlViaBridge(projectPath, browser, url, timeout);
|
|
4695
5003
|
const resolved = await resolveCdpPort(projectPath, browser);
|
|
@@ -4704,30 +5012,49 @@ async function navigateToUrl(projectPath, browser, url, timeout) {
|
|
|
4704
5012
|
try {
|
|
4705
5013
|
const targets = await CDPClient.discoverTargets(resolved.port);
|
|
4706
5014
|
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
5015
|
const browserWsUrl = await CDPClient.discoverBrowserWsUrl(resolved.port);
|
|
4716
5016
|
await cdp.connect(browserWsUrl);
|
|
4717
|
-
const
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
if (
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
5017
|
+
const reusable = pageTargets.find((t)=>isDisposableTab(String(t.url ?? ""), url));
|
|
5018
|
+
let navigatedTargetId;
|
|
5019
|
+
let openedNewTab = false;
|
|
5020
|
+
if (reusable) {
|
|
5021
|
+
navigatedTargetId = String(reusable.id);
|
|
5022
|
+
const sessionId = await cdp.attachToTarget(navigatedTargetId);
|
|
5023
|
+
await cdp.navigate(sessionId, url);
|
|
5024
|
+
} else {
|
|
5025
|
+
const created = await cdp.sendCommand("Target.createTarget", {
|
|
5026
|
+
url,
|
|
5027
|
+
background: true
|
|
5028
|
+
}).catch(()=>cdp.sendCommand("Target.createTarget", {
|
|
5029
|
+
url
|
|
5030
|
+
}));
|
|
5031
|
+
navigatedTargetId = "string" == typeof created?.targetId ? created.targetId : void 0;
|
|
5032
|
+
openedNewTab = true;
|
|
5033
|
+
}
|
|
5034
|
+
const settled = await pollForTarget(resolved.port, url, 6000, navigatedTargetId);
|
|
5035
|
+
if (!settled) {
|
|
5036
|
+
const isExtensionPage = url.startsWith("chrome-extension://");
|
|
5037
|
+
return JSON.stringify({
|
|
5038
|
+
ok: false,
|
|
5039
|
+
error: {
|
|
5040
|
+
name: "NavigateFailed",
|
|
5041
|
+
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."}`
|
|
5042
|
+
},
|
|
5043
|
+
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."
|
|
5044
|
+
});
|
|
5045
|
+
}
|
|
4728
5046
|
return JSON.stringify({
|
|
4729
5047
|
ok: true,
|
|
4730
5048
|
navigated: url,
|
|
5049
|
+
...openedNewTab ? {
|
|
5050
|
+
openedNewTab: true
|
|
5051
|
+
} : {},
|
|
5052
|
+
...settled.redirectedFrom ? {
|
|
5053
|
+
redirected: {
|
|
5054
|
+
from: settled.redirectedFrom,
|
|
5055
|
+
to: settled.url
|
|
5056
|
+
}
|
|
5057
|
+
} : {},
|
|
4731
5058
|
target: {
|
|
4732
5059
|
targetId: settled.id,
|
|
4733
5060
|
title: settled.title,
|
|
@@ -4980,6 +5307,39 @@ async function openSurfaceAsTab(projectPath, browser, surface) {
|
|
|
4980
5307
|
} catch {}
|
|
4981
5308
|
return raw;
|
|
4982
5309
|
}
|
|
5310
|
+
async function confirmSurfaceTarget(projectPath, browser, surface, raw) {
|
|
5311
|
+
if (!isChromiumFamily(browser)) return raw;
|
|
5312
|
+
const doc = surfaceDocument(projectPath, browser, surface);
|
|
5313
|
+
if (!doc) return raw;
|
|
5314
|
+
let parsed;
|
|
5315
|
+
try {
|
|
5316
|
+
parsed = JSON.parse(raw);
|
|
5317
|
+
} catch {
|
|
5318
|
+
return raw;
|
|
5319
|
+
}
|
|
5320
|
+
if (parsed?.ok === false) return raw;
|
|
5321
|
+
const resolved = await resolveCdpPort(projectPath, browser);
|
|
5322
|
+
const extensionId = resolved ? await resolveExtensionId(projectPath, browser) : null;
|
|
5323
|
+
if (!resolved || !extensionId) return raw;
|
|
5324
|
+
const wanted = `chrome-extension://${extensionId}/${doc}`;
|
|
5325
|
+
const settled = await pollForTarget(resolved.port, wanted, 3000);
|
|
5326
|
+
if (settled) {
|
|
5327
|
+
parsed.surfaceTarget = {
|
|
5328
|
+
targetId: settled.id,
|
|
5329
|
+
url: settled.url
|
|
5330
|
+
};
|
|
5331
|
+
return JSON.stringify(parsed);
|
|
5332
|
+
}
|
|
5333
|
+
return JSON.stringify({
|
|
5334
|
+
ok: false,
|
|
5335
|
+
error: {
|
|
5336
|
+
name: "SurfaceDidNotOpen",
|
|
5337
|
+
message: `The engine reported the ${surface} as opened, but no page target for ${wanted} appeared within 3s, so nothing is there to inspect.`
|
|
5338
|
+
},
|
|
5339
|
+
engineResult: parsed,
|
|
5340
|
+
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.`
|
|
5341
|
+
});
|
|
5342
|
+
}
|
|
4983
5343
|
const open_schema = {
|
|
4984
5344
|
name: "extension_open",
|
|
4985
5345
|
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`.",
|
|
@@ -5031,6 +5391,11 @@ const open_schema = {
|
|
|
5031
5391
|
]
|
|
5032
5392
|
}
|
|
5033
5393
|
};
|
|
5394
|
+
function sessionIsHeadless() {
|
|
5395
|
+
if (/^(1|true)$/i.test(process.env.EXTENSION_HEADLESS ?? "")) return true;
|
|
5396
|
+
return /(^|\s|=)-{1,2}headless\b/i.test(process.env.EXTENSION_BROWSER_FLAGS ?? "");
|
|
5397
|
+
}
|
|
5398
|
+
const HEADED_RELAUNCH = "start a headed session: extension_dev with replace: true, and in the environment set EXTENSION_HEADLESS=0 AND clear EXTENSION_BROWSER_FLAGS (it may carry --headless=new, which keeps the window hidden even with EXTENSION_HEADLESS=0)";
|
|
5034
5399
|
async function open_handler(args) {
|
|
5035
5400
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
5036
5401
|
if (args.url) return navigateToUrl(args.projectPath, browser, args.url, args.timeout);
|
|
@@ -5075,7 +5440,7 @@ async function open_handler(args) {
|
|
|
5075
5440
|
cli.push("--browser", browser);
|
|
5076
5441
|
if (null != args.timeout) cli.push("--timeout", String(args.timeout));
|
|
5077
5442
|
const raw = await runActVerb(cli, args.projectPath, args.timeout);
|
|
5078
|
-
const headless =
|
|
5443
|
+
const headless = sessionIsHeadless();
|
|
5079
5444
|
if (headless && [
|
|
5080
5445
|
"popup",
|
|
5081
5446
|
"action",
|
|
@@ -5089,16 +5454,16 @@ async function open_handler(args) {
|
|
|
5089
5454
|
try {
|
|
5090
5455
|
const parsedFallback = JSON.parse(fallback);
|
|
5091
5456
|
if (parsedFallback?.ok) {
|
|
5092
|
-
parsedFallback.note =
|
|
5457
|
+
parsedFallback.note = `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.`;
|
|
5093
5458
|
return JSON.stringify(parsedFallback);
|
|
5094
5459
|
}
|
|
5095
5460
|
} catch {}
|
|
5096
5461
|
}
|
|
5097
|
-
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." :
|
|
5462
|
+
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}.`;
|
|
5098
5463
|
return JSON.stringify(parsed);
|
|
5099
5464
|
}
|
|
5100
5465
|
} catch {}
|
|
5101
|
-
return raw;
|
|
5466
|
+
return AS_TAB_SURFACES.includes(args.surface) ? confirmSurfaceTarget(args.projectPath, browser, args.surface, raw) : raw;
|
|
5102
5467
|
}
|
|
5103
5468
|
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
5469
|
function filterPageTargets(raw) {
|
|
@@ -5480,11 +5845,78 @@ function readValidCredentials(nowSeconds = Math.floor(Date.now() / 1000)) {
|
|
|
5480
5845
|
if (creds.expiresAt && creds.expiresAt <= nowSeconds) return null;
|
|
5481
5846
|
return creds;
|
|
5482
5847
|
}
|
|
5483
|
-
const
|
|
5484
|
-
|
|
5848
|
+
const PROD_ORIGINS = {
|
|
5849
|
+
www: "https://www.extension.dev",
|
|
5850
|
+
console: "https://console.extension.dev",
|
|
5851
|
+
inspect: "https://inspect.extension.dev",
|
|
5852
|
+
templates: "https://templates.extension.dev",
|
|
5853
|
+
intelligence: "https://intelligence.extension.dev",
|
|
5854
|
+
registry: "https://registry.extension.land",
|
|
5855
|
+
media: "https://media.extension.land"
|
|
5856
|
+
};
|
|
5857
|
+
const DEV_LOCALHOST_ORIGINS = {
|
|
5858
|
+
www: "http://localhost:3100",
|
|
5859
|
+
console: "http://console.extension.localhost",
|
|
5860
|
+
inspect: "http://inspect.extension.localhost",
|
|
5861
|
+
templates: "http://templates.extension.localhost",
|
|
5862
|
+
intelligence: "http://intelligence.extension.localhost",
|
|
5863
|
+
registry: "https://registry.extension.land",
|
|
5864
|
+
media: "https://media.extension.land"
|
|
5865
|
+
};
|
|
5866
|
+
function strip(value) {
|
|
5867
|
+
return String(value ?? "").trim().replace(/\/+$/, "");
|
|
5868
|
+
}
|
|
5869
|
+
function isLocalOrigin(url) {
|
|
5870
|
+
const raw = strip(url);
|
|
5871
|
+
if (!raw) return false;
|
|
5872
|
+
let host;
|
|
5873
|
+
try {
|
|
5874
|
+
host = new URL(raw).hostname;
|
|
5875
|
+
} catch {
|
|
5876
|
+
return false;
|
|
5877
|
+
}
|
|
5878
|
+
return "localhost" === host || "127.0.0.1" === host || "::1" === host || "[::1]" === host || "extension.localhost" === host || host.endsWith(".extension.localhost");
|
|
5879
|
+
}
|
|
5880
|
+
function resolveOrigins(overrides = {}, opts = {}) {
|
|
5881
|
+
const devLike = isLocalOrigin(overrides.www) || isLocalOrigin(overrides.console) || isLocalOrigin(opts.hint);
|
|
5882
|
+
const base = devLike ? DEV_LOCALHOST_ORIGINS : PROD_ORIGINS;
|
|
5883
|
+
const pick = (key)=>strip(overrides[key]) || base[key];
|
|
5884
|
+
return {
|
|
5885
|
+
www: pick("www"),
|
|
5886
|
+
console: pick("console"),
|
|
5887
|
+
inspect: pick("inspect"),
|
|
5888
|
+
templates: pick("templates"),
|
|
5889
|
+
intelligence: pick("intelligence"),
|
|
5890
|
+
registry: pick("registry"),
|
|
5891
|
+
media: pick("media")
|
|
5892
|
+
};
|
|
5893
|
+
}
|
|
5894
|
+
const seg = (value)=>encodeURIComponent(String(value));
|
|
5895
|
+
function urls_paths_join(base, sub) {
|
|
5896
|
+
if (!sub) return base;
|
|
5897
|
+
return `${base}/${sub.replace(/^\/+/, "")}`;
|
|
5898
|
+
}
|
|
5899
|
+
function consoleProjectPath(ref, page = "") {
|
|
5900
|
+
return urls_paths_join(`/${seg(ref.workspace)}/${seg(ref.project)}`, page);
|
|
5901
|
+
}
|
|
5902
|
+
PROD_ORIGINS.registry;
|
|
5903
|
+
function mcpOrigins(apiHint) {
|
|
5904
|
+
const www = String(apiHint || process.env.EXTENSION_DEV_API_URL || "").trim() || void 0;
|
|
5905
|
+
return resolveOrigins({
|
|
5906
|
+
www,
|
|
5907
|
+
console: process.env.EXTENSION_DEV_CONSOLE_URL,
|
|
5908
|
+
inspect: process.env.EXTENSION_DEV_INSPECT_URL,
|
|
5909
|
+
registry: process.env.EXTENSION_DEV_REGISTRY_URL,
|
|
5910
|
+
media: process.env.EXTENSION_MEDIA_ORIGIN
|
|
5911
|
+
}, {
|
|
5912
|
+
hint: www
|
|
5913
|
+
});
|
|
5914
|
+
}
|
|
5915
|
+
function consoleBase(apiHint) {
|
|
5916
|
+
return mcpOrigins(apiHint).console;
|
|
5917
|
+
}
|
|
5485
5918
|
function registryBase() {
|
|
5486
|
-
|
|
5487
|
-
return (fromEnv || REGISTRY_BASE_DEFAULT).replace(/\/+$/, "");
|
|
5919
|
+
return mcpOrigins().registry;
|
|
5488
5920
|
}
|
|
5489
5921
|
function resolveProjectRef(overrides) {
|
|
5490
5922
|
const workspace = String(overrides?.workspace || "").trim();
|
|
@@ -5505,9 +5937,10 @@ function resolveProjectRef(overrides) {
|
|
|
5505
5937
|
function registryFileUrl(ref, file) {
|
|
5506
5938
|
return `${registryBase()}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/_extension-dev/${file}`;
|
|
5507
5939
|
}
|
|
5508
|
-
function consoleProjectUrl(ref, page) {
|
|
5509
|
-
|
|
5510
|
-
|
|
5940
|
+
function consoleProjectUrl(ref, page, apiHint) {
|
|
5941
|
+
const base = consoleBase(apiHint);
|
|
5942
|
+
if (!ref) return base;
|
|
5943
|
+
return `${base}${consoleProjectPath(ref, page)}`;
|
|
5511
5944
|
}
|
|
5512
5945
|
async function fetchRegistryJson(url, fetchImpl = fetch) {
|
|
5513
5946
|
let res;
|
|
@@ -5583,12 +6016,12 @@ function parseBuildIndex(json) {
|
|
|
5583
6016
|
}
|
|
5584
6017
|
return out;
|
|
5585
6018
|
}
|
|
5586
|
-
const DEFAULT_API =
|
|
6019
|
+
const DEFAULT_API = PROD_ORIGINS.www;
|
|
5587
6020
|
function tokenTtlNote(workspaceSlug, projectSlug) {
|
|
5588
6021
|
const tokensUrl = workspaceSlug && projectSlug ? consoleProjectUrl({
|
|
5589
6022
|
workspace: workspaceSlug,
|
|
5590
6023
|
project: projectSlug
|
|
5591
|
-
}, "settings/access-tokens") :
|
|
6024
|
+
}, "settings/access-tokens") : consoleBase();
|
|
5592
6025
|
return `extension.dev CLI tokens live at most 7 days (server-enforced). CI pipelines must re-mint before expiry on the console's Access tokens page: ${tokensUrl}`;
|
|
5593
6026
|
}
|
|
5594
6027
|
function resolveApiBase(api) {
|
|
@@ -5829,7 +6262,6 @@ async function publish_handler(args) {
|
|
|
5829
6262
|
}
|
|
5830
6263
|
return JSON.stringify(data);
|
|
5831
6264
|
}
|
|
5832
|
-
const release_promote_DEFAULT_API = "https://www.extension.dev";
|
|
5833
6265
|
const release_promote_schema = {
|
|
5834
6266
|
name: "extension_release_promote",
|
|
5835
6267
|
description: "Promote a built extension to a release channel (e.g. stable, preview, beta) on extension.dev, headless. Auth-gated: uses your stored login (extension_login) or a release token in EXTENSION_DEV_TOKEN (mint and revoke it in the dashboard under project settings -> Access tokens; tokens live at most 7 days, so CI must re-mint before expiry). Posts to the platform's CLI release endpoint; the project is identified by the token. Cutting a version-bump PR is not available headlessly (it writes to your source repo and needs an interactive login).",
|
|
@@ -5889,7 +6321,7 @@ async function release_promote_handler(args) {
|
|
|
5889
6321
|
const buildId = String(args.buildId || "").trim();
|
|
5890
6322
|
const channel = String(args.channel || "").trim();
|
|
5891
6323
|
if (!buildId || !channel) return release_promote_fail("ReleaseInputError", "buildId and channel are required.");
|
|
5892
|
-
const apiCheck = safeApiBase(
|
|
6324
|
+
const apiCheck = safeApiBase(resolveApiBase(args.api));
|
|
5893
6325
|
if (!apiCheck.ok) return release_promote_fail("ReleaseConfigError", apiCheck.message);
|
|
5894
6326
|
const url = `${apiCheck.base}/api/cli/release/promote`;
|
|
5895
6327
|
const body = {
|
|
@@ -5927,7 +6359,7 @@ async function release_promote_handler(args) {
|
|
|
5927
6359
|
const enrich = {};
|
|
5928
6360
|
const ref = resolveProjectRef();
|
|
5929
6361
|
if (404 === res.status || "UNKNOWN_BUILD" === code) {
|
|
5930
|
-
enrich.buildsPageUrl = consoleProjectUrl(ref, "builds");
|
|
6362
|
+
enrich.buildsPageUrl = consoleProjectUrl(ref, "builds", args.api);
|
|
5931
6363
|
enrich.hint = "Run extension_release_list to see this project's channels, their promoted shas, and recent builds.";
|
|
5932
6364
|
if (ref) {
|
|
5933
6365
|
const channelsUrl = registryFileUrl(ref, "channels.json");
|
|
@@ -6027,7 +6459,6 @@ async function release_list_handler(args) {
|
|
|
6027
6459
|
if (!buildsRes.ok) result.buildsUnavailable = `builds/index.json unreadable: ${buildsRes.message}`;
|
|
6028
6460
|
return JSON.stringify(result);
|
|
6029
6461
|
}
|
|
6030
|
-
const deploy_DEFAULT_API = "https://www.extension.dev";
|
|
6031
6462
|
function storeMdWarnings(browsers, cwd) {
|
|
6032
6463
|
const wantsFirefox = browsers.includes("firefox");
|
|
6033
6464
|
const wantsEdge = browsers.includes("edge");
|
|
@@ -6056,7 +6487,7 @@ function storeMdWarnings(browsers, cwd) {
|
|
|
6056
6487
|
}
|
|
6057
6488
|
const deploy_schema = {
|
|
6058
6489
|
name: "extension_deploy",
|
|
6059
|
-
description: "Submit a built extension to the Chrome Web Store, Firefox AMO,
|
|
6490
|
+
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
6491
|
inputSchema: {
|
|
6061
6492
|
type: "object",
|
|
6062
6493
|
properties: {
|
|
@@ -6067,7 +6498,8 @@ const deploy_schema = {
|
|
|
6067
6498
|
enum: [
|
|
6068
6499
|
"chrome",
|
|
6069
6500
|
"firefox",
|
|
6070
|
-
"edge"
|
|
6501
|
+
"edge",
|
|
6502
|
+
"safari"
|
|
6071
6503
|
]
|
|
6072
6504
|
},
|
|
6073
6505
|
description: "Stores to submit to."
|
|
@@ -6113,10 +6545,10 @@ async function deploy_handler(args) {
|
|
|
6113
6545
|
const token = resolveToken();
|
|
6114
6546
|
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).");
|
|
6115
6547
|
const browsers = (Array.isArray(args.browsers) ? args.browsers : []).map((b)=>String(b).trim().toLowerCase()).filter(Boolean);
|
|
6116
|
-
if (0 === browsers.length) return deploy_fail("DeployInputError", 'browsers is required (e.g. ["chrome","firefox","edge"]).');
|
|
6548
|
+
if (0 === browsers.length) return deploy_fail("DeployInputError", 'browsers is required (e.g. ["chrome","firefox","edge","safari"]).');
|
|
6117
6549
|
const buildSha = String(args.buildSha || "").trim();
|
|
6118
6550
|
if (!buildSha) return deploy_fail("DeployInputError", "buildSha is required (the built commit to submit).");
|
|
6119
|
-
const apiCheck = safeApiBase(
|
|
6551
|
+
const apiCheck = safeApiBase(resolveApiBase(args.api));
|
|
6120
6552
|
if (!apiCheck.ok) return deploy_fail("DeployConfigError", apiCheck.message);
|
|
6121
6553
|
const url = `${apiCheck.base}/api/cli/stores/submit`;
|
|
6122
6554
|
const dryRun = false !== args.dryRun;
|
|
@@ -6161,7 +6593,7 @@ async function deploy_handler(args) {
|
|
|
6161
6593
|
};
|
|
6162
6594
|
if (dryRun) {
|
|
6163
6595
|
const ref = resolveProjectRef();
|
|
6164
|
-
const consoleStoresUrl = consoleProjectUrl(ref, "stores");
|
|
6596
|
+
const consoleStoresUrl = consoleProjectUrl(ref, "stores", args.api);
|
|
6165
6597
|
const storeModeNote = `Store publish mode (draft / skip-publish / live) is not readable with the CLI token, so it cannot be verified from here; check per-store settings at ${consoleStoresUrl}.`;
|
|
6166
6598
|
let health = null;
|
|
6167
6599
|
let healthUnreadable = null;
|
|
@@ -6236,11 +6668,12 @@ async function deploy_handler(args) {
|
|
|
6236
6668
|
const KNOWN_STORES = [
|
|
6237
6669
|
"chrome",
|
|
6238
6670
|
"firefox",
|
|
6239
|
-
"edge"
|
|
6671
|
+
"edge",
|
|
6672
|
+
"safari"
|
|
6240
6673
|
];
|
|
6241
6674
|
const store_status_schema = {
|
|
6242
6675
|
name: "extension_store_status",
|
|
6243
|
-
description: "Report the project's browser-store state after an extension_deploy submission: per store (chrome, firefox, edge) whether it is configured, its latest credential health check, the last recorded submission (version, status, store URL, submitted-at), and the latest review status. Reads the project's public registry (registry.extension.land: stores/health.json, stores/status.json, stores/submissions.json) - read-only, dispatches nothing, no auth needed for public projects. Defaults to the logged-in project (extension_login); pass workspace + project to inspect another public project. Registry state can lag the store dashboards by up to a polling interval.",
|
|
6676
|
+
description: "Report the project's browser-store state after an extension_deploy submission: per store (chrome, firefox, edge, safari) whether it is configured, its latest credential health check, the last recorded submission (version, status, store URL, submitted-at), and the latest review status. Reads the project's public registry (registry.extension.land: stores/health.json, stores/status.json, stores/submissions.json) - read-only, dispatches nothing, no auth needed for public projects. Defaults to the logged-in project (extension_login); pass workspace + project to inspect another public project. Registry state can lag the store dashboards by up to a polling interval.",
|
|
6244
6677
|
inputSchema: {
|
|
6245
6678
|
type: "object",
|
|
6246
6679
|
properties: {
|
|
@@ -6436,6 +6869,63 @@ async function store_status_handler(args) {
|
|
|
6436
6869
|
if (!submissionsRes.ok && 404 !== submissionsRes.status) result.submissionsUnavailable = `stores/submissions.json unreadable: ${submissionsRes.message}`;
|
|
6437
6870
|
return JSON.stringify(result);
|
|
6438
6871
|
}
|
|
6872
|
+
const ENGINE_COMPANION_IDS = new Set([
|
|
6873
|
+
"kgdaecdpfkikjncaalnmmnjjfpofkcbl",
|
|
6874
|
+
CARRIER_EXTENSION_ID
|
|
6875
|
+
]);
|
|
6876
|
+
const EXTENSION_URL = /^chrome-extension:\/\/([a-p]{32})\//i;
|
|
6877
|
+
async function verifyGuestLoaded(projectPath, browser, options) {
|
|
6878
|
+
let cdpPort;
|
|
6879
|
+
try {
|
|
6880
|
+
const resolved = await resolveCdpPort(projectPath, browser, {
|
|
6881
|
+
waitMs: options?.waitMs ?? 0
|
|
6882
|
+
});
|
|
6883
|
+
if (!resolved) return {
|
|
6884
|
+
checked: false,
|
|
6885
|
+
loaded: false,
|
|
6886
|
+
guestTargets: [],
|
|
6887
|
+
guestIds: [],
|
|
6888
|
+
reason: "No CDP port in the session's ready contract, so the browser's target list could not be queried (a headless Chromium session still exposes one; a gecko/Firefox session does not)."
|
|
6889
|
+
};
|
|
6890
|
+
cdpPort = resolved.port;
|
|
6891
|
+
const timeoutMs = options?.timeoutMs ?? 3000;
|
|
6892
|
+
const targets = await Promise.race([
|
|
6893
|
+
CDPClient.discoverTargets(cdpPort),
|
|
6894
|
+
new Promise((_, reject)=>setTimeout(()=>reject(new Error(`CDP /json timed out after ${timeoutMs}ms`)), timeoutMs))
|
|
6895
|
+
]);
|
|
6896
|
+
const guestTargets = [];
|
|
6897
|
+
for (const t of targets){
|
|
6898
|
+
const match = EXTENSION_URL.exec(String(t.url ?? ""));
|
|
6899
|
+
if (!match) continue;
|
|
6900
|
+
const id = match[1].toLowerCase();
|
|
6901
|
+
if (!ENGINE_COMPANION_IDS.has(id)) guestTargets.push({
|
|
6902
|
+
id,
|
|
6903
|
+
type: String(t.type),
|
|
6904
|
+
url: String(t.url)
|
|
6905
|
+
});
|
|
6906
|
+
}
|
|
6907
|
+
const guestIds = [
|
|
6908
|
+
...new Set(guestTargets.map((t)=>t.id))
|
|
6909
|
+
];
|
|
6910
|
+
return {
|
|
6911
|
+
checked: true,
|
|
6912
|
+
loaded: guestTargets.length > 0,
|
|
6913
|
+
guestTargets,
|
|
6914
|
+
guestIds,
|
|
6915
|
+
cdpPort,
|
|
6916
|
+
reason: guestTargets.length > 0 ? `The browser lists ${guestIds.length} extension target${1 === guestIds.length ? "" : "s"} that are not the engine companion (${guestIds.join(", ")}), so the guest is loaded.` : "The browser's target list has no chrome-extension:// target other than the engine's devtools companion. On Chrome this is the signature of a silently rejected --load-extension (extension.js BUGS_TO_FIX §83): the CLI and ready.json cannot see it."
|
|
6917
|
+
};
|
|
6918
|
+
} catch (err) {
|
|
6919
|
+
return {
|
|
6920
|
+
checked: false,
|
|
6921
|
+
loaded: false,
|
|
6922
|
+
guestTargets: [],
|
|
6923
|
+
guestIds: [],
|
|
6924
|
+
cdpPort,
|
|
6925
|
+
reason: `Could not query the browser's target list${cdpPort ? ` on CDP port ${cdpPort}` : ""}: ${err?.message ?? String(err)}.`
|
|
6926
|
+
};
|
|
6927
|
+
}
|
|
6928
|
+
}
|
|
6439
6929
|
function doctor_readReadyContract(projectPath, browser) {
|
|
6440
6930
|
try {
|
|
6441
6931
|
const raw = node_fs.readFileSync(node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json"), "utf8");
|
|
@@ -6631,7 +7121,7 @@ function wait_isAlive(pid) {
|
|
|
6631
7121
|
}
|
|
6632
7122
|
const wait_schema = {
|
|
6633
7123
|
name: "extension_wait",
|
|
6634
|
-
description: "Wait for a running dev or start session to be ready. Polls the ready.json contract file and returns structured status with
|
|
7124
|
+
description: "Wait for a running dev or start session to be ready. Polls the ready.json contract file and returns structured status with these facts: compiled (the compiler finished), browserAttached (the engine's runtime executor connected), and for a browser session guestLoaded (the browser's OWN target list actually shows your extension). guestLoaded is the trustworthy load signal: it catches a silently rejected --load-extension that leaves ready.json stamped attached with empty logs (extension.js BUGS_TO_FIX §83); it is null when it could not be checked (no CDP port, e.g. a gecko session). Every result reports budgetMs (this call's wait budget) and elapsedMs; on status:'timeout' call again to keep waiting (polling resumes on the same contract). In a noBrowser (build-only) session it returns as soon as the compile lands instead of waiting for a browser that will never attach. Ports in the result come from the ready contract, so they always match what the dev server actually bound.",
|
|
6635
7125
|
inputSchema: {
|
|
6636
7126
|
type: "object",
|
|
6637
7127
|
properties: {
|
|
@@ -6707,10 +7197,20 @@ async function wait_handler(args) {
|
|
|
6707
7197
|
continue;
|
|
6708
7198
|
}
|
|
6709
7199
|
const runtimeErrors = recentErrorLogs(args.projectPath, browser, 3);
|
|
7200
|
+
const guestCheck = await verifyGuestLoaded(args.projectPath, browser);
|
|
7201
|
+
const warnings = [];
|
|
7202
|
+
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.`);
|
|
7203
|
+
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.");
|
|
6710
7204
|
return JSON.stringify({
|
|
6711
7205
|
status: "ready",
|
|
6712
7206
|
compiled: true,
|
|
6713
7207
|
browserAttached: true,
|
|
7208
|
+
guestLoaded: guestCheck.checked ? guestCheck.loaded : null,
|
|
7209
|
+
...guestCheck.checked ? {
|
|
7210
|
+
guestIds: guestCheck.guestIds
|
|
7211
|
+
} : {
|
|
7212
|
+
guestLoadNote: guestCheck.reason
|
|
7213
|
+
},
|
|
6714
7214
|
command: contract.command,
|
|
6715
7215
|
browser: contract.browser,
|
|
6716
7216
|
port: contract.port,
|
|
@@ -6722,8 +7222,10 @@ async function wait_handler(args) {
|
|
|
6722
7222
|
budgetMs,
|
|
6723
7223
|
elapsedMs: Date.now() - start,
|
|
6724
7224
|
...runtimeErrors.length ? {
|
|
6725
|
-
runtimeErrors
|
|
6726
|
-
|
|
7225
|
+
runtimeErrors
|
|
7226
|
+
} : {},
|
|
7227
|
+
...warnings.length ? {
|
|
7228
|
+
warning: warnings.join(" ")
|
|
6727
7229
|
} : {}
|
|
6728
7230
|
});
|
|
6729
7231
|
}
|
|
@@ -7422,7 +7924,10 @@ const logout_schema = {
|
|
|
7422
7924
|
};
|
|
7423
7925
|
async function logout_handler() {
|
|
7424
7926
|
const creds = readCredentials();
|
|
7425
|
-
const revokeUrl = creds?.workspaceSlug && creds?.projectSlug ?
|
|
7927
|
+
const revokeUrl = creds?.workspaceSlug && creds?.projectSlug ? consoleProjectUrl({
|
|
7928
|
+
workspace: creds.workspaceSlug,
|
|
7929
|
+
project: creds.projectSlug
|
|
7930
|
+
}, "settings/access-tokens") : null;
|
|
7426
7931
|
const result = clearCredentials();
|
|
7427
7932
|
return JSON.stringify({
|
|
7428
7933
|
ok: true,
|