@rainbow-robotics/muscat-plugin-kit 1.10.1 → 1.10.2-dev.1
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 +1 -1
- package/cli/README.md +1 -1
- package/cli/bin/muscat-pk.mjs +1 -1
- package/cli/src/dev.mjs +2 -1
- package/cli/src/mcp.mjs +78 -54
- package/cli/src/muscatLocal.mjs +20 -4
- package/cli/src/update.mjs +117 -11
- package/cli/src/util.mjs +3 -3
- package/cli/storybook-dist/project.json +1 -1
- package/cli/templates/common/py/main.py +4 -4
- package/cli/templates/cpp/cpp/main.cpp +2 -2
- package/cli/templates/readme-cpp/README.md +7 -5
- package/package.json +3 -3
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
|
|
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)
|
package/cli/bin/muscat-pk.mjs
CHANGED
|
@@ -86,7 +86,7 @@ function formatHelp() {
|
|
|
86
86
|
commandRow("dev:back", `Run the plugin back end ${color.dim("(python: watchfiles reload / ros2: Docker)")}`),
|
|
87
87
|
commandRow("storybook", `Browse the bundled rb-components Storybook ${color.dim(`(${option("--port")} <n>, ${option("--rebuild")})`)}`),
|
|
88
88
|
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")}
|
|
89
|
+
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
90
|
commandRow("uninstall", `Remove the installed ${color.cyan("muscat-pk")} CLI ${color.dim("(npm global or S3 binary)")}`),
|
|
91
91
|
commandRow("sdk <list|update [version]>", "Manage the python rainbow-rb-sdk version"),
|
|
92
92
|
commandRow("version", `Print the installed ${color.cyan("muscat-pk")} version`),
|
package/cli/src/dev.mjs
CHANGED
|
@@ -115,7 +115,8 @@ export async function runDev(args = []) {
|
|
|
115
115
|
ensureDocker();
|
|
116
116
|
const { robotIp, robotToken } = await promptRobotConnection(args);
|
|
117
117
|
process.env.MUSCAT_ROBOT_HOST = robotIp;
|
|
118
|
-
|
|
118
|
+
// Even when empty: the dev:back child (stdin ignored) must not prompt for the token again.
|
|
119
|
+
process.env.MUSCAT_ROBOT_TOKEN = robotToken;
|
|
119
120
|
|
|
120
121
|
const remoteRobot = isRemoteRobot(robotIp);
|
|
121
122
|
const hostAgent = await maybeStartHostAgent(root, { robotIp, robotToken });
|
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
|
-
|
|
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
|
-
|
|
969
|
-
|
|
970
|
-
|
|
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
|
-
|
|
983
|
-
|
|
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
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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
|
|
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") {
|
package/cli/src/muscatLocal.mjs
CHANGED
|
@@ -29,10 +29,26 @@ const dockerArch = () => {
|
|
|
29
29
|
return a === "arm64" || a === "amd64" ? a : process.arch === "arm64" ? "arm64" : "amd64";
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
// Node's fetch reports every network error as "fetch failed"; the real reason (ECONNRESET, ...) is in `cause`.
|
|
33
|
+
const fetchErrorReason = (e) => e.cause?.code ?? e.code ?? e.cause?.message ?? e.message;
|
|
34
|
+
|
|
35
|
+
// The S3 zips are ~200MB, so a dropped connection mid-download is retried. 4xx is not.
|
|
36
|
+
export async function fetchBuffer(url, attempts = 3) {
|
|
37
|
+
for (let i = 1; ; i++) {
|
|
38
|
+
let err;
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch(url);
|
|
41
|
+
if (res.ok) return Buffer.from(await res.arrayBuffer());
|
|
42
|
+
err = new Error(`HTTP ${res.status}: ${url}`);
|
|
43
|
+
if (res.status < 500 && res.status !== 429) throw err;
|
|
44
|
+
} catch (e) {
|
|
45
|
+
if (e === err) throw e;
|
|
46
|
+
err = new Error(`fetch failed (${fetchErrorReason(e)}): ${url}`, { cause: e });
|
|
47
|
+
}
|
|
48
|
+
if (i >= attempts) throw err;
|
|
49
|
+
log.warn(`${err.message} — retrying (${i + 1}/${attempts})…`);
|
|
50
|
+
await new Promise((r) => setTimeout(r, 1000 * i));
|
|
51
|
+
}
|
|
36
52
|
}
|
|
37
53
|
|
|
38
54
|
// macOS/Windows `tar` reads zip; GNU tar does not, Linux has `unzip`.
|
package/cli/src/update.mjs
CHANGED
|
@@ -11,11 +11,12 @@
|
|
|
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";
|
|
14
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { join, relative, resolve } from "node:path";
|
|
16
16
|
import { findPluginRoot, isLocalKitInstall, KIT_PUBLISHED_NAME, kitVersion, log, parseArgs, prompt } from "./util.mjs";
|
|
17
17
|
|
|
18
18
|
const PLUGIN_UI_PACKAGE = "@rainbow-robotics/plugin-ui";
|
|
19
|
+
const PREVIEW_HOST_PACKAGE = "@rainbow-robotics/preview-host";
|
|
19
20
|
const S3_BASE = "https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit";
|
|
20
21
|
|
|
21
22
|
const isBinaryMode = typeof Bun !== "undefined";
|
|
@@ -193,21 +194,120 @@ async function migrateStructure(root, issues) {
|
|
|
193
194
|
|
|
194
195
|
// ── plugin project local deps ─────────────────────────────────────────────────
|
|
195
196
|
|
|
197
|
+
// The three packages are released together on one version, so preview-host moves with the other
|
|
198
|
+
// two — left behind, an old pin keeps serving a preview that no longer matches plugin-ui.
|
|
196
199
|
function updateLocalDeps(root, tag) {
|
|
197
200
|
const pm = detectPm(root);
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
201
|
+
let pkg = {};
|
|
202
|
+
try {
|
|
203
|
+
pkg = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8"));
|
|
204
|
+
} catch { /* ignore */ }
|
|
205
|
+
const listed = (name) => Boolean(pkg.dependencies?.[name] || pkg.devDependencies?.[name]);
|
|
206
|
+
const names = [KIT_PUBLISHED_NAME, PLUGIN_UI_PACKAGE, ...(listed(PREVIEW_HOST_PACKAGE) ? [PREVIEW_HOST_PACKAGE] : [])];
|
|
207
|
+
const isDev = (name) => Boolean(pkg.devDependencies?.[name]);
|
|
208
|
+
const add = (list, dev) => {
|
|
209
|
+
const flag = dev ? " -D" : "";
|
|
210
|
+
return pm === "npm" ? `npm install${flag} ${list.join(" ")}` : `${pm} add${flag} ${list.join(" ")}`;
|
|
211
|
+
};
|
|
212
|
+
// Keep each package in the section it is already in (yarn/pnpm `add` would move devDeps).
|
|
213
|
+
const cmds = [false, true]
|
|
214
|
+
.map((dev) => [names.filter((n) => isDev(n) === dev).map((n) => `${n}@${tag}`), dev])
|
|
215
|
+
.filter(([list]) => list.length)
|
|
216
|
+
.map(([list, dev]) => add(list, dev));
|
|
217
|
+
|
|
218
|
+
log.step(`Updating plugin local deps (${pm}): ${names.map((n) => `${n}@${tag}`).join(", ")}`);
|
|
205
219
|
try {
|
|
206
|
-
execSync(cmd, { cwd: root, stdio: "inherit" });
|
|
220
|
+
for (const cmd of cmds) execSync(cmd, { cwd: root, stdio: "inherit" });
|
|
207
221
|
log.ok("Local deps updated.");
|
|
208
222
|
} catch {
|
|
209
|
-
log.warn("Local deps update failed (offline?). Run manually:\n " +
|
|
223
|
+
log.warn("Local deps update failed (offline?). Run manually:\n " + cmds.join("\n "));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
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
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
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
|
+
}
|
|
210
300
|
}
|
|
301
|
+
return found;
|
|
302
|
+
}
|
|
303
|
+
|
|
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>");
|
|
211
311
|
}
|
|
212
312
|
|
|
213
313
|
// ── main ─────────────────────────────────────────────────────────────────────
|
|
@@ -292,4 +392,10 @@ export async function runUpdate(args = []) {
|
|
|
292
392
|
}
|
|
293
393
|
|
|
294
394
|
updateLocalDeps(root, tag);
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
reportLegacyUi(root);
|
|
398
|
+
} catch (err) {
|
|
399
|
+
log.warn(`Legacy UI check failed: ${err.message}`);
|
|
400
|
+
}
|
|
295
401
|
}
|
package/cli/src/util.mjs
CHANGED
|
@@ -137,9 +137,10 @@ export async function prompt(question, fallback = "") {
|
|
|
137
137
|
const LOCAL_HOSTS = new Set(["", "localhost", "127.0.0.1", "::1"]);
|
|
138
138
|
|
|
139
139
|
/**
|
|
140
|
-
* Ask for robot IP (and token if remote). Returns { robotIp, robotToken }.
|
|
140
|
+
* Ask for robot IP (and optional token if remote). Returns { robotIp, robotToken }.
|
|
141
141
|
* Skips prompts if already set in process.env (from a parent command like `dev`).
|
|
142
142
|
* Supports --robot-ip / --robot-token flags for non-interactive use.
|
|
143
|
+
* An empty token is allowed: the robot API is then called without an Authorization header.
|
|
143
144
|
*/
|
|
144
145
|
export async function promptRobotConnection(args = []) {
|
|
145
146
|
const parsed = parseArgs(args);
|
|
@@ -153,9 +154,8 @@ export async function promptRobotConnection(args = []) {
|
|
|
153
154
|
const robotToken =
|
|
154
155
|
parsed["robot-token"] ??
|
|
155
156
|
process.env.MUSCAT_ROBOT_TOKEN ??
|
|
156
|
-
(await prompt(" SDK Token (
|
|
157
|
+
(await prompt(" SDK Token (optional, Enter to skip): "));
|
|
157
158
|
|
|
158
|
-
if (!robotToken) throw new Error("SDK token is required for remote robot connection.");
|
|
159
159
|
return { robotIp, robotToken };
|
|
160
160
|
}
|
|
161
161
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"generatedAt":
|
|
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}}}
|
|
@@ -8,7 +8,7 @@ On-robot (default):
|
|
|
8
8
|
Remote dev (from your laptop):
|
|
9
9
|
Set env vars before running:
|
|
10
10
|
MUSCAT_ROBOT_HOST=192.168.1.100
|
|
11
|
-
MUSCAT_ROBOT_TOKEN=<service token>
|
|
11
|
+
MUSCAT_ROBOT_TOKEN=<service token> (optional; needed only when the robot's login policy is on)
|
|
12
12
|
Then: muscat-pk dev:back
|
|
13
13
|
|
|
14
14
|
Handlers:
|
|
@@ -90,9 +90,9 @@ def move_j_example():
|
|
|
90
90
|
|
|
91
91
|
async def main():
|
|
92
92
|
robot_host = os.getenv("MUSCAT_ROBOT_HOST")
|
|
93
|
-
robot_token = os.getenv("MUSCAT_ROBOT_TOKEN")
|
|
93
|
+
robot_token = os.getenv("MUSCAT_ROBOT_TOKEN", "") # empty is fine when the login policy is off
|
|
94
94
|
|
|
95
|
-
if robot_host
|
|
95
|
+
if robot_host:
|
|
96
96
|
# Remote dev: connect to the robot before registering any handlers.
|
|
97
97
|
# connect() closes and reopens the Zenoh session, so handlers registered
|
|
98
98
|
# before this call would be lost.
|
|
@@ -113,7 +113,7 @@ async def main():
|
|
|
113
113
|
except (KeyboardInterrupt, asyncio.CancelledError):
|
|
114
114
|
pass
|
|
115
115
|
finally:
|
|
116
|
-
if robot_host
|
|
116
|
+
if robot_host:
|
|
117
117
|
await manipulate.disconnect()
|
|
118
118
|
print(f"[{PLUGIN_NAME}] stopped")
|
|
119
119
|
|
|
@@ -30,8 +30,8 @@ int main() {
|
|
|
30
30
|
// Remote dev: connect before registering handlers (connect() reopens the session).
|
|
31
31
|
RBManipulateSDK manipulate;
|
|
32
32
|
const char* host = std::getenv("MUSCAT_ROBOT_HOST");
|
|
33
|
-
const char* token = std::getenv("MUSCAT_ROBOT_TOKEN");
|
|
34
|
-
if (host
|
|
33
|
+
const char* token = std::getenv("MUSCAT_ROBOT_TOKEN"); // optional: needed only when the login policy is on
|
|
34
|
+
if (host) manipulate.connect({.token = token ? token : "", .host = host});
|
|
35
35
|
|
|
36
36
|
// plugin id comes from RB_PLUGIN_ID.
|
|
37
37
|
auto plugin = RBPluginCall::get_instance(RBPluginCall::resolve_plugin_id(std::nullopt));
|
|
@@ -531,14 +531,16 @@ manipulate.move.call_move_j("C500920", t, std::nullopt, MoveSpeedArg{1, 60.f, 12
|
|
|
531
531
|
manipulate.move.call_move_wait("C500920", -1);
|
|
532
532
|
```
|
|
533
533
|
|
|
534
|
-
Remote dev (`muscat-pk dev --robot-ip <ip> --robot-token <token
|
|
534
|
+
Remote dev (`muscat-pk dev --robot-ip <ip> [--robot-token <token>]`) sets
|
|
535
535
|
`MUSCAT_ROBOT_HOST` / `MUSCAT_ROBOT_TOKEN`; connect **before** registering
|
|
536
|
-
subscriptions, because `connect()` reopens the Zenoh session
|
|
536
|
+
subscriptions, because `connect()` reopens the Zenoh session. The token is
|
|
537
|
+
optional — it is needed only when the robot's login policy is on:
|
|
537
538
|
|
|
538
539
|
```cpp
|
|
539
|
-
if (const char* host = std::getenv("MUSCAT_ROBOT_HOST"))
|
|
540
|
-
|
|
541
|
-
|
|
540
|
+
if (const char* host = std::getenv("MUSCAT_ROBOT_HOST")) {
|
|
541
|
+
const char* token = std::getenv("MUSCAT_ROBOT_TOKEN");
|
|
542
|
+
manipulate.connect({.token = token ? token : "", .host = host});
|
|
543
|
+
}
|
|
542
544
|
```
|
|
543
545
|
|
|
544
546
|
AMR and RB-Y1 SDKs are Python-only for now.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rainbow-robotics/muscat-plugin-kit",
|
|
3
|
-
"version": "1.10.1",
|
|
3
|
+
"version": "1.10.2-dev.1",
|
|
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.1",
|
|
56
|
-
"@rainbow-robotics/preview-host": "1.10.1",
|
|
55
|
+
"@rainbow-robotics/plugin-ui": "1.10.2-dev.1",
|
|
56
|
+
"@rainbow-robotics/preview-host": "1.10.2-dev.1",
|
|
57
57
|
"@types/react": "^19.0.10",
|
|
58
58
|
"postcss": "^8.5.8",
|
|
59
59
|
"typescript": "^5.8.0",
|