@extension.dev/mcp 10.10.1 → 10.10.3
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 +214 -0
- package/README.md +29 -4
- package/bin/extension-mcp.js +14 -4
- package/claude/CLAUDE.md +2 -1
- package/dist/module.d.ts +1 -0
- package/dist/module.js +2194 -264
- package/dist/src/index.d.ts +3 -2
- package/dist/src/lib/approval-gate.d.ts +5 -1
- package/dist/src/lib/artifacts-api.d.ts +1 -0
- package/dist/src/lib/bridge-tabs.d.ts +8 -1
- package/dist/src/lib/cdp-devtools.d.ts +27 -0
- package/dist/src/lib/cdp-extension-page.d.ts +18 -0
- package/dist/src/lib/cdp-targets.d.ts +7 -0
- package/dist/src/lib/cdp.d.ts +2 -0
- package/dist/src/lib/common-schema.d.ts +3 -3
- package/dist/src/lib/credentials.d.ts +24 -3
- package/dist/src/lib/device-flow.d.ts +6 -3
- package/dist/src/lib/envelope.d.ts +1 -1
- package/dist/src/lib/exec.d.ts +1 -0
- package/dist/src/lib/publish.d.ts +3 -1
- package/dist/src/lib/registry.d.ts +1 -0
- package/dist/src/lib/session-browser.d.ts +1 -1
- package/dist/src/lib/session-paths.d.ts +10 -1
- package/dist/src/lib/tool-policy.d.ts +33 -0
- package/dist/src/tools/browsers.d.ts +1 -1
- package/dist/src/tools/build.d.ts +1 -1
- package/dist/src/tools/detect-browsers.d.ts +2 -0
- package/dist/src/tools/dev.d.ts +1 -1
- package/dist/src/tools/eval.d.ts +2 -0
- package/dist/src/tools/inspect-gecko.d.ts +1 -0
- package/dist/src/tools/logout.d.ts +1 -1
- package/dist/src/tools/open.d.ts +25 -1
- package/dist/src/tools/preview-web.d.ts +7 -2
- package/dist/src/tools/project-create.d.ts +15 -0
- package/dist/src/tools/publish.d.ts +5 -0
- package/dist/src/tools/release-promote.d.ts +5 -0
- package/dist/src/tools/shares.d.ts +5 -0
- package/dist/src/tools/start.d.ts +6 -1
- package/dist/src/tools/submit.d.ts +5 -0
- package/dist/src/tools/workspace-create.d.ts +42 -0
- package/package.json +18 -13
- package/server.json +3 -3
package/dist/module.js
CHANGED
|
@@ -11,7 +11,7 @@ import { PROD_ORIGINS, resolveOrigins } from "@extension.dev/urls/origins";
|
|
|
11
11
|
import node_crypto, { createHash } from "node:crypto";
|
|
12
12
|
import { consoleProjectPath, wwwNewPath } from "@extension.dev/urls/paths";
|
|
13
13
|
import { UserlandProjectPage, userlandUrl } from "@extension.dev/urls/userland";
|
|
14
|
-
import { CLOSE_BAD_HELLO, CLOSE_BAD_INSTANCE, CLOSE_CONTROL_UNAVAILABLE, CLOSE_SLOW_CONSUMER, CONTROL_ENVELOPE_VERSION, CONTROL_WS_PATH, buildSummaryPath, logsPath, matchesLogQuery, readLogEvents, readReadyContract, readyContractPath as bridge_readyContractPath, sessionArtifactsRootDir } from "extension-develop/bridge";
|
|
14
|
+
import { CLOSE_BAD_HELLO, CLOSE_BAD_INSTANCE, CLOSE_CONTROL_UNAVAILABLE, CLOSE_SLOW_CONSUMER, CONTROL_ENVELOPE_VERSION, CONTROL_WS_PATH, actionsPath, browserArtifactsDir, buildSummaryPath, eventsPath, logsPath, matchesLogQuery, readLogEvents, readReadyContract, readyContractPath as bridge_readyContractPath, sessionArtifactsRootDir, sessionStateDir as bridge_sessionStateDir } from "extension-develop/bridge";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import { execFile, execFileSync } from "node:child_process";
|
|
17
17
|
import ws_0 from "ws";
|
|
@@ -93,7 +93,9 @@ __webpack_require__.r(eval_namespaceObject);
|
|
|
93
93
|
__webpack_require__.d(eval_namespaceObject, {
|
|
94
94
|
EXTENSION_PAGE_CONTEXTS: ()=>EXTENSION_PAGE_CONTEXTS,
|
|
95
95
|
chromiumManifestVersion: ()=>chromiumManifestVersion,
|
|
96
|
+
geckoManifestVersion: ()=>geckoManifestVersion,
|
|
96
97
|
handler: ()=>eval_handler,
|
|
98
|
+
isBrowserErrorPage: ()=>isBrowserErrorPage,
|
|
97
99
|
resolveDefaultEvalContext: ()=>resolveDefaultEvalContext,
|
|
98
100
|
schema: ()=>eval_schema,
|
|
99
101
|
wantsExtensionPageOverCdp: ()=>wantsExtensionPageOverCdp
|
|
@@ -228,7 +230,13 @@ __webpack_require__.d(wait_namespaceObject, {
|
|
|
228
230
|
handler: ()=>wait_handler,
|
|
229
231
|
schema: ()=>wait_schema
|
|
230
232
|
});
|
|
231
|
-
var
|
|
233
|
+
var workspace_create_namespaceObject = {};
|
|
234
|
+
__webpack_require__.r(workspace_create_namespaceObject);
|
|
235
|
+
__webpack_require__.d(workspace_create_namespaceObject, {
|
|
236
|
+
handler: ()=>workspace_create_handler,
|
|
237
|
+
schema: ()=>workspace_create_schema
|
|
238
|
+
});
|
|
239
|
+
var package_namespaceObject = JSON.parse('{"rE":"10.10.3","El":{"OP":"4.1.31"}}');
|
|
232
240
|
const ENVELOPE_SCHEMA = 1;
|
|
233
241
|
const collectWarnings = (warnings)=>{
|
|
234
242
|
if (!warnings) return [];
|
|
@@ -274,6 +282,10 @@ function pinnedCliVersion() {
|
|
|
274
282
|
function exactVersion(spec) {
|
|
275
283
|
return spec.trim().replace(/^[\^~]/, "").replace(/^v/, "");
|
|
276
284
|
}
|
|
285
|
+
function describeExtensionInvocation(projectDir) {
|
|
286
|
+
const { command, prefixArgs } = resolveExtensionInvocation(projectDir);
|
|
287
|
+
return prefixArgs.length ? `${command} ${prefixArgs.join(" ")} (this server's pinned engine; the project has no node_modules/.bin/extension)` : `${command} (the project's own Extension.js)`;
|
|
288
|
+
}
|
|
277
289
|
function resolveExtensionInvocation(projectDir) {
|
|
278
290
|
if (projectDir) {
|
|
279
291
|
const bin = node_path.join(projectDir, "node_modules", ".bin", "win32" === process.platform ? "extension.cmd" : "extension");
|
|
@@ -379,7 +391,7 @@ function spawnExtensionCli(args, options) {
|
|
|
379
391
|
...prefixArgs,
|
|
380
392
|
...args
|
|
381
393
|
], {
|
|
382
|
-
cwd: options?.cwd,
|
|
394
|
+
cwd: options?.cwd ?? options?.projectDir,
|
|
383
395
|
detached: true,
|
|
384
396
|
stdio: [
|
|
385
397
|
"ignore",
|
|
@@ -439,31 +451,85 @@ function credentialsPath() {
|
|
|
439
451
|
const base = xdg || node_path.join(node_os.homedir(), ".config");
|
|
440
452
|
return node_path.join(base, "extension-dev", "auth.json");
|
|
441
453
|
}
|
|
442
|
-
function
|
|
454
|
+
function credentialKey(workspaceSlug, projectSlug) {
|
|
455
|
+
return `${workspaceSlug}/${projectSlug}`.toLowerCase();
|
|
456
|
+
}
|
|
457
|
+
function readEntry(data) {
|
|
458
|
+
if (!data || "object" != typeof data) return null;
|
|
459
|
+
const entry = data;
|
|
460
|
+
if (1 !== entry.version) return null;
|
|
461
|
+
const token = String(entry.token || "").trim();
|
|
462
|
+
if (!token) return null;
|
|
463
|
+
const provider = "extensiondev" === entry.provider ? entry.provider : void 0;
|
|
464
|
+
return {
|
|
465
|
+
version: 1,
|
|
466
|
+
token,
|
|
467
|
+
workspaceSlug: String(entry.workspaceSlug || ""),
|
|
468
|
+
projectSlug: String(entry.projectSlug || ""),
|
|
469
|
+
expiresAt: Number(entry.expiresAt || 0),
|
|
470
|
+
api: String(entry.api || ""),
|
|
471
|
+
...provider ? {
|
|
472
|
+
provider
|
|
473
|
+
} : {}
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
function readCredentialStore() {
|
|
477
|
+
let data;
|
|
443
478
|
try {
|
|
444
|
-
|
|
445
|
-
const data = JSON.parse(raw);
|
|
446
|
-
if (!data || "object" != typeof data) return null;
|
|
447
|
-
if (1 !== data.version) return null;
|
|
448
|
-
const token = String(data.token || "").trim();
|
|
449
|
-
if (!token) return null;
|
|
450
|
-
const provider = "extensiondev" === data.provider ? data.provider : void 0;
|
|
451
|
-
return {
|
|
452
|
-
version: 1,
|
|
453
|
-
token,
|
|
454
|
-
workspaceSlug: String(data.workspaceSlug || ""),
|
|
455
|
-
projectSlug: String(data.projectSlug || ""),
|
|
456
|
-
expiresAt: Number(data.expiresAt || 0),
|
|
457
|
-
api: String(data.api || ""),
|
|
458
|
-
...provider ? {
|
|
459
|
-
provider
|
|
460
|
-
} : {}
|
|
461
|
-
};
|
|
479
|
+
data = JSON.parse(node_fs.readFileSync(credentialsPath(), "utf8"));
|
|
462
480
|
} catch {
|
|
463
481
|
return null;
|
|
464
482
|
}
|
|
483
|
+
if (!data || "object" != typeof data) return null;
|
|
484
|
+
const record = data;
|
|
485
|
+
if (1 === record.version) {
|
|
486
|
+
const only = readEntry(record);
|
|
487
|
+
if (!only) return null;
|
|
488
|
+
const key = credentialKey(only.workspaceSlug, only.projectSlug);
|
|
489
|
+
return {
|
|
490
|
+
version: 2,
|
|
491
|
+
active: key,
|
|
492
|
+
entries: {
|
|
493
|
+
[key]: only
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
if (2 !== record.version || !record.entries || "object" != typeof record.entries) return null;
|
|
498
|
+
const entries = {};
|
|
499
|
+
for (const [key, value] of Object.entries(record.entries)){
|
|
500
|
+
const entry = readEntry(value);
|
|
501
|
+
if (entry) entries[key.toLowerCase()] = entry;
|
|
502
|
+
}
|
|
503
|
+
const keys = Object.keys(entries);
|
|
504
|
+
if (0 === keys.length) return null;
|
|
505
|
+
const active = "string" == typeof record.active && entries[record.active.toLowerCase()] ? record.active.toLowerCase() : keys[0] ?? null;
|
|
506
|
+
return {
|
|
507
|
+
version: 2,
|
|
508
|
+
active,
|
|
509
|
+
entries
|
|
510
|
+
};
|
|
465
511
|
}
|
|
466
|
-
function
|
|
512
|
+
function selectEntry(store, selector) {
|
|
513
|
+
const wanted = String(selector?.project ?? "").trim().toLowerCase();
|
|
514
|
+
if (!wanted) return store.active && store.entries[store.active] || Object.values(store.entries)[0] || null;
|
|
515
|
+
if (wanted.includes("/")) return store.entries[wanted] ?? null;
|
|
516
|
+
const bySlug = Object.values(store.entries).filter((entry)=>entry.projectSlug.toLowerCase() === wanted);
|
|
517
|
+
return 1 === bySlug.length ? bySlug[0] ?? null : null;
|
|
518
|
+
}
|
|
519
|
+
function readCredentials(selector) {
|
|
520
|
+
const store = readCredentialStore();
|
|
521
|
+
return store ? selectEntry(store, selector) : null;
|
|
522
|
+
}
|
|
523
|
+
function listCredentials() {
|
|
524
|
+
const store = readCredentialStore();
|
|
525
|
+
if (!store) return [];
|
|
526
|
+
return Object.entries(store.entries).map(([key, entry])=>({
|
|
527
|
+
...entry,
|
|
528
|
+
key,
|
|
529
|
+
active: key === store.active
|
|
530
|
+
}));
|
|
531
|
+
}
|
|
532
|
+
function writeStore(store) {
|
|
467
533
|
const file = credentialsPath();
|
|
468
534
|
const dir = node_path.dirname(file);
|
|
469
535
|
node_fs.mkdirSync(dir, {
|
|
@@ -473,7 +539,7 @@ function writeCredentials(creds) {
|
|
|
473
539
|
try {
|
|
474
540
|
node_fs.chmodSync(dir, 448);
|
|
475
541
|
} catch {}
|
|
476
|
-
node_fs.writeFileSync(file, JSON.stringify(
|
|
542
|
+
node_fs.writeFileSync(file, JSON.stringify(store, null, 2) + "\n", {
|
|
477
543
|
mode: 384
|
|
478
544
|
});
|
|
479
545
|
try {
|
|
@@ -481,27 +547,100 @@ function writeCredentials(creds) {
|
|
|
481
547
|
} catch {}
|
|
482
548
|
return file;
|
|
483
549
|
}
|
|
484
|
-
function
|
|
550
|
+
function writeCredentials(creds) {
|
|
551
|
+
const key = credentialKey(creds.workspaceSlug, creds.projectSlug);
|
|
552
|
+
const existing = readCredentialStore();
|
|
553
|
+
const entries = {
|
|
554
|
+
...existing?.entries ?? {},
|
|
555
|
+
[key]: creds
|
|
556
|
+
};
|
|
557
|
+
return writeStore({
|
|
558
|
+
version: 2,
|
|
559
|
+
active: key,
|
|
560
|
+
entries
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
function clearCredentials(selector) {
|
|
485
564
|
const file = credentialsPath();
|
|
486
|
-
|
|
565
|
+
const store = readCredentialStore();
|
|
566
|
+
const wanted = String(selector?.project ?? "").trim();
|
|
567
|
+
if (!wanted) try {
|
|
487
568
|
node_fs.unlinkSync(file);
|
|
488
569
|
return {
|
|
489
570
|
cleared: true,
|
|
490
|
-
path: file
|
|
571
|
+
path: file,
|
|
572
|
+
removed: store ? Object.keys(store.entries) : [],
|
|
573
|
+
remaining: []
|
|
491
574
|
};
|
|
492
575
|
} catch {
|
|
493
576
|
return {
|
|
494
577
|
cleared: false,
|
|
495
|
-
path: file
|
|
578
|
+
path: file,
|
|
579
|
+
removed: [],
|
|
580
|
+
remaining: []
|
|
496
581
|
};
|
|
497
582
|
}
|
|
583
|
+
if (!store) return {
|
|
584
|
+
cleared: false,
|
|
585
|
+
path: file,
|
|
586
|
+
removed: [],
|
|
587
|
+
remaining: []
|
|
588
|
+
};
|
|
589
|
+
const entry = selectEntry(store, {
|
|
590
|
+
project: wanted
|
|
591
|
+
});
|
|
592
|
+
if (!entry) return {
|
|
593
|
+
cleared: false,
|
|
594
|
+
path: file,
|
|
595
|
+
removed: [],
|
|
596
|
+
remaining: Object.keys(store.entries)
|
|
597
|
+
};
|
|
598
|
+
const key = credentialKey(entry.workspaceSlug, entry.projectSlug);
|
|
599
|
+
const entries = {
|
|
600
|
+
...store.entries
|
|
601
|
+
};
|
|
602
|
+
delete entries[key];
|
|
603
|
+
const remaining = Object.keys(entries);
|
|
604
|
+
if (0 === remaining.length) {
|
|
605
|
+
try {
|
|
606
|
+
node_fs.unlinkSync(file);
|
|
607
|
+
} catch {}
|
|
608
|
+
return {
|
|
609
|
+
cleared: true,
|
|
610
|
+
path: file,
|
|
611
|
+
removed: [
|
|
612
|
+
key
|
|
613
|
+
],
|
|
614
|
+
remaining: []
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
writeStore({
|
|
618
|
+
version: 2,
|
|
619
|
+
active: store.active === key ? remaining[0] ?? null : store.active,
|
|
620
|
+
entries
|
|
621
|
+
});
|
|
622
|
+
return {
|
|
623
|
+
cleared: true,
|
|
624
|
+
path: file,
|
|
625
|
+
removed: [
|
|
626
|
+
key
|
|
627
|
+
],
|
|
628
|
+
remaining
|
|
629
|
+
};
|
|
498
630
|
}
|
|
499
|
-
function readValidCredentials(nowSeconds = Math.floor(Date.now() / 1000)) {
|
|
500
|
-
const creds = readCredentials();
|
|
631
|
+
function readValidCredentials(nowSeconds = Math.floor(Date.now() / 1000), selector) {
|
|
632
|
+
const creds = readCredentials(selector);
|
|
501
633
|
if (!creds) return null;
|
|
502
634
|
if (creds.expiresAt && creds.expiresAt <= nowSeconds) return null;
|
|
503
635
|
return creds;
|
|
504
636
|
}
|
|
637
|
+
const PROJECT_TOKEN_INPUT = {
|
|
638
|
+
type: "string",
|
|
639
|
+
description: "Which stored login to use, as '<workspace>/<project>' (or a project slug that matches one login), when extension_auth has signed in to more than one project on this machine. Omitted, the most recent login is used, after EXTENSION_DEV_TOKEN when that is set. Named, it outranks EXTENSION_DEV_TOKEN. extension_auth (action: status) lists the stored logins."
|
|
640
|
+
};
|
|
641
|
+
function noStoredLoginHint(project) {
|
|
642
|
+
return `No stored login for ${project}. extension_auth (action: status) lists the logins on this machine; extension_auth (action: login, project: "${project}") adds one. Logging in to another project no longer replaces this one.`;
|
|
643
|
+
}
|
|
505
644
|
const DEFAULT_API = PROD_ORIGINS.www;
|
|
506
645
|
function tokenTtlNote(workspaceSlug, projectSlug) {
|
|
507
646
|
const tokensUrl = workspaceSlug && projectSlug ? consoleProjectUrl({
|
|
@@ -1060,6 +1199,10 @@ function parseChannels(json) {
|
|
|
1060
1199
|
}
|
|
1061
1200
|
return out;
|
|
1062
1201
|
}
|
|
1202
|
+
function isSuccessfulBuild(item) {
|
|
1203
|
+
const status = String(item?.status ?? "").trim().toLowerCase();
|
|
1204
|
+
return "success" === status || "ready" === status;
|
|
1205
|
+
}
|
|
1063
1206
|
function parseBuildIndex(json) {
|
|
1064
1207
|
const items = json?.items;
|
|
1065
1208
|
if (!Array.isArray(items)) return [];
|
|
@@ -1526,6 +1669,8 @@ const LAUNCHABLE_BROWSERS = [
|
|
|
1526
1669
|
"firefox",
|
|
1527
1670
|
"waterfox",
|
|
1528
1671
|
"librewolf",
|
|
1672
|
+
"zen",
|
|
1673
|
+
"floorp",
|
|
1529
1674
|
"safari",
|
|
1530
1675
|
"chromium-based",
|
|
1531
1676
|
"gecko-based",
|
|
@@ -1543,6 +1688,8 @@ const REAL_BROWSERS = [
|
|
|
1543
1688
|
"firefox",
|
|
1544
1689
|
"waterfox",
|
|
1545
1690
|
"librewolf",
|
|
1691
|
+
"zen",
|
|
1692
|
+
"floorp",
|
|
1546
1693
|
"safari"
|
|
1547
1694
|
];
|
|
1548
1695
|
const MANAGED_BROWSERS = [
|
|
@@ -1585,7 +1732,12 @@ async function requestDeviceCode(args) {
|
|
|
1585
1732
|
accept: "application/json"
|
|
1586
1733
|
},
|
|
1587
1734
|
body: JSON.stringify({
|
|
1588
|
-
|
|
1735
|
+
...args.project ? {
|
|
1736
|
+
project: args.project
|
|
1737
|
+
} : {},
|
|
1738
|
+
...args.workspace ? {
|
|
1739
|
+
workspace: args.workspace
|
|
1740
|
+
} : {},
|
|
1589
1741
|
clientName: args.clientName ?? "extension-mcp",
|
|
1590
1742
|
...args.intent ? {
|
|
1591
1743
|
intent: args.intent
|
|
@@ -1631,7 +1783,12 @@ async function pollDeviceGrant(args) {
|
|
|
1631
1783
|
},
|
|
1632
1784
|
body: JSON.stringify({
|
|
1633
1785
|
device_code: args.deviceCode,
|
|
1634
|
-
|
|
1786
|
+
...args.project ? {
|
|
1787
|
+
project: args.project
|
|
1788
|
+
} : {},
|
|
1789
|
+
...args.workspace ? {
|
|
1790
|
+
workspace: args.workspace
|
|
1791
|
+
} : {}
|
|
1635
1792
|
})
|
|
1636
1793
|
});
|
|
1637
1794
|
const text = await res.text();
|
|
@@ -1773,7 +1930,26 @@ const project_create_schema = {
|
|
|
1773
1930
|
outputDirectory: {
|
|
1774
1931
|
type: "string",
|
|
1775
1932
|
default: "dist/chrome",
|
|
1776
|
-
description: "Directory the build writes the loadable extension into."
|
|
1933
|
+
description: "Directory the build writes the loadable extension into. With more than one browser, `<browser>` in the path becomes each browser's name (Extension.js writes `dist/<browser>`), and when it is left out every browser defaults to `dist/<browser>`."
|
|
1934
|
+
},
|
|
1935
|
+
browsers: {
|
|
1936
|
+
type: "array",
|
|
1937
|
+
items: {
|
|
1938
|
+
type: "string",
|
|
1939
|
+
enum: [
|
|
1940
|
+
"chrome",
|
|
1941
|
+
"edge",
|
|
1942
|
+
"firefox"
|
|
1943
|
+
]
|
|
1944
|
+
},
|
|
1945
|
+
default: [
|
|
1946
|
+
"chrome"
|
|
1947
|
+
],
|
|
1948
|
+
description: "Browsers the platform builds, each enabled with the same install and build command and its own output directory. Pass every browser the extension targets, for example [\"chrome\", \"edge\", \"firefox\"], so the project needs no console visit to go cross-browser."
|
|
1949
|
+
},
|
|
1950
|
+
outputDirectories: {
|
|
1951
|
+
type: "object",
|
|
1952
|
+
description: "Per-browser output directory overrides, for example {\"edge\": \"build/manifestv3\"}. A browser named here wins over `outputDirectory`."
|
|
1777
1953
|
},
|
|
1778
1954
|
deviceCode: {
|
|
1779
1955
|
type: "string",
|
|
@@ -1787,7 +1963,7 @@ const project_create_schema = {
|
|
|
1787
1963
|
]
|
|
1788
1964
|
}
|
|
1789
1965
|
};
|
|
1790
|
-
function fail(name, message, status, code) {
|
|
1966
|
+
function fail(name, message, status, code, hint) {
|
|
1791
1967
|
return envelope_envelope({
|
|
1792
1968
|
ok: false,
|
|
1793
1969
|
command: project_create_COMMAND,
|
|
@@ -1796,21 +1972,23 @@ function fail(name, message, status, code) {
|
|
|
1796
1972
|
code,
|
|
1797
1973
|
name,
|
|
1798
1974
|
message
|
|
1799
|
-
}
|
|
1975
|
+
},
|
|
1976
|
+
...hint ? {
|
|
1977
|
+
hint
|
|
1978
|
+
} : {}
|
|
1800
1979
|
});
|
|
1801
1980
|
}
|
|
1981
|
+
function laneClosedHint() {
|
|
1982
|
+
return `Create the project in the console at ${consoleBase()} (workspace page, New project), then run extension_auth (action: login) against it. While the public hold is on, headless creation is open only to workspaces on the platform's allowlist (WWW_MCP_ACTION_ALLOWED_WORKSPACES), the same list that already lets publish, submit and promote run headlessly; this workspace is not on it, so the console is the route.`;
|
|
1983
|
+
}
|
|
1802
1984
|
function pendingEnvelope(start) {
|
|
1803
1985
|
const complete = String(start.verificationUriComplete || "").trim();
|
|
1804
1986
|
const hasCompleteLink = complete.length > 0 && complete !== start.verificationUri;
|
|
1805
1987
|
const message = hasCompleteLink ? `Open ${complete} and approve creating the project (code ${start.userCode} is pre-filled), then call extension_project_create again with this deviceCode and the same arguments. If the page asks for a code, enter ${start.userCode} at ${start.verificationUri}.` : `Open ${start.verificationUri}, enter code ${start.userCode}, approve creating the project, then call extension_project_create again with this deviceCode and the same arguments.`;
|
|
1806
1988
|
return envelope_envelope({
|
|
1807
|
-
ok:
|
|
1989
|
+
ok: true,
|
|
1808
1990
|
command: project_create_COMMAND,
|
|
1809
1991
|
status: "authorization-pending",
|
|
1810
|
-
error: {
|
|
1811
|
-
code: "E_AUTH_PENDING",
|
|
1812
|
-
message
|
|
1813
|
-
},
|
|
1814
1992
|
value: {
|
|
1815
1993
|
userCode: start.userCode,
|
|
1816
1994
|
verificationUri: start.verificationUri,
|
|
@@ -1825,12 +2003,23 @@ function pendingEnvelope(start) {
|
|
|
1825
2003
|
function buildCreateBody(args) {
|
|
1826
2004
|
const installCommand = String(args.installCommand || "npm install").trim();
|
|
1827
2005
|
const buildCommand = String(args.buildCommand || "npm run build").trim();
|
|
1828
|
-
const
|
|
1829
|
-
|
|
1830
|
-
|
|
2006
|
+
const wanted = new Set((Array.isArray(args.browsers) && args.browsers.length ? args.browsers : [
|
|
2007
|
+
"chrome"
|
|
2008
|
+
]).map((name)=>String(name).trim().toLowerCase()));
|
|
2009
|
+
const several = wanted.size > 1;
|
|
2010
|
+
const outputFor = (name)=>{
|
|
2011
|
+
const override = args.outputDirectories?.[name];
|
|
2012
|
+
if ("string" == typeof override && override.trim()) return override.trim();
|
|
2013
|
+
const pattern = String(args.outputDirectory || "").trim();
|
|
2014
|
+
if (pattern.includes("<browser>")) return pattern.replaceAll("<browser>", name);
|
|
2015
|
+
if (pattern && !several) return pattern;
|
|
2016
|
+
return `dist/${name}`;
|
|
2017
|
+
};
|
|
2018
|
+
const browser = (name)=>({
|
|
2019
|
+
enabled: wanted.has(name),
|
|
1831
2020
|
installCommand,
|
|
1832
2021
|
buildCommand,
|
|
1833
|
-
outputDirectory
|
|
2022
|
+
outputDirectory: outputFor(name)
|
|
1834
2023
|
});
|
|
1835
2024
|
return {
|
|
1836
2025
|
info: {
|
|
@@ -1840,9 +2029,9 @@ function buildCreateBody(args) {
|
|
|
1840
2029
|
description: String(args.description || `Browser extension project for ${args.owner}/${args.repoName}.`).trim()
|
|
1841
2030
|
},
|
|
1842
2031
|
build: {
|
|
1843
|
-
chrome: browser(
|
|
1844
|
-
edge: browser(
|
|
1845
|
-
firefox: browser(
|
|
2032
|
+
chrome: browser("chrome"),
|
|
2033
|
+
edge: browser("edge"),
|
|
2034
|
+
firefox: browser("firefox")
|
|
1846
2035
|
},
|
|
1847
2036
|
deployment: {
|
|
1848
2037
|
branch: "",
|
|
@@ -1897,7 +2086,7 @@ async function project_create_handler(args) {
|
|
|
1897
2086
|
const message = err?.message || "Could not start the device flow.";
|
|
1898
2087
|
const laneClosed = /CLI_PROJECT_CREATE_DISABLED|403/.test(String(message));
|
|
1899
2088
|
const serverMessage = "string" == typeof err?.serverMessage ? err.serverMessage.trim() : "";
|
|
1900
|
-
return fail("CreateStartError", laneClosed ? serverMessage || `Headless project creation is not open on this host yet. Create the project in the console instead, then run extension_auth (action: login) against it. (${message})` : String(message), laneClosed ? "lane-closed" : "create-failed", "E_PLATFORM");
|
|
2089
|
+
return fail("CreateStartError", laneClosed ? serverMessage || `Headless project creation is not open on this host yet. Create the project in the console instead, then run extension_auth (action: login) against it. (${message})` : String(message), laneClosed ? "lane-closed" : "create-failed", "E_PLATFORM", laneClosed ? laneClosedHint() : void 0);
|
|
1901
2090
|
}
|
|
1902
2091
|
deviceCode = start.deviceCode;
|
|
1903
2092
|
interval = start.interval;
|
|
@@ -1938,17 +2127,14 @@ async function project_create_handler(args) {
|
|
|
1938
2127
|
async function finishFromPoll(poll, ctx) {
|
|
1939
2128
|
if (!poll.ok) {
|
|
1940
2129
|
if ("pending" === poll.reason) return envelope_envelope({
|
|
1941
|
-
ok:
|
|
2130
|
+
ok: true,
|
|
1942
2131
|
command: project_create_COMMAND,
|
|
1943
2132
|
status: "authorization-pending",
|
|
1944
|
-
error: {
|
|
1945
|
-
code: "E_AUTH_PENDING",
|
|
1946
|
-
message: `Still waiting for approval at ${ctx.verificationUri}. Approve there, then call extension_project_create again with this same deviceCode.`
|
|
1947
|
-
},
|
|
1948
2133
|
value: {
|
|
1949
2134
|
verificationUri: ctx.verificationUri,
|
|
1950
2135
|
deviceCode: ctx.deviceCode
|
|
1951
|
-
}
|
|
2136
|
+
},
|
|
2137
|
+
hint: `Still waiting for approval at ${ctx.verificationUri}. Approve there, then call extension_project_create again with this same deviceCode.`
|
|
1952
2138
|
});
|
|
1953
2139
|
if ("denied" === poll.reason) return fail("CreateDenied", "Creating the project was denied at extension.dev/device.", "create-denied", "E_AUTH_DENIED");
|
|
1954
2140
|
if ("expired" === poll.reason) return fail("CreateExpired", "The device code expired. Run extension_project_create again to restart.", "create-expired", "E_AUTH_EXPIRED");
|
|
@@ -1981,7 +2167,9 @@ async function finishFromPoll(poll, ctx) {
|
|
|
1981
2167
|
description: ctx.args.description,
|
|
1982
2168
|
installCommand: ctx.args.installCommand,
|
|
1983
2169
|
buildCommand: ctx.args.buildCommand,
|
|
1984
|
-
outputDirectory: ctx.args.outputDirectory
|
|
2170
|
+
outputDirectory: ctx.args.outputDirectory,
|
|
2171
|
+
browsers: ctx.args.browsers,
|
|
2172
|
+
outputDirectories: ctx.args.outputDirectories
|
|
1985
2173
|
});
|
|
1986
2174
|
const url = `${ctx.apiBase}/api/cli/projects/create`;
|
|
1987
2175
|
let res;
|
|
@@ -2018,7 +2206,7 @@ async function finishFromPoll(poll, ctx) {
|
|
|
2018
2206
|
project: wantProject
|
|
2019
2207
|
}
|
|
2020
2208
|
});
|
|
2021
|
-
if ("CLI_PROJECT_CREATE_DISABLED" === code) return fail("CreateClosed", String(data.message || "Headless project creation is not open yet."), "lane-closed", "E_PLATFORM");
|
|
2209
|
+
if ("CLI_PROJECT_CREATE_DISABLED" === code) return fail("CreateClosed", String(data.message || "Headless project creation is not open yet."), "lane-closed", "E_PLATFORM", laneClosedHint());
|
|
2022
2210
|
const connectUrl = String(data.connectUrl || "").trim();
|
|
2023
2211
|
if ("INSTALLATION_ABSENT" === code || "INSTALLATION_ORG_UNSUPPORTED" === code || "INSTALLATION_AMBIGUOUS" === code) return envelope_envelope({
|
|
2024
2212
|
ok: false,
|
|
@@ -2077,7 +2265,247 @@ async function finishFromPoll(poll, ctx) {
|
|
|
2077
2265
|
hint: `Project ${finalWorkspace}/${finalProject} exists and its first build was dispatched. The provisioning grant is now spent and nothing was stored on this machine. Next: extension_auth (action: login, project: '${finalWorkspace}/${finalProject}') to mint the project token, then extension_publish to share it.`
|
|
2078
2266
|
});
|
|
2079
2267
|
}
|
|
2080
|
-
var templates_meta_snapshot_namespaceObject = JSON.parse('{"version":"2","sourceRepo":"extension-js/examples","generatorVersion":"2","commit":"8a565020924a0f8be3ae9fa1c807999ba1b938e4","generatedAt":"2026-08-05T08:25:06.981Z","templates":[{"slug":"action","name":"action","title":"Action (Popup)","version":"1.0.0","manifest_version":3,"description":"Adds a toolbar popup to the browser with a simple page.","uiContext":["action","background"],"surfaces":["action","background"],"entrypoints":["./action/index.html","background.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":true,"tags":["action","popup","javascript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Click the toolbar icon to open the action popup","Edit src/action/index.html and watch HMR update the popup"],"useCases":["Toolbar popup as the primary UI surface","Quick actions that don\'t need a full sidebar or page","Starting point for popup-based browser extensions"],"docsUrl":"https://extension.js.org/docs/implementation-guide/manifest-json","files":["extension.config.js","src/action/index.html","src/action/scripts.js","src/action/styles.css","src/background.js","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/action/screenshot.png","icon":"examples/action/src/images/icon.png"},{"slug":"action-locales","name":"action-locales","title":"Action Locales","version":"1.0.0","manifest_version":3,"description":"Adds a toolbar popup whose text follows your browser\'s language.","uiContext":["action","background"],"surfaces":["action","background"],"entrypoints":["./action/index.html","background.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/implementation-guide/locales","files":["_locales/en/messages.json","_locales/pt_BR/messages.json","extension.config.js","src/action/index.html","src/action/scripts.js","src/action/styles.css","src/background.js","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/action-locales/screenshot.png","icon":"examples/action-locales/src/images/icon.png"},{"slug":"ai-chatgpt","name":"ai-chatgpt","title":"ChatGPT AI Sidebar","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel where you can chat with ChatGPT about the page you are viewing.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.ts","content/scripts.ts"],"uiFramework":"react","css":"css","configFiles":["postcss.config.js","tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel","storage","activeTab","scripting","tabs"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":true,"tags":["ai","chatgpt","openai","gpt","chat","sidebar","content-scripts","react","shadcn"],"difficulty":"beginner","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","Open any web page and click the extension icon to open the sidebar","Enter your ChatGPT API key (from platform.openai.com)","Ask ChatGPT about the active tab, and the content script feeds the page text into the conversation"],"useCases":["Chat with the active tab\'s content using ChatGPT","Summarize or question articles, docs, and dashboards in place","Starting point for page-aware ChatGPT extensions"],"docsUrl":"https://platform.openai.com/docs/api-reference/chat","files":["extension.config.js","src/background.ts","src/components/ApiKeyForm.tsx","src/components/ChatInput.tsx","src/components/ChatMessage.tsx","src/components/ui/button.tsx","src/components/ui/card.tsx","src/components/ui/scroll-area.tsx","src/content/ContentApp.ts","src/content/scripts.ts","src/content/styles.css","src/images/icon.png","src/lib/client.ts","src/lib/page-context.ts","src/lib/utils.ts","src/manifest.json","src/sidebar/SidebarApp.tsx","src/sidebar/index.html","src/sidebar/scripts.tsx","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/ai-chatgpt/screenshot.png","icon":"examples/ai-chatgpt/src/images/icon.png"},{"slug":"ai-claude","name":"ai-claude","title":"Claude AI Sidebar","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel where you can chat with Claude about the page you are viewing.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.ts","content/scripts.ts"],"uiFramework":"react","css":"css","configFiles":["postcss.config.js","tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel","storage","activeTab","scripting","tabs"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":true,"tags":["ai","claude","anthropic","chat","sidebar","content-scripts","react","shadcn"],"difficulty":"beginner","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","Open any web page and click the extension icon to open the sidebar","Enter your Claude API key (from console.anthropic.com)","Ask Claude about the active tab, and the content script feeds the page text into the conversation"],"useCases":["Chat with the active tab\'s content using Claude","Summarize or question articles, docs, and dashboards in place","Starting point for page-aware Claude extensions"],"docsUrl":"https://docs.anthropic.com/en/api/getting-started","files":["extension.config.js","src/background.ts","src/components/ApiKeyForm.tsx","src/components/ChatInput.tsx","src/components/ChatMessage.tsx","src/components/ui/button.tsx","src/components/ui/card.tsx","src/components/ui/scroll-area.tsx","src/content/ContentApp.ts","src/content/scripts.ts","src/content/styles.css","src/images/icon.png","src/lib/client.ts","src/lib/page-context.ts","src/lib/utils.ts","src/manifest.json","src/sidebar/SidebarApp.tsx","src/sidebar/index.html","src/sidebar/scripts.tsx","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/ai-claude/screenshot.png","icon":"examples/ai-claude/src/images/icon.png"},{"slug":"ai-gemini","name":"ai-gemini","title":"Gemini AI Sidebar","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel where you can chat with Gemini about the page you are viewing.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.ts","content/scripts.ts"],"uiFramework":"react","css":"css","configFiles":["postcss.config.js","tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel","storage","activeTab","scripting","tabs"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":true,"tags":["ai","gemini","google","chat","sidebar","content-scripts","react","shadcn"],"difficulty":"beginner","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","Open any web page and click the extension icon to open the sidebar","Enter your Gemini API key (from aistudio.google.com)","Ask Gemini about the active tab, and the content script feeds the page text into the conversation"],"useCases":["Chat with the active tab\'s content using Gemini","Summarize or question articles, docs, and dashboards in place","Starting point for page-aware Gemini extensions"],"docsUrl":"https://ai.google.dev/gemini-api/docs","files":["extension.config.js","src/background.ts","src/components/ApiKeyForm.tsx","src/components/ChatInput.tsx","src/components/ChatMessage.tsx","src/components/ui/button.tsx","src/components/ui/card.tsx","src/components/ui/scroll-area.tsx","src/content/ContentApp.ts","src/content/scripts.ts","src/content/styles.css","src/images/icon.png","src/lib/client.ts","src/lib/page-context.ts","src/lib/utils.ts","src/manifest.json","src/sidebar/SidebarApp.tsx","src/sidebar/index.html","src/sidebar/scripts.tsx","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/ai-gemini/screenshot.png","icon":"examples/ai-gemini/src/images/icon.png"},{"slug":"ai-perplexity","name":"ai-perplexity","title":"Perplexity AI Sidebar","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel where you can chat with Perplexity about the page you are viewing.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.ts","content/scripts.ts"],"uiFramework":"react","css":"css","configFiles":["postcss.config.js","tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel","storage","activeTab","scripting","tabs"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":true,"tags":["ai","perplexity","search","chat","sidebar","content-scripts","react","shadcn"],"difficulty":"beginner","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","Open any web page and click the extension icon to open the sidebar","Enter your Perplexity API key (from perplexity.ai/settings/api)","Ask Perplexity about the active tab, and the content script feeds the page text into the conversation"],"useCases":["Chat with the active tab\'s content using Perplexity","Summarize or question articles, docs, and dashboards in place","Starting point for page-aware Perplexity extensions"],"docsUrl":"https://docs.perplexity.ai/","files":["extension.config.js","src/background.ts","src/components/ApiKeyForm.tsx","src/components/ChatInput.tsx","src/components/ChatMessage.tsx","src/components/ui/button.tsx","src/components/ui/card.tsx","src/components/ui/scroll-area.tsx","src/content/ContentApp.ts","src/content/scripts.ts","src/content/styles.css","src/images/icon.png","src/lib/client.ts","src/lib/page-context.ts","src/lib/utils.ts","src/manifest.json","src/sidebar/SidebarApp.tsx","src/sidebar/index.html","src/sidebar/scripts.tsx","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/ai-perplexity/screenshot.png","icon":"examples/ai-perplexity/src/images/icon.png"},{"slug":"content","name":"@extension-js/content","title":"Content scripts","version":"1.0.0","manifest_version":3,"description":"Injects a small badge into every web page you visit.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["background.js","content/scripts.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/implementation-guide/content-scripts","files":["extension.config.js","src/background.js","src/content/scripts.js","src/content/styles.css","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content/screenshot.png","icon":"examples/content/src/images/icon.png"},{"slug":"content-css-modules","name":"@extension-js/content-css-modules","title":"Content CSS Modules","version":"1.0.0","manifest_version":3,"description":"Injects a small styled badge into every web page you visit.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["background.js","content/scripts.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/css-modules","files":["extension.config.js","src/background.js","src/content/scripts.js","src/content/styles.module.css","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-css-modules/screenshot.png","icon":"examples/content-css-modules/src/images/icon.png"},{"slug":"content-custom-font","name":"content-custom-font","title":"Content Custom Font","version":"1.0.0","manifest_version":3,"description":"Injects a badge rendered in a custom font into every web page you visit.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["background.js","content/scripts.js"],"uiFramework":"","css":"css","configFiles":["postcss.config.js","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/implementation-guide/css","files":["extension.config.js","src/background.js","src/content/scripts.js","src/content/styles.css","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-custom-font/screenshot.png","icon":"examples/content-custom-font/src/images/icon.png"},{"slug":"content-env","name":"content-env","title":"Content Env","version":"1.0.0","manifest_version":3,"description":"Injects a small panel showing the extension\'s environment values into every web page you visit.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["background.ts","content/scripts.ts"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/features/environment-variables","files":["extension.config.js","src/background.ts","src/content/scripts.ts","src/content/styles.css","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-env/screenshot.png","icon":"examples/content-env/src/images/icon.png"},{"slug":"content-less","name":"@extension-js/content-less","title":"Content Less","version":"1.0.0","manifest_version":3,"description":"Injects a small styled badge into every web page you visit.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["background.js","content/scripts.js"],"uiFramework":"","css":"less","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":true,"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/less","files":["extension.config.js","src/background.js","src/content/scripts.js","src/content/styles.less","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-less/screenshot.png","icon":"examples/content-less/src/images/icon.png"},{"slug":"content-less-modules","name":"@extension-js/content-less-modules","title":"Content Less Modules","version":"1.0.0","manifest_version":3,"description":"Injects a small styled badge into every web page you visit.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["background.js","content/scripts.js"],"uiFramework":"","css":"less","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/less","files":["extension.config.js","src/background.js","src/content/scripts.js","src/content/styles.module.less","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-less-modules/screenshot.png","icon":"examples/content-less-modules/src/images/icon.png"},{"slug":"content-main-world","name":"content-main-world","title":"Main-world Content Script","version":"1.0.0","manifest_version":3,"description":"Injects a small UI into every web page from both the extension and the page\'s own JavaScript world.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["background.js","content/isolated-one.js","content/isolated-two.js","content/scripts.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":true,"tags":["content-scripts","main-world","isolated-world","javascript"],"difficulty":"intermediate","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","Open any page and watch the main-world script run alongside isolated entries","Inspect window.postMessage bridges between isolated and main worlds"],"useCases":["Hook into page-level globals that isolated content scripts cannot reach","Bridge isolated and main-world execution contexts cleanly","Reference for multi-group content_scripts in MV3"],"docsUrl":"https://extension.js.org/docs/implementation-guide/content-scripts","files":["extension.config.js","src/background.js","src/content/isolated-one.js","src/content/isolated-two.js","src/content/scripts.js","src/content/styles.css","src/content/utils/constants.js","src/content/utils/create-badge.js","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-main-world/screenshot.png","icon":"examples/content-main-world/src/images/icon.png"},{"slug":"content-multi-one-entry","name":"content-multi-one-entry","title":"Content Multi One Entry","version":"1.0.0","manifest_version":3,"description":"Injects four small elements into every web page you visit.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["background.js","content/script-top-left.js","content/script-top-right.js","content/script-bottom-left.js","content/script-bottom-right.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/implementation-guide/content-scripts","files":["extension.config.js","src/background.js","src/content/script-bottom-left.js","src/content/script-bottom-right.js","src/content/script-top-left.js","src/content/script-top-right.js","src/content/styles.css","src/content/utils/constants.js","src/content/utils/create-badge.js","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-multi-one-entry/screenshot.png","icon":"examples/content-multi-one-entry/src/images/icon.png"},{"slug":"content-multi-three-entries","name":"content-multi-three-entries","title":"Multi-entry Content Scripts","version":"1.0.0","manifest_version":3,"description":"Injects four small elements into every web page you visit.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["background.js","content/script-top-left.js","content/script-top-right.js","content/script-bottom-left.js","content/script-bottom-right.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":true,"tags":["content-scripts","multi-entry","javascript"],"difficulty":"intermediate","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","Open any page and three separate content scripts inject small elements","Edit any of the entries in src/content/ to see HMR update independently"],"useCases":["Split content-script logic across multiple entries with different match rules","Target different page contexts with focused, independent bundles","Reference for the multi-entry content_scripts pattern in MV3"],"docsUrl":"https://extension.js.org/docs/implementation-guide/content-scripts","files":["extension.config.js","src/background.js","src/content/script-bottom-left.js","src/content/script-bottom-right.js","src/content/script-top-left.js","src/content/script-top-right.js","src/content/styles.css","src/content/utils/constants.js","src/content/utils/create-badge.js","src/images/icon.png","src/manifest.json","src/screenshot.png"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-multi-three-entries/screenshot.png","icon":"examples/content-multi-three-entries/src/images/icon.png"},{"slug":"content-preact","name":"content-preact","title":"Content Preact","version":"1.0.0","manifest_version":3,"description":"Shows a small overlay UI on every web page you visit.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["background.ts","./content/scripts.tsx"],"uiFramework":"preact","css":"css","configFiles":["postcss.config.js","tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/preact","files":["extension.config.js","src/background.ts","src/content/ContentApp.tsx","src/content/scripts.tsx","src/content/styles.css","src/images/chromeWindow.png","src/images/icon.png","src/images/preact.png","src/images/tailwind.png","src/images/tailwind_bg.png","src/images/typescript.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-preact/screenshot.png","icon":"examples/content-preact/src/images/icon.png"},{"slug":"content-react","name":"content-react","title":"Content React","version":"1.0.0","manifest_version":3,"description":"Shows a small overlay UI on every web page you visit.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["background.ts","./content/scripts.tsx"],"uiFramework":"react","css":"css","configFiles":["postcss.config.js","tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/react","files":["extension.config.js","src/background.ts","src/content/ContentApp.tsx","src/content/scripts.tsx","src/content/styles.css","src/images/chromeWindow.png","src/images/icon.png","src/images/react.png","src/images/tailwind.png","src/images/tailwind_bg.png","src/images/typescript.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-react/screenshot.png","icon":"examples/content-react/src/images/icon.png"},{"slug":"content-sass","name":"@extension-js/content-sass","title":"Content Sass","version":"1.0.0","manifest_version":3,"description":"Injects a small styled badge into every web page you visit.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["background.js","content/scripts.js"],"uiFramework":"","css":"sass","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":true,"docsUrl":"https://extension.js.org/docs/implementation-guide/css","files":["extension.config.js","src/background.js","src/content/scripts.js","src/content/styles.scss","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-sass/screenshot.png","icon":"examples/content-sass/src/images/icon.png"},{"slug":"content-sass-modules","name":"@extension-js/content-sass-modules","title":"Content Sass Modules","version":"1.0.0","manifest_version":3,"description":"Injects a small styled badge into every web page you visit.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["background.js","content/scripts.js"],"uiFramework":"","css":"sass","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{"build":"extension build"},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/sass","files":["extension.config.js","src/background.js","src/content/scripts.js","src/content/styles.module.scss","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-sass-modules/screenshot.png","icon":"examples/content-sass-modules/src/images/icon.png"},{"slug":"content-svelte","name":"content-svelte","title":"Content Svelte","version":"1.0.0","manifest_version":3,"description":"Shows a small overlay UI on every web page you visit.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["background.ts","./content/scripts.ts"],"uiFramework":"svelte","css":"css","configFiles":["postcss.config.js","tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/svelte","files":["extension.config.js","src/background.ts","src/content/ContentApp.svelte","src/content/scripts.ts","src/content/styles.css","src/content/svelte.d.ts","src/images/chromeWindow.png","src/images/icon.png","src/images/svelte.png","src/images/tailwind.png","src/images/tailwind_bg.png","src/images/typescript.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-svelte/screenshot.png","icon":"examples/content-svelte/src/images/icon.png"},{"slug":"content-typescript","name":"content-typescript","title":"Content TypeScript","version":"1.0.0","manifest_version":3,"description":"Shows a small overlay UI on every web page you visit.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["background.ts","content/scripts.ts"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/typescript","files":["extension.config.js","src/background.ts","src/content/scripts.ts","src/content/styles.css","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-typescript/screenshot.png","icon":"examples/content-typescript/src/images/icon.png"},{"slug":"content-vue","name":"content-vue","title":"Content Vue","version":"1.0.0","manifest_version":3,"description":"Shows a small overlay UI on every web page you visit.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["background.ts","./content/scripts.ts"],"uiFramework":"vue","css":"css","configFiles":["postcss.config.js","tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/vue","files":["extension.config.js","src/background.ts","src/content/ContentApp.vue","src/content/scripts.ts","src/content/shims-vue.d.ts","src/content/styles.css","src/images/chromeWindow.png","src/images/icon.png","src/images/logo.svg","src/images/tailwind.png","src/images/tailwind_bg.png","src/images/typescript.png","src/images/vue.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-vue/screenshot.png","icon":"examples/content-vue/src/images/logo.svg"},{"slug":"init","name":"init","title":"Initial","version":"1.0.0","manifest_version":3,"description":"A minimal extension that loads in your browser, ready to build on.","uiContext":[],"surfaces":[],"entrypoints":[],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":false,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/getting-started/immediately","files":["extension.config.js","src/images/icon.png","src/images/javascript.png","src/index.js","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/init/screenshot.png","icon":"examples/init/src/images/icon.png"},{"slug":"javascript","name":"javascript","title":"JavaScript","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel to the browser with a simple page.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.js","content/scripts.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel"],"host_permissions":[],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/getting-started/templates/javascript","files":["extension.config.js","src/background.js","src/content/ContentApp.js","src/content/scripts.js","src/content/styles.css","src/images/icon.png","src/manifest.json","src/sidebar/SidebarApp.js","src/sidebar/index.html","src/sidebar/scripts.js","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/javascript/screenshot.png","icon":"examples/javascript/src/images/icon.png"},{"slug":"new","name":"new","title":"New Tab","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a simple page you can customize.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/implementation-guide/html","files":["extension.config.js","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/scripts.js","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new/screenshot.png","icon":"examples/new/src/images/icon.png"},{"slug":"new-browser-flags","name":"new-browser-flags","title":"Newtab Browser Flags","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a launcher that opens the extension\'s sidebar panel.","uiContext":["content","newtab","sidebar","background"],"surfaces":["content","newtab","sidebar","background"],"entrypoints":["newtab/index.html","sidebar/index.html","background.js","content/content.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{"build":"extension build"},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting","notifications"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/browsers/browser-flags","files":["extension.config.js","src/background.js","src/content/content.js","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/newtab.css","src/newtab/newtab.js","src/sidebar/index.html","src/sidebar/sidebar.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-browser-flags/screenshot.png","icon":"examples/new-browser-flags/src/images/icon.png"},{"slug":"new-config-eslint","name":"new-config-eslint","title":"Newtab Config ESLint","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a simple page.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js","eslint.config.mjs"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/integrations/eslint","files":["extension.config.js","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/scripts.ts","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-config-eslint/screenshot.png","icon":"examples/new-config-eslint/src/images/icon.png"},{"slug":"new-config-prettier","name":"new-config-prettier","title":"Newtab Config Prettier","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a simple page.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js",".prettierrc"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/integrations/prettier","files":["extension.config.js","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/scripts.ts","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-config-prettier/screenshot.png","icon":"examples/new-config-prettier/src/images/icon.png"},{"slug":"new-config-stylelint","name":"new-config-stylelint","title":"Newtab Config Stylelint","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a simple page.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"","css":"sass","configFiles":[".stylelintrc.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/integrations/stylelint","files":["extension.config.js","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/scripts.js","src/newtab/styles.scss"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-config-stylelint/screenshot.png","icon":"examples/new-config-stylelint/src/images/icon.png"},{"slug":"new-crypto","name":"new-crypto","title":"Newtab Crypto","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a page that hashes any text you type using SHA-256.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/node","files":["extension.config.js","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/scripts.ts","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-crypto/screenshot.png","icon":"examples/new-crypto/src/images/icon.png"},{"slug":"new-env","name":"new-env","title":"Environment Variables","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a page showing the extension\'s environment values.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":true,"tags":["env","configuration","new-tab","javascript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Copy .env.example to .env and edit the values","Run npm run dev to launch the extension","Open a new tab and the page reads injected EXTENSION_PUBLIC_* values"],"useCases":["Inject build-time configuration without leaking secrets","Switch behavior per browser, mode, or deployment target","Reference for safe env-var usage across JS, JSON, and HTML"],"docsUrl":"https://extension.js.org/docs/features/environment-variables","files":["extension.config.js","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/scripts.ts","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-env/screenshot.png","icon":"examples/new-env/src/images/icon.png"},{"slug":"new-less","name":"new-less","title":"Newtab Less","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a simple styled page.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"","css":"less","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/less","files":["extension.config.js","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/scripts.js","src/newtab/styles.less"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-less/screenshot.png","icon":"examples/new-less/src/images/icon.png"},{"slug":"new-preact","name":"new-preact","title":"Newtab Preact","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a simple Preact page.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"preact","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/preact","files":["extension.config.js","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/NewTabApp.tsx","src/newtab/index.html","src/newtab/scripts.tsx","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-preact/screenshot.png","icon":"examples/new-preact/src/images/icon.png"},{"slug":"new-react","name":"new-react","title":"Newtab React","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a simple React page.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"react","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/react","files":["extension.config.js","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/NewTabApp.tsx","src/newtab/index.html","src/newtab/scripts.tsx","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-react/screenshot.png","icon":"examples/new-react/src/images/icon.png"},{"slug":"new-react-router","name":"new-react-router","title":"Newtab React Router","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a small React app you can navigate across routes.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"react","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/react","files":["extension.config.js","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/NewTabApp.tsx","src/newtab/index.html","src/newtab/scripts.tsx","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-react-router/screenshot.png","icon":"examples/new-react-router/src/images/icon.png"},{"slug":"new-sass","name":"new-sass","title":"Newtab Sass","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a simple styled page.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"","css":"sass","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/sass","files":["extension.config.js","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/scripts.js","src/newtab/styles.scss"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-sass/screenshot.png","icon":"examples/new-sass/src/images/icon.png"},{"slug":"new-svelte","name":"new-svelte","title":"Newtab Svelte","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a simple Svelte page.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"svelte","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/svelte","files":["extension.config.js","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/NewTabApp.svelte","src/newtab/index.html","src/newtab/scripts.ts","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-svelte/screenshot.png","icon":"examples/new-svelte/src/images/icon.png"},{"slug":"new-typescript","name":"new-typescript","title":"Newtab TypeScript","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a simple page.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/typescript","files":["extension.config.js","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/scripts.ts","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-typescript/screenshot.png","icon":"examples/new-typescript/src/images/icon.png"},{"slug":"new-vue","name":"new-vue","title":"Newtab Vue","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a simple Vue page.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"vue","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/vue","files":["extension.config.js","src/background.js","src/images/icon.png","src/manifest.json","src/newtab/NewTabApp.vue","src/newtab/index.html","src/newtab/scripts.ts","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/new-vue/screenshot.png","icon":"examples/new-vue/src/images/icon.png"},{"slug":"playwright","name":"playwright","title":"Playwright E2E Sidebar","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel to the browser, covered end to end by Playwright tests.","uiContext":["sidebar","action","background"],"surfaces":["action","sidebar","background"],"entrypoints":["sidebar/index.html","background.ts"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{"test":"playwright test"},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel"],"host_permissions":[],"optional_permissions":[],"featured":true,"tags":["testing","playwright","e2e","sidebar","typescript"],"difficulty":"intermediate","timeToFirstSuccessMinutes":4,"firstSteps":["Run npm install to set up Playwright deps","Run npm run dev to launch the extension","Run npm test to execute the Playwright spec against the loaded extension"],"useCases":["End-to-end testing for browser-extension surfaces (popup, sidebar, content)","CI-ready Playwright setup wired to Extension.js builds","Reference for automating extension flows in headed and headless modes"],"docsUrl":"https://extension.js.org/docs/workflows/playwright-e2e","files":["extension.config.js","src/background.ts","src/images/icon.png","src/manifest.json","src/sidebar/SidebarApp.ts","src/sidebar/index.html","src/sidebar/scripts.ts","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/playwright/screenshot.png","icon":"examples/playwright/src/images/icon.png"},{"slug":"preact","name":"preact","title":"Preact","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel to the browser with a simple Preact page.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.ts","content/scripts.tsx"],"uiFramework":"preact","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel"],"host_permissions":[],"optional_permissions":[],"featured":true,"docsUrl":"https://extension.js.org/docs/getting-started/templates/preact","files":["extension.config.js","src/background.ts","src/content/ContentApp.tsx","src/content/scripts.tsx","src/content/styles.css","src/images/icon.png","src/manifest.json","src/public/extension.svg","src/public/logo.svg","src/sidebar/SidebarApp.tsx","src/sidebar/index.html","src/sidebar/scripts.tsx","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/preact/screenshot.png","icon":"examples/preact/src/images/icon.png"},{"slug":"react","name":"react","title":"React","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel to the browser with a simple React page.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.ts","content/scripts.tsx"],"uiFramework":"react","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel"],"host_permissions":[],"optional_permissions":[],"featured":true,"docsUrl":"https://extension.js.org/docs/getting-started/templates/react","files":["extension.config.js","src/background.ts","src/content/ContentApp.tsx","src/content/scripts.tsx","src/content/styles.css","src/images/icon.png","src/manifest.json","src/sidebar/SidebarApp.tsx","src/sidebar/index.html","src/sidebar/scripts.tsx","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/react/screenshot.png","icon":"examples/react/src/images/icon.png"},{"slug":"sidebar","name":"sidebar","title":"Sidebar page","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel to the browser with a simple page.","uiContext":["sidebar","action","background"],"surfaces":["action","sidebar","background"],"entrypoints":["sidebar/index.html","background.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel"],"host_permissions":[],"optional_permissions":[],"featured":true,"docsUrl":"https://extension.js.org/docs/implementation-guide/html","files":["extension.config.js","src/background.js","src/images/icon.png","src/manifest.json","src/sidebar/index.html","src/sidebar/scripts.js","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/sidebar/screenshot.png","icon":"examples/sidebar/src/images/icon.png"},{"slug":"sidebar-antd","name":"sidebar-antd","title":"Sidebar Ant Design","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel to the browser showing Ant Design components.","uiContext":["sidebar","action","background"],"surfaces":["action","sidebar","background"],"entrypoints":["sidebar/index.html","background.js"],"uiFramework":"react","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel"],"host_permissions":[],"optional_permissions":[],"featured":false,"docsUrl":"https://ant.design/docs/react/introduce","files":["extension.config.js","src/background.js","src/images/icon.png","src/manifest.json","src/sidebar/SidebarApp.jsx","src/sidebar/index.html","src/sidebar/scripts.jsx","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/sidebar-antd/screenshot.png","icon":"examples/sidebar-antd/src/images/icon.png"},{"slug":"sidebar-monorepo-turborepo","name":"sidebar-monorepo-turborepo","title":"Sidebar Turborepo","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel and shows a small overlay UI on every web page you visit.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.js","content/scripts.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{"dev":"extension dev packages/extension","build":"extension build packages/extension"},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel"],"host_permissions":[],"optional_permissions":[],"featured":true,"docsUrl":"https://turborepo.dev/docs","files":["extension.config.js","packages/analytics/src/index.js","packages/extension/src/background.js","packages/extension/src/content/scripts.js","packages/extension/src/content/styles.css","packages/extension/src/images/icon.png","packages/extension/src/manifest.json","packages/extension/src/sidebar/SidebarApp.js","packages/extension/src/sidebar/index.html","packages/extension/src/sidebar/scripts.js","packages/extension/src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/sidebar-monorepo-turborepo/screenshot.png","icon":null},{"slug":"sidebar-shadcn","name":"sidebar-shadcn","title":"Sidebar shadcn","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel to the browser built with shadcn/ui components.","uiContext":["sidebar","action","background"],"surfaces":["action","sidebar","background"],"entrypoints":["sidebar/index.html","background.ts"],"uiFramework":"react","css":"css","configFiles":["postcss.config.js","tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel"],"host_permissions":[],"optional_permissions":[],"featured":true,"docsUrl":"https://extension.js.org/docs/integrations/shadcnui","files":["extension.config.js","src/background.ts","src/components/ui/button.tsx","src/components/ui/card.tsx","src/components/ui/label.tsx","src/components/ui/switch.tsx","src/images/icon.png","src/lib/utils.ts","src/manifest.json","src/sidebar/SidebarApp.tsx","src/sidebar/index.html","src/sidebar/scripts.tsx","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/sidebar-shadcn/screenshot.png","icon":"examples/sidebar-shadcn/src/images/icon.png"},{"slug":"special-folders-pages","name":"special-folders-pages","title":"Special Folders: pages/","version":"1.0.0","manifest_version":3,"description":"Opens a welcome page in a new tab when the extension loads.","uiContext":["background"],"surfaces":["background"],"entrypoints":["background.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":true,"tags":["special-folders","pages","welcome-page","javascript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","A welcome page from src/pages/ opens automatically on install","Add another HTML file under src/pages/ and it ships as a separate page"],"useCases":["Onboarding, help, and welcome pages that ship with the extension","Standalone HTML routes without manifest plumbing","Reference for Extension.js\'s pages/ special folder convention"],"docsUrl":"https://extension.js.org/docs/features/special-folders","files":["extension.config.js","pages/custom.html","pages/main.html","pages/main.js","pages/welcome.html","public/css/file.css","public/html/file.html","public/js/file.js","src/background.js","src/images/icon.png","src/images/javascript.png","src/manifest.json","src/sandbox/index.html","src/sandbox/scripts.js","src/sandbox/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/special-folders-pages/screenshot.png","icon":"examples/special-folders-pages/src/images/icon.png"},{"slug":"special-folders-scripts","name":"special-folders-scripts","title":"Special Folders Scripts","version":"1.0.0","manifest_version":3,"description":"Shows a small overlay on web pages and a toolbar popup that runs the extension\'s bundled scripts.","uiContext":["content","action","background"],"surfaces":["content","action","background"],"entrypoints":["background.js","content/scripts.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"docsUrl":"https://extension.js.org/docs/features/special-folders","files":["extension.config.js","scripts/script-one.js","scripts/script-three.js","scripts/script-two.js","src/background.js","src/content/scripts.js","src/content/styles.css","src/images/icon.png","src/images/javascript.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/special-folders-scripts/screenshot.png","icon":"examples/special-folders-scripts/src/images/icon.png"},{"slug":"svelte","name":"svelte","title":"Svelte","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel to the browser with a simple Svelte page.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.ts","content/scripts.ts"],"uiFramework":"svelte","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel"],"host_permissions":[],"optional_permissions":[],"featured":true,"docsUrl":"https://extension.js.org/docs/getting-started/templates/svelte","files":["extension.config.js","src/background.ts","src/content/ContentApp.svelte","src/content/scripts.ts","src/content/styles.css","src/images/icon.png","src/manifest.json","src/public/extension.svg","src/sidebar/SidebarApp.svelte","src/sidebar/index.html","src/sidebar/scripts.ts","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/svelte/screenshot.png","icon":"examples/svelte/src/images/icon.png"},{"slug":"transformers-js","name":"transformers-js","title":"Transformers.js","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel that classifies the active page or your selected text with on-device AI models.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.js","content/scripts.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel","storage","unlimitedStorage","activeTab","scripting","tabs","contextMenus"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":true,"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/webassembly","files":["extension.config.js","src/background.js","src/constants.js","src/content/scripts.js","src/images/icon.png","src/manifest.json","src/sidebar/SidebarApp.js","src/sidebar/index.html","src/sidebar/sakura.css","src/sidebar/scripts.js","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/transformers-js/screenshot.png","icon":"examples/transformers-js/src/images/icon.png"},{"slug":"typescript","name":"typescript","title":"TypeScript","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel to the browser with a simple page.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.ts","content/scripts.ts"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel"],"host_permissions":[],"optional_permissions":[],"featured":true,"docsUrl":"https://extension.js.org/docs/getting-started/templates/typescript","files":["extension.config.js","src/background.ts","src/content/ContentApp.ts","src/content/scripts.ts","src/content/styles.css","src/content/types.d.ts","src/images/icon.png","src/manifest.json","src/sidebar/SidebarApp.ts","src/sidebar/index.html","src/sidebar/scripts.ts","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/typescript/screenshot.png","icon":"examples/typescript/src/images/icon.png"},{"slug":"vue","name":"vue","title":"Vue","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel to the browser with a simple Vue page.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.ts","content/scripts.ts"],"uiFramework":"vue","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel"],"host_permissions":[],"optional_permissions":[],"featured":true,"docsUrl":"https://extension.js.org/docs/getting-started/templates/vue","files":["extension.config.js","src/background.ts","src/content/ContentApp.vue","src/content/scripts.ts","src/content/styles.css","src/content/types.d.ts","src/images/icon.png","src/manifest.json","src/sidebar/SidebarApp.vue","src/sidebar/index.html","src/sidebar/scripts.ts","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/vue/screenshot.png","icon":"examples/vue/src/images/icon.png"}]}');
|
|
2268
|
+
const workspace_create_COMMAND = "extension_workspace_create";
|
|
2269
|
+
const workspace_create_FIRST_CALL_BUDGET_MS = 8000;
|
|
2270
|
+
const workspace_create_RESUME_BUDGET_MS = 22000;
|
|
2271
|
+
const SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/i;
|
|
2272
|
+
const workspace_create_schema = {
|
|
2273
|
+
name: workspace_create_COMMAND,
|
|
2274
|
+
description: "Create an extension.dev workspace that does not exist yet, without opening the console. Use it before extension_project_create when the project's workspace is not there: project creation can only target an existing workspace, and this tool is what brings one into existence. Two-phase, like login: the first call returns a code and a URL where a signed-in GitHub user approves creating exactly this workspace and becomes its owner; call again with the returned deviceCode to finish. Check `ownerGithubLogin` in the answer: whoever approved the code owns the workspace. The approval mints a grant that lives minutes, can only create the one named workspace, names no project, and is never stored on this machine. Then run extension_project_create against '<workspace>/<project>'.",
|
|
2275
|
+
inputSchema: {
|
|
2276
|
+
type: "object",
|
|
2277
|
+
properties: {
|
|
2278
|
+
workspace: {
|
|
2279
|
+
type: "string",
|
|
2280
|
+
description: "Slug of the new workspace, lowercase letters, digits and hyphens, no slash. It must not exist yet; a personal workspace (the GitHub login) already exists for every signed-in user, so this is for a shared or organization-style workspace."
|
|
2281
|
+
},
|
|
2282
|
+
displayName: {
|
|
2283
|
+
type: "string",
|
|
2284
|
+
description: "Human name for the workspace. Defaults to the slug."
|
|
2285
|
+
},
|
|
2286
|
+
description: {
|
|
2287
|
+
type: "string",
|
|
2288
|
+
description: "Short workspace description. Optional."
|
|
2289
|
+
},
|
|
2290
|
+
developerUrl: {
|
|
2291
|
+
type: "string",
|
|
2292
|
+
description: "Public URL for the workspace. Optional."
|
|
2293
|
+
},
|
|
2294
|
+
deviceCode: {
|
|
2295
|
+
type: "string",
|
|
2296
|
+
description: "Resume token from the prior call's `deviceCode`; omit on the first call."
|
|
2297
|
+
},
|
|
2298
|
+
api: API_BASE
|
|
2299
|
+
},
|
|
2300
|
+
required: [
|
|
2301
|
+
"workspace"
|
|
2302
|
+
]
|
|
2303
|
+
}
|
|
2304
|
+
};
|
|
2305
|
+
function workspace_create_fail(name, message, status, code, hint) {
|
|
2306
|
+
return envelope_envelope({
|
|
2307
|
+
ok: false,
|
|
2308
|
+
command: workspace_create_COMMAND,
|
|
2309
|
+
status,
|
|
2310
|
+
error: {
|
|
2311
|
+
code,
|
|
2312
|
+
name,
|
|
2313
|
+
message
|
|
2314
|
+
},
|
|
2315
|
+
...hint ? {
|
|
2316
|
+
hint
|
|
2317
|
+
} : {}
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
function workspace_create_laneClosedHint() {
|
|
2321
|
+
return `Create the workspace in the console at ${consoleBase()} (workspace switcher, New workspace), then run extension_project_create against it. Headless workspace creation opens with the platform; during the public hold only an allowlisted workspace slug can be created this way.`;
|
|
2322
|
+
}
|
|
2323
|
+
function workspace_create_pendingEnvelope(start) {
|
|
2324
|
+
const complete = String(start.verificationUriComplete || "").trim();
|
|
2325
|
+
const hasCompleteLink = complete.length > 0 && complete !== start.verificationUri;
|
|
2326
|
+
const message = hasCompleteLink ? `Open ${complete} and approve creating the workspace (code ${start.userCode} is pre-filled), then call ${workspace_create_COMMAND} again with this deviceCode and the same arguments. If the page asks for a code, enter ${start.userCode} at ${start.verificationUri}. The GitHub account that approves becomes the workspace owner.` : `Open ${start.verificationUri}, enter code ${start.userCode}, approve creating the workspace, then call ${workspace_create_COMMAND} again with this deviceCode and the same arguments. The GitHub account that approves becomes the workspace owner.`;
|
|
2327
|
+
return envelope_envelope({
|
|
2328
|
+
ok: true,
|
|
2329
|
+
command: workspace_create_COMMAND,
|
|
2330
|
+
status: "authorization-pending",
|
|
2331
|
+
value: {
|
|
2332
|
+
userCode: start.userCode,
|
|
2333
|
+
verificationUri: start.verificationUri,
|
|
2334
|
+
...hasCompleteLink ? {
|
|
2335
|
+
verificationUriComplete: complete
|
|
2336
|
+
} : {},
|
|
2337
|
+
deviceCode: start.deviceCode
|
|
2338
|
+
},
|
|
2339
|
+
hint: message
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
async function workspace_create_handler(args) {
|
|
2343
|
+
const workspace = String(args.workspace || "").trim().toLowerCase();
|
|
2344
|
+
if (!SLUG_PATTERN.test(workspace)) return workspace_create_fail("BadRequest", "workspace must be a single slug: lowercase letters, digits and hyphens, no slash, at most 64 characters.", "bad-request", "E_BAD_REQUEST");
|
|
2345
|
+
const apiCheck = safeApiBase(resolveApiBase(args.api), args.api);
|
|
2346
|
+
if (!apiCheck.ok) return workspace_create_fail("ConfigError", apiCheck.message, "bad-request", "E_BAD_REQUEST");
|
|
2347
|
+
const apiBase = apiCheck.base;
|
|
2348
|
+
let config;
|
|
2349
|
+
try {
|
|
2350
|
+
config = await fetchLoginConfig(apiBase);
|
|
2351
|
+
} catch (err) {
|
|
2352
|
+
return workspace_create_fail("ConfigError", err?.message || "Could not load login config.", "create-failed", "E_PLATFORM");
|
|
2353
|
+
}
|
|
2354
|
+
let deviceCode = String(args.deviceCode || "").trim();
|
|
2355
|
+
let interval = 5;
|
|
2356
|
+
let budgetMs = workspace_create_RESUME_BUDGET_MS;
|
|
2357
|
+
if (!deviceCode) {
|
|
2358
|
+
let start;
|
|
2359
|
+
try {
|
|
2360
|
+
start = await requestDeviceCode({
|
|
2361
|
+
apiBase,
|
|
2362
|
+
path: config.deviceCodeUrl,
|
|
2363
|
+
workspace,
|
|
2364
|
+
intent: "create-workspace"
|
|
2365
|
+
});
|
|
2366
|
+
} catch (err) {
|
|
2367
|
+
const message = err?.message || "Could not start the device flow.";
|
|
2368
|
+
const laneClosed = /CLI_WORKSPACE_CREATE_DISABLED|403/.test(String(message));
|
|
2369
|
+
const serverMessage = "string" == typeof err?.serverMessage ? err.serverMessage.trim() : "";
|
|
2370
|
+
return workspace_create_fail("CreateStartError", laneClosed ? serverMessage || `Headless workspace creation is not open on this host yet. Create the workspace in the console instead. (${message})` : String(message), laneClosed ? "lane-closed" : "create-failed", "E_PLATFORM", laneClosed ? workspace_create_laneClosedHint() : void 0);
|
|
2371
|
+
}
|
|
2372
|
+
deviceCode = start.deviceCode;
|
|
2373
|
+
interval = start.interval;
|
|
2374
|
+
budgetMs = workspace_create_FIRST_CALL_BUDGET_MS;
|
|
2375
|
+
const early = await pollDeviceGrant({
|
|
2376
|
+
apiBase,
|
|
2377
|
+
path: config.deviceTokenUrl,
|
|
2378
|
+
workspace,
|
|
2379
|
+
deviceCode,
|
|
2380
|
+
interval,
|
|
2381
|
+
budgetMs
|
|
2382
|
+
});
|
|
2383
|
+
if (!early.ok && "pending" === early.reason) return workspace_create_pendingEnvelope(start);
|
|
2384
|
+
return workspace_create_finishFromPoll(early, {
|
|
2385
|
+
apiBase,
|
|
2386
|
+
workspace,
|
|
2387
|
+
args,
|
|
2388
|
+
verificationUri: config.verificationUri,
|
|
2389
|
+
deviceCode
|
|
2390
|
+
});
|
|
2391
|
+
}
|
|
2392
|
+
const poll = await pollDeviceGrant({
|
|
2393
|
+
apiBase,
|
|
2394
|
+
path: config.deviceTokenUrl,
|
|
2395
|
+
workspace,
|
|
2396
|
+
deviceCode,
|
|
2397
|
+
interval,
|
|
2398
|
+
budgetMs
|
|
2399
|
+
});
|
|
2400
|
+
return workspace_create_finishFromPoll(poll, {
|
|
2401
|
+
apiBase,
|
|
2402
|
+
workspace,
|
|
2403
|
+
args,
|
|
2404
|
+
verificationUri: config.verificationUri,
|
|
2405
|
+
deviceCode
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
async function workspace_create_finishFromPoll(poll, ctx) {
|
|
2409
|
+
if (!poll.ok) {
|
|
2410
|
+
if ("pending" === poll.reason) return envelope_envelope({
|
|
2411
|
+
ok: true,
|
|
2412
|
+
command: workspace_create_COMMAND,
|
|
2413
|
+
status: "authorization-pending",
|
|
2414
|
+
value: {
|
|
2415
|
+
verificationUri: ctx.verificationUri,
|
|
2416
|
+
deviceCode: ctx.deviceCode
|
|
2417
|
+
},
|
|
2418
|
+
hint: `Still waiting for approval at ${ctx.verificationUri}. Approve there, then call ${workspace_create_COMMAND} again with this same deviceCode.`
|
|
2419
|
+
});
|
|
2420
|
+
if ("denied" === poll.reason) return workspace_create_fail("CreateDenied", "Creating the workspace was denied at extension.dev/device.", "create-denied", "E_AUTH_DENIED");
|
|
2421
|
+
if ("expired" === poll.reason) return workspace_create_fail("CreateExpired", `The device code expired. Run ${workspace_create_COMMAND} again to restart.`, "create-expired", "E_AUTH_EXPIRED");
|
|
2422
|
+
return workspace_create_fail("CreateAuthError", poll.message || "Device authorization failed.", "create-failed", "E_AUTH_FAILED");
|
|
2423
|
+
}
|
|
2424
|
+
const grant = poll.data;
|
|
2425
|
+
const token = String(grant.token || "").trim();
|
|
2426
|
+
const workspaceSlug = String(grant.workspaceSlug || "").trim().toLowerCase();
|
|
2427
|
+
if (workspaceSlug !== ctx.workspace) return workspace_create_fail("CreateScopeError", `The approval was scoped to workspace '${workspaceSlug}', not the requested '${ctx.workspace}'. Nothing was created. Run ${workspace_create_COMMAND} again with the intended workspace.`, "create-failed", "E_AUTH_FAILED");
|
|
2428
|
+
if ("workspace-provisioning" !== String(grant.tokenKind || "")) return envelope_envelope({
|
|
2429
|
+
ok: false,
|
|
2430
|
+
command: workspace_create_COMMAND,
|
|
2431
|
+
status: "workspace-exists",
|
|
2432
|
+
error: {
|
|
2433
|
+
code: "E_PLATFORM",
|
|
2434
|
+
message: `Workspace '${ctx.workspace}' already exists on this host, so there is nothing to create.`
|
|
2435
|
+
},
|
|
2436
|
+
hint: `Run extension_project_create with project '${ctx.workspace}/<project>' instead.`
|
|
2437
|
+
});
|
|
2438
|
+
const url = `${ctx.apiBase}/api/cli/workspaces/create`;
|
|
2439
|
+
let res;
|
|
2440
|
+
try {
|
|
2441
|
+
res = await fetch(url, {
|
|
2442
|
+
method: "POST",
|
|
2443
|
+
headers: {
|
|
2444
|
+
authorization: `Bearer ${token}`,
|
|
2445
|
+
"content-type": "application/json",
|
|
2446
|
+
...identityHeaders(workspace_create_COMMAND)
|
|
2447
|
+
},
|
|
2448
|
+
body: JSON.stringify({
|
|
2449
|
+
displayName: String(ctx.args.displayName || ctx.workspace).trim(),
|
|
2450
|
+
description: String(ctx.args.description || "").trim(),
|
|
2451
|
+
developerUrl: String(ctx.args.developerUrl || "").trim()
|
|
2452
|
+
})
|
|
2453
|
+
});
|
|
2454
|
+
} catch (err) {
|
|
2455
|
+
return workspace_create_fail("CreateNetworkError", `Could not reach ${url}: ${err?.message || err}`, "create-failed", "E_NETWORK");
|
|
2456
|
+
}
|
|
2457
|
+
const text = await res.text();
|
|
2458
|
+
let data;
|
|
2459
|
+
try {
|
|
2460
|
+
data = JSON.parse(text);
|
|
2461
|
+
} catch {
|
|
2462
|
+
data = {
|
|
2463
|
+
message: text
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2466
|
+
if (!res.ok) {
|
|
2467
|
+
const code = String(data.code || "");
|
|
2468
|
+
if (sawPlatformHold(res, data)) return platformHoldEnvelope({
|
|
2469
|
+
command: workspace_create_COMMAND,
|
|
2470
|
+
name: "CreateHeld",
|
|
2471
|
+
body: data,
|
|
2472
|
+
value: {
|
|
2473
|
+
workspace: ctx.workspace
|
|
2474
|
+
}
|
|
2475
|
+
});
|
|
2476
|
+
if ("CLI_WORKSPACE_CREATE_DISABLED" === code) return workspace_create_fail("CreateClosed", String(data.message || "Headless workspace creation is not open yet."), "lane-closed", "E_PLATFORM", workspace_create_laneClosedHint());
|
|
2477
|
+
if ("WORKSPACE_EXISTS" === code || "WORKSPACE_SLUG_RESERVED" === code) return envelope_envelope({
|
|
2478
|
+
ok: false,
|
|
2479
|
+
command: workspace_create_COMMAND,
|
|
2480
|
+
status: "workspace-exists",
|
|
2481
|
+
error: {
|
|
2482
|
+
code: "E_PLATFORM",
|
|
2483
|
+
message: String(data.message || `Workspace '${ctx.workspace}' cannot be created.`)
|
|
2484
|
+
},
|
|
2485
|
+
hint: "WORKSPACE_EXISTS" === code ? `Run extension_project_create with project '${ctx.workspace}/<project>' instead.` : "Pick a different slug and run the tool again."
|
|
2486
|
+
});
|
|
2487
|
+
return workspace_create_fail("CreateError", `create failed (${res.status}): ${String(data.message || data.error || text || "unknown error").slice(0, 500)}`, "create-failed", "E_PLATFORM");
|
|
2488
|
+
}
|
|
2489
|
+
const finalSlug = String(data.slug || ctx.workspace);
|
|
2490
|
+
const owner = String(data.ownerGithubLogin || grant.ownerGithubLogin || "");
|
|
2491
|
+
return envelope_envelope({
|
|
2492
|
+
ok: true,
|
|
2493
|
+
command: workspace_create_COMMAND,
|
|
2494
|
+
status: "created",
|
|
2495
|
+
value: {
|
|
2496
|
+
workspaceSlug: finalSlug,
|
|
2497
|
+
workspaceId: data.id ?? null,
|
|
2498
|
+
displayName: data.displayName ?? finalSlug,
|
|
2499
|
+
ownerGithubLogin: owner || null,
|
|
2500
|
+
consoleUrl: `${consoleBase()}/${encodeURIComponent(finalSlug)}`,
|
|
2501
|
+
nextSteps: [
|
|
2502
|
+
`extension_project_create (project: '${finalSlug}/<project>', repo: '<owner>/<repo>')`
|
|
2503
|
+
]
|
|
2504
|
+
},
|
|
2505
|
+
hint: `Workspace ${finalSlug} exists${owner ? `, owned by ${owner}` : ""}. The grant is now spent and nothing was stored on this machine. If ${owner || "the approver"} is not who you meant to own it, stop here: ownership follows the GitHub account that approved the code. Next: extension_project_create with project '${finalSlug}/<project>'.`
|
|
2506
|
+
});
|
|
2507
|
+
}
|
|
2508
|
+
var templates_meta_snapshot_namespaceObject = JSON.parse('{"version":"2","sourceRepo":"extension-js/examples","generatorVersion":"2","commit":"2c3a486af58dc1e0819d940db6a32e11426ef5eb","generatedAt":"2026-09-29T11:55:46.874Z","templates":[{"slug":"action","name":"action","title":"Action (Popup)","version":"1.0.0","manifest_version":3,"description":"Adds a toolbar popup to the browser with a simple page.","uiContext":["action","background"],"surfaces":["action","background"],"entrypoints":["./action/index.html","background.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":true,"tags":["action","popup","javascript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Click the toolbar icon to open the action popup","Edit src/action/index.html and watch HMR update the popup"],"useCases":["Toolbar popup as the primary UI surface","Quick actions that don\'t need a full sidebar or page","Starting point for popup-based browser extensions"],"docsUrl":"https://extension.js.org/docs/implementation-guide/manifest-json","video":"AWMlWK934qE","files":["extension.config.js","src/action/index.html","src/action/scripts.js","src/action/styles.css","src/background.js","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/action/screenshot.png","icon":"examples/action/src/images/icon.png"},{"slug":"action-locales","name":"action-locales","title":"Action Locales","version":"1.0.0","manifest_version":3,"description":"Adds a toolbar popup whose text follows your browser\'s language.","uiContext":["action","background"],"surfaces":["action","background"],"entrypoints":["./action/index.html","background.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"tags":["action","popup","i18n","javascript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Click the toolbar icon to see the popup in your browser\'s language","Add a folder under _locales/ and switch your browser language to see it"],"useCases":["A toolbar popup translated with the i18n API","Shipping _locales without hand-editing the manifest","Reference for localized extension UI across browsers"],"docsUrl":"https://extension.js.org/docs/implementation-guide/locales","video":"bY0Mtv77bVc","files":["_locales/en/messages.json","_locales/pt_BR/messages.json","extension.config.js","src/action/index.html","src/action/scripts.js","src/action/styles.css","src/background.js","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/action-locales/screenshot.png","icon":"examples/action-locales/src/images/icon.png"},{"slug":"ai-chatgpt","name":"ai-chatgpt","title":"ChatGPT AI Sidebar","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel where you can chat with ChatGPT about the page you are viewing.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.ts","content/scripts.ts"],"uiFramework":"react","css":"css","configFiles":["postcss.config.js","tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel","storage","tabs"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":true,"tags":["ai","chatgpt","openai","gpt","chat","sidebar","content-scripts","react","shadcn"],"difficulty":"beginner","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","Open any web page and click the extension icon to open the sidebar","Enter your ChatGPT API key (from platform.openai.com)","Ask ChatGPT about the active tab, and the content script feeds the page text into the conversation"],"useCases":["Chat with the active tab\'s content using ChatGPT","Summarize or question articles, docs, and dashboards in place","Starting point for page-aware ChatGPT extensions"],"docsUrl":"https://platform.openai.com/docs/api-reference/chat","video":"oEiJ_b36EtE","files":["extension.config.js","src/background.ts","src/components/ApiKeyForm.tsx","src/components/ChatInput.tsx","src/components/ChatMessage.tsx","src/components/ui/button.tsx","src/components/ui/card.tsx","src/components/ui/scroll-area.tsx","src/content/ContentApp.ts","src/content/scripts.ts","src/content/styles.css","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/lib/client.ts","src/lib/page-context.ts","src/lib/utils.ts","src/manifest.json","src/sidebar/SidebarApp.tsx","src/sidebar/index.html","src/sidebar/scripts.tsx","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/ai-chatgpt/screenshot.png","icon":"examples/ai-chatgpt/src/images/icon.png"},{"slug":"ai-claude","name":"ai-claude","title":"Claude AI Sidebar","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel where you can chat with Claude about the page you are viewing.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.ts","content/scripts.ts"],"uiFramework":"react","css":"css","configFiles":["postcss.config.js","tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel","storage","tabs"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":true,"tags":["ai","claude","anthropic","chat","sidebar","content-scripts","react","shadcn"],"difficulty":"beginner","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","Open any web page and click the extension icon to open the sidebar","Enter your Claude API key (from console.anthropic.com)","Ask Claude about the active tab, and the content script feeds the page text into the conversation"],"useCases":["Chat with the active tab\'s content using Claude","Summarize or question articles, docs, and dashboards in place","Starting point for page-aware Claude extensions"],"docsUrl":"https://docs.anthropic.com/en/api/getting-started","video":"z3JmyUlmiUw","files":["extension.config.js","src/background.ts","src/components/ApiKeyForm.tsx","src/components/ChatInput.tsx","src/components/ChatMessage.tsx","src/components/ui/button.tsx","src/components/ui/card.tsx","src/components/ui/scroll-area.tsx","src/content/ContentApp.ts","src/content/scripts.ts","src/content/styles.css","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/lib/client.ts","src/lib/page-context.ts","src/lib/utils.ts","src/manifest.json","src/sidebar/SidebarApp.tsx","src/sidebar/index.html","src/sidebar/scripts.tsx","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/ai-claude/screenshot.png","icon":"examples/ai-claude/src/images/icon.png"},{"slug":"ai-gemini","name":"ai-gemini","title":"Gemini AI Sidebar","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel where you can chat with Gemini about the page you are viewing.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.ts","content/scripts.ts"],"uiFramework":"react","css":"css","configFiles":["postcss.config.js","tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel","storage","tabs"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":true,"tags":["ai","gemini","google","chat","sidebar","content-scripts","react","shadcn"],"difficulty":"beginner","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","Open any web page and click the extension icon to open the sidebar","Enter your Gemini API key (from aistudio.google.com)","Ask Gemini about the active tab, and the content script feeds the page text into the conversation"],"useCases":["Chat with the active tab\'s content using Gemini","Summarize or question articles, docs, and dashboards in place","Starting point for page-aware Gemini extensions"],"docsUrl":"https://ai.google.dev/gemini-api/docs","video":"I2uhXovfurE","files":["extension.config.js","src/background.ts","src/components/ApiKeyForm.tsx","src/components/ChatInput.tsx","src/components/ChatMessage.tsx","src/components/ui/button.tsx","src/components/ui/card.tsx","src/components/ui/scroll-area.tsx","src/content/ContentApp.ts","src/content/scripts.ts","src/content/styles.css","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/lib/client.ts","src/lib/page-context.ts","src/lib/utils.ts","src/manifest.json","src/sidebar/SidebarApp.tsx","src/sidebar/index.html","src/sidebar/scripts.tsx","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/ai-gemini/screenshot.png","icon":"examples/ai-gemini/src/images/icon.png"},{"slug":"ai-perplexity","name":"ai-perplexity","title":"Perplexity AI Sidebar","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel where you can chat with Perplexity about the page you are viewing.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.ts","content/scripts.ts"],"uiFramework":"react","css":"css","configFiles":["postcss.config.js","tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel","storage","tabs"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":true,"tags":["ai","perplexity","search","chat","sidebar","content-scripts","react","shadcn"],"difficulty":"beginner","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","Open any web page and click the extension icon to open the sidebar","Enter your Perplexity API key (from perplexity.ai/settings/api)","Ask Perplexity about the active tab, and the content script feeds the page text into the conversation"],"useCases":["Chat with the active tab\'s content using Perplexity","Summarize or question articles, docs, and dashboards in place","Starting point for page-aware Perplexity extensions"],"docsUrl":"https://docs.perplexity.ai/","video":"W9j9rUrevjI","files":["extension.config.js","src/background.ts","src/components/ApiKeyForm.tsx","src/components/ChatInput.tsx","src/components/ChatMessage.tsx","src/components/ui/button.tsx","src/components/ui/card.tsx","src/components/ui/scroll-area.tsx","src/content/ContentApp.ts","src/content/scripts.ts","src/content/styles.css","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/lib/client.ts","src/lib/page-context.ts","src/lib/utils.ts","src/manifest.json","src/sidebar/SidebarApp.tsx","src/sidebar/index.html","src/sidebar/scripts.tsx","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/ai-perplexity/screenshot.png","icon":"examples/ai-perplexity/src/images/icon.png"},{"slug":"content","name":"@extension-js/content","title":"Content scripts","version":"1.0.0","manifest_version":3,"description":"Injects a small badge into every web page you visit, with an options page that moves it to the left or right edge.","uiContext":["content","options","background"],"surfaces":["content","options","background"],"entrypoints":["options/index.html","background.js","content/scripts.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["storage"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"tags":["content","options","storage","javascript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Click Open options on the badge injected into the page","Tick the setting and watch the badge move to the left edge"],"useCases":["A plain JavaScript UI injected into every page","One position setting shared between an options page and a content script","Starting point for content scripts that need preferences"],"docsUrl":"https://extension.js.org/docs/implementation-guide/content-scripts","video":"GflWcgdS9Tw","files":["extension.config.js","src/background.js","src/content/greet.js","src/content/scripts.js","src/content/styles.css","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/options/index.html","src/options/scripts.js","src/options/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content/screenshot.png","icon":"examples/content/src/images/icon.png"},{"slug":"content-css-modules","name":"@extension-js/content-css-modules","title":"Content CSS Modules","version":"1.0.0","manifest_version":3,"description":"Injects a small styled badge into every web page you visit.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["background.js","content/scripts.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"tags":["content-scripts","css-modules","javascript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Open any web page to see the badge this template injects","Edit src/content/styles.module.css and save to restyle the badge"],"useCases":["A content script styled with CSS modules","Class names hashed so page styles never collide","Starting point for content scripts that need scoped CSS"],"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/css-modules","video":"ondh3B28ieQ","files":["extension.config.js","src/background.js","src/content/scripts.js","src/content/styles.module.css","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-css-modules/screenshot.png","icon":"examples/content-css-modules/src/images/icon.png"},{"slug":"content-custom-font","name":"content-custom-font","title":"Custom fonts in a content script","version":"1.0.0","manifest_version":3,"description":"Injects a badge rendered in a custom font into every web page you visit, with an options page that turns the custom font on and off.","uiContext":["content","options","background"],"surfaces":["content","options","background"],"entrypoints":["options/index.html","background.js","content/scripts.js"],"uiFramework":"","css":"css","configFiles":["postcss.config.js","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["storage"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"tags":["content","fonts","options","storage","javascript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Click Open options on the badge injected into the page","Untick the setting and watch the badge fall back to the system font"],"useCases":["A content script that ships its own typeface","One font setting shared between an options page and a content script","Starting point for injected UI that must not inherit host page type"],"docsUrl":"https://extension.js.org/docs/implementation-guide/css","video":"IAbckjfGO5Y","files":["extension.config.js","public/fonts/MomoSignature-Regular.ttf","public/fonts/MomoSignature-Regular.woff2","public/fonts/OFL.txt","src/background.js","src/content/scripts.js","src/content/styles.css","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/options/index.html","src/options/scripts.js","src/options/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-custom-font/screenshot.png","icon":"examples/content-custom-font/src/images/icon.png"},{"slug":"content-env","name":"content-env","title":"Content Env","version":"1.0.0","manifest_version":3,"description":"Injects a small panel showing the extension\'s environment values into every web page you visit.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["background.ts","content/scripts.ts"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"tags":["content-scripts","env","configuration","typescript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Open any web page to see the panel print the injected values","Edit .env.chrome or .env.firefox to change what the panel reads"],"useCases":["Reading build-time configuration inside a content script","Switching behavior per browser without touching the source","Reference for EXTENSION_PUBLIC_* values in typed sources"],"docsUrl":"https://extension.js.org/docs/features/environment-variables","video":"Q-ugTIZVZ-U","files":["extension.config.js","src/background.ts","src/content/scripts.ts","src/content/styles.css","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-env/screenshot.png","icon":"examples/content-env/src/images/icon.png"},{"slug":"content-less","name":"@extension-js/content-less","title":"Content Less","version":"1.0.0","manifest_version":3,"description":"Injects a small styled badge into every web page you visit, with a Less options page that moves it to the left or right edge.","uiContext":["content","options","background"],"surfaces":["content","options","background"],"entrypoints":["options/index.html","background.js","content/scripts.js"],"uiFramework":"","css":"less","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["storage"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":true,"tags":["content","options","storage","less"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Click Open options on the badge injected into the page","Tick the setting and watch the badge move to the left edge"],"useCases":["A Less-styled badge injected into every page","One position setting shared between an options page and a content script","Starting point for Less extensions that need preferences"],"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/less","video":"VGssDbxBeMM","files":["extension.config.js","src/background.js","src/content/scripts.js","src/content/styles.less","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/options/index.html","src/options/scripts.js","src/options/styles.less"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-less/screenshot.png","icon":"examples/content-less/src/images/icon.png"},{"slug":"content-less-modules","name":"@extension-js/content-less-modules","title":"Content Less Modules","version":"1.0.0","manifest_version":3,"description":"Injects a small styled badge into every web page you visit, with a Less modules options page that moves it to the left or right edge.","uiContext":["content","options","background"],"surfaces":["content","options","background"],"entrypoints":["options/index.html","background.js","content/scripts.js"],"uiFramework":"","css":"less","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["storage"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"tags":["content","options","storage","less-modules"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Click Open options on the badge injected into the page","Tick the setting and watch the badge move to the left edge"],"useCases":["Hashed class names shared by a content script and an options page","One position setting shared between an options page and a content script","Starting point for CSS Modules extensions that need preferences"],"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/less","video":"Q1fNVB2ZuNw","files":["extension.config.js","src/background.js","src/content/scripts.js","src/content/styles.module.less","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/options/index.html","src/options/scripts.js","src/options/styles.module.less"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-less-modules/screenshot.png","icon":"examples/content-less-modules/src/images/icon.png"},{"slug":"content-main-world","name":"content-main-world","title":"Main-world Content Script","version":"1.0.0","manifest_version":3,"description":"Injects a small UI into every web page from both the extension and the page\'s own JavaScript world, with an options page that moves it to the left or right edge.","uiContext":["content","options","background"],"surfaces":["content","options","background"],"entrypoints":["options/index.html","background.js","content/isolated-one.js","content/isolated-two.js","content/scripts.js","content/isolated-options.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["storage"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":true,"tags":["content-scripts","main-world","isolated-world","options","storage","javascript"],"difficulty":"intermediate","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","Click Open options on the UI injected into the page","Tick the setting and watch the main-world UI move to the left edge"],"useCases":["Hook into page-level globals that isolated content scripts cannot reach","Bridge isolated and main-world execution contexts cleanly","Read a stored setting from a main-world script that has no chrome APIs"],"docsUrl":"https://extension.js.org/docs/implementation-guide/content-scripts","video":"3-C3wCE3fWU","files":["extension.config.js","src/background.js","src/content/isolated-one.js","src/content/isolated-options.js","src/content/isolated-two.js","src/content/scripts.js","src/content/styles.css","src/content/utils/bridge.js","src/content/utils/constants.js","src/content/utils/create-badge.js","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/options/index.html","src/options/scripts.js","src/options/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-main-world/screenshot.png","icon":"examples/content-main-world/src/images/icon.png"},{"slug":"content-multi-one-entry","name":"content-multi-one-entry","title":"Content Multi One Entry","version":"1.0.0","manifest_version":3,"description":"Injects four small elements into every web page you visit.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["background.js","content/script-top-left.js","content/script-top-right.js","content/script-bottom-left.js","content/script-bottom-right.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"tags":["content-scripts","multi-entry","javascript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Open any web page and an element appears in each of its four corners","Edit one script in src/content/ and save to see that corner update"],"useCases":["Four scripts shipped from a single content-script entry","One match rule driving several focused files","Reference for grouping content scripts under one entry"],"docsUrl":"https://extension.js.org/docs/implementation-guide/content-scripts","video":"l1QWq7lIKo8","files":["extension.config.js","src/background.js","src/content/script-bottom-left.js","src/content/script-bottom-right.js","src/content/script-top-left.js","src/content/script-top-right.js","src/content/styles.css","src/content/utils/constants.js","src/content/utils/create-badge.js","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-multi-one-entry/screenshot.png","icon":"examples/content-multi-one-entry/src/images/icon.png"},{"slug":"content-multi-three-entries","name":"content-multi-three-entries","title":"Multi-entry Content Scripts","version":"1.0.0","manifest_version":3,"description":"Injects four small elements into every web page you visit.","uiContext":["content","background"],"surfaces":["content","background"],"entrypoints":["background.js","content/script-top-left.js","content/script-top-right.js","content/script-bottom-left.js","content/script-bottom-right.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":true,"tags":["content-scripts","multi-entry","javascript"],"difficulty":"intermediate","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","Open any page and three separate content scripts inject small elements","Edit any of the entries in src/content/ to see HMR update independently"],"useCases":["Split content-script logic across multiple entries with different match rules","Target different page contexts with focused, independent bundles","Reference for the multi-entry content_scripts pattern in MV3"],"docsUrl":"https://extension.js.org/docs/implementation-guide/content-scripts","video":"oDcdmGaIBjw","files":["extension.config.js","src/background.js","src/content/script-bottom-left.js","src/content/script-bottom-right.js","src/content/script-top-left.js","src/content/script-top-right.js","src/content/styles.css","src/content/utils/constants.js","src/content/utils/create-badge.js","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-multi-three-entries/screenshot.png","icon":"examples/content-multi-three-entries/src/images/icon.png"},{"slug":"content-preact","name":"content-preact","title":"Content Preact","version":"1.0.0","manifest_version":3,"description":"Shows an overlay UI on web pages and an options page that moves it from right to left.","uiContext":["content","options","background"],"surfaces":["content","options","background"],"entrypoints":["options/index.html","background.ts","./content/scripts.tsx"],"uiFramework":"preact","css":"css","configFiles":["postcss.config.js","tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["storage"],"host_permissions":[],"optional_permissions":[],"featured":false,"tags":["content","options","storage","preact","typescript","tailwind"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Click Open options on the overlay injected into the page","Tick the setting and watch the overlay move to the left edge"],"useCases":["An on-page UI written in Preact and isolated in a Shadow DOM","One setting shared between an options page and a content script","Starting point for Preact extensions that need preferences"],"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/preact","video":"sYA3V9YIWwo","files":["extension.config.js","src/background.ts","src/content/ContentApp.tsx","src/content/scripts.tsx","src/content/styles.css","src/images/chromeWindow.png","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/images/preact.png","src/images/tailwind.png","src/images/tailwind_bg.png","src/images/typescript.png","src/manifest.json","src/options/OptionsApp.tsx","src/options/index.html","src/options/scripts.tsx","src/options/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-preact/screenshot.png","icon":"examples/content-preact/src/images/icon.png"},{"slug":"content-react","name":"content-react","title":"Content React","version":"1.0.0","manifest_version":3,"description":"Shows a small overlay UI on every web page, with a React options page that moves it from right to left.","uiContext":["content","options","background"],"surfaces":["content","options","background"],"entrypoints":["options/index.html","background.ts","./content/scripts.tsx"],"uiFramework":"react","css":"css","configFiles":["postcss.config.js","tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["storage"],"host_permissions":[],"optional_permissions":[],"featured":false,"tags":["content","options","storage","react","typescript","tailwind"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Click Open options on the overlay injected into the page","Tick the setting and watch the overlay move to the left edge"],"useCases":["A React UI injected into any page, isolated in a Shadow DOM","One setting shared between a React options page and a content script","Starting point for content-script extensions that need preferences"],"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/react","video":"oGNhj9k4bNs","files":["extension.config.js","src/background.ts","src/content/ContentApp.tsx","src/content/scripts.tsx","src/content/styles.css","src/images/chromeWindow.png","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/images/react.png","src/images/tailwind.png","src/images/tailwind_bg.png","src/images/typescript.png","src/manifest.json","src/options/OptionsApp.tsx","src/options/index.html","src/options/scripts.tsx","src/options/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-react/screenshot.png","icon":"examples/content-react/src/images/icon.png"},{"slug":"content-sass","name":"@extension-js/content-sass","title":"Content scripts with Sass","version":"1.0.0","manifest_version":3,"description":"Injects a small styled badge into every web page you visit, with a Sass options page that moves it to the left or right edge.","uiContext":["content","options","background"],"surfaces":["content","options","background"],"entrypoints":["options/index.html","background.js","content/scripts.js"],"uiFramework":"","css":"sass","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["storage"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":true,"tags":["content","sass","options","storage"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Click Open options on the badge injected into the page","Tick the setting and watch the badge move to the left edge"],"useCases":["A badge injected into every page and styled with Sass","One position setting shared between an options page and a content script","Starting point for Sass content scripts that need preferences"],"docsUrl":"https://extension.js.org/docs/implementation-guide/css","video":"-VceQYe_KMk","files":["extension.config.js","src/background.js","src/content/scripts.js","src/content/styles.scss","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/options/index.html","src/options/scripts.js","src/options/styles.scss"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-sass/screenshot.png","icon":"examples/content-sass/src/images/icon.png"},{"slug":"content-sass-modules","name":"@extension-js/content-sass-modules","title":"Content scripts with Sass modules","version":"1.0.0","manifest_version":3,"description":"Injects a small styled badge into every web page you visit, with a Sass modules options page that moves it to the left or right edge.","uiContext":["content","options","background"],"surfaces":["content","options","background"],"entrypoints":["options/index.html","background.js","content/scripts.js"],"uiFramework":"","css":"sass","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{"build":"extension build"},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["storage"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"tags":["content","sass","css-modules","options","storage"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Click Open options on the badge injected into the page","Tick the setting and watch the badge move to the left edge"],"useCases":["A badge injected into every page and styled with hashed Sass module class names","One position setting shared between an options page and a content script","Starting point for Sass module content scripts that need preferences"],"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/sass","video":"Ma9f_Q1C7Eo","files":["extension.config.js","src/background.js","src/content/scripts.js","src/content/styles.module.scss","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/options/index.html","src/options/scripts.js","src/options/styles.module.scss"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-sass-modules/screenshot.png","icon":"examples/content-sass-modules/src/images/icon.png"},{"slug":"content-svelte","name":"content-svelte","title":"Content Svelte","version":"1.0.0","manifest_version":3,"description":"Shows a Svelte overlay on every web page you visit, with an options page that moves it from right to left.","uiContext":["content","options","background"],"surfaces":["content","options","background"],"entrypoints":["options/index.html","background.ts","./content/scripts.ts"],"uiFramework":"svelte","css":"css","configFiles":["postcss.config.js","tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["storage"],"host_permissions":[],"optional_permissions":[],"featured":false,"tags":["content","svelte","typescript","tailwind","options","storage"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Click Open options on the overlay injected into the page","Tick the setting and watch the overlay move to the left edge"],"useCases":["A Svelte overlay injected into every page, isolated in a Shadow DOM","One setting shared between a Svelte options page and a content script","Starting point for Svelte extensions that need preferences"],"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/svelte","video":"g6-GZ6_Rn38","files":["extension.config.js","src/background.ts","src/content/ContentApp.svelte","src/content/scripts.ts","src/content/styles.css","src/content/svelte.d.ts","src/images/chromeWindow.png","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/images/svelte.png","src/images/tailwind.png","src/images/tailwind_bg.png","src/images/typescript.png","src/manifest.json","src/options/OptionsApp.svelte","src/options/index.html","src/options/scripts.ts","src/options/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-svelte/screenshot.png","icon":"examples/content-svelte/src/images/icon.png"},{"slug":"content-typescript","name":"content-typescript","title":"Content TypeScript","version":"1.0.0","manifest_version":3,"description":"Shows a small overlay UI on every web page, with an options page that moves it from right to left.","uiContext":["content","options","background"],"surfaces":["content","options","background"],"entrypoints":["options/index.html","background.ts","content/scripts.ts"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["storage"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"tags":["content","options","storage","typescript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Click Open options on the overlay injected into the page","Tick the setting and watch the overlay move to the left edge"],"useCases":["A typed overlay injected into every page","One setting shared between an options page and a content script","Starting point for TypeScript extensions that need preferences"],"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/typescript","video":"9OBuuh3N_cM","files":["extension.config.js","src/background.ts","src/content/scripts.ts","src/content/styles.css","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/options/index.html","src/options/scripts.ts","src/options/styles.css","src/types.ts"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-typescript/screenshot.png","icon":"examples/content-typescript/src/images/icon.png"},{"slug":"content-vue","name":"content-vue","title":"Content Vue","version":"1.0.0","manifest_version":3,"description":"Shows a small overlay UI on every web page, with a Vue options page that moves it from right to left.","uiContext":["content","options","background"],"surfaces":["content","options","background"],"entrypoints":["options/index.html","background.ts","./content/scripts.ts"],"uiFramework":"vue","css":"css","configFiles":["postcss.config.js","tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["storage"],"host_permissions":[],"optional_permissions":[],"featured":false,"tags":["content","options","storage","vue","typescript","tailwind"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Click Open options on the overlay injected into the page","Tick the setting and watch the overlay move to the left edge"],"useCases":["A Vue overlay injected into every page, isolated in a Shadow DOM","One setting shared between a Vue options page and a content script","Starting point for Vue extensions that need preferences"],"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/vue","video":"LIfZxR-B2Nk","files":["extension.config.js","src/background.ts","src/content/ContentApp.vue","src/content/scripts.ts","src/content/shims-vue.d.ts","src/content/styles.css","src/images/chromeWindow.png","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/images/logo.svg","src/images/tailwind.png","src/images/tailwind_bg.png","src/images/typescript.png","src/images/vue.png","src/manifest.json","src/options/OptionsApp.vue","src/options/index.html","src/options/scripts.ts","src/options/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/content-vue/screenshot.png","icon":"examples/content-vue/src/images/logo.svg"},{"slug":"devtools","name":"devtools","title":"Devtools panel","version":"1.0.0","manifest_version":3,"description":"Adds a devtools panel to the browser that reads the inspected page.","uiContext":["devtools"],"surfaces":["devtools"],"entrypoints":["devtools/index.html"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":false,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"tags":["devtools","panel","javascript"],"difficulty":"beginner","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","Open the browser devtools with F12 or Cmd+Option+I, then pick the Example tab","Edit src/panel/index.html and reopen devtools to see the change"],"useCases":["A devtools panel as the primary UI surface","Inspecting the current page from inside the developer tools","Starting point for debugging tools built as browser extensions"],"docsUrl":"https://extension.js.org/docs/implementation-guide/manifest-json","video":"kyJk6ksDOsU","files":["extension.config.js","src/devtools/index.html","src/devtools/scripts.js","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/panel/index.html","src/panel/scripts.js","src/panel/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/devtools/screenshot.png","icon":"examples/devtools/src/images/icon.png"},{"slug":"devtools-preact","name":"devtools-preact","title":"Devtools panel Preact","version":"1.0.0","manifest_version":3,"description":"Adds a devtools panel written in Preact that reads the inspected page.","uiContext":["devtools"],"surfaces":["devtools"],"entrypoints":["devtools/index.html"],"uiFramework":"preact","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":false,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"tags":["devtools","panel","preact","typescript"],"difficulty":"beginner","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","Open the browser devtools with F12 or Cmd+Option+I, then pick the Example tab","Edit src/panel/PanelApp.tsx and reopen devtools to see the change"],"useCases":["A devtools panel as the primary UI surface","Inspecting the current page from inside the developer tools","Starting point for Preact debugging tools built as browser extensions"],"docsUrl":"https://extension.js.org/docs/implementation-guide/manifest-json","video":"WjPVn5SlGaU","files":["extension.config.js","src/devtools/index.html","src/devtools/scripts.ts","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/images/preact.png","src/manifest.json","src/panel/PanelApp.tsx","src/panel/index.html","src/panel/scripts.tsx","src/panel/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/devtools-preact/screenshot.png","icon":"examples/devtools-preact/src/images/icon.png"},{"slug":"devtools-react","name":"devtools-react","title":"React devtools panel","version":"1.0.0","manifest_version":3,"description":"Adds a React devtools panel to the browser that reads the inspected page.","uiContext":["devtools"],"surfaces":["devtools"],"entrypoints":["devtools/index.html"],"uiFramework":"react","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":false,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"tags":["devtools","panel","react","typescript"],"difficulty":"beginner","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","Open the browser devtools with F12 or Cmd+Option+I, then pick the Example tab","Edit src/panel/PanelApp.tsx and save to see the panel update"],"useCases":["A React devtools panel as the primary UI surface","Inspecting the current page from inside the developer tools","Starting point for debugging tools built as browser extensions"],"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/react","video":"mRZO9Eg2aUQ","files":["extension.config.js","src/devtools/index.html","src/devtools/scripts.js","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/images/react.png","src/manifest.json","src/panel/PanelApp.tsx","src/panel/index.html","src/panel/scripts.tsx","src/panel/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/devtools-react/screenshot.png","icon":"examples/devtools-react/src/images/icon.png"},{"slug":"devtools-svelte","name":"devtools-svelte","title":"Devtools panel with Svelte","version":"1.0.0","manifest_version":3,"description":"Adds a Svelte devtools panel to the browser that reads the inspected page.","uiContext":["devtools"],"surfaces":["devtools"],"entrypoints":["devtools/index.html"],"uiFramework":"svelte","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":false,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"tags":["devtools","panel","svelte","typescript"],"difficulty":"beginner","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","Open the browser devtools with F12 or Cmd+Option+I, then pick the Example tab","Edit src/panel/PanelApp.svelte and reopen devtools to see the change"],"useCases":["A devtools panel as the primary UI surface","Inspecting the current page from inside the developer tools","Starting point for debugging tools built as browser extensions"],"docsUrl":"https://extension.js.org/docs/implementation-guide/manifest-json","video":"SqOxa9hRg1s","files":["extension.config.js","src/devtools/index.html","src/devtools/scripts.ts","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/images/svelte.png","src/manifest.json","src/panel/PanelApp.svelte","src/panel/index.html","src/panel/scripts.ts","src/panel/styles.css","src/panel/svelte.d.ts"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/devtools-svelte/screenshot.png","icon":"examples/devtools-svelte/src/images/icon.png"},{"slug":"devtools-typescript","name":"devtools-typescript","title":"Devtools panel (TypeScript)","version":"1.0.0","manifest_version":3,"description":"Adds a devtools panel to the browser that reads the inspected page.","uiContext":["devtools"],"surfaces":["devtools"],"entrypoints":["devtools/index.html"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":false,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"tags":["devtools","panel","typescript"],"difficulty":"beginner","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","Open the browser devtools with F12 or Cmd+Option+I, then pick the Example tab","Edit src/panel/index.html and reopen devtools to see the change"],"useCases":["A devtools panel as the primary UI surface","Inspecting the current page from inside the developer tools","Starting point for debugging tools built as browser extensions"],"docsUrl":"https://extension.js.org/docs/implementation-guide/manifest-json","video":"iI1Th6Hi_ck","files":["extension.config.js","src/devtools/index.html","src/devtools/scripts.ts","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/images/typescript.png","src/manifest.json","src/panel/index.html","src/panel/scripts.ts","src/panel/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/devtools-typescript/screenshot.png","icon":"examples/devtools-typescript/src/images/icon.png"},{"slug":"devtools-vue","name":"devtools-vue","title":"Devtools panel with Vue","version":"1.0.0","manifest_version":3,"description":"Adds a devtools panel built with Vue that reads the inspected page.","uiContext":["devtools"],"surfaces":["devtools"],"entrypoints":["devtools/index.html"],"uiFramework":"vue","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":false,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"tags":["devtools","panel","vue","typescript"],"difficulty":"beginner","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","Open the browser devtools with F12 or Cmd+Option+I, then pick the Example tab","Edit src/panel/PanelApp.vue and reopen devtools to see the change"],"useCases":["A devtools panel as the primary UI surface","Inspecting the current page from inside the developer tools","Starting point for debugging tools built as browser extensions"],"docsUrl":"https://extension.js.org/docs/implementation-guide/manifest-json","video":"sp-bjexg2X8","files":["extension.config.js","src/devtools/index.html","src/devtools/scripts.ts","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/images/vue.png","src/manifest.json","src/panel/PanelApp.vue","src/panel/index.html","src/panel/scripts.ts","src/panel/shims-vue.d.ts","src/panel/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/devtools-vue/screenshot.png","icon":"examples/devtools-vue/src/images/icon.png"},{"slug":"init","name":"init","title":"Initial","version":"1.0.0","manifest_version":3,"description":"A minimal extension that loads in your browser, ready to build on.","uiContext":[],"surfaces":[],"entrypoints":[],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":false,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"tags":["minimal","javascript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","The extension loads in a fresh profile with no surface of its own","Add src/newtab/index.html and it ships on the next reload"],"useCases":["The smallest Extension.js project that still runs","A blank slate for whichever surface you choose","Reference for the minimum an MV3 extension needs"],"docsUrl":"https://extension.js.org/docs/getting-started/immediately","files":["extension.config.js","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/images/javascript.png","src/index.js","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/init/screenshot.png","icon":"examples/init/src/images/icon.png"},{"slug":"javascript","name":"javascript","title":"JavaScript","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel to the browser with a simple page.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.js","content/scripts.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel"],"host_permissions":[],"optional_permissions":[],"featured":false,"tags":["sidebar","javascript","content-scripts"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Click the extension icon to open the sidebar panel","Edit src/sidebar/index.html and save to watch the panel update"],"useCases":["A sidebar panel as the primary UI surface","Plain JavaScript with no compiler config to maintain","Starting point for extensions written in JavaScript"],"docsUrl":"https://extension.js.org/docs/getting-started/templates/javascript","video":"Ryjuq2o-kzc","files":["extension.config.js","src/background.js","src/content/ContentApp.js","src/content/scripts.js","src/content/styles.css","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/sidebar/SidebarApp.js","src/sidebar/index.html","src/sidebar/scripts.js","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/javascript/screenshot.png","icon":"examples/javascript/src/images/icon.png"},{"slug":"newtab","name":"newtab","title":"New Tab","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a simple page you can customize.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"tags":["new-tab","javascript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Open a new tab and this page loads in place of the default","Edit src/newtab/index.html and save to watch the tab update"],"useCases":["A new tab page as the primary UI surface","Dashboards, launchers, and start pages","Starting point for new tab extensions in JavaScript"],"docsUrl":"https://extension.js.org/docs/implementation-guide/html","video":"fouoMkXLyvQ","files":["extension.config.js","src/background.js","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/scripts.js","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/newtab/screenshot.png","icon":"examples/newtab/src/images/icon.png"},{"slug":"newtab-browser-flags","name":"newtab-browser-flags","title":"New Tab Browser Flags","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a launcher that opens the extension\'s sidebar panel.","uiContext":["content","newtab","sidebar","background"],"surfaces":["content","newtab","sidebar","background"],"entrypoints":["newtab/index.html","sidebar/index.html","background.js","content/content.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{"build":"extension build"},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"tags":["new-tab","sidebar","configuration","javascript"],"difficulty":"beginner","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","The browser starts with the flags this template sets in extension.config.js","Open a new tab and use the launcher to open the sidebar panel"],"useCases":["Passing custom browser flags through extension.config.js","A new tab launcher that opens another extension surface","Reference for browser-specific differences across targets"],"docsUrl":"https://extension.js.org/docs/browsers/browser-flags","files":["extension.config.js","src/background.js","src/content/content.js","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/newtab.css","src/newtab/newtab.js","src/sidebar/index.html","src/sidebar/sidebar.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/newtab-browser-flags/screenshot.png","icon":"examples/newtab-browser-flags/src/images/icon.png"},{"slug":"newtab-config-eslint","name":"newtab-config-eslint","title":"New Tab Config ESLint","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a simple page.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js","eslint.config.mjs"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"tags":["new-tab","configuration","javascript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Open a new tab to see the page this template ships","Run npx eslint src to check the source with the bundled config"],"useCases":["A new tab extension that lints its own source","ESLint wired into an Extension.js project without ejecting","Reference for adding a linter to an extension codebase"],"docsUrl":"https://extension.js.org/docs/integrations/eslint","video":"CoS-_Cv-SvM","files":["extension.config.js","src/background.js","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/scripts.ts","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/newtab-config-eslint/screenshot.png","icon":"examples/newtab-config-eslint/src/images/icon.png"},{"slug":"newtab-config-prettier","name":"newtab-config-prettier","title":"New Tab Config Prettier","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a simple page.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js",".prettierrc"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"tags":["new-tab","configuration","javascript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Open a new tab to see the page this template ships","Run npx prettier --check src to see the rules this template ships"],"useCases":["A new tab extension that formats its own source","Prettier wired into an Extension.js project without ejecting","Reference for adding a formatter to an extension codebase"],"docsUrl":"https://extension.js.org/docs/integrations/prettier","video":"JnNNaUzefTg","files":["extension.config.js","src/background.js","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/scripts.ts","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/newtab-config-prettier/screenshot.png","icon":"examples/newtab-config-prettier/src/images/icon.png"},{"slug":"newtab-config-stylelint","name":"newtab-config-stylelint","title":"New Tab Config Stylelint","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a simple page.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"","css":"sass","configFiles":[".stylelintrc.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"tags":["new-tab","sass","configuration"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Open a new tab to see the styled page this template ships","Run npx stylelint src to check the Sass with the bundled config"],"useCases":["A new tab extension that lints its own stylesheets","Stylelint and Sass wired into an Extension.js project","Reference for adding a style linter to an extension codebase"],"docsUrl":"https://extension.js.org/docs/integrations/stylelint","video":"3DVh7oDKFxo","files":["extension.config.js","src/background.js","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/scripts.js","src/newtab/styles.scss"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/newtab-config-stylelint/screenshot.png","icon":"examples/newtab-config-stylelint/src/images/icon.png"},{"slug":"newtab-crypto","name":"newtab-crypto","title":"New Tab Crypto","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a page that hashes any text you type using SHA-256.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"tags":["new-tab","web-crypto","typescript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Open a new tab and type any text to see its SHA-256 hash","Edit src/newtab/scripts.ts to hash with a different algorithm"],"useCases":["Hashing text with the Web Crypto API inside an extension","A new tab page that does real work with no network","Reference for browser crypto primitives under MV3"],"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/node","video":"dPdh5H_sTgw","files":["extension.config.js","src/background.js","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/scripts.ts","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/newtab-crypto/screenshot.png","icon":"examples/newtab-crypto/src/images/icon.png"},{"slug":"newtab-env","name":"newtab-env","title":"Environment Variables","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a page showing the extension\'s environment values.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":true,"tags":["env","configuration","new-tab","javascript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Copy .env.example to .env and edit the values","Run npm run dev to launch the extension","Open a new tab and the page reads injected EXTENSION_PUBLIC_* values"],"useCases":["Inject build-time configuration without leaking secrets","Switch behavior per browser, mode, or deployment target","Reference for safe env-var usage across JS, JSON, and HTML"],"docsUrl":"https://extension.js.org/docs/features/environment-variables","video":"8XP2acacpK8","files":["extension.config.js","src/background.js","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/scripts.ts","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/newtab-env/screenshot.png","icon":"examples/newtab-env/src/images/icon.png"},{"slug":"newtab-less","name":"newtab-less","title":"New Tab Less","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a simple styled page.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"","css":"less","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"tags":["new-tab","less","javascript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Open a new tab to see the styled page this template ships","Edit src/newtab/styles.less and save to watch the styles reload"],"useCases":["A new tab page styled with Less","Less compiled by Extension.js with no extra config","Starting point for extensions that prefer Less to plain CSS"],"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/less","video":"T4Bgw8pCYY0","files":["extension.config.js","src/background.js","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/scripts.js","src/newtab/styles.less"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/newtab-less/screenshot.png","icon":"examples/newtab-less/src/images/icon.png"},{"slug":"newtab-preact","name":"newtab-preact","title":"New Tab Preact","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a simple Preact page.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"preact","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"tags":["new-tab","preact","typescript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Open a new tab to see the Preact page this template ships","Edit src/newtab/NewTabApp.tsx and save to see fast refresh update it"],"useCases":["A new tab page rendered by Preact","Preact and JSX compiled with no extra config","Starting point for Preact extensions with a new tab"],"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/preact","video":"ezN6818j744","files":["extension.config.js","src/background.js","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/newtab/NewTabApp.tsx","src/newtab/index.html","src/newtab/scripts.tsx","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/newtab-preact/screenshot.png","icon":"examples/newtab-preact/src/images/icon.png"},{"slug":"newtab-react","name":"newtab-react","title":"New Tab React","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a simple React page.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"react","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"tags":["new-tab","react","typescript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Open a new tab to see the React page this template ships","Edit src/newtab/NewTabApp.tsx and save to see fast refresh update it"],"useCases":["A new tab page rendered by React","React and JSX compiled with no extra config","Starting point for React extensions with a new tab"],"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/react","video":"jt4HgQPdJX8","files":["extension.config.js","src/background.js","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/newtab/NewTabApp.tsx","src/newtab/index.html","src/newtab/scripts.tsx","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/newtab-react/screenshot.png","icon":"examples/newtab-react/src/images/icon.png"},{"slug":"newtab-react-router","name":"newtab-react-router","title":"New Tab React Router","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a small React app you can navigate across routes.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"react","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"tags":["new-tab","react","routing","typescript"],"difficulty":"beginner","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","Open a new tab and follow the links to move between routes","Add a route under src/newtab/ and it renders with no server"],"useCases":["A multi-route new tab app with client-side routing","React Router running inside an extension page","Reference for routing with no server under MV3"],"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/react","video":"ysvv7Q10p3Q","files":["extension.config.js","src/background.js","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/newtab/NewTabApp.tsx","src/newtab/index.html","src/newtab/scripts.tsx","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/newtab-react-router/screenshot.png","icon":"examples/newtab-react-router/src/images/icon.png"},{"slug":"newtab-sass","name":"newtab-sass","title":"New Tab Sass","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a simple styled page.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"","css":"sass","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"tags":["new-tab","sass","javascript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Open a new tab to see the styled page this template ships","Edit src/newtab/styles.scss and save to watch the styles reload"],"useCases":["A new tab page styled with Sass","Sass compiled by Extension.js with no extra config","Starting point for extensions that prefer Sass to plain CSS"],"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/sass","video":"bUWLBJeWzpc","files":["extension.config.js","src/background.js","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/scripts.js","src/newtab/styles.scss"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/newtab-sass/screenshot.png","icon":"examples/newtab-sass/src/images/icon.png"},{"slug":"newtab-svelte","name":"newtab-svelte","title":"New Tab Svelte","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a simple Svelte page.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"svelte","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"tags":["new-tab","svelte","typescript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Open a new tab to see the Svelte page this template ships","Edit src/newtab/NewTabApp.svelte and save to watch it update"],"useCases":["A new tab page rendered by Svelte","Svelte components compiled with no extra config","Starting point for Svelte extensions with a new tab"],"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/svelte","video":"uq-HVZb51U8","files":["extension.config.js","src/background.js","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/newtab/NewTabApp.svelte","src/newtab/index.html","src/newtab/scripts.ts","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/newtab-svelte/screenshot.png","icon":"examples/newtab-svelte/src/images/icon.png"},{"slug":"newtab-typescript","name":"newtab-typescript","title":"New Tab TypeScript","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a simple page.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"tags":["new-tab","typescript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Open a new tab to see the page this template ships","Edit src/newtab/scripts.ts and save to watch the tab update"],"useCases":["A typed new tab page","TypeScript compiled by Extension.js with no extra config","Starting point for TypeScript extensions with a new tab"],"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/typescript","video":"0sRMGlx7MbA","files":["extension.config.js","src/background.js","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/newtab/index.html","src/newtab/scripts.ts","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/newtab-typescript/screenshot.png","icon":"examples/newtab-typescript/src/images/icon.png"},{"slug":"newtab-vue","name":"newtab-vue","title":"New Tab Vue","version":"1.0.0","manifest_version":3,"description":"Replaces your new tab page with a simple Vue page.","uiContext":["newtab","background"],"surfaces":["newtab","background"],"entrypoints":["newtab/index.html","background.js"],"uiFramework":"vue","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":false,"tags":["new-tab","vue","typescript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Open a new tab to see the Vue page this template ships","Edit src/newtab/NewTabApp.vue and save to watch it update"],"useCases":["A new tab page rendered by Vue","Single-file components compiled with no extra config","Starting point for Vue extensions with a new tab"],"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/vue","video":"g4rjynbMI5c","files":["extension.config.js","src/background.js","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/newtab/NewTabApp.vue","src/newtab/index.html","src/newtab/scripts.ts","src/newtab/shims-vue.d.ts","src/newtab/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/newtab-vue/screenshot.png","icon":"examples/newtab-vue/src/images/icon.png"},{"slug":"playwright","name":"playwright","title":"Playwright E2E Sidebar","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel to the browser, covered end to end by Playwright tests.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.ts","content/scripts.ts"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{"test":"playwright test"},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel"],"host_permissions":[],"optional_permissions":[],"featured":true,"tags":["testing","playwright","e2e","sidebar","typescript"],"difficulty":"intermediate","timeToFirstSuccessMinutes":4,"firstSteps":["Run npm install to set up Playwright deps","Run npm run dev to launch the extension","Run npm test to execute the Playwright spec against the loaded extension"],"useCases":["End-to-end testing for browser-extension surfaces (popup, sidebar, content)","CI-ready Playwright setup wired to Extension.js builds","Reference for automating extension flows in headed and headless modes"],"docsUrl":"https://extension.js.org/docs/workflows/playwright-e2e","video":"k8PP2Gs1AsU","files":["extension.config.js","src/background.ts","src/content/ContentApp.ts","src/content/scripts.ts","src/content/styles.css","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/sidebar/SidebarApp.ts","src/sidebar/index.html","src/sidebar/scripts.ts","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/playwright/screenshot.png","icon":"examples/playwright/src/images/icon.png"},{"slug":"preact","name":"preact","title":"Preact","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel to the browser with a simple Preact page.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.ts","content/scripts.tsx"],"uiFramework":"preact","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel"],"host_permissions":[],"optional_permissions":[],"featured":true,"tags":["sidebar","preact","typescript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Click the extension icon to open the Preact sidebar panel","Edit the component in src/sidebar/ to see fast refresh update it"],"useCases":["A sidebar panel rendered by Preact","Preact and JSX compiled with no extra config","Starting point for Preact extensions"],"docsUrl":"https://extension.js.org/docs/getting-started/templates/preact","video":"VcAPgX8KHgI","files":["extension.config.js","public/extension.svg","public/logo.svg","src/background.ts","src/content/ContentApp.tsx","src/content/scripts.tsx","src/content/styles.css","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/sidebar/SidebarApp.tsx","src/sidebar/index.html","src/sidebar/scripts.tsx","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/preact/screenshot.png","icon":"examples/preact/public/logo.svg"},{"slug":"react","name":"react","title":"React","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel to the browser with a simple React page.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.ts","content/scripts.tsx"],"uiFramework":"react","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel"],"host_permissions":[],"optional_permissions":[],"featured":true,"tags":["sidebar","react","typescript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Click the extension icon to open the React sidebar panel","Edit the component in src/sidebar/ to see fast refresh update it"],"useCases":["A sidebar panel rendered by React","React and JSX compiled with no extra config","Starting point for React extensions"],"docsUrl":"https://extension.js.org/docs/getting-started/templates/react","video":"mRgWdPgv1hE","files":["extension.config.js","src/background.ts","src/content/ContentApp.tsx","src/content/scripts.tsx","src/content/styles.css","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/sidebar/SidebarApp.tsx","src/sidebar/index.html","src/sidebar/scripts.tsx","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/react/screenshot.png","icon":"examples/react/src/images/icon.png"},{"slug":"sidebar","name":"sidebar","title":"Sidebar page","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel to the browser with a simple page.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.js","content/scripts.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel"],"host_permissions":[],"optional_permissions":[],"featured":true,"tags":["sidebar","javascript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Click the extension icon to open the sidebar panel","Edit src/sidebar/index.html and save to watch the panel update"],"useCases":["A sidebar panel that stays open while you browse","Reading and acting on the page beside it","Starting point for sidebar-first extensions"],"docsUrl":"https://extension.js.org/docs/implementation-guide/html","video":"bgGUqLiaZFI","files":["extension.config.js","src/background.js","src/content/scripts.js","src/content/styles.css","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/sidebar/index.html","src/sidebar/scripts.js","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/sidebar/screenshot.png","icon":"examples/sidebar/src/images/icon.png"},{"slug":"sidebar-antd","name":"sidebar-antd","title":"Sidebar Ant Design","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel to the browser showing Ant Design components.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.js","content/scripts.jsx"],"uiFramework":"react","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel"],"host_permissions":[],"optional_permissions":[],"featured":false,"tags":["sidebar","react","antd"],"difficulty":"beginner","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","Click the extension icon to open the sidebar panel","Swap an Ant Design component in src/sidebar/ to see it update"],"useCases":["A sidebar built from Ant Design components","A component library bundled into an extension surface","Starting point for extensions with a ready-made design system"],"docsUrl":"https://ant.design/docs/react/introduce","video":"7YSGpO-RmVE","files":["extension.config.js","src/background.js","src/content/ContentApp.jsx","src/content/scripts.jsx","src/content/styles.css","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/sidebar/SidebarApp.jsx","src/sidebar/index.html","src/sidebar/scripts.jsx","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/sidebar-antd/screenshot.png","icon":"examples/sidebar-antd/src/images/icon.png"},{"slug":"sidebar-monorepo-nx","name":"sidebar-monorepo-nx","title":"Sidebar Nx","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel and shows a small overlay UI on every web page you visit.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.js","content/scripts.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{"dev":"nx dev sidebar-monorepo-nx-packages-extension","build":"nx build sidebar-monorepo-nx-packages-extension"},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel"],"host_permissions":[],"optional_permissions":[],"featured":true,"tags":["sidebar","monorepo","nx","javascript"],"difficulty":"intermediate","timeToFirstSuccessMinutes":4,"firstSteps":["Run npm install at the workspace root to link the packages","Run npm run dev to launch the extension from the workspace","Edit the shared package and both the sidebar and the overlay follow"],"useCases":["An extension that lives inside an Nx workspace","Sharing code between the extension and other packages","Reference for Extension.js in a monorepo with no plugin"],"docsUrl":"https://nx.dev/docs","video":"2zuBnmCVvqg","files":["extension.config.js","packages/analytics/src/index.js","packages/extension/src/background.js","packages/extension/src/content/scripts.js","packages/extension/src/content/styles.css","packages/extension/src/images/icon-128.png","packages/extension/src/images/icon-16.png","packages/extension/src/images/icon-48.png","packages/extension/src/images/icon.png","packages/extension/src/manifest.json","packages/extension/src/sidebar/SidebarApp.js","packages/extension/src/sidebar/index.html","packages/extension/src/sidebar/scripts.js","packages/extension/src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/sidebar-monorepo-nx/screenshot.png","icon":null},{"slug":"sidebar-monorepo-turborepo","name":"sidebar-monorepo-turborepo","title":"Sidebar Turborepo","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel and shows a small overlay UI on every web page you visit.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.js","content/scripts.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{"dev":"extension dev packages/extension","build":"extension build packages/extension"},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel"],"host_permissions":[],"optional_permissions":[],"featured":true,"tags":["sidebar","monorepo","turborepo","javascript"],"difficulty":"intermediate","timeToFirstSuccessMinutes":4,"firstSteps":["Run npm install at the workspace root to link the packages","Run npm run dev to launch the extension from the workspace","Edit the shared package and both the sidebar and the overlay follow"],"useCases":["An extension that lives inside a Turborepo workspace","Sharing code between the extension and other packages","Reference for Extension.js in a monorepo with no plugin"],"docsUrl":"https://turborepo.dev/docs","video":"l3Z90k_4c3I","files":["extension.config.js","packages/analytics/src/index.js","packages/extension/src/background.js","packages/extension/src/content/scripts.js","packages/extension/src/content/styles.css","packages/extension/src/images/icon-128.png","packages/extension/src/images/icon-16.png","packages/extension/src/images/icon-48.png","packages/extension/src/images/icon.png","packages/extension/src/manifest.json","packages/extension/src/sidebar/SidebarApp.js","packages/extension/src/sidebar/index.html","packages/extension/src/sidebar/scripts.js","packages/extension/src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/sidebar-monorepo-turborepo/screenshot.png","icon":null},{"slug":"sidebar-shadcn","name":"sidebar-shadcn","title":"Sidebar shadcn","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel to the browser built with shadcn/ui components.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.ts","content/scripts.tsx"],"uiFramework":"react","css":"css","configFiles":["postcss.config.js","tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel"],"host_permissions":[],"optional_permissions":[],"featured":true,"tags":["sidebar","react","shadcn","tailwind"],"difficulty":"beginner","timeToFirstSuccessMinutes":3,"firstSteps":["Run npm run dev to launch the extension","Click the extension icon to open the sidebar panel","Add a shadcn/ui component and save to see it in the panel"],"useCases":["A sidebar built from shadcn/ui components","Tailwind and Radix primitives inside an extension surface","Starting point for extensions with a modern component kit"],"docsUrl":"https://extension.js.org/docs/integrations/shadcnui","video":"gTpQ7v7zSuY","files":["extension.config.js","src/background.ts","src/components/ui/button.tsx","src/components/ui/card.tsx","src/components/ui/label.tsx","src/components/ui/switch.tsx","src/content/ContentApp.tsx","src/content/scripts.tsx","src/content/styles.css","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/lib/utils.ts","src/manifest.json","src/sidebar/SidebarApp.tsx","src/sidebar/index.html","src/sidebar/scripts.tsx","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/sidebar-shadcn/screenshot.png","icon":"examples/sidebar-shadcn/src/images/icon.png"},{"slug":"special-folders-pages","name":"special-folders-pages","title":"Special Folders: pages/","version":"1.0.0","manifest_version":3,"description":"Opens a welcome page in a new tab when the extension loads.","uiContext":["background"],"surfaces":["background"],"entrypoints":["background.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":[],"host_permissions":[],"optional_permissions":[],"featured":true,"tags":["special-folders","pages","welcome-page","javascript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","A welcome page from src/pages/ opens automatically on install","Add another HTML file under src/pages/ and it ships as a separate page"],"useCases":["Onboarding, help, and welcome pages that ship with the extension","Standalone HTML routes without manifest plumbing","Reference for Extension.js\'s pages/ special folder convention"],"docsUrl":"https://extension.js.org/docs/features/special-folders","files":["extension.config.js","pages/custom.html","pages/main.html","pages/main.js","pages/welcome.html","public/css/file.css","public/html/file.html","public/js/file.js","src/background.js","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/images/javascript.png","src/manifest.json","src/sandbox/eval.js","src/sandbox/index.html","src/sandbox/scripts.js","src/sandbox/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/special-folders-pages/screenshot.png","icon":"examples/special-folders-pages/src/images/icon.png"},{"slug":"special-folders-scripts","name":"special-folders-scripts","title":"Special Folders Scripts","version":"1.0.0","manifest_version":3,"description":"Shows a small overlay on web pages and a toolbar popup that runs the extension\'s bundled scripts.","uiContext":["content","action","background"],"surfaces":["content","action","background"],"entrypoints":["background.js","content/scripts.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["activeTab","scripting"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":false,"tags":["special-folders","scripts","javascript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Open any page for the overlay, then click the toolbar icon for the popup","Add a file under scripts/ and it ships as its own bundle"],"useCases":["Scripts shared between a popup and a content script","Standalone script bundles without manifest plumbing","Reference for the scripts/ special folder convention"],"docsUrl":"https://extension.js.org/docs/features/special-folders","video":"P8Q8TDQqbQE","files":["extension.config.js","scripts/script-one.js","scripts/script-three.js","scripts/script-two.js","src/background.js","src/content/scripts.js","src/content/styles.css","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/images/javascript.png","src/manifest.json"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/special-folders-scripts/screenshot.png","icon":"examples/special-folders-scripts/src/images/icon.png"},{"slug":"svelte","name":"svelte","title":"Svelte","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel to the browser with a simple Svelte page.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.ts","content/scripts.ts"],"uiFramework":"svelte","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel"],"host_permissions":[],"optional_permissions":[],"featured":true,"tags":["sidebar","svelte","typescript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Click the extension icon to open the Svelte sidebar panel","Edit the component in src/sidebar/ and save to watch it update"],"useCases":["A sidebar panel rendered by Svelte","Svelte components compiled with no extra config","Starting point for Svelte extensions"],"docsUrl":"https://extension.js.org/docs/getting-started/templates/svelte","video":"RgMo9t-oLL4","files":["extension.config.js","public/extension.svg","src/background.ts","src/content/ContentApp.svelte","src/content/scripts.ts","src/content/styles.css","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/sidebar/SidebarApp.svelte","src/sidebar/index.html","src/sidebar/scripts.ts","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/svelte/screenshot.png","icon":"examples/svelte/src/images/icon.png"},{"slug":"transformers-js","name":"transformers-js","title":"Transformers.js","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel that classifies the active page or your selected text with on-device AI models.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.js","content/scripts.js"],"uiFramework":"","css":"css","configFiles":["extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel","storage","tabs","contextMenus"],"host_permissions":["<all_urls>"],"optional_permissions":[],"featured":true,"tags":["sidebar","ai","transformers","javascript"],"difficulty":"intermediate","timeToFirstSuccessMinutes":4,"firstSteps":["Run npm run dev to launch the extension","Click the extension icon to open the side panel","Type in the text box, or use the page text or your selection, and the model classifies it on your machine"],"useCases":["Running Transformers.js models inside an extension","Classifying page text with no server and no API key","Reference for shipping on-device AI under MV3"],"docsUrl":"https://extension.js.org/docs/languages-and-frameworks/webassembly","video":"cTxqV5HXXWE","files":["extension.config.js","src/background.js","src/constants.js","src/content/scripts.js","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/sidebar/SidebarApp.js","src/sidebar/index.html","src/sidebar/sakura.css","src/sidebar/scripts.js","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/transformers-js/screenshot.png","icon":"examples/transformers-js/src/images/icon.png"},{"slug":"typescript","name":"typescript","title":"TypeScript","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel to the browser with a simple page.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.ts","content/scripts.ts"],"uiFramework":"","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel"],"host_permissions":[],"optional_permissions":[],"featured":true,"tags":["sidebar","typescript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Click the extension icon to open the sidebar panel","Edit the typed sources in src/sidebar/ to watch the panel update"],"useCases":["A typed sidebar extension with no compiler config","TypeScript compiled by Extension.js out of the box","Starting point for TypeScript extensions"],"docsUrl":"https://extension.js.org/docs/getting-started/templates/typescript","video":"UX91cqUH2DU","files":["extension.config.js","src/background.ts","src/content/ContentApp.ts","src/content/scripts.ts","src/content/styles.css","src/content/types.d.ts","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/sidebar/SidebarApp.ts","src/sidebar/index.html","src/sidebar/scripts.ts","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/typescript/screenshot.png","icon":"examples/typescript/src/images/icon.png"},{"slug":"vue","name":"vue","title":"Vue","version":"1.0.0","manifest_version":3,"description":"Adds a sidebar panel to the browser with a simple Vue page.","uiContext":["content","sidebar","action","background"],"surfaces":["content","action","sidebar","background"],"entrypoints":["sidebar/index.html","background.ts","content/scripts.ts"],"uiFramework":"vue","css":"css","configFiles":["tsconfig.json","extension.config.js"],"hasBackground":true,"hasEnv":false,"setup":{"packageManager":"pnpm","scripts":{},"env":{"hasEnvExample":false,"hasEnv":false}},"permissions":["sidePanel"],"host_permissions":[],"optional_permissions":[],"featured":true,"tags":["sidebar","vue","typescript"],"difficulty":"beginner","timeToFirstSuccessMinutes":2,"firstSteps":["Run npm run dev to launch the extension","Click the extension icon to open the Vue sidebar panel","Edit the component in src/sidebar/ and save to watch it update"],"useCases":["A sidebar panel rendered by Vue","Single-file components compiled with no extra config","Starting point for Vue extensions"],"docsUrl":"https://extension.js.org/docs/getting-started/templates/vue","video":"x55b3Md2zIY","files":["extension.config.js","src/background.ts","src/content/ContentApp.vue","src/content/scripts.ts","src/content/styles.css","src/content/types.d.ts","src/images/icon-128.png","src/images/icon-16.png","src/images/icon-32.png","src/images/icon-48.png","src/images/icon-64.png","src/images/icon.png","src/manifest.json","src/shims-vue.d.ts","src/sidebar/SidebarApp.vue","src/sidebar/index.html","src/sidebar/scripts.ts","src/sidebar/styles.css"],"browsers":["chrome","edge","firefox"],"screenshot":"examples/vue/screenshot.png","icon":"examples/vue/src/images/icon.png"}]}');
|
|
2081
2509
|
const CACHE_DIR = node_path.join(node_os.homedir(), ".cache", "extension-js");
|
|
2082
2510
|
const CACHE_FILE = node_path.join(CACHE_DIR, "templates-meta.json");
|
|
2083
2511
|
const CACHE_TTL_MS = 3600000;
|
|
@@ -2700,11 +3128,110 @@ function removeSession(projectPath, browser, pid) {
|
|
|
2700
3128
|
function listSessions() {
|
|
2701
3129
|
return Array.from(sessions.values());
|
|
2702
3130
|
}
|
|
3131
|
+
const PROJECT_MANIFEST_FILENAMES = [
|
|
3132
|
+
"package.json",
|
|
3133
|
+
"deno.jsonc",
|
|
3134
|
+
"deno.json"
|
|
3135
|
+
];
|
|
3136
|
+
const PACKAGE_MANIFEST = "package.json";
|
|
3137
|
+
const DENO_MANIFESTS = [
|
|
3138
|
+
"deno.jsonc",
|
|
3139
|
+
"deno.json"
|
|
3140
|
+
];
|
|
3141
|
+
const EXTENSION_DEPENDENCY_NAMES = [
|
|
3142
|
+
"extension",
|
|
3143
|
+
"extension-develop",
|
|
3144
|
+
"extension-create"
|
|
3145
|
+
];
|
|
3146
|
+
const EXTENSION_CONFIG_FILENAMES = [
|
|
3147
|
+
"extension.config.js",
|
|
3148
|
+
"extension.config.mjs",
|
|
3149
|
+
"extension.config.cjs"
|
|
3150
|
+
];
|
|
3151
|
+
function isFile(file) {
|
|
3152
|
+
try {
|
|
3153
|
+
return node_fs.statSync(file).isFile();
|
|
3154
|
+
} catch {
|
|
3155
|
+
return false;
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
function nearestAbove(startDir, names) {
|
|
3159
|
+
let current = startDir;
|
|
3160
|
+
for(;;){
|
|
3161
|
+
for (const name of names)if (isFile(node_path.join(current, name))) return node_path.join(current, name);
|
|
3162
|
+
const parent = node_path.dirname(current);
|
|
3163
|
+
if (parent === current) return null;
|
|
3164
|
+
current = parent;
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
function extensionManifestPath(projectPath) {
|
|
3168
|
+
const src = node_path.join(projectPath, "src", "manifest.json");
|
|
3169
|
+
if (isFile(src)) return src;
|
|
3170
|
+
const root = node_path.join(projectPath, "manifest.json");
|
|
3171
|
+
return isFile(root) ? root : null;
|
|
3172
|
+
}
|
|
3173
|
+
function declaresExtension(projectManifestPath) {
|
|
3174
|
+
let parsed;
|
|
3175
|
+
try {
|
|
3176
|
+
parsed = JSON.parse(node_fs.readFileSync(projectManifestPath, "utf8"));
|
|
3177
|
+
} catch {
|
|
3178
|
+
return false;
|
|
3179
|
+
}
|
|
3180
|
+
if (!parsed || "object" != typeof parsed) return false;
|
|
3181
|
+
const record = parsed;
|
|
3182
|
+
const named = [];
|
|
3183
|
+
for (const field of [
|
|
3184
|
+
"dependencies",
|
|
3185
|
+
"devDependencies"
|
|
3186
|
+
]){
|
|
3187
|
+
const deps = record[field];
|
|
3188
|
+
if (deps && "object" == typeof deps) named.push(...Object.keys(deps));
|
|
3189
|
+
}
|
|
3190
|
+
const imports = record.imports;
|
|
3191
|
+
if (imports && "object" == typeof imports) for (const [key, value] of Object.entries(imports)){
|
|
3192
|
+
named.push(key);
|
|
3193
|
+
if ("string" == typeof value) named.push(value);
|
|
3194
|
+
}
|
|
3195
|
+
return named.some((entry)=>EXTENSION_DEPENDENCY_NAMES.some((name)=>entry === name || entry.includes(`${name}@`) || entry === `npm:${name}`));
|
|
3196
|
+
}
|
|
3197
|
+
function ownsManifest(projectManifestPath, manifestPath) {
|
|
3198
|
+
const projectDir = node_path.resolve(node_path.dirname(projectManifestPath));
|
|
3199
|
+
const manifestDir = node_path.resolve(node_path.dirname(manifestPath));
|
|
3200
|
+
if (projectDir === manifestDir) return true;
|
|
3201
|
+
if ("src" === node_path.basename(manifestDir) && node_path.dirname(manifestDir) === projectDir) return true;
|
|
3202
|
+
if (declaresExtension(projectManifestPath)) return true;
|
|
3203
|
+
return EXTENSION_CONFIG_FILENAMES.some((name)=>isFile(node_path.join(projectDir, name)));
|
|
3204
|
+
}
|
|
3205
|
+
function engineProjectRoot(projectPath) {
|
|
3206
|
+
const start = node_path.resolve(projectPath);
|
|
3207
|
+
const manifest = extensionManifestPath(start);
|
|
3208
|
+
if (!manifest) {
|
|
3209
|
+
const nearest = nearestAbove(start, PROJECT_MANIFEST_FILENAMES);
|
|
3210
|
+
return nearest ? node_path.dirname(nearest) : start;
|
|
3211
|
+
}
|
|
3212
|
+
const manifestDir = node_path.dirname(manifest);
|
|
3213
|
+
const packageJson = nearestAbove(manifestDir, [
|
|
3214
|
+
PACKAGE_MANIFEST
|
|
3215
|
+
]);
|
|
3216
|
+
if (packageJson && ownsManifest(packageJson, manifest)) return node_path.dirname(packageJson);
|
|
3217
|
+
const deno = nearestAbove(manifestDir, DENO_MANIFESTS);
|
|
3218
|
+
if (deno && ownsManifest(deno, manifest)) return node_path.dirname(deno);
|
|
3219
|
+
return start;
|
|
3220
|
+
}
|
|
3221
|
+
const rooted = (fn)=>(projectPath, ...rest)=>fn(engineProjectRoot(projectPath), ...rest);
|
|
3222
|
+
rooted(actionsPath);
|
|
3223
|
+
rooted(browserArtifactsDir);
|
|
3224
|
+
const session_paths_buildSummaryPath = rooted(buildSummaryPath);
|
|
3225
|
+
rooted(eventsPath);
|
|
3226
|
+
const session_paths_logsPath = rooted(logsPath);
|
|
3227
|
+
const session_paths_readyContractPath = rooted(bridge_readyContractPath);
|
|
3228
|
+
const session_paths_sessionArtifactsRootDir = rooted(sessionArtifactsRootDir);
|
|
3229
|
+
rooted(bridge_sessionStateDir);
|
|
2703
3230
|
function sessionPathHint(file) {
|
|
2704
3231
|
return `Looked at ${file} (the session-state layout this MCP's pinned engine publishes). If the project runs an older Extension.js, its layout may differ and this path will never appear.`;
|
|
2705
3232
|
}
|
|
2706
3233
|
function profilesRootDir(projectPath) {
|
|
2707
|
-
return node_path.join(
|
|
3234
|
+
return node_path.join(session_paths_sessionArtifactsRootDir(projectPath), "profiles");
|
|
2708
3235
|
}
|
|
2709
3236
|
function browserProfileRootDir(projectPath, browser) {
|
|
2710
3237
|
return node_path.join(profilesRootDir(projectPath), `${browser}-profile`);
|
|
@@ -2722,7 +3249,7 @@ function profileRemediation(input) {
|
|
|
2722
3249
|
return `The engine keeps this session's profile in a directory inside ${root}, one per run: "${PERSISTED_PROFILE_DIR_NAME}" when the profile is persisted, otherwise three random words drawn fresh on every start, which no caller can predict. List ${root} to see which run directories exist and remove the one the stuck browser holds, or remove ${root} entirely once no session is running.`;
|
|
2723
3250
|
}
|
|
2724
3251
|
function contractSightings(projectPath) {
|
|
2725
|
-
const root =
|
|
3252
|
+
const root = session_paths_sessionArtifactsRootDir(projectPath);
|
|
2726
3253
|
let dirs;
|
|
2727
3254
|
try {
|
|
2728
3255
|
dirs = node_fs.readdirSync(root);
|
|
@@ -2731,7 +3258,7 @@ function contractSightings(projectPath) {
|
|
|
2731
3258
|
}
|
|
2732
3259
|
const sightings = [];
|
|
2733
3260
|
for (const dir of dirs){
|
|
2734
|
-
const readyPath =
|
|
3261
|
+
const readyPath = session_paths_readyContractPath(projectPath, dir);
|
|
2735
3262
|
try {
|
|
2736
3263
|
const stat = node_fs.statSync(readyPath);
|
|
2737
3264
|
const contract = JSON.parse(node_fs.readFileSync(readyPath, "utf8"));
|
|
@@ -2792,15 +3319,17 @@ function liveProjectSessions(projectPath) {
|
|
|
2792
3319
|
...out.values()
|
|
2793
3320
|
];
|
|
2794
3321
|
}
|
|
2795
|
-
function deadReadySession(projectPath) {
|
|
2796
|
-
for (const sighting of contractSightings(projectPath))if (
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
3322
|
+
function deadReadySession(projectPath, browser) {
|
|
3323
|
+
for (const sighting of contractSightings(projectPath))if (!browser || sighting.browser === browser) {
|
|
3324
|
+
if (void 0 !== sighting.pid && !pidAlive(sighting.pid)) return {
|
|
3325
|
+
browser: sighting.browser,
|
|
3326
|
+
pid: sighting.pid
|
|
3327
|
+
};
|
|
3328
|
+
}
|
|
2800
3329
|
return null;
|
|
2801
3330
|
}
|
|
2802
3331
|
function contractBoundPort(projectPath, browser, since) {
|
|
2803
|
-
const readyPath =
|
|
3332
|
+
const readyPath = session_paths_readyContractPath(projectPath, browser);
|
|
2804
3333
|
try {
|
|
2805
3334
|
const stat = node_fs.statSync(readyPath);
|
|
2806
3335
|
if (stat.mtimeMs < since) return null;
|
|
@@ -2850,6 +3379,8 @@ const GECKO_FAMILY = new Set([
|
|
|
2850
3379
|
"firefox",
|
|
2851
3380
|
"waterfox",
|
|
2852
3381
|
"librewolf",
|
|
3382
|
+
"zen",
|
|
3383
|
+
"floorp",
|
|
2853
3384
|
"gecko-based",
|
|
2854
3385
|
"firefox-based"
|
|
2855
3386
|
]);
|
|
@@ -3228,7 +3759,7 @@ function readZipEntryNames(zipPath) {
|
|
|
3228
3759
|
}
|
|
3229
3760
|
const build_COMMAND = "extension_build";
|
|
3230
3761
|
function readBuildSummary(projectPath, browser, since) {
|
|
3231
|
-
const file =
|
|
3762
|
+
const file = session_paths_buildSummaryPath(projectPath, browser);
|
|
3232
3763
|
try {
|
|
3233
3764
|
const stat = node_fs.statSync(file);
|
|
3234
3765
|
if (stat.mtimeMs >= since) {
|
|
@@ -3780,6 +4311,11 @@ async function build_handler(args) {
|
|
|
3780
4311
|
}
|
|
3781
4312
|
const engineFailure = "string" == typeof engine.frame?.error?.message ? engine.frame.error.message.trim() : "";
|
|
3782
4313
|
const message = engineFailure || stderr.trim() || out || `extension build exited with code ${code}`;
|
|
4314
|
+
const compileErrors = buildCompileErrors(args.projectPath, browser, start);
|
|
4315
|
+
const tail = [
|
|
4316
|
+
out,
|
|
4317
|
+
stderr.trim()
|
|
4318
|
+
].filter(Boolean).join("\n").trim();
|
|
3783
4319
|
return envelope_envelope({
|
|
3784
4320
|
ok: false,
|
|
3785
4321
|
command: build_COMMAND,
|
|
@@ -3790,12 +4326,30 @@ async function build_handler(args) {
|
|
|
3790
4326
|
},
|
|
3791
4327
|
value: {
|
|
3792
4328
|
browser,
|
|
3793
|
-
duration
|
|
4329
|
+
duration,
|
|
4330
|
+
...compileErrors.length ? {
|
|
4331
|
+
errors: compileErrors
|
|
4332
|
+
} : {},
|
|
4333
|
+
...tail && !compileErrors.length ? {
|
|
4334
|
+
output: tail.slice(-4000)
|
|
4335
|
+
} : {}
|
|
3794
4336
|
},
|
|
3795
4337
|
warnings,
|
|
3796
|
-
hint:
|
|
4338
|
+
hint: compileErrors.length ? `Fix the ${compileErrors.length} compile error${1 === compileErrors.length ? "" : "s"} in value.errors (file, loader and message as the bundler printed them) and build again. extension_manifest_validate covers the manifest; a missing dependency is installed by the build itself.` : "Read value.output for the bundler's own report. The manifest may live at the project root or under src; a failure here is usually a compile error, a manifest the engine refuses, or a Safari toolchain the host does not have."
|
|
3797
4339
|
});
|
|
3798
4340
|
}
|
|
4341
|
+
const ANSI_SEQUENCE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
4342
|
+
function buildCompileErrors(projectPath, browser, since) {
|
|
4343
|
+
try {
|
|
4344
|
+
const file = session_paths_readyContractPath(projectPath, browser);
|
|
4345
|
+
if (node_fs.statSync(file).mtimeMs < since) return [];
|
|
4346
|
+
const contract = JSON.parse(node_fs.readFileSync(file, "utf8"));
|
|
4347
|
+
const errors = Array.isArray(contract?.errors) ? contract.errors : [];
|
|
4348
|
+
return errors.filter((e)=>"string" == typeof e && e.trim()).map((e)=>e.replace(ANSI_SEQUENCE, "").trim()).slice(0, 20);
|
|
4349
|
+
} catch {
|
|
4350
|
+
return [];
|
|
4351
|
+
}
|
|
4352
|
+
}
|
|
3799
4353
|
const MERGED_FD_CHATTER = [
|
|
3800
4354
|
/^npm warn Unknown project config/i,
|
|
3801
4355
|
/This will stop working in the next major version of npm/i,
|
|
@@ -3822,7 +4376,7 @@ const LEGACY_FIDELITY_WARNING = "This session ran a CLI that does not stamp the
|
|
|
3822
4376
|
const READY_STAMP_PROFILE_LOCKED_CODE = "profile_locked";
|
|
3823
4377
|
function readContract(projectPath, browser, since) {
|
|
3824
4378
|
try {
|
|
3825
|
-
const file =
|
|
4379
|
+
const file = session_paths_readyContractPath(projectPath, browser);
|
|
3826
4380
|
const stat = node_fs.statSync(file);
|
|
3827
4381
|
const contract = JSON.parse(node_fs.readFileSync(file, "utf8"));
|
|
3828
4382
|
if (!contract || "object" != typeof contract) return null;
|
|
@@ -4085,7 +4639,7 @@ function projectPathForms(projectPath) {
|
|
|
4085
4639
|
...forms
|
|
4086
4640
|
];
|
|
4087
4641
|
}
|
|
4088
|
-
const PLAUSIBLE_SESSION_BINARY = /chrom|edge|brave|opera|vivaldi|yandex|firefox|waterfox|librewolf|safari|node|electron|extension/i;
|
|
4642
|
+
const PLAUSIBLE_SESSION_BINARY = /chrom|edge|brave|opera|vivaldi|yandex|firefox|waterfox|librewolf|zen|floorp|safari|node|electron|extension/i;
|
|
4089
4643
|
function processCommand(pid) {
|
|
4090
4644
|
try {
|
|
4091
4645
|
const cmdline = node_fs.readFileSync(`/proc/${pid}/cmdline`, "utf8");
|
|
@@ -4107,7 +4661,7 @@ function processCommand(pid) {
|
|
|
4107
4661
|
}
|
|
4108
4662
|
function contractProcessHints(projectPath, browser) {
|
|
4109
4663
|
try {
|
|
4110
|
-
const contract = JSON.parse(node_fs.readFileSync(
|
|
4664
|
+
const contract = JSON.parse(node_fs.readFileSync(session_paths_readyContractPath(projectPath, browser), "utf8"));
|
|
4111
4665
|
const pids = [
|
|
4112
4666
|
contract.browserPid,
|
|
4113
4667
|
contract.launcherPid
|
|
@@ -4174,7 +4728,7 @@ function stop_signal(pid, sig) {
|
|
|
4174
4728
|
}
|
|
4175
4729
|
function pidFromReadyContract(projectPath, browser) {
|
|
4176
4730
|
try {
|
|
4177
|
-
const raw = node_fs.readFileSync(
|
|
4731
|
+
const raw = node_fs.readFileSync(session_paths_readyContractPath(projectPath, browser), "utf8");
|
|
4178
4732
|
const contract = JSON.parse(raw);
|
|
4179
4733
|
return "number" == typeof contract.pid ? contract.pid : null;
|
|
4180
4734
|
} catch {
|
|
@@ -4212,7 +4766,7 @@ async function stopOne(projectPath, browser) {
|
|
|
4212
4766
|
removeSession(projectPath, browser);
|
|
4213
4767
|
removeSessionMarker(projectPath, browser);
|
|
4214
4768
|
try {
|
|
4215
|
-
node_fs.rmSync(
|
|
4769
|
+
node_fs.rmSync(session_paths_readyContractPath(projectPath, browser), {
|
|
4216
4770
|
force: true
|
|
4217
4771
|
});
|
|
4218
4772
|
} catch {}
|
|
@@ -4601,6 +5155,7 @@ async function dev_handler(args) {
|
|
|
4601
5155
|
} : {}
|
|
4602
5156
|
} : {},
|
|
4603
5157
|
capabilities,
|
|
5158
|
+
engine: describeExtensionInvocation(args.projectPath),
|
|
4604
5159
|
logPath
|
|
4605
5160
|
},
|
|
4606
5161
|
warnings: [
|
|
@@ -4613,7 +5168,7 @@ async function dev_handler(args) {
|
|
|
4613
5168
|
}
|
|
4614
5169
|
const start_schema = {
|
|
4615
5170
|
name: "extension_start",
|
|
4616
|
-
description: "Run the PRODUCTION build in a browser: build the project, serve it, and launch. There is no hot module replacement and no control channel, so your edits are not picked up and extension_eval, extension_storage, extension_reload, extension_open and extension_dom_snapshot cannot attach to this session. Use extension_dev while writing code, and this to check what actually ships. Pass build:false to launch an existing dist/<browser> without rebuilding.",
|
|
5171
|
+
description: "Run the PRODUCTION build in a browser: build the project, serve it, and launch. There is no hot module replacement and no control channel, so your edits are not picked up and extension_eval, extension_storage, extension_reload, extension_open and extension_dom_snapshot cannot attach to this session. Use extension_dev while writing code, and this to check what actually ships. Pass build:false to launch an existing dist/<browser> without rebuilding, or outputPath to launch any prebuilt unpacked extension directory, one another toolchain produced included, which implies build:false.",
|
|
4617
5172
|
inputSchema: {
|
|
4618
5173
|
type: "object",
|
|
4619
5174
|
properties: {
|
|
@@ -4638,6 +5193,10 @@ const start_schema = {
|
|
|
4638
5193
|
default: false,
|
|
4639
5194
|
description: "Serve without launching a browser"
|
|
4640
5195
|
},
|
|
5196
|
+
outputPath: {
|
|
5197
|
+
type: "string",
|
|
5198
|
+
description: "An existing unpacked extension directory to launch as it is (a manifest.json at its root), for an artifact built by another toolchain or an exact release candidate. Implies build:false; projectPath still names the project the session belongs to. Relative paths resolve against projectPath."
|
|
5199
|
+
},
|
|
4641
5200
|
...LAUNCH_FLAG_SCHEMA
|
|
4642
5201
|
},
|
|
4643
5202
|
required: [
|
|
@@ -4647,7 +5206,22 @@ const start_schema = {
|
|
|
4647
5206
|
};
|
|
4648
5207
|
async function start_handler(args) {
|
|
4649
5208
|
const browser = args.browser ?? "chrome";
|
|
4650
|
-
const
|
|
5209
|
+
const outputPath = "string" == typeof args.outputPath && args.outputPath.trim() ? node_path.resolve(args.projectPath, args.outputPath.trim()) : null;
|
|
5210
|
+
if (outputPath) {
|
|
5211
|
+
const manifest = node_path.join(outputPath, "manifest.json");
|
|
5212
|
+
if (!node_fs.existsSync(manifest)) return envelope_envelope({
|
|
5213
|
+
ok: false,
|
|
5214
|
+
command: start_schema.name,
|
|
5215
|
+
status: "no-unpacked-extension",
|
|
5216
|
+
error: {
|
|
5217
|
+
code: "E_NO_DIST",
|
|
5218
|
+
name: "NoUnpackedExtension",
|
|
5219
|
+
message: `outputPath ${outputPath} holds no manifest.json, so there is no unpacked extension to launch.`
|
|
5220
|
+
},
|
|
5221
|
+
hint: "Point outputPath at the directory that holds the built manifest.json (for an Extension.js project that is dist/<browser>; for another toolchain, its build output), or omit it to build and run the project."
|
|
5222
|
+
});
|
|
5223
|
+
}
|
|
5224
|
+
const building = false !== args.build && !outputPath;
|
|
4651
5225
|
const command = building ? "start" : "preview";
|
|
4652
5226
|
const cliArgs = [
|
|
4653
5227
|
command,
|
|
@@ -4655,6 +5229,7 @@ async function start_handler(args) {
|
|
|
4655
5229
|
"--browser",
|
|
4656
5230
|
browser
|
|
4657
5231
|
];
|
|
5232
|
+
if (outputPath) cliArgs.push("--output-path", outputPath);
|
|
4658
5233
|
if (building && false === args.polyfill) cliArgs.push("--polyfill", "false");
|
|
4659
5234
|
if (void 0 !== args.port) cliArgs.push("--port", String(args.port));
|
|
4660
5235
|
if (args.noBrowser) cliArgs.push("--no-browser");
|
|
@@ -4771,7 +5346,7 @@ async function start_handler(args) {
|
|
|
4771
5346
|
});
|
|
4772
5347
|
}
|
|
4773
5348
|
function toMcpSpeak(text) {
|
|
4774
|
-
return text.replace(/`?extension dev(?: [^\s`]*)? --browser[= ]([\w-]+) --allow-control`?/g, 'extension_dev with { browser: "$1", allowControl: true }').replace(/--allow-control/g, "allowControl: true (extension_dev)").replace(/--allow-eval/g, "allowEval: true (extension_dev)").replace(/Use --context page --tab <id>/g, 'Use context: "page" (targets the active tab; pass url or tab to pick another)').replace(/--context[= ](background|popup|options|sidebar|devtools|newtab|history|bookmarks|content|page)\b/g, 'context: "$1"').replace(/--tab[= ](\d+|<[\w-]+>)/g, "tab: $1").replace(/--url[= ]"([^"]+)"/g, 'url: "$1"').replace(/--url[= ](<[\w-]+>|\S*(?:\/\/|\*)\S*)/g, 'url: "$1"').replace(/--browser[= ]([\w]+-based|chrome|chromium|edge|brave|opera|vivaldi|yandex|firefox|waterfox|librewolf|safari)\b/g, 'browser: "$1"').replace(/--timeout[= ](\d+)/g, "timeout: $1").replace(/\bextension open (popup|options|sidebar|newtab|history|bookmarks)\b/g, 'extension_open with surface: "$1"').replace(/`extension dev`/g, "extension_dev").replace(/\bextension dev\b/g, "extension_dev").replace(/--tab\b/g, "`tab`").replace(/--url\b/g, "`url`").replace(/--context\b/g, "`context`").replace(/--browser\b/g, "`browser`").replace(/--timeout\b/g, "`timeout`");
|
|
5349
|
+
return text.replace(/`?extension dev(?: [^\s`]*)? --browser[= ]([\w-]+) --allow-control`?/g, 'extension_dev with { browser: "$1", allowControl: true }').replace(/--allow-control/g, "allowControl: true (extension_dev)").replace(/--allow-eval/g, "allowEval: true (extension_dev)").replace(/Use --context page --tab <id>/g, 'Use context: "page" (targets the active tab; pass url or tab to pick another)').replace(/--context[= ](background|popup|options|sidebar|devtools|newtab|history|bookmarks|content|page)\b/g, 'context: "$1"').replace(/--tab[= ](\d+|<[\w-]+>)/g, "tab: $1").replace(/--url[= ]"([^"]+)"/g, 'url: "$1"').replace(/--url[= ](<[\w-]+>|\S*(?:\/\/|\*)\S*)/g, 'url: "$1"').replace(/--browser[= ]([\w]+-based|chrome|chromium|edge|brave|opera|vivaldi|yandex|firefox|waterfox|librewolf|zen|floorp|safari)\b/g, 'browser: "$1"').replace(/--timeout[= ](\d+)/g, "timeout: $1").replace(/\bextension open (popup|options|sidebar|newtab|history|bookmarks)\b/g, 'extension_open with surface: "$1"').replace(/`extension dev`/g, "extension_dev").replace(/\bextension dev\b/g, "extension_dev").replace(/--tab\b/g, "`tab`").replace(/--url\b/g, "`url`").replace(/--context\b/g, "`context`").replace(/--browser\b/g, "`browser`").replace(/--timeout\b/g, "`timeout`");
|
|
4775
5350
|
}
|
|
4776
5351
|
const CONTROL_CHANNEL_DOWN_CODES = new Set([
|
|
4777
5352
|
"E_CONTROL_DENIED",
|
|
@@ -4783,18 +5358,22 @@ const CONTROL_CHANNEL_DOWN_CODES = new Set([
|
|
|
4783
5358
|
function legacyControlChannelScrape(message) {
|
|
4784
5359
|
return /no active control channel|control channel refused|\b1006\b|no executor connected|is the session started with allowControl/i.test(message);
|
|
4785
5360
|
}
|
|
4786
|
-
function withSessionContext(message, projectPath, code) {
|
|
5361
|
+
function withSessionContext(message, projectPath, code, browser) {
|
|
4787
5362
|
const isControlError = code ? CONTROL_CHANNEL_DOWN_CODES.has(code) : legacyControlChannelScrape(message);
|
|
4788
5363
|
if (!isControlError) return message;
|
|
4789
|
-
const dead = deadReadySession(projectPath);
|
|
5364
|
+
const dead = deadReadySession(projectPath, browser);
|
|
4790
5365
|
if (dead) return `${message}\nLikely cause: the dev server has exited, ${dead.browser} ready.json still says ready but its pid ${dead.pid} is dead. Restart with extension_dev (this is not an allowControl problem); extension_doctor confirms.`;
|
|
4791
5366
|
const running = knownSessionBrowsers(projectPath);
|
|
4792
5367
|
if (0 === running.length) return message;
|
|
4793
5368
|
return `${message} Active session browser(s) for this project: ${running.join(", ")}, pass that as \`browser\`, or restart it via extension_dev with allowControl: true if the control channel is off.`;
|
|
4794
5369
|
}
|
|
4795
|
-
function
|
|
5370
|
+
function browserFlag(args) {
|
|
5371
|
+
const at = args.indexOf("--browser");
|
|
5372
|
+
return -1 !== at && "string" == typeof args[at + 1] ? args[at + 1] : void 0;
|
|
5373
|
+
}
|
|
5374
|
+
function translateFrame(frame, projectPath, browser) {
|
|
4796
5375
|
if (!frame || false !== frame.ok) return frame;
|
|
4797
|
-
if (frame.error && "string" == typeof frame.error.message) frame.error.message = withSessionContext(toMcpSpeak(frame.error.message), projectPath, "string" == typeof frame.error.code ? frame.error.code : void 0);
|
|
5376
|
+
if (frame.error && "string" == typeof frame.error.message) frame.error.message = withSessionContext(toMcpSpeak(frame.error.message), projectPath, "string" == typeof frame.error.code ? frame.error.code : void 0, browser);
|
|
4798
5377
|
if ("string" == typeof frame.error?.hint) frame.error.hint = toMcpSpeak(frame.error.hint);
|
|
4799
5378
|
if ("string" == typeof frame.hint) frame.hint = toMcpSpeak(frame.hint);
|
|
4800
5379
|
return frame;
|
|
@@ -4869,7 +5448,7 @@ async function runActVerb(args, projectPath, timeoutMs, tool) {
|
|
|
4869
5448
|
});
|
|
4870
5449
|
const out = stdout.trim();
|
|
4871
5450
|
if (out) try {
|
|
4872
|
-
const frame = translateFrame(JSON.parse(out), projectPath);
|
|
5451
|
+
const frame = translateFrame(JSON.parse(out), projectPath, browserFlag(args));
|
|
4873
5452
|
if (isEnvelope(frame)) {
|
|
4874
5453
|
frame.command = command;
|
|
4875
5454
|
if (!Array.isArray(frame.warnings)) frame.warnings = [];
|
|
@@ -4896,7 +5475,7 @@ async function runActVerb(args, projectPath, timeoutMs, tool) {
|
|
|
4896
5475
|
error: {
|
|
4897
5476
|
code: "E_CLI",
|
|
4898
5477
|
name: "CliError",
|
|
4899
|
-
message: withSessionContext(toMcpSpeak(message), projectPath)
|
|
5478
|
+
message: withSessionContext(toMcpSpeak(message), projectPath, void 0, browserFlag(args))
|
|
4900
5479
|
}
|
|
4901
5480
|
});
|
|
4902
5481
|
}
|
|
@@ -5240,6 +5819,26 @@ class CDPClient extends CDPConnection {
|
|
|
5240
5819
|
if ("string" == typeof data.webSocketDebuggerUrl) return data.webSocketDebuggerUrl;
|
|
5241
5820
|
throw new Error("No webSocketDebuggerUrl in /json/version response");
|
|
5242
5821
|
}
|
|
5822
|
+
static async discoverBrowserVersion(port, host = "127.0.0.1") {
|
|
5823
|
+
try {
|
|
5824
|
+
const res = await fetch(`http://${host}:${port}/json/version`);
|
|
5825
|
+
if (!res.ok) return null;
|
|
5826
|
+
const data = await res.json();
|
|
5827
|
+
return "string" == typeof data.Browser ? data.Browser : null;
|
|
5828
|
+
} catch {
|
|
5829
|
+
return null;
|
|
5830
|
+
}
|
|
5831
|
+
}
|
|
5832
|
+
static async discoverUserAgent(port, host = "127.0.0.1") {
|
|
5833
|
+
try {
|
|
5834
|
+
const res = await fetch(`http://${host}:${port}/json/version`);
|
|
5835
|
+
if (!res.ok) return null;
|
|
5836
|
+
const data = await res.json();
|
|
5837
|
+
return "string" == typeof data["User-Agent"] ? data["User-Agent"] : null;
|
|
5838
|
+
} catch {
|
|
5839
|
+
return null;
|
|
5840
|
+
}
|
|
5841
|
+
}
|
|
5243
5842
|
static async discoverTargets(port, host = "127.0.0.1") {
|
|
5244
5843
|
const res = await fetch(`http://${host}:${port}/json`);
|
|
5245
5844
|
if (!res.ok) throw new Error(`CDP /json failed: ${res.status}`);
|
|
@@ -5364,7 +5963,7 @@ class CDPClient extends CDPConnection {
|
|
|
5364
5963
|
async function resolveContractPort(projectPath, browser, field, options) {
|
|
5365
5964
|
const waitMs = options?.waitMs ?? 20000;
|
|
5366
5965
|
const graceMs = options?.graceMs ?? 2500;
|
|
5367
|
-
const readyPath =
|
|
5966
|
+
const readyPath = session_paths_readyContractPath(projectPath, browser);
|
|
5368
5967
|
const deadline = Date.now() + waitMs;
|
|
5369
5968
|
let contractSeen = false;
|
|
5370
5969
|
let contractSeenAt = null;
|
|
@@ -5441,7 +6040,7 @@ function webdriver_define_property(obj, key, value) {
|
|
|
5441
6040
|
const WEBDRIVER_SESSION_MISSING_HINT = "A Safari page is readable only through a safaridriver session the dev session records in ready.json (webdriverPort and webdriverSessionId), and no Extension.js release opens one today: Safari grants one automation session at a time, so this server never opens its own. What Safari does give you is the dev session's log file, which extension_logs and the log-based assertions read once the extension is enabled; the rest is Web Inspector, attended.";
|
|
5442
6041
|
function readWebDriverSession(projectPath, browser) {
|
|
5443
6042
|
try {
|
|
5444
|
-
const contract = JSON.parse(node_fs.readFileSync(
|
|
6043
|
+
const contract = JSON.parse(node_fs.readFileSync(session_paths_readyContractPath(projectPath, browser), "utf8"));
|
|
5445
6044
|
if ("number" == typeof contract.webdriverPort && Number.isFinite(contract.webdriverPort) && "string" == typeof contract.webdriverSessionId && contract.webdriverSessionId.length > 0) return {
|
|
5446
6045
|
port: contract.webdriverPort,
|
|
5447
6046
|
sessionId: contract.webdriverSessionId
|
|
@@ -5451,7 +6050,7 @@ function readWebDriverSession(projectPath, browser) {
|
|
|
5451
6050
|
}
|
|
5452
6051
|
function readyExtensionId(projectPath, browser) {
|
|
5453
6052
|
try {
|
|
5454
|
-
const contract = JSON.parse(node_fs.readFileSync(
|
|
6053
|
+
const contract = JSON.parse(node_fs.readFileSync(session_paths_readyContractPath(projectPath, browser), "utf8"));
|
|
5455
6054
|
const id = String(contract.extensionId || "").trim();
|
|
5456
6055
|
return id.length > 0 ? id : null;
|
|
5457
6056
|
} catch {
|
|
@@ -5721,11 +6320,18 @@ async function pollForBridgeTab(projectPath, browser, url, budgetMs) {
|
|
|
5721
6320
|
}
|
|
5722
6321
|
}
|
|
5723
6322
|
const UNKNOWN_VERB = /unknown command/i;
|
|
5724
|
-
async function navigateToUrlViaBridge(projectPath, browser, url, timeout, tool = "extension_open") {
|
|
6323
|
+
async function navigateToUrlViaBridge(projectPath, browser, url, timeout, tool = "extension_open", target = {}) {
|
|
5725
6324
|
const viaVerb = await runActVerb([
|
|
5726
6325
|
"navigate",
|
|
5727
6326
|
url,
|
|
5728
6327
|
projectPath,
|
|
6328
|
+
...null != target.tab ? [
|
|
6329
|
+
"--tab",
|
|
6330
|
+
String(target.tab)
|
|
6331
|
+
] : [],
|
|
6332
|
+
...null == target.tab && target.newTab ? [
|
|
6333
|
+
"--new-tab"
|
|
6334
|
+
] : [],
|
|
5729
6335
|
"--browser",
|
|
5730
6336
|
browser,
|
|
5731
6337
|
...null != timeout ? [
|
|
@@ -5812,7 +6418,47 @@ async function navigateToUrlViaBackgroundEval(projectPath, browser, url, timeout
|
|
|
5812
6418
|
hint: "Inspect it with extension_dom_snapshot or extension_eval using url or this numeric tab id (context: 'page'/'content')."
|
|
5813
6419
|
});
|
|
5814
6420
|
}
|
|
6421
|
+
const UUIDS_PREF = /user_pref\("extensions\.webextensions\.uuids",\s*"((?:[^"\\]|\\.)*)"\)/;
|
|
6422
|
+
function geckoAddonId(projectPath, browser) {
|
|
6423
|
+
const read = readBuiltManifest(projectPath, browser);
|
|
6424
|
+
const manifest = read?.manifest;
|
|
6425
|
+
const settings = manifest?.browser_specific_settings ?? manifest?.["firefox:browser_specific_settings"] ?? manifest?.["gecko:browser_specific_settings"] ?? manifest?.applications;
|
|
6426
|
+
const id = settings?.gecko?.id;
|
|
6427
|
+
return "string" == typeof id && id ? id : null;
|
|
6428
|
+
}
|
|
6429
|
+
function sessionProfilePath(projectPath, browser) {
|
|
6430
|
+
try {
|
|
6431
|
+
const contract = JSON.parse(node_fs.readFileSync(session_paths_readyContractPath(projectPath, browser), "utf8"));
|
|
6432
|
+
return "string" == typeof contract.profilePath && contract.profilePath.trim() ? contract.profilePath : null;
|
|
6433
|
+
} catch {
|
|
6434
|
+
return null;
|
|
6435
|
+
}
|
|
6436
|
+
}
|
|
6437
|
+
function readGeckoBaseUrlFromProfile(projectPath, browser) {
|
|
6438
|
+
const addonId = geckoAddonId(projectPath, browser);
|
|
6439
|
+
const profile = sessionProfilePath(projectPath, browser);
|
|
6440
|
+
if (!addonId || !profile) return null;
|
|
6441
|
+
let prefs;
|
|
6442
|
+
try {
|
|
6443
|
+
prefs = node_fs.readFileSync(node_path.join(profile, "prefs.js"), "utf8");
|
|
6444
|
+
} catch {
|
|
6445
|
+
return null;
|
|
6446
|
+
}
|
|
6447
|
+
const match = UUIDS_PREF.exec(prefs);
|
|
6448
|
+
if (!match) return null;
|
|
6449
|
+
try {
|
|
6450
|
+
const map = JSON.parse(JSON.parse(`"${match[1]}"`));
|
|
6451
|
+
const uuid = map[addonId];
|
|
6452
|
+
return "string" == typeof uuid && uuid ? `moz-extension://${uuid}/` : null;
|
|
6453
|
+
} catch {
|
|
6454
|
+
return null;
|
|
6455
|
+
}
|
|
6456
|
+
}
|
|
5815
6457
|
async function resolveBridgeBaseUrl(projectPath, browser, timeout) {
|
|
6458
|
+
const fromRelay = await resolveBridgeBaseUrlThroughRelay(projectPath, browser, timeout);
|
|
6459
|
+
return fromRelay ?? readGeckoBaseUrlFromProfile(projectPath, browser);
|
|
6460
|
+
}
|
|
6461
|
+
async function resolveBridgeBaseUrlThroughRelay(projectPath, browser, timeout) {
|
|
5816
6462
|
const raw = await runActVerb([
|
|
5817
6463
|
"eval",
|
|
5818
6464
|
'(typeof browser !== "undefined" ? browser : chrome).runtime.getURL("")',
|
|
@@ -5844,6 +6490,17 @@ function filterPageTargets(raw) {
|
|
|
5844
6490
|
async function listPageTargets(port) {
|
|
5845
6491
|
return filterPageTargets(await CDPClient.discoverTargets(port));
|
|
5846
6492
|
}
|
|
6493
|
+
function filterDocumentTargets(raw) {
|
|
6494
|
+
return raw.filter((t)=>("page" === t.type || "iframe" === t.type) && !String(t.url ?? "").startsWith("devtools://")).map((t)=>({
|
|
6495
|
+
targetId: String(t.id),
|
|
6496
|
+
type: String(t.type),
|
|
6497
|
+
url: String(t.url ?? ""),
|
|
6498
|
+
title: String(t.title ?? "")
|
|
6499
|
+
}));
|
|
6500
|
+
}
|
|
6501
|
+
async function listDocumentTargets(port) {
|
|
6502
|
+
return filterDocumentTargets(await CDPClient.discoverTargets(port));
|
|
6503
|
+
}
|
|
5847
6504
|
function matchTargetsByUrl(targets, needle) {
|
|
5848
6505
|
const wanted = needle.toLowerCase();
|
|
5849
6506
|
const byUrl = targets.filter((t)=>t.url.toLowerCase().includes(wanted));
|
|
@@ -5853,8 +6510,7 @@ function matchTargetsByUrl(targets, needle) {
|
|
|
5853
6510
|
const RUNTIME_EVALUATE_DEFAULTS = {
|
|
5854
6511
|
returnByValue: true,
|
|
5855
6512
|
awaitPromise: true,
|
|
5856
|
-
userGesture: true
|
|
5857
|
-
replMode: true
|
|
6513
|
+
userGesture: true
|
|
5858
6514
|
};
|
|
5859
6515
|
function stripHash(url) {
|
|
5860
6516
|
return url.replace(/#.*$/, "");
|
|
@@ -5866,9 +6522,67 @@ function matchExtensionPageTargets(targets, wantedUrl) {
|
|
|
5866
6522
|
return url === wanted || url.startsWith(wanted);
|
|
5867
6523
|
});
|
|
5868
6524
|
}
|
|
6525
|
+
const WORKER_TARGET_TYPES = new Set([
|
|
6526
|
+
"service_worker",
|
|
6527
|
+
"background_page",
|
|
6528
|
+
"worker"
|
|
6529
|
+
]);
|
|
6530
|
+
async function findExtensionWorkerTargets(port, extensionId) {
|
|
6531
|
+
try {
|
|
6532
|
+
const origin = `chrome-extension://${extensionId}/`;
|
|
6533
|
+
return (await CDPClient.discoverTargets(port)).filter((t)=>WORKER_TARGET_TYPES.has(String(t.type)) && String(t.url ?? "").startsWith(origin)).map((t)=>({
|
|
6534
|
+
targetId: String(t.id),
|
|
6535
|
+
type: String(t.type),
|
|
6536
|
+
url: String(t.url ?? "")
|
|
6537
|
+
}));
|
|
6538
|
+
} catch {
|
|
6539
|
+
return [];
|
|
6540
|
+
}
|
|
6541
|
+
}
|
|
6542
|
+
async function wakeExtensionWorker(port, extensionId) {
|
|
6543
|
+
const cdp = new CDPClient();
|
|
6544
|
+
const scopeURL = `chrome-extension://${extensionId}/`;
|
|
6545
|
+
try {
|
|
6546
|
+
await cdp.connect(await CDPClient.discoverBrowserWsUrl(port));
|
|
6547
|
+
const pages = (await CDPClient.discoverTargets(port)).filter((t)=>"page" === t.type && !String(t.url ?? "").startsWith("devtools://"));
|
|
6548
|
+
const host = pages.find((t)=>String(t.url ?? "").startsWith(scopeURL)) ?? pages[0];
|
|
6549
|
+
if (!host) return {
|
|
6550
|
+
woken: false,
|
|
6551
|
+
reason: "the session has no page target to issue ServiceWorker.startWorker from"
|
|
6552
|
+
};
|
|
6553
|
+
const sessionId = await cdp.attachToTarget(String(host.id));
|
|
6554
|
+
await cdp.sendCommand("ServiceWorker.enable", {}, sessionId);
|
|
6555
|
+
await cdp.sendCommand("ServiceWorker.startWorker", {
|
|
6556
|
+
scopeURL
|
|
6557
|
+
}, sessionId);
|
|
6558
|
+
const deadline = Date.now() + 3000;
|
|
6559
|
+
for(;;){
|
|
6560
|
+
const targets = await findExtensionWorkerTargets(port, extensionId);
|
|
6561
|
+
if (targets.length > 0) return {
|
|
6562
|
+
woken: true,
|
|
6563
|
+
targets
|
|
6564
|
+
};
|
|
6565
|
+
if (Date.now() >= deadline) break;
|
|
6566
|
+
await new Promise((r)=>setTimeout(r, 150));
|
|
6567
|
+
}
|
|
6568
|
+
return {
|
|
6569
|
+
woken: false,
|
|
6570
|
+
reason: "ServiceWorker.startWorker answered but no worker target was listed within 3s (the extension may declare no background, or the worker exited at once)"
|
|
6571
|
+
};
|
|
6572
|
+
} catch (error) {
|
|
6573
|
+
return {
|
|
6574
|
+
woken: false,
|
|
6575
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
6576
|
+
};
|
|
6577
|
+
} finally{
|
|
6578
|
+
try {
|
|
6579
|
+
cdp.disconnect();
|
|
6580
|
+
} catch {}
|
|
6581
|
+
}
|
|
6582
|
+
}
|
|
5869
6583
|
async function findExtensionPageTargets(port, wantedUrl) {
|
|
5870
6584
|
try {
|
|
5871
|
-
return matchExtensionPageTargets(await
|
|
6585
|
+
return matchExtensionPageTargets(await listDocumentTargets(port), wantedUrl);
|
|
5872
6586
|
} catch {
|
|
5873
6587
|
return [];
|
|
5874
6588
|
}
|
|
@@ -5886,15 +6600,30 @@ function describeException(details) {
|
|
|
5886
6600
|
if (exception && "value" in exception) return String(exception.value);
|
|
5887
6601
|
return details?.text || "the expression threw";
|
|
5888
6602
|
}
|
|
6603
|
+
const TOP_LEVEL_AWAIT_REFUSAL = /await is only valid in async functions/i;
|
|
5889
6604
|
async function evaluateOnExtensionPage(port, targetId, expression) {
|
|
5890
6605
|
const cdp = new CDPClient();
|
|
5891
6606
|
try {
|
|
5892
6607
|
await cdp.connect(await CDPClient.discoverBrowserWsUrl(port));
|
|
5893
6608
|
const sessionId = await cdp.attachToTarget(targetId);
|
|
5894
|
-
|
|
6609
|
+
let response = await cdp.sendCommand("Runtime.evaluate", {
|
|
5895
6610
|
expression,
|
|
5896
6611
|
...RUNTIME_EVALUATE_DEFAULTS
|
|
5897
6612
|
}, sessionId);
|
|
6613
|
+
if (response?.exceptionDetails && TOP_LEVEL_AWAIT_REFUSAL.test(describeException(response.exceptionDetails))) {
|
|
6614
|
+
const asPromise = await cdp.sendCommand("Runtime.evaluate", {
|
|
6615
|
+
expression,
|
|
6616
|
+
returnByValue: false,
|
|
6617
|
+
awaitPromise: false,
|
|
6618
|
+
userGesture: true,
|
|
6619
|
+
replMode: true
|
|
6620
|
+
}, sessionId);
|
|
6621
|
+
const promiseObjectId = asPromise?.result?.objectId;
|
|
6622
|
+
response = asPromise?.exceptionDetails || "string" != typeof promiseObjectId ? asPromise : await cdp.sendCommand("Runtime.awaitPromise", {
|
|
6623
|
+
promiseObjectId,
|
|
6624
|
+
returnByValue: true
|
|
6625
|
+
}, sessionId);
|
|
6626
|
+
}
|
|
5898
6627
|
if (response?.exceptionDetails) return {
|
|
5899
6628
|
ok: false,
|
|
5900
6629
|
thrown: true,
|
|
@@ -6050,13 +6779,153 @@ async function openSidePanelWithSyntheticGesture(port, hostUrl) {
|
|
|
6050
6779
|
};
|
|
6051
6780
|
return {
|
|
6052
6781
|
opened: true,
|
|
6053
|
-
targetId: panel.targetId,
|
|
6054
|
-
url: panel.url
|
|
6782
|
+
targetId: panel.targetId,
|
|
6783
|
+
url: panel.url
|
|
6784
|
+
};
|
|
6785
|
+
} catch (error) {
|
|
6786
|
+
await closeHost();
|
|
6787
|
+
return {
|
|
6788
|
+
opened: false,
|
|
6789
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
6790
|
+
};
|
|
6791
|
+
} finally{
|
|
6792
|
+
try {
|
|
6793
|
+
cdp.disconnect();
|
|
6794
|
+
} catch {}
|
|
6795
|
+
}
|
|
6796
|
+
}
|
|
6797
|
+
const PANEL_IDS_EXPRESSION = "import('./ui/legacy/legacy.js').then((m) => m.InspectorView.InspectorView.instance().tabbedPane.tabIds())";
|
|
6798
|
+
function showPanelExpression(panelId) {
|
|
6799
|
+
return `import('./ui/legacy/legacy.js').then((m) => m.InspectorView.InspectorView.instance().showPanel(${JSON.stringify(panelId)})).then(() => "shown")`;
|
|
6800
|
+
}
|
|
6801
|
+
function panelIdPrefix(extensionId) {
|
|
6802
|
+
return `chrome-extension://${extensionId}`;
|
|
6803
|
+
}
|
|
6804
|
+
function panelTitleFromId(panelId, extensionId) {
|
|
6805
|
+
return panelId.slice(panelIdPrefix(extensionId).length);
|
|
6806
|
+
}
|
|
6807
|
+
function isDevToolsFrontend(target) {
|
|
6808
|
+
return String(target.url ?? "").startsWith("devtools://");
|
|
6809
|
+
}
|
|
6810
|
+
async function sleep(ms) {
|
|
6811
|
+
await new Promise((r)=>setTimeout(r, ms));
|
|
6812
|
+
}
|
|
6813
|
+
async function openDevToolsPanel(port, options) {
|
|
6814
|
+
const budgetMs = options.budgetMs ?? 15000;
|
|
6815
|
+
const cdp = new CDPClient();
|
|
6816
|
+
const listTargets = async ()=>await CDPClient.discoverTargets(port);
|
|
6817
|
+
try {
|
|
6818
|
+
await cdp.connect(await CDPClient.discoverBrowserWsUrl(port));
|
|
6819
|
+
const before = new Set((await listTargets().catch(()=>[])).filter(isDevToolsFrontend).map((t)=>t.id));
|
|
6820
|
+
let devtoolsTargetId = null;
|
|
6821
|
+
try {
|
|
6822
|
+
const opened = await cdp.sendCommand("Target.openDevTools", {
|
|
6823
|
+
targetId: options.inspectedTargetId
|
|
6824
|
+
});
|
|
6825
|
+
if ("string" == typeof opened?.targetId) devtoolsTargetId = opened.targetId;
|
|
6826
|
+
} catch (error) {
|
|
6827
|
+
return {
|
|
6828
|
+
opened: false,
|
|
6829
|
+
stage: "open",
|
|
6830
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
6831
|
+
};
|
|
6832
|
+
}
|
|
6833
|
+
const deadline = Date.now() + budgetMs;
|
|
6834
|
+
while(!devtoolsTargetId && Date.now() < deadline){
|
|
6835
|
+
await sleep(200);
|
|
6836
|
+
const fresh = (await listTargets().catch(()=>[])).find((t)=>isDevToolsFrontend(t) && !before.has(t.id));
|
|
6837
|
+
if (fresh) devtoolsTargetId = fresh.id;
|
|
6838
|
+
}
|
|
6839
|
+
if (!devtoolsTargetId) return {
|
|
6840
|
+
opened: false,
|
|
6841
|
+
stage: "frontend",
|
|
6842
|
+
reason: "Target.openDevTools answered but no devtools:// frontend target appeared"
|
|
6843
|
+
};
|
|
6844
|
+
const sessionId = await cdp.attachToTarget(devtoolsTargetId);
|
|
6845
|
+
if (options.reloadInspected) {
|
|
6846
|
+
const frameDeadline = Date.now() + Math.min(5000, budgetMs / 2);
|
|
6847
|
+
while(Date.now() < frameDeadline){
|
|
6848
|
+
const frame = (await listTargets().catch(()=>[])).find((t)=>"iframe" === t.type && String(t.url ?? "").startsWith(options.devtoolsPageUrl));
|
|
6849
|
+
if (frame) break;
|
|
6850
|
+
await sleep(200);
|
|
6851
|
+
}
|
|
6852
|
+
await sleep(Math.min(2500, budgetMs / 4));
|
|
6853
|
+
try {
|
|
6854
|
+
const inspectedSession = await cdp.attachToTarget(options.inspectedTargetId);
|
|
6855
|
+
await cdp.sendCommand("Page.reload", {}, inspectedSession);
|
|
6856
|
+
} catch {}
|
|
6857
|
+
}
|
|
6858
|
+
const evaluate = async (expression)=>await cdp.sendCommand("Runtime.evaluate", {
|
|
6859
|
+
expression,
|
|
6860
|
+
returnByValue: true,
|
|
6861
|
+
awaitPromise: true
|
|
6862
|
+
}, sessionId).catch(()=>null);
|
|
6863
|
+
const wantedId = "string" == typeof options.panelTitle && options.panelTitle ? `${panelIdPrefix(options.extensionId)}${options.panelTitle}` : null;
|
|
6864
|
+
const prefix = panelIdPrefix(options.extensionId);
|
|
6865
|
+
let panels = [];
|
|
6866
|
+
let panelId = null;
|
|
6867
|
+
let nudged = false;
|
|
6868
|
+
const nudgeAt = Date.now() + Math.min(4000, budgetMs / 2);
|
|
6869
|
+
while(!panelId && Date.now() < deadline){
|
|
6870
|
+
const response = await evaluate(PANEL_IDS_EXPRESSION);
|
|
6871
|
+
const ids = response?.result?.value;
|
|
6872
|
+
if (Array.isArray(ids)) {
|
|
6873
|
+
panels = ids.map(String);
|
|
6874
|
+
panelId = panels.find((id)=>wantedId ? id === wantedId : id.startsWith(prefix)) ?? null;
|
|
6875
|
+
}
|
|
6876
|
+
if (panelId) break;
|
|
6877
|
+
if (!nudged && Date.now() >= nudgeAt) {
|
|
6878
|
+
nudged = true;
|
|
6879
|
+
const frame = (await listTargets().catch(()=>[])).find((t)=>"iframe" === t.type && String(t.url ?? "").startsWith(options.devtoolsPageUrl));
|
|
6880
|
+
if (frame) try {
|
|
6881
|
+
const frameSession = await cdp.attachToTarget(frame.id);
|
|
6882
|
+
await cdp.sendCommand("Runtime.evaluate", {
|
|
6883
|
+
expression: "1",
|
|
6884
|
+
returnByValue: true
|
|
6885
|
+
}, frameSession);
|
|
6886
|
+
} catch {}
|
|
6887
|
+
}
|
|
6888
|
+
await sleep(250);
|
|
6889
|
+
}
|
|
6890
|
+
if (!panelId) return {
|
|
6891
|
+
opened: false,
|
|
6892
|
+
stage: "panel",
|
|
6893
|
+
devtoolsTargetId,
|
|
6894
|
+
panels,
|
|
6895
|
+
reason: wantedId ? `no panel with id ${wantedId} registered within ${budgetMs}ms` : `no panel from ${prefix} registered within ${budgetMs}ms`
|
|
6896
|
+
};
|
|
6897
|
+
const framesBefore = new Set((await listTargets().catch(()=>[])).filter((t)=>"iframe" === t.type && String(t.url ?? "").startsWith(`${prefix}/`)).map((t)=>t.id));
|
|
6898
|
+
const shown = await evaluate(showPanelExpression(panelId));
|
|
6899
|
+
if (!shown || shown.exceptionDetails) return {
|
|
6900
|
+
opened: false,
|
|
6901
|
+
stage: "show",
|
|
6902
|
+
devtoolsTargetId,
|
|
6903
|
+
panels,
|
|
6904
|
+
reason: `showPanel(${panelId}) ${shown?.exceptionDetails ? `threw: ${shown.exceptionDetails.exception?.description ?? shown.exceptionDetails.text ?? "unknown"}` : "did not answer"}`
|
|
6905
|
+
};
|
|
6906
|
+
let panelTarget = null;
|
|
6907
|
+
const showDeadline = Date.now() + 3000;
|
|
6908
|
+
while(!panelTarget && Date.now() < showDeadline){
|
|
6909
|
+
const frames = (await listTargets().catch(()=>[])).filter((t)=>"iframe" === t.type && String(t.url ?? "").startsWith(`${prefix}/`) && !String(t.url ?? "").startsWith(options.devtoolsPageUrl));
|
|
6910
|
+
const fresh = frames.find((t)=>!framesBefore.has(t.id)) ?? frames[0];
|
|
6911
|
+
if (fresh) panelTarget = {
|
|
6912
|
+
targetId: fresh.id,
|
|
6913
|
+
url: String(fresh.url)
|
|
6914
|
+
};
|
|
6915
|
+
else await sleep(200);
|
|
6916
|
+
}
|
|
6917
|
+
return {
|
|
6918
|
+
opened: true,
|
|
6919
|
+
devtoolsTargetId,
|
|
6920
|
+
panelId,
|
|
6921
|
+
panelTitle: panelTitleFromId(panelId, options.extensionId),
|
|
6922
|
+
panelTarget,
|
|
6923
|
+
panels
|
|
6055
6924
|
};
|
|
6056
6925
|
} catch (error) {
|
|
6057
|
-
await closeHost();
|
|
6058
6926
|
return {
|
|
6059
6927
|
opened: false,
|
|
6928
|
+
stage: "open",
|
|
6060
6929
|
reason: error instanceof Error ? error.message : String(error)
|
|
6061
6930
|
};
|
|
6062
6931
|
} finally{
|
|
@@ -6098,9 +6967,37 @@ async function pollForTarget(port, url, budgetMs, navigatedTargetId) {
|
|
|
6098
6967
|
await new Promise((r)=>setTimeout(r, 250));
|
|
6099
6968
|
}
|
|
6100
6969
|
}
|
|
6101
|
-
function
|
|
6970
|
+
async function landedOnErrorPage(port, targetId) {
|
|
6971
|
+
await new Promise((r)=>setTimeout(r, 400));
|
|
6972
|
+
try {
|
|
6973
|
+
const target = (await CDPClient.discoverTargets(port)).find((t)=>String(t.id) === targetId);
|
|
6974
|
+
const url = String(target?.url ?? "");
|
|
6975
|
+
if (/^(chrome|edge)-error:\/\//.test(url)) return {
|
|
6976
|
+
url,
|
|
6977
|
+
title: "string" == typeof target?.title ? target.title : void 0
|
|
6978
|
+
};
|
|
6979
|
+
} catch {}
|
|
6980
|
+
return null;
|
|
6981
|
+
}
|
|
6982
|
+
async function browserRunsHeadless(projectPath, browser) {
|
|
6983
|
+
if (!isChromiumFamily(browser)) return false;
|
|
6984
|
+
try {
|
|
6985
|
+
const resolved = await resolveCdpPort(projectPath, browser, {
|
|
6986
|
+
waitMs: 2000
|
|
6987
|
+
});
|
|
6988
|
+
if (!resolved) return false;
|
|
6989
|
+
const product = await CDPClient.discoverBrowserVersion(resolved.port);
|
|
6990
|
+
if ("string" == typeof product && /headless/i.test(product)) return true;
|
|
6991
|
+
const agent = "function" == typeof CDPClient.discoverUserAgent ? await CDPClient.discoverUserAgent(resolved.port) : null;
|
|
6992
|
+
return "string" == typeof agent && /headless/i.test(agent);
|
|
6993
|
+
} catch {
|
|
6994
|
+
return false;
|
|
6995
|
+
}
|
|
6996
|
+
}
|
|
6997
|
+
function isDisposableTab(tabUrl, destination, reuse) {
|
|
6102
6998
|
if (!tabUrl || "about:blank" === tabUrl) return true;
|
|
6103
6999
|
if (/^chrome:\/\/(newtab|new-tab-page)/.test(tabUrl)) return true;
|
|
7000
|
+
if ("blank" === reuse) return false;
|
|
6104
7001
|
const origin = destination.match(/^chrome-extension:\/\/[a-p]{32}\//)?.[0];
|
|
6105
7002
|
return Boolean(origin && tabUrl.startsWith(origin));
|
|
6106
7003
|
}
|
|
@@ -6134,9 +7031,15 @@ async function navigateToUrlViaWebDriver(projectPath, browser, url) {
|
|
|
6134
7031
|
});
|
|
6135
7032
|
}
|
|
6136
7033
|
}
|
|
6137
|
-
async function navigateToUrl(projectPath, browser, url, timeout) {
|
|
7034
|
+
async function navigateToUrl(projectPath, browser, url, timeout, options = {}) {
|
|
7035
|
+
const reuse = options.reuse ?? "blank";
|
|
7036
|
+
if (null != options.tab) return navigateToUrlViaBridge(projectPath, browser, url, timeout, open_schema.name, {
|
|
7037
|
+
tab: options.tab
|
|
7038
|
+
});
|
|
6138
7039
|
if (WEBKIT_FAMILY.has(browser) && readWebDriverSession(projectPath, browser)) return navigateToUrlViaWebDriver(projectPath, browser, url);
|
|
6139
|
-
if (!isChromiumFamily(browser)) return navigateToUrlViaBridge(projectPath, browser, url, timeout
|
|
7040
|
+
if (!isChromiumFamily(browser)) return navigateToUrlViaBridge(projectPath, browser, url, timeout, open_schema.name, {
|
|
7041
|
+
newTab: true
|
|
7042
|
+
});
|
|
6140
7043
|
const resolved = await resolveCdpPort(projectPath, browser);
|
|
6141
7044
|
if (!resolved) return envelope_envelope({
|
|
6142
7045
|
ok: false,
|
|
@@ -6154,7 +7057,7 @@ async function navigateToUrl(projectPath, browser, url, timeout) {
|
|
|
6154
7057
|
const pageTargets = targets.filter((t)=>"page" === t.type && !String(t.url || "").startsWith("devtools://"));
|
|
6155
7058
|
const browserWsUrl = await CDPClient.discoverBrowserWsUrl(resolved.port);
|
|
6156
7059
|
await cdp.connect(browserWsUrl);
|
|
6157
|
-
const reusable = pageTargets.find((t)=>isDisposableTab(String(t.url ?? ""), url));
|
|
7060
|
+
const reusable = pageTargets.find((t)=>isDisposableTab(String(t.url ?? ""), url, reuse));
|
|
6158
7061
|
let navigatedTargetId;
|
|
6159
7062
|
let openedNewTab = false;
|
|
6160
7063
|
if (reusable) {
|
|
@@ -6172,6 +7075,26 @@ async function navigateToUrl(projectPath, browser, url, timeout) {
|
|
|
6172
7075
|
openedNewTab = true;
|
|
6173
7076
|
}
|
|
6174
7077
|
const settled = await pollForTarget(resolved.port, url, 6000, navigatedTargetId);
|
|
7078
|
+
const blocked = settled ? await landedOnErrorPage(resolved.port, settled.id) : null;
|
|
7079
|
+
if (blocked) return envelope_envelope({
|
|
7080
|
+
ok: false,
|
|
7081
|
+
command: open_schema.name,
|
|
7082
|
+
status: "navigate-blocked",
|
|
7083
|
+
error: {
|
|
7084
|
+
code: "E_NAVIGATE_FAILED",
|
|
7085
|
+
name: "PageBlocked",
|
|
7086
|
+
message: `The browser refused ${url} and shows its own error page instead${blocked.title ? ` ("${blocked.title}")` : ""}.`
|
|
7087
|
+
},
|
|
7088
|
+
value: {
|
|
7089
|
+
navigated: url,
|
|
7090
|
+
target: {
|
|
7091
|
+
targetId: settled.id,
|
|
7092
|
+
url: blocked.url,
|
|
7093
|
+
title: blocked.title
|
|
7094
|
+
}
|
|
7095
|
+
},
|
|
7096
|
+
hint: "Nothing is rendering the requested document. Read the tab title for the browser's reason (Edge, for one, blocks some extension pages with ERR_BLOCKED_BY_CLIENT); try another browser with extension_dev, or a different document of the same extension."
|
|
7097
|
+
});
|
|
6175
7098
|
if (!settled) {
|
|
6176
7099
|
const isExtensionPage = url.startsWith("chrome-extension://");
|
|
6177
7100
|
return envelope_envelope({
|
|
@@ -6279,7 +7202,7 @@ function declaredCommands(projectPath, browser) {
|
|
|
6279
7202
|
}
|
|
6280
7203
|
function readDistPath(projectPath, browser) {
|
|
6281
7204
|
try {
|
|
6282
|
-
const file =
|
|
7205
|
+
const file = session_paths_readyContractPath(projectPath, browser);
|
|
6283
7206
|
const contract = JSON.parse(node_fs.readFileSync(file, "utf8"));
|
|
6284
7207
|
return "string" == typeof contract?.distPath ? contract.distPath : null;
|
|
6285
7208
|
} catch {
|
|
@@ -6295,7 +7218,7 @@ function surfaceDocument(projectPath, browser, surface) {
|
|
|
6295
7218
|
continue;
|
|
6296
7219
|
}
|
|
6297
7220
|
const action = manifest.action ?? manifest.browser_action;
|
|
6298
|
-
const ref = "popup" === surface || "action" === surface ? action?.default_popup : "options" === surface ? manifest.options_ui?.page ?? manifest.options_page : "sidebar" === surface ? manifest.side_panel?.default_path ?? manifest.sidebar_action?.default_panel : "newtab" === surface || "history" === surface || "bookmarks" === surface ? manifest.chrome_url_overrides?.[surface] : null;
|
|
7221
|
+
const ref = "popup" === surface || "action" === surface ? action?.default_popup : "options" === surface ? manifest.options_ui?.page ?? manifest.options_page : "sidebar" === surface ? manifest.side_panel?.default_path ?? manifest.sidebar_action?.default_panel : "devtools" === surface ? manifest.devtools_page : "newtab" === surface || "history" === surface || "bookmarks" === surface ? manifest.chrome_url_overrides?.[surface] : null;
|
|
6299
7222
|
if ("string" == typeof ref && ref) return ref.replace(/^\.?\//, "");
|
|
6300
7223
|
}
|
|
6301
7224
|
return null;
|
|
@@ -6304,6 +7227,7 @@ const SURFACE_MANIFEST_KEYS = {
|
|
|
6304
7227
|
popup: "action.default_popup",
|
|
6305
7228
|
options: "options_ui.page (or options_page)",
|
|
6306
7229
|
sidebar: "side_panel.default_path (or sidebar_action.default_panel)",
|
|
7230
|
+
devtools: "devtools_page",
|
|
6307
7231
|
newtab: "chrome_url_overrides.newtab",
|
|
6308
7232
|
history: "chrome_url_overrides.history",
|
|
6309
7233
|
bookmarks: "chrome_url_overrides.bookmarks"
|
|
@@ -6435,14 +7359,16 @@ async function openSurfaceAsTab(projectPath, browser, surface) {
|
|
|
6435
7359
|
error: {
|
|
6436
7360
|
code: "E_NO_EXTENSION_ID",
|
|
6437
7361
|
name: "NoExtensionId",
|
|
6438
|
-
message: "Could not resolve the extension's moz-extension:// base URL
|
|
7362
|
+
message: "Could not resolve the extension's moz-extension:// base URL: the background eval of runtime.getURL did not answer (an extension CSP that forbids eval refuses it) and the profile's extensions.webextensions.uuids pref had no entry for the add-on id the built manifest declares."
|
|
6439
7363
|
},
|
|
6440
|
-
hint: "Confirm the session is ready (extension_wait) and was started with allowEval: true (extension_dev)."
|
|
7364
|
+
hint: "Confirm the session is ready (extension_wait) and was started with allowEval: true (extension_dev); the profile route needs browser_specific_settings.gecko.id in the built manifest and profilePath in the session contract."
|
|
6441
7365
|
});
|
|
6442
7366
|
url = `${base}${doc}`;
|
|
6443
7367
|
extensionId = base.replace(/^.*:\/\//, "").replace(/\/$/, "");
|
|
6444
7368
|
}
|
|
6445
|
-
const raw = await navigateToUrl(projectPath, browser, url
|
|
7369
|
+
const raw = await navigateToUrl(projectPath, browser, url, void 0, {
|
|
7370
|
+
reuse: "surface"
|
|
7371
|
+
});
|
|
6446
7372
|
try {
|
|
6447
7373
|
const parsed = JSON.parse(raw);
|
|
6448
7374
|
if (parsed?.ok) {
|
|
@@ -6509,7 +7435,7 @@ async function confirmSurfaceTarget(projectPath, browser, surface, raw) {
|
|
|
6509
7435
|
}
|
|
6510
7436
|
const open_schema = {
|
|
6511
7437
|
name: "extension_open",
|
|
6512
|
-
description: "Open an extension surface, or replay an event, in a running session. Pass surface:'popup', 'options' or 'sidebar' to open a UI surface, or 'newtab', 'history' or 'bookmarks' to open the matching chrome_url_overrides page in a tab (always a tab, resolved by the server, never sent to the engine). On Chromium, when Chrome refuses the sidebar for lack of a user gesture, the server opens the real panel through a synthetic click on the extension's own page and says so in warnings; if that fails too it renders the sidebar document as a tab. Pass surface:'action' to trigger the toolbar action, which opens its popup or replays chrome.action.onClicked when there is none. Pass surface:'command' with `name` to replay a chrome.commands.onCommand shortcut. Note that action and command replay invoke your listener without a user gesture, so the gesture-derived activeTab grant does not apply; the result reports gesture:false and warns when activeTab is declared. Start the session with allowControl:true (extension_dev).",
|
|
7438
|
+
description: "Open an extension surface, or replay an event, in a running session. Pass surface:'popup', 'options' or 'sidebar' to open a UI surface, or 'newtab', 'history' or 'bookmarks' to open the matching chrome_url_overrides page in a tab (always a tab, resolved by the server, never sent to the engine). On Chromium, when Chrome refuses the sidebar for lack of a user gesture, the server opens the real panel through a synthetic click on the extension's own page and says so in warnings; if that fails too it renders the sidebar document as a tab. Pass surface:'devtools' to open the browser's DevTools on a tab (the one `url` matches, else the first web page) and show the extension's panel there, picked by `panel` title when there are several: Chromium only, over CDP Target.openDevTools, headed or headless; the result names the panel document's url, which extension_eval reads with context 'page' and that url (the panel is no tab, so the tab-based readers do not reach it). Pass surface:'action' to trigger the toolbar action, which opens its popup or replays chrome.action.onClicked when there is none. Pass surface:'command' with `name` to replay a chrome.commands.onCommand shortcut. Note that action and command replay invoke your listener without a user gesture, so the gesture-derived activeTab grant does not apply; the result reports gesture:false and warns when activeTab is declared. Start the session with allowControl:true (extension_dev).",
|
|
6513
7439
|
inputSchema: {
|
|
6514
7440
|
type: "object",
|
|
6515
7441
|
properties: {
|
|
@@ -6520,6 +7446,7 @@ const open_schema = {
|
|
|
6520
7446
|
"popup",
|
|
6521
7447
|
"options",
|
|
6522
7448
|
"sidebar",
|
|
7449
|
+
"devtools",
|
|
6523
7450
|
"newtab",
|
|
6524
7451
|
"history",
|
|
6525
7452
|
"bookmarks",
|
|
@@ -6532,9 +7459,26 @@ const open_schema = {
|
|
|
6532
7459
|
type: "string",
|
|
6533
7460
|
description: "For surface 'command': the chrome.commands name to trigger."
|
|
6534
7461
|
},
|
|
7462
|
+
panel: {
|
|
7463
|
+
type: "string",
|
|
7464
|
+
description: "For surface 'devtools': the title the extension gave chrome.devtools.panels.create, when it registers more than one panel. Omitted, the extension's first panel is shown."
|
|
7465
|
+
},
|
|
7466
|
+
waitMs: {
|
|
7467
|
+
type: "number",
|
|
7468
|
+
description: "For surface 'devtools': how long to wait for the panel to register after DevTools opens (default 15000, up to 120000). Extensions that create their panel on a page event need longer, or `reload`."
|
|
7469
|
+
},
|
|
7470
|
+
reload: {
|
|
7471
|
+
type: "boolean",
|
|
7472
|
+
default: false,
|
|
7473
|
+
description: "For surface 'devtools': reload the inspected tab once DevTools is open, for extensions that create their panel only when the page reports to them on a load that starts with DevTools open (Preact Devtools). Discards the page state under test."
|
|
7474
|
+
},
|
|
6535
7475
|
url: {
|
|
6536
7476
|
type: "string",
|
|
6537
|
-
description: "Navigate a real tab here instead of opening a surface (
|
|
7477
|
+
description: "Navigate a real tab here instead of opening a surface, in a NEW tab unless `tab` names one (a blank or new-tab page is reused). An absolute url opens as given; a path with no scheme, such as pages/options.html, is resolved against the extension's own origin. Use for content-script test pages, or a surface as a page."
|
|
7478
|
+
},
|
|
7479
|
+
tab: {
|
|
7480
|
+
type: "number",
|
|
7481
|
+
description: "With `url`: navigate this chrome.tabs id in place instead of opening a new tab (rides the engine's navigate verb, so the session needs allowControl: true). Without it an existing page is never taken over."
|
|
6538
7482
|
},
|
|
6539
7483
|
asTab: {
|
|
6540
7484
|
type: "boolean",
|
|
@@ -6554,9 +7498,51 @@ function sessionIsHeadless() {
|
|
|
6554
7498
|
return /(^|\s|=)-{1,2}headless\b/i.test(process.env.EXTENSION_BROWSER_FLAGS ?? "");
|
|
6555
7499
|
}
|
|
6556
7500
|
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)";
|
|
7501
|
+
async function resolveExtensionDocumentUrl(projectPath, browser, relative) {
|
|
7502
|
+
const doc = relative.replace(/^\.?\//, "");
|
|
7503
|
+
if (isChromiumFamily(browser)) {
|
|
7504
|
+
const extensionId = await resolveExtensionId(projectPath, browser);
|
|
7505
|
+
if (extensionId) return `chrome-extension://${extensionId}/${doc}`;
|
|
7506
|
+
return {
|
|
7507
|
+
refusal: envelope_envelope({
|
|
7508
|
+
ok: false,
|
|
7509
|
+
command: open_schema.name,
|
|
7510
|
+
status: "no-extension-id",
|
|
7511
|
+
error: {
|
|
7512
|
+
code: "E_NO_EXTENSION_ID",
|
|
7513
|
+
name: "NoExtensionId",
|
|
7514
|
+
message: `"${relative}" has no scheme, so it was read as a document inside the extension, but the extension id could not be resolved from the live session's CDP targets.`
|
|
7515
|
+
},
|
|
7516
|
+
hint: `Confirm the session is ready (extension_wait), or pass the full chrome-extension://<id>/${doc} url. ${CDP_PORT_MISSING_HINT}`
|
|
7517
|
+
})
|
|
7518
|
+
};
|
|
7519
|
+
}
|
|
7520
|
+
const base = await resolveBridgeBaseUrl(projectPath, browser);
|
|
7521
|
+
if (base) return `${base}${doc}`;
|
|
7522
|
+
return {
|
|
7523
|
+
refusal: envelope_envelope({
|
|
7524
|
+
ok: false,
|
|
7525
|
+
command: open_schema.name,
|
|
7526
|
+
status: "no-extension-id",
|
|
7527
|
+
error: {
|
|
7528
|
+
code: "E_NO_EXTENSION_ID",
|
|
7529
|
+
name: "NoExtensionId",
|
|
7530
|
+
message: `"${relative}" has no scheme, so it was read as a document inside the extension, but the extension's moz-extension:// base could not be resolved from the live session.`
|
|
7531
|
+
},
|
|
7532
|
+
hint: `Pass the full moz-extension://<uuid>/${doc} url (extension_list_extensions reports the uuid), or start the session with allowEval: true so the base can be read from the background.`
|
|
7533
|
+
})
|
|
7534
|
+
};
|
|
7535
|
+
}
|
|
6557
7536
|
async function open_handler(args) {
|
|
6558
7537
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
6559
|
-
if (args.
|
|
7538
|
+
if ("devtools" === args.surface) return openDevToolsSurface(args, browser);
|
|
7539
|
+
if (args.url) {
|
|
7540
|
+
const absolute = /^[a-z][a-z0-9+.-]*:/i.test(args.url) ? args.url : await resolveExtensionDocumentUrl(args.projectPath, browser, args.url);
|
|
7541
|
+
if ("string" != typeof absolute) return absolute.refusal;
|
|
7542
|
+
return navigateToUrl(args.projectPath, browser, absolute, args.timeout, {
|
|
7543
|
+
tab: args.tab
|
|
7544
|
+
});
|
|
7545
|
+
}
|
|
6560
7546
|
const AS_TAB_SURFACES = [
|
|
6561
7547
|
"popup",
|
|
6562
7548
|
"options",
|
|
@@ -6596,6 +7582,20 @@ async function open_handler(args) {
|
|
|
6596
7582
|
const declared = declaredSurfaces(args.projectPath, browser);
|
|
6597
7583
|
if (declared && !declared.includes("popup")) return missingSurfaceError(args.projectPath, browser, "popup", "so there is no popup to open");
|
|
6598
7584
|
}
|
|
7585
|
+
if ([
|
|
7586
|
+
"popup",
|
|
7587
|
+
"options",
|
|
7588
|
+
"sidebar"
|
|
7589
|
+
].includes(args.surface) && !sessionIsHeadless() && await browserRunsHeadless(args.projectPath, browser)) {
|
|
7590
|
+
const asTab = await openSurfaceAsTab(args.projectPath, browser, args.surface);
|
|
7591
|
+
try {
|
|
7592
|
+
const parsedTab = JSON.parse(asTab);
|
|
7593
|
+
if (parsedTab?.ok) {
|
|
7594
|
+
addWarning(parsedTab, `The dev browser reports itself headless, and a headless browser closes a popup window before the next call and never shows an options or sidebar window, so the ${args.surface} was rendered as a tab instead (the same document and APIs). For the real window, ${HEADED_RELAUNCH}.`);
|
|
7595
|
+
return actFrameJson(parsedTab);
|
|
7596
|
+
}
|
|
7597
|
+
} catch {}
|
|
7598
|
+
}
|
|
6599
7599
|
const cli = [
|
|
6600
7600
|
"open",
|
|
6601
7601
|
args.surface,
|
|
@@ -6605,40 +7605,201 @@ async function open_handler(args) {
|
|
|
6605
7605
|
cli.push("--browser", browser);
|
|
6606
7606
|
if (null != args.timeout) cli.push("--timeout", String(args.timeout));
|
|
6607
7607
|
const raw = await runActVerb(cli, args.projectPath, args.timeout, open_schema.name);
|
|
6608
|
-
const
|
|
6609
|
-
if (
|
|
7608
|
+
const refusal = readWindowRefusal(raw, args.surface, browser);
|
|
7609
|
+
if (refusal) {
|
|
7610
|
+
if ("sidebar" === args.surface && isChromiumFamily(browser) && "gesture" === refusal.kind && !sessionIsHeadless() && !await browserRunsHeadless(args.projectPath, browser)) return openSidebarThroughGesture(args.projectPath, browser, refusal.frame);
|
|
7611
|
+
if ("sidebar" === args.surface && isGeckoFamily(browser)) return openGeckoSidebar(args.projectPath, browser, refusal.frame, args.timeout);
|
|
7612
|
+
const fallback = await openSurfaceAsTab(args.projectPath, browser, args.surface);
|
|
7613
|
+
const parsedFallback = tryParseEnvelope(fallback);
|
|
7614
|
+
if (parsedFallback?.ok) {
|
|
7615
|
+
addWarning(parsedFallback, windowRefusalWarning(refusal, args.surface, browser));
|
|
7616
|
+
return actFrameJson(parsedFallback);
|
|
7617
|
+
}
|
|
7618
|
+
addWarning(refusal.frame, `The browser refused the ${args.surface} window and the tab route failed too: ${String(parsedFallback?.error?.message ?? "no answer")}${"string" == typeof parsedFallback?.hint ? ` ${parsedFallback.hint}` : ""}`);
|
|
7619
|
+
if (!refusal.frame.hint) refusal.frame.hint = "gesture" === refusal.kind ? "This surface can only open from a real user gesture, which automation cannot produce. Retry with asTab: true to render the surface document in a tab instead." : `The dev browser has no window for this surface. Retry with asTab: true to render the surface document in a tab, or for the real window, ${HEADED_RELAUNCH}.`;
|
|
7620
|
+
return actFrameJson(refusal.frame);
|
|
7621
|
+
}
|
|
7622
|
+
if (!AS_TAB_SURFACES.includes(args.surface)) return raw;
|
|
7623
|
+
const confirmed = await confirmSurfaceTarget(args.projectPath, browser, args.surface, raw);
|
|
7624
|
+
const parsedConfirmed = tryParseEnvelope(confirmed);
|
|
7625
|
+
if (parsedConfirmed?.status !== "surface-did-not-open") return confirmed;
|
|
7626
|
+
const fallback = await openSurfaceAsTab(args.projectPath, browser, args.surface);
|
|
7627
|
+
const parsedFallback = tryParseEnvelope(fallback);
|
|
7628
|
+
if (parsedFallback?.ok) {
|
|
7629
|
+
addWarning(parsedFallback, `The engine reported the ${args.surface} as opened, but no document for it appeared within 3s (a headless browser never shows an options or popup window), so the ${args.surface} document was rendered in a tab instead (the same document and APIs). For the real window, ${HEADED_RELAUNCH}.`);
|
|
7630
|
+
patchValue(parsedFallback, {
|
|
7631
|
+
engineResult: parsedConfirmed.value?.engineResult ?? null
|
|
7632
|
+
});
|
|
7633
|
+
return actFrameJson(parsedFallback);
|
|
7634
|
+
}
|
|
7635
|
+
return confirmed;
|
|
7636
|
+
}
|
|
7637
|
+
function tryParseEnvelope(raw) {
|
|
7638
|
+
try {
|
|
7639
|
+
return JSON.parse(raw);
|
|
7640
|
+
} catch {
|
|
7641
|
+
return null;
|
|
7642
|
+
}
|
|
7643
|
+
}
|
|
7644
|
+
function readWindowRefusal(raw, surface, browser) {
|
|
7645
|
+
if (![
|
|
6610
7646
|
"popup",
|
|
6611
7647
|
"action",
|
|
6612
|
-
"sidebar"
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
|
|
6628
|
-
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
const refusal = readGestureRefusal(raw);
|
|
6635
|
-
if (refusal) return openSidebarThroughGesture(args.projectPath, browser, refusal);
|
|
7648
|
+
"sidebar",
|
|
7649
|
+
"options"
|
|
7650
|
+
].includes(surface)) return null;
|
|
7651
|
+
const gesture = readGestureRefusal(raw);
|
|
7652
|
+
if (gesture) return {
|
|
7653
|
+
kind: "gesture",
|
|
7654
|
+
frame: gesture
|
|
7655
|
+
};
|
|
7656
|
+
const parsed = tryParseEnvelope(raw);
|
|
7657
|
+
if (!parsed || false !== parsed.ok) return null;
|
|
7658
|
+
const code = "string" == typeof parsed.error?.code ? parsed.error.code : "";
|
|
7659
|
+
const message = String(parsed.error?.message ?? "");
|
|
7660
|
+
if ("E_TARGET_NOT_FOUND" === code || /active browser window|no active|headless/i.test(message)) return {
|
|
7661
|
+
kind: "no-window",
|
|
7662
|
+
frame: parsed
|
|
7663
|
+
};
|
|
7664
|
+
if (isGeckoFamily(browser) && "options" !== surface) {
|
|
7665
|
+
const unsupported = readUnsupportedRefusal(raw);
|
|
7666
|
+
if (unsupported || /popup is disabled|not implemented/i.test(message)) return {
|
|
7667
|
+
kind: "unsupported",
|
|
7668
|
+
frame: parsed
|
|
7669
|
+
};
|
|
6636
7670
|
}
|
|
6637
|
-
|
|
6638
|
-
|
|
6639
|
-
|
|
7671
|
+
return null;
|
|
7672
|
+
}
|
|
7673
|
+
function windowRefusalWarning(refusal, surface, browser) {
|
|
7674
|
+
const noun = "sidebar" === surface ? "sidebar" : "options" === surface ? "options page" : "popup";
|
|
7675
|
+
const said = String(refusal.frame.error?.message ?? "").replace(/\s+/g, " ").trim();
|
|
7676
|
+
if (isGeckoFamily(browser)) return "gesture" === refusal.kind ? `${browser} opens the ${noun} only from its toolbar; the engine refused with Chromium's gesture wording because it counts every non-Firefox name as Chromium (extension.js ledger 642). The ${noun} document was rendered in a tab instead: the same document and APIs, without the toolbar anchoring.` : `${browser} cannot open the ${noun} programmatically (${said}), so the ${noun} document was rendered in a tab instead: the same document and APIs, without the toolbar anchoring.`;
|
|
7677
|
+
return "gesture" === refusal.kind ? `Chromium opens the ${noun} only from a real user gesture, which automation cannot produce, so the ${noun} document was rendered in a tab instead (the same document and APIs). For the real window, ${HEADED_RELAUNCH} and click the toolbar icon.` : `The dev browser has no window to show the ${noun} in (${said}); a headless browser never shows one, so the ${noun} document was rendered in a tab instead (the same document and APIs). For the real window, ${HEADED_RELAUNCH}.`;
|
|
7678
|
+
}
|
|
7679
|
+
const DEVTOOLS_PANEL_BUDGET_MS = 15000;
|
|
7680
|
+
const DEVTOOLS_PANEL_BUDGET_MAX_MS = 120000;
|
|
7681
|
+
async function openDevToolsSurface(args, browser) {
|
|
7682
|
+
if (!isChromiumFamily(browser)) return envelope_envelope({
|
|
7683
|
+
ok: false,
|
|
7684
|
+
command: open_schema.name,
|
|
7685
|
+
status: "unsupported",
|
|
7686
|
+
error: {
|
|
7687
|
+
code: "E_UNSUPPORTED_BROWSER",
|
|
7688
|
+
name: "NoDevToolsAutomation",
|
|
7689
|
+
message: `${browser} exposes no protocol command that opens its developer tools or an extension panel, so this server cannot open the devtools surface there; Target.openDevTools is Chromium's.`
|
|
7690
|
+
},
|
|
7691
|
+
hint: WEBKIT_FAMILY.has(browser) ? "Open the Web Inspector by hand in the headed Safari window and switch to the panel; extension_logs shows what the devtools page writes." : "Open the developer tools by hand in the headed Firefox window (F12) and switch to the panel, or run the same project on chrome to drive the panel from here; extension_logs (context: ['devtools']) shows what the devtools page writes either way."
|
|
7692
|
+
});
|
|
7693
|
+
const doc = surfaceDocument(args.projectPath, browser, "devtools");
|
|
7694
|
+
if (!doc) return missingSurfaceError(args.projectPath, browser, "devtools", "so there is no DevTools page to register a panel");
|
|
7695
|
+
const resolved = await resolveCdpPort(args.projectPath, browser);
|
|
7696
|
+
if (!resolved) return envelope_envelope({
|
|
7697
|
+
ok: false,
|
|
7698
|
+
command: open_schema.name,
|
|
7699
|
+
status: "no-session",
|
|
7700
|
+
error: {
|
|
7701
|
+
code: "E_NO_SESSION",
|
|
7702
|
+
name: "NoSession",
|
|
7703
|
+
message: `No active dev session / CDP port for ${browser}. Start extension_dev and extension_wait for ready. ${CDP_PORT_MISSING_HINT}`
|
|
7704
|
+
}
|
|
7705
|
+
});
|
|
7706
|
+
const extensionId = await resolveExtensionId(args.projectPath, browser);
|
|
7707
|
+
if (!extensionId) return envelope_envelope({
|
|
7708
|
+
ok: false,
|
|
7709
|
+
command: open_schema.name,
|
|
7710
|
+
status: "no-extension-id",
|
|
7711
|
+
error: {
|
|
7712
|
+
code: "E_NO_EXTENSION_ID",
|
|
7713
|
+
name: "NoExtensionId",
|
|
7714
|
+
message: "Could not resolve the extension id from the live session's CDP targets."
|
|
7715
|
+
},
|
|
7716
|
+
hint: `Confirm the session is ready (extension_wait). ${CDP_PORT_MISSING_HINT}`
|
|
7717
|
+
});
|
|
7718
|
+
const pages = await listPageTargets(resolved.port).catch(()=>[]);
|
|
7719
|
+
const candidates = args.url ? matchTargetsByUrl(pages, args.url) : pages.filter((t)=>/^https?:/i.test(t.url) || !t.url.startsWith("chrome-extension://") && !t.url.startsWith("chrome://") && "about:blank" !== t.url);
|
|
7720
|
+
const inspected = candidates[0] ?? (args.url ? void 0 : pages[0]);
|
|
7721
|
+
if (!inspected) return envelope_envelope({
|
|
7722
|
+
ok: false,
|
|
7723
|
+
command: open_schema.name,
|
|
7724
|
+
status: "no-target",
|
|
7725
|
+
error: {
|
|
7726
|
+
code: "E_NO_TARGET",
|
|
7727
|
+
name: "NoTarget",
|
|
7728
|
+
message: args.url ? `No open tab matches ${args.url}, so there is nothing to open DevTools on.` : "No open tab to inspect: the session has no page target."
|
|
7729
|
+
},
|
|
7730
|
+
hint: "Open the page first with extension_open (url: the address), then open the devtools surface with the same url. extension_dom_snapshot with listTargets: true lists what is open."
|
|
7731
|
+
});
|
|
7732
|
+
const devtoolsPageUrl = `chrome-extension://${extensionId}/${doc}`;
|
|
7733
|
+
const budgetMs = "number" == typeof args.waitMs && args.waitMs > 0 ? Math.min(args.waitMs, DEVTOOLS_PANEL_BUDGET_MAX_MS) : "number" == typeof args.timeout && args.timeout > 0 ? Math.min(args.timeout, DEVTOOLS_PANEL_BUDGET_MS) : DEVTOOLS_PANEL_BUDGET_MS;
|
|
7734
|
+
const outcome = await openDevToolsPanel(resolved.port, {
|
|
7735
|
+
inspectedTargetId: inspected.targetId,
|
|
7736
|
+
extensionId,
|
|
7737
|
+
devtoolsPageUrl,
|
|
7738
|
+
panelTitle: args.panel,
|
|
7739
|
+
budgetMs,
|
|
7740
|
+
reloadInspected: true === args.reload
|
|
7741
|
+
});
|
|
7742
|
+
if (!outcome.opened) {
|
|
7743
|
+
if ("open" === outcome.stage || "frontend" === outcome.stage) return envelope_envelope({
|
|
7744
|
+
ok: false,
|
|
7745
|
+
command: open_schema.name,
|
|
7746
|
+
status: "cdp-failed",
|
|
7747
|
+
error: {
|
|
7748
|
+
code: "E_CDP",
|
|
7749
|
+
name: "CdpError",
|
|
7750
|
+
message: `Target.openDevTools on ${inspected.url} did not open a DevTools frontend: ${outcome.reason}.`
|
|
7751
|
+
},
|
|
7752
|
+
hint: "This browser build's protocol lacks or refused Target.openDevTools (Chrome 151 and Edge 154 honor it, headed or headless). Open DevTools by hand in a headed session, or run the project on a current Chrome; extension_logs (context: ['devtools']) reads the devtools page either way."
|
|
7753
|
+
});
|
|
7754
|
+
const registered = (outcome.panels ?? []).filter((id)=>id.startsWith(`chrome-extension://${extensionId}`));
|
|
7755
|
+
return envelope_envelope({
|
|
7756
|
+
ok: false,
|
|
7757
|
+
command: open_schema.name,
|
|
7758
|
+
status: "surface-did-not-open",
|
|
7759
|
+
error: {
|
|
7760
|
+
code: "E_SURFACE_DID_NOT_OPEN",
|
|
7761
|
+
name: "PanelDidNotRegister",
|
|
7762
|
+
message: "panel" === outcome.stage ? `DevTools opened on ${inspected.url} and ${doc} loaded in it, but ${outcome.reason}.${registered.length ? ` Panels this extension did register: ${registered.map((id)=>id.slice(`chrome-extension://${extensionId}`.length)).join(", ")}.` : ""}` : `DevTools opened on ${inspected.url} and the panel registered, but ${outcome.reason}.`
|
|
7763
|
+
},
|
|
7764
|
+
value: {
|
|
7765
|
+
inspected: {
|
|
7766
|
+
targetId: inspected.targetId,
|
|
7767
|
+
url: inspected.url
|
|
7768
|
+
},
|
|
7769
|
+
devtoolsTargetId: outcome.devtoolsTargetId ?? null,
|
|
7770
|
+
panels: outcome.panels ?? []
|
|
7771
|
+
},
|
|
7772
|
+
hint: args.panel && registered.length ? `No panel is titled "${args.panel}"; pass one of the registered titles as \`panel\`, or omit it for the first.` : `The devtools page registers panels with chrome.devtools.panels.create; extension_logs (context: ['devtools']) shows what ${doc} wrote or threw. An extension that creates its panel only when the page reports to it (Preact Devtools does, through its content script) needs the page loaded with DevTools already open: retry with reload: true, and waitMs for a longer wait. DevTools stays open on the tab.`
|
|
7773
|
+
});
|
|
6640
7774
|
}
|
|
6641
|
-
|
|
7775
|
+
const panelUrl = outcome.panelTarget?.url ?? null;
|
|
7776
|
+
return envelope_envelope({
|
|
7777
|
+
ok: true,
|
|
7778
|
+
command: open_schema.name,
|
|
7779
|
+
status: "opened",
|
|
7780
|
+
value: {
|
|
7781
|
+
surface: "devtools",
|
|
7782
|
+
inspected: {
|
|
7783
|
+
targetId: inspected.targetId,
|
|
7784
|
+
url: inspected.url,
|
|
7785
|
+
title: inspected.title
|
|
7786
|
+
},
|
|
7787
|
+
devtoolsTargetId: outcome.devtoolsTargetId,
|
|
7788
|
+
panel: {
|
|
7789
|
+
title: outcome.panelTitle,
|
|
7790
|
+
id: outcome.panelId,
|
|
7791
|
+
targetId: outcome.panelTarget?.targetId ?? null,
|
|
7792
|
+
url: panelUrl
|
|
7793
|
+
},
|
|
7794
|
+
panels: outcome.panels,
|
|
7795
|
+
devtoolsPage: devtoolsPageUrl,
|
|
7796
|
+
reloadedInspected: true === args.reload
|
|
7797
|
+
},
|
|
7798
|
+
warnings: outcome.panelTarget ? [] : [
|
|
7799
|
+
"The panel is shown but its document target had not appeared within 3s; call extension_open surface: \"devtools\" again once it loads to get its url."
|
|
7800
|
+
],
|
|
7801
|
+
hint: panelUrl ? `DevTools is open on ${inspected.url} with the "${outcome.panelTitle}" panel shown. Read the panel with extension_eval context: "page", url: "${panelUrl}" (over CDP, with chrome.devtools available); context: "devtools" reads the devtools page itself (${doc}), the hidden document that registered the panel. The panel is an iframe inside DevTools, not a tab, so extension_dom_snapshot and extension_inspect do not reach it.` : `DevTools is open on ${inspected.url} with the "${outcome.panelTitle}" panel shown. context: "devtools" on extension_eval reads the devtools page itself (${doc}).`
|
|
7802
|
+
});
|
|
6642
7803
|
}
|
|
6643
7804
|
const E_USER_GESTURE_REQUIRED = "E_USER_GESTURE_REQUIRED";
|
|
6644
7805
|
function readGestureRefusal(raw) {
|
|
@@ -6759,7 +7920,14 @@ async function openSidebarThroughGesture(projectPath, browser, refusal) {
|
|
|
6759
7920
|
refusal.hint = `Chrome opens the side panel only from a user gesture, which the engine's open verb cannot carry, and the server's synthetic click did not open it either (${reason}). ` + (doc && extensionId ? `To read the panel page anyway, call extension_open with url: "chrome-extension://${extensionId}/${doc}" and then extension_dom_snapshot context: 'sidebar' on it. ` : "To read the panel page anyway, open its document by url with extension_open and read it with extension_dom_snapshot context: 'sidebar'. ") + "Opening the real panel needs a toolbar click from a person in the dev browser.";
|
|
6760
7921
|
return actFrameJson(refusal);
|
|
6761
7922
|
}
|
|
6762
|
-
function resolveToken() {
|
|
7923
|
+
function resolveToken(options = {}) {
|
|
7924
|
+
const project = String(options.project ?? "").trim();
|
|
7925
|
+
if (project) {
|
|
7926
|
+
const creds = readValidCredentials(void 0, {
|
|
7927
|
+
project
|
|
7928
|
+
});
|
|
7929
|
+
return creds?.token ? String(creds.token).trim() : "";
|
|
7930
|
+
}
|
|
6763
7931
|
const fromEnv = String(process.env.EXTENSION_DEV_TOKEN || "").trim();
|
|
6764
7932
|
if (fromEnv) return fromEnv;
|
|
6765
7933
|
const creds = readValidCredentials();
|
|
@@ -7022,6 +8190,9 @@ async function revokeArtifact(options) {
|
|
|
7022
8190
|
error: {
|
|
7023
8191
|
name: "SharesRevokeError",
|
|
7024
8192
|
status: res.status,
|
|
8193
|
+
..."string" == typeof data?.code ? {
|
|
8194
|
+
code: data.code
|
|
8195
|
+
} : {},
|
|
7025
8196
|
message: `Revoking ${options.artifactId} failed (${res.status}): ${data?.message || "unknown error"}`
|
|
7026
8197
|
}
|
|
7027
8198
|
};
|
|
@@ -7524,11 +8695,16 @@ function previewOriginOf(previewUrl) {
|
|
|
7524
8695
|
return null;
|
|
7525
8696
|
}
|
|
7526
8697
|
}
|
|
7527
|
-
async function buildShare(projectPath, distDir, manifest, browser, verifyInBrowserTerms) {
|
|
8698
|
+
async function buildShare(projectPath, distDir, manifest, browser, verifyInBrowserTerms, project) {
|
|
7528
8699
|
const result = await uploadPreview({
|
|
7529
8700
|
distDir,
|
|
7530
8701
|
manifest,
|
|
7531
|
-
browser
|
|
8702
|
+
browser,
|
|
8703
|
+
...project ? {
|
|
8704
|
+
token: resolveToken({
|
|
8705
|
+
project
|
|
8706
|
+
})
|
|
8707
|
+
} : {}
|
|
7532
8708
|
});
|
|
7533
8709
|
if (!result.ok) {
|
|
7534
8710
|
const isAuth = "PreviewAuthError" === result.error.name;
|
|
@@ -7637,6 +8813,7 @@ const preview_web_schema = {
|
|
|
7637
8813
|
type: "object",
|
|
7638
8814
|
properties: {
|
|
7639
8815
|
projectPath: PROJECT_PATH,
|
|
8816
|
+
project: PROJECT_TOKEN_INPUT,
|
|
7640
8817
|
browser: {
|
|
7641
8818
|
type: "string",
|
|
7642
8819
|
enum: REAL_BROWSERS,
|
|
@@ -7804,7 +8981,7 @@ async function preview_web_handler(args) {
|
|
|
7804
8981
|
if (true !== opened.ok) previewWarnings.push("Could not open the preview in a browser. This needs a live dev session (run extension_dev, then extension_wait for ready). The deepLink above still works if you open it yourself.");
|
|
7805
8982
|
}
|
|
7806
8983
|
if (args.share) {
|
|
7807
|
-
const share = await buildShare(args.projectPath, distDir, manifest, browser, false !== args.probe);
|
|
8984
|
+
const share = await buildShare(args.projectPath, distDir, manifest, browser, false !== args.probe, args.project);
|
|
7808
8985
|
result.share = share;
|
|
7809
8986
|
if (true === share.ok && false === share.browserLoadable) {
|
|
7810
8987
|
const check = share.browserCheck;
|
|
@@ -7988,9 +9165,29 @@ const APPROVAL_GATE_ENV = "EXTENSION_DEV_APPROVAL_GATE";
|
|
|
7988
9165
|
const APPROVAL_REQUIRED_STATUS = "approval-required";
|
|
7989
9166
|
const APPROVAL_PENDING_STATUS = "approval-pending";
|
|
7990
9167
|
const APPROVAL_REJECTED_STATUS = "approval-rejected";
|
|
7991
|
-
function approvalGateEnabled() {
|
|
9168
|
+
function approvalGateEnabled(defaultOn = false) {
|
|
7992
9169
|
const raw = String(process.env[APPROVAL_GATE_ENV] || "").trim().toLowerCase();
|
|
7993
|
-
|
|
9170
|
+
if ("" === raw) return defaultOn;
|
|
9171
|
+
return "0" !== raw && "false" !== raw && "off" !== raw;
|
|
9172
|
+
}
|
|
9173
|
+
function platformRequiresApproval(res, data) {
|
|
9174
|
+
return 403 === res.status && !!data && "object" == typeof data && "APPROVAL_REQUIRED" === data.code;
|
|
9175
|
+
}
|
|
9176
|
+
async function requestApprovalAfterRefusal(input) {
|
|
9177
|
+
const gate = await evaluateApproval({
|
|
9178
|
+
...input,
|
|
9179
|
+
approvalId: void 0,
|
|
9180
|
+
enabled: true
|
|
9181
|
+
});
|
|
9182
|
+
return gate.blocked ? gate.envelope : envelope_envelope({
|
|
9183
|
+
ok: false,
|
|
9184
|
+
command: input.command,
|
|
9185
|
+
status: APPROVAL_REQUIRED_STATUS,
|
|
9186
|
+
error: {
|
|
9187
|
+
code: "E_APPROVAL_REQUIRED",
|
|
9188
|
+
message: "The platform requires an approval for this action."
|
|
9189
|
+
}
|
|
9190
|
+
});
|
|
7994
9191
|
}
|
|
7995
9192
|
function canonicalScope(scope) {
|
|
7996
9193
|
return Object.keys(scope).sort().map((key)=>{
|
|
@@ -8201,6 +9398,7 @@ const shares_schema = {
|
|
|
8201
9398
|
inputSchema: {
|
|
8202
9399
|
type: "object",
|
|
8203
9400
|
properties: {
|
|
9401
|
+
project: PROJECT_TOKEN_INPUT,
|
|
8204
9402
|
action: {
|
|
8205
9403
|
type: "string",
|
|
8206
9404
|
enum: [
|
|
@@ -8334,6 +9532,11 @@ async function listShares(args) {
|
|
|
8334
9532
|
} : {},
|
|
8335
9533
|
...args.api ? {
|
|
8336
9534
|
api: args.api
|
|
9535
|
+
} : {},
|
|
9536
|
+
...args.project ? {
|
|
9537
|
+
token: resolveToken({
|
|
9538
|
+
project: args.project
|
|
9539
|
+
})
|
|
8337
9540
|
} : {}
|
|
8338
9541
|
});
|
|
8339
9542
|
if (!listing.ok) {
|
|
@@ -8469,19 +9672,22 @@ async function revokeShare(args) {
|
|
|
8469
9672
|
message: supplied ? `The reference is malformed and nothing was sent to the platform: no share id could be read out of ${JSON.stringify(supplied.length > 200 ? `${supplied.slice(0, 200)}...` : supplied)}. A share id is gen_ followed by exactly 64 lowercase hex characters (32 on shares minted before 2026-07-30), and a url revokes only when it contains one whole. Check the ref for a copy-paste cut, or run action:"list" and copy the exact artifactId.` : 'Nothing to revoke. Pass artifactId (a gen_... id) or url (the previewUrl, zipUrl, viewUrl, or revokeUrl of the share). Run action:"list" to see both.'
|
|
8470
9673
|
}
|
|
8471
9674
|
});
|
|
8472
|
-
const token = resolveToken(
|
|
9675
|
+
const token = resolveToken({
|
|
9676
|
+
project: args.project
|
|
9677
|
+
});
|
|
9678
|
+
const gateInput = {
|
|
9679
|
+
command: "extension_shares",
|
|
9680
|
+
action: "extension_shares.revoke",
|
|
9681
|
+
scope: {
|
|
9682
|
+
artifactId: ref
|
|
9683
|
+
},
|
|
9684
|
+
description: `Permanently revoke share ${ref}, killing the link for everyone with no way to restore it.`,
|
|
9685
|
+
approvalId: args.approvalId,
|
|
9686
|
+
token: token || "",
|
|
9687
|
+
api: args.api
|
|
9688
|
+
};
|
|
8473
9689
|
if (token) {
|
|
8474
|
-
const gate = await evaluateApproval(
|
|
8475
|
-
command: "extension_shares",
|
|
8476
|
-
action: "extension_shares.revoke",
|
|
8477
|
-
scope: {
|
|
8478
|
-
artifactId: ref
|
|
8479
|
-
},
|
|
8480
|
-
description: `Permanently revoke share ${ref}, killing the link for everyone with no way to restore it.`,
|
|
8481
|
-
approvalId: args.approvalId,
|
|
8482
|
-
token,
|
|
8483
|
-
api: args.api
|
|
8484
|
-
});
|
|
9690
|
+
const gate = await evaluateApproval(gateInput);
|
|
8485
9691
|
if (gate.blocked) return gate.envelope;
|
|
8486
9692
|
}
|
|
8487
9693
|
const result = await revokeArtifact({
|
|
@@ -8507,6 +9713,7 @@ async function revokeShare(args) {
|
|
|
8507
9713
|
artifactId: ref
|
|
8508
9714
|
}
|
|
8509
9715
|
});
|
|
9716
|
+
if (token && !args.approvalId && 403 === result.error.status && "APPROVAL_REQUIRED" === result.error.code) return requestApprovalAfterRefusal(gateInput);
|
|
8510
9717
|
const isAuth = "SharesAuthError" === result.error.name;
|
|
8511
9718
|
return envelope_envelope({
|
|
8512
9719
|
ok: false,
|
|
@@ -8580,6 +9787,9 @@ async function shares_handler(args) {
|
|
|
8580
9787
|
} : {},
|
|
8581
9788
|
...args.api ? {
|
|
8582
9789
|
api: args.api
|
|
9790
|
+
} : {},
|
|
9791
|
+
...args.project ? {
|
|
9792
|
+
project: args.project
|
|
8583
9793
|
} : {}
|
|
8584
9794
|
});
|
|
8585
9795
|
return listShares({
|
|
@@ -8594,6 +9804,9 @@ async function shares_handler(args) {
|
|
|
8594
9804
|
} : {},
|
|
8595
9805
|
...args.api ? {
|
|
8596
9806
|
api: args.api
|
|
9807
|
+
} : {},
|
|
9808
|
+
...args.project ? {
|
|
9809
|
+
project: args.project
|
|
8597
9810
|
} : {}
|
|
8598
9811
|
});
|
|
8599
9812
|
}
|
|
@@ -8918,7 +10131,7 @@ function makeFilter(args) {
|
|
|
8918
10131
|
}
|
|
8919
10132
|
function readContractForDiagnosis(projectPath, browser) {
|
|
8920
10133
|
try {
|
|
8921
|
-
const raw = node_fs.readFileSync(
|
|
10134
|
+
const raw = node_fs.readFileSync(session_paths_readyContractPath(projectPath, browser), "utf8");
|
|
8922
10135
|
return JSON.parse(raw);
|
|
8923
10136
|
} catch {
|
|
8924
10137
|
return null;
|
|
@@ -8927,13 +10140,13 @@ function readContractForDiagnosis(projectPath, browser) {
|
|
|
8927
10140
|
function sightedContractBrowser(projectPath) {
|
|
8928
10141
|
let dirs;
|
|
8929
10142
|
try {
|
|
8930
|
-
dirs = node_fs.readdirSync(
|
|
10143
|
+
dirs = node_fs.readdirSync(session_paths_sessionArtifactsRootDir(projectPath));
|
|
8931
10144
|
} catch {
|
|
8932
10145
|
return null;
|
|
8933
10146
|
}
|
|
8934
10147
|
let best = null;
|
|
8935
10148
|
for (const dir of dirs)try {
|
|
8936
|
-
const stat = node_fs.statSync(
|
|
10149
|
+
const stat = node_fs.statSync(session_paths_readyContractPath(projectPath, dir));
|
|
8937
10150
|
if (!best || stat.mtimeMs > best.mtimeMs) best = {
|
|
8938
10151
|
browser: dir,
|
|
8939
10152
|
mtimeMs: stat.mtimeMs
|
|
@@ -9307,9 +10520,9 @@ function capRecent(events, limit) {
|
|
|
9307
10520
|
function emptyReason(projectPath, browser) {
|
|
9308
10521
|
let contract;
|
|
9309
10522
|
try {
|
|
9310
|
-
contract = JSON.parse(node_fs.readFileSync(
|
|
10523
|
+
contract = JSON.parse(node_fs.readFileSync(session_paths_readyContractPath(projectPath, browser), "utf8"));
|
|
9311
10524
|
} catch {
|
|
9312
|
-
return `No ready.json for this project/browser: no dev session has produced a build here, so there is nothing to log. Start one with extension_dev. ${sessionPathHint(
|
|
10525
|
+
return `No ready.json for this project/browser: no dev session has produced a build here, so there is nothing to log. Start one with extension_dev. ${sessionPathHint(session_paths_readyContractPath(projectPath, browser))}`;
|
|
9313
10526
|
}
|
|
9314
10527
|
if ("error" === contract.status) {
|
|
9315
10528
|
const errs = contract.errors;
|
|
@@ -9352,7 +10565,7 @@ function summarize(events, source, browser, runId, limit, dropped, projectPath,
|
|
|
9352
10565
|
function staleFileNote(projectPath, browser, eventsRunId) {
|
|
9353
10566
|
let contract;
|
|
9354
10567
|
try {
|
|
9355
|
-
contract = JSON.parse(node_fs.readFileSync(
|
|
10568
|
+
contract = JSON.parse(node_fs.readFileSync(session_paths_readyContractPath(projectPath, browser), "utf8"));
|
|
9356
10569
|
} catch {
|
|
9357
10570
|
return "These events survive from a previous session: no ready.json exists for this project/browser now, so nothing current is producing logs.";
|
|
9358
10571
|
}
|
|
@@ -9370,7 +10583,7 @@ function staleFileNote(projectPath, browser, eventsRunId) {
|
|
|
9370
10583
|
function readLogRunId(projectPath, browser) {
|
|
9371
10584
|
let text;
|
|
9372
10585
|
try {
|
|
9373
|
-
text = node_fs.readFileSync(
|
|
10586
|
+
text = node_fs.readFileSync(session_paths_logsPath(projectPath, browser), "utf8");
|
|
9374
10587
|
} catch {
|
|
9375
10588
|
return "";
|
|
9376
10589
|
}
|
|
@@ -9388,7 +10601,7 @@ function readLogRunId(projectPath, browser) {
|
|
|
9388
10601
|
return runId;
|
|
9389
10602
|
}
|
|
9390
10603
|
async function readFromFile(args, browser, limit) {
|
|
9391
|
-
const file =
|
|
10604
|
+
const file = session_paths_logsPath(args.projectPath, browser);
|
|
9392
10605
|
if (!node_fs.existsSync(file)) return envelope_envelope({
|
|
9393
10606
|
ok: false,
|
|
9394
10607
|
command: TOOL,
|
|
@@ -9454,7 +10667,7 @@ function controlRefusal(closeCode, reason, url, sentVersion) {
|
|
|
9454
10667
|
};
|
|
9455
10668
|
}
|
|
9456
10669
|
async function readFromStream(args, browser, limit) {
|
|
9457
|
-
const ready = readReadyContract(args.projectPath, browser);
|
|
10670
|
+
const ready = readReadyContract(engineProjectRoot(args.projectPath), browser);
|
|
9458
10671
|
if (!ready) {
|
|
9459
10672
|
const running = knownSessionBrowsers(args.projectPath).filter((b)=>b !== browser);
|
|
9460
10673
|
const retarget = running.length ? `An active session exists for browser(s): ${running.join(", ")}, pass that as \`browser\`. Otherwise run` : "Run";
|
|
@@ -9575,13 +10788,13 @@ async function readFromStream(args, browser, limit) {
|
|
|
9575
10788
|
async function logs_handler(args) {
|
|
9576
10789
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
9577
10790
|
const limit = args.limit && args.limit > 0 ? args.limit : DEFAULT_LIMIT;
|
|
9578
|
-
if (WEBKIT_FAMILY.has(browser) && !node_fs.existsSync(
|
|
10791
|
+
if (WEBKIT_FAMILY.has(browser) && !node_fs.existsSync(session_paths_logsPath(args.projectPath, browser))) return envelope_envelope({
|
|
9579
10792
|
ok: false,
|
|
9580
10793
|
command: TOOL,
|
|
9581
10794
|
status: "no-log-file",
|
|
9582
10795
|
error: {
|
|
9583
10796
|
code: "E_LOGS_MISSING",
|
|
9584
|
-
message: `No logs found at ${
|
|
10797
|
+
message: `No logs found at ${session_paths_logsPath(args.projectPath, browser)}, and ${browser} has no CDP or RDP to read instead.`
|
|
9585
10798
|
},
|
|
9586
10799
|
hint: "Safari logs arrive through the extension's bridge to the dev server, with background and content contexts, once the extension is enabled and a dev session from an Extension.js that streams Safari logs (4.1.28 or newer) is running. Start extension_dev --browser=safari, enable the extension in Safari > Settings > Extensions, then read again. The page a content script changes is also readable with extension_eval (context: 'page') or extension_assert content-script-injected."
|
|
9587
10800
|
});
|
|
@@ -9856,7 +11069,7 @@ function truncate(value, max = 200) {
|
|
|
9856
11069
|
}
|
|
9857
11070
|
return text.length > max ? `${text.slice(0, max)}…` : text;
|
|
9858
11071
|
}
|
|
9859
|
-
const
|
|
11072
|
+
const assert_WORKER_TARGET_TYPES = new Set([
|
|
9860
11073
|
"service_worker",
|
|
9861
11074
|
"worker",
|
|
9862
11075
|
"background_page",
|
|
@@ -9870,7 +11083,14 @@ async function assertBackgroundWorker(clause, stage) {
|
|
|
9870
11083
|
if ("none" === background.kind) return failCheck(id, null, `${read.file} declares no background, so no worker can boot. Nothing about the session is implicated.`, {
|
|
9871
11084
|
manifestFile: read.file
|
|
9872
11085
|
});
|
|
9873
|
-
if (!stage.chromium)
|
|
11086
|
+
if (!stage.chromium) {
|
|
11087
|
+
const listed = await listBridgeTabs(stage.projectPath, stage.browser, stage.timeout, "extension_assert");
|
|
11088
|
+
if ("tabs" in listed) return passCheck(id, null, `The ${background.kind} answered a tabs query over the control channel; the bridge executor runs inside the background, so it has booted.`, {
|
|
11089
|
+
backgroundKind: background.kind,
|
|
11090
|
+
tabsSeen: listed.tabs.length
|
|
11091
|
+
});
|
|
11092
|
+
return inconclusiveCheck(id, null, `The control channel did not answer on ${stage.browser}, so the background could not be asked.`, "Start the session with allowControl: true (extension_dev) and extension_wait for ready, then assert again; extension_logs (context: ['background']) shows what the background wrote meanwhile.");
|
|
11093
|
+
}
|
|
9874
11094
|
const targets = await stage.targets();
|
|
9875
11095
|
if (null === targets) return stage.noSession(id, null);
|
|
9876
11096
|
const guest = await verifyGuestLoaded(stage.projectPath, stage.browser);
|
|
@@ -9878,7 +11098,7 @@ async function assertBackgroundWorker(clause, stage) {
|
|
|
9878
11098
|
if (!guest.loaded) return failCheck(id, null, `The extension is not loaded in the running browser, so its ${background.kind} never started. ${guest.reason}`, {
|
|
9879
11099
|
guestIds: guest.guestIds
|
|
9880
11100
|
});
|
|
9881
|
-
const workers = targets.filter((target)=>
|
|
11101
|
+
const workers = targets.filter((target)=>assert_WORKER_TARGET_TYPES.has(target.type) && guest.guestIds.some((guestId)=>String(target.url ?? "").startsWith(`chrome-extension://${guestId}/`)));
|
|
9882
11102
|
if (workers.length > 0) return passCheck(id, null, `The browser lists ${workers.length} live ${background.kind} target for this extension (${workers.map((worker)=>worker.url).join(", ")}).`, {
|
|
9883
11103
|
targets: workers.map((worker)=>({
|
|
9884
11104
|
type: worker.type,
|
|
@@ -9915,7 +11135,47 @@ async function assertSurfaceRendered(clause, stage) {
|
|
|
9915
11135
|
if (!document) return failCheck(id, subject, `This extension declares no ${clause.surface}: nothing sets ${SURFACE_MANIFEST_KEYS[clause.surface] ?? clause.surface} in its manifest, so there is no document to render. Surfaces it does declare: ${declared.length ? declared.join(", ") : "none"}.`, {
|
|
9916
11136
|
declaredSurfaces: declared
|
|
9917
11137
|
});
|
|
9918
|
-
if (!stage.chromium)
|
|
11138
|
+
if (!stage.chromium) {
|
|
11139
|
+
const raw = await runActVerb([
|
|
11140
|
+
"inspect",
|
|
11141
|
+
stage.projectPath,
|
|
11142
|
+
"--context",
|
|
11143
|
+
clause.surface,
|
|
11144
|
+
"--include",
|
|
11145
|
+
"summary",
|
|
11146
|
+
"--browser",
|
|
11147
|
+
stage.browser,
|
|
11148
|
+
...null != stage.timeout ? [
|
|
11149
|
+
"--timeout",
|
|
11150
|
+
String(stage.timeout)
|
|
11151
|
+
] : []
|
|
11152
|
+
], stage.projectPath, stage.timeout, "extension_assert");
|
|
11153
|
+
let parsed = null;
|
|
11154
|
+
try {
|
|
11155
|
+
parsed = JSON.parse(raw);
|
|
11156
|
+
} catch {}
|
|
11157
|
+
if (parsed?.ok === true) {
|
|
11158
|
+
const summary = parsed.value?.summary ?? {};
|
|
11159
|
+
const children = Number(summary.bodyChildCount ?? 0);
|
|
11160
|
+
if (clause.selector) return inconclusiveCheck(id, subject, `The ${clause.surface} document is open (${parsed.value?.url ?? document}), but a selector cannot be probed through the surface relay on ${stage.browser}.`, `Read it with extension_inspect (url: the document's moz-extension:// address, probe: ['${clause.selector}']) or extension_eval context: '${clause.surface}'.`, {
|
|
11161
|
+
url: parsed.value?.url,
|
|
11162
|
+
summary
|
|
11163
|
+
});
|
|
11164
|
+
if (null != clause.minNodes && children < clause.minNodes) return failCheck(id, subject, `The ${clause.surface} document is open but its body has ${children} child element${1 === children ? "" : "s"}, fewer than the ${clause.minNodes} expected.`, {
|
|
11165
|
+
url: parsed.value?.url,
|
|
11166
|
+
summary
|
|
11167
|
+
});
|
|
11168
|
+
return passCheck(id, subject, `The ${clause.surface} document (${document}) is open and rendering: the surface relay answered with ${children} body child element${1 === children ? "" : "s"}.`, {
|
|
11169
|
+
url: parsed.value?.url,
|
|
11170
|
+
summary
|
|
11171
|
+
});
|
|
11172
|
+
}
|
|
11173
|
+
const message = String(parsed?.error?.message ?? raw);
|
|
11174
|
+
if (/not open|E_TARGET_NOT_FOUND|not found/i.test(message) || parsed?.error?.code === "E_TARGET_NOT_FOUND") return failCheck(id, subject, `The ${clause.surface} is declared (${document}) but nothing is rendering it: the surface relay has no open document to answer from. Open it with extension_open (surface: '${clause.surface}') before asserting.`, {
|
|
11175
|
+
document
|
|
11176
|
+
});
|
|
11177
|
+
return inconclusiveCheck(id, subject, `The surface relay could not be asked on ${stage.browser}: ${message.slice(0, 200)}`, "Start the session with allowControl: true (extension_dev) and extension_wait for ready, then assert again.");
|
|
11178
|
+
}
|
|
9919
11179
|
const targets = await stage.targets();
|
|
9920
11180
|
if (null === targets) return stage.noSession(id, subject);
|
|
9921
11181
|
const extensionId = await stage.extensionId();
|
|
@@ -10124,7 +11384,7 @@ async function assertStorageKeyPresent(clause, stage) {
|
|
|
10124
11384
|
function assertConsoleErrorsEmpty(clause, stage) {
|
|
10125
11385
|
const id = CONSOLE;
|
|
10126
11386
|
const subject = clause.subject;
|
|
10127
|
-
const file =
|
|
11387
|
+
const file = session_paths_logsPath(stage.projectPath, stage.browser);
|
|
10128
11388
|
const all = readLogEvents(stage.projectPath, stage.browser, {});
|
|
10129
11389
|
if (0 === all.length) {
|
|
10130
11390
|
const reason = emptyReason(stage.projectPath, stage.browser);
|
|
@@ -10224,6 +11484,17 @@ async function assert_handler(args) {
|
|
|
10224
11484
|
});
|
|
10225
11485
|
}
|
|
10226
11486
|
const manifest_validate_COMMAND = "extension_manifest_validate";
|
|
11487
|
+
const CHROMIUM_ONLY_KEYS = [
|
|
11488
|
+
"version_name",
|
|
11489
|
+
"minimum_chrome_version",
|
|
11490
|
+
"key",
|
|
11491
|
+
"update_url",
|
|
11492
|
+
"offline_enabled",
|
|
11493
|
+
"side_panel",
|
|
11494
|
+
"declarative_net_request",
|
|
11495
|
+
"cross_origin_embedder_policy",
|
|
11496
|
+
"cross_origin_opener_policy"
|
|
11497
|
+
];
|
|
10227
11498
|
const CHROME_DESKTOP_ONLY_KEYS = [
|
|
10228
11499
|
"file_browser_handlers",
|
|
10229
11500
|
"file_system_provider_capabilities",
|
|
@@ -10396,6 +11667,19 @@ function collectPathRefs(m) {
|
|
|
10396
11667
|
}
|
|
10397
11668
|
return refs;
|
|
10398
11669
|
}
|
|
11670
|
+
function collectWebAccessibleRefs(m) {
|
|
11671
|
+
const refs = [];
|
|
11672
|
+
const war = m.web_accessible_resources;
|
|
11673
|
+
if (!Array.isArray(war)) return refs;
|
|
11674
|
+
for (const entry of war)if ("string" == typeof entry) refs.push(entry);
|
|
11675
|
+
else if (entry && "object" == typeof entry) {
|
|
11676
|
+
const resources = entry.resources;
|
|
11677
|
+
if (Array.isArray(resources)) {
|
|
11678
|
+
for (const r of resources)if ("string" == typeof r) refs.push(r);
|
|
11679
|
+
}
|
|
11680
|
+
}
|
|
11681
|
+
return refs;
|
|
11682
|
+
}
|
|
10399
11683
|
function fileResolvesSomewhere(ref, roots) {
|
|
10400
11684
|
if (!ref || ref.includes("*") || /^(https?:|data:)/i.test(ref)) return true;
|
|
10401
11685
|
const clean = ref.replace(/^\.?\//, "");
|
|
@@ -10567,14 +11851,18 @@ async function manifest_validate_handler(args) {
|
|
|
10567
11851
|
if (!manifest.name) result.errors.push("Missing required field: name");
|
|
10568
11852
|
if (!manifest.version) result.warnings.push("Missing field: version (required for store submission)");
|
|
10569
11853
|
const chromiumManifest = filterKeysForThisBrowser(manifest, "chrome");
|
|
11854
|
+
const projectRoot = "src" === node_path.basename(manifestDir) ? node_path.dirname(manifestDir) : manifestDir;
|
|
10570
11855
|
const roots = [
|
|
10571
11856
|
manifestDir,
|
|
10572
11857
|
node_path.join(manifestDir, "src"),
|
|
10573
|
-
...
|
|
10574
|
-
|
|
10575
|
-
] : []
|
|
11858
|
+
...projectRoot !== manifestDir ? [
|
|
11859
|
+
projectRoot
|
|
11860
|
+
] : [],
|
|
11861
|
+
node_path.join(manifestDir, "public"),
|
|
11862
|
+
node_path.join(projectRoot, "public")
|
|
10576
11863
|
];
|
|
10577
11864
|
for (const ref of new Set(collectPathRefs(chromiumManifest)))if (!fileResolvesSomewhere(ref, roots)) result.errors.push(`Referenced file "${ref}" was not found near the manifest. extension_build fails on this dangling reference.`);
|
|
11865
|
+
for (const ref of new Set(collectWebAccessibleRefs(chromiumManifest)))if (!fileResolvesSomewhere(ref, roots)) result.warnings.push(`web_accessible_resources names "${ref}", which was not found near the manifest. The build ships without it and the browser answers 404 when the extension or a page asks for it.`);
|
|
10578
11866
|
const defaultLocale = manifest.default_locale;
|
|
10579
11867
|
if ("string" == typeof defaultLocale && defaultLocale) {
|
|
10580
11868
|
const hasCatalog = roots.some((root)=>node_fs.existsSync(node_path.resolve(root, "_locales", defaultLocale, "messages.json")));
|
|
@@ -10615,6 +11903,17 @@ async function manifest_validate_handler(args) {
|
|
|
10615
11903
|
const isFirefox = isGeckoFamily(browser);
|
|
10616
11904
|
const effective = effectiveByBrowser.get(browser) ?? filterKeysForThisBrowser(manifest, browser);
|
|
10617
11905
|
const issues = [];
|
|
11906
|
+
if (isFirefox && 2 === effective.manifest_version) {
|
|
11907
|
+
const porting = [];
|
|
11908
|
+
const bg2 = effective.background;
|
|
11909
|
+
if (bg2 && Array.isArray(bg2.scripts)) porting.push("background.scripts to a single chromium:service_worker");
|
|
11910
|
+
if (effective.browser_action) porting.push("browser_action to chromium:action");
|
|
11911
|
+
if (Array.isArray(effective.web_accessible_resources) && effective.web_accessible_resources.some((e)=>"string" == typeof e)) porting.push("web_accessible_resources strings to [{resources, matches}] objects");
|
|
11912
|
+
const perms2 = effective.permissions ?? [];
|
|
11913
|
+
if (perms2.some((p)=>"string" == typeof p && (p.includes("://") || "<all_urls>" === p))) porting.push("host patterns out of permissions into host_permissions");
|
|
11914
|
+
if (perms2.includes("webRequestBlocking")) porting.push("webRequestBlocking to declarativeNetRequest rules");
|
|
11915
|
+
if (porting.length) result.warnings.push(`${browser} stays on Manifest V2 here, and Firefox still runs it. To also target Chromium, which only loads MV3, the keys to port are: ${porting.join("; ")}. Keep both by prefixing the Chromium variants with chromium:.`);
|
|
11916
|
+
}
|
|
10618
11917
|
if (isChromium) {
|
|
10619
11918
|
const mv = effective.manifest_version;
|
|
10620
11919
|
if (mv && mv < 3) issues.push("Manifest V2 is deprecated on Chromium. Use chromium:manifest_version: 3.");
|
|
@@ -10633,7 +11932,15 @@ async function manifest_validate_handler(args) {
|
|
|
10633
11932
|
const note = 'content_scripts.world: "MAIN" needs Firefox 128 or later (earlier Firefox runs the script in the isolated world). If you depend on it, set browser_specific_settings.gecko.strict_min_version to "128.0".';
|
|
10634
11933
|
if (!result.warnings.includes(note)) result.warnings.push(note);
|
|
10635
11934
|
}
|
|
10636
|
-
if (chromiumManifest.side_panel && !effective.sidebar_action)
|
|
11935
|
+
if (chromiumManifest.side_panel && !effective.sidebar_action) result.warnings.push("Chromium side_panel declared but no firefox:sidebar_action: the Firefox build ships without a sidebar. If you want one there, Firefox uses sidebar_action.");
|
|
11936
|
+
const bss = effective.browser_specific_settings;
|
|
11937
|
+
const geckoId = bss?.gecko?.id;
|
|
11938
|
+
if ("string" != typeof geckoId || !geckoId) result.warnings.push('Firefox: no browser_specific_settings.gecko.id. A temporary add-on without one gets a new internal id on every launch, so storage and the moz-extension:// origin do not survive a relaunch, and a store upload needs the id. Set firefox:browser_specific_settings.gecko.id (any "name@domain" string).');
|
|
11939
|
+
const dataCollection = bss?.gecko?.data_collection_permissions;
|
|
11940
|
+
if (!dataCollection || "object" != typeof dataCollection) result.warnings.push('Firefox: no browser_specific_settings.gecko.data_collection_permissions. AMO requires it for new add-ons and the Firefox build warns on every run; the minimal form is firefox:browser_specific_settings.gecko.data_collection_permissions: {"required": ["none"]}.');
|
|
11941
|
+
for (const key of CHROMIUM_ONLY_KEYS)if (void 0 !== effective[key]) result.warnings.push(`Firefox: manifest key "${key}" is Chromium-only and is ignored or refused by Firefox. Move it under "chromium:${key}".`);
|
|
11942
|
+
const warEntries = effective.web_accessible_resources;
|
|
11943
|
+
if (Array.isArray(warEntries) && warEntries.some((entry)=>entry && "object" == typeof entry && void 0 !== entry.extension_ids)) result.warnings.push('Firefox: web_accessible_resources[].extension_ids is Chromium-only; Firefox accepts only "matches" there.');
|
|
10637
11944
|
const bg = effective.background;
|
|
10638
11945
|
if (bg) {
|
|
10639
11946
|
if (bg.service_worker && !bg.scripts) issues.push('Background service_worker declared but no firefox:scripts. Firefox uses "scripts" (array) instead of "service_worker".');
|
|
@@ -10648,7 +11955,7 @@ async function manifest_validate_handler(args) {
|
|
|
10648
11955
|
if (effectivePerms.has(perm)) continue;
|
|
10649
11956
|
if (!declaredPermSet.has(perm)) continue;
|
|
10650
11957
|
const ns = isFirefox ? "browser" : "chrome";
|
|
10651
|
-
issues.push(`Code calls ${ns}.${api} but the ${browser} build's permissions do not include "${perm}" (it is declared only under another target's prefixed key, e.g. chromium:permissions).
|
|
11958
|
+
issues.push(`Code calls ${ns}.${api} but the ${browser} build's permissions do not include "${perm}" (it is declared only under another target's prefixed key, e.g. chromium:permissions). An unguarded call crashes this target at runtime. If every call sits behind a feature check (typeof ${ns}.${api} !== "undefined"), the build is sound: pass skipValidation: true to extension_build, or grant "${perm}" to this target too.`);
|
|
10652
11959
|
}
|
|
10653
11960
|
result.browserSupport[browser] = {
|
|
10654
11961
|
supported: 0 === issues.length,
|
|
@@ -10658,19 +11965,28 @@ async function manifest_validate_handler(args) {
|
|
|
10658
11965
|
}
|
|
10659
11966
|
const surfaces = [];
|
|
10660
11967
|
if (chromiumManifest.content_scripts) surfaces.push("content");
|
|
10661
|
-
if (chromiumManifest.side_panel) surfaces.push("sidebar");
|
|
11968
|
+
if (chromiumManifest.side_panel || manifest["firefox:sidebar_action"]) surfaces.push("sidebar");
|
|
10662
11969
|
if (chromiumManifest.action || manifest["firefox:browser_action"]) surfaces.push("action");
|
|
10663
11970
|
if (chromiumManifest.chrome_url_overrides?.newtab) surfaces.push("newtab");
|
|
11971
|
+
if (chromiumManifest.devtools_page) surfaces.push("devtools");
|
|
11972
|
+
if (chromiumManifest.options_ui || chromiumManifest.options_page) surfaces.push("options");
|
|
10664
11973
|
if (chromiumManifest.background) surfaces.push("background");
|
|
10665
11974
|
const distinctive = surfaces.filter((s)=>"background" !== s);
|
|
10666
11975
|
const matchOn = distinctive.length ? distinctive : surfaces;
|
|
10667
11976
|
if (matchOn.length) try {
|
|
10668
11977
|
const templates = await listTemplates();
|
|
10669
|
-
result.similarTemplates = templates.map((t)=>
|
|
11978
|
+
result.similarTemplates = templates.map((t)=>{
|
|
11979
|
+
const shared = t.surfaces.filter((s)=>matchOn.includes(s)).length;
|
|
11980
|
+
const union = new Set([
|
|
11981
|
+
...t.surfaces,
|
|
11982
|
+
...matchOn
|
|
11983
|
+
]).size;
|
|
11984
|
+
return {
|
|
10670
11985
|
slug: t.slug,
|
|
10671
11986
|
surfaces: t.surfaces,
|
|
10672
|
-
score:
|
|
10673
|
-
}
|
|
11987
|
+
score: union ? shared / union : 0
|
|
11988
|
+
};
|
|
11989
|
+
}).filter((t)=>t.score > 0).sort((a, b)=>b.score - a.score).slice(0, 5).map((t)=>({
|
|
10674
11990
|
slug: t.slug,
|
|
10675
11991
|
surfaces: t.surfaces
|
|
10676
11992
|
}));
|
|
@@ -12091,6 +13407,7 @@ const EXTENSION_PAGE_CONTEXTS = [
|
|
|
12091
13407
|
"popup",
|
|
12092
13408
|
"options",
|
|
12093
13409
|
"sidebar",
|
|
13410
|
+
"devtools",
|
|
12094
13411
|
"newtab",
|
|
12095
13412
|
"history",
|
|
12096
13413
|
"bookmarks"
|
|
@@ -12138,14 +13455,7 @@ function buildBridgeInspectExpression(opts) {
|
|
|
12138
13455
|
} catch (e) {}`);
|
|
12139
13456
|
if (opts.domSnapshot) parts.push(`try { out.domSnapshot = ${domSnapshotScript(500)}; } catch (e) {}`);
|
|
12140
13457
|
if (opts.extensionRoots) parts.push(`try { out.extensionRoots = ${EXTENSION_ROOT_META_SCRIPT}; } catch (e) {}`);
|
|
12141
|
-
if (opts.probes.length) parts.push(`out.probes = {};
|
|
12142
|
-
for (const sel of ${JSON.stringify(opts.probes)}) {
|
|
12143
|
-
try {
|
|
12144
|
-
const nodes = document.querySelectorAll(sel);
|
|
12145
|
-
const first = nodes[0];
|
|
12146
|
-
out.probes[sel] = { count: nodes.length, sample: first ? String(first.outerHTML || "").slice(0, 200) : null };
|
|
12147
|
-
} catch (e) { out.probes[sel] = { error: String((e && e.message) || e) }; }
|
|
12148
|
-
}`);
|
|
13458
|
+
if (opts.probes.length) parts.push(`out.probes = ${probeSelectorsScript(opts.probes)};`);
|
|
12149
13459
|
parts.push("return out;");
|
|
12150
13460
|
return `(() => { ${parts.join("\n")} })()`;
|
|
12151
13461
|
}
|
|
@@ -12573,7 +13883,7 @@ function list_extensions_unpackedExtensionId(distPath) {
|
|
|
12573
13883
|
function readOwnIdentity(projectPath, browser) {
|
|
12574
13884
|
let contract;
|
|
12575
13885
|
try {
|
|
12576
|
-
contract = JSON.parse(node_fs.readFileSync(
|
|
13886
|
+
contract = JSON.parse(node_fs.readFileSync(session_paths_readyContractPath(projectPath, browser), "utf8"));
|
|
12577
13887
|
} catch {
|
|
12578
13888
|
return null;
|
|
12579
13889
|
}
|
|
@@ -12928,28 +14238,96 @@ function chromiumManifestVersion(projectPath, browser) {
|
|
|
12928
14238
|
}
|
|
12929
14239
|
return null;
|
|
12930
14240
|
}
|
|
14241
|
+
function geckoManifestVersion(projectPath, browser) {
|
|
14242
|
+
for (const file of manifestCandidates(projectPath, browser)){
|
|
14243
|
+
let manifest;
|
|
14244
|
+
try {
|
|
14245
|
+
manifest = JSON.parse(node_fs.readFileSync(file, "utf8"));
|
|
14246
|
+
} catch {
|
|
14247
|
+
continue;
|
|
14248
|
+
}
|
|
14249
|
+
const version = manifest["firefox:manifest_version"] ?? manifest["gecko:manifest_version"] ?? manifest.manifest_version;
|
|
14250
|
+
if (3 === version) return 3;
|
|
14251
|
+
if (2 === version) return 2;
|
|
14252
|
+
}
|
|
14253
|
+
return null;
|
|
14254
|
+
}
|
|
12931
14255
|
function resolveDefaultEvalContext(projectPath, browser) {
|
|
12932
14256
|
if (!isChromiumFamily(browser)) return "background";
|
|
12933
14257
|
return 3 === chromiumManifestVersion(projectPath, browser) ? "page" : "background";
|
|
12934
14258
|
}
|
|
12935
14259
|
function wantsExtensionPageOverCdp(projectPath, browser, context, url) {
|
|
12936
14260
|
if (!isChromiumFamily(browser) || !context) return false;
|
|
12937
|
-
if ("
|
|
14261
|
+
if ("background" === context) return true;
|
|
14262
|
+
if ("page" === context) return "string" == typeof url && url.length > 0;
|
|
12938
14263
|
return EXTENSION_PAGE_CONTEXTS.includes(context) && 3 === chromiumManifestVersion(projectPath, browser);
|
|
12939
14264
|
}
|
|
12940
|
-
async function evaluateOnChromiumExtensionPage(args, browser, context) {
|
|
12941
|
-
|
|
12942
|
-
if (
|
|
12943
|
-
|
|
12944
|
-
|
|
12945
|
-
|
|
12946
|
-
|
|
12947
|
-
|
|
12948
|
-
|
|
12949
|
-
|
|
14265
|
+
async function evaluateOnChromiumExtensionPage(args, browser, context, resolved) {
|
|
14266
|
+
let wanted;
|
|
14267
|
+
if ("background" === context) {
|
|
14268
|
+
const extensionId = await resolveExtensionId(args.projectPath, browser);
|
|
14269
|
+
if (!extensionId) return envelope_envelope({
|
|
14270
|
+
ok: false,
|
|
14271
|
+
command: eval_schema.name,
|
|
14272
|
+
status: "no-extension-id",
|
|
14273
|
+
error: {
|
|
14274
|
+
code: "E_NO_EXTENSION_ID",
|
|
14275
|
+
name: "NoExtensionId",
|
|
14276
|
+
message: "Could not resolve the extension id from the live session's CDP targets."
|
|
14277
|
+
},
|
|
14278
|
+
hint: `Confirm the session is ready (extension_wait). ${CDP_PORT_MISSING_HINT}`
|
|
14279
|
+
});
|
|
14280
|
+
let workers = await findExtensionWorkerTargets(resolved.port, extensionId);
|
|
14281
|
+
const wakeWarnings = [];
|
|
14282
|
+
if (0 === workers.length) {
|
|
14283
|
+
const wake = await wakeExtensionWorker(resolved.port, extensionId);
|
|
14284
|
+
if (!wake.woken) return envelope_envelope({
|
|
14285
|
+
ok: false,
|
|
14286
|
+
command: eval_schema.name,
|
|
14287
|
+
status: "no-target",
|
|
14288
|
+
error: {
|
|
14289
|
+
code: "E_NO_TARGET",
|
|
14290
|
+
name: "NoTarget",
|
|
14291
|
+
message: `No running background for chrome-extension://${extensionId}/: Chrome stops an idle MV3 service worker and lists no target for it, and starting it through ServiceWorker.startWorker did not bring one back: ${wake.reason}.`
|
|
14292
|
+
},
|
|
14293
|
+
hint: "Check the extension declares a background (extension_manifest_validate), then wake it with an event it listens to (extension_reload, open a surface with extension_open, navigate a matching tab) and retry. extension_logs (context: ['background']) holds what it wrote before it idled."
|
|
14294
|
+
});
|
|
14295
|
+
workers = wake.targets;
|
|
14296
|
+
wakeWarnings.push("The background worker was idle (Chrome stops an MV3 service worker after about 30 s without events) and was started through ServiceWorker.startWorker before evaluating; state it held before idling is gone unless it was persisted.");
|
|
12950
14297
|
}
|
|
12951
|
-
|
|
12952
|
-
|
|
14298
|
+
const target = workers[0];
|
|
14299
|
+
const outcome = await evaluateOnExtensionPage(resolved.port, target.targetId, args.expression);
|
|
14300
|
+
if (!outcome.ok) return envelope_envelope({
|
|
14301
|
+
ok: false,
|
|
14302
|
+
command: eval_schema.name,
|
|
14303
|
+
status: outcome.thrown ? "eval-failed" : "cdp-failed",
|
|
14304
|
+
error: {
|
|
14305
|
+
code: outcome.thrown ? "E_EVAL" : "E_CDP",
|
|
14306
|
+
name: outcome.thrown ? "EvalError" : "CdpError",
|
|
14307
|
+
message: outcome.message
|
|
14308
|
+
},
|
|
14309
|
+
hint: outcome.thrown ? `The expression threw inside the ${target.type} at ${target.url}; it ran over CDP with the extension APIs available and a returned promise awaited.` : "The session's debug port refused the call or the worker went away mid-call. extension_doctor names which."
|
|
14310
|
+
});
|
|
14311
|
+
return envelope_envelope({
|
|
14312
|
+
ok: true,
|
|
14313
|
+
command: eval_schema.name,
|
|
14314
|
+
status: "evaluated",
|
|
14315
|
+
value: outcome.value,
|
|
14316
|
+
warnings: [
|
|
14317
|
+
...wakeWarnings,
|
|
14318
|
+
...workers.length > 1 ? [
|
|
14319
|
+
`${workers.length} background targets match the extension; evaluated in ${target.type} ${target.targetId}.`
|
|
14320
|
+
] : []
|
|
14321
|
+
],
|
|
14322
|
+
hint: `Evaluated over CDP in the extension's ${target.type} (${target.url}), the inspector path the extension CSP does not govern; a returned promise is awaited.`
|
|
14323
|
+
});
|
|
14324
|
+
}
|
|
14325
|
+
if ("page" === context && !isExtensionUrl(args.url)) {
|
|
14326
|
+
const url = args.url;
|
|
14327
|
+
const matches = matchTargetsByUrl(await listPageTargets(resolved.port), url);
|
|
14328
|
+
if (0 === matches.length) return RELAY_INSTEAD;
|
|
14329
|
+
return evaluateOnWebTarget(args, matches, url);
|
|
14330
|
+
}
|
|
12953
14331
|
if ("page" === context) wanted = args.url;
|
|
12954
14332
|
else {
|
|
12955
14333
|
const doc = surfaceDocument(args.projectPath, browser, context);
|
|
@@ -12981,7 +14359,7 @@ async function evaluateOnChromiumExtensionPage(args, browser, context) {
|
|
|
12981
14359
|
});
|
|
12982
14360
|
wanted = `chrome-extension://${extensionId}/${doc}`;
|
|
12983
14361
|
}
|
|
12984
|
-
const targets = await findExtensionPageTargets(resolved.port, wanted);
|
|
14362
|
+
const targets = (await findExtensionPageTargets(resolved.port, wanted)).filter((t)=>!isBrowserErrorPage(t.url));
|
|
12985
14363
|
if (0 === targets.length) return envelope_envelope({
|
|
12986
14364
|
ok: false,
|
|
12987
14365
|
command: eval_schema.name,
|
|
@@ -13054,6 +14432,50 @@ async function evaluateOnWebKitPage(args, browser, info) {
|
|
|
13054
14432
|
});
|
|
13055
14433
|
}
|
|
13056
14434
|
}
|
|
14435
|
+
const RELAY_INSTEAD = "__relay_instead__";
|
|
14436
|
+
function isBrowserErrorPage(url) {
|
|
14437
|
+
return /^(chrome|edge)-error:\/\//.test(url);
|
|
14438
|
+
}
|
|
14439
|
+
async function evaluateOnWebTarget(args, matches, url) {
|
|
14440
|
+
const resolved = await resolveCdpPort(args.projectPath, args.browser ?? "chrome");
|
|
14441
|
+
if (!resolved) return RELAY_INSTEAD;
|
|
14442
|
+
const live = matches.filter((t)=>!isBrowserErrorPage(t.url));
|
|
14443
|
+
if (0 === live.length) return envelope_envelope({
|
|
14444
|
+
ok: false,
|
|
14445
|
+
command: eval_schema.name,
|
|
14446
|
+
status: "no-target",
|
|
14447
|
+
error: {
|
|
14448
|
+
code: "E_NO_TARGET",
|
|
14449
|
+
name: "NoTarget",
|
|
14450
|
+
message: `The tab matching ${url} shows the browser's own error page (${matches[0].title || matches[0].url}), so there is no document to evaluate in.`
|
|
14451
|
+
},
|
|
14452
|
+
hint: "The page was blocked or failed to load; open it again with extension_open and read the browser's reason in the tab title."
|
|
14453
|
+
});
|
|
14454
|
+
const target = live[0];
|
|
14455
|
+
const outcome = await evaluateOnExtensionPage(resolved.port, target.targetId, args.expression);
|
|
14456
|
+
if (!outcome.ok) return envelope_envelope({
|
|
14457
|
+
ok: false,
|
|
14458
|
+
command: eval_schema.name,
|
|
14459
|
+
status: outcome.thrown ? "eval-failed" : "cdp-failed",
|
|
14460
|
+
error: {
|
|
14461
|
+
code: outcome.thrown ? "E_EVAL" : "E_CDP",
|
|
14462
|
+
name: outcome.thrown ? "EvalError" : "CdpError",
|
|
14463
|
+
message: outcome.message
|
|
14464
|
+
},
|
|
14465
|
+
hint: outcome.thrown ? `The expression threw inside ${target.url}; it ran over CDP in the page's main world.` : "The session's debug port refused the call or the tab went away. extension_doctor names which."
|
|
14466
|
+
});
|
|
14467
|
+
return envelope_envelope({
|
|
14468
|
+
ok: true,
|
|
14469
|
+
command: eval_schema.name,
|
|
14470
|
+
status: "evaluated",
|
|
14471
|
+
value: outcome.value,
|
|
14472
|
+
warnings: live.length > 1 ? [
|
|
14473
|
+
`${live.length} tabs match ${url}; evaluated in ${target.url} (target ${target.targetId}). Narrow the url to pick another.`
|
|
14474
|
+
] : [],
|
|
14475
|
+
hint: `Evaluated over CDP in ${target.url} (target ${target.targetId}), the page's main world through the inspector, which the site's CSP and Trusted Types do not govern. Pass context: "content" for the extension's isolated world instead.`
|
|
14476
|
+
});
|
|
14477
|
+
}
|
|
14478
|
+
const TRUSTED_TYPES_REFUSAL = /Trusted Type/i;
|
|
13057
14479
|
const RELAY_POLL_MS = 300;
|
|
13058
14480
|
const RELAY_DEFAULT_BUDGET_MS = 30000;
|
|
13059
14481
|
function tryParseFrame(raw) {
|
|
@@ -13126,6 +14548,95 @@ async function evaluateThroughRelay(args, browser, context) {
|
|
|
13126
14548
|
if (polls > 0) parsed.hint = `The expression returned a promise; the ${context} page settled it and this call polled ${polls} time${1 === polls ? "" : "s"} for the result. ` + ("string" == typeof parsed.hint ? parsed.hint : "");
|
|
13127
14549
|
return actFrameJson(parsed);
|
|
13128
14550
|
}
|
|
14551
|
+
const CSP_EVAL_REFUSAL = /blocked by CSP|call to eval|unsafe-eval|Content Security Policy/i;
|
|
14552
|
+
function explainCspRefusal(raw, context) {
|
|
14553
|
+
const parsed = tryParseFrame(raw);
|
|
14554
|
+
if (!parsed || false !== parsed.ok) return null;
|
|
14555
|
+
const code = "string" == typeof parsed.error?.code ? parsed.error.code : "";
|
|
14556
|
+
const message = String(parsed.error?.message ?? "");
|
|
14557
|
+
if ("E_EVAL" !== code || !CSP_EVAL_REFUSAL.test(message)) return null;
|
|
14558
|
+
parsed.error.name = "CspBlocksEval";
|
|
14559
|
+
parsed.hint = `The extension's content_security_policy (or the MV3 default) forbids eval in its own ${context ?? "background"} context, so the in-page executor cannot run any expression there; this is the extension's policy, not a fault in the expression. Read the page instead with extension_dom_snapshot or extension_inspect, evaluate a web page with context: \"page\" and a url, or read the extension's console with extension_logs. On a Chromium MV3 session the extension's own pages evaluate over CDP, which the page policy does not govern.`;
|
|
14560
|
+
if ("string" == typeof parsed.error.hint) delete parsed.error.hint;
|
|
14561
|
+
return actFrameJson(parsed);
|
|
14562
|
+
}
|
|
14563
|
+
const NO_SCRIPTING_API = /scripting is not available/i;
|
|
14564
|
+
function pageEvalRefusedByCsp(parsed) {
|
|
14565
|
+
const code = "string" == typeof parsed.error?.code ? parsed.error.code : "";
|
|
14566
|
+
return "E_EVAL" === code && CSP_EVAL_REFUSAL.test(String(parsed.error?.message ?? ""));
|
|
14567
|
+
}
|
|
14568
|
+
function isSingleExpression(source) {
|
|
14569
|
+
try {
|
|
14570
|
+
new Function(`return (${source}\n)`);
|
|
14571
|
+
return true;
|
|
14572
|
+
} catch {
|
|
14573
|
+
return false;
|
|
14574
|
+
}
|
|
14575
|
+
}
|
|
14576
|
+
async function evaluateThroughExecuteScript(args, browser, context, raw) {
|
|
14577
|
+
if ("page" !== context && "content" !== context) return null;
|
|
14578
|
+
if (isChromiumFamily(browser)) return null;
|
|
14579
|
+
const parsed = tryParseFrame(raw);
|
|
14580
|
+
if (!parsed || false !== parsed.ok) return null;
|
|
14581
|
+
const noScriptingApi = NO_SCRIPTING_API.test(String(parsed.error?.message ?? ""));
|
|
14582
|
+
const cspRefused = !noScriptingApi && pageEvalRefusedByCsp(parsed) && 2 === geckoManifestVersion(args.projectPath, browser);
|
|
14583
|
+
if (!noScriptingApi && !cspRefused) return null;
|
|
14584
|
+
const why = cspRefused ? "the page's content security policy refused the in-page eval" : `${browser} MV2 has no scripting API`;
|
|
14585
|
+
const code = isSingleExpression(args.expression) ? `(function () { try { return { __extensionDevExec: 1, ok: true, value: (${args.expression}\n) }; } catch (e) { return { __extensionDevExec: 1, ok: false, name: (e && e.name) || "EvalError", message: (e && e.message) || String(e) }; } })()` : args.expression;
|
|
14586
|
+
const wrapped = await runActVerb([
|
|
14587
|
+
"eval",
|
|
14588
|
+
...commonFlags({
|
|
14589
|
+
...args,
|
|
14590
|
+
context: "background",
|
|
14591
|
+
url: void 0,
|
|
14592
|
+
tab: void 0,
|
|
14593
|
+
browser
|
|
14594
|
+
}),
|
|
14595
|
+
"--",
|
|
14596
|
+
executeScriptExpression(args.url, code),
|
|
14597
|
+
args.projectPath
|
|
14598
|
+
], args.projectPath, args.timeout, eval_schema.name);
|
|
14599
|
+
const frame = tryParseFrame(wrapped);
|
|
14600
|
+
if (!frame || true !== frame.ok) return wrapped ?? raw;
|
|
14601
|
+
const value = frame.value;
|
|
14602
|
+
if (value && "object" == typeof value && "string" == typeof value.error) return envelope_envelope({
|
|
14603
|
+
ok: false,
|
|
14604
|
+
command: eval_schema.name,
|
|
14605
|
+
status: "eval-failed",
|
|
14606
|
+
error: {
|
|
14607
|
+
code: "E_EVAL",
|
|
14608
|
+
name: "EvalError",
|
|
14609
|
+
message: value.error
|
|
14610
|
+
},
|
|
14611
|
+
hint: `Because ${why}, the expression went through tabs.executeScript from the background and that call failed. The tab must be a web page the extension holds host permissions for; extension pages cannot be injected into.`
|
|
14612
|
+
});
|
|
14613
|
+
const first = Array.isArray(value?.frames) ? value.frames[0] : void 0;
|
|
14614
|
+
const result = first && "object" == typeof first && 1 === first.__extensionDevExec ? first : {
|
|
14615
|
+
ok: true,
|
|
14616
|
+
value: void 0 === first ? null : first
|
|
14617
|
+
};
|
|
14618
|
+
if (false === result.ok) return envelope_envelope({
|
|
14619
|
+
ok: false,
|
|
14620
|
+
command: eval_schema.name,
|
|
14621
|
+
status: "eval-failed",
|
|
14622
|
+
error: {
|
|
14623
|
+
code: "E_EVAL",
|
|
14624
|
+
name: result.name || "EvalError",
|
|
14625
|
+
message: result.message || "the expression threw"
|
|
14626
|
+
},
|
|
14627
|
+
hint: "The expression threw inside the tab (run through tabs.executeScript in the content world)."
|
|
14628
|
+
});
|
|
14629
|
+
return envelope_envelope({
|
|
14630
|
+
ok: true,
|
|
14631
|
+
command: eval_schema.name,
|
|
14632
|
+
status: "evaluated",
|
|
14633
|
+
value: void 0 === result.value ? null : result.value,
|
|
14634
|
+
warnings: [
|
|
14635
|
+
`Because ${why}, this ran through tabs.executeScript from the background: the content script world of the tab, not the page's MAIN world, so globals the page defines are not visible${"page" === context ? ' even though context: "page" was asked' : ""}. An empty string or null here is the content world's reading at call time; if the element renders late, wait and retry.`
|
|
14636
|
+
],
|
|
14637
|
+
hint: "Pass url to pick the tab; the extension needs host permissions for it."
|
|
14638
|
+
});
|
|
14639
|
+
}
|
|
13129
14640
|
async function eval_handler(args) {
|
|
13130
14641
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
13131
14642
|
if (WEBKIT_FAMILY.has(browser) && "page" === args.context) {
|
|
@@ -13134,7 +14645,13 @@ async function eval_handler(args) {
|
|
|
13134
14645
|
}
|
|
13135
14646
|
const defaulted = !args.context && "page" === resolveDefaultEvalContext(args.projectPath, browser);
|
|
13136
14647
|
const context = defaulted ? "page" : args.context;
|
|
13137
|
-
if (wantsExtensionPageOverCdp(args.projectPath, browser, context, args.url))
|
|
14648
|
+
if (wantsExtensionPageOverCdp(args.projectPath, browser, context, args.url)) {
|
|
14649
|
+
const resolved = await resolveCdpPort(args.projectPath, browser);
|
|
14650
|
+
if (resolved) {
|
|
14651
|
+
const overCdp = await evaluateOnChromiumExtensionPage(args, browser, context, resolved);
|
|
14652
|
+
if (overCdp !== RELAY_INSTEAD) return overCdp;
|
|
14653
|
+
}
|
|
14654
|
+
}
|
|
13138
14655
|
if ("page" === context && isExtensionUrl(args.url)) {
|
|
13139
14656
|
const surface = surfaceForExtensionUrl(args.projectPath, browser, args.url);
|
|
13140
14657
|
if (surface) {
|
|
@@ -13175,6 +14692,15 @@ async function eval_handler(args) {
|
|
|
13175
14692
|
args.expression,
|
|
13176
14693
|
args.projectPath
|
|
13177
14694
|
], args.projectPath, args.timeout, eval_schema.name);
|
|
14695
|
+
const mv2Fallback = await evaluateThroughExecuteScript(args, browser, context, raw);
|
|
14696
|
+
if (null !== mv2Fallback) return mv2Fallback;
|
|
14697
|
+
const cspRefusal = explainCspRefusal(raw, context);
|
|
14698
|
+
if (null !== cspRefusal) return cspRefusal;
|
|
14699
|
+
const trustedTypes = tryParseFrame(raw);
|
|
14700
|
+
if (trustedTypes?.ok === false && TRUSTED_TYPES_REFUSAL.test(String(trustedTypes.error?.message ?? ""))) {
|
|
14701
|
+
trustedTypes.hint = "This site enforces Trusted Types, which refuse a string evaluated inside the page. Pass url so the tab is evaluated over CDP (the inspector path the policy does not govern), or use context: \"content\" for a DOM read from the extension's isolated world.";
|
|
14702
|
+
return actFrameJson(trustedTypes);
|
|
14703
|
+
}
|
|
13178
14704
|
if ("content" === args.context) try {
|
|
13179
14705
|
const parsed = JSON.parse(raw);
|
|
13180
14706
|
if (parsed?.ok === true && (null === parsed.value || void 0 === parsed.value)) {
|
|
@@ -13663,10 +15189,11 @@ async function dom_snapshot_handler(args) {
|
|
|
13663
15189
|
}
|
|
13664
15190
|
const publish_schema = {
|
|
13665
15191
|
name: "extension_publish",
|
|
13666
|
-
description: "Publish the project your stored token is scoped to (extension_auth, or EXTENSION_DEV_TOKEN) to extension.dev, and return its shareable URL. This is what \"deploy\" or \"ship\" an extension usually means; extension_submit is the separate store-review path. The target is the token's project: there is no projectPath, and no local file is uploaded. For a public project the URL is the canonical public page and ttlHours does not apply. For a private one it is a fresh time-limited share link (?share=) whose lifetime is ttlHours.",
|
|
15192
|
+
description: "Publish the project your stored token is scoped to (extension_auth, or EXTENSION_DEV_TOKEN) to extension.dev, and return its shareable URL. This is what \"deploy\" or \"ship\" an extension usually means; extension_submit is the separate store-review path. The target is the token's project: there is no projectPath, and no local file is uploaded. With several logins stored, pass `project` ('<workspace>/<project>') to pick which one; it outranks EXTENSION_DEV_TOKEN. For a public project the URL is the canonical public page and ttlHours does not apply. For a private one it is a fresh time-limited share link (?share=) whose lifetime is ttlHours.",
|
|
13667
15193
|
inputSchema: {
|
|
13668
15194
|
type: "object",
|
|
13669
15195
|
properties: {
|
|
15196
|
+
project: PROJECT_TOKEN_INPUT,
|
|
13670
15197
|
ttlHours: {
|
|
13671
15198
|
type: "number",
|
|
13672
15199
|
description: "Private-project share-link lifetime in hours, 1-168 (default 24). Ignored for public projects."
|
|
@@ -13680,6 +15207,13 @@ const publish_schema = {
|
|
|
13680
15207
|
required: []
|
|
13681
15208
|
}
|
|
13682
15209
|
};
|
|
15210
|
+
function previewCommandsOf(value) {
|
|
15211
|
+
if (!value || "object" != typeof value) return [];
|
|
15212
|
+
return Object.entries(value).filter(([browser, command])=>"string" == typeof browser && "" !== browser.trim() && "string" == typeof command && command.startsWith("npx -y extension@latest preview ")).map(([browser, command])=>[
|
|
15213
|
+
browser,
|
|
15214
|
+
command
|
|
15215
|
+
]);
|
|
15216
|
+
}
|
|
13683
15217
|
function publish_fail(name, message, status, code) {
|
|
13684
15218
|
return envelope_envelope({
|
|
13685
15219
|
ok: false,
|
|
@@ -13693,7 +15227,10 @@ function publish_fail(name, message, status, code) {
|
|
|
13693
15227
|
});
|
|
13694
15228
|
}
|
|
13695
15229
|
async function publish_handler(args) {
|
|
13696
|
-
const token = resolveToken(
|
|
15230
|
+
const token = resolveToken({
|
|
15231
|
+
project: args.project
|
|
15232
|
+
});
|
|
15233
|
+
if (!token && args.project) return publish_fail("PublishAuthError", noStoredLoginHint(args.project), "auth-required", "E_AUTH_REQUIRED");
|
|
13697
15234
|
if (!token) return publish_fail("PublishAuthError", "No token. Run extension_auth (action: login), or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard).", "auth-required", "E_AUTH_REQUIRED");
|
|
13698
15235
|
if (null != args.ttlHours) {
|
|
13699
15236
|
const t = Number(args.ttlHours);
|
|
@@ -13762,7 +15299,7 @@ async function publish_handler(args) {
|
|
|
13762
15299
|
buildNote = `buildSha ${args.buildSha} is pinned but was not found in the project's registry build index, so builtAt/version/channel are not filled in from another build. The URL still serves the pinned build the platform accepted.`;
|
|
13763
15300
|
}
|
|
13764
15301
|
} else {
|
|
13765
|
-
const newestSuccess = items.filter(
|
|
15302
|
+
const newestSuccess = items.filter(isSuccessfulBuild).sort((a, b)=>String(b.timestamp ?? "").localeCompare(String(a.timestamp ?? "")))[0];
|
|
13766
15303
|
if (newestSuccess) {
|
|
13767
15304
|
if (null == data.buildSha) data.buildSha = newestSuccess.sha;
|
|
13768
15305
|
if (null == data.builtAt && newestSuccess.timestamp) data.builtAt = newestSuccess.timestamp;
|
|
@@ -13779,6 +15316,8 @@ async function publish_handler(args) {
|
|
|
13779
15316
|
body: data,
|
|
13780
15317
|
api: args.api
|
|
13781
15318
|
});
|
|
15319
|
+
const previewCommands = previewCommandsOf(data.previewCommands);
|
|
15320
|
+
const previewHint = previewCommands.length > 0 ? `Run the extension without opening the share page: ${previewCommands.map(([browser, command])=>`${browser}: ${command}`).join(" | ")}. Each command carries the same share token as the URL and expires with it.` : null;
|
|
13782
15321
|
return envelope_envelope({
|
|
13783
15322
|
ok: true,
|
|
13784
15323
|
command: "extension_publish",
|
|
@@ -13787,15 +15326,19 @@ async function publish_handler(args) {
|
|
|
13787
15326
|
warnings: [
|
|
13788
15327
|
note,
|
|
13789
15328
|
buildNote
|
|
13790
|
-
]
|
|
15329
|
+
],
|
|
15330
|
+
...previewHint ? {
|
|
15331
|
+
hint: previewHint
|
|
15332
|
+
} : {}
|
|
13791
15333
|
});
|
|
13792
15334
|
}
|
|
13793
15335
|
const release_promote_schema = {
|
|
13794
15336
|
name: "extension_release_promote",
|
|
13795
|
-
description: "Promote a built extension to a release channel (stable, preview, beta, …) on extension.dev, headless. This WRITES: it is the only verb that changes what a channel points at. It is auth-gated by your stored login (extension_auth) or a release token in EXTENSION_DEV_TOKEN, minted and revoked under project settings, Access tokens. Tokens live at most 7 days, so CI must re-mint before expiry. The project comes from the token. Call extension_release_status to find a valid buildId. Cutting a version-bump PR is not available headlessly, because it writes to your source repo and needs an interactive login.",
|
|
15337
|
+
description: "Promote a built extension to a release channel (stable, preview, beta, …) on extension.dev, headless. This WRITES: it is the only verb that changes what a channel points at. It is auth-gated by your stored login (extension_auth) or a release token in EXTENSION_DEV_TOKEN, minted and revoked under project settings, Access tokens. Tokens live at most 7 days, so CI must re-mint before expiry. The project comes from the token; with several logins stored, `project` picks which one. Call extension_release_status to find a valid buildId. Cutting a version-bump PR is not available headlessly, because it writes to your source repo and needs an interactive login.",
|
|
13796
15338
|
inputSchema: {
|
|
13797
15339
|
type: "object",
|
|
13798
15340
|
properties: {
|
|
15341
|
+
project: PROJECT_TOKEN_INPUT,
|
|
13799
15342
|
buildId: {
|
|
13800
15343
|
type: "string",
|
|
13801
15344
|
description: "Build commit SHA to promote (a 7-char short SHA is fine)"
|
|
@@ -13848,7 +15391,10 @@ function release_promote_fail(name, message, status, code) {
|
|
|
13848
15391
|
});
|
|
13849
15392
|
}
|
|
13850
15393
|
async function release_promote_handler(args) {
|
|
13851
|
-
const token = resolveToken(
|
|
15394
|
+
const token = resolveToken({
|
|
15395
|
+
project: args.project
|
|
15396
|
+
});
|
|
15397
|
+
if (!token && args.project) return release_promote_fail("PromoteAuthError", noStoredLoginHint(args.project), "auth-required", "E_AUTH_REQUIRED");
|
|
13852
15398
|
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; tokens live at most 7 days, so CI must re-mint before expiry), or run extension_auth (action: login).", "auth-required", "E_AUTH_REQUIRED");
|
|
13853
15399
|
const buildId = String(args.buildId || "").trim();
|
|
13854
15400
|
const channel = String(args.channel || "").trim();
|
|
@@ -13858,7 +15404,7 @@ async function release_promote_handler(args) {
|
|
|
13858
15404
|
const url = `${apiCheck.base}/api/cli/release/promote`;
|
|
13859
15405
|
const sourceChannel = args.sourceChannel ? String(args.sourceChannel).trim() : "";
|
|
13860
15406
|
const gateBrowsers = (Array.isArray(args.browsers) ? args.browsers : []).map((b)=>String(b).trim()).filter(Boolean);
|
|
13861
|
-
const
|
|
15407
|
+
const gateInput = {
|
|
13862
15408
|
command: "extension_release_promote",
|
|
13863
15409
|
action: "extension_release_promote",
|
|
13864
15410
|
scope: {
|
|
@@ -13877,6 +15423,10 @@ async function release_promote_handler(args) {
|
|
|
13877
15423
|
approvalId: args.approvalId,
|
|
13878
15424
|
token,
|
|
13879
15425
|
api: args.api
|
|
15426
|
+
};
|
|
15427
|
+
const gate = await evaluateApproval({
|
|
15428
|
+
...gateInput,
|
|
15429
|
+
enabled: approvalGateEnabled("stable" === channel)
|
|
13880
15430
|
});
|
|
13881
15431
|
if (gate.blocked) return gate.envelope;
|
|
13882
15432
|
const body = {
|
|
@@ -13921,6 +15471,7 @@ async function release_promote_handler(args) {
|
|
|
13921
15471
|
buildId
|
|
13922
15472
|
}
|
|
13923
15473
|
});
|
|
15474
|
+
if (!args.approvalId && platformRequiresApproval(res, data)) return requestApprovalAfterRefusal(gateInput);
|
|
13924
15475
|
const code = "string" == typeof data?.code ? data.code : void 0;
|
|
13925
15476
|
const enrich = {};
|
|
13926
15477
|
let hint = "";
|
|
@@ -14082,7 +15633,7 @@ async function readReleases(args) {
|
|
|
14082
15633
|
publicProjectUrl
|
|
14083
15634
|
} : {}
|
|
14084
15635
|
},
|
|
14085
|
-
hint: promotable.length > 0 || recentBuilds.length > 0 ? `Promotable shas: channels currently pin ${promotable.length > 0 ? promotable.join(", ") : "none"}; recent builds add ${recentBuilds.filter(
|
|
15636
|
+
hint: promotable.length > 0 || recentBuilds.length > 0 ? `Promotable shas: channels currently pin ${promotable.length > 0 ? promotable.join(", ") : "none"}; recent builds add ${recentBuilds.filter(isSuccessfulBuild).map((b)=>b.sha).join(", ") || "none"}. Use one of these as buildId/buildSha for promote/deploy/publish.` : `No channels or builds are recorded on the registry yet for ${ref.workspace}/${ref.project}. Push a commit to produce a build, then check ${buildsPageUrl}.`,
|
|
14086
15637
|
warnings: [
|
|
14087
15638
|
publicUrlNote,
|
|
14088
15639
|
channelsUnavailable,
|
|
@@ -14470,10 +16021,11 @@ function storeMdWarnings(browsers, cwd) {
|
|
|
14470
16021
|
}
|
|
14471
16022
|
const submit_schema = {
|
|
14472
16023
|
name: "extension_submit",
|
|
14473
|
-
description: "Submit a built extension for store REVIEW through extension.dev, which holds your store credentials and dispatches from your project's mirror CI: the Chrome Web Store, Firefox AMO, Edge Add-ons and the App Store (Safari). This is store review only. It does not push a build to the extension.dev platform, and it does not make a shareable link: that is extension_publish, which is what \"deploy\" or \"ship\" an extension almost always means. Reach for this only when the ask is explicitly a store submission. It defaults to a dry run that dispatches nothing: the platform verifies auth, project, build and store workflow, and this tool adds each store's credential-health verdict. Trust those per-store rows over the platform's bare preflight line, which does not check store health. Pass dryRun:false to actually submit, which is irreversible and enters store review. The project comes from your token (extension_auth or EXTENSION_DEV_TOKEN; tokens live at most 7 days, so CI must re-mint from the console's Access tokens page). Store credentials are never arguments, and no local file is uploaded. Call extension_release_status for valid shas, and, after a real submission, for the recorded outcome and review state.",
|
|
16024
|
+
description: "Submit a built extension for store REVIEW through extension.dev, which holds your store credentials and dispatches from your project's mirror CI: the Chrome Web Store, Firefox AMO, Edge Add-ons and the App Store (Safari). This is store review only. It does not push a build to the extension.dev platform, and it does not make a shareable link: that is extension_publish, which is what \"deploy\" or \"ship\" an extension almost always means. Reach for this only when the ask is explicitly a store submission. It defaults to a dry run that dispatches nothing: the platform verifies auth, project, build and store workflow, and this tool adds each store's credential-health verdict. Trust those per-store rows over the platform's bare preflight line, which does not check store health. Pass dryRun:false to actually submit, which is irreversible and enters store review. The project comes from your token (extension_auth or EXTENSION_DEV_TOKEN; tokens live at most 7 days, so CI must re-mint from the console's Access tokens page); with several logins stored, `project` picks which one. Store credentials are never arguments, and no local file is uploaded. Call extension_release_status for valid shas, and, after a real submission, for the recorded outcome and review state.",
|
|
14474
16025
|
inputSchema: {
|
|
14475
16026
|
type: "object",
|
|
14476
16027
|
properties: {
|
|
16028
|
+
project: PROJECT_TOKEN_INPUT,
|
|
14477
16029
|
browsers: {
|
|
14478
16030
|
type: "array",
|
|
14479
16031
|
items: {
|
|
@@ -14533,7 +16085,10 @@ function submit_fail(name, message, status, code) {
|
|
|
14533
16085
|
});
|
|
14534
16086
|
}
|
|
14535
16087
|
async function submit_handler(args) {
|
|
14536
|
-
const token = resolveToken(
|
|
16088
|
+
const token = resolveToken({
|
|
16089
|
+
project: args.project
|
|
16090
|
+
});
|
|
16091
|
+
if (!token && args.project) return submit_fail("SubmitAuthError", noStoredLoginHint(args.project), "auth-required", "E_AUTH_REQUIRED");
|
|
14537
16092
|
if (!token) return submit_fail("SubmitAuthError", "No token. Run extension_auth (action: login), or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard under project settings -> Access tokens; tokens live at most 7 days, so CI must re-mint before expiry).", "auth-required", "E_AUTH_REQUIRED");
|
|
14538
16093
|
const browsers = (Array.isArray(args.browsers) ? args.browsers : []).map((b)=>String(b).trim().toLowerCase()).filter(Boolean);
|
|
14539
16094
|
if (0 === browsers.length) return submit_fail("SubmitInputError", 'browsers is required (e.g. ["chrome","firefox","edge","safari"]).', "bad-request", "E_BAD_REQUEST");
|
|
@@ -14551,21 +16106,25 @@ async function submit_handler(args) {
|
|
|
14551
16106
|
};
|
|
14552
16107
|
if (args.channel) body.channel = String(args.channel).trim();
|
|
14553
16108
|
if (args.version) body.version = String(args.version).trim();
|
|
16109
|
+
const gateInput = {
|
|
16110
|
+
command: "extension_submit",
|
|
16111
|
+
action: "extension_submit",
|
|
16112
|
+
scope: {
|
|
16113
|
+
browsers: [
|
|
16114
|
+
...browsers
|
|
16115
|
+
].sort(),
|
|
16116
|
+
buildSha: buildSha.toLowerCase(),
|
|
16117
|
+
channel
|
|
16118
|
+
},
|
|
16119
|
+
description: `Submit build ${buildSha} to ${browsers.join(", ")} for store review.`,
|
|
16120
|
+
approvalId: args.approvalId,
|
|
16121
|
+
token,
|
|
16122
|
+
api: args.api
|
|
16123
|
+
};
|
|
14554
16124
|
if (!dryRun) {
|
|
14555
16125
|
const gate = await evaluateApproval({
|
|
14556
|
-
|
|
14557
|
-
|
|
14558
|
-
scope: {
|
|
14559
|
-
browsers: [
|
|
14560
|
-
...browsers
|
|
14561
|
-
].sort(),
|
|
14562
|
-
buildSha: buildSha.toLowerCase(),
|
|
14563
|
-
channel
|
|
14564
|
-
},
|
|
14565
|
-
description: `Submit build ${buildSha} to ${browsers.join(", ")} for store review.`,
|
|
14566
|
-
approvalId: args.approvalId,
|
|
14567
|
-
token,
|
|
14568
|
-
api: args.api
|
|
16126
|
+
...gateInput,
|
|
16127
|
+
enabled: approvalGateEnabled(true)
|
|
14569
16128
|
});
|
|
14570
16129
|
if (gate.blocked) return gate.envelope;
|
|
14571
16130
|
if (gate.approvalId) body.approvalId = gate.approvalId;
|
|
@@ -14605,6 +16164,7 @@ async function submit_handler(args) {
|
|
|
14605
16164
|
dryRun
|
|
14606
16165
|
}
|
|
14607
16166
|
});
|
|
16167
|
+
if (!dryRun && !args.approvalId && platformRequiresApproval(res, data)) return requestApprovalAfterRefusal(gateInput);
|
|
14608
16168
|
return submit_fail("SubmitError", `${dryRun ? "preflight" : "submit"} failed (${res.status}): ${data?.message || text || "unknown error"}`, "submit-failed", "E_PLATFORM");
|
|
14609
16169
|
}
|
|
14610
16170
|
const warnings = Array.isArray(data?.warnings) ? [
|
|
@@ -14764,7 +16324,7 @@ async function wait_handler(args) {
|
|
|
14764
16324
|
const requested = args.timeoutMs ?? args.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
14765
16325
|
const budgetMs = Math.min(Math.max(requested, MIN_TIMEOUT_MS), SAFE_CEILING_MS);
|
|
14766
16326
|
const clamped = requested > SAFE_CEILING_MS;
|
|
14767
|
-
const readyPath =
|
|
16327
|
+
const readyPath = session_paths_readyContractPath(args.projectPath, browser);
|
|
14768
16328
|
const buildOnly = findSessionInfo(args.projectPath, browser)?.noBrowser === true;
|
|
14769
16329
|
const since = sessionSinceMs(args.projectPath, browser);
|
|
14770
16330
|
const start = Date.now();
|
|
@@ -14772,6 +16332,7 @@ async function wait_handler(args) {
|
|
|
14772
16332
|
let sawCompiledButUnattached = false;
|
|
14773
16333
|
let lastContractStatus = null;
|
|
14774
16334
|
let staleContractNote = null;
|
|
16335
|
+
const clampNote = clamped ? `requested ${requested}ms was clamped to ${SAFE_CEILING_MS}ms to stay under the MCP client request timeout` : null;
|
|
14775
16336
|
while(Date.now() - start < budgetMs){
|
|
14776
16337
|
try {
|
|
14777
16338
|
const stat = node_fs.statSync(readyPath);
|
|
@@ -14823,6 +16384,26 @@ async function wait_handler(args) {
|
|
|
14823
16384
|
},
|
|
14824
16385
|
hint: "Build-only session (noBrowser): the extension compiled and the dev server is live, but no browser was launched, so browserAttached will never become true. Do not call extension_wait again to wait for a browser. The control verbs (storage/reload/open/dom_snapshot/eval) need a live browser and will not work against this session."
|
|
14825
16386
|
});
|
|
16387
|
+
if (!attached && "start" === contract.command) return envelope_envelope({
|
|
16388
|
+
ok: true,
|
|
16389
|
+
command: wait_schema.name,
|
|
16390
|
+
status: "launched",
|
|
16391
|
+
value: {
|
|
16392
|
+
compiled: true,
|
|
16393
|
+
browserAttached: false,
|
|
16394
|
+
sessionCommand: "start",
|
|
16395
|
+
browser: contract.browser,
|
|
16396
|
+
pid: contract.pid,
|
|
16397
|
+
readyPath,
|
|
16398
|
+
budgetMs,
|
|
16399
|
+
elapsedMs: Date.now() - start
|
|
16400
|
+
},
|
|
16401
|
+
warnings: [
|
|
16402
|
+
clampNote,
|
|
16403
|
+
staleContractNote
|
|
16404
|
+
],
|
|
16405
|
+
hint: "This is an extension_start session: the production build is loaded in the browser, and a production build carries no dev bridge, so browserAttached stays false for good and extension_eval, extension_storage, extension_reload, extension_open and extension_dom_snapshot cannot drive it. Do not call extension_wait again. To drive or read the extension, run it with extension_dev; to check the production artifact, extension_build reports its summary and extension_preview_web renders the built dist."
|
|
16406
|
+
});
|
|
14826
16407
|
if (!attached) {
|
|
14827
16408
|
await new Promise((r)=>setTimeout(r, pollInterval));
|
|
14828
16409
|
sawCompiledButUnattached = true;
|
|
@@ -14903,6 +16484,10 @@ async function wait_handler(args) {
|
|
|
14903
16484
|
budgetMs,
|
|
14904
16485
|
elapsedMs: Date.now() - start
|
|
14905
16486
|
},
|
|
16487
|
+
warnings: [
|
|
16488
|
+
clampNote,
|
|
16489
|
+
staleContractNote
|
|
16490
|
+
],
|
|
14906
16491
|
hint: "This is usually transient: call extension_wait again. If it persists, stop and restart the session with extension_dev (a restart reliably reattaches); extension_doctor reports the executor leg."
|
|
14907
16492
|
});
|
|
14908
16493
|
return envelope_envelope({
|
|
@@ -14921,7 +16506,7 @@ async function wait_handler(args) {
|
|
|
14921
16506
|
elapsedMs: Date.now() - start
|
|
14922
16507
|
},
|
|
14923
16508
|
warnings: [
|
|
14924
|
-
|
|
16509
|
+
clampNote,
|
|
14925
16510
|
staleContractNote
|
|
14926
16511
|
],
|
|
14927
16512
|
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."
|
|
@@ -14990,11 +16575,11 @@ const FEATURE_TEMPLATE_MAP = {
|
|
|
14990
16575
|
preact: "action"
|
|
14991
16576
|
},
|
|
14992
16577
|
newtab: {
|
|
14993
|
-
react: "
|
|
14994
|
-
vue: "
|
|
14995
|
-
svelte: "
|
|
14996
|
-
preact: "
|
|
14997
|
-
vanilla: "
|
|
16578
|
+
react: "newtab-react",
|
|
16579
|
+
vue: "newtab-vue",
|
|
16580
|
+
svelte: "newtab-svelte",
|
|
16581
|
+
preact: "newtab-preact",
|
|
16582
|
+
vanilla: "newtab"
|
|
14998
16583
|
},
|
|
14999
16584
|
background: {
|
|
15000
16585
|
react: "javascript",
|
|
@@ -15229,13 +16814,9 @@ function login_pending(start) {
|
|
|
15229
16814
|
const hasCompleteLink = complete.length > 0 && complete !== start.verificationUri;
|
|
15230
16815
|
const message = hasCompleteLink ? `Open ${complete} and approve (code ${start.userCode} is pre-filled), then call extension_auth (action: login) again with this deviceCode and the same project. If the page asks for a code, enter ${start.userCode} at ${start.verificationUri}.` : `Open ${start.verificationUri} and enter code ${start.userCode}, then call extension_auth (action: login) again with this deviceCode and the same project.`;
|
|
15231
16816
|
return envelope_envelope({
|
|
15232
|
-
ok:
|
|
16817
|
+
ok: true,
|
|
15233
16818
|
command: "extension_auth",
|
|
15234
16819
|
status: "authorization-pending",
|
|
15235
|
-
error: {
|
|
15236
|
-
code: "E_AUTH_PENDING",
|
|
15237
|
-
message
|
|
15238
|
-
},
|
|
15239
16820
|
value: {
|
|
15240
16821
|
userCode: start.userCode,
|
|
15241
16822
|
verificationUri: start.verificationUri,
|
|
@@ -15254,13 +16835,9 @@ function login_pending(start) {
|
|
|
15254
16835
|
function resumePending(deviceCode, verificationUri) {
|
|
15255
16836
|
const message = `Still waiting for authorization. The one-click link and code from the previous response are still valid: open that link (or enter the code at ${verificationUri}), then call extension_auth (action: login) again with this same deviceCode and the same project.`;
|
|
15256
16837
|
return envelope_envelope({
|
|
15257
|
-
ok:
|
|
16838
|
+
ok: true,
|
|
15258
16839
|
command: "extension_auth",
|
|
15259
16840
|
status: "authorization-pending",
|
|
15260
|
-
error: {
|
|
15261
|
-
code: "E_AUTH_PENDING",
|
|
15262
|
-
message
|
|
15263
|
-
},
|
|
15264
16841
|
value: {
|
|
15265
16842
|
verificationUri,
|
|
15266
16843
|
deviceCode,
|
|
@@ -15447,12 +17024,20 @@ async function readIdentity(deps) {
|
|
|
15447
17024
|
const identityNote = expired ? "The stored token has expired. Run extension_auth (action: login) to refresh it." : `Logged in as ${creds.workspaceSlug}/${creds.projectSlug}, per the token extension_auth stored on this machine. That token is what scopes the identity: it does not follow the current working directory or project folder.`;
|
|
15448
17025
|
const apiDivergesNote = apiDiverges ? `This login was minted via ${recordedApi}: access grants for private registry reads use that recorded base when no api argument is given, while other authenticated tools target ${effectiveDefaultApi} unless given one.` : null;
|
|
15449
17026
|
const envTokenNote = envTokenSet ? "EXTENSION_DEV_TOKEN is set and takes precedence over this stored login for authenticated tools; this report describes only the stored login." : null;
|
|
17027
|
+
const logins = listCredentials().map((entry)=>({
|
|
17028
|
+
project: `${entry.workspaceSlug}/${entry.projectSlug}`,
|
|
17029
|
+
active: entry.active,
|
|
17030
|
+
expiresAt: entry.expiresAt ? new Date(1000 * entry.expiresAt).toISOString() : null,
|
|
17031
|
+
expired: Boolean(entry.expiresAt && entry.expiresAt <= now)
|
|
17032
|
+
}));
|
|
17033
|
+
const loginsNote = logins.length > 1 ? `${logins.length} logins are stored on this machine (${logins.map((l)=>l.project).join(", ")}); the active one above is the default for token-scoped tools, and each of them takes \`project\` to use another.` : null;
|
|
15450
17034
|
const message = [
|
|
15451
17035
|
identityNote,
|
|
15452
17036
|
server.note,
|
|
15453
17037
|
server.warning,
|
|
15454
17038
|
apiDivergesNote,
|
|
15455
|
-
envTokenNote
|
|
17039
|
+
envTokenNote,
|
|
17040
|
+
loginsNote
|
|
15456
17041
|
].filter(Boolean).join(" ");
|
|
15457
17042
|
return envelope_envelope({
|
|
15458
17043
|
ok: true,
|
|
@@ -15469,7 +17054,8 @@ async function readIdentity(deps) {
|
|
|
15469
17054
|
expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
|
|
15470
17055
|
expiresInSeconds: creds.expiresAt ? creds.expiresAt - now : null,
|
|
15471
17056
|
expired,
|
|
15472
|
-
server: server.value
|
|
17057
|
+
server: server.value,
|
|
17058
|
+
logins
|
|
15473
17059
|
},
|
|
15474
17060
|
hint: message,
|
|
15475
17061
|
warnings: [
|
|
@@ -15480,27 +17066,35 @@ async function readIdentity(deps) {
|
|
|
15480
17066
|
]
|
|
15481
17067
|
});
|
|
15482
17068
|
}
|
|
15483
|
-
async function clearLocalCredentials() {
|
|
15484
|
-
const
|
|
17069
|
+
async function clearLocalCredentials(project) {
|
|
17070
|
+
const wanted = String(project ?? "").trim();
|
|
17071
|
+
const creds = readCredentials(wanted ? {
|
|
17072
|
+
project: wanted
|
|
17073
|
+
} : void 0);
|
|
15485
17074
|
const revokeUrl = creds?.workspaceSlug && creds?.projectSlug ? consoleProjectUrl({
|
|
15486
17075
|
workspace: creds.workspaceSlug,
|
|
15487
17076
|
project: creds.projectSlug
|
|
15488
17077
|
}, "settings/access-tokens") : null;
|
|
15489
|
-
const result = clearCredentials(
|
|
17078
|
+
const result = clearCredentials(wanted ? {
|
|
17079
|
+
project: wanted
|
|
17080
|
+
} : void 0);
|
|
17081
|
+
const scope = wanted ? `the login for ${creds ? `${creds.workspaceSlug}/${creds.projectSlug}` : wanted}` : result.removed.length > 1 ? `all ${result.removed.length} stored logins` : "the stored login";
|
|
15490
17082
|
return envelope_envelope({
|
|
15491
17083
|
ok: true,
|
|
15492
17084
|
command: "extension_auth",
|
|
15493
17085
|
status: result.cleared ? "logged-out" : "nothing-to-clear",
|
|
15494
17086
|
value: {
|
|
15495
17087
|
cleared: result.cleared,
|
|
17088
|
+
removed: result.removed,
|
|
17089
|
+
remaining: result.remaining,
|
|
15496
17090
|
revokeUrl: result.cleared && revokeUrl ? revokeUrl : null
|
|
15497
17091
|
},
|
|
15498
|
-
hint: result.cleared ?
|
|
17092
|
+
hint: result.cleared ? `Removed ${scope} from this machine${result.remaining.length ? `; ${result.remaining.length} other login${1 === result.remaining.length ? "" : "s"} stay (${result.remaining.join(", ")})` : ""}. The token stays valid server-side until it expires; ${revokeUrl ? `revoke it now at ${revokeUrl} (takes about a minute to propagate)` : "revoke it from the project's access-tokens page if needed"}.` : wanted ? `No stored login matches ${wanted}. extension_auth (action: status) lists the logins on this machine.` : "No stored credentials to remove."
|
|
15499
17093
|
});
|
|
15500
17094
|
}
|
|
15501
17095
|
const auth_schema = {
|
|
15502
17096
|
name: "extension_auth",
|
|
15503
|
-
description: "Sign this machine in to extension.dev, report that login, or clear it. Pass action:'status' (the default) to name the workspace and project the stored token is scoped to and when it expires, never the token itself; that identity comes from the stored token alone, and does not change with the current working directory or whichever project folder you are in. Status also asks the platform's whoami endpoint whether that credential actually resolves there: the answer is reported as confirmed, refused-by-server, or unverified when the server cannot be reached, so a local file claiming a login the server would refuse is never reported as simply logged in. Pass action:'login' for a two-phase flow: call with `project` to get a code plus a URL the user authorizes at extension.dev/device, then call again with the returned `deviceCode`. GitHub federation happens server-side, so no GitHub token lands on this machine. Minted tokens live at most 7 days, server-enforced, so CI must re-mint before expiry on the console's project settings, Access tokens page. Pass action:'logout' to delete the local credentials only; the token stays valid server-side until it is revoked at the URL the response returns.",
|
|
17097
|
+
description: "Sign this machine in to extension.dev, report that login, or clear it. Pass action:'status' (the default) to name the workspace and project the stored token is scoped to and when it expires, never the token itself; that identity comes from the stored token alone, and does not change with the current working directory or whichever project folder you are in. Status also asks the platform's whoami endpoint whether that credential actually resolves there: the answer is reported as confirmed, refused-by-server, or unverified when the server cannot be reached, so a local file claiming a login the server would refuse is never reported as simply logged in. Pass action:'login' for a two-phase flow: call with `project` to get a code plus a URL the user authorizes at extension.dev/device, then call again with the returned `deviceCode`. GitHub federation happens server-side, so no GitHub token lands on this machine. Minted tokens live at most 7 days, server-enforced, so CI must re-mint before expiry on the console's project settings, Access tokens page. Pass action:'logout' to delete the local credentials only (with `project`, just that project's login); the token stays valid server-side until it is revoked at the URL the response returns. Several logins live side by side on one machine, one per workspace/project, and status lists them all under `logins`.",
|
|
15504
17098
|
inputSchema: {
|
|
15505
17099
|
type: "object",
|
|
15506
17100
|
properties: {
|
|
@@ -15515,7 +17109,7 @@ const auth_schema = {
|
|
|
15515
17109
|
},
|
|
15516
17110
|
project: {
|
|
15517
17111
|
type: "string",
|
|
15518
|
-
description: "login: target project as '<workspace>/<project>'; the token is scoped to it. The slug pair is the console address bar: an existing project's page is console.extension.dev/<workspace>/<project>. Create one at extension.dev/new if none exists yet."
|
|
17112
|
+
description: "login: target project as '<workspace>/<project>'; the token is scoped to it. The slug pair is the console address bar: an existing project's page is console.extension.dev/<workspace>/<project>. Create one at extension.dev/new if none exists yet. Logins to several projects are all kept, the latest is the default; token-scoped tools take `project` to pick another. logout: remove only this project's login (omitted, every stored login goes)."
|
|
15519
17113
|
},
|
|
15520
17114
|
deviceCode: {
|
|
15521
17115
|
type: "string",
|
|
@@ -15528,7 +17122,7 @@ const auth_schema = {
|
|
|
15528
17122
|
};
|
|
15529
17123
|
async function auth_handler(args) {
|
|
15530
17124
|
const action = args.action ?? "status";
|
|
15531
|
-
if ("logout" === action) return clearLocalCredentials();
|
|
17125
|
+
if ("logout" === action) return clearLocalCredentials(args.project);
|
|
15532
17126
|
if ("login" === action) return loginToProject({
|
|
15533
17127
|
project: String(args.project ?? ""),
|
|
15534
17128
|
deviceCode: args.deviceCode,
|
|
@@ -15548,6 +17142,8 @@ const ALL_BROWSERS = [
|
|
|
15548
17142
|
"firefox",
|
|
15549
17143
|
"waterfox",
|
|
15550
17144
|
"librewolf",
|
|
17145
|
+
"zen",
|
|
17146
|
+
"floorp",
|
|
15551
17147
|
"safari"
|
|
15552
17148
|
];
|
|
15553
17149
|
const MANAGED_INSTALLABLE = new Set([
|
|
@@ -15598,6 +17194,14 @@ const SYSTEM_PATHS = {
|
|
|
15598
17194
|
"/Applications/LibreWolf.app/Contents/MacOS/librewolf",
|
|
15599
17195
|
`${process.env.HOME}/Applications/LibreWolf.app/Contents/MacOS/librewolf`
|
|
15600
17196
|
],
|
|
17197
|
+
zen: [
|
|
17198
|
+
"/Applications/Zen.app/Contents/MacOS/zen",
|
|
17199
|
+
`${process.env.HOME}/Applications/Zen.app/Contents/MacOS/zen`
|
|
17200
|
+
],
|
|
17201
|
+
floorp: [
|
|
17202
|
+
"/Applications/Floorp.app/Contents/MacOS/floorp",
|
|
17203
|
+
`${process.env.HOME}/Applications/Floorp.app/Contents/MacOS/floorp`
|
|
17204
|
+
],
|
|
15601
17205
|
safari: [
|
|
15602
17206
|
"/Applications/Safari.app/Contents/MacOS/Safari"
|
|
15603
17207
|
]
|
|
@@ -15647,6 +17251,16 @@ const SYSTEM_PATHS = {
|
|
|
15647
17251
|
librewolf: [
|
|
15648
17252
|
"/usr/bin/librewolf",
|
|
15649
17253
|
"/opt/librewolf/librewolf"
|
|
17254
|
+
],
|
|
17255
|
+
zen: [
|
|
17256
|
+
"/usr/bin/zen",
|
|
17257
|
+
"/usr/bin/zen-browser",
|
|
17258
|
+
"/opt/zen/zen",
|
|
17259
|
+
"/opt/zen-browser/zen"
|
|
17260
|
+
],
|
|
17261
|
+
floorp: [
|
|
17262
|
+
"/usr/bin/floorp",
|
|
17263
|
+
"/opt/floorp/floorp"
|
|
15650
17264
|
]
|
|
15651
17265
|
},
|
|
15652
17266
|
win32: {
|
|
@@ -15688,6 +17302,14 @@ const SYSTEM_PATHS = {
|
|
|
15688
17302
|
librewolf: [
|
|
15689
17303
|
"C:\\Program Files\\LibreWolf\\librewolf.exe",
|
|
15690
17304
|
"C:\\Program Files (x86)\\LibreWolf\\librewolf.exe"
|
|
17305
|
+
],
|
|
17306
|
+
zen: [
|
|
17307
|
+
"C:\\Program Files\\Zen Browser\\zen.exe",
|
|
17308
|
+
`${process.env.LOCALAPPDATA || ""}\\Zen Browser\\zen.exe`
|
|
17309
|
+
],
|
|
17310
|
+
floorp: [
|
|
17311
|
+
"C:\\Program Files\\Floorp\\floorp.exe",
|
|
17312
|
+
"C:\\Program Files (x86)\\Floorp\\floorp.exe"
|
|
15691
17313
|
]
|
|
15692
17314
|
}
|
|
15693
17315
|
};
|
|
@@ -15749,16 +17371,29 @@ function executableInsideBundle(bundle, names) {
|
|
|
15749
17371
|
} catch {}
|
|
15750
17372
|
return null;
|
|
15751
17373
|
}
|
|
17374
|
+
function compareVersionNames(a, b) {
|
|
17375
|
+
const nums = (name)=>(name.match(/\d+/g) ?? []).map((n)=>Number(n));
|
|
17376
|
+
const left = nums(a);
|
|
17377
|
+
const right = nums(b);
|
|
17378
|
+
for(let i = 0; i < Math.max(left.length, right.length); i += 1){
|
|
17379
|
+
const diff = (left[i] ?? -1) - (right[i] ?? -1);
|
|
17380
|
+
if (0 !== diff) return diff;
|
|
17381
|
+
}
|
|
17382
|
+
return a.localeCompare(b);
|
|
17383
|
+
}
|
|
15752
17384
|
function findManagedBinary(browser, cacheRoot = resolveCacheRoot()) {
|
|
15753
|
-
|
|
17385
|
+
return findManagedBinaryIn(node_path.join(cacheRoot, browser), browser);
|
|
17386
|
+
}
|
|
17387
|
+
function findManagedBinaryIn(browserDir, browser) {
|
|
15754
17388
|
if (!node_fs.existsSync(browserDir)) return null;
|
|
15755
17389
|
const names = MANAGED_EXEC_NAMES[browser] ?? [];
|
|
15756
17390
|
function search(dir, depth) {
|
|
15757
17391
|
if (depth > MANAGED_SEARCH_DEPTH) return null;
|
|
15758
17392
|
try {
|
|
15759
|
-
|
|
17393
|
+
const entries = node_fs.readdirSync(dir, {
|
|
15760
17394
|
withFileTypes: true
|
|
15761
|
-
}))
|
|
17395
|
+
}).sort((a, b)=>compareVersionNames(b.name, a.name));
|
|
17396
|
+
for (const entry of entries){
|
|
15762
17397
|
const full = node_path.join(dir, entry.name);
|
|
15763
17398
|
if (entry.isFile() && names.includes(entry.name)) return full;
|
|
15764
17399
|
if (entry.isDirectory() && entry.name.endsWith(".app")) {
|
|
@@ -15921,7 +17556,8 @@ async function installManagedBrowser(browser) {
|
|
|
15921
17556
|
const start = Date.now();
|
|
15922
17557
|
try {
|
|
15923
17558
|
await extensionInstall({
|
|
15924
|
-
browser
|
|
17559
|
+
browser,
|
|
17560
|
+
locateInstalledBinary: (destination, target)=>findManagedBinaryIn(destination, target)
|
|
15925
17561
|
});
|
|
15926
17562
|
return envelope_envelope({
|
|
15927
17563
|
ok: true,
|
|
@@ -16220,6 +17856,288 @@ function inputValidationError(toolName, issues, inputSchema) {
|
|
|
16220
17856
|
}
|
|
16221
17857
|
});
|
|
16222
17858
|
}
|
|
17859
|
+
const FEATURE_GROUPS = [
|
|
17860
|
+
"local",
|
|
17861
|
+
"platform"
|
|
17862
|
+
];
|
|
17863
|
+
const FEATURES_ENV = "EXTENSION_DEV_FEATURES";
|
|
17864
|
+
const NO_SHIP_ENV = "EXTENSION_DEV_NO_SHIP";
|
|
17865
|
+
const reads = (openWorld = false)=>({
|
|
17866
|
+
readOnlyHint: true,
|
|
17867
|
+
destructiveHint: false,
|
|
17868
|
+
idempotentHint: true,
|
|
17869
|
+
openWorldHint: openWorld
|
|
17870
|
+
});
|
|
17871
|
+
const acts = (over = {})=>({
|
|
17872
|
+
readOnlyHint: false,
|
|
17873
|
+
destructiveHint: false,
|
|
17874
|
+
idempotentHint: false,
|
|
17875
|
+
openWorldHint: false,
|
|
17876
|
+
...over
|
|
17877
|
+
});
|
|
17878
|
+
const TOOL_POLICY = {
|
|
17879
|
+
extension_create: {
|
|
17880
|
+
group: "local",
|
|
17881
|
+
annotations: acts({
|
|
17882
|
+
openWorldHint: true
|
|
17883
|
+
})
|
|
17884
|
+
},
|
|
17885
|
+
extension_templates: {
|
|
17886
|
+
group: "local",
|
|
17887
|
+
annotations: reads(true)
|
|
17888
|
+
},
|
|
17889
|
+
extension_add_feature: {
|
|
17890
|
+
group: "local",
|
|
17891
|
+
annotations: reads()
|
|
17892
|
+
},
|
|
17893
|
+
extension_build: {
|
|
17894
|
+
group: "local",
|
|
17895
|
+
annotations: acts({
|
|
17896
|
+
idempotentHint: true
|
|
17897
|
+
})
|
|
17898
|
+
},
|
|
17899
|
+
extension_dev: {
|
|
17900
|
+
group: "local",
|
|
17901
|
+
annotations: acts()
|
|
17902
|
+
},
|
|
17903
|
+
extension_start: {
|
|
17904
|
+
group: "local",
|
|
17905
|
+
annotations: acts()
|
|
17906
|
+
},
|
|
17907
|
+
extension_wait: {
|
|
17908
|
+
group: "local",
|
|
17909
|
+
annotations: reads()
|
|
17910
|
+
},
|
|
17911
|
+
extension_stop: {
|
|
17912
|
+
group: "local",
|
|
17913
|
+
annotations: acts({
|
|
17914
|
+
idempotentHint: true
|
|
17915
|
+
})
|
|
17916
|
+
},
|
|
17917
|
+
extension_manifest_validate: {
|
|
17918
|
+
group: "local",
|
|
17919
|
+
annotations: reads()
|
|
17920
|
+
},
|
|
17921
|
+
extension_theme_verify: {
|
|
17922
|
+
group: "local",
|
|
17923
|
+
annotations: reads()
|
|
17924
|
+
},
|
|
17925
|
+
extension_analyze: {
|
|
17926
|
+
group: "local",
|
|
17927
|
+
annotations: reads()
|
|
17928
|
+
},
|
|
17929
|
+
extension_assert: {
|
|
17930
|
+
group: "local",
|
|
17931
|
+
annotations: acts({
|
|
17932
|
+
idempotentHint: true
|
|
17933
|
+
})
|
|
17934
|
+
},
|
|
17935
|
+
extension_inspect: {
|
|
17936
|
+
group: "local",
|
|
17937
|
+
annotations: acts({
|
|
17938
|
+
idempotentHint: true
|
|
17939
|
+
})
|
|
17940
|
+
},
|
|
17941
|
+
extension_dom_snapshot: {
|
|
17942
|
+
group: "local",
|
|
17943
|
+
annotations: reads()
|
|
17944
|
+
},
|
|
17945
|
+
extension_list_extensions: {
|
|
17946
|
+
group: "local",
|
|
17947
|
+
annotations: reads()
|
|
17948
|
+
},
|
|
17949
|
+
extension_logs: {
|
|
17950
|
+
group: "local",
|
|
17951
|
+
annotations: reads()
|
|
17952
|
+
},
|
|
17953
|
+
extension_doctor: {
|
|
17954
|
+
group: "local",
|
|
17955
|
+
annotations: reads()
|
|
17956
|
+
},
|
|
17957
|
+
extension_eval: {
|
|
17958
|
+
group: "local",
|
|
17959
|
+
annotations: acts({
|
|
17960
|
+
destructiveHint: true,
|
|
17961
|
+
openWorldHint: true
|
|
17962
|
+
})
|
|
17963
|
+
},
|
|
17964
|
+
extension_storage: {
|
|
17965
|
+
group: "local",
|
|
17966
|
+
annotations: acts({
|
|
17967
|
+
destructiveHint: true
|
|
17968
|
+
})
|
|
17969
|
+
},
|
|
17970
|
+
extension_reload: {
|
|
17971
|
+
group: "local",
|
|
17972
|
+
annotations: acts({
|
|
17973
|
+
idempotentHint: true
|
|
17974
|
+
})
|
|
17975
|
+
},
|
|
17976
|
+
extension_open: {
|
|
17977
|
+
group: "local",
|
|
17978
|
+
annotations: acts({
|
|
17979
|
+
openWorldHint: true
|
|
17980
|
+
})
|
|
17981
|
+
},
|
|
17982
|
+
extension_browsers: {
|
|
17983
|
+
group: "local",
|
|
17984
|
+
annotations: acts({
|
|
17985
|
+
destructiveHint: true,
|
|
17986
|
+
openWorldHint: true
|
|
17987
|
+
})
|
|
17988
|
+
},
|
|
17989
|
+
extension_auth: {
|
|
17990
|
+
group: "platform",
|
|
17991
|
+
annotations: acts({
|
|
17992
|
+
openWorldHint: true
|
|
17993
|
+
})
|
|
17994
|
+
},
|
|
17995
|
+
extension_workspace_create: {
|
|
17996
|
+
group: "platform",
|
|
17997
|
+
annotations: acts({
|
|
17998
|
+
openWorldHint: true
|
|
17999
|
+
})
|
|
18000
|
+
},
|
|
18001
|
+
extension_project_create: {
|
|
18002
|
+
group: "platform",
|
|
18003
|
+
annotations: acts({
|
|
18004
|
+
openWorldHint: true
|
|
18005
|
+
})
|
|
18006
|
+
},
|
|
18007
|
+
extension_preview_web: {
|
|
18008
|
+
group: "platform",
|
|
18009
|
+
annotations: acts({
|
|
18010
|
+
openWorldHint: true
|
|
18011
|
+
}),
|
|
18012
|
+
ships: (args)=>true === args.share
|
|
18013
|
+
},
|
|
18014
|
+
extension_shares: {
|
|
18015
|
+
group: "platform",
|
|
18016
|
+
annotations: acts({
|
|
18017
|
+
destructiveHint: true,
|
|
18018
|
+
openWorldHint: true
|
|
18019
|
+
}),
|
|
18020
|
+
ships: (args)=>"revoke" === args.action
|
|
18021
|
+
},
|
|
18022
|
+
extension_publish: {
|
|
18023
|
+
group: "platform",
|
|
18024
|
+
annotations: acts({
|
|
18025
|
+
openWorldHint: true
|
|
18026
|
+
}),
|
|
18027
|
+
ships: "always"
|
|
18028
|
+
},
|
|
18029
|
+
extension_release_status: {
|
|
18030
|
+
group: "platform",
|
|
18031
|
+
annotations: reads(true)
|
|
18032
|
+
},
|
|
18033
|
+
extension_release_promote: {
|
|
18034
|
+
group: "platform",
|
|
18035
|
+
annotations: acts({
|
|
18036
|
+
destructiveHint: true,
|
|
18037
|
+
openWorldHint: true
|
|
18038
|
+
}),
|
|
18039
|
+
ships: "always"
|
|
18040
|
+
},
|
|
18041
|
+
extension_submit: {
|
|
18042
|
+
group: "platform",
|
|
18043
|
+
annotations: acts({
|
|
18044
|
+
destructiveHint: true,
|
|
18045
|
+
openWorldHint: true
|
|
18046
|
+
}),
|
|
18047
|
+
ships: (args)=>false === args.dryRun
|
|
18048
|
+
}
|
|
18049
|
+
};
|
|
18050
|
+
const DEFAULT_SERVER_OPTIONS = {
|
|
18051
|
+
features: [
|
|
18052
|
+
...FEATURE_GROUPS
|
|
18053
|
+
],
|
|
18054
|
+
noShip: false
|
|
18055
|
+
};
|
|
18056
|
+
const truthy = (raw)=>{
|
|
18057
|
+
const value = String(raw ?? "").trim().toLowerCase();
|
|
18058
|
+
return "" !== value && "0" !== value && "false" !== value && "off" !== value;
|
|
18059
|
+
};
|
|
18060
|
+
function isServerFlag(arg) {
|
|
18061
|
+
return "--no-ship" === arg || "--features" === arg || arg.startsWith("--features=");
|
|
18062
|
+
}
|
|
18063
|
+
function resolveServerOptions(argv, env) {
|
|
18064
|
+
let rawFeatures = env[FEATURES_ENV];
|
|
18065
|
+
let noShip = truthy(env[NO_SHIP_ENV]);
|
|
18066
|
+
for(let i = 0; i < argv.length; i++){
|
|
18067
|
+
const arg = argv[i];
|
|
18068
|
+
if ("--no-ship" === arg) noShip = true;
|
|
18069
|
+
else if ("--features" === arg) rawFeatures = argv[++i] ?? "";
|
|
18070
|
+
else {
|
|
18071
|
+
if (!arg.startsWith("--features=")) return {
|
|
18072
|
+
ok: false,
|
|
18073
|
+
message: `Unknown flag "${arg}".`
|
|
18074
|
+
};
|
|
18075
|
+
rawFeatures = arg.slice(11);
|
|
18076
|
+
}
|
|
18077
|
+
}
|
|
18078
|
+
if (void 0 === rawFeatures || "" === rawFeatures.trim()) return {
|
|
18079
|
+
ok: true,
|
|
18080
|
+
options: {
|
|
18081
|
+
features: [
|
|
18082
|
+
...FEATURE_GROUPS
|
|
18083
|
+
],
|
|
18084
|
+
noShip
|
|
18085
|
+
}
|
|
18086
|
+
};
|
|
18087
|
+
const names = rawFeatures.split(",").map((name)=>name.trim().toLowerCase()).filter(Boolean);
|
|
18088
|
+
const unknown = names.filter((name)=>!FEATURE_GROUPS.includes(name));
|
|
18089
|
+
if (unknown.length) return {
|
|
18090
|
+
ok: false,
|
|
18091
|
+
message: `Unknown feature group ${unknown.map((n)=>`"${n}"`).join(", ")}. Use ${FEATURE_GROUPS.join(" or ")}, comma-separated.`
|
|
18092
|
+
};
|
|
18093
|
+
const features = FEATURE_GROUPS.filter((group)=>names.includes(group));
|
|
18094
|
+
return {
|
|
18095
|
+
ok: true,
|
|
18096
|
+
options: {
|
|
18097
|
+
features,
|
|
18098
|
+
noShip
|
|
18099
|
+
}
|
|
18100
|
+
};
|
|
18101
|
+
}
|
|
18102
|
+
function isToolListed(name, options) {
|
|
18103
|
+
const policy = TOOL_POLICY[name];
|
|
18104
|
+
if (!policy) return false;
|
|
18105
|
+
if (!options.features.includes(policy.group)) return false;
|
|
18106
|
+
return !(options.noShip && "always" === policy.ships);
|
|
18107
|
+
}
|
|
18108
|
+
function disabledToolEnvelope(name, args, options) {
|
|
18109
|
+
const policy = TOOL_POLICY[name];
|
|
18110
|
+
if (!policy) return null;
|
|
18111
|
+
if (!options.features.includes(policy.group)) return envelope_envelope({
|
|
18112
|
+
ok: false,
|
|
18113
|
+
command: name,
|
|
18114
|
+
status: "tool-disabled",
|
|
18115
|
+
error: {
|
|
18116
|
+
code: "E_TOOL_DISABLED",
|
|
18117
|
+
message: `${name} is in the "${policy.group}" feature group, which this server was started without.`
|
|
18118
|
+
},
|
|
18119
|
+
value: {
|
|
18120
|
+
group: policy.group,
|
|
18121
|
+
features: options.features
|
|
18122
|
+
},
|
|
18123
|
+
hint: `Add ${policy.group} to --features (or ${FEATURES_ENV}) in this server's MCP config, or ask the user to.`
|
|
18124
|
+
});
|
|
18125
|
+
const ships = "always" === policy.ships || "function" == typeof policy.ships && policy.ships(args);
|
|
18126
|
+
if (options.noShip && ships) return envelope_envelope({
|
|
18127
|
+
ok: false,
|
|
18128
|
+
command: name,
|
|
18129
|
+
status: "tool-disabled",
|
|
18130
|
+
error: {
|
|
18131
|
+
code: "E_TOOL_DISABLED",
|
|
18132
|
+
message: `${name} with these arguments reaches people outside this machine, and this server was started in no-ship mode.`
|
|
18133
|
+
},
|
|
18134
|
+
value: {
|
|
18135
|
+
noShip: true
|
|
18136
|
+
},
|
|
18137
|
+
hint: `Dry runs, previews without share, and listing still work. To ship, the user removes --no-ship (or ${NO_SHIP_ENV}) from this server's MCP config.`
|
|
18138
|
+
});
|
|
18139
|
+
return null;
|
|
18140
|
+
}
|
|
16223
18141
|
const tools = [
|
|
16224
18142
|
create_namespaceObject,
|
|
16225
18143
|
templates_namespaceObject,
|
|
@@ -16249,6 +18167,7 @@ const tools = [
|
|
|
16249
18167
|
add_feature_namespaceObject,
|
|
16250
18168
|
auth_namespaceObject,
|
|
16251
18169
|
project_create_namespaceObject,
|
|
18170
|
+
workspace_create_namespaceObject,
|
|
16252
18171
|
browsers_namespaceObject,
|
|
16253
18172
|
doctor_namespaceObject
|
|
16254
18173
|
];
|
|
@@ -16274,8 +18193,8 @@ function toolResultFrame(result) {
|
|
|
16274
18193
|
}
|
|
16275
18194
|
const toolMap = new Map();
|
|
16276
18195
|
for (const tool of tools)toolMap.set(tool.schema.name, tool);
|
|
16277
|
-
const SERVER_INSTRUCTIONS = "extension-dev runs, inspects, drives, builds and publishes browser extensions (Chrome, Edge, Firefox, Safari and the other Chromium and Gecko browsers) through Extension.js and extension.dev.\nWhen the ask is to run, start, wait for, watch, inspect, drive, test, debug or build a browser extension, search this server first and use its tools: extension_dev starts the dev session (allowEval: true also turns on control), extension_wait blocks until it is ready, extension_logs streams its console, extension_open opens a surface or a url, extension_dom_snapshot and extension_inspect read a live page, extension_eval runs code in a context, extension_build makes a store-ready bundle, extension_stop ends the session.\nThese replace hand-rolled ps, curl, remote-debugging-port lookups and CDP or Playwright scripts: the server already holds the session's debug port, the extension id and the session token.\nEvery tool answers one JSON envelope {ok, status, value, error, hint, warnings}; read hint and warnings before choosing the next call, and treat ok: false as the answer, not a transport error.";
|
|
16278
|
-
function createServer() {
|
|
18196
|
+
const SERVER_INSTRUCTIONS = "extension-dev runs, inspects, drives, builds and publishes browser extensions (Chrome, Edge, Firefox, Safari and the other Chromium and Gecko browsers) through Extension.js and extension.dev.\nWhen the ask is to run, start, wait for, watch, inspect, drive, test, debug or build a browser extension, search this server first and use its tools: extension_dev starts the dev session (allowEval: true also turns on control), extension_wait blocks until it is ready, extension_logs streams its console, extension_open opens a surface or a url, extension_dom_snapshot and extension_inspect read a live page, extension_eval runs code in a context, extension_build makes a store-ready bundle, extension_stop ends the session.\nThese replace hand-rolled ps, curl, remote-debugging-port lookups and CDP or Playwright scripts: the server already holds the session's debug port, the extension id and the session token.\nEvery tool answers one JSON envelope {ok, status, value, error, hint, warnings}; read hint and warnings before choosing the next call, and treat ok: false as the answer, not a transport error. status authorization-pending is ok: true and means a human approves at the link in hint before you call again; it is not done yet.";
|
|
18197
|
+
function createServer(options = DEFAULT_SERVER_OPTIONS) {
|
|
16279
18198
|
const server = new Server({
|
|
16280
18199
|
name: "extension-dev",
|
|
16281
18200
|
version: package_namespaceObject.rE
|
|
@@ -16286,10 +18205,11 @@ function createServer() {
|
|
|
16286
18205
|
instructions: SERVER_INSTRUCTIONS
|
|
16287
18206
|
});
|
|
16288
18207
|
server.setRequestHandler(ListToolsRequestSchema, async ()=>({
|
|
16289
|
-
tools: tools.map((t)=>({
|
|
18208
|
+
tools: tools.filter((t)=>isToolListed(t.schema.name, options)).map((t)=>({
|
|
16290
18209
|
name: t.schema.name,
|
|
16291
18210
|
description: t.schema.description,
|
|
16292
|
-
inputSchema: t.schema.inputSchema
|
|
18211
|
+
inputSchema: t.schema.inputSchema,
|
|
18212
|
+
annotations: TOOL_POLICY[t.schema.name].annotations
|
|
16293
18213
|
}))
|
|
16294
18214
|
}));
|
|
16295
18215
|
server.setRequestHandler(CallToolRequestSchema, async (request)=>{
|
|
@@ -16308,7 +18228,7 @@ function createServer() {
|
|
|
16308
18228
|
message: `Unknown tool: ${name}`
|
|
16309
18229
|
},
|
|
16310
18230
|
value: {
|
|
16311
|
-
availableTools: tools.map((t)=>t.schema.name)
|
|
18231
|
+
availableTools: tools.map((t)=>t.schema.name).filter((toolName)=>isToolListed(toolName, options))
|
|
16312
18232
|
}
|
|
16313
18233
|
})
|
|
16314
18234
|
}
|
|
@@ -16316,6 +18236,16 @@ function createServer() {
|
|
|
16316
18236
|
isError: true
|
|
16317
18237
|
};
|
|
16318
18238
|
const normalizedArgs = normalizeArgAliases(tool.schema.inputSchema, args ?? {});
|
|
18239
|
+
const disabled = disabledToolEnvelope(name, normalizedArgs, options);
|
|
18240
|
+
if (disabled) return {
|
|
18241
|
+
content: [
|
|
18242
|
+
{
|
|
18243
|
+
type: "text",
|
|
18244
|
+
text: disabled
|
|
18245
|
+
}
|
|
18246
|
+
],
|
|
18247
|
+
isError: true
|
|
18248
|
+
};
|
|
16319
18249
|
const issues = validateToolInput(tool.schema.inputSchema, normalizedArgs);
|
|
16320
18250
|
if (issues.length) return {
|
|
16321
18251
|
content: [
|
|
@@ -16352,9 +18282,9 @@ function createServer() {
|
|
|
16352
18282
|
});
|
|
16353
18283
|
return server;
|
|
16354
18284
|
}
|
|
16355
|
-
async function startServer() {
|
|
18285
|
+
async function startServer(options = DEFAULT_SERVER_OPTIONS) {
|
|
16356
18286
|
installCarrierExitCleanup();
|
|
16357
|
-
const server = createServer();
|
|
18287
|
+
const server = createServer(options);
|
|
16358
18288
|
const transport = new StdioServerTransport();
|
|
16359
18289
|
await server.connect(transport);
|
|
16360
18290
|
}
|
|
@@ -16450,4 +18380,4 @@ async function runCli(cmd, args) {
|
|
|
16450
18380
|
return 1;
|
|
16451
18381
|
}
|
|
16452
18382
|
var version_0 = package_namespaceObject.rE;
|
|
16453
|
-
export { runCli, startServer, version_0 as version };
|
|
18383
|
+
export { isServerFlag, resolveServerOptions, runCli, startServer, version_0 as version };
|