@rainbow-robotics/muscat-plugin-kit 1.10.2-dev.0 → 1.10.2-dev.2

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/README.md CHANGED
@@ -114,7 +114,7 @@ muscat-pk build # build dist/ and produce a distributable ZIP (sk
114
114
  | `muscat-pk dev:back` | Backend dev server (Docker, hot-reload) |
115
115
  | `muscat-pk storybook` | Browse the bundled rb-components Storybook |
116
116
  | `muscat-pk mcp` | Run an MCP server for AI coding agents (exposes rb-components/design-token/SDK docs) |
117
- | `muscat-pk update` | Update the kit + plugin-ui to the latest version |
117
+ | `muscat-pk update` | Update the kit + plugin-ui + preview-host to the latest version; lists old-API UI code to fix |
118
118
  | `muscat-pk sdk <list\|update>` | Manage the `rainbow-rb-sdk` version — python: `requirements.txt`, C++ (cpp / ros2-cpp): `find_package(rb_sdk <ver> EXACT REQUIRED)` in `CMakeLists.txt` |
119
119
  | `muscat-pk version` | Print the installed CLI version (`-v` / `--version` also work) |
120
120
  | `muscat-pk completion [shell]` | Print a shell completion script (`bash` / `zsh` / `fish` / `powershell`; defaults to `$SHELL`) |
package/cli/README.md CHANGED
@@ -17,7 +17,7 @@ muscat-pk dev:back # run the back end in a Docker container (python / cpp / r
17
17
  muscat-pk dev # dev:ui + dev:back
18
18
  muscat-pk storybook # browse the bundled rb-components Storybook subset
19
19
  muscat-pk mcp # start an MCP server (stdio) exposing rb-components/design-token/SDK docs to AI agents
20
- muscat-pk update # update @rainbow-robotics/muscat-plugin-kit and @rainbow-robotics/plugin-ui
20
+ muscat-pk update # update @rainbow-robotics/muscat-plugin-kit, plugin-ui and preview-host; lists old-API UI code to fix
21
21
  muscat-pk uninstall # remove muscat-pk itself (npm global or S3 binary)
22
22
  muscat-pk sdk list | sdk update [version] # manage rainbow-rb-sdk (python)
23
23
  muscat-pk version # print the installed CLI version (-v / --version too)
