@extension.dev/mcp 7.0.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 +1 -1
- package/CHANGELOG.md +224 -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 +1026 -1186
- package/dist/src/lib/common-schema.d.ts +28 -0
- package/dist/src/lib/launch-flags.d.ts +6 -6
- 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 -4
- 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.js +1 -9
- package/extensions/live-preview/chromium/background/service_worker.js +3 -11
- package/extensions/live-preview/chromium/manifest.json +1 -1
- 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/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":"7.0.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");
|
|
@@ -763,6 +713,111 @@ function persistTokenResponse(args) {
|
|
|
763
713
|
writeCredentials(creds);
|
|
764
714
|
return creds;
|
|
765
715
|
}
|
|
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() {
|
|
743
|
+
try {
|
|
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
|
+
};
|
|
757
|
+
} catch {
|
|
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
|
|
816
|
+
};
|
|
817
|
+
} catch {
|
|
818
|
+
return {};
|
|
819
|
+
}
|
|
820
|
+
}
|
|
766
821
|
function _define_property(obj, key, value) {
|
|
767
822
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
768
823
|
value: value,
|
|
@@ -830,7 +885,8 @@ class RegistryAccessTokens {
|
|
|
830
885
|
method: "POST",
|
|
831
886
|
headers: {
|
|
832
887
|
authorization: `Bearer ${token}`,
|
|
833
|
-
"content-type": "application/json"
|
|
888
|
+
"content-type": "application/json",
|
|
889
|
+
...identityHeaders("extension_registry_access")
|
|
834
890
|
},
|
|
835
891
|
body: JSON.stringify({
|
|
836
892
|
workspaceSlug: ref.workspace,
|
|
@@ -984,7 +1040,7 @@ async function fetchRegistryJson(url, fetchImpl = fetch, options) {
|
|
|
984
1040
|
};
|
|
985
1041
|
const grant = await tokens.get(ref, options?.api);
|
|
986
1042
|
if ("ok" !== grant.status) {
|
|
987
|
-
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;
|
|
988
1044
|
return {
|
|
989
1045
|
ok: false,
|
|
990
1046
|
status: res.status,
|
|
@@ -1090,7 +1146,7 @@ function detectPackageManager(projectPath) {
|
|
|
1090
1146
|
}
|
|
1091
1147
|
const create_schema = {
|
|
1092
1148
|
name: "extension_create",
|
|
1093
|
-
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.",
|
|
1094
1150
|
inputSchema: {
|
|
1095
1151
|
type: "object",
|
|
1096
1152
|
properties: {
|
|
@@ -1100,12 +1156,12 @@ const create_schema = {
|
|
|
1100
1156
|
},
|
|
1101
1157
|
parentDir: {
|
|
1102
1158
|
type: "string",
|
|
1103
|
-
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."
|
|
1104
1160
|
},
|
|
1105
1161
|
template: {
|
|
1106
1162
|
type: "string",
|
|
1107
1163
|
default: "typescript",
|
|
1108
|
-
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."
|
|
1109
1165
|
},
|
|
1110
1166
|
install: {
|
|
1111
1167
|
type: "boolean",
|
|
@@ -1204,7 +1260,7 @@ async function create_handler(args) {
|
|
|
1204
1260
|
defaultsApplied: {
|
|
1205
1261
|
parentDir: args.parentDir ? `${resolvedParent} (explicit)` : `${resolvedParent} (default: the MCP server process cwd, not yours; pass parentDir to choose)`,
|
|
1206
1262
|
...void 0 === args.template ? {
|
|
1207
|
-
template: "typescript (default;
|
|
1263
|
+
template: "typescript (default; call extension_templates to pick another, e.g. javascript for plain JS)"
|
|
1208
1264
|
} : {},
|
|
1209
1265
|
packageManager: `${packageManager} (auto-detected by the scaffolder, not asked)`,
|
|
1210
1266
|
browser: "chrome (default: extension_dev and extension_build target chrome unless you pass browser)",
|
|
@@ -1408,12 +1464,99 @@ async function getTemplateBySlug(slug) {
|
|
|
1408
1464
|
const meta = await fetchTemplatesMeta();
|
|
1409
1465
|
return meta.templates.find((t)=>t.slug === slug);
|
|
1410
1466
|
}
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
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.",
|
|
1414
1534
|
inputSchema: {
|
|
1415
1535
|
type: "object",
|
|
1416
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
|
+
},
|
|
1417
1560
|
surface: {
|
|
1418
1561
|
type: "string",
|
|
1419
1562
|
enum: [
|
|
@@ -1422,7 +1565,7 @@ const list_templates_schema = {
|
|
|
1422
1565
|
"newtab",
|
|
1423
1566
|
"background"
|
|
1424
1567
|
],
|
|
1425
|
-
description: "
|
|
1568
|
+
description: "list: filter by surface. For a popup/action starter use query:'action', not a surface."
|
|
1426
1569
|
},
|
|
1427
1570
|
framework: {
|
|
1428
1571
|
type: "string",
|
|
@@ -1433,46 +1576,104 @@ const list_templates_schema = {
|
|
|
1433
1576
|
"preact",
|
|
1434
1577
|
""
|
|
1435
1578
|
],
|
|
1436
|
-
description: "
|
|
1579
|
+
description: "list: UI framework filter (empty string = vanilla JS)."
|
|
1437
1580
|
},
|
|
1438
1581
|
tags: {
|
|
1439
1582
|
type: "array",
|
|
1440
1583
|
items: {
|
|
1441
1584
|
type: "string"
|
|
1442
1585
|
},
|
|
1443
|
-
description: "
|
|
1586
|
+
description: "list: filter by tags, e.g. ['ai', 'chat']."
|
|
1444
1587
|
},
|
|
1445
1588
|
featured: {
|
|
1446
1589
|
type: "boolean",
|
|
1447
|
-
description: "
|
|
1448
|
-
},
|
|
1449
|
-
query: {
|
|
1450
|
-
type: "string",
|
|
1451
|
-
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."
|
|
1452
1591
|
}
|
|
1453
|
-
}
|
|
1592
|
+
},
|
|
1593
|
+
required: []
|
|
1454
1594
|
}
|
|
1455
1595
|
};
|
|
1456
|
-
async function
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
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
|
|
1474
1614
|
});
|
|
1475
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
|
+
};
|
|
1476
1677
|
const PINNED_CLI_VERSION = String(package_namespaceObject.El.OP ?? "latest").replace(/^[\^~]/, "");
|
|
1477
1678
|
function pinnedCliVersion() {
|
|
1478
1679
|
const override = String(process.env.EXTENSION_MCP_CLI_VERSION || "").trim();
|
|
@@ -1968,11 +2169,11 @@ function materializeCarrier(projectPath, browser) {
|
|
|
1968
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.",
|
|
1969
2170
|
"Chromium-family only: Firefox has no externally_connectable channel for web pages."
|
|
1970
2171
|
],
|
|
1971
|
-
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.",
|
|
1972
2173
|
...carrierId ? {
|
|
1973
2174
|
bridgeProtocol: {
|
|
1974
2175
|
carrierExtensionId: carrierId,
|
|
1975
|
-
allowedOrigins: "https://preview.extension.dev, https://
|
|
2176
|
+
allowedOrigins: "https://preview.extension.dev, https://code.extension.dev, https://themes.extension.dev, http://localhost/*, http://127.0.0.1/*",
|
|
1976
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.",
|
|
1977
2178
|
example: [
|
|
1978
2179
|
`const id = '${carrierId}'`,
|
|
@@ -2092,32 +2293,8 @@ const build_schema = {
|
|
|
2092
2293
|
inputSchema: {
|
|
2093
2294
|
type: "object",
|
|
2094
2295
|
properties: {
|
|
2095
|
-
projectPath:
|
|
2096
|
-
|
|
2097
|
-
description: "Path to the extension project root"
|
|
2098
|
-
},
|
|
2099
|
-
browser: {
|
|
2100
|
-
type: "string",
|
|
2101
|
-
enum: [
|
|
2102
|
-
"chrome",
|
|
2103
|
-
"chromium",
|
|
2104
|
-
"edge",
|
|
2105
|
-
"brave",
|
|
2106
|
-
"opera",
|
|
2107
|
-
"vivaldi",
|
|
2108
|
-
"yandex",
|
|
2109
|
-
"firefox",
|
|
2110
|
-
"waterfox",
|
|
2111
|
-
"librewolf",
|
|
2112
|
-
"safari",
|
|
2113
|
-
"chromium-based",
|
|
2114
|
-
"gecko-based",
|
|
2115
|
-
"firefox-based",
|
|
2116
|
-
"webkit-based"
|
|
2117
|
-
],
|
|
2118
|
-
default: "chrome",
|
|
2119
|
-
description: "Target browser"
|
|
2120
|
-
},
|
|
2296
|
+
projectPath: PROJECT_PATH,
|
|
2297
|
+
browser: LAUNCH_BROWSER,
|
|
2121
2298
|
zip: {
|
|
2122
2299
|
type: "boolean",
|
|
2123
2300
|
default: false,
|
|
@@ -2155,7 +2332,7 @@ const build_schema = {
|
|
|
2155
2332
|
skipValidation: {
|
|
2156
2333
|
type: "boolean",
|
|
2157
2334
|
default: false,
|
|
2158
|
-
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."
|
|
2159
2336
|
}
|
|
2160
2337
|
},
|
|
2161
2338
|
required: [
|
|
@@ -2377,22 +2554,19 @@ async function build_handler(args) {
|
|
|
2377
2554
|
}
|
|
2378
2555
|
const stop_schema = {
|
|
2379
2556
|
name: "extension_stop",
|
|
2380
|
-
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.",
|
|
2381
2558
|
inputSchema: {
|
|
2382
2559
|
type: "object",
|
|
2383
2560
|
properties: {
|
|
2384
|
-
projectPath:
|
|
2385
|
-
type: "string",
|
|
2386
|
-
description: "Path to the extension project root"
|
|
2387
|
-
},
|
|
2561
|
+
projectPath: PROJECT_PATH,
|
|
2388
2562
|
browser: {
|
|
2389
2563
|
type: "string",
|
|
2390
|
-
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."
|
|
2391
2565
|
},
|
|
2392
2566
|
all: {
|
|
2393
2567
|
type: "boolean",
|
|
2394
2568
|
default: false,
|
|
2395
|
-
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."
|
|
2396
2570
|
}
|
|
2397
2571
|
},
|
|
2398
2572
|
required: []
|
|
@@ -2544,7 +2718,7 @@ async function stop_handler(args) {
|
|
|
2544
2718
|
const LAUNCH_FLAG_SCHEMA = {
|
|
2545
2719
|
profile: {
|
|
2546
2720
|
type: "string",
|
|
2547
|
-
description: '
|
|
2721
|
+
description: 'Profile path, or "false" to reuse the real user profile. Omit for a throwaway one.'
|
|
2548
2722
|
},
|
|
2549
2723
|
startingUrl: {
|
|
2550
2724
|
type: "string",
|
|
@@ -2552,26 +2726,26 @@ const LAUNCH_FLAG_SCHEMA = {
|
|
|
2552
2726
|
},
|
|
2553
2727
|
chromiumBinary: {
|
|
2554
2728
|
type: "string",
|
|
2555
|
-
description: "
|
|
2729
|
+
description: "Custom Chromium-based binary (overrides browser)"
|
|
2556
2730
|
},
|
|
2557
2731
|
geckoBinary: {
|
|
2558
2732
|
type: "string",
|
|
2559
|
-
description: "
|
|
2733
|
+
description: "Custom Gecko/Firefox binary (overrides browser)"
|
|
2560
2734
|
},
|
|
2561
2735
|
host: {
|
|
2562
2736
|
type: "string",
|
|
2563
|
-
description: "
|
|
2737
|
+
description: "Bind host, default 127.0.0.1. Use 0.0.0.0 in Docker or devcontainers."
|
|
2564
2738
|
},
|
|
2565
2739
|
publicHost: {
|
|
2566
2740
|
type: "string",
|
|
2567
|
-
description: "
|
|
2741
|
+
description: "Host the browser dials for HMR and reload when it differs from the bind host"
|
|
2568
2742
|
},
|
|
2569
2743
|
extensions: {
|
|
2570
2744
|
type: "array",
|
|
2571
2745
|
items: {
|
|
2572
2746
|
type: "string"
|
|
2573
2747
|
},
|
|
2574
|
-
description: "
|
|
2748
|
+
description: "Extra extension paths or store URLs to load alongside the project"
|
|
2575
2749
|
}
|
|
2576
2750
|
};
|
|
2577
2751
|
function launchFlagArgs(args) {
|
|
@@ -2587,35 +2761,12 @@ function launchFlagArgs(args) {
|
|
|
2587
2761
|
}
|
|
2588
2762
|
const dev_schema = {
|
|
2589
2763
|
name: "extension_dev",
|
|
2590
|
-
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.",
|
|
2591
2765
|
inputSchema: {
|
|
2592
2766
|
type: "object",
|
|
2593
2767
|
properties: {
|
|
2594
|
-
projectPath:
|
|
2595
|
-
|
|
2596
|
-
description: "Path to the extension project root"
|
|
2597
|
-
},
|
|
2598
|
-
browser: {
|
|
2599
|
-
type: "string",
|
|
2600
|
-
enum: [
|
|
2601
|
-
"chrome",
|
|
2602
|
-
"chromium",
|
|
2603
|
-
"edge",
|
|
2604
|
-
"brave",
|
|
2605
|
-
"opera",
|
|
2606
|
-
"vivaldi",
|
|
2607
|
-
"yandex",
|
|
2608
|
-
"firefox",
|
|
2609
|
-
"waterfox",
|
|
2610
|
-
"librewolf",
|
|
2611
|
-
"safari",
|
|
2612
|
-
"chromium-based",
|
|
2613
|
-
"gecko-based",
|
|
2614
|
-
"firefox-based",
|
|
2615
|
-
"webkit-based"
|
|
2616
|
-
],
|
|
2617
|
-
default: "chrome"
|
|
2618
|
-
},
|
|
2768
|
+
projectPath: PROJECT_PATH,
|
|
2769
|
+
browser: LAUNCH_BROWSER,
|
|
2619
2770
|
port: {
|
|
2620
2771
|
type: "number",
|
|
2621
2772
|
description: "Dev server port (0 for auto-assign)"
|
|
@@ -2623,7 +2774,7 @@ const dev_schema = {
|
|
|
2623
2774
|
noBrowser: {
|
|
2624
2775
|
type: "boolean",
|
|
2625
2776
|
default: false,
|
|
2626
|
-
description: "Start dev server without launching browser"
|
|
2777
|
+
description: "Start the dev server without launching a browser"
|
|
2627
2778
|
},
|
|
2628
2779
|
polyfill: {
|
|
2629
2780
|
type: "boolean",
|
|
@@ -2634,22 +2785,22 @@ const dev_schema = {
|
|
|
2634
2785
|
replace: {
|
|
2635
2786
|
type: "boolean",
|
|
2636
2787
|
default: false,
|
|
2637
|
-
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."
|
|
2638
2789
|
},
|
|
2639
2790
|
allowControl: {
|
|
2640
2791
|
type: "boolean",
|
|
2641
2792
|
default: false,
|
|
2642
|
-
description: "Enable the agent-bridge control channel
|
|
2793
|
+
description: "Enable the agent-bridge control channel that extension_storage/reload/open/dom_snapshot need"
|
|
2643
2794
|
},
|
|
2644
2795
|
allowEval: {
|
|
2645
2796
|
type: "boolean",
|
|
2646
2797
|
default: false,
|
|
2647
|
-
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."
|
|
2648
2799
|
},
|
|
2649
2800
|
carrier: {
|
|
2650
2801
|
type: "boolean",
|
|
2651
2802
|
default: false,
|
|
2652
|
-
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."
|
|
2653
2804
|
}
|
|
2654
2805
|
},
|
|
2655
2806
|
required: [
|
|
@@ -2762,11 +2913,11 @@ async function dev_handler(args) {
|
|
|
2762
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.`
|
|
2763
2914
|
});
|
|
2764
2915
|
}
|
|
2765
|
-
const controlVerbs = "storage, reload, open,
|
|
2916
|
+
const controlVerbs = "storage, reload, open, dom_snapshot";
|
|
2766
2917
|
const capabilities = {
|
|
2767
2918
|
allowControl,
|
|
2768
2919
|
allowEval: Boolean(args.allowEval),
|
|
2769
|
-
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)"
|
|
2770
2921
|
};
|
|
2771
2922
|
const boundPort = contractBoundPort(args.projectPath, browser, spawnedAt);
|
|
2772
2923
|
if (null !== boundPort && boundPort !== args.port) registerSession({
|
|
@@ -2804,7 +2955,7 @@ async function dev_handler(args) {
|
|
|
2804
2955
|
} : {}
|
|
2805
2956
|
} : {},
|
|
2806
2957
|
capabilities,
|
|
2807
|
-
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.",
|
|
2808
2959
|
earlyOutput: cleanOutput.slice(0, 500),
|
|
2809
2960
|
logPath
|
|
2810
2961
|
});
|
|
@@ -2827,39 +2978,21 @@ function denoiseEarlyOutput(raw) {
|
|
|
2827
2978
|
}
|
|
2828
2979
|
const start_schema = {
|
|
2829
2980
|
name: "extension_start",
|
|
2830
|
-
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.",
|
|
2831
2982
|
inputSchema: {
|
|
2832
2983
|
type: "object",
|
|
2833
2984
|
properties: {
|
|
2834
|
-
projectPath:
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
enum: [
|
|
2841
|
-
"chrome",
|
|
2842
|
-
"chromium",
|
|
2843
|
-
"edge",
|
|
2844
|
-
"brave",
|
|
2845
|
-
"opera",
|
|
2846
|
-
"vivaldi",
|
|
2847
|
-
"yandex",
|
|
2848
|
-
"firefox",
|
|
2849
|
-
"waterfox",
|
|
2850
|
-
"librewolf",
|
|
2851
|
-
"safari",
|
|
2852
|
-
"chromium-based",
|
|
2853
|
-
"gecko-based",
|
|
2854
|
-
"firefox-based",
|
|
2855
|
-
"webkit-based"
|
|
2856
|
-
],
|
|
2857
|
-
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."
|
|
2858
2991
|
},
|
|
2859
2992
|
polyfill: {
|
|
2860
2993
|
type: "boolean",
|
|
2861
2994
|
default: true,
|
|
2862
|
-
description: "Apply cross-browser polyfill"
|
|
2995
|
+
description: "Apply cross-browser polyfill (build only)"
|
|
2863
2996
|
},
|
|
2864
2997
|
port: {
|
|
2865
2998
|
type: "number",
|
|
@@ -2868,7 +3001,7 @@ const start_schema = {
|
|
|
2868
3001
|
noBrowser: {
|
|
2869
3002
|
type: "boolean",
|
|
2870
3003
|
default: false,
|
|
2871
|
-
description: "
|
|
3004
|
+
description: "Serve without launching a browser"
|
|
2872
3005
|
},
|
|
2873
3006
|
...LAUNCH_FLAG_SCHEMA
|
|
2874
3007
|
},
|
|
@@ -2879,13 +3012,15 @@ const start_schema = {
|
|
|
2879
3012
|
};
|
|
2880
3013
|
async function start_handler(args) {
|
|
2881
3014
|
const browser = args.browser ?? "chrome";
|
|
3015
|
+
const building = false !== args.build;
|
|
3016
|
+
const command = building ? "start" : "preview";
|
|
2882
3017
|
const cliArgs = [
|
|
2883
|
-
|
|
3018
|
+
command,
|
|
2884
3019
|
args.projectPath,
|
|
2885
3020
|
"--browser",
|
|
2886
3021
|
browser
|
|
2887
3022
|
];
|
|
2888
|
-
if (false === args.polyfill) cliArgs.push("--polyfill", "false");
|
|
3023
|
+
if (building && false === args.polyfill) cliArgs.push("--polyfill", "false");
|
|
2889
3024
|
if (void 0 !== args.port) cliArgs.push("--port", String(args.port));
|
|
2890
3025
|
if (args.noBrowser) cliArgs.push("--no-browser");
|
|
2891
3026
|
cliArgs.push(...launchFlagArgs(args));
|
|
@@ -2899,7 +3034,7 @@ async function start_handler(args) {
|
|
|
2899
3034
|
pid,
|
|
2900
3035
|
browser,
|
|
2901
3036
|
projectPath: args.projectPath,
|
|
2902
|
-
command
|
|
3037
|
+
command
|
|
2903
3038
|
});
|
|
2904
3039
|
child.on("exit", ()=>removeSession(args.projectPath, browser));
|
|
2905
3040
|
await new Promise((resolve)=>setTimeout(resolve, 5000));
|
|
@@ -2915,10 +3050,10 @@ async function start_handler(args) {
|
|
|
2915
3050
|
pid,
|
|
2916
3051
|
exitCode: code,
|
|
2917
3052
|
signal,
|
|
2918
|
-
error: `The
|
|
3053
|
+
error: `The ${command} process exited during startup (${signal ? `signal ${signal}` : `exit code ${code}`}). No session is running.`,
|
|
2919
3054
|
output: earlyOutput.slice(0, 2000),
|
|
2920
3055
|
logPath,
|
|
2921
|
-
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."
|
|
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."
|
|
2922
3057
|
});
|
|
2923
3058
|
}
|
|
2924
3059
|
const exitStamp = browserExitStamp(args.projectPath, browser, spawnedAt);
|
|
@@ -2929,7 +3064,7 @@ async function start_handler(args) {
|
|
|
2929
3064
|
browser,
|
|
2930
3065
|
pid,
|
|
2931
3066
|
...exitStamp,
|
|
2932
|
-
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.`,
|
|
2933
3068
|
output: earlyOutput.slice(0, 2000),
|
|
2934
3069
|
logPath,
|
|
2935
3070
|
hint: "Read `output` above and extension_logs for the cause, then call extension_stop to clean up before retrying."
|
|
@@ -2939,122 +3074,8 @@ async function start_handler(args) {
|
|
|
2939
3074
|
pid,
|
|
2940
3075
|
browser,
|
|
2941
3076
|
projectPath: args.projectPath,
|
|
2942
|
-
status: "started",
|
|
2943
|
-
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.",
|
|
2944
|
-
earlyOutput: earlyOutput.slice(0, 500),
|
|
2945
|
-
logPath
|
|
2946
|
-
});
|
|
2947
|
-
}
|
|
2948
|
-
const preview_schema = {
|
|
2949
|
-
name: "extension_preview",
|
|
2950
|
-
description: "Preview a production-built extension in a browser. Uses dist/ output directly. The extension must be built first with extension_build.",
|
|
2951
|
-
inputSchema: {
|
|
2952
|
-
type: "object",
|
|
2953
|
-
properties: {
|
|
2954
|
-
projectPath: {
|
|
2955
|
-
type: "string",
|
|
2956
|
-
description: "Path to the extension project root"
|
|
2957
|
-
},
|
|
2958
|
-
browser: {
|
|
2959
|
-
type: "string",
|
|
2960
|
-
enum: [
|
|
2961
|
-
"chrome",
|
|
2962
|
-
"chromium",
|
|
2963
|
-
"edge",
|
|
2964
|
-
"brave",
|
|
2965
|
-
"opera",
|
|
2966
|
-
"vivaldi",
|
|
2967
|
-
"yandex",
|
|
2968
|
-
"firefox",
|
|
2969
|
-
"waterfox",
|
|
2970
|
-
"librewolf",
|
|
2971
|
-
"safari",
|
|
2972
|
-
"chromium-based",
|
|
2973
|
-
"gecko-based",
|
|
2974
|
-
"firefox-based",
|
|
2975
|
-
"webkit-based"
|
|
2976
|
-
],
|
|
2977
|
-
default: "chrome"
|
|
2978
|
-
},
|
|
2979
|
-
port: {
|
|
2980
|
-
type: "number",
|
|
2981
|
-
description: "Server port (0 for auto-assign)"
|
|
2982
|
-
},
|
|
2983
|
-
noBrowser: {
|
|
2984
|
-
type: "boolean",
|
|
2985
|
-
default: false,
|
|
2986
|
-
description: "Serve the preview without launching a browser"
|
|
2987
|
-
},
|
|
2988
|
-
...LAUNCH_FLAG_SCHEMA
|
|
2989
|
-
},
|
|
2990
|
-
required: [
|
|
2991
|
-
"projectPath"
|
|
2992
|
-
]
|
|
2993
|
-
}
|
|
2994
|
-
};
|
|
2995
|
-
async function preview_handler(args) {
|
|
2996
|
-
const browser = args.browser ?? "chrome";
|
|
2997
|
-
const cliArgs = [
|
|
2998
|
-
"preview",
|
|
2999
|
-
args.projectPath,
|
|
3000
|
-
"--browser",
|
|
3001
|
-
browser
|
|
3002
|
-
];
|
|
3003
|
-
if (void 0 !== args.port) cliArgs.push("--port", String(args.port));
|
|
3004
|
-
if (args.noBrowser) cliArgs.push("--no-browser");
|
|
3005
|
-
cliArgs.push(...launchFlagArgs(args));
|
|
3006
|
-
const spawnedAt = Date.now();
|
|
3007
|
-
const spawned = spawnExtensionCli(cliArgs, {
|
|
3008
|
-
projectDir: args.projectPath
|
|
3009
|
-
});
|
|
3010
|
-
const { child, logPath } = spawned;
|
|
3011
|
-
const pid = child.pid;
|
|
3012
|
-
registerSession({
|
|
3013
|
-
pid,
|
|
3014
|
-
browser,
|
|
3015
|
-
projectPath: args.projectPath,
|
|
3016
|
-
command: "preview"
|
|
3017
|
-
});
|
|
3018
|
-
child.on("exit", ()=>removeSession(args.projectPath, browser));
|
|
3019
|
-
await new Promise((resolve)=>setTimeout(resolve, 5000));
|
|
3020
|
-
const earlyOutput = spawned.readOutput();
|
|
3021
|
-
if (null !== child.exitCode || null !== child.signalCode) {
|
|
3022
|
-
const code = child.exitCode;
|
|
3023
|
-
const signal = child.signalCode;
|
|
3024
|
-
return JSON.stringify({
|
|
3025
|
-
ok: false,
|
|
3026
|
-
status: "exited",
|
|
3027
|
-
projectPath: args.projectPath,
|
|
3028
|
-
browser,
|
|
3029
|
-
pid,
|
|
3030
|
-
exitCode: code,
|
|
3031
|
-
signal,
|
|
3032
|
-
error: `The preview process exited during startup (${signal ? `signal ${signal}` : `exit code ${code}`}). Nothing is running.`,
|
|
3033
|
-
output: earlyOutput.slice(0, 2000),
|
|
3034
|
-
logPath,
|
|
3035
|
-
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."
|
|
3036
|
-
});
|
|
3037
|
-
}
|
|
3038
|
-
const exitStamp = browserExitStamp(args.projectPath, browser, spawnedAt);
|
|
3039
|
-
if (exitStamp) return JSON.stringify({
|
|
3040
|
-
ok: false,
|
|
3041
|
-
status: "browser-exited",
|
|
3042
|
-
projectPath: args.projectPath,
|
|
3043
|
-
browser,
|
|
3044
|
-
pid,
|
|
3045
|
-
...exitStamp,
|
|
3046
|
-
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.",
|
|
3047
|
-
output: earlyOutput.slice(0, 2000),
|
|
3048
|
-
logPath,
|
|
3049
|
-
hint: "Read `output` above and extension_logs for the cause, then call extension_stop to clean up before retrying."
|
|
3050
|
-
});
|
|
3051
|
-
return JSON.stringify({
|
|
3052
|
-
ok: true,
|
|
3053
|
-
pid,
|
|
3054
|
-
browser,
|
|
3055
|
-
projectPath: args.projectPath,
|
|
3056
|
-
status: "launched",
|
|
3057
|
-
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.",
|
|
3058
3079
|
earlyOutput: earlyOutput.slice(0, 500),
|
|
3059
3080
|
logPath
|
|
3060
3081
|
});
|
|
@@ -3672,7 +3693,7 @@ async function navigateToUrlViaBridge(projectPath, browser, url, timeout) {
|
|
|
3672
3693
|
name: "NavigateFailed",
|
|
3673
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).`
|
|
3674
3695
|
},
|
|
3675
|
-
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."
|
|
3676
3697
|
});
|
|
3677
3698
|
return JSON.stringify({
|
|
3678
3699
|
ok: true,
|
|
@@ -3682,7 +3703,7 @@ async function navigateToUrlViaBridge(projectPath, browser, url, timeout) {
|
|
|
3682
3703
|
url: settled.url,
|
|
3683
3704
|
title: settled.title
|
|
3684
3705
|
},
|
|
3685
|
-
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')."
|
|
3686
3707
|
});
|
|
3687
3708
|
}
|
|
3688
3709
|
async function resolveBridgeBaseUrl(projectPath, browser, timeout) {
|
|
@@ -3781,7 +3802,7 @@ async function navigateToUrl(projectPath, browser, url, timeout) {
|
|
|
3781
3802
|
name: "NavigateFailed",
|
|
3782
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."}`
|
|
3783
3804
|
},
|
|
3784
|
-
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."
|
|
3785
3806
|
});
|
|
3786
3807
|
}
|
|
3787
3808
|
return JSON.stringify({
|
|
@@ -3801,7 +3822,7 @@ async function navigateToUrl(projectPath, browser, url, timeout) {
|
|
|
3801
3822
|
title: settled.title,
|
|
3802
3823
|
url: settled.url
|
|
3803
3824
|
},
|
|
3804
|
-
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."
|
|
3805
3826
|
});
|
|
3806
3827
|
} catch (e) {
|
|
3807
3828
|
return JSON.stringify({
|
|
@@ -4042,7 +4063,7 @@ async function openSurfaceAsTab(projectPath, browser, surface) {
|
|
|
4042
4063
|
popupBounds = await applyPopupBounds(projectPath, browser, parsed.target.targetId);
|
|
4043
4064
|
if (popupBounds) parsed.renderedAsTab.popupBounds = popupBounds;
|
|
4044
4065
|
}
|
|
4045
|
-
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.";
|
|
4046
4067
|
return JSON.stringify(parsed);
|
|
4047
4068
|
}
|
|
4048
4069
|
} catch {}
|
|
@@ -4083,14 +4104,11 @@ async function confirmSurfaceTarget(projectPath, browser, surface, raw) {
|
|
|
4083
4104
|
}
|
|
4084
4105
|
const open_schema = {
|
|
4085
4106
|
name: "extension_open",
|
|
4086
|
-
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).",
|
|
4087
4108
|
inputSchema: {
|
|
4088
4109
|
type: "object",
|
|
4089
4110
|
properties: {
|
|
4090
|
-
projectPath:
|
|
4091
|
-
type: "string",
|
|
4092
|
-
description: "Path to the extension project root (must have an active dev session)"
|
|
4093
|
-
},
|
|
4111
|
+
projectPath: SESSION_PROJECT_PATH,
|
|
4094
4112
|
surface: {
|
|
4095
4113
|
type: "string",
|
|
4096
4114
|
enum: [
|
|
@@ -4103,7 +4121,7 @@ const open_schema = {
|
|
|
4103
4121
|
"action",
|
|
4104
4122
|
"command"
|
|
4105
4123
|
],
|
|
4106
|
-
description: "Which surface to open or event to replay.
|
|
4124
|
+
description: "Which surface to open or event to replay."
|
|
4107
4125
|
},
|
|
4108
4126
|
name: {
|
|
4109
4127
|
type: "string",
|
|
@@ -4111,21 +4129,15 @@ const open_schema = {
|
|
|
4111
4129
|
},
|
|
4112
4130
|
url: {
|
|
4113
4131
|
type: "string",
|
|
4114
|
-
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."
|
|
4115
4133
|
},
|
|
4116
4134
|
asTab: {
|
|
4117
4135
|
type: "boolean",
|
|
4118
4136
|
default: false,
|
|
4119
|
-
description: "
|
|
4120
|
-
},
|
|
4121
|
-
browser: {
|
|
4122
|
-
type: "string",
|
|
4123
|
-
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."
|
|
4124
4138
|
},
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
description: "Command timeout in ms (default 5000)"
|
|
4128
|
-
}
|
|
4139
|
+
browser: SESSION_BROWSER,
|
|
4140
|
+
timeout: CALL_TIMEOUT
|
|
4129
4141
|
},
|
|
4130
4142
|
required: [
|
|
4131
4143
|
"projectPath"
|
|
@@ -4240,7 +4252,8 @@ async function publish(options = {}) {
|
|
|
4240
4252
|
method: "POST",
|
|
4241
4253
|
headers: {
|
|
4242
4254
|
authorization: `Bearer ${token}`,
|
|
4243
|
-
"content-type": "application/json"
|
|
4255
|
+
"content-type": "application/json",
|
|
4256
|
+
...identityHeaders("extension_publish")
|
|
4244
4257
|
},
|
|
4245
4258
|
body: JSON.stringify(body)
|
|
4246
4259
|
});
|
|
@@ -4316,7 +4329,7 @@ async function uploadPreview(options) {
|
|
|
4316
4329
|
ok: false,
|
|
4317
4330
|
error: {
|
|
4318
4331
|
name: "PreviewAuthError",
|
|
4319
|
-
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)."
|
|
4320
4333
|
}
|
|
4321
4334
|
};
|
|
4322
4335
|
const apiCheck = safeApiBase(resolveApiBase(options.api));
|
|
@@ -4370,7 +4383,8 @@ async function uploadPreview(options) {
|
|
|
4370
4383
|
method: "POST",
|
|
4371
4384
|
headers: {
|
|
4372
4385
|
authorization: `Bearer ${token}`,
|
|
4373
|
-
"content-type": "application/json"
|
|
4386
|
+
"content-type": "application/json",
|
|
4387
|
+
...identityHeaders("extension_preview_web")
|
|
4374
4388
|
},
|
|
4375
4389
|
body: JSON.stringify({
|
|
4376
4390
|
kind: "dist",
|
|
@@ -4552,7 +4566,7 @@ async function buildShare(projectPath, distDir, manifest, browser) {
|
|
|
4552
4566
|
errorName: result.error.name,
|
|
4553
4567
|
reason: result.error.message,
|
|
4554
4568
|
...isAuth ? {
|
|
4555
|
-
loginHint: "Run
|
|
4569
|
+
loginHint: "Run extension_auth (action: login), or set EXTENSION_DEV_TOKEN (create one in the extension.dev dashboard)."
|
|
4556
4570
|
} : {}
|
|
4557
4571
|
};
|
|
4558
4572
|
}
|
|
@@ -4620,40 +4634,25 @@ function detectSurfaces(manifest) {
|
|
|
4620
4634
|
}
|
|
4621
4635
|
const preview_web_schema = {
|
|
4622
4636
|
name: "extension_preview_web",
|
|
4623
|
-
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.",
|
|
4624
4638
|
inputSchema: {
|
|
4625
4639
|
type: "object",
|
|
4626
4640
|
properties: {
|
|
4627
|
-
projectPath:
|
|
4628
|
-
type: "string",
|
|
4629
|
-
description: "Path to the extension project root"
|
|
4630
|
-
},
|
|
4641
|
+
projectPath: PROJECT_PATH,
|
|
4631
4642
|
browser: {
|
|
4632
4643
|
type: "string",
|
|
4633
|
-
enum:
|
|
4634
|
-
"chrome",
|
|
4635
|
-
"chromium",
|
|
4636
|
-
"edge",
|
|
4637
|
-
"brave",
|
|
4638
|
-
"opera",
|
|
4639
|
-
"vivaldi",
|
|
4640
|
-
"yandex",
|
|
4641
|
-
"firefox",
|
|
4642
|
-
"waterfox",
|
|
4643
|
-
"librewolf",
|
|
4644
|
-
"safari"
|
|
4645
|
-
],
|
|
4644
|
+
enum: REAL_BROWSERS,
|
|
4646
4645
|
default: "chrome",
|
|
4647
|
-
description: "Which dist/<browser> output to preview. The emulator renders it as mocked Chrome
|
|
4646
|
+
description: "Which dist/<browser> output to preview. The emulator renders it as mocked Chrome either way."
|
|
4648
4647
|
},
|
|
4649
4648
|
build: {
|
|
4650
4649
|
type: "boolean",
|
|
4651
4650
|
default: true,
|
|
4652
|
-
description: "Build
|
|
4651
|
+
description: "Build first. false previews the existing dist/<browser> as-is."
|
|
4653
4652
|
},
|
|
4654
4653
|
distPath: {
|
|
4655
4654
|
type: "string",
|
|
4656
|
-
description: "Preview this built directory
|
|
4655
|
+
description: "Preview this built directory instead of dist/<browser> under projectPath. Implies build:false."
|
|
4657
4656
|
},
|
|
4658
4657
|
hostUrl: {
|
|
4659
4658
|
type: "string",
|
|
@@ -4662,34 +4661,22 @@ const preview_web_schema = {
|
|
|
4662
4661
|
probe: {
|
|
4663
4662
|
type: "boolean",
|
|
4664
4663
|
default: true,
|
|
4665
|
-
description: "
|
|
4664
|
+
description: "Fetch the surface's dev middleware first to confirm the artifact loads."
|
|
4666
4665
|
},
|
|
4667
4666
|
open: {
|
|
4668
4667
|
type: "boolean",
|
|
4669
4668
|
default: false,
|
|
4670
|
-
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."
|
|
4671
4670
|
},
|
|
4672
4671
|
openIn: {
|
|
4673
4672
|
type: "string",
|
|
4674
|
-
enum:
|
|
4675
|
-
|
|
4676
|
-
"chromium",
|
|
4677
|
-
"edge",
|
|
4678
|
-
"brave",
|
|
4679
|
-
"opera",
|
|
4680
|
-
"vivaldi",
|
|
4681
|
-
"yandex",
|
|
4682
|
-
"firefox",
|
|
4683
|
-
"waterfox",
|
|
4684
|
-
"librewolf",
|
|
4685
|
-
"safari"
|
|
4686
|
-
],
|
|
4687
|
-
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`."
|
|
4688
4675
|
},
|
|
4689
4676
|
share: {
|
|
4690
4677
|
type: "boolean",
|
|
4691
4678
|
default: false,
|
|
4692
|
-
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."
|
|
4693
4680
|
}
|
|
4694
4681
|
},
|
|
4695
4682
|
required: [
|
|
@@ -4850,7 +4837,7 @@ function authError(name) {
|
|
|
4850
4837
|
ok: false,
|
|
4851
4838
|
error: {
|
|
4852
4839
|
name,
|
|
4853
|
-
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)."
|
|
4854
4841
|
}
|
|
4855
4842
|
};
|
|
4856
4843
|
}
|
|
@@ -4884,7 +4871,8 @@ async function listArtifacts(options = {}) {
|
|
|
4884
4871
|
res = await doFetch(url.toString(), {
|
|
4885
4872
|
headers: {
|
|
4886
4873
|
authorization: `Bearer ${token}`,
|
|
4887
|
-
accept: "application/json"
|
|
4874
|
+
accept: "application/json",
|
|
4875
|
+
...identityHeaders("extension_shares")
|
|
4888
4876
|
}
|
|
4889
4877
|
});
|
|
4890
4878
|
} catch (err) {
|
|
@@ -4939,7 +4927,8 @@ async function revokeArtifact(options) {
|
|
|
4939
4927
|
method: "DELETE",
|
|
4940
4928
|
headers: {
|
|
4941
4929
|
authorization: `Bearer ${token}`,
|
|
4942
|
-
accept: "application/json"
|
|
4930
|
+
accept: "application/json",
|
|
4931
|
+
...identityHeaders("extension_shares")
|
|
4943
4932
|
}
|
|
4944
4933
|
});
|
|
4945
4934
|
} catch (err) {
|
|
@@ -4980,10 +4969,10 @@ async function revokeArtifact(options) {
|
|
|
4980
4969
|
}
|
|
4981
4970
|
};
|
|
4982
4971
|
}
|
|
4983
|
-
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).";
|
|
4984
4973
|
const shares_schema = {
|
|
4985
4974
|
name: "extension_shares",
|
|
4986
|
-
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.",
|
|
4987
4976
|
inputSchema: {
|
|
4988
4977
|
type: "object",
|
|
4989
4978
|
properties: {
|
|
@@ -5021,10 +5010,7 @@ const shares_schema = {
|
|
|
5021
5010
|
type: "number",
|
|
5022
5011
|
description: "How many shares to return, 1 to 200 (platform default 100). A cut list comes back with truncated:true."
|
|
5023
5012
|
},
|
|
5024
|
-
api:
|
|
5025
|
-
type: "string",
|
|
5026
|
-
description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)."
|
|
5027
|
-
}
|
|
5013
|
+
api: API_BASE
|
|
5028
5014
|
},
|
|
5029
5015
|
required: []
|
|
5030
5016
|
}
|
|
@@ -5273,73 +5259,6 @@ async function shares_handler(args) {
|
|
|
5273
5259
|
} : {}
|
|
5274
5260
|
});
|
|
5275
5261
|
}
|
|
5276
|
-
const get_template_source_schema = {
|
|
5277
|
-
name: "extension_get_template_source",
|
|
5278
|
-
description: "Read source files from a template in the extension.dev template catalog. Use this to learn implementation patterns before building something similar.",
|
|
5279
|
-
inputSchema: {
|
|
5280
|
-
type: "object",
|
|
5281
|
-
properties: {
|
|
5282
|
-
slug: {
|
|
5283
|
-
type: "string",
|
|
5284
|
-
description: "Template slug (e.g. 'ai-claude', 'content-react')"
|
|
5285
|
-
},
|
|
5286
|
-
files: {
|
|
5287
|
-
type: "array",
|
|
5288
|
-
items: {
|
|
5289
|
-
type: "string"
|
|
5290
|
-
},
|
|
5291
|
-
description: "Specific files to read (e.g. ['src/manifest.json', 'src/background.ts']). If omitted, returns the file listing from templates-meta.json."
|
|
5292
|
-
}
|
|
5293
|
-
},
|
|
5294
|
-
required: [
|
|
5295
|
-
"slug"
|
|
5296
|
-
]
|
|
5297
|
-
}
|
|
5298
|
-
};
|
|
5299
|
-
async function get_template_source_handler(args) {
|
|
5300
|
-
const template = await getTemplateBySlug(args.slug);
|
|
5301
|
-
if (!template) return JSON.stringify({
|
|
5302
|
-
error: `Template '${args.slug}' not found in the catalog`,
|
|
5303
|
-
hint: "Use extension_list_templates to see available templates."
|
|
5304
|
-
});
|
|
5305
|
-
const meta = {
|
|
5306
|
-
slug: template.slug,
|
|
5307
|
-
description: template.description,
|
|
5308
|
-
uiFramework: template.uiFramework || "vanilla",
|
|
5309
|
-
surfaces: template.surfaces,
|
|
5310
|
-
permissions: template.permissions,
|
|
5311
|
-
patternExplanation: template.patternExplanation,
|
|
5312
|
-
keyFiles: template.keyFiles,
|
|
5313
|
-
repositoryUrl: template.repositoryUrl
|
|
5314
|
-
};
|
|
5315
|
-
if (!args.files?.length) return JSON.stringify({
|
|
5316
|
-
...meta,
|
|
5317
|
-
files: template.files.map((f)=>stripTemplatePathPrefix(template.slug, f)),
|
|
5318
|
-
hint: "Pass specific file paths in the files parameter to read their contents."
|
|
5319
|
-
});
|
|
5320
|
-
const fileContents = {};
|
|
5321
|
-
const errors = [];
|
|
5322
|
-
await Promise.all(args.files.map(async (filePath)=>{
|
|
5323
|
-
const urls = await templateFileUrls(args.slug, filePath);
|
|
5324
|
-
let lastStatus = 0;
|
|
5325
|
-
for (const url of urls)try {
|
|
5326
|
-
const response = await fetch(url);
|
|
5327
|
-
if (response.ok) {
|
|
5328
|
-
fileContents[filePath] = await response.text();
|
|
5329
|
-
return;
|
|
5330
|
-
}
|
|
5331
|
-
lastStatus = response.status;
|
|
5332
|
-
} catch {}
|
|
5333
|
-
errors.push(`${filePath}: ${lastStatus || "fetch failed"}`);
|
|
5334
|
-
}));
|
|
5335
|
-
return JSON.stringify({
|
|
5336
|
-
...meta,
|
|
5337
|
-
fileContents,
|
|
5338
|
-
...errors.length ? {
|
|
5339
|
-
errors
|
|
5340
|
-
} : {}
|
|
5341
|
-
});
|
|
5342
|
-
}
|
|
5343
5262
|
const CHROME_DESKTOP_ONLY_KEYS = [
|
|
5344
5263
|
"file_browser_handlers",
|
|
5345
5264
|
"file_system_provider_capabilities",
|
|
@@ -6387,7 +6306,7 @@ function resolveChromeTheme(theme) {
|
|
|
6387
6306
|
}
|
|
6388
6307
|
const theme_verify_schema = {
|
|
6389
6308
|
name: "extension_theme_verify",
|
|
6390
|
-
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.",
|
|
6391
6310
|
inputSchema: {
|
|
6392
6311
|
type: "object",
|
|
6393
6312
|
properties: {
|
|
@@ -6650,20 +6569,17 @@ async function theme_verify_handler(args) {
|
|
|
6650
6569
|
attended
|
|
6651
6570
|
});
|
|
6652
6571
|
}
|
|
6653
|
-
const
|
|
6654
|
-
name: "
|
|
6655
|
-
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.",
|
|
6656
6575
|
inputSchema: {
|
|
6657
6576
|
type: "object",
|
|
6658
6577
|
properties: {
|
|
6659
|
-
projectPath:
|
|
6660
|
-
type: "string",
|
|
6661
|
-
description: "Path to the extension project root"
|
|
6662
|
-
},
|
|
6578
|
+
projectPath: PROJECT_PATH,
|
|
6663
6579
|
browser: {
|
|
6664
6580
|
type: "string",
|
|
6665
6581
|
default: "chrome",
|
|
6666
|
-
description: "Browser build to
|
|
6582
|
+
description: "Browser build to analyze"
|
|
6667
6583
|
},
|
|
6668
6584
|
format: {
|
|
6669
6585
|
type: "string",
|
|
@@ -6744,7 +6660,7 @@ function formatBytes(bytes) {
|
|
|
6744
6660
|
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
6745
6661
|
return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
6746
6662
|
}
|
|
6747
|
-
async function
|
|
6663
|
+
async function analyze_handler(args) {
|
|
6748
6664
|
const browser = args.browser ?? "chrome";
|
|
6749
6665
|
const distPath = node_path.resolve(args.projectPath, "dist", browser);
|
|
6750
6666
|
if (!node_fs.existsSync(distPath)) return JSON.stringify({
|
|
@@ -7322,26 +7238,23 @@ async function inspectViaBridge(args, browser, include, maxBytes) {
|
|
|
7322
7238
|
if (notes.length) result.notes = notes;
|
|
7323
7239
|
return JSON.stringify(result);
|
|
7324
7240
|
}
|
|
7325
|
-
const
|
|
7326
|
-
name: "
|
|
7327
|
-
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.",
|
|
7328
7244
|
inputSchema: {
|
|
7329
7245
|
type: "object",
|
|
7330
7246
|
properties: {
|
|
7331
|
-
projectPath:
|
|
7332
|
-
type: "string",
|
|
7333
|
-
description: "Path to the extension project root (must have an active dev session)"
|
|
7334
|
-
},
|
|
7247
|
+
projectPath: SESSION_PROJECT_PATH,
|
|
7335
7248
|
url: {
|
|
7336
7249
|
type: "string",
|
|
7337
|
-
description: "URL to inspect
|
|
7250
|
+
description: "URL to inspect; the tab is navigated there first"
|
|
7338
7251
|
},
|
|
7339
7252
|
probe: {
|
|
7340
7253
|
type: "array",
|
|
7341
7254
|
items: {
|
|
7342
7255
|
type: "string"
|
|
7343
7256
|
},
|
|
7344
|
-
description: "CSS selectors to query
|
|
7257
|
+
description: "CSS selectors to query; returns counts and samples for each"
|
|
7345
7258
|
},
|
|
7346
7259
|
include: {
|
|
7347
7260
|
type: "array",
|
|
@@ -7361,12 +7274,9 @@ const source_inspect_schema = {
|
|
|
7361
7274
|
"meta",
|
|
7362
7275
|
"console"
|
|
7363
7276
|
],
|
|
7364
|
-
description: "What
|
|
7365
|
-
},
|
|
7366
|
-
browser: {
|
|
7367
|
-
type: "string",
|
|
7368
|
-
description: "Browser session to target. Defaults to the active dev session's browser for this project."
|
|
7277
|
+
description: "What to include"
|
|
7369
7278
|
},
|
|
7279
|
+
browser: SESSION_BROWSER,
|
|
7370
7280
|
maxBytes: {
|
|
7371
7281
|
type: "number",
|
|
7372
7282
|
default: 262144,
|
|
@@ -7375,7 +7285,7 @@ const source_inspect_schema = {
|
|
|
7375
7285
|
deepDom: {
|
|
7376
7286
|
type: "boolean",
|
|
7377
7287
|
default: false,
|
|
7378
|
-
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)."
|
|
7379
7289
|
}
|
|
7380
7290
|
},
|
|
7381
7291
|
required: [
|
|
@@ -7383,7 +7293,7 @@ const source_inspect_schema = {
|
|
|
7383
7293
|
]
|
|
7384
7294
|
}
|
|
7385
7295
|
};
|
|
7386
|
-
async function
|
|
7296
|
+
async function inspect_handler(args) {
|
|
7387
7297
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser, "chrome");
|
|
7388
7298
|
const include = new Set(args.include ?? [
|
|
7389
7299
|
"summary",
|
|
@@ -7495,18 +7405,12 @@ async function source_inspect_handler(args) {
|
|
|
7495
7405
|
}
|
|
7496
7406
|
const list_extensions_schema = {
|
|
7497
7407
|
name: "extension_list_extensions",
|
|
7498
|
-
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.",
|
|
7499
7409
|
inputSchema: {
|
|
7500
7410
|
type: "object",
|
|
7501
7411
|
properties: {
|
|
7502
|
-
projectPath:
|
|
7503
|
-
|
|
7504
|
-
description: "Path to the extension project root (must have an active dev session)"
|
|
7505
|
-
},
|
|
7506
|
-
browser: {
|
|
7507
|
-
type: "string",
|
|
7508
|
-
description: "Browser session to target. Defaults to the active dev session's browser for this project."
|
|
7509
|
-
}
|
|
7412
|
+
projectPath: SESSION_PROJECT_PATH,
|
|
7413
|
+
browser: SESSION_BROWSER
|
|
7510
7414
|
},
|
|
7511
7415
|
required: [
|
|
7512
7416
|
"projectPath"
|
|
@@ -7772,13 +7676,10 @@ const logs_schema_schema = {
|
|
|
7772
7676
|
inputSchema: {
|
|
7773
7677
|
type: "object",
|
|
7774
7678
|
properties: {
|
|
7775
|
-
projectPath:
|
|
7776
|
-
type: "string",
|
|
7777
|
-
description: "Path to the extension project root (must have an active dev session)"
|
|
7778
|
-
},
|
|
7679
|
+
projectPath: SESSION_PROJECT_PATH,
|
|
7779
7680
|
browser: {
|
|
7780
7681
|
type: "string",
|
|
7781
|
-
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."
|
|
7782
7683
|
},
|
|
7783
7684
|
level: {
|
|
7784
7685
|
type: "string",
|
|
@@ -7792,7 +7693,7 @@ const logs_schema_schema = {
|
|
|
7792
7693
|
"all"
|
|
7793
7694
|
],
|
|
7794
7695
|
default: "all",
|
|
7795
|
-
description: "Minimum severity
|
|
7696
|
+
description: "Minimum severity; a level includes everything more severe."
|
|
7796
7697
|
},
|
|
7797
7698
|
context: {
|
|
7798
7699
|
type: "array",
|
|
@@ -7813,15 +7714,15 @@ const logs_schema_schema = {
|
|
|
7813
7714
|
signalsOnly: {
|
|
7814
7715
|
type: "boolean",
|
|
7815
7716
|
default: false,
|
|
7816
|
-
description: "Only structured dx.signal diagnostics (
|
|
7717
|
+
description: "Only structured dx.signal diagnostics (code/status/remediation), no plain console lines."
|
|
7817
7718
|
},
|
|
7818
7719
|
since: {
|
|
7819
7720
|
type: "number",
|
|
7820
|
-
description: "Only
|
|
7721
|
+
description: "Only events with seq greater than this; the cursor for polling forward."
|
|
7821
7722
|
},
|
|
7822
7723
|
url: {
|
|
7823
7724
|
type: "string",
|
|
7824
|
-
description: "Only events whose url/hostname matches (glob
|
|
7725
|
+
description: "Only events whose url/hostname matches (glob or substring), e.g. https://shop.example/*."
|
|
7825
7726
|
},
|
|
7826
7727
|
tab: {
|
|
7827
7728
|
type: "number",
|
|
@@ -7830,7 +7731,7 @@ const logs_schema_schema = {
|
|
|
7830
7731
|
follow: {
|
|
7831
7732
|
type: "boolean",
|
|
7832
7733
|
default: false,
|
|
7833
|
-
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."
|
|
7834
7735
|
},
|
|
7835
7736
|
followMs: {
|
|
7836
7737
|
type: "number",
|
|
@@ -7840,7 +7741,7 @@ const logs_schema_schema = {
|
|
|
7840
7741
|
limit: {
|
|
7841
7742
|
type: "number",
|
|
7842
7743
|
default: 200,
|
|
7843
|
-
description: "
|
|
7744
|
+
description: "How many of the most recent events to return."
|
|
7844
7745
|
}
|
|
7845
7746
|
},
|
|
7846
7747
|
required: [
|
|
@@ -8039,14 +7940,11 @@ async function logs_handler(args) {
|
|
|
8039
7940
|
}
|
|
8040
7941
|
const eval_schema = {
|
|
8041
7942
|
name: "extension_eval",
|
|
8042
|
-
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}.",
|
|
8043
7944
|
inputSchema: {
|
|
8044
7945
|
type: "object",
|
|
8045
7946
|
properties: {
|
|
8046
|
-
projectPath:
|
|
8047
|
-
type: "string",
|
|
8048
|
-
description: "Path to the extension project root (must have an active dev session)"
|
|
8049
|
-
},
|
|
7947
|
+
projectPath: SESSION_PROJECT_PATH,
|
|
8050
7948
|
expression: {
|
|
8051
7949
|
type: "string",
|
|
8052
7950
|
description: "JavaScript expression to evaluate in the target context"
|
|
@@ -8065,24 +7963,18 @@ const eval_schema = {
|
|
|
8065
7963
|
"content",
|
|
8066
7964
|
"page"
|
|
8067
7965
|
],
|
|
8068
|
-
description: "
|
|
7966
|
+
description: "Where to evaluate. Default background, except Chromium MV3 sessions default to page (the active tab)."
|
|
8069
7967
|
},
|
|
8070
7968
|
url: {
|
|
8071
7969
|
type: "string",
|
|
8072
|
-
description: "
|
|
7970
|
+
description: "content/page: pick the tab by url (match pattern, then substring). Preferred over `tab`."
|
|
8073
7971
|
},
|
|
8074
7972
|
tab: {
|
|
8075
7973
|
type: "number",
|
|
8076
|
-
description: "Numeric chrome.tabs id,
|
|
8077
|
-
},
|
|
8078
|
-
browser: {
|
|
8079
|
-
type: "string",
|
|
8080
|
-
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."
|
|
8081
7975
|
},
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
description: "Command timeout in ms (default 5000)"
|
|
8085
|
-
}
|
|
7976
|
+
browser: SESSION_BROWSER,
|
|
7977
|
+
timeout: CALL_TIMEOUT
|
|
8086
7978
|
},
|
|
8087
7979
|
required: [
|
|
8088
7980
|
"projectPath",
|
|
@@ -8137,7 +8029,7 @@ async function eval_handler(args) {
|
|
|
8137
8029
|
if (parsed && "object" == typeof parsed) {
|
|
8138
8030
|
parsed.defaultedContext = "page";
|
|
8139
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).';
|
|
8140
|
-
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.";
|
|
8141
8033
|
return JSON.stringify(parsed);
|
|
8142
8034
|
}
|
|
8143
8035
|
} catch {}
|
|
@@ -8145,14 +8037,11 @@ async function eval_handler(args) {
|
|
|
8145
8037
|
}
|
|
8146
8038
|
const storage_schema = {
|
|
8147
8039
|
name: "extension_storage",
|
|
8148
|
-
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).",
|
|
8149
8041
|
inputSchema: {
|
|
8150
8042
|
type: "object",
|
|
8151
8043
|
properties: {
|
|
8152
|
-
projectPath:
|
|
8153
|
-
type: "string",
|
|
8154
|
-
description: "Path to the extension project root (must have an active dev session)"
|
|
8155
|
-
},
|
|
8044
|
+
projectPath: SESSION_PROJECT_PATH,
|
|
8156
8045
|
action: {
|
|
8157
8046
|
type: "string",
|
|
8158
8047
|
enum: [
|
|
@@ -8189,14 +8078,8 @@ const storage_schema = {
|
|
|
8189
8078
|
],
|
|
8190
8079
|
default: "background"
|
|
8191
8080
|
},
|
|
8192
|
-
browser:
|
|
8193
|
-
|
|
8194
|
-
description: "Browser session to target. Defaults to the active dev session's browser for this project."
|
|
8195
|
-
},
|
|
8196
|
-
timeout: {
|
|
8197
|
-
type: "number",
|
|
8198
|
-
description: "Command timeout in ms (default 5000)"
|
|
8199
|
-
}
|
|
8081
|
+
browser: SESSION_BROWSER,
|
|
8082
|
+
timeout: CALL_TIMEOUT
|
|
8200
8083
|
},
|
|
8201
8084
|
required: [
|
|
8202
8085
|
"projectPath",
|
|
@@ -8237,14 +8120,11 @@ async function storage_handler(args) {
|
|
|
8237
8120
|
}
|
|
8238
8121
|
const reload_schema = {
|
|
8239
8122
|
name: "extension_reload",
|
|
8240
|
-
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).",
|
|
8241
8124
|
inputSchema: {
|
|
8242
8125
|
type: "object",
|
|
8243
8126
|
properties: {
|
|
8244
|
-
projectPath:
|
|
8245
|
-
type: "string",
|
|
8246
|
-
description: "Path to the extension project root (must have an active dev session)"
|
|
8247
|
-
},
|
|
8127
|
+
projectPath: SESSION_PROJECT_PATH,
|
|
8248
8128
|
context: {
|
|
8249
8129
|
type: "string",
|
|
8250
8130
|
enum: [
|
|
@@ -8258,14 +8138,8 @@ const reload_schema = {
|
|
|
8258
8138
|
type: "number",
|
|
8259
8139
|
description: "For content/page: a specific tab id"
|
|
8260
8140
|
},
|
|
8261
|
-
browser:
|
|
8262
|
-
|
|
8263
|
-
description: "Browser session to target. Defaults to the active dev session's browser for this project."
|
|
8264
|
-
},
|
|
8265
|
-
timeout: {
|
|
8266
|
-
type: "number",
|
|
8267
|
-
description: "Command timeout in ms (default 5000)"
|
|
8268
|
-
}
|
|
8141
|
+
browser: SESSION_BROWSER,
|
|
8142
|
+
timeout: CALL_TIMEOUT
|
|
8269
8143
|
},
|
|
8270
8144
|
required: [
|
|
8271
8145
|
"projectPath"
|
|
@@ -8283,7 +8157,7 @@ async function reload_handler(args) {
|
|
|
8283
8157
|
})
|
|
8284
8158
|
], args.projectPath, args.timeout);
|
|
8285
8159
|
}
|
|
8286
|
-
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.";
|
|
8287
8161
|
function filterPageTargets(raw) {
|
|
8288
8162
|
return raw.filter((t)=>"page" === t.type && !String(t.url ?? "").startsWith("devtools://")).map((t)=>({
|
|
8289
8163
|
targetId: String(t.id),
|
|
@@ -8301,38 +8175,35 @@ function matchTargetsByUrl(targets, needle) {
|
|
|
8301
8175
|
if (byUrl.length > 0) return byUrl;
|
|
8302
8176
|
return targets.filter((t)=>t.title.toLowerCase().includes(wanted));
|
|
8303
8177
|
}
|
|
8304
|
-
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
|
|
8305
|
-
const
|
|
8306
|
-
name: "
|
|
8307
|
-
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).",
|
|
8308
8182
|
inputSchema: {
|
|
8309
8183
|
type: "object",
|
|
8310
8184
|
properties: {
|
|
8311
|
-
projectPath:
|
|
8312
|
-
type: "string",
|
|
8313
|
-
description: "Path to the extension project root (must have an active dev session)"
|
|
8314
|
-
},
|
|
8185
|
+
projectPath: SESSION_PROJECT_PATH,
|
|
8315
8186
|
tab: {
|
|
8316
8187
|
type: "number",
|
|
8317
|
-
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."
|
|
8318
8189
|
},
|
|
8319
8190
|
url: {
|
|
8320
8191
|
type: "string",
|
|
8321
|
-
description: "
|
|
8192
|
+
description: "content/page: pick the tab by url (match pattern, then substring). Preferred over `tab`."
|
|
8322
8193
|
},
|
|
8323
8194
|
tabUrl: {
|
|
8324
8195
|
type: "string",
|
|
8325
|
-
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`."
|
|
8326
8197
|
},
|
|
8327
8198
|
listTargets: {
|
|
8328
8199
|
type: "boolean",
|
|
8329
8200
|
default: false,
|
|
8330
|
-
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."
|
|
8331
8202
|
},
|
|
8332
8203
|
listTabs: {
|
|
8333
8204
|
type: "boolean",
|
|
8334
8205
|
default: false,
|
|
8335
|
-
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."
|
|
8336
8207
|
},
|
|
8337
8208
|
context: {
|
|
8338
8209
|
type: "string",
|
|
@@ -8348,7 +8219,7 @@ const dom_inspect_schema = {
|
|
|
8348
8219
|
"bookmarks"
|
|
8349
8220
|
],
|
|
8350
8221
|
default: "content",
|
|
8351
|
-
description: "content/page
|
|
8222
|
+
description: "content/page targets `url`, else the active tab; the rest must already be OPEN"
|
|
8352
8223
|
},
|
|
8353
8224
|
include: {
|
|
8354
8225
|
type: "array",
|
|
@@ -8373,16 +8244,10 @@ const dom_inspect_schema = {
|
|
|
8373
8244
|
"number",
|
|
8374
8245
|
"boolean"
|
|
8375
8246
|
],
|
|
8376
|
-
description: "Also include recent console lines
|
|
8247
|
+
description: "Also include recent console lines. A number is how many; true means 50."
|
|
8377
8248
|
},
|
|
8378
|
-
browser:
|
|
8379
|
-
|
|
8380
|
-
description: "Browser session to target. Defaults to the active dev session's browser for this project."
|
|
8381
|
-
},
|
|
8382
|
-
timeout: {
|
|
8383
|
-
type: "number",
|
|
8384
|
-
description: "Command timeout in ms (default 5000)"
|
|
8385
|
-
}
|
|
8249
|
+
browser: SESSION_BROWSER,
|
|
8250
|
+
timeout: CALL_TIMEOUT
|
|
8386
8251
|
},
|
|
8387
8252
|
required: [
|
|
8388
8253
|
"projectPath"
|
|
@@ -8413,7 +8278,7 @@ async function cdpPortOrError(projectPath, browser, feature) {
|
|
|
8413
8278
|
port: resolved.port
|
|
8414
8279
|
};
|
|
8415
8280
|
}
|
|
8416
|
-
async function
|
|
8281
|
+
async function dom_snapshot_handler(args) {
|
|
8417
8282
|
const withConsole = true === args.withConsole ? 50 : false === args.withConsole ? void 0 : args.withConsole;
|
|
8418
8283
|
if (args.listTargets) {
|
|
8419
8284
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
@@ -8593,7 +8458,7 @@ async function dom_inspect_handler(args) {
|
|
|
8593
8458
|
}
|
|
8594
8459
|
const publish_schema = {
|
|
8595
8460
|
name: "extension_publish",
|
|
8596
|
-
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.",
|
|
8597
8462
|
inputSchema: {
|
|
8598
8463
|
type: "object",
|
|
8599
8464
|
properties: {
|
|
@@ -8603,12 +8468,9 @@ const publish_schema = {
|
|
|
8603
8468
|
},
|
|
8604
8469
|
buildSha: {
|
|
8605
8470
|
type: "string",
|
|
8606
|
-
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."
|
|
8607
8472
|
},
|
|
8608
|
-
api:
|
|
8609
|
-
type: "string",
|
|
8610
|
-
description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
|
|
8611
|
-
}
|
|
8473
|
+
api: API_BASE
|
|
8612
8474
|
},
|
|
8613
8475
|
required: []
|
|
8614
8476
|
}
|
|
@@ -8624,7 +8486,7 @@ function fail(name, message) {
|
|
|
8624
8486
|
}
|
|
8625
8487
|
async function publish_handler(args) {
|
|
8626
8488
|
const token = resolveToken();
|
|
8627
|
-
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).");
|
|
8628
8490
|
if (null != args.ttlHours) {
|
|
8629
8491
|
const t = Number(args.ttlHours);
|
|
8630
8492
|
if (!Number.isInteger(t) || t < 1 || t > 168) return fail("PublishBadRequest", "ttlHours must be an integer between 1 and 168.");
|
|
@@ -8670,7 +8532,7 @@ async function publish_handler(args) {
|
|
|
8670
8532
|
}
|
|
8671
8533
|
const release_promote_schema = {
|
|
8672
8534
|
name: "extension_release_promote",
|
|
8673
|
-
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).",
|
|
8674
8536
|
inputSchema: {
|
|
8675
8537
|
type: "object",
|
|
8676
8538
|
properties: {
|
|
@@ -8701,10 +8563,7 @@ const release_promote_schema = {
|
|
|
8701
8563
|
type: "string",
|
|
8702
8564
|
description: "Release notes markdown (optional)"
|
|
8703
8565
|
},
|
|
8704
|
-
api:
|
|
8705
|
-
type: "string",
|
|
8706
|
-
description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
|
|
8707
|
-
}
|
|
8566
|
+
api: API_BASE
|
|
8708
8567
|
},
|
|
8709
8568
|
required: [
|
|
8710
8569
|
"buildId",
|
|
@@ -8723,7 +8582,7 @@ function release_promote_fail(name, message) {
|
|
|
8723
8582
|
}
|
|
8724
8583
|
async function release_promote_handler(args) {
|
|
8725
8584
|
const token = resolveToken();
|
|
8726
|
-
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).");
|
|
8727
8586
|
const buildId = String(args.buildId || "").trim();
|
|
8728
8587
|
const channel = String(args.channel || "").trim();
|
|
8729
8588
|
if (!buildId || !channel) return release_promote_fail("ReleaseInputError", "buildId and channel are required.");
|
|
@@ -8766,7 +8625,7 @@ async function release_promote_handler(args) {
|
|
|
8766
8625
|
const ref = resolveProjectRef();
|
|
8767
8626
|
if (404 === res.status || "UNKNOWN_BUILD" === code) {
|
|
8768
8627
|
enrich.buildsPageUrl = consoleProjectUrl(ref, "builds", args.api);
|
|
8769
|
-
enrich.hint = "Run
|
|
8628
|
+
enrich.hint = "Run extension_release_status to see this project's channels, their promoted shas, and recent builds.";
|
|
8770
8629
|
if (ref) {
|
|
8771
8630
|
const channelsUrl = registryFileUrl(ref, "channels.json");
|
|
8772
8631
|
const channelsRes = await fetchRegistryJson(channelsUrl, fetch, {
|
|
@@ -8809,28 +8668,6 @@ async function release_promote_handler(args) {
|
|
|
8809
8668
|
} : data;
|
|
8810
8669
|
return JSON.stringify(enriched);
|
|
8811
8670
|
}
|
|
8812
|
-
const release_list_schema = {
|
|
8813
|
-
name: "extension_release_list",
|
|
8814
|
-
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).",
|
|
8815
|
-
inputSchema: {
|
|
8816
|
-
type: "object",
|
|
8817
|
-
properties: {
|
|
8818
|
-
workspace: {
|
|
8819
|
-
type: "string",
|
|
8820
|
-
description: "Workspace slug override (defaults to the stored login's workspace)."
|
|
8821
|
-
},
|
|
8822
|
-
project: {
|
|
8823
|
-
type: "string",
|
|
8824
|
-
description: "Project slug override (defaults to the stored login's project)."
|
|
8825
|
-
},
|
|
8826
|
-
api: {
|
|
8827
|
-
type: "string",
|
|
8828
|
-
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."
|
|
8829
|
-
}
|
|
8830
|
-
},
|
|
8831
|
-
required: []
|
|
8832
|
-
}
|
|
8833
|
-
};
|
|
8834
8671
|
function release_list_fail(name, message, extra) {
|
|
8835
8672
|
return JSON.stringify({
|
|
8836
8673
|
ok: false,
|
|
@@ -8841,9 +8678,9 @@ function release_list_fail(name, message, extra) {
|
|
|
8841
8678
|
...extra ?? {}
|
|
8842
8679
|
});
|
|
8843
8680
|
}
|
|
8844
|
-
async function
|
|
8681
|
+
async function readReleases(args) {
|
|
8845
8682
|
const ref = resolveProjectRef(args);
|
|
8846
|
-
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.");
|
|
8847
8684
|
const channelsUrl = registryFileUrl(ref, "channels.json");
|
|
8848
8685
|
const metaUrl = registryFileUrl(ref, "meta.json");
|
|
8849
8686
|
const buildsUrl = registryFileUrl(ref, "builds/index.json");
|
|
@@ -8862,7 +8699,7 @@ async function release_list_handler(args) {
|
|
|
8862
8699
|
})
|
|
8863
8700
|
]);
|
|
8864
8701
|
const buildsPageUrl = consoleProjectUrl(ref, "builds", args.api);
|
|
8865
|
-
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}`, {
|
|
8866
8703
|
workspace: ref.workspace,
|
|
8867
8704
|
project: ref.project,
|
|
8868
8705
|
registryUrl: channelsUrl,
|
|
@@ -8909,246 +8746,12 @@ async function release_list_handler(args) {
|
|
|
8909
8746
|
if (!buildsRes.ok) result.buildsUnavailable = `builds/index.json unreadable: ${buildsRes.message}`;
|
|
8910
8747
|
return JSON.stringify(result);
|
|
8911
8748
|
}
|
|
8912
|
-
function storeMdWarnings(browsers, cwd) {
|
|
8913
|
-
const wantsFirefox = browsers.includes("firefox");
|
|
8914
|
-
const wantsEdge = browsers.includes("edge");
|
|
8915
|
-
if (!wantsFirefox && !wantsEdge) return [];
|
|
8916
|
-
let content;
|
|
8917
|
-
try {
|
|
8918
|
-
content = node_fs.readFileSync(node_path.join(cwd, "STORE.md"), "utf8");
|
|
8919
|
-
} catch {
|
|
8920
|
-
return [
|
|
8921
|
-
"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."
|
|
8922
|
-
];
|
|
8923
|
-
}
|
|
8924
|
-
const hasField = (section, field)=>{
|
|
8925
|
-
const parts = content.split(/^## +/m);
|
|
8926
|
-
const match = parts.find((p)=>section.test(p.split("\n", 1)[0] ?? ""));
|
|
8927
|
-
if (!match) return false;
|
|
8928
|
-
const sub = match.split(/^### +/m).find((p)=>field.test(p.split("\n", 1)[0] ?? ""));
|
|
8929
|
-
if (!sub) return false;
|
|
8930
|
-
const body = sub.split("\n").slice(1).join("\n");
|
|
8931
|
-
return body.replace(/<!--[\s\S]*?-->/g, "").trim().length > 0;
|
|
8932
|
-
};
|
|
8933
|
-
const warnings = [];
|
|
8934
|
-
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.");
|
|
8935
|
-
if (wantsEdge && !hasField(/edge/i, /certification notes/i)) warnings.push("STORE.md has no Edge certification notes; the certification team gets no testing guidance.");
|
|
8936
|
-
return warnings;
|
|
8937
|
-
}
|
|
8938
|
-
const deploy_schema = {
|
|
8939
|
-
name: "extension_deploy",
|
|
8940
|
-
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.",
|
|
8941
|
-
inputSchema: {
|
|
8942
|
-
type: "object",
|
|
8943
|
-
properties: {
|
|
8944
|
-
browsers: {
|
|
8945
|
-
type: "array",
|
|
8946
|
-
items: {
|
|
8947
|
-
type: "string",
|
|
8948
|
-
enum: [
|
|
8949
|
-
"chrome",
|
|
8950
|
-
"firefox",
|
|
8951
|
-
"edge",
|
|
8952
|
-
"safari"
|
|
8953
|
-
]
|
|
8954
|
-
},
|
|
8955
|
-
description: "Stores to submit to."
|
|
8956
|
-
},
|
|
8957
|
-
buildSha: {
|
|
8958
|
-
type: "string",
|
|
8959
|
-
description: "The built commit SHA to submit. It must have a completed build in the project's build index; an unknown sha is rejected."
|
|
8960
|
-
},
|
|
8961
|
-
channel: {
|
|
8962
|
-
type: "string",
|
|
8963
|
-
description: "Release channel to submit from (default stable)."
|
|
8964
|
-
},
|
|
8965
|
-
version: {
|
|
8966
|
-
type: "string",
|
|
8967
|
-
description: "Version label for the submission record (optional)."
|
|
8968
|
-
},
|
|
8969
|
-
dryRun: {
|
|
8970
|
-
type: "boolean",
|
|
8971
|
-
default: true,
|
|
8972
|
-
description: "Preflight only (verify auth, project, build, and store workflow). Pass false to actually dispatch the submission (irreversible, enters store review)."
|
|
8973
|
-
},
|
|
8974
|
-
api: {
|
|
8975
|
-
type: "string",
|
|
8976
|
-
description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
|
|
8977
|
-
}
|
|
8978
|
-
},
|
|
8979
|
-
required: [
|
|
8980
|
-
"browsers",
|
|
8981
|
-
"buildSha"
|
|
8982
|
-
]
|
|
8983
|
-
}
|
|
8984
|
-
};
|
|
8985
|
-
function deploy_fail(name, message) {
|
|
8986
|
-
return JSON.stringify({
|
|
8987
|
-
ok: false,
|
|
8988
|
-
error: {
|
|
8989
|
-
name,
|
|
8990
|
-
message
|
|
8991
|
-
}
|
|
8992
|
-
});
|
|
8993
|
-
}
|
|
8994
|
-
async function deploy_handler(args) {
|
|
8995
|
-
const token = resolveToken();
|
|
8996
|
-
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).");
|
|
8997
|
-
const browsers = (Array.isArray(args.browsers) ? args.browsers : []).map((b)=>String(b).trim().toLowerCase()).filter(Boolean);
|
|
8998
|
-
if (0 === browsers.length) return deploy_fail("DeployInputError", 'browsers is required (e.g. ["chrome","firefox","edge","safari"]).');
|
|
8999
|
-
const buildSha = String(args.buildSha || "").trim();
|
|
9000
|
-
if (!buildSha) return deploy_fail("DeployInputError", "buildSha is required (the built commit to submit).");
|
|
9001
|
-
const apiCheck = safeApiBase(resolveApiBase(args.api));
|
|
9002
|
-
if (!apiCheck.ok) return deploy_fail("DeployConfigError", apiCheck.message);
|
|
9003
|
-
const url = `${apiCheck.base}/api/cli/stores/submit`;
|
|
9004
|
-
const dryRun = false !== args.dryRun;
|
|
9005
|
-
const body = {
|
|
9006
|
-
browsers,
|
|
9007
|
-
buildSha,
|
|
9008
|
-
dryRun
|
|
9009
|
-
};
|
|
9010
|
-
if (args.channel) body.channel = String(args.channel).trim();
|
|
9011
|
-
if (args.version) body.version = String(args.version).trim();
|
|
9012
|
-
let res;
|
|
9013
|
-
try {
|
|
9014
|
-
res = await fetch(url, {
|
|
9015
|
-
method: "POST",
|
|
9016
|
-
headers: {
|
|
9017
|
-
authorization: `Bearer ${token}`,
|
|
9018
|
-
"content-type": "application/json"
|
|
9019
|
-
},
|
|
9020
|
-
body: JSON.stringify(body)
|
|
9021
|
-
});
|
|
9022
|
-
} catch (err) {
|
|
9023
|
-
return deploy_fail("DeployNetworkError", `Could not reach ${url}: ${err?.message || err}`);
|
|
9024
|
-
}
|
|
9025
|
-
const text = await res.text();
|
|
9026
|
-
let data;
|
|
9027
|
-
try {
|
|
9028
|
-
data = JSON.parse(text);
|
|
9029
|
-
} catch {
|
|
9030
|
-
data = {
|
|
9031
|
-
message: text
|
|
9032
|
-
};
|
|
9033
|
-
}
|
|
9034
|
-
if (!res.ok) return deploy_fail("DeployError", `${dryRun ? "preflight" : "submit"} failed (${res.status}): ${data?.message || text || "unknown error"}`);
|
|
9035
|
-
const warnings = Array.isArray(data?.warnings) ? [
|
|
9036
|
-
...data.warnings
|
|
9037
|
-
] : [];
|
|
9038
|
-
warnings.push(...storeMdWarnings(browsers, process.cwd()));
|
|
9039
|
-
const result = {
|
|
9040
|
-
mode: "platform",
|
|
9041
|
-
dryRun,
|
|
9042
|
-
...data
|
|
9043
|
-
};
|
|
9044
|
-
if (dryRun) {
|
|
9045
|
-
const ref = resolveProjectRef();
|
|
9046
|
-
const consoleStoresUrl = consoleProjectUrl(ref, "stores", args.api);
|
|
9047
|
-
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}.`;
|
|
9048
|
-
let health = null;
|
|
9049
|
-
let healthUnreadable = null;
|
|
9050
|
-
let channelRows = null;
|
|
9051
|
-
if (ref) {
|
|
9052
|
-
const [healthRes, channelsRes] = await Promise.all([
|
|
9053
|
-
fetchRegistryJson(registryFileUrl(ref, "stores/health.json"), fetch, {
|
|
9054
|
-
ref,
|
|
9055
|
-
api: args.api
|
|
9056
|
-
}),
|
|
9057
|
-
fetchRegistryJson(registryFileUrl(ref, "channels.json"), fetch, {
|
|
9058
|
-
ref,
|
|
9059
|
-
api: args.api
|
|
9060
|
-
})
|
|
9061
|
-
]);
|
|
9062
|
-
if (healthRes.ok) {
|
|
9063
|
-
const stores = healthRes.json?.stores;
|
|
9064
|
-
health = stores && "object" == typeof stores ? stores : null;
|
|
9065
|
-
if (!health) healthUnreadable = "stores/health.json had no stores map";
|
|
9066
|
-
} else healthUnreadable = healthRes.message;
|
|
9067
|
-
if (channelsRes.ok) channelRows = parseChannels(channelsRes.json);
|
|
9068
|
-
} else healthUnreadable = "no stored workspace/project to look up (run extension_login)";
|
|
9069
|
-
const preflight = browsers.map((browser)=>{
|
|
9070
|
-
if (!health) return {
|
|
9071
|
-
browser,
|
|
9072
|
-
ok: false,
|
|
9073
|
-
configured: "unknown",
|
|
9074
|
-
publishMode: "unknown",
|
|
9075
|
-
reason: `Store configuration could not be read (${healthUnreadable}); verify the ${browser} store in the console before submitting.`
|
|
9076
|
-
};
|
|
9077
|
-
const row = health[browser];
|
|
9078
|
-
if (!row) return {
|
|
9079
|
-
browser,
|
|
9080
|
-
ok: false,
|
|
9081
|
-
configured: false,
|
|
9082
|
-
publishMode: "unknown",
|
|
9083
|
-
reason: `No ${browser} store is configured on this project; a real submission for ${browser} would fail. Configure it at ${consoleStoresUrl}.`
|
|
9084
|
-
};
|
|
9085
|
-
if (true !== row.ok) return {
|
|
9086
|
-
browser,
|
|
9087
|
-
ok: false,
|
|
9088
|
-
configured: false,
|
|
9089
|
-
publishMode: "unknown",
|
|
9090
|
-
reason: String(row.message || "").trim() || `The ${browser} store failed its last credential health check.`
|
|
9091
|
-
};
|
|
9092
|
-
return {
|
|
9093
|
-
browser,
|
|
9094
|
-
ok: true,
|
|
9095
|
-
configured: true,
|
|
9096
|
-
publishMode: "unknown"
|
|
9097
|
-
};
|
|
9098
|
-
});
|
|
9099
|
-
const actionable = preflight.filter((p)=>p.ok).map((p)=>p.browser);
|
|
9100
|
-
const blocked = preflight.filter((p)=>!p.ok);
|
|
9101
|
-
const channelDefaulted = !String(args.channel || "").trim();
|
|
9102
|
-
const resolvedChannel = String(data?.channel || "").trim() || (channelDefaulted ? "stable" : String(args.channel).trim());
|
|
9103
|
-
if (channelRows) {
|
|
9104
|
-
const exists = channelRows.some((r)=>r.channel === resolvedChannel || r.channel.endsWith(`-${resolvedChannel}`));
|
|
9105
|
-
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.`);
|
|
9106
|
-
}
|
|
9107
|
-
const summaryParts = [];
|
|
9108
|
-
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.`);
|
|
9109
|
-
for (const p of blocked)summaryParts.push(`${p.browser}: ${"unknown" === p.configured ? "cannot be verified" : "NOT actionable"} - ${p.reason}`);
|
|
9110
|
-
summaryParts.push(storeModeNote);
|
|
9111
|
-
result.ok = actionable.length > 0;
|
|
9112
|
-
result.preflight = preflight;
|
|
9113
|
-
result.channel = resolvedChannel;
|
|
9114
|
-
result.channelDefaulted = channelDefaulted;
|
|
9115
|
-
if (channelDefaulted) result.channelNote = `channel: ${resolvedChannel} (default)`;
|
|
9116
|
-
result.consoleStoresUrl = consoleStoresUrl;
|
|
9117
|
-
if ("string" == typeof data?.message) result.platformMessage = data.message;
|
|
9118
|
-
result.message = summaryParts.join(" ");
|
|
9119
|
-
}
|
|
9120
|
-
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.";
|
|
9121
|
-
if (warnings.length > 0) result.warnings = warnings;
|
|
9122
|
-
return JSON.stringify(result);
|
|
9123
|
-
}
|
|
9124
8749
|
const KNOWN_STORES = [
|
|
9125
8750
|
"chrome",
|
|
9126
8751
|
"firefox",
|
|
9127
8752
|
"edge",
|
|
9128
8753
|
"safari"
|
|
9129
8754
|
];
|
|
9130
|
-
const store_status_schema = {
|
|
9131
|
-
name: "extension_store_status",
|
|
9132
|
-
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.",
|
|
9133
|
-
inputSchema: {
|
|
9134
|
-
type: "object",
|
|
9135
|
-
properties: {
|
|
9136
|
-
workspace: {
|
|
9137
|
-
type: "string",
|
|
9138
|
-
description: "Workspace slug override (defaults to the stored login's workspace)."
|
|
9139
|
-
},
|
|
9140
|
-
project: {
|
|
9141
|
-
type: "string",
|
|
9142
|
-
description: "Project slug override (defaults to the stored login's project)."
|
|
9143
|
-
},
|
|
9144
|
-
api: {
|
|
9145
|
-
type: "string",
|
|
9146
|
-
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."
|
|
9147
|
-
}
|
|
9148
|
-
},
|
|
9149
|
-
required: []
|
|
9150
|
-
}
|
|
9151
|
-
};
|
|
9152
8755
|
function isPlainObject(value) {
|
|
9153
8756
|
return Boolean(value) && "object" == typeof value && !Array.isArray(value);
|
|
9154
8757
|
}
|
|
@@ -9237,9 +8840,9 @@ function store_status_fail(name, message, extra) {
|
|
|
9237
8840
|
...extra ?? {}
|
|
9238
8841
|
});
|
|
9239
8842
|
}
|
|
9240
|
-
async function
|
|
8843
|
+
async function readStores(args) {
|
|
9241
8844
|
const ref = resolveProjectRef(args);
|
|
9242
|
-
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.");
|
|
9243
8846
|
const healthUrl = registryFileUrl(ref, "stores/health.json");
|
|
9244
8847
|
const statusUrl = registryFileUrl(ref, "stores/status.json");
|
|
9245
8848
|
const submissionsUrl = registryFileUrl(ref, "stores/submissions.json");
|
|
@@ -9315,27 +8918,312 @@ async function store_status_handler(args) {
|
|
|
9315
8918
|
return tail.length > 0 ? `${head}; ${tail.join("; ")}` : head;
|
|
9316
8919
|
});
|
|
9317
8920
|
const result = {
|
|
9318
|
-
ok: true,
|
|
9319
|
-
workspace: ref.workspace,
|
|
9320
|
-
project: ref.project,
|
|
9321
|
-
stores: rows,
|
|
9322
|
-
...status.lastSubmission ? {
|
|
9323
|
-
lastSubmission: status.lastSubmission
|
|
9324
|
-
} : {},
|
|
9325
|
-
...status.lastPollAt ? {
|
|
9326
|
-
lastPollAt: status.lastPollAt
|
|
9327
|
-
} : {},
|
|
9328
|
-
registryUrls: {
|
|
9329
|
-
health: healthUrl,
|
|
9330
|
-
status: statusUrl,
|
|
9331
|
-
submissions: submissionsUrl
|
|
9332
|
-
},
|
|
9333
|
-
consoleStoresUrl,
|
|
9334
|
-
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.`
|
|
8921
|
+
ok: true,
|
|
8922
|
+
workspace: ref.workspace,
|
|
8923
|
+
project: ref.project,
|
|
8924
|
+
stores: rows,
|
|
8925
|
+
...status.lastSubmission ? {
|
|
8926
|
+
lastSubmission: status.lastSubmission
|
|
8927
|
+
} : {},
|
|
8928
|
+
...status.lastPollAt ? {
|
|
8929
|
+
lastPollAt: status.lastPollAt
|
|
8930
|
+
} : {},
|
|
8931
|
+
registryUrls: {
|
|
8932
|
+
health: healthUrl,
|
|
8933
|
+
status: statusUrl,
|
|
8934
|
+
submissions: submissionsUrl
|
|
8935
|
+
},
|
|
8936
|
+
consoleStoresUrl,
|
|
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.`
|
|
8938
|
+
};
|
|
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
|
|
9335
9148
|
};
|
|
9336
|
-
if (
|
|
9337
|
-
|
|
9338
|
-
|
|
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;
|
|
9339
9227
|
return JSON.stringify(result);
|
|
9340
9228
|
}
|
|
9341
9229
|
const ENGINE_COMPANION_IDS = new Set([
|
|
@@ -9405,7 +9293,7 @@ function doctor_readReadyContract(projectPath, browser) {
|
|
|
9405
9293
|
}
|
|
9406
9294
|
const doctor_schema = {
|
|
9407
9295
|
name: "extension_doctor",
|
|
9408
|
-
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.",
|
|
9409
9297
|
inputSchema: {
|
|
9410
9298
|
type: "object",
|
|
9411
9299
|
properties: {
|
|
@@ -9446,7 +9334,7 @@ async function environmentPreflight() {
|
|
|
9446
9334
|
checks.push({
|
|
9447
9335
|
check: "template-cache",
|
|
9448
9336
|
status: cacheExists ? "pass" : "warn",
|
|
9449
|
-
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)"
|
|
9450
9338
|
});
|
|
9451
9339
|
const healthy = checks.every((c)=>"fail" !== c.status);
|
|
9452
9340
|
return JSON.stringify({
|
|
@@ -9590,26 +9478,20 @@ function wait_isAlive(pid) {
|
|
|
9590
9478
|
}
|
|
9591
9479
|
const wait_schema = {
|
|
9592
9480
|
name: "extension_wait",
|
|
9593
|
-
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.",
|
|
9594
9482
|
inputSchema: {
|
|
9595
9483
|
type: "object",
|
|
9596
9484
|
properties: {
|
|
9597
|
-
projectPath:
|
|
9598
|
-
|
|
9599
|
-
description: "Path to the extension project root"
|
|
9600
|
-
},
|
|
9601
|
-
browser: {
|
|
9602
|
-
type: "string",
|
|
9603
|
-
description: "Browser to check readiness for. Defaults to the active dev session's browser for this project."
|
|
9604
|
-
},
|
|
9485
|
+
projectPath: PROJECT_PATH,
|
|
9486
|
+
browser: SESSION_BROWSER,
|
|
9605
9487
|
timeoutMs: {
|
|
9606
9488
|
type: "number",
|
|
9607
9489
|
default: DEFAULT_TIMEOUT_MS,
|
|
9608
|
-
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.`
|
|
9609
9491
|
},
|
|
9610
9492
|
timeout: {
|
|
9611
9493
|
type: "number",
|
|
9612
|
-
description: "Deprecated alias of timeoutMs,
|
|
9494
|
+
description: "Deprecated alias of timeoutMs, which wins when both are given."
|
|
9613
9495
|
}
|
|
9614
9496
|
},
|
|
9615
9497
|
required: [
|
|
@@ -9648,7 +9530,7 @@ async function wait_handler(args) {
|
|
|
9648
9530
|
buildOnly: true,
|
|
9649
9531
|
compiled: true,
|
|
9650
9532
|
browserAttached: false,
|
|
9651
|
-
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.",
|
|
9652
9534
|
command: contract.command,
|
|
9653
9535
|
browser: contract.browser,
|
|
9654
9536
|
port: contract.port,
|
|
@@ -9739,10 +9621,7 @@ const add_feature_schema = {
|
|
|
9739
9621
|
inputSchema: {
|
|
9740
9622
|
type: "object",
|
|
9741
9623
|
properties: {
|
|
9742
|
-
projectPath:
|
|
9743
|
-
type: "string",
|
|
9744
|
-
description: "Path to the extension project root"
|
|
9745
|
-
},
|
|
9624
|
+
projectPath: PROJECT_PATH,
|
|
9746
9625
|
feature: {
|
|
9747
9626
|
type: "string",
|
|
9748
9627
|
enum: [
|
|
@@ -10088,30 +9967,6 @@ async function pollDeviceToken(args) {
|
|
|
10088
9967
|
await new Promise((r)=>setTimeout(r, 1000 * interval));
|
|
10089
9968
|
}
|
|
10090
9969
|
}
|
|
10091
|
-
const login_schema = {
|
|
10092
|
-
name: "extension_login",
|
|
10093
|
-
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. You authorize at extension.dev/device; GitHub federation happens server-side, so no GitHub token ever lands on this machine. 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.",
|
|
10094
|
-
inputSchema: {
|
|
10095
|
-
type: "object",
|
|
10096
|
-
properties: {
|
|
10097
|
-
project: {
|
|
10098
|
-
type: "string",
|
|
10099
|
-
description: "Target project as '<workspace>/<project>' (the token is scoped to it)"
|
|
10100
|
-
},
|
|
10101
|
-
deviceCode: {
|
|
10102
|
-
type: "string",
|
|
10103
|
-
description: "Resume token from a prior call's `deviceCode`; omit on the first call"
|
|
10104
|
-
},
|
|
10105
|
-
api: {
|
|
10106
|
-
type: "string",
|
|
10107
|
-
description: "Platform base URL (defaults to https://www.extension.dev or EXTENSION_DEV_API_URL)"
|
|
10108
|
-
}
|
|
10109
|
-
},
|
|
10110
|
-
required: [
|
|
10111
|
-
"project"
|
|
10112
|
-
]
|
|
10113
|
-
}
|
|
10114
|
-
};
|
|
10115
9970
|
const FIRST_CALL_BUDGET_MS = 8000;
|
|
10116
9971
|
const RESUME_BUDGET_MS = 22000;
|
|
10117
9972
|
function login_fail(name, message) {
|
|
@@ -10138,7 +9993,7 @@ function success(creds) {
|
|
|
10138
9993
|
function login_pending(start) {
|
|
10139
9994
|
const complete = String(start.verificationUriComplete || "").trim();
|
|
10140
9995
|
const hasCompleteLink = complete.length > 0 && complete !== start.verificationUri;
|
|
10141
|
-
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.`;
|
|
10142
9997
|
return JSON.stringify({
|
|
10143
9998
|
ok: false,
|
|
10144
9999
|
status: "authorization_pending",
|
|
@@ -10159,10 +10014,10 @@ function resumePending(deviceCode, verificationUri) {
|
|
|
10159
10014
|
verificationUri,
|
|
10160
10015
|
deviceCode,
|
|
10161
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).",
|
|
10162
|
-
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.`
|
|
10163
10018
|
});
|
|
10164
10019
|
}
|
|
10165
|
-
async function
|
|
10020
|
+
async function loginToProject(args) {
|
|
10166
10021
|
const project = String(args.project || "").trim();
|
|
10167
10022
|
if (!/^[^/]+\/[^/]+$/.test(project)) return login_fail("BadRequest", "project must be in the form '<workspace>/<project>'.");
|
|
10168
10023
|
const apiBase = resolveApiBase(args.api);
|
|
@@ -10182,7 +10037,7 @@ async function login_handler(args) {
|
|
|
10182
10037
|
budgetMs: RESUME_BUDGET_MS
|
|
10183
10038
|
});
|
|
10184
10039
|
if (poll.ok) return success(poll.creds);
|
|
10185
|
-
if ("expired" === poll.reason) return login_fail("LoginExpired", "The device code expired. Run
|
|
10040
|
+
if ("expired" === poll.reason) return login_fail("LoginExpired", "The device code expired. Run extension_auth (action: login) again to restart.");
|
|
10186
10041
|
if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied at extension.dev/device.");
|
|
10187
10042
|
if ("error" === poll.reason) return login_fail("LoginError", poll.message || "Device login failed.");
|
|
10188
10043
|
return resumePending(String(args.deviceCode), config.verificationUri);
|
|
@@ -10214,20 +10069,12 @@ async function login_handler(args) {
|
|
|
10214
10069
|
verificationUriComplete: start.verificationUriComplete
|
|
10215
10070
|
});
|
|
10216
10071
|
}
|
|
10217
|
-
|
|
10218
|
-
name: "extension_whoami",
|
|
10219
|
-
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.",
|
|
10220
|
-
inputSchema: {
|
|
10221
|
-
type: "object",
|
|
10222
|
-
properties: {}
|
|
10223
|
-
}
|
|
10224
|
-
};
|
|
10225
|
-
async function whoami_handler() {
|
|
10072
|
+
async function readIdentity() {
|
|
10226
10073
|
const creds = readCredentials();
|
|
10227
10074
|
if (!creds) return JSON.stringify({
|
|
10228
10075
|
ok: true,
|
|
10229
10076
|
status: "logged-out",
|
|
10230
|
-
message: "No stored credentials. Run
|
|
10077
|
+
message: "No stored credentials. Run extension_auth (action: login) to authenticate."
|
|
10231
10078
|
});
|
|
10232
10079
|
const now = Math.floor(Date.now() / 1000);
|
|
10233
10080
|
const expired = Boolean(creds.expiresAt && creds.expiresAt <= now);
|
|
@@ -10236,7 +10083,7 @@ async function whoami_handler() {
|
|
|
10236
10083
|
const apiDiverges = Boolean(recordedApi) && recordedApi !== effectiveDefaultApi;
|
|
10237
10084
|
const envTokenSet = Boolean(String(process.env.EXTENSION_DEV_TOKEN || "").trim());
|
|
10238
10085
|
const messageParts = [];
|
|
10239
|
-
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.`);
|
|
10240
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.`);
|
|
10241
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.");
|
|
10242
10089
|
return JSON.stringify({
|
|
@@ -10245,227 +10092,75 @@ async function whoami_handler() {
|
|
|
10245
10092
|
workspaceSlug: creds.workspaceSlug,
|
|
10246
10093
|
projectSlug: creds.projectSlug,
|
|
10247
10094
|
...recordedApi ? {
|
|
10248
|
-
apiRecordedAtLogin: recordedApi
|
|
10249
|
-
} : {},
|
|
10250
|
-
apiDefault: effectiveDefaultApi,
|
|
10251
|
-
provider: creds.provider ?? "extensiondev",
|
|
10252
|
-
expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
|
|
10253
|
-
expiresInSeconds: creds.expiresAt ? creds.expiresAt - now : null,
|
|
10254
|
-
expired,
|
|
10255
|
-
...envTokenSet ? {
|
|
10256
|
-
envTokenOverride: true
|
|
10257
|
-
} : {},
|
|
10258
|
-
tokenTtlNote: tokenTtlNote(creds.workspaceSlug, creds.projectSlug),
|
|
10259
|
-
message: messageParts.join(" ")
|
|
10260
|
-
});
|
|
10261
|
-
}
|
|
10262
|
-
|
|
10263
|
-
|
|
10264
|
-
|
|
10265
|
-
|
|
10266
|
-
|
|
10267
|
-
|
|
10268
|
-
|
|
10269
|
-
|
|
10270
|
-
|
|
10271
|
-
|
|
10272
|
-
|
|
10273
|
-
|
|
10274
|
-
|
|
10275
|
-
|
|
10276
|
-
|
|
10277
|
-
return JSON.stringify({
|
|
10278
|
-
ok: true,
|
|
10279
|
-
cleared: result.cleared,
|
|
10280
|
-
...result.cleared && revokeUrl ? {
|
|
10281
|
-
revokeUrl
|
|
10282
|
-
} : {},
|
|
10283
|
-
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."
|
|
10284
|
-
});
|
|
10285
|
-
}
|
|
10286
|
-
const install_browser_schema = {
|
|
10287
|
-
name: "extension_install_browser",
|
|
10288
|
-
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.",
|
|
10289
|
-
inputSchema: {
|
|
10290
|
-
type: "object",
|
|
10291
|
-
properties: {
|
|
10292
|
-
browser: {
|
|
10293
|
-
type: "string",
|
|
10294
|
-
enum: [
|
|
10295
|
-
"chrome",
|
|
10296
|
-
"chromium",
|
|
10297
|
-
"edge",
|
|
10298
|
-
"firefox"
|
|
10299
|
-
],
|
|
10300
|
-
description: "Browser to install"
|
|
10301
|
-
}
|
|
10302
|
-
},
|
|
10303
|
-
required: [
|
|
10304
|
-
"browser"
|
|
10305
|
-
]
|
|
10306
|
-
}
|
|
10307
|
-
};
|
|
10308
|
-
async function install_browser_handler(args) {
|
|
10309
|
-
const start = Date.now();
|
|
10310
|
-
try {
|
|
10311
|
-
await extensionInstall({
|
|
10312
|
-
browser: args.browser
|
|
10313
|
-
});
|
|
10314
|
-
return JSON.stringify({
|
|
10315
|
-
status: "installed",
|
|
10316
|
-
browser: args.browser,
|
|
10317
|
-
duration: Date.now() - start,
|
|
10318
|
-
hint: `Browser "${args.browser}" is now available. Use extension_dev or extension_start with browser: "${args.browser}".`
|
|
10319
|
-
});
|
|
10320
|
-
} catch (err) {
|
|
10321
|
-
return JSON.stringify({
|
|
10322
|
-
status: "error",
|
|
10323
|
-
browser: args.browser,
|
|
10324
|
-
message: err instanceof Error ? err.message : String(err),
|
|
10325
|
-
duration: Date.now() - start,
|
|
10326
|
-
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."
|
|
10327
|
-
});
|
|
10328
|
-
}
|
|
10095
|
+
apiRecordedAtLogin: recordedApi
|
|
10096
|
+
} : {},
|
|
10097
|
+
apiDefault: effectiveDefaultApi,
|
|
10098
|
+
provider: creds.provider ?? "extensiondev",
|
|
10099
|
+
expiresAt: creds.expiresAt ? new Date(1000 * creds.expiresAt).toISOString() : null,
|
|
10100
|
+
expiresInSeconds: creds.expiresAt ? creds.expiresAt - now : null,
|
|
10101
|
+
expired,
|
|
10102
|
+
...envTokenSet ? {
|
|
10103
|
+
envTokenOverride: true
|
|
10104
|
+
} : {},
|
|
10105
|
+
tokenTtlNote: tokenTtlNote(creds.workspaceSlug, creds.projectSlug),
|
|
10106
|
+
message: messageParts.join(" ")
|
|
10107
|
+
});
|
|
10108
|
+
}
|
|
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
|
+
});
|
|
10329
10124
|
}
|
|
10330
|
-
const
|
|
10331
|
-
name: "
|
|
10332
|
-
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.",
|
|
10333
10128
|
inputSchema: {
|
|
10334
10129
|
type: "object",
|
|
10335
10130
|
properties: {
|
|
10336
|
-
|
|
10131
|
+
action: {
|
|
10337
10132
|
type: "string",
|
|
10338
10133
|
enum: [
|
|
10339
|
-
"
|
|
10340
|
-
"
|
|
10341
|
-
"
|
|
10342
|
-
"firefox"
|
|
10134
|
+
"status",
|
|
10135
|
+
"login",
|
|
10136
|
+
"logout"
|
|
10343
10137
|
],
|
|
10344
|
-
|
|
10138
|
+
default: "status"
|
|
10345
10139
|
},
|
|
10346
|
-
|
|
10347
|
-
type: "
|
|
10348
|
-
|
|
10349
|
-
|
|
10350
|
-
|
|
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
|
|
10351
10149
|
},
|
|
10352
10150
|
required: []
|
|
10353
10151
|
}
|
|
10354
10152
|
};
|
|
10355
|
-
async function
|
|
10356
|
-
const
|
|
10357
|
-
if (
|
|
10358
|
-
|
|
10359
|
-
|
|
10360
|
-
|
|
10361
|
-
|
|
10362
|
-
await extensionUninstall({
|
|
10363
|
-
browser: args.browser,
|
|
10364
|
-
all: args.all
|
|
10365
|
-
});
|
|
10366
|
-
return JSON.stringify({
|
|
10367
|
-
status: "uninstalled",
|
|
10368
|
-
target: args.all ? "all" : args.browser,
|
|
10369
|
-
duration: Date.now() - start,
|
|
10370
|
-
hint: "Use extension_list_browsers to confirm what remains in the managed cache."
|
|
10371
|
-
});
|
|
10372
|
-
} catch (err) {
|
|
10373
|
-
return JSON.stringify({
|
|
10374
|
-
status: "error",
|
|
10375
|
-
target: args.all ? "all" : args.browser,
|
|
10376
|
-
message: err instanceof Error ? err.message : String(err),
|
|
10377
|
-
duration: Date.now() - start
|
|
10378
|
-
});
|
|
10379
|
-
}
|
|
10380
|
-
}
|
|
10381
|
-
const list_browsers_schema = {
|
|
10382
|
-
name: "extension_list_browsers",
|
|
10383
|
-
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.",
|
|
10384
|
-
inputSchema: {
|
|
10385
|
-
type: "object",
|
|
10386
|
-
properties: {}
|
|
10387
|
-
}
|
|
10388
|
-
};
|
|
10389
|
-
const BROWSER_NAMES = [
|
|
10390
|
-
"chrome",
|
|
10391
|
-
"chromium",
|
|
10392
|
-
"edge",
|
|
10393
|
-
"firefox"
|
|
10394
|
-
];
|
|
10395
|
-
function getDirSize(dir) {
|
|
10396
|
-
let total = 0;
|
|
10397
|
-
try {
|
|
10398
|
-
for (const entry of readdirSync(dir, {
|
|
10399
|
-
withFileTypes: true
|
|
10400
|
-
})){
|
|
10401
|
-
const full = join(dir, entry.name);
|
|
10402
|
-
if (entry.isDirectory()) total += getDirSize(full);
|
|
10403
|
-
else try {
|
|
10404
|
-
total += statSync(full).size;
|
|
10405
|
-
} catch {}
|
|
10406
|
-
}
|
|
10407
|
-
} catch {}
|
|
10408
|
-
return total;
|
|
10409
|
-
}
|
|
10410
|
-
function list_browsers_formatBytes(bytes) {
|
|
10411
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
10412
|
-
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
10413
|
-
return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
10414
|
-
}
|
|
10415
|
-
async function list_browsers_handler() {
|
|
10416
|
-
const cacheRoot = getManagedBrowsersCacheRoot();
|
|
10417
|
-
const installed = [];
|
|
10418
|
-
for (const browser of BROWSER_NAMES){
|
|
10419
|
-
const browserDir = join(cacheRoot, browser);
|
|
10420
|
-
if (existsSync(browserDir)) {
|
|
10421
|
-
const size = getDirSize(browserDir);
|
|
10422
|
-
installed.push({
|
|
10423
|
-
browser,
|
|
10424
|
-
path: browserDir,
|
|
10425
|
-
size,
|
|
10426
|
-
sizeFormatted: list_browsers_formatBytes(size),
|
|
10427
|
-
engine: "firefox" === browser ? "gecko" : "chromium"
|
|
10428
|
-
});
|
|
10429
|
-
}
|
|
10430
|
-
}
|
|
10431
|
-
return JSON.stringify({
|
|
10432
|
-
cacheRoot,
|
|
10433
|
-
cacheExists: existsSync(cacheRoot),
|
|
10434
|
-
installed,
|
|
10435
|
-
availableToInstall: BROWSER_NAMES.filter((b)=>!installed.some((i)=>i.browser === b)),
|
|
10436
|
-
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
|
|
10437
10160
|
});
|
|
10161
|
+
return readIdentity();
|
|
10438
10162
|
}
|
|
10439
10163
|
const execFileAsync = promisify(execFile);
|
|
10440
|
-
const detect_browsers_schema = {
|
|
10441
|
-
name: "extension_detect_browsers",
|
|
10442
|
-
description: "Detect which browsers are available for extension development. Checks both system-installed and managed browsers, returning paths and capabilities for each.",
|
|
10443
|
-
inputSchema: {
|
|
10444
|
-
type: "object",
|
|
10445
|
-
properties: {
|
|
10446
|
-
browsers: {
|
|
10447
|
-
type: "array",
|
|
10448
|
-
items: {
|
|
10449
|
-
type: "string",
|
|
10450
|
-
enum: [
|
|
10451
|
-
"chrome",
|
|
10452
|
-
"chromium",
|
|
10453
|
-
"edge",
|
|
10454
|
-
"brave",
|
|
10455
|
-
"opera",
|
|
10456
|
-
"vivaldi",
|
|
10457
|
-
"yandex",
|
|
10458
|
-
"firefox",
|
|
10459
|
-
"waterfox",
|
|
10460
|
-
"librewolf",
|
|
10461
|
-
"safari"
|
|
10462
|
-
]
|
|
10463
|
-
},
|
|
10464
|
-
description: "Browsers to check. If omitted, checks all."
|
|
10465
|
-
}
|
|
10466
|
-
}
|
|
10467
|
-
}
|
|
10468
|
-
};
|
|
10469
10164
|
const ALL_BROWSERS = [
|
|
10470
10165
|
"chrome",
|
|
10471
10166
|
"chromium",
|
|
@@ -10713,8 +10408,8 @@ async function getVersion(binaryPath, browser) {
|
|
|
10713
10408
|
return null;
|
|
10714
10409
|
}
|
|
10715
10410
|
}
|
|
10716
|
-
async function
|
|
10717
|
-
const browsersToCheck =
|
|
10411
|
+
async function detectBrowsers(browsers) {
|
|
10412
|
+
const browsersToCheck = browsers ?? [
|
|
10718
10413
|
...ALL_BROWSERS
|
|
10719
10414
|
];
|
|
10720
10415
|
const detected = [];
|
|
@@ -10756,8 +10451,161 @@ async function detect_browsers_handler(args) {
|
|
|
10756
10451
|
available: available.map((d)=>d.browser),
|
|
10757
10452
|
missing: missing.map((d)=>d.browser)
|
|
10758
10453
|
},
|
|
10759
|
-
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
|
|
10760
10607
|
});
|
|
10608
|
+
return detectBrowsers(args.browsers);
|
|
10761
10609
|
}
|
|
10762
10610
|
function typeOf(value) {
|
|
10763
10611
|
if (Array.isArray(value)) return "array";
|
|
@@ -10925,40 +10773,32 @@ function inputValidationError(toolName, issues, inputSchema) {
|
|
|
10925
10773
|
}
|
|
10926
10774
|
const tools = [
|
|
10927
10775
|
create_namespaceObject,
|
|
10928
|
-
|
|
10776
|
+
templates_namespaceObject,
|
|
10929
10777
|
build_namespaceObject,
|
|
10930
10778
|
dev_namespaceObject,
|
|
10931
10779
|
start_namespaceObject,
|
|
10932
|
-
preview_namespaceObject,
|
|
10933
10780
|
preview_web_namespaceObject,
|
|
10934
10781
|
shares_namespaceObject,
|
|
10935
10782
|
stop_namespaceObject,
|
|
10936
|
-
get_template_source_namespaceObject,
|
|
10937
10783
|
manifest_validate_namespaceObject,
|
|
10938
10784
|
theme_verify_namespaceObject,
|
|
10785
|
+
analyze_namespaceObject,
|
|
10939
10786
|
inspect_namespaceObject,
|
|
10940
|
-
source_inspect_namespaceObject,
|
|
10941
10787
|
list_extensions_namespaceObject,
|
|
10942
10788
|
logs_namespaceObject,
|
|
10943
10789
|
eval_namespaceObject,
|
|
10944
10790
|
storage_namespaceObject,
|
|
10945
10791
|
reload_namespaceObject,
|
|
10946
10792
|
open_namespaceObject,
|
|
10947
|
-
|
|
10793
|
+
dom_snapshot_namespaceObject,
|
|
10948
10794
|
tools_publish_namespaceObject,
|
|
10949
|
-
|
|
10795
|
+
release_status_namespaceObject,
|
|
10950
10796
|
release_promote_namespaceObject,
|
|
10951
|
-
|
|
10952
|
-
store_status_namespaceObject,
|
|
10797
|
+
submit_namespaceObject,
|
|
10953
10798
|
wait_namespaceObject,
|
|
10954
10799
|
add_feature_namespaceObject,
|
|
10955
|
-
|
|
10956
|
-
|
|
10957
|
-
logout_namespaceObject,
|
|
10958
|
-
install_browser_namespaceObject,
|
|
10959
|
-
uninstall_browser_namespaceObject,
|
|
10960
|
-
list_browsers_namespaceObject,
|
|
10961
|
-
detect_browsers_namespaceObject,
|
|
10800
|
+
auth_namespaceObject,
|
|
10801
|
+
browsers_namespaceObject,
|
|
10962
10802
|
doctor_namespaceObject
|
|
10963
10803
|
];
|
|
10964
10804
|
const toolMap = new Map();
|
|
@@ -11040,7 +10880,7 @@ async function runCli(cmd, args) {
|
|
|
11040
10880
|
return i >= 0 ? args[i + 1] : void 0;
|
|
11041
10881
|
};
|
|
11042
10882
|
if ("whoami" === cmd) {
|
|
11043
|
-
log(await
|
|
10883
|
+
log(await readIdentity());
|
|
11044
10884
|
return 0;
|
|
11045
10885
|
}
|
|
11046
10886
|
if ("release" === cmd) {
|
|
@@ -11072,7 +10912,7 @@ async function runCli(cmd, args) {
|
|
|
11072
10912
|
return 1;
|
|
11073
10913
|
}
|
|
11074
10914
|
if ("logout" === cmd) {
|
|
11075
|
-
log(await
|
|
10915
|
+
log(await clearLocalCredentials());
|
|
11076
10916
|
return 0;
|
|
11077
10917
|
}
|
|
11078
10918
|
if ("login" === cmd) {
|