@extension.dev/mcp 5.7.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +50 -4
- package/LICENSE +201 -21
- package/README.md +1 -1
- package/bin/extension-mcp.js +1 -1
- package/dist/module.js +494 -229
- package/dist/src/lib/carrier.d.ts +12 -0
- package/dist/src/lib/guest-load-oracle.d.ts +27 -0
- package/dist/src/lib/registry.d.ts +22 -4
- package/dist/src/lib/template-artifact-source.d.ts +4 -0
- package/dist/src/lib/urls-origins.d.ts +34 -0
- package/dist/src/lib/urls-paths.d.ts +56 -0
- package/dist/src/tools/open.d.ts +1 -0
- package/package.json +5 -3
- package/server.json +2 -2
package/dist/module.js
CHANGED
|
@@ -138,7 +138,8 @@ __webpack_require__.r(open_namespaceObject);
|
|
|
138
138
|
__webpack_require__.d(open_namespaceObject, {
|
|
139
139
|
clampPopupBounds: ()=>clampPopupBounds,
|
|
140
140
|
handler: ()=>open_handler,
|
|
141
|
-
schema: ()=>open_schema
|
|
141
|
+
schema: ()=>open_schema,
|
|
142
|
+
sessionIsHeadless: ()=>sessionIsHeadless
|
|
142
143
|
});
|
|
143
144
|
var preview_namespaceObject = {};
|
|
144
145
|
__webpack_require__.r(preview_namespaceObject);
|
|
@@ -221,7 +222,260 @@ __webpack_require__.d(whoami_namespaceObject, {
|
|
|
221
222
|
handler: ()=>whoami_handler,
|
|
222
223
|
schema: ()=>whoami_schema
|
|
223
224
|
});
|
|
224
|
-
var package_namespaceObject = JSON.parse('{"rE":"
|
|
225
|
+
var package_namespaceObject = JSON.parse('{"rE":"6.0.0","El":{"OP":"4.0.16-canary.1784889479.74e12044"}}');
|
|
226
|
+
function credentialsPath() {
|
|
227
|
+
if ("win32" === process.platform) {
|
|
228
|
+
const base = process.env.APPDATA || process.env.LOCALAPPDATA || node_path.join(node_os.homedir(), "AppData", "Roaming");
|
|
229
|
+
return node_path.join(base, "extension-dev", "auth.json");
|
|
230
|
+
}
|
|
231
|
+
const xdg = String(process.env.XDG_CONFIG_HOME || "").trim();
|
|
232
|
+
const base = xdg || node_path.join(node_os.homedir(), ".config");
|
|
233
|
+
return node_path.join(base, "extension-dev", "auth.json");
|
|
234
|
+
}
|
|
235
|
+
function readCredentials() {
|
|
236
|
+
try {
|
|
237
|
+
const raw = node_fs.readFileSync(credentialsPath(), "utf8");
|
|
238
|
+
const data = JSON.parse(raw);
|
|
239
|
+
if (!data || "object" != typeof data) return null;
|
|
240
|
+
if (1 !== data.version) return null;
|
|
241
|
+
const token = String(data.token || "").trim();
|
|
242
|
+
if (!token) return null;
|
|
243
|
+
const provider = "extensiondev" === data.provider || "github" === data.provider ? data.provider : void 0;
|
|
244
|
+
return {
|
|
245
|
+
version: 1,
|
|
246
|
+
token,
|
|
247
|
+
workspaceSlug: String(data.workspaceSlug || ""),
|
|
248
|
+
projectSlug: String(data.projectSlug || ""),
|
|
249
|
+
expiresAt: Number(data.expiresAt || 0),
|
|
250
|
+
api: String(data.api || ""),
|
|
251
|
+
...provider ? {
|
|
252
|
+
provider
|
|
253
|
+
} : {}
|
|
254
|
+
};
|
|
255
|
+
} catch {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function writeCredentials(creds) {
|
|
260
|
+
const file = credentialsPath();
|
|
261
|
+
const dir = node_path.dirname(file);
|
|
262
|
+
node_fs.mkdirSync(dir, {
|
|
263
|
+
recursive: true,
|
|
264
|
+
mode: 448
|
|
265
|
+
});
|
|
266
|
+
try {
|
|
267
|
+
node_fs.chmodSync(dir, 448);
|
|
268
|
+
} catch {}
|
|
269
|
+
node_fs.writeFileSync(file, JSON.stringify(creds, null, 2) + "\n", {
|
|
270
|
+
mode: 384
|
|
271
|
+
});
|
|
272
|
+
try {
|
|
273
|
+
node_fs.chmodSync(file, 384);
|
|
274
|
+
} catch {}
|
|
275
|
+
return file;
|
|
276
|
+
}
|
|
277
|
+
function clearCredentials() {
|
|
278
|
+
const file = credentialsPath();
|
|
279
|
+
try {
|
|
280
|
+
node_fs.unlinkSync(file);
|
|
281
|
+
return {
|
|
282
|
+
cleared: true,
|
|
283
|
+
path: file
|
|
284
|
+
};
|
|
285
|
+
} catch {
|
|
286
|
+
return {
|
|
287
|
+
cleared: false,
|
|
288
|
+
path: file
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function readValidCredentials(nowSeconds = Math.floor(Date.now() / 1000)) {
|
|
293
|
+
const creds = readCredentials();
|
|
294
|
+
if (!creds) return null;
|
|
295
|
+
if (creds.expiresAt && creds.expiresAt <= nowSeconds) return null;
|
|
296
|
+
return creds;
|
|
297
|
+
}
|
|
298
|
+
const PROD_ORIGINS = {
|
|
299
|
+
www: "https://www.extension.dev",
|
|
300
|
+
console: "https://console.extension.dev",
|
|
301
|
+
inspect: "https://inspect.extension.dev",
|
|
302
|
+
templates: "https://templates.extension.dev",
|
|
303
|
+
intelligence: "https://intelligence.extension.dev",
|
|
304
|
+
registry: "https://registry.extension.land",
|
|
305
|
+
media: "https://media.extension.land"
|
|
306
|
+
};
|
|
307
|
+
const DEV_LOCALHOST_ORIGINS = {
|
|
308
|
+
www: "http://localhost:3100",
|
|
309
|
+
console: "http://console.extension.localhost",
|
|
310
|
+
inspect: "http://inspect.extension.localhost",
|
|
311
|
+
templates: "http://templates.extension.localhost",
|
|
312
|
+
intelligence: "http://intelligence.extension.localhost",
|
|
313
|
+
registry: "https://registry.extension.land",
|
|
314
|
+
media: "https://media.extension.land"
|
|
315
|
+
};
|
|
316
|
+
function strip(value) {
|
|
317
|
+
return String(value ?? "").trim().replace(/\/+$/, "");
|
|
318
|
+
}
|
|
319
|
+
function isLocalOrigin(url) {
|
|
320
|
+
const raw = strip(url);
|
|
321
|
+
if (!raw) return false;
|
|
322
|
+
let host;
|
|
323
|
+
try {
|
|
324
|
+
host = new URL(raw).hostname;
|
|
325
|
+
} catch {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
return "localhost" === host || "127.0.0.1" === host || "::1" === host || "[::1]" === host || "extension.localhost" === host || host.endsWith(".extension.localhost");
|
|
329
|
+
}
|
|
330
|
+
function resolveOrigins(overrides = {}, opts = {}) {
|
|
331
|
+
const devLike = isLocalOrigin(overrides.www) || isLocalOrigin(overrides.console) || isLocalOrigin(opts.hint);
|
|
332
|
+
const base = devLike ? DEV_LOCALHOST_ORIGINS : PROD_ORIGINS;
|
|
333
|
+
const pick = (key)=>strip(overrides[key]) || base[key];
|
|
334
|
+
return {
|
|
335
|
+
www: pick("www"),
|
|
336
|
+
console: pick("console"),
|
|
337
|
+
inspect: pick("inspect"),
|
|
338
|
+
templates: pick("templates"),
|
|
339
|
+
intelligence: pick("intelligence"),
|
|
340
|
+
registry: pick("registry"),
|
|
341
|
+
media: pick("media")
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
const seg = (value)=>encodeURIComponent(String(value));
|
|
345
|
+
function urls_paths_join(base, sub) {
|
|
346
|
+
if (!sub) return base;
|
|
347
|
+
return `${base}/${sub.replace(/^\/+/, "")}`;
|
|
348
|
+
}
|
|
349
|
+
function consoleProjectPath(ref, page = "") {
|
|
350
|
+
return urls_paths_join(`/${seg(ref.workspace)}/${seg(ref.project)}`, page);
|
|
351
|
+
}
|
|
352
|
+
function withQuery(path, query) {
|
|
353
|
+
if (!query) return path;
|
|
354
|
+
const params = new URLSearchParams();
|
|
355
|
+
for (const [key, value] of Object.entries(query))if (null != value && "" !== value) params.set(key, String(value));
|
|
356
|
+
const qs = params.toString();
|
|
357
|
+
return qs ? `${path}?${qs}` : path;
|
|
358
|
+
}
|
|
359
|
+
function wwwNewPath(query) {
|
|
360
|
+
return withQuery("/new", query);
|
|
361
|
+
}
|
|
362
|
+
PROD_ORIGINS.registry;
|
|
363
|
+
function mcpOrigins(apiHint) {
|
|
364
|
+
const www = String(apiHint || process.env.EXTENSION_DEV_API_URL || "").trim() || void 0;
|
|
365
|
+
return resolveOrigins({
|
|
366
|
+
www,
|
|
367
|
+
console: process.env.EXTENSION_DEV_CONSOLE_URL,
|
|
368
|
+
inspect: process.env.EXTENSION_DEV_INSPECT_URL,
|
|
369
|
+
registry: process.env.EXTENSION_DEV_REGISTRY_URL,
|
|
370
|
+
media: process.env.EXTENSION_MEDIA_ORIGIN
|
|
371
|
+
}, {
|
|
372
|
+
hint: www
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
function consoleBase(apiHint) {
|
|
376
|
+
return mcpOrigins(apiHint).console;
|
|
377
|
+
}
|
|
378
|
+
function registryBase() {
|
|
379
|
+
return mcpOrigins().registry;
|
|
380
|
+
}
|
|
381
|
+
function resolveProjectRef(overrides) {
|
|
382
|
+
const workspace = String(overrides?.workspace || "").trim();
|
|
383
|
+
const project = String(overrides?.project || "").trim();
|
|
384
|
+
if (workspace && project) return {
|
|
385
|
+
workspace,
|
|
386
|
+
project
|
|
387
|
+
};
|
|
388
|
+
const creds = readCredentials();
|
|
389
|
+
const ws = workspace || String(creds?.workspaceSlug || "").trim();
|
|
390
|
+
const proj = project || String(creds?.projectSlug || "").trim();
|
|
391
|
+
if (!ws || !proj) return null;
|
|
392
|
+
return {
|
|
393
|
+
workspace: ws,
|
|
394
|
+
project: proj
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function registryFileUrl(ref, file) {
|
|
398
|
+
return `${registryBase()}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/_extension-dev/${file}`;
|
|
399
|
+
}
|
|
400
|
+
function consoleProjectUrl(ref, page, apiHint) {
|
|
401
|
+
const base = consoleBase(apiHint);
|
|
402
|
+
if (!ref) return base;
|
|
403
|
+
return `${base}${consoleProjectPath(ref, page)}`;
|
|
404
|
+
}
|
|
405
|
+
async function fetchRegistryJson(url, fetchImpl = fetch) {
|
|
406
|
+
let res;
|
|
407
|
+
try {
|
|
408
|
+
res = await fetchImpl(url);
|
|
409
|
+
} catch (err) {
|
|
410
|
+
return {
|
|
411
|
+
ok: false,
|
|
412
|
+
message: `Could not reach ${url}: ${err?.message || err}`
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
if (!res.ok) return {
|
|
416
|
+
ok: false,
|
|
417
|
+
status: res.status,
|
|
418
|
+
message: `${url} returned ${res.status}`
|
|
419
|
+
};
|
|
420
|
+
try {
|
|
421
|
+
const text = await res.text();
|
|
422
|
+
return {
|
|
423
|
+
ok: true,
|
|
424
|
+
json: JSON.parse(text)
|
|
425
|
+
};
|
|
426
|
+
} catch {
|
|
427
|
+
return {
|
|
428
|
+
ok: false,
|
|
429
|
+
message: `${url} did not return valid JSON`
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
function parseChannels(json) {
|
|
434
|
+
if (!json || "object" != typeof json || Array.isArray(json)) return [];
|
|
435
|
+
const out = [];
|
|
436
|
+
for (const [channel, raw] of Object.entries(json)){
|
|
437
|
+
if (!raw || "object" != typeof raw) continue;
|
|
438
|
+
const row = raw;
|
|
439
|
+
const description = "string" == typeof row.description ? row.description : void 0;
|
|
440
|
+
const promotedAtField = "string" == typeof row.promotedAt && row.promotedAt ? row.promotedAt : void 0;
|
|
441
|
+
const fromDescription = description?.match(/\bon (\d{4}-\d{2}-\d{2}T[0-9:.]+Z?)/)?.[1];
|
|
442
|
+
const entry = {
|
|
443
|
+
channel,
|
|
444
|
+
sha: String(row.sha ?? "")
|
|
445
|
+
};
|
|
446
|
+
if (row.buildId) entry.buildId = String(row.buildId);
|
|
447
|
+
if (row.version) entry.version = String(row.version);
|
|
448
|
+
const promotedAt = promotedAtField || fromDescription;
|
|
449
|
+
if (promotedAt) entry.promotedAt = promotedAt;
|
|
450
|
+
if (description) entry.description = description;
|
|
451
|
+
out.push(entry);
|
|
452
|
+
}
|
|
453
|
+
return out;
|
|
454
|
+
}
|
|
455
|
+
function parseBuildIndex(json) {
|
|
456
|
+
const items = json?.items;
|
|
457
|
+
if (!Array.isArray(items)) return [];
|
|
458
|
+
const out = [];
|
|
459
|
+
for (const raw of items){
|
|
460
|
+
if (!raw || "object" != typeof raw) continue;
|
|
461
|
+
const row = raw;
|
|
462
|
+
const sha = String(row.shortSha ?? row.sha ?? row.id ?? row.buildId ?? "").trim();
|
|
463
|
+
if (!sha) continue;
|
|
464
|
+
const entry = {
|
|
465
|
+
sha
|
|
466
|
+
};
|
|
467
|
+
if (row.commit) entry.commit = String(row.commit);
|
|
468
|
+
if (row.channel) entry.channel = String(row.channel);
|
|
469
|
+
if (row.buildEnv) entry.buildEnv = String(row.buildEnv);
|
|
470
|
+
if (row.status) entry.status = String(row.status);
|
|
471
|
+
if (row.version) entry.version = String(row.version);
|
|
472
|
+
if ("string" == typeof row.message) entry.message = row.message.split("\n", 1)[0];
|
|
473
|
+
if (row.timestamp) entry.timestamp = String(row.timestamp);
|
|
474
|
+
if (Array.isArray(row.browsers)) entry.browsers = row.browsers.map((b)=>String(b)).filter(Boolean);
|
|
475
|
+
out.push(entry);
|
|
476
|
+
}
|
|
477
|
+
return out;
|
|
478
|
+
}
|
|
225
479
|
function scaffoldEnginePin(projectPath) {
|
|
226
480
|
try {
|
|
227
481
|
const pkg = JSON.parse(node_fs.readFileSync(node_path.join(projectPath, "package.json"), "utf8"));
|
|
@@ -358,6 +612,10 @@ async function create_handler(args) {
|
|
|
358
612
|
const engineWarning = pin && "latest" !== pin && !pinMatches ? `The scaffold pins "extension": "${scaffoldPin ?? "unknown"}"; the project-local engine wins over EXTENSION_MCP_CLI_VERSION=${pin}. Run \`(cd ${result.projectPath} && ${addDev(`extension@${pin}`)})\` to match the pinned engine.` : void 0;
|
|
359
613
|
const resolvedParent = args.parentDir ? node_path.resolve(args.parentDir) : process.cwd();
|
|
360
614
|
const gitInit = node_fs.existsSync(node_path.join(result.projectPath, ".git"));
|
|
615
|
+
const wwwOrigin = mcpOrigins().www;
|
|
616
|
+
const deployUrl = `${wwwOrigin}${wwwNewPath({
|
|
617
|
+
template: result.template
|
|
618
|
+
})}`;
|
|
361
619
|
return JSON.stringify({
|
|
362
620
|
resolvedPath: result.projectPath,
|
|
363
621
|
projectPath: result.projectPath,
|
|
@@ -365,20 +623,27 @@ async function create_handler(args) {
|
|
|
365
623
|
template: result.template,
|
|
366
624
|
depsInstalled: result.depsInstalled,
|
|
367
625
|
packageManager: result.depsInstalled ? packageManager : null,
|
|
626
|
+
deployUrl,
|
|
368
627
|
defaultsApplied: {
|
|
369
628
|
parentDir: args.parentDir ? `${resolvedParent} (explicit)` : `${resolvedParent} (default: the MCP server process cwd, not yours; pass parentDir to choose)`,
|
|
629
|
+
...void 0 === args.template ? {
|
|
630
|
+
template: "typescript (default; run extension_list_templates to pick another, e.g. javascript for plain JS)"
|
|
631
|
+
} : {},
|
|
370
632
|
packageManager: `${packageManager} (auto-detected by the scaffolder, not asked)`,
|
|
371
633
|
browser: "chrome (default: extension_dev and extension_build target chrome unless you pass browser)",
|
|
372
634
|
gitInit
|
|
373
635
|
},
|
|
374
636
|
duration: Date.now() - start,
|
|
375
|
-
nextSteps:
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
637
|
+
nextSteps: [
|
|
638
|
+
...result.depsInstalled ? [
|
|
639
|
+
`cd ${result.projectPath}`,
|
|
640
|
+
runDev
|
|
641
|
+
] : [
|
|
642
|
+
`cd ${result.projectPath}`,
|
|
643
|
+
`${packageManager} install`,
|
|
644
|
+
runDev
|
|
645
|
+
],
|
|
646
|
+
`To ship: extension_create scaffolds and runs locally, it does not host. Open ${deployUrl} to deploy this template to the web.`
|
|
382
647
|
],
|
|
383
648
|
...engineWarning ? {
|
|
384
649
|
engineWarning
|
|
@@ -388,10 +653,96 @@ async function create_handler(args) {
|
|
|
388
653
|
}
|
|
389
654
|
});
|
|
390
655
|
}
|
|
656
|
+
const DEFAULT_MEDIA_ORIGIN = "https://media.extension.land";
|
|
657
|
+
const DEFAULT_CHANNEL = "latest";
|
|
658
|
+
const PINNED_COMMIT = "2d2ed9668cca002148d9eecd953a08b54d0bad9d";
|
|
659
|
+
const CHANNEL_CACHE_TTL_MS = 300000;
|
|
660
|
+
function mediaOrigin() {
|
|
661
|
+
return (process.env.EXTENSION_MEDIA_ORIGIN || "").trim() || DEFAULT_MEDIA_ORIGIN;
|
|
662
|
+
}
|
|
663
|
+
function channelName() {
|
|
664
|
+
return (process.env.EXTENSION_TEMPLATES_CHANNEL || "").trim() || DEFAULT_CHANNEL;
|
|
665
|
+
}
|
|
666
|
+
function pinnedCommitOverride() {
|
|
667
|
+
return (process.env.EXTENSION_TEMPLATES_COMMIT || "").trim();
|
|
668
|
+
}
|
|
669
|
+
function rawBaseForCommit(commit) {
|
|
670
|
+
return `https://raw.githubusercontent.com/extension-js/examples/${commit}`;
|
|
671
|
+
}
|
|
672
|
+
let releaseCache = null;
|
|
673
|
+
let releaseCacheExpiresAt = 0;
|
|
674
|
+
let releaseRequest = null;
|
|
675
|
+
function releaseForCommit(origin, commit) {
|
|
676
|
+
return {
|
|
677
|
+
commit,
|
|
678
|
+
metaUrl: `${origin}/templates/${commit}/templates-meta.json`,
|
|
679
|
+
filesBaseUrl: `${origin}/templates/${commit}/files`
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
async function resolveRelease() {
|
|
683
|
+
const now = Date.now();
|
|
684
|
+
if (releaseCache && now < releaseCacheExpiresAt) return releaseCache;
|
|
685
|
+
if (releaseRequest) return releaseRequest;
|
|
686
|
+
const origin = mediaOrigin();
|
|
687
|
+
const override = pinnedCommitOverride();
|
|
688
|
+
if (override) {
|
|
689
|
+
releaseCache = releaseForCommit(origin, override);
|
|
690
|
+
releaseCacheExpiresAt = now + CHANNEL_CACHE_TTL_MS;
|
|
691
|
+
return releaseCache;
|
|
692
|
+
}
|
|
693
|
+
releaseRequest = (async ()=>{
|
|
694
|
+
try {
|
|
695
|
+
const pointerUrl = `${origin}/templates/${channelName()}.json`;
|
|
696
|
+
const response = await fetch(pointerUrl, {
|
|
697
|
+
headers: {
|
|
698
|
+
Accept: "application/json"
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
if (!response.ok) return null;
|
|
702
|
+
const pointer = await response.json();
|
|
703
|
+
const commit = String(pointer?.commit || "").trim();
|
|
704
|
+
if (!commit) return null;
|
|
705
|
+
releaseCache = releaseForCommit(origin, commit);
|
|
706
|
+
releaseCacheExpiresAt = Date.now() + CHANNEL_CACHE_TTL_MS;
|
|
707
|
+
return releaseCache;
|
|
708
|
+
} catch {
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
})();
|
|
712
|
+
try {
|
|
713
|
+
return await releaseRequest;
|
|
714
|
+
} finally{
|
|
715
|
+
releaseRequest = null;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
async function templateMetaUrls() {
|
|
719
|
+
const urls = [];
|
|
720
|
+
const release = await resolveRelease();
|
|
721
|
+
if (release) urls.push(release.metaUrl);
|
|
722
|
+
urls.push(`${rawBaseForCommit(PINNED_COMMIT)}/templates-meta.json`);
|
|
723
|
+
return urls;
|
|
724
|
+
}
|
|
725
|
+
function stripTemplatePathPrefix(slug, relativePath) {
|
|
726
|
+
for (const dir of [
|
|
727
|
+
"public",
|
|
728
|
+
"examples"
|
|
729
|
+
]){
|
|
730
|
+
const prefix = `${dir}/${slug}/`;
|
|
731
|
+
if (relativePath.startsWith(prefix)) return relativePath.slice(prefix.length);
|
|
732
|
+
}
|
|
733
|
+
return relativePath;
|
|
734
|
+
}
|
|
735
|
+
async function templateFileUrls(slug, relativePath) {
|
|
736
|
+
const relative = stripTemplatePathPrefix(slug, relativePath);
|
|
737
|
+
const urls = [];
|
|
738
|
+
const release = await resolveRelease();
|
|
739
|
+
if (release) urls.push(`${release.filesBaseUrl}/${slug}/${relative}`);
|
|
740
|
+
urls.push(`${rawBaseForCommit(PINNED_COMMIT)}/examples/${slug}/${relative}`);
|
|
741
|
+
return urls;
|
|
742
|
+
}
|
|
391
743
|
const CACHE_DIR = node_path.join(node_os.homedir(), ".cache", "extension-js");
|
|
392
744
|
const CACHE_FILE = node_path.join(CACHE_DIR, "templates-meta.json");
|
|
393
745
|
const CACHE_TTL_MS = 3600000;
|
|
394
|
-
const TEMPLATES_META_URL = "https://github.com/extension-js/examples/releases/download/nightly/templates-meta.json";
|
|
395
746
|
function isCacheValid() {
|
|
396
747
|
try {
|
|
397
748
|
const stat = node_fs.statSync(CACHE_FILE);
|
|
@@ -405,17 +756,22 @@ async function fetchTemplatesMeta() {
|
|
|
405
756
|
const cached = JSON.parse(node_fs.readFileSync(CACHE_FILE, "utf8"));
|
|
406
757
|
return cached;
|
|
407
758
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
759
|
+
let lastStatus = 0;
|
|
760
|
+
for (const url of (await templateMetaUrls()))try {
|
|
761
|
+
const response = await fetch(url);
|
|
762
|
+
if (!response.ok) {
|
|
763
|
+
lastStatus = response.status;
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
const data = await response.json();
|
|
767
|
+
node_fs.mkdirSync(CACHE_DIR, {
|
|
768
|
+
recursive: true
|
|
769
|
+
});
|
|
770
|
+
node_fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2));
|
|
771
|
+
return data;
|
|
772
|
+
} catch {}
|
|
773
|
+
if (node_fs.existsSync(CACHE_FILE)) return JSON.parse(node_fs.readFileSync(CACHE_FILE, "utf8"));
|
|
774
|
+
throw new Error(`Failed to fetch templates-meta.json (${lastStatus || "network error"}).`);
|
|
419
775
|
}
|
|
420
776
|
async function listTemplates(filters) {
|
|
421
777
|
const meta = await fetchTemplatesMeta();
|
|
@@ -525,7 +881,8 @@ async function list_templates_handler(args) {
|
|
|
525
881
|
const results = templates.map((t)=>({
|
|
526
882
|
slug: t.slug,
|
|
527
883
|
description: t.description,
|
|
528
|
-
uiFramework: t.uiFramework
|
|
884
|
+
uiFramework: t.uiFramework,
|
|
885
|
+
frameworkLabel: t.uiFramework || "vanilla",
|
|
529
886
|
surfaces: t.surfaces,
|
|
530
887
|
tags: t.tags,
|
|
531
888
|
difficulty: t.difficulty,
|
|
@@ -883,6 +1240,7 @@ function isGeckoFamily(browser) {
|
|
|
883
1240
|
return GECKO_FAMILY.has(browser);
|
|
884
1241
|
}
|
|
885
1242
|
const CARRIER_DIR_NAME = "extension-dev-live-preview";
|
|
1243
|
+
const CARRIER_EXTENSION_ID = "ibppeifnekhjjjmpjfiobccjlicbmgcb";
|
|
886
1244
|
const MARKER_FILE = "managed-by-extension-dev-mcp.json";
|
|
887
1245
|
function deriveCarrierId(source) {
|
|
888
1246
|
try {
|
|
@@ -1005,6 +1363,7 @@ function materializeCarrier(projectPath, browser) {
|
|
|
1005
1363
|
"Bridged calls run under the CARRIER's identity, not your extension's. The preview assumes a single active guest and does not namespace per-extension state, so storage, action/badge state, messaging delivery, offscreen documents and relative script paths belong to the carrier. Rows affected are badged carrier-scoped in the Trace tab.",
|
|
1006
1364
|
"Chromium-family only: Firefox has no externally_connectable channel for web pages."
|
|
1007
1365
|
],
|
|
1366
|
+
graduation: "The carrier lane is the SHARED real lane: bridged calls run as the carrier, by design (see limitations). Your guest is already loaded as ITSELF in this same session, so for its own storage, identity, badge and messaging (the isolated real thing), drive the guest directly instead of the carrier bridge: extension_storage, extension_eval and extension_dom_inspect against this projectPath all operate on the guest as itself. Start (or replace) this session with allowControl: true (or allowEval: true) to unlock them. Use the carrier bridge for the shared real-lane TRACE; use the control verbs for the guest's OWN state.",
|
|
1008
1367
|
...carrierId ? {
|
|
1009
1368
|
bridgeProtocol: {
|
|
1010
1369
|
carrierExtensionId: carrierId,
|
|
@@ -1751,6 +2110,7 @@ async function dev_handler(args) {
|
|
|
1751
2110
|
child.on("exit", ()=>removeSession(args.projectPath, browser));
|
|
1752
2111
|
await new Promise((resolve)=>setTimeout(resolve, 3000));
|
|
1753
2112
|
const earlyOutput = spawned.readOutput();
|
|
2113
|
+
const cleanOutput = denoiseEarlyOutput(earlyOutput);
|
|
1754
2114
|
if (null !== child.exitCode || null !== child.signalCode) {
|
|
1755
2115
|
const code = child.exitCode;
|
|
1756
2116
|
const signal = child.signalCode;
|
|
@@ -1763,12 +2123,12 @@ async function dev_handler(args) {
|
|
|
1763
2123
|
exitCode: code,
|
|
1764
2124
|
signal,
|
|
1765
2125
|
error: `The dev server exited during startup (${signal ? `signal ${signal}` : `exit code ${code}`}). No session is running, so extension_logs/wait/eval and the control verbs have nothing to attach to.`,
|
|
1766
|
-
output:
|
|
2126
|
+
output: cleanOutput.slice(0, 2000),
|
|
1767
2127
|
logPath,
|
|
1768
2128
|
hint: "Read `output` above for the cause: a port already in use, a manifest the build rejects, or a missing browser binary are the common ones. Fix it and call extension_dev again; extension_doctor with this projectPath will also report what the last session recorded."
|
|
1769
2129
|
});
|
|
1770
2130
|
}
|
|
1771
|
-
const compileFailed = /compiled with errors|✖✖✖|ERROR in |Module not found|NOT FOUND/i.test(
|
|
2131
|
+
const compileFailed = /compiled with errors|✖✖✖|ERROR in |Module not found|NOT FOUND/i.test(cleanOutput);
|
|
1772
2132
|
if (compileFailed) return JSON.stringify({
|
|
1773
2133
|
ok: false,
|
|
1774
2134
|
status: "compile-failed",
|
|
@@ -1776,12 +2136,12 @@ async function dev_handler(args) {
|
|
|
1776
2136
|
browser,
|
|
1777
2137
|
pid,
|
|
1778
2138
|
error: "The dev server started but the FIRST COMPILE FAILED, so the browser has nothing usable to load. The session is running; the extension is not.",
|
|
1779
|
-
output:
|
|
2139
|
+
output: cleanOutput.slice(0, 2000),
|
|
1780
2140
|
logPath,
|
|
1781
2141
|
hint: "Fix the compile error in `output` above and save: the dev server is still running and will recompile. Do not call extension_wait yet, it will report ready for a build that failed."
|
|
1782
2142
|
});
|
|
1783
2143
|
const exitStamp = args.noBrowser ? null : browserExitStamp(args.projectPath, browser, spawnedAt);
|
|
1784
|
-
const profileLockHit = !args.noBrowser && /SingletonLock|ProcessSingleton|profile[^\n]*(in use|locked)|already (open|running)/i.test(
|
|
2144
|
+
const profileLockHit = !args.noBrowser && /SingletonLock|ProcessSingleton|profile[^\n]*(in use|locked)|already (open|running)/i.test(cleanOutput);
|
|
1785
2145
|
if (exitStamp || profileLockHit) {
|
|
1786
2146
|
const profileDir = node_path.join(args.projectPath, "dist", `extension-profile-${browser}`);
|
|
1787
2147
|
return JSON.stringify({
|
|
@@ -1792,7 +2152,7 @@ async function dev_handler(args) {
|
|
|
1792
2152
|
pid,
|
|
1793
2153
|
...exitStamp ?? {},
|
|
1794
2154
|
error: `The dev server is running but the ${browser} browser it launched died during startup` + (profileLockHit ? " because its profile is locked by another browser instance." : "."),
|
|
1795
|
-
output:
|
|
2155
|
+
output: cleanOutput.slice(0, 2000),
|
|
1796
2156
|
logPath,
|
|
1797
2157
|
hint: `A locked profile means another session's browser still holds it: call extension_stop with this projectPath to kill that session, then start extension_dev again. If the lock survives a crash, remove ${profileDir} manually before retrying.`
|
|
1798
2158
|
});
|
|
@@ -1840,7 +2200,7 @@ async function dev_handler(args) {
|
|
|
1840
2200
|
} : {},
|
|
1841
2201
|
capabilities,
|
|
1842
2202
|
hint: args.noBrowser ? "Build-only session (noBrowser: true): no browser will launch, so no runtime will ever attach. extension_wait returns as soon as the first compile lands (compiled: true, browserAttached: false) instead of waiting out its budget; do not wait for a browser. The control verbs (storage/reload/open/dom_inspect/eval) need a live browser and will not work against this session. When you are done, call extension_stop to shut down the dev server." : "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). To unlock them, call extension_dev again with the flag you need plus replace: true (it stops this session first); a plain second call is refused so the session does not fork.") + " When you are done, call extension_stop to shut down the dev server and browser.",
|
|
1843
|
-
earlyOutput:
|
|
2203
|
+
earlyOutput: cleanOutput.slice(0, 500),
|
|
1844
2204
|
logPath
|
|
1845
2205
|
});
|
|
1846
2206
|
}
|
|
@@ -1849,6 +2209,8 @@ function denoiseEarlyOutput(raw) {
|
|
|
1849
2209
|
/^npm warn Unknown project config/i,
|
|
1850
2210
|
/This will stop working in the next major version of npm/i,
|
|
1851
2211
|
/^npm warn config/i,
|
|
2212
|
+
/^npm warn exec/i,
|
|
2213
|
+
/The following package(s)? (was|were) not found and will be installed/i,
|
|
1852
2214
|
/V8: .*Invalid asm\.js/i,
|
|
1853
2215
|
/^\(node:\d+\) V8:/i,
|
|
1854
2216
|
/Use `node --trace-warnings/i,
|
|
@@ -2092,7 +2454,6 @@ async function preview_handler(args) {
|
|
|
2092
2454
|
logPath
|
|
2093
2455
|
});
|
|
2094
2456
|
}
|
|
2095
|
-
const RAW_BASE = "https://raw.githubusercontent.com/extension-js/examples/main/examples";
|
|
2096
2457
|
const get_template_source_schema = {
|
|
2097
2458
|
name: "extension_get_template_source",
|
|
2098
2459
|
description: "Read source files from a template in the extension.dev template catalog. Use this to learn implementation patterns before building something similar.",
|
|
@@ -2134,20 +2495,23 @@ async function get_template_source_handler(args) {
|
|
|
2134
2495
|
};
|
|
2135
2496
|
if (!args.files?.length) return JSON.stringify({
|
|
2136
2497
|
...meta,
|
|
2137
|
-
files: template.files,
|
|
2498
|
+
files: template.files.map((f)=>stripTemplatePathPrefix(template.slug, f)),
|
|
2138
2499
|
hint: "Pass specific file paths in the files parameter to read their contents."
|
|
2139
2500
|
});
|
|
2140
2501
|
const fileContents = {};
|
|
2141
2502
|
const errors = [];
|
|
2142
2503
|
await Promise.all(args.files.map(async (filePath)=>{
|
|
2143
|
-
const
|
|
2144
|
-
|
|
2504
|
+
const urls = await templateFileUrls(args.slug, filePath);
|
|
2505
|
+
let lastStatus = 0;
|
|
2506
|
+
for (const url of urls)try {
|
|
2145
2507
|
const response = await fetch(url);
|
|
2146
|
-
if (response.ok)
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2508
|
+
if (response.ok) {
|
|
2509
|
+
fileContents[filePath] = await response.text();
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
lastStatus = response.status;
|
|
2513
|
+
} catch {}
|
|
2514
|
+
errors.push(`${filePath}: ${lastStatus || "fetch failed"}`);
|
|
2151
2515
|
}));
|
|
2152
2516
|
return JSON.stringify({
|
|
2153
2517
|
...meta,
|
|
@@ -5303,6 +5667,11 @@ const open_schema = {
|
|
|
5303
5667
|
]
|
|
5304
5668
|
}
|
|
5305
5669
|
};
|
|
5670
|
+
function sessionIsHeadless() {
|
|
5671
|
+
if (/^(1|true)$/i.test(process.env.EXTENSION_HEADLESS ?? "")) return true;
|
|
5672
|
+
return /(^|\s|=)-{1,2}headless\b/i.test(process.env.EXTENSION_BROWSER_FLAGS ?? "");
|
|
5673
|
+
}
|
|
5674
|
+
const HEADED_RELAUNCH = "start a headed session: extension_dev with replace: true, and in the environment set EXTENSION_HEADLESS=0 AND clear EXTENSION_BROWSER_FLAGS (it may carry --headless=new, which keeps the window hidden even with EXTENSION_HEADLESS=0)";
|
|
5306
5675
|
async function open_handler(args) {
|
|
5307
5676
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
5308
5677
|
if (args.url) return navigateToUrl(args.projectPath, browser, args.url, args.timeout);
|
|
@@ -5347,7 +5716,7 @@ async function open_handler(args) {
|
|
|
5347
5716
|
cli.push("--browser", browser);
|
|
5348
5717
|
if (null != args.timeout) cli.push("--timeout", String(args.timeout));
|
|
5349
5718
|
const raw = await runActVerb(cli, args.projectPath, args.timeout);
|
|
5350
|
-
const headless =
|
|
5719
|
+
const headless = sessionIsHeadless();
|
|
5351
5720
|
if (headless && [
|
|
5352
5721
|
"popup",
|
|
5353
5722
|
"action",
|
|
@@ -5361,12 +5730,12 @@ async function open_handler(args) {
|
|
|
5361
5730
|
try {
|
|
5362
5731
|
const parsedFallback = JSON.parse(fallback);
|
|
5363
5732
|
if (parsedFallback?.ok) {
|
|
5364
|
-
parsedFallback.note =
|
|
5733
|
+
parsedFallback.note = `The dev browser is headless, and a real popup/sidebar window can only open in a headed session, so the surface was rendered as a tab instead. For the real window, ${HEADED_RELAUNCH}, then open the surface again without asTab.`;
|
|
5365
5734
|
return JSON.stringify(parsedFallback);
|
|
5366
5735
|
}
|
|
5367
5736
|
} catch {}
|
|
5368
5737
|
}
|
|
5369
|
-
if (!parsed.hint) parsed.hint = /user gesture/i.test(msg) ? "This surface can only open from a real user gesture, which headless automation cannot produce. Retry with asTab: true to render the surface document in a tab instead." :
|
|
5738
|
+
if (!parsed.hint) parsed.hint = /user gesture/i.test(msg) ? "This surface can only open from a real user gesture, which headless automation cannot produce. Retry with asTab: true to render the surface document in a tab instead." : `The dev browser is running headless, and a popup/sidebar window needs a headed session. Retry with asTab: true to render the surface document in a tab, or for the real window, ${HEADED_RELAUNCH}.`;
|
|
5370
5739
|
return JSON.stringify(parsed);
|
|
5371
5740
|
}
|
|
5372
5741
|
} catch {}
|
|
@@ -5680,187 +6049,12 @@ async function dom_inspect_handler(args) {
|
|
|
5680
6049
|
return raw;
|
|
5681
6050
|
}
|
|
5682
6051
|
}
|
|
5683
|
-
|
|
5684
|
-
if ("win32" === process.platform) {
|
|
5685
|
-
const base = process.env.APPDATA || process.env.LOCALAPPDATA || node_path.join(node_os.homedir(), "AppData", "Roaming");
|
|
5686
|
-
return node_path.join(base, "extension-dev", "auth.json");
|
|
5687
|
-
}
|
|
5688
|
-
const xdg = String(process.env.XDG_CONFIG_HOME || "").trim();
|
|
5689
|
-
const base = xdg || node_path.join(node_os.homedir(), ".config");
|
|
5690
|
-
return node_path.join(base, "extension-dev", "auth.json");
|
|
5691
|
-
}
|
|
5692
|
-
function readCredentials() {
|
|
5693
|
-
try {
|
|
5694
|
-
const raw = node_fs.readFileSync(credentialsPath(), "utf8");
|
|
5695
|
-
const data = JSON.parse(raw);
|
|
5696
|
-
if (!data || "object" != typeof data) return null;
|
|
5697
|
-
if (1 !== data.version) return null;
|
|
5698
|
-
const token = String(data.token || "").trim();
|
|
5699
|
-
if (!token) return null;
|
|
5700
|
-
const provider = "extensiondev" === data.provider || "github" === data.provider ? data.provider : void 0;
|
|
5701
|
-
return {
|
|
5702
|
-
version: 1,
|
|
5703
|
-
token,
|
|
5704
|
-
workspaceSlug: String(data.workspaceSlug || ""),
|
|
5705
|
-
projectSlug: String(data.projectSlug || ""),
|
|
5706
|
-
expiresAt: Number(data.expiresAt || 0),
|
|
5707
|
-
api: String(data.api || ""),
|
|
5708
|
-
...provider ? {
|
|
5709
|
-
provider
|
|
5710
|
-
} : {}
|
|
5711
|
-
};
|
|
5712
|
-
} catch {
|
|
5713
|
-
return null;
|
|
5714
|
-
}
|
|
5715
|
-
}
|
|
5716
|
-
function writeCredentials(creds) {
|
|
5717
|
-
const file = credentialsPath();
|
|
5718
|
-
const dir = node_path.dirname(file);
|
|
5719
|
-
node_fs.mkdirSync(dir, {
|
|
5720
|
-
recursive: true,
|
|
5721
|
-
mode: 448
|
|
5722
|
-
});
|
|
5723
|
-
try {
|
|
5724
|
-
node_fs.chmodSync(dir, 448);
|
|
5725
|
-
} catch {}
|
|
5726
|
-
node_fs.writeFileSync(file, JSON.stringify(creds, null, 2) + "\n", {
|
|
5727
|
-
mode: 384
|
|
5728
|
-
});
|
|
5729
|
-
try {
|
|
5730
|
-
node_fs.chmodSync(file, 384);
|
|
5731
|
-
} catch {}
|
|
5732
|
-
return file;
|
|
5733
|
-
}
|
|
5734
|
-
function clearCredentials() {
|
|
5735
|
-
const file = credentialsPath();
|
|
5736
|
-
try {
|
|
5737
|
-
node_fs.unlinkSync(file);
|
|
5738
|
-
return {
|
|
5739
|
-
cleared: true,
|
|
5740
|
-
path: file
|
|
5741
|
-
};
|
|
5742
|
-
} catch {
|
|
5743
|
-
return {
|
|
5744
|
-
cleared: false,
|
|
5745
|
-
path: file
|
|
5746
|
-
};
|
|
5747
|
-
}
|
|
5748
|
-
}
|
|
5749
|
-
function readValidCredentials(nowSeconds = Math.floor(Date.now() / 1000)) {
|
|
5750
|
-
const creds = readCredentials();
|
|
5751
|
-
if (!creds) return null;
|
|
5752
|
-
if (creds.expiresAt && creds.expiresAt <= nowSeconds) return null;
|
|
5753
|
-
return creds;
|
|
5754
|
-
}
|
|
5755
|
-
const REGISTRY_BASE_DEFAULT = "https://registry.extension.land";
|
|
5756
|
-
const CONSOLE_BASE = "https://console.extension.dev";
|
|
5757
|
-
function registryBase() {
|
|
5758
|
-
const fromEnv = String(process.env.EXTENSION_DEV_REGISTRY_URL || "").trim();
|
|
5759
|
-
return (fromEnv || REGISTRY_BASE_DEFAULT).replace(/\/+$/, "");
|
|
5760
|
-
}
|
|
5761
|
-
function resolveProjectRef(overrides) {
|
|
5762
|
-
const workspace = String(overrides?.workspace || "").trim();
|
|
5763
|
-
const project = String(overrides?.project || "").trim();
|
|
5764
|
-
if (workspace && project) return {
|
|
5765
|
-
workspace,
|
|
5766
|
-
project
|
|
5767
|
-
};
|
|
5768
|
-
const creds = readCredentials();
|
|
5769
|
-
const ws = workspace || String(creds?.workspaceSlug || "").trim();
|
|
5770
|
-
const proj = project || String(creds?.projectSlug || "").trim();
|
|
5771
|
-
if (!ws || !proj) return null;
|
|
5772
|
-
return {
|
|
5773
|
-
workspace: ws,
|
|
5774
|
-
project: proj
|
|
5775
|
-
};
|
|
5776
|
-
}
|
|
5777
|
-
function registryFileUrl(ref, file) {
|
|
5778
|
-
return `${registryBase()}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/_extension-dev/${file}`;
|
|
5779
|
-
}
|
|
5780
|
-
function consoleProjectUrl(ref, page) {
|
|
5781
|
-
if (!ref) return `${CONSOLE_BASE}`;
|
|
5782
|
-
return `${CONSOLE_BASE}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/${page}`;
|
|
5783
|
-
}
|
|
5784
|
-
async function fetchRegistryJson(url, fetchImpl = fetch) {
|
|
5785
|
-
let res;
|
|
5786
|
-
try {
|
|
5787
|
-
res = await fetchImpl(url);
|
|
5788
|
-
} catch (err) {
|
|
5789
|
-
return {
|
|
5790
|
-
ok: false,
|
|
5791
|
-
message: `Could not reach ${url}: ${err?.message || err}`
|
|
5792
|
-
};
|
|
5793
|
-
}
|
|
5794
|
-
if (!res.ok) return {
|
|
5795
|
-
ok: false,
|
|
5796
|
-
status: res.status,
|
|
5797
|
-
message: `${url} returned ${res.status}`
|
|
5798
|
-
};
|
|
5799
|
-
try {
|
|
5800
|
-
const text = await res.text();
|
|
5801
|
-
return {
|
|
5802
|
-
ok: true,
|
|
5803
|
-
json: JSON.parse(text)
|
|
5804
|
-
};
|
|
5805
|
-
} catch {
|
|
5806
|
-
return {
|
|
5807
|
-
ok: false,
|
|
5808
|
-
message: `${url} did not return valid JSON`
|
|
5809
|
-
};
|
|
5810
|
-
}
|
|
5811
|
-
}
|
|
5812
|
-
function parseChannels(json) {
|
|
5813
|
-
if (!json || "object" != typeof json || Array.isArray(json)) return [];
|
|
5814
|
-
const out = [];
|
|
5815
|
-
for (const [channel, raw] of Object.entries(json)){
|
|
5816
|
-
if (!raw || "object" != typeof raw) continue;
|
|
5817
|
-
const row = raw;
|
|
5818
|
-
const description = "string" == typeof row.description ? row.description : void 0;
|
|
5819
|
-
const promotedAtField = "string" == typeof row.promotedAt && row.promotedAt ? row.promotedAt : void 0;
|
|
5820
|
-
const fromDescription = description?.match(/\bon (\d{4}-\d{2}-\d{2}T[0-9:.]+Z?)/)?.[1];
|
|
5821
|
-
const entry = {
|
|
5822
|
-
channel,
|
|
5823
|
-
sha: String(row.sha ?? "")
|
|
5824
|
-
};
|
|
5825
|
-
if (row.buildId) entry.buildId = String(row.buildId);
|
|
5826
|
-
if (row.version) entry.version = String(row.version);
|
|
5827
|
-
const promotedAt = promotedAtField || fromDescription;
|
|
5828
|
-
if (promotedAt) entry.promotedAt = promotedAt;
|
|
5829
|
-
if (description) entry.description = description;
|
|
5830
|
-
out.push(entry);
|
|
5831
|
-
}
|
|
5832
|
-
return out;
|
|
5833
|
-
}
|
|
5834
|
-
function parseBuildIndex(json) {
|
|
5835
|
-
const items = json?.items;
|
|
5836
|
-
if (!Array.isArray(items)) return [];
|
|
5837
|
-
const out = [];
|
|
5838
|
-
for (const raw of items){
|
|
5839
|
-
if (!raw || "object" != typeof raw) continue;
|
|
5840
|
-
const row = raw;
|
|
5841
|
-
const sha = String(row.shortSha ?? row.sha ?? row.id ?? row.buildId ?? "").trim();
|
|
5842
|
-
if (!sha) continue;
|
|
5843
|
-
const entry = {
|
|
5844
|
-
sha
|
|
5845
|
-
};
|
|
5846
|
-
if (row.commit) entry.commit = String(row.commit);
|
|
5847
|
-
if (row.channel) entry.channel = String(row.channel);
|
|
5848
|
-
if (row.buildEnv) entry.buildEnv = String(row.buildEnv);
|
|
5849
|
-
if (row.status) entry.status = String(row.status);
|
|
5850
|
-
if (row.version) entry.version = String(row.version);
|
|
5851
|
-
if ("string" == typeof row.message) entry.message = row.message.split("\n", 1)[0];
|
|
5852
|
-
if (row.timestamp) entry.timestamp = String(row.timestamp);
|
|
5853
|
-
if (Array.isArray(row.browsers)) entry.browsers = row.browsers.map((b)=>String(b)).filter(Boolean);
|
|
5854
|
-
out.push(entry);
|
|
5855
|
-
}
|
|
5856
|
-
return out;
|
|
5857
|
-
}
|
|
5858
|
-
const DEFAULT_API = "https://www.extension.dev";
|
|
6052
|
+
const DEFAULT_API = PROD_ORIGINS.www;
|
|
5859
6053
|
function tokenTtlNote(workspaceSlug, projectSlug) {
|
|
5860
6054
|
const tokensUrl = workspaceSlug && projectSlug ? consoleProjectUrl({
|
|
5861
6055
|
workspace: workspaceSlug,
|
|
5862
6056
|
project: projectSlug
|
|
5863
|
-
}, "settings/access-tokens") :
|
|
6057
|
+
}, "settings/access-tokens") : consoleBase();
|
|
5864
6058
|
return `extension.dev CLI tokens live at most 7 days (server-enforced). CI pipelines must re-mint before expiry on the console's Access tokens page: ${tokensUrl}`;
|
|
5865
6059
|
}
|
|
5866
6060
|
function resolveApiBase(api) {
|
|
@@ -6101,7 +6295,6 @@ async function publish_handler(args) {
|
|
|
6101
6295
|
}
|
|
6102
6296
|
return JSON.stringify(data);
|
|
6103
6297
|
}
|
|
6104
|
-
const release_promote_DEFAULT_API = "https://www.extension.dev";
|
|
6105
6298
|
const release_promote_schema = {
|
|
6106
6299
|
name: "extension_release_promote",
|
|
6107
6300
|
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; tokens live at most 7 days, so CI must re-mint before expiry). 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).",
|
|
@@ -6161,7 +6354,7 @@ async function release_promote_handler(args) {
|
|
|
6161
6354
|
const buildId = String(args.buildId || "").trim();
|
|
6162
6355
|
const channel = String(args.channel || "").trim();
|
|
6163
6356
|
if (!buildId || !channel) return release_promote_fail("ReleaseInputError", "buildId and channel are required.");
|
|
6164
|
-
const apiCheck = safeApiBase(
|
|
6357
|
+
const apiCheck = safeApiBase(resolveApiBase(args.api));
|
|
6165
6358
|
if (!apiCheck.ok) return release_promote_fail("ReleaseConfigError", apiCheck.message);
|
|
6166
6359
|
const url = `${apiCheck.base}/api/cli/release/promote`;
|
|
6167
6360
|
const body = {
|
|
@@ -6199,7 +6392,7 @@ async function release_promote_handler(args) {
|
|
|
6199
6392
|
const enrich = {};
|
|
6200
6393
|
const ref = resolveProjectRef();
|
|
6201
6394
|
if (404 === res.status || "UNKNOWN_BUILD" === code) {
|
|
6202
|
-
enrich.buildsPageUrl = consoleProjectUrl(ref, "builds");
|
|
6395
|
+
enrich.buildsPageUrl = consoleProjectUrl(ref, "builds", args.api);
|
|
6203
6396
|
enrich.hint = "Run extension_release_list to see this project's channels, their promoted shas, and recent builds.";
|
|
6204
6397
|
if (ref) {
|
|
6205
6398
|
const channelsUrl = registryFileUrl(ref, "channels.json");
|
|
@@ -6299,7 +6492,6 @@ async function release_list_handler(args) {
|
|
|
6299
6492
|
if (!buildsRes.ok) result.buildsUnavailable = `builds/index.json unreadable: ${buildsRes.message}`;
|
|
6300
6493
|
return JSON.stringify(result);
|
|
6301
6494
|
}
|
|
6302
|
-
const deploy_DEFAULT_API = "https://www.extension.dev";
|
|
6303
6495
|
function storeMdWarnings(browsers, cwd) {
|
|
6304
6496
|
const wantsFirefox = browsers.includes("firefox");
|
|
6305
6497
|
const wantsEdge = browsers.includes("edge");
|
|
@@ -6389,7 +6581,7 @@ async function deploy_handler(args) {
|
|
|
6389
6581
|
if (0 === browsers.length) return deploy_fail("DeployInputError", 'browsers is required (e.g. ["chrome","firefox","edge","safari"]).');
|
|
6390
6582
|
const buildSha = String(args.buildSha || "").trim();
|
|
6391
6583
|
if (!buildSha) return deploy_fail("DeployInputError", "buildSha is required (the built commit to submit).");
|
|
6392
|
-
const apiCheck = safeApiBase(
|
|
6584
|
+
const apiCheck = safeApiBase(resolveApiBase(args.api));
|
|
6393
6585
|
if (!apiCheck.ok) return deploy_fail("DeployConfigError", apiCheck.message);
|
|
6394
6586
|
const url = `${apiCheck.base}/api/cli/stores/submit`;
|
|
6395
6587
|
const dryRun = false !== args.dryRun;
|
|
@@ -6434,7 +6626,7 @@ async function deploy_handler(args) {
|
|
|
6434
6626
|
};
|
|
6435
6627
|
if (dryRun) {
|
|
6436
6628
|
const ref = resolveProjectRef();
|
|
6437
|
-
const consoleStoresUrl = consoleProjectUrl(ref, "stores");
|
|
6629
|
+
const consoleStoresUrl = consoleProjectUrl(ref, "stores", args.api);
|
|
6438
6630
|
const storeModeNote = `Store publish mode (draft / skip-publish / live) is not readable with the CLI token, so it cannot be verified from here; check per-store settings at ${consoleStoresUrl}.`;
|
|
6439
6631
|
let health = null;
|
|
6440
6632
|
let healthUnreadable = null;
|
|
@@ -6710,6 +6902,63 @@ async function store_status_handler(args) {
|
|
|
6710
6902
|
if (!submissionsRes.ok && 404 !== submissionsRes.status) result.submissionsUnavailable = `stores/submissions.json unreadable: ${submissionsRes.message}`;
|
|
6711
6903
|
return JSON.stringify(result);
|
|
6712
6904
|
}
|
|
6905
|
+
const ENGINE_COMPANION_IDS = new Set([
|
|
6906
|
+
"kgdaecdpfkikjncaalnmmnjjfpofkcbl",
|
|
6907
|
+
CARRIER_EXTENSION_ID
|
|
6908
|
+
]);
|
|
6909
|
+
const EXTENSION_URL = /^chrome-extension:\/\/([a-p]{32})\//i;
|
|
6910
|
+
async function verifyGuestLoaded(projectPath, browser, options) {
|
|
6911
|
+
let cdpPort;
|
|
6912
|
+
try {
|
|
6913
|
+
const resolved = await resolveCdpPort(projectPath, browser, {
|
|
6914
|
+
waitMs: options?.waitMs ?? 0
|
|
6915
|
+
});
|
|
6916
|
+
if (!resolved) return {
|
|
6917
|
+
checked: false,
|
|
6918
|
+
loaded: false,
|
|
6919
|
+
guestTargets: [],
|
|
6920
|
+
guestIds: [],
|
|
6921
|
+
reason: "No CDP port in the session's ready contract, so the browser's target list could not be queried (a headless Chromium session still exposes one; a gecko/Firefox session does not)."
|
|
6922
|
+
};
|
|
6923
|
+
cdpPort = resolved.port;
|
|
6924
|
+
const timeoutMs = options?.timeoutMs ?? 3000;
|
|
6925
|
+
const targets = await Promise.race([
|
|
6926
|
+
CDPClient.discoverTargets(cdpPort),
|
|
6927
|
+
new Promise((_, reject)=>setTimeout(()=>reject(new Error(`CDP /json timed out after ${timeoutMs}ms`)), timeoutMs))
|
|
6928
|
+
]);
|
|
6929
|
+
const guestTargets = [];
|
|
6930
|
+
for (const t of targets){
|
|
6931
|
+
const match = EXTENSION_URL.exec(String(t.url ?? ""));
|
|
6932
|
+
if (!match) continue;
|
|
6933
|
+
const id = match[1].toLowerCase();
|
|
6934
|
+
if (!ENGINE_COMPANION_IDS.has(id)) guestTargets.push({
|
|
6935
|
+
id,
|
|
6936
|
+
type: String(t.type),
|
|
6937
|
+
url: String(t.url)
|
|
6938
|
+
});
|
|
6939
|
+
}
|
|
6940
|
+
const guestIds = [
|
|
6941
|
+
...new Set(guestTargets.map((t)=>t.id))
|
|
6942
|
+
];
|
|
6943
|
+
return {
|
|
6944
|
+
checked: true,
|
|
6945
|
+
loaded: guestTargets.length > 0,
|
|
6946
|
+
guestTargets,
|
|
6947
|
+
guestIds,
|
|
6948
|
+
cdpPort,
|
|
6949
|
+
reason: guestTargets.length > 0 ? `The browser lists ${guestIds.length} extension target${1 === guestIds.length ? "" : "s"} that are not the engine companion (${guestIds.join(", ")}), so the guest is loaded.` : "The browser's target list has no chrome-extension:// target other than the engine's devtools companion. On Chrome this is the signature of a silently rejected --load-extension (extension.js BUGS_TO_FIX §83): the CLI and ready.json cannot see it."
|
|
6950
|
+
};
|
|
6951
|
+
} catch (err) {
|
|
6952
|
+
return {
|
|
6953
|
+
checked: false,
|
|
6954
|
+
loaded: false,
|
|
6955
|
+
guestTargets: [],
|
|
6956
|
+
guestIds: [],
|
|
6957
|
+
cdpPort,
|
|
6958
|
+
reason: `Could not query the browser's target list${cdpPort ? ` on CDP port ${cdpPort}` : ""}: ${err?.message ?? String(err)}.`
|
|
6959
|
+
};
|
|
6960
|
+
}
|
|
6961
|
+
}
|
|
6713
6962
|
function doctor_readReadyContract(projectPath, browser) {
|
|
6714
6963
|
try {
|
|
6715
6964
|
const raw = node_fs.readFileSync(node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json"), "utf8");
|
|
@@ -6905,7 +7154,7 @@ function wait_isAlive(pid) {
|
|
|
6905
7154
|
}
|
|
6906
7155
|
const wait_schema = {
|
|
6907
7156
|
name: "extension_wait",
|
|
6908
|
-
description: "Wait for a running dev or start session to be ready. Polls the ready.json contract file and returns structured status with
|
|
7157
|
+
description: "Wait for a running dev or start session to be ready. Polls the ready.json contract file and returns structured status with these facts: compiled (the compiler finished), browserAttached (the engine's runtime executor connected), and for a browser session guestLoaded (the browser's OWN target list actually shows your extension). guestLoaded is the trustworthy load signal: it catches a silently rejected --load-extension that leaves ready.json stamped attached with empty logs (extension.js BUGS_TO_FIX §83); it is null when it could not be checked (no CDP port, e.g. a gecko session). Every result reports budgetMs (this call's wait budget) and elapsedMs; on status:'timeout' call again to keep waiting (polling resumes on the same contract). In a noBrowser (build-only) session it returns as soon as the compile lands instead of waiting for a browser that will never attach. Ports in the result come from the ready contract, so they always match what the dev server actually bound.",
|
|
6909
7158
|
inputSchema: {
|
|
6910
7159
|
type: "object",
|
|
6911
7160
|
properties: {
|
|
@@ -6981,10 +7230,20 @@ async function wait_handler(args) {
|
|
|
6981
7230
|
continue;
|
|
6982
7231
|
}
|
|
6983
7232
|
const runtimeErrors = recentErrorLogs(args.projectPath, browser, 3);
|
|
7233
|
+
const guestCheck = await verifyGuestLoaded(args.projectPath, browser);
|
|
7234
|
+
const warnings = [];
|
|
7235
|
+
if (runtimeErrors.length) warnings.push(`Compiled and attached, but the extension is throwing at runtime (${runtimeErrors.length} recent error event${1 === runtimeErrors.length ? "" : "s"} above). Check extension_logs (level: error) or extension_doctor before trusting this session.`);
|
|
7236
|
+
if (guestCheck.checked && !guestCheck.loaded) warnings.push("The engine reports the runtime attached, but the browser's own target list shows no chrome-extension:// target for your extension, only the engine companion. This is the signature of a silently rejected --load-extension (extension.js BUGS_TO_FIX §83): the CLI and ready.json cannot see it, and the control verbs will fail against a guest that is not there. Check the manifest and extension_logs.");
|
|
6984
7237
|
return JSON.stringify({
|
|
6985
7238
|
status: "ready",
|
|
6986
7239
|
compiled: true,
|
|
6987
7240
|
browserAttached: true,
|
|
7241
|
+
guestLoaded: guestCheck.checked ? guestCheck.loaded : null,
|
|
7242
|
+
...guestCheck.checked ? {
|
|
7243
|
+
guestIds: guestCheck.guestIds
|
|
7244
|
+
} : {
|
|
7245
|
+
guestLoadNote: guestCheck.reason
|
|
7246
|
+
},
|
|
6988
7247
|
command: contract.command,
|
|
6989
7248
|
browser: contract.browser,
|
|
6990
7249
|
port: contract.port,
|
|
@@ -6996,8 +7255,10 @@ async function wait_handler(args) {
|
|
|
6996
7255
|
budgetMs,
|
|
6997
7256
|
elapsedMs: Date.now() - start,
|
|
6998
7257
|
...runtimeErrors.length ? {
|
|
6999
|
-
runtimeErrors
|
|
7000
|
-
|
|
7258
|
+
runtimeErrors
|
|
7259
|
+
} : {},
|
|
7260
|
+
...warnings.length ? {
|
|
7261
|
+
warning: warnings.join(" ")
|
|
7001
7262
|
} : {}
|
|
7002
7263
|
});
|
|
7003
7264
|
}
|
|
@@ -7035,6 +7296,7 @@ async function wait_handler(args) {
|
|
|
7035
7296
|
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."
|
|
7036
7297
|
});
|
|
7037
7298
|
}
|
|
7299
|
+
const EXAMPLES_TREE_BASE = `https://github.com/extension-js/examples/tree/${PINNED_COMMIT}/examples`;
|
|
7038
7300
|
const add_feature_schema = {
|
|
7039
7301
|
name: "extension_add_feature",
|
|
7040
7302
|
description: "Plan a new feature surface for an existing extension. Returns step-by-step instructions, the manifest additions to make, and reference templates from the extension.dev catalog. Does not modify files; apply the returned plan yourself.",
|
|
@@ -7278,7 +7540,7 @@ async function add_feature_handler(args) {
|
|
|
7278
7540
|
framework,
|
|
7279
7541
|
referenceTemplate: {
|
|
7280
7542
|
slug: templateSlug,
|
|
7281
|
-
repositoryUrl:
|
|
7543
|
+
repositoryUrl: `${EXAMPLES_TREE_BASE}/${templateSlug}`,
|
|
7282
7544
|
referenceFiles: referenceFiles.filter((f)=>f.includes(featureDir) || f.includes("manifest"))
|
|
7283
7545
|
},
|
|
7284
7546
|
manifestUpdates,
|
|
@@ -7292,7 +7554,7 @@ async function add_feature_handler(args) {
|
|
|
7292
7554
|
"2. Create the following files in your project:",
|
|
7293
7555
|
...filesToCreate.map((f)=>` - ${f.path} (${f.hint})`),
|
|
7294
7556
|
"sidebar" === args.feature ? "3. Add background.ts to handle sidebar open: chromium uses chrome.sidePanel.setPanelBehavior, firefox uses browser.sidebarAction.open()" : "",
|
|
7295
|
-
`4. Reference template source:
|
|
7557
|
+
`4. Reference template source: ${EXAMPLES_TREE_BASE}/${templateSlug}/src`,
|
|
7296
7558
|
"5. Run npm run dev to test"
|
|
7297
7559
|
].filter(Boolean),
|
|
7298
7560
|
hint: conflicts.length ? `Warning: ${conflicts.length} file(s) already exist and would be overwritten.` : "No conflicts detected. Safe to create all files."
|
|
@@ -7696,7 +7958,10 @@ const logout_schema = {
|
|
|
7696
7958
|
};
|
|
7697
7959
|
async function logout_handler() {
|
|
7698
7960
|
const creds = readCredentials();
|
|
7699
|
-
const revokeUrl = creds?.workspaceSlug && creds?.projectSlug ?
|
|
7961
|
+
const revokeUrl = creds?.workspaceSlug && creds?.projectSlug ? consoleProjectUrl({
|
|
7962
|
+
workspace: creds.workspaceSlug,
|
|
7963
|
+
project: creds.projectSlug
|
|
7964
|
+
}, "settings/access-tokens") : null;
|
|
7700
7965
|
const result = clearCredentials();
|
|
7701
7966
|
return JSON.stringify({
|
|
7702
7967
|
ok: true,
|