@extension.dev/mcp 6.6.0 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +259 -0
- package/README.md +13 -21
- package/claude/ARCHITECTURE.md +4 -4
- package/claude/CLAUDE.md +2 -2
- package/claude/README.md +1 -1
- package/claude/commands/extension-add.md +1 -1
- package/claude/commands/extension-debug.md +1 -1
- package/claude/commands/extension-publish.md +1 -1
- package/claude/commands/extension.md +3 -3
- package/claude/rules/mcp-tools.md +45 -49
- package/dist/module.js +1041 -1423
- package/dist/src/lib/common-schema.d.ts +28 -0
- package/dist/src/lib/credentials.d.ts +1 -1
- package/dist/src/lib/launch-flags.d.ts +6 -6
- package/dist/src/lib/login-flow.d.ts +0 -10
- package/dist/src/lib/session-identity.d.ts +16 -0
- package/dist/src/tools/add-feature.d.ts +2 -2
- package/dist/src/tools/analyze.d.ts +29 -0
- package/dist/src/tools/auth.d.ts +33 -0
- package/dist/src/tools/browsers.d.ts +39 -0
- package/dist/src/tools/build.d.ts +5 -6
- package/dist/src/tools/detect-browsers.d.ts +1 -20
- package/dist/src/tools/dev.d.ts +11 -11
- package/dist/src/tools/{dom-inspect.d.ts → dom-snapshot.d.ts} +6 -6
- package/dist/src/tools/eval.d.ts +6 -6
- package/dist/src/tools/get-template-source.d.ts +1 -22
- package/dist/src/tools/inspect.d.ts +33 -5
- package/dist/src/tools/install-browser.d.ts +1 -18
- package/dist/src/tools/list-browsers.d.ts +1 -9
- package/dist/src/tools/list-extensions.d.ts +4 -4
- package/dist/src/tools/list-templates.d.ts +1 -35
- package/dist/src/tools/login.d.ts +1 -23
- package/dist/src/tools/logout.d.ts +1 -9
- package/dist/src/tools/logs-schema.d.ts +2 -2
- package/dist/src/tools/open.d.ts +6 -6
- package/dist/src/tools/preview-web.d.ts +4 -16
- package/dist/src/tools/publish.d.ts +2 -2
- package/dist/src/tools/release-list.d.ts +1 -23
- package/dist/src/tools/release-promote.d.ts +2 -2
- package/dist/src/tools/release-status.d.ts +37 -0
- package/dist/src/tools/reload.d.ts +6 -6
- package/dist/src/tools/shares.d.ts +2 -2
- package/dist/src/tools/start.d.ts +16 -10
- package/dist/src/tools/stop.d.ts +2 -2
- package/dist/src/tools/storage.d.ts +6 -6
- package/dist/src/tools/store-status.d.ts +1 -23
- package/dist/src/tools/{deploy.d.ts → submit.d.ts} +4 -4
- package/dist/src/tools/{source-inspect.d.ts → templates.d.ts} +27 -23
- package/dist/src/tools/uninstall-browser.d.ts +1 -21
- package/dist/src/tools/wait.d.ts +4 -4
- package/dist/src/tools/whoami.d.ts +1 -9
- package/extensions/live-preview/chromium/action/index.css +1 -1
- package/extensions/live-preview/chromium/action/index.js +1 -9
- package/extensions/live-preview/chromium/background/service_worker.js +4 -12
- package/extensions/live-preview/chromium/manifest.json +1 -2
- package/package.json +2 -2
- package/server.json +3 -3
- package/dist/src/__tests__/fixtures/ready-contract.d.ts +0 -7
- package/dist/src/__tests__/setup-session-dir.d.ts +0 -1
- package/dist/src/lib/github-device.d.ts +0 -31
- package/dist/src/tools/preview.d.ts +0 -66
- /package/dist/src/tools/{source-inspect-gecko.d.ts → inspect-gecko.d.ts} +0 -0
package/dist/module.js
CHANGED
|
@@ -6,16 +6,16 @@ import node_fs, { existsSync, promises, readdirSync, statSync } from "node:fs";
|
|
|
6
6
|
import node_path, { join } from "node:path";
|
|
7
7
|
import { extensionCreate } from "extension-create";
|
|
8
8
|
import node_os from "node:os";
|
|
9
|
-
import cross_spawn from "cross-spawn";
|
|
10
9
|
import node_crypto, { createHash } from "node:crypto";
|
|
10
|
+
import cross_spawn from "cross-spawn";
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
import { execFile, execFileSync } from "node:child_process";
|
|
13
13
|
import ws_0 from "ws";
|
|
14
14
|
import node_http from "node:http";
|
|
15
15
|
import { filterKeysForThisBrowser } from "browser-extension-manifest-fields";
|
|
16
16
|
import node_net from "node:net";
|
|
17
|
-
import { extensionInstall, extensionUninstall, getManagedBrowsersCacheRoot } from "extension-install";
|
|
18
17
|
import { promisify } from "node:util";
|
|
18
|
+
import { extensionInstall, extensionUninstall, getManagedBrowsersCacheRoot } from "extension-install";
|
|
19
19
|
__webpack_require__.add({
|
|
20
20
|
"./node_modules/.pnpm/@extension.dev+urls@0.3.0/node_modules/@extension.dev/urls/dist/origins.cjs" (__unused_rspack_module, exports) {
|
|
21
21
|
var __nested_rspack_require_18_37__ = {};
|
|
@@ -410,6 +410,24 @@ __webpack_require__.d(add_feature_namespaceObject, {
|
|
|
410
410
|
handler: ()=>add_feature_handler,
|
|
411
411
|
schema: ()=>add_feature_schema
|
|
412
412
|
});
|
|
413
|
+
var analyze_namespaceObject = {};
|
|
414
|
+
__webpack_require__.r(analyze_namespaceObject);
|
|
415
|
+
__webpack_require__.d(analyze_namespaceObject, {
|
|
416
|
+
handler: ()=>analyze_handler,
|
|
417
|
+
schema: ()=>analyze_schema
|
|
418
|
+
});
|
|
419
|
+
var auth_namespaceObject = {};
|
|
420
|
+
__webpack_require__.r(auth_namespaceObject);
|
|
421
|
+
__webpack_require__.d(auth_namespaceObject, {
|
|
422
|
+
handler: ()=>auth_handler,
|
|
423
|
+
schema: ()=>auth_schema
|
|
424
|
+
});
|
|
425
|
+
var browsers_namespaceObject = {};
|
|
426
|
+
__webpack_require__.r(browsers_namespaceObject);
|
|
427
|
+
__webpack_require__.d(browsers_namespaceObject, {
|
|
428
|
+
handler: ()=>browsers_handler,
|
|
429
|
+
schema: ()=>browsers_schema
|
|
430
|
+
});
|
|
413
431
|
var build_namespaceObject = {};
|
|
414
432
|
__webpack_require__.r(build_namespaceObject);
|
|
415
433
|
__webpack_require__.d(build_namespaceObject, {
|
|
@@ -422,19 +440,6 @@ __webpack_require__.d(create_namespaceObject, {
|
|
|
422
440
|
handler: ()=>create_handler,
|
|
423
441
|
schema: ()=>create_schema
|
|
424
442
|
});
|
|
425
|
-
var deploy_namespaceObject = {};
|
|
426
|
-
__webpack_require__.r(deploy_namespaceObject);
|
|
427
|
-
__webpack_require__.d(deploy_namespaceObject, {
|
|
428
|
-
handler: ()=>deploy_handler,
|
|
429
|
-
schema: ()=>deploy_schema,
|
|
430
|
-
storeMdWarnings: ()=>storeMdWarnings
|
|
431
|
-
});
|
|
432
|
-
var detect_browsers_namespaceObject = {};
|
|
433
|
-
__webpack_require__.r(detect_browsers_namespaceObject);
|
|
434
|
-
__webpack_require__.d(detect_browsers_namespaceObject, {
|
|
435
|
-
handler: ()=>detect_browsers_handler,
|
|
436
|
-
schema: ()=>detect_browsers_schema
|
|
437
|
-
});
|
|
438
443
|
var dev_namespaceObject = {};
|
|
439
444
|
__webpack_require__.r(dev_namespaceObject);
|
|
440
445
|
__webpack_require__.d(dev_namespaceObject, {
|
|
@@ -448,11 +453,11 @@ __webpack_require__.d(doctor_namespaceObject, {
|
|
|
448
453
|
recentErrorLogs: ()=>recentErrorLogs,
|
|
449
454
|
schema: ()=>doctor_schema
|
|
450
455
|
});
|
|
451
|
-
var
|
|
452
|
-
__webpack_require__.r(
|
|
453
|
-
__webpack_require__.d(
|
|
454
|
-
handler: ()=>
|
|
455
|
-
schema: ()=>
|
|
456
|
+
var dom_snapshot_namespaceObject = {};
|
|
457
|
+
__webpack_require__.r(dom_snapshot_namespaceObject);
|
|
458
|
+
__webpack_require__.d(dom_snapshot_namespaceObject, {
|
|
459
|
+
handler: ()=>dom_snapshot_handler,
|
|
460
|
+
schema: ()=>dom_snapshot_schema
|
|
456
461
|
});
|
|
457
462
|
var eval_namespaceObject = {};
|
|
458
463
|
__webpack_require__.r(eval_namespaceObject);
|
|
@@ -461,54 +466,18 @@ __webpack_require__.d(eval_namespaceObject, {
|
|
|
461
466
|
resolveDefaultEvalContext: ()=>resolveDefaultEvalContext,
|
|
462
467
|
schema: ()=>eval_schema
|
|
463
468
|
});
|
|
464
|
-
var get_template_source_namespaceObject = {};
|
|
465
|
-
__webpack_require__.r(get_template_source_namespaceObject);
|
|
466
|
-
__webpack_require__.d(get_template_source_namespaceObject, {
|
|
467
|
-
handler: ()=>get_template_source_handler,
|
|
468
|
-
schema: ()=>get_template_source_schema
|
|
469
|
-
});
|
|
470
469
|
var inspect_namespaceObject = {};
|
|
471
470
|
__webpack_require__.r(inspect_namespaceObject);
|
|
472
471
|
__webpack_require__.d(inspect_namespaceObject, {
|
|
473
472
|
handler: ()=>inspect_handler,
|
|
474
473
|
schema: ()=>inspect_schema
|
|
475
474
|
});
|
|
476
|
-
var install_browser_namespaceObject = {};
|
|
477
|
-
__webpack_require__.r(install_browser_namespaceObject);
|
|
478
|
-
__webpack_require__.d(install_browser_namespaceObject, {
|
|
479
|
-
handler: ()=>install_browser_handler,
|
|
480
|
-
schema: ()=>install_browser_schema
|
|
481
|
-
});
|
|
482
|
-
var list_browsers_namespaceObject = {};
|
|
483
|
-
__webpack_require__.r(list_browsers_namespaceObject);
|
|
484
|
-
__webpack_require__.d(list_browsers_namespaceObject, {
|
|
485
|
-
handler: ()=>list_browsers_handler,
|
|
486
|
-
schema: ()=>list_browsers_schema
|
|
487
|
-
});
|
|
488
475
|
var list_extensions_namespaceObject = {};
|
|
489
476
|
__webpack_require__.r(list_extensions_namespaceObject);
|
|
490
477
|
__webpack_require__.d(list_extensions_namespaceObject, {
|
|
491
478
|
handler: ()=>list_extensions_handler,
|
|
492
479
|
schema: ()=>list_extensions_schema
|
|
493
480
|
});
|
|
494
|
-
var list_templates_namespaceObject = {};
|
|
495
|
-
__webpack_require__.r(list_templates_namespaceObject);
|
|
496
|
-
__webpack_require__.d(list_templates_namespaceObject, {
|
|
497
|
-
handler: ()=>list_templates_handler,
|
|
498
|
-
schema: ()=>list_templates_schema
|
|
499
|
-
});
|
|
500
|
-
var login_namespaceObject = {};
|
|
501
|
-
__webpack_require__.r(login_namespaceObject);
|
|
502
|
-
__webpack_require__.d(login_namespaceObject, {
|
|
503
|
-
handler: ()=>login_handler,
|
|
504
|
-
schema: ()=>login_schema
|
|
505
|
-
});
|
|
506
|
-
var logout_namespaceObject = {};
|
|
507
|
-
__webpack_require__.r(logout_namespaceObject);
|
|
508
|
-
__webpack_require__.d(logout_namespaceObject, {
|
|
509
|
-
handler: ()=>logout_handler,
|
|
510
|
-
schema: ()=>logout_schema
|
|
511
|
-
});
|
|
512
481
|
var logs_namespaceObject = {};
|
|
513
482
|
__webpack_require__.r(logs_namespaceObject);
|
|
514
483
|
__webpack_require__.d(logs_namespaceObject, {
|
|
@@ -536,30 +505,24 @@ __webpack_require__.d(preview_web_namespaceObject, {
|
|
|
536
505
|
handler: ()=>preview_web_handler,
|
|
537
506
|
schema: ()=>preview_web_schema
|
|
538
507
|
});
|
|
539
|
-
var preview_namespaceObject = {};
|
|
540
|
-
__webpack_require__.r(preview_namespaceObject);
|
|
541
|
-
__webpack_require__.d(preview_namespaceObject, {
|
|
542
|
-
handler: ()=>preview_handler,
|
|
543
|
-
schema: ()=>preview_schema
|
|
544
|
-
});
|
|
545
508
|
var tools_publish_namespaceObject = {};
|
|
546
509
|
__webpack_require__.r(tools_publish_namespaceObject);
|
|
547
510
|
__webpack_require__.d(tools_publish_namespaceObject, {
|
|
548
511
|
handler: ()=>publish_handler,
|
|
549
512
|
schema: ()=>publish_schema
|
|
550
513
|
});
|
|
551
|
-
var release_list_namespaceObject = {};
|
|
552
|
-
__webpack_require__.r(release_list_namespaceObject);
|
|
553
|
-
__webpack_require__.d(release_list_namespaceObject, {
|
|
554
|
-
handler: ()=>release_list_handler,
|
|
555
|
-
schema: ()=>release_list_schema
|
|
556
|
-
});
|
|
557
514
|
var release_promote_namespaceObject = {};
|
|
558
515
|
__webpack_require__.r(release_promote_namespaceObject);
|
|
559
516
|
__webpack_require__.d(release_promote_namespaceObject, {
|
|
560
517
|
handler: ()=>release_promote_handler,
|
|
561
518
|
schema: ()=>release_promote_schema
|
|
562
519
|
});
|
|
520
|
+
var release_status_namespaceObject = {};
|
|
521
|
+
__webpack_require__.r(release_status_namespaceObject);
|
|
522
|
+
__webpack_require__.d(release_status_namespaceObject, {
|
|
523
|
+
handler: ()=>release_status_handler,
|
|
524
|
+
schema: ()=>release_status_schema
|
|
525
|
+
});
|
|
563
526
|
var reload_namespaceObject = {};
|
|
564
527
|
__webpack_require__.r(reload_namespaceObject);
|
|
565
528
|
__webpack_require__.d(reload_namespaceObject, {
|
|
@@ -572,12 +535,6 @@ __webpack_require__.d(shares_namespaceObject, {
|
|
|
572
535
|
handler: ()=>shares_handler,
|
|
573
536
|
schema: ()=>shares_schema
|
|
574
537
|
});
|
|
575
|
-
var source_inspect_namespaceObject = {};
|
|
576
|
-
__webpack_require__.r(source_inspect_namespaceObject);
|
|
577
|
-
__webpack_require__.d(source_inspect_namespaceObject, {
|
|
578
|
-
handler: ()=>source_inspect_handler,
|
|
579
|
-
schema: ()=>source_inspect_schema
|
|
580
|
-
});
|
|
581
538
|
var start_namespaceObject = {};
|
|
582
539
|
__webpack_require__.r(start_namespaceObject);
|
|
583
540
|
__webpack_require__.d(start_namespaceObject, {
|
|
@@ -597,13 +554,18 @@ __webpack_require__.d(storage_namespaceObject, {
|
|
|
597
554
|
handler: ()=>storage_handler,
|
|
598
555
|
schema: ()=>storage_schema
|
|
599
556
|
});
|
|
600
|
-
var
|
|
601
|
-
__webpack_require__.r(
|
|
602
|
-
__webpack_require__.d(
|
|
603
|
-
handler: ()=>
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
557
|
+
var submit_namespaceObject = {};
|
|
558
|
+
__webpack_require__.r(submit_namespaceObject);
|
|
559
|
+
__webpack_require__.d(submit_namespaceObject, {
|
|
560
|
+
handler: ()=>submit_handler,
|
|
561
|
+
schema: ()=>submit_schema,
|
|
562
|
+
storeMdWarnings: ()=>storeMdWarnings
|
|
563
|
+
});
|
|
564
|
+
var templates_namespaceObject = {};
|
|
565
|
+
__webpack_require__.r(templates_namespaceObject);
|
|
566
|
+
__webpack_require__.d(templates_namespaceObject, {
|
|
567
|
+
handler: ()=>templates_handler,
|
|
568
|
+
schema: ()=>templates_schema
|
|
607
569
|
});
|
|
608
570
|
var theme_verify_namespaceObject = {};
|
|
609
571
|
__webpack_require__.r(theme_verify_namespaceObject);
|
|
@@ -611,25 +573,13 @@ __webpack_require__.d(theme_verify_namespaceObject, {
|
|
|
611
573
|
handler: ()=>theme_verify_handler,
|
|
612
574
|
schema: ()=>theme_verify_schema
|
|
613
575
|
});
|
|
614
|
-
var uninstall_browser_namespaceObject = {};
|
|
615
|
-
__webpack_require__.r(uninstall_browser_namespaceObject);
|
|
616
|
-
__webpack_require__.d(uninstall_browser_namespaceObject, {
|
|
617
|
-
handler: ()=>uninstall_browser_handler,
|
|
618
|
-
schema: ()=>uninstall_browser_schema
|
|
619
|
-
});
|
|
620
576
|
var wait_namespaceObject = {};
|
|
621
577
|
__webpack_require__.r(wait_namespaceObject);
|
|
622
578
|
__webpack_require__.d(wait_namespaceObject, {
|
|
623
579
|
handler: ()=>wait_handler,
|
|
624
580
|
schema: ()=>wait_schema
|
|
625
581
|
});
|
|
626
|
-
var
|
|
627
|
-
__webpack_require__.r(whoami_namespaceObject);
|
|
628
|
-
__webpack_require__.d(whoami_namespaceObject, {
|
|
629
|
-
handler: ()=>whoami_handler,
|
|
630
|
-
schema: ()=>whoami_schema
|
|
631
|
-
});
|
|
632
|
-
var package_namespaceObject = JSON.parse('{"rE":"6.5.0","El":{"OP":"4.0.16-canary.1784889479.74e12044"}}');
|
|
582
|
+
var package_namespaceObject = JSON.parse('{"rE":"9.0.0","El":{"OP":"4.0.16-canary.1784889479.74e12044"}}');
|
|
633
583
|
function credentialsPath() {
|
|
634
584
|
if ("win32" === process.platform) {
|
|
635
585
|
const base = process.env.APPDATA || process.env.LOCALAPPDATA || node_path.join(node_os.homedir(), "AppData", "Roaming");
|
|
@@ -647,7 +597,7 @@ function readCredentials() {
|
|
|
647
597
|
if (1 !== data.version) return null;
|
|
648
598
|
const token = String(data.token || "").trim();
|
|
649
599
|
if (!token) return null;
|
|
650
|
-
const provider = "extensiondev" === data.provider
|
|
600
|
+
const provider = "extensiondev" === data.provider ? data.provider : void 0;
|
|
651
601
|
return {
|
|
652
602
|
version: 1,
|
|
653
603
|
token,
|
|
@@ -735,15 +685,6 @@ function safeApiBase(raw) {
|
|
|
735
685
|
};
|
|
736
686
|
}
|
|
737
687
|
async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
|
|
738
|
-
const override = String(process.env.EXTENSION_DEV_GITHUB_CLIENT_ID || "").trim();
|
|
739
|
-
if (override) return {
|
|
740
|
-
provider: "github",
|
|
741
|
-
clientId: override,
|
|
742
|
-
scope: "read:user",
|
|
743
|
-
deviceCodeUrl: "/api/cli/device/code",
|
|
744
|
-
deviceTokenUrl: "/api/cli/device/token",
|
|
745
|
-
verificationUri: "https://github.com/login/device"
|
|
746
|
-
};
|
|
747
688
|
const res = await fetchImpl(`${apiBase}/api/cli/login/config`, {
|
|
748
689
|
headers: {
|
|
749
690
|
accept: "application/json"
|
|
@@ -751,16 +692,10 @@ async function fetchLoginConfig(apiBase, fetchImpl = fetch) {
|
|
|
751
692
|
});
|
|
752
693
|
if (!res.ok) throw new Error(`Could not fetch login config from ${apiBase} (${res.status}).`);
|
|
753
694
|
const data = await res.json().catch(()=>({}));
|
|
754
|
-
const provider = "extensiondev" === data.provider ? "extensiondev" : "github";
|
|
755
|
-
const clientId = String(data.githubClientId || "").trim();
|
|
756
|
-
if ("github" === provider && !clientId) throw new Error("Login is not configured on the server (no GitHub client id). Set EXTENSION_DEV_GITHUB_CLIENT_ID to override.");
|
|
757
695
|
return {
|
|
758
|
-
provider,
|
|
759
|
-
clientId,
|
|
760
|
-
scope: String(data.scope || "read:user"),
|
|
761
696
|
deviceCodeUrl: String(data.deviceCodeUrl || "/api/cli/device/code"),
|
|
762
697
|
deviceTokenUrl: String(data.deviceTokenUrl || "/api/cli/device/token"),
|
|
763
|
-
verificationUri: String(data.verificationUri || "
|
|
698
|
+
verificationUri: String(data.verificationUri || `${apiBase.replace(/\/+$/, "")}/device`)
|
|
764
699
|
};
|
|
765
700
|
}
|
|
766
701
|
function persistTokenResponse(args) {
|
|
@@ -773,39 +708,115 @@ function persistTokenResponse(args) {
|
|
|
773
708
|
projectSlug: String(args.data.projectSlug || ""),
|
|
774
709
|
expiresAt: Number(args.data.expiresAt || 0),
|
|
775
710
|
api: args.apiBase,
|
|
776
|
-
provider:
|
|
711
|
+
provider: "extensiondev"
|
|
777
712
|
};
|
|
778
713
|
writeCredentials(creds);
|
|
779
714
|
return creds;
|
|
780
715
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
716
|
+
const INSTALL_HEADER = "x-extensiondev-install";
|
|
717
|
+
const SESSION_HEADER = "x-extensiondev-session";
|
|
718
|
+
const TOOL_HEADER = "x-extensiondev-tool";
|
|
719
|
+
const ROTATE_AFTER_MS = 2592000000;
|
|
720
|
+
function installIdentityDir() {
|
|
721
|
+
if ("win32" === process.platform) {
|
|
722
|
+
const base = process.env.APPDATA || process.env.LOCALAPPDATA || node_path.join(node_os.homedir(), "AppData", "Roaming");
|
|
723
|
+
return node_path.join(base, "extension-dev");
|
|
724
|
+
}
|
|
725
|
+
const xdg = String(process.env.XDG_CONFIG_HOME || "").trim();
|
|
726
|
+
const base = xdg || node_path.join(node_os.homedir(), ".config");
|
|
727
|
+
return node_path.join(base, "extension-dev");
|
|
728
|
+
}
|
|
729
|
+
function installIdentityPath() {
|
|
730
|
+
return node_path.join(installIdentityDir(), "install.json");
|
|
731
|
+
}
|
|
732
|
+
function telemetryDisabled() {
|
|
733
|
+
const off = (value)=>{
|
|
734
|
+
const raw = String(value || "").trim().toLowerCase();
|
|
735
|
+
return "" !== raw && "0" !== raw && "false" !== raw;
|
|
736
|
+
};
|
|
737
|
+
return off(process.env.EXTENSION_DEV_NO_TELEMETRY) || off(process.env.DO_NOT_TRACK);
|
|
738
|
+
}
|
|
739
|
+
function randomId() {
|
|
740
|
+
return node_crypto.randomBytes(16).toString("hex");
|
|
741
|
+
}
|
|
742
|
+
function readStoredInstallIdentity() {
|
|
796
743
|
try {
|
|
797
|
-
|
|
744
|
+
const raw = node_fs.readFileSync(installIdentityPath(), "utf8");
|
|
745
|
+
const data = JSON.parse(raw);
|
|
746
|
+
if (!data || "object" != typeof data) return null;
|
|
747
|
+
if (1 !== data.version) return null;
|
|
748
|
+
const installId = String(data.installId || "").trim();
|
|
749
|
+
if (!/^[0-9a-f]{32}$/.test(installId)) return null;
|
|
750
|
+
const rotatedAt = Number(data.rotatedAt || 0);
|
|
751
|
+
if (!Number.isFinite(rotatedAt) || rotatedAt <= 0) return null;
|
|
752
|
+
return {
|
|
753
|
+
version: 1,
|
|
754
|
+
installId,
|
|
755
|
+
rotatedAt
|
|
756
|
+
};
|
|
798
757
|
} catch {
|
|
799
|
-
|
|
800
|
-
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
function writeStoredInstallIdentity(identity) {
|
|
762
|
+
const file = installIdentityPath();
|
|
763
|
+
try {
|
|
764
|
+
node_fs.mkdirSync(node_path.dirname(file), {
|
|
765
|
+
recursive: true,
|
|
766
|
+
mode: 448
|
|
767
|
+
});
|
|
768
|
+
node_fs.writeFileSync(file, JSON.stringify(identity, null, 2) + "\n", {
|
|
769
|
+
mode: 384
|
|
770
|
+
});
|
|
771
|
+
return true;
|
|
772
|
+
} catch {
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
let ephemeralInstallId = "";
|
|
777
|
+
function resolveInstallId(now = Date.now()) {
|
|
778
|
+
if (telemetryDisabled()) return "";
|
|
779
|
+
try {
|
|
780
|
+
installIdentityPath();
|
|
781
|
+
} catch {
|
|
782
|
+
return "";
|
|
783
|
+
}
|
|
784
|
+
const stored = readStoredInstallIdentity();
|
|
785
|
+
if (stored && now - stored.rotatedAt < ROTATE_AFTER_MS) return stored.installId;
|
|
786
|
+
const rotated = {
|
|
787
|
+
version: 1,
|
|
788
|
+
installId: randomId(),
|
|
789
|
+
rotatedAt: now
|
|
790
|
+
};
|
|
791
|
+
if (writeStoredInstallIdentity(rotated)) {
|
|
792
|
+
ephemeralInstallId = "";
|
|
793
|
+
return rotated.installId;
|
|
794
|
+
}
|
|
795
|
+
if (!ephemeralInstallId) ephemeralInstallId = rotated.installId;
|
|
796
|
+
return ephemeralInstallId;
|
|
797
|
+
}
|
|
798
|
+
let processSessionId = "";
|
|
799
|
+
function session_identity_sessionId() {
|
|
800
|
+
if (telemetryDisabled()) return "";
|
|
801
|
+
if (!processSessionId) processSessionId = randomId();
|
|
802
|
+
return processSessionId;
|
|
803
|
+
}
|
|
804
|
+
function identityHeaders(tool) {
|
|
805
|
+
try {
|
|
806
|
+
if (telemetryDisabled()) return {};
|
|
807
|
+
const name = String(tool || "").trim().toLowerCase();
|
|
808
|
+
if (!/^[a-z0-9_]{1,64}$/.test(name)) return {};
|
|
809
|
+
const install = resolveInstallId();
|
|
810
|
+
const session = session_identity_sessionId();
|
|
811
|
+
if (!install || !session) return {};
|
|
812
|
+
return {
|
|
813
|
+
[INSTALL_HEADER]: install,
|
|
814
|
+
[SESSION_HEADER]: session,
|
|
815
|
+
[TOOL_HEADER]: name
|
|
801
816
|
};
|
|
817
|
+
} catch {
|
|
818
|
+
return {};
|
|
802
819
|
}
|
|
803
|
-
if (!res.ok) throw new Error(`Login exchange failed (${res.status}): ${data.message || "unknown error"}`);
|
|
804
|
-
return persistTokenResponse({
|
|
805
|
-
apiBase: args.apiBase,
|
|
806
|
-
data,
|
|
807
|
-
provider: "github"
|
|
808
|
-
});
|
|
809
820
|
}
|
|
810
821
|
function _define_property(obj, key, value) {
|
|
811
822
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
@@ -874,7 +885,8 @@ class RegistryAccessTokens {
|
|
|
874
885
|
method: "POST",
|
|
875
886
|
headers: {
|
|
876
887
|
authorization: `Bearer ${token}`,
|
|
877
|
-
"content-type": "application/json"
|
|
888
|
+
"content-type": "application/json",
|
|
889
|
+
...identityHeaders("extension_registry_access")
|
|
878
890
|
},
|
|
879
891
|
body: JSON.stringify({
|
|
880
892
|
workspaceSlug: ref.workspace,
|
|
@@ -1028,7 +1040,7 @@ async function fetchRegistryJson(url, fetchImpl = fetch, options) {
|
|
|
1028
1040
|
};
|
|
1029
1041
|
const grant = await tokens.get(ref, options?.api);
|
|
1030
1042
|
if ("ok" !== grant.status) {
|
|
1031
|
-
const detail = "no-credential" === grant.status ? "This project is private. Run
|
|
1043
|
+
const detail = "no-credential" === grant.status ? "This project is private. Run extension_auth (action: login) for it, or set EXTENSION_DEV_TOKEN." : "public" === grant.status ? "The platform reports this project is public, but the registry refused the read." : grant.message;
|
|
1032
1044
|
return {
|
|
1033
1045
|
ok: false,
|
|
1034
1046
|
status: res.status,
|
|
@@ -1134,7 +1146,7 @@ function detectPackageManager(projectPath) {
|
|
|
1134
1146
|
}
|
|
1135
1147
|
const create_schema = {
|
|
1136
1148
|
name: "extension_create",
|
|
1137
|
-
description: "Create a new browser extension project from a template in the extension.dev
|
|
1149
|
+
description: "Create a new browser extension project from a template in the extension.dev catalog. extension_templates lists what is available. The scaffolder may initialize a git repository in the new project; the result's defaultsApplied block reports whether it did, along with every other decision made without being asked.",
|
|
1138
1150
|
inputSchema: {
|
|
1139
1151
|
type: "object",
|
|
1140
1152
|
properties: {
|
|
@@ -1144,12 +1156,12 @@ const create_schema = {
|
|
|
1144
1156
|
},
|
|
1145
1157
|
parentDir: {
|
|
1146
1158
|
type: "string",
|
|
1147
|
-
description: "Directory to create the project inside. Defaults to the MCP server process cwd
|
|
1159
|
+
description: "Directory to create the project inside. Defaults to the MCP server process cwd, NOT the caller's cwd, so pass it whenever you care where the project lands. Aliases: parent, into."
|
|
1148
1160
|
},
|
|
1149
1161
|
template: {
|
|
1150
1162
|
type: "string",
|
|
1151
1163
|
default: "typescript",
|
|
1152
|
-
description: "Template slug from the extension.dev
|
|
1164
|
+
description: "Template slug from the extension.dev catalog (e.g. 'react', 'ai-claude', 'content-vue'). extension_templates discovers them."
|
|
1153
1165
|
},
|
|
1154
1166
|
install: {
|
|
1155
1167
|
type: "boolean",
|
|
@@ -1248,7 +1260,7 @@ async function create_handler(args) {
|
|
|
1248
1260
|
defaultsApplied: {
|
|
1249
1261
|
parentDir: args.parentDir ? `${resolvedParent} (explicit)` : `${resolvedParent} (default: the MCP server process cwd, not yours; pass parentDir to choose)`,
|
|
1250
1262
|
...void 0 === args.template ? {
|
|
1251
|
-
template: "typescript (default;
|
|
1263
|
+
template: "typescript (default; call extension_templates to pick another, e.g. javascript for plain JS)"
|
|
1252
1264
|
} : {},
|
|
1253
1265
|
packageManager: `${packageManager} (auto-detected by the scaffolder, not asked)`,
|
|
1254
1266
|
browser: "chrome (default: extension_dev and extension_build target chrome unless you pass browser)",
|
|
@@ -1452,12 +1464,99 @@ async function getTemplateBySlug(slug) {
|
|
|
1452
1464
|
const meta = await fetchTemplatesMeta();
|
|
1453
1465
|
return meta.templates.find((t)=>t.slug === slug);
|
|
1454
1466
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1467
|
+
async function searchTemplates(args) {
|
|
1468
|
+
const templates = await listTemplates(args);
|
|
1469
|
+
const results = templates.map((t)=>({
|
|
1470
|
+
slug: t.slug,
|
|
1471
|
+
description: t.description,
|
|
1472
|
+
uiFramework: t.uiFramework,
|
|
1473
|
+
frameworkLabel: t.uiFramework || "vanilla",
|
|
1474
|
+
surfaces: t.surfaces,
|
|
1475
|
+
tags: t.tags,
|
|
1476
|
+
difficulty: t.difficulty,
|
|
1477
|
+
featured: t.featured,
|
|
1478
|
+
useCases: t.useCases,
|
|
1479
|
+
repositoryUrl: t.repositoryUrl,
|
|
1480
|
+
downloads: t.downloads
|
|
1481
|
+
}));
|
|
1482
|
+
return JSON.stringify({
|
|
1483
|
+
count: results.length,
|
|
1484
|
+
templates: results
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
async function readTemplateSource(args) {
|
|
1488
|
+
const template = await getTemplateBySlug(args.slug);
|
|
1489
|
+
if (!template) return JSON.stringify({
|
|
1490
|
+
error: `Template '${args.slug}' not found in the catalog`,
|
|
1491
|
+
hint: 'Use extension_templates with action: "list" to see available templates.'
|
|
1492
|
+
});
|
|
1493
|
+
const meta = {
|
|
1494
|
+
slug: template.slug,
|
|
1495
|
+
description: template.description,
|
|
1496
|
+
uiFramework: template.uiFramework || "vanilla",
|
|
1497
|
+
surfaces: template.surfaces,
|
|
1498
|
+
permissions: template.permissions,
|
|
1499
|
+
patternExplanation: template.patternExplanation,
|
|
1500
|
+
keyFiles: template.keyFiles,
|
|
1501
|
+
repositoryUrl: template.repositoryUrl
|
|
1502
|
+
};
|
|
1503
|
+
if (!args.files?.length) return JSON.stringify({
|
|
1504
|
+
...meta,
|
|
1505
|
+
files: template.files.map((f)=>stripTemplatePathPrefix(template.slug, f)),
|
|
1506
|
+
hint: "Pass specific file paths in the files parameter to read their contents."
|
|
1507
|
+
});
|
|
1508
|
+
const fileContents = {};
|
|
1509
|
+
const errors = [];
|
|
1510
|
+
await Promise.all(args.files.map(async (filePath)=>{
|
|
1511
|
+
const urls = await templateFileUrls(args.slug, filePath);
|
|
1512
|
+
let lastStatus = 0;
|
|
1513
|
+
for (const url of urls)try {
|
|
1514
|
+
const response = await fetch(url);
|
|
1515
|
+
if (response.ok) {
|
|
1516
|
+
fileContents[filePath] = await response.text();
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
lastStatus = response.status;
|
|
1520
|
+
} catch {}
|
|
1521
|
+
errors.push(`${filePath}: ${lastStatus || "fetch failed"}`);
|
|
1522
|
+
}));
|
|
1523
|
+
return JSON.stringify({
|
|
1524
|
+
...meta,
|
|
1525
|
+
fileContents,
|
|
1526
|
+
...errors.length ? {
|
|
1527
|
+
errors
|
|
1528
|
+
} : {}
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
const templates_schema = {
|
|
1532
|
+
name: "extension_templates",
|
|
1533
|
+
description: "Browse the extension.dev template catalog. action:'list' (default) searches and filters it and returns metadata per template; action:'source' reads one template's files by `slug`, for learning a pattern before building something similar. `framework` is the UI framework ONLY, never the language: TypeScript and JavaScript templates live under slugs ('typescript', 'content-typescript'), shadcn is a React variant ('sidebar-shadcn'), and provider AIs are tagged 'ai' ('ai-chatgpt', 'ai-claude'). Reach those with query/tags/slug.",
|
|
1458
1534
|
inputSchema: {
|
|
1459
1535
|
type: "object",
|
|
1460
1536
|
properties: {
|
|
1537
|
+
action: {
|
|
1538
|
+
type: "string",
|
|
1539
|
+
enum: [
|
|
1540
|
+
"list",
|
|
1541
|
+
"source"
|
|
1542
|
+
],
|
|
1543
|
+
default: "list"
|
|
1544
|
+
},
|
|
1545
|
+
slug: {
|
|
1546
|
+
type: "string",
|
|
1547
|
+
description: "source: which template to read (e.g. 'ai-claude', 'content-react'). Required for source."
|
|
1548
|
+
},
|
|
1549
|
+
files: {
|
|
1550
|
+
type: "array",
|
|
1551
|
+
items: {
|
|
1552
|
+
type: "string"
|
|
1553
|
+
},
|
|
1554
|
+
description: "source: paths to read (e.g. ['src/manifest.json']). Omit for the file listing."
|
|
1555
|
+
},
|
|
1556
|
+
query: {
|
|
1557
|
+
type: "string",
|
|
1558
|
+
description: "list: keyword search over slug, description, tags and useCases. Ranks by word matches, so a natural phrase works."
|
|
1559
|
+
},
|
|
1461
1560
|
surface: {
|
|
1462
1561
|
type: "string",
|
|
1463
1562
|
enum: [
|
|
@@ -1466,7 +1565,7 @@ const list_templates_schema = {
|
|
|
1466
1565
|
"newtab",
|
|
1467
1566
|
"background"
|
|
1468
1567
|
],
|
|
1469
|
-
description: "
|
|
1568
|
+
description: "list: filter by surface. For a popup/action starter use query:'action', not a surface."
|
|
1470
1569
|
},
|
|
1471
1570
|
framework: {
|
|
1472
1571
|
type: "string",
|
|
@@ -1477,46 +1576,104 @@ const list_templates_schema = {
|
|
|
1477
1576
|
"preact",
|
|
1478
1577
|
""
|
|
1479
1578
|
],
|
|
1480
|
-
description: "
|
|
1579
|
+
description: "list: UI framework filter (empty string = vanilla JS)."
|
|
1481
1580
|
},
|
|
1482
1581
|
tags: {
|
|
1483
1582
|
type: "array",
|
|
1484
1583
|
items: {
|
|
1485
1584
|
type: "string"
|
|
1486
1585
|
},
|
|
1487
|
-
description: "
|
|
1586
|
+
description: "list: filter by tags, e.g. ['ai', 'chat']."
|
|
1488
1587
|
},
|
|
1489
1588
|
featured: {
|
|
1490
1589
|
type: "boolean",
|
|
1491
|
-
description: "
|
|
1492
|
-
},
|
|
1493
|
-
query: {
|
|
1494
|
-
type: "string",
|
|
1495
|
-
description: "Keyword search across slug, description, tags, and useCases. Ranks by how many query words match, so a natural phrase works; single keywords are fine too."
|
|
1590
|
+
description: "list: only featured templates."
|
|
1496
1591
|
}
|
|
1497
|
-
}
|
|
1592
|
+
},
|
|
1593
|
+
required: []
|
|
1498
1594
|
}
|
|
1499
1595
|
};
|
|
1500
|
-
async function
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1596
|
+
async function templates_handler(args) {
|
|
1597
|
+
if ((args.action ?? "list") === "source" || !args.action && args.slug) {
|
|
1598
|
+
if (!args.slug) return JSON.stringify({
|
|
1599
|
+
ok: false,
|
|
1600
|
+
error: "action 'source' needs a slug.",
|
|
1601
|
+
hint: 'Call extension_templates with action: "list" to find one.'
|
|
1602
|
+
});
|
|
1603
|
+
return readTemplateSource({
|
|
1604
|
+
slug: args.slug,
|
|
1605
|
+
files: args.files
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
return searchTemplates({
|
|
1609
|
+
surface: args.surface,
|
|
1610
|
+
framework: args.framework,
|
|
1611
|
+
tags: args.tags,
|
|
1612
|
+
featured: args.featured,
|
|
1613
|
+
query: args.query
|
|
1518
1614
|
});
|
|
1519
1615
|
}
|
|
1616
|
+
const LAUNCHABLE_BROWSERS = [
|
|
1617
|
+
"chrome",
|
|
1618
|
+
"chromium",
|
|
1619
|
+
"edge",
|
|
1620
|
+
"brave",
|
|
1621
|
+
"opera",
|
|
1622
|
+
"vivaldi",
|
|
1623
|
+
"yandex",
|
|
1624
|
+
"firefox",
|
|
1625
|
+
"waterfox",
|
|
1626
|
+
"librewolf",
|
|
1627
|
+
"safari",
|
|
1628
|
+
"chromium-based",
|
|
1629
|
+
"gecko-based",
|
|
1630
|
+
"firefox-based",
|
|
1631
|
+
"webkit-based"
|
|
1632
|
+
];
|
|
1633
|
+
const REAL_BROWSERS = [
|
|
1634
|
+
"chrome",
|
|
1635
|
+
"chromium",
|
|
1636
|
+
"edge",
|
|
1637
|
+
"brave",
|
|
1638
|
+
"opera",
|
|
1639
|
+
"vivaldi",
|
|
1640
|
+
"yandex",
|
|
1641
|
+
"firefox",
|
|
1642
|
+
"waterfox",
|
|
1643
|
+
"librewolf",
|
|
1644
|
+
"safari"
|
|
1645
|
+
];
|
|
1646
|
+
const MANAGED_BROWSERS = [
|
|
1647
|
+
"chrome",
|
|
1648
|
+
"chromium",
|
|
1649
|
+
"edge",
|
|
1650
|
+
"firefox"
|
|
1651
|
+
];
|
|
1652
|
+
const PROJECT_PATH = {
|
|
1653
|
+
type: "string",
|
|
1654
|
+
description: "Extension project root"
|
|
1655
|
+
};
|
|
1656
|
+
const SESSION_PROJECT_PATH = {
|
|
1657
|
+
type: "string",
|
|
1658
|
+
description: "Extension project root (needs a live dev session)"
|
|
1659
|
+
};
|
|
1660
|
+
const SESSION_BROWSER = {
|
|
1661
|
+
type: "string",
|
|
1662
|
+
description: "Session browser; defaults to this project's live session"
|
|
1663
|
+
};
|
|
1664
|
+
const CALL_TIMEOUT = {
|
|
1665
|
+
type: "number",
|
|
1666
|
+
description: "Command timeout in ms (default 5000)"
|
|
1667
|
+
};
|
|
1668
|
+
const API_BASE = {
|
|
1669
|
+
type: "string",
|
|
1670
|
+
description: "Platform base URL (default EXTENSION_DEV_API_URL, else https://www.extension.dev)"
|
|
1671
|
+
};
|
|
1672
|
+
const LAUNCH_BROWSER = {
|
|
1673
|
+
type: "string",
|
|
1674
|
+
enum: LAUNCHABLE_BROWSERS,
|
|
1675
|
+
default: "chrome"
|
|
1676
|
+
};
|
|
1520
1677
|
const PINNED_CLI_VERSION = String(package_namespaceObject.El.OP ?? "latest").replace(/^[\^~]/, "");
|
|
1521
1678
|
function pinnedCliVersion() {
|
|
1522
1679
|
const override = String(process.env.EXTENSION_MCP_CLI_VERSION || "").trim();
|
|
@@ -2006,17 +2163,17 @@ function materializeCarrier(projectPath, browser) {
|
|
|
2006
2163
|
...ignored ? {
|
|
2007
2164
|
gitignored: ignored
|
|
2008
2165
|
} : {},
|
|
2009
|
-
note: "Live-preview carrier placed in ./extensions; Extension.js loads it as a companion beside your extension. Open https://
|
|
2166
|
+
note: "Live-preview carrier placed in ./extensions; Extension.js loads it as a companion beside your extension. Open https://preview.extension.dev/?session=live in the dev browser (any http://localhost origin works too) to watch the session's real-lane chrome.* trace on the Trace tab. It is a debug companion, never part of a release: extension_stop and extension_build remove it again" + (ignored ? `, and ${ignored} was added to .gitignore.` : "."),
|
|
2010
2167
|
limitations: [
|
|
2011
2168
|
"The trace shows calls a PAGE bridges to the carrier. Your extension's own chrome.* calls run directly in its contexts and never cross the carrier, so they do not appear.",
|
|
2012
2169
|
"Bridged calls run under the CARRIER's identity, not your extension's. The preview assumes a single active guest and does not namespace per-extension state, so storage, action/badge state, messaging delivery, offscreen documents and relative script paths belong to the carrier. Rows affected are badged carrier-scoped in the Trace tab.",
|
|
2013
2170
|
"Chromium-family only: Firefox has no externally_connectable channel for web pages."
|
|
2014
2171
|
],
|
|
2015
|
-
graduation: "The carrier lane is the SHARED real lane: bridged calls run as the carrier, by design (see limitations). Your guest is already loaded as ITSELF in this same session, so for its own storage, identity, badge and messaging (the isolated real thing), drive the guest directly instead of the carrier bridge: extension_storage, extension_eval and
|
|
2172
|
+
graduation: "The carrier lane is the SHARED real lane: bridged calls run as the carrier, by design (see limitations). Your guest is already loaded as ITSELF in this same session, so for its own storage, identity, badge and messaging (the isolated real thing), drive the guest directly instead of the carrier bridge: extension_storage, extension_eval and extension_dom_snapshot against this projectPath all operate on the guest as itself. Start (or replace) this session with allowControl: true (or allowEval: true) to unlock them. Use the carrier bridge for the shared real-lane TRACE; use the control verbs for the guest's OWN state.",
|
|
2016
2173
|
...carrierId ? {
|
|
2017
2174
|
bridgeProtocol: {
|
|
2018
2175
|
carrierExtensionId: carrierId,
|
|
2019
|
-
allowedOrigins: "https://
|
|
2176
|
+
allowedOrigins: "https://preview.extension.dev, https://code.extension.dev, https://themes.extension.dev, http://localhost/*, http://127.0.0.1/*",
|
|
2020
2177
|
howTo: "From a page on an allowed origin, register your guest once with a 'session' message (it declares the permissions the carrier enforces), then send 'bridge' messages to run chrome.* for real; each one streams into the Trace tab. Use the EXACT dotted wire names the bridge dispatcher accepts: storage is storage.get/set/remove/clear with the AREA AS AN ARGUMENT, NOT storage.local.get.",
|
|
2021
2178
|
example: [
|
|
2022
2179
|
`const id = '${carrierId}'`,
|
|
@@ -2136,32 +2293,8 @@ const build_schema = {
|
|
|
2136
2293
|
inputSchema: {
|
|
2137
2294
|
type: "object",
|
|
2138
2295
|
properties: {
|
|
2139
|
-
projectPath:
|
|
2140
|
-
|
|
2141
|
-
description: "Path to the extension project root"
|
|
2142
|
-
},
|
|
2143
|
-
browser: {
|
|
2144
|
-
type: "string",
|
|
2145
|
-
enum: [
|
|
2146
|
-
"chrome",
|
|
2147
|
-
"chromium",
|
|
2148
|
-
"edge",
|
|
2149
|
-
"brave",
|
|
2150
|
-
"opera",
|
|
2151
|
-
"vivaldi",
|
|
2152
|
-
"yandex",
|
|
2153
|
-
"firefox",
|
|
2154
|
-
"waterfox",
|
|
2155
|
-
"librewolf",
|
|
2156
|
-
"safari",
|
|
2157
|
-
"chromium-based",
|
|
2158
|
-
"gecko-based",
|
|
2159
|
-
"firefox-based",
|
|
2160
|
-
"webkit-based"
|
|
2161
|
-
],
|
|
2162
|
-
default: "chrome",
|
|
2163
|
-
description: "Target browser"
|
|
2164
|
-
},
|
|
2296
|
+
projectPath: PROJECT_PATH,
|
|
2297
|
+
browser: LAUNCH_BROWSER,
|
|
2165
2298
|
zip: {
|
|
2166
2299
|
type: "boolean",
|
|
2167
2300
|
default: false,
|
|
@@ -2199,7 +2332,7 @@ const build_schema = {
|
|
|
2199
2332
|
skipValidation: {
|
|
2200
2333
|
type: "boolean",
|
|
2201
2334
|
default: false,
|
|
2202
|
-
description: "Build even when extension_manifest_validate reports build-blocking errors. The build normally refuses
|
|
2335
|
+
description: "Build even when extension_manifest_validate reports build-blocking errors. The build normally refuses: a manifest error yields a broken bundle the bundler itself never flags."
|
|
2203
2336
|
}
|
|
2204
2337
|
},
|
|
2205
2338
|
required: [
|
|
@@ -2421,22 +2554,19 @@ async function build_handler(args) {
|
|
|
2421
2554
|
}
|
|
2422
2555
|
const stop_schema = {
|
|
2423
2556
|
name: "extension_stop",
|
|
2424
|
-
description: "Stop a
|
|
2557
|
+
description: "Stop a session that extension_dev or extension_start is running: terminates the server and the browser it launched, and removes the live-preview carrier if extension_dev placed one. Covers extension_start build:false too, which the registry still records as a preview session. Call it when you are done verifying so sessions do not accumulate.",
|
|
2425
2558
|
inputSchema: {
|
|
2426
2559
|
type: "object",
|
|
2427
2560
|
properties: {
|
|
2428
|
-
projectPath:
|
|
2429
|
-
type: "string",
|
|
2430
|
-
description: "Path to the extension project root"
|
|
2431
|
-
},
|
|
2561
|
+
projectPath: PROJECT_PATH,
|
|
2432
2562
|
browser: {
|
|
2433
2563
|
type: "string",
|
|
2434
|
-
description: "Browser of the session to stop
|
|
2564
|
+
description: "Browser of the session to stop. Defaults to the single live session for this project rather than assuming chrome."
|
|
2435
2565
|
},
|
|
2436
2566
|
all: {
|
|
2437
2567
|
type: "boolean",
|
|
2438
2568
|
default: false,
|
|
2439
|
-
description: "Stop every known session
|
|
2569
|
+
description: "Stop every known session across projects and browsers, found from this server's registry AND the on-disk markers earlier runs left, so it still works after an MCP restart. projectPath/browser are then ignored."
|
|
2440
2570
|
}
|
|
2441
2571
|
},
|
|
2442
2572
|
required: []
|
|
@@ -2588,7 +2718,7 @@ async function stop_handler(args) {
|
|
|
2588
2718
|
const LAUNCH_FLAG_SCHEMA = {
|
|
2589
2719
|
profile: {
|
|
2590
2720
|
type: "string",
|
|
2591
|
-
description: '
|
|
2721
|
+
description: 'Profile path, or "false" to reuse the real user profile. Omit for a throwaway one.'
|
|
2592
2722
|
},
|
|
2593
2723
|
startingUrl: {
|
|
2594
2724
|
type: "string",
|
|
@@ -2596,26 +2726,26 @@ const LAUNCH_FLAG_SCHEMA = {
|
|
|
2596
2726
|
},
|
|
2597
2727
|
chromiumBinary: {
|
|
2598
2728
|
type: "string",
|
|
2599
|
-
description: "
|
|
2729
|
+
description: "Custom Chromium-based binary (overrides browser)"
|
|
2600
2730
|
},
|
|
2601
2731
|
geckoBinary: {
|
|
2602
2732
|
type: "string",
|
|
2603
|
-
description: "
|
|
2733
|
+
description: "Custom Gecko/Firefox binary (overrides browser)"
|
|
2604
2734
|
},
|
|
2605
2735
|
host: {
|
|
2606
2736
|
type: "string",
|
|
2607
|
-
description: "
|
|
2737
|
+
description: "Bind host, default 127.0.0.1. Use 0.0.0.0 in Docker or devcontainers."
|
|
2608
2738
|
},
|
|
2609
2739
|
publicHost: {
|
|
2610
2740
|
type: "string",
|
|
2611
|
-
description: "
|
|
2741
|
+
description: "Host the browser dials for HMR and reload when it differs from the bind host"
|
|
2612
2742
|
},
|
|
2613
2743
|
extensions: {
|
|
2614
2744
|
type: "array",
|
|
2615
2745
|
items: {
|
|
2616
2746
|
type: "string"
|
|
2617
2747
|
},
|
|
2618
|
-
description: "
|
|
2748
|
+
description: "Extra extension paths or store URLs to load alongside the project"
|
|
2619
2749
|
}
|
|
2620
2750
|
};
|
|
2621
2751
|
function launchFlagArgs(args) {
|
|
@@ -2631,35 +2761,12 @@ function launchFlagArgs(args) {
|
|
|
2631
2761
|
}
|
|
2632
2762
|
const dev_schema = {
|
|
2633
2763
|
name: "extension_dev",
|
|
2634
|
-
description: "
|
|
2764
|
+
description: "Run the extension WHILE YOU EDIT IT: dev build, hot module replacement, and a browser with it loaded. The default answer to \"run my extension\". Only this tool can unlock the control channel that extension_storage/reload/open/dom_snapshot need (allowControl) and that extension_eval needs (allowEval). For the production build in a browser instead, use extension_start. Returns process info for extension_wait and extension_inspect.",
|
|
2635
2765
|
inputSchema: {
|
|
2636
2766
|
type: "object",
|
|
2637
2767
|
properties: {
|
|
2638
|
-
projectPath:
|
|
2639
|
-
|
|
2640
|
-
description: "Path to the extension project root"
|
|
2641
|
-
},
|
|
2642
|
-
browser: {
|
|
2643
|
-
type: "string",
|
|
2644
|
-
enum: [
|
|
2645
|
-
"chrome",
|
|
2646
|
-
"chromium",
|
|
2647
|
-
"edge",
|
|
2648
|
-
"brave",
|
|
2649
|
-
"opera",
|
|
2650
|
-
"vivaldi",
|
|
2651
|
-
"yandex",
|
|
2652
|
-
"firefox",
|
|
2653
|
-
"waterfox",
|
|
2654
|
-
"librewolf",
|
|
2655
|
-
"safari",
|
|
2656
|
-
"chromium-based",
|
|
2657
|
-
"gecko-based",
|
|
2658
|
-
"firefox-based",
|
|
2659
|
-
"webkit-based"
|
|
2660
|
-
],
|
|
2661
|
-
default: "chrome"
|
|
2662
|
-
},
|
|
2768
|
+
projectPath: PROJECT_PATH,
|
|
2769
|
+
browser: LAUNCH_BROWSER,
|
|
2663
2770
|
port: {
|
|
2664
2771
|
type: "number",
|
|
2665
2772
|
description: "Dev server port (0 for auto-assign)"
|
|
@@ -2667,7 +2774,7 @@ const dev_schema = {
|
|
|
2667
2774
|
noBrowser: {
|
|
2668
2775
|
type: "boolean",
|
|
2669
2776
|
default: false,
|
|
2670
|
-
description: "Start dev server without launching browser"
|
|
2777
|
+
description: "Start the dev server without launching a browser"
|
|
2671
2778
|
},
|
|
2672
2779
|
polyfill: {
|
|
2673
2780
|
type: "boolean",
|
|
@@ -2678,22 +2785,22 @@ const dev_schema = {
|
|
|
2678
2785
|
replace: {
|
|
2679
2786
|
type: "boolean",
|
|
2680
2787
|
default: false,
|
|
2681
|
-
description: "Stop
|
|
2788
|
+
description: "Stop the live session for this projectPath first, reported as replacedSession. Without it a second call is refused rather than forking: two sessions fight over one profile and the newer browser dies on the lock."
|
|
2682
2789
|
},
|
|
2683
2790
|
allowControl: {
|
|
2684
2791
|
type: "boolean",
|
|
2685
2792
|
default: false,
|
|
2686
|
-
description: "Enable the agent-bridge control channel
|
|
2793
|
+
description: "Enable the agent-bridge control channel that extension_storage/reload/open/dom_snapshot need"
|
|
2687
2794
|
},
|
|
2688
2795
|
allowEval: {
|
|
2689
2796
|
type: "boolean",
|
|
2690
2797
|
default: false,
|
|
2691
|
-
description: "Enable extension_eval (runs code in a context; writes a 0600 session token). Implies allowControl, so
|
|
2798
|
+
description: "Enable extension_eval (runs code in a context; writes a 0600 session token). Implies allowControl, so you never need to pass both."
|
|
2692
2799
|
},
|
|
2693
2800
|
carrier: {
|
|
2694
2801
|
type: "boolean",
|
|
2695
2802
|
default: false,
|
|
2696
|
-
description: "Load the bundled
|
|
2803
|
+
description: "Load the bundled Live Preview carrier beside your extension (Chromium only) so allowlisted pages (preview.extension.dev, localhost) can pair with the session and stream its real-lane chrome.* trace. Written into the auto-loaded ./extensions folder, gitignored, and removed on extension_stop or extension_build: never part of a release."
|
|
2697
2804
|
}
|
|
2698
2805
|
},
|
|
2699
2806
|
required: [
|
|
@@ -2806,11 +2913,11 @@ async function dev_handler(args) {
|
|
|
2806
2913
|
hint: `A locked profile means another session's browser still holds it: call extension_stop with this projectPath to kill that session, then start extension_dev again. If the lock survives a crash, remove ${profileDir} manually before retrying.`
|
|
2807
2914
|
});
|
|
2808
2915
|
}
|
|
2809
|
-
const controlVerbs = "storage, reload, open,
|
|
2916
|
+
const controlVerbs = "storage, reload, open, dom_snapshot";
|
|
2810
2917
|
const capabilities = {
|
|
2811
2918
|
allowControl,
|
|
2812
2919
|
allowEval: Boolean(args.allowEval),
|
|
2813
|
-
unlocked: allowControl ? args.allowEval ? `${controlVerbs}, eval` : controlVerbs : "none (read-only: logs,
|
|
2920
|
+
unlocked: allowControl ? args.allowEval ? `${controlVerbs}, eval` : controlVerbs : "none (read-only: logs, inspect, wait, doctor)"
|
|
2814
2921
|
};
|
|
2815
2922
|
const boundPort = contractBoundPort(args.projectPath, browser, spawnedAt);
|
|
2816
2923
|
if (null !== boundPort && boundPort !== args.port) registerSession({
|
|
@@ -2848,7 +2955,7 @@ async function dev_handler(args) {
|
|
|
2848
2955
|
} : {}
|
|
2849
2956
|
} : {},
|
|
2850
2957
|
capabilities,
|
|
2851
|
-
hint: args.noBrowser ? "Build-only session (noBrowser: true): no browser will launch, so no runtime will ever attach. extension_wait returns as soon as the first compile lands (compiled: true, browserAttached: false) instead of waiting out its budget; do not wait for a browser. The control verbs (storage/reload/open/
|
|
2958
|
+
hint: args.noBrowser ? "Build-only session (noBrowser: true): no browser will launch, so no runtime will ever attach. extension_wait returns as soon as the first compile lands (compiled: true, browserAttached: false) instead of waiting out its budget; do not wait for a browser. The control verbs (storage/reload/open/dom_snapshot/eval) need a live browser and will not work against this session. When you are done, call extension_stop to shut down the dev server." : "Use extension_wait to check when the extension is fully loaded, then extension_inspect to inspect the live state. " + (allowControl ? `Control channel is ON: extension_${controlVerbs.split(", ").join("/extension_")}${args.allowEval ? "/extension_eval" : ""} will work against this session.` : "Control channel is OFF: extension_storage/reload/open/dom_snapshot need allowControl: true, and extension_eval needs allowEval: true (which also implies allowControl). To unlock them, call extension_dev again with the flag you need plus replace: true (it stops this session first); a plain second call is refused so the session does not fork.") + " When you are done, call extension_stop to shut down the dev server and browser.",
|
|
2852
2959
|
earlyOutput: cleanOutput.slice(0, 500),
|
|
2853
2960
|
logPath
|
|
2854
2961
|
});
|
|
@@ -2871,39 +2978,21 @@ function denoiseEarlyOutput(raw) {
|
|
|
2871
2978
|
}
|
|
2872
2979
|
const start_schema = {
|
|
2873
2980
|
name: "extension_start",
|
|
2874
|
-
description: "
|
|
2981
|
+
description: "Run the PRODUCTION build in a browser: builds the project, then serves it and launches. No hot module replacement and no control channel, so your edits are not picked up and extension_eval/storage/reload/open/dom_snapshot cannot attach to this session. Use extension_dev while writing code; use this to check what actually ships. Pass build:false to launch on an existing dist/<browser> without rebuilding.",
|
|
2875
2982
|
inputSchema: {
|
|
2876
2983
|
type: "object",
|
|
2877
2984
|
properties: {
|
|
2878
|
-
projectPath:
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
enum: [
|
|
2885
|
-
"chrome",
|
|
2886
|
-
"chromium",
|
|
2887
|
-
"edge",
|
|
2888
|
-
"brave",
|
|
2889
|
-
"opera",
|
|
2890
|
-
"vivaldi",
|
|
2891
|
-
"yandex",
|
|
2892
|
-
"firefox",
|
|
2893
|
-
"waterfox",
|
|
2894
|
-
"librewolf",
|
|
2895
|
-
"safari",
|
|
2896
|
-
"chromium-based",
|
|
2897
|
-
"gecko-based",
|
|
2898
|
-
"firefox-based",
|
|
2899
|
-
"webkit-based"
|
|
2900
|
-
],
|
|
2901
|
-
default: "chrome"
|
|
2985
|
+
projectPath: PROJECT_PATH,
|
|
2986
|
+
browser: LAUNCH_BROWSER,
|
|
2987
|
+
build: {
|
|
2988
|
+
type: "boolean",
|
|
2989
|
+
default: true,
|
|
2990
|
+
description: "Build before serving. false serves the existing dist/<browser> as-is and fails when there is none."
|
|
2902
2991
|
},
|
|
2903
2992
|
polyfill: {
|
|
2904
2993
|
type: "boolean",
|
|
2905
2994
|
default: true,
|
|
2906
|
-
description: "Apply cross-browser polyfill"
|
|
2995
|
+
description: "Apply cross-browser polyfill (build only)"
|
|
2907
2996
|
},
|
|
2908
2997
|
port: {
|
|
2909
2998
|
type: "number",
|
|
@@ -2912,7 +3001,7 @@ const start_schema = {
|
|
|
2912
3001
|
noBrowser: {
|
|
2913
3002
|
type: "boolean",
|
|
2914
3003
|
default: false,
|
|
2915
|
-
description: "
|
|
3004
|
+
description: "Serve without launching a browser"
|
|
2916
3005
|
},
|
|
2917
3006
|
...LAUNCH_FLAG_SCHEMA
|
|
2918
3007
|
},
|
|
@@ -2923,127 +3012,15 @@ const start_schema = {
|
|
|
2923
3012
|
};
|
|
2924
3013
|
async function start_handler(args) {
|
|
2925
3014
|
const browser = args.browser ?? "chrome";
|
|
3015
|
+
const building = false !== args.build;
|
|
3016
|
+
const command = building ? "start" : "preview";
|
|
2926
3017
|
const cliArgs = [
|
|
2927
|
-
|
|
2928
|
-
args.projectPath,
|
|
2929
|
-
"--browser",
|
|
2930
|
-
browser
|
|
2931
|
-
];
|
|
2932
|
-
if (false === args.polyfill) cliArgs.push("--polyfill", "false");
|
|
2933
|
-
if (void 0 !== args.port) cliArgs.push("--port", String(args.port));
|
|
2934
|
-
if (args.noBrowser) cliArgs.push("--no-browser");
|
|
2935
|
-
cliArgs.push(...launchFlagArgs(args));
|
|
2936
|
-
const spawnedAt = Date.now();
|
|
2937
|
-
const spawned = spawnExtensionCli(cliArgs, {
|
|
2938
|
-
projectDir: args.projectPath
|
|
2939
|
-
});
|
|
2940
|
-
const { child, logPath } = spawned;
|
|
2941
|
-
const pid = child.pid;
|
|
2942
|
-
registerSession({
|
|
2943
|
-
pid,
|
|
2944
|
-
browser,
|
|
2945
|
-
projectPath: args.projectPath,
|
|
2946
|
-
command: "start"
|
|
2947
|
-
});
|
|
2948
|
-
child.on("exit", ()=>removeSession(args.projectPath, browser));
|
|
2949
|
-
await new Promise((resolve)=>setTimeout(resolve, 5000));
|
|
2950
|
-
const earlyOutput = spawned.readOutput();
|
|
2951
|
-
if (null !== child.exitCode || null !== child.signalCode) {
|
|
2952
|
-
const code = child.exitCode;
|
|
2953
|
-
const signal = child.signalCode;
|
|
2954
|
-
return JSON.stringify({
|
|
2955
|
-
ok: false,
|
|
2956
|
-
status: "exited",
|
|
2957
|
-
projectPath: args.projectPath,
|
|
2958
|
-
browser,
|
|
2959
|
-
pid,
|
|
2960
|
-
exitCode: code,
|
|
2961
|
-
signal,
|
|
2962
|
-
error: `The preview server exited during startup (${signal ? `signal ${signal}` : `exit code ${code}`}). No session is running.`,
|
|
2963
|
-
output: earlyOutput.slice(0, 2000),
|
|
2964
|
-
logPath,
|
|
2965
|
-
hint: "Read `output` above for the cause: a failed production build, a port already in use, or a missing browser binary are the common ones. extension_build will surface a build error on its own."
|
|
2966
|
-
});
|
|
2967
|
-
}
|
|
2968
|
-
const exitStamp = browserExitStamp(args.projectPath, browser, spawnedAt);
|
|
2969
|
-
if (exitStamp) return JSON.stringify({
|
|
2970
|
-
ok: false,
|
|
2971
|
-
status: "browser-exited",
|
|
2972
|
-
projectPath: args.projectPath,
|
|
2973
|
-
browser,
|
|
2974
|
-
pid,
|
|
2975
|
-
...exitStamp,
|
|
2976
|
-
error: "The preview process is running but the browser it launched has exited (the extension may have been rejected or the browser crashed). The session cannot be driven.",
|
|
2977
|
-
output: earlyOutput.slice(0, 2000),
|
|
2978
|
-
logPath,
|
|
2979
|
-
hint: "Read `output` above and extension_logs for the cause, then call extension_stop to clean up before retrying."
|
|
2980
|
-
});
|
|
2981
|
-
return JSON.stringify({
|
|
2982
|
-
ok: true,
|
|
2983
|
-
pid,
|
|
2984
|
-
browser,
|
|
2985
|
-
projectPath: args.projectPath,
|
|
2986
|
-
status: "started",
|
|
2987
|
-
hint: "Use extension_wait to check when the build and browser launch are complete. When you are done, call extension_stop to shut down the session.",
|
|
2988
|
-
earlyOutput: earlyOutput.slice(0, 500),
|
|
2989
|
-
logPath
|
|
2990
|
-
});
|
|
2991
|
-
}
|
|
2992
|
-
const preview_schema = {
|
|
2993
|
-
name: "extension_preview",
|
|
2994
|
-
description: "Preview a production-built extension in a browser. Uses dist/ output directly. The extension must be built first with extension_build.",
|
|
2995
|
-
inputSchema: {
|
|
2996
|
-
type: "object",
|
|
2997
|
-
properties: {
|
|
2998
|
-
projectPath: {
|
|
2999
|
-
type: "string",
|
|
3000
|
-
description: "Path to the extension project root"
|
|
3001
|
-
},
|
|
3002
|
-
browser: {
|
|
3003
|
-
type: "string",
|
|
3004
|
-
enum: [
|
|
3005
|
-
"chrome",
|
|
3006
|
-
"chromium",
|
|
3007
|
-
"edge",
|
|
3008
|
-
"brave",
|
|
3009
|
-
"opera",
|
|
3010
|
-
"vivaldi",
|
|
3011
|
-
"yandex",
|
|
3012
|
-
"firefox",
|
|
3013
|
-
"waterfox",
|
|
3014
|
-
"librewolf",
|
|
3015
|
-
"safari",
|
|
3016
|
-
"chromium-based",
|
|
3017
|
-
"gecko-based",
|
|
3018
|
-
"firefox-based",
|
|
3019
|
-
"webkit-based"
|
|
3020
|
-
],
|
|
3021
|
-
default: "chrome"
|
|
3022
|
-
},
|
|
3023
|
-
port: {
|
|
3024
|
-
type: "number",
|
|
3025
|
-
description: "Server port (0 for auto-assign)"
|
|
3026
|
-
},
|
|
3027
|
-
noBrowser: {
|
|
3028
|
-
type: "boolean",
|
|
3029
|
-
default: false,
|
|
3030
|
-
description: "Serve the preview without launching a browser"
|
|
3031
|
-
},
|
|
3032
|
-
...LAUNCH_FLAG_SCHEMA
|
|
3033
|
-
},
|
|
3034
|
-
required: [
|
|
3035
|
-
"projectPath"
|
|
3036
|
-
]
|
|
3037
|
-
}
|
|
3038
|
-
};
|
|
3039
|
-
async function preview_handler(args) {
|
|
3040
|
-
const browser = args.browser ?? "chrome";
|
|
3041
|
-
const cliArgs = [
|
|
3042
|
-
"preview",
|
|
3018
|
+
command,
|
|
3043
3019
|
args.projectPath,
|
|
3044
3020
|
"--browser",
|
|
3045
3021
|
browser
|
|
3046
3022
|
];
|
|
3023
|
+
if (building && false === args.polyfill) cliArgs.push("--polyfill", "false");
|
|
3047
3024
|
if (void 0 !== args.port) cliArgs.push("--port", String(args.port));
|
|
3048
3025
|
if (args.noBrowser) cliArgs.push("--no-browser");
|
|
3049
3026
|
cliArgs.push(...launchFlagArgs(args));
|
|
@@ -3057,7 +3034,7 @@ async function preview_handler(args) {
|
|
|
3057
3034
|
pid,
|
|
3058
3035
|
browser,
|
|
3059
3036
|
projectPath: args.projectPath,
|
|
3060
|
-
command
|
|
3037
|
+
command
|
|
3061
3038
|
});
|
|
3062
3039
|
child.on("exit", ()=>removeSession(args.projectPath, browser));
|
|
3063
3040
|
await new Promise((resolve)=>setTimeout(resolve, 5000));
|
|
@@ -3073,10 +3050,10 @@ async function preview_handler(args) {
|
|
|
3073
3050
|
pid,
|
|
3074
3051
|
exitCode: code,
|
|
3075
3052
|
signal,
|
|
3076
|
-
error: `The
|
|
3053
|
+
error: `The ${command} process exited during startup (${signal ? `signal ${signal}` : `exit code ${code}`}). No session is running.`,
|
|
3077
3054
|
output: earlyOutput.slice(0, 2000),
|
|
3078
3055
|
logPath,
|
|
3079
|
-
hint: "Read `output` above for the cause: a missing or broken dist/ (run extension_build first), or a missing browser binary are the common ones."
|
|
3056
|
+
hint: building ? "Read `output` above for the cause: a failed production build, a port already in use, or a missing browser binary are the common ones. extension_build will surface a build error on its own." : "Read `output` above for the cause: a missing or broken dist/ (run extension_build first, or drop build:false), or a missing browser binary are the common ones."
|
|
3080
3057
|
});
|
|
3081
3058
|
}
|
|
3082
3059
|
const exitStamp = browserExitStamp(args.projectPath, browser, spawnedAt);
|
|
@@ -3087,7 +3064,7 @@ async function preview_handler(args) {
|
|
|
3087
3064
|
browser,
|
|
3088
3065
|
pid,
|
|
3089
3066
|
...exitStamp,
|
|
3090
|
-
error:
|
|
3067
|
+
error: `The ${command} process is running but the browser it launched has exited (the extension may have been rejected or the browser crashed). The session cannot be driven.`,
|
|
3091
3068
|
output: earlyOutput.slice(0, 2000),
|
|
3092
3069
|
logPath,
|
|
3093
3070
|
hint: "Read `output` above and extension_logs for the cause, then call extension_stop to clean up before retrying."
|
|
@@ -3097,8 +3074,8 @@ async function preview_handler(args) {
|
|
|
3097
3074
|
pid,
|
|
3098
3075
|
browser,
|
|
3099
3076
|
projectPath: args.projectPath,
|
|
3100
|
-
status: "launched",
|
|
3101
|
-
hint: "Call extension_stop when you are done to close the preview browser.",
|
|
3077
|
+
status: building ? "started" : "launched",
|
|
3078
|
+
hint: building ? "Use extension_wait to check when the build and browser launch are complete. When you are done, call extension_stop to shut down the session." : "Call extension_stop when you are done to close the preview browser.",
|
|
3102
3079
|
earlyOutput: earlyOutput.slice(0, 500),
|
|
3103
3080
|
logPath
|
|
3104
3081
|
});
|
|
@@ -3716,7 +3693,7 @@ async function navigateToUrlViaBridge(projectPath, browser, url, timeout) {
|
|
|
3716
3693
|
name: "NavigateFailed",
|
|
3717
3694
|
message: `Navigation to ${url} did not produce a tab reporting that URL. The URL may not exist, or the browser refused the navigation (Firefox rejects privileged about:/chrome: URLs and other extensions' moz-extension: pages).`
|
|
3718
3695
|
},
|
|
3719
|
-
hint: "Confirm the URL, or discover open tabs with
|
|
3696
|
+
hint: "Confirm the URL, or discover open tabs with extension_dom_snapshot listTabs: true. For an extension page, the path must match the BUILT manifest."
|
|
3720
3697
|
});
|
|
3721
3698
|
return JSON.stringify({
|
|
3722
3699
|
ok: true,
|
|
@@ -3726,7 +3703,7 @@ async function navigateToUrlViaBridge(projectPath, browser, url, timeout) {
|
|
|
3726
3703
|
url: settled.url,
|
|
3727
3704
|
title: settled.title
|
|
3728
3705
|
},
|
|
3729
|
-
hint: "Inspect it with
|
|
3706
|
+
hint: "Inspect it with extension_dom_snapshot or extension_eval using url or this numeric tab id (context: 'page'/'content')."
|
|
3730
3707
|
});
|
|
3731
3708
|
}
|
|
3732
3709
|
async function resolveBridgeBaseUrl(projectPath, browser, timeout) {
|
|
@@ -3825,7 +3802,7 @@ async function navigateToUrl(projectPath, browser, url, timeout) {
|
|
|
3825
3802
|
name: "NavigateFailed",
|
|
3826
3803
|
message: `Navigation to ${url} did not produce a live page target. ${isExtensionPage ? "The URL may not exist in the extension bundle, or Chrome refused the navigation." : "The page may have failed to load, or the browser refused the navigation."}`
|
|
3827
3804
|
},
|
|
3828
|
-
hint: isExtensionPage ? "Confirm the path exists in the built dist (extension_build /
|
|
3805
|
+
hint: isExtensionPage ? "Confirm the path exists in the built dist (extension_build / extension_analyze list entrypoints). For an extension page, the path must match the BUILT manifest, which may differ from your source layout." : "Confirm the URL loads in a normal browser and that the dev session's browser has network access. Nothing about your extension bundle is implicated in a failed http(s) navigation."
|
|
3829
3806
|
});
|
|
3830
3807
|
}
|
|
3831
3808
|
return JSON.stringify({
|
|
@@ -3845,7 +3822,7 @@ async function navigateToUrl(projectPath, browser, url, timeout) {
|
|
|
3845
3822
|
title: settled.title,
|
|
3846
3823
|
url: settled.url
|
|
3847
3824
|
},
|
|
3848
|
-
hint: "Inspect it with
|
|
3825
|
+
hint: "Inspect it with extension_dom_snapshot or extension_inspect using url (context: 'page'), they resolve the tab themselves. `target.targetId` is a CDP target id, NOT a chrome.tabs id: do not pass it as `tab`. If you need a numeric tab id, call extension_dom_snapshot with listTabs: true."
|
|
3849
3826
|
});
|
|
3850
3827
|
} catch (e) {
|
|
3851
3828
|
return JSON.stringify({
|
|
@@ -4086,7 +4063,7 @@ async function openSurfaceAsTab(projectPath, browser, surface) {
|
|
|
4086
4063
|
popupBounds = await applyPopupBounds(projectPath, browser, parsed.target.targetId);
|
|
4087
4064
|
if (popupBounds) parsed.renderedAsTab.popupBounds = popupBounds;
|
|
4088
4065
|
}
|
|
4089
|
-
parsed.hint = `Rendered the ${surface} document in a real tab, which is how you inspect a surface headlessly. ` + (popupBounds ? `The window was resized to the popup's content size (${popupBounds.width}x${popupBounds.height}${popupBounds.clamped ? ", clamped to Chrome's 25x25-800x600 popup bounds" : ""}), approximating real popup rendering. This resizes the WHOLE browser window for the session. It is the same page with the same extension APIs, but window.close() closes the tab. ` : "It is the same page with the same extension APIs, but it is NOT hosted in a popup window: no popup sizing, and window.close() closes the tab. ") + `Inspect it with
|
|
4066
|
+
parsed.hint = `Rendered the ${surface} document in a real tab, which is how you inspect a surface headlessly. ` + (popupBounds ? `The window was resized to the popup's content size (${popupBounds.width}x${popupBounds.height}${popupBounds.clamped ? ", clamped to Chrome's 25x25-800x600 popup bounds" : ""}), approximating real popup rendering. This resizes the WHOLE browser window for the session. It is the same page with the same extension APIs, but window.close() closes the tab. ` : "It is the same page with the same extension APIs, but it is NOT hosted in a popup window: no popup sizing, and window.close() closes the tab. ") + `Inspect it with extension_dom_snapshot context: '${surface}' (include: ['html']), or extension_inspect with this url. ` + "Do NOT pass this extension-page url to extension_dom_snapshot or extension_eval as a tab target: script injection cannot reach extension pages, only the surface context or CDP can.";
|
|
4090
4067
|
return JSON.stringify(parsed);
|
|
4091
4068
|
}
|
|
4092
4069
|
} catch {}
|
|
@@ -4127,14 +4104,11 @@ async function confirmSurfaceTarget(projectPath, browser, surface, raw) {
|
|
|
4127
4104
|
}
|
|
4128
4105
|
const open_schema = {
|
|
4129
4106
|
name: "extension_open",
|
|
4130
|
-
description: "Open an extension surface or replay an event in a running session.
|
|
4107
|
+
description: "Open an extension surface or replay an event in a running session. popup/options/sidebar open UI surfaces; newtab/history/bookmarks open the matching chrome_url_overrides page in a tab. 'action' triggers the toolbar action, opening its popup or replaying chrome.action.onClicked when there is none; 'command' replays a chrome.commands.onCommand shortcut (pass `name`). NOTE: action/command replay invokes 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). Requires the session to have been started with allowControl: true (extension_dev).",
|
|
4131
4108
|
inputSchema: {
|
|
4132
4109
|
type: "object",
|
|
4133
4110
|
properties: {
|
|
4134
|
-
projectPath:
|
|
4135
|
-
type: "string",
|
|
4136
|
-
description: "Path to the extension project root (must have an active dev session)"
|
|
4137
|
-
},
|
|
4111
|
+
projectPath: SESSION_PROJECT_PATH,
|
|
4138
4112
|
surface: {
|
|
4139
4113
|
type: "string",
|
|
4140
4114
|
enum: [
|
|
@@ -4147,7 +4121,7 @@ const open_schema = {
|
|
|
4147
4121
|
"action",
|
|
4148
4122
|
"command"
|
|
4149
4123
|
],
|
|
4150
|
-
description: "Which surface to open or event to replay.
|
|
4124
|
+
description: "Which surface to open or event to replay."
|
|
4151
4125
|
},
|
|
4152
4126
|
name: {
|
|
4153
4127
|
type: "string",
|
|
@@ -4155,21 +4129,15 @@ const open_schema = {
|
|
|
4155
4129
|
},
|
|
4156
4130
|
url: {
|
|
4157
4131
|
type: "string",
|
|
4158
|
-
description: "Navigate a real tab
|
|
4132
|
+
description: "Navigate a real tab here instead of opening a surface (Firefox needs allowEval: true). Use for content-script test pages, or a surface as a page: chrome-extension://<id>/popup.html."
|
|
4159
4133
|
},
|
|
4160
4134
|
asTab: {
|
|
4161
4135
|
type: "boolean",
|
|
4162
4136
|
default: false,
|
|
4163
|
-
description: "
|
|
4164
|
-
},
|
|
4165
|
-
browser: {
|
|
4166
|
-
type: "string",
|
|
4167
|
-
description: "Browser session to target. Defaults to the active dev session's browser for this project."
|
|
4137
|
+
description: "popup/options/sidebar: render the surface's document in a real tab instead of a popup window. This is how you inspect a surface HEADLESSLY, and it is applied automatically when a headless session refuses to open one. Same page and APIs, but no popup sizing and window.close() closes the tab."
|
|
4168
4138
|
},
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
description: "Command timeout in ms (default 5000)"
|
|
4172
|
-
}
|
|
4139
|
+
browser: SESSION_BROWSER,
|
|
4140
|
+
timeout: CALL_TIMEOUT
|
|
4173
4141
|
},
|
|
4174
4142
|
required: [
|
|
4175
4143
|
"projectPath"
|
|
@@ -4284,7 +4252,8 @@ async function publish(options = {}) {
|
|
|
4284
4252
|
method: "POST",
|
|
4285
4253
|
headers: {
|
|
4286
4254
|
authorization: `Bearer ${token}`,
|
|
4287
|
-
"content-type": "application/json"
|
|
4255
|
+
"content-type": "application/json",
|
|
4256
|
+
...identityHeaders("extension_publish")
|
|
4288
4257
|
},
|
|
4289
4258
|
body: JSON.stringify(body)
|
|
4290
4259
|
});
|
|
@@ -4360,7 +4329,7 @@ async function uploadPreview(options) {
|
|
|
4360
4329
|
ok: false,
|
|
4361
4330
|
error: {
|
|
4362
4331
|
name: "PreviewAuthError",
|
|
4363
|
-
message: "No token. Run
|
|
4332
|
+
message: "No token. Run extension_auth (action: login), or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard)."
|
|
4364
4333
|
}
|
|
4365
4334
|
};
|
|
4366
4335
|
const apiCheck = safeApiBase(resolveApiBase(options.api));
|
|
@@ -4414,7 +4383,8 @@ async function uploadPreview(options) {
|
|
|
4414
4383
|
method: "POST",
|
|
4415
4384
|
headers: {
|
|
4416
4385
|
authorization: `Bearer ${token}`,
|
|
4417
|
-
"content-type": "application/json"
|
|
4386
|
+
"content-type": "application/json",
|
|
4387
|
+
...identityHeaders("extension_preview_web")
|
|
4418
4388
|
},
|
|
4419
4389
|
body: JSON.stringify({
|
|
4420
4390
|
kind: "dist",
|
|
@@ -4573,23 +4543,13 @@ function recordSharedPreview(projectPath, entry) {
|
|
|
4573
4543
|
} : {}
|
|
4574
4544
|
};
|
|
4575
4545
|
}
|
|
4576
|
-
const DEFAULT_INSPECT_URL = "http://localhost:3106";
|
|
4577
4546
|
const DEFAULT_PREVIEW_DEV_URL = "http://localhost:3110";
|
|
4578
|
-
const
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
label: "preview.extension.dev"
|
|
4585
|
-
},
|
|
4586
|
-
inspect: {
|
|
4587
|
-
defaultOrigin: DEFAULT_INSPECT_URL,
|
|
4588
|
-
scheme: (encoded)=>`inspect://path/${encoded}`,
|
|
4589
|
-
fetchPath: "/__inspect/fetch",
|
|
4590
|
-
devCommand: "pnpm --filter inspect.extension.dev dev",
|
|
4591
|
-
label: "inspect.extension.dev"
|
|
4592
|
-
}
|
|
4547
|
+
const SURFACE = {
|
|
4548
|
+
defaultOrigin: DEFAULT_PREVIEW_DEV_URL,
|
|
4549
|
+
scheme: (encoded)=>`preview://build/${encoded}`,
|
|
4550
|
+
fetchPath: "/__preview/fetch",
|
|
4551
|
+
devCommand: "pnpm --filter preview.extension.dev dev",
|
|
4552
|
+
label: "preview.extension.dev"
|
|
4593
4553
|
};
|
|
4594
4554
|
async function buildShare(projectPath, distDir, manifest, browser) {
|
|
4595
4555
|
const result = await uploadPreview({
|
|
@@ -4606,7 +4566,7 @@ async function buildShare(projectPath, distDir, manifest, browser) {
|
|
|
4606
4566
|
errorName: result.error.name,
|
|
4607
4567
|
reason: result.error.message,
|
|
4608
4568
|
...isAuth ? {
|
|
4609
|
-
loginHint: "Run
|
|
4569
|
+
loginHint: "Run extension_auth (action: login), or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard)."
|
|
4610
4570
|
} : {}
|
|
4611
4571
|
};
|
|
4612
4572
|
}
|
|
@@ -4674,89 +4634,49 @@ function detectSurfaces(manifest) {
|
|
|
4674
4634
|
}
|
|
4675
4635
|
const preview_web_schema = {
|
|
4676
4636
|
name: "extension_preview_web",
|
|
4677
|
-
description: "Preview an in-progress extension in the web emulator
|
|
4637
|
+
description: "Preview an in-progress extension in the web emulator, with no real browser. Builds the project (unless build:false), points preview.extension.dev at dist/<browser> over the dev-only preview://build scheme, and returns a deep link plus a loadability check. This is the author's door for a LOCAL build: it renders your build and carries the Emulated/Real lane toggle and the Trace tab, but the deep link only resolves on this machine. To reach anyone who is not at this machine, pass share:true and hand out the public link it returns. extension_shares lists and revokes every link shared this way, so one does not vanish with this response.",
|
|
4678
4638
|
inputSchema: {
|
|
4679
4639
|
type: "object",
|
|
4680
4640
|
properties: {
|
|
4681
|
-
projectPath:
|
|
4682
|
-
type: "string",
|
|
4683
|
-
description: "Path to the extension project root"
|
|
4684
|
-
},
|
|
4641
|
+
projectPath: PROJECT_PATH,
|
|
4685
4642
|
browser: {
|
|
4686
4643
|
type: "string",
|
|
4687
|
-
enum:
|
|
4688
|
-
"chrome",
|
|
4689
|
-
"chromium",
|
|
4690
|
-
"edge",
|
|
4691
|
-
"brave",
|
|
4692
|
-
"opera",
|
|
4693
|
-
"vivaldi",
|
|
4694
|
-
"yandex",
|
|
4695
|
-
"firefox",
|
|
4696
|
-
"waterfox",
|
|
4697
|
-
"librewolf",
|
|
4698
|
-
"safari"
|
|
4699
|
-
],
|
|
4644
|
+
enum: REAL_BROWSERS,
|
|
4700
4645
|
default: "chrome",
|
|
4701
|
-
description: "Which dist/<browser> output to preview.
|
|
4646
|
+
description: "Which dist/<browser> output to preview. The emulator renders it as mocked Chrome either way."
|
|
4702
4647
|
},
|
|
4703
4648
|
build: {
|
|
4704
4649
|
type: "boolean",
|
|
4705
4650
|
default: true,
|
|
4706
|
-
description: "Build
|
|
4651
|
+
description: "Build first. false previews the existing dist/<browser> as-is."
|
|
4707
4652
|
},
|
|
4708
4653
|
distPath: {
|
|
4709
4654
|
type: "string",
|
|
4710
|
-
description: "Preview this built directory
|
|
4711
|
-
},
|
|
4712
|
-
surface: {
|
|
4713
|
-
type: "string",
|
|
4714
|
-
enum: [
|
|
4715
|
-
"preview",
|
|
4716
|
-
"inspect"
|
|
4717
|
-
],
|
|
4718
|
-
default: "preview",
|
|
4719
|
-
description: "Which front door renders the build. \"preview\" (default) is preview.extension.dev, the author's door: your own in-progress build, with the Emulated/Real lane toggle and the Trace tab. \"inspect\" is inspect.extension.dev, the evaluator's door."
|
|
4655
|
+
description: "Preview this built directory instead of dist/<browser> under projectPath. Implies build:false."
|
|
4720
4656
|
},
|
|
4721
4657
|
hostUrl: {
|
|
4722
4658
|
type: "string",
|
|
4723
|
-
description: "Origin of the running dev server
|
|
4724
|
-
},
|
|
4725
|
-
inspectUrl: {
|
|
4726
|
-
type: "string",
|
|
4727
|
-
description: "Deprecated alias for hostUrl, kept for callers written before the preview surface existed. Only consulted when surface is \"inspect\"."
|
|
4659
|
+
description: "Origin of the running preview.extension.dev dev server (default http://localhost:3110)."
|
|
4728
4660
|
},
|
|
4729
4661
|
probe: {
|
|
4730
4662
|
type: "boolean",
|
|
4731
4663
|
default: true,
|
|
4732
|
-
description: "
|
|
4664
|
+
description: "Fetch the surface's dev middleware first to confirm the artifact loads."
|
|
4733
4665
|
},
|
|
4734
4666
|
open: {
|
|
4735
4667
|
type: "boolean",
|
|
4736
4668
|
default: false,
|
|
4737
|
-
description: "
|
|
4669
|
+
description: "Also open the deep link in a running session's browser, in a focus-safe background tab. Needs a live extension_dev/extension_start session."
|
|
4738
4670
|
},
|
|
4739
4671
|
openIn: {
|
|
4740
4672
|
type: "string",
|
|
4741
|
-
enum:
|
|
4742
|
-
|
|
4743
|
-
"chromium",
|
|
4744
|
-
"edge",
|
|
4745
|
-
"brave",
|
|
4746
|
-
"opera",
|
|
4747
|
-
"vivaldi",
|
|
4748
|
-
"yandex",
|
|
4749
|
-
"firefox",
|
|
4750
|
-
"waterfox",
|
|
4751
|
-
"librewolf",
|
|
4752
|
-
"safari"
|
|
4753
|
-
],
|
|
4754
|
-
description: "Which running dev session's browser to open the preview in. Defaults to the `browser` value."
|
|
4673
|
+
enum: REAL_BROWSERS,
|
|
4674
|
+
description: "Which session's browser to open it in. Defaults to `browser`."
|
|
4755
4675
|
},
|
|
4756
4676
|
share: {
|
|
4757
4677
|
type: "boolean",
|
|
4758
4678
|
default: false,
|
|
4759
|
-
description: "Upload the built dist and return a public link (share.previewUrl) that renders those exact bytes
|
|
4679
|
+
description: "Upload the built dist and return a public link (share.previewUrl) that renders those exact bytes for anyone: no install, sign-in or dev server. It also serves the build as a zip (share.zipUrl), so sharing hands over the code. Needs a token scoped to an extension.dev project (extension_auth or EXTENSION_DEV_TOKEN); without one you get a login hint and the local preview still succeeds. Live until share.expiresAt; DELETE share.revokeUrl to kill it sooner. Revocation is permanent and re-sharing mints a different link, so each share is also appended to the project's gitignored .extension.dev/shared-previews.json."
|
|
4760
4680
|
}
|
|
4761
4681
|
},
|
|
4762
4682
|
required: [
|
|
@@ -4766,9 +4686,7 @@ const preview_web_schema = {
|
|
|
4766
4686
|
};
|
|
4767
4687
|
async function preview_web_handler(args) {
|
|
4768
4688
|
const browser = args.browser ?? "chrome";
|
|
4769
|
-
const
|
|
4770
|
-
const surface = SURFACES[surfaceKey];
|
|
4771
|
-
const hostBase = (args.hostUrl ?? ("inspect" === surfaceKey ? args.inspectUrl : void 0) ?? surface.defaultOrigin).replace(/\/+$/, "");
|
|
4689
|
+
const hostBase = (args.hostUrl ?? SURFACE.defaultOrigin).replace(/\/+$/, "");
|
|
4772
4690
|
const shouldBuild = args.distPath ? false : false !== args.build;
|
|
4773
4691
|
let buildResult = null;
|
|
4774
4692
|
if (shouldBuild) {
|
|
@@ -4811,12 +4729,11 @@ async function preview_web_handler(args) {
|
|
|
4811
4729
|
});
|
|
4812
4730
|
}
|
|
4813
4731
|
const encoded = Buffer.from(distDir).toString("base64url");
|
|
4814
|
-
const internalUrl =
|
|
4732
|
+
const internalUrl = SURFACE.scheme(encoded);
|
|
4815
4733
|
const deepLink = `${hostBase}/?url=${encodeURIComponent(internalUrl)}`;
|
|
4816
4734
|
const result = {
|
|
4817
4735
|
ok: true,
|
|
4818
4736
|
deepLink,
|
|
4819
|
-
surface: surfaceKey,
|
|
4820
4737
|
distDir,
|
|
4821
4738
|
manifest: {
|
|
4822
4739
|
name: manifest.name ?? node_path.basename(distDir),
|
|
@@ -4829,7 +4746,7 @@ async function preview_web_handler(args) {
|
|
|
4829
4746
|
} : {
|
|
4830
4747
|
built: false
|
|
4831
4748
|
},
|
|
4832
|
-
hint: `Open deepLink in a browser to see the extension render in ${
|
|
4749
|
+
hint: `Open deepLink in a browser to see the extension render in ${SURFACE.label}'s emulator. It must be running (${SURFACE.devCommand}). Once it renders, the Trace tab shows every chrome.* call it makes, and the lane toggle switches between the emulated backend and a real carrier-equipped browser.`
|
|
4833
4750
|
};
|
|
4834
4751
|
if (args.open) {
|
|
4835
4752
|
const sessionBrowser = args.openIn ?? browser;
|
|
@@ -4849,7 +4766,7 @@ async function preview_web_handler(args) {
|
|
|
4849
4766
|
}
|
|
4850
4767
|
if (args.share) result.share = await buildShare(args.projectPath, distDir, manifest, browser);
|
|
4851
4768
|
if (false === args.probe) return JSON.stringify(result);
|
|
4852
|
-
const probeUrl = `${hostBase}${
|
|
4769
|
+
const probeUrl = `${hostBase}${SURFACE.fetchPath}?url=${encodeURIComponent(internalUrl)}`;
|
|
4853
4770
|
try {
|
|
4854
4771
|
const res = await fetch(probeUrl, {
|
|
4855
4772
|
headers: {
|
|
@@ -4864,7 +4781,7 @@ async function preview_web_handler(args) {
|
|
|
4864
4781
|
probe: {
|
|
4865
4782
|
status: res.status,
|
|
4866
4783
|
contentType,
|
|
4867
|
-
note: `${
|
|
4784
|
+
note: `${SURFACE.label} answered but not with a preview payload. On the deployed host ${SURFACE.fetchPath} does not exist (dev-only); run a local dev server (${SURFACE.devCommand}) to use web preview.`
|
|
4868
4785
|
}
|
|
4869
4786
|
});
|
|
4870
4787
|
const payload = await res.json();
|
|
@@ -4886,7 +4803,7 @@ async function preview_web_handler(args) {
|
|
|
4886
4803
|
previewLoadable: false,
|
|
4887
4804
|
probe: {
|
|
4888
4805
|
error: err instanceof Error ? err.message : String(err),
|
|
4889
|
-
note: `Could not reach ${
|
|
4806
|
+
note: `Could not reach ${SURFACE.label} at ${hostBase}. Start it with '${SURFACE.devCommand}', then open deepLink.`
|
|
4890
4807
|
}
|
|
4891
4808
|
});
|
|
4892
4809
|
}
|
|
@@ -4920,7 +4837,7 @@ function authError(name) {
|
|
|
4920
4837
|
ok: false,
|
|
4921
4838
|
error: {
|
|
4922
4839
|
name,
|
|
4923
|
-
message: "No token. Run
|
|
4840
|
+
message: "No token. Run extension_auth (action: login), or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard)."
|
|
4924
4841
|
}
|
|
4925
4842
|
};
|
|
4926
4843
|
}
|
|
@@ -4954,7 +4871,8 @@ async function listArtifacts(options = {}) {
|
|
|
4954
4871
|
res = await doFetch(url.toString(), {
|
|
4955
4872
|
headers: {
|
|
4956
4873
|
authorization: `Bearer ${token}`,
|
|
4957
|
-
accept: "application/json"
|
|
4874
|
+
accept: "application/json",
|
|
4875
|
+
...identityHeaders("extension_shares")
|
|
4958
4876
|
}
|
|
4959
4877
|
});
|
|
4960
4878
|
} catch (err) {
|
|
@@ -5009,7 +4927,8 @@ async function revokeArtifact(options) {
|
|
|
5009
4927
|
method: "DELETE",
|
|
5010
4928
|
headers: {
|
|
5011
4929
|
authorization: `Bearer ${token}`,
|
|
5012
|
-
accept: "application/json"
|
|
4930
|
+
accept: "application/json",
|
|
4931
|
+
...identityHeaders("extension_shares")
|
|
5013
4932
|
}
|
|
5014
4933
|
});
|
|
5015
4934
|
} catch (err) {
|
|
@@ -5050,10 +4969,10 @@ async function revokeArtifact(options) {
|
|
|
5050
4969
|
}
|
|
5051
4970
|
};
|
|
5052
4971
|
}
|
|
5053
|
-
const LOGIN_HINT = "Run
|
|
4972
|
+
const LOGIN_HINT = "Run extension_auth (action: login), or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard).";
|
|
5054
4973
|
const shares_schema = {
|
|
5055
4974
|
name: "extension_shares",
|
|
5056
|
-
description: "List and revoke the public preview links this token has shared (
|
|
4975
|
+
description: "List and revoke the public preview links this token has shared (what extension_preview_web share:true hands out). action:'list' (default) returns every artifact the logged-in project owns with its artifactId, name, version, live/dead state, createdAt, expiresAt, revokedAt, size, previewUrl, zipUrl and revokeUrl, so a link whose response you lost is findable again. Each row carries owner and sharedBy as the platform returned them plus an attribution block: attribution.ownership is 'project' when the workspace holds the share and any member can revoke it, 'personal' when one person holds it alone, 'unknown' when no owner was disclosed; attribution.credit names the publisher for credit only, never access, and reads 'CLI token <id>' or 'not recorded' when no person can be named. action:'revoke' kills one by artifactId or by pasting any of its URLs, and is PERMANENT. Pass projectPath to reconcile against the project's own append-only .extension.dev/shared-previews.json (read-only, never rewritten): a share made on another machine shows as remoteOnly, a record with no live artifact as localOnly. Needs the same token as sharing (extension_auth or EXTENSION_DEV_TOKEN); without one, listing still returns the local record with a login hint.",
|
|
5057
4976
|
inputSchema: {
|
|
5058
4977
|
type: "object",
|
|
5059
4978
|
properties: {
|
|
@@ -5091,10 +5010,7 @@ const shares_schema = {
|
|
|
5091
5010
|
type: "number",
|
|
5092
5011
|
description: "How many shares to return, 1 to 200 (platform default 100). A cut list comes back with truncated:true."
|
|
5093
5012
|
},
|
|
5094
|
-
api:
|
|
5095
|
-
type: "string",
|
|
5096
|
-
description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)."
|
|
5097
|
-
}
|
|
5013
|
+
api: API_BASE
|
|
5098
5014
|
},
|
|
5099
5015
|
required: []
|
|
5100
5016
|
}
|
|
@@ -5343,73 +5259,6 @@ async function shares_handler(args) {
|
|
|
5343
5259
|
} : {}
|
|
5344
5260
|
});
|
|
5345
5261
|
}
|
|
5346
|
-
const get_template_source_schema = {
|
|
5347
|
-
name: "extension_get_template_source",
|
|
5348
|
-
description: "Read source files from a template in the extension.dev template catalog. Use this to learn implementation patterns before building something similar.",
|
|
5349
|
-
inputSchema: {
|
|
5350
|
-
type: "object",
|
|
5351
|
-
properties: {
|
|
5352
|
-
slug: {
|
|
5353
|
-
type: "string",
|
|
5354
|
-
description: "Template slug (e.g. 'ai-claude', 'content-react')"
|
|
5355
|
-
},
|
|
5356
|
-
files: {
|
|
5357
|
-
type: "array",
|
|
5358
|
-
items: {
|
|
5359
|
-
type: "string"
|
|
5360
|
-
},
|
|
5361
|
-
description: "Specific files to read (e.g. ['src/manifest.json', 'src/background.ts']). If omitted, returns the file listing from templates-meta.json."
|
|
5362
|
-
}
|
|
5363
|
-
},
|
|
5364
|
-
required: [
|
|
5365
|
-
"slug"
|
|
5366
|
-
]
|
|
5367
|
-
}
|
|
5368
|
-
};
|
|
5369
|
-
async function get_template_source_handler(args) {
|
|
5370
|
-
const template = await getTemplateBySlug(args.slug);
|
|
5371
|
-
if (!template) return JSON.stringify({
|
|
5372
|
-
error: `Template '${args.slug}' not found in the catalog`,
|
|
5373
|
-
hint: "Use extension_list_templates to see available templates."
|
|
5374
|
-
});
|
|
5375
|
-
const meta = {
|
|
5376
|
-
slug: template.slug,
|
|
5377
|
-
description: template.description,
|
|
5378
|
-
uiFramework: template.uiFramework || "vanilla",
|
|
5379
|
-
surfaces: template.surfaces,
|
|
5380
|
-
permissions: template.permissions,
|
|
5381
|
-
patternExplanation: template.patternExplanation,
|
|
5382
|
-
keyFiles: template.keyFiles,
|
|
5383
|
-
repositoryUrl: template.repositoryUrl
|
|
5384
|
-
};
|
|
5385
|
-
if (!args.files?.length) return JSON.stringify({
|
|
5386
|
-
...meta,
|
|
5387
|
-
files: template.files.map((f)=>stripTemplatePathPrefix(template.slug, f)),
|
|
5388
|
-
hint: "Pass specific file paths in the files parameter to read their contents."
|
|
5389
|
-
});
|
|
5390
|
-
const fileContents = {};
|
|
5391
|
-
const errors = [];
|
|
5392
|
-
await Promise.all(args.files.map(async (filePath)=>{
|
|
5393
|
-
const urls = await templateFileUrls(args.slug, filePath);
|
|
5394
|
-
let lastStatus = 0;
|
|
5395
|
-
for (const url of urls)try {
|
|
5396
|
-
const response = await fetch(url);
|
|
5397
|
-
if (response.ok) {
|
|
5398
|
-
fileContents[filePath] = await response.text();
|
|
5399
|
-
return;
|
|
5400
|
-
}
|
|
5401
|
-
lastStatus = response.status;
|
|
5402
|
-
} catch {}
|
|
5403
|
-
errors.push(`${filePath}: ${lastStatus || "fetch failed"}`);
|
|
5404
|
-
}));
|
|
5405
|
-
return JSON.stringify({
|
|
5406
|
-
...meta,
|
|
5407
|
-
fileContents,
|
|
5408
|
-
...errors.length ? {
|
|
5409
|
-
errors
|
|
5410
|
-
} : {}
|
|
5411
|
-
});
|
|
5412
|
-
}
|
|
5413
5262
|
const CHROME_DESKTOP_ONLY_KEYS = [
|
|
5414
5263
|
"file_browser_handlers",
|
|
5415
5264
|
"file_system_provider_capabilities",
|
|
@@ -6457,7 +6306,7 @@ function resolveChromeTheme(theme) {
|
|
|
6457
6306
|
}
|
|
6458
6307
|
const theme_verify_schema = {
|
|
6459
6308
|
name: "extension_theme_verify",
|
|
6460
|
-
description: "Verify a Chrome theme manifest before it ships. Settles the four-leg WYSIWYG contract (app-shows == manifest-says == chrome-paints, plus chrome-accepts) as far as is possible headless: it derives every color current Chrome would paint from the manifest (the transcribed Chromium resolver) and
|
|
6309
|
+
description: "Verify a Chrome theme manifest before it ships. Settles the four-leg WYSIWYG contract (app-shows == manifest-says == chrome-paints, plus chrome-accepts) as far as is possible headless: it derives every color current Chrome would paint from the manifest (the transcribed Chromium resolver) and classifies any problem as D1 fabrication, D3 parity gap, or D4 acceptance gap (keys Chrome silently discards: dead legacy, incognito, unknown, out-of-range). Verification only: it never authors or mutates a theme. The app-rendered and real-pixel legs need a browser and come back as needsAttended pointing at the assert:theme and install-parity harnesses, never as passed.",
|
|
6461
6310
|
inputSchema: {
|
|
6462
6311
|
type: "object",
|
|
6463
6312
|
properties: {
|
|
@@ -6720,20 +6569,17 @@ async function theme_verify_handler(args) {
|
|
|
6720
6569
|
attended
|
|
6721
6570
|
});
|
|
6722
6571
|
}
|
|
6723
|
-
const
|
|
6724
|
-
name: "
|
|
6725
|
-
description: "
|
|
6572
|
+
const analyze_schema = {
|
|
6573
|
+
name: "extension_analyze",
|
|
6574
|
+
description: "Analyze a BUILT extension on disk: file sizes, declared entry points, permissions, bundle composition, and store-readiness checks. Static only, reads dist/<browser> from the filesystem and never touches a browser. The extension must be built first (extension_build). For a RUNNING extension's live DOM and console use extension_inspect.",
|
|
6726
6575
|
inputSchema: {
|
|
6727
6576
|
type: "object",
|
|
6728
6577
|
properties: {
|
|
6729
|
-
projectPath:
|
|
6730
|
-
type: "string",
|
|
6731
|
-
description: "Path to the extension project root"
|
|
6732
|
-
},
|
|
6578
|
+
projectPath: PROJECT_PATH,
|
|
6733
6579
|
browser: {
|
|
6734
6580
|
type: "string",
|
|
6735
6581
|
default: "chrome",
|
|
6736
|
-
description: "Browser build to
|
|
6582
|
+
description: "Browser build to analyze"
|
|
6737
6583
|
},
|
|
6738
6584
|
format: {
|
|
6739
6585
|
type: "string",
|
|
@@ -6814,7 +6660,7 @@ function formatBytes(bytes) {
|
|
|
6814
6660
|
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
6815
6661
|
return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
6816
6662
|
}
|
|
6817
|
-
async function
|
|
6663
|
+
async function analyze_handler(args) {
|
|
6818
6664
|
const browser = args.browser ?? "chrome";
|
|
6819
6665
|
const distPath = node_path.resolve(args.projectPath, "dist", browser);
|
|
6820
6666
|
if (!node_fs.existsSync(distPath)) return JSON.stringify({
|
|
@@ -7392,26 +7238,23 @@ async function inspectViaBridge(args, browser, include, maxBytes) {
|
|
|
7392
7238
|
if (notes.length) result.notes = notes;
|
|
7393
7239
|
return JSON.stringify(result);
|
|
7394
7240
|
}
|
|
7395
|
-
const
|
|
7396
|
-
name: "
|
|
7397
|
-
description: "
|
|
7241
|
+
const inspect_schema = {
|
|
7242
|
+
name: "extension_inspect",
|
|
7243
|
+
description: "DEEP inspection of a RUNNING extension over the browser's debugger protocol: full HTML including shadow DOM, DOM structure, content-script injection, console messages, and CSS selector queries (`probe`). The ONLY tool that pierces CLOSED shadow roots (`deepDom`), runs selector probes, and NAVIGATES a tab to `url` before reading it. It reads a web or override page and picks the first inspectable target (or the first whose url contains `url`); it cannot address an extension surface by name and takes no chrome.tabs id. To choose WHICH tab or which OPEN surface (popup/options/sidebar/devtools) to read, or to enumerate what is open, use extension_dom_snapshot. For a built extension's files and sizes on disk use extension_analyze. Chromium rides Chrome DevTools Protocol (needs the session's debug port, not allowControl). Firefox is fully paired: summary/meta/html/dom_snapshot/extension_roots/probes ride the agent bridge (needs allowEval: true), console rides the RDP watcher replay (engine 4.0.15+), and deepDom needs an MV2 session with host permissions for the target url (Firefox MV3 background CSP blocks bridge evals). Requires an active dev or start session.",
|
|
7398
7244
|
inputSchema: {
|
|
7399
7245
|
type: "object",
|
|
7400
7246
|
properties: {
|
|
7401
|
-
projectPath:
|
|
7402
|
-
type: "string",
|
|
7403
|
-
description: "Path to the extension project root (must have an active dev session)"
|
|
7404
|
-
},
|
|
7247
|
+
projectPath: SESSION_PROJECT_PATH,
|
|
7405
7248
|
url: {
|
|
7406
7249
|
type: "string",
|
|
7407
|
-
description: "URL to inspect
|
|
7250
|
+
description: "URL to inspect; the tab is navigated there first"
|
|
7408
7251
|
},
|
|
7409
7252
|
probe: {
|
|
7410
7253
|
type: "array",
|
|
7411
7254
|
items: {
|
|
7412
7255
|
type: "string"
|
|
7413
7256
|
},
|
|
7414
|
-
description: "CSS selectors to query
|
|
7257
|
+
description: "CSS selectors to query; returns counts and samples for each"
|
|
7415
7258
|
},
|
|
7416
7259
|
include: {
|
|
7417
7260
|
type: "array",
|
|
@@ -7431,12 +7274,9 @@ const source_inspect_schema = {
|
|
|
7431
7274
|
"meta",
|
|
7432
7275
|
"console"
|
|
7433
7276
|
],
|
|
7434
|
-
description: "What
|
|
7435
|
-
},
|
|
7436
|
-
browser: {
|
|
7437
|
-
type: "string",
|
|
7438
|
-
description: "Browser session to target. Defaults to the active dev session's browser for this project."
|
|
7277
|
+
description: "What to include"
|
|
7439
7278
|
},
|
|
7279
|
+
browser: SESSION_BROWSER,
|
|
7440
7280
|
maxBytes: {
|
|
7441
7281
|
type: "number",
|
|
7442
7282
|
default: 262144,
|
|
@@ -7445,7 +7285,7 @@ const source_inspect_schema = {
|
|
|
7445
7285
|
deepDom: {
|
|
7446
7286
|
type: "boolean",
|
|
7447
7287
|
default: false,
|
|
7448
|
-
description: "Pierce CLOSED shadow roots
|
|
7288
|
+
description: "Pierce CLOSED shadow roots; open ones are read anyway. Chromium: CDP DOM pierce. Firefox: a content-script walk via tabs.executeScript (MV2 only, needs host permissions for the target url)."
|
|
7449
7289
|
}
|
|
7450
7290
|
},
|
|
7451
7291
|
required: [
|
|
@@ -7453,7 +7293,7 @@ const source_inspect_schema = {
|
|
|
7453
7293
|
]
|
|
7454
7294
|
}
|
|
7455
7295
|
};
|
|
7456
|
-
async function
|
|
7296
|
+
async function inspect_handler(args) {
|
|
7457
7297
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser, "chrome");
|
|
7458
7298
|
const include = new Set(args.include ?? [
|
|
7459
7299
|
"summary",
|
|
@@ -7565,18 +7405,12 @@ async function source_inspect_handler(args) {
|
|
|
7565
7405
|
}
|
|
7566
7406
|
const list_extensions_schema = {
|
|
7567
7407
|
name: "extension_list_extensions",
|
|
7568
|
-
description: "List the extensions in the running dev browser
|
|
7408
|
+
description: "List the extensions in the running dev browser: id, name, version, and (on Chromium) live contexts. THIS session's own extension is flagged ownExtension: true, with name and version from the ready contract even when the browser exposes no identity. Chromium rides the Chrome DevTools Protocol, so an entry needs at least one live context (a dormant MV3 service worker may be absent until it wakes). Firefox rides the RDP root actor (listAddons, engine 4.0.15+), so entries are INSTALLED add-ons regardless of contexts, marked temporarilyInstalled where relevant, and carry none. Either way other extensions' contexts are never attached to or evaluated in. Requires an active dev or start session.",
|
|
7569
7409
|
inputSchema: {
|
|
7570
7410
|
type: "object",
|
|
7571
7411
|
properties: {
|
|
7572
|
-
projectPath:
|
|
7573
|
-
|
|
7574
|
-
description: "Path to the extension project root (must have an active dev session)"
|
|
7575
|
-
},
|
|
7576
|
-
browser: {
|
|
7577
|
-
type: "string",
|
|
7578
|
-
description: "Browser session to target. Defaults to the active dev session's browser for this project."
|
|
7579
|
-
}
|
|
7412
|
+
projectPath: SESSION_PROJECT_PATH,
|
|
7413
|
+
browser: SESSION_BROWSER
|
|
7580
7414
|
},
|
|
7581
7415
|
required: [
|
|
7582
7416
|
"projectPath"
|
|
@@ -7842,13 +7676,10 @@ const logs_schema_schema = {
|
|
|
7842
7676
|
inputSchema: {
|
|
7843
7677
|
type: "object",
|
|
7844
7678
|
properties: {
|
|
7845
|
-
projectPath:
|
|
7846
|
-
type: "string",
|
|
7847
|
-
description: "Path to the extension project root (must have an active dev session)"
|
|
7848
|
-
},
|
|
7679
|
+
projectPath: SESSION_PROJECT_PATH,
|
|
7849
7680
|
browser: {
|
|
7850
7681
|
type: "string",
|
|
7851
|
-
description: "Which dist/extension-js/<browser>/ to read. Defaults to
|
|
7682
|
+
description: "Which dist/extension-js/<browser>/ to read. Defaults to this project's live session, else chromium."
|
|
7852
7683
|
},
|
|
7853
7684
|
level: {
|
|
7854
7685
|
type: "string",
|
|
@@ -7862,7 +7693,7 @@ const logs_schema_schema = {
|
|
|
7862
7693
|
"all"
|
|
7863
7694
|
],
|
|
7864
7695
|
default: "all",
|
|
7865
|
-
description: "Minimum severity
|
|
7696
|
+
description: "Minimum severity; a level includes everything more severe."
|
|
7866
7697
|
},
|
|
7867
7698
|
context: {
|
|
7868
7699
|
type: "array",
|
|
@@ -7883,15 +7714,15 @@ const logs_schema_schema = {
|
|
|
7883
7714
|
signalsOnly: {
|
|
7884
7715
|
type: "boolean",
|
|
7885
7716
|
default: false,
|
|
7886
|
-
description: "Only structured dx.signal diagnostics (
|
|
7717
|
+
description: "Only structured dx.signal diagnostics (code/status/remediation), no plain console lines."
|
|
7887
7718
|
},
|
|
7888
7719
|
since: {
|
|
7889
7720
|
type: "number",
|
|
7890
|
-
description: "Only
|
|
7721
|
+
description: "Only events with seq greater than this; the cursor for polling forward."
|
|
7891
7722
|
},
|
|
7892
7723
|
url: {
|
|
7893
7724
|
type: "string",
|
|
7894
|
-
description: "Only events whose url/hostname matches (glob
|
|
7725
|
+
description: "Only events whose url/hostname matches (glob or substring), e.g. https://shop.example/*."
|
|
7895
7726
|
},
|
|
7896
7727
|
tab: {
|
|
7897
7728
|
type: "number",
|
|
@@ -7900,7 +7731,7 @@ const logs_schema_schema = {
|
|
|
7900
7731
|
follow: {
|
|
7901
7732
|
type: "boolean",
|
|
7902
7733
|
default: false,
|
|
7903
|
-
description: "Collect from the live control channel for a bounded window instead of reading the file.
|
|
7734
|
+
description: "Collect from the live control channel for a bounded window instead of reading the file."
|
|
7904
7735
|
},
|
|
7905
7736
|
followMs: {
|
|
7906
7737
|
type: "number",
|
|
@@ -7910,7 +7741,7 @@ const logs_schema_schema = {
|
|
|
7910
7741
|
limit: {
|
|
7911
7742
|
type: "number",
|
|
7912
7743
|
default: 200,
|
|
7913
|
-
description: "
|
|
7744
|
+
description: "How many of the most recent events to return."
|
|
7914
7745
|
}
|
|
7915
7746
|
},
|
|
7916
7747
|
required: [
|
|
@@ -8109,14 +7940,11 @@ async function logs_handler(args) {
|
|
|
8109
7940
|
}
|
|
8110
7941
|
const eval_schema = {
|
|
8111
7942
|
name: "extension_eval",
|
|
8112
|
-
description: "Evaluate an expression in a running extension context. Requires the
|
|
7943
|
+
description: "Evaluate an expression in a running extension context. Requires the session to have been started with allowEval: true (extension_dev; writes a 0600 session token). Context defaults to `background`, EXCEPT on a Chromium MV3 session (the default template) where it is `page`, the active tab, because the MV3 service worker CSP blocks eval; pass context:'background' to target the worker anyway and get that explanation back. For content/page, pass `url` to pick the tab or omit both `url` and `tab` for the ACTIVE tab; a numeric `tab` only disambiguates. Extension surfaces (popup/options/sidebar/devtools) and override pages evaluate over the in-bundle relay and need NO tab id, but must be OPEN (extension_open first; a closed one returns an explicit error). extension_dom_snapshot with listTabs: true enumerates {tabId,url,title}.",
|
|
8113
7944
|
inputSchema: {
|
|
8114
7945
|
type: "object",
|
|
8115
7946
|
properties: {
|
|
8116
|
-
projectPath:
|
|
8117
|
-
type: "string",
|
|
8118
|
-
description: "Path to the extension project root (must have an active dev session)"
|
|
8119
|
-
},
|
|
7947
|
+
projectPath: SESSION_PROJECT_PATH,
|
|
8120
7948
|
expression: {
|
|
8121
7949
|
type: "string",
|
|
8122
7950
|
description: "JavaScript expression to evaluate in the target context"
|
|
@@ -8135,24 +7963,18 @@ const eval_schema = {
|
|
|
8135
7963
|
"content",
|
|
8136
7964
|
"page"
|
|
8137
7965
|
],
|
|
8138
|
-
description: "
|
|
7966
|
+
description: "Where to evaluate. Default background, except Chromium MV3 sessions default to page (the active tab)."
|
|
8139
7967
|
},
|
|
8140
7968
|
url: {
|
|
8141
7969
|
type: "string",
|
|
8142
|
-
description: "
|
|
7970
|
+
description: "content/page: pick the tab by url (match pattern, then substring). Preferred over `tab`."
|
|
8143
7971
|
},
|
|
8144
7972
|
tab: {
|
|
8145
7973
|
type: "number",
|
|
8146
|
-
description: "Numeric chrome.tabs id,
|
|
8147
|
-
},
|
|
8148
|
-
browser: {
|
|
8149
|
-
type: "string",
|
|
8150
|
-
description: "Browser session to target. Defaults to the active dev session's browser for this project."
|
|
7974
|
+
description: "Numeric chrome.tabs id, only to disambiguate when several tabs match."
|
|
8151
7975
|
},
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
description: "Command timeout in ms (default 5000)"
|
|
8155
|
-
}
|
|
7976
|
+
browser: SESSION_BROWSER,
|
|
7977
|
+
timeout: CALL_TIMEOUT
|
|
8156
7978
|
},
|
|
8157
7979
|
required: [
|
|
8158
7980
|
"projectPath",
|
|
@@ -8207,7 +8029,7 @@ async function eval_handler(args) {
|
|
|
8207
8029
|
if (parsed && "object" == typeof parsed) {
|
|
8208
8030
|
parsed.defaultedContext = "page";
|
|
8209
8031
|
parsed.contextNote = 'No context given: defaulted to "page" (the active tab) because this Chromium session\'s MV3 background is a service worker whose CSP blocks eval. Pass context: "background" explicitly to target the worker (works on Firefox/MV2 builds).';
|
|
8210
|
-
if (false === parsed.ok && /cannot access|chrome-extension:\/\/|chrome:\/\//i.test(JSON.stringify(parsed.error ?? ""))) parsed.hint = "The active tab is a browser or extension page that eval cannot reach. Navigate the dev browser to a regular web page, or pass url (match pattern) or tab to pick one;
|
|
8032
|
+
if (false === parsed.ok && /cannot access|chrome-extension:\/\/|chrome:\/\//i.test(JSON.stringify(parsed.error ?? ""))) parsed.hint = "The active tab is a browser or extension page that eval cannot reach. Navigate the dev browser to a regular web page, or pass url (match pattern) or tab to pick one; extension_dom_snapshot with listTabs: true lists open tabs.";
|
|
8211
8033
|
return JSON.stringify(parsed);
|
|
8212
8034
|
}
|
|
8213
8035
|
} catch {}
|
|
@@ -8215,14 +8037,11 @@ async function eval_handler(args) {
|
|
|
8215
8037
|
}
|
|
8216
8038
|
const storage_schema = {
|
|
8217
8039
|
name: "extension_storage",
|
|
8218
|
-
description: "Read or write chrome.storage in a running extension. Requires the
|
|
8040
|
+
description: "Read or write chrome.storage in a running extension. Requires the session to have been started with allowControl: true (extension_dev).",
|
|
8219
8041
|
inputSchema: {
|
|
8220
8042
|
type: "object",
|
|
8221
8043
|
properties: {
|
|
8222
|
-
projectPath:
|
|
8223
|
-
type: "string",
|
|
8224
|
-
description: "Path to the extension project root (must have an active dev session)"
|
|
8225
|
-
},
|
|
8044
|
+
projectPath: SESSION_PROJECT_PATH,
|
|
8226
8045
|
action: {
|
|
8227
8046
|
type: "string",
|
|
8228
8047
|
enum: [
|
|
@@ -8259,14 +8078,8 @@ const storage_schema = {
|
|
|
8259
8078
|
],
|
|
8260
8079
|
default: "background"
|
|
8261
8080
|
},
|
|
8262
|
-
browser:
|
|
8263
|
-
|
|
8264
|
-
description: "Browser session to target. Defaults to the active dev session's browser for this project."
|
|
8265
|
-
},
|
|
8266
|
-
timeout: {
|
|
8267
|
-
type: "number",
|
|
8268
|
-
description: "Command timeout in ms (default 5000)"
|
|
8269
|
-
}
|
|
8081
|
+
browser: SESSION_BROWSER,
|
|
8082
|
+
timeout: CALL_TIMEOUT
|
|
8270
8083
|
},
|
|
8271
8084
|
required: [
|
|
8272
8085
|
"projectPath",
|
|
@@ -8307,14 +8120,11 @@ async function storage_handler(args) {
|
|
|
8307
8120
|
}
|
|
8308
8121
|
const reload_schema = {
|
|
8309
8122
|
name: "extension_reload",
|
|
8310
|
-
description: "Reload a running extension (background) or a tab. Requires the
|
|
8123
|
+
description: "Reload a running extension (background) or a tab. Requires the session to have been started with allowControl: true (extension_dev).",
|
|
8311
8124
|
inputSchema: {
|
|
8312
8125
|
type: "object",
|
|
8313
8126
|
properties: {
|
|
8314
|
-
projectPath:
|
|
8315
|
-
type: "string",
|
|
8316
|
-
description: "Path to the extension project root (must have an active dev session)"
|
|
8317
|
-
},
|
|
8127
|
+
projectPath: SESSION_PROJECT_PATH,
|
|
8318
8128
|
context: {
|
|
8319
8129
|
type: "string",
|
|
8320
8130
|
enum: [
|
|
@@ -8328,14 +8138,8 @@ const reload_schema = {
|
|
|
8328
8138
|
type: "number",
|
|
8329
8139
|
description: "For content/page: a specific tab id"
|
|
8330
8140
|
},
|
|
8331
|
-
browser:
|
|
8332
|
-
|
|
8333
|
-
description: "Browser session to target. Defaults to the active dev session's browser for this project."
|
|
8334
|
-
},
|
|
8335
|
-
timeout: {
|
|
8336
|
-
type: "number",
|
|
8337
|
-
description: "Command timeout in ms (default 5000)"
|
|
8338
|
-
}
|
|
8141
|
+
browser: SESSION_BROWSER,
|
|
8142
|
+
timeout: CALL_TIMEOUT
|
|
8339
8143
|
},
|
|
8340
8144
|
required: [
|
|
8341
8145
|
"projectPath"
|
|
@@ -8353,7 +8157,7 @@ async function reload_handler(args) {
|
|
|
8353
8157
|
})
|
|
8354
8158
|
], args.projectPath, args.timeout);
|
|
8355
8159
|
}
|
|
8356
|
-
const TARGET_ID_NOTE = "targetId is a CDP target id, NOT a chrome.tabs id: do not pass it as `tab`. Target a tab with `tabUrl` (URL substring) or `url`; if you need a numeric tab id, call
|
|
8160
|
+
const TARGET_ID_NOTE = "targetId is a CDP target id, NOT a chrome.tabs id: do not pass it as `tab`. Target a tab with `tabUrl` (URL substring) or `url`; if you need a numeric tab id, call extension_dom_snapshot with listTabs: true.";
|
|
8357
8161
|
function filterPageTargets(raw) {
|
|
8358
8162
|
return raw.filter((t)=>"page" === t.type && !String(t.url ?? "").startsWith("devtools://")).map((t)=>({
|
|
8359
8163
|
targetId: String(t.id),
|
|
@@ -8371,38 +8175,35 @@ function matchTargetsByUrl(targets, needle) {
|
|
|
8371
8175
|
if (byUrl.length > 0) return byUrl;
|
|
8372
8176
|
return targets.filter((t)=>t.title.toLowerCase().includes(wanted));
|
|
8373
8177
|
}
|
|
8374
|
-
const RDP_ACTOR_NOTE = "actor is an RDP tab descriptor actor id, NOT a chrome.tabs id: do not pass it as `tab`. Target a tab with `tabUrl` (URL substring) or `url`; if you need a numeric tab id, call
|
|
8375
|
-
const
|
|
8376
|
-
name: "
|
|
8377
|
-
description: "
|
|
8178
|
+
const RDP_ACTOR_NOTE = "actor is an RDP tab descriptor actor id, NOT a chrome.tabs id: do not pass it as `tab`. Target a tab with `tabUrl` (URL substring) or `url`; if you need a numeric tab id, call extension_dom_snapshot with listTabs: true.";
|
|
8179
|
+
const dom_snapshot_schema = {
|
|
8180
|
+
name: "extension_dom_snapshot",
|
|
8181
|
+
description: "Take a SHALLOW structured DOM snapshot of ONE CHOSEN surface through the agent bridge (CDP-free, localhost): element counts, extension roots, OPEN shadow roots, optional byte-capped HTML, optional recent console lines. This is the SURFACE PICKER: the only tool that reads an OPEN extension surface by name (`context`: popup/options/sidebar/devtools) or an override page, the only one that takes a numeric chrome.tabs id, and the only one that enumerates what is open (listTargets for CDP targetIds and RDP tab actors, listTabs for numeric tab ids). An ambiguous `tabUrl` returns the candidates instead of guessing. It does NOT pierce closed shadow roots, run selector probes, or navigate: for those, and for a deep read of an already-open web page, use extension_inspect. Requires the session to have been started with allowControl: true (extension_dev).",
|
|
8378
8182
|
inputSchema: {
|
|
8379
8183
|
type: "object",
|
|
8380
8184
|
properties: {
|
|
8381
|
-
projectPath:
|
|
8382
|
-
type: "string",
|
|
8383
|
-
description: "Path to the extension project root (must have an active dev session)"
|
|
8384
|
-
},
|
|
8185
|
+
projectPath: SESSION_PROJECT_PATH,
|
|
8385
8186
|
tab: {
|
|
8386
8187
|
type: "number",
|
|
8387
|
-
description: "Numeric chrome.tabs id,
|
|
8188
|
+
description: "Numeric chrome.tabs id, only to disambiguate when several tabs match. With neither `tab` nor `url`, content/page target the active tab."
|
|
8388
8189
|
},
|
|
8389
8190
|
url: {
|
|
8390
8191
|
type: "string",
|
|
8391
|
-
description: "
|
|
8192
|
+
description: "content/page: pick the tab by url (match pattern, then substring). Preferred over `tab`."
|
|
8392
8193
|
},
|
|
8393
8194
|
tabUrl: {
|
|
8394
8195
|
type: "string",
|
|
8395
|
-
description: "Target the tab whose URL contains this substring (case-insensitive; titles
|
|
8196
|
+
description: "Target the tab whose URL contains this substring (case-insensitive; titles checked only when no url matches). Resolved against the live browser first: exactly one match proceeds, zero or several return the candidates instead of a guess. Alternative to `url`."
|
|
8396
8197
|
},
|
|
8397
8198
|
listTargets: {
|
|
8398
8199
|
type: "boolean",
|
|
8399
8200
|
default: false,
|
|
8400
|
-
description: "Enumerate
|
|
8201
|
+
description: "Enumerate live page targets and return, ignoring the other args. The discovery path for `tabUrl`. Chromium: {targetId,url,title,type}. Firefox: RDP tab descriptors {actor,url,title,type}. Neither id is a numeric chrome.tabs id; for those use listTabs."
|
|
8401
8202
|
},
|
|
8402
8203
|
listTabs: {
|
|
8403
8204
|
type: "boolean",
|
|
8404
8205
|
default: false,
|
|
8405
|
-
description: "Enumerate open tabs as {tabId,url,title} and return, ignoring the other args.
|
|
8206
|
+
description: "Enumerate open tabs as {tabId,url,title} and return, ignoring the other args. Use when you need a numeric tab id."
|
|
8406
8207
|
},
|
|
8407
8208
|
context: {
|
|
8408
8209
|
type: "string",
|
|
@@ -8418,7 +8219,7 @@ const dom_inspect_schema = {
|
|
|
8418
8219
|
"bookmarks"
|
|
8419
8220
|
],
|
|
8420
8221
|
default: "content",
|
|
8421
|
-
description: "content/page
|
|
8222
|
+
description: "content/page targets `url`, else the active tab; the rest must already be OPEN"
|
|
8422
8223
|
},
|
|
8423
8224
|
include: {
|
|
8424
8225
|
type: "array",
|
|
@@ -8443,16 +8244,10 @@ const dom_inspect_schema = {
|
|
|
8443
8244
|
"number",
|
|
8444
8245
|
"boolean"
|
|
8445
8246
|
],
|
|
8446
|
-
description: "Also include recent console lines
|
|
8447
|
-
},
|
|
8448
|
-
browser: {
|
|
8449
|
-
type: "string",
|
|
8450
|
-
description: "Browser session to target. Defaults to the active dev session's browser for this project."
|
|
8247
|
+
description: "Also include recent console lines. A number is how many; true means 50."
|
|
8451
8248
|
},
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
description: "Command timeout in ms (default 5000)"
|
|
8455
|
-
}
|
|
8249
|
+
browser: SESSION_BROWSER,
|
|
8250
|
+
timeout: CALL_TIMEOUT
|
|
8456
8251
|
},
|
|
8457
8252
|
required: [
|
|
8458
8253
|
"projectPath"
|
|
@@ -8483,7 +8278,7 @@ async function cdpPortOrError(projectPath, browser, feature) {
|
|
|
8483
8278
|
port: resolved.port
|
|
8484
8279
|
};
|
|
8485
8280
|
}
|
|
8486
|
-
async function
|
|
8281
|
+
async function dom_snapshot_handler(args) {
|
|
8487
8282
|
const withConsole = true === args.withConsole ? 50 : false === args.withConsole ? void 0 : args.withConsole;
|
|
8488
8283
|
if (args.listTargets) {
|
|
8489
8284
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
@@ -8663,7 +8458,7 @@ async function dom_inspect_handler(args) {
|
|
|
8663
8458
|
}
|
|
8664
8459
|
const publish_schema = {
|
|
8665
8460
|
name: "extension_publish",
|
|
8666
|
-
description: "Publish the project your stored token is scoped to (
|
|
8461
|
+
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.",
|
|
8667
8462
|
inputSchema: {
|
|
8668
8463
|
type: "object",
|
|
8669
8464
|
properties: {
|
|
@@ -8673,12 +8468,9 @@ const publish_schema = {
|
|
|
8673
8468
|
},
|
|
8674
8469
|
buildSha: {
|
|
8675
8470
|
type: "string",
|
|
8676
|
-
description: "Pin the
|
|
8471
|
+
description: "Pin the URL to a build sha (7-40 hex chars). An unknown sha is rejected, so the returned URL always points at a real build."
|
|
8677
8472
|
},
|
|
8678
|
-
api:
|
|
8679
|
-
type: "string",
|
|
8680
|
-
description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
|
|
8681
|
-
}
|
|
8473
|
+
api: API_BASE
|
|
8682
8474
|
},
|
|
8683
8475
|
required: []
|
|
8684
8476
|
}
|
|
@@ -8694,7 +8486,7 @@ function fail(name, message) {
|
|
|
8694
8486
|
}
|
|
8695
8487
|
async function publish_handler(args) {
|
|
8696
8488
|
const token = resolveToken();
|
|
8697
|
-
if (!token) return fail("PublishAuthError", "No token. Run
|
|
8489
|
+
if (!token) return fail("PublishAuthError", "No token. Run extension_auth (action: login), or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard).");
|
|
8698
8490
|
if (null != args.ttlHours) {
|
|
8699
8491
|
const t = Number(args.ttlHours);
|
|
8700
8492
|
if (!Number.isInteger(t) || t < 1 || t > 168) return fail("PublishBadRequest", "ttlHours must be an integer between 1 and 168.");
|
|
@@ -8740,7 +8532,7 @@ async function publish_handler(args) {
|
|
|
8740
8532
|
}
|
|
8741
8533
|
const release_promote_schema = {
|
|
8742
8534
|
name: "extension_release_promote",
|
|
8743
|
-
description: "Promote a built extension to a release channel (
|
|
8535
|
+
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. 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. Use extension_release_status to find a valid buildId. Cutting a version-bump PR is not available headlessly (it writes to your source repo and needs an interactive login).",
|
|
8744
8536
|
inputSchema: {
|
|
8745
8537
|
type: "object",
|
|
8746
8538
|
properties: {
|
|
@@ -8771,10 +8563,7 @@ const release_promote_schema = {
|
|
|
8771
8563
|
type: "string",
|
|
8772
8564
|
description: "Release notes markdown (optional)"
|
|
8773
8565
|
},
|
|
8774
|
-
api:
|
|
8775
|
-
type: "string",
|
|
8776
|
-
description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
|
|
8777
|
-
}
|
|
8566
|
+
api: API_BASE
|
|
8778
8567
|
},
|
|
8779
8568
|
required: [
|
|
8780
8569
|
"buildId",
|
|
@@ -8793,7 +8582,7 @@ function release_promote_fail(name, message) {
|
|
|
8793
8582
|
}
|
|
8794
8583
|
async function release_promote_handler(args) {
|
|
8795
8584
|
const token = resolveToken();
|
|
8796
|
-
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
|
|
8585
|
+
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).");
|
|
8797
8586
|
const buildId = String(args.buildId || "").trim();
|
|
8798
8587
|
const channel = String(args.channel || "").trim();
|
|
8799
8588
|
if (!buildId || !channel) return release_promote_fail("ReleaseInputError", "buildId and channel are required.");
|
|
@@ -8836,7 +8625,7 @@ async function release_promote_handler(args) {
|
|
|
8836
8625
|
const ref = resolveProjectRef();
|
|
8837
8626
|
if (404 === res.status || "UNKNOWN_BUILD" === code) {
|
|
8838
8627
|
enrich.buildsPageUrl = consoleProjectUrl(ref, "builds", args.api);
|
|
8839
|
-
enrich.hint = "Run
|
|
8628
|
+
enrich.hint = "Run extension_release_status to see this project's channels, their promoted shas, and recent builds.";
|
|
8840
8629
|
if (ref) {
|
|
8841
8630
|
const channelsUrl = registryFileUrl(ref, "channels.json");
|
|
8842
8631
|
const channelsRes = await fetchRegistryJson(channelsUrl, fetch, {
|
|
@@ -8879,28 +8668,6 @@ async function release_promote_handler(args) {
|
|
|
8879
8668
|
} : data;
|
|
8880
8669
|
return JSON.stringify(enriched);
|
|
8881
8670
|
}
|
|
8882
|
-
const release_list_schema = {
|
|
8883
|
-
name: "extension_release_list",
|
|
8884
|
-
description: "List the project's release channels (channel -> promoted build sha) and recent builds from the registry (registry.extension.land), so you can pick a valid buildSha for extension_release_promote, extension_deploy, or extension_publish. Read-only, no dispatch. Defaults to the logged-in project (extension_login); pass workspace + project to inspect another project. Works for PRIVATE projects too when your stored login covers them. Returns the registry URLs it read, the console Builds page URL (needs a login), and a publicUrl per channel and build on the public viewer (no login needed for a public project).",
|
|
8885
|
-
inputSchema: {
|
|
8886
|
-
type: "object",
|
|
8887
|
-
properties: {
|
|
8888
|
-
workspace: {
|
|
8889
|
-
type: "string",
|
|
8890
|
-
description: "Workspace slug override (defaults to the stored login's workspace)."
|
|
8891
|
-
},
|
|
8892
|
-
project: {
|
|
8893
|
-
type: "string",
|
|
8894
|
-
description: "Project slug override (defaults to the stored login's project)."
|
|
8895
|
-
},
|
|
8896
|
-
api: {
|
|
8897
|
-
type: "string",
|
|
8898
|
-
description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL). Used to resolve link origins and, for a private project, to mint a short-lived read token."
|
|
8899
|
-
}
|
|
8900
|
-
},
|
|
8901
|
-
required: []
|
|
8902
|
-
}
|
|
8903
|
-
};
|
|
8904
8671
|
function release_list_fail(name, message, extra) {
|
|
8905
8672
|
return JSON.stringify({
|
|
8906
8673
|
ok: false,
|
|
@@ -8911,9 +8678,9 @@ function release_list_fail(name, message, extra) {
|
|
|
8911
8678
|
...extra ?? {}
|
|
8912
8679
|
});
|
|
8913
8680
|
}
|
|
8914
|
-
async function
|
|
8681
|
+
async function readReleases(args) {
|
|
8915
8682
|
const ref = resolveProjectRef(args);
|
|
8916
|
-
if (!ref) return release_list_fail("ReleaseListInputError", "No project to list. Run
|
|
8683
|
+
if (!ref) return release_list_fail("ReleaseListInputError", "No project to list. Run extension_auth (action: login), which names the project, or pass workspace + project explicitly.");
|
|
8917
8684
|
const channelsUrl = registryFileUrl(ref, "channels.json");
|
|
8918
8685
|
const metaUrl = registryFileUrl(ref, "meta.json");
|
|
8919
8686
|
const buildsUrl = registryFileUrl(ref, "builds/index.json");
|
|
@@ -8932,7 +8699,7 @@ async function release_list_handler(args) {
|
|
|
8932
8699
|
})
|
|
8933
8700
|
]);
|
|
8934
8701
|
const buildsPageUrl = consoleProjectUrl(ref, "builds", args.api);
|
|
8935
|
-
if (!channelsRes.ok && !metaRes.ok && !buildsRes.ok) return release_list_fail("ReleaseListNotFound", `No registry data for ${ref.workspace}/${ref.project} (${channelsUrl} returned ${channelsRes.status ?? "no response"}). The project may have no builds yet, or the workspace/project slugs may be wrong. If it is private, make sure
|
|
8702
|
+
if (!channelsRes.ok && !metaRes.ok && !buildsRes.ok) return release_list_fail("ReleaseListNotFound", `No registry data for ${ref.workspace}/${ref.project} (${channelsUrl} returned ${channelsRes.status ?? "no response"}). The project may have no builds yet, or the workspace/project slugs may be wrong. If it is private, make sure extension_auth covers this exact project (a token scoped elsewhere cannot read it). The console Builds page is the authoritative view: ${buildsPageUrl}`, {
|
|
8936
8703
|
workspace: ref.workspace,
|
|
8937
8704
|
project: ref.project,
|
|
8938
8705
|
registryUrl: channelsUrl,
|
|
@@ -8979,246 +8746,12 @@ async function release_list_handler(args) {
|
|
|
8979
8746
|
if (!buildsRes.ok) result.buildsUnavailable = `builds/index.json unreadable: ${buildsRes.message}`;
|
|
8980
8747
|
return JSON.stringify(result);
|
|
8981
8748
|
}
|
|
8982
|
-
function storeMdWarnings(browsers, cwd) {
|
|
8983
|
-
const wantsFirefox = browsers.includes("firefox");
|
|
8984
|
-
const wantsEdge = browsers.includes("edge");
|
|
8985
|
-
if (!wantsFirefox && !wantsEdge) return [];
|
|
8986
|
-
let content;
|
|
8987
|
-
try {
|
|
8988
|
-
content = node_fs.readFileSync(node_path.join(cwd, "STORE.md"), "utf8");
|
|
8989
|
-
} catch {
|
|
8990
|
-
return [
|
|
8991
|
-
"No STORE.md found in the current working directory. Platform submissions read STORE.md from the project's source repository, so this may not apply here; make sure STORE.md exists there for Firefox reviewer notes and Edge certification notes. See the extension-dev skill's store-md reference."
|
|
8992
|
-
];
|
|
8993
|
-
}
|
|
8994
|
-
const hasField = (section, field)=>{
|
|
8995
|
-
const parts = content.split(/^## +/m);
|
|
8996
|
-
const match = parts.find((p)=>section.test(p.split("\n", 1)[0] ?? ""));
|
|
8997
|
-
if (!match) return false;
|
|
8998
|
-
const sub = match.split(/^### +/m).find((p)=>field.test(p.split("\n", 1)[0] ?? ""));
|
|
8999
|
-
if (!sub) return false;
|
|
9000
|
-
const body = sub.split("\n").slice(1).join("\n");
|
|
9001
|
-
return body.replace(/<!--[\s\S]*?-->/g, "").trim().length > 0;
|
|
9002
|
-
};
|
|
9003
|
-
const warnings = [];
|
|
9004
|
-
if (wantsFirefox && !hasField(/firefox|amo/i, /reviewer notes/i)) warnings.push("STORE.md has no Firefox reviewer notes; AMO reviews go faster with test credentials and steps.");
|
|
9005
|
-
if (wantsEdge && !hasField(/edge/i, /certification notes/i)) warnings.push("STORE.md has no Edge certification notes; the certification team gets no testing guidance.");
|
|
9006
|
-
return warnings;
|
|
9007
|
-
}
|
|
9008
|
-
const deploy_schema = {
|
|
9009
|
-
name: "extension_deploy",
|
|
9010
|
-
description: "Submit a built extension to the Chrome Web Store, Firefox AMO, Edge Add-ons, and/or the App Store (Safari) THROUGH extension.dev, which holds your store credentials and dispatches the release from your project's mirror CI. DEFAULTS TO A DRY RUN (preflight - dispatches nothing): the platform side verifies auth, the project, that the build exists, and the store workflow, and this tool then adds the per-store verdict from each store's public credential-health record; trust the per-store rows in the result 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 target project is identified by your token (extension_login or a release token in 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 tool arguments and local files are not uploaded. Pass browsers + buildSha (extension_release_list lists valid shas); after a real submission, extension_store_status reads the recorded outcome and review state. Posts to the platform's CLI store-submission endpoint.",
|
|
9011
|
-
inputSchema: {
|
|
9012
|
-
type: "object",
|
|
9013
|
-
properties: {
|
|
9014
|
-
browsers: {
|
|
9015
|
-
type: "array",
|
|
9016
|
-
items: {
|
|
9017
|
-
type: "string",
|
|
9018
|
-
enum: [
|
|
9019
|
-
"chrome",
|
|
9020
|
-
"firefox",
|
|
9021
|
-
"edge",
|
|
9022
|
-
"safari"
|
|
9023
|
-
]
|
|
9024
|
-
},
|
|
9025
|
-
description: "Stores to submit to."
|
|
9026
|
-
},
|
|
9027
|
-
buildSha: {
|
|
9028
|
-
type: "string",
|
|
9029
|
-
description: "The built commit SHA to submit. It must have a completed build in the project's build index; an unknown sha is rejected."
|
|
9030
|
-
},
|
|
9031
|
-
channel: {
|
|
9032
|
-
type: "string",
|
|
9033
|
-
description: "Release channel to submit from (default stable)."
|
|
9034
|
-
},
|
|
9035
|
-
version: {
|
|
9036
|
-
type: "string",
|
|
9037
|
-
description: "Version label for the submission record (optional)."
|
|
9038
|
-
},
|
|
9039
|
-
dryRun: {
|
|
9040
|
-
type: "boolean",
|
|
9041
|
-
default: true,
|
|
9042
|
-
description: "Preflight only (verify auth, project, build, and store workflow). Pass false to actually dispatch the submission (irreversible, enters store review)."
|
|
9043
|
-
},
|
|
9044
|
-
api: {
|
|
9045
|
-
type: "string",
|
|
9046
|
-
description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
|
|
9047
|
-
}
|
|
9048
|
-
},
|
|
9049
|
-
required: [
|
|
9050
|
-
"browsers",
|
|
9051
|
-
"buildSha"
|
|
9052
|
-
]
|
|
9053
|
-
}
|
|
9054
|
-
};
|
|
9055
|
-
function deploy_fail(name, message) {
|
|
9056
|
-
return JSON.stringify({
|
|
9057
|
-
ok: false,
|
|
9058
|
-
error: {
|
|
9059
|
-
name,
|
|
9060
|
-
message
|
|
9061
|
-
}
|
|
9062
|
-
});
|
|
9063
|
-
}
|
|
9064
|
-
async function deploy_handler(args) {
|
|
9065
|
-
const token = resolveToken();
|
|
9066
|
-
if (!token) return deploy_fail("DeployAuthError", "No token. Run extension_login, or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard under project settings -> Access tokens; tokens live at most 7 days, so CI must re-mint before expiry).");
|
|
9067
|
-
const browsers = (Array.isArray(args.browsers) ? args.browsers : []).map((b)=>String(b).trim().toLowerCase()).filter(Boolean);
|
|
9068
|
-
if (0 === browsers.length) return deploy_fail("DeployInputError", 'browsers is required (e.g. ["chrome","firefox","edge","safari"]).');
|
|
9069
|
-
const buildSha = String(args.buildSha || "").trim();
|
|
9070
|
-
if (!buildSha) return deploy_fail("DeployInputError", "buildSha is required (the built commit to submit).");
|
|
9071
|
-
const apiCheck = safeApiBase(resolveApiBase(args.api));
|
|
9072
|
-
if (!apiCheck.ok) return deploy_fail("DeployConfigError", apiCheck.message);
|
|
9073
|
-
const url = `${apiCheck.base}/api/cli/stores/submit`;
|
|
9074
|
-
const dryRun = false !== args.dryRun;
|
|
9075
|
-
const body = {
|
|
9076
|
-
browsers,
|
|
9077
|
-
buildSha,
|
|
9078
|
-
dryRun
|
|
9079
|
-
};
|
|
9080
|
-
if (args.channel) body.channel = String(args.channel).trim();
|
|
9081
|
-
if (args.version) body.version = String(args.version).trim();
|
|
9082
|
-
let res;
|
|
9083
|
-
try {
|
|
9084
|
-
res = await fetch(url, {
|
|
9085
|
-
method: "POST",
|
|
9086
|
-
headers: {
|
|
9087
|
-
authorization: `Bearer ${token}`,
|
|
9088
|
-
"content-type": "application/json"
|
|
9089
|
-
},
|
|
9090
|
-
body: JSON.stringify(body)
|
|
9091
|
-
});
|
|
9092
|
-
} catch (err) {
|
|
9093
|
-
return deploy_fail("DeployNetworkError", `Could not reach ${url}: ${err?.message || err}`);
|
|
9094
|
-
}
|
|
9095
|
-
const text = await res.text();
|
|
9096
|
-
let data;
|
|
9097
|
-
try {
|
|
9098
|
-
data = JSON.parse(text);
|
|
9099
|
-
} catch {
|
|
9100
|
-
data = {
|
|
9101
|
-
message: text
|
|
9102
|
-
};
|
|
9103
|
-
}
|
|
9104
|
-
if (!res.ok) return deploy_fail("DeployError", `${dryRun ? "preflight" : "submit"} failed (${res.status}): ${data?.message || text || "unknown error"}`);
|
|
9105
|
-
const warnings = Array.isArray(data?.warnings) ? [
|
|
9106
|
-
...data.warnings
|
|
9107
|
-
] : [];
|
|
9108
|
-
warnings.push(...storeMdWarnings(browsers, process.cwd()));
|
|
9109
|
-
const result = {
|
|
9110
|
-
mode: "platform",
|
|
9111
|
-
dryRun,
|
|
9112
|
-
...data
|
|
9113
|
-
};
|
|
9114
|
-
if (dryRun) {
|
|
9115
|
-
const ref = resolveProjectRef();
|
|
9116
|
-
const consoleStoresUrl = consoleProjectUrl(ref, "stores", args.api);
|
|
9117
|
-
const storeModeNote = `Store publish mode (draft / skip-publish / live) is not readable with the CLI token, so it cannot be verified from here; check per-store settings at ${consoleStoresUrl}.`;
|
|
9118
|
-
let health = null;
|
|
9119
|
-
let healthUnreadable = null;
|
|
9120
|
-
let channelRows = null;
|
|
9121
|
-
if (ref) {
|
|
9122
|
-
const [healthRes, channelsRes] = await Promise.all([
|
|
9123
|
-
fetchRegistryJson(registryFileUrl(ref, "stores/health.json"), fetch, {
|
|
9124
|
-
ref,
|
|
9125
|
-
api: args.api
|
|
9126
|
-
}),
|
|
9127
|
-
fetchRegistryJson(registryFileUrl(ref, "channels.json"), fetch, {
|
|
9128
|
-
ref,
|
|
9129
|
-
api: args.api
|
|
9130
|
-
})
|
|
9131
|
-
]);
|
|
9132
|
-
if (healthRes.ok) {
|
|
9133
|
-
const stores = healthRes.json?.stores;
|
|
9134
|
-
health = stores && "object" == typeof stores ? stores : null;
|
|
9135
|
-
if (!health) healthUnreadable = "stores/health.json had no stores map";
|
|
9136
|
-
} else healthUnreadable = healthRes.message;
|
|
9137
|
-
if (channelsRes.ok) channelRows = parseChannels(channelsRes.json);
|
|
9138
|
-
} else healthUnreadable = "no stored workspace/project to look up (run extension_login)";
|
|
9139
|
-
const preflight = browsers.map((browser)=>{
|
|
9140
|
-
if (!health) return {
|
|
9141
|
-
browser,
|
|
9142
|
-
ok: false,
|
|
9143
|
-
configured: "unknown",
|
|
9144
|
-
publishMode: "unknown",
|
|
9145
|
-
reason: `Store configuration could not be read (${healthUnreadable}); verify the ${browser} store in the console before submitting.`
|
|
9146
|
-
};
|
|
9147
|
-
const row = health[browser];
|
|
9148
|
-
if (!row) return {
|
|
9149
|
-
browser,
|
|
9150
|
-
ok: false,
|
|
9151
|
-
configured: false,
|
|
9152
|
-
publishMode: "unknown",
|
|
9153
|
-
reason: `No ${browser} store is configured on this project; a real submission for ${browser} would fail. Configure it at ${consoleStoresUrl}.`
|
|
9154
|
-
};
|
|
9155
|
-
if (true !== row.ok) return {
|
|
9156
|
-
browser,
|
|
9157
|
-
ok: false,
|
|
9158
|
-
configured: false,
|
|
9159
|
-
publishMode: "unknown",
|
|
9160
|
-
reason: String(row.message || "").trim() || `The ${browser} store failed its last credential health check.`
|
|
9161
|
-
};
|
|
9162
|
-
return {
|
|
9163
|
-
browser,
|
|
9164
|
-
ok: true,
|
|
9165
|
-
configured: true,
|
|
9166
|
-
publishMode: "unknown"
|
|
9167
|
-
};
|
|
9168
|
-
});
|
|
9169
|
-
const actionable = preflight.filter((p)=>p.ok).map((p)=>p.browser);
|
|
9170
|
-
const blocked = preflight.filter((p)=>!p.ok);
|
|
9171
|
-
const channelDefaulted = !String(args.channel || "").trim();
|
|
9172
|
-
const resolvedChannel = String(data?.channel || "").trim() || (channelDefaulted ? "stable" : String(args.channel).trim());
|
|
9173
|
-
if (channelRows) {
|
|
9174
|
-
const exists = channelRows.some((r)=>r.channel === resolvedChannel || r.channel.endsWith(`-${resolvedChannel}`));
|
|
9175
|
-
if (!exists) warnings.push(`Channel "${resolvedChannel}"${channelDefaulted ? " (the default)" : ""} does not exist in this project's channels.json (existing: ${channelRows.map((r)=>r.channel).join(", ") || "none"}), so a real submission from it has no promoted build to serve. Promote a build there first (extension_release_promote) or pass an existing channel.`);
|
|
9176
|
-
}
|
|
9177
|
-
const summaryParts = [];
|
|
9178
|
-
if (actionable.length > 0) summaryParts.push(`Preflight passed for ${actionable.join(", ")}: the platform verified auth, the project, build ${data?.buildId ?? buildSha}, and the store workflow, and the store credentials passed their last health check.`);
|
|
9179
|
-
for (const p of blocked)summaryParts.push(`${p.browser}: ${"unknown" === p.configured ? "cannot be verified" : "NOT actionable"} - ${p.reason}`);
|
|
9180
|
-
summaryParts.push(storeModeNote);
|
|
9181
|
-
result.ok = actionable.length > 0;
|
|
9182
|
-
result.preflight = preflight;
|
|
9183
|
-
result.channel = resolvedChannel;
|
|
9184
|
-
result.channelDefaulted = channelDefaulted;
|
|
9185
|
-
if (channelDefaulted) result.channelNote = `channel: ${resolvedChannel} (default)`;
|
|
9186
|
-
result.consoleStoresUrl = consoleStoresUrl;
|
|
9187
|
-
if ("string" == typeof data?.message) result.platformMessage = data.message;
|
|
9188
|
-
result.message = summaryParts.join(" ");
|
|
9189
|
-
}
|
|
9190
|
-
if (!dryRun) result.statusNote = "Track this submission with extension_store_status: it reads the recorded outcome, per-store credential health, and review state from the public registry.";
|
|
9191
|
-
if (warnings.length > 0) result.warnings = warnings;
|
|
9192
|
-
return JSON.stringify(result);
|
|
9193
|
-
}
|
|
9194
8749
|
const KNOWN_STORES = [
|
|
9195
8750
|
"chrome",
|
|
9196
8751
|
"firefox",
|
|
9197
8752
|
"edge",
|
|
9198
8753
|
"safari"
|
|
9199
8754
|
];
|
|
9200
|
-
const store_status_schema = {
|
|
9201
|
-
name: "extension_store_status",
|
|
9202
|
-
description: "Report the project's browser-store state after an extension_deploy submission: per store (chrome, firefox, edge, safari) whether it is configured, its latest credential health check, the last recorded submission (version, status, store URL, submitted-at), and the latest review status. Reads the project's public registry (registry.extension.land: stores/health.json, stores/status.json, stores/submissions.json) - read-only, dispatches nothing, no auth needed for public projects. Defaults to the logged-in project (extension_login); pass workspace + project to inspect another public project. Registry state can lag the store dashboards by up to a polling interval.",
|
|
9203
|
-
inputSchema: {
|
|
9204
|
-
type: "object",
|
|
9205
|
-
properties: {
|
|
9206
|
-
workspace: {
|
|
9207
|
-
type: "string",
|
|
9208
|
-
description: "Workspace slug override (defaults to the stored login's workspace)."
|
|
9209
|
-
},
|
|
9210
|
-
project: {
|
|
9211
|
-
type: "string",
|
|
9212
|
-
description: "Project slug override (defaults to the stored login's project)."
|
|
9213
|
-
},
|
|
9214
|
-
api: {
|
|
9215
|
-
type: "string",
|
|
9216
|
-
description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL). Used to resolve link origins and, for a private project, to mint a short-lived read token."
|
|
9217
|
-
}
|
|
9218
|
-
},
|
|
9219
|
-
required: []
|
|
9220
|
-
}
|
|
9221
|
-
};
|
|
9222
8755
|
function isPlainObject(value) {
|
|
9223
8756
|
return Boolean(value) && "object" == typeof value && !Array.isArray(value);
|
|
9224
8757
|
}
|
|
@@ -9307,9 +8840,9 @@ function store_status_fail(name, message, extra) {
|
|
|
9307
8840
|
...extra ?? {}
|
|
9308
8841
|
});
|
|
9309
8842
|
}
|
|
9310
|
-
async function
|
|
8843
|
+
async function readStores(args) {
|
|
9311
8844
|
const ref = resolveProjectRef(args);
|
|
9312
|
-
if (!ref) return store_status_fail("StoreStatusInputError", "No project to inspect. Run
|
|
8845
|
+
if (!ref) return store_status_fail("StoreStatusInputError", "No project to inspect. Run extension_auth (action: login), which names the project, or pass workspace + project explicitly.");
|
|
9313
8846
|
const healthUrl = registryFileUrl(ref, "stores/health.json");
|
|
9314
8847
|
const statusUrl = registryFileUrl(ref, "stores/status.json");
|
|
9315
8848
|
const submissionsUrl = registryFileUrl(ref, "stores/submissions.json");
|
|
@@ -9403,9 +8936,294 @@ async function store_status_handler(args) {
|
|
|
9403
8936
|
consoleStoresUrl,
|
|
9404
8937
|
message: `${summaryParts.join(". ")}. This is the registry's recorded state (submissions and the review poller write it); the store dashboards are authoritative and may be ahead of it.`
|
|
9405
8938
|
};
|
|
9406
|
-
if (!healthRes.ok) result.healthUnavailable = `stores/health.json unreadable: ${healthRes.message}`;
|
|
9407
|
-
if (!statusRes.ok) result.statusUnavailable = `stores/status.json unreadable: ${statusRes.message}`;
|
|
9408
|
-
if (!submissionsRes.ok && 404 !== submissionsRes.status) result.submissionsUnavailable = `stores/submissions.json unreadable: ${submissionsRes.message}`;
|
|
8939
|
+
if (!healthRes.ok) result.healthUnavailable = `stores/health.json unreadable: ${healthRes.message}`;
|
|
8940
|
+
if (!statusRes.ok) result.statusUnavailable = `stores/status.json unreadable: ${statusRes.message}`;
|
|
8941
|
+
if (!submissionsRes.ok && 404 !== submissionsRes.status) result.submissionsUnavailable = `stores/submissions.json unreadable: ${submissionsRes.message}`;
|
|
8942
|
+
return JSON.stringify(result);
|
|
8943
|
+
}
|
|
8944
|
+
const release_status_schema = {
|
|
8945
|
+
name: "extension_release_status",
|
|
8946
|
+
description: "Read where a project stands on extension.dev, from the public registry (registry.extension.land). Read-only: it dispatches nothing and promotes nothing. include:'releases' returns the release channels (channel -> promoted build sha), recent builds, and a public build-page URL for each, which is how you find a valid sha for extension_release_promote, extension_submit, or extension_publish. include:'stores' returns the per-store picture after an extension_submit (chrome, firefox, edge, safari): configured or not, the last credential health check, the last recorded submission, and the latest review status, read from stores/health.json, stores/status.json and stores/submissions.json. Both are included by default. Defaults to the logged-in project (extension_auth); pass workspace + project to read another. Private projects work when your stored login covers them. Registry state can lag the store dashboards by up to a polling interval.",
|
|
8947
|
+
inputSchema: {
|
|
8948
|
+
type: "object",
|
|
8949
|
+
properties: {
|
|
8950
|
+
include: {
|
|
8951
|
+
type: "array",
|
|
8952
|
+
items: {
|
|
8953
|
+
type: "string",
|
|
8954
|
+
enum: [
|
|
8955
|
+
"releases",
|
|
8956
|
+
"stores"
|
|
8957
|
+
]
|
|
8958
|
+
},
|
|
8959
|
+
default: [
|
|
8960
|
+
"releases",
|
|
8961
|
+
"stores"
|
|
8962
|
+
],
|
|
8963
|
+
description: "Which sections to read. Both by default."
|
|
8964
|
+
},
|
|
8965
|
+
workspace: {
|
|
8966
|
+
type: "string",
|
|
8967
|
+
description: "Workspace slug override (default: the stored login's)."
|
|
8968
|
+
},
|
|
8969
|
+
project: {
|
|
8970
|
+
type: "string",
|
|
8971
|
+
description: "Project slug override (default: the stored login's)."
|
|
8972
|
+
},
|
|
8973
|
+
api: API_BASE
|
|
8974
|
+
},
|
|
8975
|
+
required: []
|
|
8976
|
+
}
|
|
8977
|
+
};
|
|
8978
|
+
function parse(raw) {
|
|
8979
|
+
try {
|
|
8980
|
+
return JSON.parse(raw);
|
|
8981
|
+
} catch {
|
|
8982
|
+
return {
|
|
8983
|
+
ok: false,
|
|
8984
|
+
error: {
|
|
8985
|
+
name: "ParseError",
|
|
8986
|
+
message: raw
|
|
8987
|
+
}
|
|
8988
|
+
};
|
|
8989
|
+
}
|
|
8990
|
+
}
|
|
8991
|
+
async function release_status_handler(args) {
|
|
8992
|
+
const include = Array.isArray(args.include) && args.include.length ? args.include : [
|
|
8993
|
+
"releases",
|
|
8994
|
+
"stores"
|
|
8995
|
+
];
|
|
8996
|
+
const scope = {
|
|
8997
|
+
workspace: args.workspace,
|
|
8998
|
+
project: args.project,
|
|
8999
|
+
api: args.api
|
|
9000
|
+
};
|
|
9001
|
+
const [releases, stores] = await Promise.all([
|
|
9002
|
+
include.includes("releases") ? readReleases(scope).then(parse) : null,
|
|
9003
|
+
include.includes("stores") ? readStores(scope).then(parse) : null
|
|
9004
|
+
]);
|
|
9005
|
+
const sections = [
|
|
9006
|
+
releases,
|
|
9007
|
+
stores
|
|
9008
|
+
].filter(Boolean);
|
|
9009
|
+
return JSON.stringify({
|
|
9010
|
+
ok: sections.some((section)=>true === section.ok),
|
|
9011
|
+
...releases ? {
|
|
9012
|
+
releases
|
|
9013
|
+
} : {},
|
|
9014
|
+
...stores ? {
|
|
9015
|
+
stores
|
|
9016
|
+
} : {}
|
|
9017
|
+
});
|
|
9018
|
+
}
|
|
9019
|
+
function storeMdWarnings(browsers, cwd) {
|
|
9020
|
+
const wantsFirefox = browsers.includes("firefox");
|
|
9021
|
+
const wantsEdge = browsers.includes("edge");
|
|
9022
|
+
if (!wantsFirefox && !wantsEdge) return [];
|
|
9023
|
+
let content;
|
|
9024
|
+
try {
|
|
9025
|
+
content = node_fs.readFileSync(node_path.join(cwd, "STORE.md"), "utf8");
|
|
9026
|
+
} catch {
|
|
9027
|
+
return [
|
|
9028
|
+
"No STORE.md found in the current working directory. Platform submissions read STORE.md from the project's source repository, so this may not apply here; make sure STORE.md exists there for Firefox reviewer notes and Edge certification notes. See the extension-dev skill's store-md reference."
|
|
9029
|
+
];
|
|
9030
|
+
}
|
|
9031
|
+
const hasField = (section, field)=>{
|
|
9032
|
+
const parts = content.split(/^## +/m);
|
|
9033
|
+
const match = parts.find((p)=>section.test(p.split("\n", 1)[0] ?? ""));
|
|
9034
|
+
if (!match) return false;
|
|
9035
|
+
const sub = match.split(/^### +/m).find((p)=>field.test(p.split("\n", 1)[0] ?? ""));
|
|
9036
|
+
if (!sub) return false;
|
|
9037
|
+
const body = sub.split("\n").slice(1).join("\n");
|
|
9038
|
+
return body.replace(/<!--[\s\S]*?-->/g, "").trim().length > 0;
|
|
9039
|
+
};
|
|
9040
|
+
const warnings = [];
|
|
9041
|
+
if (wantsFirefox && !hasField(/firefox|amo/i, /reviewer notes/i)) warnings.push("STORE.md has no Firefox reviewer notes; AMO reviews go faster with test credentials and steps.");
|
|
9042
|
+
if (wantsEdge && !hasField(/edge/i, /certification notes/i)) warnings.push("STORE.md has no Edge certification notes; the certification team gets no testing guidance.");
|
|
9043
|
+
return warnings;
|
|
9044
|
+
}
|
|
9045
|
+
const submit_schema = {
|
|
9046
|
+
name: "extension_submit",
|
|
9047
|
+
description: "SUBMIT FOR REVIEW: send a built extension to the Chrome Web Store, Firefox AMO, Edge Add-ons and/or the App Store (Safari) THROUGH extension.dev, which holds your store credentials and dispatches from your project's mirror CI. Store review only. It does NOT push a build to the extension.dev platform and 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. 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. dryRun:false actually submits, 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. extension_release_status lists valid shas and, after a real submission, reads the recorded outcome and review state.",
|
|
9048
|
+
inputSchema: {
|
|
9049
|
+
type: "object",
|
|
9050
|
+
properties: {
|
|
9051
|
+
browsers: {
|
|
9052
|
+
type: "array",
|
|
9053
|
+
items: {
|
|
9054
|
+
type: "string",
|
|
9055
|
+
enum: [
|
|
9056
|
+
"chrome",
|
|
9057
|
+
"firefox",
|
|
9058
|
+
"edge",
|
|
9059
|
+
"safari"
|
|
9060
|
+
]
|
|
9061
|
+
},
|
|
9062
|
+
description: "Stores to submit to."
|
|
9063
|
+
},
|
|
9064
|
+
buildSha: {
|
|
9065
|
+
type: "string",
|
|
9066
|
+
description: "The built commit SHA to submit. It needs a completed build in the project's build index; an unknown sha is rejected."
|
|
9067
|
+
},
|
|
9068
|
+
channel: {
|
|
9069
|
+
type: "string",
|
|
9070
|
+
description: "Release channel to submit from (default stable)."
|
|
9071
|
+
},
|
|
9072
|
+
version: {
|
|
9073
|
+
type: "string",
|
|
9074
|
+
description: "Version label for the submission record (optional)."
|
|
9075
|
+
},
|
|
9076
|
+
dryRun: {
|
|
9077
|
+
type: "boolean",
|
|
9078
|
+
default: true,
|
|
9079
|
+
description: "Preflight only. Pass false to actually dispatch (irreversible, enters store review)."
|
|
9080
|
+
},
|
|
9081
|
+
api: API_BASE
|
|
9082
|
+
},
|
|
9083
|
+
required: [
|
|
9084
|
+
"browsers",
|
|
9085
|
+
"buildSha"
|
|
9086
|
+
]
|
|
9087
|
+
}
|
|
9088
|
+
};
|
|
9089
|
+
function submit_fail(name, message) {
|
|
9090
|
+
return JSON.stringify({
|
|
9091
|
+
ok: false,
|
|
9092
|
+
error: {
|
|
9093
|
+
name,
|
|
9094
|
+
message
|
|
9095
|
+
}
|
|
9096
|
+
});
|
|
9097
|
+
}
|
|
9098
|
+
async function submit_handler(args) {
|
|
9099
|
+
const token = resolveToken();
|
|
9100
|
+
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).");
|
|
9101
|
+
const browsers = (Array.isArray(args.browsers) ? args.browsers : []).map((b)=>String(b).trim().toLowerCase()).filter(Boolean);
|
|
9102
|
+
if (0 === browsers.length) return submit_fail("SubmitInputError", 'browsers is required (e.g. ["chrome","firefox","edge","safari"]).');
|
|
9103
|
+
const buildSha = String(args.buildSha || "").trim();
|
|
9104
|
+
if (!buildSha) return submit_fail("SubmitInputError", "buildSha is required (the built commit to submit).");
|
|
9105
|
+
const apiCheck = safeApiBase(resolveApiBase(args.api));
|
|
9106
|
+
if (!apiCheck.ok) return submit_fail("SubmitConfigError", apiCheck.message);
|
|
9107
|
+
const url = `${apiCheck.base}/api/cli/stores/submit`;
|
|
9108
|
+
const dryRun = false !== args.dryRun;
|
|
9109
|
+
const body = {
|
|
9110
|
+
browsers,
|
|
9111
|
+
buildSha,
|
|
9112
|
+
dryRun
|
|
9113
|
+
};
|
|
9114
|
+
if (args.channel) body.channel = String(args.channel).trim();
|
|
9115
|
+
if (args.version) body.version = String(args.version).trim();
|
|
9116
|
+
let res;
|
|
9117
|
+
try {
|
|
9118
|
+
res = await fetch(url, {
|
|
9119
|
+
method: "POST",
|
|
9120
|
+
headers: {
|
|
9121
|
+
authorization: `Bearer ${token}`,
|
|
9122
|
+
"content-type": "application/json",
|
|
9123
|
+
...identityHeaders("extension_submit")
|
|
9124
|
+
},
|
|
9125
|
+
body: JSON.stringify(body)
|
|
9126
|
+
});
|
|
9127
|
+
} catch (err) {
|
|
9128
|
+
return submit_fail("SubmitNetworkError", `Could not reach ${url}: ${err?.message || err}`);
|
|
9129
|
+
}
|
|
9130
|
+
const text = await res.text();
|
|
9131
|
+
let data;
|
|
9132
|
+
try {
|
|
9133
|
+
data = JSON.parse(text);
|
|
9134
|
+
} catch {
|
|
9135
|
+
data = {
|
|
9136
|
+
message: text
|
|
9137
|
+
};
|
|
9138
|
+
}
|
|
9139
|
+
if (!res.ok) return submit_fail("SubmitError", `${dryRun ? "preflight" : "submit"} failed (${res.status}): ${data?.message || text || "unknown error"}`);
|
|
9140
|
+
const warnings = Array.isArray(data?.warnings) ? [
|
|
9141
|
+
...data.warnings
|
|
9142
|
+
] : [];
|
|
9143
|
+
warnings.push(...storeMdWarnings(browsers, process.cwd()));
|
|
9144
|
+
const result = {
|
|
9145
|
+
mode: "platform",
|
|
9146
|
+
dryRun,
|
|
9147
|
+
...data
|
|
9148
|
+
};
|
|
9149
|
+
if (dryRun) {
|
|
9150
|
+
const ref = resolveProjectRef();
|
|
9151
|
+
const consoleStoresUrl = consoleProjectUrl(ref, "stores", args.api);
|
|
9152
|
+
const storeModeNote = `Store publish mode (draft / skip-publish / live) is not readable with the CLI token, so it cannot be verified from here; check per-store settings at ${consoleStoresUrl}.`;
|
|
9153
|
+
let health = null;
|
|
9154
|
+
let healthUnreadable = null;
|
|
9155
|
+
let channelRows = null;
|
|
9156
|
+
if (ref) {
|
|
9157
|
+
const [healthRes, channelsRes] = await Promise.all([
|
|
9158
|
+
fetchRegistryJson(registryFileUrl(ref, "stores/health.json"), fetch, {
|
|
9159
|
+
ref,
|
|
9160
|
+
api: args.api
|
|
9161
|
+
}),
|
|
9162
|
+
fetchRegistryJson(registryFileUrl(ref, "channels.json"), fetch, {
|
|
9163
|
+
ref,
|
|
9164
|
+
api: args.api
|
|
9165
|
+
})
|
|
9166
|
+
]);
|
|
9167
|
+
if (healthRes.ok) {
|
|
9168
|
+
const stores = healthRes.json?.stores;
|
|
9169
|
+
health = stores && "object" == typeof stores ? stores : null;
|
|
9170
|
+
if (!health) healthUnreadable = "stores/health.json had no stores map";
|
|
9171
|
+
} else healthUnreadable = healthRes.message;
|
|
9172
|
+
if (channelsRes.ok) channelRows = parseChannels(channelsRes.json);
|
|
9173
|
+
} else healthUnreadable = "no stored workspace/project to look up (run extension_auth)";
|
|
9174
|
+
const preflight = browsers.map((browser)=>{
|
|
9175
|
+
if (!health) return {
|
|
9176
|
+
browser,
|
|
9177
|
+
ok: false,
|
|
9178
|
+
configured: "unknown",
|
|
9179
|
+
publishMode: "unknown",
|
|
9180
|
+
reason: `Store configuration could not be read (${healthUnreadable}); verify the ${browser} store in the console before submitting.`
|
|
9181
|
+
};
|
|
9182
|
+
const row = health[browser];
|
|
9183
|
+
if (!row) return {
|
|
9184
|
+
browser,
|
|
9185
|
+
ok: false,
|
|
9186
|
+
configured: false,
|
|
9187
|
+
publishMode: "unknown",
|
|
9188
|
+
reason: `No ${browser} store is configured on this project; a real submission for ${browser} would fail. Configure it at ${consoleStoresUrl}.`
|
|
9189
|
+
};
|
|
9190
|
+
if (true !== row.ok) return {
|
|
9191
|
+
browser,
|
|
9192
|
+
ok: false,
|
|
9193
|
+
configured: false,
|
|
9194
|
+
publishMode: "unknown",
|
|
9195
|
+
reason: String(row.message || "").trim() || `The ${browser} store failed its last credential health check.`
|
|
9196
|
+
};
|
|
9197
|
+
return {
|
|
9198
|
+
browser,
|
|
9199
|
+
ok: true,
|
|
9200
|
+
configured: true,
|
|
9201
|
+
publishMode: "unknown"
|
|
9202
|
+
};
|
|
9203
|
+
});
|
|
9204
|
+
const actionable = preflight.filter((p)=>p.ok).map((p)=>p.browser);
|
|
9205
|
+
const blocked = preflight.filter((p)=>!p.ok);
|
|
9206
|
+
const channelDefaulted = !String(args.channel || "").trim();
|
|
9207
|
+
const resolvedChannel = String(data?.channel || "").trim() || (channelDefaulted ? "stable" : String(args.channel).trim());
|
|
9208
|
+
if (channelRows) {
|
|
9209
|
+
const exists = channelRows.some((r)=>r.channel === resolvedChannel || r.channel.endsWith(`-${resolvedChannel}`));
|
|
9210
|
+
if (!exists) warnings.push(`Channel "${resolvedChannel}"${channelDefaulted ? " (the default)" : ""} does not exist in this project's channels.json (existing: ${channelRows.map((r)=>r.channel).join(", ") || "none"}), so a real submission from it has no promoted build to serve. Promote a build there first (extension_release_promote) or pass an existing channel.`);
|
|
9211
|
+
}
|
|
9212
|
+
const summaryParts = [];
|
|
9213
|
+
if (actionable.length > 0) summaryParts.push(`Preflight passed for ${actionable.join(", ")}: the platform verified auth, the project, build ${data?.buildId ?? buildSha}, and the store workflow, and the store credentials passed their last health check.`);
|
|
9214
|
+
for (const p of blocked)summaryParts.push(`${p.browser}: ${"unknown" === p.configured ? "cannot be verified" : "NOT actionable"} - ${p.reason}`);
|
|
9215
|
+
summaryParts.push(storeModeNote);
|
|
9216
|
+
result.ok = actionable.length > 0;
|
|
9217
|
+
result.preflight = preflight;
|
|
9218
|
+
result.channel = resolvedChannel;
|
|
9219
|
+
result.channelDefaulted = channelDefaulted;
|
|
9220
|
+
if (channelDefaulted) result.channelNote = `channel: ${resolvedChannel} (default)`;
|
|
9221
|
+
result.consoleStoresUrl = consoleStoresUrl;
|
|
9222
|
+
if ("string" == typeof data?.message) result.platformMessage = data.message;
|
|
9223
|
+
result.message = summaryParts.join(" ");
|
|
9224
|
+
}
|
|
9225
|
+
if (!dryRun) result.statusNote = "Track this submission with extension_release_status: it reads the recorded outcome, per-store credential health, and review state from the public registry.";
|
|
9226
|
+
if (warnings.length > 0) result.warnings = warnings;
|
|
9409
9227
|
return JSON.stringify(result);
|
|
9410
9228
|
}
|
|
9411
9229
|
const ENGINE_COMPANION_IDS = new Set([
|
|
@@ -9475,7 +9293,7 @@ function doctor_readReadyContract(projectPath, browser) {
|
|
|
9475
9293
|
}
|
|
9476
9294
|
const doctor_schema = {
|
|
9477
9295
|
name: "extension_doctor",
|
|
9478
|
-
description: "Diagnose a dev session end-to-end: ready contract, dev-server process, control-port agreement, control channel, eval token, executor,
|
|
9296
|
+
description: "Diagnose a dev session end-to-end: ready contract, dev-server process, control-port agreement, control channel, eval token, executor, browser liveness. Returns one {check, status, detail, remediation?} per leg in dependency order; a 'skip' names the check that blocked it and is NOT a pass. Run this first when any act tool (storage/reload/eval/open) errors unexpectedly. Call with no projectPath for a pre-flight environment check (node, extension CLI, template cache) before any project exists.",
|
|
9479
9297
|
inputSchema: {
|
|
9480
9298
|
type: "object",
|
|
9481
9299
|
properties: {
|
|
@@ -9516,7 +9334,7 @@ async function environmentPreflight() {
|
|
|
9516
9334
|
checks.push({
|
|
9517
9335
|
check: "template-cache",
|
|
9518
9336
|
status: cacheExists ? "pass" : "warn",
|
|
9519
|
-
detail: cacheExists ? `Template catalog cached at ${cacheFile}` : "Template catalog not cached yet (
|
|
9337
|
+
detail: cacheExists ? `Template catalog cached at ${cacheFile}` : "Template catalog not cached yet (extension_templates will fetch it)"
|
|
9520
9338
|
});
|
|
9521
9339
|
const healthy = checks.every((c)=>"fail" !== c.status);
|
|
9522
9340
|
return JSON.stringify({
|
|
@@ -9660,26 +9478,20 @@ function wait_isAlive(pid) {
|
|
|
9660
9478
|
}
|
|
9661
9479
|
const wait_schema = {
|
|
9662
9480
|
name: "extension_wait",
|
|
9663
|
-
description: "Wait for a running dev or start session to be ready. Polls the ready.json contract
|
|
9481
|
+
description: "Wait for a running dev or start session to be ready. Polls the ready.json contract and reports compiled (the compiler finished), browserAttached (the runtime executor connected), and guestLoaded (the browser's OWN target list shows your extension). guestLoaded is the trustworthy load signal: it catches a silently rejected --load-extension that leaves ready.json stamped attached with empty logs; it is null when it could not be checked (no CDP port, e.g. a gecko session). Every result reports budgetMs and elapsedMs; on status:'timeout' call again to keep waiting on the same contract. In a noBrowser session it returns as soon as the compile lands instead of waiting for a browser that will never attach. Ports come from the contract, so they match what the server actually bound.",
|
|
9664
9482
|
inputSchema: {
|
|
9665
9483
|
type: "object",
|
|
9666
9484
|
properties: {
|
|
9667
|
-
projectPath:
|
|
9668
|
-
|
|
9669
|
-
description: "Path to the extension project root"
|
|
9670
|
-
},
|
|
9671
|
-
browser: {
|
|
9672
|
-
type: "string",
|
|
9673
|
-
description: "Browser to check readiness for. Defaults to the active dev session's browser for this project."
|
|
9674
|
-
},
|
|
9485
|
+
projectPath: PROJECT_PATH,
|
|
9486
|
+
browser: SESSION_BROWSER,
|
|
9675
9487
|
timeoutMs: {
|
|
9676
9488
|
type: "number",
|
|
9677
9489
|
default: DEFAULT_TIMEOUT_MS,
|
|
9678
|
-
description: `Wait budget
|
|
9490
|
+
description: `Wait budget for this call. Default ${DEFAULT_TIMEOUT_MS}, clamped to ${MIN_TIMEOUT_MS}-${SAFE_CEILING_MS} so one call stays under the client's 60s request timeout. On timeout, call again to keep waiting.`
|
|
9679
9491
|
},
|
|
9680
9492
|
timeout: {
|
|
9681
9493
|
type: "number",
|
|
9682
|
-
description: "Deprecated alias of timeoutMs,
|
|
9494
|
+
description: "Deprecated alias of timeoutMs, which wins when both are given."
|
|
9683
9495
|
}
|
|
9684
9496
|
},
|
|
9685
9497
|
required: [
|
|
@@ -9718,7 +9530,7 @@ async function wait_handler(args) {
|
|
|
9718
9530
|
buildOnly: true,
|
|
9719
9531
|
compiled: true,
|
|
9720
9532
|
browserAttached: false,
|
|
9721
|
-
message: "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/
|
|
9533
|
+
message: "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.",
|
|
9722
9534
|
command: contract.command,
|
|
9723
9535
|
browser: contract.browser,
|
|
9724
9536
|
port: contract.port,
|
|
@@ -9809,10 +9621,7 @@ const add_feature_schema = {
|
|
|
9809
9621
|
inputSchema: {
|
|
9810
9622
|
type: "object",
|
|
9811
9623
|
properties: {
|
|
9812
|
-
projectPath:
|
|
9813
|
-
type: "string",
|
|
9814
|
-
description: "Path to the extension project root"
|
|
9815
|
-
},
|
|
9624
|
+
projectPath: PROJECT_PATH,
|
|
9816
9625
|
feature: {
|
|
9817
9626
|
type: "string",
|
|
9818
9627
|
enum: [
|
|
@@ -10066,78 +9875,6 @@ async function add_feature_handler(args) {
|
|
|
10066
9875
|
hint: conflicts.length ? `Warning: ${conflicts.length} file(s) already exist and would be overwritten.` : "No conflicts detected. Safe to create all files."
|
|
10067
9876
|
});
|
|
10068
9877
|
}
|
|
10069
|
-
const GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
|
|
10070
|
-
const GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
|
|
10071
|
-
const DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
10072
|
-
const defaultSleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
10073
|
-
async function startDeviceCode(args) {
|
|
10074
|
-
const doFetch = args.fetchImpl ?? fetch;
|
|
10075
|
-
const res = await doFetch(GITHUB_DEVICE_CODE_URL, {
|
|
10076
|
-
method: "POST",
|
|
10077
|
-
headers: {
|
|
10078
|
-
accept: "application/json",
|
|
10079
|
-
"content-type": "application/json"
|
|
10080
|
-
},
|
|
10081
|
-
body: JSON.stringify({
|
|
10082
|
-
client_id: args.clientId,
|
|
10083
|
-
scope: args.scope || "read:user"
|
|
10084
|
-
})
|
|
10085
|
-
});
|
|
10086
|
-
const data = await res.json().catch(()=>({}));
|
|
10087
|
-
if (!res.ok || data.error) throw new Error(`GitHub device-code request failed: ${data.error_description || data.error || res.status}`);
|
|
10088
|
-
return {
|
|
10089
|
-
deviceCode: String(data.device_code || ""),
|
|
10090
|
-
userCode: String(data.user_code || ""),
|
|
10091
|
-
verificationUri: String(data.verification_uri || "https://github.com/login/device"),
|
|
10092
|
-
interval: Number(data.interval || 5),
|
|
10093
|
-
expiresIn: Number(data.expires_in || 900)
|
|
10094
|
-
};
|
|
10095
|
-
}
|
|
10096
|
-
async function pollForToken(args) {
|
|
10097
|
-
const doFetch = args.fetchImpl ?? fetch;
|
|
10098
|
-
const sleep = args.sleepImpl ?? defaultSleep;
|
|
10099
|
-
let intervalMs = 1000 * Math.max(1, args.interval);
|
|
10100
|
-
const deadline = Date.now() + Math.max(0, args.budgetMs);
|
|
10101
|
-
while(Date.now() < deadline){
|
|
10102
|
-
await sleep(intervalMs);
|
|
10103
|
-
const res = await doFetch(GITHUB_TOKEN_URL, {
|
|
10104
|
-
method: "POST",
|
|
10105
|
-
headers: {
|
|
10106
|
-
accept: "application/json",
|
|
10107
|
-
"content-type": "application/json"
|
|
10108
|
-
},
|
|
10109
|
-
body: JSON.stringify({
|
|
10110
|
-
client_id: args.clientId,
|
|
10111
|
-
device_code: args.deviceCode,
|
|
10112
|
-
grant_type: DEVICE_GRANT_TYPE
|
|
10113
|
-
})
|
|
10114
|
-
});
|
|
10115
|
-
const data = await res.json().catch(()=>({}));
|
|
10116
|
-
const token = String(data.access_token || "").trim();
|
|
10117
|
-
if (token) return {
|
|
10118
|
-
ok: true,
|
|
10119
|
-
githubToken: token
|
|
10120
|
-
};
|
|
10121
|
-
const error = String(data.error || "");
|
|
10122
|
-
if ("authorization_pending" === error) continue;
|
|
10123
|
-
if ("slow_down" === error) {
|
|
10124
|
-
intervalMs = Math.max(intervalMs + 5000, 1000 * Number(data.interval || 0));
|
|
10125
|
-
continue;
|
|
10126
|
-
}
|
|
10127
|
-
if ("expired_token" === error) return {
|
|
10128
|
-
ok: false,
|
|
10129
|
-
reason: "expired"
|
|
10130
|
-
};
|
|
10131
|
-
if ("access_denied" === error) return {
|
|
10132
|
-
ok: false,
|
|
10133
|
-
reason: "denied"
|
|
10134
|
-
};
|
|
10135
|
-
}
|
|
10136
|
-
return {
|
|
10137
|
-
ok: false,
|
|
10138
|
-
reason: "pending"
|
|
10139
|
-
};
|
|
10140
|
-
}
|
|
10141
9878
|
async function requestDeviceCode(args) {
|
|
10142
9879
|
const doFetch = args.fetchImpl ?? fetch;
|
|
10143
9880
|
const res = await doFetch(`${args.apiBase}${args.path}`, {
|
|
@@ -10201,8 +9938,7 @@ async function pollDeviceToken(args) {
|
|
|
10201
9938
|
if (res.ok && data.token) {
|
|
10202
9939
|
const creds = persistTokenResponse({
|
|
10203
9940
|
apiBase: args.apiBase,
|
|
10204
|
-
data
|
|
10205
|
-
provider: "extensiondev"
|
|
9941
|
+
data
|
|
10206
9942
|
});
|
|
10207
9943
|
return {
|
|
10208
9944
|
ok: true,
|
|
@@ -10231,30 +9967,6 @@ async function pollDeviceToken(args) {
|
|
|
10231
9967
|
await new Promise((r)=>setTimeout(r, 1000 * interval));
|
|
10232
9968
|
}
|
|
10233
9969
|
}
|
|
10234
|
-
const login_schema = {
|
|
10235
|
-
name: "extension_login",
|
|
10236
|
-
description: "Authenticate to extension.dev and store a project-scoped access token locally so extension_publish can use it. Two-phase: call with `project` to get a code + URL for the user to authorize, then call again with the returned `deviceCode` to finish. The server picks the flow: the extension.dev-gated device flow (you authorize at extension.dev/device; GitHub federation happens server-side, no GitHub token on this machine) or, as a fallback, the legacy GitHub device flow. Never returns the token. Minted tokens live at most 7 days (server-enforced), so CI pipelines must re-mint before expiry on the console's Access tokens page (project settings -> Access tokens). This is the only tool besides extension_publish that talks to the hosted platform.",
|
|
10237
|
-
inputSchema: {
|
|
10238
|
-
type: "object",
|
|
10239
|
-
properties: {
|
|
10240
|
-
project: {
|
|
10241
|
-
type: "string",
|
|
10242
|
-
description: "Target project as '<workspace>/<project>' (the token is scoped to it)"
|
|
10243
|
-
},
|
|
10244
|
-
deviceCode: {
|
|
10245
|
-
type: "string",
|
|
10246
|
-
description: "Resume token from a prior call's `deviceCode`; omit on the first call"
|
|
10247
|
-
},
|
|
10248
|
-
api: {
|
|
10249
|
-
type: "string",
|
|
10250
|
-
description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
|
|
10251
|
-
}
|
|
10252
|
-
},
|
|
10253
|
-
required: [
|
|
10254
|
-
"project"
|
|
10255
|
-
]
|
|
10256
|
-
}
|
|
10257
|
-
};
|
|
10258
9970
|
const FIRST_CALL_BUDGET_MS = 8000;
|
|
10259
9971
|
const RESUME_BUDGET_MS = 22000;
|
|
10260
9972
|
function login_fail(name, message) {
|
|
@@ -10281,7 +9993,7 @@ function success(creds) {
|
|
|
10281
9993
|
function login_pending(start) {
|
|
10282
9994
|
const complete = String(start.verificationUriComplete || "").trim();
|
|
10283
9995
|
const hasCompleteLink = complete.length > 0 && complete !== start.verificationUri;
|
|
10284
|
-
const message = hasCompleteLink ? `Open ${complete} and approve (code ${start.userCode} is pre-filled), then call
|
|
9996
|
+
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.`;
|
|
10285
9997
|
return JSON.stringify({
|
|
10286
9998
|
ok: false,
|
|
10287
9999
|
status: "authorization_pending",
|
|
@@ -10302,10 +10014,10 @@ function resumePending(deviceCode, verificationUri) {
|
|
|
10302
10014
|
verificationUri,
|
|
10303
10015
|
deviceCode,
|
|
10304
10016
|
tokenTtlNote: "Once authorized, the minted token lives at most 7 days (server-enforced); CI must re-mint before expiry (console: project settings -> Access tokens).",
|
|
10305
|
-
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
|
|
10017
|
+
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.`
|
|
10306
10018
|
});
|
|
10307
10019
|
}
|
|
10308
|
-
async function
|
|
10020
|
+
async function loginToProject(args) {
|
|
10309
10021
|
const project = String(args.project || "").trim();
|
|
10310
10022
|
if (!/^[^/]+\/[^/]+$/.test(project)) return login_fail("BadRequest", "project must be in the form '<workspace>/<project>'.");
|
|
10311
10023
|
const apiBase = resolveApiBase(args.api);
|
|
@@ -10315,114 +10027,54 @@ async function login_handler(args) {
|
|
|
10315
10027
|
} catch (err) {
|
|
10316
10028
|
return login_fail("LoginConfigError", err?.message || "Could not load login config.");
|
|
10317
10029
|
}
|
|
10318
|
-
if (
|
|
10319
|
-
if (args.deviceCode) {
|
|
10320
|
-
const poll = await pollDeviceToken({
|
|
10321
|
-
apiBase,
|
|
10322
|
-
path: config.deviceTokenUrl,
|
|
10323
|
-
project,
|
|
10324
|
-
deviceCode: String(args.deviceCode),
|
|
10325
|
-
interval: 5,
|
|
10326
|
-
budgetMs: RESUME_BUDGET_MS
|
|
10327
|
-
});
|
|
10328
|
-
if (poll.ok) return success(poll.creds);
|
|
10329
|
-
if ("expired" === poll.reason) return login_fail("LoginExpired", "The device code expired. Run extension_login again to restart.");
|
|
10330
|
-
if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied at extension.dev/device.");
|
|
10331
|
-
if ("error" === poll.reason) return login_fail("LoginError", poll.message || "Device login failed.");
|
|
10332
|
-
return resumePending(String(args.deviceCode), config.verificationUri);
|
|
10333
|
-
}
|
|
10334
|
-
let start;
|
|
10335
|
-
try {
|
|
10336
|
-
start = await requestDeviceCode({
|
|
10337
|
-
apiBase,
|
|
10338
|
-
path: config.deviceCodeUrl,
|
|
10339
|
-
project
|
|
10340
|
-
});
|
|
10341
|
-
} catch (err) {
|
|
10342
|
-
return login_fail("LoginStartError", err?.message || "Could not start the device flow.");
|
|
10343
|
-
}
|
|
10030
|
+
if (args.deviceCode) {
|
|
10344
10031
|
const poll = await pollDeviceToken({
|
|
10345
10032
|
apiBase,
|
|
10346
10033
|
path: config.deviceTokenUrl,
|
|
10347
10034
|
project,
|
|
10348
|
-
deviceCode: start.deviceCode,
|
|
10349
|
-
interval: start.interval,
|
|
10350
|
-
budgetMs: FIRST_CALL_BUDGET_MS
|
|
10351
|
-
});
|
|
10352
|
-
if (poll.ok) return success(poll.creds);
|
|
10353
|
-
if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied at extension.dev/device.");
|
|
10354
|
-
return login_pending({
|
|
10355
|
-
deviceCode: start.deviceCode,
|
|
10356
|
-
userCode: start.userCode,
|
|
10357
|
-
verificationUri: start.verificationUri,
|
|
10358
|
-
verificationUriComplete: start.verificationUriComplete
|
|
10359
|
-
});
|
|
10360
|
-
}
|
|
10361
|
-
if (args.deviceCode) {
|
|
10362
|
-
const poll = await pollForToken({
|
|
10363
|
-
clientId: config.clientId,
|
|
10364
10035
|
deviceCode: String(args.deviceCode),
|
|
10365
10036
|
interval: 5,
|
|
10366
10037
|
budgetMs: RESUME_BUDGET_MS
|
|
10367
10038
|
});
|
|
10368
|
-
if (
|
|
10369
|
-
|
|
10370
|
-
|
|
10371
|
-
|
|
10372
|
-
|
|
10373
|
-
try {
|
|
10374
|
-
const creds = await exchangeAndPersist({
|
|
10375
|
-
apiBase,
|
|
10376
|
-
githubToken: poll.githubToken,
|
|
10377
|
-
project
|
|
10378
|
-
});
|
|
10379
|
-
return success(creds);
|
|
10380
|
-
} catch (err) {
|
|
10381
|
-
return login_fail("LoginExchangeError", err?.message || "Token exchange failed.");
|
|
10382
|
-
}
|
|
10039
|
+
if (poll.ok) return success(poll.creds);
|
|
10040
|
+
if ("expired" === poll.reason) return login_fail("LoginExpired", "The device code expired. Run extension_auth (action: login) again to restart.");
|
|
10041
|
+
if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied at extension.dev/device.");
|
|
10042
|
+
if ("error" === poll.reason) return login_fail("LoginError", poll.message || "Device login failed.");
|
|
10043
|
+
return resumePending(String(args.deviceCode), config.verificationUri);
|
|
10383
10044
|
}
|
|
10384
10045
|
let start;
|
|
10385
10046
|
try {
|
|
10386
|
-
start = await
|
|
10387
|
-
|
|
10388
|
-
|
|
10047
|
+
start = await requestDeviceCode({
|
|
10048
|
+
apiBase,
|
|
10049
|
+
path: config.deviceCodeUrl,
|
|
10050
|
+
project
|
|
10389
10051
|
});
|
|
10390
10052
|
} catch (err) {
|
|
10391
|
-
return login_fail("LoginStartError", err?.message || "Could not start device flow.");
|
|
10053
|
+
return login_fail("LoginStartError", err?.message || "Could not start the device flow.");
|
|
10392
10054
|
}
|
|
10393
|
-
const poll = await
|
|
10394
|
-
|
|
10055
|
+
const poll = await pollDeviceToken({
|
|
10056
|
+
apiBase,
|
|
10057
|
+
path: config.deviceTokenUrl,
|
|
10058
|
+
project,
|
|
10395
10059
|
deviceCode: start.deviceCode,
|
|
10396
10060
|
interval: start.interval,
|
|
10397
10061
|
budgetMs: FIRST_CALL_BUDGET_MS
|
|
10398
10062
|
});
|
|
10399
|
-
if (poll.ok)
|
|
10400
|
-
|
|
10401
|
-
|
|
10402
|
-
|
|
10403
|
-
|
|
10404
|
-
|
|
10405
|
-
|
|
10406
|
-
}
|
|
10407
|
-
return login_fail("LoginExchangeError", err?.message || "Token exchange failed.");
|
|
10408
|
-
}
|
|
10409
|
-
if (!poll.ok && "denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied on GitHub.");
|
|
10410
|
-
return login_pending(start);
|
|
10063
|
+
if (poll.ok) return success(poll.creds);
|
|
10064
|
+
if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied at extension.dev/device.");
|
|
10065
|
+
return login_pending({
|
|
10066
|
+
deviceCode: start.deviceCode,
|
|
10067
|
+
userCode: start.userCode,
|
|
10068
|
+
verificationUri: start.verificationUri,
|
|
10069
|
+
verificationUriComplete: start.verificationUriComplete
|
|
10070
|
+
});
|
|
10411
10071
|
}
|
|
10412
|
-
|
|
10413
|
-
name: "extension_whoami",
|
|
10414
|
-
description: "Report the identity carried by the locally stored extension.dev token that extension_login minted (workspace/project scoped), plus its expiry, without revealing the token. The identity comes from that stored token alone; it does not change with the current working directory or whichever project folder you are in. The result records where the login was minted (apiRecordedAtLogin) without asserting a platform base URL: authenticated tools target their own api argument, EXTENSION_DEV_API_URL, or the production default. Returns logged-out status when no credentials are stored.",
|
|
10415
|
-
inputSchema: {
|
|
10416
|
-
type: "object",
|
|
10417
|
-
properties: {}
|
|
10418
|
-
}
|
|
10419
|
-
};
|
|
10420
|
-
async function whoami_handler() {
|
|
10072
|
+
async function readIdentity() {
|
|
10421
10073
|
const creds = readCredentials();
|
|
10422
10074
|
if (!creds) return JSON.stringify({
|
|
10423
10075
|
ok: true,
|
|
10424
10076
|
status: "logged-out",
|
|
10425
|
-
message: "No stored credentials. Run
|
|
10077
|
+
message: "No stored credentials. Run extension_auth (action: login) to authenticate."
|
|
10426
10078
|
});
|
|
10427
10079
|
const now = Math.floor(Date.now() / 1000);
|
|
10428
10080
|
const expired = Boolean(creds.expiresAt && creds.expiresAt <= now);
|
|
@@ -10431,7 +10083,7 @@ async function whoami_handler() {
|
|
|
10431
10083
|
const apiDiverges = Boolean(recordedApi) && recordedApi !== effectiveDefaultApi;
|
|
10432
10084
|
const envTokenSet = Boolean(String(process.env.EXTENSION_DEV_TOKEN || "").trim());
|
|
10433
10085
|
const messageParts = [];
|
|
10434
|
-
messageParts.push(expired ? "The stored token has expired. Run
|
|
10086
|
+
messageParts.push(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.`);
|
|
10435
10087
|
if (apiDiverges) messageParts.push(`This login was minted via ${recordedApi}, but authenticated tools do not read that recorded value: they target ${effectiveDefaultApi} unless given an api argument.`);
|
|
10436
10088
|
if (envTokenSet) messageParts.push("EXTENSION_DEV_TOKEN is set and takes precedence over this stored login for authenticated tools; this report describes only the stored login.");
|
|
10437
10089
|
return JSON.stringify({
|
|
@@ -10443,7 +10095,7 @@ async function whoami_handler() {
|
|
|
10443
10095
|
apiRecordedAtLogin: recordedApi
|
|
10444
10096
|
} : {},
|
|
10445
10097
|
apiDefault: effectiveDefaultApi,
|
|
10446
|
-
provider: creds.provider ?? "
|
|
10098
|
+
provider: creds.provider ?? "extensiondev",
|
|
10447
10099
|
expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
|
|
10448
10100
|
expiresInSeconds: creds.expiresAt ? creds.expiresAt - now : null,
|
|
10449
10101
|
expired,
|
|
@@ -10454,213 +10106,61 @@ async function whoami_handler() {
|
|
|
10454
10106
|
message: messageParts.join(" ")
|
|
10455
10107
|
});
|
|
10456
10108
|
}
|
|
10457
|
-
|
|
10458
|
-
|
|
10459
|
-
|
|
10460
|
-
|
|
10461
|
-
|
|
10462
|
-
|
|
10463
|
-
|
|
10464
|
-
|
|
10465
|
-
|
|
10466
|
-
|
|
10467
|
-
|
|
10468
|
-
|
|
10469
|
-
|
|
10470
|
-
|
|
10471
|
-
|
|
10472
|
-
return JSON.stringify({
|
|
10473
|
-
ok: true,
|
|
10474
|
-
cleared: result.cleared,
|
|
10475
|
-
...result.cleared && revokeUrl ? {
|
|
10476
|
-
revokeUrl
|
|
10477
|
-
} : {},
|
|
10478
|
-
message: result.cleared ? revokeUrl ? `Local credentials removed. The token stays valid server-side until it expires; revoke it now at ${revokeUrl} (takes about a minute to propagate).` : "Local credentials removed. The token stays valid server-side until it expires; revoke it from the project's access-tokens page if needed." : "No stored credentials to remove."
|
|
10479
|
-
});
|
|
10480
|
-
}
|
|
10481
|
-
const install_browser_schema = {
|
|
10482
|
-
name: "extension_install_browser",
|
|
10483
|
-
description: "Install a managed browser binary for extension testing. Useful in CI, Docker, or fresh environments where browsers are not pre-installed. This downloads ~580-625 MB in a single blocking call (30s+ on a fast link); on a slow network it can exceed a client's default request timeout, so allow a generous timeout when calling it.",
|
|
10484
|
-
inputSchema: {
|
|
10485
|
-
type: "object",
|
|
10486
|
-
properties: {
|
|
10487
|
-
browser: {
|
|
10488
|
-
type: "string",
|
|
10489
|
-
enum: [
|
|
10490
|
-
"chrome",
|
|
10491
|
-
"chromium",
|
|
10492
|
-
"edge",
|
|
10493
|
-
"firefox"
|
|
10494
|
-
],
|
|
10495
|
-
description: "Browser to install"
|
|
10496
|
-
}
|
|
10497
|
-
},
|
|
10498
|
-
required: [
|
|
10499
|
-
"browser"
|
|
10500
|
-
]
|
|
10501
|
-
}
|
|
10502
|
-
};
|
|
10503
|
-
async function install_browser_handler(args) {
|
|
10504
|
-
const start = Date.now();
|
|
10505
|
-
try {
|
|
10506
|
-
await extensionInstall({
|
|
10507
|
-
browser: args.browser
|
|
10508
|
-
});
|
|
10509
|
-
return JSON.stringify({
|
|
10510
|
-
status: "installed",
|
|
10511
|
-
browser: args.browser,
|
|
10512
|
-
duration: Date.now() - start,
|
|
10513
|
-
hint: `Browser "${args.browser}" is now available. Use extension_dev or extension_start with browser: "${args.browser}".`
|
|
10514
|
-
});
|
|
10515
|
-
} catch (err) {
|
|
10516
|
-
return JSON.stringify({
|
|
10517
|
-
status: "error",
|
|
10518
|
-
browser: args.browser,
|
|
10519
|
-
message: err instanceof Error ? err.message : String(err),
|
|
10520
|
-
duration: Date.now() - start,
|
|
10521
|
-
hint: "edge" === args.browser ? "Edge installation on Linux may require elevated privileges. Try using Chrome or Chromium instead." : "Check network connectivity and disk space. You can also install browsers manually."
|
|
10522
|
-
});
|
|
10523
|
-
}
|
|
10109
|
+
async function clearLocalCredentials() {
|
|
10110
|
+
const creds = readCredentials();
|
|
10111
|
+
const revokeUrl = creds?.workspaceSlug && creds?.projectSlug ? consoleProjectUrl({
|
|
10112
|
+
workspace: creds.workspaceSlug,
|
|
10113
|
+
project: creds.projectSlug
|
|
10114
|
+
}, "settings/access-tokens") : null;
|
|
10115
|
+
const result = clearCredentials();
|
|
10116
|
+
return JSON.stringify({
|
|
10117
|
+
ok: true,
|
|
10118
|
+
cleared: result.cleared,
|
|
10119
|
+
...result.cleared && revokeUrl ? {
|
|
10120
|
+
revokeUrl
|
|
10121
|
+
} : {},
|
|
10122
|
+
message: result.cleared ? revokeUrl ? `Local credentials removed. The token stays valid server-side until it expires; revoke it now at ${revokeUrl} (takes about a minute to propagate).` : "Local credentials removed. The token stays valid server-side until it expires; revoke it from the project's access-tokens page if needed." : "No stored credentials to remove."
|
|
10123
|
+
});
|
|
10524
10124
|
}
|
|
10525
|
-
const
|
|
10526
|
-
name: "
|
|
10527
|
-
description: "
|
|
10125
|
+
const auth_schema = {
|
|
10126
|
+
name: "extension_auth",
|
|
10127
|
+
description: "Sign this machine in to extension.dev, report that login, or clear it. action:'status' (default) names the workspace/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. action:'login' is two-phase: 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. action:'logout' deletes the local credentials only; the token stays valid server-side until revoked at the URL the response returns.",
|
|
10528
10128
|
inputSchema: {
|
|
10529
10129
|
type: "object",
|
|
10530
10130
|
properties: {
|
|
10531
|
-
|
|
10131
|
+
action: {
|
|
10532
10132
|
type: "string",
|
|
10533
10133
|
enum: [
|
|
10534
|
-
"
|
|
10535
|
-
"
|
|
10536
|
-
"
|
|
10537
|
-
"firefox"
|
|
10134
|
+
"status",
|
|
10135
|
+
"login",
|
|
10136
|
+
"logout"
|
|
10538
10137
|
],
|
|
10539
|
-
|
|
10138
|
+
default: "status"
|
|
10540
10139
|
},
|
|
10541
|
-
|
|
10542
|
-
type: "
|
|
10543
|
-
|
|
10544
|
-
|
|
10545
|
-
|
|
10140
|
+
project: {
|
|
10141
|
+
type: "string",
|
|
10142
|
+
description: "login: target project as '<workspace>/<project>'; the token is scoped to it."
|
|
10143
|
+
},
|
|
10144
|
+
deviceCode: {
|
|
10145
|
+
type: "string",
|
|
10146
|
+
description: "login: resume token from the prior call's `deviceCode`; omit on the first call."
|
|
10147
|
+
},
|
|
10148
|
+
api: API_BASE
|
|
10546
10149
|
},
|
|
10547
10150
|
required: []
|
|
10548
10151
|
}
|
|
10549
10152
|
};
|
|
10550
|
-
async function
|
|
10551
|
-
const
|
|
10552
|
-
if (
|
|
10553
|
-
|
|
10554
|
-
|
|
10555
|
-
|
|
10556
|
-
|
|
10557
|
-
await extensionUninstall({
|
|
10558
|
-
browser: args.browser,
|
|
10559
|
-
all: args.all
|
|
10560
|
-
});
|
|
10561
|
-
return JSON.stringify({
|
|
10562
|
-
status: "uninstalled",
|
|
10563
|
-
target: args.all ? "all" : args.browser,
|
|
10564
|
-
duration: Date.now() - start,
|
|
10565
|
-
hint: "Use extension_list_browsers to confirm what remains in the managed cache."
|
|
10566
|
-
});
|
|
10567
|
-
} catch (err) {
|
|
10568
|
-
return JSON.stringify({
|
|
10569
|
-
status: "error",
|
|
10570
|
-
target: args.all ? "all" : args.browser,
|
|
10571
|
-
message: err instanceof Error ? err.message : String(err),
|
|
10572
|
-
duration: Date.now() - start
|
|
10573
|
-
});
|
|
10574
|
-
}
|
|
10575
|
-
}
|
|
10576
|
-
const list_browsers_schema = {
|
|
10577
|
-
name: "extension_list_browsers",
|
|
10578
|
-
description: "List managed browser binaries installed by the extension.dev platform. Shows what browsers are available in the managed cache without checking system-installed browsers.",
|
|
10579
|
-
inputSchema: {
|
|
10580
|
-
type: "object",
|
|
10581
|
-
properties: {}
|
|
10582
|
-
}
|
|
10583
|
-
};
|
|
10584
|
-
const BROWSER_NAMES = [
|
|
10585
|
-
"chrome",
|
|
10586
|
-
"chromium",
|
|
10587
|
-
"edge",
|
|
10588
|
-
"firefox"
|
|
10589
|
-
];
|
|
10590
|
-
function getDirSize(dir) {
|
|
10591
|
-
let total = 0;
|
|
10592
|
-
try {
|
|
10593
|
-
for (const entry of readdirSync(dir, {
|
|
10594
|
-
withFileTypes: true
|
|
10595
|
-
})){
|
|
10596
|
-
const full = join(dir, entry.name);
|
|
10597
|
-
if (entry.isDirectory()) total += getDirSize(full);
|
|
10598
|
-
else try {
|
|
10599
|
-
total += statSync(full).size;
|
|
10600
|
-
} catch {}
|
|
10601
|
-
}
|
|
10602
|
-
} catch {}
|
|
10603
|
-
return total;
|
|
10604
|
-
}
|
|
10605
|
-
function list_browsers_formatBytes(bytes) {
|
|
10606
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
10607
|
-
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
10608
|
-
return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
10609
|
-
}
|
|
10610
|
-
async function list_browsers_handler() {
|
|
10611
|
-
const cacheRoot = getManagedBrowsersCacheRoot();
|
|
10612
|
-
const installed = [];
|
|
10613
|
-
for (const browser of BROWSER_NAMES){
|
|
10614
|
-
const browserDir = join(cacheRoot, browser);
|
|
10615
|
-
if (existsSync(browserDir)) {
|
|
10616
|
-
const size = getDirSize(browserDir);
|
|
10617
|
-
installed.push({
|
|
10618
|
-
browser,
|
|
10619
|
-
path: browserDir,
|
|
10620
|
-
size,
|
|
10621
|
-
sizeFormatted: list_browsers_formatBytes(size),
|
|
10622
|
-
engine: "firefox" === browser ? "gecko" : "chromium"
|
|
10623
|
-
});
|
|
10624
|
-
}
|
|
10625
|
-
}
|
|
10626
|
-
return JSON.stringify({
|
|
10627
|
-
cacheRoot,
|
|
10628
|
-
cacheExists: existsSync(cacheRoot),
|
|
10629
|
-
installed,
|
|
10630
|
-
availableToInstall: BROWSER_NAMES.filter((b)=>!installed.some((i)=>i.browser === b)),
|
|
10631
|
-
hint: 0 === installed.length ? "No managed browsers found. Use extension_install_browser to install one, or use a system-installed browser." : `${installed.length} managed browser(s) found. Use extension_detect_browsers for a full system scan.`
|
|
10153
|
+
async function auth_handler(args) {
|
|
10154
|
+
const action = args.action ?? "status";
|
|
10155
|
+
if ("logout" === action) return clearLocalCredentials();
|
|
10156
|
+
if ("login" === action) return loginToProject({
|
|
10157
|
+
project: String(args.project ?? ""),
|
|
10158
|
+
deviceCode: args.deviceCode,
|
|
10159
|
+
api: args.api
|
|
10632
10160
|
});
|
|
10161
|
+
return readIdentity();
|
|
10633
10162
|
}
|
|
10634
10163
|
const execFileAsync = promisify(execFile);
|
|
10635
|
-
const detect_browsers_schema = {
|
|
10636
|
-
name: "extension_detect_browsers",
|
|
10637
|
-
description: "Detect which browsers are available for extension development. Checks both system-installed and managed browsers, returning paths and capabilities for each.",
|
|
10638
|
-
inputSchema: {
|
|
10639
|
-
type: "object",
|
|
10640
|
-
properties: {
|
|
10641
|
-
browsers: {
|
|
10642
|
-
type: "array",
|
|
10643
|
-
items: {
|
|
10644
|
-
type: "string",
|
|
10645
|
-
enum: [
|
|
10646
|
-
"chrome",
|
|
10647
|
-
"chromium",
|
|
10648
|
-
"edge",
|
|
10649
|
-
"brave",
|
|
10650
|
-
"opera",
|
|
10651
|
-
"vivaldi",
|
|
10652
|
-
"yandex",
|
|
10653
|
-
"firefox",
|
|
10654
|
-
"waterfox",
|
|
10655
|
-
"librewolf",
|
|
10656
|
-
"safari"
|
|
10657
|
-
]
|
|
10658
|
-
},
|
|
10659
|
-
description: "Browsers to check. If omitted, checks all."
|
|
10660
|
-
}
|
|
10661
|
-
}
|
|
10662
|
-
}
|
|
10663
|
-
};
|
|
10664
10164
|
const ALL_BROWSERS = [
|
|
10665
10165
|
"chrome",
|
|
10666
10166
|
"chromium",
|
|
@@ -10908,8 +10408,8 @@ async function getVersion(binaryPath, browser) {
|
|
|
10908
10408
|
return null;
|
|
10909
10409
|
}
|
|
10910
10410
|
}
|
|
10911
|
-
async function
|
|
10912
|
-
const browsersToCheck =
|
|
10411
|
+
async function detectBrowsers(browsers) {
|
|
10412
|
+
const browsersToCheck = browsers ?? [
|
|
10913
10413
|
...ALL_BROWSERS
|
|
10914
10414
|
];
|
|
10915
10415
|
const detected = [];
|
|
@@ -10951,8 +10451,161 @@ async function detect_browsers_handler(args) {
|
|
|
10951
10451
|
available: available.map((d)=>d.browser),
|
|
10952
10452
|
missing: missing.map((d)=>d.browser)
|
|
10953
10453
|
},
|
|
10954
|
-
hint: missing.length ? `Missing browser(s): ${missing.map((d)=>d.browser).join(", ")}.${missing.some((d)=>MANAGED_INSTALLABLE.has(d.browser)) ? ` Use
|
|
10454
|
+
hint: missing.length ? `Missing browser(s): ${missing.map((d)=>d.browser).join(", ")}.${missing.some((d)=>MANAGED_INSTALLABLE.has(d.browser)) ? ` Use extension_browsers with action: "install" to install ${missing.filter((d)=>MANAGED_INSTALLABLE.has(d.browser)).map((d)=>d.browser).join(", ")}.` : ""}` : "All requested browsers are available."
|
|
10455
|
+
});
|
|
10456
|
+
}
|
|
10457
|
+
const BROWSER_NAMES = [
|
|
10458
|
+
"chrome",
|
|
10459
|
+
"chromium",
|
|
10460
|
+
"edge",
|
|
10461
|
+
"firefox"
|
|
10462
|
+
];
|
|
10463
|
+
function getDirSize(dir) {
|
|
10464
|
+
let total = 0;
|
|
10465
|
+
try {
|
|
10466
|
+
for (const entry of readdirSync(dir, {
|
|
10467
|
+
withFileTypes: true
|
|
10468
|
+
})){
|
|
10469
|
+
const full = join(dir, entry.name);
|
|
10470
|
+
if (entry.isDirectory()) total += getDirSize(full);
|
|
10471
|
+
else try {
|
|
10472
|
+
total += statSync(full).size;
|
|
10473
|
+
} catch {}
|
|
10474
|
+
}
|
|
10475
|
+
} catch {}
|
|
10476
|
+
return total;
|
|
10477
|
+
}
|
|
10478
|
+
function list_browsers_formatBytes(bytes) {
|
|
10479
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
10480
|
+
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
10481
|
+
return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
10482
|
+
}
|
|
10483
|
+
async function listManagedBrowsers() {
|
|
10484
|
+
const cacheRoot = getManagedBrowsersCacheRoot();
|
|
10485
|
+
const installed = [];
|
|
10486
|
+
for (const browser of BROWSER_NAMES){
|
|
10487
|
+
const browserDir = join(cacheRoot, browser);
|
|
10488
|
+
if (existsSync(browserDir)) {
|
|
10489
|
+
const size = getDirSize(browserDir);
|
|
10490
|
+
installed.push({
|
|
10491
|
+
browser,
|
|
10492
|
+
path: browserDir,
|
|
10493
|
+
size,
|
|
10494
|
+
sizeFormatted: list_browsers_formatBytes(size),
|
|
10495
|
+
engine: "firefox" === browser ? "gecko" : "chromium"
|
|
10496
|
+
});
|
|
10497
|
+
}
|
|
10498
|
+
}
|
|
10499
|
+
return JSON.stringify({
|
|
10500
|
+
cacheRoot,
|
|
10501
|
+
cacheExists: existsSync(cacheRoot),
|
|
10502
|
+
installed,
|
|
10503
|
+
availableToInstall: BROWSER_NAMES.filter((b)=>!installed.some((i)=>i.browser === b)),
|
|
10504
|
+
hint: 0 === installed.length ? "No managed browsers found. Use extension_browsers with action: \"install\" to install one, or use a system-installed browser." : `${installed.length} managed browser(s) found. Use extension_browsers with action: "detect" for a full system scan.`
|
|
10505
|
+
});
|
|
10506
|
+
}
|
|
10507
|
+
async function installManagedBrowser(browser) {
|
|
10508
|
+
const start = Date.now();
|
|
10509
|
+
try {
|
|
10510
|
+
await extensionInstall({
|
|
10511
|
+
browser
|
|
10512
|
+
});
|
|
10513
|
+
return JSON.stringify({
|
|
10514
|
+
status: "installed",
|
|
10515
|
+
browser,
|
|
10516
|
+
duration: Date.now() - start,
|
|
10517
|
+
hint: `Browser "${browser}" is now available. Use extension_dev or extension_start with browser: "${browser}".`
|
|
10518
|
+
});
|
|
10519
|
+
} catch (err) {
|
|
10520
|
+
return JSON.stringify({
|
|
10521
|
+
status: "error",
|
|
10522
|
+
browser,
|
|
10523
|
+
message: err instanceof Error ? err.message : String(err),
|
|
10524
|
+
duration: Date.now() - start,
|
|
10525
|
+
hint: "edge" === browser ? "Edge installation on Linux may require elevated privileges. Try using Chrome or Chromium instead." : "Check network connectivity and disk space. You can also install browsers manually."
|
|
10526
|
+
});
|
|
10527
|
+
}
|
|
10528
|
+
}
|
|
10529
|
+
async function uninstallManagedBrowser(args) {
|
|
10530
|
+
const start = Date.now();
|
|
10531
|
+
if (!args.browser && !args.all) return JSON.stringify({
|
|
10532
|
+
status: "error",
|
|
10533
|
+
message: "Provide a browser to remove, or set all: true."
|
|
10534
|
+
});
|
|
10535
|
+
try {
|
|
10536
|
+
await extensionUninstall({
|
|
10537
|
+
browser: args.browser,
|
|
10538
|
+
all: args.all
|
|
10539
|
+
});
|
|
10540
|
+
return JSON.stringify({
|
|
10541
|
+
status: "uninstalled",
|
|
10542
|
+
target: args.all ? "all" : args.browser,
|
|
10543
|
+
duration: Date.now() - start,
|
|
10544
|
+
hint: 'Use extension_browsers with action: "list" to confirm what remains in the managed cache.'
|
|
10545
|
+
});
|
|
10546
|
+
} catch (err) {
|
|
10547
|
+
return JSON.stringify({
|
|
10548
|
+
status: "error",
|
|
10549
|
+
target: args.all ? "all" : args.browser,
|
|
10550
|
+
message: err instanceof Error ? err.message : String(err),
|
|
10551
|
+
duration: Date.now() - start
|
|
10552
|
+
});
|
|
10553
|
+
}
|
|
10554
|
+
}
|
|
10555
|
+
const browsers_schema = {
|
|
10556
|
+
name: "extension_browsers",
|
|
10557
|
+
description: "Find, install, and remove the browsers extension tooling can launch. action:'detect' (default) scans BOTH system-installed and managed browsers and reports each one's binary path, version, engine, and debugger support. action:'list' reports only the managed cache this tool downloads into, with sizes on disk. action:'install' downloads a managed binary: ~580-625 MB in one blocking call, so allow a generous client timeout. action:'uninstall' removes managed binaries and never touches a system install.",
|
|
10558
|
+
inputSchema: {
|
|
10559
|
+
type: "object",
|
|
10560
|
+
properties: {
|
|
10561
|
+
action: {
|
|
10562
|
+
type: "string",
|
|
10563
|
+
enum: [
|
|
10564
|
+
"detect",
|
|
10565
|
+
"list",
|
|
10566
|
+
"install",
|
|
10567
|
+
"uninstall"
|
|
10568
|
+
],
|
|
10569
|
+
default: "detect"
|
|
10570
|
+
},
|
|
10571
|
+
browsers: {
|
|
10572
|
+
type: "array",
|
|
10573
|
+
items: {
|
|
10574
|
+
type: "string",
|
|
10575
|
+
enum: REAL_BROWSERS
|
|
10576
|
+
},
|
|
10577
|
+
description: "detect: limit the scan to these. Omit to check all."
|
|
10578
|
+
},
|
|
10579
|
+
browser: {
|
|
10580
|
+
type: "string",
|
|
10581
|
+
enum: MANAGED_BROWSERS,
|
|
10582
|
+
description: "install/uninstall: which managed binary. Required for install."
|
|
10583
|
+
},
|
|
10584
|
+
all: {
|
|
10585
|
+
type: "boolean",
|
|
10586
|
+
default: false,
|
|
10587
|
+
description: "uninstall: remove every managed binary."
|
|
10588
|
+
}
|
|
10589
|
+
},
|
|
10590
|
+
required: []
|
|
10591
|
+
}
|
|
10592
|
+
};
|
|
10593
|
+
async function browsers_handler(args) {
|
|
10594
|
+
const action = args.action ?? "detect";
|
|
10595
|
+
if ("list" === action) return listManagedBrowsers();
|
|
10596
|
+
if ("install" === action) {
|
|
10597
|
+
if (!args.browser) return JSON.stringify({
|
|
10598
|
+
ok: false,
|
|
10599
|
+
status: "error",
|
|
10600
|
+
message: "action 'install' needs a browser: one of chrome, chromium, edge, firefox."
|
|
10601
|
+
});
|
|
10602
|
+
return installManagedBrowser(args.browser);
|
|
10603
|
+
}
|
|
10604
|
+
if ("uninstall" === action) return uninstallManagedBrowser({
|
|
10605
|
+
browser: args.browser,
|
|
10606
|
+
all: args.all
|
|
10955
10607
|
});
|
|
10608
|
+
return detectBrowsers(args.browsers);
|
|
10956
10609
|
}
|
|
10957
10610
|
function typeOf(value) {
|
|
10958
10611
|
if (Array.isArray(value)) return "array";
|
|
@@ -11120,40 +10773,32 @@ function inputValidationError(toolName, issues, inputSchema) {
|
|
|
11120
10773
|
}
|
|
11121
10774
|
const tools = [
|
|
11122
10775
|
create_namespaceObject,
|
|
11123
|
-
|
|
10776
|
+
templates_namespaceObject,
|
|
11124
10777
|
build_namespaceObject,
|
|
11125
10778
|
dev_namespaceObject,
|
|
11126
10779
|
start_namespaceObject,
|
|
11127
|
-
preview_namespaceObject,
|
|
11128
10780
|
preview_web_namespaceObject,
|
|
11129
10781
|
shares_namespaceObject,
|
|
11130
10782
|
stop_namespaceObject,
|
|
11131
|
-
get_template_source_namespaceObject,
|
|
11132
10783
|
manifest_validate_namespaceObject,
|
|
11133
10784
|
theme_verify_namespaceObject,
|
|
10785
|
+
analyze_namespaceObject,
|
|
11134
10786
|
inspect_namespaceObject,
|
|
11135
|
-
source_inspect_namespaceObject,
|
|
11136
10787
|
list_extensions_namespaceObject,
|
|
11137
10788
|
logs_namespaceObject,
|
|
11138
10789
|
eval_namespaceObject,
|
|
11139
10790
|
storage_namespaceObject,
|
|
11140
10791
|
reload_namespaceObject,
|
|
11141
10792
|
open_namespaceObject,
|
|
11142
|
-
|
|
10793
|
+
dom_snapshot_namespaceObject,
|
|
11143
10794
|
tools_publish_namespaceObject,
|
|
11144
|
-
|
|
10795
|
+
release_status_namespaceObject,
|
|
11145
10796
|
release_promote_namespaceObject,
|
|
11146
|
-
|
|
11147
|
-
store_status_namespaceObject,
|
|
10797
|
+
submit_namespaceObject,
|
|
11148
10798
|
wait_namespaceObject,
|
|
11149
10799
|
add_feature_namespaceObject,
|
|
11150
|
-
|
|
11151
|
-
|
|
11152
|
-
logout_namespaceObject,
|
|
11153
|
-
install_browser_namespaceObject,
|
|
11154
|
-
uninstall_browser_namespaceObject,
|
|
11155
|
-
list_browsers_namespaceObject,
|
|
11156
|
-
detect_browsers_namespaceObject,
|
|
10800
|
+
auth_namespaceObject,
|
|
10801
|
+
browsers_namespaceObject,
|
|
11157
10802
|
doctor_namespaceObject
|
|
11158
10803
|
];
|
|
11159
10804
|
const toolMap = new Map();
|
|
@@ -11235,7 +10880,7 @@ async function runCli(cmd, args) {
|
|
|
11235
10880
|
return i >= 0 ? args[i + 1] : void 0;
|
|
11236
10881
|
};
|
|
11237
10882
|
if ("whoami" === cmd) {
|
|
11238
|
-
log(await
|
|
10883
|
+
log(await readIdentity());
|
|
11239
10884
|
return 0;
|
|
11240
10885
|
}
|
|
11241
10886
|
if ("release" === cmd) {
|
|
@@ -11267,7 +10912,7 @@ async function runCli(cmd, args) {
|
|
|
11267
10912
|
return 1;
|
|
11268
10913
|
}
|
|
11269
10914
|
if ("logout" === cmd) {
|
|
11270
|
-
log(await
|
|
10915
|
+
log(await clearLocalCredentials());
|
|
11271
10916
|
return 0;
|
|
11272
10917
|
}
|
|
11273
10918
|
if ("login" === cmd) {
|
|
@@ -11279,55 +10924,28 @@ async function runCli(cmd, args) {
|
|
|
11279
10924
|
const apiBase = resolveApiBase(flag("api"));
|
|
11280
10925
|
try {
|
|
11281
10926
|
const config = await fetchLoginConfig(apiBase);
|
|
11282
|
-
|
|
11283
|
-
|
|
11284
|
-
|
|
11285
|
-
|
|
11286
|
-
project
|
|
11287
|
-
});
|
|
11288
|
-
log("");
|
|
11289
|
-
log(` Open ${start.verificationUri} and enter code: ${start.userCode}`);
|
|
11290
|
-
log("");
|
|
11291
|
-
log(" Waiting for authorization...");
|
|
11292
|
-
const poll = await pollDeviceToken({
|
|
11293
|
-
apiBase,
|
|
11294
|
-
path: config.deviceTokenUrl,
|
|
11295
|
-
project,
|
|
11296
|
-
deviceCode: start.deviceCode,
|
|
11297
|
-
interval: start.interval,
|
|
11298
|
-
budgetMs: 1000 * start.expiresIn
|
|
11299
|
-
});
|
|
11300
|
-
if (!poll.ok) {
|
|
11301
|
-
log("denied" === poll.reason ? "Authorization was denied at extension.dev/device." : "expired" === poll.reason ? "The device code expired. Run login again." : "Timed out waiting for authorization. Run login again.");
|
|
11302
|
-
return 1;
|
|
11303
|
-
}
|
|
11304
|
-
log(`Logged in to ${poll.creds.workspaceSlug}/${poll.creds.projectSlug}.`);
|
|
11305
|
-
return 0;
|
|
11306
|
-
}
|
|
11307
|
-
const start = await startDeviceCode({
|
|
11308
|
-
clientId: config.clientId,
|
|
11309
|
-
scope: config.scope
|
|
10927
|
+
const start = await requestDeviceCode({
|
|
10928
|
+
apiBase,
|
|
10929
|
+
path: config.deviceCodeUrl,
|
|
10930
|
+
project
|
|
11310
10931
|
});
|
|
11311
10932
|
log("");
|
|
11312
10933
|
log(` Open ${start.verificationUri} and enter code: ${start.userCode}`);
|
|
11313
10934
|
log("");
|
|
11314
10935
|
log(" Waiting for authorization...");
|
|
11315
|
-
const poll = await
|
|
11316
|
-
|
|
10936
|
+
const poll = await pollDeviceToken({
|
|
10937
|
+
apiBase,
|
|
10938
|
+
path: config.deviceTokenUrl,
|
|
10939
|
+
project,
|
|
11317
10940
|
deviceCode: start.deviceCode,
|
|
11318
10941
|
interval: start.interval,
|
|
11319
10942
|
budgetMs: 1000 * start.expiresIn
|
|
11320
10943
|
});
|
|
11321
10944
|
if (!poll.ok) {
|
|
11322
|
-
log("denied" === poll.reason ? "Authorization was denied
|
|
10945
|
+
log("denied" === poll.reason ? "Authorization was denied at extension.dev/device." : "expired" === poll.reason ? "The device code expired. Run login again." : "Timed out waiting for authorization. Run login again.");
|
|
11323
10946
|
return 1;
|
|
11324
10947
|
}
|
|
11325
|
-
|
|
11326
|
-
apiBase,
|
|
11327
|
-
githubToken: poll.githubToken,
|
|
11328
|
-
project
|
|
11329
|
-
});
|
|
11330
|
-
log(`Logged in to ${creds.workspaceSlug}/${creds.projectSlug}.`);
|
|
10948
|
+
log(`Logged in to ${poll.creds.workspaceSlug}/${poll.creds.projectSlug}.`);
|
|
11331
10949
|
return 0;
|
|
11332
10950
|
} catch (err) {
|
|
11333
10951
|
log(err instanceof Error ? err.message : String(err));
|