@extension.dev/mcp 5.2.0 → 5.3.1
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 +78 -0
- package/dist/module.js +914 -443
- package/dist/src/__tests__/setup-session-dir.d.ts +1 -0
- package/dist/src/lib/process-manager.d.ts +2 -0
- package/dist/src/lib/registry.d.ts +72 -0
- package/dist/src/lib/session-browser.d.ts +6 -0
- package/dist/src/lib/validate-input.d.ts +6 -1
- package/dist/src/tools/dev.d.ts +6 -0
- package/dist/src/tools/release-list.d.ts +22 -0
- package/dist/src/tools/stop.d.ts +10 -0
- package/package.json +2 -2
- package/server.json +3 -3
package/dist/module.js
CHANGED
|
@@ -7,11 +7,11 @@ import node_path, { join } from "node:path";
|
|
|
7
7
|
import { extensionCreate } from "extension-create";
|
|
8
8
|
import node_os from "node:os";
|
|
9
9
|
import cross_spawn from "cross-spawn";
|
|
10
|
+
import node_crypto from "node:crypto";
|
|
10
11
|
import { execFile, execFileSync } from "node:child_process";
|
|
11
12
|
import { filterKeysForThisBrowser } from "browser-extension-manifest-fields";
|
|
12
13
|
import ws_0 from "ws";
|
|
13
14
|
import node_http from "node:http";
|
|
14
|
-
import node_crypto from "node:crypto";
|
|
15
15
|
import { extensionInstall, extensionUninstall, getManagedBrowsersCacheRoot } from "extension-install";
|
|
16
16
|
import { promisify } from "node:util";
|
|
17
17
|
var add_feature_namespaceObject = {};
|
|
@@ -149,6 +149,12 @@ __webpack_require__.d(tools_publish_namespaceObject, {
|
|
|
149
149
|
handler: ()=>publish_handler,
|
|
150
150
|
schema: ()=>publish_schema
|
|
151
151
|
});
|
|
152
|
+
var release_list_namespaceObject = {};
|
|
153
|
+
__webpack_require__.r(release_list_namespaceObject);
|
|
154
|
+
__webpack_require__.d(release_list_namespaceObject, {
|
|
155
|
+
handler: ()=>release_list_handler,
|
|
156
|
+
schema: ()=>release_list_schema
|
|
157
|
+
});
|
|
152
158
|
var release_promote_namespaceObject = {};
|
|
153
159
|
__webpack_require__.r(release_promote_namespaceObject);
|
|
154
160
|
__webpack_require__.d(release_promote_namespaceObject, {
|
|
@@ -177,7 +183,8 @@ var stop_namespaceObject = {};
|
|
|
177
183
|
__webpack_require__.r(stop_namespaceObject);
|
|
178
184
|
__webpack_require__.d(stop_namespaceObject, {
|
|
179
185
|
handler: ()=>stop_handler,
|
|
180
|
-
schema: ()=>stop_schema
|
|
186
|
+
schema: ()=>stop_schema,
|
|
187
|
+
stopOne: ()=>stopOne
|
|
181
188
|
});
|
|
182
189
|
var storage_namespaceObject = {};
|
|
183
190
|
__webpack_require__.r(storage_namespaceObject);
|
|
@@ -203,7 +210,7 @@ __webpack_require__.d(whoami_namespaceObject, {
|
|
|
203
210
|
handler: ()=>whoami_handler,
|
|
204
211
|
schema: ()=>whoami_schema
|
|
205
212
|
});
|
|
206
|
-
var package_namespaceObject = JSON.parse('{"rE":"5.
|
|
213
|
+
var package_namespaceObject = JSON.parse('{"rE":"5.3.1","El":{"OP":"^4.0.13"}}');
|
|
207
214
|
function scaffoldEnginePin(projectPath) {
|
|
208
215
|
try {
|
|
209
216
|
const pkg = JSON.parse(node_fs.readFileSync(node_path.join(projectPath, "package.json"), "utf8"));
|
|
@@ -241,17 +248,17 @@ function detectPackageManager(projectPath) {
|
|
|
241
248
|
}
|
|
242
249
|
const create_schema = {
|
|
243
250
|
name: "extension_create",
|
|
244
|
-
description: "Create a new browser extension project from a template in the extension.dev template catalog. Use extension_list_templates to see available options.",
|
|
251
|
+
description: "Create a new browser extension project from a template in the extension.dev template catalog. Use extension_list_templates to see available options. 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.",
|
|
245
252
|
inputSchema: {
|
|
246
253
|
type: "object",
|
|
247
254
|
properties: {
|
|
248
255
|
projectName: {
|
|
249
256
|
type: "string",
|
|
250
|
-
description: "Name of the extension project (used as directory name)"
|
|
257
|
+
description: "Name of the extension project (used as directory name). Alias: name."
|
|
251
258
|
},
|
|
252
259
|
parentDir: {
|
|
253
260
|
type: "string",
|
|
254
|
-
description: "Directory to create the project inside. Defaults to the MCP server's
|
|
261
|
+
description: "Directory to create the project inside. Defaults to the MCP server process cwd (NOT the caller's cwd), which may not be where you expect; pass this explicitly when you care where the project lands. Aliases: parent, into."
|
|
255
262
|
},
|
|
256
263
|
template: {
|
|
257
264
|
type: "string",
|
|
@@ -338,12 +345,21 @@ async function create_handler(args) {
|
|
|
338
345
|
const scaffoldPin = scaffoldEnginePin(result.projectPath);
|
|
339
346
|
const pinMatches = null !== scaffoldPin && "" !== pin && scaffoldPin.includes(pin);
|
|
340
347
|
const engineWarning = pin && "latest" !== pin && !pinMatches ? `The scaffold pins "extension": "${scaffoldPin ?? "unknown"}"; the project-local engine wins over EXTENSION_MCP_CLI_VERSION=${pin}. Run \`(cd ${result.projectPath} && ${addDev(`extension@${pin}`)})\` to match the pinned engine.` : void 0;
|
|
348
|
+
const resolvedParent = args.parentDir ? node_path.resolve(args.parentDir) : process.cwd();
|
|
349
|
+
const gitInit = node_fs.existsSync(node_path.join(result.projectPath, ".git"));
|
|
341
350
|
return JSON.stringify({
|
|
351
|
+
resolvedPath: result.projectPath,
|
|
342
352
|
projectPath: result.projectPath,
|
|
343
353
|
projectName: result.projectName,
|
|
344
354
|
template: result.template,
|
|
345
355
|
depsInstalled: result.depsInstalled,
|
|
346
356
|
packageManager: result.depsInstalled ? packageManager : null,
|
|
357
|
+
defaultsApplied: {
|
|
358
|
+
parentDir: args.parentDir ? `${resolvedParent} (explicit)` : `${resolvedParent} (default: the MCP server process cwd, not yours; pass parentDir to choose)`,
|
|
359
|
+
packageManager: `${packageManager} (auto-detected by the scaffolder, not asked)`,
|
|
360
|
+
browser: "chrome (default: extension_dev and extension_build target chrome unless you pass browser)",
|
|
361
|
+
gitInit
|
|
362
|
+
},
|
|
347
363
|
duration: Date.now() - start,
|
|
348
364
|
nextSteps: result.depsInstalled ? [
|
|
349
365
|
`cd ${result.projectPath}`,
|
|
@@ -882,216 +898,59 @@ async function build_handler(args) {
|
|
|
882
898
|
hint: "Check that the project has a valid src/manifest.json and its dependencies are installed (extension_dev auto-installs; build does not)."
|
|
883
899
|
});
|
|
884
900
|
}
|
|
885
|
-
const
|
|
901
|
+
const sessions = new Map();
|
|
886
902
|
function sessionKey(projectPath, browser) {
|
|
887
903
|
return `${node_path.resolve(projectPath)}::${browser}`;
|
|
888
904
|
}
|
|
889
|
-
function
|
|
890
|
-
|
|
891
|
-
}
|
|
892
|
-
function getSession(projectPath, browser) {
|
|
893
|
-
return process_manager_sessions.get(sessionKey(projectPath, browser));
|
|
905
|
+
function markerDir() {
|
|
906
|
+
return process.env.EXTENSION_MCP_SESSION_DIR || node_path.join(node_os.tmpdir(), "extension-dev-mcp-sessions");
|
|
894
907
|
}
|
|
895
|
-
function
|
|
896
|
-
|
|
908
|
+
function markerPath(projectPath, browser) {
|
|
909
|
+
const digest = node_crypto.createHash("sha1").update(sessionKey(projectPath, browser)).digest("hex").slice(0, 16);
|
|
910
|
+
return node_path.join(markerDir(), `${digest}.json`);
|
|
897
911
|
}
|
|
898
|
-
function
|
|
899
|
-
|
|
912
|
+
function removeSessionMarker(projectPath, browser) {
|
|
913
|
+
try {
|
|
914
|
+
node_fs.rmSync(markerPath(projectPath, browser), {
|
|
915
|
+
force: true
|
|
916
|
+
});
|
|
917
|
+
} catch {}
|
|
900
918
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
type: "string",
|
|
908
|
-
description: "URL the browser opens on launch"
|
|
909
|
-
},
|
|
910
|
-
chromiumBinary: {
|
|
911
|
-
type: "string",
|
|
912
|
-
description: "Path to a custom Chromium-based binary (overrides browser)"
|
|
913
|
-
},
|
|
914
|
-
geckoBinary: {
|
|
915
|
-
type: "string",
|
|
916
|
-
description: "Path to a custom Gecko/Firefox binary (overrides browser)"
|
|
917
|
-
},
|
|
918
|
-
host: {
|
|
919
|
-
type: "string",
|
|
920
|
-
description: "Dev server bind host. Use 0.0.0.0 for Docker or devcontainers. Defaults to 127.0.0.1"
|
|
921
|
-
},
|
|
922
|
-
publicHost: {
|
|
923
|
-
type: "string",
|
|
924
|
-
description: "Connectable host the browser dials for HMR and reload when it differs from the bind host"
|
|
925
|
-
},
|
|
926
|
-
extensions: {
|
|
927
|
-
type: "array",
|
|
928
|
-
items: {
|
|
929
|
-
type: "string"
|
|
930
|
-
},
|
|
931
|
-
description: "Companion extension paths or store URLs to load alongside the project"
|
|
919
|
+
function listSessionMarkers() {
|
|
920
|
+
let files;
|
|
921
|
+
try {
|
|
922
|
+
files = node_fs.readdirSync(markerDir());
|
|
923
|
+
} catch {
|
|
924
|
+
return [];
|
|
932
925
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
if (args.geckoBinary) cli.push("--gecko-binary", args.geckoBinary);
|
|
940
|
-
if (args.host) cli.push("--host", args.host);
|
|
941
|
-
if (args.publicHost) cli.push("--public-host", args.publicHost);
|
|
942
|
-
if (args.extensions?.length) cli.push("--extensions", args.extensions.join(","));
|
|
943
|
-
return cli;
|
|
926
|
+
const out = [];
|
|
927
|
+
for (const file of files)if (file.endsWith(".json")) try {
|
|
928
|
+
const parsed = JSON.parse(node_fs.readFileSync(node_path.join(markerDir(), file), "utf8"));
|
|
929
|
+
if ("string" == typeof parsed?.projectPath && "string" == typeof parsed?.browser) out.push(parsed);
|
|
930
|
+
} catch {}
|
|
931
|
+
return out;
|
|
944
932
|
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
properties: {
|
|
951
|
-
projectPath: {
|
|
952
|
-
type: "string",
|
|
953
|
-
description: "Path to the extension project root"
|
|
954
|
-
},
|
|
955
|
-
browser: {
|
|
956
|
-
type: "string",
|
|
957
|
-
enum: [
|
|
958
|
-
"chrome",
|
|
959
|
-
"chromium",
|
|
960
|
-
"edge",
|
|
961
|
-
"brave",
|
|
962
|
-
"opera",
|
|
963
|
-
"vivaldi",
|
|
964
|
-
"yandex",
|
|
965
|
-
"firefox",
|
|
966
|
-
"waterfox",
|
|
967
|
-
"librewolf",
|
|
968
|
-
"safari",
|
|
969
|
-
"chromium-based",
|
|
970
|
-
"gecko-based",
|
|
971
|
-
"firefox-based",
|
|
972
|
-
"webkit-based"
|
|
973
|
-
],
|
|
974
|
-
default: "chrome"
|
|
975
|
-
},
|
|
976
|
-
port: {
|
|
977
|
-
type: "number",
|
|
978
|
-
description: "Dev server port (0 for auto-assign)"
|
|
979
|
-
},
|
|
980
|
-
noBrowser: {
|
|
981
|
-
type: "boolean",
|
|
982
|
-
default: false,
|
|
983
|
-
description: "Start dev server without launching browser"
|
|
984
|
-
},
|
|
985
|
-
polyfill: {
|
|
986
|
-
type: "boolean",
|
|
987
|
-
default: true,
|
|
988
|
-
description: "Apply cross-browser polyfill"
|
|
989
|
-
},
|
|
990
|
-
...LAUNCH_FLAG_SCHEMA,
|
|
991
|
-
allowControl: {
|
|
992
|
-
type: "boolean",
|
|
993
|
-
default: false,
|
|
994
|
-
description: "Enable the agent-bridge control channel so extension_storage/reload/open/dom_inspect work against this session"
|
|
995
|
-
},
|
|
996
|
-
allowEval: {
|
|
997
|
-
type: "boolean",
|
|
998
|
-
default: false,
|
|
999
|
-
description: "Enable extension_eval (runs code in a context; writes a 0600 session token). Implies allowControl, so a single allowEval: true also unlocks storage/reload/open/dom_inspect. You do not need to pass both."
|
|
1000
|
-
}
|
|
1001
|
-
},
|
|
1002
|
-
required: [
|
|
1003
|
-
"projectPath"
|
|
1004
|
-
]
|
|
1005
|
-
}
|
|
1006
|
-
};
|
|
1007
|
-
async function dev_handler(args) {
|
|
1008
|
-
const browser = args.browser ?? "chrome";
|
|
1009
|
-
const allowControl = Boolean(args.allowControl || args.allowEval);
|
|
1010
|
-
const cliArgs = [
|
|
1011
|
-
"dev",
|
|
1012
|
-
args.projectPath,
|
|
1013
|
-
"--browser",
|
|
1014
|
-
browser
|
|
1015
|
-
];
|
|
1016
|
-
if (void 0 !== args.port) cliArgs.push("--port", String(args.port));
|
|
1017
|
-
if (args.noBrowser) cliArgs.push("--no-browser");
|
|
1018
|
-
if (false === args.polyfill) cliArgs.push("--polyfill", "false");
|
|
1019
|
-
cliArgs.push(...launchFlagArgs(args));
|
|
1020
|
-
if (allowControl) cliArgs.push("--allow-control");
|
|
1021
|
-
if (args.allowEval) cliArgs.push("--allow-eval");
|
|
1022
|
-
const spawned = spawnExtensionCli(cliArgs, {
|
|
1023
|
-
projectDir: args.projectPath
|
|
1024
|
-
});
|
|
1025
|
-
const { child, logPath } = spawned;
|
|
1026
|
-
const pid = child.pid;
|
|
1027
|
-
registerSession({
|
|
1028
|
-
pid,
|
|
1029
|
-
browser,
|
|
1030
|
-
port: args.port,
|
|
1031
|
-
projectPath: args.projectPath,
|
|
1032
|
-
command: "dev"
|
|
1033
|
-
});
|
|
1034
|
-
child.on("exit", ()=>removeSession(args.projectPath, browser));
|
|
1035
|
-
await new Promise((resolve)=>setTimeout(resolve, 3000));
|
|
1036
|
-
const earlyOutput = spawned.readOutput();
|
|
1037
|
-
if (null !== child.exitCode || null !== child.signalCode) {
|
|
1038
|
-
const code = child.exitCode;
|
|
1039
|
-
const signal = child.signalCode;
|
|
1040
|
-
return JSON.stringify({
|
|
1041
|
-
ok: false,
|
|
1042
|
-
status: "exited",
|
|
1043
|
-
projectPath: args.projectPath,
|
|
1044
|
-
browser,
|
|
1045
|
-
pid,
|
|
1046
|
-
exitCode: code,
|
|
1047
|
-
signal,
|
|
1048
|
-
error: `The dev server exited during startup (${signal ? `signal ${signal}` : `exit code ${code}`}). No session is running, so extension_logs/wait/eval and the control verbs have nothing to attach to.`,
|
|
1049
|
-
output: denoiseEarlyOutput(earlyOutput).slice(0, 2000),
|
|
1050
|
-
logPath,
|
|
1051
|
-
hint: "Read `output` above for the cause: a port already in use, a manifest the build rejects, or a missing browser binary are the common ones. Fix it and call extension_dev again; extension_doctor with this projectPath will also report what the last session recorded."
|
|
933
|
+
function registerSession(info) {
|
|
934
|
+
sessions.set(sessionKey(info.projectPath, info.browser), info);
|
|
935
|
+
try {
|
|
936
|
+
node_fs.mkdirSync(markerDir(), {
|
|
937
|
+
recursive: true
|
|
1052
938
|
});
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
browser,
|
|
1060
|
-
pid,
|
|
1061
|
-
error: "The dev server started but the FIRST COMPILE FAILED, so the browser has nothing usable to load. The session is running; the extension is not.",
|
|
1062
|
-
output: denoiseEarlyOutput(earlyOutput).slice(0, 2000),
|
|
1063
|
-
logPath,
|
|
1064
|
-
hint: "Fix the compile error in `output` above and save: the dev server is still running and will recompile. Do not call extension_wait yet, it will report ready for a build that failed."
|
|
1065
|
-
});
|
|
1066
|
-
const controlVerbs = "storage, reload, open, dom_inspect";
|
|
1067
|
-
const capabilities = {
|
|
1068
|
-
allowControl,
|
|
1069
|
-
allowEval: Boolean(args.allowEval),
|
|
1070
|
-
unlocked: allowControl ? args.allowEval ? `${controlVerbs}, eval` : controlVerbs : "none (read-only: logs, source_inspect, wait, doctor)"
|
|
1071
|
-
};
|
|
1072
|
-
return JSON.stringify({
|
|
1073
|
-
ok: true,
|
|
1074
|
-
pid,
|
|
1075
|
-
browser,
|
|
1076
|
-
port: args.port ?? 8080,
|
|
1077
|
-
projectPath: args.projectPath,
|
|
1078
|
-
status: "started",
|
|
1079
|
-
capabilities,
|
|
1080
|
-
hint: "Use extension_wait to check when the extension is fully loaded, then extension_source_inspect to inspect the live state. " + (allowControl ? `Control channel is ON: extension_${controlVerbs.split(", ").join("/extension_")}${args.allowEval ? "/extension_eval" : ""} will work against this session.` : "Control channel is OFF: extension_storage/reload/open/dom_inspect need allowControl: true, and extension_eval needs allowEval: true (which also implies allowControl). Restart extension_dev with the flag you need.") + " When you are done, call extension_stop to shut down the dev server and browser.",
|
|
1081
|
-
earlyOutput: denoiseEarlyOutput(earlyOutput).slice(0, 500),
|
|
1082
|
-
logPath
|
|
1083
|
-
});
|
|
939
|
+
node_fs.writeFileSync(markerPath(info.projectPath, info.browser), JSON.stringify({
|
|
940
|
+
...info,
|
|
941
|
+
projectPath: node_path.resolve(info.projectPath),
|
|
942
|
+
registeredAt: new Date().toISOString()
|
|
943
|
+
}));
|
|
944
|
+
} catch {}
|
|
1084
945
|
}
|
|
1085
|
-
function
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
];
|
|
1094
|
-
return raw.split("\n").filter((line)=>!NOISE.some((re)=>re.test(line.trim()))).join("\n").replace(/\n{2,}/g, "\n").trimStart();
|
|
946
|
+
function getSession(projectPath, browser) {
|
|
947
|
+
return sessions.get(sessionKey(projectPath, browser));
|
|
948
|
+
}
|
|
949
|
+
function removeSession(projectPath, browser) {
|
|
950
|
+
sessions.delete(sessionKey(projectPath, browser));
|
|
951
|
+
}
|
|
952
|
+
function listSessions() {
|
|
953
|
+
return Array.from(sessions.values());
|
|
1095
954
|
}
|
|
1096
955
|
function contractSightings(projectPath) {
|
|
1097
956
|
const root = node_path.resolve(projectPath, "dist", "extension-js");
|
|
@@ -1132,6 +991,27 @@ function knownSessionBrowsers(projectPath) {
|
|
|
1132
991
|
for (const sighting of contractSightings(projectPath))if (void 0 === sighting.pid || pidAlive(sighting.pid)) browsers.push(sighting.browser);
|
|
1133
992
|
return Array.from(new Set(browsers));
|
|
1134
993
|
}
|
|
994
|
+
function liveProjectSessions(projectPath) {
|
|
995
|
+
const resolved = node_path.resolve(projectPath);
|
|
996
|
+
const out = new Map();
|
|
997
|
+
for (const session of listSessions())if (node_path.resolve(session.projectPath) === resolved) {
|
|
998
|
+
if (pidAlive(session.pid)) out.set(session.browser, {
|
|
999
|
+
browser: session.browser,
|
|
1000
|
+
pid: session.pid,
|
|
1001
|
+
source: "registry"
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
for (const sighting of contractSightings(projectPath))if (void 0 !== sighting.pid && pidAlive(sighting.pid)) {
|
|
1005
|
+
if (!out.has(sighting.browser)) out.set(sighting.browser, {
|
|
1006
|
+
browser: sighting.browser,
|
|
1007
|
+
pid: sighting.pid,
|
|
1008
|
+
source: "contract"
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
return [
|
|
1012
|
+
...out.values()
|
|
1013
|
+
];
|
|
1014
|
+
}
|
|
1135
1015
|
function deadReadySession(projectPath) {
|
|
1136
1016
|
for (const sighting of contractSightings(projectPath))if (void 0 !== sighting.pid && !pidAlive(sighting.pid)) return {
|
|
1137
1017
|
browser: sighting.browser,
|
|
@@ -1180,9 +1060,9 @@ function resolveSessionBrowser(projectPath, explicit, fallback = "chrome") {
|
|
|
1180
1060
|
source: "fallback"
|
|
1181
1061
|
};
|
|
1182
1062
|
}
|
|
1183
|
-
const
|
|
1184
|
-
name: "
|
|
1185
|
-
description: "
|
|
1063
|
+
const stop_schema = {
|
|
1064
|
+
name: "extension_stop",
|
|
1065
|
+
description: "Stop a running dev, start, or preview session: terminates the dev server and the browser it launched. Counterpart to extension_dev/extension_start. Call it when you are done verifying so sessions do not accumulate.",
|
|
1186
1066
|
inputSchema: {
|
|
1187
1067
|
type: "object",
|
|
1188
1068
|
properties: {
|
|
@@ -1192,58 +1072,303 @@ const start_schema = {
|
|
|
1192
1072
|
},
|
|
1193
1073
|
browser: {
|
|
1194
1074
|
type: "string",
|
|
1195
|
-
|
|
1196
|
-
"chrome",
|
|
1197
|
-
"chromium",
|
|
1198
|
-
"edge",
|
|
1199
|
-
"brave",
|
|
1200
|
-
"opera",
|
|
1201
|
-
"vivaldi",
|
|
1202
|
-
"yandex",
|
|
1203
|
-
"firefox",
|
|
1204
|
-
"waterfox",
|
|
1205
|
-
"librewolf",
|
|
1206
|
-
"safari",
|
|
1207
|
-
"chromium-based",
|
|
1208
|
-
"gecko-based",
|
|
1209
|
-
"firefox-based",
|
|
1210
|
-
"webkit-based"
|
|
1211
|
-
],
|
|
1212
|
-
default: "chrome"
|
|
1075
|
+
description: "Browser of the session to stop (matches the browser passed to extension_dev/extension_start). Defaults to the one live session for this project when omitted, instead of assuming chrome."
|
|
1213
1076
|
},
|
|
1214
|
-
|
|
1077
|
+
all: {
|
|
1215
1078
|
type: "boolean",
|
|
1216
|
-
default:
|
|
1217
|
-
description: "
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1079
|
+
default: false,
|
|
1080
|
+
description: "Stop every known session, across projects and browsers. Discovers sessions from this server's registry AND the on-disk session markers earlier runs left, so it still finds sessions after an MCP restart or after a dev child exited out from under the registry. When true, projectPath/browser are ignored."
|
|
1081
|
+
}
|
|
1082
|
+
},
|
|
1083
|
+
required: []
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
function pgrepPids(pattern) {
|
|
1087
|
+
try {
|
|
1088
|
+
const out = execFileSync("pgrep", [
|
|
1089
|
+
"-f",
|
|
1090
|
+
pattern
|
|
1091
|
+
], {
|
|
1092
|
+
encoding: "utf8"
|
|
1093
|
+
});
|
|
1094
|
+
return out.split("\n").map((s)=>parseInt(s.trim(), 10)).filter((n)=>Number.isInteger(n) && n > 0 && n !== process.pid);
|
|
1095
|
+
} catch {
|
|
1096
|
+
return [];
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
function sessionProcessPids(projectPath) {
|
|
1100
|
+
const resolved = node_path.resolve(projectPath);
|
|
1101
|
+
const pids = new Set();
|
|
1102
|
+
for (const marker of [
|
|
1103
|
+
`extension dev ${resolved}`,
|
|
1104
|
+
node_path.join(resolved, "dist")
|
|
1105
|
+
])for (const pid of pgrepPids(marker))pids.add(pid);
|
|
1106
|
+
return [
|
|
1107
|
+
...pids
|
|
1108
|
+
];
|
|
1109
|
+
}
|
|
1110
|
+
function reapSessionProcesses(projectPath) {
|
|
1111
|
+
const pids = sessionProcessPids(projectPath);
|
|
1112
|
+
for (const pid of pids)try {
|
|
1113
|
+
process.kill(pid, "SIGKILL");
|
|
1114
|
+
} catch {}
|
|
1115
|
+
return pids;
|
|
1116
|
+
}
|
|
1117
|
+
function isAlive(pid) {
|
|
1118
|
+
try {
|
|
1119
|
+
process.kill(pid, 0);
|
|
1120
|
+
return true;
|
|
1121
|
+
} catch {
|
|
1122
|
+
return false;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
function stop_signal(pid, sig) {
|
|
1126
|
+
try {
|
|
1127
|
+
process.kill(-pid, sig);
|
|
1128
|
+
return true;
|
|
1129
|
+
} catch {
|
|
1130
|
+
try {
|
|
1131
|
+
process.kill(pid, sig);
|
|
1132
|
+
return true;
|
|
1133
|
+
} catch {
|
|
1134
|
+
return false;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
function readyJsonPath(projectPath, browser) {
|
|
1139
|
+
return node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json");
|
|
1140
|
+
}
|
|
1141
|
+
function pidFromReadyContract(projectPath, browser) {
|
|
1142
|
+
try {
|
|
1143
|
+
const raw = node_fs.readFileSync(readyJsonPath(projectPath, browser), "utf8");
|
|
1144
|
+
const contract = JSON.parse(raw);
|
|
1145
|
+
return "number" == typeof contract.pid ? contract.pid : null;
|
|
1146
|
+
} catch {
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
async function stopOne(projectPath, browser) {
|
|
1151
|
+
const session = getSession(projectPath, browser);
|
|
1152
|
+
const pid = session?.pid ?? pidFromReadyContract(projectPath, browser);
|
|
1153
|
+
if (null == pid) {
|
|
1154
|
+
const reaped = reapSessionProcesses(projectPath);
|
|
1155
|
+
removeSessionMarker(projectPath, browser);
|
|
1156
|
+
return {
|
|
1157
|
+
projectPath,
|
|
1158
|
+
browser,
|
|
1159
|
+
pid: null,
|
|
1160
|
+
stopped: 0 !== reaped.length,
|
|
1161
|
+
reaped,
|
|
1162
|
+
detail: 0 === reaped.length ? "No known session for this project/browser (nothing registered in this server and no ready.json contract found)." : `No dev pid on record, but reaped ${reaped.length} orphaned browser process(es) from the profile dir.`
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
let detail;
|
|
1166
|
+
if (isAlive(pid)) {
|
|
1167
|
+
stop_signal(pid, "SIGTERM");
|
|
1168
|
+
await new Promise((resolve)=>setTimeout(resolve, 1500));
|
|
1169
|
+
if (isAlive(pid)) {
|
|
1170
|
+
stop_signal(pid, "SIGKILL");
|
|
1171
|
+
await new Promise((resolve)=>setTimeout(resolve, 250));
|
|
1172
|
+
}
|
|
1173
|
+
detail = isAlive(pid) ? "Sent SIGTERM and SIGKILL but the process still reports alive; it may be exiting." : "Terminated.";
|
|
1174
|
+
} else detail = "Process was already gone; cleaned up session records.";
|
|
1175
|
+
const reaped = reapSessionProcesses(projectPath);
|
|
1176
|
+
removeSession(projectPath, browser);
|
|
1177
|
+
removeSessionMarker(projectPath, browser);
|
|
1178
|
+
try {
|
|
1179
|
+
node_fs.rmSync(readyJsonPath(projectPath, browser), {
|
|
1180
|
+
force: true
|
|
1181
|
+
});
|
|
1182
|
+
} catch {}
|
|
1183
|
+
const survivors = sessionProcessPids(projectPath);
|
|
1184
|
+
const stopped = !isAlive(pid) && 0 === survivors.length;
|
|
1185
|
+
if (survivors.length) detail += ` Warning: ${survivors.length} browser process(es) still alive after reap (pids ${survivors.join(", ")}).`;
|
|
1186
|
+
else if (reaped.length) detail += ` Reaped ${reaped.length} browser process(es).`;
|
|
1187
|
+
return {
|
|
1188
|
+
projectPath,
|
|
1189
|
+
browser,
|
|
1190
|
+
pid,
|
|
1191
|
+
stopped,
|
|
1192
|
+
reaped,
|
|
1193
|
+
detail
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
async function stop_handler(args) {
|
|
1197
|
+
if (args.all) {
|
|
1198
|
+
const candidates = new Map();
|
|
1199
|
+
for (const s of listSessions())candidates.set(`${node_path.resolve(s.projectPath)}::${s.browser}`, s);
|
|
1200
|
+
for (const m of listSessionMarkers()){
|
|
1201
|
+
const key = `${node_path.resolve(m.projectPath)}::${m.browser}`;
|
|
1202
|
+
if (!candidates.has(key)) candidates.set(key, m);
|
|
1203
|
+
}
|
|
1204
|
+
if (0 === candidates.size) return JSON.stringify({
|
|
1205
|
+
stopped: [],
|
|
1206
|
+
message: "No sessions registered in this server and no session markers on disk. Nothing to stop."
|
|
1207
|
+
});
|
|
1208
|
+
const outcomes = [];
|
|
1209
|
+
for (const c of candidates.values())outcomes.push(await stopOne(c.projectPath, c.browser));
|
|
1210
|
+
return JSON.stringify({
|
|
1211
|
+
stopped: outcomes
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
if (!args.projectPath) return JSON.stringify({
|
|
1215
|
+
error: "projectPath is required unless all=true. Pass the same projectPath used with extension_dev/extension_start."
|
|
1216
|
+
});
|
|
1217
|
+
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
1218
|
+
const outcome = await stopOne(args.projectPath, browser);
|
|
1219
|
+
return JSON.stringify(outcome);
|
|
1220
|
+
}
|
|
1221
|
+
const LAUNCH_FLAG_SCHEMA = {
|
|
1222
|
+
profile: {
|
|
1223
|
+
type: "string",
|
|
1224
|
+
description: 'Browser profile path, or "false" to reuse the default user profile. Omit for a fresh throwaway profile.'
|
|
1225
|
+
},
|
|
1226
|
+
startingUrl: {
|
|
1227
|
+
type: "string",
|
|
1228
|
+
description: "URL the browser opens on launch"
|
|
1229
|
+
},
|
|
1230
|
+
chromiumBinary: {
|
|
1231
|
+
type: "string",
|
|
1232
|
+
description: "Path to a custom Chromium-based binary (overrides browser)"
|
|
1233
|
+
},
|
|
1234
|
+
geckoBinary: {
|
|
1235
|
+
type: "string",
|
|
1236
|
+
description: "Path to a custom Gecko/Firefox binary (overrides browser)"
|
|
1237
|
+
},
|
|
1238
|
+
host: {
|
|
1239
|
+
type: "string",
|
|
1240
|
+
description: "Dev server bind host. Use 0.0.0.0 for Docker or devcontainers. Defaults to 127.0.0.1"
|
|
1241
|
+
},
|
|
1242
|
+
publicHost: {
|
|
1243
|
+
type: "string",
|
|
1244
|
+
description: "Connectable host the browser dials for HMR and reload when it differs from the bind host"
|
|
1245
|
+
},
|
|
1246
|
+
extensions: {
|
|
1247
|
+
type: "array",
|
|
1248
|
+
items: {
|
|
1249
|
+
type: "string"
|
|
1250
|
+
},
|
|
1251
|
+
description: "Companion extension paths or store URLs to load alongside the project"
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
function launchFlagArgs(args) {
|
|
1255
|
+
const cli = [];
|
|
1256
|
+
if (void 0 !== args.profile) cli.push("--profile", args.profile);
|
|
1257
|
+
if (args.startingUrl) cli.push("--starting-url", args.startingUrl);
|
|
1258
|
+
if (args.chromiumBinary) cli.push("--chromium-binary", args.chromiumBinary);
|
|
1259
|
+
if (args.geckoBinary) cli.push("--gecko-binary", args.geckoBinary);
|
|
1260
|
+
if (args.host) cli.push("--host", args.host);
|
|
1261
|
+
if (args.publicHost) cli.push("--public-host", args.publicHost);
|
|
1262
|
+
if (args.extensions?.length) cli.push("--extensions", args.extensions.join(","));
|
|
1263
|
+
return cli;
|
|
1264
|
+
}
|
|
1265
|
+
const dev_schema = {
|
|
1266
|
+
name: "extension_dev",
|
|
1267
|
+
description: "Start the extension development server with hot module replacement. Launches a browser with the extension loaded. Returns process info for use with extension_wait and extension_source_inspect.",
|
|
1268
|
+
inputSchema: {
|
|
1269
|
+
type: "object",
|
|
1270
|
+
properties: {
|
|
1271
|
+
projectPath: {
|
|
1272
|
+
type: "string",
|
|
1273
|
+
description: "Path to the extension project root"
|
|
1274
|
+
},
|
|
1275
|
+
browser: {
|
|
1276
|
+
type: "string",
|
|
1277
|
+
enum: [
|
|
1278
|
+
"chrome",
|
|
1279
|
+
"chromium",
|
|
1280
|
+
"edge",
|
|
1281
|
+
"brave",
|
|
1282
|
+
"opera",
|
|
1283
|
+
"vivaldi",
|
|
1284
|
+
"yandex",
|
|
1285
|
+
"firefox",
|
|
1286
|
+
"waterfox",
|
|
1287
|
+
"librewolf",
|
|
1288
|
+
"safari",
|
|
1289
|
+
"chromium-based",
|
|
1290
|
+
"gecko-based",
|
|
1291
|
+
"firefox-based",
|
|
1292
|
+
"webkit-based"
|
|
1293
|
+
],
|
|
1294
|
+
default: "chrome"
|
|
1295
|
+
},
|
|
1296
|
+
port: {
|
|
1220
1297
|
type: "number",
|
|
1221
|
-
description: "
|
|
1298
|
+
description: "Dev server port (0 for auto-assign)"
|
|
1222
1299
|
},
|
|
1223
1300
|
noBrowser: {
|
|
1224
1301
|
type: "boolean",
|
|
1225
1302
|
default: false,
|
|
1226
|
-
description: "
|
|
1303
|
+
description: "Start dev server without launching browser"
|
|
1227
1304
|
},
|
|
1228
|
-
|
|
1305
|
+
polyfill: {
|
|
1306
|
+
type: "boolean",
|
|
1307
|
+
default: true,
|
|
1308
|
+
description: "Apply cross-browser polyfill"
|
|
1309
|
+
},
|
|
1310
|
+
...LAUNCH_FLAG_SCHEMA,
|
|
1311
|
+
replace: {
|
|
1312
|
+
type: "boolean",
|
|
1313
|
+
default: false,
|
|
1314
|
+
description: "Stop any live session already running for this projectPath before starting; the result then reports it as replacedSession. Without it, extension_dev refuses to start over a live session instead of silently forking it (two sessions fight over the browser profile and the newer browser dies on the profile lock)."
|
|
1315
|
+
},
|
|
1316
|
+
allowControl: {
|
|
1317
|
+
type: "boolean",
|
|
1318
|
+
default: false,
|
|
1319
|
+
description: "Enable the agent-bridge control channel so extension_storage/reload/open/dom_inspect work against this session"
|
|
1320
|
+
},
|
|
1321
|
+
allowEval: {
|
|
1322
|
+
type: "boolean",
|
|
1323
|
+
default: false,
|
|
1324
|
+
description: "Enable extension_eval (runs code in a context; writes a 0600 session token). Implies allowControl, so a single allowEval: true also unlocks storage/reload/open/dom_inspect. You do not need to pass both."
|
|
1325
|
+
}
|
|
1229
1326
|
},
|
|
1230
1327
|
required: [
|
|
1231
1328
|
"projectPath"
|
|
1232
1329
|
]
|
|
1233
1330
|
}
|
|
1234
1331
|
};
|
|
1235
|
-
async function
|
|
1332
|
+
async function dev_handler(args) {
|
|
1236
1333
|
const browser = args.browser ?? "chrome";
|
|
1334
|
+
const existing = liveProjectSessions(args.projectPath);
|
|
1335
|
+
const replaced = [];
|
|
1336
|
+
if (existing.length > 0) {
|
|
1337
|
+
if (!args.replace) {
|
|
1338
|
+
const listed = existing.map((s)=>`pid ${s.pid} (${s.browser})`).join(", ");
|
|
1339
|
+
return JSON.stringify({
|
|
1340
|
+
ok: false,
|
|
1341
|
+
status: "session-exists",
|
|
1342
|
+
projectPath: args.projectPath,
|
|
1343
|
+
sessions: existing.map((s)=>({
|
|
1344
|
+
pid: s.pid,
|
|
1345
|
+
browser: s.browser
|
|
1346
|
+
})),
|
|
1347
|
+
error: `A dev session is already running for this project (${listed}). Starting another would fork the session: both browsers contend for the same profile and the new one dies on the profile lock.`,
|
|
1348
|
+
hint: "Call extension_stop with this projectPath first, or pass replace: true to have extension_dev stop the old session before starting the new one."
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
for (const s of existing){
|
|
1352
|
+
await stopOne(args.projectPath, s.browser);
|
|
1353
|
+
replaced.push({
|
|
1354
|
+
pid: s.pid,
|
|
1355
|
+
browser: s.browser
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
const allowControl = Boolean(args.allowControl || args.allowEval);
|
|
1237
1360
|
const cliArgs = [
|
|
1238
|
-
"
|
|
1361
|
+
"dev",
|
|
1239
1362
|
args.projectPath,
|
|
1240
1363
|
"--browser",
|
|
1241
1364
|
browser
|
|
1242
1365
|
];
|
|
1243
|
-
if (false === args.polyfill) cliArgs.push("--polyfill", "false");
|
|
1244
1366
|
if (void 0 !== args.port) cliArgs.push("--port", String(args.port));
|
|
1245
1367
|
if (args.noBrowser) cliArgs.push("--no-browser");
|
|
1368
|
+
if (false === args.polyfill) cliArgs.push("--polyfill", "false");
|
|
1246
1369
|
cliArgs.push(...launchFlagArgs(args));
|
|
1370
|
+
if (allowControl) cliArgs.push("--allow-control");
|
|
1371
|
+
if (args.allowEval) cliArgs.push("--allow-eval");
|
|
1247
1372
|
const spawnedAt = Date.now();
|
|
1248
1373
|
const spawned = spawnExtensionCli(cliArgs, {
|
|
1249
1374
|
projectDir: args.projectPath
|
|
@@ -1253,11 +1378,12 @@ async function start_handler(args) {
|
|
|
1253
1378
|
registerSession({
|
|
1254
1379
|
pid,
|
|
1255
1380
|
browser,
|
|
1381
|
+
port: args.port,
|
|
1256
1382
|
projectPath: args.projectPath,
|
|
1257
|
-
command: "
|
|
1383
|
+
command: "dev"
|
|
1258
1384
|
});
|
|
1259
1385
|
child.on("exit", ()=>removeSession(args.projectPath, browser));
|
|
1260
|
-
await new Promise((resolve)=>setTimeout(resolve,
|
|
1386
|
+
await new Promise((resolve)=>setTimeout(resolve, 3000));
|
|
1261
1387
|
const earlyOutput = spawned.readOutput();
|
|
1262
1388
|
if (null !== child.exitCode || null !== child.signalCode) {
|
|
1263
1389
|
const code = child.exitCode;
|
|
@@ -1270,39 +1396,80 @@ async function start_handler(args) {
|
|
|
1270
1396
|
pid,
|
|
1271
1397
|
exitCode: code,
|
|
1272
1398
|
signal,
|
|
1273
|
-
error: `The
|
|
1274
|
-
output: earlyOutput.slice(0, 2000),
|
|
1399
|
+
error: `The dev server exited during startup (${signal ? `signal ${signal}` : `exit code ${code}`}). No session is running, so extension_logs/wait/eval and the control verbs have nothing to attach to.`,
|
|
1400
|
+
output: denoiseEarlyOutput(earlyOutput).slice(0, 2000),
|
|
1275
1401
|
logPath,
|
|
1276
|
-
hint: "Read `output` above for the cause: a
|
|
1402
|
+
hint: "Read `output` above for the cause: a port already in use, a manifest the build rejects, or a missing browser binary are the common ones. Fix it and call extension_dev again; extension_doctor with this projectPath will also report what the last session recorded."
|
|
1277
1403
|
});
|
|
1278
1404
|
}
|
|
1279
|
-
const
|
|
1280
|
-
if (
|
|
1405
|
+
const compileFailed = /compiled with errors|✖✖✖|ERROR in |Module not found|NOT FOUND/i.test(earlyOutput);
|
|
1406
|
+
if (compileFailed) return JSON.stringify({
|
|
1281
1407
|
ok: false,
|
|
1282
|
-
status: "
|
|
1408
|
+
status: "compile-failed",
|
|
1283
1409
|
projectPath: args.projectPath,
|
|
1284
1410
|
browser,
|
|
1285
1411
|
pid,
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
output: earlyOutput.slice(0, 2000),
|
|
1412
|
+
error: "The dev server started but the FIRST COMPILE FAILED, so the browser has nothing usable to load. The session is running; the extension is not.",
|
|
1413
|
+
output: denoiseEarlyOutput(earlyOutput).slice(0, 2000),
|
|
1289
1414
|
logPath,
|
|
1290
|
-
hint: "
|
|
1415
|
+
hint: "Fix the compile error in `output` above and save: the dev server is still running and will recompile. Do not call extension_wait yet, it will report ready for a build that failed."
|
|
1291
1416
|
});
|
|
1417
|
+
const exitStamp = args.noBrowser ? null : browserExitStamp(args.projectPath, browser, spawnedAt);
|
|
1418
|
+
const profileLockHit = !args.noBrowser && /SingletonLock|ProcessSingleton|profile[^\n]*(in use|locked)|already (open|running)/i.test(earlyOutput);
|
|
1419
|
+
if (exitStamp || profileLockHit) {
|
|
1420
|
+
const profileDir = node_path.join(args.projectPath, "dist", `extension-profile-${browser}`);
|
|
1421
|
+
return JSON.stringify({
|
|
1422
|
+
ok: false,
|
|
1423
|
+
status: "browser-exited",
|
|
1424
|
+
projectPath: args.projectPath,
|
|
1425
|
+
browser,
|
|
1426
|
+
pid,
|
|
1427
|
+
...exitStamp ?? {},
|
|
1428
|
+
error: `The dev server is running but the ${browser} browser it launched died during startup` + (profileLockHit ? " because its profile is locked by another browser instance." : "."),
|
|
1429
|
+
output: denoiseEarlyOutput(earlyOutput).slice(0, 2000),
|
|
1430
|
+
logPath,
|
|
1431
|
+
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.`
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
const controlVerbs = "storage, reload, open, dom_inspect";
|
|
1435
|
+
const capabilities = {
|
|
1436
|
+
allowControl,
|
|
1437
|
+
allowEval: Boolean(args.allowEval),
|
|
1438
|
+
unlocked: allowControl ? args.allowEval ? `${controlVerbs}, eval` : controlVerbs : "none (read-only: logs, source_inspect, wait, doctor)"
|
|
1439
|
+
};
|
|
1292
1440
|
return JSON.stringify({
|
|
1293
1441
|
ok: true,
|
|
1294
1442
|
pid,
|
|
1295
1443
|
browser,
|
|
1444
|
+
port: args.port ?? 8080,
|
|
1296
1445
|
projectPath: args.projectPath,
|
|
1297
1446
|
status: "started",
|
|
1298
|
-
|
|
1299
|
-
|
|
1447
|
+
...replaced.length > 0 ? {
|
|
1448
|
+
replacedSession: replaced[0],
|
|
1449
|
+
...replaced.length > 1 ? {
|
|
1450
|
+
replacedSessions: replaced
|
|
1451
|
+
} : {}
|
|
1452
|
+
} : {},
|
|
1453
|
+
capabilities,
|
|
1454
|
+
hint: "Use extension_wait to check when the extension is fully loaded, then extension_source_inspect to inspect the live state. " + (allowControl ? `Control channel is ON: extension_${controlVerbs.split(", ").join("/extension_")}${args.allowEval ? "/extension_eval" : ""} will work against this session.` : "Control channel is OFF: extension_storage/reload/open/dom_inspect need allowControl: true, and extension_eval needs allowEval: true (which also implies allowControl). 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.",
|
|
1455
|
+
earlyOutput: denoiseEarlyOutput(earlyOutput).slice(0, 500),
|
|
1300
1456
|
logPath
|
|
1301
1457
|
});
|
|
1302
1458
|
}
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1459
|
+
function denoiseEarlyOutput(raw) {
|
|
1460
|
+
const NOISE = [
|
|
1461
|
+
/^npm warn Unknown project config/i,
|
|
1462
|
+
/This will stop working in the next major version of npm/i,
|
|
1463
|
+
/^npm warn config/i,
|
|
1464
|
+
/V8: .*Invalid asm\.js/i,
|
|
1465
|
+
/^\(node:\d+\) V8:/i,
|
|
1466
|
+
/Use `node --trace-warnings/i
|
|
1467
|
+
];
|
|
1468
|
+
return raw.split("\n").filter((line)=>!NOISE.some((re)=>re.test(line.trim()))).join("\n").replace(/\n{2,}/g, "\n").trimStart();
|
|
1469
|
+
}
|
|
1470
|
+
const start_schema = {
|
|
1471
|
+
name: "extension_start",
|
|
1472
|
+
description: "Build the extension for production and immediately preview it in a browser. Combines build + preview in one step. No hot reload.",
|
|
1306
1473
|
inputSchema: {
|
|
1307
1474
|
type: "object",
|
|
1308
1475
|
properties: {
|
|
@@ -1331,6 +1498,11 @@ const preview_schema = {
|
|
|
1331
1498
|
],
|
|
1332
1499
|
default: "chrome"
|
|
1333
1500
|
},
|
|
1501
|
+
polyfill: {
|
|
1502
|
+
type: "boolean",
|
|
1503
|
+
default: true,
|
|
1504
|
+
description: "Apply cross-browser polyfill"
|
|
1505
|
+
},
|
|
1334
1506
|
port: {
|
|
1335
1507
|
type: "number",
|
|
1336
1508
|
description: "Server port (0 for auto-assign)"
|
|
@@ -1338,7 +1510,7 @@ const preview_schema = {
|
|
|
1338
1510
|
noBrowser: {
|
|
1339
1511
|
type: "boolean",
|
|
1340
1512
|
default: false,
|
|
1341
|
-
description: "
|
|
1513
|
+
description: "Build and serve without launching a browser"
|
|
1342
1514
|
},
|
|
1343
1515
|
...LAUNCH_FLAG_SCHEMA
|
|
1344
1516
|
},
|
|
@@ -1347,14 +1519,15 @@ const preview_schema = {
|
|
|
1347
1519
|
]
|
|
1348
1520
|
}
|
|
1349
1521
|
};
|
|
1350
|
-
async function
|
|
1522
|
+
async function start_handler(args) {
|
|
1351
1523
|
const browser = args.browser ?? "chrome";
|
|
1352
1524
|
const cliArgs = [
|
|
1353
|
-
"
|
|
1525
|
+
"start",
|
|
1354
1526
|
args.projectPath,
|
|
1355
1527
|
"--browser",
|
|
1356
1528
|
browser
|
|
1357
1529
|
];
|
|
1530
|
+
if (false === args.polyfill) cliArgs.push("--polyfill", "false");
|
|
1358
1531
|
if (void 0 !== args.port) cliArgs.push("--port", String(args.port));
|
|
1359
1532
|
if (args.noBrowser) cliArgs.push("--no-browser");
|
|
1360
1533
|
cliArgs.push(...launchFlagArgs(args));
|
|
@@ -1368,7 +1541,7 @@ async function preview_handler(args) {
|
|
|
1368
1541
|
pid,
|
|
1369
1542
|
browser,
|
|
1370
1543
|
projectPath: args.projectPath,
|
|
1371
|
-
command: "
|
|
1544
|
+
command: "start"
|
|
1372
1545
|
});
|
|
1373
1546
|
child.on("exit", ()=>removeSession(args.projectPath, browser));
|
|
1374
1547
|
await new Promise((resolve)=>setTimeout(resolve, 5000));
|
|
@@ -1384,186 +1557,149 @@ async function preview_handler(args) {
|
|
|
1384
1557
|
pid,
|
|
1385
1558
|
exitCode: code,
|
|
1386
1559
|
signal,
|
|
1387
|
-
error: `The preview
|
|
1560
|
+
error: `The preview server exited during startup (${signal ? `signal ${signal}` : `exit code ${code}`}). No session is running.`,
|
|
1388
1561
|
output: earlyOutput.slice(0, 2000),
|
|
1389
1562
|
logPath,
|
|
1390
|
-
hint: "Read `output` above for the cause: a
|
|
1563
|
+
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."
|
|
1391
1564
|
});
|
|
1392
1565
|
}
|
|
1393
1566
|
const exitStamp = browserExitStamp(args.projectPath, browser, spawnedAt);
|
|
1394
1567
|
if (exitStamp) return JSON.stringify({
|
|
1395
1568
|
ok: false,
|
|
1396
|
-
status: "browser-exited",
|
|
1397
|
-
projectPath: args.projectPath,
|
|
1398
|
-
browser,
|
|
1399
|
-
pid,
|
|
1400
|
-
...exitStamp,
|
|
1401
|
-
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.",
|
|
1402
|
-
output: earlyOutput.slice(0, 2000),
|
|
1403
|
-
logPath,
|
|
1404
|
-
hint: "Read `output` above and extension_logs for the cause, then call extension_stop to clean up before retrying."
|
|
1405
|
-
});
|
|
1406
|
-
return JSON.stringify({
|
|
1407
|
-
ok: true,
|
|
1408
|
-
pid,
|
|
1409
|
-
browser,
|
|
1410
|
-
projectPath: args.projectPath,
|
|
1411
|
-
status: "launched",
|
|
1412
|
-
hint: "Call extension_stop when you are done to close the preview browser.",
|
|
1413
|
-
earlyOutput: earlyOutput.slice(0, 500),
|
|
1414
|
-
logPath
|
|
1415
|
-
});
|
|
1416
|
-
}
|
|
1417
|
-
const stop_schema = {
|
|
1418
|
-
name: "extension_stop",
|
|
1419
|
-
description: "Stop a running dev, start, or preview session: terminates the dev server and the browser it launched. Counterpart to extension_dev/extension_start. Call it when you are done verifying so sessions do not accumulate.",
|
|
1420
|
-
inputSchema: {
|
|
1421
|
-
type: "object",
|
|
1422
|
-
properties: {
|
|
1423
|
-
projectPath: {
|
|
1424
|
-
type: "string",
|
|
1425
|
-
description: "Path to the extension project root"
|
|
1426
|
-
},
|
|
1427
|
-
browser: {
|
|
1428
|
-
type: "string",
|
|
1429
|
-
description: "Browser of the session to stop (matches the browser passed to extension_dev/extension_start). Defaults to the one live session for this project when omitted, instead of assuming chrome."
|
|
1430
|
-
},
|
|
1431
|
-
all: {
|
|
1432
|
-
type: "boolean",
|
|
1433
|
-
default: false,
|
|
1434
|
-
description: "Stop every session this server started, across projects and browsers. When true, projectPath/browser are ignored."
|
|
1435
|
-
}
|
|
1436
|
-
},
|
|
1437
|
-
required: []
|
|
1438
|
-
}
|
|
1439
|
-
};
|
|
1440
|
-
function pgrepPids(pattern) {
|
|
1441
|
-
try {
|
|
1442
|
-
const out = execFileSync("pgrep", [
|
|
1443
|
-
"-f",
|
|
1444
|
-
pattern
|
|
1445
|
-
], {
|
|
1446
|
-
encoding: "utf8"
|
|
1447
|
-
});
|
|
1448
|
-
return out.split("\n").map((s)=>parseInt(s.trim(), 10)).filter((n)=>Number.isInteger(n) && n > 0 && n !== process.pid);
|
|
1449
|
-
} catch {
|
|
1450
|
-
return [];
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
function sessionProcessPids(projectPath) {
|
|
1454
|
-
const resolved = node_path.resolve(projectPath);
|
|
1455
|
-
const pids = new Set();
|
|
1456
|
-
for (const marker of [
|
|
1457
|
-
`extension dev ${resolved}`,
|
|
1458
|
-
node_path.join(resolved, "dist")
|
|
1459
|
-
])for (const pid of pgrepPids(marker))pids.add(pid);
|
|
1460
|
-
return [
|
|
1461
|
-
...pids
|
|
1462
|
-
];
|
|
1463
|
-
}
|
|
1464
|
-
function reapSessionProcesses(projectPath) {
|
|
1465
|
-
const pids = sessionProcessPids(projectPath);
|
|
1466
|
-
for (const pid of pids)try {
|
|
1467
|
-
process.kill(pid, "SIGKILL");
|
|
1468
|
-
} catch {}
|
|
1469
|
-
return pids;
|
|
1470
|
-
}
|
|
1471
|
-
function isAlive(pid) {
|
|
1472
|
-
try {
|
|
1473
|
-
process.kill(pid, 0);
|
|
1474
|
-
return true;
|
|
1475
|
-
} catch {
|
|
1476
|
-
return false;
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
function stop_signal(pid, sig) {
|
|
1480
|
-
try {
|
|
1481
|
-
process.kill(-pid, sig);
|
|
1482
|
-
return true;
|
|
1483
|
-
} catch {
|
|
1484
|
-
try {
|
|
1485
|
-
process.kill(pid, sig);
|
|
1486
|
-
return true;
|
|
1487
|
-
} catch {
|
|
1488
|
-
return false;
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
}
|
|
1492
|
-
function readyJsonPath(projectPath, browser) {
|
|
1493
|
-
return node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json");
|
|
1494
|
-
}
|
|
1495
|
-
function pidFromReadyContract(projectPath, browser) {
|
|
1496
|
-
try {
|
|
1497
|
-
const raw = node_fs.readFileSync(readyJsonPath(projectPath, browser), "utf8");
|
|
1498
|
-
const contract = JSON.parse(raw);
|
|
1499
|
-
return "number" == typeof contract.pid ? contract.pid : null;
|
|
1500
|
-
} catch {
|
|
1501
|
-
return null;
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
|
-
async function stopOne(projectPath, browser) {
|
|
1505
|
-
const session = getSession(projectPath, browser);
|
|
1506
|
-
const pid = session?.pid ?? pidFromReadyContract(projectPath, browser);
|
|
1507
|
-
if (null == pid) {
|
|
1508
|
-
const reaped = reapSessionProcesses(projectPath);
|
|
1509
|
-
return {
|
|
1510
|
-
projectPath,
|
|
1511
|
-
browser,
|
|
1512
|
-
pid: null,
|
|
1513
|
-
stopped: 0 !== reaped.length,
|
|
1514
|
-
reaped,
|
|
1515
|
-
detail: 0 === reaped.length ? "No known session for this project/browser (nothing registered in this server and no ready.json contract found)." : `No dev pid on record, but reaped ${reaped.length} orphaned browser process(es) from the profile dir.`
|
|
1516
|
-
};
|
|
1517
|
-
}
|
|
1518
|
-
let detail;
|
|
1519
|
-
if (isAlive(pid)) {
|
|
1520
|
-
stop_signal(pid, "SIGTERM");
|
|
1521
|
-
await new Promise((resolve)=>setTimeout(resolve, 1500));
|
|
1522
|
-
if (isAlive(pid)) {
|
|
1523
|
-
stop_signal(pid, "SIGKILL");
|
|
1524
|
-
await new Promise((resolve)=>setTimeout(resolve, 250));
|
|
1525
|
-
}
|
|
1526
|
-
detail = isAlive(pid) ? "Sent SIGTERM and SIGKILL but the process still reports alive; it may be exiting." : "Terminated.";
|
|
1527
|
-
} else detail = "Process was already gone; cleaned up session records.";
|
|
1528
|
-
const reaped = reapSessionProcesses(projectPath);
|
|
1529
|
-
removeSession(projectPath, browser);
|
|
1530
|
-
try {
|
|
1531
|
-
node_fs.rmSync(readyJsonPath(projectPath, browser), {
|
|
1532
|
-
force: true
|
|
1533
|
-
});
|
|
1534
|
-
} catch {}
|
|
1535
|
-
const survivors = sessionProcessPids(projectPath);
|
|
1536
|
-
const stopped = !isAlive(pid) && 0 === survivors.length;
|
|
1537
|
-
if (survivors.length) detail += ` Warning: ${survivors.length} browser process(es) still alive after reap (pids ${survivors.join(", ")}).`;
|
|
1538
|
-
else if (reaped.length) detail += ` Reaped ${reaped.length} browser process(es).`;
|
|
1539
|
-
return {
|
|
1540
|
-
projectPath,
|
|
1569
|
+
status: "browser-exited",
|
|
1570
|
+
projectPath: args.projectPath,
|
|
1541
1571
|
browser,
|
|
1542
1572
|
pid,
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1573
|
+
...exitStamp,
|
|
1574
|
+
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.",
|
|
1575
|
+
output: earlyOutput.slice(0, 2000),
|
|
1576
|
+
logPath,
|
|
1577
|
+
hint: "Read `output` above and extension_logs for the cause, then call extension_stop to clean up before retrying."
|
|
1578
|
+
});
|
|
1579
|
+
return JSON.stringify({
|
|
1580
|
+
ok: true,
|
|
1581
|
+
pid,
|
|
1582
|
+
browser,
|
|
1583
|
+
projectPath: args.projectPath,
|
|
1584
|
+
status: "started",
|
|
1585
|
+
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.",
|
|
1586
|
+
earlyOutput: earlyOutput.slice(0, 500),
|
|
1587
|
+
logPath
|
|
1588
|
+
});
|
|
1547
1589
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1590
|
+
const preview_schema = {
|
|
1591
|
+
name: "extension_preview",
|
|
1592
|
+
description: "Preview a production-built extension in a browser. Uses dist/ output directly. The extension must be built first with extension_build.",
|
|
1593
|
+
inputSchema: {
|
|
1594
|
+
type: "object",
|
|
1595
|
+
properties: {
|
|
1596
|
+
projectPath: {
|
|
1597
|
+
type: "string",
|
|
1598
|
+
description: "Path to the extension project root"
|
|
1599
|
+
},
|
|
1600
|
+
browser: {
|
|
1601
|
+
type: "string",
|
|
1602
|
+
enum: [
|
|
1603
|
+
"chrome",
|
|
1604
|
+
"chromium",
|
|
1605
|
+
"edge",
|
|
1606
|
+
"brave",
|
|
1607
|
+
"opera",
|
|
1608
|
+
"vivaldi",
|
|
1609
|
+
"yandex",
|
|
1610
|
+
"firefox",
|
|
1611
|
+
"waterfox",
|
|
1612
|
+
"librewolf",
|
|
1613
|
+
"safari",
|
|
1614
|
+
"chromium-based",
|
|
1615
|
+
"gecko-based",
|
|
1616
|
+
"firefox-based",
|
|
1617
|
+
"webkit-based"
|
|
1618
|
+
],
|
|
1619
|
+
default: "chrome"
|
|
1620
|
+
},
|
|
1621
|
+
port: {
|
|
1622
|
+
type: "number",
|
|
1623
|
+
description: "Server port (0 for auto-assign)"
|
|
1624
|
+
},
|
|
1625
|
+
noBrowser: {
|
|
1626
|
+
type: "boolean",
|
|
1627
|
+
default: false,
|
|
1628
|
+
description: "Serve the preview without launching a browser"
|
|
1629
|
+
},
|
|
1630
|
+
...LAUNCH_FLAG_SCHEMA
|
|
1631
|
+
},
|
|
1632
|
+
required: [
|
|
1633
|
+
"projectPath"
|
|
1634
|
+
]
|
|
1635
|
+
}
|
|
1636
|
+
};
|
|
1637
|
+
async function preview_handler(args) {
|
|
1638
|
+
const browser = args.browser ?? "chrome";
|
|
1639
|
+
const cliArgs = [
|
|
1640
|
+
"preview",
|
|
1641
|
+
args.projectPath,
|
|
1642
|
+
"--browser",
|
|
1643
|
+
browser
|
|
1644
|
+
];
|
|
1645
|
+
if (void 0 !== args.port) cliArgs.push("--port", String(args.port));
|
|
1646
|
+
if (args.noBrowser) cliArgs.push("--no-browser");
|
|
1647
|
+
cliArgs.push(...launchFlagArgs(args));
|
|
1648
|
+
const spawnedAt = Date.now();
|
|
1649
|
+
const spawned = spawnExtensionCli(cliArgs, {
|
|
1650
|
+
projectDir: args.projectPath
|
|
1651
|
+
});
|
|
1652
|
+
const { child, logPath } = spawned;
|
|
1653
|
+
const pid = child.pid;
|
|
1654
|
+
registerSession({
|
|
1655
|
+
pid,
|
|
1656
|
+
browser,
|
|
1657
|
+
projectPath: args.projectPath,
|
|
1658
|
+
command: "preview"
|
|
1659
|
+
});
|
|
1660
|
+
child.on("exit", ()=>removeSession(args.projectPath, browser));
|
|
1661
|
+
await new Promise((resolve)=>setTimeout(resolve, 5000));
|
|
1662
|
+
const earlyOutput = spawned.readOutput();
|
|
1663
|
+
if (null !== child.exitCode || null !== child.signalCode) {
|
|
1664
|
+
const code = child.exitCode;
|
|
1665
|
+
const signal = child.signalCode;
|
|
1557
1666
|
return JSON.stringify({
|
|
1558
|
-
|
|
1667
|
+
ok: false,
|
|
1668
|
+
status: "exited",
|
|
1669
|
+
projectPath: args.projectPath,
|
|
1670
|
+
browser,
|
|
1671
|
+
pid,
|
|
1672
|
+
exitCode: code,
|
|
1673
|
+
signal,
|
|
1674
|
+
error: `The preview process exited during startup (${signal ? `signal ${signal}` : `exit code ${code}`}). Nothing is running.`,
|
|
1675
|
+
output: earlyOutput.slice(0, 2000),
|
|
1676
|
+
logPath,
|
|
1677
|
+
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."
|
|
1559
1678
|
});
|
|
1560
1679
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1680
|
+
const exitStamp = browserExitStamp(args.projectPath, browser, spawnedAt);
|
|
1681
|
+
if (exitStamp) return JSON.stringify({
|
|
1682
|
+
ok: false,
|
|
1683
|
+
status: "browser-exited",
|
|
1684
|
+
projectPath: args.projectPath,
|
|
1685
|
+
browser,
|
|
1686
|
+
pid,
|
|
1687
|
+
...exitStamp,
|
|
1688
|
+
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.",
|
|
1689
|
+
output: earlyOutput.slice(0, 2000),
|
|
1690
|
+
logPath,
|
|
1691
|
+
hint: "Read `output` above and extension_logs for the cause, then call extension_stop to clean up before retrying."
|
|
1692
|
+
});
|
|
1693
|
+
return JSON.stringify({
|
|
1694
|
+
ok: true,
|
|
1695
|
+
pid,
|
|
1696
|
+
browser,
|
|
1697
|
+
projectPath: args.projectPath,
|
|
1698
|
+
status: "launched",
|
|
1699
|
+
hint: "Call extension_stop when you are done to close the preview browser.",
|
|
1700
|
+
earlyOutput: earlyOutput.slice(0, 500),
|
|
1701
|
+
logPath
|
|
1563
1702
|
});
|
|
1564
|
-
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
1565
|
-
const outcome = await stopOne(args.projectPath, browser);
|
|
1566
|
-
return JSON.stringify(outcome);
|
|
1567
1703
|
}
|
|
1568
1704
|
const RAW_BASE = "https://raw.githubusercontent.com/extension-js/examples/main/examples";
|
|
1569
1705
|
const get_template_source_schema = {
|
|
@@ -4327,6 +4463,109 @@ async function publish(options = {}) {
|
|
|
4327
4463
|
data
|
|
4328
4464
|
};
|
|
4329
4465
|
}
|
|
4466
|
+
const REGISTRY_BASE_DEFAULT = "https://registry.extension.land";
|
|
4467
|
+
const CONSOLE_BASE = "https://console.extension.dev";
|
|
4468
|
+
function registryBase() {
|
|
4469
|
+
const fromEnv = String(process.env.EXTENSION_DEV_REGISTRY_URL || "").trim();
|
|
4470
|
+
return (fromEnv || REGISTRY_BASE_DEFAULT).replace(/\/+$/, "");
|
|
4471
|
+
}
|
|
4472
|
+
function resolveProjectRef(overrides) {
|
|
4473
|
+
const workspace = String(overrides?.workspace || "").trim();
|
|
4474
|
+
const project = String(overrides?.project || "").trim();
|
|
4475
|
+
if (workspace && project) return {
|
|
4476
|
+
workspace,
|
|
4477
|
+
project
|
|
4478
|
+
};
|
|
4479
|
+
const creds = readCredentials();
|
|
4480
|
+
const ws = workspace || String(creds?.workspaceSlug || "").trim();
|
|
4481
|
+
const proj = project || String(creds?.projectSlug || "").trim();
|
|
4482
|
+
if (!ws || !proj) return null;
|
|
4483
|
+
return {
|
|
4484
|
+
workspace: ws,
|
|
4485
|
+
project: proj
|
|
4486
|
+
};
|
|
4487
|
+
}
|
|
4488
|
+
function registryFileUrl(ref, file) {
|
|
4489
|
+
return `${registryBase()}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/_extension-dev/${file}`;
|
|
4490
|
+
}
|
|
4491
|
+
function consoleProjectUrl(ref, page) {
|
|
4492
|
+
if (!ref) return `${CONSOLE_BASE}`;
|
|
4493
|
+
return `${CONSOLE_BASE}/${encodeURIComponent(ref.workspace)}/${encodeURIComponent(ref.project)}/${page}`;
|
|
4494
|
+
}
|
|
4495
|
+
async function fetchRegistryJson(url, fetchImpl = fetch) {
|
|
4496
|
+
let res;
|
|
4497
|
+
try {
|
|
4498
|
+
res = await fetchImpl(url);
|
|
4499
|
+
} catch (err) {
|
|
4500
|
+
return {
|
|
4501
|
+
ok: false,
|
|
4502
|
+
message: `Could not reach ${url}: ${err?.message || err}`
|
|
4503
|
+
};
|
|
4504
|
+
}
|
|
4505
|
+
if (!res.ok) return {
|
|
4506
|
+
ok: false,
|
|
4507
|
+
status: res.status,
|
|
4508
|
+
message: `${url} returned ${res.status}`
|
|
4509
|
+
};
|
|
4510
|
+
try {
|
|
4511
|
+
const text = await res.text();
|
|
4512
|
+
return {
|
|
4513
|
+
ok: true,
|
|
4514
|
+
json: JSON.parse(text)
|
|
4515
|
+
};
|
|
4516
|
+
} catch {
|
|
4517
|
+
return {
|
|
4518
|
+
ok: false,
|
|
4519
|
+
message: `${url} did not return valid JSON`
|
|
4520
|
+
};
|
|
4521
|
+
}
|
|
4522
|
+
}
|
|
4523
|
+
function parseChannels(json) {
|
|
4524
|
+
if (!json || "object" != typeof json || Array.isArray(json)) return [];
|
|
4525
|
+
const out = [];
|
|
4526
|
+
for (const [channel, raw] of Object.entries(json)){
|
|
4527
|
+
if (!raw || "object" != typeof raw) continue;
|
|
4528
|
+
const row = raw;
|
|
4529
|
+
const description = "string" == typeof row.description ? row.description : void 0;
|
|
4530
|
+
const promotedAtField = "string" == typeof row.promotedAt && row.promotedAt ? row.promotedAt : void 0;
|
|
4531
|
+
const fromDescription = description?.match(/\bon (\d{4}-\d{2}-\d{2}T[0-9:.]+Z?)/)?.[1];
|
|
4532
|
+
const entry = {
|
|
4533
|
+
channel,
|
|
4534
|
+
sha: String(row.sha ?? "")
|
|
4535
|
+
};
|
|
4536
|
+
if (row.buildId) entry.buildId = String(row.buildId);
|
|
4537
|
+
if (row.version) entry.version = String(row.version);
|
|
4538
|
+
const promotedAt = promotedAtField || fromDescription;
|
|
4539
|
+
if (promotedAt) entry.promotedAt = promotedAt;
|
|
4540
|
+
if (description) entry.description = description;
|
|
4541
|
+
out.push(entry);
|
|
4542
|
+
}
|
|
4543
|
+
return out;
|
|
4544
|
+
}
|
|
4545
|
+
function parseBuildIndex(json) {
|
|
4546
|
+
const items = json?.items;
|
|
4547
|
+
if (!Array.isArray(items)) return [];
|
|
4548
|
+
const out = [];
|
|
4549
|
+
for (const raw of items){
|
|
4550
|
+
if (!raw || "object" != typeof raw) continue;
|
|
4551
|
+
const row = raw;
|
|
4552
|
+
const sha = String(row.shortSha ?? row.sha ?? row.id ?? row.buildId ?? "").trim();
|
|
4553
|
+
if (!sha) continue;
|
|
4554
|
+
const entry = {
|
|
4555
|
+
sha
|
|
4556
|
+
};
|
|
4557
|
+
if (row.commit) entry.commit = String(row.commit);
|
|
4558
|
+
if (row.channel) entry.channel = String(row.channel);
|
|
4559
|
+
if (row.buildEnv) entry.buildEnv = String(row.buildEnv);
|
|
4560
|
+
if (row.status) entry.status = String(row.status);
|
|
4561
|
+
if (row.version) entry.version = String(row.version);
|
|
4562
|
+
if ("string" == typeof row.message) entry.message = row.message.split("\n", 1)[0];
|
|
4563
|
+
if (row.timestamp) entry.timestamp = String(row.timestamp);
|
|
4564
|
+
if (Array.isArray(row.browsers)) entry.browsers = row.browsers.map((b)=>String(b)).filter(Boolean);
|
|
4565
|
+
out.push(entry);
|
|
4566
|
+
}
|
|
4567
|
+
return out;
|
|
4568
|
+
}
|
|
4330
4569
|
const publish_schema = {
|
|
4331
4570
|
name: "extension_publish",
|
|
4332
4571
|
description: "Publish the project your stored token is scoped to (from extension_login, or EXTENSION_DEV_TOKEN) to extension.dev and return its shareable URL. The publish target is the token's project -- there is no projectPath, the local files are not uploaded. For a PUBLIC project the URL is the canonical public page and ttlHours does not apply; for a PRIVATE project it is a fresh time-limited share link (?share=) whose lifetime is ttlHours. Posts to the platform's CLI publish endpoint. Besides extension_login this is the only tool that talks to the hosted platform.",
|
|
@@ -4377,6 +4616,28 @@ async function publish_handler(args) {
|
|
|
4377
4616
|
if (!result.ok) return JSON.stringify(result);
|
|
4378
4617
|
const data = result.data;
|
|
4379
4618
|
if (null != args.ttlHours && "public" === data.visibility) data.note = "ttlHours was ignored: this is a public project, whose share URL is its canonical public page.";
|
|
4619
|
+
const ref = resolveProjectRef();
|
|
4620
|
+
if (ref) {
|
|
4621
|
+
const buildsUrl = registryFileUrl(ref, "builds/index.json");
|
|
4622
|
+
const buildsRes = await fetchRegistryJson(buildsUrl);
|
|
4623
|
+
if (buildsRes.ok) {
|
|
4624
|
+
const items = parseBuildIndex(buildsRes.json);
|
|
4625
|
+
const pinned = args.buildSha ? items.find((item)=>{
|
|
4626
|
+
const short = String(args.buildSha).slice(0, 7).toLowerCase();
|
|
4627
|
+
return item.sha.toLowerCase() === short || String(item.commit ?? "").toLowerCase().startsWith(short);
|
|
4628
|
+
}) : void 0;
|
|
4629
|
+
const newestSuccess = items.filter((item)=>"success" === item.status).sort((a, b)=>String(b.timestamp ?? "").localeCompare(String(a.timestamp ?? "")))[0];
|
|
4630
|
+
const served = pinned ?? newestSuccess;
|
|
4631
|
+
if (served) {
|
|
4632
|
+
if (null == data.buildSha) data.buildSha = served.sha;
|
|
4633
|
+
if (null == data.builtAt && served.timestamp) data.builtAt = served.timestamp;
|
|
4634
|
+
if (null == data.version && served.version) data.version = served.version;
|
|
4635
|
+
if (null == data.channel && served.channel) data.channel = served.channel;
|
|
4636
|
+
data.registryUrl = buildsUrl;
|
|
4637
|
+
if (!pinned && null == args.buildSha) data.buildNote = "buildSha/builtAt/version describe the newest successful build in the project's registry index, which is what the share link serves. Pin buildSha to serve a specific build.";
|
|
4638
|
+
}
|
|
4639
|
+
}
|
|
4640
|
+
}
|
|
4380
4641
|
return JSON.stringify(data);
|
|
4381
4642
|
}
|
|
4382
4643
|
const release_promote_DEFAULT_API = "https://www.extension.dev";
|
|
@@ -4472,9 +4733,111 @@ async function release_promote_handler(args) {
|
|
|
4472
4733
|
message: text
|
|
4473
4734
|
};
|
|
4474
4735
|
}
|
|
4475
|
-
if (!res.ok)
|
|
4736
|
+
if (!res.ok) {
|
|
4737
|
+
const code = "string" == typeof data?.code ? data.code : void 0;
|
|
4738
|
+
const enrich = {};
|
|
4739
|
+
const ref = resolveProjectRef();
|
|
4740
|
+
if (404 === res.status || "UNKNOWN_BUILD" === code) {
|
|
4741
|
+
enrich.buildsPageUrl = consoleProjectUrl(ref, "builds");
|
|
4742
|
+
enrich.hint = "Run extension_release_list to see this project's channels, their promoted shas, and recent builds.";
|
|
4743
|
+
if (ref) {
|
|
4744
|
+
const channelsUrl = registryFileUrl(ref, "channels.json");
|
|
4745
|
+
const channelsRes = await fetchRegistryJson(channelsUrl);
|
|
4746
|
+
if (channelsRes.ok) {
|
|
4747
|
+
const rows = parseChannels(channelsRes.json).filter((c)=>c.sha);
|
|
4748
|
+
enrich.validChannelShas = Object.fromEntries(rows.map((c)=>[
|
|
4749
|
+
c.channel,
|
|
4750
|
+
c.sha
|
|
4751
|
+
]));
|
|
4752
|
+
enrich.registryChannelsUrl = channelsUrl;
|
|
4753
|
+
}
|
|
4754
|
+
}
|
|
4755
|
+
}
|
|
4756
|
+
return JSON.stringify({
|
|
4757
|
+
ok: false,
|
|
4758
|
+
error: {
|
|
4759
|
+
name: "ReleaseError",
|
|
4760
|
+
message: `promote failed (${res.status}): ${data?.message || text || "unknown error"}`,
|
|
4761
|
+
...code ? {
|
|
4762
|
+
code
|
|
4763
|
+
} : {}
|
|
4764
|
+
},
|
|
4765
|
+
...enrich
|
|
4766
|
+
});
|
|
4767
|
+
}
|
|
4476
4768
|
return JSON.stringify(data);
|
|
4477
4769
|
}
|
|
4770
|
+
const release_list_schema = {
|
|
4771
|
+
name: "extension_release_list",
|
|
4772
|
+
description: "List the project's release channels (channel -> promoted build sha) and recent builds from the public 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 public project. Also returns the registry URLs it read and the console Builds page URL.",
|
|
4773
|
+
inputSchema: {
|
|
4774
|
+
type: "object",
|
|
4775
|
+
properties: {
|
|
4776
|
+
workspace: {
|
|
4777
|
+
type: "string",
|
|
4778
|
+
description: "Workspace slug override (defaults to the stored login's workspace)."
|
|
4779
|
+
},
|
|
4780
|
+
project: {
|
|
4781
|
+
type: "string",
|
|
4782
|
+
description: "Project slug override (defaults to the stored login's project)."
|
|
4783
|
+
}
|
|
4784
|
+
},
|
|
4785
|
+
required: []
|
|
4786
|
+
}
|
|
4787
|
+
};
|
|
4788
|
+
function release_list_fail(name, message, extra) {
|
|
4789
|
+
return JSON.stringify({
|
|
4790
|
+
ok: false,
|
|
4791
|
+
error: {
|
|
4792
|
+
name,
|
|
4793
|
+
message
|
|
4794
|
+
},
|
|
4795
|
+
...extra ?? {}
|
|
4796
|
+
});
|
|
4797
|
+
}
|
|
4798
|
+
async function release_list_handler(args) {
|
|
4799
|
+
const ref = resolveProjectRef(args);
|
|
4800
|
+
if (!ref) return release_list_fail("ReleaseListInputError", "No project to list. Run extension_login (the stored login names the project), or pass workspace + project explicitly.");
|
|
4801
|
+
const channelsUrl = registryFileUrl(ref, "channels.json");
|
|
4802
|
+
const metaUrl = registryFileUrl(ref, "meta.json");
|
|
4803
|
+
const buildsUrl = registryFileUrl(ref, "builds/index.json");
|
|
4804
|
+
const [channelsRes, metaRes, buildsRes] = await Promise.all([
|
|
4805
|
+
fetchRegistryJson(channelsUrl),
|
|
4806
|
+
fetchRegistryJson(metaUrl),
|
|
4807
|
+
fetchRegistryJson(buildsUrl)
|
|
4808
|
+
]);
|
|
4809
|
+
const buildsPageUrl = consoleProjectUrl(ref, "builds");
|
|
4810
|
+
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, be private (private registry data needs a share token), or the workspace/project slugs may be wrong. The console Builds page is the authoritative view: ${buildsPageUrl}`, {
|
|
4811
|
+
workspace: ref.workspace,
|
|
4812
|
+
project: ref.project,
|
|
4813
|
+
registryUrl: channelsUrl,
|
|
4814
|
+
buildsPageUrl
|
|
4815
|
+
});
|
|
4816
|
+
const channels = channelsRes.ok ? parseChannels(channelsRes.json) : [];
|
|
4817
|
+
const recentBuilds = buildsRes.ok ? parseBuildIndex(buildsRes.json) : [];
|
|
4818
|
+
recentBuilds.sort((a, b)=>String(b.timestamp ?? "").localeCompare(String(a.timestamp ?? "")));
|
|
4819
|
+
const meta = metaRes.ok ? metaRes.json : void 0;
|
|
4820
|
+
const promotable = Array.from(new Set(channels.map((c)=>c.sha).filter(Boolean)));
|
|
4821
|
+
const result = {
|
|
4822
|
+
ok: true,
|
|
4823
|
+
workspace: ref.workspace,
|
|
4824
|
+
project: ref.project,
|
|
4825
|
+
...meta?.name ? {
|
|
4826
|
+
name: meta.name
|
|
4827
|
+
} : {},
|
|
4828
|
+
...meta?.visibility ? {
|
|
4829
|
+
visibility: meta.visibility
|
|
4830
|
+
} : {},
|
|
4831
|
+
channels,
|
|
4832
|
+
recentBuilds,
|
|
4833
|
+
registryUrl: channelsUrl,
|
|
4834
|
+
buildsPageUrl,
|
|
4835
|
+
message: promotable.length > 0 || recentBuilds.length > 0 ? `Promotable shas: channels currently pin ${promotable.length > 0 ? promotable.join(", ") : "none"}; recent builds add ${recentBuilds.filter((b)=>"success" === b.status).map((b)=>b.sha).join(", ") || "none"}. Use one of these as buildId/buildSha for promote/deploy/publish.` : `No channels or builds are recorded on the registry yet for ${ref.workspace}/${ref.project}. Push a commit to produce a build, then check ${buildsPageUrl}.`
|
|
4836
|
+
};
|
|
4837
|
+
if (!channelsRes.ok) result.channelsUnavailable = `channels.json unreadable: ${channelsRes.message}`;
|
|
4838
|
+
if (!buildsRes.ok) result.buildsUnavailable = `builds/index.json unreadable: ${buildsRes.message}`;
|
|
4839
|
+
return JSON.stringify(result);
|
|
4840
|
+
}
|
|
4478
4841
|
const deploy_DEFAULT_API = "https://www.extension.dev";
|
|
4479
4842
|
function storeMdWarnings(browsers, cwd) {
|
|
4480
4843
|
const wantsFirefox = browsers.includes("firefox");
|
|
@@ -4598,13 +4961,86 @@ async function deploy_handler(args) {
|
|
|
4598
4961
|
message: text
|
|
4599
4962
|
};
|
|
4600
4963
|
}
|
|
4601
|
-
if (!res.ok) return deploy_fail("DeployError",
|
|
4602
|
-
const warnings =
|
|
4964
|
+
if (!res.ok) return deploy_fail("DeployError", `${dryRun ? "preflight" : "submit"} failed (${res.status}): ${data?.message || text || "unknown error"}`);
|
|
4965
|
+
const warnings = Array.isArray(data?.warnings) ? [
|
|
4966
|
+
...data.warnings
|
|
4967
|
+
] : [];
|
|
4968
|
+
warnings.push(...storeMdWarnings(browsers, process.cwd()));
|
|
4603
4969
|
const result = {
|
|
4604
4970
|
mode: "platform",
|
|
4605
4971
|
dryRun,
|
|
4606
4972
|
...data
|
|
4607
4973
|
};
|
|
4974
|
+
if (dryRun) {
|
|
4975
|
+
const ref = resolveProjectRef();
|
|
4976
|
+
const consoleStoresUrl = consoleProjectUrl(ref, "stores");
|
|
4977
|
+
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}.`;
|
|
4978
|
+
let health = null;
|
|
4979
|
+
let healthUnreadable = null;
|
|
4980
|
+
let channelRows = null;
|
|
4981
|
+
if (ref) {
|
|
4982
|
+
const [healthRes, channelsRes] = await Promise.all([
|
|
4983
|
+
fetchRegistryJson(registryFileUrl(ref, "stores/health.json")),
|
|
4984
|
+
fetchRegistryJson(registryFileUrl(ref, "channels.json"))
|
|
4985
|
+
]);
|
|
4986
|
+
if (healthRes.ok) {
|
|
4987
|
+
const stores = healthRes.json?.stores;
|
|
4988
|
+
health = stores && "object" == typeof stores ? stores : null;
|
|
4989
|
+
if (!health) healthUnreadable = "stores/health.json had no stores map";
|
|
4990
|
+
} else healthUnreadable = healthRes.message;
|
|
4991
|
+
if (channelsRes.ok) channelRows = parseChannels(channelsRes.json);
|
|
4992
|
+
} else healthUnreadable = "no stored workspace/project to look up (run extension_login)";
|
|
4993
|
+
const preflight = browsers.map((browser)=>{
|
|
4994
|
+
if (!health) return {
|
|
4995
|
+
browser,
|
|
4996
|
+
ok: false,
|
|
4997
|
+
configured: "unknown",
|
|
4998
|
+
publishMode: "unknown",
|
|
4999
|
+
reason: `Store configuration could not be read (${healthUnreadable}); verify the ${browser} store in the console before submitting.`
|
|
5000
|
+
};
|
|
5001
|
+
const row = health[browser];
|
|
5002
|
+
if (!row) return {
|
|
5003
|
+
browser,
|
|
5004
|
+
ok: false,
|
|
5005
|
+
configured: false,
|
|
5006
|
+
publishMode: "unknown",
|
|
5007
|
+
reason: `No ${browser} store is configured on this project; a real submission for ${browser} would fail. Configure it at ${consoleStoresUrl}.`
|
|
5008
|
+
};
|
|
5009
|
+
if (true !== row.ok) return {
|
|
5010
|
+
browser,
|
|
5011
|
+
ok: false,
|
|
5012
|
+
configured: false,
|
|
5013
|
+
publishMode: "unknown",
|
|
5014
|
+
reason: String(row.message || "").trim() || `The ${browser} store failed its last credential health check.`
|
|
5015
|
+
};
|
|
5016
|
+
return {
|
|
5017
|
+
browser,
|
|
5018
|
+
ok: true,
|
|
5019
|
+
configured: true,
|
|
5020
|
+
publishMode: "unknown"
|
|
5021
|
+
};
|
|
5022
|
+
});
|
|
5023
|
+
const actionable = preflight.filter((p)=>p.ok).map((p)=>p.browser);
|
|
5024
|
+
const blocked = preflight.filter((p)=>!p.ok);
|
|
5025
|
+
const channelDefaulted = !String(args.channel || "").trim();
|
|
5026
|
+
const resolvedChannel = String(data?.channel || "").trim() || (channelDefaulted ? "stable" : String(args.channel).trim());
|
|
5027
|
+
if (channelRows) {
|
|
5028
|
+
const exists = channelRows.some((r)=>r.channel === resolvedChannel || r.channel.endsWith(`-${resolvedChannel}`));
|
|
5029
|
+
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.`);
|
|
5030
|
+
}
|
|
5031
|
+
const summaryParts = [];
|
|
5032
|
+
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.`);
|
|
5033
|
+
for (const p of blocked)summaryParts.push(`${p.browser}: ${"unknown" === p.configured ? "cannot be verified" : "NOT actionable"} - ${p.reason}`);
|
|
5034
|
+
summaryParts.push(storeModeNote);
|
|
5035
|
+
result.ok = actionable.length > 0;
|
|
5036
|
+
result.preflight = preflight;
|
|
5037
|
+
result.channel = resolvedChannel;
|
|
5038
|
+
result.channelDefaulted = channelDefaulted;
|
|
5039
|
+
if (channelDefaulted) result.channelNote = `channel: ${resolvedChannel} (default)`;
|
|
5040
|
+
result.consoleStoresUrl = consoleStoresUrl;
|
|
5041
|
+
if ("string" == typeof data?.message) result.platformMessage = data.message;
|
|
5042
|
+
result.message = summaryParts.join(" ");
|
|
5043
|
+
}
|
|
4608
5044
|
if (warnings.length > 0) result.warnings = warnings;
|
|
4609
5045
|
return JSON.stringify(result);
|
|
4610
5046
|
}
|
|
@@ -5370,13 +5806,28 @@ function success(creds) {
|
|
|
5370
5806
|
});
|
|
5371
5807
|
}
|
|
5372
5808
|
function login_pending(start) {
|
|
5809
|
+
const complete = String(start.verificationUriComplete || "").trim();
|
|
5810
|
+
const hasCompleteLink = complete.length > 0 && complete !== start.verificationUri;
|
|
5811
|
+
const message = hasCompleteLink ? `Open ${complete} and approve (code ${start.userCode} is pre-filled), then call extension_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_login again with this deviceCode (and the same project).`;
|
|
5373
5812
|
return JSON.stringify({
|
|
5374
5813
|
ok: false,
|
|
5375
5814
|
status: "authorization_pending",
|
|
5376
5815
|
userCode: start.userCode,
|
|
5377
5816
|
verificationUri: start.verificationUri,
|
|
5817
|
+
...hasCompleteLink ? {
|
|
5818
|
+
verificationUriComplete: complete
|
|
5819
|
+
} : {},
|
|
5378
5820
|
deviceCode: start.deviceCode,
|
|
5379
|
-
message
|
|
5821
|
+
message
|
|
5822
|
+
});
|
|
5823
|
+
}
|
|
5824
|
+
function resumePending(deviceCode, verificationUri) {
|
|
5825
|
+
return JSON.stringify({
|
|
5826
|
+
ok: false,
|
|
5827
|
+
status: "authorization_pending",
|
|
5828
|
+
verificationUri,
|
|
5829
|
+
deviceCode,
|
|
5830
|
+
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_login again with this same deviceCode (and the same project).`
|
|
5380
5831
|
});
|
|
5381
5832
|
}
|
|
5382
5833
|
async function login_handler(args) {
|
|
@@ -5403,11 +5854,7 @@ async function login_handler(args) {
|
|
|
5403
5854
|
if ("expired" === poll.reason) return login_fail("LoginExpired", "The device code expired. Run extension_login again to restart.");
|
|
5404
5855
|
if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied at extension.dev/device.");
|
|
5405
5856
|
if ("error" === poll.reason) return login_fail("LoginError", poll.message || "Device login failed.");
|
|
5406
|
-
return
|
|
5407
|
-
deviceCode: String(args.deviceCode),
|
|
5408
|
-
userCode: "(see the previous response)",
|
|
5409
|
-
verificationUri: config.verificationUri
|
|
5410
|
-
});
|
|
5857
|
+
return resumePending(String(args.deviceCode), config.verificationUri);
|
|
5411
5858
|
}
|
|
5412
5859
|
let start;
|
|
5413
5860
|
try {
|
|
@@ -5432,7 +5879,8 @@ async function login_handler(args) {
|
|
|
5432
5879
|
return login_pending({
|
|
5433
5880
|
deviceCode: start.deviceCode,
|
|
5434
5881
|
userCode: start.userCode,
|
|
5435
|
-
verificationUri: start.verificationUri
|
|
5882
|
+
verificationUri: start.verificationUri,
|
|
5883
|
+
verificationUriComplete: start.verificationUriComplete
|
|
5436
5884
|
});
|
|
5437
5885
|
}
|
|
5438
5886
|
if (args.deviceCode) {
|
|
@@ -5445,11 +5893,7 @@ async function login_handler(args) {
|
|
|
5445
5893
|
if (!poll.ok) {
|
|
5446
5894
|
if ("expired" === poll.reason) return login_fail("LoginExpired", "The device code expired. Run extension_login again to restart.");
|
|
5447
5895
|
if ("denied" === poll.reason) return login_fail("LoginDenied", "Authorization was denied on GitHub.");
|
|
5448
|
-
return
|
|
5449
|
-
deviceCode: String(args.deviceCode),
|
|
5450
|
-
userCode: "(see the previous response)",
|
|
5451
|
-
verificationUri: "https://github.com/login/device"
|
|
5452
|
-
});
|
|
5896
|
+
return resumePending(String(args.deviceCode), "https://github.com/login/device");
|
|
5453
5897
|
}
|
|
5454
5898
|
try {
|
|
5455
5899
|
const creds = await exchangeAndPersist({
|
|
@@ -6102,6 +6546,12 @@ const ARG_ALIASES = {
|
|
|
6102
6546
|
],
|
|
6103
6547
|
browser: [
|
|
6104
6548
|
"browserName"
|
|
6549
|
+
],
|
|
6550
|
+
buildSha: [
|
|
6551
|
+
"buildId"
|
|
6552
|
+
],
|
|
6553
|
+
buildId: [
|
|
6554
|
+
"buildSha"
|
|
6105
6555
|
]
|
|
6106
6556
|
};
|
|
6107
6557
|
function normalizeArgAliases(inputSchema, args) {
|
|
@@ -6145,13 +6595,33 @@ function validateToolInput(inputSchema, args) {
|
|
|
6145
6595
|
}
|
|
6146
6596
|
return issues;
|
|
6147
6597
|
}
|
|
6148
|
-
function
|
|
6598
|
+
function describeToolArgs(inputSchema) {
|
|
6599
|
+
const schema = inputSchema;
|
|
6600
|
+
const props = schema.properties ?? {};
|
|
6601
|
+
const required = schema.required ?? [];
|
|
6602
|
+
const optional = Object.keys(props).filter((k)=>!required.includes(k));
|
|
6603
|
+
const aliases = {};
|
|
6604
|
+
for (const [canonical, list] of Object.entries(ARG_ALIASES)){
|
|
6605
|
+
if (!(canonical in props)) continue;
|
|
6606
|
+
const usable = list.filter((alias)=>!(alias in props));
|
|
6607
|
+
if (usable.length) aliases[canonical] = usable;
|
|
6608
|
+
}
|
|
6609
|
+
return {
|
|
6610
|
+
required,
|
|
6611
|
+
optional,
|
|
6612
|
+
aliases
|
|
6613
|
+
};
|
|
6614
|
+
}
|
|
6615
|
+
function inputValidationError(toolName, issues, inputSchema) {
|
|
6149
6616
|
return JSON.stringify({
|
|
6150
6617
|
ok: false,
|
|
6151
6618
|
error: {
|
|
6152
6619
|
name: "InputValidationError",
|
|
6153
6620
|
message: `Invalid arguments for ${toolName}: ${issues.map((i)=>`${i.path}: ${i.message}`).join("; ")}`,
|
|
6154
|
-
issues
|
|
6621
|
+
issues,
|
|
6622
|
+
...inputSchema ? {
|
|
6623
|
+
args: describeToolArgs(inputSchema)
|
|
6624
|
+
} : {}
|
|
6155
6625
|
}
|
|
6156
6626
|
});
|
|
6157
6627
|
}
|
|
@@ -6175,6 +6645,7 @@ const tools = [
|
|
|
6175
6645
|
open_namespaceObject,
|
|
6176
6646
|
dom_inspect_namespaceObject,
|
|
6177
6647
|
tools_publish_namespaceObject,
|
|
6648
|
+
release_list_namespaceObject,
|
|
6178
6649
|
release_promote_namespaceObject,
|
|
6179
6650
|
deploy_namespaceObject,
|
|
6180
6651
|
wait_namespaceObject,
|
|
@@ -6227,7 +6698,7 @@ async function startServer() {
|
|
|
6227
6698
|
content: [
|
|
6228
6699
|
{
|
|
6229
6700
|
type: "text",
|
|
6230
|
-
text: inputValidationError(name, issues)
|
|
6701
|
+
text: inputValidationError(name, issues, tool.schema.inputSchema)
|
|
6231
6702
|
}
|
|
6232
6703
|
],
|
|
6233
6704
|
isError: true
|