@@ -5,13 +5,13 @@ import { readFileSync, realpathSync } from "node:fs";
5
5
  import { resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { spawnSync } from "node:child_process";
8
- import { color, kitVersion, log, resolveLocalCliBin } from "../src/util.mjs";
8
+ import { color, compareKitVersions, kitVersion, log, resolveLocalCliBin } from "../src/util.mjs";
9
9
  import { runNew } from "../src/new.mjs";
10
10
  import { runBuild } from "../src/build.mjs";
11
11
  import { runDevUi } from "../src/devUi.mjs";
12
12
  import { runDevBack } from "../src/devBack.mjs";
13
13
  import { runDev } from "../src/dev.mjs";
14
- import { runUpdate } from "../src/update.mjs";
14
+ import { runUpdate, runUpdateCheck } from "../src/update.mjs";
15
15
  import { runUninstall } from "../src/uninstall.mjs";
16
16
  import { runSdk } from "../src/sdk.mjs";
17
17
  import { runType } from "../src/type.mjs";
@@ -50,6 +50,13 @@ if (isBinaryMode && !process.env.MUSCAT_PK_VERSION) {
50
50
  // Say so — otherwise an outdated local copy silently answers instead, and
51
51
  // its missing/renamed subcommands look like the CLI is broken.
52
52
  const localVersion = readVersion(localBin);
53
+ // An older local copy can't update a project correctly — it doesn't know what changed since
54
+ // it was published. Run this CLI's update instead, still touching this project only.
55
+ if (process.argv[2] === "update" && localVersion && compareKitVersions(localVersion, kitVersion()) < 0) {
56
+ console.error(color.dim(`[muscat-pk] project's local install (v${localVersion}) is older — updating it with this CLI (v${kitVersion()})`));
57
+ process.env.MUSCAT_PK_UPDATE_PROJECT_ONLY = "1";
58
+ return;
59
+ }
53
60
  console.error(
54
61
  color.dim(`[muscat-pk] using this project's local install${localVersion ? ` (v${localVersion})` : ""}: ${localBin}`),
55
62
  );
@@ -86,7 +93,7 @@ function formatHelp() {
86
93
  commandRow("dev:back", `Run the plugin back end ${color.dim("(python: watchfiles reload / ros2: Docker)")}`),
87
94
  commandRow("storybook", `Browse the bundled rb-components Storybook ${color.dim(`(${option("--port")} <n>, ${option("--rebuild")})`)}`),
88
95
  commandRow("mcp", `Start an MCP server ${color.dim("(stdio)")} exposing rb-components/rb SDK docs to AI agents ${color.dim(`(${option("--install")} codex: register in ~/.codex/config.toml)`)}`),
89
- commandRow("update", `Update ${color.magenta("@rainbow-robotics/muscat-plugin-kit")} and ${color.magenta("@rainbow-robotics/plugin-ui")} ${color.dim(`(local-only if run from a project's local install; ${option("--global")} also updates the global CLI)`)}`),
96
+ commandRow("update", `Update ${color.magenta("@rainbow-robotics/muscat-plugin-kit")}, ${color.magenta("@rainbow-robotics/plugin-ui")} and ${color.magenta("@rainbow-robotics/preview-host")} ${color.dim(`(local-only if run from a project's local install; ${option("--global")} also updates the global CLI)`)}`),
90
97
  commandRow("uninstall", `Remove the installed ${color.cyan("muscat-pk")} CLI ${color.dim("(npm global or S3 binary)")}`),
91
98
  commandRow("sdk <list|update [version]>", "Manage the python rainbow-rb-sdk version"),
92
99
  commandRow("version", `Print the installed ${color.cyan("muscat-pk")} version`),
@@ -121,6 +128,8 @@ async function main() {
121
128
  }
122
129
  case "update":
123
130
  return await runUpdate(args);
131
+ case "update:check": // internal: update runs it with the freshly installed local kit
132
+ return await runUpdateCheck(args);
124
133
  case "uninstall":
125
134
  return await runUninstall(args);
126
135
  case "sdk":
package/cli/src/mcp.mjs CHANGED
@@ -958,46 +958,88 @@ print(json.dumps(out))
958
958
  }
959
959
 
960
960
  const { data, error } = introspectRbSdk(`
961
- import inspect, json
961
+ import inspect, json, re, sys as _sys
962
962
  mod = importlib.import_module("rainbow.rb_sdk")
963
+ try:
964
+ _cpp = importlib.import_module("rainbow.rb_sdk.rb_sdk_cpp") # C++ SDK (>= 0.10): sub-SDK classes live here
965
+ except Exception:
966
+ _cpp = None
963
967
  name = ${JSON.stringify(className)}
964
968
  method = ${methodName ? JSON.stringify(methodName) : "None"}
965
- obj = getattr(mod, name, None)
969
+
970
+ def _lookup(n, near=None):
971
+ for m in (_sys.modules.get(getattr(near, "__module__", "")) if near else None, mod, _cpp):
972
+ o = getattr(m, n, None) if m else None
973
+ if o is not None:
974
+ return o
975
+ return None
976
+
977
+ obj = _lookup(name)
966
978
  if obj is None:
967
979
  print(json.dumps({"__missing__": True})); raise SystemExit(0)
968
- def sig(o):
969
- try: return str(inspect.signature(o))
970
- except Exception: return "(...)"
980
+
981
+ def sig(o, n=""):
982
+ try:
983
+ return str(inspect.signature(o))
984
+ except Exception:
985
+ pass
986
+ # pybind11 (C++ SDK): the signature is the docstring's first line.
987
+ first = (getattr(o, "__doc__", None) or "").strip().split("\\n")[0]
988
+ m = re.match(r"^\\w*(\\(.*\\))(\\s*->\\s*.+)?$", first)
989
+ if not m:
990
+ return "(...)"
991
+ return re.sub(r"self(: [\\w.]+)?(, )?", "", m.group(1), count=1) + (m.group(2) or "")
992
+
993
+ def summary(o):
994
+ lines = [l.strip() for l in (inspect.getdoc(o) or "").split("\\n") if l.strip()]
995
+ if lines and re.match(r"^\\w*\\(.*\\)", lines[0]): # pybind signature line
996
+ lines = lines[1:]
997
+ return lines[0] if lines else ""
998
+
999
+ def routines(cls):
1000
+ for m in dir(cls):
1001
+ if m.startswith("_"):
1002
+ continue
1003
+ if isinstance(inspect.getattr_static(cls, m, None), property):
1004
+ continue
1005
+ fn = getattr(cls, m, None)
1006
+ if inspect.isroutine(fn):
1007
+ yield m, fn
1008
+
1009
+ def sub_sdks(cls):
1010
+ """(attr, class) for sub-SDKs: C++ properties (\`move\` -> RBManipulateMoveSDK)
1011
+ and pure-Python \`self.<attr> = <Class>(...)\` in __init__."""
1012
+ out = []
1013
+ for attr in dir(cls):
1014
+ p = inspect.getattr_static(cls, attr, None)
1015
+ if attr.startswith("_") or not isinstance(p, property) or p.fget is None:
1016
+ continue
1017
+ ret = (getattr(p.fget, "__annotations__", {}) or {}).get("return")
1018
+ tname = ret.__name__ if inspect.isclass(ret) else None
1019
+ if not tname:
1020
+ m = re.search(r"->\\s*([\\w.]+)", p.fget.__doc__ or "")
1021
+ tname = m and m.group(1).split(".")[-1]
1022
+ sub = _lookup(tname, cls) if tname else None
1023
+ if inspect.isclass(sub):
1024
+ out.append((attr, sub))
1025
+ try:
1026
+ src = inspect.getsource(cls.__init__)
1027
+ except Exception:
1028
+ src = ""
1029
+ for attr, nested_name in re.findall(r"self\\.(\\w+)\\s*=\\s*(\\w+)\\(", src):
1030
+ sub = _lookup(nested_name, cls)
1031
+ if inspect.isclass(sub):
1032
+ out.append((attr, sub))
1033
+ return out
971
1034
 
972
1035
  if method:
973
- # Direct method on the class itself.
974
1036
  fn = getattr(obj, method, None) if inspect.isclass(obj) else None
975
- # rb_sdk classes often group calls under a nested namespace set in
976
- # __init__ (e.g. RBManipulateSDK().move.call_move_j) — that only exists on
977
- # an INSTANCE, which we won't construct (side effects, needs a robot
978
- # connection). Best-effort: search the class's own __init__ source text
979
- # for self.<attr> = <NestedClass>(...) and check each nested class too.
980
1037
  owner = name
981
1038
  if fn is None and inspect.isclass(obj):
982
- try:
983
- src = inspect.getsource(obj.__init__)
984
- except Exception:
985
- src = ""
986
- import re, sys as _sys
987
- # The nested sub-SDK classes (e.g. RBManipulateMoveSDK) are imported in
988
- # the module that DEFINES obj (rainbow.rb_sdk.manipulate), not in the
989
- # rainbow.rb_sdk top-level namespace — resolve names against both.
990
- _defmod = _sys.modules.get(getattr(obj, "__module__", ""))
991
- for attr, nested_name in re.findall(r"self\\.(\\w+)\\s*=\\s*(\\w+)\\(", src):
992
- nested = (
993
- (getattr(_defmod, nested_name, None) if _defmod else None)
994
- or mod.__dict__.get(nested_name)
995
- or globals().get(nested_name)
996
- )
997
- cand = getattr(nested, method, None) if nested else None
1039
+ for attr, sub in sub_sdks(obj):
1040
+ cand = getattr(sub, method, None)
998
1041
  if cand:
999
- fn = cand
1000
- owner = f"{name}.{attr} ({nested_name})"
1042
+ fn, owner = cand, f"{name}.{attr} ({sub.__name__})"
1001
1043
  break
1002
1044
  if fn is None:
1003
1045
  print(json.dumps({"__missing__": True, "__method__": True})); raise SystemExit(0)
@@ -1007,31 +1049,14 @@ if method:
1007
1049
  }))
1008
1050
  raise SystemExit(0)
1009
1051
 
1010
- res = {"name": name, "doc": inspect.getdoc(obj) or ""}
1052
+ res = {"name": name, "doc": summary(obj) if inspect.isclass(obj) else (inspect.getdoc(obj) or "")}
1011
1053
  if inspect.isclass(obj):
1012
1054
  res["kind"] = "class"
1013
1055
  res["init"] = name + sig(obj.__init__ if "__init__" in dir(obj) else obj)
1014
- def _methods_of(cls, prefix=""):
1015
- out = []
1016
- for m, fn in inspect.getmembers(cls, predicate=inspect.isfunction):
1017
- if m.startswith("_"): continue
1018
- out.append({"name": prefix + m, "sig": prefix + m + sig(fn),
1019
- "doc": (inspect.getdoc(fn) or "").split("\\n")[0]})
1020
- return out
1021
- methods = _methods_of(obj)
1022
- # Most rb_sdk calls live on nested sub-SDKs assigned in __init__
1023
- # (e.g. self.move = RBManipulateMoveSDK() -> obj.move.call_move_j). List
1024
- # those too, name-prefixed, so the API is discoverable without an instance.
1025
- import re as _re, sys as _sys
1026
- _defmod = _sys.modules.get(getattr(obj, "__module__", ""))
1027
- try:
1028
- _src = inspect.getsource(obj.__init__)
1029
- except Exception:
1030
- _src = ""
1031
- for _attr, _nested_name in _re.findall(r"self\\.(\\w+)\\s*=\\s*(\\w+)\\(", _src):
1032
- _nested = (getattr(_defmod, _nested_name, None) if _defmod else None) or mod.__dict__.get(_nested_name)
1033
- if inspect.isclass(_nested):
1034
- methods.extend(_methods_of(_nested, prefix=_attr + "."))
1056
+ methods = [{"name": m, "sig": m + sig(fn), "doc": summary(fn)} for m, fn in routines(obj)]
1057
+ # Most calls live on sub-SDKs (sdk.move.call_move_j) — list them name-prefixed.
1058
+ for attr, sub in sub_sdks(obj):
1059
+ methods += [{"name": f"{attr}.{m}", "sig": f"{attr}.{m}" + sig(fn), "doc": summary(fn)} for m, fn in routines(sub)]
1035
1060
  res["methods"] = methods
1036
1061
  else:
1037
1062
  res["kind"] = "function"
@@ -1041,10 +1066,9 @@ print(json.dumps(res))
1041
1066
  if (error) return { isError: true, content: [{ type: "text", text: error }] };
1042
1067
  if (data.__missing__) {
1043
1068
  const text = data.__method__
1044
- ? `No method "${methodName}" found directly on "${className}" (nor in an obvious nested ` +
1045
- `namespace set in __init__). Call get_rb_sdk_api with just \`name: "${className}"\` to see ` +
1069
+ ? `No method "${methodName}" found on "${className}" or its sub-SDKs (e.g. .move, .state). Call get_rb_sdk_api with just \`name: "${className}"\` to see ` +
1046
1070
  `its full method list and constructor first.`
1047
- : `No "${className}" in rainbow.rb_sdk. Call list_rb_sdk for valid names.`;
1071
+ : `No "${className}" in rainbow.rb_sdk (or rb_sdk_cpp). Call list_rb_sdk for valid names.`;
1048
1072
  return { isError: true, content: [{ type: "text", text }] };
1049
1073
  }
1050
1074
  if (data.kind === "method") {
@@ -11,11 +11,15 @@
11
11
  // After updating the CLI itself, also bumps the plugin project's local deps
12
12
  // and migrates the project structure if it's outdated (src/ → ui/, root main.py → py/).
13
13
  import { execSync, spawn, spawnSync } from "node:child_process";
14
- import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
15
- import { join, resolve } from "node:path";
16
- import { findPluginRoot, isLocalKitInstall, KIT_PUBLISHED_NAME, kitVersion, log, parseArgs, prompt } from "./util.mjs";
14
+ import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
15
+ import { join, relative, resolve } from "node:path";
16
+ import {
17
+ compareKitVersions, findPluginRoot, isLocalKitInstall, KIT_PUBLISHED_NAME, kitVersion, log, parseArgs, prompt,
18
+ resolveLocalCliBin,
19
+ } from "./util.mjs";
17
20
 
18
21
  const PLUGIN_UI_PACKAGE = "@rainbow-robotics/plugin-ui";
22
+ const PREVIEW_HOST_PACKAGE = "@rainbow-robotics/preview-host";
19
23
  const S3_BASE = "https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit";
20
24
 
21
25
  const isBinaryMode = typeof Bun !== "undefined";
@@ -193,21 +197,158 @@ async function migrateStructure(root, issues) {
193
197
 
194
198
  // ── plugin project local deps ─────────────────────────────────────────────────
195
199
 
200
+ // The three packages are released together on one version, so preview-host moves with the other
201
+ // two — left behind, an old pin keeps serving a preview that no longer matches plugin-ui.
196
202
  function updateLocalDeps(root, tag) {
197
203
  const pm = detectPm(root);
198
- const list = [`${KIT_PUBLISHED_NAME}@${tag}`, `${PLUGIN_UI_PACKAGE}@${tag}`];
199
- const cmd =
200
- pm === "npm" ? `npm install ${list.join(" ")}`
201
- : pm === "yarn" ? `yarn add ${list.join(" ")}`
202
- : `${pm} add ${list.join(" ")}`;
203
-
204
- log.step(`Updating plugin local deps (${pm}): ${list.join(", ")}`);
204
+ let pkg = {};
205
+ try {
206
+ pkg = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8"));
207
+ } catch { /* ignore */ }
208
+ const listed = (name) => Boolean(pkg.dependencies?.[name] || pkg.devDependencies?.[name]);
209
+ const names = [KIT_PUBLISHED_NAME, PLUGIN_UI_PACKAGE, ...(listed(PREVIEW_HOST_PACKAGE) ? [PREVIEW_HOST_PACKAGE] : [])];
210
+ const isDev = (name) => Boolean(pkg.devDependencies?.[name]);
211
+ const add = (list, dev) => {
212
+ const flag = dev ? " -D" : "";
213
+ return pm === "npm" ? `npm install${flag} ${list.join(" ")}` : `${pm} add${flag} ${list.join(" ")}`;
214
+ };
215
+ // Keep each package in the section it is already in (yarn/pnpm `add` would move devDeps).
216
+ const cmds = [false, true]
217
+ .map((dev) => [names.filter((n) => isDev(n) === dev).map((n) => `${n}@${tag}`), dev])
218
+ .filter(([list]) => list.length)
219
+ .map(([list, dev]) => add(list, dev));
220
+
221
+ log.step(`Updating plugin local deps (${pm}): ${names.map((n) => `${n}@${tag}`).join(", ")}`);
205
222
  try {
206
- execSync(cmd, { cwd: root, stdio: "inherit" });
223
+ for (const cmd of cmds) execSync(cmd, { cwd: root, stdio: "inherit" });
207
224
  log.ok("Local deps updated.");
208
225
  } catch {
209
- log.warn("Local deps update failed (offline?). Run manually:\n " + cmd);
226
+ log.warn("Local deps update failed (offline?). Run manually:\n " + cmds.join("\n "));
227
+ }
228
+ }
229
+
230
+ // ── legacy plugin-ui API ──────────────────────────────────────────────────────
231
+ // plugin-ui 1.x replaced the remote-dom components and connect() bootstrap. update can't rewrite
232
+ // UI code, so after bumping it lists what the user has to change.
233
+
234
+ const LEGACY_SUBPATHS = {
235
+ connect: "removed — drop `connect(App)` and `export default App` instead (the host renders the entry's default export)",
236
+ elements: "removed — use the RB* components from @rainbow-robotics/plugin-ui/react",
237
+ "dist-browser": "removed — import from @rainbow-robotics/plugin-ui/react",
238
+ };
239
+ const LEGACY_COMPONENTS = {
240
+ Slider: "RBSlider",
241
+ Button: "RBLabelButton",
242
+ IconButton: "RBIconButton",
243
+ Text: "a plain element or RBTextLabel",
244
+ TextInput: "RBTextInput / RBTextField",
245
+ Switch: "RBSwitch",
246
+ Select: "RBSelectBox",
247
+ };
248
+ // The clause can't hold quotes or `;`, so a match can't start at an earlier import statement.
249
+ const PLUGIN_UI_IMPORT = /import\s+([^;'"]*?)\s+from\s+["']@rainbow-robotics\/plugin-ui\/([\w-]+)["']/g;
250
+
251
+ function listUiFiles(dir) {
252
+ return readdirSync(dir, { withFileTypes: true }).flatMap((e) =>
253
+ e.isDirectory() ? listUiFiles(join(dir, e.name)) : /\.[jt]sx?$/.test(e.name) ? [join(dir, e.name)] : [],
254
+ );
255
+ }
256
+
257
+ // rb.toast takes one { message, type } object now; flag a string or multi-argument call.
258
+ const TOAST_CALL = /\.toast\(([^()]*)\)/g;
259
+
260
+ /** Uses of the pre-1.x plugin-ui API in the project (ui/ and vite.config), as { file, line, text }. */
261
+ export function findLegacyUiUsage(root) {
262
+ const uiDir = resolve(root, "ui");
263
+ if (!existsSync(uiDir)) return [];
264
+ const found = [];
265
+ // The old hand-written config builds entries without their exports (connect() rendered as a side
266
+ // effect), so `export default App` is dropped from dist. The muscat() preset keeps it.
267
+ const viteConfig = ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"].find((f) => existsSync(resolve(root, f)));
268
+ if (viteConfig && !readFileSync(resolve(root, viteConfig), "utf8").includes("@rainbow-robotics/muscat-plugin-kit/vite")) {
269
+ found.push({
270
+ file: viteConfig,
271
+ line: 1,
272
+ text: 'not using the muscat() preset — the build drops the entry\'s default export. Use: import { muscat } from "@rainbow-robotics/muscat-plugin-kit/vite"; defineConfig({ plugins: [muscat()] })',
273
+ });
274
+ }
275
+ for (const file of listUiFiles(uiDir)) {
276
+ const src = readFileSync(file, "utf8");
277
+ const rel = relative(root, file);
278
+ const lineOf = (index) => src.slice(0, index).split("\n").length;
279
+ for (const m of src.matchAll(PLUGIN_UI_IMPORT)) {
280
+ const [, clause, subpath] = m;
281
+ if (LEGACY_SUBPATHS[subpath]) {
282
+ found.push({ file: rel, line: lineOf(m.index), text: `@rainbow-robotics/plugin-ui/${subpath} ${LEGACY_SUBPATHS[subpath]}` });
283
+ } else if (subpath === "react") {
284
+ const named = clause.match(/\{([\s\S]*)\}/)?.[1] ?? "";
285
+ for (const spec of named.split(",")) {
286
+ const name = spec.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0];
287
+ if (LEGACY_COMPONENTS[name]) {
288
+ found.push({ file: rel, line: lineOf(m.index), text: `${name} → ${LEGACY_COMPONENTS[name]}` });
289
+ }
290
+ }
291
+ }
292
+ }
293
+ for (const m of src.matchAll(TOAST_CALL)) {
294
+ const arg = m[1].trim();
295
+ if (!arg.startsWith("{") && (/^["'`]/.test(arg) || arg.includes(","))) {
296
+ found.push({ file: rel, line: lineOf(m.index), text: "rb.toast(message, type) → rb.toast({ message, type })" });
297
+ }
298
+ }
299
+ // Top-level ui/ files are the entries; the host mounts their default export.
300
+ if (relative(uiDir, file).split(/[\\/]/).length === 1 && !/export\s+default\b/.test(src)) {
301
+ found.push({ file: rel, line: 1, text: "no default export — the host renders the entry's default export" });
302
+ }
210
303
  }
304
+ return found;
305
+ }
306
+
307
+ // The known-API list above only knows past changes. Type-checking against the plugin-ui that was
308
+ // just installed catches whatever a later release removes or reshapes, with no list to keep up.
309
+ function typeScriptErrors(root) {
310
+ const tsc = resolve(root, "node_modules/typescript/bin/tsc");
311
+ if (!existsSync(tsc) || !existsSync(resolve(root, "tsconfig.json"))) return [];
312
+ const runtime = isBinaryMode ? "node" : process.execPath;
313
+ const r = spawnSync(runtime, [tsc, "--noEmit", "-p", root, "--pretty", "false"], { cwd: root, encoding: "utf8" });
314
+ return `${r.stdout ?? ""}`
315
+ .split("\n")
316
+ .map((l) => /^(.+?)\((\d+),\d+\): error (TS\d+): (.*)$/.exec(l))
317
+ .filter(Boolean)
318
+ .map(([, file, line, code, text]) => ({ file: relative(root, resolve(root, file)), line: Number(line), text: `${code} ${text}` }));
319
+ }
320
+
321
+ function reportProject(root) {
322
+ const found = findLegacyUiUsage(root);
323
+ const tsErrors = typeScriptErrors(root);
324
+ if (!found.length && !tsErrors.length) return;
325
+ log.warn("This project needs changes for the updated plugin-ui. Fix these before `muscat-pk dev` / `build`:");
326
+ for (const f of found) log.info(` ${f.file}:${f.line} ${f.text}`);
327
+ if (tsErrors.length) {
328
+ log.info(" TypeScript (tsc --noEmit):");
329
+ for (const e of tsErrors) log.info(` ${e.file}:${e.line} ${e.text}`);
330
+ }
331
+ log.info(" For working examples, generate a fresh project and read its ui/content.tsx: muscat-pk new <dir>");
332
+ }
333
+
334
+ /** `muscat-pk update:check` — the post-update project check, run by the freshly installed kit. */
335
+ export async function runUpdateCheck() {
336
+ reportProject(findPluginRoot());
337
+ }
338
+
339
+ // Prefer the kit update just installed: it knows the plugin-ui it shipped with, this process may not.
340
+ function checkProjectAfterUpdate(root) {
341
+ const localBin = resolveLocalCliBin(root);
342
+ let localVersion = "";
343
+ try {
344
+ localVersion = JSON.parse(readFileSync(resolve(localBin, "../../../package.json"), "utf8")).version;
345
+ } catch { /* no local kit */ }
346
+ if (localBin && localVersion && compareKitVersions(localVersion, kitVersion()) >= 0) {
347
+ const runtime = isBinaryMode ? "node" : process.execPath;
348
+ const r = spawnSync(runtime, [localBin, "update:check"], { cwd: root, stdio: "inherit" });
349
+ if (!r.error && r.status === 0) return;
350
+ }
351
+ reportProject(root);
211
352
  }
212
353
 
213
354
  // ── main ─────────────────────────────────────────────────────────────────────
@@ -236,7 +377,7 @@ export async function runUpdate(args = []) {
236
377
 
237
378
  // 지금 도는 게 프로젝트의 로컬 설치면 그 프로젝트만 건드린다. 이 머신의 다른
238
379
  // 프로젝트들이 쓰는 글로벌 CLI까지 같이 올리려면 --global을 명시해야 한다.
239
- const runningLocal = isLocalKitInstall();
380
+ const runningLocal = isLocalKitInstall() || process.env.MUSCAT_PK_UPDATE_PROJECT_ONLY === "1";
240
381
 
241
382
  // 1. Update the CLI itself
242
383
  if (!runningLocal || opts.global) {
@@ -292,4 +433,10 @@ export async function runUpdate(args = []) {
292
433
  }
293
434
 
294
435
  updateLocalDeps(root, tag);
436
+
437
+ try {
438
+ checkProjectAfterUpdate(root);
439
+ } catch (err) {
440
+ log.warn(`Project check failed: ${err.message}`);
441
+ }
295
442
  }
package/cli/src/util.mjs CHANGED
@@ -403,6 +403,18 @@ export function isNewer(a, b) {
403
403
  return a3 > b3;
404
404
  }
405
405
 
406
+ /** Compare "x.y.z[-dev.N]" including the dev number (a release sorts after its dev builds). <0, 0, >0. */
407
+ export function compareKitVersions(a, b) {
408
+ const parse = (v) => {
409
+ const [rel, pre] = String(v).split("-");
410
+ const nums = rel.split(".").map((n) => parseInt(n, 10) || 0);
411
+ return [...nums, pre === undefined ? Infinity : parseInt(pre.split(".").pop(), 10) || 0];
412
+ };
413
+ const pa = parse(a), pb = parse(b);
414
+ for (let i = 0; i < 4; i++) if (pa[i] !== pb[i]) return pa[i] > pb[i] ? 1 : -1;
415
+ return 0;
416
+ }
417
+
406
418
  /**
407
419
  * Non-blocking check for a newer published kit. Prints an update suggestion if
408
420
  * one exists. Fails silently when offline. Honors `RB_REGISTRY` and is skipped
@@ -1 +1 @@
1
- {"generatedAt":1792687510358,"userSince":1750997484097,"hasCustomBabel":false,"hasCustomWebpack":false,"hasStaticDirs":false,"hasStorybookEslint":true,"refCount":0,"testPackages":{"@vitest/browser":"3.2.4","@vitest/coverage-v8":"3.2.4","playwright":"1.59.1","vitest":"3.2.4"},"hasRouterPackage":false,"monorepo":"Turborepo","packageManager":{"type":"yarn","version":"berry","agent":"yarn@berry"},"preview":{"usesGlobals":false},"framework":{"name":"@storybook/react-vite","options":{}},"builder":"@storybook/builder-vite","renderer":"@storybook/react","portableStoriesFileCount":0,"applicationFileCount":0,"storybookVersion":"8.6.18","storybookVersionSpecifier":"^8.6.4","language":"typescript","storybookPackages":{"@chromatic-com/storybook":{"version":"3.2.7"},"@storybook/addon-essentials":{"version":"8.6.18"},"@storybook/addon-links":{"version":"8.6.18"},"@storybook/addon-onboarding":{"version":"8.6.18"},"@storybook/addon-themes":{"version":"8.6.18"},"@storybook/blocks":{"version":"8.6.18"},"@storybook/experimental-addon-test":{"version":"8.6.18"},"@storybook/react":{"version":"8.6.18"},"@storybook/react-vite":{"version":"8.6.18"},"@storybook/test":{"version":"8.6.18"},"@storybook/theming":{"version":"8.6.18"},"eslint-plugin-storybook":{"version":"0.11.6"},"storybook":{"version":"8.6.18"},"storybook-addon-react-docgen":{"version":"1.2.44"}},"addons":{"$SNIP/node_modules/@storybook/addon-links":{"version":null},"$SNIP/node_modules/@storybook/addon-essentials":{"version":null},"$SNIP/node_modules/@chromatic-com/storybook":{"version":null},"$SNIP/node_modules/@storybook/addon-themes":{"version":null},"$SNIP/node_modules/@storybook/experimental-addon-test":{"version":null}}}
1
+ {"generatedAt":1792689973585,"userSince":1750997484097,"hasCustomBabel":false,"hasCustomWebpack":false,"hasStaticDirs":false,"hasStorybookEslint":true,"refCount":0,"testPackages":{"@vitest/browser":"3.2.4","@vitest/coverage-v8":"3.2.4","playwright":"1.59.1","vitest":"3.2.4"},"hasRouterPackage":false,"monorepo":"Turborepo","packageManager":{"type":"yarn","version":"berry","agent":"yarn@berry"},"preview":{"usesGlobals":false},"framework":{"name":"@storybook/react-vite","options":{}},"builder":"@storybook/builder-vite","renderer":"@storybook/react","portableStoriesFileCount":0,"applicationFileCount":0,"storybookVersion":"8.6.18","storybookVersionSpecifier":"^8.6.4","language":"typescript","storybookPackages":{"@chromatic-com/storybook":{"version":"3.2.7"},"@storybook/addon-essentials":{"version":"8.6.18"},"@storybook/addon-links":{"version":"8.6.18"},"@storybook/addon-onboarding":{"version":"8.6.18"},"@storybook/addon-themes":{"version":"8.6.18"},"@storybook/blocks":{"version":"8.6.18"},"@storybook/experimental-addon-test":{"version":"8.6.18"},"@storybook/react":{"version":"8.6.18"},"@storybook/react-vite":{"version":"8.6.18"},"@storybook/test":{"version":"8.6.18"},"@storybook/theming":{"version":"8.6.18"},"eslint-plugin-storybook":{"version":"0.11.6"},"storybook":{"version":"8.6.18"},"storybook-addon-react-docgen":{"version":"1.2.44"}},"addons":{"$SNIP/node_modules/@storybook/addon-links":{"version":null},"$SNIP/node_modules/@storybook/addon-essentials":{"version":null},"$SNIP/node_modules/@chromatic-com/storybook":{"version":null},"$SNIP/node_modules/@storybook/addon-themes":{"version":null},"$SNIP/node_modules/@storybook/experimental-addon-test":{"version":null}}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rainbow-robotics/muscat-plugin-kit",
3
- "version": "1.10.2-dev.0",
3
+ "version": "1.10.2-dev.2",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=20"
@@ -52,8 +52,8 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@modelcontextprotocol/sdk": "^1.29.0",
55
- "@rainbow-robotics/plugin-ui": "1.10.2-dev.0",
56
- "@rainbow-robotics/preview-host": "1.10.2-dev.0",
55
+ "@rainbow-robotics/plugin-ui": "1.10.2-dev.2",
56
+ "@rainbow-robotics/preview-host": "1.10.2-dev.2",
57
57
  "@types/react": "^19.0.10",
58
58
  "postcss": "^8.5.8",
59
59
  "typescript": "^5.8.0",