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,101 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parseKeypress } from "@opentui/core";
|
|
4
|
+
import fs from "fs-extra";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A shifted key never arrives as an uppercase `name`.
|
|
8
|
+
*
|
|
9
|
+
* OpenTUI reports Shift+U as `{name: "u", shift: true, sequence: "U"}`, so
|
|
10
|
+
* `event.name === "U"` is unreachable. That is not merely dead code: in
|
|
11
|
+
* PluginsScreen the dead branch sat BELOW `event.name === "u"`, so pressing
|
|
12
|
+
* Shift+U — advertised in the footer as "update" — fell through to the plain "u"
|
|
13
|
+
* branch and installed the plugin at user scope instead. Reported from the real
|
|
14
|
+
* UI: "U instead of update install user scope".
|
|
15
|
+
*
|
|
16
|
+
* The same shape was in McpRegistryScreen (`"R"` for refresh), where nothing
|
|
17
|
+
* shadowed it, so refresh silently did nothing at all.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const UI_DIR = path.join(import.meta.dir, "..", "ui");
|
|
21
|
+
|
|
22
|
+
async function uiSourceFiles(dir: string): Promise<string[]> {
|
|
23
|
+
const out: string[] = [];
|
|
24
|
+
for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
|
|
25
|
+
const full = path.join(dir, entry.name);
|
|
26
|
+
if (entry.isDirectory()) out.push(...(await uiSourceFiles(full)));
|
|
27
|
+
else if (/\.tsx?$/.test(entry.name)) out.push(full);
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const isComment = (line: string) => {
|
|
33
|
+
const t = line.trim();
|
|
34
|
+
return t.startsWith("//") || t.startsWith("*") || t.startsWith("/*");
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
describe("shifted keys parse to a lowercase name plus a flag", () => {
|
|
38
|
+
// Pin the platform behaviour the rule depends on. If OpenTUI ever changed
|
|
39
|
+
// this, the guard below would be enforcing a rule that no longer holds.
|
|
40
|
+
test("Shift+letter keeps the unshifted name and sets shift", () => {
|
|
41
|
+
for (const [raw, name] of [
|
|
42
|
+
["U", "u"],
|
|
43
|
+
["A", "a"],
|
|
44
|
+
["R", "r"],
|
|
45
|
+
] as const) {
|
|
46
|
+
const k = parseKeypress(raw);
|
|
47
|
+
expect({ raw, name: k?.name, shift: k?.shift }).toEqual({
|
|
48
|
+
raw,
|
|
49
|
+
name,
|
|
50
|
+
shift: true,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("an unshifted letter reports shift false", () => {
|
|
56
|
+
const k = parseKeypress("u");
|
|
57
|
+
expect({ name: k?.name, shift: k?.shift }).toEqual({
|
|
58
|
+
name: "u",
|
|
59
|
+
shift: false,
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("no binding compares name to an uppercase letter", () => {
|
|
65
|
+
test("uppercase name comparisons are absent from the UI", async () => {
|
|
66
|
+
const files = await uiSourceFiles(UI_DIR);
|
|
67
|
+
expect(files.length).toBeGreaterThan(10); // guard: the walk found real files
|
|
68
|
+
|
|
69
|
+
const offenders: string[] = [];
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
const text = await fs.readFile(file, "utf8");
|
|
72
|
+
text.split("\n").forEach((line, i) => {
|
|
73
|
+
if (isComment(line)) return;
|
|
74
|
+
if (/\bname\s*===\s*"[A-Z]"/.test(line)) {
|
|
75
|
+
offenders.push(
|
|
76
|
+
`${path.relative(UI_DIR, file)}:${i + 1} ${line.trim()}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
expect(offenders).toEqual([]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("an uppercase binding is reachable only via the shift flag", async () => {
|
|
85
|
+
// The positive half: Shift+U must actually route to update. Asserting only
|
|
86
|
+
// the absence of `=== "U"` would also pass if someone deleted the binding.
|
|
87
|
+
const src = await fs.readFile(
|
|
88
|
+
path.join(UI_DIR, "screens", "PluginsScreen.tsx"),
|
|
89
|
+
"utf8",
|
|
90
|
+
);
|
|
91
|
+
const update = src.indexOf('event.name === "u" && event.shift');
|
|
92
|
+
const userScope = src.indexOf(
|
|
93
|
+
'event.name === "u") handleScopeToggle("user")',
|
|
94
|
+
);
|
|
95
|
+
expect(update).toBeGreaterThan(-1);
|
|
96
|
+
expect(userScope).toBeGreaterThan(-1);
|
|
97
|
+
// Order matters: the shifted branch has to be tested first, or the plain
|
|
98
|
+
// "u" branch swallows it again — which is exactly the original bug.
|
|
99
|
+
expect(update).toBeLessThan(userScope);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { buildGitignoreModal } from "../ui/hooks/useGitignoreModal";
|
|
3
|
+
|
|
4
|
+
describe("buildGitignoreModal", () => {
|
|
5
|
+
it("builds a select modal with three options", () => {
|
|
6
|
+
let dismissed = false;
|
|
7
|
+
let fixed = false;
|
|
8
|
+
let opened = false;
|
|
9
|
+
const modal = buildGitignoreModal({
|
|
10
|
+
violationCount: 5,
|
|
11
|
+
safeCount: 3,
|
|
12
|
+
onFixSafe: () => {
|
|
13
|
+
fixed = true;
|
|
14
|
+
},
|
|
15
|
+
onOpenTab: () => {
|
|
16
|
+
opened = true;
|
|
17
|
+
},
|
|
18
|
+
onDismiss: () => {
|
|
19
|
+
dismissed = true;
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
expect(modal.type).toBe("select");
|
|
23
|
+
if (modal.type !== "select") throw new Error("type guard");
|
|
24
|
+
expect(modal.options.length).toBe(3);
|
|
25
|
+
expect(modal.options[0].value).toBe("fix-safe");
|
|
26
|
+
expect(modal.options[1].value).toBe("open-tab");
|
|
27
|
+
expect(modal.options[2].value).toBe("dismiss");
|
|
28
|
+
expect(modal.options[0].label).toContain("3");
|
|
29
|
+
|
|
30
|
+
modal.onSelect("fix-safe");
|
|
31
|
+
expect(fixed).toBe(true);
|
|
32
|
+
|
|
33
|
+
modal.onSelect("open-tab");
|
|
34
|
+
expect(opened).toBe(true);
|
|
35
|
+
|
|
36
|
+
modal.onSelect("dismiss");
|
|
37
|
+
expect(dismissed).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("shows no missing entries label when safeCount is 0", () => {
|
|
41
|
+
const modal = buildGitignoreModal({
|
|
42
|
+
violationCount: 2,
|
|
43
|
+
safeCount: 0,
|
|
44
|
+
onFixSafe: () => {},
|
|
45
|
+
onOpenTab: () => {},
|
|
46
|
+
onDismiss: () => {},
|
|
47
|
+
});
|
|
48
|
+
if (modal.type !== "select") throw new Error("type guard");
|
|
49
|
+
expect(modal.options[0].label).toContain("no missing .gitignore");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("includes violation count in the message", () => {
|
|
53
|
+
const modal = buildGitignoreModal({
|
|
54
|
+
violationCount: 7,
|
|
55
|
+
safeCount: 2,
|
|
56
|
+
onFixSafe: () => {},
|
|
57
|
+
onOpenTab: () => {},
|
|
58
|
+
onDismiss: () => {},
|
|
59
|
+
});
|
|
60
|
+
if (modal.type !== "select") throw new Error("type guard");
|
|
61
|
+
expect(modal.message).toContain("7");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("calls onCancel as dismiss", () => {
|
|
65
|
+
let dismissed = false;
|
|
66
|
+
const modal = buildGitignoreModal({
|
|
67
|
+
violationCount: 1,
|
|
68
|
+
safeCount: 0,
|
|
69
|
+
onFixSafe: () => {},
|
|
70
|
+
onOpenTab: () => {},
|
|
71
|
+
onDismiss: () => {
|
|
72
|
+
dismissed = true;
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
if (modal.type !== "select") throw new Error("type guard");
|
|
76
|
+
modal.onCancel();
|
|
77
|
+
expect(dismissed).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guards the update-check refresh fix. checkForUpdates caches within a session
|
|
3
|
+
* (so we don't hammer the registry), but `force` bypasses that cache so a
|
|
4
|
+
* release landing mid-session is actually seen — the 4.25-vs-4.26 banner
|
|
5
|
+
* staleness. (The fetch itself also sets `cache: "no-store"` so a CDN-stale
|
|
6
|
+
* response can't mask a fresh release.)
|
|
7
|
+
*/
|
|
8
|
+
import { afterEach, describe, expect, it } from "bun:test";
|
|
9
|
+
import { checkForUpdates } from "../services/version-check.js";
|
|
10
|
+
|
|
11
|
+
describe("checkForUpdates — session cache vs forced re-check", () => {
|
|
12
|
+
const realFetch = globalThis.fetch;
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
globalThis.fetch = realFetch;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("serves the session cache by default and re-fetches only when forced", async () => {
|
|
18
|
+
let calls = 0;
|
|
19
|
+
let latest = "9.9.9";
|
|
20
|
+
globalThis.fetch = (async () => {
|
|
21
|
+
calls++;
|
|
22
|
+
return new Response(JSON.stringify({ version: latest }), {
|
|
23
|
+
status: 200,
|
|
24
|
+
headers: { "content-type": "application/json" },
|
|
25
|
+
});
|
|
26
|
+
}) as typeof fetch;
|
|
27
|
+
|
|
28
|
+
// Force the first call so the test is independent of any cache another
|
|
29
|
+
// test in the same process may have primed.
|
|
30
|
+
const first = await checkForUpdates(true);
|
|
31
|
+
expect(calls).toBe(1);
|
|
32
|
+
expect(first.latestVersion).toBe("9.9.9");
|
|
33
|
+
|
|
34
|
+
// Default call is served from the session cache — no new fetch.
|
|
35
|
+
const cached = await checkForUpdates();
|
|
36
|
+
expect(calls).toBe(1);
|
|
37
|
+
expect(cached.latestVersion).toBe("9.9.9");
|
|
38
|
+
|
|
39
|
+
// A release lands; a forced re-check bypasses the cache and sees it.
|
|
40
|
+
latest = "9.9.10";
|
|
41
|
+
const forced = await checkForUpdates(true);
|
|
42
|
+
expect(calls).toBe(2);
|
|
43
|
+
expect(forced.latestVersion).toBe("9.9.10");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
UNKNOWN_VERSION,
|
|
7
|
+
diffVersions,
|
|
8
|
+
isKnownVersion,
|
|
9
|
+
loadSeenVersions,
|
|
10
|
+
saveSeenVersions,
|
|
11
|
+
} from "../services/version-snapshot.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Plugins get updated by things magus does not control — Claude Code's own
|
|
15
|
+
* `plugin update` writes installed_plugins.json without touching magus's
|
|
16
|
+
* bookkeeping. Diffing what we last rendered against reality catches every
|
|
17
|
+
* source; logging only magus's own actions catches none of them.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
describe("isKnownVersion", () => {
|
|
21
|
+
test('"0.0.0" is not a usable version', () => {
|
|
22
|
+
// Claude Code records this for a plugin whose version it cannot determine.
|
|
23
|
+
expect(isKnownVersion(UNKNOWN_VERSION)).toBe(false);
|
|
24
|
+
expect(isKnownVersion(undefined)).toBe(false);
|
|
25
|
+
expect(isKnownVersion("")).toBe(false);
|
|
26
|
+
expect(isKnownVersion("1.0.0")).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("diffVersions — first run", () => {
|
|
31
|
+
test("no baseline reports nothing at all", () => {
|
|
32
|
+
// Without this, the first launch would flag every installed plugin as new,
|
|
33
|
+
// which is both wrong and the fastest way to train someone to ignore badges.
|
|
34
|
+
expect(
|
|
35
|
+
diffVersions(null, { "dev@magus": "2.12.1", "a@mp": UNKNOWN_VERSION }),
|
|
36
|
+
).toEqual([]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("an empty baseline is NOT the same as no baseline", () => {
|
|
40
|
+
// A real snapshot that happens to be empty means we have looked before and
|
|
41
|
+
// saw nothing, so anything present now is genuinely new.
|
|
42
|
+
const changes = diffVersions({}, { "dev@magus": "2.12.1" });
|
|
43
|
+
expect(changes).toEqual([
|
|
44
|
+
{ pluginId: "dev@magus", kind: "installed", to: "2.12.1" },
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("diffVersions — installs", () => {
|
|
50
|
+
test("a plugin absent from the baseline is reported as installed, not updated", () => {
|
|
51
|
+
const changes = diffVersions(
|
|
52
|
+
{ "other@mp": "1.0.0" },
|
|
53
|
+
{ "new@mp": "3.1.0" },
|
|
54
|
+
);
|
|
55
|
+
expect(changes).toEqual([
|
|
56
|
+
{ pluginId: "new@mp", kind: "installed", to: "3.1.0" },
|
|
57
|
+
]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("an install with an unknown version is still reported", () => {
|
|
61
|
+
// The version is unusable but the install is real — the UI shows "new"
|
|
62
|
+
// with no version rather than suppressing the plugin entirely.
|
|
63
|
+
const changes = diffVersions({}, { "a@mp": UNKNOWN_VERSION });
|
|
64
|
+
expect(changes).toEqual([
|
|
65
|
+
{ pluginId: "a@mp", kind: "installed", to: UNKNOWN_VERSION },
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("diffVersions — updates", () => {
|
|
71
|
+
test("reports a genuine version transition", () => {
|
|
72
|
+
expect(
|
|
73
|
+
diffVersions({ "dev@magus": "2.12.0" }, { "dev@magus": "2.12.1" }),
|
|
74
|
+
).toEqual([
|
|
75
|
+
{ pluginId: "dev@magus", kind: "updated", from: "2.12.0", to: "2.12.1" },
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("unchanged versions report nothing", () => {
|
|
80
|
+
expect(
|
|
81
|
+
diffVersions({ "dev@magus": "2.12.1" }, { "dev@magus": "2.12.1" }),
|
|
82
|
+
).toEqual([]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("learning a previously unknown version is not an update", () => {
|
|
86
|
+
// This is exactly what v4.22.0's registry overlay caused: versions that
|
|
87
|
+
// could not be resolved suddenly could be. Reporting it would announce
|
|
88
|
+
// magus's own bugfix as if the user's plugins had changed.
|
|
89
|
+
expect(
|
|
90
|
+
diffVersions({ "a@mp": UNKNOWN_VERSION }, { "a@mp": "5.3.0" }),
|
|
91
|
+
).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("losing a known version is not an update either", () => {
|
|
95
|
+
expect(
|
|
96
|
+
diffVersions({ "a@mp": "5.3.0" }, { "a@mp": UNKNOWN_VERSION }),
|
|
97
|
+
).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("a downgrade IS reported", () => {
|
|
101
|
+
// Rolling back is a real change, arguably the most important to surface.
|
|
102
|
+
expect(diffVersions({ "a@mp": "2.0.0" }, { "a@mp": "1.0.0" })).toEqual([
|
|
103
|
+
{ pluginId: "a@mp", kind: "updated", from: "2.0.0", to: "1.0.0" },
|
|
104
|
+
]);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("diffVersions — removals and mixed sets", () => {
|
|
109
|
+
test("a removed plugin is not reported", () => {
|
|
110
|
+
expect(diffVersions({ "gone@mp": "1.0.0" }, {})).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("classifies each plugin independently in one pass", () => {
|
|
114
|
+
const changes = diffVersions(
|
|
115
|
+
{ same: "1.0.0", moved: "2.0.0", learned: UNKNOWN_VERSION },
|
|
116
|
+
{ same: "1.0.0", moved: "2.1.0", learned: "9.0.0", fresh: "0.1.0" },
|
|
117
|
+
);
|
|
118
|
+
expect(changes).toEqual([
|
|
119
|
+
{ pluginId: "moved", kind: "updated", from: "2.0.0", to: "2.1.0" },
|
|
120
|
+
{ pluginId: "fresh", kind: "installed", to: "0.1.0" },
|
|
121
|
+
]);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("per-project baselines", () => {
|
|
126
|
+
/**
|
|
127
|
+
* The baseline used to be one flat map for the whole machine, but the
|
|
128
|
+
* versions written into it are resolved per project. Two projects
|
|
129
|
+
* legitimately on different versions overwrote each other's baseline, so
|
|
130
|
+
* every differing plugin was reported as "updated" on the next render — a
|
|
131
|
+
* change that never happened.
|
|
132
|
+
*
|
|
133
|
+
* CLAUDE_CONFIG_DIR, not HOME: os.homedir() reads the passwd database on
|
|
134
|
+
* macOS and ignores $HOME, so a HOME-only override writes to the operator's
|
|
135
|
+
* real ~/.claude. That is not hypothetical — it happened while writing these.
|
|
136
|
+
*/
|
|
137
|
+
const withSandbox = async (fn: () => Promise<void>): Promise<void> => {
|
|
138
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "vsnap-"));
|
|
139
|
+
const prev = process.env.CLAUDE_CONFIG_DIR;
|
|
140
|
+
process.env.CLAUDE_CONFIG_DIR = dir;
|
|
141
|
+
try {
|
|
142
|
+
await fn();
|
|
143
|
+
} finally {
|
|
144
|
+
// biome-ignore lint/performance/noDelete: assigning undefined to process.env stores the string "undefined"; only delete unsets the variable
|
|
145
|
+
if (prev === undefined) delete process.env.CLAUDE_CONFIG_DIR;
|
|
146
|
+
else process.env.CLAUDE_CONFIG_DIR = prev;
|
|
147
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
test("a project with no baseline of its own reports nothing", async () => {
|
|
152
|
+
await withSandbox(async () => {
|
|
153
|
+
await saveSeenVersions("/proj/a", { "setup@magus": "2.2.0" });
|
|
154
|
+
const baseline = await loadSeenVersions("/proj/b");
|
|
155
|
+
expect(baseline).toBeNull();
|
|
156
|
+
expect(diffVersions(baseline, { "setup@magus": "2.5.0" })).toEqual([]);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("saving one project preserves the others", async () => {
|
|
161
|
+
await withSandbox(async () => {
|
|
162
|
+
await saveSeenVersions("/proj/a", { "designer@magus": "2.0.0" });
|
|
163
|
+
await saveSeenVersions("/proj/b", { "designer@magus": "2.0.1" });
|
|
164
|
+
expect(await loadSeenVersions("/proj/a")).toEqual({
|
|
165
|
+
"designer@magus": "2.0.0",
|
|
166
|
+
});
|
|
167
|
+
expect(await loadSeenVersions("/proj/b")).toEqual({
|
|
168
|
+
"designer@magus": "2.0.1",
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("a real update inside one project is still reported", async () => {
|
|
174
|
+
await withSandbox(async () => {
|
|
175
|
+
await saveSeenVersions("/proj/a", { "dev@magus": "3.0.0" });
|
|
176
|
+
const previous = await loadSeenVersions("/proj/a");
|
|
177
|
+
expect(diffVersions(previous, { "dev@magus": "3.0.1" })).toEqual([
|
|
178
|
+
{ pluginId: "dev@magus", kind: "updated", from: "3.0.0", to: "3.0.1" },
|
|
179
|
+
]);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("a pre-migration flat baseline is adopted once, then replaced", async () => {
|
|
184
|
+
await withSandbox(async () => {
|
|
185
|
+
const file = path.join(
|
|
186
|
+
process.env.CLAUDE_CONFIG_DIR as string,
|
|
187
|
+
"magus-version-snapshot.json",
|
|
188
|
+
);
|
|
189
|
+
await fs.writeFile(
|
|
190
|
+
file,
|
|
191
|
+
JSON.stringify({
|
|
192
|
+
seen: { "dev@magus": "3.0.0" },
|
|
193
|
+
seededAt: "2026-07-29T00:00:00.000Z",
|
|
194
|
+
updatedAt: "2026-07-29T00:00:00.000Z",
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// Legacy shape carries no project, so it stands in for whoever asks first
|
|
199
|
+
// rather than being thrown away.
|
|
200
|
+
expect(await loadSeenVersions("/proj/a")).toEqual({
|
|
201
|
+
"dev@magus": "3.0.0",
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
await saveSeenVersions("/proj/a", { "dev@magus": "3.0.1" });
|
|
205
|
+
// Once per-project entries exist, the flat map is no longer consulted.
|
|
206
|
+
expect(await loadSeenVersions("/proj/b")).toBeNull();
|
|
207
|
+
expect(await loadSeenVersions("/proj/a")).toEqual({
|
|
208
|
+
"dev@magus": "3.0.1",
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
});
|