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,232 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
PYTHON_CLI_INSTALLERS,
|
|
4
|
+
type PythonCliInstaller,
|
|
5
|
+
binInstallCommand,
|
|
6
|
+
binUpgradeCommand,
|
|
7
|
+
detectToolchains,
|
|
8
|
+
resolvePythonCliInstaller,
|
|
9
|
+
} from "../services/toolchain.js";
|
|
10
|
+
import type { ResolvedBin } from "../types/index.js";
|
|
11
|
+
import { formatCommand } from "../utils/command-utils.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Pin a Python installer explicitly in every `via: "pip"` assertion.
|
|
15
|
+
*
|
|
16
|
+
* The production default is resolved from PATH, so asserting a literal command
|
|
17
|
+
* would encode whatever happens to be installed on the machine running the
|
|
18
|
+
* suite — green here, red on a runner without uv.
|
|
19
|
+
*/
|
|
20
|
+
const byProbe = (probe: string): PythonCliInstaller => {
|
|
21
|
+
const found = PYTHON_CLI_INSTALLERS.find((i) => i.probe === probe);
|
|
22
|
+
if (!found) throw new Error(`no python installer with probe ${probe}`);
|
|
23
|
+
return found;
|
|
24
|
+
};
|
|
25
|
+
const UV = byProbe("uv");
|
|
26
|
+
const PIPX = byProbe("pipx");
|
|
27
|
+
const PY = byProbe("python3");
|
|
28
|
+
|
|
29
|
+
describe("binInstallCommand", () => {
|
|
30
|
+
test("bun with and without a version pin", () => {
|
|
31
|
+
expect(
|
|
32
|
+
formatCommand(
|
|
33
|
+
binInstallCommand({
|
|
34
|
+
name: "claudish",
|
|
35
|
+
via: "bun",
|
|
36
|
+
package: "claudish",
|
|
37
|
+
version: "1.2.0",
|
|
38
|
+
}),
|
|
39
|
+
),
|
|
40
|
+
).toBe("bun install -g claudish@1.2.0");
|
|
41
|
+
expect(
|
|
42
|
+
formatCommand(binInstallCommand({ name: "claudish", via: "bun" })),
|
|
43
|
+
).toBe("bun install -g claudish");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("the package name is ONE argv token, whatever is in it", () => {
|
|
47
|
+
// `bin.package` is hand-authored data — it arrives from
|
|
48
|
+
// `.claude/profiles.json` and from plugin `requires.bin`, committed by
|
|
49
|
+
// teammates and fetched from marketplaces, and nothing validates it.
|
|
50
|
+
// While commands were strings handed to `/bin/sh`, this string was four
|
|
51
|
+
// words and a command separator. As argv it cannot be anything but a
|
|
52
|
+
// (nonexistent) package name, so the install fails instead of running.
|
|
53
|
+
const evil = "a b; rm -rf /";
|
|
54
|
+
const cmd = binInstallCommand({ name: "x", via: "bun", package: evil });
|
|
55
|
+
expect(cmd.cmd).toBe("bun");
|
|
56
|
+
expect(cmd.args).toEqual(["install", "-g", evil]);
|
|
57
|
+
expect(cmd.args.at(-1)).toBe(evil); // unsplit: still exactly one token
|
|
58
|
+
// ...and rendering it for display quotes it rather than emitting syntax.
|
|
59
|
+
expect(formatCommand(cmd)).toBe("bun install -g 'a b; rm -rf /'");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("a version pin joins the package as one token, not two", () => {
|
|
63
|
+
// Splitting `pkg@version` would ask the installer for two packages, the
|
|
64
|
+
// second of which is a version number.
|
|
65
|
+
const cmd = binInstallCommand({
|
|
66
|
+
name: "claudish",
|
|
67
|
+
via: "bun",
|
|
68
|
+
version: "1.2.0",
|
|
69
|
+
});
|
|
70
|
+
expect(cmd.args.at(-1)).toBe("claudish@1.2.0");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("npm, pip, brew, go each render correctly", () => {
|
|
74
|
+
// Rendered, not raw, because these literals are also the display-
|
|
75
|
+
// neutrality claim: every token here is shell-safe, so `formatCommand`
|
|
76
|
+
// quotes nothing and the printed line is byte-identical to the string
|
|
77
|
+
// these builders returned before they returned argv.
|
|
78
|
+
expect(
|
|
79
|
+
formatCommand(binInstallCommand({ name: "x", via: "npm", package: "x" })),
|
|
80
|
+
).toBe("npm install -g x");
|
|
81
|
+
expect(
|
|
82
|
+
formatCommand(
|
|
83
|
+
binInstallCommand(
|
|
84
|
+
{ name: "aider", via: "pip", package: "aider", version: "0.1" },
|
|
85
|
+
UV,
|
|
86
|
+
),
|
|
87
|
+
),
|
|
88
|
+
).toBe("uv tool install aider==0.1");
|
|
89
|
+
expect(
|
|
90
|
+
formatCommand(
|
|
91
|
+
binInstallCommand({ name: "tmux", via: "brew", formula: "tmux" }),
|
|
92
|
+
),
|
|
93
|
+
).toBe("brew install tmux");
|
|
94
|
+
expect(
|
|
95
|
+
formatCommand(
|
|
96
|
+
binInstallCommand({ name: "t", via: "go", module: "github.com/x/t" }),
|
|
97
|
+
),
|
|
98
|
+
).toBe("go install github.com/x/t@latest");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("binUpgradeCommand", () => {
|
|
103
|
+
test("brew and pip do NOT reuse their install command", () => {
|
|
104
|
+
// This is the whole reason the function exists: `brew install` on an
|
|
105
|
+
// installed formula prints "already installed" and changes nothing, and
|
|
106
|
+
// `pip install` without --upgrade does the same. Reusing them would let
|
|
107
|
+
// `magus update` report success while advancing nothing.
|
|
108
|
+
expect(
|
|
109
|
+
formatCommand(
|
|
110
|
+
binUpgradeCommand({ name: "tmux", via: "brew", formula: "tmux" }),
|
|
111
|
+
),
|
|
112
|
+
).toBe("brew upgrade tmux");
|
|
113
|
+
expect(
|
|
114
|
+
formatCommand(
|
|
115
|
+
binUpgradeCommand({ name: "aider", via: "pip", package: "aider" }, PY),
|
|
116
|
+
),
|
|
117
|
+
).toBe("python3 -m pip install --user --upgrade aider");
|
|
118
|
+
expect(
|
|
119
|
+
formatCommand(
|
|
120
|
+
binUpgradeCommand({ name: "aider", via: "pip", package: "aider" }, UV),
|
|
121
|
+
),
|
|
122
|
+
).toBe("uv tool install --force --upgrade aider");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("every Python installer distinguishes install from upgrade", () => {
|
|
126
|
+
// The same trap as brew: `uv tool install` on an already-installed tool
|
|
127
|
+
// is a no-op, so reusing the install command would report success while
|
|
128
|
+
// advancing nothing.
|
|
129
|
+
for (const python of PYTHON_CLI_INSTALLERS) {
|
|
130
|
+
const bin = { name: "aider", via: "pip" as const, package: "aider" };
|
|
131
|
+
// Compared RENDERED. Two `Command` objects are never `toBe`-equal —
|
|
132
|
+
// they are distinct references — so comparing them directly would be
|
|
133
|
+
// an assertion that cannot fail.
|
|
134
|
+
expect(formatCommand(binUpgradeCommand(bin, python))).not.toBe(
|
|
135
|
+
formatCommand(binInstallCommand(bin, python)),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("resolvePythonCliInstaller", () => {
|
|
142
|
+
test("never emits a bare `pip`", () => {
|
|
143
|
+
// The bug this exists for: `pip` is frequently a zsh ALIAS for pip3 and
|
|
144
|
+
// no such executable exists. Commands were spawned through /bin/sh, which
|
|
145
|
+
// sources no profile, so this died with `/bin/sh: pip: command not found`
|
|
146
|
+
// on a machine where typing `pip` at a prompt works.
|
|
147
|
+
//
|
|
148
|
+
// Now that a command is argv, `cmd` is the exact executable that gets
|
|
149
|
+
// looked up on PATH — so asserting on it is asserting on the syscall,
|
|
150
|
+
// not on a string a shell might later re-read.
|
|
151
|
+
for (const python of PYTHON_CLI_INSTALLERS) {
|
|
152
|
+
for (const cmd of [python.install("x"), python.upgrade("x")]) {
|
|
153
|
+
expect(cmd.cmd).not.toBe("pip");
|
|
154
|
+
expect(cmd.cmd).toBe(python.probe);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("prefers an isolated-venv installer over the system interpreter", () => {
|
|
160
|
+
// `pip install` into a Homebrew interpreter is refused outright by PEP
|
|
161
|
+
// 668 (`externally-managed-environment`), so uv and pipx must outrank it.
|
|
162
|
+
const order = PYTHON_CLI_INSTALLERS.map((i) => i.probe);
|
|
163
|
+
expect(order.indexOf("uv")).toBeLessThan(order.indexOf("python3"));
|
|
164
|
+
expect(order.indexOf("pipx")).toBeLessThan(order.indexOf("python3"));
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("always resolves to something, and to one of the known options", () => {
|
|
168
|
+
// Returning null would force every caller to handle "no installer",
|
|
169
|
+
// which in practice means printing nothing and silently skipping.
|
|
170
|
+
const resolved = resolvePythonCliInstaller();
|
|
171
|
+
expect(PYTHON_CLI_INSTALLERS).toContain(resolved);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("a version pin is expressed in the installer's own syntax", () => {
|
|
175
|
+
expect(
|
|
176
|
+
formatCommand(
|
|
177
|
+
binInstallCommand({ name: "a", via: "pip", version: "1.2" }, UV),
|
|
178
|
+
),
|
|
179
|
+
).toBe("uv tool install a==1.2");
|
|
180
|
+
expect(
|
|
181
|
+
formatCommand(
|
|
182
|
+
binInstallCommand({ name: "a", via: "pip", version: "1.2" }, PIPX),
|
|
183
|
+
),
|
|
184
|
+
).toBe("pipx install a==1.2");
|
|
185
|
+
// `a==1.2` is one token. Two would ask uv to install a package called
|
|
186
|
+
// "1.2".
|
|
187
|
+
expect(
|
|
188
|
+
binInstallCommand({ name: "a", via: "pip", version: "1.2" }, UV).args,
|
|
189
|
+
).toEqual(["tool", "install", "a==1.2"]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("bun and npm state @latest explicitly, so the printed command reads true", () => {
|
|
193
|
+
expect(
|
|
194
|
+
formatCommand(binUpgradeCommand({ name: "claudish", via: "bun" })),
|
|
195
|
+
).toBe("bun install -g claudish@latest");
|
|
196
|
+
expect(
|
|
197
|
+
formatCommand(binUpgradeCommand({ name: "x", via: "npm", package: "x" })),
|
|
198
|
+
).toBe("npm install -g x@latest");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("go installs the module at latest", () => {
|
|
202
|
+
expect(
|
|
203
|
+
formatCommand(
|
|
204
|
+
binUpgradeCommand({ name: "t", via: "go", module: "github.com/x/t" }),
|
|
205
|
+
),
|
|
206
|
+
).toBe("go install github.com/x/t@latest");
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("detectToolchains", () => {
|
|
211
|
+
test("groups bins by installer and reports requiredBy + presence", async () => {
|
|
212
|
+
const bins: ResolvedBin[] = [
|
|
213
|
+
{ name: "claudish", via: "bun", sources: ["a"] },
|
|
214
|
+
{ name: "mnemex", via: "bun", sources: ["b"] },
|
|
215
|
+
{ name: "tmux", via: "brew", sources: ["c"] },
|
|
216
|
+
];
|
|
217
|
+
const statuses = await detectToolchains(bins);
|
|
218
|
+
const bun = statuses.find((s) => s.name === "bun")!;
|
|
219
|
+
const brew = statuses.find((s) => s.name === "brew")!;
|
|
220
|
+
expect(bun.requiredBy.sort()).toEqual(["claudish", "mnemex"]);
|
|
221
|
+
expect(brew.requiredBy).toEqual(["tmux"]);
|
|
222
|
+
// bun/brew are bootstrappable; the field is populated.
|
|
223
|
+
expect(bun.bootstrap).toContain("bun.sh");
|
|
224
|
+
expect(typeof bun.present).toBe("boolean");
|
|
225
|
+
// stable order: bun before brew
|
|
226
|
+
expect(statuses.map((s) => s.name)).toEqual(["bun", "brew"]);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("returns only toolchains actually needed (empty for no bins)", async () => {
|
|
230
|
+
expect(await detectToolchains([])).toEqual([]);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The UI records a plugin version only through a function that read it back.
|
|
3
|
+
*
|
|
4
|
+
* `installedPluginVersions` is magus's own bookkeeping — the Claude CLI does
|
|
5
|
+
* not maintain it — and `install --check` reads it to decide whether a machine
|
|
6
|
+
* has drifted from its profile. So a version written from an EXPECTATION rather
|
|
7
|
+
* than from disk is not a cosmetic inaccuracy: the drift gate ends up comparing
|
|
8
|
+
* a number against a copy of itself and reporting clean forever. That is what
|
|
9
|
+
* PluginsScreen's install branches did, via a local `saveVersionForScope`
|
|
10
|
+
* helper that duplicated `plugin-manager.ts`'s scope dispatcher and took the
|
|
11
|
+
* version as a parameter.
|
|
12
|
+
*
|
|
13
|
+
* Deleting the helper removed the UI's *ability* to make that mistake. This
|
|
14
|
+
* test is what keeps it removed: a screen may still install and update, but
|
|
15
|
+
* only through `installPluginInScope`, which reads the version off disk and
|
|
16
|
+
* writes nothing when it cannot.
|
|
17
|
+
*
|
|
18
|
+
* A source scan rather than a behavioural test because these are `.tsx` screens
|
|
19
|
+
* this suite cannot render. It encodes the rule directly instead of the symptom
|
|
20
|
+
* — the precedent for source-scanning guards here is
|
|
21
|
+
* `uppercase-keybindings.test.ts`.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { describe, expect, test } from "bun:test";
|
|
25
|
+
import path from "node:path";
|
|
26
|
+
import fs from "fs-extra";
|
|
27
|
+
|
|
28
|
+
const UI_DIR = path.join(import.meta.dir, "..", "ui");
|
|
29
|
+
|
|
30
|
+
/** The version writers that take the number on trust from their caller. */
|
|
31
|
+
const RAW_WRITERS = [
|
|
32
|
+
"saveGlobalInstalledPluginVersion",
|
|
33
|
+
"saveLocalInstalledPluginVersion",
|
|
34
|
+
"saveInstalledPluginVersion",
|
|
35
|
+
"saveInstalledPluginVersionForScope",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
async function uiSourceFiles(dir: string): Promise<string[]> {
|
|
39
|
+
const out: string[] = [];
|
|
40
|
+
for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
|
|
41
|
+
const full = path.join(dir, entry.name);
|
|
42
|
+
if (entry.isDirectory()) out.push(...(await uiSourceFiles(full)));
|
|
43
|
+
else if (/\.tsx?$/.test(entry.name)) out.push(full);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const isComment = (line: string) => {
|
|
49
|
+
const t = line.trim();
|
|
50
|
+
return t.startsWith("//") || t.startsWith("*") || t.startsWith("/*");
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
describe("no UI file writes a plugin version it did not read back", () => {
|
|
54
|
+
test("the raw version writers are absent from src/ui", async () => {
|
|
55
|
+
const files = await uiSourceFiles(UI_DIR);
|
|
56
|
+
expect(files.length).toBeGreaterThan(10); // guard: the walk found real files
|
|
57
|
+
|
|
58
|
+
const offenders: string[] = [];
|
|
59
|
+
for (const file of files) {
|
|
60
|
+
const text = await fs.readFile(file, "utf8");
|
|
61
|
+
text.split("\n").forEach((line, i) => {
|
|
62
|
+
if (isComment(line)) return;
|
|
63
|
+
for (const writer of RAW_WRITERS) {
|
|
64
|
+
// Word-bounded: `saveInstalledPluginVersion` is a prefix of
|
|
65
|
+
// `saveInstalledPluginVersionForScope`, and both are banned, so a
|
|
66
|
+
// substring match would just double-report the same line.
|
|
67
|
+
if (new RegExp(`\\b${writer}\\b`).test(line)) {
|
|
68
|
+
offenders.push(
|
|
69
|
+
`${path.relative(UI_DIR, file)}:${i + 1} ${line.trim()}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
expect(offenders).toEqual([]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("the guard can actually fail", () => {
|
|
80
|
+
// Negative control. The assertion above passes trivially if the walk or
|
|
81
|
+
// the pattern is broken, and a guard that cannot fail is not evidence.
|
|
82
|
+
const line = "\t\t\tawait saveGlobalInstalledPluginVersion(id, version);";
|
|
83
|
+
expect(isComment(line)).toBe(false);
|
|
84
|
+
expect(RAW_WRITERS.some((w) => new RegExp(`\\b${w}\\b`).test(line))).toBe(
|
|
85
|
+
true,
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
});
|