@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.15 → 0.1.2-dev.17

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/src/build.mjs CHANGED
@@ -147,7 +147,7 @@ const HOST_PROVIDED_EXTERNALS = [
147
147
 
148
148
  export async function buildPlugin({ root, minify = false } = {}) {
149
149
  const pluginRoot = root ?? findPluginRoot();
150
- const entry = resolve(pluginRoot, "src/content.tsx");
150
+ const entry = resolve(pluginRoot, "ui/content.tsx");
151
151
  if (!existsSync(entry)) {
152
152
  throw new Error(`missing entry: ${entry}`);
153
153
  }
@@ -155,7 +155,7 @@ export async function buildPlugin({ root, minify = false } = {}) {
155
155
  rmSync(distDir, { recursive: true, force: true });
156
156
  mkdirSync(distDir, { recursive: true });
157
157
 
158
- log.step("bundling src/content.tsx → dist/content.js");
158
+ log.step("bundling ui/content.tsx → dist/content.js");
159
159
  await buildWithFallback({
160
160
  entryPoints: [entry],
161
161
  outfile: resolve(distDir, "content.js"),
@@ -176,11 +176,22 @@ export async function buildPlugin({ root, minify = false } = {}) {
176
176
  // Generate sandbox.html — plugin authors don't manage this file.
177
177
  writeFileSync(resolve(distDir, "sandbox.html"), SANDBOX_HTML, "utf8");
178
178
  log.step("generated dist/sandbox.html");
179
+
179
180
  const assetsSrc = resolve(pluginRoot, "assets");
180
181
  if (existsSync(assetsSrc)) {
181
182
  cpSync(assetsSrc, resolve(distDir, "assets"), { recursive: true });
182
183
  }
183
184
 
185
+ // Backend runtime files — copy entire py/ into dist/, then requirements.txt from root.
186
+ const pySrc = resolve(pluginRoot, "py");
187
+ if (existsSync(pySrc)) {
188
+ cpSync(pySrc, distDir, { recursive: true });
189
+ }
190
+ const rootReqs = resolve(pluginRoot, "requirements.txt");
191
+ if (existsSync(rootReqs)) {
192
+ copyFileSync(rootReqs, resolve(distDir, "requirements.txt"));
193
+ }
194
+
184
195
  log.ok(`built → ${distDir}`);
185
196
  return { pluginRoot, distDir };
186
197
  }
@@ -31,6 +31,7 @@ function manifestRuntime(root) {
31
31
  function runPython(root) {
32
32
  const venv = resolve(root, ".venv");
33
33
  const py = venvPython(venv);
34
+ const pyDir = resolve(root, "py");
34
35
 
35
36
  if (!existsSync(py)) {
36
37
  log.step("creating .venv…");
@@ -43,7 +44,7 @@ function runPython(root) {
43
44
  if (!is312) log.warn("python3.12 not found; using fallback (robot targets 3.12).");
44
45
  spawnSync(cmd[0], [...cmd.slice(1), "-m", "venv", venv], { cwd: root, stdio: "inherit" });
45
46
  } else {
46
- throw new Error("Python not found (uv / conda / python3 none available). Install Python 3.12 to run the backend.");
47
+ throw new Error("Python not found (uv / conda / python3 none available). Install Python 3.12 to run the py.");
47
48
  }
48
49
  }
49
50
 
@@ -53,9 +54,6 @@ function runPython(root) {
53
54
  spawnSync(py, ["-m", "pip", "install", "-q", "-r", reqs], { cwd: root, stdio: "inherit" });
54
55
  }
55
56
 
56
- // watchfiles = uvicorn's reloader. --filter python watches every *.py in the
57
- // project (and ignores .venv/__pycache__/node_modules), so interdependent
58
- // modules trigger reloads — not just main.py.
59
57
  log.step("pip install -q watchfiles (dev reloader)…");
60
58
  spawnSync(py, ["-m", "pip", "install", "-q", "watchfiles"], { cwd: root, stdio: "inherit" });
61
59
 
@@ -63,10 +61,10 @@ function runPython(root) {
63
61
  const backEnv = { ...process.env, ...(pluginId ? { RB_PLUGIN_ID: pluginId } : {}) };
64
62
  if (pluginId) log.step(`RB_PLUGIN_ID=${pluginId}`);
65
63
 
66
- log.step("watchfiles → python main.py (reload on any .py change)");
64
+ log.step("watchfiles → python py/main.py (reload on any .py change in py/)");
67
65
  const child = spawn(
68
66
  py,
69
- ["-m", "watchfiles", "--filter", "python", "--target-type", "command", "python main.py", "."],
67
+ ["-m", "watchfiles", "--filter", "python", "--target-type", "command", "python py/main.py", "py"],
70
68
  { cwd: root, stdio: "inherit", env: backEnv },
71
69
  );
72
70
  const cleanup = () => { if (!child.killed) child.kill(); };
package/cli/src/pack.mjs CHANGED
@@ -29,11 +29,8 @@ export async function runPack() {
29
29
  const zipPath = resolve(pluginRoot, zipName);
30
30
  rmSync(zipPath, { force: true });
31
31
 
32
- // Contents: dist/ + back-end files if present.
32
+ // dist/ already contains main.py, requirements.txt, manifest.json, assets/.
33
33
  const entries = ["dist"];
34
- for (const f of ["main.py", "requirements.txt"]) {
35
- if (existsSync(resolve(pluginRoot, f))) entries.push(f);
36
- }
37
34
  if (existsSync(resolve(pluginRoot, "ros2_ws/install"))) {
38
35
  entries.push("ros2_ws/install");
39
36
  }
@@ -8,9 +8,11 @@
8
8
  // local : plugin project's node_modules (fallback)
9
9
  // → <pm> add @rainbow-robotics/muscat-plugin-kit@<tag>
10
10
  //
11
- // After updating the CLI itself, also bumps the plugin project's local deps.
11
+ // After updating the CLI itself, also bumps the plugin project's local deps
12
+ // and migrates the project structure if it's outdated (src/ → ui/, root main.py → py/).
13
+ import { createInterface } from "node:readline";
12
14
  import { execSync, spawn } from "node:child_process";
13
- import { chmodSync, copyFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
15
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
14
16
  import { tmpdir } from "node:os";
15
17
  import { join, resolve } from "node:path";
16
18
  import { findPluginRoot, KIT_PUBLISHED_NAME, kitVersion, log, parseArgs } from "./util.mjs";
@@ -65,13 +67,13 @@ async function updateBinary(tag) {
65
67
  const version = await resolveS3Version(tag);
66
68
  const current = kitVersion();
67
69
  if (version === current) {
68
- log.ok(`이미 최신 버전입니다 (v${current})`);
70
+ log.ok(`Already up to date (v${current})`);
69
71
  return;
70
72
  }
71
73
 
72
74
  const binaryName = s3BinaryName();
73
75
  const url = `${S3_BASE}/v${version}/${binaryName}`;
74
- log.step(`바이너리 다운로드: v${current} → v${version}`);
76
+ log.step(`Downloading binary: v${current} → v${version}`);
75
77
 
76
78
  const data = await fetchBinary(url);
77
79
  const execPath = process.execPath;
@@ -88,14 +90,14 @@ async function updateBinary(tag) {
88
90
  stdio: "ignore",
89
91
  }).unref();
90
92
 
91
- log.ok(`v${version} 다운로드 완료. 다음 실행 시 자동 적용됩니다.`);
93
+ log.ok(`v${version} downloaded. Will be applied on next run.`);
92
94
  } else {
93
95
  // macOS / Linux: can replace the file while it's mapped into memory
94
96
  const tmp = join(tmpdir(), `muscat-pk-${version}`);
95
97
  writeFileSync(tmp, data);
96
98
  chmodSync(tmp, 0o755);
97
99
  copyFileSync(tmp, execPath);
98
- log.ok(`v${version} 업데이트 완료.`);
100
+ log.ok(`Updated to v${version}.`);
99
101
  }
100
102
  }
101
103
 
@@ -104,7 +106,79 @@ async function updateBinary(tag) {
104
106
  function updateNpmGlobal(tag) {
105
107
  log.step(`npm install -g ${KIT_PUBLISHED_NAME}@${tag}`);
106
108
  execSync(`npm install -g ${KIT_PUBLISHED_NAME}@${tag}`, { stdio: "inherit" });
107
- log.ok("글로벌 CLI 업데이트 완료.");
109
+ log.ok("Global CLI updated.");
110
+ }
111
+
112
+ // ── project structure migration ───────────────────────────────────────────────
113
+
114
+ function detectOldStructure(root) {
115
+ const issues = [];
116
+ if (existsSync(resolve(root, "src", "content.tsx")) && !existsSync(resolve(root, "ui"))) {
117
+ issues.push({ kind: "src→ui", desc: "src/content.tsx → ui/content.tsx" });
118
+ }
119
+ if (existsSync(resolve(root, "main.py")) && !existsSync(resolve(root, "py"))) {
120
+ issues.push({ kind: "main→py", desc: "main.py → py/main.py" });
121
+ }
122
+ if (existsSync(resolve(root, "backend"))) {
123
+ issues.push({ kind: "backend→py", desc: "backend/ → py/" });
124
+ }
125
+ return issues;
126
+ }
127
+
128
+ function prompt(question) {
129
+ return new Promise((resolve) => {
130
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
131
+ rl.question(question, (answer) => { rl.close(); resolve(answer.trim()); });
132
+ });
133
+ }
134
+
135
+ async function migrateStructure(root, issues) {
136
+ log.warn("This plugin project's structure is outdated:");
137
+ for (const issue of issues) log.info(" • " + issue.desc);
138
+ log.warn("Files will be moved and modified. Back up or commit your work before proceeding.");
139
+
140
+ const answer = await prompt("Migrate project structure? [y/N] ");
141
+ if (answer.toLowerCase() !== "y") {
142
+ log.info("Structure migration skipped.");
143
+ return;
144
+ }
145
+
146
+ for (const issue of issues) {
147
+ if (issue.kind === "src→ui") {
148
+ renameSync(resolve(root, "src"), resolve(root, "ui"));
149
+ log.ok("src/ → ui/");
150
+
151
+ const vitePath = resolve(root, "vite.config.ts");
152
+ if (existsSync(vitePath)) {
153
+ writeFileSync(vitePath, readFileSync(vitePath, "utf8").replace(/["']src\/content\.tsx["']/g, '"ui/content.tsx"'));
154
+ log.ok("vite.config.ts entry updated");
155
+ }
156
+ const tscPath = resolve(root, "tsconfig.json");
157
+ if (existsSync(tscPath)) {
158
+ writeFileSync(tscPath, readFileSync(tscPath, "utf8").replace(/"src"/g, '"ui"'));
159
+ log.ok("tsconfig.json include updated");
160
+ }
161
+ }
162
+
163
+ if (issue.kind === "main→py") {
164
+ mkdirSync(resolve(root, "py"), { recursive: true });
165
+ renameSync(resolve(root, "main.py"), resolve(root, "py", "main.py"));
166
+ log.ok("main.py → py/main.py");
167
+ }
168
+
169
+ if (issue.kind === "backend→py") {
170
+ renameSync(resolve(root, "backend"), resolve(root, "py"));
171
+ log.ok("backend/ → py/");
172
+ const pyReqs = resolve(root, "py", "requirements.txt");
173
+ const rootReqs = resolve(root, "requirements.txt");
174
+ if (existsSync(pyReqs) && !existsSync(rootReqs)) {
175
+ renameSync(pyReqs, rootReqs);
176
+ log.ok("py/requirements.txt → requirements.txt (project root)");
177
+ }
178
+ }
179
+ }
180
+
181
+ log.ok("Migration complete. Run `muscat-pk build` to verify.");
108
182
  }
109
183
 
110
184
  // ── plugin project local deps ─────────────────────────────────────────────────
@@ -117,31 +191,48 @@ function updateLocalDeps(root, tag) {
117
191
  : pm === "yarn" ? `yarn add ${list.join(" ")}`
118
192
  : `${pm} add ${list.join(" ")}`;
119
193
 
120
- log.step(`플러그인 로컬 deps 업데이트 (${pm}): ${list.join(", ")}`);
194
+ log.step(`Updating plugin local deps (${pm}): ${list.join(", ")}`);
121
195
  try {
122
196
  execSync(cmd, { cwd: root, stdio: "inherit" });
123
- log.ok("로컬 deps 업데이트 완료.");
197
+ log.ok("Local deps updated.");
124
198
  } catch {
125
- log.warn("로컬 deps 업데이트 실패 (오프라인?). 수동 실행:\n " + cmd);
199
+ log.warn("Local deps update failed (offline?). Run manually:\n " + cmd);
126
200
  }
127
201
  }
128
202
 
129
203
  // ── main ─────────────────────────────────────────────────────────────────────
130
204
 
205
+ async function resolveTag(explicitTag) {
206
+ if (explicitTag) return explicitTag;
207
+
208
+ const current = kitVersion();
209
+ const isDevVersion = current.includes("-dev.");
210
+
211
+ if (!isDevVersion) return "latest";
212
+
213
+ const answer = await prompt(
214
+ `You are on a dev version (v${current}).\n` +
215
+ ` [1] Stay on dev — update to latest dev build\n` +
216
+ ` [2] Switch to stable — update to latest stable release\n` +
217
+ `Choice [1/2, default: 1]: `
218
+ );
219
+ return answer.trim() === "2" ? "latest" : "dev";
220
+ }
221
+
131
222
  export async function runUpdate(args = []) {
132
223
  const opts = parseArgs(args);
133
- const tag = opts._[0] || opts.version || opts.tag || "latest";
224
+ const explicitTag = opts._[0] || opts.version || opts.tag || "";
225
+ const tag = await resolveTag(explicitTag);
134
226
 
135
227
  // 1. Update the CLI itself
136
228
  if (isBinaryMode) {
137
- log.step("바이너리 자기 업데이트 중…");
229
+ log.step("Updating binary…");
138
230
  try {
139
231
  await updateBinary(tag);
140
232
  } catch (err) {
141
- log.warn(`바이너리 업데이트 실패: ${err.message}`);
233
+ log.warn(`Binary update failed: ${err.message}`);
142
234
  }
143
235
  } else {
144
- // Check if globally installed via npm (execPath's npm prefix contains this package)
145
236
  const isGlobal = (() => {
146
237
  try {
147
238
  const prefix = execSync("npm prefix -g", { encoding: "utf8" }).trim();
@@ -155,16 +246,22 @@ export async function runUpdate(args = []) {
155
246
  try {
156
247
  updateNpmGlobal(tag);
157
248
  } catch (err) {
158
- log.warn(`글로벌 CLI 업데이트 실패: ${err.message}`);
249
+ log.warn(`Global CLI update failed: ${err.message}`);
159
250
  }
160
251
  }
161
252
  }
162
253
 
163
- // 2. Update the plugin project's local deps (best-effort)
254
+ // 2. Plugin project: structure migration + local deps
164
255
  try {
165
256
  const root = findPluginRoot();
257
+
258
+ const structureIssues = detectOldStructure(root);
259
+ if (structureIssues.length > 0) {
260
+ await migrateStructure(root, structureIssues);
261
+ }
262
+
166
263
  updateLocalDeps(root, tag);
167
264
  } catch {
168
- // Not inside a plugin project — skip local update silently
265
+ // Not inside a plugin project — skip silently
169
266
  }
170
267
  }
@@ -14,12 +14,5 @@
14
14
  "opens": "popover"
15
15
  }
16
16
  ],
17
- "actions": [
18
- {
19
- "id": "grip",
20
- "topic": "rby2/plugin/__PLUGIN_ID__/grip",
21
- "schema": { "width": "number" }
22
- }
23
- ],
24
17
  "runtime": { "type": "__RUNTIME_TYPE__", "entry": "main.py" }
25
18
  }
@@ -9,5 +9,5 @@
9
9
  "noEmit": true,
10
10
  "lib": ["DOM", "DOM.Iterable", "ESNext"]
11
11
  },
12
- "include": ["src"]
12
+ "include": ["ui"]
13
13
  }
@@ -17,7 +17,7 @@ export default defineConfig({
17
17
  plugins: [react()],
18
18
  build: {
19
19
  lib: {
20
- entry: "src/content.tsx",
20
+ entry: "ui/content.tsx",
21
21
  formats: ["es"],
22
22
  fileName: "content",
23
23
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rainbow-robotics/muscat-plugin-kit",
3
- "version": "0.1.2-dev.15",
3
+ "version": "0.1.2-dev.17",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=20"
File without changes
File without changes