@rainbow-robotics/muscat-plugin-kit 1.10.2-dev.1 → 1.10.2-dev.3

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.
@@ -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
  );
@@ -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":
@@ -29,6 +29,33 @@ const UI_RULES = [
29
29
  "Colors and type come from tokens (bg-bg-*, text-fg-*, typo-*), not hex values or arbitrary Tailwind colors.",
30
30
  ];
31
31
 
32
+ // plugin-ui 1.x replaced the remote-dom components and the connect() bootstrap. The project check
33
+ // (update, MCP check_project) flags these; the migration rules below tell an agent how to fix them.
34
+ export const LEGACY_SUBPATHS = {
35
+ connect: "removed — drop `connect(App)` and `export default App` instead (the host renders the entry's default export)",
36
+ elements: "removed — use the RB* components from @rainbow-robotics/plugin-ui/react",
37
+ "dist-browser": "removed — import from @rainbow-robotics/plugin-ui/react",
38
+ };
39
+ export const LEGACY_COMPONENTS = {
40
+ Slider: "RBSlider",
41
+ Button: "RBLabelButton",
42
+ IconButton: "RBIconButton",
43
+ Text: "a plain element or RBTextLabel",
44
+ TextInput: "RBTextInput / RBTextField",
45
+ Switch: "RBSwitch",
46
+ Select: "RBSelectBox",
47
+ };
48
+ const MIGRATION_RULES = [
49
+ "After `muscat-pk update`, or when the UI fails to build or the preview says \"no default export\", call check_project and fix every item it lists. Call it again until it reports no issues.",
50
+ "Each top-level ui/*.tsx is an entry and ends in `export default App`. The old `connect(App)` bootstrap and @rainbow-robotics/plugin-ui/connect are gone.",
51
+ "vite.config.ts uses the muscat() preset (`plugins: [muscat()]` from @rainbow-robotics/muscat-plugin-kit/vite); keep any other plugins the project adds. A hand-written config drops the entry's default export from the build.",
52
+ "rb.toast takes one object: rb.toast({ message, type }).",
53
+ `Old components → new: ${Object.entries(LEGACY_COMPONENTS).map(([o, n]) => `${o} → ${n}`).join(", ")}. Their props differ (no event.detail) — call get_component_example for the new component before rewriting.`,
54
+ ];
55
+
56
+ // Claude Code reads this to start the MCP server; `muscat-pk` hands off to the project's local kit.
57
+ export const MCP_JSON = `${JSON.stringify({ mcpServers: { muscat: { command: "muscat-pk", args: ["mcp"] } } }, null, 2)}\n`;
58
+
32
59
  const nativeTable = () => NATIVE_TO_RB.map(([el, rb]) => `- ${el}: ${rb}`).join("\n");
33
60
 
34
61
  export const SERVER_INSTRUCTIONS = `Muscat plugin UI (ui/*.tsx) is built only from @rainbow-robotics/plugin-ui/react, styled with the design-token Tailwind classes (list_design_tokens).
@@ -39,6 +66,9 @@ ${UI_RULES.map((r) => `- ${r}`).join("\n")}
39
66
  Native element -> plugin-ui component:
40
67
  ${nativeTable()}
41
68
 
69
+ Upgrading a project made with an older kit:
70
+ ${MIGRATION_RULES.map((r) => `- ${r}`).join("\n")}
71
+
42
72
  Backend, manifest and SDK questions: search_rb_sdk, list_rb_sdk, get_rb_sdk_api.`;
43
73
 
44
74
  // AGENTS.md (Codex, Cursor, Gemini CLI, Copilot …). CLAUDE.md imports it with `@AGENTS.md`.
@@ -64,6 +94,13 @@ ${UI_RULES.map((r) => `- ${r}`).join("\n")}
64
94
  Native element → plugin-ui component:
65
95
 
66
96
  ${nativeTable()}
97
+
98
+ ## Upgrading from an older kit
99
+
100
+ \`muscat-pk update\` bumps the kit, plugin-ui and preview-host and lists what the project must change.
101
+ The MCP tool \`check_project\` runs the same check (known old APIs + \`tsc --noEmit\`).
102
+
103
+ ${MIGRATION_RULES.map((r) => `- ${r}`).join("\n")}
67
104
  `;
68
105
  }
69
106
 
package/cli/src/mcp.mjs CHANGED
@@ -43,6 +43,7 @@ import { PYTHON_DEV_IMAGE } from "./python.mjs";
43
43
  import { SHIPPED_STORIES_SRC, SHIPPED_DESIGN_TOKENS } from "./storybook.mjs";
44
44
  import { CPP_DEV_IMAGE } from "./cpp.mjs";
45
45
  import { NATIVE_TO_RB, SERVER_INSTRUCTIONS } from "./ai-rules.mjs";
46
+ import { checkProject, formatProjectCheck } from "./projectCheck.mjs";
46
47
 
47
48
  const TEMPLATE_READMES = { python: "common/README.md", cpp: "readme-cpp/README.md" };
48
49
  // Design tokens: prefer the shipped copy, fall back to the monorepo source.
@@ -1090,6 +1091,35 @@ print(json.dumps(res))
1090
1091
  },
1091
1092
  );
1092
1093
 
1094
+ server.registerTool(
1095
+ "check_project",
1096
+ {
1097
+ title: "Check the plugin project for changes the installed plugin-ui needs",
1098
+ description:
1099
+ "List what the plugin project must change to work with the plugin-ui it has installed: old APIs " +
1100
+ "(connect(App), Slider/Text/Switch…, rb.toast(message, type), a vite.config without the muscat() " +
1101
+ "preset, entries without a default export) and `tsc --noEmit` errors, each as file:line. Same check " +
1102
+ "as `muscat-pk update`. Call it after an update or when the UI fails to build, fix each item " +
1103
+ "(get_component_example for replacement components), and call it again until it reports no issues.",
1104
+ inputSchema: {
1105
+ path: z.string().optional().describe("Plugin project directory (default: the server's working directory)"),
1106
+ },
1107
+ },
1108
+ async ({ path }) => {
1109
+ let root;
1110
+ try {
1111
+ root = findPluginRoot(path ? resolve(path) : process.cwd());
1112
+ } catch (err) {
1113
+ return { isError: true, content: [{ type: "text", text: err.message }] };
1114
+ }
1115
+ const lines = formatProjectCheck(checkProject(root));
1116
+ const text = lines.length
1117
+ ? `${root}: ${lines.filter((l) => /:\d+ {2}/.test(l)).length} item(s) to fix\n${lines.join("\n")}`
1118
+ : `${root}: no issues — the project matches the installed plugin-ui.`;
1119
+ return { content: [{ type: "text", text }] };
1120
+ },
1121
+ );
1122
+
1093
1123
  const transport = new StdioServerTransport();
1094
1124
  await server.connect(transport);
1095
1125
  }
package/cli/src/new.mjs CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  } from "node:fs";
14
14
  import { basename, dirname, resolve } from "node:path";
15
15
  import { color, ensureIdeVenv, hasBin, kitVersion, parseArgs, prompt, templatesDir, log } from "./util.mjs";
16
- import { agentsMd, CLAUDE_MD } from "./ai-rules.mjs";
16
+ import { agentsMd, CLAUDE_MD, MCP_JSON } from "./ai-rules.mjs";
17
17
 
18
18
  // Like cpSync(src, dst, {recursive:true}) but reads from the embedded snapshot
19
19
  // when the templates dir doesn't exist on disk (compiled binary mode).
