@extension.dev/mcp 4.6.0 → 4.8.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 +48 -2
- package/README.md +3 -3
- package/dist/module.js +1103 -432
- 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/lib/validate-input.d.ts +1 -0
- package/dist/src/tools/deploy.d.ts +14 -70
- package/dist/src/tools/doctor.d.ts +1 -2
- package/dist/src/tools/manifest-validate.d.ts +7 -2
- package/dist/src/tools/publish.d.ts +1 -6
- package/dist/src/tools/stop.d.ts +0 -1
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/module.js
CHANGED
|
@@ -2,17 +2,16 @@ 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";
|
|
10
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
11
11
|
import { filterKeysForThisBrowser } from "browser-extension-manifest-fields";
|
|
12
12
|
import ws from "ws";
|
|
13
13
|
import node_http from "node:http";
|
|
14
14
|
import { extensionInstall, extensionUninstall, getManagedBrowsersCacheRoot } from "extension-install";
|
|
15
|
-
import { execFile } from "node:child_process";
|
|
16
15
|
import { promisify } from "node:util";
|
|
17
16
|
var add_feature_namespaceObject = {};
|
|
18
17
|
__webpack_require__.r(add_feature_namespaceObject);
|
|
@@ -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.7.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}`;
|
|
@@ -676,7 +769,7 @@ const dev_schema = {
|
|
|
676
769
|
allowEval: {
|
|
677
770
|
type: "boolean",
|
|
678
771
|
default: false,
|
|
679
|
-
description: "
|
|
772
|
+
description: "Enable extension_eval (runs code in a context; writes a 0600 session token). Implies allowControl, so a single allowEval: true also unlocks storage/reload/open/dom_inspect — you do not need to pass both."
|
|
680
773
|
}
|
|
681
774
|
},
|
|
682
775
|
required: [
|
|
@@ -686,6 +779,7 @@ const dev_schema = {
|
|
|
686
779
|
};
|
|
687
780
|
async function dev_handler(args) {
|
|
688
781
|
const browser = args.browser ?? "chrome";
|
|
782
|
+
const allowControl = Boolean(args.allowControl || args.allowEval);
|
|
689
783
|
const cliArgs = [
|
|
690
784
|
"dev",
|
|
691
785
|
args.projectPath,
|
|
@@ -696,7 +790,7 @@ async function dev_handler(args) {
|
|
|
696
790
|
if (args.noBrowser) cliArgs.push("--no-browser");
|
|
697
791
|
if (false === args.polyfill) cliArgs.push("--polyfill", "false");
|
|
698
792
|
cliArgs.push(...launchFlagArgs(args));
|
|
699
|
-
if (
|
|
793
|
+
if (allowControl) cliArgs.push("--allow-control");
|
|
700
794
|
if (args.allowEval) cliArgs.push("--allow-eval");
|
|
701
795
|
const child = spawnExtensionCli(cliArgs, {
|
|
702
796
|
projectDir: args.projectPath
|
|
@@ -719,16 +813,34 @@ async function dev_handler(args) {
|
|
|
719
813
|
await new Promise((resolve)=>setTimeout(resolve, 3000));
|
|
720
814
|
child.stdout?.off("data", collector);
|
|
721
815
|
child.stderr?.off("data", collector);
|
|
816
|
+
const controlVerbs = "storage, reload, open, dom_inspect";
|
|
817
|
+
const capabilities = {
|
|
818
|
+
allowControl,
|
|
819
|
+
allowEval: Boolean(args.allowEval),
|
|
820
|
+
unlocked: allowControl ? args.allowEval ? `${controlVerbs}, eval` : controlVerbs : "none (read-only: logs, source_inspect, wait, doctor)"
|
|
821
|
+
};
|
|
722
822
|
return JSON.stringify({
|
|
723
823
|
pid,
|
|
724
824
|
browser,
|
|
725
825
|
port: args.port ?? 8080,
|
|
726
826
|
projectPath: args.projectPath,
|
|
727
827
|
status: "started",
|
|
728
|
-
|
|
729
|
-
|
|
828
|
+
capabilities,
|
|
829
|
+
hint: "Use extension_wait to check when the extension is fully loaded, then extension_source_inspect to inspect the live state. " + (allowControl ? `Control channel is ON: extension_${controlVerbs.split(", ").join("/extension_")}${args.allowEval ? "/extension_eval" : ""} will work against this session.` : "Control channel is OFF: extension_storage/reload/open/dom_inspect need allowControl: true, and extension_eval needs allowEval: true (which also implies allowControl). Restart extension_dev with the flag you need.") + " When you are done, call extension_stop to shut down the dev server and browser.",
|
|
830
|
+
earlyOutput: denoiseEarlyOutput(earlyOutput).slice(0, 500)
|
|
730
831
|
});
|
|
731
832
|
}
|
|
833
|
+
function denoiseEarlyOutput(raw) {
|
|
834
|
+
const NOISE = [
|
|
835
|
+
/^npm warn Unknown project config/i,
|
|
836
|
+
/This will stop working in the next major version of npm/i,
|
|
837
|
+
/^npm warn config/i,
|
|
838
|
+
/V8: .*Invalid asm\.js/i,
|
|
839
|
+
/^\(node:\d+\) V8:/i,
|
|
840
|
+
/Use `node --trace-warnings/i
|
|
841
|
+
];
|
|
842
|
+
return raw.split("\n").filter((line)=>!NOISE.some((re)=>re.test(line.trim()))).join("\n").replace(/\n{2,}/g, "\n").trimStart();
|
|
843
|
+
}
|
|
732
844
|
const start_schema = {
|
|
733
845
|
name: "extension_start",
|
|
734
846
|
description: "Build the extension for production and immediately preview it in a browser. Combines build + preview in one step. No hot reload.",
|
|
@@ -899,6 +1011,66 @@ async function preview_handler(args) {
|
|
|
899
1011
|
hint: "Call extension_stop when you are done to close the preview browser."
|
|
900
1012
|
});
|
|
901
1013
|
}
|
|
1014
|
+
function contractSightings(projectPath) {
|
|
1015
|
+
const root = node_path.resolve(projectPath, "dist", "extension-js");
|
|
1016
|
+
let dirs;
|
|
1017
|
+
try {
|
|
1018
|
+
dirs = node_fs.readdirSync(root);
|
|
1019
|
+
} catch {
|
|
1020
|
+
return [];
|
|
1021
|
+
}
|
|
1022
|
+
const sightings = [];
|
|
1023
|
+
for (const dir of dirs){
|
|
1024
|
+
const readyPath = node_path.join(root, dir, "ready.json");
|
|
1025
|
+
try {
|
|
1026
|
+
const stat = node_fs.statSync(readyPath);
|
|
1027
|
+
const contract = JSON.parse(node_fs.readFileSync(readyPath, "utf8"));
|
|
1028
|
+
if (contract?.status !== "ready") continue;
|
|
1029
|
+
sightings.push({
|
|
1030
|
+
browser: dir,
|
|
1031
|
+
mtimeMs: stat.mtimeMs,
|
|
1032
|
+
pid: "number" == typeof contract.pid ? contract.pid : void 0
|
|
1033
|
+
});
|
|
1034
|
+
} catch {}
|
|
1035
|
+
}
|
|
1036
|
+
return sightings;
|
|
1037
|
+
}
|
|
1038
|
+
function pidAlive(pid) {
|
|
1039
|
+
try {
|
|
1040
|
+
process.kill(pid, 0);
|
|
1041
|
+
return true;
|
|
1042
|
+
} catch {
|
|
1043
|
+
return false;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
function knownSessionBrowsers(projectPath) {
|
|
1047
|
+
const resolved = node_path.resolve(projectPath);
|
|
1048
|
+
const browsers = [];
|
|
1049
|
+
for (const session of listSessions())if (node_path.resolve(session.projectPath) === resolved) browsers.push(session.browser);
|
|
1050
|
+
for (const sighting of contractSightings(projectPath))if (void 0 === sighting.pid || pidAlive(sighting.pid)) browsers.push(sighting.browser);
|
|
1051
|
+
return Array.from(new Set(browsers));
|
|
1052
|
+
}
|
|
1053
|
+
function resolveSessionBrowser(projectPath, explicit, fallback = "chromium") {
|
|
1054
|
+
if (explicit) return {
|
|
1055
|
+
browser: explicit,
|
|
1056
|
+
source: "explicit"
|
|
1057
|
+
};
|
|
1058
|
+
const resolved = node_path.resolve(projectPath);
|
|
1059
|
+
const mine = listSessions().filter((s)=>node_path.resolve(s.projectPath) === resolved);
|
|
1060
|
+
if (mine.length > 0) return {
|
|
1061
|
+
browser: mine[mine.length - 1].browser,
|
|
1062
|
+
source: "session"
|
|
1063
|
+
};
|
|
1064
|
+
const sightings = contractSightings(projectPath).filter((s)=>void 0 === s.pid || pidAlive(s.pid)).sort((a, b)=>b.mtimeMs - a.mtimeMs);
|
|
1065
|
+
if (sightings.length > 0) return {
|
|
1066
|
+
browser: sightings[0].browser,
|
|
1067
|
+
source: "contract"
|
|
1068
|
+
};
|
|
1069
|
+
return {
|
|
1070
|
+
browser: fallback,
|
|
1071
|
+
source: "fallback"
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
902
1074
|
const stop_schema = {
|
|
903
1075
|
name: "extension_stop",
|
|
904
1076
|
description: "Stop a running dev, start, or preview session: terminates the dev server and the browser it launched. Counterpart to extension_dev/extension_start. Call it when you are done verifying so sessions do not accumulate.",
|
|
@@ -911,8 +1083,7 @@ const stop_schema = {
|
|
|
911
1083
|
},
|
|
912
1084
|
browser: {
|
|
913
1085
|
type: "string",
|
|
914
|
-
|
|
915
|
-
description: "Browser of the session to stop (matches the browser passed to extension_dev/extension_start)"
|
|
1086
|
+
description: "Browser of the session to stop (matches the browser passed to extension_dev/extension_start). Defaults to the one live session for this project when omitted, instead of assuming chrome."
|
|
916
1087
|
},
|
|
917
1088
|
all: {
|
|
918
1089
|
type: "boolean",
|
|
@@ -923,6 +1094,27 @@ const stop_schema = {
|
|
|
923
1094
|
required: []
|
|
924
1095
|
}
|
|
925
1096
|
};
|
|
1097
|
+
function profileProcessPids(projectPath, browser) {
|
|
1098
|
+
const marker = node_path.resolve(projectPath, "dist", `extension-profile-${browser}`);
|
|
1099
|
+
try {
|
|
1100
|
+
const out = execFileSync("pgrep", [
|
|
1101
|
+
"-f",
|
|
1102
|
+
marker
|
|
1103
|
+
], {
|
|
1104
|
+
encoding: "utf8"
|
|
1105
|
+
});
|
|
1106
|
+
return out.split("\n").map((s)=>parseInt(s.trim(), 10)).filter((n)=>Number.isInteger(n) && n > 0 && n !== process.pid);
|
|
1107
|
+
} catch {
|
|
1108
|
+
return [];
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
function reapProfileProcesses(projectPath, browser) {
|
|
1112
|
+
const pids = profileProcessPids(projectPath, browser);
|
|
1113
|
+
for (const pid of pids)try {
|
|
1114
|
+
process.kill(pid, "SIGKILL");
|
|
1115
|
+
} catch {}
|
|
1116
|
+
return pids;
|
|
1117
|
+
}
|
|
926
1118
|
function isAlive(pid) {
|
|
927
1119
|
try {
|
|
928
1120
|
process.kill(pid, 0);
|
|
@@ -959,13 +1151,17 @@ function pidFromReadyContract(projectPath, browser) {
|
|
|
959
1151
|
async function stopOne(projectPath, browser) {
|
|
960
1152
|
const session = getSession(projectPath, browser);
|
|
961
1153
|
const pid = session?.pid ?? pidFromReadyContract(projectPath, browser);
|
|
962
|
-
if (null == pid)
|
|
963
|
-
projectPath,
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1154
|
+
if (null == pid) {
|
|
1155
|
+
const reaped = reapProfileProcesses(projectPath, browser);
|
|
1156
|
+
return {
|
|
1157
|
+
projectPath,
|
|
1158
|
+
browser,
|
|
1159
|
+
pid: null,
|
|
1160
|
+
stopped: 0 !== reaped.length,
|
|
1161
|
+
reaped,
|
|
1162
|
+
detail: 0 === reaped.length ? "No known session for this project/browser (nothing registered in this server and no ready.json contract found)." : `No dev pid on record, but reaped ${reaped.length} orphaned browser process(es) from the profile dir.`
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
969
1165
|
let detail;
|
|
970
1166
|
if (isAlive(pid)) {
|
|
971
1167
|
signal(pid, "SIGTERM");
|
|
@@ -976,17 +1172,23 @@ async function stopOne(projectPath, browser) {
|
|
|
976
1172
|
}
|
|
977
1173
|
detail = isAlive(pid) ? "Sent SIGTERM and SIGKILL but the process still reports alive; it may be exiting." : "Terminated.";
|
|
978
1174
|
} else detail = "Process was already gone; cleaned up session records.";
|
|
1175
|
+
const reaped = reapProfileProcesses(projectPath, browser);
|
|
979
1176
|
removeSession(projectPath, browser);
|
|
980
1177
|
try {
|
|
981
1178
|
node_fs.rmSync(readyJsonPath(projectPath, browser), {
|
|
982
1179
|
force: true
|
|
983
1180
|
});
|
|
984
1181
|
} catch {}
|
|
1182
|
+
const survivors = profileProcessPids(projectPath, browser);
|
|
1183
|
+
const stopped = !isAlive(pid) && 0 === survivors.length;
|
|
1184
|
+
if (survivors.length) detail += ` Warning: ${survivors.length} browser process(es) still alive after reap (pids ${survivors.join(", ")}).`;
|
|
1185
|
+
else if (reaped.length) detail += ` Reaped ${reaped.length} browser process(es).`;
|
|
985
1186
|
return {
|
|
986
1187
|
projectPath,
|
|
987
1188
|
browser,
|
|
988
1189
|
pid,
|
|
989
|
-
stopped
|
|
1190
|
+
stopped,
|
|
1191
|
+
reaped,
|
|
990
1192
|
detail
|
|
991
1193
|
};
|
|
992
1194
|
}
|
|
@@ -1006,7 +1208,8 @@ async function stop_handler(args) {
|
|
|
1006
1208
|
if (!args.projectPath) return JSON.stringify({
|
|
1007
1209
|
error: "projectPath is required unless all=true. Pass the same projectPath used with extension_dev/extension_start."
|
|
1008
1210
|
});
|
|
1009
|
-
const
|
|
1211
|
+
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
1212
|
+
const outcome = await stopOne(args.projectPath, browser);
|
|
1010
1213
|
return JSON.stringify(outcome);
|
|
1011
1214
|
}
|
|
1012
1215
|
const RAW_BASE = "https://raw.githubusercontent.com/extension-js/examples/main/examples";
|
|
@@ -1018,7 +1221,7 @@ const get_template_source_schema = {
|
|
|
1018
1221
|
properties: {
|
|
1019
1222
|
slug: {
|
|
1020
1223
|
type: "string",
|
|
1021
|
-
description: "Template slug (e.g. '
|
|
1224
|
+
description: "Template slug (e.g. 'ai-claude', 'content-react')"
|
|
1022
1225
|
},
|
|
1023
1226
|
files: {
|
|
1024
1227
|
type: "array",
|
|
@@ -1101,6 +1304,91 @@ function isChromiumFamily(browser) {
|
|
|
1101
1304
|
function isGeckoFamily(browser) {
|
|
1102
1305
|
return GECKO_FAMILY.has(browser);
|
|
1103
1306
|
}
|
|
1307
|
+
const KNOWN_PERMISSIONS = new Set([
|
|
1308
|
+
"activeTab",
|
|
1309
|
+
"alarms",
|
|
1310
|
+
"background",
|
|
1311
|
+
"bookmarks",
|
|
1312
|
+
"browsingData",
|
|
1313
|
+
"certificateProvider",
|
|
1314
|
+
"clipboardRead",
|
|
1315
|
+
"clipboardWrite",
|
|
1316
|
+
"contentSettings",
|
|
1317
|
+
"contextMenus",
|
|
1318
|
+
"cookies",
|
|
1319
|
+
"debugger",
|
|
1320
|
+
"declarativeContent",
|
|
1321
|
+
"declarativeNetRequest",
|
|
1322
|
+
"declarativeNetRequestWithHostAccess",
|
|
1323
|
+
"declarativeNetRequestFeedback",
|
|
1324
|
+
"desktopCapture",
|
|
1325
|
+
"dns",
|
|
1326
|
+
"documentScan",
|
|
1327
|
+
"downloads",
|
|
1328
|
+
"downloads.open",
|
|
1329
|
+
"downloads.ui",
|
|
1330
|
+
"enterprise.deviceAttributes",
|
|
1331
|
+
"enterprise.hardwarePlatform",
|
|
1332
|
+
"enterprise.networkingAttributes",
|
|
1333
|
+
"enterprise.platformKeys",
|
|
1334
|
+
"favicon",
|
|
1335
|
+
"fileBrowserHandler",
|
|
1336
|
+
"fileSystemProvider",
|
|
1337
|
+
"fontSettings",
|
|
1338
|
+
"gcm",
|
|
1339
|
+
"geolocation",
|
|
1340
|
+
"history",
|
|
1341
|
+
"identity",
|
|
1342
|
+
"identity.email",
|
|
1343
|
+
"idle",
|
|
1344
|
+
"loginState",
|
|
1345
|
+
"management",
|
|
1346
|
+
"nativeMessaging",
|
|
1347
|
+
"notifications",
|
|
1348
|
+
"offscreen",
|
|
1349
|
+
"pageCapture",
|
|
1350
|
+
"power",
|
|
1351
|
+
"printerProvider",
|
|
1352
|
+
"printing",
|
|
1353
|
+
"printingMetrics",
|
|
1354
|
+
"privacy",
|
|
1355
|
+
"processes",
|
|
1356
|
+
"proxy",
|
|
1357
|
+
"readingList",
|
|
1358
|
+
"runtime",
|
|
1359
|
+
"scripting",
|
|
1360
|
+
"search",
|
|
1361
|
+
"sessions",
|
|
1362
|
+
"sidePanel",
|
|
1363
|
+
"storage",
|
|
1364
|
+
"system.cpu",
|
|
1365
|
+
"system.display",
|
|
1366
|
+
"system.memory",
|
|
1367
|
+
"system.storage",
|
|
1368
|
+
"tabCapture",
|
|
1369
|
+
"tabGroups",
|
|
1370
|
+
"tabs",
|
|
1371
|
+
"topSites",
|
|
1372
|
+
"tts",
|
|
1373
|
+
"ttsEngine",
|
|
1374
|
+
"unlimitedStorage",
|
|
1375
|
+
"vpnProvider",
|
|
1376
|
+
"wallpaper",
|
|
1377
|
+
"webAuthenticationProxy",
|
|
1378
|
+
"webNavigation",
|
|
1379
|
+
"webRequest",
|
|
1380
|
+
"webRequestBlocking",
|
|
1381
|
+
"webRequestAuthProvider",
|
|
1382
|
+
"browserSettings",
|
|
1383
|
+
"captivePortal",
|
|
1384
|
+
"contextualIdentities",
|
|
1385
|
+
"dns",
|
|
1386
|
+
"menus",
|
|
1387
|
+
"menus.overrideContext",
|
|
1388
|
+
"pkcs11",
|
|
1389
|
+
"theme",
|
|
1390
|
+
"webRequestFilterResponse"
|
|
1391
|
+
]);
|
|
1104
1392
|
const manifest_validate_schema = {
|
|
1105
1393
|
name: "extension_manifest_validate",
|
|
1106
1394
|
description: "Validate a manifest.json file for correctness across browsers. Reports missing fields, invalid permissions, and cross-browser compatibility issues.",
|
|
@@ -1109,7 +1397,11 @@ const manifest_validate_schema = {
|
|
|
1109
1397
|
properties: {
|
|
1110
1398
|
manifestPath: {
|
|
1111
1399
|
type: "string",
|
|
1112
|
-
description: "Path to manifest.json"
|
|
1400
|
+
description: "Path to manifest.json. Or pass projectPath and the manifest is located for you."
|
|
1401
|
+
},
|
|
1402
|
+
projectPath: {
|
|
1403
|
+
type: "string",
|
|
1404
|
+
description: "Path to the extension project root; manifest.json is resolved from it (root or src/). Accepted in place of manifestPath."
|
|
1113
1405
|
},
|
|
1114
1406
|
browsers: {
|
|
1115
1407
|
type: "array",
|
|
@@ -1123,11 +1415,64 @@ const manifest_validate_schema = {
|
|
|
1123
1415
|
description: "Browsers to validate against"
|
|
1124
1416
|
}
|
|
1125
1417
|
},
|
|
1126
|
-
required: [
|
|
1127
|
-
"manifestPath"
|
|
1128
|
-
]
|
|
1418
|
+
required: []
|
|
1129
1419
|
}
|
|
1130
1420
|
};
|
|
1421
|
+
function collectPathRefs(m) {
|
|
1422
|
+
const refs = [];
|
|
1423
|
+
const push = (v)=>{
|
|
1424
|
+
if ("string" == typeof v) refs.push(v);
|
|
1425
|
+
};
|
|
1426
|
+
const action = m.action || m.browser_action;
|
|
1427
|
+
if (action) {
|
|
1428
|
+
push(action.default_popup);
|
|
1429
|
+
if ("string" == typeof action.default_icon) push(action.default_icon);
|
|
1430
|
+
else if (action.default_icon) Object.values(action.default_icon).forEach(push);
|
|
1431
|
+
}
|
|
1432
|
+
const bg = m.background;
|
|
1433
|
+
if (bg) {
|
|
1434
|
+
push(bg.service_worker);
|
|
1435
|
+
push(bg.page);
|
|
1436
|
+
if (Array.isArray(bg.scripts)) bg.scripts.forEach(push);
|
|
1437
|
+
}
|
|
1438
|
+
if (m.icons) Object.values(m.icons).forEach(push);
|
|
1439
|
+
const cs = m.content_scripts;
|
|
1440
|
+
if (Array.isArray(cs)) for (const c of cs){
|
|
1441
|
+
if (Array.isArray(c.js)) c.js.forEach(push);
|
|
1442
|
+
if (Array.isArray(c.css)) c.css.forEach(push);
|
|
1443
|
+
}
|
|
1444
|
+
push(m.options_page);
|
|
1445
|
+
const oui = m.options_ui;
|
|
1446
|
+
if (oui) push(oui.page);
|
|
1447
|
+
const sp = m.side_panel;
|
|
1448
|
+
if (sp) push(sp.default_path);
|
|
1449
|
+
const sa = m.sidebar_action;
|
|
1450
|
+
if (sa) push(sa.default_panel);
|
|
1451
|
+
const cuo = m.chrome_url_overrides;
|
|
1452
|
+
if (cuo) Object.values(cuo).forEach(push);
|
|
1453
|
+
return refs;
|
|
1454
|
+
}
|
|
1455
|
+
function fileResolvesSomewhere(ref, roots) {
|
|
1456
|
+
if (!ref || ref.includes("*") || /^(https?:|data:)/i.test(ref)) return true;
|
|
1457
|
+
const clean = ref.replace(/^\.?\//, "");
|
|
1458
|
+
return roots.some((root)=>{
|
|
1459
|
+
try {
|
|
1460
|
+
return node_fs.existsSync(node_path.resolve(root, clean));
|
|
1461
|
+
} catch {
|
|
1462
|
+
return false;
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
function findManifest(projectPath) {
|
|
1467
|
+
for (const rel of [
|
|
1468
|
+
"manifest.json",
|
|
1469
|
+
node_path.join("src", "manifest.json")
|
|
1470
|
+
]){
|
|
1471
|
+
const candidate = node_path.resolve(projectPath, rel);
|
|
1472
|
+
if (node_fs.existsSync(candidate)) return candidate;
|
|
1473
|
+
}
|
|
1474
|
+
return null;
|
|
1475
|
+
}
|
|
1131
1476
|
async function manifest_validate_handler(args) {
|
|
1132
1477
|
const browsers = args.browsers ?? [
|
|
1133
1478
|
"chrome",
|
|
@@ -1140,9 +1485,20 @@ async function manifest_validate_handler(args) {
|
|
|
1140
1485
|
browserSupport: {},
|
|
1141
1486
|
similarTemplates: []
|
|
1142
1487
|
};
|
|
1488
|
+
const manifestPath = args.manifestPath ?? (args.projectPath ? findManifest(args.projectPath) : null);
|
|
1489
|
+
if (!manifestPath) return JSON.stringify({
|
|
1490
|
+
valid: false,
|
|
1491
|
+
errors: [
|
|
1492
|
+
args.projectPath ? `No manifest.json found under ${args.projectPath} (looked in the root and src/).` : "Pass manifestPath (path to manifest.json) or projectPath (project root)."
|
|
1493
|
+
],
|
|
1494
|
+
warnings: [],
|
|
1495
|
+
browserSupport: {},
|
|
1496
|
+
similarTemplates: []
|
|
1497
|
+
});
|
|
1498
|
+
const manifestDir = node_path.dirname(node_path.resolve(manifestPath));
|
|
1143
1499
|
let manifest;
|
|
1144
1500
|
try {
|
|
1145
|
-
const raw = node_fs.readFileSync(node_path.resolve(
|
|
1501
|
+
const raw = node_fs.readFileSync(node_path.resolve(manifestPath), "utf8");
|
|
1146
1502
|
manifest = JSON.parse(raw);
|
|
1147
1503
|
} catch (err) {
|
|
1148
1504
|
return JSON.stringify({
|
|
@@ -1158,7 +1514,22 @@ async function manifest_validate_handler(args) {
|
|
|
1158
1514
|
if (!manifest.name) result.errors.push("Missing required field: name");
|
|
1159
1515
|
if (!manifest.version) result.warnings.push("Missing field: version (required for store submission)");
|
|
1160
1516
|
const chromiumManifest = filterKeysForThisBrowser(manifest, "chrome");
|
|
1517
|
+
const roots = [
|
|
1518
|
+
manifestDir,
|
|
1519
|
+
node_path.join(manifestDir, "src"),
|
|
1520
|
+
..."src" === node_path.basename(manifestDir) ? [
|
|
1521
|
+
node_path.dirname(manifestDir)
|
|
1522
|
+
] : []
|
|
1523
|
+
];
|
|
1524
|
+
for (const ref of new Set(collectPathRefs(chromiumManifest)))if (!fileResolvesSomewhere(ref, roots)) result.warnings.push(`Referenced file "${ref}" was not found near the manifest — this is the kind of dangling reference extension_build fails on. Verify with extension_build.`);
|
|
1161
1525
|
if (!chromiumManifest.manifest_version) result.errors.push('Missing manifest_version. Use "chromium:manifest_version": 3 and "firefox:manifest_version": 2 for cross-browser support.');
|
|
1526
|
+
const declaredPerms = [
|
|
1527
|
+
...chromiumManifest.permissions ?? [],
|
|
1528
|
+
...chromiumManifest.optional_permissions ?? []
|
|
1529
|
+
].filter((p)=>"string" == typeof p);
|
|
1530
|
+
for (const perm of declaredPerms)if (!(perm.includes("://") || perm.includes("*")) && "<all_urls>" !== perm) {
|
|
1531
|
+
if (!KNOWN_PERMISSIONS.has(perm)) result.warnings.push(`Unrecognized permission "${perm}" — check for a typo (host/match patterns belong in host_permissions, not permissions).`);
|
|
1532
|
+
}
|
|
1162
1533
|
for (const browser of browsers){
|
|
1163
1534
|
const isChromium = isChromiumFamily(browser);
|
|
1164
1535
|
const isFirefox = isGeckoFamily(browser);
|
|
@@ -1196,9 +1567,15 @@ async function manifest_validate_handler(args) {
|
|
|
1196
1567
|
if (chromiumManifest.action || manifest["firefox:browser_action"]) surfaces.push("action");
|
|
1197
1568
|
if (chromiumManifest.chrome_url_overrides?.newtab) surfaces.push("newtab");
|
|
1198
1569
|
if (chromiumManifest.background) surfaces.push("background");
|
|
1199
|
-
|
|
1570
|
+
const distinctive = surfaces.filter((s)=>"background" !== s);
|
|
1571
|
+
const matchOn = distinctive.length ? distinctive : surfaces;
|
|
1572
|
+
if (matchOn.length) try {
|
|
1200
1573
|
const templates = await listTemplates();
|
|
1201
|
-
result.similarTemplates = templates.
|
|
1574
|
+
result.similarTemplates = templates.map((t)=>({
|
|
1575
|
+
slug: t.slug,
|
|
1576
|
+
surfaces: t.surfaces,
|
|
1577
|
+
score: t.surfaces.filter((s)=>matchOn.includes(s)).length
|
|
1578
|
+
})).filter((t)=>t.score > 0).sort((a, b)=>b.score - a.score).slice(0, 5).map((t)=>({
|
|
1202
1579
|
slug: t.slug,
|
|
1203
1580
|
surfaces: t.surfaces
|
|
1204
1581
|
}));
|
|
@@ -1320,12 +1697,58 @@ async function inspect_handler(args) {
|
|
|
1320
1697
|
byType[f.type].count++;
|
|
1321
1698
|
byType[f.type].size += f.size;
|
|
1322
1699
|
}
|
|
1700
|
+
const sourcemapSize = byType.sourcemap?.size ?? 0;
|
|
1701
|
+
const buildType = sourcemapSize > 0 ? "development" : "production";
|
|
1702
|
+
const shippableSize = totalSize - sourcemapSize;
|
|
1703
|
+
const sizeByPath = new Map(files.map((f)=>[
|
|
1704
|
+
f.path,
|
|
1705
|
+
f.size
|
|
1706
|
+
]));
|
|
1707
|
+
const entrypoints = [];
|
|
1708
|
+
const addEntry = (role, ref)=>{
|
|
1709
|
+
if ("string" != typeof ref) return;
|
|
1710
|
+
const size = sizeByPath.get(ref.replace(/^\.?\//, ""));
|
|
1711
|
+
entrypoints.push({
|
|
1712
|
+
role,
|
|
1713
|
+
path: ref,
|
|
1714
|
+
present: void 0 !== size,
|
|
1715
|
+
...void 0 !== size ? {
|
|
1716
|
+
sizeFormatted: formatBytes(size)
|
|
1717
|
+
} : {}
|
|
1718
|
+
});
|
|
1719
|
+
};
|
|
1720
|
+
const bg = manifest.background;
|
|
1721
|
+
if (bg?.service_worker) addEntry("background.service_worker", bg.service_worker);
|
|
1722
|
+
if (Array.isArray(bg?.scripts)) bg.scripts.forEach((s)=>addEntry("background.scripts", s));
|
|
1723
|
+
const actionField = manifest.action || manifest.browser_action;
|
|
1724
|
+
if (actionField?.default_popup) addEntry("action.default_popup", actionField.default_popup);
|
|
1725
|
+
const contentScripts = manifest.content_scripts;
|
|
1726
|
+
if (Array.isArray(contentScripts)) contentScripts.forEach((c, i)=>{
|
|
1727
|
+
if (Array.isArray(c.js)) c.js.forEach((j)=>addEntry(`content_scripts[${i}].js`, j));
|
|
1728
|
+
if (Array.isArray(c.css)) c.css.forEach((s)=>addEntry(`content_scripts[${i}].css`, s));
|
|
1729
|
+
});
|
|
1730
|
+
const PROMO_RE = /(screenshot|promo|marquee|tile|banner|preview)[-_.]?\d*\.(png|jpe?g|webp|gif)$/i;
|
|
1731
|
+
const sizeWarnings = [];
|
|
1732
|
+
for (const f of files)if ("sourcemap" !== f.type) {
|
|
1733
|
+
if (PROMO_RE.test(f.path)) sizeWarnings.push(`${f.path} (${formatBytes(f.size)}) looks like a store-listing promo image shipped inside the extension package — move it out of the bundled sources so it does not inflate the store zip.`);
|
|
1734
|
+
else if ("image" === f.type && !f.path.includes("icon") && f.size > 51200 && shippableSize > 0 && f.size / shippableSize > 0.25) sizeWarnings.push(`${f.path} (${formatBytes(f.size)}) is ${Math.round(f.size / shippableSize * 100)}% of the shipped bundle — unusually large for a shipped asset.`);
|
|
1735
|
+
}
|
|
1323
1736
|
const result = {
|
|
1324
1737
|
browser,
|
|
1325
1738
|
distPath,
|
|
1739
|
+
entrypoints,
|
|
1740
|
+
...sizeWarnings.length ? {
|
|
1741
|
+
sizeWarnings
|
|
1742
|
+
} : {},
|
|
1743
|
+
buildType,
|
|
1326
1744
|
totalSize,
|
|
1327
1745
|
totalSizeFormatted: formatBytes(totalSize),
|
|
1746
|
+
shippableSize,
|
|
1747
|
+
shippableSizeFormatted: formatBytes(shippableSize),
|
|
1328
1748
|
fileCount: files.length,
|
|
1749
|
+
..."development" === buildType ? {
|
|
1750
|
+
note: `This dist contains ${formatBytes(sourcemapSize)} of sourcemaps and looks like a dev build; run extension_build for production sizes. shippableSize excludes sourcemaps.`
|
|
1751
|
+
} : {},
|
|
1329
1752
|
manifest: {
|
|
1330
1753
|
name: manifest.name,
|
|
1331
1754
|
version: manifest.version,
|
|
@@ -1356,6 +1779,7 @@ async function inspect_handler(args) {
|
|
|
1356
1779
|
hasManifest: node_fs.existsSync(manifestPath),
|
|
1357
1780
|
hasIcons: files.some((f)=>"image" === f.type && f.path.includes("icon")),
|
|
1358
1781
|
noSourceMaps: !files.some((f)=>"sourcemap" === f.type),
|
|
1782
|
+
noPromoAssets: !files.some((f)=>PROMO_RE.test(f.path)),
|
|
1359
1783
|
under10MB: totalSize < 10485760
|
|
1360
1784
|
}
|
|
1361
1785
|
};
|
|
@@ -1770,13 +2194,16 @@ class CDPClient extends CDPConnection {
|
|
|
1770
2194
|
}
|
|
1771
2195
|
async function resolveCdpPort(projectPath, browser, options) {
|
|
1772
2196
|
const waitMs = options?.waitMs ?? 20000;
|
|
2197
|
+
const graceMs = options?.graceMs ?? 2500;
|
|
1773
2198
|
const readyPath = node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json");
|
|
1774
2199
|
const deadline = Date.now() + waitMs;
|
|
1775
2200
|
let contractSeen = false;
|
|
2201
|
+
let contractSeenAt = null;
|
|
1776
2202
|
for(;;){
|
|
1777
2203
|
try {
|
|
1778
2204
|
const contract = JSON.parse(node_fs.readFileSync(readyPath, "utf8"));
|
|
1779
2205
|
contractSeen = true;
|
|
2206
|
+
if (null == contractSeenAt) contractSeenAt = Date.now();
|
|
1780
2207
|
if ("number" == typeof contract.cdpPort) return {
|
|
1781
2208
|
port: contract.cdpPort,
|
|
1782
2209
|
source: "contract"
|
|
@@ -1784,7 +2211,8 @@ async function resolveCdpPort(projectPath, browser, options) {
|
|
|
1784
2211
|
} catch {
|
|
1785
2212
|
if (!contractSeen) break;
|
|
1786
2213
|
}
|
|
1787
|
-
|
|
2214
|
+
const effectiveDeadline = null != contractSeenAt ? Math.min(deadline, contractSeenAt + graceMs) : deadline;
|
|
2215
|
+
if (Date.now() >= effectiveDeadline) break;
|
|
1788
2216
|
await new Promise((resolve)=>setTimeout(resolve, 500));
|
|
1789
2217
|
}
|
|
1790
2218
|
if (!contractSeen && await isCdpEndpoint(9222)) return {
|
|
@@ -1812,66 +2240,6 @@ function isCdpEndpoint(port) {
|
|
|
1812
2240
|
});
|
|
1813
2241
|
});
|
|
1814
2242
|
}
|
|
1815
|
-
function contractSightings(projectPath) {
|
|
1816
|
-
const root = node_path.resolve(projectPath, "dist", "extension-js");
|
|
1817
|
-
let dirs;
|
|
1818
|
-
try {
|
|
1819
|
-
dirs = node_fs.readdirSync(root);
|
|
1820
|
-
} catch {
|
|
1821
|
-
return [];
|
|
1822
|
-
}
|
|
1823
|
-
const sightings = [];
|
|
1824
|
-
for (const dir of dirs){
|
|
1825
|
-
const readyPath = node_path.join(root, dir, "ready.json");
|
|
1826
|
-
try {
|
|
1827
|
-
const stat = node_fs.statSync(readyPath);
|
|
1828
|
-
const contract = JSON.parse(node_fs.readFileSync(readyPath, "utf8"));
|
|
1829
|
-
if (contract?.status !== "ready") continue;
|
|
1830
|
-
sightings.push({
|
|
1831
|
-
browser: dir,
|
|
1832
|
-
mtimeMs: stat.mtimeMs,
|
|
1833
|
-
pid: "number" == typeof contract.pid ? contract.pid : void 0
|
|
1834
|
-
});
|
|
1835
|
-
} catch {}
|
|
1836
|
-
}
|
|
1837
|
-
return sightings;
|
|
1838
|
-
}
|
|
1839
|
-
function pidAlive(pid) {
|
|
1840
|
-
try {
|
|
1841
|
-
process.kill(pid, 0);
|
|
1842
|
-
return true;
|
|
1843
|
-
} catch {
|
|
1844
|
-
return false;
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
function knownSessionBrowsers(projectPath) {
|
|
1848
|
-
const resolved = node_path.resolve(projectPath);
|
|
1849
|
-
const browsers = [];
|
|
1850
|
-
for (const session of listSessions())if (node_path.resolve(session.projectPath) === resolved) browsers.push(session.browser);
|
|
1851
|
-
for (const sighting of contractSightings(projectPath))if (void 0 === sighting.pid || pidAlive(sighting.pid)) browsers.push(sighting.browser);
|
|
1852
|
-
return Array.from(new Set(browsers));
|
|
1853
|
-
}
|
|
1854
|
-
function resolveSessionBrowser(projectPath, explicit, fallback = "chromium") {
|
|
1855
|
-
if (explicit) return {
|
|
1856
|
-
browser: explicit,
|
|
1857
|
-
source: "explicit"
|
|
1858
|
-
};
|
|
1859
|
-
const resolved = node_path.resolve(projectPath);
|
|
1860
|
-
const mine = listSessions().filter((s)=>node_path.resolve(s.projectPath) === resolved);
|
|
1861
|
-
if (mine.length > 0) return {
|
|
1862
|
-
browser: mine[mine.length - 1].browser,
|
|
1863
|
-
source: "session"
|
|
1864
|
-
};
|
|
1865
|
-
const sightings = contractSightings(projectPath).filter((s)=>void 0 === s.pid || pidAlive(s.pid)).sort((a, b)=>b.mtimeMs - a.mtimeMs);
|
|
1866
|
-
if (sightings.length > 0) return {
|
|
1867
|
-
browser: sightings[0].browser,
|
|
1868
|
-
source: "contract"
|
|
1869
|
-
};
|
|
1870
|
-
return {
|
|
1871
|
-
browser: fallback,
|
|
1872
|
-
source: "fallback"
|
|
1873
|
-
};
|
|
1874
|
-
}
|
|
1875
2243
|
const source_inspect_schema = {
|
|
1876
2244
|
name: "extension_source_inspect",
|
|
1877
2245
|
description: "Inspect a running extension's live state via Chrome DevTools Protocol: full HTML (with shadow DOM), DOM structure, content script injection, console messages, and CSS selector queries. Requires an active dev or start session.",
|
|
@@ -1942,8 +2310,8 @@ async function source_inspect_handler(args) {
|
|
|
1942
2310
|
]);
|
|
1943
2311
|
const maxBytes = args.maxBytes ?? 262144;
|
|
1944
2312
|
if (!isChromiumFamily(browser)) return JSON.stringify({
|
|
1945
|
-
error: `Source inspection
|
|
1946
|
-
hint:
|
|
2313
|
+
error: `Source inspection reads the live DOM over Chrome DevTools Protocol, which ${browser} (Gecko) does not expose. This is a capability limit, not a missing session.`,
|
|
2314
|
+
hint: `For the ${browser} session, read runtime state with extension_logs, or extension_eval (context: "content"/"page") which works against Firefox over the control channel. To get CDP DOM inspection, run a Chromium-family dev session (extension_dev with browser: "chrome") in parallel.`
|
|
1947
2315
|
});
|
|
1948
2316
|
const resolved = await resolveCdpPort(args.projectPath, browser);
|
|
1949
2317
|
if (!resolved) return JSON.stringify({
|
|
@@ -1954,16 +2322,26 @@ async function source_inspect_handler(args) {
|
|
|
1954
2322
|
const cdp = new CDPClient();
|
|
1955
2323
|
try {
|
|
1956
2324
|
const allTargets = await CDPClient.discoverTargets(cdpPort);
|
|
1957
|
-
const
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
2325
|
+
const OVERRIDE_PAGES = [
|
|
2326
|
+
"chrome://newtab/",
|
|
2327
|
+
"chrome://new-tab-page/",
|
|
2328
|
+
"chrome://bookmarks/",
|
|
2329
|
+
"chrome://history/"
|
|
2330
|
+
];
|
|
2331
|
+
const isOverridePage = (url)=>OVERRIDE_PAGES.some((p)=>url.startsWith(p));
|
|
2332
|
+
const pageTargets = allTargets.filter((t)=>"page" === t.type && !t.url.startsWith("devtools://") && (!t.url.startsWith("chrome://") || isOverridePage(t.url)));
|
|
2333
|
+
if (0 === pageTargets.length) {
|
|
2334
|
+
const chromeOnly = allTargets.some((t)=>"page" === t.type && t.url.startsWith("chrome://"));
|
|
2335
|
+
return JSON.stringify({
|
|
2336
|
+
cdpPort,
|
|
2337
|
+
browser,
|
|
2338
|
+
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.",
|
|
2339
|
+
allTargets: allTargets.map((t)=>({
|
|
2340
|
+
type: t.type,
|
|
2341
|
+
url: t.url?.slice(0, 100)
|
|
2342
|
+
}))
|
|
2343
|
+
});
|
|
2344
|
+
}
|
|
1967
2345
|
const target = args.url ? pageTargets.find((t)=>t.url.includes(args.url)) ?? pageTargets[0] : pageTargets[0];
|
|
1968
2346
|
const browserWsUrl = await CDPClient.discoverBrowserWsUrl(cdpPort);
|
|
1969
2347
|
await cdp.connect(browserWsUrl);
|
|
@@ -2459,7 +2837,7 @@ function commonFlags(args) {
|
|
|
2459
2837
|
}
|
|
2460
2838
|
const eval_schema = {
|
|
2461
2839
|
name: "extension_eval",
|
|
2462
|
-
description: "Evaluate an expression in a running extension context
|
|
2840
|
+
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
2841
|
inputSchema: {
|
|
2464
2842
|
type: "object",
|
|
2465
2843
|
properties: {
|
|
@@ -2487,11 +2865,11 @@ const eval_schema = {
|
|
|
2487
2865
|
},
|
|
2488
2866
|
url: {
|
|
2489
2867
|
type: "string",
|
|
2490
|
-
description: "For content/page: document(s) to target"
|
|
2868
|
+
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
2869
|
},
|
|
2492
2870
|
tab: {
|
|
2493
2871
|
type: "number",
|
|
2494
|
-
description: "
|
|
2872
|
+
description: "Numeric chrome.tabs id. Required for context content/page/popup on Chromium (the `url` arg does not substitute for it)."
|
|
2495
2873
|
},
|
|
2496
2874
|
browser: {
|
|
2497
2875
|
type: "string",
|
|
@@ -2703,7 +3081,21 @@ async function open_handler(args) {
|
|
|
2703
3081
|
if ("command" === args.surface && args.name) cli.push("--name", args.name);
|
|
2704
3082
|
cli.push("--browser", browser);
|
|
2705
3083
|
if (null != args.timeout) cli.push("--timeout", String(args.timeout));
|
|
2706
|
-
|
|
3084
|
+
const raw = await runActVerb(cli, args.projectPath, args.timeout);
|
|
3085
|
+
const headless = /^(1|true)$/i.test(process.env.EXTENSION_HEADLESS ?? "");
|
|
3086
|
+
if (headless && [
|
|
3087
|
+
"popup",
|
|
3088
|
+
"action",
|
|
3089
|
+
"sidebar"
|
|
3090
|
+
].includes(args.surface)) try {
|
|
3091
|
+
const parsed = JSON.parse(raw);
|
|
3092
|
+
const msg = String(parsed?.error?.message ?? "");
|
|
3093
|
+
if (parsed?.ok === false && !parsed.hint && /active browser window|no active|headless/i.test(msg)) {
|
|
3094
|
+
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.";
|
|
3095
|
+
return JSON.stringify(parsed);
|
|
3096
|
+
}
|
|
3097
|
+
} catch {}
|
|
3098
|
+
return raw;
|
|
2707
3099
|
}
|
|
2708
3100
|
const dom_inspect_schema = {
|
|
2709
3101
|
name: "extension_dom_inspect",
|
|
@@ -2780,8 +3172,9 @@ async function dom_inspect_handler(args) {
|
|
|
2780
3172
|
ok: false,
|
|
2781
3173
|
error: {
|
|
2782
3174
|
name: "BadRequest",
|
|
2783
|
-
message: "content/page inspect requires a tab id"
|
|
2784
|
-
}
|
|
3175
|
+
message: "content/page inspect requires a numeric tab id (chrome.tabs id, not a CDP target id)."
|
|
3176
|
+
},
|
|
3177
|
+
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
3178
|
});
|
|
2786
3179
|
const cli = [
|
|
2787
3180
|
"inspect",
|
|
@@ -2813,13 +3206,17 @@ function readCredentials() {
|
|
|
2813
3206
|
if (1 !== data.version) return null;
|
|
2814
3207
|
const token = String(data.token || "").trim();
|
|
2815
3208
|
if (!token) return null;
|
|
3209
|
+
const provider = "extensiondev" === data.provider || "github" === data.provider ? data.provider : void 0;
|
|
2816
3210
|
return {
|
|
2817
3211
|
version: 1,
|
|
2818
3212
|
token,
|
|
2819
3213
|
workspaceSlug: String(data.workspaceSlug || ""),
|
|
2820
3214
|
projectSlug: String(data.projectSlug || ""),
|
|
2821
3215
|
expiresAt: Number(data.expiresAt || 0),
|
|
2822
|
-
api: String(data.api || "")
|
|
3216
|
+
api: String(data.api || ""),
|
|
3217
|
+
...provider ? {
|
|
3218
|
+
provider
|
|
3219
|
+
} : {}
|
|
2823
3220
|
};
|
|
2824
3221
|
} catch {
|
|
2825
3222
|
return null;
|
|
@@ -2891,8 +3288,12 @@ function safeApiBase(raw) {
|
|
|
2891
3288
|
async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
|
|
2892
3289
|
const override = String(process.env.EXTENSION_DEV_GITHUB_CLIENT_ID || "").trim();
|
|
2893
3290
|
if (override) return {
|
|
3291
|
+
provider: "github",
|
|
2894
3292
|
clientId: override,
|
|
2895
|
-
scope: "read:user"
|
|
3293
|
+
scope: "read:user",
|
|
3294
|
+
deviceCodeUrl: "/api/cli/device/code",
|
|
3295
|
+
deviceTokenUrl: "/api/cli/device/token",
|
|
3296
|
+
verificationUri: "https://github.com/login/device"
|
|
2896
3297
|
};
|
|
2897
3298
|
const res = await fetchImpl(`${apiBase}/api/cli/login/config`, {
|
|
2898
3299
|
headers: {
|
|
@@ -2901,12 +3302,32 @@ async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
|
|
|
2901
3302
|
});
|
|
2902
3303
|
if (!res.ok) throw new Error(`Could not fetch login config from ${apiBase} (${res.status}).`);
|
|
2903
3304
|
const data = await res.json().catch(()=>({}));
|
|
3305
|
+
const provider = "extensiondev" === data.provider ? "extensiondev" : "github";
|
|
2904
3306
|
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.");
|
|
3307
|
+
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
3308
|
return {
|
|
3309
|
+
provider,
|
|
2907
3310
|
clientId,
|
|
2908
|
-
scope: String(data.scope || "read:user")
|
|
3311
|
+
scope: String(data.scope || "read:user"),
|
|
3312
|
+
deviceCodeUrl: String(data.deviceCodeUrl || "/api/cli/device/code"),
|
|
3313
|
+
deviceTokenUrl: String(data.deviceTokenUrl || "/api/cli/device/token"),
|
|
3314
|
+
verificationUri: String(data.verificationUri || "https://github.com/login/device")
|
|
3315
|
+
};
|
|
3316
|
+
}
|
|
3317
|
+
function persistTokenResponse(args) {
|
|
3318
|
+
const token = String(args.data.token || "").trim();
|
|
3319
|
+
if (!token) throw new Error("Login returned no token.");
|
|
3320
|
+
const creds = {
|
|
3321
|
+
version: 1,
|
|
3322
|
+
token,
|
|
3323
|
+
workspaceSlug: String(args.data.workspaceSlug || ""),
|
|
3324
|
+
projectSlug: String(args.data.projectSlug || ""),
|
|
3325
|
+
expiresAt: Number(args.data.expiresAt || 0),
|
|
3326
|
+
api: args.apiBase,
|
|
3327
|
+
provider: args.provider
|
|
2909
3328
|
};
|
|
3329
|
+
writeCredentials(creds);
|
|
3330
|
+
return creds;
|
|
2910
3331
|
}
|
|
2911
3332
|
async function exchangeAndPersist(args) {
|
|
2912
3333
|
const doFetch = args.fetchImpl ?? fetch;
|
|
@@ -2931,18 +3352,11 @@ async function exchangeAndPersist(args) {
|
|
|
2931
3352
|
};
|
|
2932
3353
|
}
|
|
2933
3354
|
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;
|
|
3355
|
+
return persistTokenResponse({
|
|
3356
|
+
apiBase: args.apiBase,
|
|
3357
|
+
data,
|
|
3358
|
+
provider: "github"
|
|
3359
|
+
});
|
|
2946
3360
|
}
|
|
2947
3361
|
function resolveToken() {
|
|
2948
3362
|
const fromEnv = String(process.env.EXTENSION_DEV_TOKEN || "").trim();
|
|
@@ -3014,53 +3428,60 @@ async function publish(options = {}) {
|
|
|
3014
3428
|
}
|
|
3015
3429
|
const publish_schema = {
|
|
3016
3430
|
name: "extension_publish",
|
|
3017
|
-
description: "Publish
|
|
3431
|
+
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
3432
|
inputSchema: {
|
|
3019
3433
|
type: "object",
|
|
3020
3434
|
properties: {
|
|
3021
|
-
projectPath: {
|
|
3022
|
-
type: "string",
|
|
3023
|
-
description: "Path to the extension project root"
|
|
3024
|
-
},
|
|
3025
3435
|
ttlHours: {
|
|
3026
3436
|
type: "number",
|
|
3027
|
-
description: "
|
|
3437
|
+
description: "Private-project share-link lifetime in hours, 1-168 (default 24). Ignored for public projects."
|
|
3028
3438
|
},
|
|
3029
3439
|
buildSha: {
|
|
3030
3440
|
type: "string",
|
|
3031
|
-
description: "Pin the share URL to a specific build sha"
|
|
3441
|
+
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
3442
|
},
|
|
3033
3443
|
api: {
|
|
3034
3444
|
type: "string",
|
|
3035
3445
|
description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
|
|
3036
3446
|
}
|
|
3037
3447
|
},
|
|
3038
|
-
required: [
|
|
3039
|
-
"projectPath"
|
|
3040
|
-
]
|
|
3448
|
+
required: []
|
|
3041
3449
|
}
|
|
3042
3450
|
};
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
if (!token) return JSON.stringify({
|
|
3451
|
+
function fail(name, message) {
|
|
3452
|
+
return JSON.stringify({
|
|
3046
3453
|
ok: false,
|
|
3047
3454
|
error: {
|
|
3048
|
-
name
|
|
3049
|
-
message
|
|
3455
|
+
name,
|
|
3456
|
+
message
|
|
3050
3457
|
}
|
|
3051
3458
|
});
|
|
3459
|
+
}
|
|
3460
|
+
async function publish_handler(args) {
|
|
3461
|
+
const token = resolveToken();
|
|
3462
|
+
if (!token) return fail("PublishAuthError", "No token. Run extension_login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard).");
|
|
3463
|
+
if (null != args.ttlHours) {
|
|
3464
|
+
const t = Number(args.ttlHours);
|
|
3465
|
+
if (!Number.isInteger(t) || t < 1 || t > 168) return fail("PublishBadRequest", "ttlHours must be an integer between 1 and 168.");
|
|
3466
|
+
}
|
|
3467
|
+
if (null != args.buildSha && "" !== args.buildSha) {
|
|
3468
|
+
if (!/^[0-9a-f]{7,40}$/i.test(args.buildSha)) return fail("PublishBadRequest", "buildSha must be a 7-40 character hex git sha.");
|
|
3469
|
+
}
|
|
3052
3470
|
const result = await publish({
|
|
3053
3471
|
ttlHours: args.ttlHours,
|
|
3054
3472
|
buildSha: args.buildSha,
|
|
3055
3473
|
api: args.api,
|
|
3056
3474
|
token
|
|
3057
3475
|
});
|
|
3058
|
-
|
|
3476
|
+
if (!result.ok) return JSON.stringify(result);
|
|
3477
|
+
const data = result.data;
|
|
3478
|
+
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.";
|
|
3479
|
+
return JSON.stringify(data);
|
|
3059
3480
|
}
|
|
3060
3481
|
const release_promote_DEFAULT_API = "https://www.extension.dev";
|
|
3061
3482
|
const release_promote_schema = {
|
|
3062
3483
|
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:
|
|
3484
|
+
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
3485
|
inputSchema: {
|
|
3065
3486
|
type: "object",
|
|
3066
3487
|
properties: {
|
|
@@ -3102,7 +3523,7 @@ const release_promote_schema = {
|
|
|
3102
3523
|
]
|
|
3103
3524
|
}
|
|
3104
3525
|
};
|
|
3105
|
-
function
|
|
3526
|
+
function release_promote_fail(name, message) {
|
|
3106
3527
|
return JSON.stringify({
|
|
3107
3528
|
ok: false,
|
|
3108
3529
|
error: {
|
|
@@ -3113,12 +3534,12 @@ function fail(name, message) {
|
|
|
3113
3534
|
}
|
|
3114
3535
|
async function release_promote_handler(args) {
|
|
3115
3536
|
const token = resolveToken();
|
|
3116
|
-
if (!token) return
|
|
3537
|
+
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
3538
|
const buildId = String(args.buildId || "").trim();
|
|
3118
3539
|
const channel = String(args.channel || "").trim();
|
|
3119
|
-
if (!buildId || !channel) return
|
|
3540
|
+
if (!buildId || !channel) return release_promote_fail("ReleaseInputError", "buildId and channel are required.");
|
|
3120
3541
|
const apiCheck = safeApiBase(String(args.api || process.env.EXTENSION_DEV_API_URL || release_promote_DEFAULT_API));
|
|
3121
|
-
if (!apiCheck.ok) return
|
|
3542
|
+
if (!apiCheck.ok) return release_promote_fail("ReleaseConfigError", apiCheck.message);
|
|
3122
3543
|
const url = `${apiCheck.base}/api/cli/release/promote`;
|
|
3123
3544
|
const body = {
|
|
3124
3545
|
buildId,
|
|
@@ -3139,7 +3560,7 @@ async function release_promote_handler(args) {
|
|
|
3139
3560
|
body: JSON.stringify(body)
|
|
3140
3561
|
});
|
|
3141
3562
|
} catch (err) {
|
|
3142
|
-
return
|
|
3563
|
+
return release_promote_fail("ReleaseNetworkError", `Could not reach ${url}: ${err?.message || err}`);
|
|
3143
3564
|
}
|
|
3144
3565
|
const text = await res.text();
|
|
3145
3566
|
let data;
|
|
@@ -3150,175 +3571,117 @@ async function release_promote_handler(args) {
|
|
|
3150
3571
|
message: text
|
|
3151
3572
|
};
|
|
3152
3573
|
}
|
|
3153
|
-
if (!res.ok) return
|
|
3574
|
+
if (!res.ok) return release_promote_fail("ReleaseError", `promote failed (${res.status}): ${data?.message || text || "unknown error"}`);
|
|
3154
3575
|
return JSON.stringify(data);
|
|
3155
3576
|
}
|
|
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
|
-
}
|
|
3577
|
+
const deploy_DEFAULT_API = "https://www.extension.dev";
|
|
3183
3578
|
const deploy_schema = {
|
|
3184
3579
|
name: "extension_deploy",
|
|
3185
|
-
description: "Submit a built extension to the Chrome Web Store, Firefox AMO, and/or Edge Add-ons
|
|
3580
|
+
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
3581
|
inputSchema: {
|
|
3187
3582
|
type: "object",
|
|
3188
3583
|
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"
|
|
3584
|
+
browsers: {
|
|
3585
|
+
type: "array",
|
|
3586
|
+
items: {
|
|
3587
|
+
type: "string",
|
|
3588
|
+
enum: [
|
|
3589
|
+
"chrome",
|
|
3590
|
+
"firefox",
|
|
3591
|
+
"edge",
|
|
3592
|
+
"safari"
|
|
3593
|
+
]
|
|
3594
|
+
},
|
|
3595
|
+
description: "Stores to submit to."
|
|
3216
3596
|
},
|
|
3217
|
-
|
|
3597
|
+
buildSha: {
|
|
3218
3598
|
type: "string",
|
|
3219
|
-
|
|
3220
|
-
"listed",
|
|
3221
|
-
"unlisted"
|
|
3222
|
-
],
|
|
3223
|
-
description: "Firefox channel (unlisted is required for a first submission)"
|
|
3599
|
+
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
3600
|
},
|
|
3225
|
-
|
|
3601
|
+
channel: {
|
|
3226
3602
|
type: "string",
|
|
3227
|
-
description: "
|
|
3603
|
+
description: "Release channel to submit from (default stable)."
|
|
3228
3604
|
},
|
|
3229
|
-
|
|
3605
|
+
version: {
|
|
3230
3606
|
type: "string",
|
|
3231
|
-
description: "
|
|
3607
|
+
description: "Version label for the submission record (optional)."
|
|
3232
3608
|
},
|
|
3233
3609
|
dryRun: {
|
|
3234
3610
|
type: "boolean",
|
|
3235
3611
|
default: true,
|
|
3236
|
-
description: "
|
|
3237
|
-
},
|
|
3238
|
-
stagedPublish: {
|
|
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."
|
|
3612
|
+
description: "Preflight only (verify auth, project, build, and store workflow). Pass false to actually dispatch the submission (irreversible, enters store review)."
|
|
3256
3613
|
},
|
|
3257
|
-
|
|
3614
|
+
api: {
|
|
3258
3615
|
type: "string",
|
|
3259
|
-
description: "
|
|
3616
|
+
description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
|
|
3260
3617
|
}
|
|
3261
3618
|
},
|
|
3262
3619
|
required: [
|
|
3263
|
-
"
|
|
3620
|
+
"browsers",
|
|
3621
|
+
"buildSha"
|
|
3264
3622
|
]
|
|
3265
3623
|
}
|
|
3266
3624
|
};
|
|
3267
|
-
|
|
3268
|
-
|
|
3625
|
+
function deploy_fail(name, message) {
|
|
3626
|
+
return JSON.stringify({
|
|
3269
3627
|
ok: false,
|
|
3270
3628
|
error: {
|
|
3271
|
-
name
|
|
3272
|
-
message
|
|
3629
|
+
name,
|
|
3630
|
+
message
|
|
3273
3631
|
}
|
|
3274
3632
|
});
|
|
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
|
-
}));
|
|
3633
|
+
}
|
|
3634
|
+
async function deploy_handler(args) {
|
|
3635
|
+
const token = resolveToken();
|
|
3636
|
+
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).");
|
|
3637
|
+
const browsers = (Array.isArray(args.browsers) ? args.browsers : []).map((b)=>String(b).trim().toLowerCase()).filter(Boolean);
|
|
3638
|
+
if (0 === browsers.length) return deploy_fail("DeployInputError", 'browsers is required (e.g. ["chrome","firefox","edge"]).');
|
|
3639
|
+
const buildSha = String(args.buildSha || "").trim();
|
|
3640
|
+
if (!buildSha) return deploy_fail("DeployInputError", "buildSha is required (the built commit to submit).");
|
|
3641
|
+
const apiCheck = safeApiBase(String(args.api || process.env.EXTENSION_DEV_API_URL || deploy_DEFAULT_API));
|
|
3642
|
+
if (!apiCheck.ok) return deploy_fail("DeployConfigError", apiCheck.message);
|
|
3643
|
+
const url = `${apiCheck.base}/api/cli/stores/submit`;
|
|
3644
|
+
const dryRun = false !== args.dryRun;
|
|
3645
|
+
const body = {
|
|
3646
|
+
browsers,
|
|
3647
|
+
buildSha,
|
|
3648
|
+
dryRun
|
|
3649
|
+
};
|
|
3650
|
+
if (args.channel) body.channel = String(args.channel).trim();
|
|
3651
|
+
if (args.version) body.version = String(args.version).trim();
|
|
3652
|
+
let res;
|
|
3653
|
+
try {
|
|
3654
|
+
res = await fetch(url, {
|
|
3655
|
+
method: "POST",
|
|
3656
|
+
headers: {
|
|
3657
|
+
authorization: `Bearer ${token}`,
|
|
3658
|
+
"content-type": "application/json"
|
|
3659
|
+
},
|
|
3660
|
+
body: JSON.stringify(body)
|
|
3316
3661
|
});
|
|
3662
|
+
} catch (err) {
|
|
3663
|
+
return deploy_fail("DeployNetworkError", `Could not reach ${url}: ${err?.message || err}`);
|
|
3664
|
+
}
|
|
3665
|
+
const text = await res.text();
|
|
3666
|
+
let data;
|
|
3667
|
+
try {
|
|
3668
|
+
data = JSON.parse(text);
|
|
3669
|
+
} catch {
|
|
3670
|
+
data = {
|
|
3671
|
+
message: text
|
|
3672
|
+
};
|
|
3673
|
+
}
|
|
3674
|
+
if (!res.ok) return deploy_fail("DeployError", `submit failed (${res.status}): ${data?.message || text || "unknown error"}`);
|
|
3675
|
+
return JSON.stringify({
|
|
3676
|
+
mode: "platform",
|
|
3677
|
+
dryRun,
|
|
3678
|
+
...data
|
|
3317
3679
|
});
|
|
3318
3680
|
}
|
|
3681
|
+
const SAFE_CEILING_MS = 50000;
|
|
3319
3682
|
const wait_schema = {
|
|
3320
3683
|
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.",
|
|
3684
|
+
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
3685
|
inputSchema: {
|
|
3323
3686
|
type: "object",
|
|
3324
3687
|
properties: {
|
|
@@ -3332,8 +3695,8 @@ const wait_schema = {
|
|
|
3332
3695
|
},
|
|
3333
3696
|
timeout: {
|
|
3334
3697
|
type: "number",
|
|
3335
|
-
default:
|
|
3336
|
-
description: "
|
|
3698
|
+
default: 45000,
|
|
3699
|
+
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
3700
|
}
|
|
3338
3701
|
},
|
|
3339
3702
|
required: [
|
|
@@ -3343,7 +3706,9 @@ const wait_schema = {
|
|
|
3343
3706
|
};
|
|
3344
3707
|
async function wait_handler(args) {
|
|
3345
3708
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser, "chrome");
|
|
3346
|
-
const
|
|
3709
|
+
const requested = args.timeout ?? 45000;
|
|
3710
|
+
const timeout = Math.min(Math.max(requested, 1000), SAFE_CEILING_MS);
|
|
3711
|
+
const clamped = requested > SAFE_CEILING_MS;
|
|
3347
3712
|
const readyPath = node_path.resolve(args.projectPath, "dist", "extension-js", browser, "ready.json");
|
|
3348
3713
|
const start = Date.now();
|
|
3349
3714
|
const pollInterval = 1000;
|
|
@@ -3376,9 +3741,11 @@ async function wait_handler(args) {
|
|
|
3376
3741
|
}
|
|
3377
3742
|
return JSON.stringify({
|
|
3378
3743
|
status: "timeout",
|
|
3379
|
-
message: `Extension
|
|
3744
|
+
message: `Extension not ready after ${timeout}ms this call`,
|
|
3380
3745
|
readyPath,
|
|
3381
|
-
|
|
3746
|
+
waitDuration: Date.now() - start,
|
|
3747
|
+
clamped: clamped ? `requested ${requested}ms was clamped to ${SAFE_CEILING_MS}ms to stay under the MCP client request timeout` : void 0,
|
|
3748
|
+
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
3749
|
});
|
|
3383
3750
|
}
|
|
3384
3751
|
const add_feature_schema = {
|
|
@@ -3548,7 +3915,23 @@ async function add_feature_handler(args) {
|
|
|
3548
3915
|
});
|
|
3549
3916
|
const template = await getTemplateBySlug(templateSlug);
|
|
3550
3917
|
const referenceFiles = template?.keyFiles ?? template?.files ?? [];
|
|
3551
|
-
const
|
|
3918
|
+
const FEATURE_DIR = {
|
|
3919
|
+
"content-script": "content",
|
|
3920
|
+
popup: "action"
|
|
3921
|
+
};
|
|
3922
|
+
const featureDir = FEATURE_DIR[args.feature] ?? args.feature;
|
|
3923
|
+
const scriptExt = "react" === framework || "preact" === framework ? "tsx" : "ts";
|
|
3924
|
+
const componentExt = "vue" === framework ? "vue" : "svelte" === framework ? "svelte" : "tsx";
|
|
3925
|
+
const COMPONENT_BASE = {
|
|
3926
|
+
content: "Content",
|
|
3927
|
+
sidebar: "Sidebar",
|
|
3928
|
+
action: "Action",
|
|
3929
|
+
newtab: "NewTab",
|
|
3930
|
+
options: "Options",
|
|
3931
|
+
devtools: "DevTools",
|
|
3932
|
+
background: "Background"
|
|
3933
|
+
};
|
|
3934
|
+
const componentBase = COMPONENT_BASE[featureDir] ?? featureDir.charAt(0).toUpperCase() + featureDir.slice(1);
|
|
3552
3935
|
const filesToCreate = [];
|
|
3553
3936
|
const manifestUpdates = MANIFEST_ADDITIONS[args.feature] ?? {};
|
|
3554
3937
|
if ([
|
|
@@ -3562,24 +3945,42 @@ async function add_feature_handler(args) {
|
|
|
3562
3945
|
path: `src/${featureDir}/index.html`,
|
|
3563
3946
|
hint: "HTML entry point"
|
|
3564
3947
|
}, {
|
|
3565
|
-
path: `src/${featureDir}/scripts.${
|
|
3948
|
+
path: `src/${featureDir}/scripts.${scriptExt}`,
|
|
3566
3949
|
hint: "vanilla" === framework ? "Script entry point" : `${framework} mount point`
|
|
3567
3950
|
}, {
|
|
3568
3951
|
path: `src/${featureDir}/styles.css`,
|
|
3569
3952
|
hint: "Stylesheet"
|
|
3570
3953
|
});
|
|
3571
|
-
if ("vanilla" !== framework)
|
|
3572
|
-
|
|
3573
|
-
|
|
3954
|
+
if ("vanilla" !== framework) {
|
|
3955
|
+
filesToCreate.push({
|
|
3956
|
+
path: `src/${featureDir}/${componentBase}App.${componentExt}`,
|
|
3957
|
+
hint: `Main ${framework} component`
|
|
3958
|
+
});
|
|
3959
|
+
if ("vue" === framework) filesToCreate.push({
|
|
3960
|
+
path: `src/${featureDir}/shims-vue.d.ts`,
|
|
3961
|
+
hint: "Vue SFC type shim (lets TS import .vue components)"
|
|
3962
|
+
});
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3965
|
+
if ("content-script" === args.feature) {
|
|
3966
|
+
filesToCreate.push({
|
|
3967
|
+
path: "src/content/scripts.ts",
|
|
3968
|
+
hint: "vanilla" === framework ? "Content script entry point" : `${framework} content-script mount point`
|
|
3969
|
+
}, {
|
|
3970
|
+
path: "src/content/styles.css",
|
|
3971
|
+
hint: "Content script styles"
|
|
3574
3972
|
});
|
|
3973
|
+
if ("vanilla" !== framework) {
|
|
3974
|
+
filesToCreate.push({
|
|
3975
|
+
path: `src/content/ContentApp.${componentExt}`,
|
|
3976
|
+
hint: `Main ${framework} component mounted by the content script`
|
|
3977
|
+
});
|
|
3978
|
+
if ("vue" === framework) filesToCreate.push({
|
|
3979
|
+
path: "src/content/shims-vue.d.ts",
|
|
3980
|
+
hint: "Vue SFC type shim (lets TS import .vue components)"
|
|
3981
|
+
});
|
|
3982
|
+
}
|
|
3575
3983
|
}
|
|
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
3984
|
if ("background" === args.feature) filesToCreate.push({
|
|
3584
3985
|
path: "src/background.ts",
|
|
3585
3986
|
hint: "Background service worker / script"
|
|
@@ -3682,9 +4083,102 @@ async function pollForToken(args) {
|
|
|
3682
4083
|
reason: "pending"
|
|
3683
4084
|
};
|
|
3684
4085
|
}
|
|
4086
|
+
async function requestDeviceCode(args) {
|
|
4087
|
+
const doFetch = args.fetchImpl ?? fetch;
|
|
4088
|
+
const res = await doFetch(`${args.apiBase}${args.path}`, {
|
|
4089
|
+
method: "POST",
|
|
4090
|
+
headers: {
|
|
4091
|
+
"content-type": "application/json",
|
|
4092
|
+
accept: "application/json"
|
|
4093
|
+
},
|
|
4094
|
+
body: JSON.stringify({
|
|
4095
|
+
project: args.project,
|
|
4096
|
+
clientName: args.clientName ?? "extension-mcp"
|
|
4097
|
+
})
|
|
4098
|
+
});
|
|
4099
|
+
const text = await res.text();
|
|
4100
|
+
let data;
|
|
4101
|
+
try {
|
|
4102
|
+
data = JSON.parse(text);
|
|
4103
|
+
} catch {
|
|
4104
|
+
data = {
|
|
4105
|
+
message: text
|
|
4106
|
+
};
|
|
4107
|
+
}
|
|
4108
|
+
if (!res.ok) throw new Error(`Device code request failed (${res.status}): ${data.message || "unknown error"}`);
|
|
4109
|
+
const deviceCode = String(data.device_code || "").trim();
|
|
4110
|
+
const userCode = String(data.user_code || "").trim();
|
|
4111
|
+
if (!deviceCode || !userCode) throw new Error("Device code response missing device_code/user_code.");
|
|
4112
|
+
return {
|
|
4113
|
+
deviceCode,
|
|
4114
|
+
userCode,
|
|
4115
|
+
verificationUri: String(data.verification_uri || "https://extension.dev/device"),
|
|
4116
|
+
verificationUriComplete: String(data.verification_uri_complete || data.verification_uri || ""),
|
|
4117
|
+
interval: Number(data.interval || 5),
|
|
4118
|
+
expiresIn: Number(data.expires_in || 900)
|
|
4119
|
+
};
|
|
4120
|
+
}
|
|
4121
|
+
async function pollDeviceToken(args) {
|
|
4122
|
+
const doFetch = args.fetchImpl ?? fetch;
|
|
4123
|
+
const deadline = Date.now() + args.budgetMs;
|
|
4124
|
+
let interval = Math.max(1, args.interval);
|
|
4125
|
+
for(;;){
|
|
4126
|
+
const res = await doFetch(`${args.apiBase}${args.path}`, {
|
|
4127
|
+
method: "POST",
|
|
4128
|
+
headers: {
|
|
4129
|
+
"content-type": "application/json",
|
|
4130
|
+
accept: "application/json"
|
|
4131
|
+
},
|
|
4132
|
+
body: JSON.stringify({
|
|
4133
|
+
device_code: args.deviceCode,
|
|
4134
|
+
project: args.project
|
|
4135
|
+
})
|
|
4136
|
+
});
|
|
4137
|
+
const text = await res.text();
|
|
4138
|
+
let data;
|
|
4139
|
+
try {
|
|
4140
|
+
data = JSON.parse(text);
|
|
4141
|
+
} catch {
|
|
4142
|
+
data = {
|
|
4143
|
+
message: text
|
|
4144
|
+
};
|
|
4145
|
+
}
|
|
4146
|
+
if (res.ok && data.token) {
|
|
4147
|
+
const creds = persistTokenResponse({
|
|
4148
|
+
apiBase: args.apiBase,
|
|
4149
|
+
data,
|
|
4150
|
+
provider: "extensiondev"
|
|
4151
|
+
});
|
|
4152
|
+
return {
|
|
4153
|
+
ok: true,
|
|
4154
|
+
creds
|
|
4155
|
+
};
|
|
4156
|
+
}
|
|
4157
|
+
const error = String(data.error || "");
|
|
4158
|
+
if ("access_denied" === error) return {
|
|
4159
|
+
ok: false,
|
|
4160
|
+
reason: "denied"
|
|
4161
|
+
};
|
|
4162
|
+
if ("expired_token" === error) return {
|
|
4163
|
+
ok: false,
|
|
4164
|
+
reason: "expired"
|
|
4165
|
+
};
|
|
4166
|
+
if ("slow_down" === error) interval += 5;
|
|
4167
|
+
else if (error && "authorization_pending" !== error) return {
|
|
4168
|
+
ok: false,
|
|
4169
|
+
reason: "error",
|
|
4170
|
+
message: String(data.message || error)
|
|
4171
|
+
};
|
|
4172
|
+
if (Date.now() + 1000 * interval >= deadline) return {
|
|
4173
|
+
ok: false,
|
|
4174
|
+
reason: "pending"
|
|
4175
|
+
};
|
|
4176
|
+
await new Promise((r)=>setTimeout(r, 1000 * interval));
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
3685
4179
|
const login_schema = {
|
|
3686
4180
|
name: "extension_login",
|
|
3687
|
-
description: "Authenticate to extension.dev
|
|
4181
|
+
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
4182
|
inputSchema: {
|
|
3689
4183
|
type: "object",
|
|
3690
4184
|
properties: {
|
|
@@ -3747,6 +4241,52 @@ async function login_handler(args) {
|
|
|
3747
4241
|
} catch (err) {
|
|
3748
4242
|
return login_fail("LoginConfigError", err?.message || "Could not load login config.");
|
|
3749
4243
|
}
|
|
4244
|
+
if ("extensiondev" === config.provider) {
|
|
4245
|
+
if (args.deviceCode) {
|
|
4246
|
+
const poll = await pollDeviceToken({
|
|
4247
|
+
apiBase,
|
|
4248
|
+
path: config.deviceTokenUrl,
|
|
4249
|
+
project,
|
|
4250
|
+
deviceCode: String(args.deviceCode),
|
|
4251
|
+
interval: 5,
|
|
4252
|
+
budgetMs: RESUME_BUDGET_MS
|
|
4253
|
+
});
|
|
4254
|
+
if (poll.ok) return success(poll.creds);
|
|
4255
|
+
if ("expired" === poll.reason) return login_fail("LoginExpired", "The device code expired. Run extension_login again to restart.");
|
|
4256
|
+
if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied at extension.dev/device.");
|
|
4257
|
+
if ("error" === poll.reason) return login_fail("LoginError", poll.message || "Device login failed.");
|
|
4258
|
+
return login_pending({
|
|
4259
|
+
deviceCode: String(args.deviceCode),
|
|
4260
|
+
userCode: "(see the previous response)",
|
|
4261
|
+
verificationUri: config.verificationUri
|
|
4262
|
+
});
|
|
4263
|
+
}
|
|
4264
|
+
let start;
|
|
4265
|
+
try {
|
|
4266
|
+
start = await requestDeviceCode({
|
|
4267
|
+
apiBase,
|
|
4268
|
+
path: config.deviceCodeUrl,
|
|
4269
|
+
project
|
|
4270
|
+
});
|
|
4271
|
+
} catch (err) {
|
|
4272
|
+
return login_fail("LoginStartError", err?.message || "Could not start the device flow.");
|
|
4273
|
+
}
|
|
4274
|
+
const poll = await pollDeviceToken({
|
|
4275
|
+
apiBase,
|
|
4276
|
+
path: config.deviceTokenUrl,
|
|
4277
|
+
project,
|
|
4278
|
+
deviceCode: start.deviceCode,
|
|
4279
|
+
interval: start.interval,
|
|
4280
|
+
budgetMs: FIRST_CALL_BUDGET_MS
|
|
4281
|
+
});
|
|
4282
|
+
if (poll.ok) return success(poll.creds);
|
|
4283
|
+
if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied at extension.dev/device.");
|
|
4284
|
+
return login_pending({
|
|
4285
|
+
deviceCode: start.deviceCode,
|
|
4286
|
+
userCode: start.userCode,
|
|
4287
|
+
verificationUri: start.verificationUri
|
|
4288
|
+
});
|
|
4289
|
+
}
|
|
3750
4290
|
if (args.deviceCode) {
|
|
3751
4291
|
const poll = await pollForToken({
|
|
3752
4292
|
clientId: config.clientId,
|
|
@@ -3825,6 +4365,7 @@ async function whoami_handler() {
|
|
|
3825
4365
|
workspaceSlug: creds.workspaceSlug,
|
|
3826
4366
|
projectSlug: creds.projectSlug,
|
|
3827
4367
|
api: creds.api,
|
|
4368
|
+
provider: creds.provider ?? "github",
|
|
3828
4369
|
expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
|
|
3829
4370
|
expiresInSeconds: creds.expiresAt ? creds.expiresAt - now : null,
|
|
3830
4371
|
expired,
|
|
@@ -3849,7 +4390,7 @@ async function logout_handler() {
|
|
|
3849
4390
|
}
|
|
3850
4391
|
const install_browser_schema = {
|
|
3851
4392
|
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.",
|
|
4393
|
+
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
4394
|
inputSchema: {
|
|
3854
4395
|
type: "object",
|
|
3855
4396
|
properties: {
|
|
@@ -4323,37 +4864,80 @@ async function detect_browsers_handler(args) {
|
|
|
4323
4864
|
hint: missing.length ? `Missing browser(s): ${missing.map((d)=>d.browser).join(", ")}.${missing.some((d)=>MANAGED_INSTALLABLE.has(d.browser)) ? ` Use extension_install_browser to install ${missing.filter((d)=>MANAGED_INSTALLABLE.has(d.browser)).map((d)=>d.browser).join(", ")}.` : ""}` : "All requested browsers are available."
|
|
4324
4865
|
});
|
|
4325
4866
|
}
|
|
4867
|
+
function doctor_readReadyContract(projectPath, browser) {
|
|
4868
|
+
try {
|
|
4869
|
+
const raw = node_fs.readFileSync(node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json"), "utf8");
|
|
4870
|
+
return JSON.parse(raw);
|
|
4871
|
+
} catch {
|
|
4872
|
+
return null;
|
|
4873
|
+
}
|
|
4874
|
+
}
|
|
4326
4875
|
const doctor_schema = {
|
|
4327
4876
|
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`.",
|
|
4877
|
+
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
4878
|
inputSchema: {
|
|
4330
4879
|
type: "object",
|
|
4331
4880
|
properties: {
|
|
4332
4881
|
projectPath: {
|
|
4333
4882
|
type: "string",
|
|
4334
|
-
description: "Path to the extension project root"
|
|
4883
|
+
description: "Path to the extension project root. Omit for a pre-flight environment check with no project."
|
|
4335
4884
|
},
|
|
4336
4885
|
browser: {
|
|
4337
4886
|
type: "string",
|
|
4338
4887
|
description: "Browser session to diagnose. Defaults to the active dev session's browser for this project."
|
|
4339
4888
|
}
|
|
4340
|
-
}
|
|
4341
|
-
required: [
|
|
4342
|
-
"projectPath"
|
|
4343
|
-
]
|
|
4889
|
+
}
|
|
4344
4890
|
}
|
|
4345
4891
|
};
|
|
4892
|
+
async function environmentPreflight() {
|
|
4893
|
+
const checks = [];
|
|
4894
|
+
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
4895
|
+
checks.push({
|
|
4896
|
+
check: "node",
|
|
4897
|
+
status: nodeMajor >= 20 ? "pass" : "fail",
|
|
4898
|
+
detail: `Node ${process.versions.node} on ${process.platform}/${process.arch}`,
|
|
4899
|
+
remediation: nodeMajor >= 20 ? void 0 : "Extension.js needs Node >= 20.18."
|
|
4900
|
+
});
|
|
4901
|
+
const { code, stdout, stderr } = await runExtensionCli([
|
|
4902
|
+
"--version"
|
|
4903
|
+
], {
|
|
4904
|
+
timeoutMs: 60000
|
|
4905
|
+
});
|
|
4906
|
+
const cliVersion = stdout.trim() || stderr.trim();
|
|
4907
|
+
checks.push({
|
|
4908
|
+
check: "extension-cli",
|
|
4909
|
+
status: 0 === code ? "pass" : "fail",
|
|
4910
|
+
detail: 0 === code ? `extension CLI resolvable (${cliVersion})` : "extension CLI could not be resolved",
|
|
4911
|
+
remediation: 0 === code ? void 0 : "Install locally (npm i -D extension) or rely on npx; check network access to the npm registry."
|
|
4912
|
+
});
|
|
4913
|
+
const cacheFile = node_path.join(node_os.homedir(), ".cache", "extension-js", "templates-meta.json");
|
|
4914
|
+
const cacheExists = node_fs.existsSync(cacheFile);
|
|
4915
|
+
checks.push({
|
|
4916
|
+
check: "template-cache",
|
|
4917
|
+
status: cacheExists ? "pass" : "warn",
|
|
4918
|
+
detail: cacheExists ? `Template catalog cached at ${cacheFile}` : "Template catalog not cached yet (first extension_list_templates will fetch it)"
|
|
4919
|
+
});
|
|
4920
|
+
const healthy = checks.every((c)=>"fail" !== c.status);
|
|
4921
|
+
return JSON.stringify({
|
|
4922
|
+
mode: "environment",
|
|
4923
|
+
healthy,
|
|
4924
|
+
checks,
|
|
4925
|
+
hint: "Pass projectPath to diagnose a live dev session end-to-end."
|
|
4926
|
+
});
|
|
4927
|
+
}
|
|
4346
4928
|
async function doctor_handler(args) {
|
|
4347
|
-
|
|
4929
|
+
if (!args.projectPath) return environmentPreflight();
|
|
4930
|
+
const projectPath = args.projectPath;
|
|
4931
|
+
const { browser } = resolveSessionBrowser(projectPath, args.browser);
|
|
4348
4932
|
const { code, stdout, stderr } = await runExtensionCli([
|
|
4349
4933
|
"doctor",
|
|
4350
|
-
|
|
4934
|
+
projectPath,
|
|
4351
4935
|
"--browser",
|
|
4352
4936
|
browser,
|
|
4353
4937
|
"--output",
|
|
4354
4938
|
"json"
|
|
4355
4939
|
], {
|
|
4356
|
-
cwd:
|
|
4940
|
+
cwd: projectPath
|
|
4357
4941
|
});
|
|
4358
4942
|
const out = stdout.trim();
|
|
4359
4943
|
try {
|
|
@@ -4363,9 +4947,21 @@ async function doctor_handler(args) {
|
|
|
4363
4947
|
if ("string" == typeof check.detail) check.detail = toMcpSpeak(check.detail);
|
|
4364
4948
|
if ("string" == typeof check.remediation) check.remediation = toMcpSpeak(check.remediation);
|
|
4365
4949
|
}
|
|
4950
|
+
let healthy = 0 === code;
|
|
4951
|
+
const contract = doctor_readReadyContract(projectPath, browser);
|
|
4952
|
+
if (contract?.status === "error") {
|
|
4953
|
+
healthy = false;
|
|
4954
|
+
const detail = contract.errors && contract.errors.length ? contract.errors.join("; ") : contract.message || "The dev session recorded status: error in ready.json.";
|
|
4955
|
+
checks.push({
|
|
4956
|
+
check: "runtime-errors",
|
|
4957
|
+
status: "fail",
|
|
4958
|
+
detail: toMcpSpeak(detail),
|
|
4959
|
+
remediation: "The build or extension load failed. Fix the reported error, let the dev server recompile, then re-run doctor."
|
|
4960
|
+
});
|
|
4961
|
+
}
|
|
4366
4962
|
return JSON.stringify({
|
|
4367
4963
|
browser,
|
|
4368
|
-
healthy
|
|
4964
|
+
healthy,
|
|
4369
4965
|
checks
|
|
4370
4966
|
});
|
|
4371
4967
|
} catch {
|
|
@@ -4412,6 +5008,55 @@ function checkPrimitive(path, value, schema, issues) {
|
|
|
4412
5008
|
});
|
|
4413
5009
|
}
|
|
4414
5010
|
}
|
|
5011
|
+
const ARG_ALIASES = {
|
|
5012
|
+
projectPath: [
|
|
5013
|
+
"path",
|
|
5014
|
+
"dir",
|
|
5015
|
+
"projectDir",
|
|
5016
|
+
"cwd"
|
|
5017
|
+
],
|
|
5018
|
+
projectName: [
|
|
5019
|
+
"name"
|
|
5020
|
+
],
|
|
5021
|
+
parentDir: [
|
|
5022
|
+
"parent",
|
|
5023
|
+
"into"
|
|
5024
|
+
],
|
|
5025
|
+
slug: [
|
|
5026
|
+
"template",
|
|
5027
|
+
"templateSlug"
|
|
5028
|
+
],
|
|
5029
|
+
expression: [
|
|
5030
|
+
"code",
|
|
5031
|
+
"js",
|
|
5032
|
+
"script"
|
|
5033
|
+
],
|
|
5034
|
+
manifestPath: [
|
|
5035
|
+
"manifest"
|
|
5036
|
+
],
|
|
5037
|
+
surface: [
|
|
5038
|
+
"view"
|
|
5039
|
+
]
|
|
5040
|
+
};
|
|
5041
|
+
function normalizeArgAliases(inputSchema, args) {
|
|
5042
|
+
const schema = inputSchema;
|
|
5043
|
+
const props = schema.properties ?? {};
|
|
5044
|
+
const out = {
|
|
5045
|
+
...args
|
|
5046
|
+
};
|
|
5047
|
+
for (const [canonical, aliases] of Object.entries(ARG_ALIASES))if (canonical in props) {
|
|
5048
|
+
if (void 0 === out[canonical]) {
|
|
5049
|
+
for (const alias of aliases)if (!(alias in props)) {
|
|
5050
|
+
if (void 0 !== out[alias]) {
|
|
5051
|
+
out[canonical] = out[alias];
|
|
5052
|
+
delete out[alias];
|
|
5053
|
+
break;
|
|
5054
|
+
}
|
|
5055
|
+
}
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
5058
|
+
return out;
|
|
5059
|
+
}
|
|
4415
5060
|
function validateToolInput(inputSchema, args) {
|
|
4416
5061
|
const schema = inputSchema;
|
|
4417
5062
|
const issues = [];
|
|
@@ -4510,7 +5155,8 @@ async function startServer() {
|
|
|
4510
5155
|
],
|
|
4511
5156
|
isError: true
|
|
4512
5157
|
};
|
|
4513
|
-
const
|
|
5158
|
+
const normalizedArgs = normalizeArgAliases(tool.schema.inputSchema, args ?? {});
|
|
5159
|
+
const issues = validateToolInput(tool.schema.inputSchema, normalizedArgs);
|
|
4514
5160
|
if (issues.length) return {
|
|
4515
5161
|
content: [
|
|
4516
5162
|
{
|
|
@@ -4521,7 +5167,7 @@ async function startServer() {
|
|
|
4521
5167
|
isError: true
|
|
4522
5168
|
};
|
|
4523
5169
|
try {
|
|
4524
|
-
const result = await tool.handler(
|
|
5170
|
+
const result = await tool.handler(normalizedArgs);
|
|
4525
5171
|
return {
|
|
4526
5172
|
content: [
|
|
4527
5173
|
{
|
|
@@ -4599,6 +5245,31 @@ async function runCli(cmd, args) {
|
|
|
4599
5245
|
const apiBase = resolveApiBase(flag("api"));
|
|
4600
5246
|
try {
|
|
4601
5247
|
const config = await fetchLoginConfig(apiBase);
|
|
5248
|
+
if ("extensiondev" === config.provider) {
|
|
5249
|
+
const start = await requestDeviceCode({
|
|
5250
|
+
apiBase,
|
|
5251
|
+
path: config.deviceCodeUrl,
|
|
5252
|
+
project
|
|
5253
|
+
});
|
|
5254
|
+
log("");
|
|
5255
|
+
log(` Open ${start.verificationUri} and enter code: ${start.userCode}`);
|
|
5256
|
+
log("");
|
|
5257
|
+
log(" Waiting for authorization...");
|
|
5258
|
+
const poll = await pollDeviceToken({
|
|
5259
|
+
apiBase,
|
|
5260
|
+
path: config.deviceTokenUrl,
|
|
5261
|
+
project,
|
|
5262
|
+
deviceCode: start.deviceCode,
|
|
5263
|
+
interval: start.interval,
|
|
5264
|
+
budgetMs: 1000 * start.expiresIn
|
|
5265
|
+
});
|
|
5266
|
+
if (!poll.ok) {
|
|
5267
|
+
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.");
|
|
5268
|
+
return 1;
|
|
5269
|
+
}
|
|
5270
|
+
log(`Logged in to ${poll.creds.workspaceSlug}/${poll.creds.projectSlug}.`);
|
|
5271
|
+
return 0;
|
|
5272
|
+
}
|
|
4602
5273
|
const start = await startDeviceCode({
|
|
4603
5274
|
clientId: config.clientId,
|
|
4604
5275
|
scope: config.scope
|