rovecode 0.4.0-beta.2 → 0.4.0
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 +67 -69
- package/THIRD_PARTY_NOTICES.md +0 -44
- package/bin/rovecode.ts +21 -0
- package/package.json +16 -37
- package/src/account/keys.ts +97 -0
- package/src/account/login.ts +158 -0
- package/src/account/provision.ts +47 -0
- package/src/account/store.ts +63 -0
- package/src/acp/server.ts +373 -0
- package/src/cli/account-cmd.ts +116 -0
- package/src/cli/connect.ts +244 -0
- package/src/cli/context-cmd.ts +199 -0
- package/src/cli/dispatch.ts +109 -0
- package/src/cli/doctor.ts +324 -0
- package/src/cli/export.ts +278 -0
- package/src/cli/help.ts +240 -0
- package/src/cli/is-tui-invocation.ts +8 -0
- package/src/cli/main.ts +599 -0
- package/src/cli/market-cmd.ts +658 -0
- package/src/cli/mcp-market-cmd.ts +299 -0
- package/src/cli/output.ts +382 -0
- package/src/cli/repl.ts +172 -0
- package/src/cli/resume.ts +32 -0
- package/src/cli/run-limits.ts +78 -0
- package/src/cli/runtime.ts +792 -0
- package/src/cli/setup.ts +187 -0
- package/src/cli/update-cmd.ts +78 -0
- package/src/cli/workflow-cmd.ts +100 -0
- package/src/coding/checkpoints.ts +270 -0
- package/src/coding/diff.ts +136 -0
- package/src/coding/files.ts +339 -0
- package/src/coding/hashline.ts +319 -0
- package/src/coding/lsp.ts +406 -0
- package/src/coding/repomap-cache.ts +99 -0
- package/src/coding/repomap-files.ts +110 -0
- package/src/coding/repomap.ts +392 -0
- package/src/core/compaction.ts +399 -0
- package/src/core/config.ts +289 -0
- package/src/core/context-report.ts +228 -0
- package/src/core/context.ts +60 -0
- package/src/core/count-remote.ts +107 -0
- package/src/core/execpolicy-rules.ts +196 -0
- package/src/core/execpolicy.ts +385 -0
- package/src/core/executor.ts +397 -0
- package/src/core/guardrails.ts +400 -0
- package/src/core/hooks.ts +398 -0
- package/src/core/images.ts +230 -0
- package/src/core/intro.ts +236 -0
- package/src/core/loop.ts +621 -0
- package/src/core/modes.ts +372 -0
- package/src/core/orchestrator.ts +207 -0
- package/src/core/reflection.ts +165 -0
- package/src/core/sandbox-config.ts +167 -0
- package/src/core/session-images.ts +73 -0
- package/src/core/session.ts +398 -0
- package/src/core/settings.ts +98 -0
- package/src/core/stuck-detector.ts +273 -0
- package/src/core/tasks.ts +374 -0
- package/src/core/token-scale.ts +108 -0
- package/src/core/tool-output-budget.ts +166 -0
- package/src/core/tools.ts +288 -0
- package/src/core/types.ts +330 -0
- package/src/core/update-check.ts +171 -0
- package/src/core/update.ts +158 -0
- package/src/core/usage.ts +204 -0
- package/src/core/validate.ts +121 -0
- package/src/core/verify-gate.ts +159 -0
- package/src/core/verify.ts +237 -0
- package/src/core/voice.ts +158 -0
- package/src/core/win-job.ts +183 -0
- package/src/design/audit.ts +797 -0
- package/src/design/direction.ts +190 -0
- package/src/design/rules.ts +157 -0
- package/src/eval/bench.ts +150 -0
- package/src/eval/gauntlet-runner.ts +218 -0
- package/src/eval/gauntlet.ts +226 -0
- package/src/eval/grader.ts +186 -0
- package/src/eval/record.ts +202 -0
- package/src/eval/redact.ts +141 -0
- package/src/eval/replay.ts +147 -0
- package/src/eval/trajectory.ts +373 -0
- package/src/index.ts +17 -0
- package/src/market/catalogs/mcp-docs.json +111 -0
- package/src/market/catalogs/plugins.json +111 -0
- package/src/market/catalogs/skills.json +478 -0
- package/src/market/clone.ts +72 -0
- package/src/market/context-cost.ts +121 -0
- package/src/market/digest.ts +106 -0
- package/src/market/index.ts +22 -0
- package/src/market/install.ts +578 -0
- package/src/market/manifest.ts +187 -0
- package/src/market/prereq.ts +145 -0
- package/src/market/registry.ts +363 -0
- package/src/market/resolve.ts +111 -0
- package/src/market/types.ts +236 -0
- package/src/market/validate.ts +227 -0
- package/src/mcp/client.ts +431 -0
- package/src/mcp/config.ts +239 -0
- package/src/mcp/local-package.ts +211 -0
- package/src/mcp/market-catalog.ts +84 -0
- package/src/mcp/market-install.ts +289 -0
- package/src/mcp/market.ts +0 -0
- package/src/mcp/tools.ts +131 -0
- package/src/mcp/trust.ts +49 -0
- package/src/memory/blocks.ts +175 -0
- package/src/memory/recall.ts +355 -0
- package/src/memory/store.ts +105 -0
- package/src/memory/tools.ts +99 -0
- package/src/plugins/cli.ts +123 -0
- package/src/plugins/discover.ts +108 -0
- package/src/plugins/index.ts +50 -0
- package/src/plugins/init.ts +140 -0
- package/src/plugins/install.ts +184 -0
- package/src/plugins/load.ts +149 -0
- package/src/plugins/manifest.ts +106 -0
- package/src/plugins/state.ts +83 -0
- package/src/providers/auth.ts +293 -0
- package/src/providers/cache.ts +223 -0
- package/src/providers/catalog-local.ts +160 -0
- package/src/providers/catalog.ts +408 -0
- package/src/providers/middleware-context.ts +86 -0
- package/src/providers/middleware.ts +373 -0
- package/src/providers/profile-glm53.ts +111 -0
- package/src/providers/profile-sonnet5-persona.ts +65 -0
- package/src/providers/profile-sonnet5-voice.ts +23 -0
- package/src/providers/profiles.ts +156 -0
- package/src/providers/provider-config.ts +311 -0
- package/src/providers/registry.ts +302 -0
- package/src/providers/response-validation.ts +80 -0
- package/src/providers/retry.ts +234 -0
- package/src/providers/router.ts +294 -0
- package/src/providers/sse.ts +26 -0
- package/src/providers/stream-errors.ts +117 -0
- package/src/providers/stream.ts +569 -0
- package/src/providers/thinking.ts +189 -0
- package/src/providers/wire-messages.ts +129 -0
- package/src/sdk/client.ts +225 -0
- package/src/sdk/index.ts +3 -0
- package/src/server/dashboard.ts +144 -0
- package/src/server/http.ts +343 -0
- package/src/server/openapi.ts +246 -0
- package/src/sextant/card-hits.ts +102 -0
- package/src/sextant/card-keys.ts +55 -0
- package/src/sextant/context-source.ts +157 -0
- package/src/sextant/draw-agents.ts +273 -0
- package/src/sextant/draw-code.ts +388 -0
- package/src/sextant/draw-context.ts +222 -0
- package/src/sextant/draw-frame.ts +164 -0
- package/src/sextant/draw-market.ts +573 -0
- package/src/sextant/draw-messages.ts +386 -0
- package/src/sextant/draw-pet.ts +230 -0
- package/src/sextant/draw-plan.ts +159 -0
- package/src/sextant/draw-tabs.ts +85 -0
- package/src/sextant/draw-util.ts +65 -0
- package/src/sextant/engine.ts +230 -0
- package/src/sextant/frame-hits.ts +25 -0
- package/src/sextant/frame.ts +101 -0
- package/src/sextant/git-status.ts +197 -0
- package/src/sextant/grid.ts +59 -0
- package/src/sextant/input.ts +119 -0
- package/src/sextant/keys.ts +488 -0
- package/src/sextant/layout.ts +86 -0
- package/src/sextant/local-commands.ts +156 -0
- package/src/sextant/market-source.ts +287 -0
- package/src/sextant/mentions.ts +141 -0
- package/src/sextant/message-hits.ts +26 -0
- package/src/sextant/model.ts +387 -0
- package/src/sextant/overlays.ts +451 -0
- package/src/sextant/panel-hits.ts +38 -0
- package/src/sextant/pet.ts +399 -0
- package/src/sextant/screen.ts +324 -0
- package/src/sextant/scroll-hits.ts +66 -0
- package/src/sextant/scrollbar.ts +82 -0
- package/src/sextant/selection.ts +123 -0
- package/src/sextant/sextant-bridge.ts +174 -0
- package/src/sextant/sextant-cards.ts +142 -0
- package/src/sextant/sextant-diff-base.ts +63 -0
- package/src/sextant/sextant-files.ts +154 -0
- package/src/sextant/sextant-frame-loop.ts +314 -0
- package/src/sextant/sextant-renderer.ts +478 -0
- package/src/sextant/sextant-repo.ts +131 -0
- package/src/sextant/theme.ts +66 -0
- package/src/sextant/tool-rows.ts +189 -0
- package/src/sextant/types.ts +473 -0
- package/src/skills/index.ts +306 -0
- package/src/skills/tools.ts +69 -0
- package/src/skills/versioned.ts +227 -0
- package/src/telemetry/otel.ts +353 -0
- package/src/telemetry/otlp.ts +68 -0
- package/src/tools/ask-user.ts +156 -0
- package/src/tools/design.ts +151 -0
- package/src/tools/evalcell.ts +338 -0
- package/src/tools/html-text.ts +139 -0
- package/src/tools/provider.ts +149 -0
- package/src/tools/task.ts +216 -0
- package/src/tools/todo.ts +320 -0
- package/src/tools/webfetch.ts +331 -0
- package/src/tui/app.ts +608 -0
- package/src/tui/attach.ts +127 -0
- package/src/tui/checkpoints-cmd.ts +70 -0
- package/src/tui/clipboard-image.ts +81 -0
- package/src/tui/commands.ts +277 -0
- package/src/tui/cost.ts +108 -0
- package/src/tui/info-cmd.ts +144 -0
- package/src/tui/mcp-cmd.ts +128 -0
- package/src/tui/modes-cmd.ts +45 -0
- package/src/tui/overlays.ts +97 -0
- package/src/tui/pi-renderer.ts +424 -0
- package/src/tui/providers-cmd.ts +366 -0
- package/src/tui/renderer.ts +101 -0
- package/src/tui/replay-marker.ts +29 -0
- package/src/tui/session-cmd.ts +146 -0
- package/src/tui/sextant-attach.ts +68 -0
- package/src/tui/sextant-io.ts +184 -0
- package/src/tui/sextant-smoke.ts +110 -0
- package/src/tui/smoke.ts +72 -0
- package/src/tui/theme.ts +59 -0
- package/src/tui/todo-label.ts +7 -0
- package/src/workflow/engine.ts +266 -0
- package/tsconfig.json +30 -0
- package/vendor/pi-tui/LICENSE +21 -0
- package/vendor/pi-tui/PATCHES.md +12 -0
- package/vendor/pi-tui/PROVENANCE.md +12 -0
- package/vendor/pi-tui/README.upstream.md +854 -0
- package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
- package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
- package/vendor/pi-tui/src/alt-screen-search.ts +158 -0
- package/vendor/pi-tui/src/autocomplete.ts +827 -0
- package/vendor/pi-tui/src/components/alt-screen-flash.ts +52 -0
- package/vendor/pi-tui/src/components/box.ts +138 -0
- package/vendor/pi-tui/src/components/cancellable-loader.ts +41 -0
- package/vendor/pi-tui/src/components/editor.ts +2364 -0
- package/vendor/pi-tui/src/components/h-stack.ts +45 -0
- package/vendor/pi-tui/src/components/image.ts +128 -0
- package/vendor/pi-tui/src/components/input.ts +448 -0
- package/vendor/pi-tui/src/components/loader.ts +93 -0
- package/vendor/pi-tui/src/components/markdown.ts +1016 -0
- package/vendor/pi-tui/src/components/scroll-view.ts +217 -0
- package/vendor/pi-tui/src/components/select-list.ts +230 -0
- package/vendor/pi-tui/src/components/settings-list.ts +277 -0
- package/vendor/pi-tui/src/components/spacer.ts +29 -0
- package/vendor/pi-tui/src/components/stack.ts +155 -0
- package/vendor/pi-tui/src/components/text.ts +108 -0
- package/vendor/pi-tui/src/components/truncated-text.ts +66 -0
- package/vendor/pi-tui/src/components/v-stack.ts +34 -0
- package/vendor/pi-tui/src/editor-component.ts +75 -0
- package/vendor/pi-tui/src/fuzzy.ts +138 -0
- package/vendor/pi-tui/src/index.ts +149 -0
- package/vendor/pi-tui/src/keybindings.ts +321 -0
- package/vendor/pi-tui/src/keys.ts +1402 -0
- package/vendor/pi-tui/src/kill-ring.ts +47 -0
- package/vendor/pi-tui/src/latex.ts +1381 -0
- package/vendor/pi-tui/src/layout-node.ts +52 -0
- package/vendor/pi-tui/src/layout.ts +411 -0
- package/vendor/pi-tui/src/native-modifiers.ts +60 -0
- package/vendor/pi-tui/src/native-module-path.ts +32 -0
- package/vendor/pi-tui/src/stdin-buffer.ts +445 -0
- package/vendor/pi-tui/src/terminal-colors.ts +74 -0
- package/vendor/pi-tui/src/terminal-image.ts +701 -0
- package/vendor/pi-tui/src/terminal.ts +554 -0
- package/vendor/pi-tui/src/tui-alt-screen.ts +1379 -0
- package/vendor/pi-tui/src/tui-main-screen.ts +655 -0
- package/vendor/pi-tui/src/tui.ts +1264 -0
- package/vendor/pi-tui/src/undo-stack.ts +29 -0
- package/vendor/pi-tui/src/utils.ts +1327 -0
- package/vendor/pi-tui/src/word-navigation.ts +118 -0
- package/vendor/pi-tui/test/test-themes.ts +39 -0
- package/vendor/pi-tui/test/virtual-terminal.ts +219 -0
- package/CHANGELOG.md +0 -512
- package/bin/rovecode.js +0 -24
- package/dist/cli/app-dybnr56b.js +0 -2
- package/dist/cli/ask-user-p8hq4xgj.js +0 -2
- package/dist/cli/auth-login-ewpgw5sm.js +0 -2
- package/dist/cli/auth-m8p9grty.js +0 -2
- package/dist/cli/bench-xv3ypwev.js +0 -9
- package/dist/cli/catalog-737wb2s0.js +0 -2
- package/dist/cli/cli-arhg40m0.js +0 -2
- package/dist/cli/client-cf2pxx8q.js +0 -2
- package/dist/cli/commands-3p7e4xxs.js +0 -2
- package/dist/cli/connect-3q93d7cb.js +0 -2
- package/dist/cli/context-cmd-eqxmxhzq.js +0 -2
- package/dist/cli/context-report-hbw9zfes.js +0 -2
- package/dist/cli/count-remote-mby98cd0.js +0 -2
- package/dist/cli/design-122y0axd.js +0 -2
- package/dist/cli/dispatch-b4egzvvh.js +0 -2
- package/dist/cli/doctor-x4jkv72e.js +0 -3
- package/dist/cli/executor-ftvg6tsy.js +0 -2
- package/dist/cli/export-pdgdhkch.js +0 -2
- package/dist/cli/files-cez9a96p.js +0 -2
- package/dist/cli/gauntlet-r3xxaszc.js +0 -2
- package/dist/cli/gauntlet-runner-r515m7kk.js +0 -10
- package/dist/cli/gauntlet-wave3-bnkjk2v2.js +0 -5
- package/dist/cli/gauntlet-wave4-acs9s60q.js +0 -14
- package/dist/cli/hashline-ewg5hbe3.js +0 -2
- package/dist/cli/http-n0kehsk8.js +0 -5
- package/dist/cli/index-z5qt1s76.js +0 -2
- package/dist/cli/install-80mp63kx.js +0 -2
- package/dist/cli/loop-12twjcat.js +0 -2
- package/dist/cli/main-01pv9206.js +0 -4
- package/dist/cli/main-0jys2ccn.js +0 -3
- package/dist/cli/main-1ztz6fkj.js +0 -10
- package/dist/cli/main-23q7cmww.js +0 -9
- package/dist/cli/main-2rzbexn2.js +0 -3
- package/dist/cli/main-2wyax8k9.js +0 -9
- package/dist/cli/main-2yeveeve.js +0 -6
- package/dist/cli/main-2z3dek0b.js +0 -3
- package/dist/cli/main-2zgsknth.js +0 -3
- package/dist/cli/main-45ejth3a.js +0 -4
- package/dist/cli/main-45rn3trk.js +0 -22
- package/dist/cli/main-4p4e2w7x.js +0 -4
- package/dist/cli/main-4y0tnfpa.js +0 -16
- package/dist/cli/main-5py0rkmc.js +0 -4
- package/dist/cli/main-6dtqmbt6.js +0 -7
- package/dist/cli/main-6h9x282m.js +0 -4
- package/dist/cli/main-6vjeds42.js +0 -3
- package/dist/cli/main-78gq4bt9.js +0 -6
- package/dist/cli/main-7jd5vh3x.js +0 -4
- package/dist/cli/main-7kt6r53y.js +0 -4
- package/dist/cli/main-8c1tbazx.js +0 -58
- package/dist/cli/main-9a9rnh47.js +0 -19
- package/dist/cli/main-9ht36z12.js +0 -3
- package/dist/cli/main-a2yfvcy9.js +0 -7
- package/dist/cli/main-a3f51n0x.js +0 -5
- package/dist/cli/main-b8zq261k.js +0 -3
- package/dist/cli/main-bxtvnf6d.js +0 -13
- package/dist/cli/main-edxc3yzt.js +0 -4
- package/dist/cli/main-evgz4mp5.js +0 -21
- package/dist/cli/main-f33fc5je.js +0 -9
- package/dist/cli/main-fvnpq46y.js +0 -12
- package/dist/cli/main-gbbty4d4.js +0 -3
- package/dist/cli/main-gth53dnt.js +0 -25
- package/dist/cli/main-hqbz10aw.js +0 -9
- package/dist/cli/main-hrrvcfan.js +0 -38
- package/dist/cli/main-hzwtsb2m.js +0 -5
- package/dist/cli/main-j7ttv0sd.js +0 -34
- package/dist/cli/main-jak598k9.js +0 -5
- package/dist/cli/main-kba6zeyd.js +0 -6
- package/dist/cli/main-kwwsz6rq.js +0 -3
- package/dist/cli/main-m8vm17zq.js +0 -3
- package/dist/cli/main-mg4f96e1.js +0 -3
- package/dist/cli/main-mg9b20ac.js +0 -18
- package/dist/cli/main-mgb9ccnx.js +0 -3
- package/dist/cli/main-mjt2p7aj.js +0 -3
- package/dist/cli/main-n6qrdbmy.js +0 -3
- package/dist/cli/main-na7wse0x.js +0 -5
- package/dist/cli/main-nqveez48.js +0 -4
- package/dist/cli/main-ntqef02r.js +0 -10
- package/dist/cli/main-nvc3yjay.js +0 -136
- package/dist/cli/main-p0cfn6nr.js +0 -16
- package/dist/cli/main-qj2djy17.js +0 -19
- package/dist/cli/main-qsevpgsv.js +0 -3
- package/dist/cli/main-qvarybsp.js +0 -3
- package/dist/cli/main-rebtt91r.js +0 -5
- package/dist/cli/main-rpg7h8mb.js +0 -3
- package/dist/cli/main-rsy72qmw.js +0 -15
- package/dist/cli/main-rvetps99.js +0 -18
- package/dist/cli/main-s4bb0jav.js +0 -3
- package/dist/cli/main-s9v8k74e.js +0 -3
- package/dist/cli/main-tjvwmscs.js +0 -3
- package/dist/cli/main-tkgarpjj.js +0 -4
- package/dist/cli/main-v8y60bb2.js +0 -3
- package/dist/cli/main-vhrrq337.js +0 -3
- package/dist/cli/main-vp2dfb7s.js +0 -4
- package/dist/cli/main-vqbr22sz.js +0 -8
- package/dist/cli/main-vxnwe5xx.js +0 -18
- package/dist/cli/main-wgph00xf.js +0 -5
- package/dist/cli/main-wk2csfnj.js +0 -5
- package/dist/cli/main-wm997zjx.js +0 -3
- package/dist/cli/main-wpkyraxh.js +0 -3
- package/dist/cli/main-wqt32p5x.js +0 -4
- package/dist/cli/main-x9ct6y1a.js +0 -3
- package/dist/cli/main-xfekqh9m.js +0 -7
- package/dist/cli/main-xt9zc3n6.js +0 -7
- package/dist/cli/main-xx2z3zh5.js +0 -4
- package/dist/cli/main-y5c82rxr.js +0 -3
- package/dist/cli/main-yrjt2sqt.js +0 -14
- package/dist/cli/main-ys6zj3yr.js +0 -3
- package/dist/cli/main-ywbxshqc.js +0 -8
- package/dist/cli/main-z13755t8.js +0 -25
- package/dist/cli/main-zc7pyrbj.js +0 -4
- package/dist/cli/main.js +0 -279
- package/dist/cli/market-cmd-bm5xvn9f.js +0 -5
- package/dist/cli/mcp-login-bthtfpt7.js +0 -2
- package/dist/cli/mcp-market-cmd-mbeshfyd.js +0 -2
- package/dist/cli/notify-54v5z9dz.js +0 -2
- package/dist/cli/oauth-g5gme95c.js +0 -2
- package/dist/cli/output-satndjap.js +0 -16
- package/dist/cli/profiles-sfhpbq3m.js +0 -2
- package/dist/cli/provider-config-hv3xtdt4.js +0 -2
- package/dist/cli/provider-kwzq6g84.js +0 -2
- package/dist/cli/registry-fh0hdnyn.js +0 -2
- package/dist/cli/registry-y1y8e94r.js +0 -2
- package/dist/cli/repl-t4z03mqq.js +0 -11
- package/dist/cli/resume-fqt4chg8.js +0 -2
- package/dist/cli/run-flags-rysbag9t.js +0 -2
- package/dist/cli/runtime-j19fjbsa.js +0 -2
- package/dist/cli/sandbox-config-g4qxd7y5.js +0 -2
- package/dist/cli/server-r0b6bksk.js +0 -5
- package/dist/cli/session-arg-txmn5g4x.js +0 -2
- package/dist/cli/session-ed250d9j.js +0 -2
- package/dist/cli/sessions-cmd-adw7svfn.js +0 -7
- package/dist/cli/settings-y9rzcqx8.js +0 -2
- package/dist/cli/setup-jmbr11j0.js +0 -2
- package/dist/cli/sextant-smoke-tcth0vea.js +0 -5
- package/dist/cli/skills-cmd-zbdy99v6.js +0 -2
- package/dist/cli/smoke-1bg937kx.js +0 -8
- package/dist/cli/start-chat-p01cdks3.js +0 -12
- package/dist/cli/stream-4wmyaypz.js +0 -2
- package/dist/cli/task-eg4s093s.js +0 -2
- package/dist/cli/tasks-12v9rr9k.js +0 -2
- package/dist/cli/thinking-a5ngvqyh.js +0 -2
- package/dist/cli/todo-1wxpcecx.js +0 -2
- package/dist/cli/tools-2ftsya7w.js +0 -2
- package/dist/cli/tools-x1tj4fxm.js +0 -2
- package/dist/cli/trust-cmd-hccxehzb.js +0 -2
- package/dist/cli/update-check-ygt3vd7m.js +0 -2
- package/dist/cli/update-cmd-v23qhr8c.js +0 -2
- package/dist/cli/voice-g1gtck92.js +0 -2
- package/dist/cli/webfetch-0nnrjgb5.js +0 -2
- package/dist/cli/websearch-f0vr2p7d.js +0 -2
- package/dist/cli/workspace-9rq1w4ta.js +0 -2
- package/dist/lib/index.js +0 -62
- package/dist/lib/models-index.json +0 -1
- package/dist/lib/plugins.js +0 -6
- package/dist/lib/providers.js +0 -17
- package/dist/lib/public-api.js +0 -20
- package/dist/rovecode.exe +0 -4
- /package/{dist/cli → src/providers}/models-index.json +0 -0
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
/** `rovecode market …` — one command for all three kinds. search · info · install · remove · list ·
|
|
2
|
+
* update · sources, each with `--json` so a script or another surface reads the same data the terminal
|
|
3
|
+
* shows. A pure function over injected cwd/home/streams/prompts: tests drive it without a process, a
|
|
4
|
+
* network or a TTY.
|
|
5
|
+
*
|
|
6
|
+
* The rule install lives by is the MCP market's, unchanged: the human SEES the plan — the exact command,
|
|
7
|
+
* the folder, the file list, the source, the publisher, what it will ask for — then says yes, on a
|
|
8
|
+
* terminal through y/N or in a script through `--yes`. Without a TTY and without --yes nothing is
|
|
9
|
+
* written. Secrets are asked by NAME through the masked prompt: never echoed, never on the command line,
|
|
10
|
+
* never into a project file (there they are written as `${NAME}` and read from the environment).
|
|
11
|
+
*
|
|
12
|
+
* `rovecode mcp …` keeps working and is not deprecated here — it is the MCP-only door into the same
|
|
13
|
+
* code; `market` is the door that also sees skills and plugins. */
|
|
14
|
+
|
|
15
|
+
import { readSecret, rovecodeHome } from "../providers/auth.ts";
|
|
16
|
+
import { itemLine, qualify, type MarketItem, type MarketKind, type MarketRow, type MarketScope } from "../market/types.ts";
|
|
17
|
+
import { allItems, searchMarket, type RegistryDeps } from "../market/registry.ts";
|
|
18
|
+
import { resolveTarget } from "../market/resolve.ts";
|
|
19
|
+
import { installedState, needsNetwork, planInstall, removeItem, runInstall, withInstalled, type PlanOptions, type RunDeps } from "../market/install.ts";
|
|
20
|
+
import { npxPackage, type NpxPackage } from "../mcp/local-package.ts";
|
|
21
|
+
import type { PrereqEnv } from "../market/prereq.ts";
|
|
22
|
+
import { originLine, readManifest, recordFor } from "../market/manifest.ts";
|
|
23
|
+
import { verifyDigest, verifyLine } from "../market/digest.ts";
|
|
24
|
+
import { disposeCloneCache } from "../plugins/install.ts";
|
|
25
|
+
import { reportLines, validateCatalog } from "../market/validate.ts";
|
|
26
|
+
import { readFileSync } from "node:fs";
|
|
27
|
+
import { createInterface } from "node:readline";
|
|
28
|
+
|
|
29
|
+
export interface MarketCliDeps {
|
|
30
|
+
cwd?: string;
|
|
31
|
+
home?: string;
|
|
32
|
+
out?: (line: string) => void;
|
|
33
|
+
err?: (line: string) => void;
|
|
34
|
+
/** source access (offline / fixture fetch / catalog paths) */
|
|
35
|
+
registry?: RegistryDeps;
|
|
36
|
+
/** how a clone runs, and force */
|
|
37
|
+
run?: RunDeps;
|
|
38
|
+
/** one masked line (default readSecret) */
|
|
39
|
+
secret?: (prompt: string) => Promise<string>;
|
|
40
|
+
/** one plain line (default: readline on stdin) */
|
|
41
|
+
plain?: (prompt: string) => Promise<string>;
|
|
42
|
+
/** default process.stdin.isTTY === true; a pipe is never consumed by a prompt */
|
|
43
|
+
tty?: boolean;
|
|
44
|
+
/** PATH lookup for the plan's prerequisite row — tests inject a fixed environment */
|
|
45
|
+
prereqEnv?: PrereqEnv;
|
|
46
|
+
/** the model to scale the plan's token estimate for; tests inject, production reads the configured one */
|
|
47
|
+
model?: { provider: string; model: string };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const MARKET_USAGE = [
|
|
51
|
+
"usage: rovecode market <command>",
|
|
52
|
+
" search [query] [--kind mcp|skill|plugin] every source at once: the curated MCP shelf, the MCP registry,",
|
|
53
|
+
" rovecode's skill and plugin catalogs (skills/plugins work offline)",
|
|
54
|
+
" info <id> one item in full: publisher, version, what it installs, what it asks",
|
|
55
|
+
" docs <id> the item's own documentation, as the catalog carries it",
|
|
56
|
+
" install <id|kind:id|git-url|npm-package> [--project] [--as <name>] [--pick N] [--ref <branch|tag|commit>] [--yes] [--force]",
|
|
57
|
+
" shows the plan, asks (masked) for keys by name, then writes",
|
|
58
|
+
" --local / --no-local: an npx server installed ONCE (npm, ~25 MB, starts in 0.4 s not 2 s)",
|
|
59
|
+
" or the npx line as it is; without either, a terminal asks and --yes keeps npx",
|
|
60
|
+
" --dry-run shows the plan and stops; nothing is fetched or written",
|
|
61
|
+
" remove <id|kind:id> [--project] undo an install of any kind",
|
|
62
|
+
" list [--all] [--kind mcp|skill|plugin] what is installed here (--all: the whole market, with badges)",
|
|
63
|
+
" update [id] [--all] [--yes] what is out of date; with an id or --all: plan, approve, reinstall",
|
|
64
|
+
" --all --yes skips plugins (new code): name one, or pass --yes-plugins",
|
|
65
|
+
" sources [probe] where rows come from right now; really asks the registry (--offline to skip)",
|
|
66
|
+
" verify [id] re-hash what is installed and say what has changed since",
|
|
67
|
+
" validate <path|url> [--kind skill|plugin] check a catalog you wrote before anyone trusts it: what would",
|
|
68
|
+
" load, what would be dropped, and which fields will not survive",
|
|
69
|
+
"every command takes --json · --offline skips the network entirely",
|
|
70
|
+
"an id is a bare slug inside its kind (filesystem); say mcp:filesystem when two kinds share a name",
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
/** Flags every subcommand reads. */
|
|
74
|
+
const COMMON_FLAGS: ReadonlySet<string> = new Set(["--json", "--offline"]);
|
|
75
|
+
/** Flags that take a value — the token after them is never a positional. */
|
|
76
|
+
const VALUE_FLAGS: ReadonlySet<string> = new Set(["--as", "--pick", "--kind", "--ref"]);
|
|
77
|
+
/** What each subcommand reads, beyond COMMON_FLAGS. A flag not listed for the subcommand it was given to is
|
|
78
|
+
* a usage error (exit 2), whether or not another subcommand knows it — `list --kind mcp` used to be
|
|
79
|
+
* accepted and ignored. Pinned by test/unit/market-cmd-flags.test.ts, one case per subcommand. */
|
|
80
|
+
export const SUBCOMMAND_FLAGS: Readonly<Record<string, ReadonlySet<string>>> = {
|
|
81
|
+
search: new Set(["--kind"]),
|
|
82
|
+
info: new Set(),
|
|
83
|
+
docs: new Set(),
|
|
84
|
+
install: new Set(["--project", "--as", "--pick", "--ref", "--yes", "--force", "--dry-run", "--local", "--no-local"]),
|
|
85
|
+
remove: new Set(["--project", "--yes"]),
|
|
86
|
+
list: new Set(["--all", "--kind"]),
|
|
87
|
+
update: new Set(["--all", "--yes", "--yes-plugins", "--dry-run"]),
|
|
88
|
+
sources: new Set(),
|
|
89
|
+
verify: new Set(),
|
|
90
|
+
validate: new Set(["--kind"]),
|
|
91
|
+
help: new Set(),
|
|
92
|
+
};
|
|
93
|
+
/** every flag some subcommand takes — only to word the error: "does not take" vs "unknown flag" */
|
|
94
|
+
const KNOWN_FLAGS: ReadonlySet<string> = new Set([...COMMON_FLAGS, ...Object.values(SUBCOMMAND_FLAGS).flatMap((s) => [...s])]);
|
|
95
|
+
|
|
96
|
+
/** C0/C1 control characters, minus the three that are legitimately part of a text file (tab, newline,
|
|
97
|
+
* carriage return). A catalog body is UNTRUSTED text from a third party: printed raw it can clear the
|
|
98
|
+
* screen, retitle the window, or hide itself with ESC[8m. The TUI already strips this (sextant/
|
|
99
|
+
* market-source.ts, and again in screen.ts); stdout had no such pass. */
|
|
100
|
+
const CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g;
|
|
101
|
+
/** what `market docs` is allowed to put on a terminal */
|
|
102
|
+
export const safeForTerminal = (text: string): string => text.replace(/\r\n?/g, "\n").replace(CONTROL, "");
|
|
103
|
+
|
|
104
|
+
/** a size a person reads, from a byte count */
|
|
105
|
+
const kb = (bytes: number): string => (bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`);
|
|
106
|
+
|
|
107
|
+
const jsonOut = (deps: MarketCliDeps, value: unknown): void => (deps.out ?? console.log)(JSON.stringify(value, null, 2));
|
|
108
|
+
|
|
109
|
+
function badge(row: MarketRow): string {
|
|
110
|
+
// the publisher's status rides along with the install state rather than replacing it: "installed" and
|
|
111
|
+
// "archived" are both true at once and a reader needs both — the second is why they might remove it
|
|
112
|
+
const status = row.status ? ` [${row.status}]` : "";
|
|
113
|
+
if (!row.installed) return status;
|
|
114
|
+
if (row.installed.updateAvailable) return `${status} [installed ${row.installed.version ?? "?"} · update ${"available"}]`;
|
|
115
|
+
if (row.installed.trusted === false) return `${status} [installed · NOT approved on this machine]`;
|
|
116
|
+
return `${status} [installed]`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** everything about one item, in the order a person asks it */
|
|
120
|
+
export function infoLines(item: MarketItem, cwd: string, home: string): string[] {
|
|
121
|
+
const lines = [
|
|
122
|
+
`${item.title}${item.version ? ` ${item.version}` : ""}${item.status ? ` [${item.status}]` : ""}`,
|
|
123
|
+
` id ${qualify(item)}`,
|
|
124
|
+
` kind ${item.kind === "mcp" ? "MCP server — rovecode launches it and the model gets its tools" : item.kind === "skill" ? "skill — instructions the model reads; files only, nothing runs" : "plugin — a folder of code rovecode loads and runs"}`,
|
|
125
|
+
` publisher ${item.publisher}`,
|
|
126
|
+
` source ${item.source === "curated" ? "curated list (built into rovecode)" : item.source === "registry" ? "MCP registry (registry.modelcontextprotocol.io)" : "rovecode catalog (in the repository)"}`,
|
|
127
|
+
` ${item.description}`,
|
|
128
|
+
];
|
|
129
|
+
if (item.license) lines.push(` licence ${item.license}`);
|
|
130
|
+
lines.push(item.docs
|
|
131
|
+
? ` docs ${kb(item.docs.bytes)} from ${item.docs.source}${item.docs.truncated ? " (truncated)" : ""} — rovecode market docs ${qualify(item)}`
|
|
132
|
+
: ` docs none${item.repository ? ` — try ${item.repository}` : ""}`);
|
|
133
|
+
if (item.repository) lines.push(` repo ${item.repository}`);
|
|
134
|
+
if (item.homepage) lines.push(` home ${item.homepage}`);
|
|
135
|
+
if (item.tags.length) lines.push(` tags ${item.tags.join(", ")}`);
|
|
136
|
+
const { install } = item;
|
|
137
|
+
if (install.kind === "mcp") {
|
|
138
|
+
for (const i of install.entry.installs) lines.push(i.kind === "stdio" ? ` runs ${i.command} ${i.args.join(" ")}` : ` connects ${i.url}`);
|
|
139
|
+
} else if (install.kind === "plugin") {
|
|
140
|
+
lines.push(` installs ${install.git ? `git clone ${install.source}` : `copy of ${install.source}`}${install.subfolder ? ` (subfolder ${install.subfolder})` : ""}`);
|
|
141
|
+
} else {
|
|
142
|
+
lines.push(install.source ? ` installs git clone ${install.source.git}${install.source.subfolder ? ` (${install.source.subfolder})` : ""}` : ` installs ${(install.files ?? []).length} file(s) from the catalog`);
|
|
143
|
+
}
|
|
144
|
+
for (const e of item.env) lines.push(` needs ${e.name}${e.secret ? " (secret, asked masked)" : ""}${e.required ? "" : " (optional)"}${e.description ? ` — ${e.description}` : ""}`);
|
|
145
|
+
const state = installedState(item, cwd, home);
|
|
146
|
+
lines.push(state ? ` installed ${state.path} (${state.scope}${state.version ? `, ${state.version}` : ""}${state.trusted === false ? ", NOT approved here" : ""})` : ` installed no`);
|
|
147
|
+
return lines;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** ask for the plan's variables; null = the human said no or there is no way to ask */
|
|
151
|
+
async function askFor(plan: { asks: { name: string; secret: boolean; description?: string }[]; pending?: string[] }, deps: { secret: (p: string) => Promise<string>; plain: (p: string) => Promise<string>; tty: boolean; err: (l: string) => void }): Promise<Record<string, string>> {
|
|
152
|
+
const answers: Record<string, string> = {};
|
|
153
|
+
for (const a of plan.asks) {
|
|
154
|
+
if (!deps.tty) { deps.err(`${a.name} is not set — it will be written as \${${a.name}} and read from your environment`); continue; }
|
|
155
|
+
const label = `${a.name}${a.description ? ` (${a.description})` : ""}: `;
|
|
156
|
+
const v = (await (a.secret ? deps.secret(label) : deps.plain(label))).trim();
|
|
157
|
+
if (v.length > 0) answers[a.name] = v;
|
|
158
|
+
}
|
|
159
|
+
// a `pending` value is a required argument only the human knows (the directory the filesystem server may
|
|
160
|
+
// touch). It is never secret, and it is keyed by the placeholder text, which is what fillPlan reads.
|
|
161
|
+
for (const p of plan.pending ?? []) {
|
|
162
|
+
if (!deps.tty) continue; // written as the placeholder; the loader names it rather than launching
|
|
163
|
+
const v = (await deps.plain(`${p}: `)).trim();
|
|
164
|
+
if (v.length > 0) answers[p] = v;
|
|
165
|
+
}
|
|
166
|
+
return answers;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** the npx package the chosen install form would run — the thing the install-once offer is about */
|
|
170
|
+
function offeredPackage(item: MarketItem, opts: PlanOptions): NpxPackage | undefined {
|
|
171
|
+
if (item.install.kind !== "mcp") return undefined;
|
|
172
|
+
// a project file is shared with every clone; install-once writes this machine's absolute path — no offer there
|
|
173
|
+
if (opts.scope === "project") return undefined;
|
|
174
|
+
const form = item.install.entry.installs[opts.pick ?? 0];
|
|
175
|
+
return form === undefined ? undefined : npxPackage(form);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** the question, in the human's terms: what it costs, what they get, and that "no" changes nothing */
|
|
179
|
+
function offerLines(pkg: NpxPackage): string[] {
|
|
180
|
+
return [
|
|
181
|
+
`${pkg.spec} would start through npx: ~2 s at every start, re-resolving the package (and asking the npm registry) each time.`,
|
|
182
|
+
`Install it once instead? npm puts the package's code under ~/.rovecode/mcp — typically 20–30 MB and a few seconds, one time;`,
|
|
183
|
+
`it then starts in ~0.4 s and needs no network to start. No keeps the npx line exactly as it is today.`,
|
|
184
|
+
];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** One item, the whole ceremony: plan → show → ask → write. Shared by `install` and `update`, so an
|
|
188
|
+
* update can never become a quieter install that skips the preview. Returns the process exit code. */
|
|
189
|
+
async function installOne(item: MarketItem, opts: PlanOptions, ctx: {
|
|
190
|
+
out: (l: string) => void; err: (l: string) => void; json: boolean; yes: boolean; tty: boolean;
|
|
191
|
+
secret: (p: string) => Promise<string>; plain: (p: string) => Promise<string>;
|
|
192
|
+
run: RunDeps; verb: string; dryRun?: boolean;
|
|
193
|
+
/** --json with several items (`update --all`): each item's document goes here instead of stdout, and the
|
|
194
|
+
* caller prints ONE document around them. Absent = this call owns stdout (`install`, `update <id>`). */
|
|
195
|
+
collect?: (doc: unknown) => void;
|
|
196
|
+
}): Promise<number> {
|
|
197
|
+
const doc = (d: unknown): void => { if (ctx.collect) ctx.collect(d); else jsonOut({ out: ctx.out }, d); };
|
|
198
|
+
// The install-once offer, BEFORE the plan is drawn, so the plan the human then reads is the one that will
|
|
199
|
+
// run. Asked only where a person can answer (a terminal, no --yes, no --json, no --dry-run) and only when
|
|
200
|
+
// nothing decided it already (--local / --no-local, or an update keeping what the record says). Every
|
|
201
|
+
// other path keeps today's npx line: the offer is never silent and never the only way.
|
|
202
|
+
let local = opts.local;
|
|
203
|
+
const offer = offeredPackage(item, opts);
|
|
204
|
+
if (offer !== undefined && local === undefined && !ctx.yes && !ctx.json && !ctx.dryRun && ctx.tty) {
|
|
205
|
+
for (const l of offerLines(offer)) ctx.out(l);
|
|
206
|
+
const a = (await ctx.plain(`install ${offer.spec} once? [y/N] `)).trim().toLowerCase();
|
|
207
|
+
local = a === "y" || a === "yes";
|
|
208
|
+
}
|
|
209
|
+
const plan = planInstall(item, local === undefined ? opts : { ...opts, local });
|
|
210
|
+
if ("error" in plan) { ctx.err(plan.error); if (ctx.json) doc({ ok: false, error: plan.error, id: qualify(item) }); return 1; }
|
|
211
|
+
// In --json mode the plan travels as FIELDS, not as prose printed above the JSON. It used to be both,
|
|
212
|
+
// which meant `market install --json` emitted human lines and then an object on the same stream and
|
|
213
|
+
// nothing could parse the result — a flag whose whole promise is "a script reads what the terminal
|
|
214
|
+
// shows" has to produce one document. The same lines are still there, inside `preview`.
|
|
215
|
+
if (!ctx.json) {
|
|
216
|
+
for (const l of plan.preview) ctx.out(l);
|
|
217
|
+
if (plan.replaces) ctx.out(` replaces ${plan.replaces}`);
|
|
218
|
+
for (const p of plan.pending) ctx.out(` fill in ${p} — after the install, in ${plan.target}`);
|
|
219
|
+
}
|
|
220
|
+
// --dry-run stops HERE: after the plan is complete and before anything is asked for. It is a success,
|
|
221
|
+
// not a refusal — the question was "what would this do", and it has been answered. It also overrides
|
|
222
|
+
// --yes rather than arguing with it: between "show me" and "go ahead", the one that writes nothing wins.
|
|
223
|
+
//
|
|
224
|
+
// What it does NOT do is fetch. A git-sourced skill is not cloned here, so this says what would be
|
|
225
|
+
// written and where, never what is inside the repository — and the sentence below says so rather than
|
|
226
|
+
// letting the silence imply a stronger check than happened.
|
|
227
|
+
if (ctx.dryRun) {
|
|
228
|
+
if (ctx.json) { doc({ dryRun: true, item, target: plan.target, scope: plan.scope,
|
|
229
|
+
preview: plan.preview, asks: plan.asks, pending: plan.pending, ...(plan.replaces ? { replaces: plan.replaces } : {}) }); return 0; }
|
|
230
|
+
ctx.out(`nothing written — --dry-run. ${needsNetwork(item.install) ? "The source was not fetched, so this is the plan, not its contents." : "This is the whole plan."}`);
|
|
231
|
+
return 0;
|
|
232
|
+
}
|
|
233
|
+
if (!ctx.yes) {
|
|
234
|
+
// --json never prompts, terminal or not. A y/N is a question for a person, and in --json mode the
|
|
235
|
+
// preview that would let a person answer it is inside the document rather than on the screen — so
|
|
236
|
+
// asking would mean asking someone to approve a plan they were not shown. The plan is returned with
|
|
237
|
+
// `needsApproval` and exit 1; rerun with --yes, or --dry-run if reading it was the whole point.
|
|
238
|
+
if (ctx.json) {
|
|
239
|
+
doc({ ok: false, needsApproval: true, item, target: plan.target, scope: plan.scope,
|
|
240
|
+
preview: plan.preview, asks: plan.asks, pending: plan.pending, ...(plan.replaces ? { replaces: plan.replaces } : {}) });
|
|
241
|
+
ctx.err(`nothing written: pass --yes to accept this plan, or --dry-run to read it`);
|
|
242
|
+
return 1;
|
|
243
|
+
}
|
|
244
|
+
if (!ctx.tty) { ctx.err(`nothing written: rerun on a terminal, or pass --yes to accept this plan in a script`); return 1; }
|
|
245
|
+
const answer = (await ctx.plain(`${ctx.verb} this? [y/N] `)).trim().toLowerCase();
|
|
246
|
+
if (answer !== "y" && answer !== "yes") { ctx.out("nothing written"); return 1; }
|
|
247
|
+
}
|
|
248
|
+
const answers = await askFor(plan, {
|
|
249
|
+
secret: ctx.secret, plain: ctx.plain,
|
|
250
|
+
// a project scope never takes a typed secret: it is written as ${NAME}
|
|
251
|
+
tty: ctx.tty && !(opts.scope === "project" && plan.asks.some((a) => a.secret)), err: ctx.err,
|
|
252
|
+
});
|
|
253
|
+
const outcome = await runInstall(plan, answers, local === undefined ? opts : { ...opts, local }, ctx.run);
|
|
254
|
+
if (!outcome.ok) { ctx.err(outcome.error); if (ctx.json) doc(outcome); return 1; }
|
|
255
|
+
if (ctx.json) { doc(outcome); return 0; }
|
|
256
|
+
ctx.out(`${ctx.verb === "update" ? "updated" : "installed"} ${qualify(item)} → ${outcome.target}${outcome.trusted === true ? " (trusted as written)" : ""}`);
|
|
257
|
+
if (outcome.package) {
|
|
258
|
+
ctx.out(` package ${outcome.package.name} ${outcome.package.version} → ${outcome.package.prefix}${outcome.package.integrity !== undefined ? " (integrity recorded in installed.json)" : ""}`);
|
|
259
|
+
// a hole in the record is said, not smoothed over: the reader decides whether it matters
|
|
260
|
+
if (outcome.package.missing) ctx.err(` record incomplete: ${outcome.package.missing.join("; ")}`);
|
|
261
|
+
}
|
|
262
|
+
if (outcome.trusted === false) ctx.err(`that file already held entries you have not approved, so it is NOT trusted yet — rovecode mcp trust`);
|
|
263
|
+
if (outcome.envNames.length) ctx.err(`set ${outcome.envNames.join(", ")} in your environment before the restart`);
|
|
264
|
+
if (outcome.next) ctx.out(outcome.next);
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export async function cmdMarket(args: string[], deps: MarketCliDeps = {}): Promise<number> {
|
|
269
|
+
// The clone cache lives exactly as long as ONE command: `update --all` out of a monorepo clones it once
|
|
270
|
+
// instead of once per item. The cache OWNS every directory in it (plugins/install.ts), so disposing it
|
|
271
|
+
// here is not tidiness — without this finally the clones outlive the process in the temp directory.
|
|
272
|
+
const cloneCache = new Map<string, string>();
|
|
273
|
+
try { return await runMarket(args, { ...deps, run: { ...deps.run, cloneCache } }); }
|
|
274
|
+
catch (e) { (deps.err ?? ((l: string) => console.error(l)))(`market: ${e instanceof Error ? e.message : String(e)}`); return 1; }
|
|
275
|
+
finally { disposeCloneCache(cloneCache); }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function runMarket(args: string[], deps: MarketCliDeps): Promise<number> {
|
|
279
|
+
const out = deps.out ?? console.log;
|
|
280
|
+
const err = deps.err ?? ((l: string) => console.error(l));
|
|
281
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
282
|
+
const home = deps.home ?? rovecodeHome();
|
|
283
|
+
const json = args.includes("--json");
|
|
284
|
+
const offline = args.includes("--offline");
|
|
285
|
+
const scope: MarketScope = args.includes("--project") ? "project" : "user";
|
|
286
|
+
const registry: RegistryDeps = { ...deps.registry, ...(offline ? { offline: true } : {}) };
|
|
287
|
+
const flag = (name: string): string | undefined => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : undefined; };
|
|
288
|
+
const positional = args.filter((a, i) => !a.startsWith("--") && !(i > 0 && VALUE_FLAGS.has(args[i - 1]!)));
|
|
289
|
+
// A usage error is exit 2 and prose on stderr. In --json mode it is ALSO a document on stdout: "one
|
|
290
|
+
// document, every subcommand, every exit code" (docs/market.md) — a script that passed a flag `list`
|
|
291
|
+
// does not take must be able to read why, not just see an empty stdout and a number.
|
|
292
|
+
const usage = (msg: string): 2 => {
|
|
293
|
+
err(msg); err(MARKET_USAGE.join("\n"));
|
|
294
|
+
if (json) jsonOut(deps, { ok: false, error: msg, usage: MARKET_USAGE });
|
|
295
|
+
return 2;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const sub = positional[0];
|
|
299
|
+
if (sub === undefined) return usage("market needs a command");
|
|
300
|
+
const allowed = SUBCOMMAND_FLAGS[sub];
|
|
301
|
+
if (allowed === undefined) return usage(`unknown command "${sub}"`);
|
|
302
|
+
// Per SUBCOMMAND, not against the union of every flag any subcommand takes: that global set accepted
|
|
303
|
+
// `list --kind mcp` and then ignored it, so a caller who asked for MCP servers got a skill row and no
|
|
304
|
+
// error to notice. A flag a subcommand does not read is refused, in the same words for every one.
|
|
305
|
+
for (const a of args) {
|
|
306
|
+
if (!a.startsWith("--")) continue;
|
|
307
|
+
if (COMMON_FLAGS.has(a) || allowed.has(a)) continue;
|
|
308
|
+
return usage(KNOWN_FLAGS.has(a) ? `market ${sub} does not take ${a}` : `unknown flag ${a}`);
|
|
309
|
+
}
|
|
310
|
+
// `help` is the one subcommand whose stdout IS the usage text, --json or not: it is for a person
|
|
311
|
+
if (sub === "help") { out(MARKET_USAGE.join("\n")); return 0; }
|
|
312
|
+
const kindFilter = (): MarketKind | undefined | 2 => {
|
|
313
|
+
const kind = flag("--kind");
|
|
314
|
+
if (kind === undefined) return undefined;
|
|
315
|
+
return ["mcp", "skill", "plugin"].includes(kind) ? (kind as MarketKind) : usage(`--kind takes mcp, skill or plugin`);
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// ---------------- search
|
|
319
|
+
if (sub === "search") {
|
|
320
|
+
const kind = kindFilter();
|
|
321
|
+
if (kind === 2) return 2;
|
|
322
|
+
const query = positional.slice(1).join(" ");
|
|
323
|
+
const r = await searchMarket(query, registry);
|
|
324
|
+
const items = kind ? r.items.filter((i) => i.kind === kind) : r.items;
|
|
325
|
+
const rows = withInstalled(items, cwd, home);
|
|
326
|
+
// exit code carries the same signal as the text form: a script piping --json must not read "no
|
|
327
|
+
// matches" as success when the human-readable run would have said otherwise
|
|
328
|
+
if (json) { jsonOut(deps, { items: rows, sources: r.sources, notes: r.notes }); return rows.length === 0 ? 1 : 0; }
|
|
329
|
+
for (const n of r.notes) err(`market: ${n}`);
|
|
330
|
+
if (rows.length === 0) {
|
|
331
|
+
const dead = Object.entries(r.sources).filter(([, s]) => !s.ok);
|
|
332
|
+
err(query ? `nothing matches "${query}"` : "the market is empty");
|
|
333
|
+
for (const [name, s] of dead) if (!s.ok) err(` ${name}: ${s.reason}`);
|
|
334
|
+
return 1;
|
|
335
|
+
}
|
|
336
|
+
for (const row of rows) out(`${itemLine(row)}${badge(row)}`);
|
|
337
|
+
return 0;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ---------------- sources
|
|
341
|
+
if (sub === "sources") {
|
|
342
|
+
// `sources` exists to answer "is the registry up?", and it was the one command that never asked: it
|
|
343
|
+
// ran the empty query, which by design never leaves the machine, and then reported the registry as
|
|
344
|
+
// "not consulted". A real probe with a real term is the whole job.
|
|
345
|
+
const probe = positional.slice(1).join(" ") || "mcp";
|
|
346
|
+
const r = offline ? await allItems(registry) : await searchMarket(probe, registry);
|
|
347
|
+
if (json) { jsonOut(deps, { sources: r.sources, notes: r.notes, count: r.items.length, probe: offline ? null : probe, offline }); return Object.values(r.sources).every((x) => x.ok) ? 0 : 1; }
|
|
348
|
+
if (offline) out(`--offline: the registry was not asked`);
|
|
349
|
+
else out(`probed with "${probe}"`);
|
|
350
|
+
for (const [name, s] of Object.entries(r.sources)) {
|
|
351
|
+
const how = !s.ok ? `FAILED — ${s.reason}`
|
|
352
|
+
: s.from === "live" ? "answered just now"
|
|
353
|
+
: s.from === "cache" ? `from the cache${s.ageMs ? ` (${Math.round(s.ageMs / 1000)}s old)` : ""}`
|
|
354
|
+
: s.from === "skipped" ? `not consulted — ${s.why}`
|
|
355
|
+
: "built in / on disk";
|
|
356
|
+
out(`${name.padEnd(14)} ${how}`);
|
|
357
|
+
}
|
|
358
|
+
out(`${String(r.items.length).padStart(14)} items visible right now`);
|
|
359
|
+
return Object.values(r.sources).every((s) => s.ok) ? 0 : 1;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ---------------- verify
|
|
363
|
+
if (sub === "verify") {
|
|
364
|
+
// `--ref` pins what was ASKED for. This is what ARRIVED, checked again now. It detects drift; it does
|
|
365
|
+
// not prove provenance, and nothing here claims otherwise — nobody in this space signs anything yet.
|
|
366
|
+
const which = positional[1];
|
|
367
|
+
const rows: { id: string; result: ReturnType<typeof verifyDigest> }[] = [];
|
|
368
|
+
for (const scope of ["user", "project"] as const) {
|
|
369
|
+
for (const record of readManifest(scope, cwd, home)) {
|
|
370
|
+
const id = `${record.kind}:${record.id}`;
|
|
371
|
+
if (which !== undefined && which !== id && which !== record.id) continue;
|
|
372
|
+
rows.push({ id, result: record.kind === "mcp"
|
|
373
|
+
? { state: "not-applicable", why: "an MCP entry is a line inside a shared mcp.json, not a folder of its own" }
|
|
374
|
+
: verifyDigest(record.digest, record.target) });
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
// the same exit rule as the text path below, including the one that is easy to lose here: naming an id
|
|
378
|
+
// that has no record is a 1. An empty array with a 0 reads as "checked it, all fine", which is the
|
|
379
|
+
// opposite of what happened — nothing was checked, because nothing was found.
|
|
380
|
+
if (json) {
|
|
381
|
+
jsonOut(deps, rows);
|
|
382
|
+
if (rows.length === 0) return which !== undefined ? 1 : 0;
|
|
383
|
+
return rows.some((r) => r.result.state === "changed" || r.result.state === "missing") ? 1 : 0;
|
|
384
|
+
}
|
|
385
|
+
if (rows.length === 0) {
|
|
386
|
+
out(which !== undefined ? `nothing recorded for "${which}"` : "nothing installed through the market yet");
|
|
387
|
+
return which !== undefined ? 1 : 0;
|
|
388
|
+
}
|
|
389
|
+
for (const r of rows) out(verifyLine(r.id, r.result));
|
|
390
|
+
const bad = rows.filter((r) => r.result.state === "changed" || r.result.state === "missing").length;
|
|
391
|
+
if (bad > 0) err(`${bad} item${bad === 1 ? " is" : "s are"} not what was installed — reinstall with \`market install <id> --force\`, or keep the edit`);
|
|
392
|
+
return bad > 0 ? 1 : 0;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ---------------- list
|
|
396
|
+
if (sub === "list") {
|
|
397
|
+
const all = args.includes("--all");
|
|
398
|
+
const kind = kindFilter();
|
|
399
|
+
if (kind === 2) return 2;
|
|
400
|
+
const r = await allItems(registry);
|
|
401
|
+
const rows = withInstalled(r.items, cwd, home).filter((row) => (all || row.installed) && (kind === undefined || row.kind === kind));
|
|
402
|
+
// where each installed row came from: the catalog row, the clone URL, the commit. The disk still says
|
|
403
|
+
// WHETHER it is installed; the manifest says where it came from, and says so honestly when it cannot.
|
|
404
|
+
const withOrigin = rows.map((row) => row.installed
|
|
405
|
+
? { ...row, origin: recordFor(row, row.installed.scope, cwd, home) ?? null }
|
|
406
|
+
: row);
|
|
407
|
+
if (json) { jsonOut(deps, withOrigin); return 0; }
|
|
408
|
+
if (rows.length === 0) {
|
|
409
|
+
const what = kind === undefined ? "" : `${kind === "mcp" ? "MCP server" : kind} `;
|
|
410
|
+
out(all ? (kind === undefined ? "the market is empty" : `the market has no ${what}items`)
|
|
411
|
+
: `${kind === undefined ? "nothing " : `no ${what}`}installed here yet — \`rovecode market search${kind ? ` --kind ${kind}` : ""}\` to look around`);
|
|
412
|
+
return 0;
|
|
413
|
+
}
|
|
414
|
+
for (const row of rows) {
|
|
415
|
+
out(`${itemLine(row)}${badge(row)}`);
|
|
416
|
+
if (row.installed) out(` from ${originLine(recordFor(row, row.installed.scope, cwd, home))}`);
|
|
417
|
+
}
|
|
418
|
+
return 0;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ---------------- update
|
|
422
|
+
if (sub === "update") {
|
|
423
|
+
const r = await allItems(registry);
|
|
424
|
+
const installed = withInstalled(r.items, cwd, home).filter((row) => row.installed);
|
|
425
|
+
// "stale" is either a version that differs, or a version nobody can compare — a skill without one in
|
|
426
|
+
// its SKILL.md is NOT silently skipped, it is offered as a reinstall and says why
|
|
427
|
+
const stale = installed.filter((row) => row.installed!.updateAvailable === true || row.version === undefined || row.installed!.version === undefined);
|
|
428
|
+
const which = positional[1];
|
|
429
|
+
const all = args.includes("--all");
|
|
430
|
+
|
|
431
|
+
if (which === undefined && !all) { // the dry list
|
|
432
|
+
if (json) { jsonOut(deps, stale); return 0; }
|
|
433
|
+
if (stale.length === 0) { out("everything installed is at the catalog's version"); return 0; }
|
|
434
|
+
for (const row of stale) {
|
|
435
|
+
const from = row.installed!.version, to = row.version;
|
|
436
|
+
out(from !== undefined && to !== undefined
|
|
437
|
+
? `${qualify(row)} ${from} → ${to}`
|
|
438
|
+
: `${qualify(row)} version unknown (${from === undefined ? "nothing on disk says one" : "the catalog states none"}) — updating reinstalls it`);
|
|
439
|
+
}
|
|
440
|
+
out(`rovecode market update <id> · rovecode market update --all`);
|
|
441
|
+
return 0;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// `--all --yes` must not silently re-clone every PLUGIN from whatever its source's HEAD says today and
|
|
445
|
+
// re-record project trust for the result: that is running new code with no question asked. A skill is
|
|
446
|
+
// text and a server entry is config, so those go; plugins need `--yes-plugins` or a named update.
|
|
447
|
+
const skipPlugins = all && args.includes("--yes") && !args.includes("--yes-plugins");
|
|
448
|
+
let targets = stale;
|
|
449
|
+
if (which !== undefined) {
|
|
450
|
+
const pick = await resolveTarget(which, registry);
|
|
451
|
+
if (!pick.ok) { err(pick.error); if (json) jsonOut(deps, { ok: false, error: pick.error, candidates: pick.ambiguous ?? [] }); return pick.ambiguous ? 2 : 1; }
|
|
452
|
+
const row = installed.find((x) => x.id === pick.item.id && x.kind === pick.item.kind);
|
|
453
|
+
if (row === undefined) {
|
|
454
|
+
const msg = `${qualify(pick.item)} is not installed here — rovecode market install ${qualify(pick.item)}`;
|
|
455
|
+
err(msg); if (json) jsonOut(deps, { ok: false, error: msg, id: qualify(pick.item), installed: false });
|
|
456
|
+
return 1;
|
|
457
|
+
}
|
|
458
|
+
targets = [row];
|
|
459
|
+
}
|
|
460
|
+
const skipped = skipPlugins ? targets.filter((r) => r.kind === "plugin") : [];
|
|
461
|
+
if (skipped.length) targets = targets.filter((r) => r.kind !== "plugin");
|
|
462
|
+
// In --json mode the whole update is ONE document, however many items it touched: each item's own
|
|
463
|
+
// outcome (the same object `install --json` prints) is collected under `results`, the plugins that
|
|
464
|
+
// `--all --yes` set aside under `skipped`. It used to print one document per item and, with nothing
|
|
465
|
+
// to do, a sentence — so `update --all --yes --json` was parseable only when exactly one item was stale.
|
|
466
|
+
const results: unknown[] = [];
|
|
467
|
+
const wrap = (ok: boolean): void => { if (json) jsonOut(deps, { ok, results, skipped: skipped.map(qualify) }); };
|
|
468
|
+
if (targets.length === 0 && skipped.length === 0) { if (json) wrap(true); else out("everything installed is at the catalog's version"); return 0; }
|
|
469
|
+
|
|
470
|
+
const ctx = {
|
|
471
|
+
out, err, json, yes: args.includes("--yes"), tty: deps.tty ?? process.stdin.isTTY === true,
|
|
472
|
+
secret: deps.secret ?? readSecret, plain: deps.plain ?? defaultPlain, verb: "update", dryRun: args.includes("--dry-run"),
|
|
473
|
+
run: { ...deps.run, force: true, ...(offline ? { offline: true } : {}) },
|
|
474
|
+
...(json ? { collect: (doc: unknown) => { results.push(doc); } } : {}),
|
|
475
|
+
};
|
|
476
|
+
let worst = 0;
|
|
477
|
+
if (skipped.length && !json) {
|
|
478
|
+
out(`${skipped.length} plugin${skipped.length > 1 ? "s" : ""} skipped — a plugin update runs new code: ${skipped.map(qualify).join(", ")}`);
|
|
479
|
+
out(` rovecode market update <id> --yes · or --yes-plugins to take them all`);
|
|
480
|
+
}
|
|
481
|
+
for (const row of targets) {
|
|
482
|
+
// an item is updated in the scope it is installed in, not the flag's default. An MCP server that was
|
|
483
|
+
// installed ONCE stays installed once (the record says so); one on an npx line stays on npx — an update
|
|
484
|
+
// never asks the install-once question, because it must not change how the server starts.
|
|
485
|
+
const keepLocal = row.kind === "mcp" ? recordFor(row, row.installed!.scope, cwd, home)?.package !== undefined : undefined;
|
|
486
|
+
const opts: PlanOptions = { scope: row.installed!.scope, cwd, home, ...(deps.prereqEnv !== undefined ? { prereqEnv: deps.prereqEnv } : {}),
|
|
487
|
+
...(keepLocal !== undefined ? { local: keepLocal } : {}) };
|
|
488
|
+
const code = await installOne(row, opts, ctx);
|
|
489
|
+
if (code !== 0) worst = code;
|
|
490
|
+
}
|
|
491
|
+
wrap(worst === 0);
|
|
492
|
+
return worst;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const target = positional[1];
|
|
496
|
+
if (["info", "install", "remove", "docs"].includes(sub) && target === undefined) return usage(`market ${sub} needs a name`);
|
|
497
|
+
|
|
498
|
+
// ---------------- info
|
|
499
|
+
if (sub === "info") {
|
|
500
|
+
// `install` may reasonably treat an unknown dashed word as an npm package — the human is naming a
|
|
501
|
+
// package to install. `info` and `docs` must NOT: a typo would come back as a confident record for a
|
|
502
|
+
// server nobody has ever published ("runs npx -y totally-bogus-name") with exit 0, and there would be
|
|
503
|
+
// no way left to ask "does this exist?".
|
|
504
|
+
const r = await resolveTarget(target!, registry);
|
|
505
|
+
if (r.ok && r.item.source === "catalog" && r.item.publisher.startsWith("unknown (") && r.item.kind === "mcp") {
|
|
506
|
+
err(`"${target}" is not in the catalog or the registry — \`market install\` would treat it as an npm package, but there is nothing here to describe`);
|
|
507
|
+
if (json) jsonOut(deps, { error: "not found", id: target });
|
|
508
|
+
return 1;
|
|
509
|
+
}
|
|
510
|
+
if (!r.ok) { err(r.error); if (json) jsonOut(deps, { error: r.error, candidates: r.ambiguous ?? [] }); return r.ambiguous ? 2 : 1; }
|
|
511
|
+
if (json) { jsonOut(deps, { ...r.item, installed: installedState(r.item, cwd, home) }); return 0; }
|
|
512
|
+
for (const l of infoLines(r.item, cwd, home)) out(l);
|
|
513
|
+
return 0;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ---------------- docs
|
|
517
|
+
if (sub === "docs") {
|
|
518
|
+
const r = await resolveTarget(target!, registry);
|
|
519
|
+
if (r.ok && r.item.source === "catalog" && r.item.publisher.startsWith("unknown (") && r.item.kind === "mcp") {
|
|
520
|
+
err(`"${target}" is not in the catalog or the registry — nothing here has documentation`);
|
|
521
|
+
if (json) jsonOut(deps, { error: "not found", id: target });
|
|
522
|
+
return 1;
|
|
523
|
+
}
|
|
524
|
+
if (!r.ok) { err(r.error); if (json) jsonOut(deps, { error: r.error, candidates: r.ambiguous ?? [] }); return r.ambiguous ? 2 : 1; }
|
|
525
|
+
const d = r.item.docs;
|
|
526
|
+
if (!d || d.body === undefined) {
|
|
527
|
+
// never silently empty: say where the documentation would be if the reader wants to go looking
|
|
528
|
+
const where = r.item.repository ?? r.item.homepage;
|
|
529
|
+
err(`${qualify(r.item)} carries no documentation in the catalog${where ? ` — the publisher's own is at ${where}` : ""}`);
|
|
530
|
+
if (json) jsonOut(deps, { id: qualify(r.item), docs: null, ...(where ? { repository: where } : {}) });
|
|
531
|
+
return 1;
|
|
532
|
+
}
|
|
533
|
+
if (json) { jsonOut(deps, { id: qualify(r.item), docs: d }); return 0; }
|
|
534
|
+
out(safeForTerminal(d.body));
|
|
535
|
+
if (d.truncated) err(`— truncated: ${kb(d.bytes)} upstream, read the rest at ${d.source}`);
|
|
536
|
+
return 0;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ---------------- validate
|
|
540
|
+
if (sub === "validate") {
|
|
541
|
+
if (target === undefined) return usage("usage: rovecode market validate <path|url> [--kind skill|plugin]");
|
|
542
|
+
const kindFlag = flag("--kind");
|
|
543
|
+
if (kindFlag !== undefined && kindFlag !== "skill" && kindFlag !== "plugin") return usage("validate takes --kind skill or --kind plugin");
|
|
544
|
+
|
|
545
|
+
let text: string;
|
|
546
|
+
const isUrl = /^https?:\/\//i.test(target);
|
|
547
|
+
// an unreadable path or a failed fetch is a document too: the report a script asked for says why there is none
|
|
548
|
+
const unreadable = (msg: string): 1 => { err(msg); if (json) jsonOut(deps, { ok: false, error: msg, source: target }); return 1; };
|
|
549
|
+
if (isUrl) {
|
|
550
|
+
// a URL is the network, so --offline means it: the flag says "skips the network entirely"
|
|
551
|
+
if (offline) return usage(`--offline and a URL cannot both be meant — give a local path, or drop --offline`);
|
|
552
|
+
try {
|
|
553
|
+
const res = await fetch(target, { headers: { "user-agent": "rovecode-market-validate" } });
|
|
554
|
+
if (!res.ok) return unreadable(`${target}: HTTP ${res.status}`);
|
|
555
|
+
text = await res.text();
|
|
556
|
+
} catch (e) { return unreadable(`${target}: ${e instanceof Error ? e.message : String(e)}`); }
|
|
557
|
+
} else {
|
|
558
|
+
try { text = readFileSync(target, "utf8"); }
|
|
559
|
+
catch (e) { return unreadable(`${target}: ${e instanceof Error ? e.message : String(e)}`); }
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const report = validateCatalog(text, {
|
|
563
|
+
...(kindFlag ? { kind: kindFlag as "skill" | "plugin" } : {}),
|
|
564
|
+
filename: target,
|
|
565
|
+
});
|
|
566
|
+
if (json) { jsonOut(deps, report); return report.ok ? 0 : 1; }
|
|
567
|
+
for (const line of reportLines(report, target)) out(line);
|
|
568
|
+
return report.ok ? 0 : 1;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// ---------------- remove
|
|
572
|
+
if (sub === "remove") {
|
|
573
|
+
// every exit from here is a document in --json mode, including the ones that only used to write to
|
|
574
|
+
// stderr. A script that asks to remove something it already removed gets an answer it can read, not
|
|
575
|
+
// an empty stdout and a number
|
|
576
|
+
const r = await resolveTarget(target!, registry);
|
|
577
|
+
if (!r.ok) { err(r.error); if (json) jsonOut(deps, { ok: false, error: r.error, candidates: r.ambiguous ?? [] }); return r.ambiguous ? 2 : 1; }
|
|
578
|
+
// install writes nothing without a yes; remove deleted a folder in silence. Same rule both ways.
|
|
579
|
+
const state = installedState(r.item, cwd, home, args.includes("--project") ? "project" : undefined);
|
|
580
|
+
if (state === undefined) {
|
|
581
|
+
const msg = `${qualify(r.item)} is not installed here`;
|
|
582
|
+
err(msg); if (json) jsonOut(deps, { ok: false, error: msg, id: qualify(r.item), installed: false });
|
|
583
|
+
return 1;
|
|
584
|
+
}
|
|
585
|
+
const tty = deps.tty ?? process.stdin.isTTY === true;
|
|
586
|
+
if (!args.includes("--yes")) {
|
|
587
|
+
// --json never prompts, for the same reason install does not: the line that tells you WHAT you are
|
|
588
|
+
// about to delete belongs in the document, and a y/N without it is a question nobody can answer
|
|
589
|
+
if (json) {
|
|
590
|
+
const msg = `nothing removed: pass --yes to confirm`;
|
|
591
|
+
jsonOut(deps, { ok: false, needsApproval: true, id: qualify(r.item), path: state.path, scope: state.scope });
|
|
592
|
+
err(msg); return 1;
|
|
593
|
+
}
|
|
594
|
+
if (!tty) { err(`nothing removed: ${qualify(r.item)} lives at ${state.path} — rerun on a terminal, or pass --yes`); return 1; }
|
|
595
|
+
out(`${qualify(r.item)} ${state.path}${state.scope === "project" ? " (this repo)" : ""}`);
|
|
596
|
+
const answer = (await (deps.plain ?? defaultPlain)("remove this? [y/N] ")).trim().toLowerCase();
|
|
597
|
+
if (answer !== "y" && answer !== "yes") { out("nothing removed"); return 1; }
|
|
598
|
+
}
|
|
599
|
+
const done = removeItem(r.item, cwd, home, args.includes("--project") ? "project" : undefined);
|
|
600
|
+
if (!done.ok) { err(done.error); if (json) jsonOut(deps, { ok: false, error: done.error, id: qualify(r.item) }); return 1; }
|
|
601
|
+
if (json) { jsonOut(deps, { ok: true, removed: qualify(r.item), path: done.path }); return 0; }
|
|
602
|
+
out(`removed ${qualify(r.item)} from ${done.path}`);
|
|
603
|
+
return 0;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// ---------------- install
|
|
607
|
+
if (sub === "install") {
|
|
608
|
+
const r = await resolveTarget(target!, registry);
|
|
609
|
+
if (!r.ok) {
|
|
610
|
+
err(r.error);
|
|
611
|
+
if (json) jsonOut(deps, { error: r.error, candidates: r.ambiguous ?? [] });
|
|
612
|
+
else for (const c of r.ambiguous ?? []) err(` ${qualify(c)} ${c.description}`);
|
|
613
|
+
return r.ambiguous ? 2 : 1;
|
|
614
|
+
}
|
|
615
|
+
const pickRaw = flag("--pick");
|
|
616
|
+
const pick = pickRaw === undefined ? undefined : Number(pickRaw);
|
|
617
|
+
if (pick !== undefined && !Number.isInteger(pick)) return usage(`--pick takes a number`);
|
|
618
|
+
const asName = flag("--as");
|
|
619
|
+
// the configured default model, so the estimate is scaled to the tokenizer the person actually runs.
|
|
620
|
+
// Nothing configured → undefined, and the line says the numbers are unscaled rather than guessing.
|
|
621
|
+
const model = deps.model ?? (await defaultModelRef());
|
|
622
|
+
const ref = flag("--ref");
|
|
623
|
+
if (ref !== undefined && ref.trim() === "") return usage(`--ref needs a branch, tag or commit`);
|
|
624
|
+
// --local / --no-local decide the install-once question up front; neither → it is asked on a terminal
|
|
625
|
+
const local = args.includes("--local") ? true : args.includes("--no-local") ? false : undefined;
|
|
626
|
+
const opts = { scope, cwd, home, ...(pick !== undefined ? { pick } : {}), ...(asName !== undefined ? { as: asName } : {}),
|
|
627
|
+
...(ref !== undefined ? { ref } : {}), ...(deps.prereqEnv !== undefined ? { prereqEnv: deps.prereqEnv } : {}),
|
|
628
|
+
...(model !== undefined ? { model } : {}), ...(local !== undefined ? { local } : {}) };
|
|
629
|
+
return installOne(r.item, opts, {
|
|
630
|
+
out, err, json, yes: args.includes("--yes"), tty: deps.tty ?? process.stdin.isTTY === true,
|
|
631
|
+
secret: deps.secret ?? readSecret, plain: deps.plain ?? defaultPlain, verb: "install", dryRun: args.includes("--dry-run"),
|
|
632
|
+
// --force was accepted, documented, and read by nobody: the plan said "replaces …" and the write
|
|
633
|
+
// then refused with "already exists (use --force to replace)" — asking for the flag the user passed.
|
|
634
|
+
run: { ...deps.run, ...(args.includes("--force") ? { force: true } : {}), ...(offline ? { offline: true } : {}) },
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// every name in SUBCOMMAND_FLAGS is handled above; an unknown one was refused before the branches
|
|
639
|
+
return usage(`unknown command "${sub}"`);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** The configured default provider/model, or undefined. Imported lazily: `market search` has no business
|
|
643
|
+
* loading the provider stack, and this is only wanted while drawing an install plan. Never throws — a
|
|
644
|
+
* broken provider config must not stop an install. */
|
|
645
|
+
async function defaultModelRef(): Promise<{ provider: string; model: string } | undefined> {
|
|
646
|
+
try {
|
|
647
|
+
const { resolveProvider } = await import("../providers/stream.ts");
|
|
648
|
+
const cfg = resolveProvider();
|
|
649
|
+
if (!cfg) return undefined;
|
|
650
|
+
const model = process.env.ROVECODE_MODEL ?? cfg.defaultModel;
|
|
651
|
+
return model ? { provider: cfg.id, model } : undefined;
|
|
652
|
+
} catch { return undefined; }
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
async function defaultPlain(prompt: string): Promise<string> {
|
|
656
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
657
|
+
try { return await new Promise<string>((res) => rl.question(prompt, res)); } finally { rl.close(); }
|
|
658
|
+
}
|