@extension.dev/mcp 4.5.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 +23 -0
- package/README.md +4 -3
- package/dist/module.js +832 -213
- 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 +48 -0
- package/dist/src/tools/doctor.d.ts +1 -2
- package/dist/src/tools/publish.d.ts +1 -6
- package/package.json +2 -2
- package/server.json +3 -3
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";
|
|
@@ -32,6 +31,12 @@ __webpack_require__.d(create_namespaceObject, {
|
|
|
32
31
|
handler: ()=>create_handler,
|
|
33
32
|
schema: ()=>create_schema
|
|
34
33
|
});
|
|
34
|
+
var deploy_namespaceObject = {};
|
|
35
|
+
__webpack_require__.r(deploy_namespaceObject);
|
|
36
|
+
__webpack_require__.d(deploy_namespaceObject, {
|
|
37
|
+
handler: ()=>deploy_handler,
|
|
38
|
+
schema: ()=>deploy_schema
|
|
39
|
+
});
|
|
35
40
|
var detect_browsers_namespaceObject = {};
|
|
36
41
|
__webpack_require__.r(detect_browsers_namespaceObject);
|
|
37
42
|
__webpack_require__.d(detect_browsers_namespaceObject, {
|
|
@@ -194,7 +199,33 @@ __webpack_require__.d(whoami_namespaceObject, {
|
|
|
194
199
|
handler: ()=>whoami_handler,
|
|
195
200
|
schema: ()=>whoami_schema
|
|
196
201
|
});
|
|
197
|
-
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
|
+
}
|
|
198
229
|
const create_schema = {
|
|
199
230
|
name: "extension_create",
|
|
200
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.",
|
|
@@ -212,7 +243,7 @@ const create_schema = {
|
|
|
212
243
|
template: {
|
|
213
244
|
type: "string",
|
|
214
245
|
default: "typescript",
|
|
215
|
-
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."
|
|
216
247
|
},
|
|
217
248
|
install: {
|
|
218
249
|
type: "boolean",
|
|
@@ -228,30 +259,47 @@ const create_schema = {
|
|
|
228
259
|
async function create_handler(args) {
|
|
229
260
|
const start = Date.now();
|
|
230
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);
|
|
231
268
|
try {
|
|
232
269
|
const result = await extensionCreate(projectInput, {
|
|
233
270
|
template: args.template ?? "typescript",
|
|
234
271
|
install: args.install ?? true,
|
|
235
272
|
logger: {
|
|
236
|
-
log: ()
|
|
237
|
-
error: ()
|
|
273
|
+
log: capture("log"),
|
|
274
|
+
error: capture("error")
|
|
238
275
|
}
|
|
239
276
|
});
|
|
277
|
+
const packageManager = result.depsInstalled ? detectPackageManager(result.projectPath) : "npm";
|
|
278
|
+
const runDev = `${packageManager} run dev`;
|
|
240
279
|
return JSON.stringify({
|
|
241
280
|
projectPath: result.projectPath,
|
|
242
281
|
projectName: result.projectName,
|
|
243
282
|
template: result.template,
|
|
244
283
|
depsInstalled: result.depsInstalled,
|
|
284
|
+
packageManager: result.depsInstalled ? packageManager : null,
|
|
245
285
|
duration: Date.now() - start,
|
|
246
|
-
nextSteps: [
|
|
286
|
+
nextSteps: result.depsInstalled ? [
|
|
247
287
|
`cd ${result.projectPath}`,
|
|
288
|
+
runDev
|
|
289
|
+
] : [
|
|
290
|
+
`cd ${result.projectPath}`,
|
|
291
|
+
"npm install",
|
|
248
292
|
"npm run dev"
|
|
249
|
-
]
|
|
293
|
+
],
|
|
294
|
+
...result.depsInstalled ? {} : {
|
|
295
|
+
warnings: logTail()
|
|
296
|
+
}
|
|
250
297
|
});
|
|
251
298
|
} catch (err) {
|
|
252
299
|
return JSON.stringify({
|
|
253
300
|
error: err instanceof Error ? err.message : String(err),
|
|
254
|
-
duration: Date.now() - start
|
|
301
|
+
duration: Date.now() - start,
|
|
302
|
+
log: logTail()
|
|
255
303
|
});
|
|
256
304
|
}
|
|
257
305
|
}
|
|
@@ -292,8 +340,49 @@ async function listTemplates(filters) {
|
|
|
292
340
|
if (filters?.tags?.length) templates = templates.filter((t)=>filters.tags.some((tag)=>t.tags?.includes(tag) || t.aiRecommendFor?.includes(tag)));
|
|
293
341
|
if (filters?.featured) templates = templates.filter((t)=>t.featured);
|
|
294
342
|
if (filters?.query) {
|
|
295
|
-
const
|
|
296
|
-
|
|
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);
|
|
297
386
|
}
|
|
298
387
|
return templates;
|
|
299
388
|
}
|
|
@@ -303,7 +392,7 @@ async function getTemplateBySlug(slug) {
|
|
|
303
392
|
}
|
|
304
393
|
const list_templates_schema = {
|
|
305
394
|
name: "extension_list_templates",
|
|
306
|
-
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.",
|
|
307
396
|
inputSchema: {
|
|
308
397
|
type: "object",
|
|
309
398
|
properties: {
|
|
@@ -312,13 +401,10 @@ const list_templates_schema = {
|
|
|
312
401
|
enum: [
|
|
313
402
|
"content",
|
|
314
403
|
"sidebar",
|
|
315
|
-
"action",
|
|
316
404
|
"newtab",
|
|
317
|
-
"devtools",
|
|
318
|
-
"options",
|
|
319
405
|
"background"
|
|
320
406
|
],
|
|
321
|
-
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."
|
|
322
408
|
},
|
|
323
409
|
framework: {
|
|
324
410
|
type: "string",
|
|
@@ -329,7 +415,7 @@ const list_templates_schema = {
|
|
|
329
415
|
"preact",
|
|
330
416
|
""
|
|
331
417
|
],
|
|
332
|
-
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."
|
|
333
419
|
},
|
|
334
420
|
tags: {
|
|
335
421
|
type: "array",
|
|
@@ -344,7 +430,7 @@ const list_templates_schema = {
|
|
|
344
430
|
},
|
|
345
431
|
query: {
|
|
346
432
|
type: "string",
|
|
347
|
-
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."
|
|
348
434
|
}
|
|
349
435
|
}
|
|
350
436
|
}
|
|
@@ -368,112 +454,6 @@ async function list_templates_handler(args) {
|
|
|
368
454
|
templates: results
|
|
369
455
|
});
|
|
370
456
|
}
|
|
371
|
-
const build_schema = {
|
|
372
|
-
name: "extension_build",
|
|
373
|
-
description: "Build a browser extension for production. Outputs to dist/<browser>/. Optionally creates .zip for store submission.",
|
|
374
|
-
inputSchema: {
|
|
375
|
-
type: "object",
|
|
376
|
-
properties: {
|
|
377
|
-
projectPath: {
|
|
378
|
-
type: "string",
|
|
379
|
-
description: "Path to the extension project root"
|
|
380
|
-
},
|
|
381
|
-
browser: {
|
|
382
|
-
type: "string",
|
|
383
|
-
enum: [
|
|
384
|
-
"chrome",
|
|
385
|
-
"chromium",
|
|
386
|
-
"edge",
|
|
387
|
-
"brave",
|
|
388
|
-
"opera",
|
|
389
|
-
"vivaldi",
|
|
390
|
-
"yandex",
|
|
391
|
-
"firefox",
|
|
392
|
-
"waterfox",
|
|
393
|
-
"librewolf",
|
|
394
|
-
"safari",
|
|
395
|
-
"chromium-based",
|
|
396
|
-
"gecko-based",
|
|
397
|
-
"firefox-based",
|
|
398
|
-
"webkit-based"
|
|
399
|
-
],
|
|
400
|
-
default: "chrome",
|
|
401
|
-
description: "Target browser"
|
|
402
|
-
},
|
|
403
|
-
zip: {
|
|
404
|
-
type: "boolean",
|
|
405
|
-
default: false,
|
|
406
|
-
description: "Create a .zip file for store distribution"
|
|
407
|
-
},
|
|
408
|
-
zipSource: {
|
|
409
|
-
type: "boolean",
|
|
410
|
-
default: false,
|
|
411
|
-
description: "Include source code zip (required by some stores)"
|
|
412
|
-
},
|
|
413
|
-
zipFilename: {
|
|
414
|
-
type: "string",
|
|
415
|
-
description: "Custom .zip file name (defaults to name and version)"
|
|
416
|
-
},
|
|
417
|
-
polyfill: {
|
|
418
|
-
type: "boolean",
|
|
419
|
-
default: false,
|
|
420
|
-
description: "Apply cross-browser polyfill"
|
|
421
|
-
},
|
|
422
|
-
silent: {
|
|
423
|
-
type: "boolean",
|
|
424
|
-
default: false,
|
|
425
|
-
description: "Suppress build output"
|
|
426
|
-
},
|
|
427
|
-
mode: {
|
|
428
|
-
type: "string",
|
|
429
|
-
enum: [
|
|
430
|
-
"development",
|
|
431
|
-
"production",
|
|
432
|
-
"none"
|
|
433
|
-
],
|
|
434
|
-
default: "production",
|
|
435
|
-
description: "Bundler mode override (also sets NODE_ENV)"
|
|
436
|
-
}
|
|
437
|
-
},
|
|
438
|
-
required: [
|
|
439
|
-
"projectPath"
|
|
440
|
-
]
|
|
441
|
-
}
|
|
442
|
-
};
|
|
443
|
-
async function build_handler(args) {
|
|
444
|
-
const start = Date.now();
|
|
445
|
-
try {
|
|
446
|
-
const summary = await extensionBuild(args.projectPath, {
|
|
447
|
-
browser: args.browser ?? "chrome",
|
|
448
|
-
zip: args.zip ?? false,
|
|
449
|
-
zipSource: args.zipSource ?? false,
|
|
450
|
-
...args.zipFilename ? {
|
|
451
|
-
zipFilename: args.zipFilename
|
|
452
|
-
} : {},
|
|
453
|
-
...void 0 !== args.polyfill ? {
|
|
454
|
-
polyfill: args.polyfill
|
|
455
|
-
} : {},
|
|
456
|
-
...void 0 !== args.silent ? {
|
|
457
|
-
silent: args.silent
|
|
458
|
-
} : {},
|
|
459
|
-
...args.mode ? {
|
|
460
|
-
mode: args.mode
|
|
461
|
-
} : {},
|
|
462
|
-
exitOnError: false
|
|
463
|
-
});
|
|
464
|
-
return JSON.stringify({
|
|
465
|
-
success: true,
|
|
466
|
-
...summary,
|
|
467
|
-
duration: Date.now() - start
|
|
468
|
-
});
|
|
469
|
-
} catch (err) {
|
|
470
|
-
return JSON.stringify({
|
|
471
|
-
error: err instanceof Error ? err.message : String(err),
|
|
472
|
-
duration: Date.now() - start,
|
|
473
|
-
hint: "Check that the project has a valid src/manifest.json and all dependencies are installed."
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
457
|
const PINNED_CLI_VERSION = String(package_namespaceObject.El.OP ?? "latest").replace(/^[\^~]/, "");
|
|
478
458
|
function pinnedCliVersion() {
|
|
479
459
|
const override = String(process.env.EXTENSION_MCP_CLI_VERSION || "").trim();
|
|
@@ -555,6 +535,126 @@ function spawnExtensionCli(args, options) {
|
|
|
555
535
|
child.unref();
|
|
556
536
|
return child;
|
|
557
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
|
+
}
|
|
558
658
|
const process_manager_sessions = new Map();
|
|
559
659
|
function sessionKey(projectPath, browser) {
|
|
560
660
|
return `${node_path.resolve(projectPath)}::${browser}`;
|
|
@@ -719,9 +819,20 @@ async function dev_handler(args) {
|
|
|
719
819
|
projectPath: args.projectPath,
|
|
720
820
|
status: "started",
|
|
721
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.",
|
|
722
|
-
earlyOutput: earlyOutput.slice(0, 500)
|
|
822
|
+
earlyOutput: denoiseEarlyOutput(earlyOutput).slice(0, 500)
|
|
723
823
|
});
|
|
724
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
|
+
}
|
|
725
836
|
const start_schema = {
|
|
726
837
|
name: "extension_start",
|
|
727
838
|
description: "Build the extension for production and immediately preview it in a browser. Combines build + preview in one step. No hot reload.",
|
|
@@ -1011,7 +1122,7 @@ const get_template_source_schema = {
|
|
|
1011
1122
|
properties: {
|
|
1012
1123
|
slug: {
|
|
1013
1124
|
type: "string",
|
|
1014
|
-
description: "Template slug (e.g. '
|
|
1125
|
+
description: "Template slug (e.g. 'ai-claude', 'content-react')"
|
|
1015
1126
|
},
|
|
1016
1127
|
files: {
|
|
1017
1128
|
type: "array",
|
|
@@ -1094,6 +1205,91 @@ function isChromiumFamily(browser) {
|
|
|
1094
1205
|
function isGeckoFamily(browser) {
|
|
1095
1206
|
return GECKO_FAMILY.has(browser);
|
|
1096
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
|
+
]);
|
|
1097
1293
|
const manifest_validate_schema = {
|
|
1098
1294
|
name: "extension_manifest_validate",
|
|
1099
1295
|
description: "Validate a manifest.json file for correctness across browsers. Reports missing fields, invalid permissions, and cross-browser compatibility issues.",
|
|
@@ -1152,6 +1348,13 @@ async function manifest_validate_handler(args) {
|
|
|
1152
1348
|
if (!manifest.version) result.warnings.push("Missing field: version (required for store submission)");
|
|
1153
1349
|
const chromiumManifest = filterKeysForThisBrowser(manifest, "chrome");
|
|
1154
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
|
+
}
|
|
1155
1358
|
for (const browser of browsers){
|
|
1156
1359
|
const isChromium = isChromiumFamily(browser);
|
|
1157
1360
|
const isFirefox = isGeckoFamily(browser);
|
|
@@ -1189,9 +1392,15 @@ async function manifest_validate_handler(args) {
|
|
|
1189
1392
|
if (chromiumManifest.action || manifest["firefox:browser_action"]) surfaces.push("action");
|
|
1190
1393
|
if (chromiumManifest.chrome_url_overrides?.newtab) surfaces.push("newtab");
|
|
1191
1394
|
if (chromiumManifest.background) surfaces.push("background");
|
|
1192
|
-
|
|
1395
|
+
const distinctive = surfaces.filter((s)=>"background" !== s);
|
|
1396
|
+
const matchOn = distinctive.length ? distinctive : surfaces;
|
|
1397
|
+
if (matchOn.length) try {
|
|
1193
1398
|
const templates = await listTemplates();
|
|
1194
|
-
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)=>({
|
|
1195
1404
|
slug: t.slug,
|
|
1196
1405
|
surfaces: t.surfaces
|
|
1197
1406
|
}));
|
|
@@ -1313,12 +1522,21 @@ async function inspect_handler(args) {
|
|
|
1313
1522
|
byType[f.type].count++;
|
|
1314
1523
|
byType[f.type].size += f.size;
|
|
1315
1524
|
}
|
|
1525
|
+
const sourcemapSize = byType.sourcemap?.size ?? 0;
|
|
1526
|
+
const buildType = sourcemapSize > 0 ? "development" : "production";
|
|
1527
|
+
const shippableSize = totalSize - sourcemapSize;
|
|
1316
1528
|
const result = {
|
|
1317
1529
|
browser,
|
|
1318
1530
|
distPath,
|
|
1531
|
+
buildType,
|
|
1319
1532
|
totalSize,
|
|
1320
1533
|
totalSizeFormatted: formatBytes(totalSize),
|
|
1534
|
+
shippableSize,
|
|
1535
|
+
shippableSizeFormatted: formatBytes(shippableSize),
|
|
1321
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
|
+
} : {},
|
|
1322
1540
|
manifest: {
|
|
1323
1541
|
name: manifest.name,
|
|
1324
1542
|
version: manifest.version,
|
|
@@ -1763,13 +1981,16 @@ class CDPClient extends CDPConnection {
|
|
|
1763
1981
|
}
|
|
1764
1982
|
async function resolveCdpPort(projectPath, browser, options) {
|
|
1765
1983
|
const waitMs = options?.waitMs ?? 20000;
|
|
1984
|
+
const graceMs = options?.graceMs ?? 2500;
|
|
1766
1985
|
const readyPath = node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json");
|
|
1767
1986
|
const deadline = Date.now() + waitMs;
|
|
1768
1987
|
let contractSeen = false;
|
|
1988
|
+
let contractSeenAt = null;
|
|
1769
1989
|
for(;;){
|
|
1770
1990
|
try {
|
|
1771
1991
|
const contract = JSON.parse(node_fs.readFileSync(readyPath, "utf8"));
|
|
1772
1992
|
contractSeen = true;
|
|
1993
|
+
if (null == contractSeenAt) contractSeenAt = Date.now();
|
|
1773
1994
|
if ("number" == typeof contract.cdpPort) return {
|
|
1774
1995
|
port: contract.cdpPort,
|
|
1775
1996
|
source: "contract"
|
|
@@ -1777,7 +1998,8 @@ async function resolveCdpPort(projectPath, browser, options) {
|
|
|
1777
1998
|
} catch {
|
|
1778
1999
|
if (!contractSeen) break;
|
|
1779
2000
|
}
|
|
1780
|
-
|
|
2001
|
+
const effectiveDeadline = null != contractSeenAt ? Math.min(deadline, contractSeenAt + graceMs) : deadline;
|
|
2002
|
+
if (Date.now() >= effectiveDeadline) break;
|
|
1781
2003
|
await new Promise((resolve)=>setTimeout(resolve, 500));
|
|
1782
2004
|
}
|
|
1783
2005
|
if (!contractSeen && await isCdpEndpoint(9222)) return {
|
|
@@ -1947,16 +2169,26 @@ async function source_inspect_handler(args) {
|
|
|
1947
2169
|
const cdp = new CDPClient();
|
|
1948
2170
|
try {
|
|
1949
2171
|
const allTargets = await CDPClient.discoverTargets(cdpPort);
|
|
1950
|
-
const
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
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
|
+
}
|
|
1960
2192
|
const target = args.url ? pageTargets.find((t)=>t.url.includes(args.url)) ?? pageTargets[0] : pageTargets[0];
|
|
1961
2193
|
const browserWsUrl = await CDPClient.discoverBrowserWsUrl(cdpPort);
|
|
1962
2194
|
await cdp.connect(browserWsUrl);
|
|
@@ -2452,7 +2684,7 @@ function commonFlags(args) {
|
|
|
2452
2684
|
}
|
|
2453
2685
|
const eval_schema = {
|
|
2454
2686
|
name: "extension_eval",
|
|
2455
|
-
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`.",
|
|
2456
2688
|
inputSchema: {
|
|
2457
2689
|
type: "object",
|
|
2458
2690
|
properties: {
|
|
@@ -2480,11 +2712,11 @@ const eval_schema = {
|
|
|
2480
2712
|
},
|
|
2481
2713
|
url: {
|
|
2482
2714
|
type: "string",
|
|
2483
|
-
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."
|
|
2484
2716
|
},
|
|
2485
2717
|
tab: {
|
|
2486
2718
|
type: "number",
|
|
2487
|
-
description: "
|
|
2719
|
+
description: "Numeric chrome.tabs id. Required for context content/page/popup on Chromium (the `url` arg does not substitute for it)."
|
|
2488
2720
|
},
|
|
2489
2721
|
browser: {
|
|
2490
2722
|
type: "string",
|
|
@@ -2696,7 +2928,21 @@ async function open_handler(args) {
|
|
|
2696
2928
|
if ("command" === args.surface && args.name) cli.push("--name", args.name);
|
|
2697
2929
|
cli.push("--browser", browser);
|
|
2698
2930
|
if (null != args.timeout) cli.push("--timeout", String(args.timeout));
|
|
2699
|
-
|
|
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;
|
|
2700
2946
|
}
|
|
2701
2947
|
const dom_inspect_schema = {
|
|
2702
2948
|
name: "extension_dom_inspect",
|
|
@@ -2773,8 +3019,9 @@ async function dom_inspect_handler(args) {
|
|
|
2773
3019
|
ok: false,
|
|
2774
3020
|
error: {
|
|
2775
3021
|
name: "BadRequest",
|
|
2776
|
-
message: "content/page inspect requires a tab id"
|
|
2777
|
-
}
|
|
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)."
|
|
2778
3025
|
});
|
|
2779
3026
|
const cli = [
|
|
2780
3027
|
"inspect",
|
|
@@ -2806,13 +3053,17 @@ function readCredentials() {
|
|
|
2806
3053
|
if (1 !== data.version) return null;
|
|
2807
3054
|
const token = String(data.token || "").trim();
|
|
2808
3055
|
if (!token) return null;
|
|
3056
|
+
const provider = "extensiondev" === data.provider || "github" === data.provider ? data.provider : void 0;
|
|
2809
3057
|
return {
|
|
2810
3058
|
version: 1,
|
|
2811
3059
|
token,
|
|
2812
3060
|
workspaceSlug: String(data.workspaceSlug || ""),
|
|
2813
3061
|
projectSlug: String(data.projectSlug || ""),
|
|
2814
3062
|
expiresAt: Number(data.expiresAt || 0),
|
|
2815
|
-
api: String(data.api || "")
|
|
3063
|
+
api: String(data.api || ""),
|
|
3064
|
+
...provider ? {
|
|
3065
|
+
provider
|
|
3066
|
+
} : {}
|
|
2816
3067
|
};
|
|
2817
3068
|
} catch {
|
|
2818
3069
|
return null;
|
|
@@ -2884,8 +3135,12 @@ function safeApiBase(raw) {
|
|
|
2884
3135
|
async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
|
|
2885
3136
|
const override = String(process.env.EXTENSION_DEV_GITHUB_CLIENT_ID || "").trim();
|
|
2886
3137
|
if (override) return {
|
|
3138
|
+
provider: "github",
|
|
2887
3139
|
clientId: override,
|
|
2888
|
-
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"
|
|
2889
3144
|
};
|
|
2890
3145
|
const res = await fetchImpl(`${apiBase}/api/cli/login/config`, {
|
|
2891
3146
|
headers: {
|
|
@@ -2894,12 +3149,32 @@ async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
|
|
|
2894
3149
|
});
|
|
2895
3150
|
if (!res.ok) throw new Error(`Could not fetch login config from ${apiBase} (${res.status}).`);
|
|
2896
3151
|
const data = await res.json().catch(()=>({}));
|
|
3152
|
+
const provider = "extensiondev" === data.provider ? "extensiondev" : "github";
|
|
2897
3153
|
const clientId = String(data.githubClientId || "").trim();
|
|
2898
|
-
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.");
|
|
2899
3155
|
return {
|
|
3156
|
+
provider,
|
|
2900
3157
|
clientId,
|
|
2901
|
-
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
|
|
2902
3175
|
};
|
|
3176
|
+
writeCredentials(creds);
|
|
3177
|
+
return creds;
|
|
2903
3178
|
}
|
|
2904
3179
|
async function exchangeAndPersist(args) {
|
|
2905
3180
|
const doFetch = args.fetchImpl ?? fetch;
|
|
@@ -2924,18 +3199,11 @@ async function exchangeAndPersist(args) {
|
|
|
2924
3199
|
};
|
|
2925
3200
|
}
|
|
2926
3201
|
if (!res.ok) throw new Error(`Login exchange failed (${res.status}): ${data.message || "unknown error"}`);
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
workspaceSlug: String(data.workspaceSlug || ""),
|
|
2933
|
-
projectSlug: String(data.projectSlug || ""),
|
|
2934
|
-
expiresAt: Number(data.expiresAt || 0),
|
|
2935
|
-
api: args.apiBase
|
|
2936
|
-
};
|
|
2937
|
-
writeCredentials(creds);
|
|
2938
|
-
return creds;
|
|
3202
|
+
return persistTokenResponse({
|
|
3203
|
+
apiBase: args.apiBase,
|
|
3204
|
+
data,
|
|
3205
|
+
provider: "github"
|
|
3206
|
+
});
|
|
2939
3207
|
}
|
|
2940
3208
|
function resolveToken() {
|
|
2941
3209
|
const fromEnv = String(process.env.EXTENSION_DEV_TOKEN || "").trim();
|
|
@@ -3007,53 +3275,60 @@ async function publish(options = {}) {
|
|
|
3007
3275
|
}
|
|
3008
3276
|
const publish_schema = {
|
|
3009
3277
|
name: "extension_publish",
|
|
3010
|
-
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.",
|
|
3011
3279
|
inputSchema: {
|
|
3012
3280
|
type: "object",
|
|
3013
3281
|
properties: {
|
|
3014
|
-
projectPath: {
|
|
3015
|
-
type: "string",
|
|
3016
|
-
description: "Path to the extension project root"
|
|
3017
|
-
},
|
|
3018
3282
|
ttlHours: {
|
|
3019
3283
|
type: "number",
|
|
3020
|
-
description: "
|
|
3284
|
+
description: "Private-project share-link lifetime in hours, 1-168 (default 24). Ignored for public projects."
|
|
3021
3285
|
},
|
|
3022
3286
|
buildSha: {
|
|
3023
3287
|
type: "string",
|
|
3024
|
-
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."
|
|
3025
3289
|
},
|
|
3026
3290
|
api: {
|
|
3027
3291
|
type: "string",
|
|
3028
3292
|
description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
|
|
3029
3293
|
}
|
|
3030
3294
|
},
|
|
3031
|
-
required: [
|
|
3032
|
-
"projectPath"
|
|
3033
|
-
]
|
|
3295
|
+
required: []
|
|
3034
3296
|
}
|
|
3035
3297
|
};
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
if (!token) return JSON.stringify({
|
|
3298
|
+
function fail(name, message) {
|
|
3299
|
+
return JSON.stringify({
|
|
3039
3300
|
ok: false,
|
|
3040
3301
|
error: {
|
|
3041
|
-
name
|
|
3042
|
-
message
|
|
3302
|
+
name,
|
|
3303
|
+
message
|
|
3043
3304
|
}
|
|
3044
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
|
+
}
|
|
3045
3317
|
const result = await publish({
|
|
3046
3318
|
ttlHours: args.ttlHours,
|
|
3047
3319
|
buildSha: args.buildSha,
|
|
3048
3320
|
api: args.api,
|
|
3049
3321
|
token
|
|
3050
3322
|
});
|
|
3051
|
-
|
|
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);
|
|
3052
3327
|
}
|
|
3053
3328
|
const release_promote_DEFAULT_API = "https://www.extension.dev";
|
|
3054
3329
|
const release_promote_schema = {
|
|
3055
3330
|
name: "extension_release_promote",
|
|
3056
|
-
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).",
|
|
3057
3332
|
inputSchema: {
|
|
3058
3333
|
type: "object",
|
|
3059
3334
|
properties: {
|
|
@@ -3095,7 +3370,7 @@ const release_promote_schema = {
|
|
|
3095
3370
|
]
|
|
3096
3371
|
}
|
|
3097
3372
|
};
|
|
3098
|
-
function
|
|
3373
|
+
function release_promote_fail(name, message) {
|
|
3099
3374
|
return JSON.stringify({
|
|
3100
3375
|
ok: false,
|
|
3101
3376
|
error: {
|
|
@@ -3106,12 +3381,12 @@ function fail(name, message) {
|
|
|
3106
3381
|
}
|
|
3107
3382
|
async function release_promote_handler(args) {
|
|
3108
3383
|
const token = resolveToken();
|
|
3109
|
-
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.");
|
|
3110
3385
|
const buildId = String(args.buildId || "").trim();
|
|
3111
3386
|
const channel = String(args.channel || "").trim();
|
|
3112
|
-
if (!buildId || !channel) return
|
|
3387
|
+
if (!buildId || !channel) return release_promote_fail("ReleaseInputError", "buildId and channel are required.");
|
|
3113
3388
|
const apiCheck = safeApiBase(String(args.api || process.env.EXTENSION_DEV_API_URL || release_promote_DEFAULT_API));
|
|
3114
|
-
if (!apiCheck.ok) return
|
|
3389
|
+
if (!apiCheck.ok) return release_promote_fail("ReleaseConfigError", apiCheck.message);
|
|
3115
3390
|
const url = `${apiCheck.base}/api/cli/release/promote`;
|
|
3116
3391
|
const body = {
|
|
3117
3392
|
buildId,
|
|
@@ -3132,7 +3407,7 @@ async function release_promote_handler(args) {
|
|
|
3132
3407
|
body: JSON.stringify(body)
|
|
3133
3408
|
});
|
|
3134
3409
|
} catch (err) {
|
|
3135
|
-
return
|
|
3410
|
+
return release_promote_fail("ReleaseNetworkError", `Could not reach ${url}: ${err?.message || err}`);
|
|
3136
3411
|
}
|
|
3137
3412
|
const text = await res.text();
|
|
3138
3413
|
let data;
|
|
@@ -3143,12 +3418,117 @@ async function release_promote_handler(args) {
|
|
|
3143
3418
|
message: text
|
|
3144
3419
|
};
|
|
3145
3420
|
}
|
|
3146
|
-
if (!res.ok) return
|
|
3421
|
+
if (!res.ok) return release_promote_fail("ReleaseError", `promote failed (${res.status}): ${data?.message || text || "unknown error"}`);
|
|
3147
3422
|
return JSON.stringify(data);
|
|
3148
3423
|
}
|
|
3424
|
+
const deploy_DEFAULT_API = "https://www.extension.dev";
|
|
3425
|
+
const deploy_schema = {
|
|
3426
|
+
name: "extension_deploy",
|
|
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.",
|
|
3428
|
+
inputSchema: {
|
|
3429
|
+
type: "object",
|
|
3430
|
+
properties: {
|
|
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."
|
|
3443
|
+
},
|
|
3444
|
+
buildSha: {
|
|
3445
|
+
type: "string",
|
|
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."
|
|
3447
|
+
},
|
|
3448
|
+
channel: {
|
|
3449
|
+
type: "string",
|
|
3450
|
+
description: "Release channel to submit from (default stable)."
|
|
3451
|
+
},
|
|
3452
|
+
version: {
|
|
3453
|
+
type: "string",
|
|
3454
|
+
description: "Version label for the submission record (optional)."
|
|
3455
|
+
},
|
|
3456
|
+
dryRun: {
|
|
3457
|
+
type: "boolean",
|
|
3458
|
+
default: true,
|
|
3459
|
+
description: "Preflight only (verify auth, project, build, and store workflow). Pass false to actually dispatch the submission (irreversible, enters store review)."
|
|
3460
|
+
},
|
|
3461
|
+
api: {
|
|
3462
|
+
type: "string",
|
|
3463
|
+
description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
|
|
3464
|
+
}
|
|
3465
|
+
},
|
|
3466
|
+
required: [
|
|
3467
|
+
"browsers",
|
|
3468
|
+
"buildSha"
|
|
3469
|
+
]
|
|
3470
|
+
}
|
|
3471
|
+
};
|
|
3472
|
+
function deploy_fail(name, message) {
|
|
3473
|
+
return JSON.stringify({
|
|
3474
|
+
ok: false,
|
|
3475
|
+
error: {
|
|
3476
|
+
name,
|
|
3477
|
+
message
|
|
3478
|
+
}
|
|
3479
|
+
});
|
|
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)
|
|
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
|
|
3526
|
+
});
|
|
3527
|
+
}
|
|
3528
|
+
const SAFE_CEILING_MS = 50000;
|
|
3149
3529
|
const wait_schema = {
|
|
3150
3530
|
name: "extension_wait",
|
|
3151
|
-
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).",
|
|
3152
3532
|
inputSchema: {
|
|
3153
3533
|
type: "object",
|
|
3154
3534
|
properties: {
|
|
@@ -3162,8 +3542,8 @@ const wait_schema = {
|
|
|
3162
3542
|
},
|
|
3163
3543
|
timeout: {
|
|
3164
3544
|
type: "number",
|
|
3165
|
-
default:
|
|
3166
|
-
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."
|
|
3167
3547
|
}
|
|
3168
3548
|
},
|
|
3169
3549
|
required: [
|
|
@@ -3173,7 +3553,9 @@ const wait_schema = {
|
|
|
3173
3553
|
};
|
|
3174
3554
|
async function wait_handler(args) {
|
|
3175
3555
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser, "chrome");
|
|
3176
|
-
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;
|
|
3177
3559
|
const readyPath = node_path.resolve(args.projectPath, "dist", "extension-js", browser, "ready.json");
|
|
3178
3560
|
const start = Date.now();
|
|
3179
3561
|
const pollInterval = 1000;
|
|
@@ -3206,9 +3588,11 @@ async function wait_handler(args) {
|
|
|
3206
3588
|
}
|
|
3207
3589
|
return JSON.stringify({
|
|
3208
3590
|
status: "timeout",
|
|
3209
|
-
message: `Extension
|
|
3591
|
+
message: `Extension not ready after ${timeout}ms this call`,
|
|
3210
3592
|
readyPath,
|
|
3211
|
-
|
|
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."
|
|
3212
3596
|
});
|
|
3213
3597
|
}
|
|
3214
3598
|
const add_feature_schema = {
|
|
@@ -3378,7 +3762,23 @@ async function add_feature_handler(args) {
|
|
|
3378
3762
|
});
|
|
3379
3763
|
const template = await getTemplateBySlug(templateSlug);
|
|
3380
3764
|
const referenceFiles = template?.keyFiles ?? template?.files ?? [];
|
|
3381
|
-
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);
|
|
3382
3782
|
const filesToCreate = [];
|
|
3383
3783
|
const manifestUpdates = MANIFEST_ADDITIONS[args.feature] ?? {};
|
|
3384
3784
|
if ([
|
|
@@ -3392,24 +3792,42 @@ async function add_feature_handler(args) {
|
|
|
3392
3792
|
path: `src/${featureDir}/index.html`,
|
|
3393
3793
|
hint: "HTML entry point"
|
|
3394
3794
|
}, {
|
|
3395
|
-
path: `src/${featureDir}/scripts.${
|
|
3795
|
+
path: `src/${featureDir}/scripts.${scriptExt}`,
|
|
3396
3796
|
hint: "vanilla" === framework ? "Script entry point" : `${framework} mount point`
|
|
3397
3797
|
}, {
|
|
3398
3798
|
path: `src/${featureDir}/styles.css`,
|
|
3399
3799
|
hint: "Stylesheet"
|
|
3400
3800
|
});
|
|
3401
|
-
if ("vanilla" !== framework)
|
|
3402
|
-
|
|
3403
|
-
|
|
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"
|
|
3404
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
|
+
}
|
|
3405
3830
|
}
|
|
3406
|
-
if ("content-script" === args.feature) filesToCreate.push({
|
|
3407
|
-
path: "src/content/scripts.ts",
|
|
3408
|
-
hint: "Content script entry point"
|
|
3409
|
-
}, {
|
|
3410
|
-
path: "src/content/styles.css",
|
|
3411
|
-
hint: "Content script styles"
|
|
3412
|
-
});
|
|
3413
3831
|
if ("background" === args.feature) filesToCreate.push({
|
|
3414
3832
|
path: "src/background.ts",
|
|
3415
3833
|
hint: "Background service worker / script"
|
|
@@ -3512,9 +3930,102 @@ async function pollForToken(args) {
|
|
|
3512
3930
|
reason: "pending"
|
|
3513
3931
|
};
|
|
3514
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
|
+
}
|
|
3515
4026
|
const login_schema = {
|
|
3516
4027
|
name: "extension_login",
|
|
3517
|
-
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.",
|
|
3518
4029
|
inputSchema: {
|
|
3519
4030
|
type: "object",
|
|
3520
4031
|
properties: {
|
|
@@ -3577,6 +4088,52 @@ async function login_handler(args) {
|
|
|
3577
4088
|
} catch (err) {
|
|
3578
4089
|
return login_fail("LoginConfigError", err?.message || "Could not load login config.");
|
|
3579
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
|
+
}
|
|
3580
4137
|
if (args.deviceCode) {
|
|
3581
4138
|
const poll = await pollForToken({
|
|
3582
4139
|
clientId: config.clientId,
|
|
@@ -3655,6 +4212,7 @@ async function whoami_handler() {
|
|
|
3655
4212
|
workspaceSlug: creds.workspaceSlug,
|
|
3656
4213
|
projectSlug: creds.projectSlug,
|
|
3657
4214
|
api: creds.api,
|
|
4215
|
+
provider: creds.provider ?? "github",
|
|
3658
4216
|
expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
|
|
3659
4217
|
expiresInSeconds: creds.expiresAt ? creds.expiresAt - now : null,
|
|
3660
4218
|
expired,
|
|
@@ -3679,7 +4237,7 @@ async function logout_handler() {
|
|
|
3679
4237
|
}
|
|
3680
4238
|
const install_browser_schema = {
|
|
3681
4239
|
name: "extension_install_browser",
|
|
3682
|
-
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.",
|
|
3683
4241
|
inputSchema: {
|
|
3684
4242
|
type: "object",
|
|
3685
4243
|
properties: {
|
|
@@ -4155,35 +4713,70 @@ async function detect_browsers_handler(args) {
|
|
|
4155
4713
|
}
|
|
4156
4714
|
const doctor_schema = {
|
|
4157
4715
|
name: "extension_doctor",
|
|
4158
|
-
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.",
|
|
4159
4717
|
inputSchema: {
|
|
4160
4718
|
type: "object",
|
|
4161
4719
|
properties: {
|
|
4162
4720
|
projectPath: {
|
|
4163
4721
|
type: "string",
|
|
4164
|
-
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."
|
|
4165
4723
|
},
|
|
4166
4724
|
browser: {
|
|
4167
4725
|
type: "string",
|
|
4168
4726
|
description: "Browser session to diagnose. Defaults to the active dev session's browser for this project."
|
|
4169
4727
|
}
|
|
4170
|
-
}
|
|
4171
|
-
required: [
|
|
4172
|
-
"projectPath"
|
|
4173
|
-
]
|
|
4728
|
+
}
|
|
4174
4729
|
}
|
|
4175
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
|
+
}
|
|
4176
4767
|
async function doctor_handler(args) {
|
|
4177
|
-
|
|
4768
|
+
if (!args.projectPath) return environmentPreflight();
|
|
4769
|
+
const projectPath = args.projectPath;
|
|
4770
|
+
const { browser } = resolveSessionBrowser(projectPath, args.browser);
|
|
4178
4771
|
const { code, stdout, stderr } = await runExtensionCli([
|
|
4179
4772
|
"doctor",
|
|
4180
|
-
|
|
4773
|
+
projectPath,
|
|
4181
4774
|
"--browser",
|
|
4182
4775
|
browser,
|
|
4183
4776
|
"--output",
|
|
4184
4777
|
"json"
|
|
4185
4778
|
], {
|
|
4186
|
-
cwd:
|
|
4779
|
+
cwd: projectPath
|
|
4187
4780
|
});
|
|
4188
4781
|
const out = stdout.trim();
|
|
4189
4782
|
try {
|
|
@@ -4295,6 +4888,7 @@ const tools = [
|
|
|
4295
4888
|
dom_inspect_namespaceObject,
|
|
4296
4889
|
tools_publish_namespaceObject,
|
|
4297
4890
|
release_promote_namespaceObject,
|
|
4891
|
+
deploy_namespaceObject,
|
|
4298
4892
|
wait_namespaceObject,
|
|
4299
4893
|
add_feature_namespaceObject,
|
|
4300
4894
|
login_namespaceObject,
|
|
@@ -4428,6 +5022,31 @@ async function runCli(cmd, args) {
|
|
|
4428
5022
|
const apiBase = resolveApiBase(flag("api"));
|
|
4429
5023
|
try {
|
|
4430
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
|
+
}
|
|
4431
5050
|
const start = await startDeviceCode({
|
|
4432
5051
|
clientId: config.clientId,
|
|
4433
5052
|
scope: config.scope
|