@extension.dev/mcp 3.17.0 → 4.0.8
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/CHANGELOG.md +21 -0
- package/bin/extension-mcp.js +1 -1
- package/dist/module.js +442 -280
- package/dist/src/lib/cdp-connection.d.ts +20 -0
- package/dist/src/lib/cdp-page-scripts.d.ts +5 -0
- package/dist/src/lib/cdp.d.ts +2 -19
- package/dist/src/lib/exec.d.ts +1 -1
- package/dist/src/tools/logs-constants.d.ts +6 -0
- package/dist/src/tools/logs-filter.d.ts +14 -0
- package/dist/src/tools/logs-schema.d.ts +65 -0
- package/dist/src/tools/logs.d.ts +2 -79
- package/dist/src/tools/publish.d.ts +15 -0
- package/dist/src/tools/release-promote.d.ts +50 -0
- package/package.json +15 -15
package/dist/module.js
CHANGED
|
@@ -7,10 +7,12 @@ import node_fs, { existsSync, readdirSync, statSync } from "node:fs";
|
|
|
7
7
|
import node_path, { join } from "node:path";
|
|
8
8
|
import node_os from "node:os";
|
|
9
9
|
import { extensionBuild } from "extension-develop";
|
|
10
|
-
import
|
|
10
|
+
import cross_spawn from "cross-spawn";
|
|
11
|
+
import { filterKeysForThisBrowser } from "browser-extension-manifest-fields";
|
|
11
12
|
import node_net from "node:net";
|
|
12
13
|
import ws from "ws";
|
|
13
14
|
import { extensionInstall, getManagedBrowsersCacheRoot } from "extension-install";
|
|
15
|
+
import { execFile } from "node:child_process";
|
|
14
16
|
import { promisify } from "node:util";
|
|
15
17
|
var add_feature_namespaceObject = {};
|
|
16
18
|
__webpack_require__.r(add_feature_namespaceObject);
|
|
@@ -106,7 +108,7 @@ var logs_namespaceObject = {};
|
|
|
106
108
|
__webpack_require__.r(logs_namespaceObject);
|
|
107
109
|
__webpack_require__.d(logs_namespaceObject, {
|
|
108
110
|
handler: ()=>logs_handler,
|
|
109
|
-
schema: ()=>
|
|
111
|
+
schema: ()=>logs_schema_schema
|
|
110
112
|
});
|
|
111
113
|
var manifest_validate_namespaceObject = {};
|
|
112
114
|
__webpack_require__.r(manifest_validate_namespaceObject);
|
|
@@ -131,8 +133,15 @@ __webpack_require__.r(publish_namespaceObject);
|
|
|
131
133
|
__webpack_require__.d(publish_namespaceObject, {
|
|
132
134
|
handler: ()=>publish_handler,
|
|
133
135
|
resolveToken: ()=>resolveToken,
|
|
136
|
+
safeApiBase: ()=>safeApiBase,
|
|
134
137
|
schema: ()=>publish_schema
|
|
135
138
|
});
|
|
139
|
+
var release_promote_namespaceObject = {};
|
|
140
|
+
__webpack_require__.r(release_promote_namespaceObject);
|
|
141
|
+
__webpack_require__.d(release_promote_namespaceObject, {
|
|
142
|
+
handler: ()=>release_promote_handler,
|
|
143
|
+
schema: ()=>release_promote_schema
|
|
144
|
+
});
|
|
136
145
|
var reload_namespaceObject = {};
|
|
137
146
|
__webpack_require__.r(reload_namespaceObject);
|
|
138
147
|
__webpack_require__.d(reload_namespaceObject, {
|
|
@@ -169,6 +178,9 @@ __webpack_require__.d(whoami_namespaceObject, {
|
|
|
169
178
|
handler: ()=>whoami_handler,
|
|
170
179
|
schema: ()=>whoami_schema
|
|
171
180
|
});
|
|
181
|
+
var package_namespaceObject = {
|
|
182
|
+
rE: "4.0.8"
|
|
183
|
+
};
|
|
172
184
|
const schema = {
|
|
173
185
|
name: "extension_create",
|
|
174
186
|
description: "Create a new browser extension project from a template in the extension.dev template catalog. Use extension_list_templates to see available options.",
|
|
@@ -398,7 +410,7 @@ async function build_handler(args) {
|
|
|
398
410
|
}
|
|
399
411
|
function runExtensionCli(args, options) {
|
|
400
412
|
return new Promise((resolve)=>{
|
|
401
|
-
const child =
|
|
413
|
+
const child = cross_spawn("npx", [
|
|
402
414
|
"extension",
|
|
403
415
|
...args
|
|
404
416
|
], {
|
|
@@ -413,7 +425,6 @@ function runExtensionCli(args, options) {
|
|
|
413
425
|
FORCE_COLOR: "0",
|
|
414
426
|
NO_COLOR: "1"
|
|
415
427
|
},
|
|
416
|
-
shell: true,
|
|
417
428
|
timeout: options?.timeoutMs ?? 30000
|
|
418
429
|
});
|
|
419
430
|
let stdout = "";
|
|
@@ -433,7 +444,7 @@ function runExtensionCli(args, options) {
|
|
|
433
444
|
});
|
|
434
445
|
}
|
|
435
446
|
function spawnExtensionCli(args, options) {
|
|
436
|
-
const child =
|
|
447
|
+
const child = cross_spawn("npx", [
|
|
437
448
|
"extension",
|
|
438
449
|
...args
|
|
439
450
|
], {
|
|
@@ -448,8 +459,7 @@ function spawnExtensionCli(args, options) {
|
|
|
448
459
|
...process.env,
|
|
449
460
|
FORCE_COLOR: "0",
|
|
450
461
|
NO_COLOR: "1"
|
|
451
|
-
}
|
|
452
|
-
shell: true
|
|
462
|
+
}
|
|
453
463
|
});
|
|
454
464
|
child.unref();
|
|
455
465
|
return child;
|
|
@@ -778,9 +788,8 @@ async function manifest_validate_handler(args) {
|
|
|
778
788
|
}
|
|
779
789
|
if (!manifest.name) result.errors.push("Missing required field: name");
|
|
780
790
|
if (!manifest.version) result.warnings.push("Missing field: version (required for store submission)");
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
if (!chromiumMv && !manifest.manifest_version) result.errors.push('Missing manifest_version. Use "chromium:manifest_version": 3 and "firefox:manifest_version": 2 for cross-browser support.');
|
|
791
|
+
const chromiumManifest = filterKeysForThisBrowser(manifest, "chrome");
|
|
792
|
+
if (!chromiumManifest.manifest_version) result.errors.push('Missing manifest_version. Use "chromium:manifest_version": 3 and "firefox:manifest_version": 2 for cross-browser support.');
|
|
784
793
|
for (const browser of browsers){
|
|
785
794
|
const isChromium = [
|
|
786
795
|
"chrome",
|
|
@@ -791,25 +800,26 @@ async function manifest_validate_handler(args) {
|
|
|
791
800
|
"firefox",
|
|
792
801
|
"gecko-based"
|
|
793
802
|
].includes(browser);
|
|
803
|
+
const effective = filterKeysForThisBrowser(manifest, browser);
|
|
794
804
|
const issues = [];
|
|
795
805
|
if (isChromium) {
|
|
796
|
-
const mv =
|
|
806
|
+
const mv = effective.manifest_version;
|
|
797
807
|
if (mv && mv < 3) issues.push("Manifest V2 is deprecated on Chromium. Use chromium:manifest_version: 3.");
|
|
798
|
-
if (
|
|
799
|
-
const perms =
|
|
808
|
+
if (effective.side_panel) {
|
|
809
|
+
const perms = effective.permissions ?? [];
|
|
800
810
|
if (!perms.includes("sidePanel")) issues.push('Side panel declared but "sidePanel" permission is missing.');
|
|
801
811
|
}
|
|
802
|
-
if (manifest["firefox:browser_action"] && !
|
|
812
|
+
if (manifest["firefox:browser_action"] && !effective.action) issues.push('Firefox browser_action found but no chromium:action. Chromium MV3 uses "action" instead of "browser_action".');
|
|
803
813
|
}
|
|
804
814
|
if (isFirefox) {
|
|
805
|
-
const contentScripts =
|
|
815
|
+
const contentScripts = effective.content_scripts;
|
|
806
816
|
if (contentScripts) {
|
|
807
817
|
for (const cs of contentScripts)if ("MAIN" === cs.world || "MAIN" === cs["world"]) issues.push('content_scripts.world: "MAIN" is Chromium-only. Use "chromium:world": "MAIN" and provide a Firefox fallback.');
|
|
808
818
|
}
|
|
809
|
-
if (
|
|
810
|
-
const bg =
|
|
819
|
+
if (chromiumManifest.side_panel && !effective.sidebar_action) issues.push("Chromium side_panel declared but no firefox:sidebar_action. Firefox uses sidebar_action for sidebars.");
|
|
820
|
+
const bg = effective.background;
|
|
811
821
|
if (bg) {
|
|
812
|
-
if (bg.service_worker && !bg
|
|
822
|
+
if (bg.service_worker && !bg.scripts) issues.push('Background service_worker declared but no firefox:scripts. Firefox uses "scripts" (array) instead of "service_worker".');
|
|
813
823
|
}
|
|
814
824
|
}
|
|
815
825
|
result.browserSupport[browser] = {
|
|
@@ -819,11 +829,11 @@ async function manifest_validate_handler(args) {
|
|
|
819
829
|
if (issues.length) result.valid = false;
|
|
820
830
|
}
|
|
821
831
|
const surfaces = [];
|
|
822
|
-
if (
|
|
823
|
-
if (
|
|
824
|
-
if (
|
|
825
|
-
if (
|
|
826
|
-
if (
|
|
832
|
+
if (chromiumManifest.content_scripts) surfaces.push("content");
|
|
833
|
+
if (chromiumManifest.side_panel) surfaces.push("sidebar");
|
|
834
|
+
if (chromiumManifest.action || manifest["firefox:browser_action"]) surfaces.push("action");
|
|
835
|
+
if (chromiumManifest.chrome_url_overrides?.newtab) surfaces.push("newtab");
|
|
836
|
+
if (chromiumManifest.background) surfaces.push("background");
|
|
827
837
|
if (surfaces.length) try {
|
|
828
838
|
const templates = await listTemplates();
|
|
829
839
|
result.similarTemplates = templates.filter((t)=>t.surfaces.some((s)=>surfaces.includes(s))).slice(0, 5).map((t)=>({
|
|
@@ -1000,7 +1010,7 @@ function _define_property(obj, key, value) {
|
|
|
1000
1010
|
return obj;
|
|
1001
1011
|
}
|
|
1002
1012
|
const COMMAND_TIMEOUT_MS = 15000;
|
|
1003
|
-
class
|
|
1013
|
+
class CDPConnection {
|
|
1004
1014
|
async connect(wsUrl) {
|
|
1005
1015
|
return new Promise((resolve, reject)=>{
|
|
1006
1016
|
this.ws = new ws(wsUrl);
|
|
@@ -1092,61 +1102,52 @@ class CDPClient {
|
|
|
1092
1102
|
this.ws.send(JSON.stringify(message));
|
|
1093
1103
|
});
|
|
1094
1104
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
if ("string" == typeof data.webSocketDebuggerUrl) return data.webSocketDebuggerUrl;
|
|
1100
|
-
throw new Error("No webSocketDebuggerUrl in /json/version response");
|
|
1101
|
-
}
|
|
1102
|
-
static async discoverTargets(port, host = "127.0.0.1") {
|
|
1103
|
-
const res = await fetch(`http://${host}:${port}/json`);
|
|
1104
|
-
if (!res.ok) throw new Error(`CDP /json failed: ${res.status}`);
|
|
1105
|
-
return await res.json();
|
|
1106
|
-
}
|
|
1107
|
-
async getTargets() {
|
|
1108
|
-
const response = await this.sendCommand("Target.getTargets");
|
|
1109
|
-
return response?.targetInfos ?? [];
|
|
1110
|
-
}
|
|
1111
|
-
async attachToTarget(targetId) {
|
|
1112
|
-
const response = await this.sendCommand("Target.attachToTarget", {
|
|
1113
|
-
targetId,
|
|
1114
|
-
flatten: true
|
|
1115
|
-
});
|
|
1116
|
-
return response.sessionId ?? "";
|
|
1117
|
-
}
|
|
1118
|
-
async enableDomains(sessionId) {
|
|
1119
|
-
await Promise.all([
|
|
1120
|
-
this.sendCommand("Runtime.enable", {}, sessionId),
|
|
1121
|
-
this.sendCommand("Log.enable", {}, sessionId),
|
|
1122
|
-
this.sendCommand("Page.enable", {}, sessionId)
|
|
1123
|
-
]);
|
|
1105
|
+
getConsoleMessages() {
|
|
1106
|
+
return [
|
|
1107
|
+
...this.consoleMessages
|
|
1108
|
+
];
|
|
1124
1109
|
}
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
const
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1110
|
+
getConsoleSummary() {
|
|
1111
|
+
const counts = {};
|
|
1112
|
+
const uniqueByLevel = {};
|
|
1113
|
+
for (const msg of this.consoleMessages){
|
|
1114
|
+
counts[msg.level] = (counts[msg.level] ?? 0) + 1;
|
|
1115
|
+
if (!uniqueByLevel[msg.level]) uniqueByLevel[msg.level] = new Map();
|
|
1116
|
+
const key = msg.text.slice(0, 200);
|
|
1117
|
+
uniqueByLevel[msg.level].set(key, (uniqueByLevel[msg.level].get(key) ?? 0) + 1);
|
|
1118
|
+
}
|
|
1119
|
+
const topMessages = [];
|
|
1120
|
+
for (const [level, msgs] of Object.entries(uniqueByLevel)){
|
|
1121
|
+
const sorted = [
|
|
1122
|
+
...msgs.entries()
|
|
1123
|
+
].sort((a, b)=>b[1] - a[1]);
|
|
1124
|
+
for (const [text, count] of sorted.slice(0, 5))topMessages.push({
|
|
1125
|
+
level,
|
|
1126
|
+
text,
|
|
1127
|
+
count
|
|
1137
1128
|
});
|
|
1138
|
-
}
|
|
1129
|
+
}
|
|
1130
|
+
return {
|
|
1131
|
+
total: this.consoleMessages.length,
|
|
1132
|
+
counts,
|
|
1133
|
+
topMessages: topMessages.sort((a, b)=>b.count - a.count).slice(0, 10)
|
|
1134
|
+
};
|
|
1139
1135
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
}, sessionId);
|
|
1146
|
-
return response.result?.value;
|
|
1136
|
+
onEvent(handler) {
|
|
1137
|
+
this.eventListeners.add(handler);
|
|
1138
|
+
return ()=>{
|
|
1139
|
+
this.eventListeners.delete(handler);
|
|
1140
|
+
};
|
|
1147
1141
|
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1142
|
+
constructor(){
|
|
1143
|
+
_define_property(this, "ws", null);
|
|
1144
|
+
_define_property(this, "messageId", 0);
|
|
1145
|
+
_define_property(this, "eventListeners", new Set());
|
|
1146
|
+
_define_property(this, "pendingRequests", new Map());
|
|
1147
|
+
_define_property(this, "consoleMessages", []);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
const PAGE_HTML_SCRIPT = `(() => {
|
|
1150
1151
|
try {
|
|
1151
1152
|
const doctype = document.doctype;
|
|
1152
1153
|
const dt = doctype
|
|
@@ -1179,61 +1180,8 @@ class CDPClient {
|
|
|
1179
1180
|
}
|
|
1180
1181
|
return dt + '\\n' + document.documentElement.outerHTML;
|
|
1181
1182
|
} catch (e) { return ''; }
|
|
1182
|
-
})()
|
|
1183
|
-
|
|
1184
|
-
}
|
|
1185
|
-
async getClosedShadowRoots(sessionId, maxBytes = 65536) {
|
|
1186
|
-
await this.sendCommand("DOM.enable", {}, sessionId);
|
|
1187
|
-
const doc = await this.sendCommand("DOM.getDocument", {
|
|
1188
|
-
depth: -1,
|
|
1189
|
-
pierce: true
|
|
1190
|
-
}, sessionId);
|
|
1191
|
-
const found = [];
|
|
1192
|
-
const walk = (node, hostName)=>{
|
|
1193
|
-
if (!node || "object" != typeof node) return;
|
|
1194
|
-
const name = node.localName || node.nodeName || hostName;
|
|
1195
|
-
if (Array.isArray(node.shadowRoots)) for (const sr of node.shadowRoots){
|
|
1196
|
-
if (sr && "closed" === sr.shadowRootType && "number" == typeof sr.nodeId) found.push({
|
|
1197
|
-
nodeId: sr.nodeId,
|
|
1198
|
-
host: String(name),
|
|
1199
|
-
type: "closed"
|
|
1200
|
-
});
|
|
1201
|
-
walk(sr, name);
|
|
1202
|
-
}
|
|
1203
|
-
if (Array.isArray(node.children)) for (const c of node.children)walk(c, name);
|
|
1204
|
-
if (node.contentDocument) walk(node.contentDocument, name);
|
|
1205
|
-
};
|
|
1206
|
-
walk(doc.root, "html");
|
|
1207
|
-
const out = [];
|
|
1208
|
-
for (const f of found)try {
|
|
1209
|
-
const oh = await this.sendCommand("DOM.getOuterHTML", {
|
|
1210
|
-
nodeId: f.nodeId
|
|
1211
|
-
}, sessionId);
|
|
1212
|
-
let html = String(oh?.outerHTML ?? "");
|
|
1213
|
-
let truncated = false;
|
|
1214
|
-
if (maxBytes > 0 && html.length > maxBytes) {
|
|
1215
|
-
html = html.slice(0, maxBytes);
|
|
1216
|
-
truncated = true;
|
|
1217
|
-
}
|
|
1218
|
-
out.push({
|
|
1219
|
-
host: f.host,
|
|
1220
|
-
type: f.type,
|
|
1221
|
-
html,
|
|
1222
|
-
...truncated ? {
|
|
1223
|
-
truncated
|
|
1224
|
-
} : {}
|
|
1225
|
-
});
|
|
1226
|
-
} catch {
|
|
1227
|
-
out.push({
|
|
1228
|
-
host: f.host,
|
|
1229
|
-
type: f.type,
|
|
1230
|
-
html: ""
|
|
1231
|
-
});
|
|
1232
|
-
}
|
|
1233
|
-
return out;
|
|
1234
|
-
}
|
|
1235
|
-
async getPageMeta(sessionId) {
|
|
1236
|
-
const result = await this.evaluate(sessionId, `(() => {
|
|
1183
|
+
})()`;
|
|
1184
|
+
const PAGE_META_SCRIPT = `(() => {
|
|
1237
1185
|
try {
|
|
1238
1186
|
return {
|
|
1239
1187
|
title: document.title,
|
|
@@ -1249,11 +1197,42 @@ class CDPClient {
|
|
|
1249
1197
|
styleCount: document.querySelectorAll('style,link[rel="stylesheet"]').length
|
|
1250
1198
|
};
|
|
1251
1199
|
} catch { return {}; }
|
|
1252
|
-
})()
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1200
|
+
})()`;
|
|
1201
|
+
const EXTENSION_ROOT_META_SCRIPT = `(() => {
|
|
1202
|
+
try {
|
|
1203
|
+
const readGeneration = (node) => {
|
|
1204
|
+
const raw = node.getAttribute && node.getAttribute('data-extjs-reinject-generation');
|
|
1205
|
+
const parsed = Number(raw);
|
|
1206
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
1207
|
+
};
|
|
1208
|
+
const normalize = (node) => ({
|
|
1209
|
+
tag: node.tagName ? String(node.tagName).toLowerCase() : 'unknown',
|
|
1210
|
+
id: node.id || undefined,
|
|
1211
|
+
key: node.getAttribute ? node.getAttribute('data-extjs-reinject-key') || undefined : undefined,
|
|
1212
|
+
generation: readGeneration(node),
|
|
1213
|
+
status: node.getAttribute ? node.getAttribute('data-extjs-reinject-status') || undefined : undefined
|
|
1214
|
+
});
|
|
1215
|
+
const roots = Array.from(
|
|
1216
|
+
document.querySelectorAll('#extension-root,[data-extension-root]:not([data-extension-root="extension-js-devtools"])')
|
|
1217
|
+
).slice(0, 10).map(normalize);
|
|
1218
|
+
const markers = Array.from(
|
|
1219
|
+
document.querySelectorAll('[data-extjs-reinject-marker="true"]')
|
|
1220
|
+
).slice(0, 10).map(normalize);
|
|
1221
|
+
if (!roots.length && !markers.length) return null;
|
|
1222
|
+
const generations = [...roots, ...markers]
|
|
1223
|
+
.map(e => e.generation)
|
|
1224
|
+
.filter(g => typeof g === 'number');
|
|
1225
|
+
return {
|
|
1226
|
+
rootCount: roots.length,
|
|
1227
|
+
markerCount: markers.length,
|
|
1228
|
+
latestGeneration: generations.length ? Math.max(...generations) : 0,
|
|
1229
|
+
roots,
|
|
1230
|
+
markers
|
|
1231
|
+
};
|
|
1232
|
+
} catch { return null; }
|
|
1233
|
+
})()`;
|
|
1234
|
+
function probeSelectorsScript(selectors) {
|
|
1235
|
+
return `(() => {
|
|
1257
1236
|
const selectors = ${JSON.stringify(selectors)};
|
|
1258
1237
|
return selectors.map(selector => {
|
|
1259
1238
|
try {
|
|
@@ -1275,11 +1254,10 @@ class CDPClient {
|
|
|
1275
1254
|
return { selector, count: 0, samples: [], error: String(e) };
|
|
1276
1255
|
}
|
|
1277
1256
|
});
|
|
1278
|
-
})()
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
const result = await this.evaluate(sessionId, `(() => {
|
|
1257
|
+
})()`;
|
|
1258
|
+
}
|
|
1259
|
+
function domSnapshotScript(maxNodes) {
|
|
1260
|
+
return `(() => {
|
|
1283
1261
|
const maxNodes = ${maxNodes};
|
|
1284
1262
|
const nodes = [];
|
|
1285
1263
|
const walk = (node, depth) => {
|
|
@@ -1301,88 +1279,131 @@ class CDPClient {
|
|
|
1301
1279
|
};
|
|
1302
1280
|
walk(document.documentElement, 0);
|
|
1303
1281
|
return nodes;
|
|
1304
|
-
})()
|
|
1305
|
-
|
|
1282
|
+
})()`;
|
|
1283
|
+
}
|
|
1284
|
+
class CDPClient extends CDPConnection {
|
|
1285
|
+
static async discoverBrowserWsUrl(port, host = "127.0.0.1") {
|
|
1286
|
+
const res = await fetch(`http://${host}:${port}/json/version`);
|
|
1287
|
+
if (!res.ok) throw new Error(`CDP /json/version failed: ${res.status}`);
|
|
1288
|
+
const data = await res.json();
|
|
1289
|
+
if ("string" == typeof data.webSocketDebuggerUrl) return data.webSocketDebuggerUrl;
|
|
1290
|
+
throw new Error("No webSocketDebuggerUrl in /json/version response");
|
|
1306
1291
|
}
|
|
1307
|
-
async
|
|
1308
|
-
const
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
const raw = node.getAttribute && node.getAttribute('data-extjs-reinject-generation');
|
|
1312
|
-
const parsed = Number(raw);
|
|
1313
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
1314
|
-
};
|
|
1315
|
-
const normalize = (node) => ({
|
|
1316
|
-
tag: node.tagName ? String(node.tagName).toLowerCase() : 'unknown',
|
|
1317
|
-
id: node.id || undefined,
|
|
1318
|
-
key: node.getAttribute ? node.getAttribute('data-extjs-reinject-key') || undefined : undefined,
|
|
1319
|
-
generation: readGeneration(node),
|
|
1320
|
-
status: node.getAttribute ? node.getAttribute('data-extjs-reinject-status') || undefined : undefined
|
|
1321
|
-
});
|
|
1322
|
-
const roots = Array.from(
|
|
1323
|
-
document.querySelectorAll('#extension-root,[data-extension-root]:not([data-extension-root="extension-js-devtools"])')
|
|
1324
|
-
).slice(0, 10).map(normalize);
|
|
1325
|
-
const markers = Array.from(
|
|
1326
|
-
document.querySelectorAll('[data-extjs-reinject-marker="true"]')
|
|
1327
|
-
).slice(0, 10).map(normalize);
|
|
1328
|
-
if (!roots.length && !markers.length) return null;
|
|
1329
|
-
const generations = [...roots, ...markers]
|
|
1330
|
-
.map(e => e.generation)
|
|
1331
|
-
.filter(g => typeof g === 'number');
|
|
1332
|
-
return {
|
|
1333
|
-
rootCount: roots.length,
|
|
1334
|
-
markerCount: markers.length,
|
|
1335
|
-
latestGeneration: generations.length ? Math.max(...generations) : 0,
|
|
1336
|
-
roots,
|
|
1337
|
-
markers
|
|
1338
|
-
};
|
|
1339
|
-
} catch { return null; }
|
|
1340
|
-
})()`);
|
|
1341
|
-
return result ?? null;
|
|
1292
|
+
static async discoverTargets(port, host = "127.0.0.1") {
|
|
1293
|
+
const res = await fetch(`http://${host}:${port}/json`);
|
|
1294
|
+
if (!res.ok) throw new Error(`CDP /json failed: ${res.status}`);
|
|
1295
|
+
return await res.json();
|
|
1342
1296
|
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
];
|
|
1297
|
+
async getTargets() {
|
|
1298
|
+
const response = await this.sendCommand("Target.getTargets");
|
|
1299
|
+
return response?.targetInfos ?? [];
|
|
1347
1300
|
}
|
|
1348
|
-
|
|
1349
|
-
const
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1301
|
+
async attachToTarget(targetId) {
|
|
1302
|
+
const response = await this.sendCommand("Target.attachToTarget", {
|
|
1303
|
+
targetId,
|
|
1304
|
+
flatten: true
|
|
1305
|
+
});
|
|
1306
|
+
return response.sessionId ?? "";
|
|
1307
|
+
}
|
|
1308
|
+
async enableDomains(sessionId) {
|
|
1309
|
+
await Promise.all([
|
|
1310
|
+
this.sendCommand("Runtime.enable", {}, sessionId),
|
|
1311
|
+
this.sendCommand("Log.enable", {}, sessionId),
|
|
1312
|
+
this.sendCommand("Page.enable", {}, sessionId)
|
|
1313
|
+
]);
|
|
1314
|
+
}
|
|
1315
|
+
async navigate(sessionId, url) {
|
|
1316
|
+
await this.sendCommand("Page.navigate", {
|
|
1317
|
+
url
|
|
1318
|
+
}, sessionId);
|
|
1319
|
+
await new Promise((resolve)=>{
|
|
1320
|
+
const timeout = setTimeout(resolve, 5000);
|
|
1321
|
+
const unsubscribe = this.onEvent((msg)=>{
|
|
1322
|
+
if ("Page.loadEventFired" === msg.method) {
|
|
1323
|
+
clearTimeout(timeout);
|
|
1324
|
+
unsubscribe();
|
|
1325
|
+
resolve();
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
async evaluate(sessionId, expression) {
|
|
1331
|
+
const response = await this.sendCommand("Runtime.evaluate", {
|
|
1332
|
+
expression,
|
|
1333
|
+
returnByValue: true,
|
|
1334
|
+
awaitPromise: false
|
|
1335
|
+
}, sessionId);
|
|
1336
|
+
return response.result?.value;
|
|
1337
|
+
}
|
|
1338
|
+
async getPageHTML(sessionId) {
|
|
1339
|
+
const result = await this.evaluate(sessionId, PAGE_HTML_SCRIPT);
|
|
1340
|
+
return "string" == typeof result ? result : "";
|
|
1341
|
+
}
|
|
1342
|
+
async getClosedShadowRoots(sessionId, maxBytes = 65536) {
|
|
1343
|
+
await this.sendCommand("DOM.enable", {}, sessionId);
|
|
1344
|
+
const doc = await this.sendCommand("DOM.getDocument", {
|
|
1345
|
+
depth: -1,
|
|
1346
|
+
pierce: true
|
|
1347
|
+
}, sessionId);
|
|
1348
|
+
const found = [];
|
|
1349
|
+
const walk = (node, hostName)=>{
|
|
1350
|
+
if (!node || "object" != typeof node) return;
|
|
1351
|
+
const name = node.localName || node.nodeName || hostName;
|
|
1352
|
+
if (Array.isArray(node.shadowRoots)) for (const sr of node.shadowRoots){
|
|
1353
|
+
if (sr && "closed" === sr.shadowRootType && "number" == typeof sr.nodeId) found.push({
|
|
1354
|
+
nodeId: sr.nodeId,
|
|
1355
|
+
host: String(name),
|
|
1356
|
+
type: "closed"
|
|
1357
|
+
});
|
|
1358
|
+
walk(sr, name);
|
|
1359
|
+
}
|
|
1360
|
+
if (Array.isArray(node.children)) for (const c of node.children)walk(c, name);
|
|
1361
|
+
if (node.contentDocument) walk(node.contentDocument, name);
|
|
1362
|
+
};
|
|
1363
|
+
walk(doc.root, "html");
|
|
1364
|
+
const out = [];
|
|
1365
|
+
for (const f of found)try {
|
|
1366
|
+
const oh = await this.sendCommand("DOM.getOuterHTML", {
|
|
1367
|
+
nodeId: f.nodeId
|
|
1368
|
+
}, sessionId);
|
|
1369
|
+
let html = String(oh?.outerHTML ?? "");
|
|
1370
|
+
let truncated = false;
|
|
1371
|
+
if (maxBytes > 0 && html.length > maxBytes) {
|
|
1372
|
+
html = html.slice(0, maxBytes);
|
|
1373
|
+
truncated = true;
|
|
1374
|
+
}
|
|
1375
|
+
out.push({
|
|
1376
|
+
host: f.host,
|
|
1377
|
+
type: f.type,
|
|
1378
|
+
html,
|
|
1379
|
+
...truncated ? {
|
|
1380
|
+
truncated
|
|
1381
|
+
} : {}
|
|
1382
|
+
});
|
|
1383
|
+
} catch {
|
|
1384
|
+
out.push({
|
|
1385
|
+
host: f.host,
|
|
1386
|
+
type: f.type,
|
|
1387
|
+
html: ""
|
|
1366
1388
|
});
|
|
1367
1389
|
}
|
|
1368
|
-
return
|
|
1369
|
-
total: this.consoleMessages.length,
|
|
1370
|
-
counts,
|
|
1371
|
-
topMessages: topMessages.sort((a, b)=>b.count - a.count).slice(0, 10)
|
|
1372
|
-
};
|
|
1390
|
+
return out;
|
|
1373
1391
|
}
|
|
1374
|
-
|
|
1375
|
-
this.
|
|
1376
|
-
return
|
|
1377
|
-
this.eventListeners.delete(handler);
|
|
1378
|
-
};
|
|
1392
|
+
async getPageMeta(sessionId) {
|
|
1393
|
+
const result = await this.evaluate(sessionId, PAGE_META_SCRIPT);
|
|
1394
|
+
return result ?? {};
|
|
1379
1395
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1396
|
+
async probeSelectors(sessionId, selectors) {
|
|
1397
|
+
const result = await this.evaluate(sessionId, probeSelectorsScript(selectors));
|
|
1398
|
+
return result ?? [];
|
|
1399
|
+
}
|
|
1400
|
+
async getDomSnapshot(sessionId, maxNodes = 500) {
|
|
1401
|
+
const result = await this.evaluate(sessionId, domSnapshotScript(maxNodes));
|
|
1402
|
+
return result ?? [];
|
|
1403
|
+
}
|
|
1404
|
+
async getExtensionRootMeta(sessionId) {
|
|
1405
|
+
const result = await this.evaluate(sessionId, EXTENSION_ROOT_META_SCRIPT);
|
|
1406
|
+
return result ?? null;
|
|
1386
1407
|
}
|
|
1387
1408
|
}
|
|
1388
1409
|
const source_inspect_schema = {
|
|
@@ -1685,13 +1706,57 @@ async function list_extensions_findCdpPort(projectPath, browser) {
|
|
|
1685
1706
|
socket.connect(defaultPort, "127.0.0.1");
|
|
1686
1707
|
});
|
|
1687
1708
|
}
|
|
1688
|
-
const CONTROL_ENVELOPE_VERSION = 1;
|
|
1689
1709
|
const CONTROL_WS_PATH = "/extjs-control";
|
|
1690
|
-
const
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1710
|
+
const LEVEL_ORDER = [
|
|
1711
|
+
"error",
|
|
1712
|
+
"warn",
|
|
1713
|
+
"info",
|
|
1714
|
+
"debug",
|
|
1715
|
+
"trace"
|
|
1716
|
+
];
|
|
1717
|
+
function levelRank(level) {
|
|
1718
|
+
const l = "log" === level ? "info" : level;
|
|
1719
|
+
const i = LEVEL_ORDER.indexOf(l);
|
|
1720
|
+
return -1 === i ? LEVEL_ORDER.length : i;
|
|
1721
|
+
}
|
|
1722
|
+
function makeUrlMatcher(pattern) {
|
|
1723
|
+
const hasGlob = pattern.includes("*");
|
|
1724
|
+
let re = null;
|
|
1725
|
+
if (hasGlob) {
|
|
1726
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1727
|
+
re = new RegExp(escaped);
|
|
1728
|
+
}
|
|
1729
|
+
return (event)=>{
|
|
1730
|
+
const candidates = [
|
|
1731
|
+
event.url,
|
|
1732
|
+
event.hostname
|
|
1733
|
+
].filter((v)=>"string" == typeof v);
|
|
1734
|
+
if (0 === candidates.length) return false;
|
|
1735
|
+
return candidates.some((c)=>re ? re.test(c) : c.includes(pattern));
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
function makeFilter(args) {
|
|
1739
|
+
const minLevel = String(args.level || "all").toLowerCase();
|
|
1740
|
+
const rawContexts = Array.isArray(args.context) ? args.context : "string" == typeof args.context ? args.context.split(",") : null;
|
|
1741
|
+
const contexts = rawContexts && rawContexts.length ? new Set(rawContexts.map((c)=>c.trim()).filter(Boolean)) : null;
|
|
1742
|
+
const sinceSeq = null != args.since ? Number(args.since) : null;
|
|
1743
|
+
const urlMatches = args.url ? makeUrlMatcher(args.url) : null;
|
|
1744
|
+
const tabId = null != args.tab ? Number(args.tab) : null;
|
|
1745
|
+
return (event)=>{
|
|
1746
|
+
if (!event || "object" != typeof event) return false;
|
|
1747
|
+
if ("header" === event.type) return false;
|
|
1748
|
+
if (args.signalsOnly && "dx.signal" !== event.eventType) return false;
|
|
1749
|
+
if (contexts && !contexts.has(event.context)) return false;
|
|
1750
|
+
if ("all" !== minLevel && "off" !== minLevel) {
|
|
1751
|
+
if (levelRank(event.level) > levelRank(minLevel)) return false;
|
|
1752
|
+
}
|
|
1753
|
+
if (null != sinceSeq && Number.isFinite(sinceSeq) && "number" == typeof event.seq && event.seq <= sinceSeq) return false;
|
|
1754
|
+
if (urlMatches && !urlMatches(event)) return false;
|
|
1755
|
+
if (null != tabId && Number.isFinite(tabId) && event.tabId !== tabId) return false;
|
|
1756
|
+
return true;
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
const logs_schema_schema = {
|
|
1695
1760
|
name: "extension_logs",
|
|
1696
1761
|
description: "Read or stream logs from every context of a running dev session (service worker, content scripts, popup, options, sidebar, devtools, pages) in one ordered timeline. Reads the same agent-bridge plane as the `extension logs` CLI: a one-shot returns the most recent matching lines from logs.ndjson; `follow:true` collects from the live control channel for a bounded window. Requires an active `extension dev` session.",
|
|
1697
1762
|
inputSchema: {
|
|
@@ -1760,12 +1825,12 @@ const logs_schema = {
|
|
|
1760
1825
|
},
|
|
1761
1826
|
followMs: {
|
|
1762
1827
|
type: "number",
|
|
1763
|
-
default:
|
|
1764
|
-
description:
|
|
1828
|
+
default: 4000,
|
|
1829
|
+
description: "How long to collect live frames when follow=true (clamped 500–15000ms)."
|
|
1765
1830
|
},
|
|
1766
1831
|
limit: {
|
|
1767
1832
|
type: "number",
|
|
1768
|
-
default:
|
|
1833
|
+
default: 200,
|
|
1769
1834
|
description: "Maximum number of (most recent) events to return."
|
|
1770
1835
|
}
|
|
1771
1836
|
},
|
|
@@ -1774,55 +1839,6 @@ const logs_schema = {
|
|
|
1774
1839
|
]
|
|
1775
1840
|
}
|
|
1776
1841
|
};
|
|
1777
|
-
const LEVEL_ORDER = [
|
|
1778
|
-
"error",
|
|
1779
|
-
"warn",
|
|
1780
|
-
"info",
|
|
1781
|
-
"debug",
|
|
1782
|
-
"trace"
|
|
1783
|
-
];
|
|
1784
|
-
function levelRank(level) {
|
|
1785
|
-
const l = "log" === level ? "info" : level;
|
|
1786
|
-
const i = LEVEL_ORDER.indexOf(l);
|
|
1787
|
-
return -1 === i ? LEVEL_ORDER.length : i;
|
|
1788
|
-
}
|
|
1789
|
-
function makeUrlMatcher(pattern) {
|
|
1790
|
-
const hasGlob = pattern.includes("*");
|
|
1791
|
-
let re = null;
|
|
1792
|
-
if (hasGlob) {
|
|
1793
|
-
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1794
|
-
re = new RegExp(escaped);
|
|
1795
|
-
}
|
|
1796
|
-
return (event)=>{
|
|
1797
|
-
const candidates = [
|
|
1798
|
-
event.url,
|
|
1799
|
-
event.hostname
|
|
1800
|
-
].filter((v)=>"string" == typeof v);
|
|
1801
|
-
if (0 === candidates.length) return false;
|
|
1802
|
-
return candidates.some((c)=>re ? re.test(c) : c.includes(pattern));
|
|
1803
|
-
};
|
|
1804
|
-
}
|
|
1805
|
-
function makeFilter(args) {
|
|
1806
|
-
const minLevel = String(args.level || "all").toLowerCase();
|
|
1807
|
-
const rawContexts = Array.isArray(args.context) ? args.context : "string" == typeof args.context ? args.context.split(",") : null;
|
|
1808
|
-
const contexts = rawContexts && rawContexts.length ? new Set(rawContexts.map((c)=>c.trim()).filter(Boolean)) : null;
|
|
1809
|
-
const sinceSeq = null != args.since ? Number(args.since) : null;
|
|
1810
|
-
const urlMatches = args.url ? makeUrlMatcher(args.url) : null;
|
|
1811
|
-
const tabId = null != args.tab ? Number(args.tab) : null;
|
|
1812
|
-
return (event)=>{
|
|
1813
|
-
if (!event || "object" != typeof event) return false;
|
|
1814
|
-
if ("header" === event.type) return false;
|
|
1815
|
-
if (args.signalsOnly && "dx.signal" !== event.eventType) return false;
|
|
1816
|
-
if (contexts && !contexts.has(event.context)) return false;
|
|
1817
|
-
if ("all" !== minLevel && "off" !== minLevel) {
|
|
1818
|
-
if (levelRank(event.level) > levelRank(minLevel)) return false;
|
|
1819
|
-
}
|
|
1820
|
-
if (null != sinceSeq && Number.isFinite(sinceSeq) && "number" == typeof event.seq && event.seq <= sinceSeq) return false;
|
|
1821
|
-
if (urlMatches && !urlMatches(event)) return false;
|
|
1822
|
-
if (null != tabId && Number.isFinite(tabId) && event.tabId !== tabId) return false;
|
|
1823
|
-
return true;
|
|
1824
|
-
};
|
|
1825
|
-
}
|
|
1826
1842
|
function logsFilePath(projectPath, browser) {
|
|
1827
1843
|
return node_path.resolve(projectPath, "dist", "extension-js", browser, "logs.ndjson");
|
|
1828
1844
|
}
|
|
@@ -1898,7 +1914,7 @@ async function readFromStream(args, browser, limit) {
|
|
|
1898
1914
|
error: `No active control channel found for ${browser}.`,
|
|
1899
1915
|
hint: `Run extension_dev (browser: ${browser}) and wait for it to be ready, then retry. For past logs without a live channel, call without follow.`
|
|
1900
1916
|
});
|
|
1901
|
-
const followMs = Math.min(Math.max(args.followMs ??
|
|
1917
|
+
const followMs = Math.min(Math.max(args.followMs ?? 4000, 500), 15000);
|
|
1902
1918
|
const matches = makeFilter(args);
|
|
1903
1919
|
const events = [];
|
|
1904
1920
|
let dropped = 0;
|
|
@@ -1929,7 +1945,7 @@ async function readFromStream(args, browser, limit) {
|
|
|
1929
1945
|
try {
|
|
1930
1946
|
socket.send(JSON.stringify({
|
|
1931
1947
|
type: "hello",
|
|
1932
|
-
v:
|
|
1948
|
+
v: 1,
|
|
1933
1949
|
role: "consumer",
|
|
1934
1950
|
instanceId: ready.instanceId
|
|
1935
1951
|
}));
|
|
@@ -1961,7 +1977,7 @@ async function readFromStream(args, browser, limit) {
|
|
|
1961
1977
|
}
|
|
1962
1978
|
async function logs_handler(args) {
|
|
1963
1979
|
const browser = args.browser ?? "chromium";
|
|
1964
|
-
const limit = args.limit && args.limit > 0 ? args.limit :
|
|
1980
|
+
const limit = args.limit && args.limit > 0 ? args.limit : 200;
|
|
1965
1981
|
if (args.follow) return readFromStream(args, browser, limit);
|
|
1966
1982
|
return readFromFile(args, browser, limit);
|
|
1967
1983
|
}
|
|
@@ -2423,6 +2439,26 @@ function resolveToken() {
|
|
|
2423
2439
|
const creds = readValidCredentials();
|
|
2424
2440
|
return creds?.token ? String(creds.token).trim() : "";
|
|
2425
2441
|
}
|
|
2442
|
+
function safeApiBase(raw) {
|
|
2443
|
+
let parsed;
|
|
2444
|
+
try {
|
|
2445
|
+
parsed = new URL(raw);
|
|
2446
|
+
} catch {
|
|
2447
|
+
return {
|
|
2448
|
+
ok: false,
|
|
2449
|
+
message: `Invalid platform URL: ${raw}`
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
2452
|
+
const isLocalhost = "localhost" === parsed.hostname || "127.0.0.1" === parsed.hostname || "[::1]" === parsed.hostname || "::1" === parsed.hostname;
|
|
2453
|
+
if ("https:" !== parsed.protocol && !("http:" === parsed.protocol && isLocalhost)) return {
|
|
2454
|
+
ok: false,
|
|
2455
|
+
message: `Refusing to send the access token to ${raw}: use https (http is allowed only for localhost).`
|
|
2456
|
+
};
|
|
2457
|
+
return {
|
|
2458
|
+
ok: true,
|
|
2459
|
+
base: `${parsed.origin}${parsed.pathname}`.replace(/\/+$/, "")
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2426
2462
|
function fail(name, message) {
|
|
2427
2463
|
return JSON.stringify({
|
|
2428
2464
|
ok: false,
|
|
@@ -2435,8 +2471,9 @@ function fail(name, message) {
|
|
|
2435
2471
|
async function publish_handler(args) {
|
|
2436
2472
|
const token = resolveToken();
|
|
2437
2473
|
if (!token) return fail("PublishAuthError", "No token. Run extension_login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard).");
|
|
2438
|
-
const
|
|
2439
|
-
|
|
2474
|
+
const apiCheck = safeApiBase(String(args.api || process.env.EXTENSION_DEV_API_URL || DEFAULT_API));
|
|
2475
|
+
if (!apiCheck.ok) return fail("PublishConfigError", apiCheck.message);
|
|
2476
|
+
const url = `${apiCheck.base}/api/cli/publish`;
|
|
2440
2477
|
const body = {};
|
|
2441
2478
|
if (null != args.ttlHours) body.ttlHours = Number(args.ttlHours);
|
|
2442
2479
|
if (args.buildSha) body.buildSha = args.buildSha;
|
|
@@ -2465,6 +2502,102 @@ async function publish_handler(args) {
|
|
|
2465
2502
|
if (!res.ok) return fail("PublishError", `publish failed (${res.status}): ${data?.message || text || "unknown error"}`);
|
|
2466
2503
|
return JSON.stringify(data);
|
|
2467
2504
|
}
|
|
2505
|
+
const release_promote_DEFAULT_API = "https://www.extension.dev";
|
|
2506
|
+
const release_promote_schema = {
|
|
2507
|
+
name: "extension_release_promote",
|
|
2508
|
+
description: "Promote a built extension to a release channel (e.g. stable, preview, beta) on extension.dev, headless. Auth-gated: requires 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).",
|
|
2509
|
+
inputSchema: {
|
|
2510
|
+
type: "object",
|
|
2511
|
+
properties: {
|
|
2512
|
+
buildId: {
|
|
2513
|
+
type: "string",
|
|
2514
|
+
description: "Build commit SHA to promote (a 7-char short SHA is fine)"
|
|
2515
|
+
},
|
|
2516
|
+
channel: {
|
|
2517
|
+
type: "string",
|
|
2518
|
+
description: "Target release channel, e.g. stable, preview, beta"
|
|
2519
|
+
},
|
|
2520
|
+
sourceChannel: {
|
|
2521
|
+
type: "string",
|
|
2522
|
+
description: "Channel to promote from (optional; inferred otherwise)"
|
|
2523
|
+
},
|
|
2524
|
+
browsers: {
|
|
2525
|
+
type: "array",
|
|
2526
|
+
items: {
|
|
2527
|
+
type: "string"
|
|
2528
|
+
},
|
|
2529
|
+
description: "Browsers to release (optional; auto-detected from the build)"
|
|
2530
|
+
},
|
|
2531
|
+
version: {
|
|
2532
|
+
type: "string",
|
|
2533
|
+
description: "Version label for the release (optional)"
|
|
2534
|
+
},
|
|
2535
|
+
releaseNotes: {
|
|
2536
|
+
type: "string",
|
|
2537
|
+
description: "Release notes markdown (optional)"
|
|
2538
|
+
},
|
|
2539
|
+
api: {
|
|
2540
|
+
type: "string",
|
|
2541
|
+
description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
|
|
2542
|
+
}
|
|
2543
|
+
},
|
|
2544
|
+
required: [
|
|
2545
|
+
"buildId",
|
|
2546
|
+
"channel"
|
|
2547
|
+
]
|
|
2548
|
+
}
|
|
2549
|
+
};
|
|
2550
|
+
function release_promote_fail(name, message) {
|
|
2551
|
+
return JSON.stringify({
|
|
2552
|
+
ok: false,
|
|
2553
|
+
error: {
|
|
2554
|
+
name,
|
|
2555
|
+
message
|
|
2556
|
+
}
|
|
2557
|
+
});
|
|
2558
|
+
}
|
|
2559
|
+
async function release_promote_handler(args) {
|
|
2560
|
+
const token = resolveToken();
|
|
2561
|
+
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.");
|
|
2562
|
+
const buildId = String(args.buildId || "").trim();
|
|
2563
|
+
const channel = String(args.channel || "").trim();
|
|
2564
|
+
if (!buildId || !channel) return release_promote_fail("ReleaseInputError", "buildId and channel are required.");
|
|
2565
|
+
const apiCheck = safeApiBase(String(args.api || process.env.EXTENSION_DEV_API_URL || release_promote_DEFAULT_API));
|
|
2566
|
+
if (!apiCheck.ok) return release_promote_fail("ReleaseConfigError", apiCheck.message);
|
|
2567
|
+
const url = `${apiCheck.base}/api/cli/release/promote`;
|
|
2568
|
+
const body = {
|
|
2569
|
+
buildId,
|
|
2570
|
+
channel
|
|
2571
|
+
};
|
|
2572
|
+
if (args.sourceChannel) body.sourceChannel = String(args.sourceChannel).trim();
|
|
2573
|
+
if (Array.isArray(args.browsers) && args.browsers.length) body.browsers = args.browsers.map((b)=>String(b).trim()).filter(Boolean);
|
|
2574
|
+
if (args.version) body.version = String(args.version).trim();
|
|
2575
|
+
if (args.releaseNotes) body.releaseNotes = String(args.releaseNotes);
|
|
2576
|
+
let res;
|
|
2577
|
+
try {
|
|
2578
|
+
res = await fetch(url, {
|
|
2579
|
+
method: "POST",
|
|
2580
|
+
headers: {
|
|
2581
|
+
authorization: `Bearer ${token}`,
|
|
2582
|
+
"content-type": "application/json"
|
|
2583
|
+
},
|
|
2584
|
+
body: JSON.stringify(body)
|
|
2585
|
+
});
|
|
2586
|
+
} catch (err) {
|
|
2587
|
+
return release_promote_fail("ReleaseNetworkError", `Could not reach ${url}: ${err?.message || err}`);
|
|
2588
|
+
}
|
|
2589
|
+
const text = await res.text();
|
|
2590
|
+
let data;
|
|
2591
|
+
try {
|
|
2592
|
+
data = JSON.parse(text);
|
|
2593
|
+
} catch {
|
|
2594
|
+
data = {
|
|
2595
|
+
message: text
|
|
2596
|
+
};
|
|
2597
|
+
}
|
|
2598
|
+
if (!res.ok) return release_promote_fail("ReleaseError", `promote failed (${res.status}): ${data?.message || text || "unknown error"}`);
|
|
2599
|
+
return JSON.stringify(data);
|
|
2600
|
+
}
|
|
2468
2601
|
const wait_schema = {
|
|
2469
2602
|
name: "extension_wait",
|
|
2470
2603
|
description: "Wait for a running dev or start session to be ready. Polls the ready.json contract file and returns structured status.",
|
|
@@ -3404,6 +3537,7 @@ const tools = [
|
|
|
3404
3537
|
open_namespaceObject,
|
|
3405
3538
|
dom_inspect_namespaceObject,
|
|
3406
3539
|
publish_namespaceObject,
|
|
3540
|
+
release_promote_namespaceObject,
|
|
3407
3541
|
wait_namespaceObject,
|
|
3408
3542
|
add_feature_namespaceObject,
|
|
3409
3543
|
login_namespaceObject,
|
|
@@ -3418,7 +3552,7 @@ for (const tool of tools)toolMap.set(tool.schema.name, tool);
|
|
|
3418
3552
|
async function startServer() {
|
|
3419
3553
|
const server = new Server({
|
|
3420
3554
|
name: "extension-dev",
|
|
3421
|
-
version:
|
|
3555
|
+
version: package_namespaceObject.rE
|
|
3422
3556
|
}, {
|
|
3423
3557
|
capabilities: {
|
|
3424
3558
|
tools: {}
|
|
@@ -3484,6 +3618,34 @@ async function runCli(cmd, args) {
|
|
|
3484
3618
|
log(await whoami_handler());
|
|
3485
3619
|
return 0;
|
|
3486
3620
|
}
|
|
3621
|
+
if ("release" === cmd) {
|
|
3622
|
+
const sub = String(args[0] || "").trim();
|
|
3623
|
+
if ("promote" === sub) {
|
|
3624
|
+
const buildId = String(flag("build") || flag("build-id") || "").trim();
|
|
3625
|
+
const channel = String(flag("channel") || "").trim();
|
|
3626
|
+
if (!buildId || !channel) {
|
|
3627
|
+
log("Usage: extension-mcp release promote --build <sha> --channel <channel> [--source-channel <c>] [--version <v>] [--api <url>]");
|
|
3628
|
+
return 1;
|
|
3629
|
+
}
|
|
3630
|
+
const out = await release_promote_handler({
|
|
3631
|
+
buildId,
|
|
3632
|
+
channel,
|
|
3633
|
+
sourceChannel: flag("source-channel"),
|
|
3634
|
+
version: flag("version"),
|
|
3635
|
+
api: flag("api")
|
|
3636
|
+
});
|
|
3637
|
+
log(out);
|
|
3638
|
+
let parsed = null;
|
|
3639
|
+
try {
|
|
3640
|
+
parsed = JSON.parse(out);
|
|
3641
|
+
} catch {
|
|
3642
|
+
parsed = null;
|
|
3643
|
+
}
|
|
3644
|
+
return parsed?.ok === false ? 1 : 0;
|
|
3645
|
+
}
|
|
3646
|
+
log("Usage: extension-mcp release promote --build <sha> --channel <channel>");
|
|
3647
|
+
return 1;
|
|
3648
|
+
}
|
|
3487
3649
|
if ("logout" === cmd) {
|
|
3488
3650
|
log(await logout_handler());
|
|
3489
3651
|
return 0;
|
|
@@ -3527,7 +3689,7 @@ async function runCli(cmd, args) {
|
|
|
3527
3689
|
return 1;
|
|
3528
3690
|
}
|
|
3529
3691
|
}
|
|
3530
|
-
log(`Unknown command: ${cmd}. Expected one of: login, logout, whoami.`);
|
|
3692
|
+
log(`Unknown command: ${cmd}. Expected one of: login, logout, whoami, release.`);
|
|
3531
3693
|
return 1;
|
|
3532
3694
|
}
|
|
3533
3695
|
export { runCli, startServer };
|