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,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile symlink manager.
|
|
3
|
+
*
|
|
4
|
+
* Materialized profiles live at `.claude/_profiles/<name>/` (generated,
|
|
5
|
+
* gitignored). The active profile is made "live" by symlinking the shared
|
|
6
|
+
* config files into place:
|
|
7
|
+
*
|
|
8
|
+
* .claude/settings.json -> .claude/_profiles/<name>/settings.json
|
|
9
|
+
* .claude/skills -> .claude/_profiles/<name>/skills
|
|
10
|
+
* .mcp.json -> .claude/_profiles/<name>/mcp.json
|
|
11
|
+
* .claude/models.json -> .claude/_profiles/<name>/models.json
|
|
12
|
+
*
|
|
13
|
+
* Switching profiles just repoints these links — no network, no reinstall.
|
|
14
|
+
* settings.local.json is NEVER linked: it holds personal credentials and must
|
|
15
|
+
* survive switches untouched.
|
|
16
|
+
*
|
|
17
|
+
* models.json is the one link whose target is OPTIONAL: a profile that declares
|
|
18
|
+
* no routing has none, and the rule below (remove a link into _profiles/ whose
|
|
19
|
+
* target is gone) is what keeps a dangling `.claude/models.json` out of the
|
|
20
|
+
* repo. A dangling one would not be harmless — `.claude/` is committed, so it
|
|
21
|
+
* would ship to every teammate.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import fs from "fs-extra";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Paths that must be gitignored once a project uses profiles: the generated
|
|
29
|
+
* build output plus the active-profile symlinks. Only .claude/profiles.json is
|
|
30
|
+
* the committed source of truth. settings.local.json is intentionally NOT here
|
|
31
|
+
* (it's already gitignored elsewhere and holds personal credentials).
|
|
32
|
+
*/
|
|
33
|
+
export const PROFILE_GITIGNORE_ENTRIES = [
|
|
34
|
+
".claude/_profiles/",
|
|
35
|
+
".claude/settings.json",
|
|
36
|
+
".claude/skills",
|
|
37
|
+
".claude/models.json",
|
|
38
|
+
".mcp.json",
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/** The generated, gitignored directory holding materialized profiles. */
|
|
42
|
+
export function profilesRoot(projectPath?: string): string {
|
|
43
|
+
const base = projectPath ?? process.cwd();
|
|
44
|
+
return path.join(base, ".claude", "_profiles");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The materialized directory for one profile. */
|
|
48
|
+
export function profileDir(name: string, projectPath?: string): string {
|
|
49
|
+
return path.join(profilesRoot(projectPath), name);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The artifacts an active profile links into place. */
|
|
53
|
+
export type ProfileLinkKey = "settings" | "skills" | "mcp" | "models";
|
|
54
|
+
|
|
55
|
+
/** The set of links an active profile owns, as [linkPath, targetPath] pairs. */
|
|
56
|
+
export function linkTargets(
|
|
57
|
+
name: string,
|
|
58
|
+
projectPath?: string,
|
|
59
|
+
): Array<{ key: ProfileLinkKey; link: string; target: string }> {
|
|
60
|
+
const base = projectPath ?? process.cwd();
|
|
61
|
+
const dir = profileDir(name, projectPath);
|
|
62
|
+
return [
|
|
63
|
+
{
|
|
64
|
+
key: "settings",
|
|
65
|
+
link: path.join(base, ".claude", "settings.json"),
|
|
66
|
+
target: path.join(dir, "settings.json"),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
key: "skills",
|
|
70
|
+
link: path.join(base, ".claude", "skills"),
|
|
71
|
+
target: path.join(dir, "skills"),
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
key: "mcp",
|
|
75
|
+
link: path.join(base, ".mcp.json"),
|
|
76
|
+
target: path.join(dir, "mcp.json"),
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
key: "models",
|
|
80
|
+
link: path.join(base, ".claude", "models.json"),
|
|
81
|
+
target: path.join(dir, "models.json"),
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Point `link` at `target` with a relative symlink. Any existing file, dir, or
|
|
88
|
+
* link at `link` is removed first. The target need not exist yet (dangling
|
|
89
|
+
* links are tolerated so a profile can be activated before every artifact is
|
|
90
|
+
* materialized), but its parent is created.
|
|
91
|
+
*/
|
|
92
|
+
async function relink(link: string, target: string): Promise<void> {
|
|
93
|
+
await fs.ensureDir(path.dirname(link));
|
|
94
|
+
await fs.remove(link);
|
|
95
|
+
const rel = path.relative(path.dirname(link), target);
|
|
96
|
+
await fs.symlink(rel, link);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Activate a profile: repoint every shared-config symlink into its
|
|
101
|
+
* materialized directory. Only links whose target exists in the profile dir
|
|
102
|
+
* are created (e.g. a profile with no skills/ dir gets no skills symlink).
|
|
103
|
+
*/
|
|
104
|
+
export async function activateProfile(
|
|
105
|
+
name: string,
|
|
106
|
+
projectPath?: string,
|
|
107
|
+
): Promise<void> {
|
|
108
|
+
const dir = profileDir(name, projectPath);
|
|
109
|
+
if (!(await fs.pathExists(dir))) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Profile "${name}" is not materialized (${dir} missing). Run \`magus install ${name}\`.`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
for (const { link, target } of linkTargets(name, projectPath)) {
|
|
115
|
+
await syncLink(link, target, projectPath);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Point one link at its target, or remove it when the profile has no such
|
|
121
|
+
* artifact. A link is only removed when it points into `_profiles/` — a real
|
|
122
|
+
* file the user put there is never clobbered.
|
|
123
|
+
*/
|
|
124
|
+
async function syncLink(
|
|
125
|
+
link: string,
|
|
126
|
+
target: string,
|
|
127
|
+
projectPath?: string,
|
|
128
|
+
): Promise<void> {
|
|
129
|
+
if (await fs.pathExists(target)) {
|
|
130
|
+
await relink(link, target);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (await isProfileLink(link, projectPath)) await fs.remove(link);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Sync ONE of the active profile's links, leaving the others alone.
|
|
138
|
+
*
|
|
139
|
+
* `activateProfile` repoints all four, which is right when switching profiles
|
|
140
|
+
* and wrong when only one artifact changed: `magus models use` would replace
|
|
141
|
+
* a project's real `.mcp.json` with a symlink as a side effect of choosing a
|
|
142
|
+
* model. This is the narrow door for that case.
|
|
143
|
+
*/
|
|
144
|
+
export async function syncProfileLink(
|
|
145
|
+
name: string,
|
|
146
|
+
key: ProfileLinkKey,
|
|
147
|
+
projectPath?: string,
|
|
148
|
+
): Promise<void> {
|
|
149
|
+
const entry = linkTargets(name, projectPath).find((t) => t.key === key);
|
|
150
|
+
if (!entry) return;
|
|
151
|
+
await syncLink(entry.link, entry.target, projectPath);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Is `link` a symlink pointing into `.claude/_profiles/`? */
|
|
155
|
+
async function isProfileLink(
|
|
156
|
+
link: string,
|
|
157
|
+
projectPath?: string,
|
|
158
|
+
): Promise<boolean> {
|
|
159
|
+
try {
|
|
160
|
+
const stat = await fs.lstat(link);
|
|
161
|
+
if (!stat.isSymbolicLink()) return false;
|
|
162
|
+
const resolved = path.resolve(path.dirname(link), await fs.readlink(link));
|
|
163
|
+
return resolved.startsWith(profilesRoot(projectPath) + path.sep);
|
|
164
|
+
} catch {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The name of the currently active profile, derived from where
|
|
171
|
+
* .claude/settings.json points. Returns null if it isn't a profile symlink.
|
|
172
|
+
*/
|
|
173
|
+
export async function activeProfile(
|
|
174
|
+
projectPath?: string,
|
|
175
|
+
): Promise<string | null> {
|
|
176
|
+
const base = projectPath ?? process.cwd();
|
|
177
|
+
const link = path.join(base, ".claude", "settings.json");
|
|
178
|
+
try {
|
|
179
|
+
const stat = await fs.lstat(link);
|
|
180
|
+
if (!stat.isSymbolicLink()) return null;
|
|
181
|
+
const resolved = path.resolve(path.dirname(link), await fs.readlink(link));
|
|
182
|
+
const root = profilesRoot(projectPath) + path.sep;
|
|
183
|
+
if (!resolved.startsWith(root)) return null;
|
|
184
|
+
// resolved = <root>/<name>/settings.json → take the first segment.
|
|
185
|
+
return resolved.slice(root.length).split(path.sep)[0] ?? null;
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fill the `terminology` template preset from the project's own codebase.
|
|
3
|
+
*
|
|
4
|
+
* `terminology` ships as a template: a vocabulary table with a placeholder row,
|
|
5
|
+
* useless until someone fills it with THIS project's words. Doing that by hand
|
|
6
|
+
* means reading the codebase and noticing where two names describe one concept
|
|
7
|
+
* — exactly the sort of survey a coding agent is good at.
|
|
8
|
+
*
|
|
9
|
+
* ## Why Claude only returns rows
|
|
10
|
+
*
|
|
11
|
+
* The subprocess is asked for table rows and nothing else; magus does the
|
|
12
|
+
* file writing. Two reasons, both practical:
|
|
13
|
+
*
|
|
14
|
+
* 1. **No write permission is needed.** `claude -p` runs with read-only tools,
|
|
15
|
+
* so the worst case of a bad run is a useless answer, never a modified
|
|
16
|
+
* repository.
|
|
17
|
+
* 2. **The template's fixed rules cannot be lost.** Everything below the table
|
|
18
|
+
* is substantive guidance that must survive verbatim. Asking a model to
|
|
19
|
+
* reproduce a whole file invites it to paraphrase; asking for six table
|
|
20
|
+
* rows does not.
|
|
21
|
+
*
|
|
22
|
+
* The TUI stays on screen throughout: the child's stdio is piped, never
|
|
23
|
+
* inherited, so it cannot take the terminal away from OpenTUI the way
|
|
24
|
+
* `runClaude` deliberately does for CLI use.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { spawn } from "node:child_process";
|
|
28
|
+
import path from "node:path";
|
|
29
|
+
import fs from "fs-extra";
|
|
30
|
+
import { which } from "../utils/command-utils.js";
|
|
31
|
+
|
|
32
|
+
export interface TerminologyRow {
|
|
33
|
+
use: string;
|
|
34
|
+
not: string;
|
|
35
|
+
because: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The placeholder row the shipped template carries. */
|
|
39
|
+
const PLACEHOLDER = /^\|.*filled during.*\|.*$/im;
|
|
40
|
+
|
|
41
|
+
/** Table rows are asked for in this exact shape, so the parse can be strict. */
|
|
42
|
+
export function parseTerminologyRows(stdout: string): TerminologyRow[] {
|
|
43
|
+
const rows: TerminologyRow[] = [];
|
|
44
|
+
for (const raw of stdout.split("\n")) {
|
|
45
|
+
const line = raw.trim();
|
|
46
|
+
if (!line.startsWith("|")) continue;
|
|
47
|
+
|
|
48
|
+
// The trailing pipe is optional. Both are valid markdown and a model
|
|
49
|
+
// asked for `| a | b | c |` will sometimes emit it without the closer —
|
|
50
|
+
// discarding that row would silently lose a real term.
|
|
51
|
+
const cells = line
|
|
52
|
+
.slice(1)
|
|
53
|
+
.replace(/\|$/, "")
|
|
54
|
+
.split("|")
|
|
55
|
+
.map((cell) => cell.trim());
|
|
56
|
+
if (cells.length !== 3) continue;
|
|
57
|
+
|
|
58
|
+
// A model that ignores "no header row" tends to emit the header and the
|
|
59
|
+
// `|---|---|---|` separator anyway. Both are noise, not data.
|
|
60
|
+
if (cells.every((cell) => /^:?-{2,}:?$/.test(cell))) continue;
|
|
61
|
+
if (cells[0].toLowerCase() === "use" && cells[1].toLowerCase() === "not") {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!cells[0] || !cells[1]) continue;
|
|
65
|
+
|
|
66
|
+
rows.push({ use: cells[0], not: cells[1], because: cells[2] });
|
|
67
|
+
}
|
|
68
|
+
return rows;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Put the rows into the template, replacing its placeholder.
|
|
73
|
+
*
|
|
74
|
+
* Everything else in the body — the heading, the opening statement, and the
|
|
75
|
+
* rules below the table — is copied through untouched.
|
|
76
|
+
*/
|
|
77
|
+
export function fillTemplate(
|
|
78
|
+
templateBody: string,
|
|
79
|
+
rows: TerminologyRow[],
|
|
80
|
+
): string {
|
|
81
|
+
if (rows.length === 0) {
|
|
82
|
+
throw new Error("No usable table rows were produced.");
|
|
83
|
+
}
|
|
84
|
+
const rendered = rows
|
|
85
|
+
.map((row) => `| ${row.use} | ${row.not} | ${row.because} |`)
|
|
86
|
+
.join("\n");
|
|
87
|
+
|
|
88
|
+
if (PLACEHOLDER.test(templateBody)) {
|
|
89
|
+
return templateBody.replace(PLACEHOLDER, rendered);
|
|
90
|
+
}
|
|
91
|
+
// The template changed shape upstream. Appending after the separator is
|
|
92
|
+
// still better than silently returning a table with no rows.
|
|
93
|
+
return templateBody.replace(/^(\|\s*-{2,}.*\|)\s*$/im, `$1\n${rendered}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The complete output-style file for the filled terminology preset. */
|
|
97
|
+
export function terminologyStyleFile(body: string): string {
|
|
98
|
+
return [
|
|
99
|
+
"---",
|
|
100
|
+
"name: terminology",
|
|
101
|
+
'description: "Project vocabulary — one name per concept"',
|
|
102
|
+
// Same reasoning as everywhere else: a communication style must not
|
|
103
|
+
// switch off Claude Code's coding rules.
|
|
104
|
+
"keep-coding-instructions: true",
|
|
105
|
+
'generated-by: "magus Styles tab, filled from the codebase by Claude Code."',
|
|
106
|
+
"---",
|
|
107
|
+
"",
|
|
108
|
+
body.trim(),
|
|
109
|
+
"",
|
|
110
|
+
].join("\n");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function buildPrompt(templateBody: string): string {
|
|
114
|
+
return [
|
|
115
|
+
"You are filling in one table for a project's communication-style guide.",
|
|
116
|
+
"",
|
|
117
|
+
"Read enough of this codebase to identify its domain vocabulary: the nouns",
|
|
118
|
+
"that name its core concepts, and the places where two different words are",
|
|
119
|
+
"used for the same thing.",
|
|
120
|
+
"",
|
|
121
|
+
"Output ONLY markdown table rows, one per line, in exactly this form:",
|
|
122
|
+
"",
|
|
123
|
+
"| use | not | because |",
|
|
124
|
+
"",
|
|
125
|
+
"Rules:",
|
|
126
|
+
"- No header row, no `|---|` separator, no code fence, no commentary.",
|
|
127
|
+
"- Between 5 and 15 rows.",
|
|
128
|
+
// Bounded on purpose: without a budget this wanders a large repository
|
|
129
|
+
// for many minutes, and the marginal term found on the 80th file is not
|
|
130
|
+
// worth the wait.
|
|
131
|
+
"- Budget yourself: read at most ~25 files. Prefer README, the main",
|
|
132
|
+
" entry points, type definitions and directory names over exhaustive",
|
|
133
|
+
" coverage — the vocabulary shows up there first.",
|
|
134
|
+
"- Every row must come from THIS codebase. Do not invent generic examples.",
|
|
135
|
+
"- `use` is the name the project should standardise on.",
|
|
136
|
+
"- `not` is a synonym actually present in the repo, or one a newcomer would",
|
|
137
|
+
" plausibly reach for.",
|
|
138
|
+
"- `because` is one clause, lower case, no trailing full stop.",
|
|
139
|
+
"",
|
|
140
|
+
"For context, this is the guide the table goes into:",
|
|
141
|
+
"",
|
|
142
|
+
templateBody,
|
|
143
|
+
].join("\n");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* How long to let the survey run before giving up.
|
|
148
|
+
*
|
|
149
|
+
* Generous, because reading a large repository legitimately takes minutes — but
|
|
150
|
+
* bounded, because without it a wedged subprocess leaves the screen showing
|
|
151
|
+
* "reading the codebase" forever with no way back.
|
|
152
|
+
*/
|
|
153
|
+
export const FILL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
154
|
+
|
|
155
|
+
export interface FillTerminologyArgs {
|
|
156
|
+
projectPath: string;
|
|
157
|
+
/** Body of the shipped `terminology` preset, table placeholder included. */
|
|
158
|
+
templateBody: string;
|
|
159
|
+
/** Injectable for tests. Returns the subprocess's stdout. */
|
|
160
|
+
run?: (prompt: string, projectPath: string) => Promise<string>;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface FillTerminologyResult {
|
|
164
|
+
path: string;
|
|
165
|
+
rows: TerminologyRow[];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Run Claude Code over the project and write the filled preset.
|
|
170
|
+
*
|
|
171
|
+
* Written to the PROJECT's output-styles directory, so the vocabulary it
|
|
172
|
+
* discovers commits with the repository — a project's terminology is a team
|
|
173
|
+
* fact, not one person's setting.
|
|
174
|
+
*/
|
|
175
|
+
export async function fillTerminology({
|
|
176
|
+
projectPath,
|
|
177
|
+
templateBody,
|
|
178
|
+
run = runClaudeHeadless,
|
|
179
|
+
}: FillTerminologyArgs): Promise<FillTerminologyResult> {
|
|
180
|
+
const stdout = await run(buildPrompt(templateBody), projectPath);
|
|
181
|
+
const rows = parseTerminologyRows(stdout);
|
|
182
|
+
if (rows.length === 0) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
"Claude Code returned no usable table rows — nothing was written.",
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const file = path.join(
|
|
189
|
+
projectPath,
|
|
190
|
+
".claude",
|
|
191
|
+
"output-styles",
|
|
192
|
+
"terminology.md",
|
|
193
|
+
);
|
|
194
|
+
await fs.ensureDir(path.dirname(file));
|
|
195
|
+
await fs.writeFile(
|
|
196
|
+
file,
|
|
197
|
+
terminologyStyleFile(fillTemplate(templateBody, rows)),
|
|
198
|
+
"utf8",
|
|
199
|
+
);
|
|
200
|
+
return { path: file, rows };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* `claude -p` with read-only tools, stdio piped.
|
|
205
|
+
*
|
|
206
|
+
* `--allowedTools` is the point: the subprocess can look at the repository and
|
|
207
|
+
* cannot change it, so a bad run costs time and nothing else.
|
|
208
|
+
*/
|
|
209
|
+
async function runClaudeHeadless(
|
|
210
|
+
prompt: string,
|
|
211
|
+
projectPath: string,
|
|
212
|
+
): Promise<string> {
|
|
213
|
+
const claudePath = await which("claude");
|
|
214
|
+
if (!claudePath) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
"claude CLI not found in PATH. Install it with: npm install -g @anthropic-ai/claude-code",
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return new Promise((resolve, reject) => {
|
|
221
|
+
const child = spawn(
|
|
222
|
+
claudePath,
|
|
223
|
+
["-p", prompt, "--allowedTools", "Read,Grep,Glob"],
|
|
224
|
+
{
|
|
225
|
+
cwd: projectPath,
|
|
226
|
+
// Piped, never inherited — inheriting would hand our TTY to the
|
|
227
|
+
// child and tear the TUI apart mid-render.
|
|
228
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
229
|
+
},
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
let out = "";
|
|
233
|
+
let err = "";
|
|
234
|
+
child.stdout.on("data", (chunk) => {
|
|
235
|
+
out += String(chunk);
|
|
236
|
+
});
|
|
237
|
+
child.stderr.on("data", (chunk) => {
|
|
238
|
+
err += String(chunk);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const timer = setTimeout(() => {
|
|
242
|
+
child.kill("SIGTERM");
|
|
243
|
+
reject(
|
|
244
|
+
new Error(
|
|
245
|
+
`Gave up after ${Math.round(FILL_TIMEOUT_MS / 60000)} minutes — nothing was written. Try again, or fill the table by hand.`,
|
|
246
|
+
),
|
|
247
|
+
);
|
|
248
|
+
}, FILL_TIMEOUT_MS);
|
|
249
|
+
|
|
250
|
+
child.once("error", (error) => {
|
|
251
|
+
clearTimeout(timer);
|
|
252
|
+
reject(error);
|
|
253
|
+
});
|
|
254
|
+
child.once("close", (code) => {
|
|
255
|
+
clearTimeout(timer);
|
|
256
|
+
if (code === 0) resolve(out);
|
|
257
|
+
else {
|
|
258
|
+
reject(
|
|
259
|
+
new Error(
|
|
260
|
+
`claude exited ${code}${err.trim() ? `: ${err.trim().split("\n")[0]}` : ""}`,
|
|
261
|
+
),
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
}
|