@@ -281,6 +281,7 @@ export async function runNew(args) {
281
281
  // Rules for AI agents, generated from the same source as the MCP server's instructions.
282
282
  writeFileSync(resolve(target, "AGENTS.md"), agentsMd({ pluginName }));
283
283
  writeFileSync(resolve(target, "CLAUDE.md"), CLAUDE_MD);
284
+ writeFileSync(resolve(target, ".mcp.json"), MCP_JSON);
284
285
 
285
286
  if (!opts["no-install"]) {
286
287
  try {
@@ -0,0 +1,102 @@
1
+ // What a project must change for the plugin-ui it has installed: known old APIs (ai-rules.mjs) plus
2
+ // `tsc --noEmit`. Shared by `muscat-pk update` and the MCP tool check_project, so a user and an
3
+ // agent see the same list.
4
+ import { spawnSync } from "node:child_process";
5
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
6
+ import { join, relative, resolve } from "node:path";
7
+ import { LEGACY_COMPONENTS, LEGACY_SUBPATHS } from "./ai-rules.mjs";
8
+
9
+ const isBinaryMode = typeof Bun !== "undefined";
10
+
11
+ // The clause can't hold quotes or `;`, so a match can't start at an earlier import statement.
12
+ const PLUGIN_UI_IMPORT = /import\s+([^;'"]*?)\s+from\s+["']@rainbow-robotics\/plugin-ui\/([\w-]+)["']/g;
13
+
14
+ function listUiFiles(dir) {
15
+ return readdirSync(dir, { withFileTypes: true }).flatMap((e) =>
16
+ e.isDirectory() ? listUiFiles(join(dir, e.name)) : /\.[jt]sx?$/.test(e.name) ? [join(dir, e.name)] : [],
17
+ );
18
+ }
19
+
20
+ // rb.toast takes one { message, type } object now; flag a string or multi-argument call.
21
+ const TOAST_CALL = /\.toast\(([^()]*)\)/g;
22
+
23
+ /** Uses of the pre-1.x plugin-ui API in the project (ui/ and vite.config), as { file, line, text }. */
24
+ function findLegacyUiUsage(root) {
25
+ const uiDir = resolve(root, "ui");
26
+ if (!existsSync(uiDir)) return [];
27
+ const found = [];
28
+ // The old hand-written config builds entries without their exports (connect() rendered as a side
29
+ // effect), so `export default App` is dropped from dist. The muscat() preset keeps it.
30
+ const viteConfig = ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"].find((f) => existsSync(resolve(root, f)));
31
+ if (viteConfig && !readFileSync(resolve(root, viteConfig), "utf8").includes("@rainbow-robotics/muscat-plugin-kit/vite")) {
32
+ found.push({
33
+ file: viteConfig,
34
+ line: 1,
35
+ 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()] })',
36
+ });
37
+ }
38
+ for (const file of listUiFiles(uiDir)) {
39
+ const src = readFileSync(file, "utf8");
40
+ const rel = relative(root, file);
41
+ const lineOf = (index) => src.slice(0, index).split("\n").length;
42
+ for (const m of src.matchAll(PLUGIN_UI_IMPORT)) {
43
+ const [, clause, subpath] = m;
44
+ if (LEGACY_SUBPATHS[subpath]) {
45
+ found.push({ file: rel, line: lineOf(m.index), text: `@rainbow-robotics/plugin-ui/${subpath} ${LEGACY_SUBPATHS[subpath]}` });
46
+ } else if (subpath === "react") {
47
+ const named = clause.match(/\{([\s\S]*)\}/)?.[1] ?? "";
48
+ for (const spec of named.split(",")) {
49
+ const name = spec.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0];
50
+ if (LEGACY_COMPONENTS[name]) {
51
+ found.push({ file: rel, line: lineOf(m.index), text: `${name} → ${LEGACY_COMPONENTS[name]}` });
52
+ }
53
+ }
54
+ }
55
+ }
56
+ for (const m of src.matchAll(TOAST_CALL)) {
57
+ const arg = m[1].trim();
58
+ if (!arg.startsWith("{") && (/^["'`]/.test(arg) || arg.includes(","))) {
59
+ found.push({ file: rel, line: lineOf(m.index), text: "rb.toast(message, type) → rb.toast({ message, type })" });
60
+ }
61
+ }
62
+ // Top-level ui/ files are the entries; the host mounts their default export.
63
+ if (relative(uiDir, file).split(/[\\/]/).length === 1 && !/export\s+default\b/.test(src)) {
64
+ found.push({ file: rel, line: 1, text: "no default export — the host renders the entry's default export" });
65
+ }
66
+ }
67
+ return found;
68
+ }
69
+
70
+ // The known-API list above only knows past changes. Type-checking against the plugin-ui that was
71
+ // just installed catches whatever a later release removes or reshapes, with no list to keep up.
72
+ function typeScriptErrors(root) {
73
+ const tsc = resolve(root, "node_modules/typescript/bin/tsc");
74
+ if (!existsSync(tsc) || !existsSync(resolve(root, "tsconfig.json"))) return [];
75
+ const runtime = isBinaryMode ? "node" : process.execPath;
76
+ const r = spawnSync(runtime, [tsc, "--noEmit", "-p", root, "--pretty", "false"], { cwd: root, encoding: "utf8" });
77
+ return `${r.stdout ?? ""}`
78
+ .split("\n")
79
+ .map((l) => /^(.+?)\((\d+),\d+\): error (TS\d+): (.*)$/.exec(l))
80
+ .filter(Boolean)
81
+ .map(([, file, line, code, text]) => ({ file: relative(root, resolve(root, file)), line: Number(line), text: `${code} ${text}` }));
82
+ }
83
+
84
+ /** Everything the project must change, as { file, line, text, source: "known" | "tsc" }. */
85
+ export function checkProject(root) {
86
+ return [
87
+ ...findLegacyUiUsage(root).map((f) => ({ ...f, source: "known" })),
88
+ ...typeScriptErrors(root).map((e) => ({ ...e, source: "tsc" })),
89
+ ];
90
+ }
91
+
92
+ /** Plain-text lines for a checkProject() result (empty when there is nothing to fix). */
93
+ export function formatProjectCheck(items) {
94
+ if (!items.length) return [];
95
+ const line = (i) => ` ${i.file}:${i.line} ${i.text}`;
96
+ const tsc = items.filter((i) => i.source === "tsc");
97
+ return [
98
+ ...items.filter((i) => i.source !== "tsc").map(line),
99
+ ...(tsc.length ? [" TypeScript (tsc --noEmit):", ...tsc.map(line)] : []),
100
+ " For working examples, generate a fresh project and read its ui/content.tsx: muscat-pk new <dir>",
101
+ ];
102
+ }
@@ -11,9 +11,14 @@
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, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
15
- import { join, relative, resolve } from "node:path";
16
- import { findPluginRoot, isLocalKitInstall, KIT_PUBLISHED_NAME, kitVersion, log, parseArgs, prompt } from "./util.mjs";
14
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
15
+ import { join, resolve } from "node:path";
16
+ import { agentsMd, CLAUDE_MD, MCP_JSON } from "./ai-rules.mjs";
17
+ import { checkProject, formatProjectCheck } from "./projectCheck.mjs";
18
+ import {
19
+ compareKitVersions, findPluginRoot, isLocalKitInstall, KIT_PUBLISHED_NAME, kitVersion, log, parseArgs, prompt,
20
+ resolveLocalCliBin,
21
+ } from "./util.mjs";
17
22
 
18
23
  const PLUGIN_UI_PACKAGE = "@rainbow-robotics/plugin-ui";
19
24
  const PREVIEW_HOST_PACKAGE = "@rainbow-robotics/preview-host";
@@ -224,90 +229,59 @@ function updateLocalDeps(root, tag) {
224
229
  }
225
230
  }
226
231
 
227
- // ── legacy plugin-ui API ──────────────────────────────────────────────────────
228
- // plugin-ui 1.x replaced the remote-dom components and connect() bootstrap. update can't rewrite
229
- // UI code, so after bumping it lists what the user has to change.
230
-
231
- const LEGACY_SUBPATHS = {
232
- connect: "removed — drop `connect(App)` and `export default App` instead (the host renders the entry's default export)",
233
- elements: "removed — use the RB* components from @rainbow-robotics/plugin-ui/react",
234
- "dist-browser": "removed — import from @rainbow-robotics/plugin-ui/react",
235
- };
236
- const LEGACY_COMPONENTS = {
237
- Slider: "RBSlider",
238
- Button: "RBLabelButton",
239
- IconButton: "RBIconButton",
240
- Text: "a plain element or RBTextLabel",
241
- TextInput: "RBTextInput / RBTextField",
242
- Switch: "RBSwitch",
243
- Select: "RBSelectBox",
244
- };
245
- // The clause can't hold quotes or `;`, so a match can't start at an earlier import statement.
246
- const PLUGIN_UI_IMPORT = /import\s+([^;'"]*?)\s+from\s+["']@rainbow-robotics\/plugin-ui\/([\w-]+)["']/g;
247
-
248
- function listUiFiles(dir) {
249
- return readdirSync(dir, { withFileTypes: true }).flatMap((e) =>
250
- e.isDirectory() ? listUiFiles(join(dir, e.name)) : /\.[jt]sx?$/.test(e.name) ? [join(dir, e.name)] : [],
251
- );
232
+ const PROJECT_CHECK_HEADER = "This project needs changes for the updated plugin-ui. Fix these before `muscat-pk dev` / `build`:";
233
+
234
+ function reportProject(root) {
235
+ const lines = formatProjectCheck(checkProject(root));
236
+ if (!lines.length) return;
237
+ log.warn(PROJECT_CHECK_HEADER);
238
+ for (const l of lines) log.info(l);
252
239
  }
253
240
 
254
- // rb.toast takes one { message, type } object now; flag a string or multi-argument call.
255
- const TOAST_CALL = /\.toast\(([^()]*)\)/g;
256
-
257
- /** Uses of the pre-1.x plugin-ui API in the project (ui/ and vite.config), as { file, line, text }. */
258
- export function findLegacyUiUsage(root) {
259
- const uiDir = resolve(root, "ui");
260
- if (!existsSync(uiDir)) return [];
261
- const found = [];
262
- // The old hand-written config builds entries without their exports (connect() rendered as a side
263
- // effect), so `export default App` is dropped from dist. The muscat() preset keeps it.
264
- const viteConfig = ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"].find((f) => existsSync(resolve(root, f)));
265
- if (viteConfig && !readFileSync(resolve(root, viteConfig), "utf8").includes("@rainbow-robotics/muscat-plugin-kit/vite")) {
266
- found.push({
267
- file: viteConfig,
268
- line: 1,
269
- 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()] })',
270
- });
271
- }
272
- for (const file of listUiFiles(uiDir)) {
273
- const src = readFileSync(file, "utf8");
274
- const rel = relative(root, file);
275
- const lineOf = (index) => src.slice(0, index).split("\n").length;
276
- for (const m of src.matchAll(PLUGIN_UI_IMPORT)) {
277
- const [, clause, subpath] = m;
278
- if (LEGACY_SUBPATHS[subpath]) {
279
- found.push({ file: rel, line: lineOf(m.index), text: `@rainbow-robotics/plugin-ui/${subpath} ${LEGACY_SUBPATHS[subpath]}` });
280
- } else if (subpath === "react") {
281
- const named = clause.match(/\{([\s\S]*)\}/)?.[1] ?? "";
282
- for (const spec of named.split(",")) {
283
- const name = spec.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0];
284
- if (LEGACY_COMPONENTS[name]) {
285
- found.push({ file: rel, line: lineOf(m.index), text: `${name} → ${LEGACY_COMPONENTS[name]}` });
286
- }
287
- }
288
- }
289
- }
290
- for (const m of src.matchAll(TOAST_CALL)) {
291
- const arg = m[1].trim();
292
- if (!arg.startsWith("{") && (/^["'`]/.test(arg) || arg.includes(","))) {
293
- found.push({ file: rel, line: lineOf(m.index), text: "rb.toast(message, type) → rb.toast({ message, type })" });
294
- }
295
- }
296
- // Top-level ui/ files are the entries; the host mounts their default export.
297
- if (relative(uiDir, file).split(/[\\/]/).length === 1 && !/export\s+default\b/.test(src)) {
298
- found.push({ file: rel, line: 1, text: "no default export — the host renders the entry's default export" });
299
- }
241
+ /** `muscat-pk update:check` — the post-update project check, run by the freshly installed kit. */
242
+ export async function runUpdateCheck() {
243
+ reportProject(findPluginRoot());
244
+ }
245
+
246
+ // Prefer the kit update just installed: it knows the plugin-ui it shipped with, this process may not.
247
+ function checkProjectAfterUpdate(root) {
248
+ const localBin = resolveLocalCliBin(root);
249
+ let localVersion = "";
250
+ try {
251
+ localVersion = JSON.parse(readFileSync(resolve(localBin, "../../../package.json"), "utf8")).version;
252
+ } catch { /* no local kit */ }
253
+ if (localBin && localVersion && compareKitVersions(localVersion, kitVersion()) >= 0) {
254
+ const runtime = isBinaryMode ? "node" : process.execPath;
255
+ const r = spawnSync(runtime, [localBin, "update:check"], { cwd: root, stdio: "inherit" });
256
+ if (!r.error && r.status === 0) return;
300
257
  }
301
- return found;
258
+ reportProject(root);
302
259
  }
303
260
 
304
- function reportLegacyUi(root) {
305
- const found = findLegacyUiUsage(root);
306
- if (!found.length) return;
307
- log.warn("This project still uses the old plugin-ui API (removed in plugin-ui 1.x). Fix these before `muscat-pk dev` / `build`:");
308
- for (const f of found) log.info(` ${f.file}:${f.line} ${f.text}`);
309
- log.info(" Props changed too (e.g. RBSlider onValueChange(values[]), RBSwitch onCheckedChange(bool) instead of event.detail).");
310
- log.info(" For working examples, generate a fresh project and read its ui/content.tsx: muscat-pk new <dir>");
261
+ // ── AI agent files ────────────────────────────────────────────────────────────
262
+ // Projects made before `new` scaffolded these have no MCP server or agent rules, so an agent asked
263
+ // to fix the update can't call check_project. Only missing files are written; edited ones stay.
264
+
265
+ function pluginNameOf(root) {
266
+ try {
267
+ const { name } = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8"));
268
+ if (typeof name === "string") return name;
269
+ if (name && typeof name === "object") return name.en ?? Object.values(name)[0] ?? "";
270
+ } catch { /* fall through */ }
271
+ return "";
272
+ }
273
+
274
+ function ensureAiFiles(root) {
275
+ const files = {
276
+ ".mcp.json": () => MCP_JSON,
277
+ "AGENTS.md": () => agentsMd({ pluginName: pluginNameOf(root) || "Plugin" }),
278
+ "CLAUDE.md": () => CLAUDE_MD,
279
+ };
280
+ for (const [file, content] of Object.entries(files)) {
281
+ if (existsSync(resolve(root, file))) continue;
282
+ writeFileSync(resolve(root, file), content());
283
+ log.ok(`${file} added (MCP server / AI agent rules)`);
284
+ }
311
285
  }
312
286
 
313
287
  // ── main ─────────────────────────────────────────────────────────────────────
@@ -336,7 +310,7 @@ export async function runUpdate(args = []) {
336
310
 
337
311
  // 지금 도는 게 프로젝트의 로컬 설치면 그 프로젝트만 건드린다. 이 머신의 다른
338
312
  // 프로젝트들이 쓰는 글로벌 CLI까지 같이 올리려면 --global을 명시해야 한다.
339
- const runningLocal = isLocalKitInstall();
313
+ const runningLocal = isLocalKitInstall() || process.env.MUSCAT_PK_UPDATE_PROJECT_ONLY === "1";
340
314
 
341
315
  // 1. Update the CLI itself
342
316
  if (!runningLocal || opts.global) {
@@ -391,11 +365,17 @@ export async function runUpdate(args = []) {
391
365
  log.warn(`Structure check failed: ${err.message}`);
392
366
  }
393
367
 
368
+ try {
369
+ ensureAiFiles(root);
370
+ } catch (err) {
371
+ log.warn(`AI agent files: ${err.message}`);
372
+ }
373
+
394
374
  updateLocalDeps(root, tag);
395
375
 
396
376
  try {
397
- reportLegacyUi(root);
377
+ checkProjectAfterUpdate(root);
398
378
  } catch (err) {
399
- log.warn(`Legacy UI check failed: ${err.message}`);
379
+ log.warn(`Project check failed: ${err.message}`);
400
380
  }
401
381
  }
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":1792689204059,"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":1792691105012,"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}}}
@@ -1132,6 +1132,10 @@ letting it guess. `muscat-pk mcp` is an MCP server exposing:
1132
1132
  backend) classes and signatures; with `lang: "cpp"`, the C++ SDK headers
1133
1133
  (`rainbow-rb-sdk-dev`, read out of the `muscat-pk` cpp dev image — build it
1134
1134
  once with `muscat-pk dev:back`).
1135
+ - `check_project` — what this project must change for the plugin-ui it has
1136
+ installed (old APIs such as `connect(App)` or `Slider`, a `vite.config` without
1137
+ the `muscat()` preset, `tsc` errors), the same list `muscat-pk update` prints.
1138
+ Ask the assistant to fix the project after an update; it re-checks until clean.
1135
1139
 
1136
1140
  **Browse the components live** with `muscat-pk storybook`.
1137
1141
 
@@ -898,6 +898,10 @@ letting it guess. `muscat-pk mcp` is an MCP server exposing:
898
898
  backend) classes and signatures; with `lang: "cpp"`, the C++ SDK headers
899
899
  (`rainbow-rb-sdk-dev`, read out of the `muscat-pk` cpp dev image — build it
900
900
  once with `muscat-pk dev:back`).
901
+ - `check_project` — what this project must change for the plugin-ui it has
902
+ installed (old APIs such as `connect(App)` or `Slider`, a `vite.config` without
903
+ the `muscat()` preset, `tsc` errors), the same list `muscat-pk update` prints.
904
+ Ask the assistant to fix the project after an update; it re-checks until clean.
901
905
 
902
906
  **Browse the components live** with `muscat-pk storybook`.
903
907
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rainbow-robotics/muscat-plugin-kit",
3
- "version": "1.10.2-dev.1",
3
+ "version": "1.10.2-dev.3",
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.1",
56
- "@rainbow-robotics/preview-host": "1.10.2-dev.1",
55
+ "@rainbow-robotics/plugin-ui": "1.10.2-dev.3",
56
+ "@rainbow-robotics/preview-host": "1.10.2-dev.3",
57
57
  "@types/react": "^19.0.10",
58
58
  "postcss": "^8.5.8",
59
59
  "typescript": "^5.8.0",
@@ -1,8 +0,0 @@
1
- {
2
- "mcpServers": {
3
- "muscat": {
4
- "command": "muscat-pk",
5
- "args": ["mcp"]
6
- }
7
- }
8
- }