@extension.dev/mcp 5.5.0 → 5.5.2
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 +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +52 -2
- package/README.md +3 -3
- package/dist/module.js +735 -228
- package/dist/src/lib/bridge-tabs.d.ts +14 -0
- package/dist/src/lib/login-flow.d.ts +8 -0
- package/dist/src/tools/store-status.d.ts +56 -0
- package/package.json +3 -3
- package/server.json +3 -3
package/dist/module.js
CHANGED
|
@@ -193,6 +193,14 @@ __webpack_require__.d(storage_namespaceObject, {
|
|
|
193
193
|
handler: ()=>storage_handler,
|
|
194
194
|
schema: ()=>storage_schema
|
|
195
195
|
});
|
|
196
|
+
var store_status_namespaceObject = {};
|
|
197
|
+
__webpack_require__.r(store_status_namespaceObject);
|
|
198
|
+
__webpack_require__.d(store_status_namespaceObject, {
|
|
199
|
+
handler: ()=>store_status_handler,
|
|
200
|
+
latestSubmissionsByStore: ()=>latestSubmissionsByStore,
|
|
201
|
+
normalizeStoresStatus: ()=>normalizeStoresStatus,
|
|
202
|
+
schema: ()=>store_status_schema
|
|
203
|
+
});
|
|
196
204
|
var uninstall_browser_namespaceObject = {};
|
|
197
205
|
__webpack_require__.r(uninstall_browser_namespaceObject);
|
|
198
206
|
__webpack_require__.d(uninstall_browser_namespaceObject, {
|
|
@@ -211,7 +219,7 @@ __webpack_require__.d(whoami_namespaceObject, {
|
|
|
211
219
|
handler: ()=>whoami_handler,
|
|
212
220
|
schema: ()=>whoami_schema
|
|
213
221
|
});
|
|
214
|
-
var package_namespaceObject = JSON.parse('{"rE":"5.5.
|
|
222
|
+
var package_namespaceObject = JSON.parse('{"rE":"5.5.2","El":{"OP":"^4.0.13"}}');
|
|
215
223
|
function scaffoldEnginePin(projectPath) {
|
|
216
224
|
try {
|
|
217
225
|
const pkg = JSON.parse(node_fs.readFileSync(node_path.join(projectPath, "package.json"), "utf8"));
|
|
@@ -3004,9 +3012,178 @@ function isCdpEndpoint(port) {
|
|
|
3004
3012
|
});
|
|
3005
3013
|
});
|
|
3006
3014
|
}
|
|
3015
|
+
function toMcpSpeak(text) {
|
|
3016
|
+
return text.replace(/`?extension dev(?: [^\s`]*)? --browser[= ]([\w-]+) --allow-control`?/g, 'extension_dev with { browser: "$1", allowControl: true }').replace(/--allow-control/g, "allowControl: true (extension_dev)").replace(/--allow-eval/g, "allowEval: true (extension_dev)").replace(/Use --context page --tab <id>/g, 'Use context: "page" (targets the active tab; pass url or tab to pick another)').replace(/--context[= ](background|popup|options|sidebar|devtools|newtab|history|bookmarks|content|page)\b/g, 'context: "$1"').replace(/--tab[= ](\d+|<[\w-]+>)/g, "tab: $1").replace(/--url[= ]"([^"]+)"/g, 'url: "$1"').replace(/--url[= ](<[\w-]+>|\S*(?:\/\/|\*)\S*)/g, 'url: "$1"').replace(/--browser[= ]([\w]+-based|chrome|chromium|edge|brave|opera|vivaldi|yandex|firefox|waterfox|librewolf|safari)\b/g, 'browser: "$1"').replace(/--timeout[= ](\d+)/g, "timeout: $1").replace(/`extension dev`/g, "extension_dev").replace(/\bextension dev\b/g, "extension_dev").replace(/--tab\b/g, "`tab`").replace(/--url\b/g, "`url`").replace(/--context\b/g, "`context`").replace(/--browser\b/g, "`browser`").replace(/--timeout\b/g, "`timeout`");
|
|
3017
|
+
}
|
|
3018
|
+
function withSessionContext(message, projectPath) {
|
|
3019
|
+
const isControlError = /no active control channel|control channel refused|\b1006\b|no executor connected|is the session started with allowControl/i.test(message);
|
|
3020
|
+
if (!isControlError) return message;
|
|
3021
|
+
const dead = deadReadySession(projectPath);
|
|
3022
|
+
if (dead) return `${message}\nLikely cause: the dev server has exited, ${dead.browser} ready.json still says ready but its pid ${dead.pid} is dead. Restart with extension_dev (this is not an allowControl problem); extension_doctor confirms.`;
|
|
3023
|
+
const running = knownSessionBrowsers(projectPath);
|
|
3024
|
+
if (0 === running.length) return message;
|
|
3025
|
+
return `${message} Active session browser(s) for this project: ${running.join(", ")}, pass that as \`browser\`, or restart it via extension_dev with allowControl: true if the control channel is off.`;
|
|
3026
|
+
}
|
|
3027
|
+
function translateFrame(frame, projectPath) {
|
|
3028
|
+
if (!frame || false !== frame.ok) return frame;
|
|
3029
|
+
if (frame.error && "string" == typeof frame.error.message) frame.error.message = withSessionContext(toMcpSpeak(frame.error.message), projectPath);
|
|
3030
|
+
if ("string" == typeof frame.error?.hint) frame.error.hint = toMcpSpeak(frame.error.hint);
|
|
3031
|
+
if ("string" == typeof frame.hint) frame.hint = toMcpSpeak(frame.hint);
|
|
3032
|
+
return frame;
|
|
3033
|
+
}
|
|
3034
|
+
async function runActVerb(args, projectPath, timeoutMs) {
|
|
3035
|
+
const { code, stdout, stderr } = await runExtensionCli([
|
|
3036
|
+
...args,
|
|
3037
|
+
"--output",
|
|
3038
|
+
"json"
|
|
3039
|
+
], {
|
|
3040
|
+
cwd: projectPath,
|
|
3041
|
+
timeoutMs
|
|
3042
|
+
});
|
|
3043
|
+
const out = stdout.trim();
|
|
3044
|
+
if (out) try {
|
|
3045
|
+
const frame = JSON.parse(out);
|
|
3046
|
+
if (frame && false === frame.ok) return JSON.stringify(translateFrame(frame, projectPath));
|
|
3047
|
+
return out;
|
|
3048
|
+
} catch {}
|
|
3049
|
+
const message = stderr.trim() || `extension exited with code ${code}`;
|
|
3050
|
+
return JSON.stringify({
|
|
3051
|
+
ok: false,
|
|
3052
|
+
error: {
|
|
3053
|
+
name: "CliError",
|
|
3054
|
+
message: withSessionContext(toMcpSpeak(message), projectPath)
|
|
3055
|
+
}
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
function commonFlags(args) {
|
|
3059
|
+
const flags = [];
|
|
3060
|
+
if (args.context) flags.push("--context", args.context);
|
|
3061
|
+
if (args.url) flags.push("--url", args.url);
|
|
3062
|
+
if (null != args.tab) flags.push("--tab", String(args.tab));
|
|
3063
|
+
if (args.browser) flags.push("--browser", args.browser);
|
|
3064
|
+
if (null != args.timeout) flags.push("--timeout", String(args.timeout));
|
|
3065
|
+
return flags;
|
|
3066
|
+
}
|
|
3067
|
+
async function listBridgeTabs(projectPath, browser, timeout) {
|
|
3068
|
+
const raw = await runActVerb([
|
|
3069
|
+
"inspect",
|
|
3070
|
+
projectPath,
|
|
3071
|
+
"--list-tabs",
|
|
3072
|
+
"--browser",
|
|
3073
|
+
browser,
|
|
3074
|
+
...null != timeout ? [
|
|
3075
|
+
"--timeout",
|
|
3076
|
+
String(timeout)
|
|
3077
|
+
] : []
|
|
3078
|
+
], projectPath, timeout);
|
|
3079
|
+
let parsed;
|
|
3080
|
+
try {
|
|
3081
|
+
parsed = JSON.parse(raw);
|
|
3082
|
+
} catch {
|
|
3083
|
+
return {
|
|
3084
|
+
error: raw
|
|
3085
|
+
};
|
|
3086
|
+
}
|
|
3087
|
+
if (parsed?.ok === false) return {
|
|
3088
|
+
error: raw
|
|
3089
|
+
};
|
|
3090
|
+
const list = Array.isArray(parsed?.tabs) ? parsed.tabs : Array.isArray(parsed?.value) ? parsed.value : Array.isArray(parsed?.value?.tabs) ? parsed.value.tabs : null;
|
|
3091
|
+
if (!list) return {
|
|
3092
|
+
error: raw
|
|
3093
|
+
};
|
|
3094
|
+
return {
|
|
3095
|
+
tabs: list.map((t)=>({
|
|
3096
|
+
tabId: "number" == typeof t?.tabId ? t.tabId : "number" == typeof t?.id ? t.id : null,
|
|
3097
|
+
url: String(t?.url ?? ""),
|
|
3098
|
+
title: String(t?.title ?? "")
|
|
3099
|
+
}))
|
|
3100
|
+
};
|
|
3101
|
+
}
|
|
3102
|
+
function matchTabsByUrl(tabs, needle) {
|
|
3103
|
+
const wanted = needle.toLowerCase();
|
|
3104
|
+
const byUrl = tabs.filter((t)=>t.url.toLowerCase().includes(wanted));
|
|
3105
|
+
if (byUrl.length > 0) return byUrl;
|
|
3106
|
+
return tabs.filter((t)=>t.title.toLowerCase().includes(wanted));
|
|
3107
|
+
}
|
|
3108
|
+
async function pollForBridgeTab(projectPath, browser, url, budgetMs) {
|
|
3109
|
+
const deadline = Date.now() + budgetMs;
|
|
3110
|
+
const wanted = url.replace(/#.*$/, "");
|
|
3111
|
+
for(;;){
|
|
3112
|
+
const listed = await listBridgeTabs(projectPath, browser);
|
|
3113
|
+
if ("tabs" in listed) {
|
|
3114
|
+
for (const t of listed.tabs)if (t.url === wanted || t.url.startsWith(wanted)) return t;
|
|
3115
|
+
}
|
|
3116
|
+
if (Date.now() >= deadline) return null;
|
|
3117
|
+
await new Promise((r)=>setTimeout(r, 250));
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
async function navigateToUrlViaBridge(projectPath, browser, url, timeout) {
|
|
3121
|
+
const expression = `(async () => { const api = typeof browser !== "undefined" ? browser : chrome; const tabs = await api.tabs.query({ active: true, currentWindow: true }); const active = tabs && tabs[0]; const tab = active && active.id != null ? await api.tabs.update(active.id, { url: ${JSON.stringify(url)} }) : await api.tabs.create({ url: ${JSON.stringify(url)} }); return { tabId: tab && tab.id != null ? tab.id : null }; })()`;
|
|
3122
|
+
const raw = await runActVerb([
|
|
3123
|
+
"eval",
|
|
3124
|
+
expression,
|
|
3125
|
+
projectPath,
|
|
3126
|
+
"--context",
|
|
3127
|
+
"background",
|
|
3128
|
+
"--browser",
|
|
3129
|
+
browser,
|
|
3130
|
+
...null != timeout ? [
|
|
3131
|
+
"--timeout",
|
|
3132
|
+
String(timeout)
|
|
3133
|
+
] : []
|
|
3134
|
+
], projectPath, timeout);
|
|
3135
|
+
try {
|
|
3136
|
+
const parsed = JSON.parse(raw);
|
|
3137
|
+
if (parsed?.ok === false) {
|
|
3138
|
+
if (!parsed.hint) parsed.hint = "On this browser family URL navigation rides the agent bridge (a background eval of tabs.update), so the dev session must be started with allowEval: true (extension_dev).";
|
|
3139
|
+
return JSON.stringify(parsed);
|
|
3140
|
+
}
|
|
3141
|
+
} catch {
|
|
3142
|
+
return raw;
|
|
3143
|
+
}
|
|
3144
|
+
const settled = await pollForBridgeTab(projectPath, browser, url, null != timeout ? Math.min(timeout, 6000) : 6000);
|
|
3145
|
+
if (!settled) return JSON.stringify({
|
|
3146
|
+
ok: false,
|
|
3147
|
+
error: {
|
|
3148
|
+
name: "NavigateFailed",
|
|
3149
|
+
message: `Navigation to ${url} did not produce a tab reporting that URL. The URL may not exist, or the browser refused the navigation (Firefox rejects privileged about:/chrome: URLs and other extensions' moz-extension: pages).`
|
|
3150
|
+
},
|
|
3151
|
+
hint: "Confirm the URL, or discover open tabs with extension_dom_inspect listTabs: true. For an extension page, the path must match the BUILT manifest."
|
|
3152
|
+
});
|
|
3153
|
+
return JSON.stringify({
|
|
3154
|
+
ok: true,
|
|
3155
|
+
navigated: url,
|
|
3156
|
+
tab: {
|
|
3157
|
+
tabId: settled.tabId,
|
|
3158
|
+
url: settled.url,
|
|
3159
|
+
title: settled.title
|
|
3160
|
+
},
|
|
3161
|
+
hint: "Inspect it with extension_dom_inspect or extension_eval using url or this numeric tab id (context: 'page'/'content')."
|
|
3162
|
+
});
|
|
3163
|
+
}
|
|
3164
|
+
async function resolveBridgeBaseUrl(projectPath, browser, timeout) {
|
|
3165
|
+
const raw = await runActVerb([
|
|
3166
|
+
"eval",
|
|
3167
|
+
'(typeof browser !== "undefined" ? browser : chrome).runtime.getURL("")',
|
|
3168
|
+
projectPath,
|
|
3169
|
+
"--context",
|
|
3170
|
+
"background",
|
|
3171
|
+
"--browser",
|
|
3172
|
+
browser,
|
|
3173
|
+
...null != timeout ? [
|
|
3174
|
+
"--timeout",
|
|
3175
|
+
String(timeout)
|
|
3176
|
+
] : []
|
|
3177
|
+
], projectPath, timeout);
|
|
3178
|
+
try {
|
|
3179
|
+
const parsed = JSON.parse(raw);
|
|
3180
|
+
if (parsed?.ok && "string" == typeof parsed.value && parsed.value) return parsed.value.endsWith("/") ? parsed.value : `${parsed.value}/`;
|
|
3181
|
+
} catch {}
|
|
3182
|
+
return null;
|
|
3183
|
+
}
|
|
3007
3184
|
const source_inspect_schema = {
|
|
3008
3185
|
name: "extension_source_inspect",
|
|
3009
|
-
description: "Inspect a running extension's live state
|
|
3186
|
+
description: "Inspect a running extension's live state: full HTML (with shadow DOM), DOM structure, content script injection, console messages, and CSS selector queries. Chromium sessions ride Chrome DevTools Protocol; Firefox sessions ride the agent bridge (needs allowEval: true) and cover summary/meta/html/probes, while dom_snapshot, extension_roots, console, and deepDom stay CDP-only. Requires an active dev or start session.",
|
|
3010
3187
|
inputSchema: {
|
|
3011
3188
|
type: "object",
|
|
3012
3189
|
properties: {
|
|
@@ -3073,10 +3250,7 @@ async function source_inspect_handler(args) {
|
|
|
3073
3250
|
"console"
|
|
3074
3251
|
]);
|
|
3075
3252
|
const maxBytes = args.maxBytes ?? 262144;
|
|
3076
|
-
if (!isChromiumFamily(browser)) return
|
|
3077
|
-
error: `Source inspection reads the live DOM over Chrome DevTools Protocol, which ${browser} (Gecko) does not expose. This is a capability limit, not a missing session.`,
|
|
3078
|
-
hint: `For the ${browser} session, read runtime state with extension_logs, or extension_eval / extension_dom_inspect (content/page, an open surface like popup/options/sidebar, or an override page like newtab) which work against Firefox over the control channel. To get CDP DOM inspection, run a Chromium-family dev session (extension_dev with browser: "chrome") in parallel.`
|
|
3079
|
-
});
|
|
3253
|
+
if (!isChromiumFamily(browser)) return inspectViaBridge(args, browser, include, maxBytes);
|
|
3080
3254
|
const resolved = await resolveCdpPort(args.projectPath, browser);
|
|
3081
3255
|
if (!resolved) return JSON.stringify({
|
|
3082
3256
|
error: "No active dev session found. Cannot connect to Chrome DevTools Protocol.",
|
|
@@ -3178,6 +3352,117 @@ async function source_inspect_handler(args) {
|
|
|
3178
3352
|
cdp.disconnect();
|
|
3179
3353
|
}
|
|
3180
3354
|
}
|
|
3355
|
+
function buildBridgeInspectExpression(opts) {
|
|
3356
|
+
const parts = [
|
|
3357
|
+
"const out = {};"
|
|
3358
|
+
];
|
|
3359
|
+
if (opts.meta) parts.push("try { out.meta = { url: location.href, title: document.title, readyState: document.readyState }; } catch (e) {}");
|
|
3360
|
+
if (opts.summary) parts.push(`try {
|
|
3361
|
+
const roots = document.querySelectorAll('#extension-root,[data-extension-root]:not([data-extension-root="extension-js-devtools"])');
|
|
3362
|
+
out.summary = {
|
|
3363
|
+
htmlLength: document.documentElement.outerHTML.length,
|
|
3364
|
+
scriptCount: document.querySelectorAll('script').length,
|
|
3365
|
+
styleCount: document.querySelectorAll('style').length,
|
|
3366
|
+
linkCount: document.querySelectorAll('link').length,
|
|
3367
|
+
extensionRootCount: roots.length,
|
|
3368
|
+
bodyChildCount: document.body ? document.body.children.length : 0
|
|
3369
|
+
};
|
|
3370
|
+
} catch (e) { out.summary = {}; }`);
|
|
3371
|
+
if (opts.html) parts.push(`try {
|
|
3372
|
+
const html = document.documentElement.outerHTML;
|
|
3373
|
+
const cap = ${JSON.stringify(opts.maxBytes)};
|
|
3374
|
+
out.htmlTruncated = cap > 0 && html.length > cap;
|
|
3375
|
+
out.html = out.htmlTruncated ? html.slice(0, cap) : html;
|
|
3376
|
+
} catch (e) {}`);
|
|
3377
|
+
if (opts.probes.length) parts.push(`out.probes = {};
|
|
3378
|
+
for (const sel of ${JSON.stringify(opts.probes)}) {
|
|
3379
|
+
try {
|
|
3380
|
+
const nodes = document.querySelectorAll(sel);
|
|
3381
|
+
const first = nodes[0];
|
|
3382
|
+
out.probes[sel] = { count: nodes.length, sample: first ? String(first.outerHTML || "").slice(0, 200) : null };
|
|
3383
|
+
} catch (e) { out.probes[sel] = { error: String((e && e.message) || e) }; }
|
|
3384
|
+
}`);
|
|
3385
|
+
parts.push("return out;");
|
|
3386
|
+
return `(() => { ${parts.join("\n")} })()`;
|
|
3387
|
+
}
|
|
3388
|
+
async function inspectViaBridge(args, browser, include, maxBytes) {
|
|
3389
|
+
const notes = [];
|
|
3390
|
+
const cdpOnly = [
|
|
3391
|
+
"dom_snapshot",
|
|
3392
|
+
"extension_roots"
|
|
3393
|
+
].filter((k)=>include.has(k));
|
|
3394
|
+
if (cdpOnly.length) notes.push(`${cdpOnly.join(" and ")} require CDP and are Chromium-only; on ${browser}, extension_dom_inspect's summary reports extension roots and open shadow roots.`);
|
|
3395
|
+
if (args.deepDom) notes.push("deepDom (closed shadow roots) requires CDP and is Chromium-only.");
|
|
3396
|
+
if (include.has("console")) notes.push(`Console capture rides CDP; on ${browser} read extension_logs, where the engine streams console output.`);
|
|
3397
|
+
if (args.url) {
|
|
3398
|
+
const listed = await listBridgeTabs(args.projectPath, browser, args.timeout);
|
|
3399
|
+
if ("error" in listed) return listed.error;
|
|
3400
|
+
const already = listed.tabs.some((t)=>t.url.includes(args.url));
|
|
3401
|
+
if (!already) {
|
|
3402
|
+
const nav = await navigateToUrlViaBridge(args.projectPath, browser, args.url, args.timeout);
|
|
3403
|
+
try {
|
|
3404
|
+
if (JSON.parse(nav)?.ok !== true) return nav;
|
|
3405
|
+
} catch {
|
|
3406
|
+
return nav;
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
const expression = buildBridgeInspectExpression({
|
|
3411
|
+
summary: include.has("summary"),
|
|
3412
|
+
meta: true,
|
|
3413
|
+
html: include.has("html"),
|
|
3414
|
+
probes: args.probe ?? [],
|
|
3415
|
+
maxBytes
|
|
3416
|
+
});
|
|
3417
|
+
const raw = await runActVerb([
|
|
3418
|
+
"eval",
|
|
3419
|
+
expression,
|
|
3420
|
+
args.projectPath,
|
|
3421
|
+
"--context",
|
|
3422
|
+
"page",
|
|
3423
|
+
...args.url ? [
|
|
3424
|
+
"--url",
|
|
3425
|
+
args.url
|
|
3426
|
+
] : [],
|
|
3427
|
+
"--browser",
|
|
3428
|
+
browser,
|
|
3429
|
+
...null != args.timeout ? [
|
|
3430
|
+
"--timeout",
|
|
3431
|
+
String(args.timeout)
|
|
3432
|
+
] : []
|
|
3433
|
+
], args.projectPath, args.timeout);
|
|
3434
|
+
let parsed;
|
|
3435
|
+
try {
|
|
3436
|
+
parsed = JSON.parse(raw);
|
|
3437
|
+
} catch {
|
|
3438
|
+
return raw;
|
|
3439
|
+
}
|
|
3440
|
+
if (parsed?.ok !== true) return raw;
|
|
3441
|
+
const value = parsed.value ?? {};
|
|
3442
|
+
const result = {
|
|
3443
|
+
browser,
|
|
3444
|
+
transport: "bridge"
|
|
3445
|
+
};
|
|
3446
|
+
if (value.meta) {
|
|
3447
|
+
result.target = {
|
|
3448
|
+
url: value.meta.url,
|
|
3449
|
+
title: value.meta.title
|
|
3450
|
+
};
|
|
3451
|
+
if (include.has("meta")) result.meta = value.meta;
|
|
3452
|
+
}
|
|
3453
|
+
if (include.has("summary") && value.summary) result.summary = value.summary;
|
|
3454
|
+
if (include.has("html") && "string" == typeof value.html) {
|
|
3455
|
+
result.html = value.html;
|
|
3456
|
+
if (value.htmlTruncated) result.htmlTruncated = true;
|
|
3457
|
+
}
|
|
3458
|
+
if (value.probes) {
|
|
3459
|
+
result.probes = value.probes;
|
|
3460
|
+
const jsLooking = (args.probe ?? []).filter((p)=>/^typeof\s|^(chrome|browser|window|document)\.|\(\)|=>|===/.test(p));
|
|
3461
|
+
if (jsLooking.length) result.probeWarning = `Probes are CSS selectors run through querySelectorAll against the live page, NOT JavaScript expressions. ${jsLooking.map((s)=>`"${s}"`).join(", ")} parsed as selectors and will match nothing. To evaluate JS, use extension_eval.`;
|
|
3462
|
+
}
|
|
3463
|
+
if (notes.length) result.notes = notes;
|
|
3464
|
+
return JSON.stringify(result);
|
|
3465
|
+
}
|
|
3181
3466
|
const list_extensions_schema = {
|
|
3182
3467
|
name: "extension_list_extensions",
|
|
3183
3468
|
description: "List the extensions with a live context in the running dev browser via Chrome DevTools Protocol. Returns each extension's id, name, version, and live contexts (service worker, page). The entry for THIS dev session's extension (the project being served) is flagged ownExtension: true, with name and version resolved from the session's ready contract even when the browser exposes no identity. Other extensions resolve via the read-only Extensions domain when available; their contexts are never attached to or evaluated in, so an id that the domain does not describe stays id-only with a note saying why. A dormant MV3 service worker with no open page may be absent until it wakes. Chromium only (Firefox uses RDP, not yet supported). Requires an active dev or start session.",
|
|
@@ -3657,58 +3942,6 @@ async function logs_handler(args) {
|
|
|
3657
3942
|
if (args.follow) return readFromStream(args, browser, limit);
|
|
3658
3943
|
return readFromFile(args, browser, limit);
|
|
3659
3944
|
}
|
|
3660
|
-
function toMcpSpeak(text) {
|
|
3661
|
-
return text.replace(/`?extension dev(?: [^\s`]*)? --browser[= ]([\w-]+) --allow-control`?/g, 'extension_dev with { browser: "$1", allowControl: true }').replace(/--allow-control/g, "allowControl: true (extension_dev)").replace(/--allow-eval/g, "allowEval: true (extension_dev)").replace(/Use --context page --tab <id>/g, 'Use context: "page" (targets the active tab; pass url or tab to pick another)').replace(/--context[= ](background|popup|options|sidebar|devtools|newtab|history|bookmarks|content|page)\b/g, 'context: "$1"').replace(/--tab[= ](\d+|<[\w-]+>)/g, "tab: $1").replace(/--url[= ]"([^"]+)"/g, 'url: "$1"').replace(/--url[= ](<[\w-]+>|\S*(?:\/\/|\*)\S*)/g, 'url: "$1"').replace(/--browser[= ]([\w]+-based|chrome|chromium|edge|brave|opera|vivaldi|yandex|firefox|waterfox|librewolf|safari)\b/g, 'browser: "$1"').replace(/--timeout[= ](\d+)/g, "timeout: $1").replace(/`extension dev`/g, "extension_dev").replace(/\bextension dev\b/g, "extension_dev").replace(/--tab\b/g, "`tab`").replace(/--url\b/g, "`url`").replace(/--context\b/g, "`context`").replace(/--browser\b/g, "`browser`").replace(/--timeout\b/g, "`timeout`");
|
|
3662
|
-
}
|
|
3663
|
-
function withSessionContext(message, projectPath) {
|
|
3664
|
-
const isControlError = /no active control channel|control channel refused|\b1006\b|no executor connected|is the session started with allowControl/i.test(message);
|
|
3665
|
-
if (!isControlError) return message;
|
|
3666
|
-
const dead = deadReadySession(projectPath);
|
|
3667
|
-
if (dead) return `${message}\nLikely cause: the dev server has exited, ${dead.browser} ready.json still says ready but its pid ${dead.pid} is dead. Restart with extension_dev (this is not an allowControl problem); extension_doctor confirms.`;
|
|
3668
|
-
const running = knownSessionBrowsers(projectPath);
|
|
3669
|
-
if (0 === running.length) return message;
|
|
3670
|
-
return `${message} Active session browser(s) for this project: ${running.join(", ")}, pass that as \`browser\`, or restart it via extension_dev with allowControl: true if the control channel is off.`;
|
|
3671
|
-
}
|
|
3672
|
-
function translateFrame(frame, projectPath) {
|
|
3673
|
-
if (!frame || false !== frame.ok) return frame;
|
|
3674
|
-
if (frame.error && "string" == typeof frame.error.message) frame.error.message = withSessionContext(toMcpSpeak(frame.error.message), projectPath);
|
|
3675
|
-
if ("string" == typeof frame.error?.hint) frame.error.hint = toMcpSpeak(frame.error.hint);
|
|
3676
|
-
if ("string" == typeof frame.hint) frame.hint = toMcpSpeak(frame.hint);
|
|
3677
|
-
return frame;
|
|
3678
|
-
}
|
|
3679
|
-
async function runActVerb(args, projectPath, timeoutMs) {
|
|
3680
|
-
const { code, stdout, stderr } = await runExtensionCli([
|
|
3681
|
-
...args,
|
|
3682
|
-
"--output",
|
|
3683
|
-
"json"
|
|
3684
|
-
], {
|
|
3685
|
-
cwd: projectPath,
|
|
3686
|
-
timeoutMs
|
|
3687
|
-
});
|
|
3688
|
-
const out = stdout.trim();
|
|
3689
|
-
if (out) try {
|
|
3690
|
-
const frame = JSON.parse(out);
|
|
3691
|
-
if (frame && false === frame.ok) return JSON.stringify(translateFrame(frame, projectPath));
|
|
3692
|
-
return out;
|
|
3693
|
-
} catch {}
|
|
3694
|
-
const message = stderr.trim() || `extension exited with code ${code}`;
|
|
3695
|
-
return JSON.stringify({
|
|
3696
|
-
ok: false,
|
|
3697
|
-
error: {
|
|
3698
|
-
name: "CliError",
|
|
3699
|
-
message: withSessionContext(toMcpSpeak(message), projectPath)
|
|
3700
|
-
}
|
|
3701
|
-
});
|
|
3702
|
-
}
|
|
3703
|
-
function commonFlags(args) {
|
|
3704
|
-
const flags = [];
|
|
3705
|
-
if (args.context) flags.push("--context", args.context);
|
|
3706
|
-
if (args.url) flags.push("--url", args.url);
|
|
3707
|
-
if (null != args.tab) flags.push("--tab", String(args.tab));
|
|
3708
|
-
if (args.browser) flags.push("--browser", args.browser);
|
|
3709
|
-
if (null != args.timeout) flags.push("--timeout", String(args.timeout));
|
|
3710
|
-
return flags;
|
|
3711
|
-
}
|
|
3712
3945
|
const eval_schema = {
|
|
3713
3946
|
name: "extension_eval",
|
|
3714
3947
|
description: "Evaluate an expression in a running extension context. Requires the dev session to be started with allowEval: true (extension_dev; writes a 0600 session token the CLI reads). Context default: on a Chromium session whose manifest is MV3 (the default template) the default is `page` (the active tab), because the MV3 background is a service worker whose CSP blocks eval, so a background default would fail on the most common path; on Firefox/MV2 sessions the default stays `background`. Pass `context: \"background\"` explicitly to target the worker anyway (on Chromium MV3 that returns the CSP explanation). Targeting for context content/page: pass `url` to pick the matching tab, or omit both `url` and `tab` to use the ACTIVE tab; a numeric `tab` id is only needed to disambiguate. Extension surfaces (popup/options/sidebar/devtools) and override pages (newtab/history/bookmarks) evaluate over the in-bundle relay and need NO tab id; the surface must be OPEN (extension_open first; a closed surface returns an explicit error). Use extension_dom_inspect with listTabs: true to enumerate {tabId,url,title}. Wraps `extension eval`.",
|
|
@@ -3976,14 +4209,8 @@ async function pollForTarget(port, url, budgetMs) {
|
|
|
3976
4209
|
await new Promise((r)=>setTimeout(r, 250));
|
|
3977
4210
|
}
|
|
3978
4211
|
}
|
|
3979
|
-
async function navigateToUrl(projectPath, browser, url) {
|
|
3980
|
-
if (!isChromiumFamily(browser)) return
|
|
3981
|
-
ok: false,
|
|
3982
|
-
error: {
|
|
3983
|
-
name: "Unsupported",
|
|
3984
|
-
message: `URL navigation drives a tab over Chrome DevTools Protocol, which ${browser} (Gecko) does not expose. On Firefox, drive the page via extension_eval (context: "page"/"content") or read extension_logs.`
|
|
3985
|
-
}
|
|
3986
|
-
});
|
|
4212
|
+
async function navigateToUrl(projectPath, browser, url, timeout) {
|
|
4213
|
+
if (!isChromiumFamily(browser)) return navigateToUrlViaBridge(projectPath, browser, url, timeout);
|
|
3987
4214
|
const resolved = await resolveCdpPort(projectPath, browser);
|
|
3988
4215
|
if (!resolved) return JSON.stringify({
|
|
3989
4216
|
ok: false,
|
|
@@ -4226,16 +4453,32 @@ async function applyPopupBounds(projectPath, browser, targetId) {
|
|
|
4226
4453
|
async function openSurfaceAsTab(projectPath, browser, surface) {
|
|
4227
4454
|
const doc = surfaceDocument(projectPath, browser, surface);
|
|
4228
4455
|
if (!doc) return missingSurfaceError(projectPath, browser, surface, "so there is no page to render as a tab");
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4456
|
+
let url;
|
|
4457
|
+
let extensionId = null;
|
|
4458
|
+
if (isChromiumFamily(browser)) {
|
|
4459
|
+
extensionId = await resolveExtensionId(projectPath, browser);
|
|
4460
|
+
if (!extensionId) return JSON.stringify({
|
|
4461
|
+
ok: false,
|
|
4462
|
+
error: {
|
|
4463
|
+
name: "NoExtensionId",
|
|
4464
|
+
message: "Could not resolve the extension id from the live session's CDP targets."
|
|
4465
|
+
},
|
|
4466
|
+
hint: `Confirm the session is ready (extension_wait). ${CDP_PORT_MISSING_HINT}`
|
|
4467
|
+
});
|
|
4468
|
+
url = `chrome-extension://${extensionId}/${doc}`;
|
|
4469
|
+
} else {
|
|
4470
|
+
const base = await resolveBridgeBaseUrl(projectPath, browser);
|
|
4471
|
+
if (!base) return JSON.stringify({
|
|
4472
|
+
ok: false,
|
|
4473
|
+
error: {
|
|
4474
|
+
name: "NoExtensionId",
|
|
4475
|
+
message: "Could not resolve the extension's moz-extension:// base URL from the live session (a background eval of runtime.getURL)."
|
|
4476
|
+
},
|
|
4477
|
+
hint: "Confirm the session is ready (extension_wait) and was started with allowEval: true (extension_dev)."
|
|
4478
|
+
});
|
|
4479
|
+
url = `${base}${doc}`;
|
|
4480
|
+
extensionId = base.replace(/^.*:\/\//, "").replace(/\/$/, "");
|
|
4481
|
+
}
|
|
4239
4482
|
const raw = await navigateToUrl(projectPath, browser, url);
|
|
4240
4483
|
try {
|
|
4241
4484
|
const parsed = JSON.parse(raw);
|
|
@@ -4243,14 +4486,14 @@ async function openSurfaceAsTab(projectPath, browser, surface) {
|
|
|
4243
4486
|
parsed.renderedAsTab = {
|
|
4244
4487
|
surface,
|
|
4245
4488
|
document: doc,
|
|
4246
|
-
extensionId
|
|
4489
|
+
extensionId
|
|
4247
4490
|
};
|
|
4248
4491
|
let popupBounds = null;
|
|
4249
4492
|
if (("popup" === surface || "action" === surface) && "string" == typeof parsed.target?.targetId) {
|
|
4250
4493
|
popupBounds = await applyPopupBounds(projectPath, browser, parsed.target.targetId);
|
|
4251
4494
|
if (popupBounds) parsed.renderedAsTab.popupBounds = popupBounds;
|
|
4252
4495
|
}
|
|
4253
|
-
parsed.hint = `Rendered the ${surface} document in a real tab, which is how you inspect a surface headlessly. ` + (popupBounds ? `The window was resized to the popup's content size (${popupBounds.width}x${popupBounds.height}${popupBounds.clamped ? ", clamped to Chrome's 25x25-800x600 popup bounds" : ""}), approximating real popup rendering. This resizes the WHOLE browser window for the session. It is the same page with the same extension APIs, but window.close() closes the tab. ` : "It is the same page with the same extension APIs, but it is NOT hosted in a popup window: no popup sizing, and window.close() closes the tab. ") + `Inspect it with extension_dom_inspect context: '${surface}' (include: ['html']), or extension_source_inspect with this url. ` + "Do NOT pass this
|
|
4496
|
+
parsed.hint = `Rendered the ${surface} document in a real tab, which is how you inspect a surface headlessly. ` + (popupBounds ? `The window was resized to the popup's content size (${popupBounds.width}x${popupBounds.height}${popupBounds.clamped ? ", clamped to Chrome's 25x25-800x600 popup bounds" : ""}), approximating real popup rendering. This resizes the WHOLE browser window for the session. It is the same page with the same extension APIs, but window.close() closes the tab. ` : "It is the same page with the same extension APIs, but it is NOT hosted in a popup window: no popup sizing, and window.close() closes the tab. ") + `Inspect it with extension_dom_inspect context: '${surface}' (include: ['html']), or extension_source_inspect with this url. ` + "Do NOT pass this extension-page url to extension_dom_inspect or extension_eval as a tab target: script injection cannot reach extension pages, only the surface context or CDP can.";
|
|
4254
4497
|
return JSON.stringify(parsed);
|
|
4255
4498
|
}
|
|
4256
4499
|
} catch {}
|
|
@@ -4286,7 +4529,7 @@ const open_schema = {
|
|
|
4286
4529
|
},
|
|
4287
4530
|
url: {
|
|
4288
4531
|
type: "string",
|
|
4289
|
-
description: "Navigate a real tab to this URL (Chromium
|
|
4532
|
+
description: "Navigate a real tab to this URL instead of opening a surface (Chromium via CDP; Firefox via the agent bridge, which needs allowEval: true). Use for content-script/webNavigation test pages, or the popup as a page: chrome-extension://<id>/popup.html. Alternative to `surface`."
|
|
4290
4533
|
},
|
|
4291
4534
|
asTab: {
|
|
4292
4535
|
type: "boolean",
|
|
@@ -4309,7 +4552,7 @@ const open_schema = {
|
|
|
4309
4552
|
};
|
|
4310
4553
|
async function open_handler(args) {
|
|
4311
4554
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
4312
|
-
if (args.url) return navigateToUrl(args.projectPath, browser, args.url);
|
|
4555
|
+
if (args.url) return navigateToUrl(args.projectPath, browser, args.url, args.timeout);
|
|
4313
4556
|
const AS_TAB_SURFACES = [
|
|
4314
4557
|
"popup",
|
|
4315
4558
|
"options",
|
|
@@ -4360,7 +4603,7 @@ async function open_handler(args) {
|
|
|
4360
4603
|
const parsed = JSON.parse(raw);
|
|
4361
4604
|
const msg = String(parsed?.error?.message ?? "");
|
|
4362
4605
|
if (parsed?.ok === false && /active browser window|no active|headless|user gesture/i.test(msg)) {
|
|
4363
|
-
if (AS_TAB_SURFACES.includes(args.surface)
|
|
4606
|
+
if (AS_TAB_SURFACES.includes(args.surface)) {
|
|
4364
4607
|
const fallback = await openSurfaceAsTab(args.projectPath, browser, args.surface);
|
|
4365
4608
|
try {
|
|
4366
4609
|
const parsedFallback = JSON.parse(fallback);
|
|
@@ -4414,7 +4657,7 @@ const dom_inspect_schema = {
|
|
|
4414
4657
|
},
|
|
4415
4658
|
tabUrl: {
|
|
4416
4659
|
type: "string",
|
|
4417
|
-
description: "Target the tab whose URL contains this substring (case-insensitive; titles are checked only when no url matches). Resolved against the live browser
|
|
4660
|
+
description: "Target the tab whose URL contains this substring (case-insensitive; titles are checked only when no url matches). Resolved against the live browser BEFORE inspecting (Chromium: CDP page targets; Firefox: the agent bridge tab list): exactly one match proceeds; zero or several matches return the candidates so you can narrow, never a guess. Alternative to `url`."
|
|
4418
4661
|
},
|
|
4419
4662
|
listTargets: {
|
|
4420
4663
|
type: "boolean",
|
|
@@ -4542,6 +4785,7 @@ async function dom_inspect_handler(args) {
|
|
|
4542
4785
|
] : []
|
|
4543
4786
|
], args.projectPath, args.timeout);
|
|
4544
4787
|
let targetUrl = args.url;
|
|
4788
|
+
let targetTab = args.tab;
|
|
4545
4789
|
let resolvedTarget = null;
|
|
4546
4790
|
if (args.tabUrl) {
|
|
4547
4791
|
if (null != args.tab || args.url) return JSON.stringify({
|
|
@@ -4552,48 +4796,79 @@ async function dom_inspect_handler(args) {
|
|
|
4552
4796
|
}
|
|
4553
4797
|
});
|
|
4554
4798
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4799
|
+
if (isChromiumFamily(browser)) {
|
|
4800
|
+
const cdp = await cdpPortOrError(args.projectPath, browser, "tabUrl");
|
|
4801
|
+
if ("error" in cdp) return cdp.error;
|
|
4802
|
+
let targets;
|
|
4803
|
+
try {
|
|
4804
|
+
targets = await listPageTargets(cdp.port);
|
|
4805
|
+
} catch (e) {
|
|
4806
|
+
return JSON.stringify({
|
|
4807
|
+
ok: false,
|
|
4808
|
+
error: {
|
|
4809
|
+
name: "CdpError",
|
|
4810
|
+
message: `Could not list page targets to resolve tabUrl: ${e instanceof Error ? e.message : String(e)}`
|
|
4811
|
+
},
|
|
4812
|
+
hint: "Confirm the session is ready (extension_wait), then retry, or target with `url`/`tab` instead."
|
|
4813
|
+
});
|
|
4814
|
+
}
|
|
4815
|
+
const matches = matchTargetsByUrl(targets, args.tabUrl);
|
|
4816
|
+
if (0 === matches.length) return JSON.stringify({
|
|
4562
4817
|
ok: false,
|
|
4563
4818
|
error: {
|
|
4564
|
-
name: "
|
|
4565
|
-
message: `
|
|
4819
|
+
name: "NoMatchingTarget",
|
|
4820
|
+
message: `No open page target's url (or title) contains "${args.tabUrl}" (case-insensitive).`
|
|
4821
|
+
},
|
|
4822
|
+
availableTargets: targets,
|
|
4823
|
+
hint: `Pick one from availableTargets and retry with a \`tabUrl\` substring of its url, or open the page first (extension_open with \`url\`). ${TARGET_ID_NOTE}`
|
|
4824
|
+
});
|
|
4825
|
+
if (matches.length > 1) return JSON.stringify({
|
|
4826
|
+
ok: false,
|
|
4827
|
+
error: {
|
|
4828
|
+
name: "AmbiguousTabUrl",
|
|
4829
|
+
message: `${matches.length} page targets match "${args.tabUrl}"; refusing to guess which tab you mean.`
|
|
4830
|
+
},
|
|
4831
|
+
matchingTargets: matches,
|
|
4832
|
+
hint: `Narrow \`tabUrl\` to a longer substring that matches exactly one url in matchingTargets. ${TARGET_ID_NOTE}`
|
|
4833
|
+
});
|
|
4834
|
+
resolvedTarget = {
|
|
4835
|
+
...matches[0]
|
|
4836
|
+
};
|
|
4837
|
+
targetUrl = matches[0].url;
|
|
4838
|
+
} else {
|
|
4839
|
+
const listed = await listBridgeTabs(args.projectPath, browser, args.timeout);
|
|
4840
|
+
if ("error" in listed) return listed.error;
|
|
4841
|
+
const matches = matchTabsByUrl(listed.tabs, args.tabUrl);
|
|
4842
|
+
if (0 === matches.length) return JSON.stringify({
|
|
4843
|
+
ok: false,
|
|
4844
|
+
error: {
|
|
4845
|
+
name: "NoMatchingTarget",
|
|
4846
|
+
message: `No open tab's url (or title) contains "${args.tabUrl}" (case-insensitive).`
|
|
4847
|
+
},
|
|
4848
|
+
availableTabs: listed.tabs,
|
|
4849
|
+
hint: "Pick one from availableTabs and retry with a `tabUrl` substring of its url, or open the page first (extension_open with `url`)."
|
|
4850
|
+
});
|
|
4851
|
+
if (matches.length > 1) return JSON.stringify({
|
|
4852
|
+
ok: false,
|
|
4853
|
+
error: {
|
|
4854
|
+
name: "AmbiguousTabUrl",
|
|
4855
|
+
message: `${matches.length} tabs match "${args.tabUrl}"; refusing to guess which tab you mean.`
|
|
4566
4856
|
},
|
|
4567
|
-
|
|
4857
|
+
matchingTabs: matches,
|
|
4858
|
+
hint: "Narrow `tabUrl` to a longer substring that matches exactly one url in matchingTabs, or pass its numeric tabId as `tab`."
|
|
4568
4859
|
});
|
|
4860
|
+
resolvedTarget = {
|
|
4861
|
+
...matches[0]
|
|
4862
|
+
};
|
|
4863
|
+
if (null != matches[0].tabId) targetTab = matches[0].tabId;
|
|
4864
|
+
else targetUrl = matches[0].url;
|
|
4569
4865
|
}
|
|
4570
|
-
const matches = matchTargetsByUrl(targets, args.tabUrl);
|
|
4571
|
-
if (0 === matches.length) return JSON.stringify({
|
|
4572
|
-
ok: false,
|
|
4573
|
-
error: {
|
|
4574
|
-
name: "NoMatchingTarget",
|
|
4575
|
-
message: `No open page target's url (or title) contains "${args.tabUrl}" (case-insensitive).`
|
|
4576
|
-
},
|
|
4577
|
-
availableTargets: targets,
|
|
4578
|
-
hint: `Pick one from availableTargets and retry with a \`tabUrl\` substring of its url, or open the page first (extension_open with \`url\`). ${TARGET_ID_NOTE}`
|
|
4579
|
-
});
|
|
4580
|
-
if (matches.length > 1) return JSON.stringify({
|
|
4581
|
-
ok: false,
|
|
4582
|
-
error: {
|
|
4583
|
-
name: "AmbiguousTabUrl",
|
|
4584
|
-
message: `${matches.length} page targets match "${args.tabUrl}"; refusing to guess which tab you mean.`
|
|
4585
|
-
},
|
|
4586
|
-
matchingTargets: matches,
|
|
4587
|
-
hint: `Narrow \`tabUrl\` to a longer substring that matches exactly one url in matchingTargets. ${TARGET_ID_NOTE}`
|
|
4588
|
-
});
|
|
4589
|
-
resolvedTarget = matches[0];
|
|
4590
|
-
targetUrl = resolvedTarget.url;
|
|
4591
4866
|
}
|
|
4592
4867
|
const cli = [
|
|
4593
4868
|
"inspect",
|
|
4594
4869
|
args.projectPath
|
|
4595
4870
|
];
|
|
4596
|
-
if (null !=
|
|
4871
|
+
if (null != targetTab) cli.push("--tab", String(targetTab));
|
|
4597
4872
|
if (targetUrl) cli.push("--url", targetUrl);
|
|
4598
4873
|
if (args.context) cli.push("--context", args.context);
|
|
4599
4874
|
if (args.include?.length) cli.push("--include", args.include.join(","));
|
|
@@ -4686,7 +4961,117 @@ function readValidCredentials(nowSeconds = Math.floor(Date.now() / 1000)) {
|
|
|
4686
4961
|
if (creds.expiresAt && creds.expiresAt <= nowSeconds) return null;
|
|
4687
4962
|
return creds;
|
|
4688
4963
|
}
|
|
4964
|
+
const REGISTRY_BASE_DEFAULT = "https://registry.extension.land";
|
|
4965
|
+
const CONSOLE_BASE = "https://console.extension.dev";
|
|
4966
|
+
function registryBase() {
|
|
4967
|
+
const fromEnv = String(process.env.EXTENSION_DEV_REGISTRY_URL || "").trim();
|
|
4968
|
+
return (fromEnv || REGISTRY_BASE_DEFAULT).replace(/\/+$/, "");
|
|
4969
|
+
}
|
|
4970
|
+
function resolveProjectRef(overrides) {
|
|
4971
|
+
const workspace = String(overrides?.workspace || "").trim();
|
|
4972
|
+
const project = String(overrides?.project || "").trim();
|
|
4973
|
+
if (workspace && project) return {
|
|
4974
|
+
workspace,
|
|
4975
|
+
project
|
|
4976
|
+
};
|
|
4977
|
+
const creds = readCredentials();
|
|
4978
|
+
const ws = workspace || String(creds?.workspaceSlug || "").trim();
|
|
4979
|
+
const proj = project || String(creds?.projectSlug || "").trim();
|
|
4980
|
+
if (!ws || !proj) return null;
|
|
4981
|
+
return {
|
|
4982
|
+
workspace: ws,
|
|
4983
|
+
project: proj
|
|
4984
|
+
};
|
|
4985
|
+
}
|
|
4986
|
+
function registryFileUrl(ref, file) {
|
|
4987
|
+
return `${registryBase()}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/_extension-dev/${file}`;
|
|
4988
|
+
}
|
|
4989
|
+
function consoleProjectUrl(ref, page) {
|
|
4990
|
+
if (!ref) return `${CONSOLE_BASE}`;
|
|
4991
|
+
return `${CONSOLE_BASE}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/${page}`;
|
|
4992
|
+
}
|
|
4993
|
+
async function fetchRegistryJson(url, fetchImpl = fetch) {
|
|
4994
|
+
let res;
|
|
4995
|
+
try {
|
|
4996
|
+
res = await fetchImpl(url);
|
|
4997
|
+
} catch (err) {
|
|
4998
|
+
return {
|
|
4999
|
+
ok: false,
|
|
5000
|
+
message: `Could not reach ${url}: ${err?.message || err}`
|
|
5001
|
+
};
|
|
5002
|
+
}
|
|
5003
|
+
if (!res.ok) return {
|
|
5004
|
+
ok: false,
|
|
5005
|
+
status: res.status,
|
|
5006
|
+
message: `${url} returned ${res.status}`
|
|
5007
|
+
};
|
|
5008
|
+
try {
|
|
5009
|
+
const text = await res.text();
|
|
5010
|
+
return {
|
|
5011
|
+
ok: true,
|
|
5012
|
+
json: JSON.parse(text)
|
|
5013
|
+
};
|
|
5014
|
+
} catch {
|
|
5015
|
+
return {
|
|
5016
|
+
ok: false,
|
|
5017
|
+
message: `${url} did not return valid JSON`
|
|
5018
|
+
};
|
|
5019
|
+
}
|
|
5020
|
+
}
|
|
5021
|
+
function parseChannels(json) {
|
|
5022
|
+
if (!json || "object" != typeof json || Array.isArray(json)) return [];
|
|
5023
|
+
const out = [];
|
|
5024
|
+
for (const [channel, raw] of Object.entries(json)){
|
|
5025
|
+
if (!raw || "object" != typeof raw) continue;
|
|
5026
|
+
const row = raw;
|
|
5027
|
+
const description = "string" == typeof row.description ? row.description : void 0;
|
|
5028
|
+
const promotedAtField = "string" == typeof row.promotedAt && row.promotedAt ? row.promotedAt : void 0;
|
|
5029
|
+
const fromDescription = description?.match(/\bon (\d{4}-\d{2}-\d{2}T[0-9:.]+Z?)/)?.[1];
|
|
5030
|
+
const entry = {
|
|
5031
|
+
channel,
|
|
5032
|
+
sha: String(row.sha ?? "")
|
|
5033
|
+
};
|
|
5034
|
+
if (row.buildId) entry.buildId = String(row.buildId);
|
|
5035
|
+
if (row.version) entry.version = String(row.version);
|
|
5036
|
+
const promotedAt = promotedAtField || fromDescription;
|
|
5037
|
+
if (promotedAt) entry.promotedAt = promotedAt;
|
|
5038
|
+
if (description) entry.description = description;
|
|
5039
|
+
out.push(entry);
|
|
5040
|
+
}
|
|
5041
|
+
return out;
|
|
5042
|
+
}
|
|
5043
|
+
function parseBuildIndex(json) {
|
|
5044
|
+
const items = json?.items;
|
|
5045
|
+
if (!Array.isArray(items)) return [];
|
|
5046
|
+
const out = [];
|
|
5047
|
+
for (const raw of items){
|
|
5048
|
+
if (!raw || "object" != typeof raw) continue;
|
|
5049
|
+
const row = raw;
|
|
5050
|
+
const sha = String(row.shortSha ?? row.sha ?? row.id ?? row.buildId ?? "").trim();
|
|
5051
|
+
if (!sha) continue;
|
|
5052
|
+
const entry = {
|
|
5053
|
+
sha
|
|
5054
|
+
};
|
|
5055
|
+
if (row.commit) entry.commit = String(row.commit);
|
|
5056
|
+
if (row.channel) entry.channel = String(row.channel);
|
|
5057
|
+
if (row.buildEnv) entry.buildEnv = String(row.buildEnv);
|
|
5058
|
+
if (row.status) entry.status = String(row.status);
|
|
5059
|
+
if (row.version) entry.version = String(row.version);
|
|
5060
|
+
if ("string" == typeof row.message) entry.message = row.message.split("\n", 1)[0];
|
|
5061
|
+
if (row.timestamp) entry.timestamp = String(row.timestamp);
|
|
5062
|
+
if (Array.isArray(row.browsers)) entry.browsers = row.browsers.map((b)=>String(b)).filter(Boolean);
|
|
5063
|
+
out.push(entry);
|
|
5064
|
+
}
|
|
5065
|
+
return out;
|
|
5066
|
+
}
|
|
4689
5067
|
const DEFAULT_API = "https://www.extension.dev";
|
|
5068
|
+
function tokenTtlNote(workspaceSlug, projectSlug) {
|
|
5069
|
+
const tokensUrl = workspaceSlug && projectSlug ? consoleProjectUrl({
|
|
5070
|
+
workspace: workspaceSlug,
|
|
5071
|
+
project: projectSlug
|
|
5072
|
+
}, "settings/access-tokens") : CONSOLE_BASE;
|
|
5073
|
+
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}`;
|
|
5074
|
+
}
|
|
4690
5075
|
function resolveApiBase(api) {
|
|
4691
5076
|
return String(api || process.env.EXTENSION_DEV_API_URL || DEFAULT_API).replace(/\/+$/, "");
|
|
4692
5077
|
}
|
|
@@ -4851,109 +5236,6 @@ async function publish(options = {}) {
|
|
|
4851
5236
|
data
|
|
4852
5237
|
};
|
|
4853
5238
|
}
|
|
4854
|
-
const REGISTRY_BASE_DEFAULT = "https://registry.extension.land";
|
|
4855
|
-
const CONSOLE_BASE = "https://console.extension.dev";
|
|
4856
|
-
function registryBase() {
|
|
4857
|
-
const fromEnv = String(process.env.EXTENSION_DEV_REGISTRY_URL || "").trim();
|
|
4858
|
-
return (fromEnv || REGISTRY_BASE_DEFAULT).replace(/\/+$/, "");
|
|
4859
|
-
}
|
|
4860
|
-
function resolveProjectRef(overrides) {
|
|
4861
|
-
const workspace = String(overrides?.workspace || "").trim();
|
|
4862
|
-
const project = String(overrides?.project || "").trim();
|
|
4863
|
-
if (workspace && project) return {
|
|
4864
|
-
workspace,
|
|
4865
|
-
project
|
|
4866
|
-
};
|
|
4867
|
-
const creds = readCredentials();
|
|
4868
|
-
const ws = workspace || String(creds?.workspaceSlug || "").trim();
|
|
4869
|
-
const proj = project || String(creds?.projectSlug || "").trim();
|
|
4870
|
-
if (!ws || !proj) return null;
|
|
4871
|
-
return {
|
|
4872
|
-
workspace: ws,
|
|
4873
|
-
project: proj
|
|
4874
|
-
};
|
|
4875
|
-
}
|
|
4876
|
-
function registryFileUrl(ref, file) {
|
|
4877
|
-
return `${registryBase()}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/_extension-dev/${file}`;
|
|
4878
|
-
}
|
|
4879
|
-
function consoleProjectUrl(ref, page) {
|
|
4880
|
-
if (!ref) return `${CONSOLE_BASE}`;
|
|
4881
|
-
return `${CONSOLE_BASE}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/${page}`;
|
|
4882
|
-
}
|
|
4883
|
-
async function fetchRegistryJson(url, fetchImpl = fetch) {
|
|
4884
|
-
let res;
|
|
4885
|
-
try {
|
|
4886
|
-
res = await fetchImpl(url);
|
|
4887
|
-
} catch (err) {
|
|
4888
|
-
return {
|
|
4889
|
-
ok: false,
|
|
4890
|
-
message: `Could not reach ${url}: ${err?.message || err}`
|
|
4891
|
-
};
|
|
4892
|
-
}
|
|
4893
|
-
if (!res.ok) return {
|
|
4894
|
-
ok: false,
|
|
4895
|
-
status: res.status,
|
|
4896
|
-
message: `${url} returned ${res.status}`
|
|
4897
|
-
};
|
|
4898
|
-
try {
|
|
4899
|
-
const text = await res.text();
|
|
4900
|
-
return {
|
|
4901
|
-
ok: true,
|
|
4902
|
-
json: JSON.parse(text)
|
|
4903
|
-
};
|
|
4904
|
-
} catch {
|
|
4905
|
-
return {
|
|
4906
|
-
ok: false,
|
|
4907
|
-
message: `${url} did not return valid JSON`
|
|
4908
|
-
};
|
|
4909
|
-
}
|
|
4910
|
-
}
|
|
4911
|
-
function parseChannels(json) {
|
|
4912
|
-
if (!json || "object" != typeof json || Array.isArray(json)) return [];
|
|
4913
|
-
const out = [];
|
|
4914
|
-
for (const [channel, raw] of Object.entries(json)){
|
|
4915
|
-
if (!raw || "object" != typeof raw) continue;
|
|
4916
|
-
const row = raw;
|
|
4917
|
-
const description = "string" == typeof row.description ? row.description : void 0;
|
|
4918
|
-
const promotedAtField = "string" == typeof row.promotedAt && row.promotedAt ? row.promotedAt : void 0;
|
|
4919
|
-
const fromDescription = description?.match(/\bon (\d{4}-\d{2}-\d{2}T[0-9:.]+Z?)/)?.[1];
|
|
4920
|
-
const entry = {
|
|
4921
|
-
channel,
|
|
4922
|
-
sha: String(row.sha ?? "")
|
|
4923
|
-
};
|
|
4924
|
-
if (row.buildId) entry.buildId = String(row.buildId);
|
|
4925
|
-
if (row.version) entry.version = String(row.version);
|
|
4926
|
-
const promotedAt = promotedAtField || fromDescription;
|
|
4927
|
-
if (promotedAt) entry.promotedAt = promotedAt;
|
|
4928
|
-
if (description) entry.description = description;
|
|
4929
|
-
out.push(entry);
|
|
4930
|
-
}
|
|
4931
|
-
return out;
|
|
4932
|
-
}
|
|
4933
|
-
function parseBuildIndex(json) {
|
|
4934
|
-
const items = json?.items;
|
|
4935
|
-
if (!Array.isArray(items)) return [];
|
|
4936
|
-
const out = [];
|
|
4937
|
-
for (const raw of items){
|
|
4938
|
-
if (!raw || "object" != typeof raw) continue;
|
|
4939
|
-
const row = raw;
|
|
4940
|
-
const sha = String(row.shortSha ?? row.sha ?? row.id ?? row.buildId ?? "").trim();
|
|
4941
|
-
if (!sha) continue;
|
|
4942
|
-
const entry = {
|
|
4943
|
-
sha
|
|
4944
|
-
};
|
|
4945
|
-
if (row.commit) entry.commit = String(row.commit);
|
|
4946
|
-
if (row.channel) entry.channel = String(row.channel);
|
|
4947
|
-
if (row.buildEnv) entry.buildEnv = String(row.buildEnv);
|
|
4948
|
-
if (row.status) entry.status = String(row.status);
|
|
4949
|
-
if (row.version) entry.version = String(row.version);
|
|
4950
|
-
if ("string" == typeof row.message) entry.message = row.message.split("\n", 1)[0];
|
|
4951
|
-
if (row.timestamp) entry.timestamp = String(row.timestamp);
|
|
4952
|
-
if (Array.isArray(row.browsers)) entry.browsers = row.browsers.map((b)=>String(b)).filter(Boolean);
|
|
4953
|
-
out.push(entry);
|
|
4954
|
-
}
|
|
4955
|
-
return out;
|
|
4956
|
-
}
|
|
4957
5239
|
const publish_schema = {
|
|
4958
5240
|
name: "extension_publish",
|
|
4959
5241
|
description: "Publish the project your stored token is scoped to (from extension_login, or EXTENSION_DEV_TOKEN) to extension.dev and return its shareable URL. The publish target is the token's project -- there is no projectPath, the local files are not uploaded. For a PUBLIC project the URL is the canonical public page and ttlHours does not apply; for a PRIVATE project it is a fresh time-limited share link (?share=) whose lifetime is ttlHours. Posts to the platform's CLI publish endpoint. Besides extension_login this is the only tool that talks to the hosted platform.",
|
|
@@ -5031,7 +5313,7 @@ async function publish_handler(args) {
|
|
|
5031
5313
|
const release_promote_DEFAULT_API = "https://www.extension.dev";
|
|
5032
5314
|
const release_promote_schema = {
|
|
5033
5315
|
name: "extension_release_promote",
|
|
5034
|
-
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). 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).",
|
|
5316
|
+
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).",
|
|
5035
5317
|
inputSchema: {
|
|
5036
5318
|
type: "object",
|
|
5037
5319
|
properties: {
|
|
@@ -5084,7 +5366,7 @@ function release_promote_fail(name, message) {
|
|
|
5084
5366
|
}
|
|
5085
5367
|
async function release_promote_handler(args) {
|
|
5086
5368
|
const token = resolveToken();
|
|
5087
|
-
if (!token) return release_promote_fail("ReleaseAuthError", "No token. Set EXTENSION_DEV_TOKEN to a release token (create one in the extension.dev dashboard under project settings -> Access tokens), or run extension_login.");
|
|
5369
|
+
if (!token) return release_promote_fail("ReleaseAuthError", "No token. Set EXTENSION_DEV_TOKEN to a release token (create one in the extension.dev dashboard under project settings -> Access tokens; tokens live at most 7 days, so CI must re-mint before expiry), or run extension_login.");
|
|
5088
5370
|
const buildId = String(args.buildId || "").trim();
|
|
5089
5371
|
const channel = String(args.channel || "").trim();
|
|
5090
5372
|
if (!buildId || !channel) return release_promote_fail("ReleaseInputError", "buildId and channel are required.");
|
|
@@ -5236,7 +5518,7 @@ function storeMdWarnings(browsers, cwd) {
|
|
|
5236
5518
|
content = node_fs.readFileSync(node_path.join(cwd, "STORE.md"), "utf8");
|
|
5237
5519
|
} catch {
|
|
5238
5520
|
return [
|
|
5239
|
-
"No STORE.md found in the
|
|
5521
|
+
"No STORE.md found in the current working directory. Platform submissions read STORE.md from the project's source repository, so this may not apply here; make sure STORE.md exists there for Firefox reviewer notes and Edge certification notes. See the extension-dev skill's store-md reference."
|
|
5240
5522
|
];
|
|
5241
5523
|
}
|
|
5242
5524
|
const hasField = (section, field)=>{
|
|
@@ -5255,7 +5537,7 @@ function storeMdWarnings(browsers, cwd) {
|
|
|
5255
5537
|
}
|
|
5256
5538
|
const deploy_schema = {
|
|
5257
5539
|
name: "extension_deploy",
|
|
5258
|
-
description: "Submit a built extension to the Chrome Web Store, Firefox AMO, and/or Edge Add-ons THROUGH extension.dev, which holds your store credentials and dispatches the release from your project's mirror CI. DEFAULTS TO A DRY RUN (preflight: verifies auth, the project, that the build exists, and the store workflow -
|
|
5540
|
+
description: "Submit a built extension to the Chrome Web Store, Firefox AMO, and/or Edge Add-ons 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.",
|
|
5259
5541
|
inputSchema: {
|
|
5260
5542
|
type: "object",
|
|
5261
5543
|
properties: {
|
|
@@ -5311,7 +5593,7 @@ function deploy_fail(name, message) {
|
|
|
5311
5593
|
}
|
|
5312
5594
|
async function deploy_handler(args) {
|
|
5313
5595
|
const token = resolveToken();
|
|
5314
|
-
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).");
|
|
5596
|
+
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).");
|
|
5315
5597
|
const browsers = (Array.isArray(args.browsers) ? args.browsers : []).map((b)=>String(b).trim().toLowerCase()).filter(Boolean);
|
|
5316
5598
|
if (0 === browsers.length) return deploy_fail("DeployInputError", 'browsers is required (e.g. ["chrome","firefox","edge"]).');
|
|
5317
5599
|
const buildSha = String(args.buildSha || "").trim();
|
|
@@ -5429,9 +5711,214 @@ async function deploy_handler(args) {
|
|
|
5429
5711
|
if ("string" == typeof data?.message) result.platformMessage = data.message;
|
|
5430
5712
|
result.message = summaryParts.join(" ");
|
|
5431
5713
|
}
|
|
5714
|
+
if (!dryRun) result.statusNote = "Track this submission with extension_store_status: it reads the recorded outcome, per-store credential health, and review state from the public registry.";
|
|
5432
5715
|
if (warnings.length > 0) result.warnings = warnings;
|
|
5433
5716
|
return JSON.stringify(result);
|
|
5434
5717
|
}
|
|
5718
|
+
const KNOWN_STORES = [
|
|
5719
|
+
"chrome",
|
|
5720
|
+
"firefox",
|
|
5721
|
+
"edge",
|
|
5722
|
+
"safari"
|
|
5723
|
+
];
|
|
5724
|
+
const store_status_schema = {
|
|
5725
|
+
name: "extension_store_status",
|
|
5726
|
+
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.",
|
|
5727
|
+
inputSchema: {
|
|
5728
|
+
type: "object",
|
|
5729
|
+
properties: {
|
|
5730
|
+
workspace: {
|
|
5731
|
+
type: "string",
|
|
5732
|
+
description: "Workspace slug override (defaults to the stored login's workspace)."
|
|
5733
|
+
},
|
|
5734
|
+
project: {
|
|
5735
|
+
type: "string",
|
|
5736
|
+
description: "Project slug override (defaults to the stored login's project)."
|
|
5737
|
+
}
|
|
5738
|
+
},
|
|
5739
|
+
required: []
|
|
5740
|
+
}
|
|
5741
|
+
};
|
|
5742
|
+
function isPlainObject(value) {
|
|
5743
|
+
return Boolean(value) && "object" == typeof value && !Array.isArray(value);
|
|
5744
|
+
}
|
|
5745
|
+
function str(value) {
|
|
5746
|
+
const text = String(value ?? "").trim();
|
|
5747
|
+
return text || void 0;
|
|
5748
|
+
}
|
|
5749
|
+
function submissionView(row) {
|
|
5750
|
+
const view = {};
|
|
5751
|
+
const version = str(row.version);
|
|
5752
|
+
const status = str(row.status);
|
|
5753
|
+
const storeUrl = str(row.storeUrl);
|
|
5754
|
+
const submittedAt = str(row.submittedAt) || str(row.timestamp);
|
|
5755
|
+
const channel = str(row.channel);
|
|
5756
|
+
const buildSha = str(row.buildSha) || str(row.buildId);
|
|
5757
|
+
const storeSubmissionId = str(row.storeSubmissionId);
|
|
5758
|
+
const failureReason = str(row.failureReason);
|
|
5759
|
+
if (version) view.version = version;
|
|
5760
|
+
if (status) view.status = status;
|
|
5761
|
+
if (storeUrl) view.storeUrl = storeUrl;
|
|
5762
|
+
if (submittedAt) view.submittedAt = submittedAt;
|
|
5763
|
+
if (channel) view.channel = channel;
|
|
5764
|
+
if (buildSha) view.buildSha = buildSha;
|
|
5765
|
+
if (storeSubmissionId) view.storeSubmissionId = storeSubmissionId;
|
|
5766
|
+
if (failureReason) view.failureReason = failureReason;
|
|
5767
|
+
return view;
|
|
5768
|
+
}
|
|
5769
|
+
function latestSubmissionsByStore(json) {
|
|
5770
|
+
const list = json?.submissions;
|
|
5771
|
+
const out = {};
|
|
5772
|
+
for (const raw of Array.isArray(list) ? list : []){
|
|
5773
|
+
if (!isPlainObject(raw)) continue;
|
|
5774
|
+
const store = str(raw.store);
|
|
5775
|
+
if (!store) continue;
|
|
5776
|
+
const at = Date.parse(String(raw.submittedAt || raw.updatedAt || ""));
|
|
5777
|
+
const stamp = Number.isFinite(at) ? at : 0;
|
|
5778
|
+
if (!out[store] || stamp >= out[store].at) out[store] = {
|
|
5779
|
+
at: stamp,
|
|
5780
|
+
view: submissionView(raw)
|
|
5781
|
+
};
|
|
5782
|
+
}
|
|
5783
|
+
const flat = {};
|
|
5784
|
+
for (const [store, entry] of Object.entries(out))flat[store] = entry.view;
|
|
5785
|
+
return flat;
|
|
5786
|
+
}
|
|
5787
|
+
function normalizeStoresStatus(json) {
|
|
5788
|
+
const base = isPlainObject(json) ? json : {};
|
|
5789
|
+
const out = {
|
|
5790
|
+
reviews: {}
|
|
5791
|
+
};
|
|
5792
|
+
if (isPlainObject(base.lastSubmission)) out.lastSubmission = base.lastSubmission;
|
|
5793
|
+
if (isPlainObject(base.lastOverride)) out.lastOverride = base.lastOverride;
|
|
5794
|
+
if (isPlainObject(base.reviews)) {
|
|
5795
|
+
for (const [store, raw] of Object.entries(base.reviews))if (isPlainObject(raw)) out.reviews[store] = {
|
|
5796
|
+
status: str(raw.status),
|
|
5797
|
+
version: str(raw.version),
|
|
5798
|
+
buildId: str(raw.buildId),
|
|
5799
|
+
checkedAt: str(raw.checkedAt)
|
|
5800
|
+
};
|
|
5801
|
+
}
|
|
5802
|
+
const lastPoll = isPlainObject(base.lastPoll) ? base.lastPoll : null;
|
|
5803
|
+
const looksLikeLegacyPoll = !lastPoll && ("store_status" === base.kind || Array.isArray(base.updates));
|
|
5804
|
+
if (lastPoll) out.lastPollAt = str(lastPoll.timestamp);
|
|
5805
|
+
else if (looksLikeLegacyPoll) {
|
|
5806
|
+
out.lastPollAt = str(base.updatedAt);
|
|
5807
|
+
for (const raw of Array.isArray(base.updates) ? base.updates : []){
|
|
5808
|
+
if (!isPlainObject(raw)) continue;
|
|
5809
|
+
const store = str(raw.store);
|
|
5810
|
+
if (store && !out.reviews[store]) out.reviews[store] = {
|
|
5811
|
+
status: str(raw.status),
|
|
5812
|
+
version: str(raw.version),
|
|
5813
|
+
buildId: str(raw.buildId),
|
|
5814
|
+
checkedAt: str(base.updatedAt)
|
|
5815
|
+
};
|
|
5816
|
+
}
|
|
5817
|
+
}
|
|
5818
|
+
return out;
|
|
5819
|
+
}
|
|
5820
|
+
function store_status_fail(name, message, extra) {
|
|
5821
|
+
return JSON.stringify({
|
|
5822
|
+
ok: false,
|
|
5823
|
+
error: {
|
|
5824
|
+
name,
|
|
5825
|
+
message
|
|
5826
|
+
},
|
|
5827
|
+
...extra ?? {}
|
|
5828
|
+
});
|
|
5829
|
+
}
|
|
5830
|
+
async function store_status_handler(args) {
|
|
5831
|
+
const ref = resolveProjectRef(args);
|
|
5832
|
+
if (!ref) return store_status_fail("StoreStatusInputError", "No project to inspect. Run extension_login (the stored login names the project), or pass workspace + project explicitly.");
|
|
5833
|
+
const healthUrl = registryFileUrl(ref, "stores/health.json");
|
|
5834
|
+
const statusUrl = registryFileUrl(ref, "stores/status.json");
|
|
5835
|
+
const submissionsUrl = registryFileUrl(ref, "stores/submissions.json");
|
|
5836
|
+
const consoleStoresUrl = consoleProjectUrl(ref, "stores");
|
|
5837
|
+
const [healthRes, statusRes, submissionsRes] = await Promise.all([
|
|
5838
|
+
fetchRegistryJson(healthUrl),
|
|
5839
|
+
fetchRegistryJson(statusUrl),
|
|
5840
|
+
fetchRegistryJson(submissionsUrl)
|
|
5841
|
+
]);
|
|
5842
|
+
if (!healthRes.ok && !statusRes.ok && !submissionsRes.ok) return store_status_fail("StoreStatusNotFound", `No store data on the registry for ${ref.workspace}/${ref.project} (${healthUrl} returned ${healthRes.status ?? "no response"}). The project may have no stores configured yet, be private (private registry data needs a share token), or the workspace/project slugs may be wrong. Configure stores at ${consoleStoresUrl}/new; the console Stores page is the authoritative view: ${consoleStoresUrl}`, {
|
|
5843
|
+
workspace: ref.workspace,
|
|
5844
|
+
project: ref.project,
|
|
5845
|
+
registryUrls: {
|
|
5846
|
+
health: healthUrl,
|
|
5847
|
+
status: statusUrl,
|
|
5848
|
+
submissions: submissionsUrl
|
|
5849
|
+
},
|
|
5850
|
+
consoleStoresUrl
|
|
5851
|
+
});
|
|
5852
|
+
const healthStores = healthRes.ok ? isPlainObject(healthRes.json?.stores) ? healthRes.json.stores ?? null : null : null;
|
|
5853
|
+
const status = normalizeStoresStatus(statusRes.ok ? statusRes.json : null);
|
|
5854
|
+
const submissionsByStore = submissionsRes.ok ? latestSubmissionsByStore(submissionsRes.json) : {};
|
|
5855
|
+
const statusLastStore = str(status.lastSubmission?.store);
|
|
5856
|
+
if (statusLastStore && !submissionsByStore[statusLastStore] && status.lastSubmission) submissionsByStore[statusLastStore] = submissionView(status.lastSubmission);
|
|
5857
|
+
const stores = [
|
|
5858
|
+
...KNOWN_STORES,
|
|
5859
|
+
...Object.keys({
|
|
5860
|
+
...healthStores,
|
|
5861
|
+
...submissionsByStore,
|
|
5862
|
+
...status.reviews
|
|
5863
|
+
}).filter((s)=>!KNOWN_STORES.includes(s))
|
|
5864
|
+
];
|
|
5865
|
+
const rows = stores.map((store)=>{
|
|
5866
|
+
const healthRow = healthStores?.[store];
|
|
5867
|
+
const configured = healthStores ? Boolean(healthRow) : "unknown";
|
|
5868
|
+
const row = {
|
|
5869
|
+
store,
|
|
5870
|
+
configured
|
|
5871
|
+
};
|
|
5872
|
+
if (healthRow) row.health = {
|
|
5873
|
+
ok: true === healthRow.ok,
|
|
5874
|
+
checkedAt: str(healthRow.checkedAt),
|
|
5875
|
+
message: str(healthRow.message)
|
|
5876
|
+
};
|
|
5877
|
+
const submission = submissionsByStore[store];
|
|
5878
|
+
if (submission && Object.keys(submission).length > 0) row.lastSubmission = submission;
|
|
5879
|
+
const review = status.reviews[store];
|
|
5880
|
+
if (review && Object.values(review).some(Boolean)) row.review = review;
|
|
5881
|
+
return row;
|
|
5882
|
+
});
|
|
5883
|
+
const summaryParts = rows.map((row)=>{
|
|
5884
|
+
const store = String(row.store);
|
|
5885
|
+
const health = row.health;
|
|
5886
|
+
const submission = row.lastSubmission;
|
|
5887
|
+
const review = row.review;
|
|
5888
|
+
let head;
|
|
5889
|
+
head = "unknown" === row.configured ? `${store}: configuration unknown (stores/health.json is unreadable)` : false === row.configured ? `${store}: not configured (add it at ${consoleStoresUrl}/new)` : health && !health.ok ? `${store}: configured but its credentials FAILED the last health check (${health.message || "no reason recorded"}) - fix them at ${consoleStoresUrl}/${store}` : `${store}: configured, credentials healthy`;
|
|
5890
|
+
const tail = [];
|
|
5891
|
+
if (submission) {
|
|
5892
|
+
tail.push(`last submission${submission.version ? ` v${submission.version}` : ""} ${submission.status || "recorded"}${submission.submittedAt ? ` at ${submission.submittedAt}` : ""}${submission.failureReason ? ` (${submission.failureReason})` : ""}`);
|
|
5893
|
+
if (submission.storeUrl) tail.push(`listing ${submission.storeUrl}`);
|
|
5894
|
+
} else if (true === row.configured) tail.push("no submissions recorded");
|
|
5895
|
+
if (review?.status) tail.push(`review ${review.status}${review.checkedAt ? ` (checked ${review.checkedAt})` : ""}`);
|
|
5896
|
+
return tail.length > 0 ? `${head}; ${tail.join("; ")}` : head;
|
|
5897
|
+
});
|
|
5898
|
+
const result = {
|
|
5899
|
+
ok: true,
|
|
5900
|
+
workspace: ref.workspace,
|
|
5901
|
+
project: ref.project,
|
|
5902
|
+
stores: rows,
|
|
5903
|
+
...status.lastSubmission ? {
|
|
5904
|
+
lastSubmission: status.lastSubmission
|
|
5905
|
+
} : {},
|
|
5906
|
+
...status.lastPollAt ? {
|
|
5907
|
+
lastPollAt: status.lastPollAt
|
|
5908
|
+
} : {},
|
|
5909
|
+
registryUrls: {
|
|
5910
|
+
health: healthUrl,
|
|
5911
|
+
status: statusUrl,
|
|
5912
|
+
submissions: submissionsUrl
|
|
5913
|
+
},
|
|
5914
|
+
consoleStoresUrl,
|
|
5915
|
+
message: `${summaryParts.join(". ")}. This is the registry's recorded state (submissions and the review poller write it); the store dashboards are authoritative and may be ahead of it.`
|
|
5916
|
+
};
|
|
5917
|
+
if (!healthRes.ok) result.healthUnavailable = `stores/health.json unreadable: ${healthRes.message}`;
|
|
5918
|
+
if (!statusRes.ok) result.statusUnavailable = `stores/status.json unreadable: ${statusRes.message}`;
|
|
5919
|
+
if (!submissionsRes.ok && 404 !== submissionsRes.status) result.submissionsUnavailable = `stores/submissions.json unreadable: ${submissionsRes.message}`;
|
|
5920
|
+
return JSON.stringify(result);
|
|
5921
|
+
}
|
|
5435
5922
|
function doctor_readReadyContract(projectPath, browser) {
|
|
5436
5923
|
try {
|
|
5437
5924
|
const raw = node_fs.readFileSync(node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json"), "utf8");
|
|
@@ -6187,7 +6674,7 @@ async function pollDeviceToken(args) {
|
|
|
6187
6674
|
}
|
|
6188
6675
|
const login_schema = {
|
|
6189
6676
|
name: "extension_login",
|
|
6190
|
-
description: "Authenticate to extension.dev and store a project-scoped access token locally so extension_publish can use it. Two-phase: call with `project` to get a code + URL for the user to authorize, then call again with the returned `deviceCode` to finish. The server picks the flow: the extension.dev-gated device flow (you authorize at extension.dev/device; GitHub federation happens server-side, no GitHub token on this machine) or, as a fallback, the legacy GitHub device flow. Never returns the token. This is the only tool besides extension_publish that talks to the hosted platform.",
|
|
6677
|
+
description: "Authenticate to extension.dev and store a project-scoped access token locally so extension_publish can use it. Two-phase: call with `project` to get a code + URL for the user to authorize, then call again with the returned `deviceCode` to finish. The server picks the flow: the extension.dev-gated device flow (you authorize at extension.dev/device; GitHub federation happens server-side, no GitHub token on this machine) or, as a fallback, the legacy GitHub device flow. Never returns the token. Minted tokens live at most 7 days (server-enforced), so CI pipelines must re-mint before expiry on the console's Access tokens page (project settings -> Access tokens). This is the only tool besides extension_publish that talks to the hosted platform.",
|
|
6191
6678
|
inputSchema: {
|
|
6192
6679
|
type: "object",
|
|
6193
6680
|
properties: {
|
|
@@ -6221,13 +6708,15 @@ function login_fail(name, message) {
|
|
|
6221
6708
|
});
|
|
6222
6709
|
}
|
|
6223
6710
|
function success(creds) {
|
|
6711
|
+
const expiresAt = creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null;
|
|
6224
6712
|
return JSON.stringify({
|
|
6225
6713
|
ok: true,
|
|
6226
6714
|
status: "logged-in",
|
|
6227
6715
|
workspaceSlug: creds.workspaceSlug,
|
|
6228
6716
|
projectSlug: creds.projectSlug,
|
|
6229
|
-
expiresAt
|
|
6230
|
-
|
|
6717
|
+
expiresAt,
|
|
6718
|
+
tokenTtlNote: tokenTtlNote(creds.workspaceSlug, creds.projectSlug),
|
|
6719
|
+
message: `Logged in to ${creds.workspaceSlug}/${creds.projectSlug}. extension_publish can now use the stored token. The token expires ${expiresAt ?? "within 7 days"}: extension.dev CLI tokens live at most 7 days, so CI must re-mint before then (console: project settings -> Access tokens).`
|
|
6231
6720
|
});
|
|
6232
6721
|
}
|
|
6233
6722
|
function login_pending(start) {
|
|
@@ -6243,6 +6732,7 @@ function login_pending(start) {
|
|
|
6243
6732
|
verificationUriComplete: complete
|
|
6244
6733
|
} : {},
|
|
6245
6734
|
deviceCode: start.deviceCode,
|
|
6735
|
+
tokenTtlNote: "Once authorized, the minted token lives at most 7 days (server-enforced); CI must re-mint before expiry (console: project settings -> Access tokens).",
|
|
6246
6736
|
message
|
|
6247
6737
|
});
|
|
6248
6738
|
}
|
|
@@ -6252,6 +6742,7 @@ function resumePending(deviceCode, verificationUri) {
|
|
|
6252
6742
|
status: "authorization_pending",
|
|
6253
6743
|
verificationUri,
|
|
6254
6744
|
deviceCode,
|
|
6745
|
+
tokenTtlNote: "Once authorized, the minted token lives at most 7 days (server-enforced); CI must re-mint before expiry (console: project settings -> Access tokens).",
|
|
6255
6746
|
message: `Still waiting for authorization. The one-click link and code from the previous response are still valid: open that link (or enter the code at ${verificationUri}), then call extension_login again with this same deviceCode (and the same project).`
|
|
6256
6747
|
});
|
|
6257
6748
|
}
|
|
@@ -6361,7 +6852,7 @@ async function login_handler(args) {
|
|
|
6361
6852
|
}
|
|
6362
6853
|
const whoami_schema = {
|
|
6363
6854
|
name: "extension_whoami",
|
|
6364
|
-
description: "Report the identity carried by the locally stored extension.dev token that extension_login minted (workspace/project scoped), plus its expiry, without revealing the token. The identity comes from that stored token alone; it does not change with the current working directory or whichever project folder you are in. Returns logged-out status when no credentials are stored.",
|
|
6855
|
+
description: "Report the identity carried by the locally stored extension.dev token that extension_login minted (workspace/project scoped), plus its expiry, without revealing the token. The identity comes from that stored token alone; it does not change with the current working directory or whichever project folder you are in. The result records where the login was minted (apiRecordedAtLogin) without asserting a platform base URL: authenticated tools target their own api argument, EXTENSION_DEV_API_URL, or the production default. Returns logged-out status when no credentials are stored.",
|
|
6365
6856
|
inputSchema: {
|
|
6366
6857
|
type: "object",
|
|
6367
6858
|
properties: {}
|
|
@@ -6376,17 +6867,32 @@ async function whoami_handler() {
|
|
|
6376
6867
|
});
|
|
6377
6868
|
const now = Math.floor(Date.now() / 1000);
|
|
6378
6869
|
const expired = Boolean(creds.expiresAt && creds.expiresAt <= now);
|
|
6870
|
+
const recordedApi = String(creds.api || "").trim();
|
|
6871
|
+
const effectiveDefaultApi = resolveApiBase();
|
|
6872
|
+
const apiDiverges = Boolean(recordedApi) && recordedApi !== effectiveDefaultApi;
|
|
6873
|
+
const envTokenSet = Boolean(String(process.env.EXTENSION_DEV_TOKEN || "").trim());
|
|
6874
|
+
const messageParts = [];
|
|
6875
|
+
messageParts.push(expired ? "The stored token has expired. Run extension_login to refresh it." : `Logged in as ${creds.workspaceSlug}/${creds.projectSlug}, per the token extension_login stored on this machine. That token is what scopes the identity: it does not follow the current working directory or project folder.`);
|
|
6876
|
+
if (apiDiverges) messageParts.push(`This login was minted via ${recordedApi}, but authenticated tools do not read that recorded value: they target ${effectiveDefaultApi} unless given an api argument.`);
|
|
6877
|
+
if (envTokenSet) messageParts.push("EXTENSION_DEV_TOKEN is set and takes precedence over this stored login for authenticated tools; this report describes only the stored login.");
|
|
6379
6878
|
return JSON.stringify({
|
|
6380
6879
|
ok: true,
|
|
6381
6880
|
status: expired ? "expired" : "logged-in",
|
|
6382
6881
|
workspaceSlug: creds.workspaceSlug,
|
|
6383
6882
|
projectSlug: creds.projectSlug,
|
|
6384
|
-
|
|
6883
|
+
...recordedApi ? {
|
|
6884
|
+
apiRecordedAtLogin: recordedApi
|
|
6885
|
+
} : {},
|
|
6886
|
+
apiDefault: effectiveDefaultApi,
|
|
6385
6887
|
provider: creds.provider ?? "github",
|
|
6386
6888
|
expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
|
|
6387
6889
|
expiresInSeconds: creds.expiresAt ? creds.expiresAt - now : null,
|
|
6388
6890
|
expired,
|
|
6389
|
-
|
|
6891
|
+
...envTokenSet ? {
|
|
6892
|
+
envTokenOverride: true
|
|
6893
|
+
} : {},
|
|
6894
|
+
tokenTtlNote: tokenTtlNote(creds.workspaceSlug, creds.projectSlug),
|
|
6895
|
+
message: messageParts.join(" ")
|
|
6390
6896
|
});
|
|
6391
6897
|
}
|
|
6392
6898
|
const logout_schema = {
|
|
@@ -7073,6 +7579,7 @@ const tools = [
|
|
|
7073
7579
|
release_list_namespaceObject,
|
|
7074
7580
|
release_promote_namespaceObject,
|
|
7075
7581
|
deploy_namespaceObject,
|
|
7582
|
+
store_status_namespaceObject,
|
|
7076
7583
|
wait_namespaceObject,
|
|
7077
7584
|
add_feature_namespaceObject,
|
|
7078
7585
|
login_namespaceObject,
|