@extension.dev/mcp 5.4.0 → 5.5.1
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 +81 -2
- package/README.md +2 -2
- package/dist/module.js +593 -127
- package/dist/src/lib/cdp-targets.d.ts +15 -0
- package/dist/src/lib/login-flow.d.ts +8 -0
- package/dist/src/tools/dom-inspect.d.ts +11 -0
- package/dist/src/tools/eval.d.ts +1 -1
- 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
|
@@ -68,6 +68,7 @@ var eval_namespaceObject = {};
|
|
|
68
68
|
__webpack_require__.r(eval_namespaceObject);
|
|
69
69
|
__webpack_require__.d(eval_namespaceObject, {
|
|
70
70
|
handler: ()=>eval_handler,
|
|
71
|
+
resolveDefaultEvalContext: ()=>resolveDefaultEvalContext,
|
|
71
72
|
schema: ()=>eval_schema
|
|
72
73
|
});
|
|
73
74
|
var get_template_source_namespaceObject = {};
|
|
@@ -192,6 +193,14 @@ __webpack_require__.d(storage_namespaceObject, {
|
|
|
192
193
|
handler: ()=>storage_handler,
|
|
193
194
|
schema: ()=>storage_schema
|
|
194
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
|
+
});
|
|
195
204
|
var uninstall_browser_namespaceObject = {};
|
|
196
205
|
__webpack_require__.r(uninstall_browser_namespaceObject);
|
|
197
206
|
__webpack_require__.d(uninstall_browser_namespaceObject, {
|
|
@@ -210,7 +219,7 @@ __webpack_require__.d(whoami_namespaceObject, {
|
|
|
210
219
|
handler: ()=>whoami_handler,
|
|
211
220
|
schema: ()=>whoami_schema
|
|
212
221
|
});
|
|
213
|
-
var package_namespaceObject = JSON.parse('{"rE":"5.
|
|
222
|
+
var package_namespaceObject = JSON.parse('{"rE":"5.5.1","El":{"OP":"^4.0.13"}}');
|
|
214
223
|
function scaffoldEnginePin(projectPath) {
|
|
215
224
|
try {
|
|
216
225
|
const pkg = JSON.parse(node_fs.readFileSync(node_path.join(projectPath, "package.json"), "utf8"));
|
|
@@ -3179,7 +3188,7 @@ async function source_inspect_handler(args) {
|
|
|
3179
3188
|
}
|
|
3180
3189
|
const list_extensions_schema = {
|
|
3181
3190
|
name: "extension_list_extensions",
|
|
3182
|
-
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).
|
|
3191
|
+
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.",
|
|
3183
3192
|
inputSchema: {
|
|
3184
3193
|
type: "object",
|
|
3185
3194
|
properties: {
|
|
@@ -3197,6 +3206,47 @@ const list_extensions_schema = {
|
|
|
3197
3206
|
]
|
|
3198
3207
|
}
|
|
3199
3208
|
};
|
|
3209
|
+
function unpackedExtensionId(distPath) {
|
|
3210
|
+
const digest = node_crypto.createHash("sha256").update(distPath).digest();
|
|
3211
|
+
let id = "";
|
|
3212
|
+
for(let i = 0; i < 16; i++){
|
|
3213
|
+
id += String.fromCharCode(97 + (digest[i] >> 4));
|
|
3214
|
+
id += String.fromCharCode(97 + (0x0f & digest[i]));
|
|
3215
|
+
}
|
|
3216
|
+
return id;
|
|
3217
|
+
}
|
|
3218
|
+
function readOwnIdentity(projectPath, browser) {
|
|
3219
|
+
let contract;
|
|
3220
|
+
try {
|
|
3221
|
+
const file = node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json");
|
|
3222
|
+
contract = JSON.parse(node_fs.readFileSync(file, "utf8"));
|
|
3223
|
+
} catch {
|
|
3224
|
+
return null;
|
|
3225
|
+
}
|
|
3226
|
+
const distPath = "string" == typeof contract?.distPath ? contract.distPath : null;
|
|
3227
|
+
const ids = [];
|
|
3228
|
+
if (distPath) {
|
|
3229
|
+
ids.push(unpackedExtensionId(distPath));
|
|
3230
|
+
try {
|
|
3231
|
+
const real = node_fs.realpathSync(distPath);
|
|
3232
|
+
if (real !== distPath) ids.push(unpackedExtensionId(real));
|
|
3233
|
+
} catch {}
|
|
3234
|
+
}
|
|
3235
|
+
let name = "string" == typeof contract?.extensionName ? contract.extensionName : void 0;
|
|
3236
|
+
let version = "string" == typeof contract?.extensionVersion ? contract.extensionVersion : void 0;
|
|
3237
|
+
if ((!name || !version) && distPath) try {
|
|
3238
|
+
const manifest = JSON.parse(node_fs.readFileSync(node_path.join(distPath, "manifest.json"), "utf8"));
|
|
3239
|
+
if (!name && "string" == typeof manifest?.name && !manifest.name.startsWith("__MSG_")) name = manifest.name;
|
|
3240
|
+
if (!version && "string" == typeof manifest?.version) version = manifest.version;
|
|
3241
|
+
} catch {}
|
|
3242
|
+
if (0 === ids.length && !name) return null;
|
|
3243
|
+
return {
|
|
3244
|
+
ids,
|
|
3245
|
+
name,
|
|
3246
|
+
version
|
|
3247
|
+
};
|
|
3248
|
+
}
|
|
3249
|
+
const UNRESOLVED_NOTE = "Identity unresolved: the browser's Extensions CDP domain returned nothing for this id, and other extensions' contexts are never attached to or evaluated in to read a manifest.";
|
|
3200
3250
|
async function list_extensions_handler(args) {
|
|
3201
3251
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser, "chrome");
|
|
3202
3252
|
if (!isChromiumFamily(browser)) return JSON.stringify({
|
|
@@ -3237,6 +3287,7 @@ async function list_extensions_handler(args) {
|
|
|
3237
3287
|
});
|
|
3238
3288
|
byId.set(id, list);
|
|
3239
3289
|
}
|
|
3290
|
+
const own = readOwnIdentity(args.projectPath, browser);
|
|
3240
3291
|
const extensions = [];
|
|
3241
3292
|
for (const [id, ctxTargets] of byId){
|
|
3242
3293
|
const entry = {
|
|
@@ -3257,15 +3308,33 @@ async function list_extensions_handler(args) {
|
|
|
3257
3308
|
entry.source = "extensions-domain";
|
|
3258
3309
|
}
|
|
3259
3310
|
} catch {}
|
|
3311
|
+
if (own?.ids.includes(id)) {
|
|
3312
|
+
entry.ownExtension = true;
|
|
3313
|
+
if (void 0 === entry.name && void 0 !== own.name) {
|
|
3314
|
+
entry.name = own.name;
|
|
3315
|
+
if (void 0 !== own.version) entry.version = own.version;
|
|
3316
|
+
entry.source = "session-contract";
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3260
3319
|
extensions.push(entry);
|
|
3261
3320
|
}
|
|
3262
|
-
|
|
3321
|
+
if (own?.name && !extensions.some((e)=>e.ownExtension)) {
|
|
3322
|
+
const byName = extensions.filter((e)=>e.name === own.name);
|
|
3323
|
+
if (1 === byName.length) byName[0].ownExtension = true;
|
|
3324
|
+
}
|
|
3325
|
+
for (const entry of extensions)if (void 0 === entry.name) entry.note = UNRESOLVED_NOTE;
|
|
3326
|
+
extensions.sort((a, b)=>{
|
|
3327
|
+
if ((a.ownExtension ?? false) !== (b.ownExtension ?? false)) return a.ownExtension ? -1 : 1;
|
|
3328
|
+
return (a.name ?? a.id).localeCompare(b.name ?? b.id);
|
|
3329
|
+
});
|
|
3330
|
+
const ownEntry = extensions.find((e)=>e.ownExtension);
|
|
3263
3331
|
return JSON.stringify({
|
|
3264
3332
|
cdpPort,
|
|
3265
3333
|
browser,
|
|
3266
3334
|
count: extensions.length,
|
|
3335
|
+
ownExtensionId: ownEntry?.id ?? null,
|
|
3267
3336
|
extensions,
|
|
3268
|
-
note: "Lists extensions that currently have at least one live context (service worker or open page). An MV3 service worker that has gone dormant with no open page may be absent until it wakes.
|
|
3337
|
+
note: "Lists extensions that currently have at least one live context (service worker or open page). An MV3 service worker that has gone dormant with no open page may be absent until it wakes. ownExtension marks the extension this dev session serves, identified from the session's ready contract. Other identity is read read-only via the Extensions domain; other extensions' contexts are never attached to or evaluated in."
|
|
3269
3338
|
});
|
|
3270
3339
|
} catch (error) {
|
|
3271
3340
|
return JSON.stringify({
|
|
@@ -3597,7 +3666,7 @@ async function logs_handler(args) {
|
|
|
3597
3666
|
return readFromFile(args, browser, limit);
|
|
3598
3667
|
}
|
|
3599
3668
|
function toMcpSpeak(text) {
|
|
3600
|
-
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(/--context[= ](
|
|
3669
|
+
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`");
|
|
3601
3670
|
}
|
|
3602
3671
|
function withSessionContext(message, projectPath) {
|
|
3603
3672
|
const isControlError = /no active control channel|control channel refused|\b1006\b|no executor connected|is the session started with allowControl/i.test(message);
|
|
@@ -3650,7 +3719,7 @@ function commonFlags(args) {
|
|
|
3650
3719
|
}
|
|
3651
3720
|
const eval_schema = {
|
|
3652
3721
|
name: "extension_eval",
|
|
3653
|
-
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). 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).
|
|
3722
|
+
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`.",
|
|
3654
3723
|
inputSchema: {
|
|
3655
3724
|
type: "object",
|
|
3656
3725
|
properties: {
|
|
@@ -3676,8 +3745,7 @@ const eval_schema = {
|
|
|
3676
3745
|
"content",
|
|
3677
3746
|
"page"
|
|
3678
3747
|
],
|
|
3679
|
-
default: "background"
|
|
3680
|
-
description: "Which extension surface to evaluate in"
|
|
3748
|
+
description: "Which extension surface to evaluate in. Default: `background`, EXCEPT on Chromium sessions whose manifest is MV3, where the default is `page` (the active tab) because the MV3 service worker CSP blocks eval; pass `context: \"background\"` explicitly to target the worker anyway."
|
|
3681
3749
|
},
|
|
3682
3750
|
url: {
|
|
3683
3751
|
type: "string",
|
|
@@ -3702,14 +3770,38 @@ const eval_schema = {
|
|
|
3702
3770
|
]
|
|
3703
3771
|
}
|
|
3704
3772
|
};
|
|
3773
|
+
function resolveDefaultEvalContext(projectPath, browser) {
|
|
3774
|
+
if (!isChromiumFamily(browser)) return "background";
|
|
3775
|
+
const candidates = [
|
|
3776
|
+
node_path.join(projectPath, "dist", browser, "manifest.json"),
|
|
3777
|
+
node_path.join(projectPath, "dist", "manifest.json"),
|
|
3778
|
+
node_path.join(projectPath, "src", "manifest.json"),
|
|
3779
|
+
node_path.join(projectPath, "manifest.json")
|
|
3780
|
+
];
|
|
3781
|
+
for (const file of candidates){
|
|
3782
|
+
let manifest;
|
|
3783
|
+
try {
|
|
3784
|
+
manifest = JSON.parse(node_fs.readFileSync(file, "utf8"));
|
|
3785
|
+
} catch {
|
|
3786
|
+
continue;
|
|
3787
|
+
}
|
|
3788
|
+
const version = manifest["chromium:manifest_version"] ?? manifest.manifest_version;
|
|
3789
|
+
if (3 === version) return "page";
|
|
3790
|
+
if (2 === version) break;
|
|
3791
|
+
}
|
|
3792
|
+
return "background";
|
|
3793
|
+
}
|
|
3705
3794
|
async function eval_handler(args) {
|
|
3706
3795
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
3796
|
+
const defaulted = !args.context && "page" === resolveDefaultEvalContext(args.projectPath, browser);
|
|
3797
|
+
const context = defaulted ? "page" : args.context;
|
|
3707
3798
|
const raw = await runActVerb([
|
|
3708
3799
|
"eval",
|
|
3709
3800
|
args.expression,
|
|
3710
3801
|
args.projectPath,
|
|
3711
3802
|
...commonFlags({
|
|
3712
3803
|
...args,
|
|
3804
|
+
context,
|
|
3713
3805
|
browser
|
|
3714
3806
|
})
|
|
3715
3807
|
], args.projectPath, args.timeout);
|
|
@@ -3720,6 +3812,15 @@ async function eval_handler(args) {
|
|
|
3720
3812
|
return JSON.stringify(parsed);
|
|
3721
3813
|
}
|
|
3722
3814
|
} catch {}
|
|
3815
|
+
if (defaulted) try {
|
|
3816
|
+
const parsed = JSON.parse(raw);
|
|
3817
|
+
if (parsed && "object" == typeof parsed) {
|
|
3818
|
+
parsed.defaultedContext = "page";
|
|
3819
|
+
parsed.contextNote = 'No context given: defaulted to "page" (the active tab) because this Chromium session\'s MV3 background is a service worker whose CSP blocks eval. Pass context: "background" explicitly to target the worker (works on Firefox/MV2 builds).';
|
|
3820
|
+
if (false === parsed.ok && /cannot access|chrome-extension:\/\/|chrome:\/\//i.test(JSON.stringify(parsed.error ?? ""))) parsed.hint = "The active tab is a browser or extension page that eval cannot reach. Navigate the dev browser to a regular web page, or pass url (match pattern) or tab to pick one; extension_dom_inspect with listTabs: true lists open tabs.";
|
|
3821
|
+
return JSON.stringify(parsed);
|
|
3822
|
+
}
|
|
3823
|
+
} catch {}
|
|
3723
3824
|
return raw;
|
|
3724
3825
|
}
|
|
3725
3826
|
const storage_schema = {
|
|
@@ -3948,7 +4049,7 @@ async function navigateToUrl(projectPath, browser, url) {
|
|
|
3948
4049
|
} catch {}
|
|
3949
4050
|
}
|
|
3950
4051
|
}
|
|
3951
|
-
function
|
|
4052
|
+
function open_unpackedExtensionId(distPath) {
|
|
3952
4053
|
const digest = node_crypto.createHash("sha256").update(distPath).digest();
|
|
3953
4054
|
let id = "";
|
|
3954
4055
|
for(let i = 0; i < 16; i++){
|
|
@@ -3959,7 +4060,7 @@ function unpackedExtensionId(distPath) {
|
|
|
3959
4060
|
}
|
|
3960
4061
|
async function resolveExtensionId(projectPath, browser) {
|
|
3961
4062
|
const distPath = readDistPath(projectPath, browser);
|
|
3962
|
-
const computed = distPath ?
|
|
4063
|
+
const computed = distPath ? open_unpackedExtensionId(distPath) : null;
|
|
3963
4064
|
const resolved = await resolveCdpPort(projectPath, browser);
|
|
3964
4065
|
if (!resolved) return computed;
|
|
3965
4066
|
const ids = new Set();
|
|
@@ -4283,9 +4384,27 @@ async function open_handler(args) {
|
|
|
4283
4384
|
} catch {}
|
|
4284
4385
|
return raw;
|
|
4285
4386
|
}
|
|
4387
|
+
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.";
|
|
4388
|
+
function filterPageTargets(raw) {
|
|
4389
|
+
return raw.filter((t)=>"page" === t.type && !String(t.url ?? "").startsWith("devtools://")).map((t)=>({
|
|
4390
|
+
targetId: String(t.id),
|
|
4391
|
+
type: String(t.type),
|
|
4392
|
+
url: String(t.url ?? ""),
|
|
4393
|
+
title: String(t.title ?? "")
|
|
4394
|
+
}));
|
|
4395
|
+
}
|
|
4396
|
+
async function listPageTargets(port) {
|
|
4397
|
+
return filterPageTargets(await CDPClient.discoverTargets(port));
|
|
4398
|
+
}
|
|
4399
|
+
function matchTargetsByUrl(targets, needle) {
|
|
4400
|
+
const wanted = needle.toLowerCase();
|
|
4401
|
+
const byUrl = targets.filter((t)=>t.url.toLowerCase().includes(wanted));
|
|
4402
|
+
if (byUrl.length > 0) return byUrl;
|
|
4403
|
+
return targets.filter((t)=>t.title.toLowerCase().includes(wanted));
|
|
4404
|
+
}
|
|
4286
4405
|
const dom_inspect_schema = {
|
|
4287
4406
|
name: "extension_dom_inspect",
|
|
4288
|
-
description: "Inspect a page/content-script DOM via the agent bridge (CDP-free, localhost). Returns a structured snapshot (counts, extension roots, open shadow roots, optional capped HTML). Requires the dev session to be started with allowControl: true (extension_dev). For closed shadow roots or deep CDP inspection use extension_source_inspect. Wraps `extension inspect`.",
|
|
4407
|
+
description: "Inspect a page/content-script DOM via the agent bridge (CDP-free, localhost). Returns a structured snapshot (counts, extension roots, open shadow roots, optional capped HTML). Target a tab by `tabUrl` (case-insensitive URL substring resolved against the browser's live page targets; zero or several matches return the candidates instead of guessing), by `url`, or by numeric `tab`. Discover what is open with listTargets: true (CDP targetIds) or listTabs: true (numeric chrome.tabs ids). Requires the dev session to be started with allowControl: true (extension_dev). For closed shadow roots or deep CDP inspection use extension_source_inspect. Wraps `extension inspect`.",
|
|
4289
4408
|
inputSchema: {
|
|
4290
4409
|
type: "object",
|
|
4291
4410
|
properties: {
|
|
@@ -4301,6 +4420,15 @@ const dom_inspect_schema = {
|
|
|
4301
4420
|
type: "string",
|
|
4302
4421
|
description: "For content/page: selects the target tab by url (match pattern, then substring fallback). Preferred over `tab`."
|
|
4303
4422
|
},
|
|
4423
|
+
tabUrl: {
|
|
4424
|
+
type: "string",
|
|
4425
|
+
description: "Target the tab whose URL contains this substring (case-insensitive; titles are checked only when no url matches). Resolved against the live browser's CDP page targets BEFORE inspecting: exactly one match proceeds; zero or several matches return the candidate targets (targetId/url/title) so you can narrow, never a guess. Chromium sessions only; on Firefox use `url`/`tab`. Alternative to `url`."
|
|
4426
|
+
},
|
|
4427
|
+
listTargets: {
|
|
4428
|
+
type: "boolean",
|
|
4429
|
+
default: false,
|
|
4430
|
+
description: "Enumerate the browser's CDP page targets as {targetId,url,title,type} and return, ignoring the other args. The discovery path for `tabUrl`. targetId is a CDP target id, NOT a numeric chrome.tabs id (for those use listTabs)."
|
|
4431
|
+
},
|
|
4304
4432
|
listTabs: {
|
|
4305
4433
|
type: "boolean",
|
|
4306
4434
|
default: false,
|
|
@@ -4361,8 +4489,55 @@ const dom_inspect_schema = {
|
|
|
4361
4489
|
]
|
|
4362
4490
|
}
|
|
4363
4491
|
};
|
|
4492
|
+
async function cdpPortOrError(projectPath, browser, feature) {
|
|
4493
|
+
if (!isChromiumFamily(browser)) return {
|
|
4494
|
+
error: JSON.stringify({
|
|
4495
|
+
ok: false,
|
|
4496
|
+
error: {
|
|
4497
|
+
name: "Unsupported",
|
|
4498
|
+
message: `${feature} reads the browser's CDP page targets, which ${browser} (Gecko) does not expose. Target the tab with \`url\` or \`tab\` instead, and discover tabs with listTabs: true (agent bridge, works on every browser).`
|
|
4499
|
+
}
|
|
4500
|
+
})
|
|
4501
|
+
};
|
|
4502
|
+
const resolved = await resolveCdpPort(projectPath, browser);
|
|
4503
|
+
if (!resolved) return {
|
|
4504
|
+
error: JSON.stringify({
|
|
4505
|
+
ok: false,
|
|
4506
|
+
error: {
|
|
4507
|
+
name: "NoSession",
|
|
4508
|
+
message: `No active dev session / CDP port for ${browser}, so ${feature} has no browser to ask. Start extension_dev and extension_wait for ready. ${CDP_PORT_MISSING_HINT}`
|
|
4509
|
+
}
|
|
4510
|
+
})
|
|
4511
|
+
};
|
|
4512
|
+
return {
|
|
4513
|
+
port: resolved.port
|
|
4514
|
+
};
|
|
4515
|
+
}
|
|
4364
4516
|
async function dom_inspect_handler(args) {
|
|
4365
4517
|
const withConsole = true === args.withConsole ? 50 : false === args.withConsole ? void 0 : args.withConsole;
|
|
4518
|
+
if (args.listTargets) {
|
|
4519
|
+
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
4520
|
+
const cdp = await cdpPortOrError(args.projectPath, browser, "listTargets");
|
|
4521
|
+
if ("error" in cdp) return cdp.error;
|
|
4522
|
+
try {
|
|
4523
|
+
const targets = await listPageTargets(cdp.port);
|
|
4524
|
+
return JSON.stringify({
|
|
4525
|
+
ok: true,
|
|
4526
|
+
browser,
|
|
4527
|
+
targets,
|
|
4528
|
+
note: TARGET_ID_NOTE
|
|
4529
|
+
});
|
|
4530
|
+
} catch (e) {
|
|
4531
|
+
return JSON.stringify({
|
|
4532
|
+
ok: false,
|
|
4533
|
+
error: {
|
|
4534
|
+
name: "CdpError",
|
|
4535
|
+
message: `Could not list page targets: ${e instanceof Error ? e.message : String(e)}`
|
|
4536
|
+
},
|
|
4537
|
+
hint: "Confirm the session is ready (extension_wait), then retry. listTabs: true is the CDP-free alternative."
|
|
4538
|
+
});
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
4366
4541
|
if (args.listTabs) return runActVerb([
|
|
4367
4542
|
"inspect",
|
|
4368
4543
|
args.projectPath,
|
|
@@ -4374,19 +4549,78 @@ async function dom_inspect_handler(args) {
|
|
|
4374
4549
|
String(args.timeout)
|
|
4375
4550
|
] : []
|
|
4376
4551
|
], args.projectPath, args.timeout);
|
|
4552
|
+
let targetUrl = args.url;
|
|
4553
|
+
let resolvedTarget = null;
|
|
4554
|
+
if (args.tabUrl) {
|
|
4555
|
+
if (null != args.tab || args.url) return JSON.stringify({
|
|
4556
|
+
ok: false,
|
|
4557
|
+
error: {
|
|
4558
|
+
name: "BadRequest",
|
|
4559
|
+
message: "Pass ONE tab selector: `tabUrl` (URL substring, resolved against live targets), `url` (engine-side match), or `tab` (numeric chrome.tabs id), not several."
|
|
4560
|
+
}
|
|
4561
|
+
});
|
|
4562
|
+
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
4563
|
+
const cdp = await cdpPortOrError(args.projectPath, browser, "tabUrl");
|
|
4564
|
+
if ("error" in cdp) return cdp.error;
|
|
4565
|
+
let targets;
|
|
4566
|
+
try {
|
|
4567
|
+
targets = await listPageTargets(cdp.port);
|
|
4568
|
+
} catch (e) {
|
|
4569
|
+
return JSON.stringify({
|
|
4570
|
+
ok: false,
|
|
4571
|
+
error: {
|
|
4572
|
+
name: "CdpError",
|
|
4573
|
+
message: `Could not list page targets to resolve tabUrl: ${e instanceof Error ? e.message : String(e)}`
|
|
4574
|
+
},
|
|
4575
|
+
hint: "Confirm the session is ready (extension_wait), then retry, or target with `url`/`tab` instead."
|
|
4576
|
+
});
|
|
4577
|
+
}
|
|
4578
|
+
const matches = matchTargetsByUrl(targets, args.tabUrl);
|
|
4579
|
+
if (0 === matches.length) return JSON.stringify({
|
|
4580
|
+
ok: false,
|
|
4581
|
+
error: {
|
|
4582
|
+
name: "NoMatchingTarget",
|
|
4583
|
+
message: `No open page target's url (or title) contains "${args.tabUrl}" (case-insensitive).`
|
|
4584
|
+
},
|
|
4585
|
+
availableTargets: targets,
|
|
4586
|
+
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}`
|
|
4587
|
+
});
|
|
4588
|
+
if (matches.length > 1) return JSON.stringify({
|
|
4589
|
+
ok: false,
|
|
4590
|
+
error: {
|
|
4591
|
+
name: "AmbiguousTabUrl",
|
|
4592
|
+
message: `${matches.length} page targets match "${args.tabUrl}"; refusing to guess which tab you mean.`
|
|
4593
|
+
},
|
|
4594
|
+
matchingTargets: matches,
|
|
4595
|
+
hint: `Narrow \`tabUrl\` to a longer substring that matches exactly one url in matchingTargets. ${TARGET_ID_NOTE}`
|
|
4596
|
+
});
|
|
4597
|
+
resolvedTarget = matches[0];
|
|
4598
|
+
targetUrl = resolvedTarget.url;
|
|
4599
|
+
}
|
|
4377
4600
|
const cli = [
|
|
4378
4601
|
"inspect",
|
|
4379
4602
|
args.projectPath
|
|
4380
4603
|
];
|
|
4381
4604
|
if (null != args.tab) cli.push("--tab", String(args.tab));
|
|
4382
|
-
if (
|
|
4605
|
+
if (targetUrl) cli.push("--url", targetUrl);
|
|
4383
4606
|
if (args.context) cli.push("--context", args.context);
|
|
4384
4607
|
if (args.include?.length) cli.push("--include", args.include.join(","));
|
|
4385
4608
|
if (null != args.maxBytes) cli.push("--max-bytes", String(args.maxBytes));
|
|
4386
4609
|
if (null != withConsole) cli.push("--with-console", String(withConsole));
|
|
4387
4610
|
cli.push("--browser", resolveSessionBrowser(args.projectPath, args.browser).browser);
|
|
4388
4611
|
if (null != args.timeout) cli.push("--timeout", String(args.timeout));
|
|
4389
|
-
|
|
4612
|
+
const raw = await runActVerb(cli, args.projectPath, args.timeout);
|
|
4613
|
+
if (!resolvedTarget) return raw;
|
|
4614
|
+
try {
|
|
4615
|
+
const parsed = JSON.parse(raw);
|
|
4616
|
+
parsed.resolvedTarget = {
|
|
4617
|
+
...resolvedTarget,
|
|
4618
|
+
matchedBy: "tabUrl"
|
|
4619
|
+
};
|
|
4620
|
+
return JSON.stringify(parsed);
|
|
4621
|
+
} catch {
|
|
4622
|
+
return raw;
|
|
4623
|
+
}
|
|
4390
4624
|
}
|
|
4391
4625
|
function credentialsPath() {
|
|
4392
4626
|
if ("win32" === process.platform) {
|
|
@@ -4460,7 +4694,117 @@ function readValidCredentials(nowSeconds = Math.floor(Date.now() / 1000)) {
|
|
|
4460
4694
|
if (creds.expiresAt && creds.expiresAt <= nowSeconds) return null;
|
|
4461
4695
|
return creds;
|
|
4462
4696
|
}
|
|
4697
|
+
const REGISTRY_BASE_DEFAULT = "https://registry.extension.land";
|
|
4698
|
+
const CONSOLE_BASE = "https://console.extension.dev";
|
|
4699
|
+
function registryBase() {
|
|
4700
|
+
const fromEnv = String(process.env.EXTENSION_DEV_REGISTRY_URL || "").trim();
|
|
4701
|
+
return (fromEnv || REGISTRY_BASE_DEFAULT).replace(/\/+$/, "");
|
|
4702
|
+
}
|
|
4703
|
+
function resolveProjectRef(overrides) {
|
|
4704
|
+
const workspace = String(overrides?.workspace || "").trim();
|
|
4705
|
+
const project = String(overrides?.project || "").trim();
|
|
4706
|
+
if (workspace && project) return {
|
|
4707
|
+
workspace,
|
|
4708
|
+
project
|
|
4709
|
+
};
|
|
4710
|
+
const creds = readCredentials();
|
|
4711
|
+
const ws = workspace || String(creds?.workspaceSlug || "").trim();
|
|
4712
|
+
const proj = project || String(creds?.projectSlug || "").trim();
|
|
4713
|
+
if (!ws || !proj) return null;
|
|
4714
|
+
return {
|
|
4715
|
+
workspace: ws,
|
|
4716
|
+
project: proj
|
|
4717
|
+
};
|
|
4718
|
+
}
|
|
4719
|
+
function registryFileUrl(ref, file) {
|
|
4720
|
+
return `${registryBase()}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/_extension-dev/${file}`;
|
|
4721
|
+
}
|
|
4722
|
+
function consoleProjectUrl(ref, page) {
|
|
4723
|
+
if (!ref) return `${CONSOLE_BASE}`;
|
|
4724
|
+
return `${CONSOLE_BASE}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/${page}`;
|
|
4725
|
+
}
|
|
4726
|
+
async function fetchRegistryJson(url, fetchImpl = fetch) {
|
|
4727
|
+
let res;
|
|
4728
|
+
try {
|
|
4729
|
+
res = await fetchImpl(url);
|
|
4730
|
+
} catch (err) {
|
|
4731
|
+
return {
|
|
4732
|
+
ok: false,
|
|
4733
|
+
message: `Could not reach ${url}: ${err?.message || err}`
|
|
4734
|
+
};
|
|
4735
|
+
}
|
|
4736
|
+
if (!res.ok) return {
|
|
4737
|
+
ok: false,
|
|
4738
|
+
status: res.status,
|
|
4739
|
+
message: `${url} returned ${res.status}`
|
|
4740
|
+
};
|
|
4741
|
+
try {
|
|
4742
|
+
const text = await res.text();
|
|
4743
|
+
return {
|
|
4744
|
+
ok: true,
|
|
4745
|
+
json: JSON.parse(text)
|
|
4746
|
+
};
|
|
4747
|
+
} catch {
|
|
4748
|
+
return {
|
|
4749
|
+
ok: false,
|
|
4750
|
+
message: `${url} did not return valid JSON`
|
|
4751
|
+
};
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
4754
|
+
function parseChannels(json) {
|
|
4755
|
+
if (!json || "object" != typeof json || Array.isArray(json)) return [];
|
|
4756
|
+
const out = [];
|
|
4757
|
+
for (const [channel, raw] of Object.entries(json)){
|
|
4758
|
+
if (!raw || "object" != typeof raw) continue;
|
|
4759
|
+
const row = raw;
|
|
4760
|
+
const description = "string" == typeof row.description ? row.description : void 0;
|
|
4761
|
+
const promotedAtField = "string" == typeof row.promotedAt && row.promotedAt ? row.promotedAt : void 0;
|
|
4762
|
+
const fromDescription = description?.match(/\bon (\d{4}-\d{2}-\d{2}T[0-9:.]+Z?)/)?.[1];
|
|
4763
|
+
const entry = {
|
|
4764
|
+
channel,
|
|
4765
|
+
sha: String(row.sha ?? "")
|
|
4766
|
+
};
|
|
4767
|
+
if (row.buildId) entry.buildId = String(row.buildId);
|
|
4768
|
+
if (row.version) entry.version = String(row.version);
|
|
4769
|
+
const promotedAt = promotedAtField || fromDescription;
|
|
4770
|
+
if (promotedAt) entry.promotedAt = promotedAt;
|
|
4771
|
+
if (description) entry.description = description;
|
|
4772
|
+
out.push(entry);
|
|
4773
|
+
}
|
|
4774
|
+
return out;
|
|
4775
|
+
}
|
|
4776
|
+
function parseBuildIndex(json) {
|
|
4777
|
+
const items = json?.items;
|
|
4778
|
+
if (!Array.isArray(items)) return [];
|
|
4779
|
+
const out = [];
|
|
4780
|
+
for (const raw of items){
|
|
4781
|
+
if (!raw || "object" != typeof raw) continue;
|
|
4782
|
+
const row = raw;
|
|
4783
|
+
const sha = String(row.shortSha ?? row.sha ?? row.id ?? row.buildId ?? "").trim();
|
|
4784
|
+
if (!sha) continue;
|
|
4785
|
+
const entry = {
|
|
4786
|
+
sha
|
|
4787
|
+
};
|
|
4788
|
+
if (row.commit) entry.commit = String(row.commit);
|
|
4789
|
+
if (row.channel) entry.channel = String(row.channel);
|
|
4790
|
+
if (row.buildEnv) entry.buildEnv = String(row.buildEnv);
|
|
4791
|
+
if (row.status) entry.status = String(row.status);
|
|
4792
|
+
if (row.version) entry.version = String(row.version);
|
|
4793
|
+
if ("string" == typeof row.message) entry.message = row.message.split("\n", 1)[0];
|
|
4794
|
+
if (row.timestamp) entry.timestamp = String(row.timestamp);
|
|
4795
|
+
if (Array.isArray(row.browsers)) entry.browsers = row.browsers.map((b)=>String(b)).filter(Boolean);
|
|
4796
|
+
out.push(entry);
|
|
4797
|
+
}
|
|
4798
|
+
return out;
|
|
4799
|
+
}
|
|
4463
4800
|
const DEFAULT_API = "https://www.extension.dev";
|
|
4801
|
+
function tokenTtlNote(workspaceSlug, projectSlug) {
|
|
4802
|
+
const tokensUrl = workspaceSlug && projectSlug ? consoleProjectUrl({
|
|
4803
|
+
workspace: workspaceSlug,
|
|
4804
|
+
project: projectSlug
|
|
4805
|
+
}, "settings/access-tokens") : CONSOLE_BASE;
|
|
4806
|
+
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}`;
|
|
4807
|
+
}
|
|
4464
4808
|
function resolveApiBase(api) {
|
|
4465
4809
|
return String(api || process.env.EXTENSION_DEV_API_URL || DEFAULT_API).replace(/\/+$/, "");
|
|
4466
4810
|
}
|
|
@@ -4625,109 +4969,6 @@ async function publish(options = {}) {
|
|
|
4625
4969
|
data
|
|
4626
4970
|
};
|
|
4627
4971
|
}
|
|
4628
|
-
const REGISTRY_BASE_DEFAULT = "https://registry.extension.land";
|
|
4629
|
-
const CONSOLE_BASE = "https://console.extension.dev";
|
|
4630
|
-
function registryBase() {
|
|
4631
|
-
const fromEnv = String(process.env.EXTENSION_DEV_REGISTRY_URL || "").trim();
|
|
4632
|
-
return (fromEnv || REGISTRY_BASE_DEFAULT).replace(/\/+$/, "");
|
|
4633
|
-
}
|
|
4634
|
-
function resolveProjectRef(overrides) {
|
|
4635
|
-
const workspace = String(overrides?.workspace || "").trim();
|
|
4636
|
-
const project = String(overrides?.project || "").trim();
|
|
4637
|
-
if (workspace && project) return {
|
|
4638
|
-
workspace,
|
|
4639
|
-
project
|
|
4640
|
-
};
|
|
4641
|
-
const creds = readCredentials();
|
|
4642
|
-
const ws = workspace || String(creds?.workspaceSlug || "").trim();
|
|
4643
|
-
const proj = project || String(creds?.projectSlug || "").trim();
|
|
4644
|
-
if (!ws || !proj) return null;
|
|
4645
|
-
return {
|
|
4646
|
-
workspace: ws,
|
|
4647
|
-
project: proj
|
|
4648
|
-
};
|
|
4649
|
-
}
|
|
4650
|
-
function registryFileUrl(ref, file) {
|
|
4651
|
-
return `${registryBase()}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/_extension-dev/${file}`;
|
|
4652
|
-
}
|
|
4653
|
-
function consoleProjectUrl(ref, page) {
|
|
4654
|
-
if (!ref) return `${CONSOLE_BASE}`;
|
|
4655
|
-
return `${CONSOLE_BASE}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/${page}`;
|
|
4656
|
-
}
|
|
4657
|
-
async function fetchRegistryJson(url, fetchImpl = fetch) {
|
|
4658
|
-
let res;
|
|
4659
|
-
try {
|
|
4660
|
-
res = await fetchImpl(url);
|
|
4661
|
-
} catch (err) {
|
|
4662
|
-
return {
|
|
4663
|
-
ok: false,
|
|
4664
|
-
message: `Could not reach ${url}: ${err?.message || err}`
|
|
4665
|
-
};
|
|
4666
|
-
}
|
|
4667
|
-
if (!res.ok) return {
|
|
4668
|
-
ok: false,
|
|
4669
|
-
status: res.status,
|
|
4670
|
-
message: `${url} returned ${res.status}`
|
|
4671
|
-
};
|
|
4672
|
-
try {
|
|
4673
|
-
const text = await res.text();
|
|
4674
|
-
return {
|
|
4675
|
-
ok: true,
|
|
4676
|
-
json: JSON.parse(text)
|
|
4677
|
-
};
|
|
4678
|
-
} catch {
|
|
4679
|
-
return {
|
|
4680
|
-
ok: false,
|
|
4681
|
-
message: `${url} did not return valid JSON`
|
|
4682
|
-
};
|
|
4683
|
-
}
|
|
4684
|
-
}
|
|
4685
|
-
function parseChannels(json) {
|
|
4686
|
-
if (!json || "object" != typeof json || Array.isArray(json)) return [];
|
|
4687
|
-
const out = [];
|
|
4688
|
-
for (const [channel, raw] of Object.entries(json)){
|
|
4689
|
-
if (!raw || "object" != typeof raw) continue;
|
|
4690
|
-
const row = raw;
|
|
4691
|
-
const description = "string" == typeof row.description ? row.description : void 0;
|
|
4692
|
-
const promotedAtField = "string" == typeof row.promotedAt && row.promotedAt ? row.promotedAt : void 0;
|
|
4693
|
-
const fromDescription = description?.match(/\bon (\d{4}-\d{2}-\d{2}T[0-9:.]+Z?)/)?.[1];
|
|
4694
|
-
const entry = {
|
|
4695
|
-
channel,
|
|
4696
|
-
sha: String(row.sha ?? "")
|
|
4697
|
-
};
|
|
4698
|
-
if (row.buildId) entry.buildId = String(row.buildId);
|
|
4699
|
-
if (row.version) entry.version = String(row.version);
|
|
4700
|
-
const promotedAt = promotedAtField || fromDescription;
|
|
4701
|
-
if (promotedAt) entry.promotedAt = promotedAt;
|
|
4702
|
-
if (description) entry.description = description;
|
|
4703
|
-
out.push(entry);
|
|
4704
|
-
}
|
|
4705
|
-
return out;
|
|
4706
|
-
}
|
|
4707
|
-
function parseBuildIndex(json) {
|
|
4708
|
-
const items = json?.items;
|
|
4709
|
-
if (!Array.isArray(items)) return [];
|
|
4710
|
-
const out = [];
|
|
4711
|
-
for (const raw of items){
|
|
4712
|
-
if (!raw || "object" != typeof raw) continue;
|
|
4713
|
-
const row = raw;
|
|
4714
|
-
const sha = String(row.shortSha ?? row.sha ?? row.id ?? row.buildId ?? "").trim();
|
|
4715
|
-
if (!sha) continue;
|
|
4716
|
-
const entry = {
|
|
4717
|
-
sha
|
|
4718
|
-
};
|
|
4719
|
-
if (row.commit) entry.commit = String(row.commit);
|
|
4720
|
-
if (row.channel) entry.channel = String(row.channel);
|
|
4721
|
-
if (row.buildEnv) entry.buildEnv = String(row.buildEnv);
|
|
4722
|
-
if (row.status) entry.status = String(row.status);
|
|
4723
|
-
if (row.version) entry.version = String(row.version);
|
|
4724
|
-
if ("string" == typeof row.message) entry.message = row.message.split("\n", 1)[0];
|
|
4725
|
-
if (row.timestamp) entry.timestamp = String(row.timestamp);
|
|
4726
|
-
if (Array.isArray(row.browsers)) entry.browsers = row.browsers.map((b)=>String(b)).filter(Boolean);
|
|
4727
|
-
out.push(entry);
|
|
4728
|
-
}
|
|
4729
|
-
return out;
|
|
4730
|
-
}
|
|
4731
4972
|
const publish_schema = {
|
|
4732
4973
|
name: "extension_publish",
|
|
4733
4974
|
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.",
|
|
@@ -4805,7 +5046,7 @@ async function publish_handler(args) {
|
|
|
4805
5046
|
const release_promote_DEFAULT_API = "https://www.extension.dev";
|
|
4806
5047
|
const release_promote_schema = {
|
|
4807
5048
|
name: "extension_release_promote",
|
|
4808
|
-
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).",
|
|
5049
|
+
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).",
|
|
4809
5050
|
inputSchema: {
|
|
4810
5051
|
type: "object",
|
|
4811
5052
|
properties: {
|
|
@@ -4858,7 +5099,7 @@ function release_promote_fail(name, message) {
|
|
|
4858
5099
|
}
|
|
4859
5100
|
async function release_promote_handler(args) {
|
|
4860
5101
|
const token = resolveToken();
|
|
4861
|
-
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.");
|
|
5102
|
+
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.");
|
|
4862
5103
|
const buildId = String(args.buildId || "").trim();
|
|
4863
5104
|
const channel = String(args.channel || "").trim();
|
|
4864
5105
|
if (!buildId || !channel) return release_promote_fail("ReleaseInputError", "buildId and channel are required.");
|
|
@@ -5010,7 +5251,7 @@ function storeMdWarnings(browsers, cwd) {
|
|
|
5010
5251
|
content = node_fs.readFileSync(node_path.join(cwd, "STORE.md"), "utf8");
|
|
5011
5252
|
} catch {
|
|
5012
5253
|
return [
|
|
5013
|
-
"No STORE.md found in the
|
|
5254
|
+
"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."
|
|
5014
5255
|
];
|
|
5015
5256
|
}
|
|
5016
5257
|
const hasField = (section, field)=>{
|
|
@@ -5029,7 +5270,7 @@ function storeMdWarnings(browsers, cwd) {
|
|
|
5029
5270
|
}
|
|
5030
5271
|
const deploy_schema = {
|
|
5031
5272
|
name: "extension_deploy",
|
|
5032
|
-
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 -
|
|
5273
|
+
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.",
|
|
5033
5274
|
inputSchema: {
|
|
5034
5275
|
type: "object",
|
|
5035
5276
|
properties: {
|
|
@@ -5085,7 +5326,7 @@ function deploy_fail(name, message) {
|
|
|
5085
5326
|
}
|
|
5086
5327
|
async function deploy_handler(args) {
|
|
5087
5328
|
const token = resolveToken();
|
|
5088
|
-
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).");
|
|
5329
|
+
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).");
|
|
5089
5330
|
const browsers = (Array.isArray(args.browsers) ? args.browsers : []).map((b)=>String(b).trim().toLowerCase()).filter(Boolean);
|
|
5090
5331
|
if (0 === browsers.length) return deploy_fail("DeployInputError", 'browsers is required (e.g. ["chrome","firefox","edge"]).');
|
|
5091
5332
|
const buildSha = String(args.buildSha || "").trim();
|
|
@@ -5203,9 +5444,214 @@ async function deploy_handler(args) {
|
|
|
5203
5444
|
if ("string" == typeof data?.message) result.platformMessage = data.message;
|
|
5204
5445
|
result.message = summaryParts.join(" ");
|
|
5205
5446
|
}
|
|
5447
|
+
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.";
|
|
5206
5448
|
if (warnings.length > 0) result.warnings = warnings;
|
|
5207
5449
|
return JSON.stringify(result);
|
|
5208
5450
|
}
|
|
5451
|
+
const KNOWN_STORES = [
|
|
5452
|
+
"chrome",
|
|
5453
|
+
"firefox",
|
|
5454
|
+
"edge",
|
|
5455
|
+
"safari"
|
|
5456
|
+
];
|
|
5457
|
+
const store_status_schema = {
|
|
5458
|
+
name: "extension_store_status",
|
|
5459
|
+
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.",
|
|
5460
|
+
inputSchema: {
|
|
5461
|
+
type: "object",
|
|
5462
|
+
properties: {
|
|
5463
|
+
workspace: {
|
|
5464
|
+
type: "string",
|
|
5465
|
+
description: "Workspace slug override (defaults to the stored login's workspace)."
|
|
5466
|
+
},
|
|
5467
|
+
project: {
|
|
5468
|
+
type: "string",
|
|
5469
|
+
description: "Project slug override (defaults to the stored login's project)."
|
|
5470
|
+
}
|
|
5471
|
+
},
|
|
5472
|
+
required: []
|
|
5473
|
+
}
|
|
5474
|
+
};
|
|
5475
|
+
function isPlainObject(value) {
|
|
5476
|
+
return Boolean(value) && "object" == typeof value && !Array.isArray(value);
|
|
5477
|
+
}
|
|
5478
|
+
function str(value) {
|
|
5479
|
+
const text = String(value ?? "").trim();
|
|
5480
|
+
return text || void 0;
|
|
5481
|
+
}
|
|
5482
|
+
function submissionView(row) {
|
|
5483
|
+
const view = {};
|
|
5484
|
+
const version = str(row.version);
|
|
5485
|
+
const status = str(row.status);
|
|
5486
|
+
const storeUrl = str(row.storeUrl);
|
|
5487
|
+
const submittedAt = str(row.submittedAt) || str(row.timestamp);
|
|
5488
|
+
const channel = str(row.channel);
|
|
5489
|
+
const buildSha = str(row.buildSha) || str(row.buildId);
|
|
5490
|
+
const storeSubmissionId = str(row.storeSubmissionId);
|
|
5491
|
+
const failureReason = str(row.failureReason);
|
|
5492
|
+
if (version) view.version = version;
|
|
5493
|
+
if (status) view.status = status;
|
|
5494
|
+
if (storeUrl) view.storeUrl = storeUrl;
|
|
5495
|
+
if (submittedAt) view.submittedAt = submittedAt;
|
|
5496
|
+
if (channel) view.channel = channel;
|
|
5497
|
+
if (buildSha) view.buildSha = buildSha;
|
|
5498
|
+
if (storeSubmissionId) view.storeSubmissionId = storeSubmissionId;
|
|
5499
|
+
if (failureReason) view.failureReason = failureReason;
|
|
5500
|
+
return view;
|
|
5501
|
+
}
|
|
5502
|
+
function latestSubmissionsByStore(json) {
|
|
5503
|
+
const list = json?.submissions;
|
|
5504
|
+
const out = {};
|
|
5505
|
+
for (const raw of Array.isArray(list) ? list : []){
|
|
5506
|
+
if (!isPlainObject(raw)) continue;
|
|
5507
|
+
const store = str(raw.store);
|
|
5508
|
+
if (!store) continue;
|
|
5509
|
+
const at = Date.parse(String(raw.submittedAt || raw.updatedAt || ""));
|
|
5510
|
+
const stamp = Number.isFinite(at) ? at : 0;
|
|
5511
|
+
if (!out[store] || stamp >= out[store].at) out[store] = {
|
|
5512
|
+
at: stamp,
|
|
5513
|
+
view: submissionView(raw)
|
|
5514
|
+
};
|
|
5515
|
+
}
|
|
5516
|
+
const flat = {};
|
|
5517
|
+
for (const [store, entry] of Object.entries(out))flat[store] = entry.view;
|
|
5518
|
+
return flat;
|
|
5519
|
+
}
|
|
5520
|
+
function normalizeStoresStatus(json) {
|
|
5521
|
+
const base = isPlainObject(json) ? json : {};
|
|
5522
|
+
const out = {
|
|
5523
|
+
reviews: {}
|
|
5524
|
+
};
|
|
5525
|
+
if (isPlainObject(base.lastSubmission)) out.lastSubmission = base.lastSubmission;
|
|
5526
|
+
if (isPlainObject(base.lastOverride)) out.lastOverride = base.lastOverride;
|
|
5527
|
+
if (isPlainObject(base.reviews)) {
|
|
5528
|
+
for (const [store, raw] of Object.entries(base.reviews))if (isPlainObject(raw)) out.reviews[store] = {
|
|
5529
|
+
status: str(raw.status),
|
|
5530
|
+
version: str(raw.version),
|
|
5531
|
+
buildId: str(raw.buildId),
|
|
5532
|
+
checkedAt: str(raw.checkedAt)
|
|
5533
|
+
};
|
|
5534
|
+
}
|
|
5535
|
+
const lastPoll = isPlainObject(base.lastPoll) ? base.lastPoll : null;
|
|
5536
|
+
const looksLikeLegacyPoll = !lastPoll && ("store_status" === base.kind || Array.isArray(base.updates));
|
|
5537
|
+
if (lastPoll) out.lastPollAt = str(lastPoll.timestamp);
|
|
5538
|
+
else if (looksLikeLegacyPoll) {
|
|
5539
|
+
out.lastPollAt = str(base.updatedAt);
|
|
5540
|
+
for (const raw of Array.isArray(base.updates) ? base.updates : []){
|
|
5541
|
+
if (!isPlainObject(raw)) continue;
|
|
5542
|
+
const store = str(raw.store);
|
|
5543
|
+
if (store && !out.reviews[store]) out.reviews[store] = {
|
|
5544
|
+
status: str(raw.status),
|
|
5545
|
+
version: str(raw.version),
|
|
5546
|
+
buildId: str(raw.buildId),
|
|
5547
|
+
checkedAt: str(base.updatedAt)
|
|
5548
|
+
};
|
|
5549
|
+
}
|
|
5550
|
+
}
|
|
5551
|
+
return out;
|
|
5552
|
+
}
|
|
5553
|
+
function store_status_fail(name, message, extra) {
|
|
5554
|
+
return JSON.stringify({
|
|
5555
|
+
ok: false,
|
|
5556
|
+
error: {
|
|
5557
|
+
name,
|
|
5558
|
+
message
|
|
5559
|
+
},
|
|
5560
|
+
...extra ?? {}
|
|
5561
|
+
});
|
|
5562
|
+
}
|
|
5563
|
+
async function store_status_handler(args) {
|
|
5564
|
+
const ref = resolveProjectRef(args);
|
|
5565
|
+
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.");
|
|
5566
|
+
const healthUrl = registryFileUrl(ref, "stores/health.json");
|
|
5567
|
+
const statusUrl = registryFileUrl(ref, "stores/status.json");
|
|
5568
|
+
const submissionsUrl = registryFileUrl(ref, "stores/submissions.json");
|
|
5569
|
+
const consoleStoresUrl = consoleProjectUrl(ref, "stores");
|
|
5570
|
+
const [healthRes, statusRes, submissionsRes] = await Promise.all([
|
|
5571
|
+
fetchRegistryJson(healthUrl),
|
|
5572
|
+
fetchRegistryJson(statusUrl),
|
|
5573
|
+
fetchRegistryJson(submissionsUrl)
|
|
5574
|
+
]);
|
|
5575
|
+
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}`, {
|
|
5576
|
+
workspace: ref.workspace,
|
|
5577
|
+
project: ref.project,
|
|
5578
|
+
registryUrls: {
|
|
5579
|
+
health: healthUrl,
|
|
5580
|
+
status: statusUrl,
|
|
5581
|
+
submissions: submissionsUrl
|
|
5582
|
+
},
|
|
5583
|
+
consoleStoresUrl
|
|
5584
|
+
});
|
|
5585
|
+
const healthStores = healthRes.ok ? isPlainObject(healthRes.json?.stores) ? healthRes.json.stores ?? null : null : null;
|
|
5586
|
+
const status = normalizeStoresStatus(statusRes.ok ? statusRes.json : null);
|
|
5587
|
+
const submissionsByStore = submissionsRes.ok ? latestSubmissionsByStore(submissionsRes.json) : {};
|
|
5588
|
+
const statusLastStore = str(status.lastSubmission?.store);
|
|
5589
|
+
if (statusLastStore && !submissionsByStore[statusLastStore] && status.lastSubmission) submissionsByStore[statusLastStore] = submissionView(status.lastSubmission);
|
|
5590
|
+
const stores = [
|
|
5591
|
+
...KNOWN_STORES,
|
|
5592
|
+
...Object.keys({
|
|
5593
|
+
...healthStores,
|
|
5594
|
+
...submissionsByStore,
|
|
5595
|
+
...status.reviews
|
|
5596
|
+
}).filter((s)=>!KNOWN_STORES.includes(s))
|
|
5597
|
+
];
|
|
5598
|
+
const rows = stores.map((store)=>{
|
|
5599
|
+
const healthRow = healthStores?.[store];
|
|
5600
|
+
const configured = healthStores ? Boolean(healthRow) : "unknown";
|
|
5601
|
+
const row = {
|
|
5602
|
+
store,
|
|
5603
|
+
configured
|
|
5604
|
+
};
|
|
5605
|
+
if (healthRow) row.health = {
|
|
5606
|
+
ok: true === healthRow.ok,
|
|
5607
|
+
checkedAt: str(healthRow.checkedAt),
|
|
5608
|
+
message: str(healthRow.message)
|
|
5609
|
+
};
|
|
5610
|
+
const submission = submissionsByStore[store];
|
|
5611
|
+
if (submission && Object.keys(submission).length > 0) row.lastSubmission = submission;
|
|
5612
|
+
const review = status.reviews[store];
|
|
5613
|
+
if (review && Object.values(review).some(Boolean)) row.review = review;
|
|
5614
|
+
return row;
|
|
5615
|
+
});
|
|
5616
|
+
const summaryParts = rows.map((row)=>{
|
|
5617
|
+
const store = String(row.store);
|
|
5618
|
+
const health = row.health;
|
|
5619
|
+
const submission = row.lastSubmission;
|
|
5620
|
+
const review = row.review;
|
|
5621
|
+
let head;
|
|
5622
|
+
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`;
|
|
5623
|
+
const tail = [];
|
|
5624
|
+
if (submission) {
|
|
5625
|
+
tail.push(`last submission${submission.version ? ` v${submission.version}` : ""} ${submission.status || "recorded"}${submission.submittedAt ? ` at ${submission.submittedAt}` : ""}${submission.failureReason ? ` (${submission.failureReason})` : ""}`);
|
|
5626
|
+
if (submission.storeUrl) tail.push(`listing ${submission.storeUrl}`);
|
|
5627
|
+
} else if (true === row.configured) tail.push("no submissions recorded");
|
|
5628
|
+
if (review?.status) tail.push(`review ${review.status}${review.checkedAt ? ` (checked ${review.checkedAt})` : ""}`);
|
|
5629
|
+
return tail.length > 0 ? `${head}; ${tail.join("; ")}` : head;
|
|
5630
|
+
});
|
|
5631
|
+
const result = {
|
|
5632
|
+
ok: true,
|
|
5633
|
+
workspace: ref.workspace,
|
|
5634
|
+
project: ref.project,
|
|
5635
|
+
stores: rows,
|
|
5636
|
+
...status.lastSubmission ? {
|
|
5637
|
+
lastSubmission: status.lastSubmission
|
|
5638
|
+
} : {},
|
|
5639
|
+
...status.lastPollAt ? {
|
|
5640
|
+
lastPollAt: status.lastPollAt
|
|
5641
|
+
} : {},
|
|
5642
|
+
registryUrls: {
|
|
5643
|
+
health: healthUrl,
|
|
5644
|
+
status: statusUrl,
|
|
5645
|
+
submissions: submissionsUrl
|
|
5646
|
+
},
|
|
5647
|
+
consoleStoresUrl,
|
|
5648
|
+
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.`
|
|
5649
|
+
};
|
|
5650
|
+
if (!healthRes.ok) result.healthUnavailable = `stores/health.json unreadable: ${healthRes.message}`;
|
|
5651
|
+
if (!statusRes.ok) result.statusUnavailable = `stores/status.json unreadable: ${statusRes.message}`;
|
|
5652
|
+
if (!submissionsRes.ok && 404 !== submissionsRes.status) result.submissionsUnavailable = `stores/submissions.json unreadable: ${submissionsRes.message}`;
|
|
5653
|
+
return JSON.stringify(result);
|
|
5654
|
+
}
|
|
5209
5655
|
function doctor_readReadyContract(projectPath, browser) {
|
|
5210
5656
|
try {
|
|
5211
5657
|
const raw = node_fs.readFileSync(node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json"), "utf8");
|
|
@@ -5961,7 +6407,7 @@ async function pollDeviceToken(args) {
|
|
|
5961
6407
|
}
|
|
5962
6408
|
const login_schema = {
|
|
5963
6409
|
name: "extension_login",
|
|
5964
|
-
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.",
|
|
6410
|
+
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.",
|
|
5965
6411
|
inputSchema: {
|
|
5966
6412
|
type: "object",
|
|
5967
6413
|
properties: {
|
|
@@ -5995,13 +6441,15 @@ function login_fail(name, message) {
|
|
|
5995
6441
|
});
|
|
5996
6442
|
}
|
|
5997
6443
|
function success(creds) {
|
|
6444
|
+
const expiresAt = creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null;
|
|
5998
6445
|
return JSON.stringify({
|
|
5999
6446
|
ok: true,
|
|
6000
6447
|
status: "logged-in",
|
|
6001
6448
|
workspaceSlug: creds.workspaceSlug,
|
|
6002
6449
|
projectSlug: creds.projectSlug,
|
|
6003
|
-
expiresAt
|
|
6004
|
-
|
|
6450
|
+
expiresAt,
|
|
6451
|
+
tokenTtlNote: tokenTtlNote(creds.workspaceSlug, creds.projectSlug),
|
|
6452
|
+
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).`
|
|
6005
6453
|
});
|
|
6006
6454
|
}
|
|
6007
6455
|
function login_pending(start) {
|
|
@@ -6017,6 +6465,7 @@ function login_pending(start) {
|
|
|
6017
6465
|
verificationUriComplete: complete
|
|
6018
6466
|
} : {},
|
|
6019
6467
|
deviceCode: start.deviceCode,
|
|
6468
|
+
tokenTtlNote: "Once authorized, the minted token lives at most 7 days (server-enforced); CI must re-mint before expiry (console: project settings -> Access tokens).",
|
|
6020
6469
|
message
|
|
6021
6470
|
});
|
|
6022
6471
|
}
|
|
@@ -6026,6 +6475,7 @@ function resumePending(deviceCode, verificationUri) {
|
|
|
6026
6475
|
status: "authorization_pending",
|
|
6027
6476
|
verificationUri,
|
|
6028
6477
|
deviceCode,
|
|
6478
|
+
tokenTtlNote: "Once authorized, the minted token lives at most 7 days (server-enforced); CI must re-mint before expiry (console: project settings -> Access tokens).",
|
|
6029
6479
|
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).`
|
|
6030
6480
|
});
|
|
6031
6481
|
}
|
|
@@ -6135,7 +6585,7 @@ async function login_handler(args) {
|
|
|
6135
6585
|
}
|
|
6136
6586
|
const whoami_schema = {
|
|
6137
6587
|
name: "extension_whoami",
|
|
6138
|
-
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.",
|
|
6588
|
+
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.",
|
|
6139
6589
|
inputSchema: {
|
|
6140
6590
|
type: "object",
|
|
6141
6591
|
properties: {}
|
|
@@ -6150,17 +6600,32 @@ async function whoami_handler() {
|
|
|
6150
6600
|
});
|
|
6151
6601
|
const now = Math.floor(Date.now() / 1000);
|
|
6152
6602
|
const expired = Boolean(creds.expiresAt && creds.expiresAt <= now);
|
|
6603
|
+
const recordedApi = String(creds.api || "").trim();
|
|
6604
|
+
const effectiveDefaultApi = resolveApiBase();
|
|
6605
|
+
const apiDiverges = Boolean(recordedApi) && recordedApi !== effectiveDefaultApi;
|
|
6606
|
+
const envTokenSet = Boolean(String(process.env.EXTENSION_DEV_TOKEN || "").trim());
|
|
6607
|
+
const messageParts = [];
|
|
6608
|
+
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.`);
|
|
6609
|
+
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.`);
|
|
6610
|
+
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.");
|
|
6153
6611
|
return JSON.stringify({
|
|
6154
6612
|
ok: true,
|
|
6155
6613
|
status: expired ? "expired" : "logged-in",
|
|
6156
6614
|
workspaceSlug: creds.workspaceSlug,
|
|
6157
6615
|
projectSlug: creds.projectSlug,
|
|
6158
|
-
|
|
6616
|
+
...recordedApi ? {
|
|
6617
|
+
apiRecordedAtLogin: recordedApi
|
|
6618
|
+
} : {},
|
|
6619
|
+
apiDefault: effectiveDefaultApi,
|
|
6159
6620
|
provider: creds.provider ?? "github",
|
|
6160
6621
|
expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
|
|
6161
6622
|
expiresInSeconds: creds.expiresAt ? creds.expiresAt - now : null,
|
|
6162
6623
|
expired,
|
|
6163
|
-
|
|
6624
|
+
...envTokenSet ? {
|
|
6625
|
+
envTokenOverride: true
|
|
6626
|
+
} : {},
|
|
6627
|
+
tokenTtlNote: tokenTtlNote(creds.workspaceSlug, creds.projectSlug),
|
|
6628
|
+
message: messageParts.join(" ")
|
|
6164
6629
|
});
|
|
6165
6630
|
}
|
|
6166
6631
|
const logout_schema = {
|
|
@@ -6847,6 +7312,7 @@ const tools = [
|
|
|
6847
7312
|
release_list_namespaceObject,
|
|
6848
7313
|
release_promote_namespaceObject,
|
|
6849
7314
|
deploy_namespaceObject,
|
|
7315
|
+
store_status_namespaceObject,
|
|
6850
7316
|
wait_namespaceObject,
|
|
6851
7317
|
add_feature_namespaceObject,
|
|
6852
7318
|
login_namespaceObject,
|