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.
- package/README.md +309 -0
- package/bin/magus.js +170 -0
- package/package.json +67 -3
- package/scripts/build-binaries.ts +113 -0
- package/scripts/capture-builtin.ts +412 -0
- package/scripts/check-optional-deps.ts +49 -0
- package/scripts/test-isolated.ts +49 -0
- package/scripts/verify-binary.ts +95 -0
- package/scripts/verify-community-registry.ts +272 -0
- package/src/__tests__/active-closure.test.ts +184 -0
- package/src/__tests__/alias-adopt.test.ts +365 -0
- package/src/__tests__/alias-parser.test.ts +417 -0
- package/src/__tests__/alias-shell-writer.test.ts +681 -0
- package/src/__tests__/alias-store.test.ts +163 -0
- package/src/__tests__/binary-signing.test.ts +120 -0
- package/src/__tests__/blackbox-active-closure.test.ts +119 -0
- package/src/__tests__/blackbox-convergence.test.ts +334 -0
- package/src/__tests__/blackbox-parity.test.ts +684 -0
- package/src/__tests__/blackbox-plugin-state.test.ts +523 -0
- package/src/__tests__/blackbox-tool-state.test.ts +669 -0
- package/src/__tests__/cache-provenance.test.ts +84 -0
- package/src/__tests__/capture-builtin.test.ts +92 -0
- package/src/__tests__/catalog-cache-store.test.ts +309 -0
- package/src/__tests__/catalog-notice.test.ts +155 -0
- package/src/__tests__/cli-ansi.test.ts +297 -0
- package/src/__tests__/cli-apply-seams.test.ts +288 -0
- package/src/__tests__/cli-live.test.ts +391 -0
- package/src/__tests__/cli-router.test.ts +240 -0
- package/src/__tests__/cli-tool-commands.test.ts +239 -0
- package/src/__tests__/cli-tools-adapter.test.ts +294 -0
- package/src/__tests__/cli-update-view.test.ts +445 -0
- package/src/__tests__/community-fetch.test.ts +545 -0
- package/src/__tests__/community-registry.test.ts +269 -0
- package/src/__tests__/community-staleness.test.ts +720 -0
- package/src/__tests__/component-badge.test.ts +98 -0
- package/src/__tests__/component-icons.test.ts +94 -0
- package/src/__tests__/content-drift.test.ts +353 -0
- package/src/__tests__/conventions-integration.test.ts +830 -0
- package/src/__tests__/conventions-manager.test.ts +1236 -0
- package/src/__tests__/doctor-bins.test.ts +43 -0
- package/src/__tests__/doctor.test.ts +463 -0
- package/src/__tests__/dotenv.test.ts +195 -0
- package/src/__tests__/enabled-not-installed.test.ts +94 -0
- package/src/__tests__/file-locking.test.ts +208 -0
- package/src/__tests__/fixtures/tool-probe/brew-list-versions-tmux.txt +2 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-behind-homebrew-spec.txt +29 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-tmux-current.txt +7 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-unknown-formula.txt +6 -0
- package/src/__tests__/fixtures/tool-probe/go-version-m-tmux-mcp.txt +27 -0
- package/src/__tests__/fixtures/tool-probe/npm-dist-tags-missing-scoped.txt +4 -0
- package/src/__tests__/fixtures/tool-probe/npm-dist-tags-openai-codex.txt +4 -0
- package/src/__tests__/fixtures/tool-probe/pypi-cowsay.txt +3 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-browser-use.toml +10 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-git.toml +8 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-pinned.toml +8 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-graphifyy.toml +7 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-mcp.toml +6 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-serena-agent.toml +9 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated-pinned.txt +5 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated.txt +11 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list.txt +22 -0
- package/src/__tests__/footer-hints.test.ts +40 -0
- package/src/__tests__/format-command.test.ts +78 -0
- package/src/__tests__/git-worktree.test.ts +108 -0
- package/src/__tests__/github-budget.test.ts +206 -0
- package/src/__tests__/gitignore-check.test.ts +71 -0
- package/src/__tests__/gitignore-detector.test.ts +176 -0
- package/src/__tests__/gitignore-fixer.test.ts +317 -0
- package/src/__tests__/gitignore-resolver.test.ts +193 -0
- package/src/__tests__/gitignore-service.test.ts +137 -0
- package/src/__tests__/go-module.test.ts +43 -0
- package/src/__tests__/hook-import-policy.test.ts +90 -0
- package/src/__tests__/hook-process.test.ts +256 -0
- package/src/__tests__/hook-registration.test.ts +224 -0
- package/src/__tests__/install-command.test.ts +81 -0
- package/src/__tests__/install-plan.test.ts +104 -0
- package/src/__tests__/launcher-signal.test.ts +308 -0
- package/src/__tests__/local-marketplace-scan.test.ts +174 -0
- package/src/__tests__/manifest.test.ts +281 -0
- package/src/__tests__/markdown-renderer.test.ts +142 -0
- package/src/__tests__/marketplace-badge.test.ts +140 -0
- package/src/__tests__/marketplace-refresh.test.ts +335 -0
- package/src/__tests__/marketplace-version.test.ts +84 -0
- package/src/__tests__/marketplaces.test.ts +128 -0
- package/src/__tests__/mate-availability.test.ts +156 -0
- package/src/__tests__/mate-catalog.test.ts +295 -0
- package/src/__tests__/mcp-registry.test.ts +158 -0
- package/src/__tests__/model-visuals.test.tsx +2466 -0
- package/src/__tests__/models-adapter.test.ts +332 -0
- package/src/__tests__/models-cli.test.ts +273 -0
- package/src/__tests__/models-core.test.ts +802 -0
- package/src/__tests__/models-manager.test.ts +500 -0
- package/src/__tests__/models-presets-marketplace.test.ts +168 -0
- package/src/__tests__/models-screen-state.test.ts +315 -0
- package/src/__tests__/moved-marketplace.test.ts +86 -0
- package/src/__tests__/open-file.test.ts +59 -0
- package/src/__tests__/owned-marketplaces.test.ts +93 -0
- package/src/__tests__/plugin-checkout.test.ts +66 -0
- package/src/__tests__/plugin-cli-argv.test.ts +109 -0
- package/src/__tests__/plugin-contents.test.ts +371 -0
- package/src/__tests__/plugin-manager-fallback.test.ts +320 -0
- package/src/__tests__/plugin-releases.test.ts +98 -0
- package/src/__tests__/plugin-requires.test.ts +187 -0
- package/src/__tests__/plugin-row-text.test.ts +336 -0
- package/src/__tests__/plugin-setup.test.ts +1016 -0
- package/src/__tests__/profile-adopt.test.ts +177 -0
- package/src/__tests__/profile-command.test.ts +110 -0
- package/src/__tests__/profile-materializer.test.ts +238 -0
- package/src/__tests__/profile-sync.test.ts +154 -0
- package/src/__tests__/registry-version-resolution.test.ts +88 -0
- package/src/__tests__/resolve-executable.test.ts +42 -0
- package/src/__tests__/resolver.test.ts +351 -0
- package/src/__tests__/scope-action.test.ts +134 -0
- package/src/__tests__/scope-squares.test.tsx +164 -0
- package/src/__tests__/selection-scope.test.ts +100 -0
- package/src/__tests__/settings-file.test.ts +179 -0
- package/src/__tests__/shell-script-callers.test.ts +92 -0
- package/src/__tests__/skill-install-files.test.ts +136 -0
- package/src/__tests__/skills-catalog-client.test.ts +48 -0
- package/src/__tests__/style-wrap.test.ts +220 -0
- package/src/__tests__/styles-manager.test.ts +1098 -0
- package/src/__tests__/styles-origins.test.ts +414 -0
- package/src/__tests__/styles-screen-state.test.ts +457 -0
- package/src/__tests__/styles-status-line.test.ts +72 -0
- package/src/__tests__/styles-sync.test.ts +452 -0
- package/src/__tests__/symlink-manager.test.ts +180 -0
- package/src/__tests__/tabbar-layout.test.ts +100 -0
- package/src/__tests__/terminology-filler.test.ts +214 -0
- package/src/__tests__/theme-adaptive-colors.test.ts +354 -0
- package/src/__tests__/theme-env.test.ts +281 -0
- package/src/__tests__/theme-resolve.test.ts +380 -0
- package/src/__tests__/tool-probe.test.ts +1400 -0
- package/src/__tests__/toolchain.test.ts +232 -0
- package/src/__tests__/ui-version-writers.test.ts +88 -0
- package/src/__tests__/update-apply.test.ts +637 -0
- package/src/__tests__/update-plan.test.ts +1373 -0
- package/src/__tests__/uppercase-keybindings.test.ts +101 -0
- package/src/__tests__/useGitignoreModal.test.ts +79 -0
- package/src/__tests__/version-check-refresh.test.ts +45 -0
- package/src/__tests__/version-snapshot.test.ts +212 -0
- package/src/__tests__/version-unknown-state.test.ts +388 -0
- package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
- package/src/cli/ansi.ts +520 -0
- package/src/cli/bootstrap.ts +134 -0
- package/src/cli/doctor.ts +218 -0
- package/src/cli/hook.ts +129 -0
- package/src/cli/install.ts +382 -0
- package/src/cli/live.ts +374 -0
- package/src/cli/models.ts +297 -0
- package/src/cli/profile.ts +197 -0
- package/src/cli/prompt.ts +32 -0
- package/src/cli/router.ts +207 -0
- package/src/cli/update-view.ts +526 -0
- package/src/cli/update.ts +613 -0
- package/src/cli/upgrade.ts +97 -0
- package/src/data/alias-flags.ts +301 -0
- package/src/data/cli-tools.ts +146 -0
- package/src/data/community-styles.ts +531 -0
- package/src/data/gitignore-defaults.ts +33 -0
- package/src/data/gitignore-reasons.ts +93 -0
- package/src/data/gitignore-templates.ts +25 -0
- package/src/data/marketplaces.ts +278 -0
- package/src/data/mcp-servers.ts +51 -0
- package/src/data/models-presets.ts +329 -0
- package/src/data/predefined-profiles.ts +252 -0
- package/src/data/settings-catalog.ts +714 -0
- package/src/data/skill-repos.ts +91 -0
- package/src/data/styles/asd-ste100.md +38 -0
- package/src/data/styles/calibrated.md +24 -0
- package/src/data/styles/direct.md +23 -0
- package/src/data/styles/evidence-first.md +22 -0
- package/src/data/styles/explanatory.md +23 -0
- package/src/data/styles/index.ts +57 -0
- package/src/data/styles/no-slop.md +32 -0
- package/src/data/styles/plain-language.md +23 -0
- package/src/data/styles/structured.md +30 -0
- package/src/data/styles/terminology.md +28 -0
- package/src/data/styles/terse.md +20 -0
- package/src/main.tsx +110 -0
- package/src/markdown.d.ts +13 -0
- package/src/opentui.d.ts +203 -0
- package/src/services/active-closure.ts +117 -0
- package/src/services/alias-settings.ts +60 -0
- package/src/services/alias-shell-writer.ts +1244 -0
- package/src/services/alias-store.ts +172 -0
- package/src/services/binary-signing.ts +78 -0
- package/src/services/catalog-cache-store.ts +316 -0
- package/src/services/claude-cli.ts +304 -0
- package/src/services/claude-runner.ts +31 -0
- package/src/services/claude-settings.ts +1589 -0
- package/src/services/cli-tool-commands.ts +133 -0
- package/src/services/community-fetcher.ts +90 -0
- package/src/services/community-styles.ts +1194 -0
- package/src/services/content-drift.ts +208 -0
- package/src/services/conventions-manager.ts +945 -0
- package/src/services/doctor-bins.ts +63 -0
- package/src/services/doctor.ts +570 -0
- package/src/services/dotenv.ts +181 -0
- package/src/services/git-worktree.ts +129 -0
- package/src/services/github-budget.ts +274 -0
- package/src/services/gitignore-check.ts +63 -0
- package/src/services/gitignore-detector.ts +167 -0
- package/src/services/gitignore-fixer.ts +280 -0
- package/src/services/gitignore-resolver.ts +208 -0
- package/src/services/gitignore-service.ts +167 -0
- package/src/services/hook-registration.ts +218 -0
- package/src/services/install-plan.ts +107 -0
- package/src/services/local-marketplace.ts +587 -0
- package/src/services/manifest.ts +250 -0
- package/src/services/marketplace-catalog-git.ts +173 -0
- package/src/services/marketplace-catalog.ts +129 -0
- package/src/services/marketplace-fetcher.ts +400 -0
- package/src/services/marketplace-refresh.ts +253 -0
- package/src/services/marketplace-sync.ts +145 -0
- package/src/services/mate-availability.ts +133 -0
- package/src/services/mate-catalog.ts +265 -0
- package/src/services/mcp-registry.ts +171 -0
- package/src/services/models-core.ts +969 -0
- package/src/services/models-manager.ts +606 -0
- package/src/services/plugin-checkout.ts +186 -0
- package/src/services/plugin-manager.ts +878 -0
- package/src/services/plugin-mcp-config.ts +242 -0
- package/src/services/plugin-releases.ts +47 -0
- package/src/services/plugin-requires.ts +227 -0
- package/src/services/plugin-setup.ts +965 -0
- package/src/services/profile-adopt.ts +206 -0
- package/src/services/profile-materializer.ts +133 -0
- package/src/services/profile-sync.ts +158 -0
- package/src/services/profiles.ts +225 -0
- package/src/services/resolver.ts +344 -0
- package/src/services/settings-file.ts +69 -0
- package/src/services/settings-manager.ts +307 -0
- package/src/services/skills-manager.ts +1111 -0
- package/src/services/skillsmp-client.ts +193 -0
- package/src/services/styles-manager.ts +1294 -0
- package/src/services/symlink-manager.ts +189 -0
- package/src/services/terminology-filler.ts +266 -0
- package/src/services/tool-probe.ts +1637 -0
- package/src/services/toolchain.ts +431 -0
- package/src/services/update-engine.ts +691 -0
- package/src/services/update-plan.ts +863 -0
- package/src/services/version-check.ts +127 -0
- package/src/services/version-snapshot.ts +189 -0
- package/src/tui.tsx +112 -0
- package/src/types/bun.d.ts +27 -0
- package/src/types/gitignore.ts +68 -0
- package/src/types/index.ts +447 -0
- package/src/ui/App.tsx +538 -0
- package/src/ui/adapters/catalogNotice.ts +122 -0
- package/src/ui/adapters/cliToolsAdapter.ts +176 -0
- package/src/ui/adapters/modelsAdapter.ts +213 -0
- package/src/ui/adapters/pluginsAdapter.ts +413 -0
- package/src/ui/adapters/settingsAdapter.ts +168 -0
- package/src/ui/adapters/skillsAdapter.ts +229 -0
- package/src/ui/adapters/stylesAdapter.ts +403 -0
- package/src/ui/components/CategoryHeader.tsx +64 -0
- package/src/ui/components/EmptyFilterState.tsx +32 -0
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScopeIndicator.tsx +63 -0
- package/src/ui/components/ScrollableList.tsx +95 -0
- package/src/ui/components/SearchInput.tsx +57 -0
- package/src/ui/components/StyledText.tsx +75 -0
- package/src/ui/components/TabBar.tsx +101 -0
- package/src/ui/components/layout/FooterHints.tsx +78 -0
- package/src/ui/components/layout/Panel.tsx +63 -0
- package/src/ui/components/layout/ProgressBar.tsx +44 -0
- package/src/ui/components/layout/ScopeTabs.tsx +54 -0
- package/src/ui/components/layout/ScreenLayout.tsx +321 -0
- package/src/ui/components/layout/index.ts +9 -0
- package/src/ui/components/modals/ConfirmModal.tsx +62 -0
- package/src/ui/components/modals/InputModal.tsx +75 -0
- package/src/ui/components/modals/LoadingModal.tsx +51 -0
- package/src/ui/components/modals/MessageModal.tsx +66 -0
- package/src/ui/components/modals/ModalContainer.tsx +159 -0
- package/src/ui/components/modals/SelectModal.tsx +90 -0
- package/src/ui/components/modals/index.ts +6 -0
- package/src/ui/components/primitives/ActionHints.tsx +44 -0
- package/src/ui/components/primitives/DetailSection.tsx +23 -0
- package/src/ui/components/primitives/KeyValueLine.tsx +26 -0
- package/src/ui/components/primitives/ListCategoryRow.tsx +45 -0
- package/src/ui/components/primitives/MetaText.tsx +40 -0
- package/src/ui/components/primitives/ScopeDetail.tsx +67 -0
- package/src/ui/components/primitives/ScopeSquares.tsx +58 -0
- package/src/ui/components/primitives/SelectableRow.tsx +30 -0
- package/src/ui/components/primitives/index.ts +9 -0
- package/src/ui/hooks/index.ts +2 -0
- package/src/ui/hooks/useGitignoreModal.ts +95 -0
- package/src/ui/hooks/useKeyboard.ts +24 -0
- package/src/ui/hooks/useKeyboardHandler.ts +63 -0
- package/src/ui/registry.ts +33 -0
- package/src/ui/renderers/cliToolRenderers.tsx +326 -0
- package/src/ui/renderers/gitignoreRenderers.tsx +80 -0
- package/src/ui/renderers/markdownRenderer.tsx +302 -0
- package/src/ui/renderers/mcpRenderers.tsx +149 -0
- package/src/ui/renderers/modelRenderers.tsx +1309 -0
- package/src/ui/renderers/modelVisuals.tsx +1402 -0
- package/src/ui/renderers/pluginRenderers.tsx +1089 -0
- package/src/ui/renderers/pluginRowText.ts +70 -0
- package/src/ui/renderers/pluginStatusText.ts +56 -0
- package/src/ui/renderers/profileRenderers.tsx +406 -0
- package/src/ui/renderers/settingsRenderers.tsx +218 -0
- package/src/ui/renderers/skillRenderers.tsx +479 -0
- package/src/ui/renderers/styleRenderers.tsx +813 -0
- package/src/ui/screens/AliasScreen.tsx +1558 -0
- package/src/ui/screens/CliToolsScreen.tsx +367 -0
- package/src/ui/screens/EnvVarsScreen.tsx +221 -0
- package/src/ui/screens/GitignoreScreen.tsx +562 -0
- package/src/ui/screens/McpRegistryScreen.tsx +366 -0
- package/src/ui/screens/McpScreen.tsx +256 -0
- package/src/ui/screens/ModelsScreen.tsx +542 -0
- package/src/ui/screens/PluginsScreen.tsx +1464 -0
- package/src/ui/screens/ProfilesScreen.tsx +438 -0
- package/src/ui/screens/SkillsScreen.tsx +853 -0
- package/src/ui/screens/StylesScreen.tsx +1064 -0
- package/src/ui/screens/index.ts +11 -0
- package/src/ui/state/AppContext.tsx +233 -0
- package/src/ui/state/DimensionsContext.tsx +154 -0
- package/src/ui/state/reducer.ts +810 -0
- package/src/ui/state/types.ts +503 -0
- package/src/ui/theme-mode.ts +352 -0
- package/src/ui/theme-resolve.ts +147 -0
- package/src/ui/theme.ts +175 -0
- package/src/utils/clipboard.ts +58 -0
- package/src/utils/command-utils.ts +144 -0
- package/src/utils/config-dir.ts +45 -0
- package/src/utils/file-locking.ts +146 -0
- package/src/utils/fuzzy-search.ts +138 -0
- package/src/utils/go-module.ts +36 -0
- package/src/utils/open-file.ts +84 -0
- package/src/utils/run.ts +67 -0
- package/src/utils/string-utils.ts +88 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `magus doctor [--fix]`
|
|
3
|
+
*
|
|
4
|
+
* Diagnoses (and with --fix, repairs) project health:
|
|
5
|
+
* 1. Binary dependencies — every requires.bin / cliTools binary on PATH.
|
|
6
|
+
* This is the check that would have caught a dead `tmux-mcp`.
|
|
7
|
+
* 2. Profile symlink integrity — the active profile's links resolve.
|
|
8
|
+
* 3. Convention compliance — .gitignore + CLAUDE.md blocks (ported engine).
|
|
9
|
+
* 4. Model routing — config validity, hook registration, and drift.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import fs from "fs-extra";
|
|
14
|
+
import { checkBinaries, missingBinaries } from "../services/doctor-bins.js";
|
|
15
|
+
import { checkProject, fixDoctorIssues } from "../services/doctor.js";
|
|
16
|
+
import {
|
|
17
|
+
isAgentModelHookRegistered,
|
|
18
|
+
registerAgentModelHook,
|
|
19
|
+
} from "../services/hook-registration.js";
|
|
20
|
+
import { readManifest } from "../services/manifest.js";
|
|
21
|
+
import {
|
|
22
|
+
readModelsConfig,
|
|
23
|
+
readModelsStatus,
|
|
24
|
+
} from "../services/models-manager.js";
|
|
25
|
+
import { resolveAllProfiles } from "../services/resolver.js";
|
|
26
|
+
import { activeProfile, profileDir } from "../services/symlink-manager.js";
|
|
27
|
+
import { resolveExecutable } from "../utils/command-utils.js";
|
|
28
|
+
|
|
29
|
+
export async function runDoctorCommand(args: string[]): Promise<number> {
|
|
30
|
+
const fix = args.includes("--fix");
|
|
31
|
+
const projectPath = process.cwd();
|
|
32
|
+
let problems = 0;
|
|
33
|
+
|
|
34
|
+
// 1. Binary dependencies.
|
|
35
|
+
const manifest = await readManifest(projectPath);
|
|
36
|
+
let pluginIds: string[] = [];
|
|
37
|
+
if (Object.keys(manifest.profiles).length > 0) {
|
|
38
|
+
const closure = await resolveAllProfiles(manifest);
|
|
39
|
+
pluginIds = Object.keys(closure.plugins);
|
|
40
|
+
if (closure.bins.length > 0) {
|
|
41
|
+
const results = await checkBinaries(closure.bins);
|
|
42
|
+
const missing = missingBinaries(results);
|
|
43
|
+
if (missing.length > 0) {
|
|
44
|
+
console.log("Binary dependencies:");
|
|
45
|
+
for (const m of missing) {
|
|
46
|
+
console.log(
|
|
47
|
+
` ✗ ${m.name} — run: ${m.installCommand} (needed by ${m.sources.join(", ")})`,
|
|
48
|
+
);
|
|
49
|
+
problems++;
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
console.log(
|
|
53
|
+
`✓ All ${closure.bins.length} binary dependencies present.`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 2. Profile symlink integrity.
|
|
60
|
+
const active = await activeProfile(projectPath);
|
|
61
|
+
if (active) {
|
|
62
|
+
const dangling = await danglingLinks(active, projectPath);
|
|
63
|
+
if (dangling.length > 0) {
|
|
64
|
+
console.log(`Profile "${active}" has broken symlinks:`);
|
|
65
|
+
for (const l of dangling) console.log(` ✗ ${l}`);
|
|
66
|
+
console.log(` Fix: magus install ${active}`);
|
|
67
|
+
problems += dangling.length;
|
|
68
|
+
} else {
|
|
69
|
+
console.log(`✓ Active profile "${active}" symlinks resolve.`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 3. Convention compliance — scoped to THIS project only (the ported engine
|
|
74
|
+
// can scan every registered project; that's noisy for a project command).
|
|
75
|
+
if (pluginIds.length > 0) {
|
|
76
|
+
try {
|
|
77
|
+
const project = await checkProject(projectPath, pluginIds);
|
|
78
|
+
const actionable = project.checks.filter((c) => c.status !== "pass");
|
|
79
|
+
if (actionable.length > 0) {
|
|
80
|
+
console.log("\nConventions:");
|
|
81
|
+
for (const check of actionable) {
|
|
82
|
+
console.log(
|
|
83
|
+
` ${check.status === "fail" ? "✗" : "•"} ${check.message}`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
const fixable = actionable.filter((c) => c.fixable).length;
|
|
87
|
+
const failures = actionable.filter((c) => c.status === "fail").length;
|
|
88
|
+
if (fix && fixable > 0) {
|
|
89
|
+
const { fixed, failed } = await fixDoctorIssues({
|
|
90
|
+
projects: [project],
|
|
91
|
+
summary: {
|
|
92
|
+
totalProjects: 1,
|
|
93
|
+
totalChecks: project.checks.length,
|
|
94
|
+
passed: 0,
|
|
95
|
+
warnings: 0,
|
|
96
|
+
failures,
|
|
97
|
+
fixable,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
console.log(`\nFixed ${fixed}, failed ${failed}.`);
|
|
101
|
+
} else if (fixable > 0) {
|
|
102
|
+
console.log(`\n${fixable} fixable — re-run with --fix.`);
|
|
103
|
+
problems += failures;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
// Convention doctor is best-effort; never fail the whole command on it.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 4. Model routing.
|
|
112
|
+
problems += await checkModels(projectPath, fix);
|
|
113
|
+
|
|
114
|
+
if (problems === 0) console.log("\n✓ No problems found.");
|
|
115
|
+
return problems > 0 && !fix ? 1 : 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Model routing health. Silent when the project has no routing at all — an
|
|
120
|
+
* absent `models.json` is the normal state, not a finding.
|
|
121
|
+
*
|
|
122
|
+
* Ordered by what blocks what: an invalid config routes nothing and cannot be
|
|
123
|
+
* fixed from here (the values are a human's decision); an unregistered hook
|
|
124
|
+
* routes nothing no matter how good the config is; drift only matters once
|
|
125
|
+
* both of those are sound.
|
|
126
|
+
*/
|
|
127
|
+
async function checkModels(projectPath: string, fix: boolean): Promise<number> {
|
|
128
|
+
const { config, errors, path: file } = await readModelsConfig(projectPath);
|
|
129
|
+
if (config === null && errors.length === 0) return 0;
|
|
130
|
+
|
|
131
|
+
console.log("\nModel routing:");
|
|
132
|
+
let problems = 0;
|
|
133
|
+
|
|
134
|
+
if (errors.length > 0) {
|
|
135
|
+
console.log(` ✗ ${file} is invalid — nothing is routed:`);
|
|
136
|
+
for (const e of errors) {
|
|
137
|
+
console.log(` ${e.path || "(root)"}: ${e.message}`);
|
|
138
|
+
}
|
|
139
|
+
console.log(
|
|
140
|
+
" Not auto-fixable: these are choices, not damage. Edit the file (or",
|
|
141
|
+
);
|
|
142
|
+
console.log(" re-run `magus models use <preset>`) and check again.");
|
|
143
|
+
// An invalid config is worth exactly one problem, not one per message —
|
|
144
|
+
// the count drives the exit code, not the severity.
|
|
145
|
+
return 1;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// The hook is what applies the routing. Without it the config is inert.
|
|
149
|
+
if (await isAgentModelHookRegistered()) {
|
|
150
|
+
console.log(" ✓ agent-model hook registered at user scope.");
|
|
151
|
+
} else if (fix) {
|
|
152
|
+
await registerAgentModelHook();
|
|
153
|
+
console.log(" ✓ agent-model hook registered at user scope (fixed).");
|
|
154
|
+
} else {
|
|
155
|
+
console.log(
|
|
156
|
+
" ✗ agent-model hook is not registered — the config is there, but nothing runs it.",
|
|
157
|
+
);
|
|
158
|
+
console.log(" Fix: magus doctor --fix");
|
|
159
|
+
problems++;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// The hook runs as a bare `magus`, deliberately (an absolute path dies on
|
|
163
|
+
// the next upgrade) — so PATH has to carry it. Warn only: PATH inside a
|
|
164
|
+
// Claude Code session is not necessarily PATH here.
|
|
165
|
+
if (!(await resolveExecutable("magus"))) {
|
|
166
|
+
console.log(
|
|
167
|
+
" • `magus` is not on PATH in this shell. The hook invokes it by name,",
|
|
168
|
+
);
|
|
169
|
+
console.log(
|
|
170
|
+
" so it must resolve in the shell Claude Code spawns hooks from.",
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// No drift check. Settings differing from the config is what a CUSTOM config is, and
|
|
175
|
+
// `doctor` reports problems — a project running models no shipped preset names has none.
|
|
176
|
+
// So `doctor --fix` no longer reapplies anything: `invalid` returns early above (those
|
|
177
|
+
// are choices, not damage) and `unhooked` is repaired by registering the hook. Writing
|
|
178
|
+
// settings back from the config is `magus models use <preset>`, which is a decision
|
|
179
|
+
// the user makes, not a repair.
|
|
180
|
+
const status = await readModelsStatus(projectPath);
|
|
181
|
+
if (status.state === "on") {
|
|
182
|
+
console.log(" ✓ routing is applied.");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const warning of status.warnings) console.log(` ⚠ ${warning}`);
|
|
186
|
+
|
|
187
|
+
return problems;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Which of the active profile's expected symlinks are missing or dangling. */
|
|
191
|
+
async function danglingLinks(
|
|
192
|
+
name: string,
|
|
193
|
+
projectPath: string,
|
|
194
|
+
): Promise<string[]> {
|
|
195
|
+
const dir = profileDir(name, projectPath);
|
|
196
|
+
const checks: Array<{ link: string; target: string }> = [
|
|
197
|
+
{
|
|
198
|
+
link: path.join(projectPath, ".claude", "settings.json"),
|
|
199
|
+
target: path.join(dir, "settings.json"),
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
link: path.join(projectPath, ".mcp.json"),
|
|
203
|
+
target: path.join(dir, "mcp.json"),
|
|
204
|
+
},
|
|
205
|
+
];
|
|
206
|
+
const broken: string[] = [];
|
|
207
|
+
for (const { link, target } of checks) {
|
|
208
|
+
try {
|
|
209
|
+
const stat = await fs.lstat(link);
|
|
210
|
+
if (stat.isSymbolicLink() && !(await fs.pathExists(target))) {
|
|
211
|
+
broken.push(path.relative(projectPath, link));
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
// link absent — not "broken", the profile may not provide it.
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return broken;
|
|
218
|
+
}
|
package/src/cli/hook.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `magus hook agent-model` — the PreToolUse hook Claude Code runs on every Agent call.
|
|
3
|
+
*
|
|
4
|
+
* This is the hottest path magus has: Claude Code spawns it once per subagent dispatch, in
|
|
5
|
+
* every session on the machine, and waits for it. Two consequences shape the whole file.
|
|
6
|
+
*
|
|
7
|
+
* IT MUST BE FAST. `main.tsx` routes here before it loads the project's `.env` and before it
|
|
8
|
+
* imports the router or the TUI, so nothing in this module may import `src/ui/` or anything
|
|
9
|
+
* that pulls in `@opentui`. `src/__tests__/hook-import-policy.test.ts` fails the build if that
|
|
10
|
+
* ever regresses. Measured: 0.02s on this path against 0.16s once the router is loaded.
|
|
11
|
+
*
|
|
12
|
+
* IT MUST FAIL OPEN. Every error path exits 0 with empty stdout, which Claude Code reads as
|
|
13
|
+
* "no opinion". A hook that throws, blocks, or exits non-zero takes the user's subagents with
|
|
14
|
+
* it, and the failure would surface as delegation mysteriously not working — in every project,
|
|
15
|
+
* not just the one with the bad config. Silence is always the safe answer, so it is the
|
|
16
|
+
* default and the fallback.
|
|
17
|
+
*
|
|
18
|
+
* It never writes a file, never exits 2, and never reads a `.env`.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
21
|
+
import { dirname, join, parse } from "node:path";
|
|
22
|
+
import {
|
|
23
|
+
type ModelsConfig,
|
|
24
|
+
evaluateAgentHook,
|
|
25
|
+
validateModelsConfig,
|
|
26
|
+
} from "../services/models-core.js";
|
|
27
|
+
|
|
28
|
+
/** How far up the tree to look for a config before giving up. */
|
|
29
|
+
const MAX_WALK_UP = 32;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Find the nearest `.claude/models.json` at or above `startDir`.
|
|
33
|
+
*
|
|
34
|
+
* The walk matters: a session's cwd is frequently a subdirectory of the project, and this repo
|
|
35
|
+
* in particular runs most work inside git worktrees. Anchoring on the hook payload's `cwd`
|
|
36
|
+
* rather than `process.cwd()` is deliberate — Claude Code tells us where the session is, and
|
|
37
|
+
* that is not necessarily where this process was started.
|
|
38
|
+
*/
|
|
39
|
+
export function findModelsConfig(startDir: string): string | null {
|
|
40
|
+
let dir = startDir;
|
|
41
|
+
const root = parse(dir).root;
|
|
42
|
+
for (let i = 0; i < MAX_WALK_UP; i += 1) {
|
|
43
|
+
const candidate = join(dir, ".claude", "models.json");
|
|
44
|
+
if (existsSync(candidate)) return candidate;
|
|
45
|
+
if (dir === root) return null;
|
|
46
|
+
const parent = dirname(dir);
|
|
47
|
+
if (parent === dir) return null;
|
|
48
|
+
dir = parent;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Read and validate a config. Returns null for every unusable state — missing, unreadable,
|
|
55
|
+
* unparseable, or invalid — because the hook's answer to all four is the same: do nothing.
|
|
56
|
+
*
|
|
57
|
+
* An invalid config is NOT an error here even though it is one everywhere else. `models use`
|
|
58
|
+
* refuses to write one, `magus install` refuses to apply one, and `doctor` reports one; by
|
|
59
|
+
* the time a session is running, the only useful behaviour left is to get out of the way.
|
|
60
|
+
*/
|
|
61
|
+
export function loadConfig(path: string): ModelsConfig | null {
|
|
62
|
+
let raw: unknown;
|
|
63
|
+
try {
|
|
64
|
+
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
if (validateModelsConfig(raw).length > 0) return null;
|
|
69
|
+
return raw as ModelsConfig;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function readStdin(): Promise<string> {
|
|
73
|
+
// A hook always receives its payload on stdin. Guard the TTY case anyway so that a human
|
|
74
|
+
// typing the subcommand by hand gets an immediate no-op instead of a hang.
|
|
75
|
+
if (process.stdin.isTTY) return "";
|
|
76
|
+
const chunks: Uint8Array[] = [];
|
|
77
|
+
for await (const chunk of Bun.stdin.stream()) chunks.push(chunk);
|
|
78
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function runHookCommand(args: string[]): Promise<number> {
|
|
82
|
+
// `hook` with no recognised name is a no-op rather than a usage error: this process is
|
|
83
|
+
// wired into someone's session, and printing to stdout would be interpreted as a decision.
|
|
84
|
+
if (args[0] !== "agent-model") {
|
|
85
|
+
await readStdin();
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const raw = await readStdin();
|
|
91
|
+
if (raw.trim().length === 0) return 0;
|
|
92
|
+
|
|
93
|
+
let payload: Record<string, unknown>;
|
|
94
|
+
try {
|
|
95
|
+
payload = JSON.parse(raw) as Record<string, unknown>;
|
|
96
|
+
} catch {
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// `cwd` is the session's directory, which is not this process's cwd.
|
|
101
|
+
const cwd =
|
|
102
|
+
typeof payload.cwd === "string" && payload.cwd.length > 0
|
|
103
|
+
? payload.cwd
|
|
104
|
+
: process.cwd();
|
|
105
|
+
const configPath = findModelsConfig(cwd);
|
|
106
|
+
const config = configPath === null ? null : loadConfig(configPath);
|
|
107
|
+
|
|
108
|
+
const decision = evaluateAgentHook(
|
|
109
|
+
{
|
|
110
|
+
tool_name:
|
|
111
|
+
typeof payload.tool_name === "string" ? payload.tool_name : undefined,
|
|
112
|
+
tool_input:
|
|
113
|
+
typeof payload.tool_input === "object" && payload.tool_input !== null
|
|
114
|
+
? (payload.tool_input as Record<string, unknown>)
|
|
115
|
+
: undefined,
|
|
116
|
+
cwd,
|
|
117
|
+
},
|
|
118
|
+
config,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
if (decision.kind === "update")
|
|
122
|
+
process.stdout.write(JSON.stringify(decision.output));
|
|
123
|
+
return 0;
|
|
124
|
+
} catch {
|
|
125
|
+
// Belt and braces. Nothing above should throw, and if it ever does the session must
|
|
126
|
+
// not notice.
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
}
|