@extension.dev/mcp 3.17.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/bin/extension-mcp.js +1 -1
- package/dist/module.js +402 -489
- 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 +0 -2
- package/dist/src/tools/release-promote.d.ts +50 -0
- package/package.json +16 -15
- package/dist/src/lib/credentials.d.ts +0 -24
- package/dist/src/lib/github-device.d.ts +0 -40
- package/dist/src/lib/login-flow.d.ts +0 -25
package/dist/module.js
CHANGED
|
@@ -7,10 +7,13 @@ 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";
|
|
14
|
+
import { clearCredentials, exchangeAndPersist, fetchLoginConfig, pollForToken, publish, readCredentials, resolveApiBase, resolveToken, safeApiBase, startDeviceCode } from "@extension.dev/core";
|
|
13
15
|
import { extensionInstall, getManagedBrowsersCacheRoot } from "extension-install";
|
|
16
|
+
import { execFile } from "node:child_process";
|
|
14
17
|
import { promisify } from "node:util";
|
|
15
18
|
var add_feature_namespaceObject = {};
|
|
16
19
|
__webpack_require__.r(add_feature_namespaceObject);
|
|
@@ -106,7 +109,7 @@ var logs_namespaceObject = {};
|
|
|
106
109
|
__webpack_require__.r(logs_namespaceObject);
|
|
107
110
|
__webpack_require__.d(logs_namespaceObject, {
|
|
108
111
|
handler: ()=>logs_handler,
|
|
109
|
-
schema: ()=>
|
|
112
|
+
schema: ()=>logs_schema_schema
|
|
110
113
|
});
|
|
111
114
|
var manifest_validate_namespaceObject = {};
|
|
112
115
|
__webpack_require__.r(manifest_validate_namespaceObject);
|
|
@@ -130,9 +133,14 @@ var publish_namespaceObject = {};
|
|
|
130
133
|
__webpack_require__.r(publish_namespaceObject);
|
|
131
134
|
__webpack_require__.d(publish_namespaceObject, {
|
|
132
135
|
handler: ()=>publish_handler,
|
|
133
|
-
resolveToken: ()=>resolveToken,
|
|
134
136
|
schema: ()=>publish_schema
|
|
135
137
|
});
|
|
138
|
+
var release_promote_namespaceObject = {};
|
|
139
|
+
__webpack_require__.r(release_promote_namespaceObject);
|
|
140
|
+
__webpack_require__.d(release_promote_namespaceObject, {
|
|
141
|
+
handler: ()=>release_promote_handler,
|
|
142
|
+
schema: ()=>release_promote_schema
|
|
143
|
+
});
|
|
136
144
|
var reload_namespaceObject = {};
|
|
137
145
|
__webpack_require__.r(reload_namespaceObject);
|
|
138
146
|
__webpack_require__.d(reload_namespaceObject, {
|
|
@@ -169,6 +177,9 @@ __webpack_require__.d(whoami_namespaceObject, {
|
|
|
169
177
|
handler: ()=>whoami_handler,
|
|
170
178
|
schema: ()=>whoami_schema
|
|
171
179
|
});
|
|
180
|
+
var package_namespaceObject = {
|
|
181
|
+
rE: "4.1.0"
|
|
182
|
+
};
|
|
172
183
|
const schema = {
|
|
173
184
|
name: "extension_create",
|
|
174
185
|
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 +409,7 @@ async function build_handler(args) {
|
|
|
398
409
|
}
|
|
399
410
|
function runExtensionCli(args, options) {
|
|
400
411
|
return new Promise((resolve)=>{
|
|
401
|
-
const child =
|
|
412
|
+
const child = cross_spawn("npx", [
|
|
402
413
|
"extension",
|
|
403
414
|
...args
|
|
404
415
|
], {
|
|
@@ -413,7 +424,6 @@ function runExtensionCli(args, options) {
|
|
|
413
424
|
FORCE_COLOR: "0",
|
|
414
425
|
NO_COLOR: "1"
|
|
415
426
|
},
|
|
416
|
-
shell: true,
|
|
417
427
|
timeout: options?.timeoutMs ?? 30000
|
|
418
428
|
});
|
|
419
429
|
let stdout = "";
|
|
@@ -433,7 +443,7 @@ function runExtensionCli(args, options) {
|
|
|
433
443
|
});
|
|
434
444
|
}
|
|
435
445
|
function spawnExtensionCli(args, options) {
|
|
436
|
-
const child =
|
|
446
|
+
const child = cross_spawn("npx", [
|
|
437
447
|
"extension",
|
|
438
448
|
...args
|
|
439
449
|
], {
|
|
@@ -448,8 +458,7 @@ function spawnExtensionCli(args, options) {
|
|
|
448
458
|
...process.env,
|
|
449
459
|
FORCE_COLOR: "0",
|
|
450
460
|
NO_COLOR: "1"
|
|
451
|
-
}
|
|
452
|
-
shell: true
|
|
461
|
+
}
|
|
453
462
|
});
|
|
454
463
|
child.unref();
|
|
455
464
|
return child;
|
|
@@ -778,9 +787,8 @@ async function manifest_validate_handler(args) {
|
|
|
778
787
|
}
|
|
779
788
|
if (!manifest.name) result.errors.push("Missing required field: name");
|
|
780
789
|
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.');
|
|
790
|
+
const chromiumManifest = filterKeysForThisBrowser(manifest, "chrome");
|
|
791
|
+
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
792
|
for (const browser of browsers){
|
|
785
793
|
const isChromium = [
|
|
786
794
|
"chrome",
|
|
@@ -791,25 +799,26 @@ async function manifest_validate_handler(args) {
|
|
|
791
799
|
"firefox",
|
|
792
800
|
"gecko-based"
|
|
793
801
|
].includes(browser);
|
|
802
|
+
const effective = filterKeysForThisBrowser(manifest, browser);
|
|
794
803
|
const issues = [];
|
|
795
804
|
if (isChromium) {
|
|
796
|
-
const mv =
|
|
805
|
+
const mv = effective.manifest_version;
|
|
797
806
|
if (mv && mv < 3) issues.push("Manifest V2 is deprecated on Chromium. Use chromium:manifest_version: 3.");
|
|
798
|
-
if (
|
|
799
|
-
const perms =
|
|
807
|
+
if (effective.side_panel) {
|
|
808
|
+
const perms = effective.permissions ?? [];
|
|
800
809
|
if (!perms.includes("sidePanel")) issues.push('Side panel declared but "sidePanel" permission is missing.');
|
|
801
810
|
}
|
|
802
|
-
if (manifest["firefox:browser_action"] && !
|
|
811
|
+
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
812
|
}
|
|
804
813
|
if (isFirefox) {
|
|
805
|
-
const contentScripts =
|
|
814
|
+
const contentScripts = effective.content_scripts;
|
|
806
815
|
if (contentScripts) {
|
|
807
816
|
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
817
|
}
|
|
809
|
-
if (
|
|
810
|
-
const bg =
|
|
818
|
+
if (chromiumManifest.side_panel && !effective.sidebar_action) issues.push("Chromium side_panel declared but no firefox:sidebar_action. Firefox uses sidebar_action for sidebars.");
|
|
819
|
+
const bg = effective.background;
|
|
811
820
|
if (bg) {
|
|
812
|
-
if (bg.service_worker && !bg
|
|
821
|
+
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
822
|
}
|
|
814
823
|
}
|
|
815
824
|
result.browserSupport[browser] = {
|
|
@@ -819,11 +828,11 @@ async function manifest_validate_handler(args) {
|
|
|
819
828
|
if (issues.length) result.valid = false;
|
|
820
829
|
}
|
|
821
830
|
const surfaces = [];
|
|
822
|
-
if (
|
|
823
|
-
if (
|
|
824
|
-
if (
|
|
825
|
-
if (
|
|
826
|
-
if (
|
|
831
|
+
if (chromiumManifest.content_scripts) surfaces.push("content");
|
|
832
|
+
if (chromiumManifest.side_panel) surfaces.push("sidebar");
|
|
833
|
+
if (chromiumManifest.action || manifest["firefox:browser_action"]) surfaces.push("action");
|
|
834
|
+
if (chromiumManifest.chrome_url_overrides?.newtab) surfaces.push("newtab");
|
|
835
|
+
if (chromiumManifest.background) surfaces.push("background");
|
|
827
836
|
if (surfaces.length) try {
|
|
828
837
|
const templates = await listTemplates();
|
|
829
838
|
result.similarTemplates = templates.filter((t)=>t.surfaces.some((s)=>surfaces.includes(s))).slice(0, 5).map((t)=>({
|
|
@@ -1000,7 +1009,7 @@ function _define_property(obj, key, value) {
|
|
|
1000
1009
|
return obj;
|
|
1001
1010
|
}
|
|
1002
1011
|
const COMMAND_TIMEOUT_MS = 15000;
|
|
1003
|
-
class
|
|
1012
|
+
class CDPConnection {
|
|
1004
1013
|
async connect(wsUrl) {
|
|
1005
1014
|
return new Promise((resolve, reject)=>{
|
|
1006
1015
|
this.ws = new ws(wsUrl);
|
|
@@ -1092,61 +1101,52 @@ class CDPClient {
|
|
|
1092
1101
|
this.ws.send(JSON.stringify(message));
|
|
1093
1102
|
});
|
|
1094
1103
|
}
|
|
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
|
-
]);
|
|
1104
|
+
getConsoleMessages() {
|
|
1105
|
+
return [
|
|
1106
|
+
...this.consoleMessages
|
|
1107
|
+
];
|
|
1124
1108
|
}
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
const
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1109
|
+
getConsoleSummary() {
|
|
1110
|
+
const counts = {};
|
|
1111
|
+
const uniqueByLevel = {};
|
|
1112
|
+
for (const msg of this.consoleMessages){
|
|
1113
|
+
counts[msg.level] = (counts[msg.level] ?? 0) + 1;
|
|
1114
|
+
if (!uniqueByLevel[msg.level]) uniqueByLevel[msg.level] = new Map();
|
|
1115
|
+
const key = msg.text.slice(0, 200);
|
|
1116
|
+
uniqueByLevel[msg.level].set(key, (uniqueByLevel[msg.level].get(key) ?? 0) + 1);
|
|
1117
|
+
}
|
|
1118
|
+
const topMessages = [];
|
|
1119
|
+
for (const [level, msgs] of Object.entries(uniqueByLevel)){
|
|
1120
|
+
const sorted = [
|
|
1121
|
+
...msgs.entries()
|
|
1122
|
+
].sort((a, b)=>b[1] - a[1]);
|
|
1123
|
+
for (const [text, count] of sorted.slice(0, 5))topMessages.push({
|
|
1124
|
+
level,
|
|
1125
|
+
text,
|
|
1126
|
+
count
|
|
1137
1127
|
});
|
|
1138
|
-
}
|
|
1128
|
+
}
|
|
1129
|
+
return {
|
|
1130
|
+
total: this.consoleMessages.length,
|
|
1131
|
+
counts,
|
|
1132
|
+
topMessages: topMessages.sort((a, b)=>b.count - a.count).slice(0, 10)
|
|
1133
|
+
};
|
|
1139
1134
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
}, sessionId);
|
|
1146
|
-
return response.result?.value;
|
|
1135
|
+
onEvent(handler) {
|
|
1136
|
+
this.eventListeners.add(handler);
|
|
1137
|
+
return ()=>{
|
|
1138
|
+
this.eventListeners.delete(handler);
|
|
1139
|
+
};
|
|
1147
1140
|
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1141
|
+
constructor(){
|
|
1142
|
+
_define_property(this, "ws", null);
|
|
1143
|
+
_define_property(this, "messageId", 0);
|
|
1144
|
+
_define_property(this, "eventListeners", new Set());
|
|
1145
|
+
_define_property(this, "pendingRequests", new Map());
|
|
1146
|
+
_define_property(this, "consoleMessages", []);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
const PAGE_HTML_SCRIPT = `(() => {
|
|
1150
1150
|
try {
|
|
1151
1151
|
const doctype = document.doctype;
|
|
1152
1152
|
const dt = doctype
|
|
@@ -1179,61 +1179,8 @@ class CDPClient {
|
|
|
1179
1179
|
}
|
|
1180
1180
|
return dt + '\\n' + document.documentElement.outerHTML;
|
|
1181
1181
|
} 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, `(() => {
|
|
1182
|
+
})()`;
|
|
1183
|
+
const PAGE_META_SCRIPT = `(() => {
|
|
1237
1184
|
try {
|
|
1238
1185
|
return {
|
|
1239
1186
|
title: document.title,
|
|
@@ -1249,11 +1196,42 @@ class CDPClient {
|
|
|
1249
1196
|
styleCount: document.querySelectorAll('style,link[rel="stylesheet"]').length
|
|
1250
1197
|
};
|
|
1251
1198
|
} catch { return {}; }
|
|
1252
|
-
})()
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1199
|
+
})()`;
|
|
1200
|
+
const EXTENSION_ROOT_META_SCRIPT = `(() => {
|
|
1201
|
+
try {
|
|
1202
|
+
const readGeneration = (node) => {
|
|
1203
|
+
const raw = node.getAttribute && node.getAttribute('data-extjs-reinject-generation');
|
|
1204
|
+
const parsed = Number(raw);
|
|
1205
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
1206
|
+
};
|
|
1207
|
+
const normalize = (node) => ({
|
|
1208
|
+
tag: node.tagName ? String(node.tagName).toLowerCase() : 'unknown',
|
|
1209
|
+
id: node.id || undefined,
|
|
1210
|
+
key: node.getAttribute ? node.getAttribute('data-extjs-reinject-key') || undefined : undefined,
|
|
1211
|
+
generation: readGeneration(node),
|
|
1212
|
+
status: node.getAttribute ? node.getAttribute('data-extjs-reinject-status') || undefined : undefined
|
|
1213
|
+
});
|
|
1214
|
+
const roots = Array.from(
|
|
1215
|
+
document.querySelectorAll('#extension-root,[data-extension-root]:not([data-extension-root="extension-js-devtools"])')
|
|
1216
|
+
).slice(0, 10).map(normalize);
|
|
1217
|
+
const markers = Array.from(
|
|
1218
|
+
document.querySelectorAll('[data-extjs-reinject-marker="true"]')
|
|
1219
|
+
).slice(0, 10).map(normalize);
|
|
1220
|
+
if (!roots.length && !markers.length) return null;
|
|
1221
|
+
const generations = [...roots, ...markers]
|
|
1222
|
+
.map(e => e.generation)
|
|
1223
|
+
.filter(g => typeof g === 'number');
|
|
1224
|
+
return {
|
|
1225
|
+
rootCount: roots.length,
|
|
1226
|
+
markerCount: markers.length,
|
|
1227
|
+
latestGeneration: generations.length ? Math.max(...generations) : 0,
|
|
1228
|
+
roots,
|
|
1229
|
+
markers
|
|
1230
|
+
};
|
|
1231
|
+
} catch { return null; }
|
|
1232
|
+
})()`;
|
|
1233
|
+
function probeSelectorsScript(selectors) {
|
|
1234
|
+
return `(() => {
|
|
1257
1235
|
const selectors = ${JSON.stringify(selectors)};
|
|
1258
1236
|
return selectors.map(selector => {
|
|
1259
1237
|
try {
|
|
@@ -1275,11 +1253,10 @@ class CDPClient {
|
|
|
1275
1253
|
return { selector, count: 0, samples: [], error: String(e) };
|
|
1276
1254
|
}
|
|
1277
1255
|
});
|
|
1278
|
-
})()
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
const result = await this.evaluate(sessionId, `(() => {
|
|
1256
|
+
})()`;
|
|
1257
|
+
}
|
|
1258
|
+
function domSnapshotScript(maxNodes) {
|
|
1259
|
+
return `(() => {
|
|
1283
1260
|
const maxNodes = ${maxNodes};
|
|
1284
1261
|
const nodes = [];
|
|
1285
1262
|
const walk = (node, depth) => {
|
|
@@ -1301,88 +1278,131 @@ class CDPClient {
|
|
|
1301
1278
|
};
|
|
1302
1279
|
walk(document.documentElement, 0);
|
|
1303
1280
|
return nodes;
|
|
1304
|
-
})()
|
|
1305
|
-
|
|
1281
|
+
})()`;
|
|
1282
|
+
}
|
|
1283
|
+
class CDPClient extends CDPConnection {
|
|
1284
|
+
static async discoverBrowserWsUrl(port, host = "127.0.0.1") {
|
|
1285
|
+
const res = await fetch(`http://${host}:${port}/json/version`);
|
|
1286
|
+
if (!res.ok) throw new Error(`CDP /json/version failed: ${res.status}`);
|
|
1287
|
+
const data = await res.json();
|
|
1288
|
+
if ("string" == typeof data.webSocketDebuggerUrl) return data.webSocketDebuggerUrl;
|
|
1289
|
+
throw new Error("No webSocketDebuggerUrl in /json/version response");
|
|
1306
1290
|
}
|
|
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;
|
|
1291
|
+
static async discoverTargets(port, host = "127.0.0.1") {
|
|
1292
|
+
const res = await fetch(`http://${host}:${port}/json`);
|
|
1293
|
+
if (!res.ok) throw new Error(`CDP /json failed: ${res.status}`);
|
|
1294
|
+
return await res.json();
|
|
1342
1295
|
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
];
|
|
1296
|
+
async getTargets() {
|
|
1297
|
+
const response = await this.sendCommand("Target.getTargets");
|
|
1298
|
+
return response?.targetInfos ?? [];
|
|
1347
1299
|
}
|
|
1348
|
-
|
|
1349
|
-
const
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1300
|
+
async attachToTarget(targetId) {
|
|
1301
|
+
const response = await this.sendCommand("Target.attachToTarget", {
|
|
1302
|
+
targetId,
|
|
1303
|
+
flatten: true
|
|
1304
|
+
});
|
|
1305
|
+
return response.sessionId ?? "";
|
|
1306
|
+
}
|
|
1307
|
+
async enableDomains(sessionId) {
|
|
1308
|
+
await Promise.all([
|
|
1309
|
+
this.sendCommand("Runtime.enable", {}, sessionId),
|
|
1310
|
+
this.sendCommand("Log.enable", {}, sessionId),
|
|
1311
|
+
this.sendCommand("Page.enable", {}, sessionId)
|
|
1312
|
+
]);
|
|
1313
|
+
}
|
|
1314
|
+
async navigate(sessionId, url) {
|
|
1315
|
+
await this.sendCommand("Page.navigate", {
|
|
1316
|
+
url
|
|
1317
|
+
}, sessionId);
|
|
1318
|
+
await new Promise((resolve)=>{
|
|
1319
|
+
const timeout = setTimeout(resolve, 5000);
|
|
1320
|
+
const unsubscribe = this.onEvent((msg)=>{
|
|
1321
|
+
if ("Page.loadEventFired" === msg.method) {
|
|
1322
|
+
clearTimeout(timeout);
|
|
1323
|
+
unsubscribe();
|
|
1324
|
+
resolve();
|
|
1325
|
+
}
|
|
1326
|
+
});
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
async evaluate(sessionId, expression) {
|
|
1330
|
+
const response = await this.sendCommand("Runtime.evaluate", {
|
|
1331
|
+
expression,
|
|
1332
|
+
returnByValue: true,
|
|
1333
|
+
awaitPromise: false
|
|
1334
|
+
}, sessionId);
|
|
1335
|
+
return response.result?.value;
|
|
1336
|
+
}
|
|
1337
|
+
async getPageHTML(sessionId) {
|
|
1338
|
+
const result = await this.evaluate(sessionId, PAGE_HTML_SCRIPT);
|
|
1339
|
+
return "string" == typeof result ? result : "";
|
|
1340
|
+
}
|
|
1341
|
+
async getClosedShadowRoots(sessionId, maxBytes = 65536) {
|
|
1342
|
+
await this.sendCommand("DOM.enable", {}, sessionId);
|
|
1343
|
+
const doc = await this.sendCommand("DOM.getDocument", {
|
|
1344
|
+
depth: -1,
|
|
1345
|
+
pierce: true
|
|
1346
|
+
}, sessionId);
|
|
1347
|
+
const found = [];
|
|
1348
|
+
const walk = (node, hostName)=>{
|
|
1349
|
+
if (!node || "object" != typeof node) return;
|
|
1350
|
+
const name = node.localName || node.nodeName || hostName;
|
|
1351
|
+
if (Array.isArray(node.shadowRoots)) for (const sr of node.shadowRoots){
|
|
1352
|
+
if (sr && "closed" === sr.shadowRootType && "number" == typeof sr.nodeId) found.push({
|
|
1353
|
+
nodeId: sr.nodeId,
|
|
1354
|
+
host: String(name),
|
|
1355
|
+
type: "closed"
|
|
1356
|
+
});
|
|
1357
|
+
walk(sr, name);
|
|
1358
|
+
}
|
|
1359
|
+
if (Array.isArray(node.children)) for (const c of node.children)walk(c, name);
|
|
1360
|
+
if (node.contentDocument) walk(node.contentDocument, name);
|
|
1361
|
+
};
|
|
1362
|
+
walk(doc.root, "html");
|
|
1363
|
+
const out = [];
|
|
1364
|
+
for (const f of found)try {
|
|
1365
|
+
const oh = await this.sendCommand("DOM.getOuterHTML", {
|
|
1366
|
+
nodeId: f.nodeId
|
|
1367
|
+
}, sessionId);
|
|
1368
|
+
let html = String(oh?.outerHTML ?? "");
|
|
1369
|
+
let truncated = false;
|
|
1370
|
+
if (maxBytes > 0 && html.length > maxBytes) {
|
|
1371
|
+
html = html.slice(0, maxBytes);
|
|
1372
|
+
truncated = true;
|
|
1373
|
+
}
|
|
1374
|
+
out.push({
|
|
1375
|
+
host: f.host,
|
|
1376
|
+
type: f.type,
|
|
1377
|
+
html,
|
|
1378
|
+
...truncated ? {
|
|
1379
|
+
truncated
|
|
1380
|
+
} : {}
|
|
1381
|
+
});
|
|
1382
|
+
} catch {
|
|
1383
|
+
out.push({
|
|
1384
|
+
host: f.host,
|
|
1385
|
+
type: f.type,
|
|
1386
|
+
html: ""
|
|
1366
1387
|
});
|
|
1367
1388
|
}
|
|
1368
|
-
return
|
|
1369
|
-
total: this.consoleMessages.length,
|
|
1370
|
-
counts,
|
|
1371
|
-
topMessages: topMessages.sort((a, b)=>b.count - a.count).slice(0, 10)
|
|
1372
|
-
};
|
|
1389
|
+
return out;
|
|
1373
1390
|
}
|
|
1374
|
-
|
|
1375
|
-
this.
|
|
1376
|
-
return
|
|
1377
|
-
this.eventListeners.delete(handler);
|
|
1378
|
-
};
|
|
1391
|
+
async getPageMeta(sessionId) {
|
|
1392
|
+
const result = await this.evaluate(sessionId, PAGE_META_SCRIPT);
|
|
1393
|
+
return result ?? {};
|
|
1379
1394
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1395
|
+
async probeSelectors(sessionId, selectors) {
|
|
1396
|
+
const result = await this.evaluate(sessionId, probeSelectorsScript(selectors));
|
|
1397
|
+
return result ?? [];
|
|
1398
|
+
}
|
|
1399
|
+
async getDomSnapshot(sessionId, maxNodes = 500) {
|
|
1400
|
+
const result = await this.evaluate(sessionId, domSnapshotScript(maxNodes));
|
|
1401
|
+
return result ?? [];
|
|
1402
|
+
}
|
|
1403
|
+
async getExtensionRootMeta(sessionId) {
|
|
1404
|
+
const result = await this.evaluate(sessionId, EXTENSION_ROOT_META_SCRIPT);
|
|
1405
|
+
return result ?? null;
|
|
1386
1406
|
}
|
|
1387
1407
|
}
|
|
1388
1408
|
const source_inspect_schema = {
|
|
@@ -1685,13 +1705,57 @@ async function list_extensions_findCdpPort(projectPath, browser) {
|
|
|
1685
1705
|
socket.connect(defaultPort, "127.0.0.1");
|
|
1686
1706
|
});
|
|
1687
1707
|
}
|
|
1688
|
-
const CONTROL_ENVELOPE_VERSION = 1;
|
|
1689
1708
|
const CONTROL_WS_PATH = "/extjs-control";
|
|
1690
|
-
const
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1709
|
+
const LEVEL_ORDER = [
|
|
1710
|
+
"error",
|
|
1711
|
+
"warn",
|
|
1712
|
+
"info",
|
|
1713
|
+
"debug",
|
|
1714
|
+
"trace"
|
|
1715
|
+
];
|
|
1716
|
+
function levelRank(level) {
|
|
1717
|
+
const l = "log" === level ? "info" : level;
|
|
1718
|
+
const i = LEVEL_ORDER.indexOf(l);
|
|
1719
|
+
return -1 === i ? LEVEL_ORDER.length : i;
|
|
1720
|
+
}
|
|
1721
|
+
function makeUrlMatcher(pattern) {
|
|
1722
|
+
const hasGlob = pattern.includes("*");
|
|
1723
|
+
let re = null;
|
|
1724
|
+
if (hasGlob) {
|
|
1725
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1726
|
+
re = new RegExp(escaped);
|
|
1727
|
+
}
|
|
1728
|
+
return (event)=>{
|
|
1729
|
+
const candidates = [
|
|
1730
|
+
event.url,
|
|
1731
|
+
event.hostname
|
|
1732
|
+
].filter((v)=>"string" == typeof v);
|
|
1733
|
+
if (0 === candidates.length) return false;
|
|
1734
|
+
return candidates.some((c)=>re ? re.test(c) : c.includes(pattern));
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
function makeFilter(args) {
|
|
1738
|
+
const minLevel = String(args.level || "all").toLowerCase();
|
|
1739
|
+
const rawContexts = Array.isArray(args.context) ? args.context : "string" == typeof args.context ? args.context.split(",") : null;
|
|
1740
|
+
const contexts = rawContexts && rawContexts.length ? new Set(rawContexts.map((c)=>c.trim()).filter(Boolean)) : null;
|
|
1741
|
+
const sinceSeq = null != args.since ? Number(args.since) : null;
|
|
1742
|
+
const urlMatches = args.url ? makeUrlMatcher(args.url) : null;
|
|
1743
|
+
const tabId = null != args.tab ? Number(args.tab) : null;
|
|
1744
|
+
return (event)=>{
|
|
1745
|
+
if (!event || "object" != typeof event) return false;
|
|
1746
|
+
if ("header" === event.type) return false;
|
|
1747
|
+
if (args.signalsOnly && "dx.signal" !== event.eventType) return false;
|
|
1748
|
+
if (contexts && !contexts.has(event.context)) return false;
|
|
1749
|
+
if ("all" !== minLevel && "off" !== minLevel) {
|
|
1750
|
+
if (levelRank(event.level) > levelRank(minLevel)) return false;
|
|
1751
|
+
}
|
|
1752
|
+
if (null != sinceSeq && Number.isFinite(sinceSeq) && "number" == typeof event.seq && event.seq <= sinceSeq) return false;
|
|
1753
|
+
if (urlMatches && !urlMatches(event)) return false;
|
|
1754
|
+
if (null != tabId && Number.isFinite(tabId) && event.tabId !== tabId) return false;
|
|
1755
|
+
return true;
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
const logs_schema_schema = {
|
|
1695
1759
|
name: "extension_logs",
|
|
1696
1760
|
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
1761
|
inputSchema: {
|
|
@@ -1760,12 +1824,12 @@ const logs_schema = {
|
|
|
1760
1824
|
},
|
|
1761
1825
|
followMs: {
|
|
1762
1826
|
type: "number",
|
|
1763
|
-
default:
|
|
1764
|
-
description:
|
|
1827
|
+
default: 4000,
|
|
1828
|
+
description: "How long to collect live frames when follow=true (clamped 500–15000ms)."
|
|
1765
1829
|
},
|
|
1766
1830
|
limit: {
|
|
1767
1831
|
type: "number",
|
|
1768
|
-
default:
|
|
1832
|
+
default: 200,
|
|
1769
1833
|
description: "Maximum number of (most recent) events to return."
|
|
1770
1834
|
}
|
|
1771
1835
|
},
|
|
@@ -1774,55 +1838,6 @@ const logs_schema = {
|
|
|
1774
1838
|
]
|
|
1775
1839
|
}
|
|
1776
1840
|
};
|
|
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
1841
|
function logsFilePath(projectPath, browser) {
|
|
1827
1842
|
return node_path.resolve(projectPath, "dist", "extension-js", browser, "logs.ndjson");
|
|
1828
1843
|
}
|
|
@@ -1898,7 +1913,7 @@ async function readFromStream(args, browser, limit) {
|
|
|
1898
1913
|
error: `No active control channel found for ${browser}.`,
|
|
1899
1914
|
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
1915
|
});
|
|
1901
|
-
const followMs = Math.min(Math.max(args.followMs ??
|
|
1916
|
+
const followMs = Math.min(Math.max(args.followMs ?? 4000, 500), 15000);
|
|
1902
1917
|
const matches = makeFilter(args);
|
|
1903
1918
|
const events = [];
|
|
1904
1919
|
let dropped = 0;
|
|
@@ -1929,7 +1944,7 @@ async function readFromStream(args, browser, limit) {
|
|
|
1929
1944
|
try {
|
|
1930
1945
|
socket.send(JSON.stringify({
|
|
1931
1946
|
type: "hello",
|
|
1932
|
-
v:
|
|
1947
|
+
v: 1,
|
|
1933
1948
|
role: "consumer",
|
|
1934
1949
|
instanceId: ready.instanceId
|
|
1935
1950
|
}));
|
|
@@ -1961,7 +1976,7 @@ async function readFromStream(args, browser, limit) {
|
|
|
1961
1976
|
}
|
|
1962
1977
|
async function logs_handler(args) {
|
|
1963
1978
|
const browser = args.browser ?? "chromium";
|
|
1964
|
-
const limit = args.limit && args.limit > 0 ? args.limit :
|
|
1979
|
+
const limit = args.limit && args.limit > 0 ? args.limit : 200;
|
|
1965
1980
|
if (args.follow) return readFromStream(args, browser, limit);
|
|
1966
1981
|
return readFromFile(args, browser, limit);
|
|
1967
1982
|
}
|
|
@@ -2325,70 +2340,6 @@ async function dom_inspect_handler(args) {
|
|
|
2325
2340
|
if (null != args.timeout) cli.push("--timeout", String(args.timeout));
|
|
2326
2341
|
return runActVerb(cli, args.projectPath, args.timeout);
|
|
2327
2342
|
}
|
|
2328
|
-
function credentialsPath() {
|
|
2329
|
-
if ("win32" === process.platform) {
|
|
2330
|
-
const base = process.env.APPDATA || process.env.LOCALAPPDATA || node_path.join(node_os.homedir(), "AppData", "Roaming");
|
|
2331
|
-
return node_path.join(base, "extension-dev", "auth.json");
|
|
2332
|
-
}
|
|
2333
|
-
const xdg = String(process.env.XDG_CONFIG_HOME || "").trim();
|
|
2334
|
-
const base = xdg || node_path.join(node_os.homedir(), ".config");
|
|
2335
|
-
return node_path.join(base, "extension-dev", "auth.json");
|
|
2336
|
-
}
|
|
2337
|
-
function readCredentials() {
|
|
2338
|
-
try {
|
|
2339
|
-
const raw = node_fs.readFileSync(credentialsPath(), "utf8");
|
|
2340
|
-
const data = JSON.parse(raw);
|
|
2341
|
-
if (!data || "object" != typeof data) return null;
|
|
2342
|
-
if (1 !== data.version) return null;
|
|
2343
|
-
const token = String(data.token || "").trim();
|
|
2344
|
-
if (!token) return null;
|
|
2345
|
-
return {
|
|
2346
|
-
version: 1,
|
|
2347
|
-
token,
|
|
2348
|
-
workspaceSlug: String(data.workspaceSlug || ""),
|
|
2349
|
-
projectSlug: String(data.projectSlug || ""),
|
|
2350
|
-
expiresAt: Number(data.expiresAt || 0),
|
|
2351
|
-
api: String(data.api || "")
|
|
2352
|
-
};
|
|
2353
|
-
} catch {
|
|
2354
|
-
return null;
|
|
2355
|
-
}
|
|
2356
|
-
}
|
|
2357
|
-
function writeCredentials(creds) {
|
|
2358
|
-
const file = credentialsPath();
|
|
2359
|
-
node_fs.mkdirSync(node_path.dirname(file), {
|
|
2360
|
-
recursive: true
|
|
2361
|
-
});
|
|
2362
|
-
node_fs.writeFileSync(file, JSON.stringify(creds, null, 2) + "\n", {
|
|
2363
|
-
mode: 384
|
|
2364
|
-
});
|
|
2365
|
-
try {
|
|
2366
|
-
node_fs.chmodSync(file, 384);
|
|
2367
|
-
} catch {}
|
|
2368
|
-
return file;
|
|
2369
|
-
}
|
|
2370
|
-
function clearCredentials() {
|
|
2371
|
-
const file = credentialsPath();
|
|
2372
|
-
try {
|
|
2373
|
-
node_fs.unlinkSync(file);
|
|
2374
|
-
return {
|
|
2375
|
-
cleared: true,
|
|
2376
|
-
path: file
|
|
2377
|
-
};
|
|
2378
|
-
} catch {
|
|
2379
|
-
return {
|
|
2380
|
-
cleared: false,
|
|
2381
|
-
path: file
|
|
2382
|
-
};
|
|
2383
|
-
}
|
|
2384
|
-
}
|
|
2385
|
-
function readValidCredentials(nowSeconds = Math.floor(Date.now() / 1000)) {
|
|
2386
|
-
const creds = readCredentials();
|
|
2387
|
-
if (!creds) return null;
|
|
2388
|
-
if (creds.expiresAt && creds.expiresAt <= nowSeconds) return null;
|
|
2389
|
-
return creds;
|
|
2390
|
-
}
|
|
2391
|
-
const DEFAULT_API = "https://www.extension.dev";
|
|
2392
2343
|
const publish_schema = {
|
|
2393
2344
|
name: "extension_publish",
|
|
2394
2345
|
description: "Publish a project to extension.dev and return a shareable URL. Auth-gated: requires EXTENSION_DEV_TOKEN (a workspace/project access token) in the environment. Posts to the platform's CLI publish endpoint directly. This is the only tool that talks to the hosted platform rather than the local browser.",
|
|
@@ -2417,12 +2368,68 @@ const publish_schema = {
|
|
|
2417
2368
|
]
|
|
2418
2369
|
}
|
|
2419
2370
|
};
|
|
2420
|
-
function
|
|
2421
|
-
const
|
|
2422
|
-
if (
|
|
2423
|
-
|
|
2424
|
-
|
|
2371
|
+
async function publish_handler(args) {
|
|
2372
|
+
const token = resolveToken();
|
|
2373
|
+
if (!token) return JSON.stringify({
|
|
2374
|
+
ok: false,
|
|
2375
|
+
error: {
|
|
2376
|
+
name: "PublishAuthError",
|
|
2377
|
+
message: "No token. Run extension_login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard)."
|
|
2378
|
+
}
|
|
2379
|
+
});
|
|
2380
|
+
const result = await publish({
|
|
2381
|
+
ttlHours: args.ttlHours,
|
|
2382
|
+
buildSha: args.buildSha,
|
|
2383
|
+
api: args.api,
|
|
2384
|
+
token
|
|
2385
|
+
});
|
|
2386
|
+
return result.ok ? JSON.stringify(result.data) : JSON.stringify(result);
|
|
2425
2387
|
}
|
|
2388
|
+
const DEFAULT_API = "https://www.extension.dev";
|
|
2389
|
+
const release_promote_schema = {
|
|
2390
|
+
name: "extension_release_promote",
|
|
2391
|
+
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).",
|
|
2392
|
+
inputSchema: {
|
|
2393
|
+
type: "object",
|
|
2394
|
+
properties: {
|
|
2395
|
+
buildId: {
|
|
2396
|
+
type: "string",
|
|
2397
|
+
description: "Build commit SHA to promote (a 7-char short SHA is fine)"
|
|
2398
|
+
},
|
|
2399
|
+
channel: {
|
|
2400
|
+
type: "string",
|
|
2401
|
+
description: "Target release channel, e.g. stable, preview, beta"
|
|
2402
|
+
},
|
|
2403
|
+
sourceChannel: {
|
|
2404
|
+
type: "string",
|
|
2405
|
+
description: "Channel to promote from (optional; inferred otherwise)"
|
|
2406
|
+
},
|
|
2407
|
+
browsers: {
|
|
2408
|
+
type: "array",
|
|
2409
|
+
items: {
|
|
2410
|
+
type: "string"
|
|
2411
|
+
},
|
|
2412
|
+
description: "Browsers to release (optional; auto-detected from the build)"
|
|
2413
|
+
},
|
|
2414
|
+
version: {
|
|
2415
|
+
type: "string",
|
|
2416
|
+
description: "Version label for the release (optional)"
|
|
2417
|
+
},
|
|
2418
|
+
releaseNotes: {
|
|
2419
|
+
type: "string",
|
|
2420
|
+
description: "Release notes markdown (optional)"
|
|
2421
|
+
},
|
|
2422
|
+
api: {
|
|
2423
|
+
type: "string",
|
|
2424
|
+
description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
|
|
2425
|
+
}
|
|
2426
|
+
},
|
|
2427
|
+
required: [
|
|
2428
|
+
"buildId",
|
|
2429
|
+
"channel"
|
|
2430
|
+
]
|
|
2431
|
+
}
|
|
2432
|
+
};
|
|
2426
2433
|
function fail(name, message) {
|
|
2427
2434
|
return JSON.stringify({
|
|
2428
2435
|
ok: false,
|
|
@@ -2432,14 +2439,23 @@ function fail(name, message) {
|
|
|
2432
2439
|
}
|
|
2433
2440
|
});
|
|
2434
2441
|
}
|
|
2435
|
-
async function
|
|
2442
|
+
async function release_promote_handler(args) {
|
|
2436
2443
|
const token = resolveToken();
|
|
2437
|
-
if (!token) return fail("
|
|
2438
|
-
const
|
|
2439
|
-
const
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
if (
|
|
2444
|
+
if (!token) return 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.");
|
|
2445
|
+
const buildId = String(args.buildId || "").trim();
|
|
2446
|
+
const channel = String(args.channel || "").trim();
|
|
2447
|
+
if (!buildId || !channel) return fail("ReleaseInputError", "buildId and channel are required.");
|
|
2448
|
+
const apiCheck = safeApiBase(String(args.api || process.env.EXTENSION_DEV_API_URL || DEFAULT_API));
|
|
2449
|
+
if (!apiCheck.ok) return fail("ReleaseConfigError", apiCheck.message);
|
|
2450
|
+
const url = `${apiCheck.base}/api/cli/release/promote`;
|
|
2451
|
+
const body = {
|
|
2452
|
+
buildId,
|
|
2453
|
+
channel
|
|
2454
|
+
};
|
|
2455
|
+
if (args.sourceChannel) body.sourceChannel = String(args.sourceChannel).trim();
|
|
2456
|
+
if (Array.isArray(args.browsers) && args.browsers.length) body.browsers = args.browsers.map((b)=>String(b).trim()).filter(Boolean);
|
|
2457
|
+
if (args.version) body.version = String(args.version).trim();
|
|
2458
|
+
if (args.releaseNotes) body.releaseNotes = String(args.releaseNotes);
|
|
2443
2459
|
let res;
|
|
2444
2460
|
try {
|
|
2445
2461
|
res = await fetch(url, {
|
|
@@ -2451,7 +2467,7 @@ async function publish_handler(args) {
|
|
|
2451
2467
|
body: JSON.stringify(body)
|
|
2452
2468
|
});
|
|
2453
2469
|
} catch (err) {
|
|
2454
|
-
return fail("
|
|
2470
|
+
return fail("ReleaseNetworkError", `Could not reach ${url}: ${err?.message || err}`);
|
|
2455
2471
|
}
|
|
2456
2472
|
const text = await res.text();
|
|
2457
2473
|
let data;
|
|
@@ -2462,7 +2478,7 @@ async function publish_handler(args) {
|
|
|
2462
2478
|
message: text
|
|
2463
2479
|
};
|
|
2464
2480
|
}
|
|
2465
|
-
if (!res.ok) return fail("
|
|
2481
|
+
if (!res.ok) return fail("ReleaseError", `promote failed (${res.status}): ${data?.message || text || "unknown error"}`);
|
|
2466
2482
|
return JSON.stringify(data);
|
|
2467
2483
|
}
|
|
2468
2484
|
const wait_schema = {
|
|
@@ -2760,138 +2776,6 @@ async function add_feature_handler(args) {
|
|
|
2760
2776
|
hint: conflicts.length ? `Warning: ${conflicts.length} file(s) already exist and would be overwritten.` : "No conflicts detected. Safe to create all files."
|
|
2761
2777
|
});
|
|
2762
2778
|
}
|
|
2763
|
-
const GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
|
|
2764
|
-
const GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
|
|
2765
|
-
const DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
2766
|
-
const defaultSleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
2767
|
-
async function startDeviceCode(args) {
|
|
2768
|
-
const doFetch = args.fetchImpl ?? fetch;
|
|
2769
|
-
const res = await doFetch(GITHUB_DEVICE_CODE_URL, {
|
|
2770
|
-
method: "POST",
|
|
2771
|
-
headers: {
|
|
2772
|
-
accept: "application/json",
|
|
2773
|
-
"content-type": "application/json"
|
|
2774
|
-
},
|
|
2775
|
-
body: JSON.stringify({
|
|
2776
|
-
client_id: args.clientId,
|
|
2777
|
-
scope: args.scope || "read:user"
|
|
2778
|
-
})
|
|
2779
|
-
});
|
|
2780
|
-
const data = await res.json().catch(()=>({}));
|
|
2781
|
-
if (!res.ok || data.error) throw new Error(`GitHub device-code request failed: ${data.error_description || data.error || res.status}`);
|
|
2782
|
-
return {
|
|
2783
|
-
deviceCode: String(data.device_code || ""),
|
|
2784
|
-
userCode: String(data.user_code || ""),
|
|
2785
|
-
verificationUri: String(data.verification_uri || "https://github.com/login/device"),
|
|
2786
|
-
interval: Number(data.interval || 5),
|
|
2787
|
-
expiresIn: Number(data.expires_in || 900)
|
|
2788
|
-
};
|
|
2789
|
-
}
|
|
2790
|
-
async function pollForToken(args) {
|
|
2791
|
-
const doFetch = args.fetchImpl ?? fetch;
|
|
2792
|
-
const sleep = args.sleepImpl ?? defaultSleep;
|
|
2793
|
-
let intervalMs = 1000 * Math.max(1, args.interval);
|
|
2794
|
-
const deadline = Date.now() + Math.max(0, args.budgetMs);
|
|
2795
|
-
while(Date.now() < deadline){
|
|
2796
|
-
await sleep(intervalMs);
|
|
2797
|
-
const res = await doFetch(GITHUB_TOKEN_URL, {
|
|
2798
|
-
method: "POST",
|
|
2799
|
-
headers: {
|
|
2800
|
-
accept: "application/json",
|
|
2801
|
-
"content-type": "application/json"
|
|
2802
|
-
},
|
|
2803
|
-
body: JSON.stringify({
|
|
2804
|
-
client_id: args.clientId,
|
|
2805
|
-
device_code: args.deviceCode,
|
|
2806
|
-
grant_type: DEVICE_GRANT_TYPE
|
|
2807
|
-
})
|
|
2808
|
-
});
|
|
2809
|
-
const data = await res.json().catch(()=>({}));
|
|
2810
|
-
const token = String(data.access_token || "").trim();
|
|
2811
|
-
if (token) return {
|
|
2812
|
-
ok: true,
|
|
2813
|
-
githubToken: token
|
|
2814
|
-
};
|
|
2815
|
-
const error = String(data.error || "");
|
|
2816
|
-
if ("authorization_pending" === error) continue;
|
|
2817
|
-
if ("slow_down" === error) {
|
|
2818
|
-
intervalMs = Math.max(intervalMs + 5000, 1000 * Number(data.interval || 0));
|
|
2819
|
-
continue;
|
|
2820
|
-
}
|
|
2821
|
-
if ("expired_token" === error) return {
|
|
2822
|
-
ok: false,
|
|
2823
|
-
reason: "expired"
|
|
2824
|
-
};
|
|
2825
|
-
if ("access_denied" === error) return {
|
|
2826
|
-
ok: false,
|
|
2827
|
-
reason: "denied"
|
|
2828
|
-
};
|
|
2829
|
-
}
|
|
2830
|
-
return {
|
|
2831
|
-
ok: false,
|
|
2832
|
-
reason: "pending"
|
|
2833
|
-
};
|
|
2834
|
-
}
|
|
2835
|
-
const login_flow_DEFAULT_API = "https://www.extension.dev";
|
|
2836
|
-
function resolveApiBase(api) {
|
|
2837
|
-
return String(api || process.env.EXTENSION_DEV_API_URL || login_flow_DEFAULT_API).replace(/\/+$/, "");
|
|
2838
|
-
}
|
|
2839
|
-
async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
|
|
2840
|
-
const override = String(process.env.EXTENSION_DEV_GITHUB_CLIENT_ID || "").trim();
|
|
2841
|
-
if (override) return {
|
|
2842
|
-
clientId: override,
|
|
2843
|
-
scope: "read:user"
|
|
2844
|
-
};
|
|
2845
|
-
const res = await fetchImpl(`${apiBase}/api/cli/login/config`, {
|
|
2846
|
-
headers: {
|
|
2847
|
-
accept: "application/json"
|
|
2848
|
-
}
|
|
2849
|
-
});
|
|
2850
|
-
if (!res.ok) throw new Error(`Could not fetch login config from ${apiBase} (${res.status}).`);
|
|
2851
|
-
const data = await res.json().catch(()=>({}));
|
|
2852
|
-
const clientId = String(data.githubClientId || "").trim();
|
|
2853
|
-
if (!clientId) throw new Error("Login is not configured on the server (no GitHub client id). Set EXTENSION_DEV_GITHUB_CLIENT_ID to override.");
|
|
2854
|
-
return {
|
|
2855
|
-
clientId,
|
|
2856
|
-
scope: String(data.scope || "read:user")
|
|
2857
|
-
};
|
|
2858
|
-
}
|
|
2859
|
-
async function exchangeAndPersist(args) {
|
|
2860
|
-
const doFetch = args.fetchImpl ?? fetch;
|
|
2861
|
-
const res = await doFetch(`${args.apiBase}/api/cli/login/exchange`, {
|
|
2862
|
-
method: "POST",
|
|
2863
|
-
headers: {
|
|
2864
|
-
"content-type": "application/json",
|
|
2865
|
-
accept: "application/json"
|
|
2866
|
-
},
|
|
2867
|
-
body: JSON.stringify({
|
|
2868
|
-
githubToken: args.githubToken,
|
|
2869
|
-
project: args.project
|
|
2870
|
-
})
|
|
2871
|
-
});
|
|
2872
|
-
const text = await res.text();
|
|
2873
|
-
let data;
|
|
2874
|
-
try {
|
|
2875
|
-
data = JSON.parse(text);
|
|
2876
|
-
} catch {
|
|
2877
|
-
data = {
|
|
2878
|
-
message: text
|
|
2879
|
-
};
|
|
2880
|
-
}
|
|
2881
|
-
if (!res.ok) throw new Error(`Login exchange failed (${res.status}): ${data.message || "unknown error"}`);
|
|
2882
|
-
const token = String(data.token || "").trim();
|
|
2883
|
-
if (!token) throw new Error("Login exchange returned no token.");
|
|
2884
|
-
const creds = {
|
|
2885
|
-
version: 1,
|
|
2886
|
-
token,
|
|
2887
|
-
workspaceSlug: String(data.workspaceSlug || ""),
|
|
2888
|
-
projectSlug: String(data.projectSlug || ""),
|
|
2889
|
-
expiresAt: Number(data.expiresAt || 0),
|
|
2890
|
-
api: args.apiBase
|
|
2891
|
-
};
|
|
2892
|
-
writeCredentials(creds);
|
|
2893
|
-
return creds;
|
|
2894
|
-
}
|
|
2895
2779
|
const login_schema = {
|
|
2896
2780
|
name: "extension_login",
|
|
2897
2781
|
description: "Authenticate to extension.dev via GitHub device-code 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. Never returns the token. This is the only tool besides extension_publish that talks to the hosted platform.",
|
|
@@ -3404,6 +3288,7 @@ const tools = [
|
|
|
3404
3288
|
open_namespaceObject,
|
|
3405
3289
|
dom_inspect_namespaceObject,
|
|
3406
3290
|
publish_namespaceObject,
|
|
3291
|
+
release_promote_namespaceObject,
|
|
3407
3292
|
wait_namespaceObject,
|
|
3408
3293
|
add_feature_namespaceObject,
|
|
3409
3294
|
login_namespaceObject,
|
|
@@ -3418,7 +3303,7 @@ for (const tool of tools)toolMap.set(tool.schema.name, tool);
|
|
|
3418
3303
|
async function startServer() {
|
|
3419
3304
|
const server = new Server({
|
|
3420
3305
|
name: "extension-dev",
|
|
3421
|
-
version:
|
|
3306
|
+
version: package_namespaceObject.rE
|
|
3422
3307
|
}, {
|
|
3423
3308
|
capabilities: {
|
|
3424
3309
|
tools: {}
|
|
@@ -3484,6 +3369,34 @@ async function runCli(cmd, args) {
|
|
|
3484
3369
|
log(await whoami_handler());
|
|
3485
3370
|
return 0;
|
|
3486
3371
|
}
|
|
3372
|
+
if ("release" === cmd) {
|
|
3373
|
+
const sub = String(args[0] || "").trim();
|
|
3374
|
+
if ("promote" === sub) {
|
|
3375
|
+
const buildId = String(flag("build") || flag("build-id") || "").trim();
|
|
3376
|
+
const channel = String(flag("channel") || "").trim();
|
|
3377
|
+
if (!buildId || !channel) {
|
|
3378
|
+
log("Usage: extension-mcp release promote --build <sha> --channel <channel> [--source-channel <c>] [--version <v>] [--api <url>]");
|
|
3379
|
+
return 1;
|
|
3380
|
+
}
|
|
3381
|
+
const out = await release_promote_handler({
|
|
3382
|
+
buildId,
|
|
3383
|
+
channel,
|
|
3384
|
+
sourceChannel: flag("source-channel"),
|
|
3385
|
+
version: flag("version"),
|
|
3386
|
+
api: flag("api")
|
|
3387
|
+
});
|
|
3388
|
+
log(out);
|
|
3389
|
+
let parsed = null;
|
|
3390
|
+
try {
|
|
3391
|
+
parsed = JSON.parse(out);
|
|
3392
|
+
} catch {
|
|
3393
|
+
parsed = null;
|
|
3394
|
+
}
|
|
3395
|
+
return parsed?.ok === false ? 1 : 0;
|
|
3396
|
+
}
|
|
3397
|
+
log("Usage: extension-mcp release promote --build <sha> --channel <channel>");
|
|
3398
|
+
return 1;
|
|
3399
|
+
}
|
|
3487
3400
|
if ("logout" === cmd) {
|
|
3488
3401
|
log(await logout_handler());
|
|
3489
3402
|
return 0;
|
|
@@ -3527,7 +3440,7 @@ async function runCli(cmd, args) {
|
|
|
3527
3440
|
return 1;
|
|
3528
3441
|
}
|
|
3529
3442
|
}
|
|
3530
|
-
log(`Unknown command: ${cmd}. Expected one of: login, logout, whoami.`);
|
|
3443
|
+
log(`Unknown command: ${cmd}. Expected one of: login, logout, whoami, release.`);
|
|
3531
3444
|
return 1;
|
|
3532
3445
|
}
|
|
3533
3446
|
export { runCli, startServer };
|