@extension.dev/mcp 3.17.0-canary.1779905934.1151df3
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 +36 -0
- package/README.md +101 -0
- package/bin/extension-mcp.js +3 -0
- package/claude/ARCHITECTURE.md +161 -0
- package/claude/CLAUDE.md +264 -0
- package/claude/README.md +72 -0
- package/claude/commands/extension-add.md +63 -0
- package/claude/commands/extension-debug.md +43 -0
- package/claude/commands/extension-publish.md +62 -0
- package/claude/commands/extension.md +76 -0
- package/claude/examples/add-sidebar.md +56 -0
- package/claude/examples/create-extension.md +49 -0
- package/claude/rules/cross-browser.md +84 -0
- package/claude/rules/extension-dev.md +83 -0
- package/claude/rules/mcp-tools.md +889 -0
- package/dist/module.cjs +2979 -0
- package/dist/module.d.ts +1 -0
- package/dist/rslib.config.d.ts +2 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/lib/act.d.ts +21 -0
- package/dist/src/lib/cdp.d.ts +48 -0
- package/dist/src/lib/exec.d.ts +13 -0
- package/dist/src/lib/process-manager.d.ts +5 -0
- package/dist/src/lib/templates-cache.d.ts +11 -0
- package/dist/src/lib/types.d.ts +68 -0
- package/dist/src/tools/add-feature.d.ts +29 -0
- package/dist/src/tools/build.d.ts +36 -0
- package/dist/src/tools/create.d.ts +29 -0
- package/dist/src/tools/detect-browsers.d.ts +20 -0
- package/dist/src/tools/dev.d.ts +34 -0
- package/dist/src/tools/dom-inspect.d.ts +56 -0
- package/dist/src/tools/eval.d.ts +44 -0
- package/dist/src/tools/get-template-source.d.ts +25 -0
- package/dist/src/tools/inspect.d.ts +29 -0
- package/dist/src/tools/install-browser.d.ts +18 -0
- package/dist/src/tools/list-browsers.d.ts +9 -0
- package/dist/src/tools/list-templates.d.ts +41 -0
- package/dist/src/tools/logs.d.ts +80 -0
- package/dist/src/tools/manifest-validate.d.ts +26 -0
- package/dist/src/tools/open.d.ts +31 -0
- package/dist/src/tools/preview.d.ts +23 -0
- package/dist/src/tools/publish.d.ts +32 -0
- package/dist/src/tools/reload.d.ts +33 -0
- package/dist/src/tools/source-inspect.d.ts +57 -0
- package/dist/src/tools/start.d.ts +29 -0
- package/dist/src/tools/storage.d.ts +51 -0
- package/dist/src/tools/wait.d.ts +29 -0
- package/dist/vitest.config.d.ts +3 -0
- package/package.json +84 -0
package/dist/module.cjs
ADDED
|
@@ -0,0 +1,2979 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __webpack_require__ = {};
|
|
3
|
+
(()=>{
|
|
4
|
+
__webpack_require__.n = (module)=>{
|
|
5
|
+
var getter = module && module.__esModule ? ()=>module['default'] : ()=>module;
|
|
6
|
+
__webpack_require__.d(getter, {
|
|
7
|
+
a: getter
|
|
8
|
+
});
|
|
9
|
+
return getter;
|
|
10
|
+
};
|
|
11
|
+
})();
|
|
12
|
+
(()=>{
|
|
13
|
+
__webpack_require__.d = (exports1, definition)=>{
|
|
14
|
+
for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
get: definition[key]
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
})();
|
|
20
|
+
(()=>{
|
|
21
|
+
__webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
|
|
22
|
+
})();
|
|
23
|
+
(()=>{
|
|
24
|
+
__webpack_require__.r = (exports1)=>{
|
|
25
|
+
if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
|
|
26
|
+
value: 'Module'
|
|
27
|
+
});
|
|
28
|
+
Object.defineProperty(exports1, '__esModule', {
|
|
29
|
+
value: true
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
})();
|
|
33
|
+
var __webpack_exports__ = {};
|
|
34
|
+
__webpack_require__.r(__webpack_exports__);
|
|
35
|
+
__webpack_require__.d(__webpack_exports__, {
|
|
36
|
+
startServer: ()=>startServer
|
|
37
|
+
});
|
|
38
|
+
var create_namespaceObject = {};
|
|
39
|
+
__webpack_require__.r(create_namespaceObject);
|
|
40
|
+
__webpack_require__.d(create_namespaceObject, {
|
|
41
|
+
handler: ()=>create_handler,
|
|
42
|
+
schema: ()=>schema
|
|
43
|
+
});
|
|
44
|
+
var list_templates_namespaceObject = {};
|
|
45
|
+
__webpack_require__.r(list_templates_namespaceObject);
|
|
46
|
+
__webpack_require__.d(list_templates_namespaceObject, {
|
|
47
|
+
handler: ()=>list_templates_handler,
|
|
48
|
+
schema: ()=>list_templates_schema
|
|
49
|
+
});
|
|
50
|
+
var build_namespaceObject = {};
|
|
51
|
+
__webpack_require__.r(build_namespaceObject);
|
|
52
|
+
__webpack_require__.d(build_namespaceObject, {
|
|
53
|
+
handler: ()=>build_handler,
|
|
54
|
+
schema: ()=>build_schema
|
|
55
|
+
});
|
|
56
|
+
var dev_namespaceObject = {};
|
|
57
|
+
__webpack_require__.r(dev_namespaceObject);
|
|
58
|
+
__webpack_require__.d(dev_namespaceObject, {
|
|
59
|
+
handler: ()=>dev_handler,
|
|
60
|
+
schema: ()=>dev_schema
|
|
61
|
+
});
|
|
62
|
+
var start_namespaceObject = {};
|
|
63
|
+
__webpack_require__.r(start_namespaceObject);
|
|
64
|
+
__webpack_require__.d(start_namespaceObject, {
|
|
65
|
+
handler: ()=>start_handler,
|
|
66
|
+
schema: ()=>start_schema
|
|
67
|
+
});
|
|
68
|
+
var preview_namespaceObject = {};
|
|
69
|
+
__webpack_require__.r(preview_namespaceObject);
|
|
70
|
+
__webpack_require__.d(preview_namespaceObject, {
|
|
71
|
+
handler: ()=>preview_handler,
|
|
72
|
+
schema: ()=>preview_schema
|
|
73
|
+
});
|
|
74
|
+
var get_template_source_namespaceObject = {};
|
|
75
|
+
__webpack_require__.r(get_template_source_namespaceObject);
|
|
76
|
+
__webpack_require__.d(get_template_source_namespaceObject, {
|
|
77
|
+
handler: ()=>get_template_source_handler,
|
|
78
|
+
schema: ()=>get_template_source_schema
|
|
79
|
+
});
|
|
80
|
+
var manifest_validate_namespaceObject = {};
|
|
81
|
+
__webpack_require__.r(manifest_validate_namespaceObject);
|
|
82
|
+
__webpack_require__.d(manifest_validate_namespaceObject, {
|
|
83
|
+
handler: ()=>manifest_validate_handler,
|
|
84
|
+
schema: ()=>manifest_validate_schema
|
|
85
|
+
});
|
|
86
|
+
var inspect_namespaceObject = {};
|
|
87
|
+
__webpack_require__.r(inspect_namespaceObject);
|
|
88
|
+
__webpack_require__.d(inspect_namespaceObject, {
|
|
89
|
+
handler: ()=>inspect_handler,
|
|
90
|
+
schema: ()=>inspect_schema
|
|
91
|
+
});
|
|
92
|
+
var source_inspect_namespaceObject = {};
|
|
93
|
+
__webpack_require__.r(source_inspect_namespaceObject);
|
|
94
|
+
__webpack_require__.d(source_inspect_namespaceObject, {
|
|
95
|
+
handler: ()=>source_inspect_handler,
|
|
96
|
+
schema: ()=>source_inspect_schema
|
|
97
|
+
});
|
|
98
|
+
var logs_namespaceObject = {};
|
|
99
|
+
__webpack_require__.r(logs_namespaceObject);
|
|
100
|
+
__webpack_require__.d(logs_namespaceObject, {
|
|
101
|
+
handler: ()=>logs_handler,
|
|
102
|
+
schema: ()=>logs_schema
|
|
103
|
+
});
|
|
104
|
+
var eval_namespaceObject = {};
|
|
105
|
+
__webpack_require__.r(eval_namespaceObject);
|
|
106
|
+
__webpack_require__.d(eval_namespaceObject, {
|
|
107
|
+
handler: ()=>eval_handler,
|
|
108
|
+
schema: ()=>eval_schema
|
|
109
|
+
});
|
|
110
|
+
var storage_namespaceObject = {};
|
|
111
|
+
__webpack_require__.r(storage_namespaceObject);
|
|
112
|
+
__webpack_require__.d(storage_namespaceObject, {
|
|
113
|
+
handler: ()=>storage_handler,
|
|
114
|
+
schema: ()=>storage_schema
|
|
115
|
+
});
|
|
116
|
+
var reload_namespaceObject = {};
|
|
117
|
+
__webpack_require__.r(reload_namespaceObject);
|
|
118
|
+
__webpack_require__.d(reload_namespaceObject, {
|
|
119
|
+
handler: ()=>reload_handler,
|
|
120
|
+
schema: ()=>reload_schema
|
|
121
|
+
});
|
|
122
|
+
var open_namespaceObject = {};
|
|
123
|
+
__webpack_require__.r(open_namespaceObject);
|
|
124
|
+
__webpack_require__.d(open_namespaceObject, {
|
|
125
|
+
handler: ()=>open_handler,
|
|
126
|
+
schema: ()=>open_schema
|
|
127
|
+
});
|
|
128
|
+
var dom_inspect_namespaceObject = {};
|
|
129
|
+
__webpack_require__.r(dom_inspect_namespaceObject);
|
|
130
|
+
__webpack_require__.d(dom_inspect_namespaceObject, {
|
|
131
|
+
handler: ()=>dom_inspect_handler,
|
|
132
|
+
schema: ()=>dom_inspect_schema
|
|
133
|
+
});
|
|
134
|
+
var publish_namespaceObject = {};
|
|
135
|
+
__webpack_require__.r(publish_namespaceObject);
|
|
136
|
+
__webpack_require__.d(publish_namespaceObject, {
|
|
137
|
+
handler: ()=>publish_handler,
|
|
138
|
+
schema: ()=>publish_schema
|
|
139
|
+
});
|
|
140
|
+
var wait_namespaceObject = {};
|
|
141
|
+
__webpack_require__.r(wait_namespaceObject);
|
|
142
|
+
__webpack_require__.d(wait_namespaceObject, {
|
|
143
|
+
handler: ()=>wait_handler,
|
|
144
|
+
schema: ()=>wait_schema
|
|
145
|
+
});
|
|
146
|
+
var add_feature_namespaceObject = {};
|
|
147
|
+
__webpack_require__.r(add_feature_namespaceObject);
|
|
148
|
+
__webpack_require__.d(add_feature_namespaceObject, {
|
|
149
|
+
handler: ()=>add_feature_handler,
|
|
150
|
+
schema: ()=>add_feature_schema
|
|
151
|
+
});
|
|
152
|
+
var install_browser_namespaceObject = {};
|
|
153
|
+
__webpack_require__.r(install_browser_namespaceObject);
|
|
154
|
+
__webpack_require__.d(install_browser_namespaceObject, {
|
|
155
|
+
handler: ()=>install_browser_handler,
|
|
156
|
+
schema: ()=>install_browser_schema
|
|
157
|
+
});
|
|
158
|
+
var list_browsers_namespaceObject = {};
|
|
159
|
+
__webpack_require__.r(list_browsers_namespaceObject);
|
|
160
|
+
__webpack_require__.d(list_browsers_namespaceObject, {
|
|
161
|
+
handler: ()=>list_browsers_handler,
|
|
162
|
+
schema: ()=>list_browsers_schema
|
|
163
|
+
});
|
|
164
|
+
var detect_browsers_namespaceObject = {};
|
|
165
|
+
__webpack_require__.r(detect_browsers_namespaceObject);
|
|
166
|
+
__webpack_require__.d(detect_browsers_namespaceObject, {
|
|
167
|
+
handler: ()=>detect_browsers_handler,
|
|
168
|
+
schema: ()=>detect_browsers_schema
|
|
169
|
+
});
|
|
170
|
+
const index_js_namespaceObject = require("@modelcontextprotocol/sdk/server/index.js");
|
|
171
|
+
const stdio_js_namespaceObject = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
172
|
+
const types_js_namespaceObject = require("@modelcontextprotocol/sdk/types.js");
|
|
173
|
+
const external_extension_create_namespaceObject = require("extension-create");
|
|
174
|
+
const schema = {
|
|
175
|
+
name: "extension_create",
|
|
176
|
+
description: "Create a new browser extension project from a template in the extension.dev template catalog. Use extension_list_templates to see available options.",
|
|
177
|
+
inputSchema: {
|
|
178
|
+
type: "object",
|
|
179
|
+
properties: {
|
|
180
|
+
projectName: {
|
|
181
|
+
type: "string",
|
|
182
|
+
description: "Name of the extension project (used as directory name)"
|
|
183
|
+
},
|
|
184
|
+
template: {
|
|
185
|
+
type: "string",
|
|
186
|
+
default: "typescript",
|
|
187
|
+
description: "Template slug from the extension.dev template catalog (e.g. 'react', 'sidebar-claude', 'content-vue'). Use extension_list_templates to discover options."
|
|
188
|
+
},
|
|
189
|
+
install: {
|
|
190
|
+
type: "boolean",
|
|
191
|
+
default: true,
|
|
192
|
+
description: "Install dependencies after creation"
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
required: [
|
|
196
|
+
"projectName"
|
|
197
|
+
]
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
async function create_handler(args) {
|
|
201
|
+
const start = Date.now();
|
|
202
|
+
try {
|
|
203
|
+
const result = await (0, external_extension_create_namespaceObject.extensionCreate)(args.projectName, {
|
|
204
|
+
template: args.template ?? "typescript",
|
|
205
|
+
install: args.install ?? true,
|
|
206
|
+
logger: {
|
|
207
|
+
log: ()=>{},
|
|
208
|
+
error: ()=>{}
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
return JSON.stringify({
|
|
212
|
+
projectPath: result.projectPath,
|
|
213
|
+
projectName: result.projectName,
|
|
214
|
+
template: result.template,
|
|
215
|
+
depsInstalled: result.depsInstalled,
|
|
216
|
+
duration: Date.now() - start,
|
|
217
|
+
nextSteps: [
|
|
218
|
+
`cd ${result.projectName}`,
|
|
219
|
+
"npm run dev"
|
|
220
|
+
]
|
|
221
|
+
});
|
|
222
|
+
} catch (err) {
|
|
223
|
+
return JSON.stringify({
|
|
224
|
+
error: err instanceof Error ? err.message : String(err),
|
|
225
|
+
duration: Date.now() - start
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const external_node_fs_namespaceObject = require("node:fs");
|
|
230
|
+
var external_node_fs_default = /*#__PURE__*/ __webpack_require__.n(external_node_fs_namespaceObject);
|
|
231
|
+
const external_node_path_namespaceObject = require("node:path");
|
|
232
|
+
var external_node_path_default = /*#__PURE__*/ __webpack_require__.n(external_node_path_namespaceObject);
|
|
233
|
+
const external_node_os_namespaceObject = require("node:os");
|
|
234
|
+
var external_node_os_default = /*#__PURE__*/ __webpack_require__.n(external_node_os_namespaceObject);
|
|
235
|
+
const CACHE_DIR = external_node_path_default().join(external_node_os_default().homedir(), ".cache", "extension-js");
|
|
236
|
+
const CACHE_FILE = external_node_path_default().join(CACHE_DIR, "templates-meta.json");
|
|
237
|
+
const CACHE_TTL_MS = 3600000;
|
|
238
|
+
const TEMPLATES_META_URL = "https://github.com/extension-js/examples/releases/download/nightly/templates-meta.json";
|
|
239
|
+
function isCacheValid() {
|
|
240
|
+
try {
|
|
241
|
+
const stat = external_node_fs_default().statSync(CACHE_FILE);
|
|
242
|
+
return Date.now() - stat.mtimeMs < CACHE_TTL_MS;
|
|
243
|
+
} catch {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async function fetchTemplatesMeta() {
|
|
248
|
+
if (isCacheValid()) {
|
|
249
|
+
const cached = JSON.parse(external_node_fs_default().readFileSync(CACHE_FILE, "utf8"));
|
|
250
|
+
return cached;
|
|
251
|
+
}
|
|
252
|
+
const response = await fetch(TEMPLATES_META_URL);
|
|
253
|
+
if (!response.ok) {
|
|
254
|
+
if (external_node_fs_default().existsSync(CACHE_FILE)) return JSON.parse(external_node_fs_default().readFileSync(CACHE_FILE, "utf8"));
|
|
255
|
+
throw new Error(`Failed to fetch templates-meta.json: ${response.status}`);
|
|
256
|
+
}
|
|
257
|
+
const data = await response.json();
|
|
258
|
+
external_node_fs_default().mkdirSync(CACHE_DIR, {
|
|
259
|
+
recursive: true
|
|
260
|
+
});
|
|
261
|
+
external_node_fs_default().writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2));
|
|
262
|
+
return data;
|
|
263
|
+
}
|
|
264
|
+
async function listTemplates(filters) {
|
|
265
|
+
const meta = await fetchTemplatesMeta();
|
|
266
|
+
let templates = meta.templates;
|
|
267
|
+
if (filters?.surface) templates = templates.filter((t)=>t.surfaces.includes(filters.surface));
|
|
268
|
+
if (filters?.framework !== void 0) templates = templates.filter((t)=>t.uiFramework === filters.framework);
|
|
269
|
+
if (filters?.tags?.length) templates = templates.filter((t)=>filters.tags.some((tag)=>t.tags?.includes(tag) || t.aiRecommendFor?.includes(tag)));
|
|
270
|
+
if (filters?.featured) templates = templates.filter((t)=>t.featured);
|
|
271
|
+
if (filters?.query) {
|
|
272
|
+
const q = filters.query.toLowerCase();
|
|
273
|
+
templates = templates.filter((t)=>t.slug.includes(q) || t.description.toLowerCase().includes(q) || t.tags?.some((tag)=>tag.includes(q)) || t.useCases?.some((uc)=>uc.toLowerCase().includes(q)) || t.aiPromptExamples?.some((ex)=>ex.toLowerCase().includes(q)));
|
|
274
|
+
}
|
|
275
|
+
return templates;
|
|
276
|
+
}
|
|
277
|
+
async function getTemplateBySlug(slug) {
|
|
278
|
+
const meta = await fetchTemplatesMeta();
|
|
279
|
+
return meta.templates.find((t)=>t.slug === slug);
|
|
280
|
+
}
|
|
281
|
+
const list_templates_schema = {
|
|
282
|
+
name: "extension_list_templates",
|
|
283
|
+
description: "List available extension templates from the extension.dev template catalog. Filter by surface, framework, or tags. Returns structured metadata from templates-meta.json.",
|
|
284
|
+
inputSchema: {
|
|
285
|
+
type: "object",
|
|
286
|
+
properties: {
|
|
287
|
+
surface: {
|
|
288
|
+
type: "string",
|
|
289
|
+
enum: [
|
|
290
|
+
"content",
|
|
291
|
+
"sidebar",
|
|
292
|
+
"action",
|
|
293
|
+
"newtab",
|
|
294
|
+
"devtools",
|
|
295
|
+
"options",
|
|
296
|
+
"background"
|
|
297
|
+
],
|
|
298
|
+
description: "Filter by extension surface type"
|
|
299
|
+
},
|
|
300
|
+
framework: {
|
|
301
|
+
type: "string",
|
|
302
|
+
enum: [
|
|
303
|
+
"react",
|
|
304
|
+
"vue",
|
|
305
|
+
"svelte",
|
|
306
|
+
"preact",
|
|
307
|
+
""
|
|
308
|
+
],
|
|
309
|
+
description: "Filter by UI framework (empty string = vanilla JS)"
|
|
310
|
+
},
|
|
311
|
+
tags: {
|
|
312
|
+
type: "array",
|
|
313
|
+
items: {
|
|
314
|
+
type: "string"
|
|
315
|
+
},
|
|
316
|
+
description: "Filter by tags (e.g. ['ai', 'chat'])"
|
|
317
|
+
},
|
|
318
|
+
featured: {
|
|
319
|
+
type: "boolean",
|
|
320
|
+
description: "Only show featured templates"
|
|
321
|
+
},
|
|
322
|
+
query: {
|
|
323
|
+
type: "string",
|
|
324
|
+
description: "Free-text search across slug, description, tags, and useCases"
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
async function list_templates_handler(args) {
|
|
330
|
+
const templates = await listTemplates(args);
|
|
331
|
+
const results = templates.map((t)=>({
|
|
332
|
+
slug: t.slug,
|
|
333
|
+
description: t.description,
|
|
334
|
+
uiFramework: t.uiFramework || "vanilla",
|
|
335
|
+
surfaces: t.surfaces,
|
|
336
|
+
tags: t.tags,
|
|
337
|
+
difficulty: t.difficulty,
|
|
338
|
+
featured: t.featured,
|
|
339
|
+
useCases: t.useCases,
|
|
340
|
+
repositoryUrl: t.repositoryUrl,
|
|
341
|
+
downloads: t.downloads
|
|
342
|
+
}));
|
|
343
|
+
return JSON.stringify({
|
|
344
|
+
count: results.length,
|
|
345
|
+
templates: results
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
const external_extension_develop_namespaceObject = require("extension-develop");
|
|
349
|
+
const build_schema = {
|
|
350
|
+
name: "extension_build",
|
|
351
|
+
description: "Build a browser extension for production. Outputs to dist/<browser>/. Optionally creates .zip for store submission.",
|
|
352
|
+
inputSchema: {
|
|
353
|
+
type: "object",
|
|
354
|
+
properties: {
|
|
355
|
+
projectPath: {
|
|
356
|
+
type: "string",
|
|
357
|
+
description: "Path to the extension project root"
|
|
358
|
+
},
|
|
359
|
+
browser: {
|
|
360
|
+
type: "string",
|
|
361
|
+
enum: [
|
|
362
|
+
"chrome",
|
|
363
|
+
"edge",
|
|
364
|
+
"firefox",
|
|
365
|
+
"chromium-based",
|
|
366
|
+
"gecko-based"
|
|
367
|
+
],
|
|
368
|
+
default: "chrome",
|
|
369
|
+
description: "Target browser"
|
|
370
|
+
},
|
|
371
|
+
zip: {
|
|
372
|
+
type: "boolean",
|
|
373
|
+
default: false,
|
|
374
|
+
description: "Create a .zip file for store distribution"
|
|
375
|
+
},
|
|
376
|
+
zipSource: {
|
|
377
|
+
type: "boolean",
|
|
378
|
+
default: false,
|
|
379
|
+
description: "Include source code zip (required by some stores)"
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
required: [
|
|
383
|
+
"projectPath"
|
|
384
|
+
]
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
async function build_handler(args) {
|
|
388
|
+
const start = Date.now();
|
|
389
|
+
try {
|
|
390
|
+
const summary = await (0, external_extension_develop_namespaceObject.extensionBuild)(args.projectPath, {
|
|
391
|
+
browser: args.browser ?? "chrome",
|
|
392
|
+
zip: args.zip ?? false,
|
|
393
|
+
zipSource: args.zipSource ?? false
|
|
394
|
+
});
|
|
395
|
+
return JSON.stringify({
|
|
396
|
+
success: true,
|
|
397
|
+
...summary,
|
|
398
|
+
duration: Date.now() - start
|
|
399
|
+
});
|
|
400
|
+
} catch (err) {
|
|
401
|
+
return JSON.stringify({
|
|
402
|
+
error: err instanceof Error ? err.message : String(err),
|
|
403
|
+
duration: Date.now() - start,
|
|
404
|
+
hint: "Check that the project has a valid src/manifest.json and all dependencies are installed."
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const external_node_child_process_namespaceObject = require("node:child_process");
|
|
409
|
+
function runExtensionCli(args, options) {
|
|
410
|
+
return new Promise((resolve)=>{
|
|
411
|
+
const child = (0, external_node_child_process_namespaceObject.spawn)("npx", [
|
|
412
|
+
"extension",
|
|
413
|
+
...args
|
|
414
|
+
], {
|
|
415
|
+
cwd: options?.cwd,
|
|
416
|
+
stdio: [
|
|
417
|
+
"ignore",
|
|
418
|
+
"pipe",
|
|
419
|
+
"pipe"
|
|
420
|
+
],
|
|
421
|
+
env: {
|
|
422
|
+
...process.env,
|
|
423
|
+
FORCE_COLOR: "0",
|
|
424
|
+
NO_COLOR: "1"
|
|
425
|
+
},
|
|
426
|
+
shell: true,
|
|
427
|
+
timeout: options?.timeoutMs ?? 30000
|
|
428
|
+
});
|
|
429
|
+
let stdout = "";
|
|
430
|
+
let stderr = "";
|
|
431
|
+
child.stdout?.on("data", (d)=>stdout += d.toString());
|
|
432
|
+
child.stderr?.on("data", (d)=>stderr += d.toString());
|
|
433
|
+
child.on("close", (code)=>resolve({
|
|
434
|
+
code,
|
|
435
|
+
stdout,
|
|
436
|
+
stderr
|
|
437
|
+
}));
|
|
438
|
+
child.on("error", (err)=>resolve({
|
|
439
|
+
code: 1,
|
|
440
|
+
stdout,
|
|
441
|
+
stderr: stderr || String(err)
|
|
442
|
+
}));
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
function spawnExtensionCli(args, options) {
|
|
446
|
+
const child = (0, external_node_child_process_namespaceObject.spawn)("npx", [
|
|
447
|
+
"extension",
|
|
448
|
+
...args
|
|
449
|
+
], {
|
|
450
|
+
cwd: options?.cwd,
|
|
451
|
+
detached: true,
|
|
452
|
+
stdio: [
|
|
453
|
+
"ignore",
|
|
454
|
+
"pipe",
|
|
455
|
+
"pipe"
|
|
456
|
+
],
|
|
457
|
+
env: {
|
|
458
|
+
...process.env,
|
|
459
|
+
FORCE_COLOR: "0",
|
|
460
|
+
NO_COLOR: "1"
|
|
461
|
+
},
|
|
462
|
+
shell: true
|
|
463
|
+
});
|
|
464
|
+
child.unref();
|
|
465
|
+
return child;
|
|
466
|
+
}
|
|
467
|
+
const sessions = new Map();
|
|
468
|
+
function sessionKey(projectPath, browser) {
|
|
469
|
+
return `${projectPath}::${browser}`;
|
|
470
|
+
}
|
|
471
|
+
function registerSession(info) {
|
|
472
|
+
sessions.set(sessionKey(info.projectPath, info.browser), info);
|
|
473
|
+
}
|
|
474
|
+
const dev_schema = {
|
|
475
|
+
name: "extension_dev",
|
|
476
|
+
description: "Start the extension development server with hot module replacement. Launches a browser with the extension loaded. Returns process info for use with extension_wait and extension_source_inspect.",
|
|
477
|
+
inputSchema: {
|
|
478
|
+
type: "object",
|
|
479
|
+
properties: {
|
|
480
|
+
projectPath: {
|
|
481
|
+
type: "string",
|
|
482
|
+
description: "Path to the extension project root"
|
|
483
|
+
},
|
|
484
|
+
browser: {
|
|
485
|
+
type: "string",
|
|
486
|
+
enum: [
|
|
487
|
+
"chrome",
|
|
488
|
+
"edge",
|
|
489
|
+
"firefox",
|
|
490
|
+
"chromium-based",
|
|
491
|
+
"gecko-based"
|
|
492
|
+
],
|
|
493
|
+
default: "chrome"
|
|
494
|
+
},
|
|
495
|
+
port: {
|
|
496
|
+
type: "number",
|
|
497
|
+
description: "Dev server port (0 for auto-assign)"
|
|
498
|
+
},
|
|
499
|
+
noBrowser: {
|
|
500
|
+
type: "boolean",
|
|
501
|
+
default: false,
|
|
502
|
+
description: "Start dev server without launching browser"
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
required: [
|
|
506
|
+
"projectPath"
|
|
507
|
+
]
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
async function dev_handler(args) {
|
|
511
|
+
const browser = args.browser ?? "chrome";
|
|
512
|
+
const cliArgs = [
|
|
513
|
+
"dev",
|
|
514
|
+
args.projectPath,
|
|
515
|
+
"--browser",
|
|
516
|
+
browser
|
|
517
|
+
];
|
|
518
|
+
if (void 0 !== args.port) cliArgs.push("--port", String(args.port));
|
|
519
|
+
if (args.noBrowser) cliArgs.push("--no-browser");
|
|
520
|
+
const child = spawnExtensionCli(cliArgs);
|
|
521
|
+
const pid = child.pid;
|
|
522
|
+
registerSession({
|
|
523
|
+
pid,
|
|
524
|
+
browser,
|
|
525
|
+
port: args.port,
|
|
526
|
+
projectPath: args.projectPath,
|
|
527
|
+
command: "dev"
|
|
528
|
+
});
|
|
529
|
+
let earlyOutput = "";
|
|
530
|
+
const collector = (data)=>{
|
|
531
|
+
earlyOutput += data.toString();
|
|
532
|
+
};
|
|
533
|
+
child.stdout?.on("data", collector);
|
|
534
|
+
child.stderr?.on("data", collector);
|
|
535
|
+
await new Promise((resolve)=>setTimeout(resolve, 3000));
|
|
536
|
+
child.stdout?.off("data", collector);
|
|
537
|
+
child.stderr?.off("data", collector);
|
|
538
|
+
return JSON.stringify({
|
|
539
|
+
pid,
|
|
540
|
+
browser,
|
|
541
|
+
port: args.port ?? 8080,
|
|
542
|
+
projectPath: args.projectPath,
|
|
543
|
+
status: "started",
|
|
544
|
+
hint: "Use extension_wait to check when the extension is fully loaded, then extension_source_inspect to inspect the live state.",
|
|
545
|
+
earlyOutput: earlyOutput.slice(0, 500)
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
const start_schema = {
|
|
549
|
+
name: "extension_start",
|
|
550
|
+
description: "Build the extension for production and immediately preview it in a browser. Combines build + preview in one step. No hot reload.",
|
|
551
|
+
inputSchema: {
|
|
552
|
+
type: "object",
|
|
553
|
+
properties: {
|
|
554
|
+
projectPath: {
|
|
555
|
+
type: "string",
|
|
556
|
+
description: "Path to the extension project root"
|
|
557
|
+
},
|
|
558
|
+
browser: {
|
|
559
|
+
type: "string",
|
|
560
|
+
enum: [
|
|
561
|
+
"chrome",
|
|
562
|
+
"edge",
|
|
563
|
+
"firefox",
|
|
564
|
+
"chromium-based",
|
|
565
|
+
"gecko-based"
|
|
566
|
+
],
|
|
567
|
+
default: "chrome"
|
|
568
|
+
},
|
|
569
|
+
polyfill: {
|
|
570
|
+
type: "boolean",
|
|
571
|
+
default: true,
|
|
572
|
+
description: "Apply cross-browser polyfill"
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
required: [
|
|
576
|
+
"projectPath"
|
|
577
|
+
]
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
async function start_handler(args) {
|
|
581
|
+
const browser = args.browser ?? "chrome";
|
|
582
|
+
const cliArgs = [
|
|
583
|
+
"start",
|
|
584
|
+
args.projectPath,
|
|
585
|
+
"--browser",
|
|
586
|
+
browser
|
|
587
|
+
];
|
|
588
|
+
if (false === args.polyfill) cliArgs.push("--no-polyfill");
|
|
589
|
+
const child = spawnExtensionCli(cliArgs);
|
|
590
|
+
const pid = child.pid;
|
|
591
|
+
registerSession({
|
|
592
|
+
pid,
|
|
593
|
+
browser,
|
|
594
|
+
projectPath: args.projectPath,
|
|
595
|
+
command: "start"
|
|
596
|
+
});
|
|
597
|
+
let earlyOutput = "";
|
|
598
|
+
const collector = (data)=>{
|
|
599
|
+
earlyOutput += data.toString();
|
|
600
|
+
};
|
|
601
|
+
child.stdout?.on("data", collector);
|
|
602
|
+
child.stderr?.on("data", collector);
|
|
603
|
+
await new Promise((resolve)=>setTimeout(resolve, 5000));
|
|
604
|
+
child.stdout?.off("data", collector);
|
|
605
|
+
child.stderr?.off("data", collector);
|
|
606
|
+
return JSON.stringify({
|
|
607
|
+
pid,
|
|
608
|
+
browser,
|
|
609
|
+
projectPath: args.projectPath,
|
|
610
|
+
status: "started",
|
|
611
|
+
hint: "Use extension_wait to check when the build and browser launch are complete.",
|
|
612
|
+
earlyOutput: earlyOutput.slice(0, 500)
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
const preview_schema = {
|
|
616
|
+
name: "extension_preview",
|
|
617
|
+
description: "Preview a production-built extension in a browser. Uses dist/ output directly. The extension must be built first with extension_build.",
|
|
618
|
+
inputSchema: {
|
|
619
|
+
type: "object",
|
|
620
|
+
properties: {
|
|
621
|
+
projectPath: {
|
|
622
|
+
type: "string",
|
|
623
|
+
description: "Path to the extension project root"
|
|
624
|
+
},
|
|
625
|
+
browser: {
|
|
626
|
+
type: "string",
|
|
627
|
+
enum: [
|
|
628
|
+
"chrome",
|
|
629
|
+
"edge",
|
|
630
|
+
"firefox",
|
|
631
|
+
"chromium-based",
|
|
632
|
+
"gecko-based"
|
|
633
|
+
],
|
|
634
|
+
default: "chrome"
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
required: [
|
|
638
|
+
"projectPath"
|
|
639
|
+
]
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
async function preview_handler(args) {
|
|
643
|
+
const browser = args.browser ?? "chrome";
|
|
644
|
+
const cliArgs = [
|
|
645
|
+
"preview",
|
|
646
|
+
args.projectPath,
|
|
647
|
+
"--browser",
|
|
648
|
+
browser
|
|
649
|
+
];
|
|
650
|
+
const child = spawnExtensionCli(cliArgs);
|
|
651
|
+
return JSON.stringify({
|
|
652
|
+
pid: child.pid,
|
|
653
|
+
browser,
|
|
654
|
+
projectPath: args.projectPath,
|
|
655
|
+
status: "launched"
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
const RAW_BASE = "https://raw.githubusercontent.com/extension-js/examples/main/examples";
|
|
659
|
+
const get_template_source_schema = {
|
|
660
|
+
name: "extension_get_template_source",
|
|
661
|
+
description: "Read source files from a template in the extension.dev template catalog. Use this to learn implementation patterns before building something similar.",
|
|
662
|
+
inputSchema: {
|
|
663
|
+
type: "object",
|
|
664
|
+
properties: {
|
|
665
|
+
slug: {
|
|
666
|
+
type: "string",
|
|
667
|
+
description: "Template slug (e.g. 'sidebar-claude', 'content-react')"
|
|
668
|
+
},
|
|
669
|
+
files: {
|
|
670
|
+
type: "array",
|
|
671
|
+
items: {
|
|
672
|
+
type: "string"
|
|
673
|
+
},
|
|
674
|
+
description: "Specific files to read (e.g. ['src/manifest.json', 'src/background.ts']). If omitted, returns the file listing from templates-meta.json."
|
|
675
|
+
}
|
|
676
|
+
},
|
|
677
|
+
required: [
|
|
678
|
+
"slug"
|
|
679
|
+
]
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
async function get_template_source_handler(args) {
|
|
683
|
+
const template = await getTemplateBySlug(args.slug);
|
|
684
|
+
if (!template) return JSON.stringify({
|
|
685
|
+
error: `Template '${args.slug}' not found in the catalog`,
|
|
686
|
+
hint: "Use extension_list_templates to see available templates."
|
|
687
|
+
});
|
|
688
|
+
const meta = {
|
|
689
|
+
slug: template.slug,
|
|
690
|
+
description: template.description,
|
|
691
|
+
uiFramework: template.uiFramework || "vanilla",
|
|
692
|
+
surfaces: template.surfaces,
|
|
693
|
+
permissions: template.permissions,
|
|
694
|
+
patternExplanation: template.patternExplanation,
|
|
695
|
+
keyFiles: template.keyFiles,
|
|
696
|
+
repositoryUrl: template.repositoryUrl
|
|
697
|
+
};
|
|
698
|
+
if (!args.files?.length) return JSON.stringify({
|
|
699
|
+
...meta,
|
|
700
|
+
files: template.files,
|
|
701
|
+
hint: "Pass specific file paths in the files parameter to read their contents."
|
|
702
|
+
});
|
|
703
|
+
const fileContents = {};
|
|
704
|
+
const errors = [];
|
|
705
|
+
await Promise.all(args.files.map(async (filePath)=>{
|
|
706
|
+
const url = `${RAW_BASE}/${args.slug}/${filePath}`;
|
|
707
|
+
try {
|
|
708
|
+
const response = await fetch(url);
|
|
709
|
+
if (response.ok) fileContents[filePath] = await response.text();
|
|
710
|
+
else errors.push(`${filePath}: ${response.status}`);
|
|
711
|
+
} catch (err) {
|
|
712
|
+
errors.push(`${filePath}: ${err instanceof Error ? err.message : "fetch failed"}`);
|
|
713
|
+
}
|
|
714
|
+
}));
|
|
715
|
+
return JSON.stringify({
|
|
716
|
+
...meta,
|
|
717
|
+
fileContents,
|
|
718
|
+
...errors.length ? {
|
|
719
|
+
errors
|
|
720
|
+
} : {}
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
const manifest_validate_schema = {
|
|
724
|
+
name: "extension_manifest_validate",
|
|
725
|
+
description: "Validate a manifest.json file for correctness across browsers. Reports missing fields, invalid permissions, and cross-browser compatibility issues.",
|
|
726
|
+
inputSchema: {
|
|
727
|
+
type: "object",
|
|
728
|
+
properties: {
|
|
729
|
+
manifestPath: {
|
|
730
|
+
type: "string",
|
|
731
|
+
description: "Path to manifest.json"
|
|
732
|
+
},
|
|
733
|
+
browsers: {
|
|
734
|
+
type: "array",
|
|
735
|
+
items: {
|
|
736
|
+
type: "string"
|
|
737
|
+
},
|
|
738
|
+
default: [
|
|
739
|
+
"chrome",
|
|
740
|
+
"firefox"
|
|
741
|
+
],
|
|
742
|
+
description: "Browsers to validate against"
|
|
743
|
+
}
|
|
744
|
+
},
|
|
745
|
+
required: [
|
|
746
|
+
"manifestPath"
|
|
747
|
+
]
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
async function manifest_validate_handler(args) {
|
|
751
|
+
const browsers = args.browsers ?? [
|
|
752
|
+
"chrome",
|
|
753
|
+
"firefox"
|
|
754
|
+
];
|
|
755
|
+
const result = {
|
|
756
|
+
valid: true,
|
|
757
|
+
errors: [],
|
|
758
|
+
warnings: [],
|
|
759
|
+
browserSupport: {},
|
|
760
|
+
similarTemplates: []
|
|
761
|
+
};
|
|
762
|
+
let manifest;
|
|
763
|
+
try {
|
|
764
|
+
const raw = external_node_fs_default().readFileSync(external_node_path_default().resolve(args.manifestPath), "utf8");
|
|
765
|
+
manifest = JSON.parse(raw);
|
|
766
|
+
} catch (err) {
|
|
767
|
+
return JSON.stringify({
|
|
768
|
+
valid: false,
|
|
769
|
+
errors: [
|
|
770
|
+
`Cannot read manifest: ${err instanceof Error ? err.message : err}`
|
|
771
|
+
],
|
|
772
|
+
warnings: [],
|
|
773
|
+
browserSupport: {},
|
|
774
|
+
similarTemplates: []
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
if (!manifest.name) result.errors.push("Missing required field: name");
|
|
778
|
+
if (!manifest.version) result.warnings.push("Missing field: version (required for store submission)");
|
|
779
|
+
const chromiumMv = manifest["chromium:manifest_version"] ?? manifest.manifest_version;
|
|
780
|
+
manifest["firefox:manifest_version"] ?? manifest.manifest_version;
|
|
781
|
+
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.');
|
|
782
|
+
for (const browser of browsers){
|
|
783
|
+
const isChromium = [
|
|
784
|
+
"chrome",
|
|
785
|
+
"edge",
|
|
786
|
+
"chromium-based"
|
|
787
|
+
].includes(browser);
|
|
788
|
+
const isFirefox = [
|
|
789
|
+
"firefox",
|
|
790
|
+
"gecko-based"
|
|
791
|
+
].includes(browser);
|
|
792
|
+
const issues = [];
|
|
793
|
+
if (isChromium) {
|
|
794
|
+
const mv = chromiumMv;
|
|
795
|
+
if (mv && mv < 3) issues.push("Manifest V2 is deprecated on Chromium. Use chromium:manifest_version: 3.");
|
|
796
|
+
if (manifest["chromium:side_panel"] || manifest.side_panel) {
|
|
797
|
+
const perms = manifest["chromium:permissions"] ?? manifest.permissions ?? [];
|
|
798
|
+
if (!perms.includes("sidePanel")) issues.push('Side panel declared but "sidePanel" permission is missing.');
|
|
799
|
+
}
|
|
800
|
+
if (manifest["firefox:browser_action"] && !manifest["chromium:action"] && !manifest.action) issues.push('Firefox browser_action found but no chromium:action. Chromium MV3 uses "action" instead of "browser_action".');
|
|
801
|
+
}
|
|
802
|
+
if (isFirefox) {
|
|
803
|
+
const contentScripts = manifest.content_scripts;
|
|
804
|
+
if (contentScripts) {
|
|
805
|
+
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.');
|
|
806
|
+
}
|
|
807
|
+
if (manifest["chromium:side_panel"] && !manifest["firefox:sidebar_action"]) issues.push("Chromium side_panel declared but no firefox:sidebar_action. Firefox uses sidebar_action for sidebars.");
|
|
808
|
+
const bg = manifest.background;
|
|
809
|
+
if (bg) {
|
|
810
|
+
if (bg.service_worker && !bg["firefox:scripts"] && !bg.scripts) issues.push('Background service_worker declared but no firefox:scripts. Firefox uses "scripts" (array) instead of "service_worker".');
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
result.browserSupport[browser] = {
|
|
814
|
+
supported: 0 === issues.length,
|
|
815
|
+
issues
|
|
816
|
+
};
|
|
817
|
+
if (issues.length) result.valid = false;
|
|
818
|
+
}
|
|
819
|
+
const surfaces = [];
|
|
820
|
+
if (manifest.content_scripts) surfaces.push("content");
|
|
821
|
+
if (manifest["chromium:side_panel"] || manifest.side_panel) surfaces.push("sidebar");
|
|
822
|
+
if (manifest.action || manifest["chromium:action"] || manifest["firefox:browser_action"]) surfaces.push("action");
|
|
823
|
+
if (manifest.chrome_url_overrides?.newtab) surfaces.push("newtab");
|
|
824
|
+
if (manifest.background) surfaces.push("background");
|
|
825
|
+
if (surfaces.length) try {
|
|
826
|
+
const templates = await listTemplates();
|
|
827
|
+
result.similarTemplates = templates.filter((t)=>t.surfaces.some((s)=>surfaces.includes(s))).slice(0, 5).map((t)=>({
|
|
828
|
+
slug: t.slug,
|
|
829
|
+
surfaces: t.surfaces
|
|
830
|
+
}));
|
|
831
|
+
} catch {}
|
|
832
|
+
if (0 === result.errors.length) result.valid = Object.values(result.browserSupport).every((b)=>b.supported);
|
|
833
|
+
return JSON.stringify(result);
|
|
834
|
+
}
|
|
835
|
+
const inspect_schema = {
|
|
836
|
+
name: "extension_inspect",
|
|
837
|
+
description: "Inspect a built extension: file sizes, entry points, permissions used, and structure analysis. The extension must be built first.",
|
|
838
|
+
inputSchema: {
|
|
839
|
+
type: "object",
|
|
840
|
+
properties: {
|
|
841
|
+
projectPath: {
|
|
842
|
+
type: "string",
|
|
843
|
+
description: "Path to the extension project root"
|
|
844
|
+
},
|
|
845
|
+
browser: {
|
|
846
|
+
type: "string",
|
|
847
|
+
default: "chrome",
|
|
848
|
+
description: "Browser build to inspect"
|
|
849
|
+
},
|
|
850
|
+
format: {
|
|
851
|
+
type: "string",
|
|
852
|
+
enum: [
|
|
853
|
+
"summary",
|
|
854
|
+
"tree",
|
|
855
|
+
"json"
|
|
856
|
+
],
|
|
857
|
+
default: "summary"
|
|
858
|
+
}
|
|
859
|
+
},
|
|
860
|
+
required: [
|
|
861
|
+
"projectPath"
|
|
862
|
+
]
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
function walkDir(dir, base = "") {
|
|
866
|
+
const entries = [];
|
|
867
|
+
try {
|
|
868
|
+
for (const entry of external_node_fs_default().readdirSync(dir, {
|
|
869
|
+
withFileTypes: true
|
|
870
|
+
})){
|
|
871
|
+
const rel = base ? `${base}/${entry.name}` : entry.name;
|
|
872
|
+
if (entry.isDirectory()) entries.push(...walkDir(external_node_path_default().join(dir, entry.name), rel));
|
|
873
|
+
else {
|
|
874
|
+
const stat = external_node_fs_default().statSync(external_node_path_default().join(dir, entry.name));
|
|
875
|
+
const ext = external_node_path_default().extname(entry.name).toLowerCase();
|
|
876
|
+
let type = "other";
|
|
877
|
+
if ([
|
|
878
|
+
".js",
|
|
879
|
+
".mjs"
|
|
880
|
+
].includes(ext)) type = "javascript";
|
|
881
|
+
else if ([
|
|
882
|
+
".css"
|
|
883
|
+
].includes(ext)) type = "stylesheet";
|
|
884
|
+
else if ([
|
|
885
|
+
".html",
|
|
886
|
+
".htm"
|
|
887
|
+
].includes(ext)) type = "html";
|
|
888
|
+
else if ([
|
|
889
|
+
".json"
|
|
890
|
+
].includes(ext)) type = "json";
|
|
891
|
+
else if ([
|
|
892
|
+
".png",
|
|
893
|
+
".jpg",
|
|
894
|
+
".svg",
|
|
895
|
+
".gif",
|
|
896
|
+
".ico",
|
|
897
|
+
".webp"
|
|
898
|
+
].includes(ext)) type = "image";
|
|
899
|
+
else if ([
|
|
900
|
+
".woff",
|
|
901
|
+
".woff2",
|
|
902
|
+
".ttf",
|
|
903
|
+
".otf"
|
|
904
|
+
].includes(ext)) type = "font";
|
|
905
|
+
else if ([
|
|
906
|
+
".wasm"
|
|
907
|
+
].includes(ext)) type = "wasm";
|
|
908
|
+
else if ([
|
|
909
|
+
".map"
|
|
910
|
+
].includes(ext)) type = "sourcemap";
|
|
911
|
+
entries.push({
|
|
912
|
+
path: rel,
|
|
913
|
+
size: stat.size,
|
|
914
|
+
type
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
} catch {}
|
|
919
|
+
return entries;
|
|
920
|
+
}
|
|
921
|
+
function formatBytes(bytes) {
|
|
922
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
923
|
+
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
924
|
+
return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
925
|
+
}
|
|
926
|
+
async function inspect_handler(args) {
|
|
927
|
+
const browser = args.browser ?? "chrome";
|
|
928
|
+
const distPath = external_node_path_default().resolve(args.projectPath, "dist", browser);
|
|
929
|
+
if (!external_node_fs_default().existsSync(distPath)) return JSON.stringify({
|
|
930
|
+
error: `Build output not found at ${distPath}. Run extension_build first.`,
|
|
931
|
+
hint: `Use extension_build with browser: "${browser}" to build the extension.`
|
|
932
|
+
});
|
|
933
|
+
const files = walkDir(distPath);
|
|
934
|
+
const totalSize = files.reduce((sum, f)=>sum + f.size, 0);
|
|
935
|
+
let manifest = {};
|
|
936
|
+
const manifestPath = external_node_path_default().join(distPath, "manifest.json");
|
|
937
|
+
try {
|
|
938
|
+
manifest = JSON.parse(external_node_fs_default().readFileSync(manifestPath, "utf8"));
|
|
939
|
+
} catch {}
|
|
940
|
+
const byType = {};
|
|
941
|
+
for (const f of files){
|
|
942
|
+
if (!byType[f.type]) byType[f.type] = {
|
|
943
|
+
count: 0,
|
|
944
|
+
size: 0
|
|
945
|
+
};
|
|
946
|
+
byType[f.type].count++;
|
|
947
|
+
byType[f.type].size += f.size;
|
|
948
|
+
}
|
|
949
|
+
const result = {
|
|
950
|
+
browser,
|
|
951
|
+
distPath,
|
|
952
|
+
totalSize,
|
|
953
|
+
totalSizeFormatted: formatBytes(totalSize),
|
|
954
|
+
fileCount: files.length,
|
|
955
|
+
manifest: {
|
|
956
|
+
name: manifest.name,
|
|
957
|
+
version: manifest.version,
|
|
958
|
+
manifest_version: manifest.manifest_version,
|
|
959
|
+
permissions: manifest.permissions
|
|
960
|
+
},
|
|
961
|
+
byType: Object.fromEntries(Object.entries(byType).map(([type, data])=>[
|
|
962
|
+
type,
|
|
963
|
+
{
|
|
964
|
+
count: data.count,
|
|
965
|
+
size: data.size,
|
|
966
|
+
sizeFormatted: formatBytes(data.size)
|
|
967
|
+
}
|
|
968
|
+
])),
|
|
969
|
+
..."tree" === args.format || "json" === args.format ? {
|
|
970
|
+
files: files.map((f)=>({
|
|
971
|
+
...f,
|
|
972
|
+
sizeFormatted: formatBytes(f.size)
|
|
973
|
+
}))
|
|
974
|
+
} : {
|
|
975
|
+
largestFiles: files.sort((a, b)=>b.size - a.size).slice(0, 10).map((f)=>({
|
|
976
|
+
path: f.path,
|
|
977
|
+
size: f.size,
|
|
978
|
+
sizeFormatted: formatBytes(f.size)
|
|
979
|
+
}))
|
|
980
|
+
},
|
|
981
|
+
storeReadiness: {
|
|
982
|
+
hasManifest: external_node_fs_default().existsSync(manifestPath),
|
|
983
|
+
hasIcons: files.some((f)=>"image" === f.type && f.path.includes("icon")),
|
|
984
|
+
noSourceMaps: !files.some((f)=>"sourcemap" === f.type),
|
|
985
|
+
under10MB: totalSize < 10485760
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
return JSON.stringify(result);
|
|
989
|
+
}
|
|
990
|
+
const external_node_net_namespaceObject = require("node:net");
|
|
991
|
+
var external_node_net_default = /*#__PURE__*/ __webpack_require__.n(external_node_net_namespaceObject);
|
|
992
|
+
const external_ws_namespaceObject = require("ws");
|
|
993
|
+
var external_ws_default = /*#__PURE__*/ __webpack_require__.n(external_ws_namespaceObject);
|
|
994
|
+
function _define_property(obj, key, value) {
|
|
995
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
996
|
+
value: value,
|
|
997
|
+
enumerable: true,
|
|
998
|
+
configurable: true,
|
|
999
|
+
writable: true
|
|
1000
|
+
});
|
|
1001
|
+
else obj[key] = value;
|
|
1002
|
+
return obj;
|
|
1003
|
+
}
|
|
1004
|
+
const COMMAND_TIMEOUT_MS = 15000;
|
|
1005
|
+
class CDPClient {
|
|
1006
|
+
async connect(wsUrl) {
|
|
1007
|
+
return new Promise((resolve, reject)=>{
|
|
1008
|
+
this.ws = new (external_ws_default())(wsUrl);
|
|
1009
|
+
this.ws.on("open", ()=>resolve());
|
|
1010
|
+
this.ws.on("message", (data)=>{
|
|
1011
|
+
this.handleMessage(data.toString());
|
|
1012
|
+
});
|
|
1013
|
+
this.ws.on("error", (err)=>{
|
|
1014
|
+
this.rejectAllPending(err.message);
|
|
1015
|
+
reject(err);
|
|
1016
|
+
});
|
|
1017
|
+
this.ws.on("close", ()=>{
|
|
1018
|
+
this.rejectAllPending("CDP connection closed");
|
|
1019
|
+
});
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
disconnect() {
|
|
1023
|
+
if (this.ws) {
|
|
1024
|
+
try {
|
|
1025
|
+
this.ws.close();
|
|
1026
|
+
} catch {}
|
|
1027
|
+
this.ws = null;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
handleMessage(data) {
|
|
1031
|
+
try {
|
|
1032
|
+
const message = JSON.parse(data);
|
|
1033
|
+
if ("number" == typeof message.id) {
|
|
1034
|
+
const pending = this.pendingRequests.get(message.id);
|
|
1035
|
+
if (pending) {
|
|
1036
|
+
clearTimeout(pending.timeout);
|
|
1037
|
+
this.pendingRequests.delete(message.id);
|
|
1038
|
+
if (message.error) pending.reject(new Error(JSON.stringify(message.error)));
|
|
1039
|
+
else pending.resolve(message.result);
|
|
1040
|
+
}
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
if ("Log.entryAdded" === message.method) {
|
|
1044
|
+
const entry = message.params?.entry;
|
|
1045
|
+
if (entry) this.consoleMessages.push({
|
|
1046
|
+
level: String(entry.level ?? "info"),
|
|
1047
|
+
text: String(entry.text ?? ""),
|
|
1048
|
+
source: String(entry.source ?? "other"),
|
|
1049
|
+
timestamp: Number(entry.timestamp ?? Date.now())
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
if ("Runtime.consoleAPICalled" === message.method) {
|
|
1053
|
+
const params = message.params;
|
|
1054
|
+
if (params) {
|
|
1055
|
+
const args = params.args ?? [];
|
|
1056
|
+
const text = args.map((a)=>String(a.value ?? a.description ?? "")).join(" ");
|
|
1057
|
+
this.consoleMessages.push({
|
|
1058
|
+
level: String(params.type ?? "log"),
|
|
1059
|
+
text,
|
|
1060
|
+
source: "console-api",
|
|
1061
|
+
timestamp: Number(params.timestamp ?? Date.now())
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
for (const listener of this.eventListeners)listener(message);
|
|
1066
|
+
} catch {}
|
|
1067
|
+
}
|
|
1068
|
+
rejectAllPending(reason) {
|
|
1069
|
+
for (const [id, pending] of this.pendingRequests){
|
|
1070
|
+
clearTimeout(pending.timeout);
|
|
1071
|
+
pending.reject(new Error(reason));
|
|
1072
|
+
this.pendingRequests.delete(id);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
async sendCommand(method, params = {}, sessionId) {
|
|
1076
|
+
return new Promise((resolve, reject)=>{
|
|
1077
|
+
if (!this.ws || this.ws.readyState !== external_ws_default().OPEN) return reject(new Error("CDP WebSocket is not connected"));
|
|
1078
|
+
const id = ++this.messageId;
|
|
1079
|
+
const message = {
|
|
1080
|
+
id,
|
|
1081
|
+
method,
|
|
1082
|
+
params
|
|
1083
|
+
};
|
|
1084
|
+
if (sessionId) message.sessionId = sessionId;
|
|
1085
|
+
const timeout = setTimeout(()=>{
|
|
1086
|
+
this.pendingRequests.delete(id);
|
|
1087
|
+
reject(new Error(`CDP command timed out (${COMMAND_TIMEOUT_MS}ms): ${method}`));
|
|
1088
|
+
}, COMMAND_TIMEOUT_MS);
|
|
1089
|
+
this.pendingRequests.set(id, {
|
|
1090
|
+
resolve,
|
|
1091
|
+
reject,
|
|
1092
|
+
timeout
|
|
1093
|
+
});
|
|
1094
|
+
this.ws.send(JSON.stringify(message));
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
static async discoverBrowserWsUrl(port, host = "127.0.0.1") {
|
|
1098
|
+
const res = await fetch(`http://${host}:${port}/json/version`);
|
|
1099
|
+
if (!res.ok) throw new Error(`CDP /json/version failed: ${res.status}`);
|
|
1100
|
+
const data = await res.json();
|
|
1101
|
+
if ("string" == typeof data.webSocketDebuggerUrl) return data.webSocketDebuggerUrl;
|
|
1102
|
+
throw new Error("No webSocketDebuggerUrl in /json/version response");
|
|
1103
|
+
}
|
|
1104
|
+
static async discoverTargets(port, host = "127.0.0.1") {
|
|
1105
|
+
const res = await fetch(`http://${host}:${port}/json`);
|
|
1106
|
+
if (!res.ok) throw new Error(`CDP /json failed: ${res.status}`);
|
|
1107
|
+
return await res.json();
|
|
1108
|
+
}
|
|
1109
|
+
async getTargets() {
|
|
1110
|
+
const response = await this.sendCommand("Target.getTargets");
|
|
1111
|
+
return response?.targetInfos ?? [];
|
|
1112
|
+
}
|
|
1113
|
+
async attachToTarget(targetId) {
|
|
1114
|
+
const response = await this.sendCommand("Target.attachToTarget", {
|
|
1115
|
+
targetId,
|
|
1116
|
+
flatten: true
|
|
1117
|
+
});
|
|
1118
|
+
return response.sessionId ?? "";
|
|
1119
|
+
}
|
|
1120
|
+
async enableDomains(sessionId) {
|
|
1121
|
+
await Promise.all([
|
|
1122
|
+
this.sendCommand("Runtime.enable", {}, sessionId),
|
|
1123
|
+
this.sendCommand("Log.enable", {}, sessionId),
|
|
1124
|
+
this.sendCommand("Page.enable", {}, sessionId)
|
|
1125
|
+
]);
|
|
1126
|
+
}
|
|
1127
|
+
async navigate(sessionId, url) {
|
|
1128
|
+
await this.sendCommand("Page.navigate", {
|
|
1129
|
+
url
|
|
1130
|
+
}, sessionId);
|
|
1131
|
+
await new Promise((resolve)=>{
|
|
1132
|
+
const timeout = setTimeout(resolve, 5000);
|
|
1133
|
+
const unsubscribe = this.onEvent((msg)=>{
|
|
1134
|
+
if ("Page.loadEventFired" === msg.method) {
|
|
1135
|
+
clearTimeout(timeout);
|
|
1136
|
+
unsubscribe();
|
|
1137
|
+
resolve();
|
|
1138
|
+
}
|
|
1139
|
+
});
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
async evaluate(sessionId, expression) {
|
|
1143
|
+
const response = await this.sendCommand("Runtime.evaluate", {
|
|
1144
|
+
expression,
|
|
1145
|
+
returnByValue: true,
|
|
1146
|
+
awaitPromise: false
|
|
1147
|
+
}, sessionId);
|
|
1148
|
+
return response.result?.value;
|
|
1149
|
+
}
|
|
1150
|
+
async getPageHTML(sessionId) {
|
|
1151
|
+
const result = await this.evaluate(sessionId, `(() => {
|
|
1152
|
+
try {
|
|
1153
|
+
const doctype = document.doctype;
|
|
1154
|
+
const dt = doctype
|
|
1155
|
+
? '<!DOCTYPE ' + doctype.name
|
|
1156
|
+
+ (doctype.publicId ? ' PUBLIC "' + doctype.publicId + '"' : '')
|
|
1157
|
+
+ (doctype.systemId ? ' "' + doctype.systemId + '"' : '')
|
|
1158
|
+
+ '>'
|
|
1159
|
+
: '';
|
|
1160
|
+
// Include shadow DOM content from extension roots
|
|
1161
|
+
const roots = Array.from(document.querySelectorAll(
|
|
1162
|
+
'#extension-root,[data-extension-root]:not([data-extension-root="extension-js-devtools"])'
|
|
1163
|
+
));
|
|
1164
|
+
if (roots.length) {
|
|
1165
|
+
const clone = document.documentElement.cloneNode(true);
|
|
1166
|
+
const clonedRoots = Array.from(clone.querySelectorAll(
|
|
1167
|
+
'#extension-root,[data-extension-root]:not([data-extension-root="extension-js-devtools"])'
|
|
1168
|
+
));
|
|
1169
|
+
const s = new XMLSerializer();
|
|
1170
|
+
for (let i = 0; i < Math.min(roots.length, clonedRoots.length); i++) {
|
|
1171
|
+
const sr = roots[i].shadowRoot;
|
|
1172
|
+
if (!sr) continue;
|
|
1173
|
+
try {
|
|
1174
|
+
const shadow = Array.from(sr.childNodes).map(n => {
|
|
1175
|
+
try { return s.serializeToString(n); } catch { return ''; }
|
|
1176
|
+
}).join('');
|
|
1177
|
+
if (shadow) clonedRoots[i].innerHTML = shadow;
|
|
1178
|
+
} catch {}
|
|
1179
|
+
}
|
|
1180
|
+
return dt + '\\n' + clone.outerHTML;
|
|
1181
|
+
}
|
|
1182
|
+
return dt + '\\n' + document.documentElement.outerHTML;
|
|
1183
|
+
} catch (e) { return ''; }
|
|
1184
|
+
})()`);
|
|
1185
|
+
return "string" == typeof result ? result : "";
|
|
1186
|
+
}
|
|
1187
|
+
async getClosedShadowRoots(sessionId, maxBytes = 65536) {
|
|
1188
|
+
await this.sendCommand("DOM.enable", {}, sessionId);
|
|
1189
|
+
const doc = await this.sendCommand("DOM.getDocument", {
|
|
1190
|
+
depth: -1,
|
|
1191
|
+
pierce: true
|
|
1192
|
+
}, sessionId);
|
|
1193
|
+
const found = [];
|
|
1194
|
+
const walk = (node, hostName)=>{
|
|
1195
|
+
if (!node || "object" != typeof node) return;
|
|
1196
|
+
const name = node.localName || node.nodeName || hostName;
|
|
1197
|
+
if (Array.isArray(node.shadowRoots)) for (const sr of node.shadowRoots){
|
|
1198
|
+
if (sr && "closed" === sr.shadowRootType && "number" == typeof sr.nodeId) found.push({
|
|
1199
|
+
nodeId: sr.nodeId,
|
|
1200
|
+
host: String(name),
|
|
1201
|
+
type: "closed"
|
|
1202
|
+
});
|
|
1203
|
+
walk(sr, name);
|
|
1204
|
+
}
|
|
1205
|
+
if (Array.isArray(node.children)) for (const c of node.children)walk(c, name);
|
|
1206
|
+
if (node.contentDocument) walk(node.contentDocument, name);
|
|
1207
|
+
};
|
|
1208
|
+
walk(doc.root, "html");
|
|
1209
|
+
const out = [];
|
|
1210
|
+
for (const f of found)try {
|
|
1211
|
+
const oh = await this.sendCommand("DOM.getOuterHTML", {
|
|
1212
|
+
nodeId: f.nodeId
|
|
1213
|
+
}, sessionId);
|
|
1214
|
+
let html = String(oh?.outerHTML ?? "");
|
|
1215
|
+
let truncated = false;
|
|
1216
|
+
if (maxBytes > 0 && html.length > maxBytes) {
|
|
1217
|
+
html = html.slice(0, maxBytes);
|
|
1218
|
+
truncated = true;
|
|
1219
|
+
}
|
|
1220
|
+
out.push({
|
|
1221
|
+
host: f.host,
|
|
1222
|
+
type: f.type,
|
|
1223
|
+
html,
|
|
1224
|
+
...truncated ? {
|
|
1225
|
+
truncated
|
|
1226
|
+
} : {}
|
|
1227
|
+
});
|
|
1228
|
+
} catch {
|
|
1229
|
+
out.push({
|
|
1230
|
+
host: f.host,
|
|
1231
|
+
type: f.type,
|
|
1232
|
+
html: ""
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
return out;
|
|
1236
|
+
}
|
|
1237
|
+
async getPageMeta(sessionId) {
|
|
1238
|
+
const result = await this.evaluate(sessionId, `(() => {
|
|
1239
|
+
try {
|
|
1240
|
+
return {
|
|
1241
|
+
title: document.title,
|
|
1242
|
+
url: location.href,
|
|
1243
|
+
readyState: document.readyState,
|
|
1244
|
+
viewport: {
|
|
1245
|
+
width: window.innerWidth,
|
|
1246
|
+
height: window.innerHeight,
|
|
1247
|
+
devicePixelRatio: window.devicePixelRatio
|
|
1248
|
+
},
|
|
1249
|
+
frameCount: window.frames.length,
|
|
1250
|
+
scriptCount: document.querySelectorAll('script').length,
|
|
1251
|
+
styleCount: document.querySelectorAll('style,link[rel="stylesheet"]').length
|
|
1252
|
+
};
|
|
1253
|
+
} catch { return {}; }
|
|
1254
|
+
})()`);
|
|
1255
|
+
return result ?? {};
|
|
1256
|
+
}
|
|
1257
|
+
async probeSelectors(sessionId, selectors) {
|
|
1258
|
+
const result = await this.evaluate(sessionId, `(() => {
|
|
1259
|
+
const selectors = ${JSON.stringify(selectors)};
|
|
1260
|
+
return selectors.map(selector => {
|
|
1261
|
+
try {
|
|
1262
|
+
const els = Array.from(document.querySelectorAll(selector));
|
|
1263
|
+
return {
|
|
1264
|
+
selector,
|
|
1265
|
+
count: els.length,
|
|
1266
|
+
samples: els.slice(0, 3).map(el => ({
|
|
1267
|
+
tag: el.tagName.toLowerCase(),
|
|
1268
|
+
id: el.id || undefined,
|
|
1269
|
+
classes: Array.from(el.classList).join(' ') || undefined,
|
|
1270
|
+
role: el.getAttribute('role') || undefined,
|
|
1271
|
+
ariaLabel: el.getAttribute('aria-label') || undefined,
|
|
1272
|
+
textLength: (el.textContent || '').length,
|
|
1273
|
+
textSnippet: (el.textContent || '').trim().slice(0, 80)
|
|
1274
|
+
}))
|
|
1275
|
+
};
|
|
1276
|
+
} catch (e) {
|
|
1277
|
+
return { selector, count: 0, samples: [], error: String(e) };
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
})()`);
|
|
1281
|
+
return result ?? [];
|
|
1282
|
+
}
|
|
1283
|
+
async getDomSnapshot(sessionId, maxNodes = 500) {
|
|
1284
|
+
const result = await this.evaluate(sessionId, `(() => {
|
|
1285
|
+
const maxNodes = ${maxNodes};
|
|
1286
|
+
const nodes = [];
|
|
1287
|
+
const walk = (node, depth) => {
|
|
1288
|
+
if (nodes.length >= maxNodes || depth > 20) return;
|
|
1289
|
+
if (node.nodeType !== 1) return;
|
|
1290
|
+
const el = node;
|
|
1291
|
+
nodes.push({
|
|
1292
|
+
tag: el.tagName.toLowerCase(),
|
|
1293
|
+
depth,
|
|
1294
|
+
id: el.id || undefined,
|
|
1295
|
+
classes: Array.from(el.classList).slice(0, 5).join(' ') || undefined,
|
|
1296
|
+
role: el.getAttribute('role') || undefined,
|
|
1297
|
+
ariaLabel: el.getAttribute('aria-label') || undefined,
|
|
1298
|
+
childCount: el.children.length
|
|
1299
|
+
});
|
|
1300
|
+
for (const child of el.children) {
|
|
1301
|
+
walk(child, depth + 1);
|
|
1302
|
+
}
|
|
1303
|
+
};
|
|
1304
|
+
walk(document.documentElement, 0);
|
|
1305
|
+
return nodes;
|
|
1306
|
+
})()`);
|
|
1307
|
+
return result ?? [];
|
|
1308
|
+
}
|
|
1309
|
+
async getExtensionRootMeta(sessionId) {
|
|
1310
|
+
const result = await this.evaluate(sessionId, `(() => {
|
|
1311
|
+
try {
|
|
1312
|
+
const readGeneration = (node) => {
|
|
1313
|
+
const raw = node.getAttribute && node.getAttribute('data-extjs-reinject-generation');
|
|
1314
|
+
const parsed = Number(raw);
|
|
1315
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
1316
|
+
};
|
|
1317
|
+
const normalize = (node) => ({
|
|
1318
|
+
tag: node.tagName ? String(node.tagName).toLowerCase() : 'unknown',
|
|
1319
|
+
id: node.id || undefined,
|
|
1320
|
+
key: node.getAttribute ? node.getAttribute('data-extjs-reinject-key') || undefined : undefined,
|
|
1321
|
+
generation: readGeneration(node),
|
|
1322
|
+
status: node.getAttribute ? node.getAttribute('data-extjs-reinject-status') || undefined : undefined
|
|
1323
|
+
});
|
|
1324
|
+
const roots = Array.from(
|
|
1325
|
+
document.querySelectorAll('#extension-root,[data-extension-root]:not([data-extension-root="extension-js-devtools"])')
|
|
1326
|
+
).slice(0, 10).map(normalize);
|
|
1327
|
+
const markers = Array.from(
|
|
1328
|
+
document.querySelectorAll('[data-extjs-reinject-marker="true"]')
|
|
1329
|
+
).slice(0, 10).map(normalize);
|
|
1330
|
+
if (!roots.length && !markers.length) return null;
|
|
1331
|
+
const generations = [...roots, ...markers]
|
|
1332
|
+
.map(e => e.generation)
|
|
1333
|
+
.filter(g => typeof g === 'number');
|
|
1334
|
+
return {
|
|
1335
|
+
rootCount: roots.length,
|
|
1336
|
+
markerCount: markers.length,
|
|
1337
|
+
latestGeneration: generations.length ? Math.max(...generations) : 0,
|
|
1338
|
+
roots,
|
|
1339
|
+
markers
|
|
1340
|
+
};
|
|
1341
|
+
} catch { return null; }
|
|
1342
|
+
})()`);
|
|
1343
|
+
return result ?? null;
|
|
1344
|
+
}
|
|
1345
|
+
getConsoleMessages() {
|
|
1346
|
+
return [
|
|
1347
|
+
...this.consoleMessages
|
|
1348
|
+
];
|
|
1349
|
+
}
|
|
1350
|
+
getConsoleSummary() {
|
|
1351
|
+
const counts = {};
|
|
1352
|
+
const uniqueByLevel = {};
|
|
1353
|
+
for (const msg of this.consoleMessages){
|
|
1354
|
+
counts[msg.level] = (counts[msg.level] ?? 0) + 1;
|
|
1355
|
+
if (!uniqueByLevel[msg.level]) uniqueByLevel[msg.level] = new Map();
|
|
1356
|
+
const key = msg.text.slice(0, 200);
|
|
1357
|
+
uniqueByLevel[msg.level].set(key, (uniqueByLevel[msg.level].get(key) ?? 0) + 1);
|
|
1358
|
+
}
|
|
1359
|
+
const topMessages = [];
|
|
1360
|
+
for (const [level, msgs] of Object.entries(uniqueByLevel)){
|
|
1361
|
+
const sorted = [
|
|
1362
|
+
...msgs.entries()
|
|
1363
|
+
].sort((a, b)=>b[1] - a[1]);
|
|
1364
|
+
for (const [text, count] of sorted.slice(0, 5))topMessages.push({
|
|
1365
|
+
level,
|
|
1366
|
+
text,
|
|
1367
|
+
count
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
return {
|
|
1371
|
+
total: this.consoleMessages.length,
|
|
1372
|
+
counts,
|
|
1373
|
+
topMessages: topMessages.sort((a, b)=>b.count - a.count).slice(0, 10)
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
onEvent(handler) {
|
|
1377
|
+
this.eventListeners.add(handler);
|
|
1378
|
+
return ()=>{
|
|
1379
|
+
this.eventListeners.delete(handler);
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
constructor(){
|
|
1383
|
+
_define_property(this, "ws", null);
|
|
1384
|
+
_define_property(this, "messageId", 0);
|
|
1385
|
+
_define_property(this, "eventListeners", new Set());
|
|
1386
|
+
_define_property(this, "pendingRequests", new Map());
|
|
1387
|
+
_define_property(this, "consoleMessages", []);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
const source_inspect_schema = {
|
|
1391
|
+
name: "extension_source_inspect",
|
|
1392
|
+
description: "Inspect a running extension's live state via Chrome DevTools Protocol: full HTML (with shadow DOM), DOM structure, content script injection, console messages, and CSS selector queries. Requires an active dev or start session.",
|
|
1393
|
+
inputSchema: {
|
|
1394
|
+
type: "object",
|
|
1395
|
+
properties: {
|
|
1396
|
+
projectPath: {
|
|
1397
|
+
type: "string",
|
|
1398
|
+
description: "Path to the extension project root (must have an active dev session)"
|
|
1399
|
+
},
|
|
1400
|
+
url: {
|
|
1401
|
+
type: "string",
|
|
1402
|
+
description: "URL to inspect (navigates the browser tab to this URL first)"
|
|
1403
|
+
},
|
|
1404
|
+
probe: {
|
|
1405
|
+
type: "array",
|
|
1406
|
+
items: {
|
|
1407
|
+
type: "string"
|
|
1408
|
+
},
|
|
1409
|
+
description: "CSS selectors to query — returns element counts and samples for each"
|
|
1410
|
+
},
|
|
1411
|
+
include: {
|
|
1412
|
+
type: "array",
|
|
1413
|
+
items: {
|
|
1414
|
+
type: "string",
|
|
1415
|
+
enum: [
|
|
1416
|
+
"html",
|
|
1417
|
+
"summary",
|
|
1418
|
+
"meta",
|
|
1419
|
+
"dom_snapshot",
|
|
1420
|
+
"console",
|
|
1421
|
+
"extension_roots"
|
|
1422
|
+
]
|
|
1423
|
+
},
|
|
1424
|
+
default: [
|
|
1425
|
+
"summary",
|
|
1426
|
+
"meta",
|
|
1427
|
+
"console"
|
|
1428
|
+
],
|
|
1429
|
+
description: "What data to include in the response"
|
|
1430
|
+
},
|
|
1431
|
+
browser: {
|
|
1432
|
+
type: "string",
|
|
1433
|
+
default: "chrome"
|
|
1434
|
+
},
|
|
1435
|
+
maxBytes: {
|
|
1436
|
+
type: "number",
|
|
1437
|
+
default: 262144,
|
|
1438
|
+
description: "Truncate HTML output at this byte count (0 = unlimited)"
|
|
1439
|
+
},
|
|
1440
|
+
deepDom: {
|
|
1441
|
+
type: "boolean",
|
|
1442
|
+
default: false,
|
|
1443
|
+
description: "Pierce CLOSED shadow roots via CDP (Chromium only). The default path reads open shadow roots; closed ones need this escape hatch."
|
|
1444
|
+
}
|
|
1445
|
+
},
|
|
1446
|
+
required: [
|
|
1447
|
+
"projectPath"
|
|
1448
|
+
]
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1451
|
+
async function findCdpPort(projectPath, browser) {
|
|
1452
|
+
const readyPath = external_node_path_default().resolve(projectPath, "dist", "extension-js", browser, "ready.json");
|
|
1453
|
+
try {
|
|
1454
|
+
const contract = JSON.parse(external_node_fs_default().readFileSync(readyPath, "utf8"));
|
|
1455
|
+
if (contract.port) return contract.port;
|
|
1456
|
+
} catch {}
|
|
1457
|
+
const defaultPort = 9222;
|
|
1458
|
+
return new Promise((resolve)=>{
|
|
1459
|
+
const socket = new (external_node_net_default()).Socket();
|
|
1460
|
+
socket.setTimeout(1000);
|
|
1461
|
+
socket.on("connect", ()=>{
|
|
1462
|
+
socket.destroy();
|
|
1463
|
+
resolve(defaultPort);
|
|
1464
|
+
});
|
|
1465
|
+
socket.on("error", ()=>resolve(null));
|
|
1466
|
+
socket.on("timeout", ()=>{
|
|
1467
|
+
socket.destroy();
|
|
1468
|
+
resolve(null);
|
|
1469
|
+
});
|
|
1470
|
+
socket.connect(defaultPort, "127.0.0.1");
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
async function source_inspect_handler(args) {
|
|
1474
|
+
const browser = args.browser ?? "chrome";
|
|
1475
|
+
const include = new Set(args.include ?? [
|
|
1476
|
+
"summary",
|
|
1477
|
+
"meta",
|
|
1478
|
+
"console"
|
|
1479
|
+
]);
|
|
1480
|
+
const maxBytes = args.maxBytes ?? 262144;
|
|
1481
|
+
const isChromium = [
|
|
1482
|
+
"chrome",
|
|
1483
|
+
"edge",
|
|
1484
|
+
"chromium-based"
|
|
1485
|
+
].includes(browser);
|
|
1486
|
+
if (!isChromium) return JSON.stringify({
|
|
1487
|
+
error: `Source inspection for ${browser} uses RDP (Remote Debug Protocol). Currently only Chromium CDP is supported.`,
|
|
1488
|
+
hint: "Use --browser=chrome for source inspection, or use the CLI: npx extension dev --source"
|
|
1489
|
+
});
|
|
1490
|
+
const cdpPort = await findCdpPort(args.projectPath, browser);
|
|
1491
|
+
if (!cdpPort) return JSON.stringify({
|
|
1492
|
+
error: "No active dev session found. Cannot connect to Chrome DevTools Protocol.",
|
|
1493
|
+
hint: "Start a dev session first with extension_dev, then use extension_wait to confirm it is ready."
|
|
1494
|
+
});
|
|
1495
|
+
const cdp = new CDPClient();
|
|
1496
|
+
try {
|
|
1497
|
+
const allTargets = await CDPClient.discoverTargets(cdpPort);
|
|
1498
|
+
const pageTargets = allTargets.filter((t)=>"page" === t.type && !t.url.startsWith("chrome://") && !t.url.startsWith("devtools://"));
|
|
1499
|
+
if (0 === pageTargets.length) return JSON.stringify({
|
|
1500
|
+
cdpPort,
|
|
1501
|
+
browser,
|
|
1502
|
+
warning: "No inspectable page targets found. The extension may not have opened a page yet.",
|
|
1503
|
+
allTargets: allTargets.map((t)=>({
|
|
1504
|
+
type: t.type,
|
|
1505
|
+
url: t.url?.slice(0, 100)
|
|
1506
|
+
}))
|
|
1507
|
+
});
|
|
1508
|
+
const target = args.url ? pageTargets.find((t)=>t.url.includes(args.url)) ?? pageTargets[0] : pageTargets[0];
|
|
1509
|
+
const browserWsUrl = await CDPClient.discoverBrowserWsUrl(cdpPort);
|
|
1510
|
+
await cdp.connect(browserWsUrl);
|
|
1511
|
+
const sessionId = await cdp.attachToTarget(target.id);
|
|
1512
|
+
await cdp.enableDomains(sessionId);
|
|
1513
|
+
if (args.url && !target.url.includes(args.url)) {
|
|
1514
|
+
await cdp.navigate(sessionId, args.url);
|
|
1515
|
+
await new Promise((r)=>setTimeout(r, 1500));
|
|
1516
|
+
} else await new Promise((r)=>setTimeout(r, 500));
|
|
1517
|
+
const result = {
|
|
1518
|
+
cdpPort,
|
|
1519
|
+
browser,
|
|
1520
|
+
target: {
|
|
1521
|
+
id: target.id,
|
|
1522
|
+
url: target.url,
|
|
1523
|
+
title: target.title
|
|
1524
|
+
},
|
|
1525
|
+
targets: pageTargets.map((t)=>({
|
|
1526
|
+
id: t.id,
|
|
1527
|
+
url: t.url,
|
|
1528
|
+
title: t.title
|
|
1529
|
+
}))
|
|
1530
|
+
};
|
|
1531
|
+
if (include.has("html")) {
|
|
1532
|
+
let html = await cdp.getPageHTML(sessionId);
|
|
1533
|
+
if (maxBytes > 0 && html.length > maxBytes) {
|
|
1534
|
+
html = html.slice(0, maxBytes);
|
|
1535
|
+
result.htmlTruncated = true;
|
|
1536
|
+
}
|
|
1537
|
+
result.html = html;
|
|
1538
|
+
}
|
|
1539
|
+
if (include.has("summary")) {
|
|
1540
|
+
const summary = await cdp.evaluate(sessionId, `(() => {
|
|
1541
|
+
try {
|
|
1542
|
+
const roots = document.querySelectorAll('#extension-root,[data-extension-root]:not([data-extension-root="extension-js-devtools"])');
|
|
1543
|
+
return {
|
|
1544
|
+
htmlLength: document.documentElement.outerHTML.length,
|
|
1545
|
+
scriptCount: document.querySelectorAll('script').length,
|
|
1546
|
+
styleCount: document.querySelectorAll('style').length,
|
|
1547
|
+
linkCount: document.querySelectorAll('link').length,
|
|
1548
|
+
extensionRootCount: roots.length,
|
|
1549
|
+
bodyChildCount: document.body ? document.body.children.length : 0
|
|
1550
|
+
};
|
|
1551
|
+
} catch { return {}; }
|
|
1552
|
+
})()`);
|
|
1553
|
+
result.summary = summary;
|
|
1554
|
+
}
|
|
1555
|
+
if (include.has("meta")) result.meta = await cdp.getPageMeta(sessionId);
|
|
1556
|
+
if (include.has("dom_snapshot")) result.domSnapshot = await cdp.getDomSnapshot(sessionId);
|
|
1557
|
+
if (include.has("console")) result.console = cdp.getConsoleSummary();
|
|
1558
|
+
if (include.has("extension_roots")) result.extensionRoots = await cdp.getExtensionRootMeta(sessionId);
|
|
1559
|
+
if (args.probe?.length) result.probes = await cdp.probeSelectors(sessionId, args.probe);
|
|
1560
|
+
if (args.deepDom) {
|
|
1561
|
+
const closed = await cdp.getClosedShadowRoots(sessionId, maxBytes > 0 ? maxBytes : 65536);
|
|
1562
|
+
result.closedShadowRoots = closed;
|
|
1563
|
+
result.deepDom = true;
|
|
1564
|
+
}
|
|
1565
|
+
return JSON.stringify(result);
|
|
1566
|
+
} catch (err) {
|
|
1567
|
+
return JSON.stringify({
|
|
1568
|
+
error: `CDP inspection failed: ${err instanceof Error ? err.message : err}`,
|
|
1569
|
+
cdpPort,
|
|
1570
|
+
hint: "Ensure a dev session is running. The browser may have closed or the CDP port may have changed."
|
|
1571
|
+
});
|
|
1572
|
+
} finally{
|
|
1573
|
+
cdp.disconnect();
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
const CONTROL_ENVELOPE_VERSION = 1;
|
|
1577
|
+
const CONTROL_WS_PATH = "/extjs-control";
|
|
1578
|
+
const DEFAULT_LIMIT = 200;
|
|
1579
|
+
const DEFAULT_FOLLOW_MS = 4000;
|
|
1580
|
+
const MIN_FOLLOW_MS = 500;
|
|
1581
|
+
const MAX_FOLLOW_MS = 15000;
|
|
1582
|
+
const logs_schema = {
|
|
1583
|
+
name: "extension_logs",
|
|
1584
|
+
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.",
|
|
1585
|
+
inputSchema: {
|
|
1586
|
+
type: "object",
|
|
1587
|
+
properties: {
|
|
1588
|
+
projectPath: {
|
|
1589
|
+
type: "string",
|
|
1590
|
+
description: "Path to the extension project root (must have an active dev session)"
|
|
1591
|
+
},
|
|
1592
|
+
browser: {
|
|
1593
|
+
type: "string",
|
|
1594
|
+
default: "chromium",
|
|
1595
|
+
description: "Which dist/extension-js/<browser>/ to read. Defaults to chromium (the default dev target)."
|
|
1596
|
+
},
|
|
1597
|
+
level: {
|
|
1598
|
+
type: "string",
|
|
1599
|
+
enum: [
|
|
1600
|
+
"off",
|
|
1601
|
+
"error",
|
|
1602
|
+
"warn",
|
|
1603
|
+
"info",
|
|
1604
|
+
"debug",
|
|
1605
|
+
"trace",
|
|
1606
|
+
"all"
|
|
1607
|
+
],
|
|
1608
|
+
default: "all",
|
|
1609
|
+
description: "Minimum severity to include; selecting a level includes it plus everything more severe."
|
|
1610
|
+
},
|
|
1611
|
+
context: {
|
|
1612
|
+
type: "array",
|
|
1613
|
+
items: {
|
|
1614
|
+
type: "string",
|
|
1615
|
+
enum: [
|
|
1616
|
+
"background",
|
|
1617
|
+
"content",
|
|
1618
|
+
"page",
|
|
1619
|
+
"sidebar",
|
|
1620
|
+
"popup",
|
|
1621
|
+
"options",
|
|
1622
|
+
"devtools"
|
|
1623
|
+
]
|
|
1624
|
+
},
|
|
1625
|
+
description: "Restrict to these contexts. Omit for all."
|
|
1626
|
+
},
|
|
1627
|
+
signalsOnly: {
|
|
1628
|
+
type: "boolean",
|
|
1629
|
+
default: false,
|
|
1630
|
+
description: "Only structured dx.signal diagnostics (which carry code/status/remediation), skipping plain console lines."
|
|
1631
|
+
},
|
|
1632
|
+
since: {
|
|
1633
|
+
type: "number",
|
|
1634
|
+
description: "Only return events with seq greater than this (cursor for polling forward)."
|
|
1635
|
+
},
|
|
1636
|
+
url: {
|
|
1637
|
+
type: "string",
|
|
1638
|
+
description: "Only events whose url/hostname matches (glob with * or plain substring), e.g. https://shop.example/*."
|
|
1639
|
+
},
|
|
1640
|
+
tab: {
|
|
1641
|
+
type: "number",
|
|
1642
|
+
description: "Only events from this tab id."
|
|
1643
|
+
},
|
|
1644
|
+
follow: {
|
|
1645
|
+
type: "boolean",
|
|
1646
|
+
default: false,
|
|
1647
|
+
description: "Collect from the live control channel for a bounded window instead of reading the file. Use with followMs."
|
|
1648
|
+
},
|
|
1649
|
+
followMs: {
|
|
1650
|
+
type: "number",
|
|
1651
|
+
default: DEFAULT_FOLLOW_MS,
|
|
1652
|
+
description: `How long to collect live frames when follow=true (clamped ${MIN_FOLLOW_MS}–${MAX_FOLLOW_MS}ms).`
|
|
1653
|
+
},
|
|
1654
|
+
limit: {
|
|
1655
|
+
type: "number",
|
|
1656
|
+
default: DEFAULT_LIMIT,
|
|
1657
|
+
description: "Maximum number of (most recent) events to return."
|
|
1658
|
+
}
|
|
1659
|
+
},
|
|
1660
|
+
required: [
|
|
1661
|
+
"projectPath"
|
|
1662
|
+
]
|
|
1663
|
+
}
|
|
1664
|
+
};
|
|
1665
|
+
const LEVEL_ORDER = [
|
|
1666
|
+
"error",
|
|
1667
|
+
"warn",
|
|
1668
|
+
"info",
|
|
1669
|
+
"debug",
|
|
1670
|
+
"trace"
|
|
1671
|
+
];
|
|
1672
|
+
function levelRank(level) {
|
|
1673
|
+
const l = "log" === level ? "info" : level;
|
|
1674
|
+
const i = LEVEL_ORDER.indexOf(l);
|
|
1675
|
+
return -1 === i ? LEVEL_ORDER.length : i;
|
|
1676
|
+
}
|
|
1677
|
+
function makeUrlMatcher(pattern) {
|
|
1678
|
+
const hasGlob = pattern.includes("*");
|
|
1679
|
+
let re = null;
|
|
1680
|
+
if (hasGlob) {
|
|
1681
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1682
|
+
re = new RegExp(escaped);
|
|
1683
|
+
}
|
|
1684
|
+
return (event)=>{
|
|
1685
|
+
const candidates = [
|
|
1686
|
+
event.url,
|
|
1687
|
+
event.hostname
|
|
1688
|
+
].filter((v)=>"string" == typeof v);
|
|
1689
|
+
if (0 === candidates.length) return false;
|
|
1690
|
+
return candidates.some((c)=>re ? re.test(c) : c.includes(pattern));
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
function makeFilter(args) {
|
|
1694
|
+
const minLevel = String(args.level || "all").toLowerCase();
|
|
1695
|
+
const rawContexts = Array.isArray(args.context) ? args.context : "string" == typeof args.context ? args.context.split(",") : null;
|
|
1696
|
+
const contexts = rawContexts && rawContexts.length ? new Set(rawContexts.map((c)=>c.trim()).filter(Boolean)) : null;
|
|
1697
|
+
const sinceSeq = null != args.since ? Number(args.since) : null;
|
|
1698
|
+
const urlMatches = args.url ? makeUrlMatcher(args.url) : null;
|
|
1699
|
+
const tabId = null != args.tab ? Number(args.tab) : null;
|
|
1700
|
+
return (event)=>{
|
|
1701
|
+
if (!event || "object" != typeof event) return false;
|
|
1702
|
+
if ("header" === event.type) return false;
|
|
1703
|
+
if (args.signalsOnly && "dx.signal" !== event.eventType) return false;
|
|
1704
|
+
if (contexts && !contexts.has(event.context)) return false;
|
|
1705
|
+
if ("all" !== minLevel && "off" !== minLevel) {
|
|
1706
|
+
if (levelRank(event.level) > levelRank(minLevel)) return false;
|
|
1707
|
+
}
|
|
1708
|
+
if (null != sinceSeq && Number.isFinite(sinceSeq) && "number" == typeof event.seq && event.seq <= sinceSeq) return false;
|
|
1709
|
+
if (urlMatches && !urlMatches(event)) return false;
|
|
1710
|
+
if (null != tabId && Number.isFinite(tabId) && event.tabId !== tabId) return false;
|
|
1711
|
+
return true;
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
function logsFilePath(projectPath, browser) {
|
|
1715
|
+
return external_node_path_default().resolve(projectPath, "dist", "extension-js", browser, "logs.ndjson");
|
|
1716
|
+
}
|
|
1717
|
+
function readReadyContract(projectPath, browser) {
|
|
1718
|
+
const readyPath = external_node_path_default().resolve(projectPath, "dist", "extension-js", browser, "ready.json");
|
|
1719
|
+
try {
|
|
1720
|
+
const c = JSON.parse(external_node_fs_default().readFileSync(readyPath, "utf8"));
|
|
1721
|
+
if ("number" != typeof c.controlPort || !c.instanceId) return null;
|
|
1722
|
+
return {
|
|
1723
|
+
controlPort: c.controlPort,
|
|
1724
|
+
instanceId: String(c.instanceId),
|
|
1725
|
+
runId: String(c.runId || "")
|
|
1726
|
+
};
|
|
1727
|
+
} catch {
|
|
1728
|
+
return null;
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
function capRecent(events, limit) {
|
|
1732
|
+
if (events.length <= limit) return {
|
|
1733
|
+
events,
|
|
1734
|
+
truncated: false
|
|
1735
|
+
};
|
|
1736
|
+
return {
|
|
1737
|
+
events: events.slice(events.length - limit),
|
|
1738
|
+
truncated: true
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
function summarize(events, source, browser, runId, limit, dropped) {
|
|
1742
|
+
const matched = events.length;
|
|
1743
|
+
const { events: out, truncated } = capRecent(events, limit);
|
|
1744
|
+
const lastSeq = out.length ? out.reduce((m, e)=>"number" == typeof e.seq && e.seq > m ? e.seq : m, -1) : -1;
|
|
1745
|
+
return JSON.stringify({
|
|
1746
|
+
ok: true,
|
|
1747
|
+
source,
|
|
1748
|
+
browser,
|
|
1749
|
+
runId: runId || void 0,
|
|
1750
|
+
matched,
|
|
1751
|
+
count: out.length,
|
|
1752
|
+
truncated,
|
|
1753
|
+
dropped: dropped || void 0,
|
|
1754
|
+
nextSince: lastSeq >= 0 ? lastSeq : void 0,
|
|
1755
|
+
events: out
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
async function readFromFile(args, browser, limit) {
|
|
1759
|
+
const file = logsFilePath(args.projectPath, browser);
|
|
1760
|
+
if (!external_node_fs_default().existsSync(file)) return JSON.stringify({
|
|
1761
|
+
error: `No logs found at ${file}.`,
|
|
1762
|
+
hint: "Start a dev session first (extension_dev), or pass browser to match it. For live frames before any line is written, use follow:true."
|
|
1763
|
+
});
|
|
1764
|
+
const matches = makeFilter(args);
|
|
1765
|
+
const lines = external_node_fs_default().readFileSync(file, "utf8").split("\n").filter(Boolean);
|
|
1766
|
+
let runId = "";
|
|
1767
|
+
const events = [];
|
|
1768
|
+
for (const line of lines){
|
|
1769
|
+
let event;
|
|
1770
|
+
try {
|
|
1771
|
+
event = JSON.parse(line);
|
|
1772
|
+
} catch {
|
|
1773
|
+
continue;
|
|
1774
|
+
}
|
|
1775
|
+
if (event && "header" === event.type && event.runId) {
|
|
1776
|
+
runId = String(event.runId);
|
|
1777
|
+
continue;
|
|
1778
|
+
}
|
|
1779
|
+
if (matches(event)) events.push(event);
|
|
1780
|
+
}
|
|
1781
|
+
return summarize(events, "file", browser, runId, limit, 0);
|
|
1782
|
+
}
|
|
1783
|
+
async function readFromStream(args, browser, limit) {
|
|
1784
|
+
const ready = readReadyContract(args.projectPath, browser);
|
|
1785
|
+
if (!ready) return JSON.stringify({
|
|
1786
|
+
error: `No active control channel found for ${browser}.`,
|
|
1787
|
+
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.`
|
|
1788
|
+
});
|
|
1789
|
+
const followMs = Math.min(Math.max(args.followMs ?? DEFAULT_FOLLOW_MS, MIN_FOLLOW_MS), MAX_FOLLOW_MS);
|
|
1790
|
+
const matches = makeFilter(args);
|
|
1791
|
+
const events = [];
|
|
1792
|
+
let dropped = 0;
|
|
1793
|
+
let runId = ready.runId;
|
|
1794
|
+
return await new Promise((resolve)=>{
|
|
1795
|
+
let settled = false;
|
|
1796
|
+
const url = `ws://127.0.0.1:${ready.controlPort}${CONTROL_WS_PATH}`;
|
|
1797
|
+
let socket;
|
|
1798
|
+
try {
|
|
1799
|
+
socket = new (external_ws_default())(url);
|
|
1800
|
+
} catch (err) {
|
|
1801
|
+
resolve(JSON.stringify({
|
|
1802
|
+
error: `Could not open control channel at ${url}: ${err instanceof Error ? err.message : String(err)}`
|
|
1803
|
+
}));
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
const finish = ()=>{
|
|
1807
|
+
if (settled) return;
|
|
1808
|
+
settled = true;
|
|
1809
|
+
clearTimeout(timer);
|
|
1810
|
+
try {
|
|
1811
|
+
socket.close();
|
|
1812
|
+
} catch {}
|
|
1813
|
+
resolve(summarize(events, "stream", browser, runId, limit, dropped));
|
|
1814
|
+
};
|
|
1815
|
+
const timer = setTimeout(finish, followMs);
|
|
1816
|
+
socket.on("open", ()=>{
|
|
1817
|
+
try {
|
|
1818
|
+
socket.send(JSON.stringify({
|
|
1819
|
+
type: "hello",
|
|
1820
|
+
v: CONTROL_ENVELOPE_VERSION,
|
|
1821
|
+
role: "consumer",
|
|
1822
|
+
instanceId: ready.instanceId
|
|
1823
|
+
}));
|
|
1824
|
+
} catch {}
|
|
1825
|
+
});
|
|
1826
|
+
socket.on("message", (data)=>{
|
|
1827
|
+
let frame;
|
|
1828
|
+
try {
|
|
1829
|
+
frame = JSON.parse(data.toString());
|
|
1830
|
+
} catch {
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
if ("ready" === frame.type && frame.runId) runId = String(frame.runId);
|
|
1834
|
+
else if ("log" === frame.type && frame.event) {
|
|
1835
|
+
if (matches(frame.event)) events.push(frame.event);
|
|
1836
|
+
} else if ("gap" === frame.type && "number" == typeof frame.dropped) dropped += frame.dropped;
|
|
1837
|
+
});
|
|
1838
|
+
socket.on("error", ()=>{
|
|
1839
|
+
if (settled) return;
|
|
1840
|
+
settled = true;
|
|
1841
|
+
clearTimeout(timer);
|
|
1842
|
+
resolve(JSON.stringify({
|
|
1843
|
+
error: `Control channel error at ${url}.`,
|
|
1844
|
+
hint: "The dev session may have stopped or the control port changed. Re-check with extension_wait."
|
|
1845
|
+
}));
|
|
1846
|
+
});
|
|
1847
|
+
socket.on("close", finish);
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
async function logs_handler(args) {
|
|
1851
|
+
const browser = args.browser ?? "chromium";
|
|
1852
|
+
const limit = args.limit && args.limit > 0 ? args.limit : DEFAULT_LIMIT;
|
|
1853
|
+
if (args.follow) return readFromStream(args, browser, limit);
|
|
1854
|
+
return readFromFile(args, browser, limit);
|
|
1855
|
+
}
|
|
1856
|
+
async function runActVerb(args, projectPath, timeoutMs) {
|
|
1857
|
+
const { code, stdout, stderr } = await runExtensionCli([
|
|
1858
|
+
...args,
|
|
1859
|
+
"--output",
|
|
1860
|
+
"json"
|
|
1861
|
+
], {
|
|
1862
|
+
cwd: projectPath,
|
|
1863
|
+
timeoutMs
|
|
1864
|
+
});
|
|
1865
|
+
const out = stdout.trim();
|
|
1866
|
+
if (out) try {
|
|
1867
|
+
JSON.parse(out);
|
|
1868
|
+
return out;
|
|
1869
|
+
} catch {}
|
|
1870
|
+
return JSON.stringify({
|
|
1871
|
+
ok: false,
|
|
1872
|
+
error: {
|
|
1873
|
+
name: "CliError",
|
|
1874
|
+
message: stderr.trim() || `extension exited with code ${code}`
|
|
1875
|
+
}
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
function commonFlags(args) {
|
|
1879
|
+
const flags = [];
|
|
1880
|
+
if (args.context) flags.push("--context", args.context);
|
|
1881
|
+
if (args.url) flags.push("--url", args.url);
|
|
1882
|
+
if (null != args.tab) flags.push("--tab", String(args.tab));
|
|
1883
|
+
if (args.browser) flags.push("--browser", args.browser);
|
|
1884
|
+
if (null != args.timeout) flags.push("--timeout", String(args.timeout));
|
|
1885
|
+
return flags;
|
|
1886
|
+
}
|
|
1887
|
+
const eval_schema = {
|
|
1888
|
+
name: "extension_eval",
|
|
1889
|
+
description: "Evaluate an expression in a running extension context (service worker, content script, popup, options, sidebar). Requires the dev session to be started with --allow-eval (writes a 0600 session token the CLI reads). Wraps `extension eval`.",
|
|
1890
|
+
inputSchema: {
|
|
1891
|
+
type: "object",
|
|
1892
|
+
properties: {
|
|
1893
|
+
projectPath: {
|
|
1894
|
+
type: "string",
|
|
1895
|
+
description: "Path to the extension project root (must have an active dev session)"
|
|
1896
|
+
},
|
|
1897
|
+
expression: {
|
|
1898
|
+
type: "string",
|
|
1899
|
+
description: "JavaScript expression to evaluate in the target context"
|
|
1900
|
+
},
|
|
1901
|
+
context: {
|
|
1902
|
+
type: "string",
|
|
1903
|
+
enum: [
|
|
1904
|
+
"background",
|
|
1905
|
+
"popup",
|
|
1906
|
+
"options",
|
|
1907
|
+
"sidebar",
|
|
1908
|
+
"devtools",
|
|
1909
|
+
"content",
|
|
1910
|
+
"page"
|
|
1911
|
+
],
|
|
1912
|
+
default: "background",
|
|
1913
|
+
description: "Which extension surface to evaluate in"
|
|
1914
|
+
},
|
|
1915
|
+
url: {
|
|
1916
|
+
type: "string",
|
|
1917
|
+
description: "For content/page: document(s) to target"
|
|
1918
|
+
},
|
|
1919
|
+
tab: {
|
|
1920
|
+
type: "number",
|
|
1921
|
+
description: "For content/page: a specific tab id"
|
|
1922
|
+
},
|
|
1923
|
+
browser: {
|
|
1924
|
+
type: "string",
|
|
1925
|
+
default: "chromium"
|
|
1926
|
+
},
|
|
1927
|
+
timeout: {
|
|
1928
|
+
type: "number",
|
|
1929
|
+
description: "Command timeout in ms (default 5000)"
|
|
1930
|
+
}
|
|
1931
|
+
},
|
|
1932
|
+
required: [
|
|
1933
|
+
"projectPath",
|
|
1934
|
+
"expression"
|
|
1935
|
+
]
|
|
1936
|
+
}
|
|
1937
|
+
};
|
|
1938
|
+
async function eval_handler(args) {
|
|
1939
|
+
return runActVerb([
|
|
1940
|
+
"eval",
|
|
1941
|
+
args.expression,
|
|
1942
|
+
args.projectPath,
|
|
1943
|
+
...commonFlags(args)
|
|
1944
|
+
], args.projectPath, args.timeout);
|
|
1945
|
+
}
|
|
1946
|
+
const storage_schema = {
|
|
1947
|
+
name: "extension_storage",
|
|
1948
|
+
description: "Read or write chrome.storage in a running extension. Requires the dev session to be started with --allow-control. Wraps `extension storage get|set`.",
|
|
1949
|
+
inputSchema: {
|
|
1950
|
+
type: "object",
|
|
1951
|
+
properties: {
|
|
1952
|
+
projectPath: {
|
|
1953
|
+
type: "string",
|
|
1954
|
+
description: "Path to the extension project root (must have an active dev session)"
|
|
1955
|
+
},
|
|
1956
|
+
action: {
|
|
1957
|
+
type: "string",
|
|
1958
|
+
enum: [
|
|
1959
|
+
"get",
|
|
1960
|
+
"set"
|
|
1961
|
+
],
|
|
1962
|
+
description: "get reads a key (or the whole area); set writes a key"
|
|
1963
|
+
},
|
|
1964
|
+
area: {
|
|
1965
|
+
type: "string",
|
|
1966
|
+
enum: [
|
|
1967
|
+
"local",
|
|
1968
|
+
"sync",
|
|
1969
|
+
"session",
|
|
1970
|
+
"managed"
|
|
1971
|
+
],
|
|
1972
|
+
default: "local"
|
|
1973
|
+
},
|
|
1974
|
+
key: {
|
|
1975
|
+
type: "string",
|
|
1976
|
+
description: "Key to get or set"
|
|
1977
|
+
},
|
|
1978
|
+
value: {
|
|
1979
|
+
description: "Value to set (any JSON value); required for action=set"
|
|
1980
|
+
},
|
|
1981
|
+
context: {
|
|
1982
|
+
type: "string",
|
|
1983
|
+
enum: [
|
|
1984
|
+
"background",
|
|
1985
|
+
"popup",
|
|
1986
|
+
"options",
|
|
1987
|
+
"sidebar",
|
|
1988
|
+
"content"
|
|
1989
|
+
],
|
|
1990
|
+
default: "background"
|
|
1991
|
+
},
|
|
1992
|
+
browser: {
|
|
1993
|
+
type: "string",
|
|
1994
|
+
default: "chromium"
|
|
1995
|
+
},
|
|
1996
|
+
timeout: {
|
|
1997
|
+
type: "number",
|
|
1998
|
+
description: "Command timeout in ms (default 5000)"
|
|
1999
|
+
}
|
|
2000
|
+
},
|
|
2001
|
+
required: [
|
|
2002
|
+
"projectPath",
|
|
2003
|
+
"action"
|
|
2004
|
+
]
|
|
2005
|
+
}
|
|
2006
|
+
};
|
|
2007
|
+
async function storage_handler(args) {
|
|
2008
|
+
const cli = [
|
|
2009
|
+
"storage",
|
|
2010
|
+
args.action,
|
|
2011
|
+
args.projectPath
|
|
2012
|
+
];
|
|
2013
|
+
if (args.area) cli.push("--area", args.area);
|
|
2014
|
+
if (args.key) cli.push("--key", args.key);
|
|
2015
|
+
if ("set" === args.action) {
|
|
2016
|
+
if (void 0 === args.value) return JSON.stringify({
|
|
2017
|
+
ok: false,
|
|
2018
|
+
error: {
|
|
2019
|
+
name: "BadRequest",
|
|
2020
|
+
message: "storage set requires a value"
|
|
2021
|
+
}
|
|
2022
|
+
});
|
|
2023
|
+
cli.push("--value", JSON.stringify(args.value));
|
|
2024
|
+
}
|
|
2025
|
+
if (args.context) cli.push("--context", args.context);
|
|
2026
|
+
if (args.browser) cli.push("--browser", args.browser);
|
|
2027
|
+
if (null != args.timeout) cli.push("--timeout", String(args.timeout));
|
|
2028
|
+
return runActVerb(cli, args.projectPath, args.timeout);
|
|
2029
|
+
}
|
|
2030
|
+
const reload_schema = {
|
|
2031
|
+
name: "extension_reload",
|
|
2032
|
+
description: "Reload a running extension (background) or a tab. Requires the dev session to be started with --allow-control. Wraps `extension reload`.",
|
|
2033
|
+
inputSchema: {
|
|
2034
|
+
type: "object",
|
|
2035
|
+
properties: {
|
|
2036
|
+
projectPath: {
|
|
2037
|
+
type: "string",
|
|
2038
|
+
description: "Path to the extension project root (must have an active dev session)"
|
|
2039
|
+
},
|
|
2040
|
+
context: {
|
|
2041
|
+
type: "string",
|
|
2042
|
+
enum: [
|
|
2043
|
+
"background",
|
|
2044
|
+
"content",
|
|
2045
|
+
"page"
|
|
2046
|
+
],
|
|
2047
|
+
default: "background"
|
|
2048
|
+
},
|
|
2049
|
+
tab: {
|
|
2050
|
+
type: "number",
|
|
2051
|
+
description: "For content/page: a specific tab id"
|
|
2052
|
+
},
|
|
2053
|
+
browser: {
|
|
2054
|
+
type: "string",
|
|
2055
|
+
default: "chromium"
|
|
2056
|
+
},
|
|
2057
|
+
timeout: {
|
|
2058
|
+
type: "number",
|
|
2059
|
+
description: "Command timeout in ms (default 5000)"
|
|
2060
|
+
}
|
|
2061
|
+
},
|
|
2062
|
+
required: [
|
|
2063
|
+
"projectPath"
|
|
2064
|
+
]
|
|
2065
|
+
}
|
|
2066
|
+
};
|
|
2067
|
+
async function reload_handler(args) {
|
|
2068
|
+
return runActVerb([
|
|
2069
|
+
"reload",
|
|
2070
|
+
args.projectPath,
|
|
2071
|
+
...commonFlags(args)
|
|
2072
|
+
], args.projectPath, args.timeout);
|
|
2073
|
+
}
|
|
2074
|
+
const open_schema = {
|
|
2075
|
+
name: "extension_open",
|
|
2076
|
+
description: "Open an extension surface — popup, options, or sidebar — in a running session. Requires the dev session to be started with --allow-control. Wraps `extension open`.",
|
|
2077
|
+
inputSchema: {
|
|
2078
|
+
type: "object",
|
|
2079
|
+
properties: {
|
|
2080
|
+
projectPath: {
|
|
2081
|
+
type: "string",
|
|
2082
|
+
description: "Path to the extension project root (must have an active dev session)"
|
|
2083
|
+
},
|
|
2084
|
+
surface: {
|
|
2085
|
+
type: "string",
|
|
2086
|
+
enum: [
|
|
2087
|
+
"popup",
|
|
2088
|
+
"options",
|
|
2089
|
+
"sidebar"
|
|
2090
|
+
],
|
|
2091
|
+
description: "Which surface to open"
|
|
2092
|
+
},
|
|
2093
|
+
browser: {
|
|
2094
|
+
type: "string",
|
|
2095
|
+
default: "chromium"
|
|
2096
|
+
},
|
|
2097
|
+
timeout: {
|
|
2098
|
+
type: "number",
|
|
2099
|
+
description: "Command timeout in ms (default 5000)"
|
|
2100
|
+
}
|
|
2101
|
+
},
|
|
2102
|
+
required: [
|
|
2103
|
+
"projectPath",
|
|
2104
|
+
"surface"
|
|
2105
|
+
]
|
|
2106
|
+
}
|
|
2107
|
+
};
|
|
2108
|
+
async function open_handler(args) {
|
|
2109
|
+
const cli = [
|
|
2110
|
+
"open",
|
|
2111
|
+
args.surface,
|
|
2112
|
+
args.projectPath
|
|
2113
|
+
];
|
|
2114
|
+
if (args.browser) cli.push("--browser", args.browser);
|
|
2115
|
+
if (null != args.timeout) cli.push("--timeout", String(args.timeout));
|
|
2116
|
+
return runActVerb(cli, args.projectPath, args.timeout);
|
|
2117
|
+
}
|
|
2118
|
+
const dom_inspect_schema = {
|
|
2119
|
+
name: "extension_dom_inspect",
|
|
2120
|
+
description: "Inspect a page/content-script DOM via the agent bridge (CDP-free, localhost). Returns a structured snapshot (counts, extension roots, open shadow roots, optional capped HTML). Requires --allow-control. For closed shadow roots or deep CDP inspection use extension_source_inspect. Wraps `extension inspect`.",
|
|
2121
|
+
inputSchema: {
|
|
2122
|
+
type: "object",
|
|
2123
|
+
properties: {
|
|
2124
|
+
projectPath: {
|
|
2125
|
+
type: "string",
|
|
2126
|
+
description: "Path to the extension project root (must have an active dev session)"
|
|
2127
|
+
},
|
|
2128
|
+
tab: {
|
|
2129
|
+
type: "number",
|
|
2130
|
+
description: "Tab id (required for content/page; omit for surfaces)"
|
|
2131
|
+
},
|
|
2132
|
+
context: {
|
|
2133
|
+
type: "string",
|
|
2134
|
+
enum: [
|
|
2135
|
+
"content",
|
|
2136
|
+
"page",
|
|
2137
|
+
"popup",
|
|
2138
|
+
"options",
|
|
2139
|
+
"sidebar",
|
|
2140
|
+
"devtools"
|
|
2141
|
+
],
|
|
2142
|
+
default: "content",
|
|
2143
|
+
description: "content/page (needs tab) or an OPEN extension surface (popup/options/sidebar/devtools)"
|
|
2144
|
+
},
|
|
2145
|
+
include: {
|
|
2146
|
+
type: "array",
|
|
2147
|
+
items: {
|
|
2148
|
+
type: "string",
|
|
2149
|
+
enum: [
|
|
2150
|
+
"summary",
|
|
2151
|
+
"html"
|
|
2152
|
+
]
|
|
2153
|
+
},
|
|
2154
|
+
default: [
|
|
2155
|
+
"summary"
|
|
2156
|
+
],
|
|
2157
|
+
description: "What to include; html is byte-capped"
|
|
2158
|
+
},
|
|
2159
|
+
maxBytes: {
|
|
2160
|
+
type: "number",
|
|
2161
|
+
default: 262144
|
|
2162
|
+
},
|
|
2163
|
+
withConsole: {
|
|
2164
|
+
type: "number",
|
|
2165
|
+
description: "Also include the last N console lines for the target (DOM + recent console in one call)"
|
|
2166
|
+
},
|
|
2167
|
+
browser: {
|
|
2168
|
+
type: "string",
|
|
2169
|
+
default: "chromium"
|
|
2170
|
+
},
|
|
2171
|
+
timeout: {
|
|
2172
|
+
type: "number",
|
|
2173
|
+
description: "Command timeout in ms (default 5000)"
|
|
2174
|
+
}
|
|
2175
|
+
},
|
|
2176
|
+
required: [
|
|
2177
|
+
"projectPath"
|
|
2178
|
+
]
|
|
2179
|
+
}
|
|
2180
|
+
};
|
|
2181
|
+
async function dom_inspect_handler(args) {
|
|
2182
|
+
const surfaces = [
|
|
2183
|
+
"popup",
|
|
2184
|
+
"options",
|
|
2185
|
+
"sidebar",
|
|
2186
|
+
"devtools"
|
|
2187
|
+
];
|
|
2188
|
+
const isSurface = !!args.context && surfaces.includes(args.context);
|
|
2189
|
+
if (!isSurface && null == args.tab) return JSON.stringify({
|
|
2190
|
+
ok: false,
|
|
2191
|
+
error: {
|
|
2192
|
+
name: "BadRequest",
|
|
2193
|
+
message: "content/page inspect requires a tab id"
|
|
2194
|
+
}
|
|
2195
|
+
});
|
|
2196
|
+
const cli = [
|
|
2197
|
+
"inspect",
|
|
2198
|
+
args.projectPath
|
|
2199
|
+
];
|
|
2200
|
+
if (null != args.tab) cli.push("--tab", String(args.tab));
|
|
2201
|
+
if (args.context) cli.push("--context", args.context);
|
|
2202
|
+
if (args.include?.length) cli.push("--include", args.include.join(","));
|
|
2203
|
+
if (null != args.maxBytes) cli.push("--max-bytes", String(args.maxBytes));
|
|
2204
|
+
if (null != args.withConsole) cli.push("--with-console", String(args.withConsole));
|
|
2205
|
+
if (args.browser) cli.push("--browser", args.browser);
|
|
2206
|
+
if (null != args.timeout) cli.push("--timeout", String(args.timeout));
|
|
2207
|
+
return runActVerb(cli, args.projectPath, args.timeout);
|
|
2208
|
+
}
|
|
2209
|
+
const publish_schema = {
|
|
2210
|
+
name: "extension_publish",
|
|
2211
|
+
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. Wraps `extension publish` (which posts to the platform's CLI publish endpoint). This is the only tool that talks to the hosted platform rather than the local browser.",
|
|
2212
|
+
inputSchema: {
|
|
2213
|
+
type: "object",
|
|
2214
|
+
properties: {
|
|
2215
|
+
projectPath: {
|
|
2216
|
+
type: "string",
|
|
2217
|
+
description: "Path to the extension project root"
|
|
2218
|
+
},
|
|
2219
|
+
ttlHours: {
|
|
2220
|
+
type: "number",
|
|
2221
|
+
description: "Share-link lifetime in hours (1–168, default 24)"
|
|
2222
|
+
},
|
|
2223
|
+
buildSha: {
|
|
2224
|
+
type: "string",
|
|
2225
|
+
description: "Pin the share URL to a specific build sha"
|
|
2226
|
+
},
|
|
2227
|
+
api: {
|
|
2228
|
+
type: "string",
|
|
2229
|
+
description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
|
|
2230
|
+
}
|
|
2231
|
+
},
|
|
2232
|
+
required: [
|
|
2233
|
+
"projectPath"
|
|
2234
|
+
]
|
|
2235
|
+
}
|
|
2236
|
+
};
|
|
2237
|
+
async function publish_handler(args) {
|
|
2238
|
+
const cli = [
|
|
2239
|
+
"publish",
|
|
2240
|
+
args.projectPath,
|
|
2241
|
+
"--output",
|
|
2242
|
+
"json"
|
|
2243
|
+
];
|
|
2244
|
+
if (null != args.ttlHours) cli.push("--ttl", String(args.ttlHours));
|
|
2245
|
+
if (args.buildSha) cli.push("--build-sha", args.buildSha);
|
|
2246
|
+
if (args.api) cli.push("--api", args.api);
|
|
2247
|
+
const { code, stdout, stderr } = await runExtensionCli(cli, {
|
|
2248
|
+
cwd: args.projectPath
|
|
2249
|
+
});
|
|
2250
|
+
const out = stdout.trim();
|
|
2251
|
+
if (out) try {
|
|
2252
|
+
JSON.parse(out);
|
|
2253
|
+
return out;
|
|
2254
|
+
} catch {}
|
|
2255
|
+
return JSON.stringify({
|
|
2256
|
+
ok: false,
|
|
2257
|
+
error: {
|
|
2258
|
+
name: "PublishError",
|
|
2259
|
+
message: stderr.trim() || `extension publish exited with code ${code}`
|
|
2260
|
+
}
|
|
2261
|
+
});
|
|
2262
|
+
}
|
|
2263
|
+
const wait_schema = {
|
|
2264
|
+
name: "extension_wait",
|
|
2265
|
+
description: "Wait for a running dev or start session to be ready. Polls the ready.json contract file and returns structured status.",
|
|
2266
|
+
inputSchema: {
|
|
2267
|
+
type: "object",
|
|
2268
|
+
properties: {
|
|
2269
|
+
projectPath: {
|
|
2270
|
+
type: "string",
|
|
2271
|
+
description: "Path to the extension project root"
|
|
2272
|
+
},
|
|
2273
|
+
browser: {
|
|
2274
|
+
type: "string",
|
|
2275
|
+
default: "chrome",
|
|
2276
|
+
description: "Browser to check readiness for"
|
|
2277
|
+
},
|
|
2278
|
+
timeout: {
|
|
2279
|
+
type: "number",
|
|
2280
|
+
default: 60000,
|
|
2281
|
+
description: "Timeout in milliseconds"
|
|
2282
|
+
}
|
|
2283
|
+
},
|
|
2284
|
+
required: [
|
|
2285
|
+
"projectPath"
|
|
2286
|
+
]
|
|
2287
|
+
}
|
|
2288
|
+
};
|
|
2289
|
+
async function wait_handler(args) {
|
|
2290
|
+
const browser = args.browser ?? "chrome";
|
|
2291
|
+
const timeout = args.timeout ?? 60000;
|
|
2292
|
+
const readyPath = external_node_path_default().resolve(args.projectPath, "dist", "extension-js", browser, "ready.json");
|
|
2293
|
+
const start = Date.now();
|
|
2294
|
+
const pollInterval = 1000;
|
|
2295
|
+
while(Date.now() - start < timeout){
|
|
2296
|
+
try {
|
|
2297
|
+
const raw = external_node_fs_default().readFileSync(readyPath, "utf8");
|
|
2298
|
+
const contract = JSON.parse(raw);
|
|
2299
|
+
if ("ready" === contract.status) return JSON.stringify({
|
|
2300
|
+
status: "ready",
|
|
2301
|
+
command: contract.command,
|
|
2302
|
+
browser: contract.browser,
|
|
2303
|
+
port: contract.port,
|
|
2304
|
+
pid: contract.pid,
|
|
2305
|
+
distPath: contract.distPath,
|
|
2306
|
+
manifestPath: contract.manifestPath,
|
|
2307
|
+
compiledAt: contract.compiledAt,
|
|
2308
|
+
startedAt: contract.startedAt,
|
|
2309
|
+
waitDuration: Date.now() - start
|
|
2310
|
+
});
|
|
2311
|
+
if ("error" === contract.status) return JSON.stringify({
|
|
2312
|
+
status: "error",
|
|
2313
|
+
message: contract.message,
|
|
2314
|
+
errors: contract.errors,
|
|
2315
|
+
code: contract.code,
|
|
2316
|
+
browser: contract.browser,
|
|
2317
|
+
waitDuration: Date.now() - start
|
|
2318
|
+
});
|
|
2319
|
+
} catch {}
|
|
2320
|
+
await new Promise((resolve)=>setTimeout(resolve, pollInterval));
|
|
2321
|
+
}
|
|
2322
|
+
return JSON.stringify({
|
|
2323
|
+
status: "timeout",
|
|
2324
|
+
message: `Extension did not become ready within ${timeout}ms`,
|
|
2325
|
+
readyPath,
|
|
2326
|
+
hint: "The dev session may still be building. Try increasing the timeout, or check if the dev process is still running."
|
|
2327
|
+
});
|
|
2328
|
+
}
|
|
2329
|
+
const add_feature_schema = {
|
|
2330
|
+
name: "extension_add_feature",
|
|
2331
|
+
description: "Add a new feature surface to an existing extension. Generates the required files and updates manifest.json. Sources patterns from the extension.dev template catalog.",
|
|
2332
|
+
inputSchema: {
|
|
2333
|
+
type: "object",
|
|
2334
|
+
properties: {
|
|
2335
|
+
projectPath: {
|
|
2336
|
+
type: "string",
|
|
2337
|
+
description: "Path to the extension project root"
|
|
2338
|
+
},
|
|
2339
|
+
feature: {
|
|
2340
|
+
type: "string",
|
|
2341
|
+
enum: [
|
|
2342
|
+
"sidebar",
|
|
2343
|
+
"popup",
|
|
2344
|
+
"options",
|
|
2345
|
+
"content-script",
|
|
2346
|
+
"background",
|
|
2347
|
+
"newtab",
|
|
2348
|
+
"devtools"
|
|
2349
|
+
],
|
|
2350
|
+
description: "Feature surface to add"
|
|
2351
|
+
},
|
|
2352
|
+
framework: {
|
|
2353
|
+
type: "string",
|
|
2354
|
+
enum: [
|
|
2355
|
+
"react",
|
|
2356
|
+
"vue",
|
|
2357
|
+
"svelte",
|
|
2358
|
+
"preact",
|
|
2359
|
+
"vanilla"
|
|
2360
|
+
],
|
|
2361
|
+
default: "react"
|
|
2362
|
+
}
|
|
2363
|
+
},
|
|
2364
|
+
required: [
|
|
2365
|
+
"projectPath",
|
|
2366
|
+
"feature"
|
|
2367
|
+
]
|
|
2368
|
+
}
|
|
2369
|
+
};
|
|
2370
|
+
const FEATURE_TEMPLATE_MAP = {
|
|
2371
|
+
sidebar: {
|
|
2372
|
+
react: "sidebar-shadcn",
|
|
2373
|
+
vanilla: "sidebar",
|
|
2374
|
+
vue: "sidebar",
|
|
2375
|
+
svelte: "sidebar",
|
|
2376
|
+
preact: "sidebar"
|
|
2377
|
+
},
|
|
2378
|
+
"content-script": {
|
|
2379
|
+
react: "content-react",
|
|
2380
|
+
vue: "content-vue",
|
|
2381
|
+
svelte: "content-svelte",
|
|
2382
|
+
preact: "content-preact",
|
|
2383
|
+
vanilla: "content"
|
|
2384
|
+
},
|
|
2385
|
+
popup: {
|
|
2386
|
+
react: "action",
|
|
2387
|
+
vanilla: "action",
|
|
2388
|
+
vue: "action",
|
|
2389
|
+
svelte: "action",
|
|
2390
|
+
preact: "action"
|
|
2391
|
+
},
|
|
2392
|
+
newtab: {
|
|
2393
|
+
react: "new-react",
|
|
2394
|
+
vue: "new-vue",
|
|
2395
|
+
svelte: "new-svelte",
|
|
2396
|
+
preact: "new-preact",
|
|
2397
|
+
vanilla: "new"
|
|
2398
|
+
},
|
|
2399
|
+
background: {
|
|
2400
|
+
react: "javascript",
|
|
2401
|
+
vanilla: "javascript",
|
|
2402
|
+
vue: "javascript",
|
|
2403
|
+
svelte: "javascript",
|
|
2404
|
+
preact: "javascript"
|
|
2405
|
+
},
|
|
2406
|
+
options: {
|
|
2407
|
+
react: "javascript",
|
|
2408
|
+
vanilla: "javascript",
|
|
2409
|
+
vue: "javascript",
|
|
2410
|
+
svelte: "javascript",
|
|
2411
|
+
preact: "javascript"
|
|
2412
|
+
},
|
|
2413
|
+
devtools: {
|
|
2414
|
+
react: "javascript",
|
|
2415
|
+
vanilla: "javascript",
|
|
2416
|
+
vue: "javascript",
|
|
2417
|
+
svelte: "javascript",
|
|
2418
|
+
preact: "javascript"
|
|
2419
|
+
}
|
|
2420
|
+
};
|
|
2421
|
+
const MANIFEST_ADDITIONS = {
|
|
2422
|
+
sidebar: {
|
|
2423
|
+
"chromium:side_panel": {
|
|
2424
|
+
default_path: "sidebar/index.html"
|
|
2425
|
+
},
|
|
2426
|
+
"firefox:sidebar_action": {
|
|
2427
|
+
default_panel: "sidebar/index.html"
|
|
2428
|
+
},
|
|
2429
|
+
"chromium:permissions": [
|
|
2430
|
+
"sidePanel"
|
|
2431
|
+
]
|
|
2432
|
+
},
|
|
2433
|
+
popup: {
|
|
2434
|
+
"chromium:action": {
|
|
2435
|
+
default_popup: "action/index.html",
|
|
2436
|
+
default_title: "Extension Popup"
|
|
2437
|
+
},
|
|
2438
|
+
"firefox:browser_action": {
|
|
2439
|
+
default_popup: "action/index.html",
|
|
2440
|
+
default_title: "Extension Popup"
|
|
2441
|
+
}
|
|
2442
|
+
},
|
|
2443
|
+
"content-script": {
|
|
2444
|
+
content_scripts: [
|
|
2445
|
+
{
|
|
2446
|
+
matches: [
|
|
2447
|
+
"<all_urls>"
|
|
2448
|
+
],
|
|
2449
|
+
js: [
|
|
2450
|
+
"content/scripts.ts"
|
|
2451
|
+
],
|
|
2452
|
+
css: [
|
|
2453
|
+
"content/styles.css"
|
|
2454
|
+
]
|
|
2455
|
+
}
|
|
2456
|
+
]
|
|
2457
|
+
},
|
|
2458
|
+
newtab: {
|
|
2459
|
+
chrome_url_overrides: {
|
|
2460
|
+
newtab: "newtab/index.html"
|
|
2461
|
+
}
|
|
2462
|
+
},
|
|
2463
|
+
options: {
|
|
2464
|
+
options_ui: {
|
|
2465
|
+
page: "options/index.html",
|
|
2466
|
+
open_in_tab: true
|
|
2467
|
+
}
|
|
2468
|
+
},
|
|
2469
|
+
background: {
|
|
2470
|
+
background: {
|
|
2471
|
+
"chromium:service_worker": "background.ts",
|
|
2472
|
+
"firefox:scripts": [
|
|
2473
|
+
"background.ts"
|
|
2474
|
+
]
|
|
2475
|
+
}
|
|
2476
|
+
},
|
|
2477
|
+
devtools: {
|
|
2478
|
+
devtools_page: "devtools/index.html"
|
|
2479
|
+
}
|
|
2480
|
+
};
|
|
2481
|
+
async function add_feature_handler(args) {
|
|
2482
|
+
const framework = args.framework ?? "react";
|
|
2483
|
+
const projectPath = external_node_path_default().resolve(args.projectPath);
|
|
2484
|
+
const srcDir = external_node_path_default().join(projectPath, "src");
|
|
2485
|
+
const manifestPath = external_node_path_default().join(srcDir, "manifest.json");
|
|
2486
|
+
if (!external_node_fs_default().existsSync(manifestPath)) return JSON.stringify({
|
|
2487
|
+
error: `No manifest.json found at ${manifestPath}`,
|
|
2488
|
+
hint: "Ensure projectPath points to an extension project root with src/manifest.json"
|
|
2489
|
+
});
|
|
2490
|
+
const templateSlug = FEATURE_TEMPLATE_MAP[args.feature]?.[framework];
|
|
2491
|
+
if (!templateSlug) return JSON.stringify({
|
|
2492
|
+
error: `No reference template for feature "${args.feature}" with framework "${framework}"`
|
|
2493
|
+
});
|
|
2494
|
+
const template = await getTemplateBySlug(templateSlug);
|
|
2495
|
+
const referenceFiles = template?.keyFiles ?? template?.files ?? [];
|
|
2496
|
+
const featureDir = "content-script" === args.feature ? "content" : args.feature;
|
|
2497
|
+
const filesToCreate = [];
|
|
2498
|
+
const manifestUpdates = MANIFEST_ADDITIONS[args.feature] ?? {};
|
|
2499
|
+
if ([
|
|
2500
|
+
"sidebar",
|
|
2501
|
+
"popup",
|
|
2502
|
+
"newtab",
|
|
2503
|
+
"options",
|
|
2504
|
+
"devtools"
|
|
2505
|
+
].includes(args.feature)) {
|
|
2506
|
+
filesToCreate.push({
|
|
2507
|
+
path: `src/${featureDir}/index.html`,
|
|
2508
|
+
hint: "HTML entry point"
|
|
2509
|
+
}, {
|
|
2510
|
+
path: `src/${featureDir}/scripts.${"vanilla" === framework ? "ts" : "tsx"}`,
|
|
2511
|
+
hint: "vanilla" === framework ? "Script entry point" : `${framework} mount point`
|
|
2512
|
+
}, {
|
|
2513
|
+
path: `src/${featureDir}/styles.css`,
|
|
2514
|
+
hint: "Stylesheet"
|
|
2515
|
+
});
|
|
2516
|
+
if ("vanilla" !== framework) filesToCreate.push({
|
|
2517
|
+
path: `src/${featureDir}/${featureDir.charAt(0).toUpperCase() + featureDir.slice(1)}App.tsx`,
|
|
2518
|
+
hint: `Main ${framework} component`
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
if ("content-script" === args.feature) filesToCreate.push({
|
|
2522
|
+
path: "src/content/scripts.ts",
|
|
2523
|
+
hint: "Content script entry point"
|
|
2524
|
+
}, {
|
|
2525
|
+
path: "src/content/styles.css",
|
|
2526
|
+
hint: "Content script styles"
|
|
2527
|
+
});
|
|
2528
|
+
if ("background" === args.feature) filesToCreate.push({
|
|
2529
|
+
path: "src/background.ts",
|
|
2530
|
+
hint: "Background service worker / script"
|
|
2531
|
+
});
|
|
2532
|
+
const conflicts = filesToCreate.filter((f)=>external_node_fs_default().existsSync(external_node_path_default().join(projectPath, f.path)));
|
|
2533
|
+
return JSON.stringify({
|
|
2534
|
+
feature: args.feature,
|
|
2535
|
+
framework,
|
|
2536
|
+
referenceTemplate: {
|
|
2537
|
+
slug: templateSlug,
|
|
2538
|
+
repositoryUrl: `https://github.com/extension-js/examples/tree/main/examples/${templateSlug}`,
|
|
2539
|
+
referenceFiles: referenceFiles.filter((f)=>f.includes(featureDir) || f.includes("manifest"))
|
|
2540
|
+
},
|
|
2541
|
+
manifestUpdates,
|
|
2542
|
+
filesToCreate: filesToCreate.map((f)=>({
|
|
2543
|
+
...f,
|
|
2544
|
+
exists: external_node_fs_default().existsSync(external_node_path_default().join(projectPath, f.path))
|
|
2545
|
+
})),
|
|
2546
|
+
conflicts: conflicts.map((c)=>c.path),
|
|
2547
|
+
instructions: [
|
|
2548
|
+
`1. Add these fields to your src/manifest.json:\n${JSON.stringify(manifestUpdates, null, 2)}`,
|
|
2549
|
+
"2. Create the following files in your project:",
|
|
2550
|
+
...filesToCreate.map((f)=>` - ${f.path} (${f.hint})`),
|
|
2551
|
+
"sidebar" === args.feature ? "3. Add background.ts to handle sidebar open: chromium uses chrome.sidePanel.setPanelBehavior, firefox uses browser.sidebarAction.open()" : "",
|
|
2552
|
+
`4. Reference template source: https://github.com/extension-js/examples/tree/main/examples/${templateSlug}/src`,
|
|
2553
|
+
"5. Run npm run dev to test"
|
|
2554
|
+
].filter(Boolean),
|
|
2555
|
+
hint: conflicts.length ? `Warning: ${conflicts.length} file(s) already exist and would be overwritten.` : "No conflicts detected. Safe to create all files."
|
|
2556
|
+
});
|
|
2557
|
+
}
|
|
2558
|
+
const external_extension_install_namespaceObject = require("extension-install");
|
|
2559
|
+
const install_browser_schema = {
|
|
2560
|
+
name: "extension_install_browser",
|
|
2561
|
+
description: "Install a managed browser binary for extension testing. Useful in CI, Docker, or fresh environments where browsers are not pre-installed.",
|
|
2562
|
+
inputSchema: {
|
|
2563
|
+
type: "object",
|
|
2564
|
+
properties: {
|
|
2565
|
+
browser: {
|
|
2566
|
+
type: "string",
|
|
2567
|
+
enum: [
|
|
2568
|
+
"chrome",
|
|
2569
|
+
"chromium",
|
|
2570
|
+
"edge",
|
|
2571
|
+
"firefox"
|
|
2572
|
+
],
|
|
2573
|
+
description: "Browser to install"
|
|
2574
|
+
}
|
|
2575
|
+
},
|
|
2576
|
+
required: [
|
|
2577
|
+
"browser"
|
|
2578
|
+
]
|
|
2579
|
+
}
|
|
2580
|
+
};
|
|
2581
|
+
async function install_browser_handler(args) {
|
|
2582
|
+
const start = Date.now();
|
|
2583
|
+
try {
|
|
2584
|
+
await (0, external_extension_install_namespaceObject.extensionInstall)({
|
|
2585
|
+
browser: args.browser
|
|
2586
|
+
});
|
|
2587
|
+
return JSON.stringify({
|
|
2588
|
+
status: "installed",
|
|
2589
|
+
browser: args.browser,
|
|
2590
|
+
duration: Date.now() - start,
|
|
2591
|
+
hint: `Browser "${args.browser}" is now available. Use extension_dev or extension_start with --browser=${args.browser}.`
|
|
2592
|
+
});
|
|
2593
|
+
} catch (err) {
|
|
2594
|
+
return JSON.stringify({
|
|
2595
|
+
status: "error",
|
|
2596
|
+
browser: args.browser,
|
|
2597
|
+
message: err instanceof Error ? err.message : String(err),
|
|
2598
|
+
duration: Date.now() - start,
|
|
2599
|
+
hint: "edge" === args.browser ? "Edge installation on Linux may require elevated privileges. Try using Chrome or Chromium instead." : "Check network connectivity and disk space. You can also install browsers manually."
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
const list_browsers_schema = {
|
|
2604
|
+
name: "extension_list_browsers",
|
|
2605
|
+
description: "List managed browser binaries installed by the extension.dev platform. Shows what browsers are available in the managed cache without checking system-installed browsers.",
|
|
2606
|
+
inputSchema: {
|
|
2607
|
+
type: "object",
|
|
2608
|
+
properties: {}
|
|
2609
|
+
}
|
|
2610
|
+
};
|
|
2611
|
+
const BROWSER_NAMES = [
|
|
2612
|
+
"chrome",
|
|
2613
|
+
"chromium",
|
|
2614
|
+
"edge",
|
|
2615
|
+
"firefox"
|
|
2616
|
+
];
|
|
2617
|
+
function getDirSize(dir) {
|
|
2618
|
+
let total = 0;
|
|
2619
|
+
try {
|
|
2620
|
+
for (const entry of external_node_fs_namespaceObject.readdirSync(dir, {
|
|
2621
|
+
withFileTypes: true
|
|
2622
|
+
})){
|
|
2623
|
+
const full = external_node_path_namespaceObject.join(dir, entry.name);
|
|
2624
|
+
if (entry.isDirectory()) total += getDirSize(full);
|
|
2625
|
+
else try {
|
|
2626
|
+
total += external_node_fs_namespaceObject.statSync(full).size;
|
|
2627
|
+
} catch {}
|
|
2628
|
+
}
|
|
2629
|
+
} catch {}
|
|
2630
|
+
return total;
|
|
2631
|
+
}
|
|
2632
|
+
function list_browsers_formatBytes(bytes) {
|
|
2633
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
2634
|
+
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
2635
|
+
return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
2636
|
+
}
|
|
2637
|
+
async function list_browsers_handler() {
|
|
2638
|
+
const cacheRoot = (0, external_extension_install_namespaceObject.getManagedBrowsersCacheRoot)();
|
|
2639
|
+
const installed = [];
|
|
2640
|
+
for (const browser of BROWSER_NAMES){
|
|
2641
|
+
const browserDir = external_node_path_namespaceObject.join(cacheRoot, browser);
|
|
2642
|
+
if (external_node_fs_namespaceObject.existsSync(browserDir)) {
|
|
2643
|
+
const size = getDirSize(browserDir);
|
|
2644
|
+
installed.push({
|
|
2645
|
+
browser,
|
|
2646
|
+
path: browserDir,
|
|
2647
|
+
size,
|
|
2648
|
+
sizeFormatted: list_browsers_formatBytes(size),
|
|
2649
|
+
engine: "firefox" === browser ? "gecko" : "chromium"
|
|
2650
|
+
});
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
return JSON.stringify({
|
|
2654
|
+
cacheRoot,
|
|
2655
|
+
cacheExists: external_node_fs_namespaceObject.existsSync(cacheRoot),
|
|
2656
|
+
installed,
|
|
2657
|
+
availableToInstall: BROWSER_NAMES.filter((b)=>!installed.some((i)=>i.browser === b)),
|
|
2658
|
+
hint: 0 === installed.length ? "No managed browsers found. Use extension_install_browser to install one, or use a system-installed browser." : `${installed.length} managed browser(s) found. Use extension_detect_browsers for a full system scan.`
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2661
|
+
const external_node_util_namespaceObject = require("node:util");
|
|
2662
|
+
const execFileAsync = (0, external_node_util_namespaceObject.promisify)(external_node_child_process_namespaceObject.execFile);
|
|
2663
|
+
const detect_browsers_schema = {
|
|
2664
|
+
name: "extension_detect_browsers",
|
|
2665
|
+
description: "Detect which browsers are available for extension development. Checks both system-installed and managed browsers, returning paths and capabilities for each.",
|
|
2666
|
+
inputSchema: {
|
|
2667
|
+
type: "object",
|
|
2668
|
+
properties: {
|
|
2669
|
+
browsers: {
|
|
2670
|
+
type: "array",
|
|
2671
|
+
items: {
|
|
2672
|
+
type: "string",
|
|
2673
|
+
enum: [
|
|
2674
|
+
"chrome",
|
|
2675
|
+
"chromium",
|
|
2676
|
+
"edge",
|
|
2677
|
+
"firefox"
|
|
2678
|
+
]
|
|
2679
|
+
},
|
|
2680
|
+
description: "Browsers to check. If omitted, checks all."
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
};
|
|
2685
|
+
const ALL_BROWSERS = [
|
|
2686
|
+
"chrome",
|
|
2687
|
+
"chromium",
|
|
2688
|
+
"edge",
|
|
2689
|
+
"firefox"
|
|
2690
|
+
];
|
|
2691
|
+
const SYSTEM_PATHS = {
|
|
2692
|
+
darwin: {
|
|
2693
|
+
chrome: [
|
|
2694
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
2695
|
+
`${process.env.HOME}/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`
|
|
2696
|
+
],
|
|
2697
|
+
chromium: [
|
|
2698
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
2699
|
+
`${process.env.HOME}/Applications/Chromium.app/Contents/MacOS/Chromium`
|
|
2700
|
+
],
|
|
2701
|
+
edge: [
|
|
2702
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
2703
|
+
`${process.env.HOME}/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge`
|
|
2704
|
+
],
|
|
2705
|
+
firefox: [
|
|
2706
|
+
"/Applications/Firefox.app/Contents/MacOS/firefox",
|
|
2707
|
+
`${process.env.HOME}/Applications/Firefox.app/Contents/MacOS/firefox`
|
|
2708
|
+
]
|
|
2709
|
+
},
|
|
2710
|
+
linux: {
|
|
2711
|
+
chrome: [
|
|
2712
|
+
"/usr/bin/google-chrome",
|
|
2713
|
+
"/usr/bin/google-chrome-stable",
|
|
2714
|
+
"/usr/local/bin/google-chrome"
|
|
2715
|
+
],
|
|
2716
|
+
chromium: [
|
|
2717
|
+
"/usr/bin/chromium",
|
|
2718
|
+
"/usr/bin/chromium-browser",
|
|
2719
|
+
"/snap/bin/chromium"
|
|
2720
|
+
],
|
|
2721
|
+
edge: [
|
|
2722
|
+
"/usr/bin/microsoft-edge",
|
|
2723
|
+
"/usr/bin/microsoft-edge-stable",
|
|
2724
|
+
"/opt/microsoft/msedge/msedge"
|
|
2725
|
+
],
|
|
2726
|
+
firefox: [
|
|
2727
|
+
"/usr/bin/firefox",
|
|
2728
|
+
"/snap/bin/firefox",
|
|
2729
|
+
"/usr/lib/firefox/firefox"
|
|
2730
|
+
]
|
|
2731
|
+
},
|
|
2732
|
+
win32: {
|
|
2733
|
+
chrome: [
|
|
2734
|
+
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
2735
|
+
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe"
|
|
2736
|
+
],
|
|
2737
|
+
chromium: [
|
|
2738
|
+
"C:\\Program Files\\Chromium\\Application\\chrome.exe"
|
|
2739
|
+
],
|
|
2740
|
+
edge: [
|
|
2741
|
+
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
2742
|
+
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe"
|
|
2743
|
+
],
|
|
2744
|
+
firefox: [
|
|
2745
|
+
"C:\\Program Files\\Mozilla Firefox\\firefox.exe",
|
|
2746
|
+
"C:\\Program Files (x86)\\Mozilla Firefox\\firefox.exe"
|
|
2747
|
+
]
|
|
2748
|
+
}
|
|
2749
|
+
};
|
|
2750
|
+
function resolveCacheRoot() {
|
|
2751
|
+
const explicit = String(process.env.EXT_BROWSERS_CACHE_DIR || "").trim();
|
|
2752
|
+
if (explicit) return external_node_path_default().resolve(explicit);
|
|
2753
|
+
const isWin = "win32" === process.platform;
|
|
2754
|
+
const isMac = "darwin" === process.platform;
|
|
2755
|
+
if (isWin) {
|
|
2756
|
+
const local = String(process.env.LOCALAPPDATA || "").trim();
|
|
2757
|
+
if (local) return external_node_path_default().join(local, "extension.js", "browsers");
|
|
2758
|
+
const userProfile = String(process.env.USERPROFILE || "").trim();
|
|
2759
|
+
if (userProfile) return external_node_path_default().join(userProfile, "AppData", "Local", "extension.js", "browsers");
|
|
2760
|
+
return external_node_path_default().resolve(process.cwd(), ".cache", "extension.js", "browsers");
|
|
2761
|
+
}
|
|
2762
|
+
if (isMac) {
|
|
2763
|
+
const home = String(process.env.HOME || "").trim();
|
|
2764
|
+
if (home) return external_node_path_default().join(home, "Library", "Caches", "extension.js", "browsers");
|
|
2765
|
+
return external_node_path_default().resolve(process.cwd(), ".cache", "extension.js", "browsers");
|
|
2766
|
+
}
|
|
2767
|
+
const xdg = String(process.env.XDG_CACHE_HOME || "").trim();
|
|
2768
|
+
if (xdg) return external_node_path_default().join(xdg, "extension.js", "browsers");
|
|
2769
|
+
const home = String(process.env.HOME || "").trim();
|
|
2770
|
+
if (home) return external_node_path_default().join(home, ".cache", "extension.js", "browsers");
|
|
2771
|
+
return external_node_path_default().resolve(process.cwd(), ".cache", "extension.js", "browsers");
|
|
2772
|
+
}
|
|
2773
|
+
function findManagedBinary(browser) {
|
|
2774
|
+
const browserDir = external_node_path_default().join(resolveCacheRoot(), browser);
|
|
2775
|
+
if (!external_node_fs_default().existsSync(browserDir)) return null;
|
|
2776
|
+
const execNames = {
|
|
2777
|
+
chrome: [
|
|
2778
|
+
"chrome",
|
|
2779
|
+
"chrome.exe",
|
|
2780
|
+
"Google Chrome for Testing"
|
|
2781
|
+
],
|
|
2782
|
+
chromium: [
|
|
2783
|
+
"chrome",
|
|
2784
|
+
"chromium",
|
|
2785
|
+
"chrome.exe",
|
|
2786
|
+
"chromium.exe",
|
|
2787
|
+
"Chromium.app"
|
|
2788
|
+
],
|
|
2789
|
+
edge: [
|
|
2790
|
+
"msedge",
|
|
2791
|
+
"msedge.exe",
|
|
2792
|
+
"microsoft-edge",
|
|
2793
|
+
"Microsoft Edge"
|
|
2794
|
+
],
|
|
2795
|
+
firefox: [
|
|
2796
|
+
"firefox",
|
|
2797
|
+
"firefox.exe",
|
|
2798
|
+
"Firefox.app"
|
|
2799
|
+
]
|
|
2800
|
+
};
|
|
2801
|
+
const names = execNames[browser] ?? [];
|
|
2802
|
+
function search(dir, depth) {
|
|
2803
|
+
if (depth > 4) return null;
|
|
2804
|
+
try {
|
|
2805
|
+
for (const entry of external_node_fs_default().readdirSync(dir, {
|
|
2806
|
+
withFileTypes: true
|
|
2807
|
+
})){
|
|
2808
|
+
const full = external_node_path_default().join(dir, entry.name);
|
|
2809
|
+
if (entry.isFile() && names.includes(entry.name)) return full;
|
|
2810
|
+
if (entry.isDirectory() && depth < 4) {
|
|
2811
|
+
const found = search(full, depth + 1);
|
|
2812
|
+
if (found) return found;
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
} catch {}
|
|
2816
|
+
return null;
|
|
2817
|
+
}
|
|
2818
|
+
return search(browserDir, 0);
|
|
2819
|
+
}
|
|
2820
|
+
function findSystemBinary(browser) {
|
|
2821
|
+
const platform = process.platform;
|
|
2822
|
+
const paths = SYSTEM_PATHS[platform]?.[browser] ?? [];
|
|
2823
|
+
for (const p of paths)if (external_node_fs_default().existsSync(p)) return p;
|
|
2824
|
+
return null;
|
|
2825
|
+
}
|
|
2826
|
+
async function getVersion(binaryPath, browser) {
|
|
2827
|
+
try {
|
|
2828
|
+
const flag = "firefox" === browser ? "--version" : "--version";
|
|
2829
|
+
const { stdout } = await execFileAsync(binaryPath, [
|
|
2830
|
+
flag
|
|
2831
|
+
], {
|
|
2832
|
+
timeout: 5000,
|
|
2833
|
+
env: {
|
|
2834
|
+
...process.env
|
|
2835
|
+
}
|
|
2836
|
+
});
|
|
2837
|
+
const match = stdout.match(/[\d]+\.[\d]+[\d.]*/);
|
|
2838
|
+
return match ? match[0] : stdout.trim().slice(0, 50);
|
|
2839
|
+
} catch {
|
|
2840
|
+
return null;
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
async function detect_browsers_handler(args) {
|
|
2844
|
+
const browsersToCheck = args.browsers ?? [
|
|
2845
|
+
...ALL_BROWSERS
|
|
2846
|
+
];
|
|
2847
|
+
const detected = [];
|
|
2848
|
+
const managed = {
|
|
2849
|
+
cacheRoot: resolveCacheRoot(),
|
|
2850
|
+
installed: []
|
|
2851
|
+
};
|
|
2852
|
+
for (const browser of ALL_BROWSERS){
|
|
2853
|
+
const browserDir = external_node_path_default().join(managed.cacheRoot, browser);
|
|
2854
|
+
if (external_node_fs_default().existsSync(browserDir)) managed.installed.push(browser);
|
|
2855
|
+
}
|
|
2856
|
+
for (const browser of browsersToCheck){
|
|
2857
|
+
const isGecko = "firefox" === browser;
|
|
2858
|
+
let binaryPath = findManagedBinary(browser);
|
|
2859
|
+
let source = "managed";
|
|
2860
|
+
if (!binaryPath) {
|
|
2861
|
+
binaryPath = findSystemBinary(browser);
|
|
2862
|
+
source = binaryPath ? "system" : "not_found";
|
|
2863
|
+
}
|
|
2864
|
+
let version = null;
|
|
2865
|
+
if (binaryPath) version = await getVersion(binaryPath, browser);
|
|
2866
|
+
detected.push({
|
|
2867
|
+
browser,
|
|
2868
|
+
binaryPath,
|
|
2869
|
+
source,
|
|
2870
|
+
engine: isGecko ? "gecko" : "chromium",
|
|
2871
|
+
version,
|
|
2872
|
+
cdpSupport: !isGecko,
|
|
2873
|
+
rdpSupport: isGecko
|
|
2874
|
+
});
|
|
2875
|
+
}
|
|
2876
|
+
const available = detected.filter((d)=>"not_found" !== d.source);
|
|
2877
|
+
const missing = detected.filter((d)=>"not_found" === d.source);
|
|
2878
|
+
return JSON.stringify({
|
|
2879
|
+
detected,
|
|
2880
|
+
managed,
|
|
2881
|
+
summary: {
|
|
2882
|
+
available: available.map((d)=>d.browser),
|
|
2883
|
+
missing: missing.map((d)=>d.browser)
|
|
2884
|
+
},
|
|
2885
|
+
hint: missing.length ? `Missing browser(s): ${missing.map((d)=>d.browser).join(", ")}. Use extension_install_browser to install them.` : "All requested browsers are available."
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
const tools = [
|
|
2889
|
+
create_namespaceObject,
|
|
2890
|
+
list_templates_namespaceObject,
|
|
2891
|
+
build_namespaceObject,
|
|
2892
|
+
dev_namespaceObject,
|
|
2893
|
+
start_namespaceObject,
|
|
2894
|
+
preview_namespaceObject,
|
|
2895
|
+
get_template_source_namespaceObject,
|
|
2896
|
+
manifest_validate_namespaceObject,
|
|
2897
|
+
inspect_namespaceObject,
|
|
2898
|
+
source_inspect_namespaceObject,
|
|
2899
|
+
logs_namespaceObject,
|
|
2900
|
+
eval_namespaceObject,
|
|
2901
|
+
storage_namespaceObject,
|
|
2902
|
+
reload_namespaceObject,
|
|
2903
|
+
open_namespaceObject,
|
|
2904
|
+
dom_inspect_namespaceObject,
|
|
2905
|
+
publish_namespaceObject,
|
|
2906
|
+
wait_namespaceObject,
|
|
2907
|
+
add_feature_namespaceObject,
|
|
2908
|
+
install_browser_namespaceObject,
|
|
2909
|
+
list_browsers_namespaceObject,
|
|
2910
|
+
detect_browsers_namespaceObject
|
|
2911
|
+
];
|
|
2912
|
+
const toolMap = new Map();
|
|
2913
|
+
for (const tool of tools)toolMap.set(tool.schema.name, tool);
|
|
2914
|
+
async function startServer() {
|
|
2915
|
+
const server = new index_js_namespaceObject.Server({
|
|
2916
|
+
name: "extension-dev",
|
|
2917
|
+
version: "3.17.0-canary.1779905934.1151df3"
|
|
2918
|
+
}, {
|
|
2919
|
+
capabilities: {
|
|
2920
|
+
tools: {}
|
|
2921
|
+
}
|
|
2922
|
+
});
|
|
2923
|
+
server.setRequestHandler(types_js_namespaceObject.ListToolsRequestSchema, async ()=>({
|
|
2924
|
+
tools: tools.map((t)=>({
|
|
2925
|
+
name: t.schema.name,
|
|
2926
|
+
description: t.schema.description,
|
|
2927
|
+
inputSchema: t.schema.inputSchema
|
|
2928
|
+
}))
|
|
2929
|
+
}));
|
|
2930
|
+
server.setRequestHandler(types_js_namespaceObject.CallToolRequestSchema, async (request)=>{
|
|
2931
|
+
const { name, arguments: args } = request.params;
|
|
2932
|
+
const tool = toolMap.get(name);
|
|
2933
|
+
if (!tool) return {
|
|
2934
|
+
content: [
|
|
2935
|
+
{
|
|
2936
|
+
type: "text",
|
|
2937
|
+
text: JSON.stringify({
|
|
2938
|
+
error: `Unknown tool: ${name}`,
|
|
2939
|
+
availableTools: tools.map((t)=>t.schema.name)
|
|
2940
|
+
})
|
|
2941
|
+
}
|
|
2942
|
+
],
|
|
2943
|
+
isError: true
|
|
2944
|
+
};
|
|
2945
|
+
try {
|
|
2946
|
+
const result = await tool.handler(args ?? {});
|
|
2947
|
+
return {
|
|
2948
|
+
content: [
|
|
2949
|
+
{
|
|
2950
|
+
type: "text",
|
|
2951
|
+
text: result
|
|
2952
|
+
}
|
|
2953
|
+
]
|
|
2954
|
+
};
|
|
2955
|
+
} catch (err) {
|
|
2956
|
+
return {
|
|
2957
|
+
content: [
|
|
2958
|
+
{
|
|
2959
|
+
type: "text",
|
|
2960
|
+
text: JSON.stringify({
|
|
2961
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2962
|
+
tool: name
|
|
2963
|
+
})
|
|
2964
|
+
}
|
|
2965
|
+
],
|
|
2966
|
+
isError: true
|
|
2967
|
+
};
|
|
2968
|
+
}
|
|
2969
|
+
});
|
|
2970
|
+
const transport = new stdio_js_namespaceObject.StdioServerTransport();
|
|
2971
|
+
await server.connect(transport);
|
|
2972
|
+
}
|
|
2973
|
+
exports.startServer = __webpack_exports__.startServer;
|
|
2974
|
+
for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
2975
|
+
"startServer"
|
|
2976
|
+
].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
|
|
2977
|
+
Object.defineProperty(exports, '__esModule', {
|
|
2978
|
+
value: true
|
|
2979
|
+
});
|