@rainbow-robotics/muscat-plugin-kit 1.10.2-dev.6 → 1.10.2-dev.8
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/cli/bin/muscat-pk.mjs +3 -0
- package/cli/src/build.mjs +4 -9
- package/cli/src/dev.mjs +2 -1
- package/cli/src/devBack.mjs +6 -12
- package/cli/src/devSession.mjs +67 -0
- package/cli/src/devUi.mjs +15 -0
- package/cli/src/mcp.mjs +7 -1
- package/cli/src/mcpProxy.mjs +110 -0
- package/cli/src/muscatLocal.mjs +15 -1
- package/cli/src/templates-embedded.mjs +2 -2
- package/cli/src/zip.mjs +121 -0
- package/cli/storybook-dist/assets/{Color-YHDXOIA2-BoqBLKNL.js → Color-YHDXOIA2-B1KKcB6K.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-BlIu-JrJ.js → Docs-2Fjmybwk.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-BhJTZ0C_.js → Docs-CHv6SVVZ.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-VKVuhalv.js → Docs-Ccehtd2V.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-C4ktkQce.js → Docs-DLOo5tzV.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-Cg3A7xDz.js → Docs-DtdcK7tN.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-_i1XHVXU.js → Docs-exoI2PyN.js} +1 -1
- package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-BGHjhFiI.js → DocsRenderer-CFRXHY34-DHVj1Lpb.js} +1 -1
- package/cli/storybook-dist/assets/{entry-preview-C2IBQfq7.js → entry-preview-fUNTcv77.js} +1 -1
- package/cli/storybook-dist/assets/{iframe-D-mTyH7q.js → iframe-Ck1s_iK9.js} +2 -2
- package/cli/storybook-dist/assets/{index-DKrg3vQJ.js → index-BkRHCy11.js} +3 -3
- package/cli/storybook-dist/assets/{preview-CpePTsLG.js → preview-CDcrEKt5.js} +2 -4
- package/cli/storybook-dist/assets/{preview-i5rYb1IR.js → preview-Cf2hNwTD.js} +2 -2
- package/cli/storybook-dist/iframe.html +1 -1
- package/cli/storybook-dist/project.json +1 -1
- package/cli/templates/common/README.md +1 -1
- package/cli/templates/common/py/main.py +2 -2
- package/cli/templates/cpp/cpp/main.cpp +1 -1
- package/cli/templates/readme-cpp/README.md +3 -3
- package/package.json +3 -3
package/cli/bin/muscat-pk.mjs
CHANGED
|
@@ -121,6 +121,9 @@ async function main() {
|
|
|
121
121
|
case "storybook":
|
|
122
122
|
return await runStorybook(args);
|
|
123
123
|
case "mcp": {
|
|
124
|
+
// Relay by default so `muscat-pk update` reaches a running session; --serve is the server.
|
|
125
|
+
if (args[0] !== "--serve" && args[0] !== "--install" && args[0] !== "install")
|
|
126
|
+
return (await import("../src/mcpProxy.mjs")).runMcpProxy();
|
|
124
127
|
const { runMcp, installMcp } = await import("../src/mcp.mjs");
|
|
125
128
|
if (args[0] === "--install" || args[0] === "install")
|
|
126
129
|
return installMcp(args[1]);
|
package/cli/src/build.mjs
CHANGED
|
@@ -13,9 +13,10 @@ import { chmodSync, cpSync, copyFileSync, existsSync, mkdirSync, readdirSync, re
|
|
|
13
13
|
import { createRequire } from "node:module";
|
|
14
14
|
import { basename, dirname, resolve } from "node:path";
|
|
15
15
|
import { createInterface } from "node:readline/promises";
|
|
16
|
-
import {
|
|
16
|
+
import { spawnSync } from "node:child_process";
|
|
17
17
|
import { ensureDocker, findPluginRoot, hasBin, humanSize, log, parseArgs, prompt } from "./util.mjs";
|
|
18
18
|
import { assertManifest, manifestRuntime } from "./manifest.mjs";
|
|
19
|
+
import { writeZip } from "./zip.mjs";
|
|
19
20
|
|
|
20
21
|
// Find a usable JS runtime for running vite as a subprocess.
|
|
21
22
|
// In binary (Bun) mode, try `bun` first (it can run vite directly), then node.
|
|
@@ -343,14 +344,8 @@ function packZip(pluginRoot) {
|
|
|
343
344
|
const entries = ["dist"];
|
|
344
345
|
|
|
345
346
|
log.step(`packing ${entries.join(", ")} → ${zipName}`);
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
cwd: pluginRoot,
|
|
349
|
-
stdio: "inherit",
|
|
350
|
-
});
|
|
351
|
-
} catch {
|
|
352
|
-
throw new Error("`zip` not found. Install zip or pack manually.");
|
|
353
|
-
}
|
|
347
|
+
// In-process, not the `zip` CLI: Windows has none, and it keeps the robot's executable bits (zip.mjs).
|
|
348
|
+
writeZip(zipPath, entries, pluginRoot);
|
|
354
349
|
|
|
355
350
|
const zipBytes = statSync(zipPath).size;
|
|
356
351
|
log.ok(`${zipName} ${humanSize(zipBytes)}`);
|
package/cli/src/dev.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { connect as netConnect } from "node:net";
|
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { runDevUi } from "./devUi.mjs";
|
|
8
|
-
import { stopDevBackContainers } from "./
|
|
8
|
+
import { claimDevSession, stopDevBackContainers } from "./devSession.mjs";
|
|
9
9
|
import { ensureLocalMuscat, GATEWAY_PORT, isPortInUse, robotHasMuscat } from "./muscatLocal.mjs";
|
|
10
10
|
import { assertManifestPorts, assertManifestVariables, readManifest } from "./manifest.mjs";
|
|
11
11
|
import { devPluginId } from "./devId.mjs";
|
|
@@ -111,6 +111,7 @@ async function maybeStartHostAgent(root, { robotIp, robotToken }) {
|
|
|
111
111
|
|
|
112
112
|
export async function runDev(args = []) {
|
|
113
113
|
const root = findPluginRoot();
|
|
114
|
+
await claimDevSession(root, "dev");
|
|
114
115
|
assertManifestVariables(root);
|
|
115
116
|
const portRanges = assertManifestPorts(root);
|
|
116
117
|
ensureDocker();
|
package/cli/src/devBack.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { isIP } from "node:net";
|
|
|
13
13
|
import { resolve } from "node:path";
|
|
14
14
|
import { dockerUserFlags, ensureDevDir, ensureDocker, ensureIdeVenv, findPluginRoot, log, promptRobotConnection, realtimeFlags } from "./util.mjs";
|
|
15
15
|
import { ensureRemoteZenohEnv, getFreePort, isRemoteRobot, usesHostNetwork } from "./zenohDev.mjs";
|
|
16
|
+
import { claimDevSession, DEV_CONTAINER, stopDevBackContainers } from "./devSession.mjs";
|
|
16
17
|
import { GATEWAY_PORT, localMuscatContainer, robotHasMuscat } from "./muscatLocal.mjs";
|
|
17
18
|
import { devPluginId } from "./devId.mjs";
|
|
18
19
|
import { PLUGIN_ID_RE, assertManifestPorts, assertManifestVariables, manifestRuntime, readManifest, rosNamespace, validateManifest } from "./manifest.mjs";
|
|
@@ -44,15 +45,7 @@ function exitAfterCleanup(code = 0) {
|
|
|
44
45
|
if (process.env[MANAGED_DEV_ENV] !== "1") process.exit(code ?? 0);
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
export
|
|
48
|
-
for (const container of [
|
|
49
|
-
`${PYTHON_DEV_IMAGE}-run`,
|
|
50
|
-
`${ROS2_DEV_IMAGE}-run`,
|
|
51
|
-
`${CPP_DEV_IMAGE}-run`,
|
|
52
|
-
]) {
|
|
53
|
-
spawnSync("docker", ["rm", "-f", container], { stdio: "ignore" });
|
|
54
|
-
}
|
|
55
|
-
}
|
|
48
|
+
export { stopDevBackContainers };
|
|
56
49
|
|
|
57
50
|
// Where the backend's 127.0.0.1 points. A local Muscat container (muscat-pk-muscat, muscat-dev):
|
|
58
51
|
// join its netns so the gateway and MediaMTX resolve like on the robot. Otherwise its own
|
|
@@ -149,7 +142,7 @@ async function runPython(root, portRanges, robotGateway) {
|
|
|
149
142
|
|
|
150
143
|
const manifest = readManifest(root);
|
|
151
144
|
const pluginId = process.env.MUSCAT_PK_DEV_PLUGIN_ID || (manifest.id ?? "");
|
|
152
|
-
const container =
|
|
145
|
+
const container = DEV_CONTAINER;
|
|
153
146
|
const robotIp = process.env.MUSCAT_ROBOT_HOST ?? "";
|
|
154
147
|
const robotToken = process.env.MUSCAT_ROBOT_TOKEN ?? "";
|
|
155
148
|
const zenohAdvertiseHost = process.env.MUSCAT_ZENOH_ADVERTISE_HOST ?? "";
|
|
@@ -529,7 +522,7 @@ async function runRos2(root, portRanges, robotGateway) {
|
|
|
529
522
|
await ensureImage();
|
|
530
523
|
|
|
531
524
|
const pluginId = process.env.MUSCAT_PK_DEV_PLUGIN_ID || (manifest.id ?? "");
|
|
532
|
-
const container =
|
|
525
|
+
const container = DEV_CONTAINER;
|
|
533
526
|
const robotIp = process.env.MUSCAT_ROBOT_HOST ?? "";
|
|
534
527
|
const robotToken = process.env.MUSCAT_ROBOT_TOKEN ?? "";
|
|
535
528
|
const net = backendNetwork(portRanges, robotGateway);
|
|
@@ -624,7 +617,7 @@ async function runCpp(root, portRanges, robotGateway) {
|
|
|
624
617
|
await cppEnsureImage();
|
|
625
618
|
|
|
626
619
|
const pluginId = process.env.MUSCAT_PK_DEV_PLUGIN_ID || (manifest.id ?? "");
|
|
627
|
-
const container =
|
|
620
|
+
const container = DEV_CONTAINER;
|
|
628
621
|
const robotIp = process.env.MUSCAT_ROBOT_HOST ?? "";
|
|
629
622
|
const robotToken = process.env.MUSCAT_ROBOT_TOKEN ?? "";
|
|
630
623
|
const net = backendNetwork(portRanges, robotGateway);
|
|
@@ -699,6 +692,7 @@ function warnManifest(root) {
|
|
|
699
692
|
|
|
700
693
|
export async function runDevBack(args = []) {
|
|
701
694
|
const root = findPluginRoot();
|
|
695
|
+
await claimDevSession(root, "dev:back");
|
|
702
696
|
assertManifestVariables(root);
|
|
703
697
|
warnManifest(root);
|
|
704
698
|
const portRanges = assertManifestPorts(root);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// One Muscat plugin dev environment per PC: the backend container, the local Muscat runtime (and
|
|
2
|
+
// the plugin ports it publishes) and the host_agent relay port are shared. A new dev / dev:ui /
|
|
3
|
+
// dev:back offers to stop the running one and take over.
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { dirname, resolve } from "node:path";
|
|
8
|
+
import { log, prompt } from "./util.mjs";
|
|
9
|
+
|
|
10
|
+
export const DEV_CONTAINER = "muscat-pk-dev-run";
|
|
11
|
+
// Per-runtime names from older kits.
|
|
12
|
+
const OLD_CONTAINERS = ["muscat-pk-python-dev-run", "muscat-pk-ros2-dev-run", "muscat-pk-cpp-dev-run"];
|
|
13
|
+
const LOCK = resolve(homedir(), ".muscat-pk", "dev-session.json");
|
|
14
|
+
|
|
15
|
+
export function stopDevBackContainers() {
|
|
16
|
+
for (const c of [DEV_CONTAINER, ...OLD_CONTAINERS]) spawnSync("docker", ["rm", "-f", c], { stdio: "ignore" });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const alive = (pid) => {
|
|
20
|
+
try {
|
|
21
|
+
process.kill(pid, 0);
|
|
22
|
+
return true;
|
|
23
|
+
} catch (e) {
|
|
24
|
+
return e.code === "EPERM";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function runningSession() {
|
|
29
|
+
try {
|
|
30
|
+
const s = JSON.parse(readFileSync(LOCK, "utf8"));
|
|
31
|
+
return s.pid !== process.pid && alive(s.pid) ? s : null;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function stopSession(pid) {
|
|
38
|
+
// SIGINT runs its own cleanup (UI, host_agent, containers). Windows has no signals — kill the tree.
|
|
39
|
+
if (process.platform === "win32") spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
|
|
40
|
+
else process.kill(pid, "SIGINT");
|
|
41
|
+
for (let i = 0; i < 60 && alive(pid); i++) await new Promise((r) => setTimeout(r, 500));
|
|
42
|
+
if (alive(pid)) process.kill(pid, "SIGKILL");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function claimDevSession(root, command) {
|
|
46
|
+
if (process.env.MUSCAT_PK_MANAGED_DEV === "1") return; // `dev` claimed it for its dev:ui / dev:back
|
|
47
|
+
const other = runningSession();
|
|
48
|
+
if (other?.pid === process.ppid) return; // `dev`'s own dev:back child
|
|
49
|
+
if (other) {
|
|
50
|
+
log.warn(`A Muscat plugin dev environment is already running: muscat-pk ${other.command} in ${other.root} (pid ${other.pid}).`);
|
|
51
|
+
log.warn("Only one can run per PC — they share the backend container, the local Muscat and its ports.");
|
|
52
|
+
const answer = (await prompt(" Stop it and start this one? [y/N]: ")).toLowerCase();
|
|
53
|
+
if (answer !== "y" && answer !== "yes") throw new Error("another plugin dev environment is running — stop it first.");
|
|
54
|
+
await stopSession(other.pid);
|
|
55
|
+
stopDevBackContainers(); // whatever it could not clean up
|
|
56
|
+
log.ok("stopped the other plugin dev environment");
|
|
57
|
+
}
|
|
58
|
+
mkdirSync(dirname(LOCK), { recursive: true });
|
|
59
|
+
writeFileSync(LOCK, JSON.stringify({ pid: process.pid, root, command }));
|
|
60
|
+
process.on("exit", () => {
|
|
61
|
+
try {
|
|
62
|
+
if (JSON.parse(readFileSync(LOCK, "utf8")).pid === process.pid) rmSync(LOCK, { force: true });
|
|
63
|
+
} catch {
|
|
64
|
+
/* already gone */
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
package/cli/src/devUi.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { devPluginId } from "./devId.mjs";
|
|
|
11
11
|
import { readManifest } from "./manifest.mjs";
|
|
12
12
|
import { ensureLocalMuscat, GATEWAY_PORT, localFullMuscat, robotHasMuscat } from "./muscatLocal.mjs";
|
|
13
13
|
import { isRemoteRobot } from "./zenohDev.mjs";
|
|
14
|
+
import { claimDevSession } from "./devSession.mjs";
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
const DEV_PLUGIN_PORT = 4181;
|
|
@@ -82,6 +83,14 @@ function syncAssets(pluginRoot, outDir) {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
|
|
86
|
+
// "(dev)" after every name the plugin shows, so a dev session is never mistaken for an installed copy.
|
|
87
|
+
const devLabel = (v) =>
|
|
88
|
+
typeof v === "string"
|
|
89
|
+
? `${v} (dev)`
|
|
90
|
+
: v && typeof v === "object"
|
|
91
|
+
? Object.fromEntries(Object.entries(v).map(([lang, s]) => [lang, `${s} (dev)`]))
|
|
92
|
+
: v;
|
|
93
|
+
|
|
85
94
|
// preview-host takes the plugin id from this copy, so the UI talks under the session's dev id.
|
|
86
95
|
function stageDevAssets(pluginRoot, pluginId) {
|
|
87
96
|
const outDir = ensureDevDir(pluginRoot);
|
|
@@ -89,6 +98,11 @@ function stageDevAssets(pluginRoot, pluginId) {
|
|
|
89
98
|
if (existsSync(manifestSrc)) {
|
|
90
99
|
const manifest = JSON.parse(readFileSync(manifestSrc, "utf8"));
|
|
91
100
|
if (pluginId) manifest.id = pluginId;
|
|
101
|
+
manifest.name = devLabel(manifest.name);
|
|
102
|
+
for (const c of manifest.contributes ?? []) {
|
|
103
|
+
c.label = devLabel(c.label);
|
|
104
|
+
if (c.commandInfo?.name) c.commandInfo.name = devLabel(c.commandInfo.name);
|
|
105
|
+
}
|
|
92
106
|
writeFileSync(resolve(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
93
107
|
}
|
|
94
108
|
syncAssets(pluginRoot, outDir);
|
|
@@ -163,6 +177,7 @@ function waitForPluginDevPort(readyFile, proc, timeoutMs = 120_000) {
|
|
|
163
177
|
export async function runDevUi(args = []) {
|
|
164
178
|
const parsed = parseArgs(args);
|
|
165
179
|
const requestedPort = Number(parsed.port ?? "4180");
|
|
180
|
+
await claimDevSession(findPluginRoot(), "dev:ui");
|
|
166
181
|
const robotIp = parsed["robot-ip"] ?? process.env.MUSCAT_ROBOT_HOST ?? await prompt(" Robot IP (skip if no robot): ");
|
|
167
182
|
|
|
168
183
|
await checkKitUpdate();
|
package/cli/src/mcp.mjs
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
readdirSync,
|
|
32
32
|
readFileSync,
|
|
33
33
|
renameSync,
|
|
34
|
+
rmSync,
|
|
34
35
|
statSync,
|
|
35
36
|
writeFileSync,
|
|
36
37
|
} from "node:fs";
|
|
@@ -470,7 +471,12 @@ function introspectInDocker(probe) {
|
|
|
470
471
|
/* not a python plugin — latest */
|
|
471
472
|
}
|
|
472
473
|
// ponytail: "latest" cache is never refreshed; delete ~/.cache/... rb-sdk-py/latest to update.
|
|
473
|
-
const
|
|
474
|
+
const cacheRoot = join(UI_INDEX_CACHE_DIR, "rb-sdk-py");
|
|
475
|
+
const cache = join(cacheRoot, ver || "latest");
|
|
476
|
+
// Only the SDK version in use is kept (~165MB each) — other versions don't pile up.
|
|
477
|
+
for (const old of existsSync(cacheRoot) ? readdirSync(cacheRoot) : []) {
|
|
478
|
+
if (join(cacheRoot, old) !== cache) rmSync(join(cacheRoot, old), { recursive: true, force: true });
|
|
479
|
+
}
|
|
474
480
|
mkdirSync(cache, { recursive: true });
|
|
475
481
|
const pkg = ver ? `rainbow-rb-sdk==${ver}` : "rainbow-rb-sdk";
|
|
476
482
|
const script =
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// `muscat-pk mcp` — AI clients keep one stdio MCP server per session, so a `muscat-pk update`
|
|
2
|
+
// never reached a running one. This relays stdio to the real server (`mcp --serve`) and, when the
|
|
3
|
+
// installed kit changes, swaps that child for the new one: it replays the client's initialize and
|
|
4
|
+
// sends tools/list_changed, so the connection stays up and the next calls use the new tools.
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { statSync } from "node:fs";
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
import { createInterface } from "node:readline";
|
|
9
|
+
import { kitRoot, resolveLocalCliBin } from "./util.mjs";
|
|
10
|
+
|
|
11
|
+
const isBinaryMode = typeof Bun !== "undefined";
|
|
12
|
+
const REINIT_ID = "__muscat_pk_reinit__";
|
|
13
|
+
const POLL_MS = 3000;
|
|
14
|
+
|
|
15
|
+
// Re-resolved on every (re)start: an update may install, bump or remove the project's local kit.
|
|
16
|
+
function serverCommand() {
|
|
17
|
+
const local = resolveLocalCliBin(process.cwd());
|
|
18
|
+
if (local) {
|
|
19
|
+
return { cmd: isBinaryMode ? "node" : process.execPath, args: [local, "mcp", "--serve"], watch: resolve(local, "../../../package.json") };
|
|
20
|
+
}
|
|
21
|
+
if (isBinaryMode) return { cmd: process.execPath, args: ["mcp", "--serve"], watch: process.execPath };
|
|
22
|
+
return { cmd: process.execPath, args: [process.argv[1], "mcp", "--serve"], watch: resolve(kitRoot, "package.json") };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const stamp = (file) => {
|
|
26
|
+
try {
|
|
27
|
+
return `${file}:${statSync(file).mtimeMs}`;
|
|
28
|
+
} catch {
|
|
29
|
+
return file;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function runMcpProxy() {
|
|
34
|
+
let child = null;
|
|
35
|
+
let initRequest = null;
|
|
36
|
+
const pending = new Set(); // client request ids the current child hasn't answered
|
|
37
|
+
const toClient = (msg) => process.stdout.write(JSON.stringify(msg) + "\n");
|
|
38
|
+
|
|
39
|
+
const failPending = () => {
|
|
40
|
+
for (const id of pending) {
|
|
41
|
+
toClient({ jsonrpc: "2.0", id, error: { code: -32603, message: "muscat-pk was updated during this call — retry it." } });
|
|
42
|
+
}
|
|
43
|
+
pending.clear();
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const start = () => {
|
|
47
|
+
const { cmd, args } = serverCommand();
|
|
48
|
+
const c = spawn(cmd, args, { stdio: ["pipe", "pipe", "inherit"] });
|
|
49
|
+
createInterface({ input: c.stdout }).on("line", (line) => {
|
|
50
|
+
let msg = null;
|
|
51
|
+
try {
|
|
52
|
+
msg = JSON.parse(line);
|
|
53
|
+
} catch {
|
|
54
|
+
/* not JSON — pass through */
|
|
55
|
+
}
|
|
56
|
+
if (msg?.id === REINIT_ID) {
|
|
57
|
+
// Our replayed initialize: the client already has its answer from the first child.
|
|
58
|
+
c.stdin.write(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\n");
|
|
59
|
+
toClient({ jsonrpc: "2.0", method: "notifications/tools/list_changed" });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (msg && msg.id !== undefined && !msg.method) pending.delete(msg.id);
|
|
63
|
+
process.stdout.write(line + "\n");
|
|
64
|
+
});
|
|
65
|
+
c.on("exit", () => {
|
|
66
|
+
if (c !== child) return; // replaced on purpose
|
|
67
|
+
child = null;
|
|
68
|
+
failPending();
|
|
69
|
+
});
|
|
70
|
+
child = c;
|
|
71
|
+
if (initRequest) c.stdin.write(JSON.stringify({ ...initRequest, id: REINIT_ID }) + "\n");
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const restart = () => {
|
|
75
|
+
const old = child;
|
|
76
|
+
child = null;
|
|
77
|
+
old?.kill();
|
|
78
|
+
failPending();
|
|
79
|
+
start();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
start();
|
|
83
|
+
let last = stamp(serverCommand().watch);
|
|
84
|
+
setInterval(() => {
|
|
85
|
+
const now = stamp(serverCommand().watch);
|
|
86
|
+
if (now !== last) {
|
|
87
|
+
last = now;
|
|
88
|
+
process.stderr.write("[muscat-pk] kit updated — restarting the MCP server\n");
|
|
89
|
+
restart();
|
|
90
|
+
}
|
|
91
|
+
}, POLL_MS).unref();
|
|
92
|
+
|
|
93
|
+
createInterface({ input: process.stdin })
|
|
94
|
+
.on("line", (line) => {
|
|
95
|
+
let msg = null;
|
|
96
|
+
try {
|
|
97
|
+
msg = JSON.parse(line);
|
|
98
|
+
} catch {
|
|
99
|
+
/* forward as-is */
|
|
100
|
+
}
|
|
101
|
+
if (msg?.method === "initialize") initRequest = msg;
|
|
102
|
+
if (!child) start(); // the server crashed — bring it back (it replays initialize)
|
|
103
|
+
if (msg && msg.id !== undefined && msg.method) pending.add(msg.id);
|
|
104
|
+
child.stdin.write(line + "\n");
|
|
105
|
+
})
|
|
106
|
+
.on("close", () => {
|
|
107
|
+
child?.kill();
|
|
108
|
+
process.exit(0);
|
|
109
|
+
});
|
|
110
|
+
}
|
package/cli/src/muscatLocal.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// start our own: the S3 runtime image with just the `common` service.
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
8
|
import { createServer } from "node:net";
|
|
9
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { resolve } from "node:path";
|
|
12
12
|
import { mediaPortFlags, MEDIA_PORTS, publishPortFlags } from "./ports.mjs";
|
|
@@ -78,8 +78,22 @@ async function resolveVersions() {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// A new Muscat version replaces the previous runtime image (~2.8GB) and its scripts. One still in
|
|
82
|
+
// use by the running local Muscat stays until a later run (rmi refuses it).
|
|
83
|
+
export function removeOldRuntimes(total) {
|
|
84
|
+
const tags = spawnSync("docker", ["images", IMAGE, "--format", "{{.Tag}}"], { encoding: "utf8" }).stdout ?? "";
|
|
85
|
+
for (const tag of tags.split("\n").map((t) => t.trim()).filter((t) => t && t !== total)) {
|
|
86
|
+
spawnSync("docker", ["rmi", `${IMAGE}:${tag}`], { stdio: "ignore" });
|
|
87
|
+
}
|
|
88
|
+
const dir = resolve(CACHE, "runtime");
|
|
89
|
+
for (const old of existsSync(dir) ? readdirSync(dir) : []) {
|
|
90
|
+
if (old !== total) rmSync(resolve(dir, old), { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
81
94
|
async function ensureRuntimeImage(total) {
|
|
82
95
|
const image = `${IMAGE}:${total}`;
|
|
96
|
+
removeOldRuntimes(total);
|
|
83
97
|
if (canSkipImageBuild(image)) return image;
|
|
84
98
|
const dir = resolve(CACHE, "runtime", total);
|
|
85
99
|
if (!existsSync(resolve(dir, "Dockerfile.runtime"))) {
|