magus-cli 0.0.0-placeholder → 7.1.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 +725 -0
- package/src/__tests__/blackbox-plugin-state.test.ts +545 -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 +524 -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 +248 -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 +340 -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__/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 +345 -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 +310 -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 +1396 -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 +403 -0
- package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
- package/src/cli/ansi.ts +528 -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 +264 -0
- package/src/cli/update-view.ts +717 -0
- package/src/cli/update.ts +600 -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 +1564 -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 +657 -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 +245 -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 +847 -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 +377 -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 +820 -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 +535 -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 +1423 -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 +440 -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 +149 -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,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI-facing wrapper around the gitignore core services. Loads everything
|
|
3
|
+
* the Gitignore screen needs in one call, and exposes a small set of
|
|
4
|
+
* action helpers (apply safe fixes, apply one destructive fix, apply a
|
|
5
|
+
* built-in template).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { BUILTIN_TEMPLATES } from "../data/gitignore-templates.js";
|
|
12
|
+
import type {
|
|
13
|
+
GitignoreManifest,
|
|
14
|
+
ResolvedManifest,
|
|
15
|
+
Violation,
|
|
16
|
+
} from "../types/gitignore.js";
|
|
17
|
+
import { detectViolations } from "./gitignore-detector.js";
|
|
18
|
+
import {
|
|
19
|
+
type DestructiveFixResult,
|
|
20
|
+
type SafeFixResult,
|
|
21
|
+
applyDestructiveFix as coreApplyDestructiveFix,
|
|
22
|
+
applySafeFixes as coreApplySafeFixes,
|
|
23
|
+
} from "./gitignore-fixer.js";
|
|
24
|
+
import { resolveGitignore } from "./gitignore-resolver.js";
|
|
25
|
+
|
|
26
|
+
export interface GitignoreState {
|
|
27
|
+
resolved: ResolvedManifest;
|
|
28
|
+
violations: Violation[];
|
|
29
|
+
/** Logger lines from resolution (malformed JSON, missing template, etc). */
|
|
30
|
+
warnings: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function loadGitignoreState(cwd: string): Promise<GitignoreState> {
|
|
34
|
+
const warnings: string[] = [];
|
|
35
|
+
const resolved = await resolveGitignore(cwd, {
|
|
36
|
+
logger: (m) => warnings.push(m),
|
|
37
|
+
});
|
|
38
|
+
const violations = await detectViolations(cwd, resolved);
|
|
39
|
+
return { resolved, violations, warnings };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function applyAllSafeFixes(
|
|
43
|
+
cwd: string,
|
|
44
|
+
violations: Violation[],
|
|
45
|
+
): Promise<SafeFixResult> {
|
|
46
|
+
return coreApplySafeFixes(cwd, violations);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function applyDestructiveFix(
|
|
50
|
+
cwd: string,
|
|
51
|
+
violation: Violation,
|
|
52
|
+
rulePattern?: string,
|
|
53
|
+
): Promise<DestructiveFixResult> {
|
|
54
|
+
return coreApplyDestructiveFix(cwd, violation, rulePattern);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function ensureProjectManifestForEdit(
|
|
58
|
+
cwd: string,
|
|
59
|
+
resolved: ResolvedManifest,
|
|
60
|
+
): Promise<string> {
|
|
61
|
+
const projectManifestPath = join(cwd, ".claude", "gitignore.json");
|
|
62
|
+
if (existsSync(projectManifestPath)) {
|
|
63
|
+
const existing = await readFile(projectManifestPath, "utf8");
|
|
64
|
+
if (existing.trim().length > 0) return projectManifestPath;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const manifest: GitignoreManifest = { ignore: [], track: [] };
|
|
68
|
+
for (const rule of resolved.rules) {
|
|
69
|
+
if (rule.action === "ignore") manifest.ignore.push(rule.pattern);
|
|
70
|
+
else manifest.track.push(rule.pattern);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await mkdir(dirname(projectManifestPath), { recursive: true });
|
|
74
|
+
await writeFile(
|
|
75
|
+
projectManifestPath,
|
|
76
|
+
`${JSON.stringify(manifest, null, 2)}\n`,
|
|
77
|
+
);
|
|
78
|
+
return projectManifestPath;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ApplyTemplateResult {
|
|
82
|
+
appliedTo: string;
|
|
83
|
+
added: { ignore: string[]; track: string[] };
|
|
84
|
+
conflicts: string[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Merge a built-in template into the project manifest at .claude/gitignore.json.
|
|
89
|
+
* Conflicts (a pattern that already exists with the opposite action) are
|
|
90
|
+
* skipped and reported back to the caller.
|
|
91
|
+
*/
|
|
92
|
+
export async function applyTemplate(
|
|
93
|
+
cwd: string,
|
|
94
|
+
templateName: string,
|
|
95
|
+
): Promise<ApplyTemplateResult> {
|
|
96
|
+
const tpl = BUILTIN_TEMPLATES[templateName];
|
|
97
|
+
if (!tpl) throw new Error(`Unknown template: ${templateName}`);
|
|
98
|
+
|
|
99
|
+
const projectManifestPath = join(cwd, ".claude", "gitignore.json");
|
|
100
|
+
const existing = await readManifestOrEmpty(projectManifestPath);
|
|
101
|
+
|
|
102
|
+
const existingIgnore = new Set(existing.ignore);
|
|
103
|
+
const existingTrack = new Set(existing.track);
|
|
104
|
+
|
|
105
|
+
const conflicts: string[] = [];
|
|
106
|
+
const newIgnore: string[] = [];
|
|
107
|
+
const newTrack: string[] = [];
|
|
108
|
+
|
|
109
|
+
for (const p of tpl.ignore) {
|
|
110
|
+
if (existingTrack.has(p)) {
|
|
111
|
+
conflicts.push(p);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (!existingIgnore.has(p)) {
|
|
115
|
+
newIgnore.push(p);
|
|
116
|
+
existingIgnore.add(p);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const p of tpl.track) {
|
|
120
|
+
if (existingIgnore.has(p)) {
|
|
121
|
+
conflicts.push(p);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!existingTrack.has(p)) {
|
|
125
|
+
newTrack.push(p);
|
|
126
|
+
existingTrack.add(p);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const merged: GitignoreManifest = {
|
|
131
|
+
ignore: [...existing.ignore, ...newIgnore],
|
|
132
|
+
track: [...existing.track, ...newTrack],
|
|
133
|
+
extends: existing.extends,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
await mkdir(dirname(projectManifestPath), { recursive: true });
|
|
137
|
+
await writeFile(projectManifestPath, `${JSON.stringify(merged, null, 2)}\n`);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
appliedTo: projectManifestPath,
|
|
141
|
+
added: { ignore: newIgnore, track: newTrack },
|
|
142
|
+
conflicts,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function readManifestOrEmpty(path: string): Promise<GitignoreManifest> {
|
|
147
|
+
if (!existsSync(path)) return { ignore: [], track: [] };
|
|
148
|
+
try {
|
|
149
|
+
const raw = await readFile(path, "utf8");
|
|
150
|
+
const parsed = JSON.parse(raw);
|
|
151
|
+
if (!isManifest(parsed)) return { ignore: [], track: [] };
|
|
152
|
+
return parsed;
|
|
153
|
+
} catch {
|
|
154
|
+
return { ignore: [], track: [] };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isManifest(x: unknown): x is GitignoreManifest {
|
|
159
|
+
if (!x || typeof x !== "object") return false;
|
|
160
|
+
const m = x as Record<string, unknown>;
|
|
161
|
+
return (
|
|
162
|
+
Array.isArray(m.ignore) &&
|
|
163
|
+
m.ignore.every((p) => typeof p === "string") &&
|
|
164
|
+
Array.isArray(m.track) &&
|
|
165
|
+
m.track.every((p) => typeof p === "string")
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register `magus hook agent-model` as a `PreToolUse` hook on the Agent tool.
|
|
3
|
+
*
|
|
4
|
+
* The hook is what makes per-subagent routing exist at all: Claude Code has no
|
|
5
|
+
* per-agent model knob, and a PreToolUse hook rewriting the Agent tool's input
|
|
6
|
+
* is the only dynamic seam (measured — see `models-core.ts`). A config with no
|
|
7
|
+
* hook registered routes NOTHING, which is why `computeModelsStatus` reports
|
|
8
|
+
* `unhooked` ahead of any drift.
|
|
9
|
+
*
|
|
10
|
+
* Three rules, each earned:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Identity is the command string.** Registration is idempotent because it
|
|
13
|
+
* looks for the command, not for a block shape. An existing `Agent` matcher
|
|
14
|
+
* block gets the command APPENDED rather than a second block created, so a
|
|
15
|
+
* user's own Agent hooks keep running alongside this one.
|
|
16
|
+
* 2. **The command is BARE, never an absolute path.** `magus upgrade`
|
|
17
|
+
* replaces the binary, and on npm the path carries a version directory — an
|
|
18
|
+
* absolute path recorded today is a dead path after the next upgrade, and
|
|
19
|
+
* the failure is invisible (a hook that cannot start is a hook that
|
|
20
|
+
* silently does not route).
|
|
21
|
+
* 3. **User scope, through `updateSettingsFile`.** NOT
|
|
22
|
+
* `readGlobalSettings`/`writeGlobalSettings`: those swallow a parse error
|
|
23
|
+
* and return `{}`, so a settings file a human is mid-edit in would be
|
|
24
|
+
* overwritten with one hook and nothing else. `updateSettingsFile` refuses
|
|
25
|
+
* a file it cannot parse.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import path from "node:path";
|
|
29
|
+
import { claudeConfigDirOrNull } from "../utils/config-dir.js";
|
|
30
|
+
import { readSettingsFile, updateSettingsFile } from "./settings-file.js";
|
|
31
|
+
|
|
32
|
+
/** The command Claude Code runs. Bare — see rule 2 above. */
|
|
33
|
+
export const AGENT_MODEL_HOOK_COMMAND = "magus hook agent-model";
|
|
34
|
+
|
|
35
|
+
/** The tool whose input the hook rewrites. */
|
|
36
|
+
const MATCHER = "Agent";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Seconds. Generous for a process that reads one small JSON file and exits, and
|
|
40
|
+
* deliberately finite: Claude Code waits for this on every Agent call.
|
|
41
|
+
*/
|
|
42
|
+
const TIMEOUT_SECONDS = 5;
|
|
43
|
+
|
|
44
|
+
interface HookCommand {
|
|
45
|
+
type: "command";
|
|
46
|
+
command: string;
|
|
47
|
+
timeout?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const isRec = (v: unknown): v is Record<string, unknown> =>
|
|
51
|
+
typeof v === "object" && v !== null && !Array.isArray(v);
|
|
52
|
+
|
|
53
|
+
/** The `hooks.PreToolUse` array, or [] for any shape that is not one. */
|
|
54
|
+
function preToolUseBlocks(
|
|
55
|
+
settings: Record<string, unknown>,
|
|
56
|
+
): Record<string, unknown>[] {
|
|
57
|
+
const hooks = settings.hooks;
|
|
58
|
+
if (!isRec(hooks)) return [];
|
|
59
|
+
const blocks = hooks.PreToolUse;
|
|
60
|
+
if (!Array.isArray(blocks)) return [];
|
|
61
|
+
return blocks.filter(isRec);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The command entries inside one matcher block. */
|
|
65
|
+
function commandsIn(block: Record<string, unknown>): Record<string, unknown>[] {
|
|
66
|
+
return Array.isArray(block.hooks) ? block.hooks.filter(isRec) : [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isOurCommand(entry: Record<string, unknown>): boolean {
|
|
70
|
+
return (
|
|
71
|
+
typeof entry.command === "string" &&
|
|
72
|
+
entry.command.trim() === AGENT_MODEL_HOOK_COMMAND
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Is the agent-model hook already registered in this settings object? */
|
|
77
|
+
export function isHookRegistered(settings: Record<string, unknown>): boolean {
|
|
78
|
+
return preToolUseBlocks(settings).some((block) =>
|
|
79
|
+
commandsIn(block).some(isOurCommand),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function newCommandEntry(): HookCommand {
|
|
84
|
+
return {
|
|
85
|
+
type: "command",
|
|
86
|
+
command: AGENT_MODEL_HOOK_COMMAND,
|
|
87
|
+
timeout: TIMEOUT_SECONDS,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Add the hook if it is missing. Returns a NEW settings object — the input is
|
|
93
|
+
* never mutated, so a caller can diff before deciding to write.
|
|
94
|
+
*/
|
|
95
|
+
export function ensureHookRegistered(settings: Record<string, unknown>): {
|
|
96
|
+
settings: Record<string, unknown>;
|
|
97
|
+
changed: boolean;
|
|
98
|
+
} {
|
|
99
|
+
if (isHookRegistered(settings)) return { settings, changed: false };
|
|
100
|
+
|
|
101
|
+
const next = structuredClone(settings);
|
|
102
|
+
const hooks = isRec(next.hooks) ? next.hooks : {};
|
|
103
|
+
const blocks = Array.isArray(hooks.PreToolUse) ? [...hooks.PreToolUse] : [];
|
|
104
|
+
|
|
105
|
+
// Append into an existing Agent block rather than adding a second one: two
|
|
106
|
+
// blocks with the same matcher both fire, which works, but it makes the
|
|
107
|
+
// user's settings file progressively less readable and is indistinguishable
|
|
108
|
+
// from a bug when they open it.
|
|
109
|
+
const existing = blocks.findIndex(
|
|
110
|
+
(b) => isRec(b) && b.matcher === MATCHER && Array.isArray(b.hooks),
|
|
111
|
+
);
|
|
112
|
+
if (existing >= 0) {
|
|
113
|
+
const block = blocks[existing] as Record<string, unknown>;
|
|
114
|
+
blocks[existing] = {
|
|
115
|
+
...block,
|
|
116
|
+
hooks: [...(block.hooks as unknown[]), newCommandEntry()],
|
|
117
|
+
};
|
|
118
|
+
} else {
|
|
119
|
+
blocks.push({ matcher: MATCHER, hooks: [newCommandEntry()] });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
next.hooks = { ...hooks, PreToolUse: blocks };
|
|
123
|
+
return { settings: next, changed: true };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Remove every registration of the hook. An Agent block left with no commands
|
|
128
|
+
* is dropped, so uninstalling leaves no trace; other blocks and other commands
|
|
129
|
+
* inside a shared Agent block are untouched.
|
|
130
|
+
*/
|
|
131
|
+
export function removeHookRegistration(settings: Record<string, unknown>): {
|
|
132
|
+
settings: Record<string, unknown>;
|
|
133
|
+
changed: boolean;
|
|
134
|
+
} {
|
|
135
|
+
if (!isHookRegistered(settings)) return { settings, changed: false };
|
|
136
|
+
|
|
137
|
+
const next = structuredClone(settings);
|
|
138
|
+
const hooks = next.hooks as Record<string, unknown>;
|
|
139
|
+
const blocks: unknown[] = [];
|
|
140
|
+
for (const block of hooks.PreToolUse as unknown[]) {
|
|
141
|
+
if (!isRec(block) || !Array.isArray(block.hooks)) {
|
|
142
|
+
blocks.push(block);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const kept = block.hooks.filter((e) => !(isRec(e) && isOurCommand(e)));
|
|
146
|
+
if (kept.length === 0 && block.matcher === MATCHER) continue;
|
|
147
|
+
blocks.push({ ...block, hooks: kept });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (blocks.length > 0) {
|
|
151
|
+
hooks.PreToolUse = blocks;
|
|
152
|
+
} else {
|
|
153
|
+
// biome-ignore lint/performance/noDelete: an empty PreToolUse array is noise in a file people read
|
|
154
|
+
delete hooks.PreToolUse;
|
|
155
|
+
}
|
|
156
|
+
if (Object.keys(hooks).length === 0) {
|
|
157
|
+
// biome-ignore lint/performance/noDelete: same reason — leave no empty husk
|
|
158
|
+
delete next.hooks;
|
|
159
|
+
}
|
|
160
|
+
return { settings: next, changed: true };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The user-scope settings file this writes to.
|
|
165
|
+
*
|
|
166
|
+
* `claudeConfigDirOrNull` honours `CLAUDE_CONFIG_DIR` and returns null under
|
|
167
|
+
* `bun test` when no test has chosen a directory — which is a refusal, not a
|
|
168
|
+
* fallback: writing a hook into the operator's real `~/.claude/settings.json`
|
|
169
|
+
* from a test run is exactly the damage that guard exists to prevent.
|
|
170
|
+
*/
|
|
171
|
+
function userSettingsPath(configDir?: string): string {
|
|
172
|
+
const dir = configDir ?? claudeConfigDirOrNull();
|
|
173
|
+
if (!dir) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
"No Claude config directory resolved. Set CLAUDE_CONFIG_DIR (a test must isolate it) before registering the agent-model hook.",
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return path.join(dir, "settings.json");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Register the hook at user scope, once.
|
|
183
|
+
*
|
|
184
|
+
* User scope on purpose: routing is configured per project, but the hook that
|
|
185
|
+
* applies it has to be running in whatever session opens that project, and a
|
|
186
|
+
* project-scope hook would only exist for projects magus had already
|
|
187
|
+
* touched. The hook itself is a no-op for a project with no `models.json`, so
|
|
188
|
+
* one global registration costs nothing everywhere else.
|
|
189
|
+
*/
|
|
190
|
+
export async function registerAgentModelHook(
|
|
191
|
+
opts: { configDir?: string } = {},
|
|
192
|
+
): Promise<"registered" | "already"> {
|
|
193
|
+
const settingsPath = userSettingsPath(opts.configDir);
|
|
194
|
+
// Read first so an unchanged file is not rewritten (and its mtime not
|
|
195
|
+
// touched) on every `models use`.
|
|
196
|
+
if (isHookRegistered(await readSettingsFile(settingsPath))) return "already";
|
|
197
|
+
|
|
198
|
+
await updateSettingsFile(settingsPath, (settings) => {
|
|
199
|
+
return ensureHookRegistered(settings).settings;
|
|
200
|
+
});
|
|
201
|
+
return "registered";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Is the hook registered in the user-scope settings file? */
|
|
205
|
+
export async function isAgentModelHookRegistered(
|
|
206
|
+
opts: { configDir?: string } = {},
|
|
207
|
+
): Promise<boolean> {
|
|
208
|
+
try {
|
|
209
|
+
return isHookRegistered(
|
|
210
|
+
await readSettingsFile(userSettingsPath(opts.configDir)),
|
|
211
|
+
);
|
|
212
|
+
} catch {
|
|
213
|
+
// No config dir, or a settings file we cannot parse. Either way we cannot
|
|
214
|
+
// claim it is registered — and the caller (doctor / status) reports that
|
|
215
|
+
// as "not routed", which is the honest answer.
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure planning helpers for `magus install`.
|
|
3
|
+
*
|
|
4
|
+
* The impure "what does this machine already have" probing lives in the command
|
|
5
|
+
* (cli/install.ts); everything here is a pure function of a ResolvedClosure and
|
|
6
|
+
* some captured machine state, so it can be unit-tested and reused by
|
|
7
|
+
* `install --check` (the CI drift gate).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import semver from "semver";
|
|
11
|
+
import type {
|
|
12
|
+
InstalledPluginsRegistry,
|
|
13
|
+
ResolvedClosure,
|
|
14
|
+
} from "../types/index.js";
|
|
15
|
+
|
|
16
|
+
export interface VersionDrift {
|
|
17
|
+
pluginId: string;
|
|
18
|
+
/** The version the manifest pins. */
|
|
19
|
+
pinned: string;
|
|
20
|
+
/** The highest installed version, or null if not installed. */
|
|
21
|
+
installed: string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Highest valid semver in a list, or the raw first value if none parse. */
|
|
25
|
+
function maxVersion(versions: string[]): string | null {
|
|
26
|
+
const valid = versions.filter((v) => semver.valid(v));
|
|
27
|
+
const highest = valid.sort(semver.rcompare)[0];
|
|
28
|
+
return highest ?? versions[0] ?? null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Compare the closure's pinned plugin versions against the installed registry.
|
|
33
|
+
* Only plugins with an explicit pin (not "latest") are checked. Returns the set
|
|
34
|
+
* of plugins whose installed version doesn't match the pin — the signal
|
|
35
|
+
* `install --check` fails on.
|
|
36
|
+
*/
|
|
37
|
+
export function computeVersionDrift(
|
|
38
|
+
closure: ResolvedClosure,
|
|
39
|
+
registry: InstalledPluginsRegistry,
|
|
40
|
+
): VersionDrift[] {
|
|
41
|
+
const drifts: VersionDrift[] = [];
|
|
42
|
+
for (const [pluginId, pinned] of Object.entries(closure.plugins)) {
|
|
43
|
+
if (pinned === "latest") continue;
|
|
44
|
+
const entries = registry.plugins?.[pluginId] ?? [];
|
|
45
|
+
const installed = entries.length
|
|
46
|
+
? maxVersion(entries.map((e) => e.version))
|
|
47
|
+
: null;
|
|
48
|
+
if (installed !== pinned) {
|
|
49
|
+
drifts.push({ pluginId, pinned, installed });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return drifts;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface EnvStatus {
|
|
56
|
+
required: { name: string; present: boolean }[];
|
|
57
|
+
optional: { name: string; present: boolean }[];
|
|
58
|
+
/** Required vars that are unset — must be collected before the profile works. */
|
|
59
|
+
missingRequired: string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Which of the closure's declared env vars are present in `env` (process.env). */
|
|
63
|
+
export function computeEnvStatus(
|
|
64
|
+
closure: ResolvedClosure,
|
|
65
|
+
env: Record<string, string | undefined> = process.env,
|
|
66
|
+
): EnvStatus {
|
|
67
|
+
const check = (name: string) => ({
|
|
68
|
+
name,
|
|
69
|
+
present: typeof env[name] === "string" && env[name] !== "",
|
|
70
|
+
});
|
|
71
|
+
const required = closure.env.required.map(check);
|
|
72
|
+
const optional = closure.env.optional.map(check);
|
|
73
|
+
return {
|
|
74
|
+
required,
|
|
75
|
+
optional,
|
|
76
|
+
missingRequired: required.filter((e) => !e.present).map((e) => e.name),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A one-line-per-item human summary of what a closure contains, for the
|
|
82
|
+
* pre-install plan display. Machine-state annotations (already installed, etc.)
|
|
83
|
+
* are layered on by the caller.
|
|
84
|
+
*/
|
|
85
|
+
export function summarizeClosure(closure: ResolvedClosure): string[] {
|
|
86
|
+
const lines: string[] = [];
|
|
87
|
+
const mps = Object.keys(closure.marketplaces);
|
|
88
|
+
if (mps.length) lines.push(`marketplaces ${mps.join(", ")}`);
|
|
89
|
+
const plugins = Object.entries(closure.plugins).map(([id, v]) =>
|
|
90
|
+
v === "latest" ? id : `${id}@${v}`,
|
|
91
|
+
);
|
|
92
|
+
if (plugins.length) lines.push(`plugins ${plugins.join(", ")}`);
|
|
93
|
+
const mcps = Object.keys(closure.mcpServers);
|
|
94
|
+
if (mcps.length) lines.push(`mcp servers ${mcps.join(", ")}`);
|
|
95
|
+
if (closure.bins.length) {
|
|
96
|
+
lines.push(
|
|
97
|
+
`cli tools ${closure.bins.map((b) => (b.version ? `${b.name}@${b.version}` : b.name)).join(", ")}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (closure.skills.length) {
|
|
101
|
+
lines.push(`skills ${closure.skills.map((s) => s.name).join(", ")}`);
|
|
102
|
+
}
|
|
103
|
+
if (closure.env.required.length) {
|
|
104
|
+
lines.push(`env ${closure.env.required.join(", ")}`);
|
|
105
|
+
}
|
|
106
|
+
return lines;
|
|
107
|
+
}
|