magus-cli 0.0.0-placeholder → 7.0.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.
Files changed (331) hide show
  1. package/README.md +309 -0
  2. package/bin/magus.js +170 -0
  3. package/package.json +67 -3
  4. package/scripts/build-binaries.ts +113 -0
  5. package/scripts/capture-builtin.ts +412 -0
  6. package/scripts/check-optional-deps.ts +49 -0
  7. package/scripts/test-isolated.ts +49 -0
  8. package/scripts/verify-binary.ts +95 -0
  9. package/scripts/verify-community-registry.ts +272 -0
  10. package/src/__tests__/active-closure.test.ts +184 -0
  11. package/src/__tests__/alias-adopt.test.ts +365 -0
  12. package/src/__tests__/alias-parser.test.ts +417 -0
  13. package/src/__tests__/alias-shell-writer.test.ts +681 -0
  14. package/src/__tests__/alias-store.test.ts +163 -0
  15. package/src/__tests__/binary-signing.test.ts +120 -0
  16. package/src/__tests__/blackbox-active-closure.test.ts +119 -0
  17. package/src/__tests__/blackbox-convergence.test.ts +334 -0
  18. package/src/__tests__/blackbox-parity.test.ts +684 -0
  19. package/src/__tests__/blackbox-plugin-state.test.ts +523 -0
  20. package/src/__tests__/blackbox-tool-state.test.ts +669 -0
  21. package/src/__tests__/cache-provenance.test.ts +84 -0
  22. package/src/__tests__/capture-builtin.test.ts +92 -0
  23. package/src/__tests__/catalog-cache-store.test.ts +309 -0
  24. package/src/__tests__/catalog-notice.test.ts +155 -0
  25. package/src/__tests__/cli-ansi.test.ts +297 -0
  26. package/src/__tests__/cli-apply-seams.test.ts +288 -0
  27. package/src/__tests__/cli-live.test.ts +391 -0
  28. package/src/__tests__/cli-router.test.ts +240 -0
  29. package/src/__tests__/cli-tool-commands.test.ts +239 -0
  30. package/src/__tests__/cli-tools-adapter.test.ts +294 -0
  31. package/src/__tests__/cli-update-view.test.ts +445 -0
  32. package/src/__tests__/community-fetch.test.ts +545 -0
  33. package/src/__tests__/community-registry.test.ts +269 -0
  34. package/src/__tests__/community-staleness.test.ts +720 -0
  35. package/src/__tests__/component-badge.test.ts +98 -0
  36. package/src/__tests__/component-icons.test.ts +94 -0
  37. package/src/__tests__/content-drift.test.ts +353 -0
  38. package/src/__tests__/conventions-integration.test.ts +830 -0
  39. package/src/__tests__/conventions-manager.test.ts +1236 -0
  40. package/src/__tests__/doctor-bins.test.ts +43 -0
  41. package/src/__tests__/doctor.test.ts +463 -0
  42. package/src/__tests__/dotenv.test.ts +195 -0
  43. package/src/__tests__/enabled-not-installed.test.ts +94 -0
  44. package/src/__tests__/file-locking.test.ts +208 -0
  45. package/src/__tests__/fixtures/tool-probe/brew-list-versions-tmux.txt +2 -0
  46. package/src/__tests__/fixtures/tool-probe/brew-outdated-behind-homebrew-spec.txt +29 -0
  47. package/src/__tests__/fixtures/tool-probe/brew-outdated-tmux-current.txt +7 -0
  48. package/src/__tests__/fixtures/tool-probe/brew-outdated-unknown-formula.txt +6 -0
  49. package/src/__tests__/fixtures/tool-probe/go-version-m-tmux-mcp.txt +27 -0
  50. package/src/__tests__/fixtures/tool-probe/npm-dist-tags-missing-scoped.txt +4 -0
  51. package/src/__tests__/fixtures/tool-probe/npm-dist-tags-openai-codex.txt +4 -0
  52. package/src/__tests__/fixtures/tool-probe/pypi-cowsay.txt +3 -0
  53. package/src/__tests__/fixtures/tool-probe/uv-receipt-browser-use.toml +10 -0
  54. package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-git.toml +8 -0
  55. package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-pinned.toml +8 -0
  56. package/src/__tests__/fixtures/tool-probe/uv-receipt-graphifyy.toml +7 -0
  57. package/src/__tests__/fixtures/tool-probe/uv-receipt-mcp.toml +6 -0
  58. package/src/__tests__/fixtures/tool-probe/uv-receipt-serena-agent.toml +9 -0
  59. package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated-pinned.txt +5 -0
  60. package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated.txt +11 -0
  61. package/src/__tests__/fixtures/tool-probe/uv-tool-list.txt +22 -0
  62. package/src/__tests__/footer-hints.test.ts +40 -0
  63. package/src/__tests__/format-command.test.ts +78 -0
  64. package/src/__tests__/git-worktree.test.ts +108 -0
  65. package/src/__tests__/github-budget.test.ts +206 -0
  66. package/src/__tests__/gitignore-check.test.ts +71 -0
  67. package/src/__tests__/gitignore-detector.test.ts +176 -0
  68. package/src/__tests__/gitignore-fixer.test.ts +317 -0
  69. package/src/__tests__/gitignore-resolver.test.ts +193 -0
  70. package/src/__tests__/gitignore-service.test.ts +137 -0
  71. package/src/__tests__/go-module.test.ts +43 -0
  72. package/src/__tests__/hook-import-policy.test.ts +90 -0
  73. package/src/__tests__/hook-process.test.ts +256 -0
  74. package/src/__tests__/hook-registration.test.ts +224 -0
  75. package/src/__tests__/install-command.test.ts +81 -0
  76. package/src/__tests__/install-plan.test.ts +104 -0
  77. package/src/__tests__/launcher-signal.test.ts +308 -0
  78. package/src/__tests__/local-marketplace-scan.test.ts +174 -0
  79. package/src/__tests__/manifest.test.ts +281 -0
  80. package/src/__tests__/markdown-renderer.test.ts +142 -0
  81. package/src/__tests__/marketplace-badge.test.ts +140 -0
  82. package/src/__tests__/marketplace-refresh.test.ts +335 -0
  83. package/src/__tests__/marketplace-version.test.ts +84 -0
  84. package/src/__tests__/marketplaces.test.ts +128 -0
  85. package/src/__tests__/mate-availability.test.ts +156 -0
  86. package/src/__tests__/mate-catalog.test.ts +295 -0
  87. package/src/__tests__/mcp-registry.test.ts +158 -0
  88. package/src/__tests__/model-visuals.test.tsx +2466 -0
  89. package/src/__tests__/models-adapter.test.ts +332 -0
  90. package/src/__tests__/models-cli.test.ts +273 -0
  91. package/src/__tests__/models-core.test.ts +802 -0
  92. package/src/__tests__/models-manager.test.ts +500 -0
  93. package/src/__tests__/models-presets-marketplace.test.ts +168 -0
  94. package/src/__tests__/models-screen-state.test.ts +315 -0
  95. package/src/__tests__/moved-marketplace.test.ts +86 -0
  96. package/src/__tests__/open-file.test.ts +59 -0
  97. package/src/__tests__/owned-marketplaces.test.ts +93 -0
  98. package/src/__tests__/plugin-checkout.test.ts +66 -0
  99. package/src/__tests__/plugin-cli-argv.test.ts +109 -0
  100. package/src/__tests__/plugin-contents.test.ts +371 -0
  101. package/src/__tests__/plugin-manager-fallback.test.ts +320 -0
  102. package/src/__tests__/plugin-releases.test.ts +98 -0
  103. package/src/__tests__/plugin-requires.test.ts +187 -0
  104. package/src/__tests__/plugin-row-text.test.ts +336 -0
  105. package/src/__tests__/plugin-setup.test.ts +1016 -0
  106. package/src/__tests__/profile-adopt.test.ts +177 -0
  107. package/src/__tests__/profile-command.test.ts +110 -0
  108. package/src/__tests__/profile-materializer.test.ts +238 -0
  109. package/src/__tests__/profile-sync.test.ts +154 -0
  110. package/src/__tests__/registry-version-resolution.test.ts +88 -0
  111. package/src/__tests__/resolve-executable.test.ts +42 -0
  112. package/src/__tests__/resolver.test.ts +351 -0
  113. package/src/__tests__/scope-action.test.ts +134 -0
  114. package/src/__tests__/scope-squares.test.tsx +164 -0
  115. package/src/__tests__/selection-scope.test.ts +100 -0
  116. package/src/__tests__/settings-file.test.ts +179 -0
  117. package/src/__tests__/shell-script-callers.test.ts +92 -0
  118. package/src/__tests__/skill-install-files.test.ts +136 -0
  119. package/src/__tests__/skills-catalog-client.test.ts +48 -0
  120. package/src/__tests__/style-wrap.test.ts +220 -0
  121. package/src/__tests__/styles-manager.test.ts +1098 -0
  122. package/src/__tests__/styles-origins.test.ts +414 -0
  123. package/src/__tests__/styles-screen-state.test.ts +457 -0
  124. package/src/__tests__/styles-status-line.test.ts +72 -0
  125. package/src/__tests__/styles-sync.test.ts +452 -0
  126. package/src/__tests__/symlink-manager.test.ts +180 -0
  127. package/src/__tests__/tabbar-layout.test.ts +100 -0
  128. package/src/__tests__/terminology-filler.test.ts +214 -0
  129. package/src/__tests__/theme-adaptive-colors.test.ts +354 -0
  130. package/src/__tests__/theme-env.test.ts +281 -0
  131. package/src/__tests__/theme-resolve.test.ts +380 -0
  132. package/src/__tests__/tool-probe.test.ts +1400 -0
  133. package/src/__tests__/toolchain.test.ts +232 -0
  134. package/src/__tests__/ui-version-writers.test.ts +88 -0
  135. package/src/__tests__/update-apply.test.ts +637 -0
  136. package/src/__tests__/update-plan.test.ts +1373 -0
  137. package/src/__tests__/uppercase-keybindings.test.ts +101 -0
  138. package/src/__tests__/useGitignoreModal.test.ts +79 -0
  139. package/src/__tests__/version-check-refresh.test.ts +45 -0
  140. package/src/__tests__/version-snapshot.test.ts +212 -0
  141. package/src/__tests__/version-unknown-state.test.ts +388 -0
  142. package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
  143. package/src/cli/ansi.ts +520 -0
  144. package/src/cli/bootstrap.ts +134 -0
  145. package/src/cli/doctor.ts +218 -0
  146. package/src/cli/hook.ts +129 -0
  147. package/src/cli/install.ts +382 -0
  148. package/src/cli/live.ts +374 -0
  149. package/src/cli/models.ts +297 -0
  150. package/src/cli/profile.ts +197 -0
  151. package/src/cli/prompt.ts +32 -0
  152. package/src/cli/router.ts +207 -0
  153. package/src/cli/update-view.ts +526 -0
  154. package/src/cli/update.ts +613 -0
  155. package/src/cli/upgrade.ts +97 -0
  156. package/src/data/alias-flags.ts +301 -0
  157. package/src/data/cli-tools.ts +146 -0
  158. package/src/data/community-styles.ts +531 -0
  159. package/src/data/gitignore-defaults.ts +33 -0
  160. package/src/data/gitignore-reasons.ts +93 -0
  161. package/src/data/gitignore-templates.ts +25 -0
  162. package/src/data/marketplaces.ts +278 -0
  163. package/src/data/mcp-servers.ts +51 -0
  164. package/src/data/models-presets.ts +329 -0
  165. package/src/data/predefined-profiles.ts +252 -0
  166. package/src/data/settings-catalog.ts +714 -0
  167. package/src/data/skill-repos.ts +91 -0
  168. package/src/data/styles/asd-ste100.md +38 -0
  169. package/src/data/styles/calibrated.md +24 -0
  170. package/src/data/styles/direct.md +23 -0
  171. package/src/data/styles/evidence-first.md +22 -0
  172. package/src/data/styles/explanatory.md +23 -0
  173. package/src/data/styles/index.ts +57 -0
  174. package/src/data/styles/no-slop.md +32 -0
  175. package/src/data/styles/plain-language.md +23 -0
  176. package/src/data/styles/structured.md +30 -0
  177. package/src/data/styles/terminology.md +28 -0
  178. package/src/data/styles/terse.md +20 -0
  179. package/src/main.tsx +110 -0
  180. package/src/markdown.d.ts +13 -0
  181. package/src/opentui.d.ts +203 -0
  182. package/src/services/active-closure.ts +117 -0
  183. package/src/services/alias-settings.ts +60 -0
  184. package/src/services/alias-shell-writer.ts +1244 -0
  185. package/src/services/alias-store.ts +172 -0
  186. package/src/services/binary-signing.ts +78 -0
  187. package/src/services/catalog-cache-store.ts +316 -0
  188. package/src/services/claude-cli.ts +304 -0
  189. package/src/services/claude-runner.ts +31 -0
  190. package/src/services/claude-settings.ts +1589 -0
  191. package/src/services/cli-tool-commands.ts +133 -0
  192. package/src/services/community-fetcher.ts +90 -0
  193. package/src/services/community-styles.ts +1194 -0
  194. package/src/services/content-drift.ts +208 -0
  195. package/src/services/conventions-manager.ts +945 -0
  196. package/src/services/doctor-bins.ts +63 -0
  197. package/src/services/doctor.ts +570 -0
  198. package/src/services/dotenv.ts +181 -0
  199. package/src/services/git-worktree.ts +129 -0
  200. package/src/services/github-budget.ts +274 -0
  201. package/src/services/gitignore-check.ts +63 -0
  202. package/src/services/gitignore-detector.ts +167 -0
  203. package/src/services/gitignore-fixer.ts +280 -0
  204. package/src/services/gitignore-resolver.ts +208 -0
  205. package/src/services/gitignore-service.ts +167 -0
  206. package/src/services/hook-registration.ts +218 -0
  207. package/src/services/install-plan.ts +107 -0
  208. package/src/services/local-marketplace.ts +587 -0
  209. package/src/services/manifest.ts +250 -0
  210. package/src/services/marketplace-catalog-git.ts +173 -0
  211. package/src/services/marketplace-catalog.ts +129 -0
  212. package/src/services/marketplace-fetcher.ts +400 -0
  213. package/src/services/marketplace-refresh.ts +253 -0
  214. package/src/services/marketplace-sync.ts +145 -0
  215. package/src/services/mate-availability.ts +133 -0
  216. package/src/services/mate-catalog.ts +265 -0
  217. package/src/services/mcp-registry.ts +171 -0
  218. package/src/services/models-core.ts +969 -0
  219. package/src/services/models-manager.ts +606 -0
  220. package/src/services/plugin-checkout.ts +186 -0
  221. package/src/services/plugin-manager.ts +878 -0
  222. package/src/services/plugin-mcp-config.ts +242 -0
  223. package/src/services/plugin-releases.ts +47 -0
  224. package/src/services/plugin-requires.ts +227 -0
  225. package/src/services/plugin-setup.ts +965 -0
  226. package/src/services/profile-adopt.ts +206 -0
  227. package/src/services/profile-materializer.ts +133 -0
  228. package/src/services/profile-sync.ts +158 -0
  229. package/src/services/profiles.ts +225 -0
  230. package/src/services/resolver.ts +344 -0
  231. package/src/services/settings-file.ts +69 -0
  232. package/src/services/settings-manager.ts +307 -0
  233. package/src/services/skills-manager.ts +1111 -0
  234. package/src/services/skillsmp-client.ts +193 -0
  235. package/src/services/styles-manager.ts +1294 -0
  236. package/src/services/symlink-manager.ts +189 -0
  237. package/src/services/terminology-filler.ts +266 -0
  238. package/src/services/tool-probe.ts +1637 -0
  239. package/src/services/toolchain.ts +431 -0
  240. package/src/services/update-engine.ts +691 -0
  241. package/src/services/update-plan.ts +863 -0
  242. package/src/services/version-check.ts +127 -0
  243. package/src/services/version-snapshot.ts +189 -0
  244. package/src/tui.tsx +112 -0
  245. package/src/types/bun.d.ts +27 -0
  246. package/src/types/gitignore.ts +68 -0
  247. package/src/types/index.ts +447 -0
  248. package/src/ui/App.tsx +538 -0
  249. package/src/ui/adapters/catalogNotice.ts +122 -0
  250. package/src/ui/adapters/cliToolsAdapter.ts +176 -0
  251. package/src/ui/adapters/modelsAdapter.ts +213 -0
  252. package/src/ui/adapters/pluginsAdapter.ts +413 -0
  253. package/src/ui/adapters/settingsAdapter.ts +168 -0
  254. package/src/ui/adapters/skillsAdapter.ts +229 -0
  255. package/src/ui/adapters/stylesAdapter.ts +403 -0
  256. package/src/ui/components/CategoryHeader.tsx +64 -0
  257. package/src/ui/components/EmptyFilterState.tsx +32 -0
  258. package/src/ui/components/FlagDetailEditor.tsx +0 -0
  259. package/src/ui/components/ScopeIndicator.tsx +63 -0
  260. package/src/ui/components/ScrollableList.tsx +95 -0
  261. package/src/ui/components/SearchInput.tsx +57 -0
  262. package/src/ui/components/StyledText.tsx +75 -0
  263. package/src/ui/components/TabBar.tsx +101 -0
  264. package/src/ui/components/layout/FooterHints.tsx +78 -0
  265. package/src/ui/components/layout/Panel.tsx +63 -0
  266. package/src/ui/components/layout/ProgressBar.tsx +44 -0
  267. package/src/ui/components/layout/ScopeTabs.tsx +54 -0
  268. package/src/ui/components/layout/ScreenLayout.tsx +321 -0
  269. package/src/ui/components/layout/index.ts +9 -0
  270. package/src/ui/components/modals/ConfirmModal.tsx +62 -0
  271. package/src/ui/components/modals/InputModal.tsx +75 -0
  272. package/src/ui/components/modals/LoadingModal.tsx +51 -0
  273. package/src/ui/components/modals/MessageModal.tsx +66 -0
  274. package/src/ui/components/modals/ModalContainer.tsx +159 -0
  275. package/src/ui/components/modals/SelectModal.tsx +90 -0
  276. package/src/ui/components/modals/index.ts +6 -0
  277. package/src/ui/components/primitives/ActionHints.tsx +44 -0
  278. package/src/ui/components/primitives/DetailSection.tsx +23 -0
  279. package/src/ui/components/primitives/KeyValueLine.tsx +26 -0
  280. package/src/ui/components/primitives/ListCategoryRow.tsx +45 -0
  281. package/src/ui/components/primitives/MetaText.tsx +40 -0
  282. package/src/ui/components/primitives/ScopeDetail.tsx +67 -0
  283. package/src/ui/components/primitives/ScopeSquares.tsx +58 -0
  284. package/src/ui/components/primitives/SelectableRow.tsx +30 -0
  285. package/src/ui/components/primitives/index.ts +9 -0
  286. package/src/ui/hooks/index.ts +2 -0
  287. package/src/ui/hooks/useGitignoreModal.ts +95 -0
  288. package/src/ui/hooks/useKeyboard.ts +24 -0
  289. package/src/ui/hooks/useKeyboardHandler.ts +63 -0
  290. package/src/ui/registry.ts +33 -0
  291. package/src/ui/renderers/cliToolRenderers.tsx +326 -0
  292. package/src/ui/renderers/gitignoreRenderers.tsx +80 -0
  293. package/src/ui/renderers/markdownRenderer.tsx +302 -0
  294. package/src/ui/renderers/mcpRenderers.tsx +149 -0
  295. package/src/ui/renderers/modelRenderers.tsx +1309 -0
  296. package/src/ui/renderers/modelVisuals.tsx +1402 -0
  297. package/src/ui/renderers/pluginRenderers.tsx +1089 -0
  298. package/src/ui/renderers/pluginRowText.ts +70 -0
  299. package/src/ui/renderers/pluginStatusText.ts +56 -0
  300. package/src/ui/renderers/profileRenderers.tsx +406 -0
  301. package/src/ui/renderers/settingsRenderers.tsx +218 -0
  302. package/src/ui/renderers/skillRenderers.tsx +479 -0
  303. package/src/ui/renderers/styleRenderers.tsx +813 -0
  304. package/src/ui/screens/AliasScreen.tsx +1558 -0
  305. package/src/ui/screens/CliToolsScreen.tsx +367 -0
  306. package/src/ui/screens/EnvVarsScreen.tsx +221 -0
  307. package/src/ui/screens/GitignoreScreen.tsx +562 -0
  308. package/src/ui/screens/McpRegistryScreen.tsx +366 -0
  309. package/src/ui/screens/McpScreen.tsx +256 -0
  310. package/src/ui/screens/ModelsScreen.tsx +542 -0
  311. package/src/ui/screens/PluginsScreen.tsx +1464 -0
  312. package/src/ui/screens/ProfilesScreen.tsx +438 -0
  313. package/src/ui/screens/SkillsScreen.tsx +853 -0
  314. package/src/ui/screens/StylesScreen.tsx +1064 -0
  315. package/src/ui/screens/index.ts +11 -0
  316. package/src/ui/state/AppContext.tsx +233 -0
  317. package/src/ui/state/DimensionsContext.tsx +154 -0
  318. package/src/ui/state/reducer.ts +810 -0
  319. package/src/ui/state/types.ts +503 -0
  320. package/src/ui/theme-mode.ts +352 -0
  321. package/src/ui/theme-resolve.ts +147 -0
  322. package/src/ui/theme.ts +175 -0
  323. package/src/utils/clipboard.ts +58 -0
  324. package/src/utils/command-utils.ts +144 -0
  325. package/src/utils/config-dir.ts +45 -0
  326. package/src/utils/file-locking.ts +146 -0
  327. package/src/utils/fuzzy-search.ts +138 -0
  328. package/src/utils/go-module.ts +36 -0
  329. package/src/utils/open-file.ts +84 -0
  330. package/src/utils/run.ts +67 -0
  331. package/src/utils/string-utils.ts +88 -0
