@extension.dev/mcp 4.6.0 → 4.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +16 -2
- package/README.md +3 -3
- package/dist/module.js +795 -347
- package/dist/src/lib/cdp-port.d.ts +1 -0
- package/dist/src/lib/credentials.d.ts +2 -0
- package/dist/src/lib/device-flow.d.ts +41 -0
- package/dist/src/lib/login-flow.d.ts +23 -3
- package/dist/src/tools/deploy.d.ts +14 -70
- package/dist/src/tools/doctor.d.ts +1 -2
- package/dist/src/tools/publish.d.ts +1 -6
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/module.js
CHANGED
|
@@ -2,11 +2,10 @@ import { __webpack_require__ } from "./rslib-runtime.js";
|
|
|
2
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import node_fs, { existsSync, readdirSync, statSync } from "node:fs";
|
|
5
6
|
import node_path, { join } from "node:path";
|
|
6
7
|
import { extensionCreate } from "extension-create";
|
|
7
|
-
import node_fs, { existsSync, readdirSync, statSync } from "node:fs";
|
|
8
8
|
import node_os from "node:os";
|
|
9
|
-
import { extensionBuild } from "extension-develop";
|
|
10
9
|
import cross_spawn from "cross-spawn";
|
|
11
10
|
import { filterKeysForThisBrowser } from "browser-extension-manifest-fields";
|
|
12
11
|
import ws from "ws";
|
|
@@ -35,7 +34,6 @@ __webpack_require__.d(create_namespaceObject, {
|
|
|
35
34
|
var deploy_namespaceObject = {};
|
|
36
35
|
__webpack_require__.r(deploy_namespaceObject);
|
|
37
36
|
__webpack_require__.d(deploy_namespaceObject, {
|
|
38
|
-
buildDeployArgs: ()=>buildDeployArgs,
|
|
39
37
|
handler: ()=>deploy_handler,
|
|
40
38
|
schema: ()=>deploy_schema
|
|
41
39
|
});
|
|
@@ -201,7 +199,33 @@ __webpack_require__.d(whoami_namespaceObject, {
|
|
|
201
199
|
handler: ()=>whoami_handler,
|
|
202
200
|
schema: ()=>whoami_schema
|
|
203
201
|
});
|
|
204
|
-
var package_namespaceObject = JSON.parse('{"rE":"4.
|
|
202
|
+
var package_namespaceObject = JSON.parse('{"rE":"4.6.0","El":{"OP":"^4.0.11"}}');
|
|
203
|
+
function detectPackageManager(projectPath) {
|
|
204
|
+
const byLockfile = [
|
|
205
|
+
[
|
|
206
|
+
"bun.lock",
|
|
207
|
+
"bun"
|
|
208
|
+
],
|
|
209
|
+
[
|
|
210
|
+
"bun.lockb",
|
|
211
|
+
"bun"
|
|
212
|
+
],
|
|
213
|
+
[
|
|
214
|
+
"pnpm-lock.yaml",
|
|
215
|
+
"pnpm"
|
|
216
|
+
],
|
|
217
|
+
[
|
|
218
|
+
"yarn.lock",
|
|
219
|
+
"yarn"
|
|
220
|
+
],
|
|
221
|
+
[
|
|
222
|
+
"package-lock.json",
|
|
223
|
+
"npm"
|
|
224
|
+
]
|
|
225
|
+
];
|
|
226
|
+
for (const [lockfile, pm] of byLockfile)if (node_fs.existsSync(node_path.join(projectPath, lockfile))) return pm;
|
|
227
|
+
return "npm";
|
|
228
|
+
}
|
|
205
229
|
const create_schema = {
|
|
206
230
|
name: "extension_create",
|
|
207
231
|
description: "Create a new browser extension project from a template in the extension.dev template catalog. Use extension_list_templates to see available options.",
|
|
@@ -219,7 +243,7 @@ const create_schema = {
|
|
|
219
243
|
template: {
|
|
220
244
|
type: "string",
|
|
221
245
|
default: "typescript",
|
|
222
|
-
description: "Template slug from the extension.dev template catalog (e.g. 'react', '
|
|
246
|
+
description: "Template slug from the extension.dev template catalog (e.g. 'react', 'ai-claude', 'content-vue'). Use extension_list_templates to discover options."
|
|
223
247
|
},
|
|
224
248
|
install: {
|
|
225
249
|
type: "boolean",
|
|
@@ -235,30 +259,47 @@ const create_schema = {
|
|
|
235
259
|
async function create_handler(args) {
|
|
236
260
|
const start = Date.now();
|
|
237
261
|
const projectInput = args.parentDir ? node_path.resolve(args.parentDir, args.projectName) : args.projectName;
|
|
262
|
+
const logLines = [];
|
|
263
|
+
const capture = (stream)=>(...parts)=>{
|
|
264
|
+
const line = parts.map((p)=>"string" == typeof p ? p : String(p)).join(" ").trim();
|
|
265
|
+
if (line) logLines.push("error" === stream ? `[error] ${line}` : line);
|
|
266
|
+
};
|
|
267
|
+
const logTail = (max = 20)=>logLines.slice(-max);
|
|
238
268
|
try {
|
|
239
269
|
const result = await extensionCreate(projectInput, {
|
|
240
270
|
template: args.template ?? "typescript",
|
|
241
271
|
install: args.install ?? true,
|
|
242
272
|
logger: {
|
|
243
|
-
log: ()
|
|
244
|
-
error: ()
|
|
273
|
+
log: capture("log"),
|
|
274
|
+
error: capture("error")
|
|
245
275
|
}
|
|
246
276
|
});
|
|
277
|
+
const packageManager = result.depsInstalled ? detectPackageManager(result.projectPath) : "npm";
|
|
278
|
+
const runDev = `${packageManager} run dev`;
|
|
247
279
|
return JSON.stringify({
|
|
248
280
|
projectPath: result.projectPath,
|
|
249
281
|
projectName: result.projectName,
|
|
250
282
|
template: result.template,
|
|
251
283
|
depsInstalled: result.depsInstalled,
|
|
284
|
+
packageManager: result.depsInstalled ? packageManager : null,
|
|
252
285
|
duration: Date.now() - start,
|
|
253
|
-
nextSteps: [
|
|
286
|
+
nextSteps: result.depsInstalled ? [
|
|
254
287
|
`cd ${result.projectPath}`,
|
|
288
|
+
runDev
|
|
289
|
+
] : [
|
|
290
|
+
`cd ${result.projectPath}`,
|
|
291
|
+
"npm install",
|
|
255
292
|
"npm run dev"
|
|
256
|
-
]
|
|
293
|
+
],
|
|
294
|
+
...result.depsInstalled ? {} : {
|
|
295
|
+
warnings: logTail()
|
|
296
|
+
}
|
|
257
297
|
});
|
|
258
298
|
} catch (err) {
|
|
259
299
|
return JSON.stringify({
|
|
260
300
|
error: err instanceof Error ? err.message : String(err),
|
|
261
|
-
duration: Date.now() - start
|
|
301
|
+
duration: Date.now() - start,
|
|
302
|
+
log: logTail()
|
|
262
303
|
});
|
|
263
304
|
}
|
|
264
305
|
}
|
|
@@ -299,8 +340,49 @@ async function listTemplates(filters) {
|
|
|
299
340
|
if (filters?.tags?.length) templates = templates.filter((t)=>filters.tags.some((tag)=>t.tags?.includes(tag) || t.aiRecommendFor?.includes(tag)));
|
|
300
341
|
if (filters?.featured) templates = templates.filter((t)=>t.featured);
|
|
301
342
|
if (filters?.query) {
|
|
302
|
-
const
|
|
303
|
-
|
|
343
|
+
const phrase = filters.query.toLowerCase().trim();
|
|
344
|
+
const STOP = new Set([
|
|
345
|
+
"the",
|
|
346
|
+
"a",
|
|
347
|
+
"an",
|
|
348
|
+
"and",
|
|
349
|
+
"or",
|
|
350
|
+
"for",
|
|
351
|
+
"with",
|
|
352
|
+
"that",
|
|
353
|
+
"this",
|
|
354
|
+
"to",
|
|
355
|
+
"of",
|
|
356
|
+
"on",
|
|
357
|
+
"in",
|
|
358
|
+
"into",
|
|
359
|
+
"your",
|
|
360
|
+
"my",
|
|
361
|
+
"me",
|
|
362
|
+
"it",
|
|
363
|
+
"is"
|
|
364
|
+
]);
|
|
365
|
+
const tokens = phrase.split(/\s+/).filter((tok)=>tok.length >= 2 && !STOP.has(tok));
|
|
366
|
+
const bodyOf = (t)=>[
|
|
367
|
+
t.description,
|
|
368
|
+
...t.tags ?? [],
|
|
369
|
+
...t.useCases ?? [],
|
|
370
|
+
...t.aiPromptExamples ?? []
|
|
371
|
+
].join(" ").toLowerCase();
|
|
372
|
+
const scored = templates.map((t)=>{
|
|
373
|
+
const slug = t.slug.toLowerCase();
|
|
374
|
+
const body = bodyOf(t);
|
|
375
|
+
const hay = `${slug} ${body}`;
|
|
376
|
+
let score = 0;
|
|
377
|
+
if (phrase && hay.includes(phrase)) score += 100;
|
|
378
|
+
for (const tok of tokens)if (slug.includes(tok)) score += 3;
|
|
379
|
+
else if (body.includes(tok)) score += 1;
|
|
380
|
+
return {
|
|
381
|
+
t,
|
|
382
|
+
score
|
|
383
|
+
};
|
|
384
|
+
}).filter((entry)=>entry.score > 0).sort((a, b)=>b.score - a.score);
|
|
385
|
+
templates = scored.map((entry)=>entry.t);
|
|
304
386
|
}
|
|
305
387
|
return templates;
|
|
306
388
|
}
|
|
@@ -310,7 +392,7 @@ async function getTemplateBySlug(slug) {
|
|
|
310
392
|
}
|
|
311
393
|
const list_templates_schema = {
|
|
312
394
|
name: "extension_list_templates",
|
|
313
|
-
description: "List available extension templates from the extension.dev template catalog. Filter by surface, framework, or tags. Returns structured metadata from templates-meta.json.",
|
|
395
|
+
description: "List available extension templates from the extension.dev template catalog. Filter by surface, framework, or tags. Returns structured metadata from templates-meta.json. Note: 'framework' is the UI framework only (react/vue/svelte/preact/vanilla) - it is not the language. TypeScript and JavaScript templates live under slugs (e.g. 'typescript', 'content-typescript'); shadcn is a React variant ('sidebar-shadcn') and provider AIs are tagged 'ai' ('ai-chatgpt', 'ai-claude'). Reach those with query/tags/slug, not framework.",
|
|
314
396
|
inputSchema: {
|
|
315
397
|
type: "object",
|
|
316
398
|
properties: {
|
|
@@ -319,13 +401,10 @@ const list_templates_schema = {
|
|
|
319
401
|
enum: [
|
|
320
402
|
"content",
|
|
321
403
|
"sidebar",
|
|
322
|
-
"action",
|
|
323
404
|
"newtab",
|
|
324
|
-
"devtools",
|
|
325
|
-
"options",
|
|
326
405
|
"background"
|
|
327
406
|
],
|
|
328
|
-
description: "Filter by extension surface type"
|
|
407
|
+
description: "Filter by extension surface type. For a popup/action starter use the 'action' slug (query:'action'), not a surface filter."
|
|
329
408
|
},
|
|
330
409
|
framework: {
|
|
331
410
|
type: "string",
|
|
@@ -336,7 +415,7 @@ const list_templates_schema = {
|
|
|
336
415
|
"preact",
|
|
337
416
|
""
|
|
338
417
|
],
|
|
339
|
-
description: "Filter by UI framework (empty string = vanilla JS)"
|
|
418
|
+
description: "Filter by UI framework only (empty string = vanilla JS). Not a language filter - for TypeScript/JavaScript use query or slug."
|
|
340
419
|
},
|
|
341
420
|
tags: {
|
|
342
421
|
type: "array",
|
|
@@ -351,7 +430,7 @@ const list_templates_schema = {
|
|
|
351
430
|
},
|
|
352
431
|
query: {
|
|
353
432
|
type: "string",
|
|
354
|
-
description: "
|
|
433
|
+
description: "Keyword search across slug, description, tags, and useCases. Ranks by how many query words match, so a natural phrase works; single keywords are fine too."
|
|
355
434
|
}
|
|
356
435
|
}
|
|
357
436
|
}
|
|
@@ -375,112 +454,6 @@ async function list_templates_handler(args) {
|
|
|
375
454
|
templates: results
|
|
376
455
|
});
|
|
377
456
|
}
|
|
378
|
-
const build_schema = {
|
|
379
|
-
name: "extension_build",
|
|
380
|
-
description: "Build a browser extension for production. Outputs to dist/<browser>/. Optionally creates .zip for store submission.",
|
|
381
|
-
inputSchema: {
|
|
382
|
-
type: "object",
|
|
383
|
-
properties: {
|
|
384
|
-
projectPath: {
|
|
385
|
-
type: "string",
|
|
386
|
-
description: "Path to the extension project root"
|
|
387
|
-
},
|
|
388
|
-
browser: {
|
|
389
|
-
type: "string",
|
|
390
|
-
enum: [
|
|
391
|
-
"chrome",
|
|
392
|
-
"chromium",
|
|
393
|
-
"edge",
|
|
394
|
-
"brave",
|
|
395
|
-
"opera",
|
|
396
|
-
"vivaldi",
|
|
397
|
-
"yandex",
|
|
398
|
-
"firefox",
|
|
399
|
-
"waterfox",
|
|
400
|
-
"librewolf",
|
|
401
|
-
"safari",
|
|
402
|
-
"chromium-based",
|
|
403
|
-
"gecko-based",
|
|
404
|
-
"firefox-based",
|
|
405
|
-
"webkit-based"
|
|
406
|
-
],
|
|
407
|
-
default: "chrome",
|
|
408
|
-
description: "Target browser"
|
|
409
|
-
},
|
|
410
|
-
zip: {
|
|
411
|
-
type: "boolean",
|
|
412
|
-
default: false,
|
|
413
|
-
description: "Create a .zip file for store distribution"
|
|
414
|
-
},
|
|
415
|
-
zipSource: {
|
|
416
|
-
type: "boolean",
|
|
417
|
-
default: false,
|
|
418
|
-
description: "Include source code zip (required by some stores)"
|
|
419
|
-
},
|
|
420
|
-
zipFilename: {
|
|
421
|
-
type: "string",
|
|
422
|
-
description: "Custom .zip file name (defaults to name and version)"
|
|
423
|
-
},
|
|
424
|
-
polyfill: {
|
|
425
|
-
type: "boolean",
|
|
426
|
-
default: false,
|
|
427
|
-
description: "Apply cross-browser polyfill"
|
|
428
|
-
},
|
|
429
|
-
silent: {
|
|
430
|
-
type: "boolean",
|
|
431
|
-
default: false,
|
|
432
|
-
description: "Suppress build output"
|
|
433
|
-
},
|
|
434
|
-
mode: {
|
|
435
|
-
type: "string",
|
|
436
|
-
enum: [
|
|
437
|
-
"development",
|
|
438
|
-
"production",
|
|
439
|
-
"none"
|
|
440
|
-
],
|
|
441
|
-
default: "production",
|
|
442
|
-
description: "Bundler mode override (also sets NODE_ENV)"
|
|
443
|
-
}
|
|
444
|
-
},
|
|
445
|
-
required: [
|
|
446
|
-
"projectPath"
|
|
447
|
-
]
|
|
448
|
-
}
|
|
449
|
-
};
|
|
450
|
-
async function build_handler(args) {
|
|
451
|
-
const start = Date.now();
|
|
452
|
-
try {
|
|
453
|
-
const summary = await extensionBuild(args.projectPath, {
|
|
454
|
-
browser: args.browser ?? "chrome",
|
|
455
|
-
zip: args.zip ?? false,
|
|
456
|
-
zipSource: args.zipSource ?? false,
|
|
457
|
-
...args.zipFilename ? {
|
|
458
|
-
zipFilename: args.zipFilename
|
|
459
|
-
} : {},
|
|
460
|
-
...void 0 !== args.polyfill ? {
|
|
461
|
-
polyfill: args.polyfill
|
|
462
|
-
} : {},
|
|
463
|
-
...void 0 !== args.silent ? {
|
|
464
|
-
silent: args.silent
|
|
465
|
-
} : {},
|
|
466
|
-
...args.mode ? {
|
|
467
|
-
mode: args.mode
|
|
468
|
-
} : {},
|
|
469
|
-
exitOnError: false
|
|
470
|
-
});
|
|
471
|
-
return JSON.stringify({
|
|
472
|
-
success: true,
|
|
473
|
-
...summary,
|
|
474
|
-
duration: Date.now() - start
|
|
475
|
-
});
|
|
476
|
-
} catch (err) {
|
|
477
|
-
return JSON.stringify({
|
|
478
|
-
error: err instanceof Error ? err.message : String(err),
|
|
479
|
-
duration: Date.now() - start,
|
|
480
|
-
hint: "Check that the project has a valid src/manifest.json and all dependencies are installed."
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
457
|
const PINNED_CLI_VERSION = String(package_namespaceObject.El.OP ?? "latest").replace(/^[\^~]/, "");
|
|
485
458
|
function pinnedCliVersion() {
|
|
486
459
|
const override = String(process.env.EXTENSION_MCP_CLI_VERSION || "").trim();
|
|
@@ -562,6 +535,126 @@ function spawnExtensionCli(args, options) {
|
|
|
562
535
|
child.unref();
|
|
563
536
|
return child;
|
|
564
537
|
}
|
|
538
|
+
const build_schema = {
|
|
539
|
+
name: "extension_build",
|
|
540
|
+
description: "Build a browser extension for production. Outputs to dist/<browser>/. Optionally creates .zip for store submission.",
|
|
541
|
+
inputSchema: {
|
|
542
|
+
type: "object",
|
|
543
|
+
properties: {
|
|
544
|
+
projectPath: {
|
|
545
|
+
type: "string",
|
|
546
|
+
description: "Path to the extension project root"
|
|
547
|
+
},
|
|
548
|
+
browser: {
|
|
549
|
+
type: "string",
|
|
550
|
+
enum: [
|
|
551
|
+
"chrome",
|
|
552
|
+
"chromium",
|
|
553
|
+
"edge",
|
|
554
|
+
"brave",
|
|
555
|
+
"opera",
|
|
556
|
+
"vivaldi",
|
|
557
|
+
"yandex",
|
|
558
|
+
"firefox",
|
|
559
|
+
"waterfox",
|
|
560
|
+
"librewolf",
|
|
561
|
+
"safari",
|
|
562
|
+
"chromium-based",
|
|
563
|
+
"gecko-based",
|
|
564
|
+
"firefox-based",
|
|
565
|
+
"webkit-based"
|
|
566
|
+
],
|
|
567
|
+
default: "chrome",
|
|
568
|
+
description: "Target browser"
|
|
569
|
+
},
|
|
570
|
+
zip: {
|
|
571
|
+
type: "boolean",
|
|
572
|
+
default: false,
|
|
573
|
+
description: "Create a .zip file for store distribution"
|
|
574
|
+
},
|
|
575
|
+
zipSource: {
|
|
576
|
+
type: "boolean",
|
|
577
|
+
default: false,
|
|
578
|
+
description: "Include source code zip (required by some stores)"
|
|
579
|
+
},
|
|
580
|
+
zipFilename: {
|
|
581
|
+
type: "string",
|
|
582
|
+
description: "Custom .zip file name (defaults to name and version)"
|
|
583
|
+
},
|
|
584
|
+
polyfill: {
|
|
585
|
+
type: "boolean",
|
|
586
|
+
default: false,
|
|
587
|
+
description: "Apply cross-browser polyfill"
|
|
588
|
+
},
|
|
589
|
+
silent: {
|
|
590
|
+
type: "boolean",
|
|
591
|
+
default: false,
|
|
592
|
+
description: "Suppress build output"
|
|
593
|
+
},
|
|
594
|
+
mode: {
|
|
595
|
+
type: "string",
|
|
596
|
+
enum: [
|
|
597
|
+
"development",
|
|
598
|
+
"production",
|
|
599
|
+
"none"
|
|
600
|
+
],
|
|
601
|
+
default: "production",
|
|
602
|
+
description: "Bundler mode override (also sets NODE_ENV)"
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
required: [
|
|
606
|
+
"projectPath"
|
|
607
|
+
]
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
async function build_handler(args) {
|
|
611
|
+
const start = Date.now();
|
|
612
|
+
const browser = args.browser ?? "chrome";
|
|
613
|
+
const cliArgs = [
|
|
614
|
+
"build",
|
|
615
|
+
args.projectPath,
|
|
616
|
+
"--browser",
|
|
617
|
+
browser
|
|
618
|
+
];
|
|
619
|
+
if (args.zip) cliArgs.push("--zip");
|
|
620
|
+
if (args.zipSource) cliArgs.push("--zip-source");
|
|
621
|
+
if (args.zipFilename) cliArgs.push("--zip-filename", args.zipFilename);
|
|
622
|
+
if (args.polyfill) cliArgs.push("--polyfill");
|
|
623
|
+
if (args.silent) cliArgs.push("--silent");
|
|
624
|
+
if (args.mode) cliArgs.push("--mode", args.mode);
|
|
625
|
+
const { code, stdout, stderr } = await runExtensionCli(cliArgs, {
|
|
626
|
+
cwd: args.projectPath,
|
|
627
|
+
timeoutMs: 180000
|
|
628
|
+
});
|
|
629
|
+
const duration = Date.now() - start;
|
|
630
|
+
const out = (stdout ?? "").trim();
|
|
631
|
+
const lastLines = (text, n)=>text.split("\n").slice(-n).join("\n");
|
|
632
|
+
if (0 === code) {
|
|
633
|
+
const size = out.match(/Size:\s*([\d.]+\s*[kKmMgG]?B)/)?.[1];
|
|
634
|
+
const status = out.match(/Build Status:\s*(\w+)/)?.[1];
|
|
635
|
+
return JSON.stringify({
|
|
636
|
+
success: true,
|
|
637
|
+
browser,
|
|
638
|
+
...size ? {
|
|
639
|
+
size
|
|
640
|
+
} : {},
|
|
641
|
+
...status ? {
|
|
642
|
+
status
|
|
643
|
+
} : {},
|
|
644
|
+
zip: args.zip ?? false,
|
|
645
|
+
duration,
|
|
646
|
+
output: lastLines(out, 12)
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
const message = stderr.trim() || out || `extension build exited with code ${code}`;
|
|
650
|
+
return JSON.stringify({
|
|
651
|
+
success: false,
|
|
652
|
+
browser,
|
|
653
|
+
error: message.slice(0, 1200),
|
|
654
|
+
duration,
|
|
655
|
+
hint: "Check that the project has a valid src/manifest.json and its dependencies are installed (extension_dev auto-installs; build does not)."
|
|
656
|
+
});
|
|
657
|
+
}
|
|
565
658
|
const process_manager_sessions = new Map();
|
|
566
659
|
function sessionKey(projectPath, browser) {
|
|
567
660
|
return `${node_path.resolve(projectPath)}::${browser}`;
|
|
@@ -726,9 +819,20 @@ async function dev_handler(args) {
|
|
|
726
819
|
projectPath: args.projectPath,
|
|
727
820
|
status: "started",
|
|
728
821
|
hint: "Use extension_wait to check when the extension is fully loaded, then extension_source_inspect to inspect the live state. When you are done, call extension_stop to shut down the dev server and browser.",
|
|
729
|
-
earlyOutput: earlyOutput.slice(0, 500)
|
|
822
|
+
earlyOutput: denoiseEarlyOutput(earlyOutput).slice(0, 500)
|
|
730
823
|
});
|
|
731
824
|
}
|
|
825
|
+
function denoiseEarlyOutput(raw) {
|
|
826
|
+
const NOISE = [
|
|
827
|
+
/^npm warn Unknown project config/i,
|
|
828
|
+
/This will stop working in the next major version of npm/i,
|
|
829
|
+
/^npm warn config/i,
|
|
830
|
+
/V8: .*Invalid asm\.js/i,
|
|
831
|
+
/^\(node:\d+\) V8:/i,
|
|
832
|
+
/Use `node --trace-warnings/i
|
|
833
|
+
];
|
|
834
|
+
return raw.split("\n").filter((line)=>!NOISE.some((re)=>re.test(line.trim()))).join("\n").replace(/\n{2,}/g, "\n").trimStart();
|
|
835
|
+
}
|
|
732
836
|
const start_schema = {
|
|
733
837
|
name: "extension_start",
|
|
734
838
|
description: "Build the extension for production and immediately preview it in a browser. Combines build + preview in one step. No hot reload.",
|
|
@@ -1018,7 +1122,7 @@ const get_template_source_schema = {
|
|
|
1018
1122
|
properties: {
|
|
1019
1123
|
slug: {
|
|
1020
1124
|
type: "string",
|
|
1021
|
-
description: "Template slug (e.g. '
|
|
1125
|
+
description: "Template slug (e.g. 'ai-claude', 'content-react')"
|
|
1022
1126
|
},
|
|
1023
1127
|
files: {
|
|
1024
1128
|
type: "array",
|
|
@@ -1101,6 +1205,91 @@ function isChromiumFamily(browser) {
|
|
|
1101
1205
|
function isGeckoFamily(browser) {
|
|
1102
1206
|
return GECKO_FAMILY.has(browser);
|
|
1103
1207
|
}
|
|
1208
|
+
const KNOWN_PERMISSIONS = new Set([
|
|
1209
|
+
"activeTab",
|
|
1210
|
+
"alarms",
|
|
1211
|
+
"background",
|
|
1212
|
+
"bookmarks",
|
|
1213
|
+
"browsingData",
|
|
1214
|
+
"certificateProvider",
|
|
1215
|
+
"clipboardRead",
|
|
1216
|
+
"clipboardWrite",
|
|
1217
|
+
"contentSettings",
|
|
1218
|
+
"contextMenus",
|
|
1219
|
+
"cookies",
|
|
1220
|
+
"debugger",
|
|
1221
|
+
"declarativeContent",
|
|
1222
|
+
"declarativeNetRequest",
|
|
1223
|
+
"declarativeNetRequestWithHostAccess",
|
|
1224
|
+
"declarativeNetRequestFeedback",
|
|
1225
|
+
"desktopCapture",
|
|
1226
|
+
"dns",
|
|
1227
|
+
"documentScan",
|
|
1228
|
+
"downloads",
|
|
1229
|
+
"downloads.open",
|
|
1230
|
+
"downloads.ui",
|
|
1231
|
+
"enterprise.deviceAttributes",
|
|
1232
|
+
"enterprise.hardwarePlatform",
|
|
1233
|
+
"enterprise.networkingAttributes",
|
|
1234
|
+
"enterprise.platformKeys",
|
|
1235
|
+
"favicon",
|
|
1236
|
+
"fileBrowserHandler",
|
|
1237
|
+
"fileSystemProvider",
|
|
1238
|
+
"fontSettings",
|
|
1239
|
+
"gcm",
|
|
1240
|
+
"geolocation",
|
|
1241
|
+
"history",
|
|
1242
|
+
"identity",
|
|
1243
|
+
"identity.email",
|
|
1244
|
+
"idle",
|
|
1245
|
+
"loginState",
|
|
1246
|
+
"management",
|
|
1247
|
+
"nativeMessaging",
|
|
1248
|
+
"notifications",
|
|
1249
|
+
"offscreen",
|
|
1250
|
+
"pageCapture",
|
|
1251
|
+
"power",
|
|
1252
|
+
"printerProvider",
|
|
1253
|
+
"printing",
|
|
1254
|
+
"printingMetrics",
|
|
1255
|
+
"privacy",
|
|
1256
|
+
"processes",
|
|
1257
|
+
"proxy",
|
|
1258
|
+
"readingList",
|
|
1259
|
+
"runtime",
|
|
1260
|
+
"scripting",
|
|
1261
|
+
"search",
|
|
1262
|
+
"sessions",
|
|
1263
|
+
"sidePanel",
|
|
1264
|
+
"storage",
|
|
1265
|
+
"system.cpu",
|
|
1266
|
+
"system.display",
|
|
1267
|
+
"system.memory",
|
|
1268
|
+
"system.storage",
|
|
1269
|
+
"tabCapture",
|
|
1270
|
+
"tabGroups",
|
|
1271
|
+
"tabs",
|
|
1272
|
+
"topSites",
|
|
1273
|
+
"tts",
|
|
1274
|
+
"ttsEngine",
|
|
1275
|
+
"unlimitedStorage",
|
|
1276
|
+
"vpnProvider",
|
|
1277
|
+
"wallpaper",
|
|
1278
|
+
"webAuthenticationProxy",
|
|
1279
|
+
"webNavigation",
|
|
1280
|
+
"webRequest",
|
|
1281
|
+
"webRequestBlocking",
|
|
1282
|
+
"webRequestAuthProvider",
|
|
1283
|
+
"browserSettings",
|
|
1284
|
+
"captivePortal",
|
|
1285
|
+
"contextualIdentities",
|
|
1286
|
+
"dns",
|
|
1287
|
+
"menus",
|
|
1288
|
+
"menus.overrideContext",
|
|
1289
|
+
"pkcs11",
|
|
1290
|
+
"theme",
|
|
1291
|
+
"webRequestFilterResponse"
|
|
1292
|
+
]);
|
|
1104
1293
|
const manifest_validate_schema = {
|
|
1105
1294
|
name: "extension_manifest_validate",
|
|
1106
1295
|
description: "Validate a manifest.json file for correctness across browsers. Reports missing fields, invalid permissions, and cross-browser compatibility issues.",
|
|
@@ -1159,6 +1348,13 @@ async function manifest_validate_handler(args) {
|
|
|
1159
1348
|
if (!manifest.version) result.warnings.push("Missing field: version (required for store submission)");
|
|
1160
1349
|
const chromiumManifest = filterKeysForThisBrowser(manifest, "chrome");
|
|
1161
1350
|
if (!chromiumManifest.manifest_version) result.errors.push('Missing manifest_version. Use "chromium:manifest_version": 3 and "firefox:manifest_version": 2 for cross-browser support.');
|
|
1351
|
+
const declaredPerms = [
|
|
1352
|
+
...chromiumManifest.permissions ?? [],
|
|
1353
|
+
...chromiumManifest.optional_permissions ?? []
|
|
1354
|
+
].filter((p)=>"string" == typeof p);
|
|
1355
|
+
for (const perm of declaredPerms)if (!(perm.includes("://") || perm.includes("*")) && "<all_urls>" !== perm) {
|
|
1356
|
+
if (!KNOWN_PERMISSIONS.has(perm)) result.warnings.push(`Unrecognized permission "${perm}" — check for a typo (host/match patterns belong in host_permissions, not permissions).`);
|
|
1357
|
+
}
|
|
1162
1358
|
for (const browser of browsers){
|
|
1163
1359
|
const isChromium = isChromiumFamily(browser);
|
|
1164
1360
|
const isFirefox = isGeckoFamily(browser);
|
|
@@ -1196,9 +1392,15 @@ async function manifest_validate_handler(args) {
|
|
|
1196
1392
|
if (chromiumManifest.action || manifest["firefox:browser_action"]) surfaces.push("action");
|
|
1197
1393
|
if (chromiumManifest.chrome_url_overrides?.newtab) surfaces.push("newtab");
|
|
1198
1394
|
if (chromiumManifest.background) surfaces.push("background");
|
|
1199
|
-
|
|
1395
|
+
const distinctive = surfaces.filter((s)=>"background" !== s);
|
|
1396
|
+
const matchOn = distinctive.length ? distinctive : surfaces;
|
|
1397
|
+
if (matchOn.length) try {
|
|
1200
1398
|
const templates = await listTemplates();
|
|
1201
|
-
result.similarTemplates = templates.
|
|
1399
|
+
result.similarTemplates = templates.map((t)=>({
|
|
1400
|
+
slug: t.slug,
|
|
1401
|
+
surfaces: t.surfaces,
|
|
1402
|
+
score: t.surfaces.filter((s)=>matchOn.includes(s)).length
|
|
1403
|
+
})).filter((t)=>t.score > 0).sort((a, b)=>b.score - a.score).slice(0, 5).map((t)=>({
|
|
1202
1404
|
slug: t.slug,
|
|
1203
1405
|
surfaces: t.surfaces
|
|
1204
1406
|
}));
|
|
@@ -1320,12 +1522,21 @@ async function inspect_handler(args) {
|
|
|
1320
1522
|
byType[f.type].count++;
|
|
1321
1523
|
byType[f.type].size += f.size;
|
|
1322
1524
|
}
|
|
1525
|
+
const sourcemapSize = byType.sourcemap?.size ?? 0;
|
|
1526
|
+
const buildType = sourcemapSize > 0 ? "development" : "production";
|
|
1527
|
+
const shippableSize = totalSize - sourcemapSize;
|
|
1323
1528
|
const result = {
|
|
1324
1529
|
browser,
|
|
1325
1530
|
distPath,
|
|
1531
|
+
buildType,
|
|
1326
1532
|
totalSize,
|
|
1327
1533
|
totalSizeFormatted: formatBytes(totalSize),
|
|
1534
|
+
shippableSize,
|
|
1535
|
+
shippableSizeFormatted: formatBytes(shippableSize),
|
|
1328
1536
|
fileCount: files.length,
|
|
1537
|
+
..."development" === buildType ? {
|
|
1538
|
+
note: `This dist contains ${formatBytes(sourcemapSize)} of sourcemaps and looks like a dev build; run extension_build for production sizes. shippableSize excludes sourcemaps.`
|
|
1539
|
+
} : {},
|
|
1329
1540
|
manifest: {
|
|
1330
1541
|
name: manifest.name,
|
|
1331
1542
|
version: manifest.version,
|
|
@@ -1770,13 +1981,16 @@ class CDPClient extends CDPConnection {
|
|
|
1770
1981
|
}
|
|
1771
1982
|
async function resolveCdpPort(projectPath, browser, options) {
|
|
1772
1983
|
const waitMs = options?.waitMs ?? 20000;
|
|
1984
|
+
const graceMs = options?.graceMs ?? 2500;
|
|
1773
1985
|
const readyPath = node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json");
|
|
1774
1986
|
const deadline = Date.now() + waitMs;
|
|
1775
1987
|
let contractSeen = false;
|
|
1988
|
+
let contractSeenAt = null;
|
|
1776
1989
|
for(;;){
|
|
1777
1990
|
try {
|
|
1778
1991
|
const contract = JSON.parse(node_fs.readFileSync(readyPath, "utf8"));
|
|
1779
1992
|
contractSeen = true;
|
|
1993
|
+
if (null == contractSeenAt) contractSeenAt = Date.now();
|
|
1780
1994
|
if ("number" == typeof contract.cdpPort) return {
|
|
1781
1995
|
port: contract.cdpPort,
|
|
1782
1996
|
source: "contract"
|
|
@@ -1784,7 +1998,8 @@ async function resolveCdpPort(projectPath, browser, options) {
|
|
|
1784
1998
|
} catch {
|
|
1785
1999
|
if (!contractSeen) break;
|
|
1786
2000
|
}
|
|
1787
|
-
|
|
2001
|
+
const effectiveDeadline = null != contractSeenAt ? Math.min(deadline, contractSeenAt + graceMs) : deadline;
|
|
2002
|
+
if (Date.now() >= effectiveDeadline) break;
|
|
1788
2003
|
await new Promise((resolve)=>setTimeout(resolve, 500));
|
|
1789
2004
|
}
|
|
1790
2005
|
if (!contractSeen && await isCdpEndpoint(9222)) return {
|
|
@@ -1954,16 +2169,26 @@ async function source_inspect_handler(args) {
|
|
|
1954
2169
|
const cdp = new CDPClient();
|
|
1955
2170
|
try {
|
|
1956
2171
|
const allTargets = await CDPClient.discoverTargets(cdpPort);
|
|
1957
|
-
const
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
2172
|
+
const OVERRIDE_PAGES = [
|
|
2173
|
+
"chrome://newtab/",
|
|
2174
|
+
"chrome://new-tab-page/",
|
|
2175
|
+
"chrome://bookmarks/",
|
|
2176
|
+
"chrome://history/"
|
|
2177
|
+
];
|
|
2178
|
+
const isOverridePage = (url)=>OVERRIDE_PAGES.some((p)=>url.startsWith(p));
|
|
2179
|
+
const pageTargets = allTargets.filter((t)=>"page" === t.type && !t.url.startsWith("devtools://") && (!t.url.startsWith("chrome://") || isOverridePage(t.url)));
|
|
2180
|
+
if (0 === pageTargets.length) {
|
|
2181
|
+
const chromeOnly = allTargets.some((t)=>"page" === t.type && t.url.startsWith("chrome://"));
|
|
2182
|
+
return JSON.stringify({
|
|
2183
|
+
cdpPort,
|
|
2184
|
+
browser,
|
|
2185
|
+
warning: chromeOnly ? "No inspectable page targets found. Only internal chrome:// pages are open; open the extension's surface (or pass a url to navigate a tab) first." : "No inspectable page targets found. The extension may not have opened a page yet.",
|
|
2186
|
+
allTargets: allTargets.map((t)=>({
|
|
2187
|
+
type: t.type,
|
|
2188
|
+
url: t.url?.slice(0, 100)
|
|
2189
|
+
}))
|
|
2190
|
+
});
|
|
2191
|
+
}
|
|
1967
2192
|
const target = args.url ? pageTargets.find((t)=>t.url.includes(args.url)) ?? pageTargets[0] : pageTargets[0];
|
|
1968
2193
|
const browserWsUrl = await CDPClient.discoverBrowserWsUrl(cdpPort);
|
|
1969
2194
|
await cdp.connect(browserWsUrl);
|
|
@@ -2459,7 +2684,7 @@ function commonFlags(args) {
|
|
|
2459
2684
|
}
|
|
2460
2685
|
const eval_schema = {
|
|
2461
2686
|
name: "extension_eval",
|
|
2462
|
-
description: "Evaluate an expression in a running extension context
|
|
2687
|
+
description: "Evaluate an expression in a running extension context. Requires the dev session to be started with allowEval: true (extension_dev; writes a 0600 session token the CLI reads). Chromium caveats: eval in the MV3 background/service_worker is blocked by CSP (use an MV2/Firefox build for that context), and context content/page/popup require a numeric `tab` id (a chrome.tabs id) — the `url` arg alone does not target a tab. To read a content-script DOM without a tab id, prefer extension_source_inspect (it auto-selects the active page and can navigate by url). Wraps `extension eval`.",
|
|
2463
2688
|
inputSchema: {
|
|
2464
2689
|
type: "object",
|
|
2465
2690
|
properties: {
|
|
@@ -2487,11 +2712,11 @@ const eval_schema = {
|
|
|
2487
2712
|
},
|
|
2488
2713
|
url: {
|
|
2489
2714
|
type: "string",
|
|
2490
|
-
description: "For content/page: document(s) to target"
|
|
2715
|
+
description: "For content/page: filters which matching document(s) to target, but does NOT by itself select a tab — a numeric `tab` id is still required on Chromium."
|
|
2491
2716
|
},
|
|
2492
2717
|
tab: {
|
|
2493
2718
|
type: "number",
|
|
2494
|
-
description: "
|
|
2719
|
+
description: "Numeric chrome.tabs id. Required for context content/page/popup on Chromium (the `url` arg does not substitute for it)."
|
|
2495
2720
|
},
|
|
2496
2721
|
browser: {
|
|
2497
2722
|
type: "string",
|
|
@@ -2703,7 +2928,21 @@ async function open_handler(args) {
|
|
|
2703
2928
|
if ("command" === args.surface && args.name) cli.push("--name", args.name);
|
|
2704
2929
|
cli.push("--browser", browser);
|
|
2705
2930
|
if (null != args.timeout) cli.push("--timeout", String(args.timeout));
|
|
2706
|
-
|
|
2931
|
+
const raw = await runActVerb(cli, args.projectPath, args.timeout);
|
|
2932
|
+
const headless = /^(1|true)$/i.test(process.env.EXTENSION_HEADLESS ?? "");
|
|
2933
|
+
if (headless && [
|
|
2934
|
+
"popup",
|
|
2935
|
+
"action",
|
|
2936
|
+
"sidebar"
|
|
2937
|
+
].includes(args.surface)) try {
|
|
2938
|
+
const parsed = JSON.parse(raw);
|
|
2939
|
+
const msg = String(parsed?.error?.message ?? "");
|
|
2940
|
+
if (parsed?.ok === false && !parsed.hint && /active browser window|no active|headless/i.test(msg)) {
|
|
2941
|
+
parsed.hint = "The dev browser is running headless (EXTENSION_HEADLESS), which has no visible window to attach a popup/sidebar to. Relaunch a headed dev session to open UI surfaces.";
|
|
2942
|
+
return JSON.stringify(parsed);
|
|
2943
|
+
}
|
|
2944
|
+
} catch {}
|
|
2945
|
+
return raw;
|
|
2707
2946
|
}
|
|
2708
2947
|
const dom_inspect_schema = {
|
|
2709
2948
|
name: "extension_dom_inspect",
|
|
@@ -2780,8 +3019,9 @@ async function dom_inspect_handler(args) {
|
|
|
2780
3019
|
ok: false,
|
|
2781
3020
|
error: {
|
|
2782
3021
|
name: "BadRequest",
|
|
2783
|
-
message: "content/page inspect requires a tab id"
|
|
2784
|
-
}
|
|
3022
|
+
message: "content/page inspect requires a numeric tab id (chrome.tabs id, not a CDP target id)."
|
|
3023
|
+
},
|
|
3024
|
+
hint: "To inspect a content script's DOM without hunting for a tab id, use extension_source_inspect (it auto-selects the active page and can navigate via its url arg). Use extension_dom_inspect with context popup/options/sidebar/devtools for an open surface (no tab needed)."
|
|
2785
3025
|
});
|
|
2786
3026
|
const cli = [
|
|
2787
3027
|
"inspect",
|
|
@@ -2813,13 +3053,17 @@ function readCredentials() {
|
|
|
2813
3053
|
if (1 !== data.version) return null;
|
|
2814
3054
|
const token = String(data.token || "").trim();
|
|
2815
3055
|
if (!token) return null;
|
|
3056
|
+
const provider = "extensiondev" === data.provider || "github" === data.provider ? data.provider : void 0;
|
|
2816
3057
|
return {
|
|
2817
3058
|
version: 1,
|
|
2818
3059
|
token,
|
|
2819
3060
|
workspaceSlug: String(data.workspaceSlug || ""),
|
|
2820
3061
|
projectSlug: String(data.projectSlug || ""),
|
|
2821
3062
|
expiresAt: Number(data.expiresAt || 0),
|
|
2822
|
-
api: String(data.api || "")
|
|
3063
|
+
api: String(data.api || ""),
|
|
3064
|
+
...provider ? {
|
|
3065
|
+
provider
|
|
3066
|
+
} : {}
|
|
2823
3067
|
};
|
|
2824
3068
|
} catch {
|
|
2825
3069
|
return null;
|
|
@@ -2891,8 +3135,12 @@ function safeApiBase(raw) {
|
|
|
2891
3135
|
async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
|
|
2892
3136
|
const override = String(process.env.EXTENSION_DEV_GITHUB_CLIENT_ID || "").trim();
|
|
2893
3137
|
if (override) return {
|
|
3138
|
+
provider: "github",
|
|
2894
3139
|
clientId: override,
|
|
2895
|
-
scope: "read:user"
|
|
3140
|
+
scope: "read:user",
|
|
3141
|
+
deviceCodeUrl: "/api/cli/device/code",
|
|
3142
|
+
deviceTokenUrl: "/api/cli/device/token",
|
|
3143
|
+
verificationUri: "https://github.com/login/device"
|
|
2896
3144
|
};
|
|
2897
3145
|
const res = await fetchImpl(`${apiBase}/api/cli/login/config`, {
|
|
2898
3146
|
headers: {
|
|
@@ -2901,12 +3149,32 @@ async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
|
|
|
2901
3149
|
});
|
|
2902
3150
|
if (!res.ok) throw new Error(`Could not fetch login config from ${apiBase} (${res.status}).`);
|
|
2903
3151
|
const data = await res.json().catch(()=>({}));
|
|
3152
|
+
const provider = "extensiondev" === data.provider ? "extensiondev" : "github";
|
|
2904
3153
|
const clientId = String(data.githubClientId || "").trim();
|
|
2905
|
-
if (!clientId) throw new Error("Login is not configured on the server (no GitHub client id). Set EXTENSION_DEV_GITHUB_CLIENT_ID to override.");
|
|
3154
|
+
if ("github" === provider && !clientId) throw new Error("Login is not configured on the server (no GitHub client id). Set EXTENSION_DEV_GITHUB_CLIENT_ID to override.");
|
|
2906
3155
|
return {
|
|
3156
|
+
provider,
|
|
2907
3157
|
clientId,
|
|
2908
|
-
scope: String(data.scope || "read:user")
|
|
3158
|
+
scope: String(data.scope || "read:user"),
|
|
3159
|
+
deviceCodeUrl: String(data.deviceCodeUrl || "/api/cli/device/code"),
|
|
3160
|
+
deviceTokenUrl: String(data.deviceTokenUrl || "/api/cli/device/token"),
|
|
3161
|
+
verificationUri: String(data.verificationUri || "https://github.com/login/device")
|
|
3162
|
+
};
|
|
3163
|
+
}
|
|
3164
|
+
function persistTokenResponse(args) {
|
|
3165
|
+
const token = String(args.data.token || "").trim();
|
|
3166
|
+
if (!token) throw new Error("Login returned no token.");
|
|
3167
|
+
const creds = {
|
|
3168
|
+
version: 1,
|
|
3169
|
+
token,
|
|
3170
|
+
workspaceSlug: String(args.data.workspaceSlug || ""),
|
|
3171
|
+
projectSlug: String(args.data.projectSlug || ""),
|
|
3172
|
+
expiresAt: Number(args.data.expiresAt || 0),
|
|
3173
|
+
api: args.apiBase,
|
|
3174
|
+
provider: args.provider
|
|
2909
3175
|
};
|
|
3176
|
+
writeCredentials(creds);
|
|
3177
|
+
return creds;
|
|
2910
3178
|
}
|
|
2911
3179
|
async function exchangeAndPersist(args) {
|
|
2912
3180
|
const doFetch = args.fetchImpl ?? fetch;
|
|
@@ -2931,18 +3199,11 @@ async function exchangeAndPersist(args) {
|
|
|
2931
3199
|
};
|
|
2932
3200
|
}
|
|
2933
3201
|
if (!res.ok) throw new Error(`Login exchange failed (${res.status}): ${data.message || "unknown error"}`);
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
workspaceSlug: String(data.workspaceSlug || ""),
|
|
2940
|
-
projectSlug: String(data.projectSlug || ""),
|
|
2941
|
-
expiresAt: Number(data.expiresAt || 0),
|
|
2942
|
-
api: args.apiBase
|
|
2943
|
-
};
|
|
2944
|
-
writeCredentials(creds);
|
|
2945
|
-
return creds;
|
|
3202
|
+
return persistTokenResponse({
|
|
3203
|
+
apiBase: args.apiBase,
|
|
3204
|
+
data,
|
|
3205
|
+
provider: "github"
|
|
3206
|
+
});
|
|
2946
3207
|
}
|
|
2947
3208
|
function resolveToken() {
|
|
2948
3209
|
const fromEnv = String(process.env.EXTENSION_DEV_TOKEN || "").trim();
|
|
@@ -3014,53 +3275,60 @@ async function publish(options = {}) {
|
|
|
3014
3275
|
}
|
|
3015
3276
|
const publish_schema = {
|
|
3016
3277
|
name: "extension_publish",
|
|
3017
|
-
description: "Publish
|
|
3278
|
+
description: "Publish the project your stored token is scoped to (from extension_login, or EXTENSION_DEV_TOKEN) to extension.dev and return its shareable URL. The publish target is the token's project -- there is no projectPath, the local files are not uploaded. For a PUBLIC project the URL is the canonical public page and ttlHours does not apply; for a PRIVATE project it is a fresh time-limited share link (?share=) whose lifetime is ttlHours. Posts to the platform's CLI publish endpoint. Besides extension_login this is the only tool that talks to the hosted platform.",
|
|
3018
3279
|
inputSchema: {
|
|
3019
3280
|
type: "object",
|
|
3020
3281
|
properties: {
|
|
3021
|
-
projectPath: {
|
|
3022
|
-
type: "string",
|
|
3023
|
-
description: "Path to the extension project root"
|
|
3024
|
-
},
|
|
3025
3282
|
ttlHours: {
|
|
3026
3283
|
type: "number",
|
|
3027
|
-
description: "
|
|
3284
|
+
description: "Private-project share-link lifetime in hours, 1-168 (default 24). Ignored for public projects."
|
|
3028
3285
|
},
|
|
3029
3286
|
buildSha: {
|
|
3030
3287
|
type: "string",
|
|
3031
|
-
description: "Pin the share URL to a specific build sha"
|
|
3288
|
+
description: "Pin the share URL to a specific build sha (7-40 hex chars). The platform verifies the build exists in the project's build index and rejects an unknown sha, so the returned URL always points at a real build."
|
|
3032
3289
|
},
|
|
3033
3290
|
api: {
|
|
3034
3291
|
type: "string",
|
|
3035
3292
|
description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
|
|
3036
3293
|
}
|
|
3037
3294
|
},
|
|
3038
|
-
required: [
|
|
3039
|
-
"projectPath"
|
|
3040
|
-
]
|
|
3295
|
+
required: []
|
|
3041
3296
|
}
|
|
3042
3297
|
};
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
if (!token) return JSON.stringify({
|
|
3298
|
+
function fail(name, message) {
|
|
3299
|
+
return JSON.stringify({
|
|
3046
3300
|
ok: false,
|
|
3047
3301
|
error: {
|
|
3048
|
-
name
|
|
3049
|
-
message
|
|
3302
|
+
name,
|
|
3303
|
+
message
|
|
3050
3304
|
}
|
|
3051
3305
|
});
|
|
3306
|
+
}
|
|
3307
|
+
async function publish_handler(args) {
|
|
3308
|
+
const token = resolveToken();
|
|
3309
|
+
if (!token) return fail("PublishAuthError", "No token. Run extension_login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard).");
|
|
3310
|
+
if (null != args.ttlHours) {
|
|
3311
|
+
const t = Number(args.ttlHours);
|
|
3312
|
+
if (!Number.isInteger(t) || t < 1 || t > 168) return fail("PublishBadRequest", "ttlHours must be an integer between 1 and 168.");
|
|
3313
|
+
}
|
|
3314
|
+
if (null != args.buildSha && "" !== args.buildSha) {
|
|
3315
|
+
if (!/^[0-9a-f]{7,40}$/i.test(args.buildSha)) return fail("PublishBadRequest", "buildSha must be a 7-40 character hex git sha.");
|
|
3316
|
+
}
|
|
3052
3317
|
const result = await publish({
|
|
3053
3318
|
ttlHours: args.ttlHours,
|
|
3054
3319
|
buildSha: args.buildSha,
|
|
3055
3320
|
api: args.api,
|
|
3056
3321
|
token
|
|
3057
3322
|
});
|
|
3058
|
-
|
|
3323
|
+
if (!result.ok) return JSON.stringify(result);
|
|
3324
|
+
const data = result.data;
|
|
3325
|
+
if (null != args.ttlHours && "public" === data.visibility) data.note = "ttlHours was ignored: this is a public project, whose share URL is its canonical public page.";
|
|
3326
|
+
return JSON.stringify(data);
|
|
3059
3327
|
}
|
|
3060
3328
|
const release_promote_DEFAULT_API = "https://www.extension.dev";
|
|
3061
3329
|
const release_promote_schema = {
|
|
3062
3330
|
name: "extension_release_promote",
|
|
3063
|
-
description: "Promote a built extension to a release channel (e.g. stable, preview, beta) on extension.dev, headless. Auth-gated:
|
|
3331
|
+
description: "Promote a built extension to a release channel (e.g. stable, preview, beta) on extension.dev, headless. Auth-gated: uses your stored login (extension_login) or a release token in EXTENSION_DEV_TOKEN (mint and revoke it in the dashboard under project settings -> Access tokens). Posts to the platform's CLI release endpoint; the project is identified by the token. Cutting a version-bump PR is not available headlessly (it writes to your source repo and needs an interactive login).",
|
|
3064
3332
|
inputSchema: {
|
|
3065
3333
|
type: "object",
|
|
3066
3334
|
properties: {
|
|
@@ -3102,7 +3370,7 @@ const release_promote_schema = {
|
|
|
3102
3370
|
]
|
|
3103
3371
|
}
|
|
3104
3372
|
};
|
|
3105
|
-
function
|
|
3373
|
+
function release_promote_fail(name, message) {
|
|
3106
3374
|
return JSON.stringify({
|
|
3107
3375
|
ok: false,
|
|
3108
3376
|
error: {
|
|
@@ -3113,12 +3381,12 @@ function fail(name, message) {
|
|
|
3113
3381
|
}
|
|
3114
3382
|
async function release_promote_handler(args) {
|
|
3115
3383
|
const token = resolveToken();
|
|
3116
|
-
if (!token) return
|
|
3384
|
+
if (!token) return release_promote_fail("ReleaseAuthError", "No token. Set EXTENSION_DEV_TOKEN to a release token (create one in the extension.dev dashboard under project settings -> Access tokens), or run extension_login.");
|
|
3117
3385
|
const buildId = String(args.buildId || "").trim();
|
|
3118
3386
|
const channel = String(args.channel || "").trim();
|
|
3119
|
-
if (!buildId || !channel) return
|
|
3387
|
+
if (!buildId || !channel) return release_promote_fail("ReleaseInputError", "buildId and channel are required.");
|
|
3120
3388
|
const apiCheck = safeApiBase(String(args.api || process.env.EXTENSION_DEV_API_URL || release_promote_DEFAULT_API));
|
|
3121
|
-
if (!apiCheck.ok) return
|
|
3389
|
+
if (!apiCheck.ok) return release_promote_fail("ReleaseConfigError", apiCheck.message);
|
|
3122
3390
|
const url = `${apiCheck.base}/api/cli/release/promote`;
|
|
3123
3391
|
const body = {
|
|
3124
3392
|
buildId,
|
|
@@ -3139,7 +3407,7 @@ async function release_promote_handler(args) {
|
|
|
3139
3407
|
body: JSON.stringify(body)
|
|
3140
3408
|
});
|
|
3141
3409
|
} catch (err) {
|
|
3142
|
-
return
|
|
3410
|
+
return release_promote_fail("ReleaseNetworkError", `Could not reach ${url}: ${err?.message || err}`);
|
|
3143
3411
|
}
|
|
3144
3412
|
const text = await res.text();
|
|
3145
3413
|
let data;
|
|
@@ -3150,175 +3418,117 @@ async function release_promote_handler(args) {
|
|
|
3150
3418
|
message: text
|
|
3151
3419
|
};
|
|
3152
3420
|
}
|
|
3153
|
-
if (!res.ok) return
|
|
3421
|
+
if (!res.ok) return release_promote_fail("ReleaseError", `promote failed (${res.status}): ${data?.message || text || "unknown error"}`);
|
|
3154
3422
|
return JSON.stringify(data);
|
|
3155
3423
|
}
|
|
3156
|
-
const
|
|
3157
|
-
function pinnedDeployVersion() {
|
|
3158
|
-
return String(process.env.EXTENSION_DEPLOY_VERSION || "").trim() || DEFAULT_DEPLOY_VERSION;
|
|
3159
|
-
}
|
|
3160
|
-
function buildDeployArgs(args) {
|
|
3161
|
-
const argv = [];
|
|
3162
|
-
const pushValue = (flag, value)=>{
|
|
3163
|
-
if (void 0 === value || "" === value) return;
|
|
3164
|
-
argv.push(flag, String(value));
|
|
3165
|
-
};
|
|
3166
|
-
if (false !== args.dryRun) argv.push("--dry-run");
|
|
3167
|
-
pushValue("--chrome-zip", args.chromeZip);
|
|
3168
|
-
pushValue("--chrome-extension-id", args.chromeExtensionId);
|
|
3169
|
-
pushValue("--chrome-publisher-id", args.chromePublisherId);
|
|
3170
|
-
if (args.stagedPublish) argv.push("--chrome-staged-publish");
|
|
3171
|
-
pushValue("--chrome-deploy-percentage", args.chromeDeployPercentage);
|
|
3172
|
-
if (args.chromeSkipSubmitReview) argv.push("--chrome-skip-submit-review");
|
|
3173
|
-
pushValue("--firefox-zip", args.firefoxZip);
|
|
3174
|
-
pushValue("--firefox-sources-zip", args.firefoxSourcesZip);
|
|
3175
|
-
pushValue("--firefox-extension-id", args.firefoxExtensionId);
|
|
3176
|
-
pushValue("--firefox-channel", args.firefoxChannel);
|
|
3177
|
-
pushValue("--edge-zip", args.edgeZip);
|
|
3178
|
-
pushValue("--edge-product-id", args.edgeProductId);
|
|
3179
|
-
if (args.edgeSkipSubmitReview) argv.push("--edge-skip-submit-review");
|
|
3180
|
-
pushValue("--output-json", args.outputJson);
|
|
3181
|
-
return argv;
|
|
3182
|
-
}
|
|
3424
|
+
const deploy_DEFAULT_API = "https://www.extension.dev";
|
|
3183
3425
|
const deploy_schema = {
|
|
3184
3426
|
name: "extension_deploy",
|
|
3185
|
-
description: "Submit a built extension to the Chrome Web Store, Firefox AMO, and/or Edge Add-ons
|
|
3427
|
+
description: "Submit a built extension to the Chrome Web Store, Firefox AMO, and/or Edge Add-ons THROUGH extension.dev, which holds your store credentials and dispatches the release from your project's mirror CI. DEFAULTS TO A DRY RUN (preflight: verifies auth, the project, that the build exists, and the store workflow - dispatches nothing); pass dryRun:false to actually submit, which is irreversible and enters store review. The target project is identified by your token (extension_login or a release token in EXTENSION_DEV_TOKEN); store credentials are never tool arguments and local files are not uploaded. Pass browsers + buildSha. Posts to the platform's CLI store-submission endpoint.",
|
|
3186
3428
|
inputSchema: {
|
|
3187
3429
|
type: "object",
|
|
3188
3430
|
properties: {
|
|
3189
|
-
|
|
3190
|
-
type: "
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
chromePublisherId: {
|
|
3202
|
-
type: "string",
|
|
3203
|
-
description: "Chrome publisher UUID (or set CHROME_PUBLISHER_ID)"
|
|
3204
|
-
},
|
|
3205
|
-
firefoxZip: {
|
|
3206
|
-
type: "string",
|
|
3207
|
-
description: "Path to the built Firefox extension .zip"
|
|
3208
|
-
},
|
|
3209
|
-
firefoxSourcesZip: {
|
|
3210
|
-
type: "string",
|
|
3211
|
-
description: "Path to the Firefox sources .zip (optional)"
|
|
3212
|
-
},
|
|
3213
|
-
firefoxExtensionId: {
|
|
3214
|
-
type: "string",
|
|
3215
|
-
description: "Firefox add-on GUID or email-style ID"
|
|
3431
|
+
browsers: {
|
|
3432
|
+
type: "array",
|
|
3433
|
+
items: {
|
|
3434
|
+
type: "string",
|
|
3435
|
+
enum: [
|
|
3436
|
+
"chrome",
|
|
3437
|
+
"firefox",
|
|
3438
|
+
"edge",
|
|
3439
|
+
"safari"
|
|
3440
|
+
]
|
|
3441
|
+
},
|
|
3442
|
+
description: "Stores to submit to."
|
|
3216
3443
|
},
|
|
3217
|
-
|
|
3444
|
+
buildSha: {
|
|
3218
3445
|
type: "string",
|
|
3219
|
-
|
|
3220
|
-
"listed",
|
|
3221
|
-
"unlisted"
|
|
3222
|
-
],
|
|
3223
|
-
description: "Firefox channel (unlisted is required for a first submission)"
|
|
3446
|
+
description: "The built commit SHA to submit. It must have a completed build in the project's build index; an unknown sha is rejected."
|
|
3224
3447
|
},
|
|
3225
|
-
|
|
3448
|
+
channel: {
|
|
3226
3449
|
type: "string",
|
|
3227
|
-
description: "
|
|
3450
|
+
description: "Release channel to submit from (default stable)."
|
|
3228
3451
|
},
|
|
3229
|
-
|
|
3452
|
+
version: {
|
|
3230
3453
|
type: "string",
|
|
3231
|
-
description: "
|
|
3454
|
+
description: "Version label for the submission record (optional)."
|
|
3232
3455
|
},
|
|
3233
3456
|
dryRun: {
|
|
3234
3457
|
type: "boolean",
|
|
3235
3458
|
default: true,
|
|
3236
|
-
description: "
|
|
3459
|
+
description: "Preflight only (verify auth, project, build, and store workflow). Pass false to actually dispatch the submission (irreversible, enters store review)."
|
|
3237
3460
|
},
|
|
3238
|
-
|
|
3239
|
-
type: "boolean",
|
|
3240
|
-
default: false,
|
|
3241
|
-
description: "Chrome: publish as STAGED_PUBLISH so the rollout can be raised review-free later."
|
|
3242
|
-
},
|
|
3243
|
-
chromeDeployPercentage: {
|
|
3244
|
-
type: "number",
|
|
3245
|
-
description: "Chrome staged rollout percentage (1-100)."
|
|
3246
|
-
},
|
|
3247
|
-
chromeSkipSubmitReview: {
|
|
3248
|
-
type: "boolean",
|
|
3249
|
-
default: false,
|
|
3250
|
-
description: "Chrome: upload only, skip the publish/submit-for-review step."
|
|
3251
|
-
},
|
|
3252
|
-
edgeSkipSubmitReview: {
|
|
3253
|
-
type: "boolean",
|
|
3254
|
-
default: false,
|
|
3255
|
-
description: "Edge: upload only, skip the publish/submit-for-review step."
|
|
3256
|
-
},
|
|
3257
|
-
outputJson: {
|
|
3461
|
+
api: {
|
|
3258
3462
|
type: "string",
|
|
3259
|
-
description: "
|
|
3463
|
+
description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
|
|
3260
3464
|
}
|
|
3261
3465
|
},
|
|
3262
3466
|
required: [
|
|
3263
|
-
"
|
|
3467
|
+
"browsers",
|
|
3468
|
+
"buildSha"
|
|
3264
3469
|
]
|
|
3265
3470
|
}
|
|
3266
3471
|
};
|
|
3267
|
-
|
|
3268
|
-
|
|
3472
|
+
function deploy_fail(name, message) {
|
|
3473
|
+
return JSON.stringify({
|
|
3269
3474
|
ok: false,
|
|
3270
3475
|
error: {
|
|
3271
|
-
name
|
|
3272
|
-
message
|
|
3476
|
+
name,
|
|
3477
|
+
message
|
|
3273
3478
|
}
|
|
3274
3479
|
});
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
];
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
code,
|
|
3304
|
-
stdout: stdout.trim(),
|
|
3305
|
-
stderr: stderr.trim()
|
|
3306
|
-
}));
|
|
3307
|
-
});
|
|
3308
|
-
child.on("error", (err)=>{
|
|
3309
|
-
resolve(JSON.stringify({
|
|
3310
|
-
ok: false,
|
|
3311
|
-
error: {
|
|
3312
|
-
name: "DeploySpawnError",
|
|
3313
|
-
message: `Could not run extension-deploy: ${err instanceof Error ? err.message : String(err)}`
|
|
3314
|
-
}
|
|
3315
|
-
}));
|
|
3480
|
+
}
|
|
3481
|
+
async function deploy_handler(args) {
|
|
3482
|
+
const token = resolveToken();
|
|
3483
|
+
if (!token) return deploy_fail("DeployAuthError", "No token. Run extension_login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard under project settings -> Access tokens).");
|
|
3484
|
+
const browsers = (Array.isArray(args.browsers) ? args.browsers : []).map((b)=>String(b).trim().toLowerCase()).filter(Boolean);
|
|
3485
|
+
if (0 === browsers.length) return deploy_fail("DeployInputError", 'browsers is required (e.g. ["chrome","firefox","edge"]).');
|
|
3486
|
+
const buildSha = String(args.buildSha || "").trim();
|
|
3487
|
+
if (!buildSha) return deploy_fail("DeployInputError", "buildSha is required (the built commit to submit).");
|
|
3488
|
+
const apiCheck = safeApiBase(String(args.api || process.env.EXTENSION_DEV_API_URL || deploy_DEFAULT_API));
|
|
3489
|
+
if (!apiCheck.ok) return deploy_fail("DeployConfigError", apiCheck.message);
|
|
3490
|
+
const url = `${apiCheck.base}/api/cli/stores/submit`;
|
|
3491
|
+
const dryRun = false !== args.dryRun;
|
|
3492
|
+
const body = {
|
|
3493
|
+
browsers,
|
|
3494
|
+
buildSha,
|
|
3495
|
+
dryRun
|
|
3496
|
+
};
|
|
3497
|
+
if (args.channel) body.channel = String(args.channel).trim();
|
|
3498
|
+
if (args.version) body.version = String(args.version).trim();
|
|
3499
|
+
let res;
|
|
3500
|
+
try {
|
|
3501
|
+
res = await fetch(url, {
|
|
3502
|
+
method: "POST",
|
|
3503
|
+
headers: {
|
|
3504
|
+
authorization: `Bearer ${token}`,
|
|
3505
|
+
"content-type": "application/json"
|
|
3506
|
+
},
|
|
3507
|
+
body: JSON.stringify(body)
|
|
3316
3508
|
});
|
|
3509
|
+
} catch (err) {
|
|
3510
|
+
return deploy_fail("DeployNetworkError", `Could not reach ${url}: ${err?.message || err}`);
|
|
3511
|
+
}
|
|
3512
|
+
const text = await res.text();
|
|
3513
|
+
let data;
|
|
3514
|
+
try {
|
|
3515
|
+
data = JSON.parse(text);
|
|
3516
|
+
} catch {
|
|
3517
|
+
data = {
|
|
3518
|
+
message: text
|
|
3519
|
+
};
|
|
3520
|
+
}
|
|
3521
|
+
if (!res.ok) return deploy_fail("DeployError", `submit failed (${res.status}): ${data?.message || text || "unknown error"}`);
|
|
3522
|
+
return JSON.stringify({
|
|
3523
|
+
mode: "platform",
|
|
3524
|
+
dryRun,
|
|
3525
|
+
...data
|
|
3317
3526
|
});
|
|
3318
3527
|
}
|
|
3528
|
+
const SAFE_CEILING_MS = 50000;
|
|
3319
3529
|
const wait_schema = {
|
|
3320
3530
|
name: "extension_wait",
|
|
3321
|
-
description: "Wait for a running dev or start session to be ready. Polls the ready.json contract file and returns structured status.",
|
|
3531
|
+
description: "Wait for a running dev or start session to be ready. Polls the ready.json contract file and returns structured status. A single call is bounded to ~50s to stay under the MCP client request timeout; if it returns status:'timeout', call it again to keep waiting (polling resumes on the same contract).",
|
|
3322
3532
|
inputSchema: {
|
|
3323
3533
|
type: "object",
|
|
3324
3534
|
properties: {
|
|
@@ -3332,8 +3542,8 @@ const wait_schema = {
|
|
|
3332
3542
|
},
|
|
3333
3543
|
timeout: {
|
|
3334
3544
|
type: "number",
|
|
3335
|
-
default:
|
|
3336
|
-
description: "
|
|
3545
|
+
default: 45000,
|
|
3546
|
+
description: "Requested wait in milliseconds. Clamped to ~50s per call so it never exceeds the MCP client request timeout; call again to keep waiting for slower builds."
|
|
3337
3547
|
}
|
|
3338
3548
|
},
|
|
3339
3549
|
required: [
|
|
@@ -3343,7 +3553,9 @@ const wait_schema = {
|
|
|
3343
3553
|
};
|
|
3344
3554
|
async function wait_handler(args) {
|
|
3345
3555
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser, "chrome");
|
|
3346
|
-
const
|
|
3556
|
+
const requested = args.timeout ?? 45000;
|
|
3557
|
+
const timeout = Math.min(Math.max(requested, 1000), SAFE_CEILING_MS);
|
|
3558
|
+
const clamped = requested > SAFE_CEILING_MS;
|
|
3347
3559
|
const readyPath = node_path.resolve(args.projectPath, "dist", "extension-js", browser, "ready.json");
|
|
3348
3560
|
const start = Date.now();
|
|
3349
3561
|
const pollInterval = 1000;
|
|
@@ -3376,9 +3588,11 @@ async function wait_handler(args) {
|
|
|
3376
3588
|
}
|
|
3377
3589
|
return JSON.stringify({
|
|
3378
3590
|
status: "timeout",
|
|
3379
|
-
message: `Extension
|
|
3591
|
+
message: `Extension not ready after ${timeout}ms this call`,
|
|
3380
3592
|
readyPath,
|
|
3381
|
-
|
|
3593
|
+
waitDuration: Date.now() - start,
|
|
3594
|
+
clamped: clamped ? `requested ${requested}ms was clamped to ${SAFE_CEILING_MS}ms to stay under the MCP client request timeout` : void 0,
|
|
3595
|
+
hint: "Still building — call extension_wait again to keep waiting (it resumes polling the same contract). If it never readies, check the dev process with extension_doctor."
|
|
3382
3596
|
});
|
|
3383
3597
|
}
|
|
3384
3598
|
const add_feature_schema = {
|
|
@@ -3548,7 +3762,23 @@ async function add_feature_handler(args) {
|
|
|
3548
3762
|
});
|
|
3549
3763
|
const template = await getTemplateBySlug(templateSlug);
|
|
3550
3764
|
const referenceFiles = template?.keyFiles ?? template?.files ?? [];
|
|
3551
|
-
const
|
|
3765
|
+
const FEATURE_DIR = {
|
|
3766
|
+
"content-script": "content",
|
|
3767
|
+
popup: "action"
|
|
3768
|
+
};
|
|
3769
|
+
const featureDir = FEATURE_DIR[args.feature] ?? args.feature;
|
|
3770
|
+
const scriptExt = "react" === framework || "preact" === framework ? "tsx" : "ts";
|
|
3771
|
+
const componentExt = "vue" === framework ? "vue" : "svelte" === framework ? "svelte" : "tsx";
|
|
3772
|
+
const COMPONENT_BASE = {
|
|
3773
|
+
content: "Content",
|
|
3774
|
+
sidebar: "Sidebar",
|
|
3775
|
+
action: "Action",
|
|
3776
|
+
newtab: "NewTab",
|
|
3777
|
+
options: "Options",
|
|
3778
|
+
devtools: "DevTools",
|
|
3779
|
+
background: "Background"
|
|
3780
|
+
};
|
|
3781
|
+
const componentBase = COMPONENT_BASE[featureDir] ?? featureDir.charAt(0).toUpperCase() + featureDir.slice(1);
|
|
3552
3782
|
const filesToCreate = [];
|
|
3553
3783
|
const manifestUpdates = MANIFEST_ADDITIONS[args.feature] ?? {};
|
|
3554
3784
|
if ([
|
|
@@ -3562,24 +3792,42 @@ async function add_feature_handler(args) {
|
|
|
3562
3792
|
path: `src/${featureDir}/index.html`,
|
|
3563
3793
|
hint: "HTML entry point"
|
|
3564
3794
|
}, {
|
|
3565
|
-
path: `src/${featureDir}/scripts.${
|
|
3795
|
+
path: `src/${featureDir}/scripts.${scriptExt}`,
|
|
3566
3796
|
hint: "vanilla" === framework ? "Script entry point" : `${framework} mount point`
|
|
3567
3797
|
}, {
|
|
3568
3798
|
path: `src/${featureDir}/styles.css`,
|
|
3569
3799
|
hint: "Stylesheet"
|
|
3570
3800
|
});
|
|
3571
|
-
if ("vanilla" !== framework)
|
|
3572
|
-
|
|
3573
|
-
|
|
3801
|
+
if ("vanilla" !== framework) {
|
|
3802
|
+
filesToCreate.push({
|
|
3803
|
+
path: `src/${featureDir}/${componentBase}App.${componentExt}`,
|
|
3804
|
+
hint: `Main ${framework} component`
|
|
3805
|
+
});
|
|
3806
|
+
if ("vue" === framework) filesToCreate.push({
|
|
3807
|
+
path: `src/${featureDir}/shims-vue.d.ts`,
|
|
3808
|
+
hint: "Vue SFC type shim (lets TS import .vue components)"
|
|
3809
|
+
});
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
if ("content-script" === args.feature) {
|
|
3813
|
+
filesToCreate.push({
|
|
3814
|
+
path: "src/content/scripts.ts",
|
|
3815
|
+
hint: "vanilla" === framework ? "Content script entry point" : `${framework} content-script mount point`
|
|
3816
|
+
}, {
|
|
3817
|
+
path: "src/content/styles.css",
|
|
3818
|
+
hint: "Content script styles"
|
|
3574
3819
|
});
|
|
3820
|
+
if ("vanilla" !== framework) {
|
|
3821
|
+
filesToCreate.push({
|
|
3822
|
+
path: `src/content/ContentApp.${componentExt}`,
|
|
3823
|
+
hint: `Main ${framework} component mounted by the content script`
|
|
3824
|
+
});
|
|
3825
|
+
if ("vue" === framework) filesToCreate.push({
|
|
3826
|
+
path: "src/content/shims-vue.d.ts",
|
|
3827
|
+
hint: "Vue SFC type shim (lets TS import .vue components)"
|
|
3828
|
+
});
|
|
3829
|
+
}
|
|
3575
3830
|
}
|
|
3576
|
-
if ("content-script" === args.feature) filesToCreate.push({
|
|
3577
|
-
path: "src/content/scripts.ts",
|
|
3578
|
-
hint: "Content script entry point"
|
|
3579
|
-
}, {
|
|
3580
|
-
path: "src/content/styles.css",
|
|
3581
|
-
hint: "Content script styles"
|
|
3582
|
-
});
|
|
3583
3831
|
if ("background" === args.feature) filesToCreate.push({
|
|
3584
3832
|
path: "src/background.ts",
|
|
3585
3833
|
hint: "Background service worker / script"
|
|
@@ -3682,9 +3930,102 @@ async function pollForToken(args) {
|
|
|
3682
3930
|
reason: "pending"
|
|
3683
3931
|
};
|
|
3684
3932
|
}
|
|
3933
|
+
async function requestDeviceCode(args) {
|
|
3934
|
+
const doFetch = args.fetchImpl ?? fetch;
|
|
3935
|
+
const res = await doFetch(`${args.apiBase}${args.path}`, {
|
|
3936
|
+
method: "POST",
|
|
3937
|
+
headers: {
|
|
3938
|
+
"content-type": "application/json",
|
|
3939
|
+
accept: "application/json"
|
|
3940
|
+
},
|
|
3941
|
+
body: JSON.stringify({
|
|
3942
|
+
project: args.project,
|
|
3943
|
+
clientName: args.clientName ?? "extension-mcp"
|
|
3944
|
+
})
|
|
3945
|
+
});
|
|
3946
|
+
const text = await res.text();
|
|
3947
|
+
let data;
|
|
3948
|
+
try {
|
|
3949
|
+
data = JSON.parse(text);
|
|
3950
|
+
} catch {
|
|
3951
|
+
data = {
|
|
3952
|
+
message: text
|
|
3953
|
+
};
|
|
3954
|
+
}
|
|
3955
|
+
if (!res.ok) throw new Error(`Device code request failed (${res.status}): ${data.message || "unknown error"}`);
|
|
3956
|
+
const deviceCode = String(data.device_code || "").trim();
|
|
3957
|
+
const userCode = String(data.user_code || "").trim();
|
|
3958
|
+
if (!deviceCode || !userCode) throw new Error("Device code response missing device_code/user_code.");
|
|
3959
|
+
return {
|
|
3960
|
+
deviceCode,
|
|
3961
|
+
userCode,
|
|
3962
|
+
verificationUri: String(data.verification_uri || "https://extension.dev/device"),
|
|
3963
|
+
verificationUriComplete: String(data.verification_uri_complete || data.verification_uri || ""),
|
|
3964
|
+
interval: Number(data.interval || 5),
|
|
3965
|
+
expiresIn: Number(data.expires_in || 900)
|
|
3966
|
+
};
|
|
3967
|
+
}
|
|
3968
|
+
async function pollDeviceToken(args) {
|
|
3969
|
+
const doFetch = args.fetchImpl ?? fetch;
|
|
3970
|
+
const deadline = Date.now() + args.budgetMs;
|
|
3971
|
+
let interval = Math.max(1, args.interval);
|
|
3972
|
+
for(;;){
|
|
3973
|
+
const res = await doFetch(`${args.apiBase}${args.path}`, {
|
|
3974
|
+
method: "POST",
|
|
3975
|
+
headers: {
|
|
3976
|
+
"content-type": "application/json",
|
|
3977
|
+
accept: "application/json"
|
|
3978
|
+
},
|
|
3979
|
+
body: JSON.stringify({
|
|
3980
|
+
device_code: args.deviceCode,
|
|
3981
|
+
project: args.project
|
|
3982
|
+
})
|
|
3983
|
+
});
|
|
3984
|
+
const text = await res.text();
|
|
3985
|
+
let data;
|
|
3986
|
+
try {
|
|
3987
|
+
data = JSON.parse(text);
|
|
3988
|
+
} catch {
|
|
3989
|
+
data = {
|
|
3990
|
+
message: text
|
|
3991
|
+
};
|
|
3992
|
+
}
|
|
3993
|
+
if (res.ok && data.token) {
|
|
3994
|
+
const creds = persistTokenResponse({
|
|
3995
|
+
apiBase: args.apiBase,
|
|
3996
|
+
data,
|
|
3997
|
+
provider: "extensiondev"
|
|
3998
|
+
});
|
|
3999
|
+
return {
|
|
4000
|
+
ok: true,
|
|
4001
|
+
creds
|
|
4002
|
+
};
|
|
4003
|
+
}
|
|
4004
|
+
const error = String(data.error || "");
|
|
4005
|
+
if ("access_denied" === error) return {
|
|
4006
|
+
ok: false,
|
|
4007
|
+
reason: "denied"
|
|
4008
|
+
};
|
|
4009
|
+
if ("expired_token" === error) return {
|
|
4010
|
+
ok: false,
|
|
4011
|
+
reason: "expired"
|
|
4012
|
+
};
|
|
4013
|
+
if ("slow_down" === error) interval += 5;
|
|
4014
|
+
else if (error && "authorization_pending" !== error) return {
|
|
4015
|
+
ok: false,
|
|
4016
|
+
reason: "error",
|
|
4017
|
+
message: String(data.message || error)
|
|
4018
|
+
};
|
|
4019
|
+
if (Date.now() + 1000 * interval >= deadline) return {
|
|
4020
|
+
ok: false,
|
|
4021
|
+
reason: "pending"
|
|
4022
|
+
};
|
|
4023
|
+
await new Promise((r)=>setTimeout(r, 1000 * interval));
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
3685
4026
|
const login_schema = {
|
|
3686
4027
|
name: "extension_login",
|
|
3687
|
-
description: "Authenticate to extension.dev
|
|
4028
|
+
description: "Authenticate to extension.dev and store a project-scoped access token locally so extension_publish can use it. Two-phase: call with `project` to get a code + URL for the user to authorize, then call again with the returned `deviceCode` to finish. The server picks the flow: the extension.dev-gated device flow (you authorize at extension.dev/device; GitHub federation happens server-side, no GitHub token on this machine) or, as a fallback, the legacy GitHub device flow. Never returns the token. This is the only tool besides extension_publish that talks to the hosted platform.",
|
|
3688
4029
|
inputSchema: {
|
|
3689
4030
|
type: "object",
|
|
3690
4031
|
properties: {
|
|
@@ -3747,6 +4088,52 @@ async function login_handler(args) {
|
|
|
3747
4088
|
} catch (err) {
|
|
3748
4089
|
return login_fail("LoginConfigError", err?.message || "Could not load login config.");
|
|
3749
4090
|
}
|
|
4091
|
+
if ("extensiondev" === config.provider) {
|
|
4092
|
+
if (args.deviceCode) {
|
|
4093
|
+
const poll = await pollDeviceToken({
|
|
4094
|
+
apiBase,
|
|
4095
|
+
path: config.deviceTokenUrl,
|
|
4096
|
+
project,
|
|
4097
|
+
deviceCode: String(args.deviceCode),
|
|
4098
|
+
interval: 5,
|
|
4099
|
+
budgetMs: RESUME_BUDGET_MS
|
|
4100
|
+
});
|
|
4101
|
+
if (poll.ok) return success(poll.creds);
|
|
4102
|
+
if ("expired" === poll.reason) return login_fail("LoginExpired", "The device code expired. Run extension_login again to restart.");
|
|
4103
|
+
if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied at extension.dev/device.");
|
|
4104
|
+
if ("error" === poll.reason) return login_fail("LoginError", poll.message || "Device login failed.");
|
|
4105
|
+
return login_pending({
|
|
4106
|
+
deviceCode: String(args.deviceCode),
|
|
4107
|
+
userCode: "(see the previous response)",
|
|
4108
|
+
verificationUri: config.verificationUri
|
|
4109
|
+
});
|
|
4110
|
+
}
|
|
4111
|
+
let start;
|
|
4112
|
+
try {
|
|
4113
|
+
start = await requestDeviceCode({
|
|
4114
|
+
apiBase,
|
|
4115
|
+
path: config.deviceCodeUrl,
|
|
4116
|
+
project
|
|
4117
|
+
});
|
|
4118
|
+
} catch (err) {
|
|
4119
|
+
return login_fail("LoginStartError", err?.message || "Could not start the device flow.");
|
|
4120
|
+
}
|
|
4121
|
+
const poll = await pollDeviceToken({
|
|
4122
|
+
apiBase,
|
|
4123
|
+
path: config.deviceTokenUrl,
|
|
4124
|
+
project,
|
|
4125
|
+
deviceCode: start.deviceCode,
|
|
4126
|
+
interval: start.interval,
|
|
4127
|
+
budgetMs: FIRST_CALL_BUDGET_MS
|
|
4128
|
+
});
|
|
4129
|
+
if (poll.ok) return success(poll.creds);
|
|
4130
|
+
if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied at extension.dev/device.");
|
|
4131
|
+
return login_pending({
|
|
4132
|
+
deviceCode: start.deviceCode,
|
|
4133
|
+
userCode: start.userCode,
|
|
4134
|
+
verificationUri: start.verificationUri
|
|
4135
|
+
});
|
|
4136
|
+
}
|
|
3750
4137
|
if (args.deviceCode) {
|
|
3751
4138
|
const poll = await pollForToken({
|
|
3752
4139
|
clientId: config.clientId,
|
|
@@ -3825,6 +4212,7 @@ async function whoami_handler() {
|
|
|
3825
4212
|
workspaceSlug: creds.workspaceSlug,
|
|
3826
4213
|
projectSlug: creds.projectSlug,
|
|
3827
4214
|
api: creds.api,
|
|
4215
|
+
provider: creds.provider ?? "github",
|
|
3828
4216
|
expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
|
|
3829
4217
|
expiresInSeconds: creds.expiresAt ? creds.expiresAt - now : null,
|
|
3830
4218
|
expired,
|
|
@@ -3849,7 +4237,7 @@ async function logout_handler() {
|
|
|
3849
4237
|
}
|
|
3850
4238
|
const install_browser_schema = {
|
|
3851
4239
|
name: "extension_install_browser",
|
|
3852
|
-
description: "Install a managed browser binary for extension testing. Useful in CI, Docker, or fresh environments where browsers are not pre-installed.",
|
|
4240
|
+
description: "Install a managed browser binary for extension testing. Useful in CI, Docker, or fresh environments where browsers are not pre-installed. This downloads ~580-625 MB in a single blocking call (30s+ on a fast link); on a slow network it can exceed a client's default request timeout, so allow a generous timeout when calling it.",
|
|
3853
4241
|
inputSchema: {
|
|
3854
4242
|
type: "object",
|
|
3855
4243
|
properties: {
|
|
@@ -4325,35 +4713,70 @@ async function detect_browsers_handler(args) {
|
|
|
4325
4713
|
}
|
|
4326
4714
|
const doctor_schema = {
|
|
4327
4715
|
name: "extension_doctor",
|
|
4328
|
-
description: "Diagnose a dev session end-to-end: ready contract, dev-server process, control-port agreement, control channel, eval token, executor, and browser liveness. Returns one {check, status, detail, remediation?} entry per leg in dependency order — a 'skip' names the check that blocked it and is NOT a pass. Run this first when any act tool (storage/reload/eval/open) errors unexpectedly. Wraps `extension doctor`.",
|
|
4716
|
+
description: "Diagnose a dev session end-to-end: ready contract, dev-server process, control-port agreement, control channel, eval token, executor, and browser liveness. Returns one {check, status, detail, remediation?} entry per leg in dependency order — a 'skip' names the check that blocked it and is NOT a pass. Run this first when any act tool (storage/reload/eval/open) errors unexpectedly. Wraps `extension doctor`. Call with no projectPath for a pre-flight environment check (node, extension CLI, template cache) before any project exists.",
|
|
4329
4717
|
inputSchema: {
|
|
4330
4718
|
type: "object",
|
|
4331
4719
|
properties: {
|
|
4332
4720
|
projectPath: {
|
|
4333
4721
|
type: "string",
|
|
4334
|
-
description: "Path to the extension project root"
|
|
4722
|
+
description: "Path to the extension project root. Omit for a pre-flight environment check with no project."
|
|
4335
4723
|
},
|
|
4336
4724
|
browser: {
|
|
4337
4725
|
type: "string",
|
|
4338
4726
|
description: "Browser session to diagnose. Defaults to the active dev session's browser for this project."
|
|
4339
4727
|
}
|
|
4340
|
-
}
|
|
4341
|
-
required: [
|
|
4342
|
-
"projectPath"
|
|
4343
|
-
]
|
|
4728
|
+
}
|
|
4344
4729
|
}
|
|
4345
4730
|
};
|
|
4731
|
+
async function environmentPreflight() {
|
|
4732
|
+
const checks = [];
|
|
4733
|
+
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
4734
|
+
checks.push({
|
|
4735
|
+
check: "node",
|
|
4736
|
+
status: nodeMajor >= 20 ? "pass" : "fail",
|
|
4737
|
+
detail: `Node ${process.versions.node} on ${process.platform}/${process.arch}`,
|
|
4738
|
+
remediation: nodeMajor >= 20 ? void 0 : "Extension.js needs Node >= 20.18."
|
|
4739
|
+
});
|
|
4740
|
+
const { code, stdout, stderr } = await runExtensionCli([
|
|
4741
|
+
"--version"
|
|
4742
|
+
], {
|
|
4743
|
+
timeoutMs: 60000
|
|
4744
|
+
});
|
|
4745
|
+
const cliVersion = stdout.trim() || stderr.trim();
|
|
4746
|
+
checks.push({
|
|
4747
|
+
check: "extension-cli",
|
|
4748
|
+
status: 0 === code ? "pass" : "fail",
|
|
4749
|
+
detail: 0 === code ? `extension CLI resolvable (${cliVersion})` : "extension CLI could not be resolved",
|
|
4750
|
+
remediation: 0 === code ? void 0 : "Install locally (npm i -D extension) or rely on npx; check network access to the npm registry."
|
|
4751
|
+
});
|
|
4752
|
+
const cacheFile = node_path.join(node_os.homedir(), ".cache", "extension-js", "templates-meta.json");
|
|
4753
|
+
const cacheExists = node_fs.existsSync(cacheFile);
|
|
4754
|
+
checks.push({
|
|
4755
|
+
check: "template-cache",
|
|
4756
|
+
status: cacheExists ? "pass" : "warn",
|
|
4757
|
+
detail: cacheExists ? `Template catalog cached at ${cacheFile}` : "Template catalog not cached yet (first extension_list_templates will fetch it)"
|
|
4758
|
+
});
|
|
4759
|
+
const healthy = checks.every((c)=>"fail" !== c.status);
|
|
4760
|
+
return JSON.stringify({
|
|
4761
|
+
mode: "environment",
|
|
4762
|
+
healthy,
|
|
4763
|
+
checks,
|
|
4764
|
+
hint: "Pass projectPath to diagnose a live dev session end-to-end."
|
|
4765
|
+
});
|
|
4766
|
+
}
|
|
4346
4767
|
async function doctor_handler(args) {
|
|
4347
|
-
|
|
4768
|
+
if (!args.projectPath) return environmentPreflight();
|
|
4769
|
+
const projectPath = args.projectPath;
|
|
4770
|
+
const { browser } = resolveSessionBrowser(projectPath, args.browser);
|
|
4348
4771
|
const { code, stdout, stderr } = await runExtensionCli([
|
|
4349
4772
|
"doctor",
|
|
4350
|
-
|
|
4773
|
+
projectPath,
|
|
4351
4774
|
"--browser",
|
|
4352
4775
|
browser,
|
|
4353
4776
|
"--output",
|
|
4354
4777
|
"json"
|
|
4355
4778
|
], {
|
|
4356
|
-
cwd:
|
|
4779
|
+
cwd: projectPath
|
|
4357
4780
|
});
|
|
4358
4781
|
const out = stdout.trim();
|
|
4359
4782
|
try {
|
|
@@ -4599,6 +5022,31 @@ async function runCli(cmd, args) {
|
|
|
4599
5022
|
const apiBase = resolveApiBase(flag("api"));
|
|
4600
5023
|
try {
|
|
4601
5024
|
const config = await fetchLoginConfig(apiBase);
|
|
5025
|
+
if ("extensiondev" === config.provider) {
|
|
5026
|
+
const start = await requestDeviceCode({
|
|
5027
|
+
apiBase,
|
|
5028
|
+
path: config.deviceCodeUrl,
|
|
5029
|
+
project
|
|
5030
|
+
});
|
|
5031
|
+
log("");
|
|
5032
|
+
log(` Open ${start.verificationUri} and enter code: ${start.userCode}`);
|
|
5033
|
+
log("");
|
|
5034
|
+
log(" Waiting for authorization...");
|
|
5035
|
+
const poll = await pollDeviceToken({
|
|
5036
|
+
apiBase,
|
|
5037
|
+
path: config.deviceTokenUrl,
|
|
5038
|
+
project,
|
|
5039
|
+
deviceCode: start.deviceCode,
|
|
5040
|
+
interval: start.interval,
|
|
5041
|
+
budgetMs: 1000 * start.expiresIn
|
|
5042
|
+
});
|
|
5043
|
+
if (!poll.ok) {
|
|
5044
|
+
log("denied" === poll.reason ? "Authorization was denied at extension.dev/device." : "expired" === poll.reason ? "The device code expired. Run login again." : "Timed out waiting for authorization. Run login again.");
|
|
5045
|
+
return 1;
|
|
5046
|
+
}
|
|
5047
|
+
log(`Logged in to ${poll.creds.workspaceSlug}/${poll.creds.projectSlug}.`);
|
|
5048
|
+
return 0;
|
|
5049
|
+
}
|
|
4602
5050
|
const start = await startDeviceCode({
|
|
4603
5051
|
clientId: config.clientId,
|
|
4604
5052
|
scope: config.scope
|