diff --git a/pmoves/configs/cli_tools.yaml b/pmoves/configs/cli_tools.yaml index 7a2b889f93..f470a7a6c2 100644 --- a/pmoves/configs/cli_tools.yaml +++ b/pmoves/configs/cli_tools.yaml @@ -117,6 +117,62 @@ host_clis: windows: "npm install -g skills" linux: "npm install -g skills" check: "npx skills --version" + tailscale: + purpose: "Tailscale — fleet mesh. NOTE: raw 'tailscale status' leaks IPs — use 'make fleet-status' (Known Road)" + required: false + install: + windows: "winget install Tailscale.Tailscale" + linux: "curl -fsSL https://tailscale.com/install.sh | sh" + check: "tailscale version" + opencode: + purpose: "OpenCode — KiloCode runtime (kilo-pmoves selects per-node configs)" + required: false + install: + windows: "winget install sst.opencode # or: curl -fsSL https://opencode.ai/install | bash" + linux: "curl -fsSL https://opencode.ai/install | bash" + check: "opencode --version" + node: + purpose: "Node.js — web toolchains (jest/eslint run via package.json scripts, not global). Windows: nvm4w manages versions" + required: false + install: + windows: "winget install CoreyButler.NVMforWindows # then: nvm install lts" + linux: "curl -fsSL https://fnm.vercel.app/install | bash # or nvm" + check: "node --version" + npm: + purpose: "npm — package management for JS toolchains; global installs discouraged, prefer package.json+npx" + required: false + install: + windows: "ships with node (nvm4w)" + linux: "ships with node" + check: "npm --version" + +project_devtools: + # Tools pinned by package.json (NOT host CLIs — always run via the owning + # workspace's npm scripts so every node gets the version from ITS lockfile; + # never install globally, never bare `npx` from the repo root: there is no + # root package.json, and bare npx would fetch unrelated versions) + jest: + entry: "jest.config.js (pmoves/ui)" + run_via: "npm --prefix pmoves/ui test # or: cd pmoves/ui && npm test" + purpose: "JS test runner (pmoves/ui workspace)" + eslint: + entry: "pmoves/ui package.json devDependencies" + run_via: "npm --prefix pmoves/ui run lint" + purpose: "JS/TS lint (pmoves/ui workspace)" + +pinokio_ecosystem: + # Fleet Pinokio plane — launchers self-install deps into per-app venvs + # (the pattern the PMOVES launchers mirror via make env-bootstrap-lite) + pinokio: + purpose: "Pinokio app server — fleet launcher plane (D:/pinokio on Windows nodes)" + docs: "D:/pinokio/prototype/PINOKIO.md + .claude/PINOKIO_LAUNCHER_GUIDE.md" + check: "pinokio --version # via Pinokio install" + pterm: + purpose: "Pinokio terminal CLI — clipboard, notifications, dialogs, script testing" + docs: ".claude/skills/pterm/SKILL.md" + gepeto: + purpose: "Pinokio built-in code-assistant skill (Jetson JONS native skill)" + docs: "~/.agents/skills/gepeto" service_clis: # CLIs that live inside PMOVES services/submodules diff --git a/pmoves/tools/install_tools.py b/pmoves/tools/install_tools.py index 67b9080544..b92d533d3e 100644 --- a/pmoves/tools/install_tools.py +++ b/pmoves/tools/install_tools.py @@ -134,7 +134,30 @@ def warn_if_not_on_path(bin_dir: Path) -> None: print(f"WARN: {bin_dir} is not on PATH - add it to your shell profile.") -def check_host_clis(strict: bool) -> int: +def _cli_version(check_cmd: str) -> str: + """Best-effort version capture: run the manifest's check command, keep line 1.""" + import shlex + import subprocess + + try: + completed = subprocess.run( + shlex.split(check_cmd), + check=False, + capture_output=True, + text=True, + timeout=10, + ) + except (OSError, ValueError, subprocess.TimeoutExpired): + return "" + stream = completed.stdout or completed.stderr + for line in stream.splitlines(): + line = line.strip() + if line: + return line[:120] + return "" + + +def check_host_clis(strict: bool, report_versions: bool = False) -> int: """Doctor mode: validate host CLIs against configs/cli_tools.yaml.""" import shutil @@ -159,7 +182,8 @@ def check_host_clis(strict: bool) -> int: binary = shutil.which(name) required = bool(spec.get("required", False)) if binary: - print(f" OK {name}: {binary}") + version = f" {_cli_version(spec.get('check', ''))}" if report_versions and spec.get("check") else "" + print(f" OK {name}: {binary}{version}") continue hint = (spec.get("install", {}) or {}).get(platform_key, "") marker = "REQUIRED" if required else "optional" @@ -200,10 +224,20 @@ def main(argv: list[str] | None = None) -> int: action="store_true", help="With --check: also fail on missing optional CLIs.", ) + parser.add_argument( + "--no-report-versions", + action="store_true", + help="With --check: skip the per-CLI version capture line.", + ) args = parser.parse_args(argv) if args.check: - return check_host_clis(strict=args.strict) + # Doctor mode reports versions by default -- the version capture is the + # point of the check output; --no-report-versions opts out for + # deterministic-script consumers. + return check_host_clis( + strict=args.strict, report_versions=not args.no_report_versions + ) bin_dir = Path(args.bin).expanduser() if args.bin else default_bin_dir() if args.dry_run: