@extension.dev/mcp 5.2.0 → 5.3.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 +53 -0
- package/dist/module.js +722 -265
- 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.0","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,21 +898,325 @@ 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
|
}
|
|
905
|
+
function markerDir() {
|
|
906
|
+
return process.env.EXTENSION_MCP_SESSION_DIR || node_path.join(node_os.tmpdir(), "extension-dev-mcp-sessions");
|
|
907
|
+
}
|
|
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`);
|
|
911
|
+
}
|
|
912
|
+
function removeSessionMarker(projectPath, browser) {
|
|
913
|
+
try {
|
|
914
|
+
node_fs.rmSync(markerPath(projectPath, browser), {
|
|
915
|
+
force: true
|
|
916
|
+
});
|
|
917
|
+
} catch {}
|
|
918
|
+
}
|
|
919
|
+
function listSessionMarkers() {
|
|
920
|
+
let files;
|
|
921
|
+
try {
|
|
922
|
+
files = node_fs.readdirSync(markerDir());
|
|
923
|
+
} catch {
|
|
924
|
+
return [];
|
|
925
|
+
}
|
|
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;
|
|
932
|
+
}
|
|
889
933
|
function registerSession(info) {
|
|
890
|
-
|
|
934
|
+
sessions.set(sessionKey(info.projectPath, info.browser), info);
|
|
935
|
+
try {
|
|
936
|
+
node_fs.mkdirSync(markerDir(), {
|
|
937
|
+
recursive: true
|
|
938
|
+
});
|
|
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 {}
|
|
891
945
|
}
|
|
892
946
|
function getSession(projectPath, browser) {
|
|
893
|
-
return
|
|
947
|
+
return sessions.get(sessionKey(projectPath, browser));
|
|
894
948
|
}
|
|
895
949
|
function removeSession(projectPath, browser) {
|
|
896
|
-
|
|
950
|
+
sessions.delete(sessionKey(projectPath, browser));
|
|
897
951
|
}
|
|
898
952
|
function listSessions() {
|
|
899
|
-
return Array.from(
|
|
953
|
+
return Array.from(sessions.values());
|
|
954
|
+
}
|
|
955
|
+
function contractSightings(projectPath) {
|
|
956
|
+
const root = node_path.resolve(projectPath, "dist", "extension-js");
|
|
957
|
+
let dirs;
|
|
958
|
+
try {
|
|
959
|
+
dirs = node_fs.readdirSync(root);
|
|
960
|
+
} catch {
|
|
961
|
+
return [];
|
|
962
|
+
}
|
|
963
|
+
const sightings = [];
|
|
964
|
+
for (const dir of dirs){
|
|
965
|
+
const readyPath = node_path.join(root, dir, "ready.json");
|
|
966
|
+
try {
|
|
967
|
+
const stat = node_fs.statSync(readyPath);
|
|
968
|
+
const contract = JSON.parse(node_fs.readFileSync(readyPath, "utf8"));
|
|
969
|
+
if (contract?.status !== "ready") continue;
|
|
970
|
+
sightings.push({
|
|
971
|
+
browser: dir,
|
|
972
|
+
mtimeMs: stat.mtimeMs,
|
|
973
|
+
pid: "number" == typeof contract.pid ? contract.pid : void 0
|
|
974
|
+
});
|
|
975
|
+
} catch {}
|
|
976
|
+
}
|
|
977
|
+
return sightings;
|
|
978
|
+
}
|
|
979
|
+
function pidAlive(pid) {
|
|
980
|
+
try {
|
|
981
|
+
process.kill(pid, 0);
|
|
982
|
+
return true;
|
|
983
|
+
} catch {
|
|
984
|
+
return false;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
function knownSessionBrowsers(projectPath) {
|
|
988
|
+
const resolved = node_path.resolve(projectPath);
|
|
989
|
+
const browsers = [];
|
|
990
|
+
for (const session of listSessions())if (node_path.resolve(session.projectPath) === resolved) browsers.push(session.browser);
|
|
991
|
+
for (const sighting of contractSightings(projectPath))if (void 0 === sighting.pid || pidAlive(sighting.pid)) browsers.push(sighting.browser);
|
|
992
|
+
return Array.from(new Set(browsers));
|
|
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
|
+
}
|
|
1015
|
+
function deadReadySession(projectPath) {
|
|
1016
|
+
for (const sighting of contractSightings(projectPath))if (void 0 !== sighting.pid && !pidAlive(sighting.pid)) return {
|
|
1017
|
+
browser: sighting.browser,
|
|
1018
|
+
pid: sighting.pid
|
|
1019
|
+
};
|
|
1020
|
+
return null;
|
|
1021
|
+
}
|
|
1022
|
+
function browserExitStamp(projectPath, browser, since) {
|
|
1023
|
+
const readyPath = node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json");
|
|
1024
|
+
try {
|
|
1025
|
+
const stat = node_fs.statSync(readyPath);
|
|
1026
|
+
if (stat.mtimeMs < since) return null;
|
|
1027
|
+
const contract = JSON.parse(node_fs.readFileSync(readyPath, "utf8"));
|
|
1028
|
+
const exited = contract?.code === "browser_exited" || contract?.browserExitCode !== void 0 || contract?.browserExitedAt !== void 0;
|
|
1029
|
+
if (contract?.status === "error" && exited) return {
|
|
1030
|
+
code: contract.code,
|
|
1031
|
+
browserExitCode: contract.browserExitCode ?? null,
|
|
1032
|
+
browserExitedAt: contract.browserExitedAt
|
|
1033
|
+
};
|
|
1034
|
+
} catch {}
|
|
1035
|
+
return null;
|
|
1036
|
+
}
|
|
1037
|
+
function resolveSessionBrowser(projectPath, explicit, fallback = "chrome") {
|
|
1038
|
+
if (explicit) return {
|
|
1039
|
+
browser: explicit,
|
|
1040
|
+
source: "explicit"
|
|
1041
|
+
};
|
|
1042
|
+
const resolved = node_path.resolve(projectPath);
|
|
1043
|
+
const mine = listSessions().filter((s)=>node_path.resolve(s.projectPath) === resolved);
|
|
1044
|
+
if (mine.length > 0) return {
|
|
1045
|
+
browser: mine[mine.length - 1].browser,
|
|
1046
|
+
source: "session"
|
|
1047
|
+
};
|
|
1048
|
+
const sightings = contractSightings(projectPath).sort((a, b)=>b.mtimeMs - a.mtimeMs);
|
|
1049
|
+
const live = sightings.filter((s)=>void 0 === s.pid || pidAlive(s.pid));
|
|
1050
|
+
if (live.length > 0) return {
|
|
1051
|
+
browser: live[0].browser,
|
|
1052
|
+
source: "contract"
|
|
1053
|
+
};
|
|
1054
|
+
if (sightings.length > 0) return {
|
|
1055
|
+
browser: sightings[0].browser,
|
|
1056
|
+
source: "stale"
|
|
1057
|
+
};
|
|
1058
|
+
return {
|
|
1059
|
+
browser: fallback,
|
|
1060
|
+
source: "fallback"
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
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.",
|
|
1066
|
+
inputSchema: {
|
|
1067
|
+
type: "object",
|
|
1068
|
+
properties: {
|
|
1069
|
+
projectPath: {
|
|
1070
|
+
type: "string",
|
|
1071
|
+
description: "Path to the extension project root"
|
|
1072
|
+
},
|
|
1073
|
+
browser: {
|
|
1074
|
+
type: "string",
|
|
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."
|
|
1076
|
+
},
|
|
1077
|
+
all: {
|
|
1078
|
+
type: "boolean",
|
|
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);
|
|
900
1220
|
}
|
|
901
1221
|
const LAUNCH_FLAG_SCHEMA = {
|
|
902
1222
|
profile: {
|
|
@@ -988,6 +1308,11 @@ const dev_schema = {
|
|
|
988
1308
|
description: "Apply cross-browser polyfill"
|
|
989
1309
|
},
|
|
990
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
|
+
},
|
|
991
1316
|
allowControl: {
|
|
992
1317
|
type: "boolean",
|
|
993
1318
|
default: false,
|
|
@@ -1006,6 +1331,31 @@ const dev_schema = {
|
|
|
1006
1331
|
};
|
|
1007
1332
|
async function dev_handler(args) {
|
|
1008
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
|
+
}
|
|
1009
1359
|
const allowControl = Boolean(args.allowControl || args.allowEval);
|
|
1010
1360
|
const cliArgs = [
|
|
1011
1361
|
"dev",
|
|
@@ -1019,6 +1369,7 @@ async function dev_handler(args) {
|
|
|
1019
1369
|
cliArgs.push(...launchFlagArgs(args));
|
|
1020
1370
|
if (allowControl) cliArgs.push("--allow-control");
|
|
1021
1371
|
if (args.allowEval) cliArgs.push("--allow-eval");
|
|
1372
|
+
const spawnedAt = Date.now();
|
|
1022
1373
|
const spawned = spawnExtensionCli(cliArgs, {
|
|
1023
1374
|
projectDir: args.projectPath
|
|
1024
1375
|
});
|
|
@@ -1063,6 +1414,23 @@ async function dev_handler(args) {
|
|
|
1063
1414
|
logPath,
|
|
1064
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."
|
|
1065
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
|
+
}
|
|
1066
1434
|
const controlVerbs = "storage, reload, open, dom_inspect";
|
|
1067
1435
|
const capabilities = {
|
|
1068
1436
|
allowControl,
|
|
@@ -1076,109 +1444,28 @@ async function dev_handler(args) {
|
|
|
1076
1444
|
port: args.port ?? 8080,
|
|
1077
1445
|
projectPath: args.projectPath,
|
|
1078
1446
|
status: "started",
|
|
1447
|
+
...replaced.length > 0 ? {
|
|
1448
|
+
replacedSession: replaced[0],
|
|
1449
|
+
...replaced.length > 1 ? {
|
|
1450
|
+
replacedSessions: replaced
|
|
1451
|
+
} : {}
|
|
1452
|
+
} : {},
|
|
1079
1453
|
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).
|
|
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.",
|
|
1081
1455
|
earlyOutput: denoiseEarlyOutput(earlyOutput).slice(0, 500),
|
|
1082
1456
|
logPath
|
|
1083
1457
|
});
|
|
1084
1458
|
}
|
|
1085
1459
|
function denoiseEarlyOutput(raw) {
|
|
1086
|
-
const NOISE = [
|
|
1087
|
-
/^npm warn Unknown project config/i,
|
|
1088
|
-
/This will stop working in the next major version of npm/i,
|
|
1089
|
-
/^npm warn config/i,
|
|
1090
|
-
/V8: .*Invalid asm\.js/i,
|
|
1091
|
-
/^\(node:\d+\) V8:/i,
|
|
1092
|
-
/Use `node --trace-warnings/i
|
|
1093
|
-
];
|
|
1094
|
-
return raw.split("\n").filter((line)=>!NOISE.some((re)=>re.test(line.trim()))).join("\n").replace(/\n{2,}/g, "\n").trimStart();
|
|
1095
|
-
}
|
|
1096
|
-
function contractSightings(projectPath) {
|
|
1097
|
-
const root = node_path.resolve(projectPath, "dist", "extension-js");
|
|
1098
|
-
let dirs;
|
|
1099
|
-
try {
|
|
1100
|
-
dirs = node_fs.readdirSync(root);
|
|
1101
|
-
} catch {
|
|
1102
|
-
return [];
|
|
1103
|
-
}
|
|
1104
|
-
const sightings = [];
|
|
1105
|
-
for (const dir of dirs){
|
|
1106
|
-
const readyPath = node_path.join(root, dir, "ready.json");
|
|
1107
|
-
try {
|
|
1108
|
-
const stat = node_fs.statSync(readyPath);
|
|
1109
|
-
const contract = JSON.parse(node_fs.readFileSync(readyPath, "utf8"));
|
|
1110
|
-
if (contract?.status !== "ready") continue;
|
|
1111
|
-
sightings.push({
|
|
1112
|
-
browser: dir,
|
|
1113
|
-
mtimeMs: stat.mtimeMs,
|
|
1114
|
-
pid: "number" == typeof contract.pid ? contract.pid : void 0
|
|
1115
|
-
});
|
|
1116
|
-
} catch {}
|
|
1117
|
-
}
|
|
1118
|
-
return sightings;
|
|
1119
|
-
}
|
|
1120
|
-
function pidAlive(pid) {
|
|
1121
|
-
try {
|
|
1122
|
-
process.kill(pid, 0);
|
|
1123
|
-
return true;
|
|
1124
|
-
} catch {
|
|
1125
|
-
return false;
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
function knownSessionBrowsers(projectPath) {
|
|
1129
|
-
const resolved = node_path.resolve(projectPath);
|
|
1130
|
-
const browsers = [];
|
|
1131
|
-
for (const session of listSessions())if (node_path.resolve(session.projectPath) === resolved) browsers.push(session.browser);
|
|
1132
|
-
for (const sighting of contractSightings(projectPath))if (void 0 === sighting.pid || pidAlive(sighting.pid)) browsers.push(sighting.browser);
|
|
1133
|
-
return Array.from(new Set(browsers));
|
|
1134
|
-
}
|
|
1135
|
-
function deadReadySession(projectPath) {
|
|
1136
|
-
for (const sighting of contractSightings(projectPath))if (void 0 !== sighting.pid && !pidAlive(sighting.pid)) return {
|
|
1137
|
-
browser: sighting.browser,
|
|
1138
|
-
pid: sighting.pid
|
|
1139
|
-
};
|
|
1140
|
-
return null;
|
|
1141
|
-
}
|
|
1142
|
-
function browserExitStamp(projectPath, browser, since) {
|
|
1143
|
-
const readyPath = node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json");
|
|
1144
|
-
try {
|
|
1145
|
-
const stat = node_fs.statSync(readyPath);
|
|
1146
|
-
if (stat.mtimeMs < since) return null;
|
|
1147
|
-
const contract = JSON.parse(node_fs.readFileSync(readyPath, "utf8"));
|
|
1148
|
-
const exited = contract?.code === "browser_exited" || contract?.browserExitCode !== void 0 || contract?.browserExitedAt !== void 0;
|
|
1149
|
-
if (contract?.status === "error" && exited) return {
|
|
1150
|
-
code: contract.code,
|
|
1151
|
-
browserExitCode: contract.browserExitCode ?? null,
|
|
1152
|
-
browserExitedAt: contract.browserExitedAt
|
|
1153
|
-
};
|
|
1154
|
-
} catch {}
|
|
1155
|
-
return null;
|
|
1156
|
-
}
|
|
1157
|
-
function resolveSessionBrowser(projectPath, explicit, fallback = "chrome") {
|
|
1158
|
-
if (explicit) return {
|
|
1159
|
-
browser: explicit,
|
|
1160
|
-
source: "explicit"
|
|
1161
|
-
};
|
|
1162
|
-
const resolved = node_path.resolve(projectPath);
|
|
1163
|
-
const mine = listSessions().filter((s)=>node_path.resolve(s.projectPath) === resolved);
|
|
1164
|
-
if (mine.length > 0) return {
|
|
1165
|
-
browser: mine[mine.length - 1].browser,
|
|
1166
|
-
source: "session"
|
|
1167
|
-
};
|
|
1168
|
-
const sightings = contractSightings(projectPath).sort((a, b)=>b.mtimeMs - a.mtimeMs);
|
|
1169
|
-
const live = sightings.filter((s)=>void 0 === s.pid || pidAlive(s.pid));
|
|
1170
|
-
if (live.length > 0) return {
|
|
1171
|
-
browser: live[0].browser,
|
|
1172
|
-
source: "contract"
|
|
1173
|
-
};
|
|
1174
|
-
if (sightings.length > 0) return {
|
|
1175
|
-
browser: sightings[0].browser,
|
|
1176
|
-
source: "stale"
|
|
1177
|
-
};
|
|
1178
|
-
return {
|
|
1179
|
-
browser: fallback,
|
|
1180
|
-
source: "fallback"
|
|
1181
|
-
};
|
|
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();
|
|
1182
1469
|
}
|
|
1183
1470
|
const start_schema = {
|
|
1184
1471
|
name: "extension_start",
|
|
@@ -1414,157 +1701,6 @@ async function preview_handler(args) {
|
|
|
1414
1701
|
logPath
|
|
1415
1702
|
});
|
|
1416
1703
|
}
|
|
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,
|
|
1541
|
-
browser,
|
|
1542
|
-
pid,
|
|
1543
|
-
stopped,
|
|
1544
|
-
reaped,
|
|
1545
|
-
detail
|
|
1546
|
-
};
|
|
1547
|
-
}
|
|
1548
|
-
async function stop_handler(args) {
|
|
1549
|
-
if (args.all) {
|
|
1550
|
-
const sessions = listSessions();
|
|
1551
|
-
if (0 === sessions.length) return JSON.stringify({
|
|
1552
|
-
stopped: [],
|
|
1553
|
-
message: "No sessions registered in this server."
|
|
1554
|
-
});
|
|
1555
|
-
const outcomes = [];
|
|
1556
|
-
for (const s of sessions)outcomes.push(await stopOne(s.projectPath, s.browser));
|
|
1557
|
-
return JSON.stringify({
|
|
1558
|
-
stopped: outcomes
|
|
1559
|
-
});
|
|
1560
|
-
}
|
|
1561
|
-
if (!args.projectPath) return JSON.stringify({
|
|
1562
|
-
error: "projectPath is required unless all=true. Pass the same projectPath used with extension_dev/extension_start."
|
|
1563
|
-
});
|
|
1564
|
-
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
1565
|
-
const outcome = await stopOne(args.projectPath, browser);
|
|
1566
|
-
return JSON.stringify(outcome);
|
|
1567
|
-
}
|
|
1568
1704
|
const RAW_BASE = "https://raw.githubusercontent.com/extension-js/examples/main/examples";
|
|
1569
1705
|
const get_template_source_schema = {
|
|
1570
1706
|
name: "extension_get_template_source",
|
|
@@ -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
|
}
|
|
@@ -6145,13 +6581,33 @@ function validateToolInput(inputSchema, args) {
|
|
|
6145
6581
|
}
|
|
6146
6582
|
return issues;
|
|
6147
6583
|
}
|
|
6148
|
-
function
|
|
6584
|
+
function describeToolArgs(inputSchema) {
|
|
6585
|
+
const schema = inputSchema;
|
|
6586
|
+
const props = schema.properties ?? {};
|
|
6587
|
+
const required = schema.required ?? [];
|
|
6588
|
+
const optional = Object.keys(props).filter((k)=>!required.includes(k));
|
|
6589
|
+
const aliases = {};
|
|
6590
|
+
for (const [canonical, list] of Object.entries(ARG_ALIASES)){
|
|
6591
|
+
if (!(canonical in props)) continue;
|
|
6592
|
+
const usable = list.filter((alias)=>!(alias in props));
|
|
6593
|
+
if (usable.length) aliases[canonical] = usable;
|
|
6594
|
+
}
|
|
6595
|
+
return {
|
|
6596
|
+
required,
|
|
6597
|
+
optional,
|
|
6598
|
+
aliases
|
|
6599
|
+
};
|
|
6600
|
+
}
|
|
6601
|
+
function inputValidationError(toolName, issues, inputSchema) {
|
|
6149
6602
|
return JSON.stringify({
|
|
6150
6603
|
ok: false,
|
|
6151
6604
|
error: {
|
|
6152
6605
|
name: "InputValidationError",
|
|
6153
6606
|
message: `Invalid arguments for ${toolName}: ${issues.map((i)=>`${i.path}: ${i.message}`).join("; ")}`,
|
|
6154
|
-
issues
|
|
6607
|
+
issues,
|
|
6608
|
+
...inputSchema ? {
|
|
6609
|
+
args: describeToolArgs(inputSchema)
|
|
6610
|
+
} : {}
|
|
6155
6611
|
}
|
|
6156
6612
|
});
|
|
6157
6613
|
}
|
|
@@ -6175,6 +6631,7 @@ const tools = [
|
|
|
6175
6631
|
open_namespaceObject,
|
|
6176
6632
|
dom_inspect_namespaceObject,
|
|
6177
6633
|
tools_publish_namespaceObject,
|
|
6634
|
+
release_list_namespaceObject,
|
|
6178
6635
|
release_promote_namespaceObject,
|
|
6179
6636
|
deploy_namespaceObject,
|
|
6180
6637
|
wait_namespaceObject,
|
|
@@ -6227,7 +6684,7 @@ async function startServer() {
|
|
|
6227
6684
|
content: [
|
|
6228
6685
|
{
|
|
6229
6686
|
type: "text",
|
|
6230
|
-
text: inputValidationError(name, issues)
|
|
6687
|
+
text: inputValidationError(name, issues, tool.schema.inputSchema)
|
|
6231
6688
|
}
|
|
6232
6689
|
],
|
|
6233
6690
|
isError: true
|