@@ -0,0 +1,197 @@
1
+ /**
2
+ * `magus profile <list|show|switch|sync> [name]`
3
+ *
4
+ * Inspect and switch between team profiles materialized under
5
+ * `.claude/_profiles/`. Switching is cheap and offline — it just repoints the
6
+ * shared-config symlinks. `sync` (Phase 5) promotes local drift back into the
7
+ * committed manifest.
8
+ */
9
+
10
+ import path from "node:path";
11
+ import fs from "fs-extra";
12
+ import { ensureGitignoreEntries } from "../services/conventions-manager.js";
13
+ import { checkBinaries, missingBinaries } from "../services/doctor-bins.js";
14
+ import { summarizeClosure } from "../services/install-plan.js";
15
+ import { getManifestPath, readManifest } from "../services/manifest.js";
16
+ import { syncProfile } from "../services/profile-sync.js";
17
+ import { resolveProfile } from "../services/resolver.js";
18
+ import {
19
+ PROFILE_GITIGNORE_ENTRIES,
20
+ activateProfile,
21
+ activeProfile,
22
+ profileDir,
23
+ } from "../services/symlink-manager.js";
24
+ import { ensureManifest } from "./bootstrap.js";
25
+
26
+ export async function runProfileCommand(args: string[]): Promise<number> {
27
+ const [sub, ...rest] = args;
28
+ const projectPath = process.cwd();
29
+
30
+ switch (sub) {
31
+ case undefined:
32
+ case "list":
33
+ return listProfiles(projectPath);
34
+ case "show":
35
+ return showProfile(rest[0], projectPath);
36
+ case "switch":
37
+ return switchProfile(rest[0], projectPath);
38
+ case "init":
39
+ return initProfile(
40
+ args.includes("--yes") || args.includes("-y"),
41
+ projectPath,
42
+ );
43
+ case "sync":
44
+ return syncActiveProfile(rest[0], projectPath);
45
+ default:
46
+ console.error(
47
+ `Unknown profile command "${sub}". Use: list | show <name> | switch <name> | init | sync`,
48
+ );
49
+ return 1;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * `profile init` — write a manifest describing this project as it stands.
55
+ *
56
+ * The same adoption `install`/`update` offer, available on its own so a repo
57
+ * can be brought under profile management without installing anything.
58
+ */
59
+ export async function initProfile(
60
+ yes: boolean,
61
+ projectPath: string,
62
+ ): Promise<number> {
63
+ const existing = await readManifest(projectPath);
64
+ if (Object.keys(existing.profiles).length > 0) {
65
+ console.error(
66
+ `${getManifestPath(projectPath)} already defines: ${Object.keys(existing.profiles).join(", ")}`,
67
+ );
68
+ console.error("Use `magus profile sync` to promote local edits into it.");
69
+ return 1;
70
+ }
71
+ // `profile init` exists to create the manifest, so it is the one caller that
72
+ // may do so unattended.
73
+ return (await ensureManifest(projectPath, { allowAdopt: yes })) ? 0 : 1;
74
+ }
75
+
76
+ export async function listProfiles(projectPath: string): Promise<number> {
77
+ const manifest = await readManifest(projectPath);
78
+ const ids = Object.keys(manifest.profiles);
79
+ if (ids.length === 0) {
80
+ console.log("No profiles defined in .claude/profiles.json.");
81
+ return 0;
82
+ }
83
+ const active = await activeProfile(projectPath);
84
+ console.log("Profiles:");
85
+ for (const id of ids) {
86
+ const entry = manifest.profiles[id];
87
+ const marker = id === active ? "●" : " ";
88
+ const materialized = (await fs.pathExists(profileDir(id, projectPath)))
89
+ ? ""
90
+ : " (not installed)";
91
+ console.log(` ${marker} ${id} — ${entry.name}${materialized}`);
92
+ }
93
+ if (active) console.log(`\nActive: ${active}`);
94
+ return 0;
95
+ }
96
+
97
+ export async function showProfile(
98
+ name: string | undefined,
99
+ projectPath: string,
100
+ ): Promise<number> {
101
+ if (!name) {
102
+ console.error("Usage: magus profile show <name>");
103
+ return 1;
104
+ }
105
+ const manifest = await readManifest(projectPath);
106
+ const entry = manifest.profiles[name];
107
+ if (!entry) {
108
+ console.error(
109
+ `Profile "${name}" not found. Available: ${Object.keys(manifest.profiles).join(", ")}`,
110
+ );
111
+ return 1;
112
+ }
113
+ const closure = await resolveProfile(manifest, name);
114
+ console.log(`Profile: ${name} (${entry.name})`);
115
+ for (const line of summarizeClosure(closure)) console.log(` ${line}`);
116
+ return 0;
117
+ }
118
+
119
+ export async function switchProfile(
120
+ name: string | undefined,
121
+ projectPath: string,
122
+ ): Promise<number> {
123
+ if (!name) {
124
+ console.error("Usage: magus profile switch <name>");
125
+ return 1;
126
+ }
127
+ const manifest = await readManifest(projectPath);
128
+ if (!manifest.profiles[name]) {
129
+ console.error(
130
+ `Profile "${name}" not found. Available: ${Object.keys(manifest.profiles).join(", ")}`,
131
+ );
132
+ return 1;
133
+ }
134
+ if (!(await fs.pathExists(profileDir(name, projectPath)))) {
135
+ console.error(
136
+ `Profile "${name}" isn't installed yet. Run: magus install ${name}`,
137
+ );
138
+ return 1;
139
+ }
140
+
141
+ // In profile mode the source of truth is .claude/profiles.json; the
142
+ // generated dir AND the per-developer symlinks must stay out of git so
143
+ // switching profiles causes no churn.
144
+ await ensureGitignoreEntries(
145
+ path.join(projectPath, ".gitignore"),
146
+ PROFILE_GITIGNORE_ENTRIES,
147
+ "# magus: generated profile build output + active-profile symlinks",
148
+ );
149
+
150
+ await activateProfile(name, projectPath);
151
+ console.log(`✓ Switched to profile: ${name}`);
152
+
153
+ // Verify the profile's binary deps are actually usable — the switch itself
154
+ // succeeds (symlinks are valid), but warn so "switched" isn't misleading.
155
+ try {
156
+ const closure = await resolveProfile(manifest, name);
157
+ const missing = missingBinaries(await checkBinaries(closure.bins));
158
+ if (missing.length > 0) {
159
+ console.log(
160
+ `\n⚠ ${missing.length} binary dependenc${missing.length === 1 ? "y is" : "ies are"} missing:`,
161
+ );
162
+ for (const m of missing)
163
+ console.log(` ✗ ${m.name} — run: ${m.installCommand}`);
164
+ console.log(`Run \`magus install ${name}\` to install them.`);
165
+ }
166
+ } catch {
167
+ // Verification is best-effort — never fail a valid switch on it.
168
+ }
169
+ return 0;
170
+ }
171
+
172
+ export async function syncActiveProfile(
173
+ nameArg: string | undefined,
174
+ projectPath: string,
175
+ ): Promise<number> {
176
+ const name = nameArg ?? (await activeProfile(projectPath));
177
+ if (!name) {
178
+ console.error(
179
+ "No active profile to sync. Pass a name: magus profile sync <name>",
180
+ );
181
+ return 1;
182
+ }
183
+ try {
184
+ const { changes, wrote } = await syncProfile(name, projectPath);
185
+ if (!wrote) {
186
+ console.log(`Profile "${name}" is already in sync with the manifest.`);
187
+ return 0;
188
+ }
189
+ console.log(`Synced "${name}" → .claude/profiles.json:`);
190
+ for (const c of changes) console.log(` ${c.kind}: ${c.detail}`);
191
+ console.log("\nReview the diff and commit .claude/profiles.json.");
192
+ return 0;
193
+ } catch (e) {
194
+ console.error((e as Error).message);
195
+ return 1;
196
+ }
197
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Terminal prompting shared by the non-interactive commands.
3
+ *
4
+ * These lived inside cli/install.ts until `update` and the adopt flow needed
5
+ * the same three. One copy, so a fix to (say) the Y/n default reaches every
6
+ * command at once.
7
+ */
8
+
9
+ import { createInterface } from "node:readline/promises";
10
+
11
+ /** Y/n prompt. Empty input means yes. */
12
+ export async function confirm(question: string): Promise<boolean> {
13
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
14
+ try {
15
+ const answer = (await rl.question(`${question} [Y/n] `))
16
+ .trim()
17
+ .toLowerCase();
18
+ return answer === "" || answer === "y" || answer === "yes";
19
+ } finally {
20
+ rl.close();
21
+ }
22
+ }
23
+
24
+ /** Free-text prompt, trimmed. */
25
+ export async function promptValue(question: string): Promise<string> {
26
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
27
+ try {
28
+ return (await rl.question(question)).trim();
29
+ } finally {
30
+ rl.close();
31
+ }
32
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * magus subcommand router.
3
+ *
4
+ * Parses argv and dispatches to a command handler. Commands run to completion
5
+ * and return an exit code; bare invocation (or an unknown token) falls through
6
+ * to the interactive TUI, preserving magus's historical behavior.
7
+ */
8
+
9
+ import type { ThemeMode } from "@opentui/core";
10
+ import { checkForUpdates } from "../services/version-check.js";
11
+ import { parseThemeWord } from "../ui/theme-resolve.js";
12
+ import { runDoctorCommand } from "./doctor.js";
13
+ import { runInstallCommand } from "./install.js";
14
+ import { runModelsCommand } from "./models.js";
15
+ import { runProfileCommand } from "./profile.js";
16
+ import { runUpdateCommand } from "./update.js";
17
+ import { runUpgradeCommand } from "./upgrade.js";
18
+
19
+ export interface RouteOutcome {
20
+ /** A subcommand (or top-level flag) handled the invocation. */
21
+ handled: boolean;
22
+ /** Exit code to use when `handled` is true. */
23
+ exitCode?: number;
24
+ /** No subcommand matched — the caller should launch the TUI. */
25
+ launchTui: boolean;
26
+ /** From --theme, validated. Only meaningful when launchTui is true. */
27
+ theme?: ThemeMode;
28
+ }
29
+
30
+ export type ThemeFlagParse =
31
+ | { ok: true; theme?: ThemeMode; rest: string[] }
32
+ | { ok: false; message: string };
33
+
34
+ /**
35
+ * Pull `--theme light|dark` (or `--theme=…`) out of argv.
36
+ *
37
+ * Exact words only, the same rule every other theme source follows — so
38
+ * `--theme Dark` errors instead of silently differing from `TERM_THEME=Dark`.
39
+ * A flag is typed on purpose, so a bad value is an error rather than the "no
40
+ * opinion" an inherited variable gets. `--theme` as the last token, or followed
41
+ * by another `--flag`, is the same error.
42
+ */
43
+ export function parseThemeFlag(argv: string[]): ThemeFlagParse {
44
+ const rest: string[] = [];
45
+ let theme: ThemeMode | undefined;
46
+
47
+ const invalid = (got: string | undefined): ThemeFlagParse => ({
48
+ ok: false,
49
+ message:
50
+ got === undefined
51
+ ? 'magus: --theme must be "light" or "dark" (no value given)'
52
+ : `magus: --theme must be "light" or "dark" (got "${got}")`,
53
+ });
54
+
55
+ for (let i = 0; i < argv.length; i += 1) {
56
+ const token = argv[i] as string;
57
+ let value: string | undefined;
58
+
59
+ if (token === "--theme") {
60
+ const next = argv[i + 1];
61
+ if (next === undefined || next.startsWith("--"))
62
+ return invalid(undefined);
63
+ value = next;
64
+ i += 1;
65
+ } else if (token.startsWith("--theme=")) {
66
+ value = token.slice("--theme=".length);
67
+ } else {
68
+ rest.push(token);
69
+ continue;
70
+ }
71
+
72
+ const parsed = parseThemeWord(value);
73
+ if (parsed === null) return invalid(value);
74
+ theme = parsed;
75
+ }
76
+
77
+ return theme === undefined ? { ok: true, rest } : { ok: true, theme, rest };
78
+ }
79
+
80
+ const HANDLED = (exitCode: number): RouteOutcome => ({
81
+ handled: true,
82
+ exitCode,
83
+ launchTui: false,
84
+ });
85
+ const LAUNCH_TUI: RouteOutcome = { handled: false, launchTui: true };
86
+
87
+ export async function route(
88
+ argv: string[],
89
+ version: string,
90
+ ): Promise<RouteOutcome> {
91
+ // Top-level flags take precedence over any command token.
92
+ if (argv.includes("--version") || argv.includes("-v")) {
93
+ console.log(`magus v${version}`);
94
+ const result = await checkForUpdates();
95
+ if (result.updateAvailable) {
96
+ console.log(`\nUpdate available: v${result.latestVersion}`);
97
+ console.log("Run: magus upgrade");
98
+ }
99
+ return HANDLED(0);
100
+ }
101
+
102
+ if (argv.includes("--help") || argv.includes("-h")) {
103
+ printHelp(version);
104
+ return HANDLED(0);
105
+ }
106
+
107
+ const themeFlag = parseThemeFlag(argv);
108
+ if (!themeFlag.ok) {
109
+ console.error(themeFlag.message);
110
+ return HANDLED(1);
111
+ }
112
+
113
+ // Dispatch on argv with the theme tokens removed, so `--theme dark update`
114
+ // still runs `update`. Subcommands ignore the flag; only the TUI carries it.
115
+ const [command, ...rest] = themeFlag.rest;
116
+
117
+ switch (command) {
118
+ case "update":
119
+ return HANDLED(await runUpdateCommand(rest));
120
+ case "upgrade":
121
+ return HANDLED(await runUpgradeCommand());
122
+ case "install":
123
+ return HANDLED(await runInstallCommand(rest));
124
+ case "profile":
125
+ return HANDLED(await runProfileCommand(rest));
126
+ case "models":
127
+ return HANDLED(await runModelsCommand(rest));
128
+ case "doctor":
129
+ return HANDLED(await runDoctorCommand(rest));
130
+ default:
131
+ // Bare `magus` or an unrecognized token → interactive TUI.
132
+ return themeFlag.theme === undefined
133
+ ? LAUNCH_TUI
134
+ : { ...LAUNCH_TUI, theme: themeFlag.theme };
135
+ }
136
+ }
137
+
138
+ function printHelp(version: string): void {
139
+ console.log(`magus v${version}
140
+
141
+ TUI tool for managing Claude Code plugins, MCPs, and configuration.
142
+
143
+ Usage: magus Open the interactive TUI
144
+ magus <command> [args]
145
+
146
+ Options:
147
+ -v, --version Show version and check for updates
148
+ -h, --help Show this help message
149
+ --no-refresh Skip auto-refresh of marketplaces on startup
150
+ --theme light|dark Force the palette. Otherwise: MAGUS_THEME, then
151
+ TERM_THEME, then a 250 ms terminal query, then COLORFGBG,
152
+ then dark. Only the exact words "light"/"dark" count.
153
+
154
+ Dependency management — keep this project's plugins, CLI tools and skills
155
+ installed and current. The manifest is .claude/profiles.json, committed; a repo
156
+ without one is offered adoption on first run. See docs/team-configuration.md.
157
+ install [profile] Install the profile's plugins, binaries, skills and env at
158
+ the manifest's PINNED versions, then activate it.
159
+ No argument installs every profile.
160
+ --check Report drift only, write nothing (exits 1 in strict mode)
161
+ --yes, -y Skip the confirmation prompt
162
+ --force Discard unsynced local edits instead of refusing
163
+ update [profile] Keep the ACTIVE profile current: install whatever it
164
+ declares but this machine lacks, and advance anything
165
+ behind the version its marketplace publishes. Only the
166
+ newest version is installable, so a pin naming an older
167
+ one is reported, not chased.
168
+ APPLIES WITHOUT ASKING. It can only move forward — there
169
+ is no downgrade and no delete — so the plan is printed as
170
+ it runs rather than gated behind a prompt. This includes
171
+ running a profile's CLI-tool installers (brew, bun).
172
+ --dry-run Show the plan and stop. Writes nothing, exits 0.
173
+ --check Report only, write nothing. Exits 1 if anything is
174
+ missing or known to be behind — the CI gate.
175
+ profile list Show every profile; ● marks the active one
176
+ profile show <n> Print a profile's fully resolved closure
177
+ profile switch <n> Repoint the active profile — offline, no reinstall
178
+ profile init Write .claude/profiles.json from this project's setup
179
+ profile sync Promote local edits back into .claude/profiles.json
180
+ doctor Check binary deps, profile symlinks, and conventions
181
+ --fix Apply the repairs it can make itself
182
+
183
+ Per-subagent model routing — give each subagent its own model and effort. The
184
+ routing lives on the active profile (committed in .claude/profiles.json) and is
185
+ applied by a PreToolUse hook magus registers at user scope.
186
+ models list Show the built-in presets; ● marks the active one
187
+ models use <p> Apply a preset, record it on the active profile, and
188
+ register the hook
189
+ models status What is routed now, plus any drift from the config
190
+ models off Stop routing; subagents inherit the session model again
191
+
192
+ Other commands:
193
+ upgrade Update magus ITSELF to the latest version
194
+
195
+ Navigation (TUI):
196
+ [1] Plugins [4] Settings [7] Git State
197
+ [2] Skills [5] Profiles [8] Alias
198
+ [3] MCP [6] CLI Tools
199
+
200
+ Keys:
201
+ ↑/↓ or j/k Navigate
202
+ Enter Select / Toggle
203
+ r Refresh current screen
204
+ ? Show help
205
+ q / Escape Back / Quit
206
+ `);
207
+ }