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,317 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import {
|
|
7
|
+
applyDestructiveFix,
|
|
8
|
+
applySafeFixes,
|
|
9
|
+
formatViolationSummary,
|
|
10
|
+
} from "../services/gitignore-fixer";
|
|
11
|
+
import type { Violation } from "../types/gitignore";
|
|
12
|
+
|
|
13
|
+
// `commit.gpgsign=false` is not tidiness — without it these tests HANG.
|
|
14
|
+
//
|
|
15
|
+
// Two tests here run `git commit` in a temp repo, which inherits the
|
|
16
|
+
// developer's global config. On any machine with commit signing enabled and a
|
|
17
|
+
// GUI signer (`gpg.format=ssh` pointing at 1Password's `op-ssh-sign` is the
|
|
18
|
+
// common setup here), the commit blocks waiting for an approval dialog nobody
|
|
19
|
+
// is watching, and both tests fail on a 5s timeout. It presents as a flake
|
|
20
|
+
// because the approval is cached once granted, so the suite goes green until
|
|
21
|
+
// the cache expires. `marketplace-refresh.test.ts` already guards this the same
|
|
22
|
+
// way; this file did not.
|
|
23
|
+
const GIT_CFG = [
|
|
24
|
+
"-c",
|
|
25
|
+
"core.excludesFile=/dev/null",
|
|
26
|
+
"-c",
|
|
27
|
+
"commit.gpgsign=false",
|
|
28
|
+
"-c",
|
|
29
|
+
"init.defaultBranch=main",
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function git(
|
|
33
|
+
cwd: string,
|
|
34
|
+
...args: string[]
|
|
35
|
+
): { stdout: string; status: number } {
|
|
36
|
+
const r = spawnSync("git", [...GIT_CFG, ...args], {
|
|
37
|
+
cwd,
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
});
|
|
40
|
+
return { stdout: r.stdout, status: r.status ?? -1 };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function makeRepo(): Promise<string> {
|
|
44
|
+
const dir = await mkdtemp(join(tmpdir(), "gitig-fix-"));
|
|
45
|
+
git(dir, "init", "-q");
|
|
46
|
+
git(dir, "config", "user.email", "t@t");
|
|
47
|
+
git(dir, "config", "user.name", "t");
|
|
48
|
+
return dir;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe("applySafeFixes", () => {
|
|
52
|
+
let repo: string;
|
|
53
|
+
|
|
54
|
+
beforeEach(async () => {
|
|
55
|
+
repo = await makeRepo();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterEach(async () => {
|
|
59
|
+
await rm(repo, { recursive: true, force: true });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("appends missing patterns to .gitignore under a header", async () => {
|
|
63
|
+
const violations: Violation[] = [
|
|
64
|
+
{
|
|
65
|
+
kind: "missing-from-gitignore",
|
|
66
|
+
path: ".env",
|
|
67
|
+
source: "builtin",
|
|
68
|
+
severity: "safe",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
kind: "missing-from-gitignore",
|
|
72
|
+
path: ".mnemex/",
|
|
73
|
+
source: "builtin",
|
|
74
|
+
severity: "safe",
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
const r = await applySafeFixes(repo, violations);
|
|
78
|
+
expect(r.appended).toEqual([".env", ".mnemex/"]);
|
|
79
|
+
expect(r.alreadyPresent).toEqual([]);
|
|
80
|
+
|
|
81
|
+
const content = await readFile(join(repo, ".gitignore"), "utf8");
|
|
82
|
+
expect(content).toContain(".env");
|
|
83
|
+
expect(content).toContain(".mnemex/");
|
|
84
|
+
expect(content).toContain("# Added by magus");
|
|
85
|
+
expect(content).toContain("Environment files commonly contain");
|
|
86
|
+
expect(content).toContain("Local memory/index data");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("creates .gitignore if absent", async () => {
|
|
90
|
+
const r = await applySafeFixes(repo, [
|
|
91
|
+
{
|
|
92
|
+
kind: "missing-from-gitignore",
|
|
93
|
+
path: "foo",
|
|
94
|
+
source: "builtin",
|
|
95
|
+
severity: "safe",
|
|
96
|
+
},
|
|
97
|
+
]);
|
|
98
|
+
expect(r.appended).toEqual(["foo"]);
|
|
99
|
+
const content = await readFile(join(repo, ".gitignore"), "utf8");
|
|
100
|
+
expect(content).toMatch(/foo/);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("is idempotent — re-running does not duplicate entries", async () => {
|
|
104
|
+
const v: Violation = {
|
|
105
|
+
kind: "missing-from-gitignore",
|
|
106
|
+
path: "dup",
|
|
107
|
+
source: "builtin",
|
|
108
|
+
severity: "safe",
|
|
109
|
+
};
|
|
110
|
+
await applySafeFixes(repo, [v]);
|
|
111
|
+
const second = await applySafeFixes(repo, [v]);
|
|
112
|
+
expect(second.appended).toEqual([]);
|
|
113
|
+
expect(second.alreadyPresent).toEqual(["dup"]);
|
|
114
|
+
const content = await readFile(join(repo, ".gitignore"), "utf8");
|
|
115
|
+
expect(content.match(/^dup$/gm)?.length).toBe(1);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("ignores destructive violations", async () => {
|
|
119
|
+
const r = await applySafeFixes(repo, [
|
|
120
|
+
{
|
|
121
|
+
kind: "tracked-but-should-ignore",
|
|
122
|
+
path: "secret",
|
|
123
|
+
source: "builtin",
|
|
124
|
+
severity: "destructive",
|
|
125
|
+
},
|
|
126
|
+
]);
|
|
127
|
+
expect(r.appended).toEqual([]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("preserves existing .gitignore content", async () => {
|
|
131
|
+
await writeFile(join(repo, ".gitignore"), "node_modules/\n.DS_Store\n");
|
|
132
|
+
await applySafeFixes(repo, [
|
|
133
|
+
{
|
|
134
|
+
kind: "missing-from-gitignore",
|
|
135
|
+
path: ".env",
|
|
136
|
+
source: "builtin",
|
|
137
|
+
severity: "safe",
|
|
138
|
+
},
|
|
139
|
+
]);
|
|
140
|
+
const content = await readFile(join(repo, ".gitignore"), "utf8");
|
|
141
|
+
expect(content).toContain("node_modules/");
|
|
142
|
+
expect(content).toContain(".DS_Store");
|
|
143
|
+
expect(content).toContain(".env");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("applyDestructiveFix", () => {
|
|
148
|
+
let repo: string;
|
|
149
|
+
|
|
150
|
+
beforeEach(async () => {
|
|
151
|
+
repo = await makeRepo();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
afterEach(async () => {
|
|
155
|
+
await rm(repo, { recursive: true, force: true });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("removes tracked file from index and adds to .gitignore", async () => {
|
|
159
|
+
await writeFile(join(repo, "secret"), "x");
|
|
160
|
+
git(repo, "add", "secret");
|
|
161
|
+
git(repo, "commit", "-q", "-m", "add");
|
|
162
|
+
|
|
163
|
+
const r = await applyDestructiveFix(repo, {
|
|
164
|
+
kind: "tracked-but-should-ignore",
|
|
165
|
+
path: "secret",
|
|
166
|
+
source: "builtin",
|
|
167
|
+
severity: "destructive",
|
|
168
|
+
});
|
|
169
|
+
expect(r.applied).toBe(true);
|
|
170
|
+
|
|
171
|
+
const ls = git(repo, "ls-files");
|
|
172
|
+
expect(ls.stdout.includes("secret")).toBe(false);
|
|
173
|
+
const ignore = await readFile(join(repo, ".gitignore"), "utf8");
|
|
174
|
+
expect(ignore).toContain("secret");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("strips .gitignore line and stages file for ignored-but-should-track", async () => {
|
|
178
|
+
await writeFile(join(repo, ".mcp.json"), "{}");
|
|
179
|
+
await writeFile(join(repo, ".gitignore"), ".mcp.json\nnode_modules/\n");
|
|
180
|
+
|
|
181
|
+
const r = await applyDestructiveFix(repo, {
|
|
182
|
+
kind: "ignored-but-should-track",
|
|
183
|
+
path: ".mcp.json",
|
|
184
|
+
source: "builtin",
|
|
185
|
+
severity: "destructive",
|
|
186
|
+
});
|
|
187
|
+
expect(r.applied).toBe(true);
|
|
188
|
+
|
|
189
|
+
const ignore = await readFile(join(repo, ".gitignore"), "utf8");
|
|
190
|
+
expect(ignore).not.toMatch(/^\.mcp\.json$/m);
|
|
191
|
+
expect(ignore).toContain("node_modules/");
|
|
192
|
+
|
|
193
|
+
const ls = git(repo, "ls-files");
|
|
194
|
+
// staged but not yet committed; ls-files only shows index
|
|
195
|
+
expect(ls.stdout).toContain(".mcp.json");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("adds unignore exceptions when a parent rule ignores a tracked path", async () => {
|
|
199
|
+
await mkdir(join(repo, ".claude"), { recursive: true });
|
|
200
|
+
await writeFile(join(repo, ".claude", "settings.json"), "{}");
|
|
201
|
+
await writeFile(join(repo, ".gitignore"), "/*\n");
|
|
202
|
+
|
|
203
|
+
const r = await applyDestructiveFix(repo, {
|
|
204
|
+
kind: "ignored-but-should-track",
|
|
205
|
+
path: ".claude/settings.json",
|
|
206
|
+
source: "builtin",
|
|
207
|
+
severity: "destructive",
|
|
208
|
+
});
|
|
209
|
+
expect(r.applied).toBe(true);
|
|
210
|
+
|
|
211
|
+
const ignore = await readFile(join(repo, ".gitignore"), "utf8");
|
|
212
|
+
expect(ignore).toContain("!/.claude/");
|
|
213
|
+
expect(ignore).toContain("!/.claude/settings.json");
|
|
214
|
+
expect(ignore).toContain("Team Claude settings should be committed");
|
|
215
|
+
|
|
216
|
+
const ls = git(repo, "ls-files");
|
|
217
|
+
expect(ls.stdout).toContain(".claude/settings.json");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("does not fail when making an absent tracked path trackable", async () => {
|
|
221
|
+
await writeFile(join(repo, ".gitignore"), "/*\n");
|
|
222
|
+
|
|
223
|
+
const r = await applyDestructiveFix(repo, {
|
|
224
|
+
kind: "ignored-but-should-track",
|
|
225
|
+
path: "plugin.json",
|
|
226
|
+
source: "builtin",
|
|
227
|
+
severity: "destructive",
|
|
228
|
+
});
|
|
229
|
+
expect(r.applied).toBe(true);
|
|
230
|
+
expect(r.message).toContain("does not exist yet");
|
|
231
|
+
|
|
232
|
+
const ignore = await readFile(join(repo, ".gitignore"), "utf8");
|
|
233
|
+
expect(ignore).toContain("!/plugin.json");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("uses the managed rule pattern when untracking child violations", async () => {
|
|
237
|
+
await mkdir(join(repo, "logs"), { recursive: true });
|
|
238
|
+
await writeFile(join(repo, "logs", "app.log"), "x");
|
|
239
|
+
git(repo, "add", "logs/app.log");
|
|
240
|
+
git(repo, "commit", "-q", "-m", "add log");
|
|
241
|
+
|
|
242
|
+
const r = await applyDestructiveFix(
|
|
243
|
+
repo,
|
|
244
|
+
{
|
|
245
|
+
kind: "tracked-but-should-ignore",
|
|
246
|
+
path: "logs/app.log",
|
|
247
|
+
source: "builtin",
|
|
248
|
+
severity: "destructive",
|
|
249
|
+
},
|
|
250
|
+
"logs/",
|
|
251
|
+
);
|
|
252
|
+
expect(r.applied).toBe(true);
|
|
253
|
+
|
|
254
|
+
const ignore = await readFile(join(repo, ".gitignore"), "utf8");
|
|
255
|
+
expect(ignore).toContain("logs/");
|
|
256
|
+
expect(ignore).not.toMatch(/^logs\/app\.log$/m);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("stages an untracked file", async () => {
|
|
260
|
+
await writeFile(join(repo, ".mcp.json"), "{}");
|
|
261
|
+
const r = await applyDestructiveFix(repo, {
|
|
262
|
+
kind: "untracked-and-should-track",
|
|
263
|
+
path: ".mcp.json",
|
|
264
|
+
source: "builtin",
|
|
265
|
+
severity: "destructive",
|
|
266
|
+
});
|
|
267
|
+
expect(r.applied).toBe(true);
|
|
268
|
+
expect(git(repo, "ls-files").stdout).toContain(".mcp.json");
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe("formatViolationSummary", () => {
|
|
273
|
+
it("returns friendly message when empty", () => {
|
|
274
|
+
expect(formatViolationSummary([])).toMatch(/No gitignore issues/);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("groups by kind with counts and sample paths", () => {
|
|
278
|
+
const summary = formatViolationSummary([
|
|
279
|
+
{
|
|
280
|
+
kind: "missing-from-gitignore",
|
|
281
|
+
path: "a",
|
|
282
|
+
source: "builtin",
|
|
283
|
+
severity: "safe",
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
kind: "missing-from-gitignore",
|
|
287
|
+
path: "b",
|
|
288
|
+
source: "builtin",
|
|
289
|
+
severity: "safe",
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
kind: "tracked-but-should-ignore",
|
|
293
|
+
path: "c",
|
|
294
|
+
source: "project",
|
|
295
|
+
severity: "destructive",
|
|
296
|
+
},
|
|
297
|
+
]);
|
|
298
|
+
expect(summary).toContain("missing from .gitignore: 2");
|
|
299
|
+
expect(summary).toContain("tracked but should be ignored: 1");
|
|
300
|
+
expect(summary).toContain("- a");
|
|
301
|
+
expect(summary).toContain("- c");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("truncates long lists with `… and N more`", () => {
|
|
305
|
+
const vs: Violation[] = [];
|
|
306
|
+
for (let i = 0; i < 8; i++) {
|
|
307
|
+
vs.push({
|
|
308
|
+
kind: "missing-from-gitignore",
|
|
309
|
+
path: `p${i}`,
|
|
310
|
+
source: "builtin",
|
|
311
|
+
severity: "safe",
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
const summary = formatViolationSummary(vs);
|
|
315
|
+
expect(summary).toContain("and 3 more");
|
|
316
|
+
});
|
|
317
|
+
});
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { BUILTIN_DEFAULTS } from "../data/gitignore-defaults";
|
|
6
|
+
import { resolveGitignore } from "../services/gitignore-resolver";
|
|
7
|
+
|
|
8
|
+
describe("resolveGitignore", () => {
|
|
9
|
+
let projectDir: string;
|
|
10
|
+
let homeDir: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
projectDir = await mkdtemp(join(tmpdir(), "gitig-proj-"));
|
|
14
|
+
homeDir = await mkdtemp(join(tmpdir(), "gitig-home-"));
|
|
15
|
+
await mkdir(join(projectDir, ".claude"), { recursive: true });
|
|
16
|
+
await mkdir(join(homeDir, ".claude"), { recursive: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
await rm(projectDir, { recursive: true, force: true });
|
|
21
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("returns built-in defaults when no global or project manifest exists", async () => {
|
|
25
|
+
const r = await resolveGitignore(projectDir, { homeDir });
|
|
26
|
+
expect(r.conflicts).toEqual([]);
|
|
27
|
+
const ignored = r.rules
|
|
28
|
+
.filter((x) => x.action === "ignore")
|
|
29
|
+
.map((x) => x.pattern);
|
|
30
|
+
for (const p of BUILTIN_DEFAULTS.ignore) {
|
|
31
|
+
expect(ignored).toContain(p);
|
|
32
|
+
}
|
|
33
|
+
const tracked = r.rules
|
|
34
|
+
.filter((x) => x.action === "track")
|
|
35
|
+
.map((x) => x.pattern);
|
|
36
|
+
for (const p of BUILTIN_DEFAULTS.track) {
|
|
37
|
+
expect(tracked).toContain(p);
|
|
38
|
+
}
|
|
39
|
+
for (const rule of r.rules) {
|
|
40
|
+
expect(rule.source).toBe("builtin");
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("project tier overrides global tier", async () => {
|
|
45
|
+
await writeFile(
|
|
46
|
+
join(homeDir, ".claude", "gitignore.json"),
|
|
47
|
+
JSON.stringify({ ignore: [".env"], track: [] }),
|
|
48
|
+
);
|
|
49
|
+
await writeFile(
|
|
50
|
+
join(projectDir, ".claude", "gitignore.json"),
|
|
51
|
+
JSON.stringify({ ignore: [], track: [".env"] }),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const r = await resolveGitignore(projectDir, { homeDir });
|
|
55
|
+
const env = r.rules.find((x) => x.pattern === ".env");
|
|
56
|
+
expect(env?.action).toBe("track");
|
|
57
|
+
expect(env?.source).toBe("project");
|
|
58
|
+
expect(r.conflicts.find((c) => c.pattern === ".env")?.winner).toBe(
|
|
59
|
+
"project",
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("global tier overrides built-in", async () => {
|
|
64
|
+
await writeFile(
|
|
65
|
+
join(homeDir, ".claude", "gitignore.json"),
|
|
66
|
+
JSON.stringify({ ignore: [], track: [".claude/settings.local.json"] }),
|
|
67
|
+
);
|
|
68
|
+
const r = await resolveGitignore(projectDir, { homeDir });
|
|
69
|
+
const rule = r.rules.find(
|
|
70
|
+
(x) => x.pattern === ".claude/settings.local.json",
|
|
71
|
+
);
|
|
72
|
+
expect(rule?.action).toBe("track");
|
|
73
|
+
expect(rule?.source).toBe("global");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("expands `extends: ['common-dev']` into the tier's rules", async () => {
|
|
77
|
+
await writeFile(
|
|
78
|
+
join(projectDir, ".claude", "gitignore.json"),
|
|
79
|
+
JSON.stringify({ ignore: [], track: [], extends: ["common-dev"] }),
|
|
80
|
+
);
|
|
81
|
+
const r = await resolveGitignore(projectDir, { homeDir });
|
|
82
|
+
const ignored = r.rules
|
|
83
|
+
.filter((x) => x.action === "ignore")
|
|
84
|
+
.map((x) => x.pattern);
|
|
85
|
+
expect(ignored).toContain("node_modules/");
|
|
86
|
+
expect(ignored).toContain(".env");
|
|
87
|
+
expect(ignored).toContain(".DS_Store");
|
|
88
|
+
// Template rules carry the project source since they merged into project tier
|
|
89
|
+
const nodeMods = r.rules.find((x) => x.pattern === "node_modules/");
|
|
90
|
+
expect(nodeMods?.source).toBe("project");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("logs and skips unknown templates", async () => {
|
|
94
|
+
await writeFile(
|
|
95
|
+
join(projectDir, ".claude", "gitignore.json"),
|
|
96
|
+
JSON.stringify({ ignore: [], track: [], extends: ["does-not-exist"] }),
|
|
97
|
+
);
|
|
98
|
+
const messages: string[] = [];
|
|
99
|
+
const r = await resolveGitignore(projectDir, {
|
|
100
|
+
homeDir,
|
|
101
|
+
logger: (m) => messages.push(m),
|
|
102
|
+
});
|
|
103
|
+
expect(messages.some((m) => m.includes("does-not-exist"))).toBe(true);
|
|
104
|
+
// Should still return built-in defaults
|
|
105
|
+
expect(r.rules.length).toBeGreaterThan(0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("throws on intra-tier conflict", async () => {
|
|
109
|
+
await writeFile(
|
|
110
|
+
join(projectDir, ".claude", "gitignore.json"),
|
|
111
|
+
JSON.stringify({ ignore: ["foo"], track: ["foo"] }),
|
|
112
|
+
);
|
|
113
|
+
await expect(resolveGitignore(projectDir, { homeDir })).rejects.toThrow(
|
|
114
|
+
/Intra-tier conflict/,
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("ignores malformed manifest JSON without throwing", async () => {
|
|
119
|
+
await writeFile(join(projectDir, ".claude", "gitignore.json"), "{not json");
|
|
120
|
+
const messages: string[] = [];
|
|
121
|
+
const r = await resolveGitignore(projectDir, {
|
|
122
|
+
homeDir,
|
|
123
|
+
logger: (m) => messages.push(m),
|
|
124
|
+
});
|
|
125
|
+
expect(messages.some((m) => /failed to read/.test(m))).toBe(true);
|
|
126
|
+
// Falls back to built-in
|
|
127
|
+
expect(r.rules.find((x) => x.pattern === ".mnemex/")?.source).toBe(
|
|
128
|
+
"builtin",
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("ignores manifest with wrong shape without throwing", async () => {
|
|
133
|
+
await writeFile(
|
|
134
|
+
join(projectDir, ".claude", "gitignore.json"),
|
|
135
|
+
JSON.stringify({ ignore: "not-array" }),
|
|
136
|
+
);
|
|
137
|
+
const messages: string[] = [];
|
|
138
|
+
const r = await resolveGitignore(projectDir, {
|
|
139
|
+
homeDir,
|
|
140
|
+
logger: (m) => messages.push(m),
|
|
141
|
+
});
|
|
142
|
+
expect(messages.some((m) => /not a valid manifest/.test(m))).toBe(true);
|
|
143
|
+
expect(r.rules.length).toBeGreaterThan(0);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("dedupes patterns within a tier", async () => {
|
|
147
|
+
await writeFile(
|
|
148
|
+
join(projectDir, ".claude", "gitignore.json"),
|
|
149
|
+
JSON.stringify({ ignore: ["foo", "foo", "bar"], track: [] }),
|
|
150
|
+
);
|
|
151
|
+
const r = await resolveGitignore(projectDir, { homeDir });
|
|
152
|
+
const fooRules = r.rules.filter((x) => x.pattern === "foo");
|
|
153
|
+
expect(fooRules.length).toBe(1);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("does not mark same-action overrides as conflicts", async () => {
|
|
157
|
+
await writeFile(
|
|
158
|
+
join(homeDir, ".claude", "gitignore.json"),
|
|
159
|
+
JSON.stringify({ ignore: [".env"], track: [] }),
|
|
160
|
+
);
|
|
161
|
+
await writeFile(
|
|
162
|
+
join(projectDir, ".claude", "gitignore.json"),
|
|
163
|
+
JSON.stringify({ ignore: [".env"], track: [] }),
|
|
164
|
+
);
|
|
165
|
+
const r = await resolveGitignore(projectDir, { homeDir });
|
|
166
|
+
expect(r.conflicts.find((c) => c.pattern === ".env")).toBeUndefined();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("records conflict losers across multiple tiers", async () => {
|
|
170
|
+
// built-in ignores .claude/settings.local.json; global tracks it; project ignores it again
|
|
171
|
+
await writeFile(
|
|
172
|
+
join(homeDir, ".claude", "gitignore.json"),
|
|
173
|
+
JSON.stringify({ ignore: [], track: [".claude/settings.local.json"] }),
|
|
174
|
+
);
|
|
175
|
+
await writeFile(
|
|
176
|
+
join(projectDir, ".claude", "gitignore.json"),
|
|
177
|
+
JSON.stringify({ ignore: [".claude/settings.local.json"], track: [] }),
|
|
178
|
+
);
|
|
179
|
+
const r = await resolveGitignore(projectDir, { homeDir });
|
|
180
|
+
const winner = r.rules.find(
|
|
181
|
+
(x) => x.pattern === ".claude/settings.local.json",
|
|
182
|
+
);
|
|
183
|
+
expect(winner?.action).toBe("ignore");
|
|
184
|
+
expect(winner?.source).toBe("project");
|
|
185
|
+
const conflict = r.conflicts.find(
|
|
186
|
+
(c) => c.pattern === ".claude/settings.local.json",
|
|
187
|
+
);
|
|
188
|
+
expect(conflict?.winner).toBe("project");
|
|
189
|
+
expect(
|
|
190
|
+
conflict?.losers.some((l) => l.tier === "global" && l.action === "track"),
|
|
191
|
+
).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import {
|
|
7
|
+
applyTemplate,
|
|
8
|
+
ensureProjectManifestForEdit,
|
|
9
|
+
loadGitignoreState,
|
|
10
|
+
} from "../services/gitignore-service";
|
|
11
|
+
|
|
12
|
+
function git(cwd: string, ...args: string[]): void {
|
|
13
|
+
spawnSync("git", ["-c", "core.excludesFile=/dev/null", ...args], { cwd });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("loadGitignoreState", () => {
|
|
17
|
+
let projectDir: string;
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
projectDir = await mkdtemp(join(tmpdir(), "gitig-svc-"));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
await rm(projectDir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("returns resolved manifest + violations + warnings", async () => {
|
|
28
|
+
git(projectDir, "init", "-q");
|
|
29
|
+
const s = await loadGitignoreState(projectDir);
|
|
30
|
+
expect(s.resolved).toBeDefined();
|
|
31
|
+
expect(Array.isArray(s.resolved.rules)).toBe(true);
|
|
32
|
+
expect(Array.isArray(s.violations)).toBe(true);
|
|
33
|
+
expect(Array.isArray(s.warnings)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("applyTemplate", () => {
|
|
38
|
+
let projectDir: string;
|
|
39
|
+
|
|
40
|
+
beforeEach(async () => {
|
|
41
|
+
projectDir = await mkdtemp(join(tmpdir(), "gitig-tpl-"));
|
|
42
|
+
await mkdir(join(projectDir, ".claude"), { recursive: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(async () => {
|
|
46
|
+
await rm(projectDir, { recursive: true, force: true });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("creates project manifest and merges template entries", async () => {
|
|
50
|
+
const r = await applyTemplate(projectDir, "common-dev");
|
|
51
|
+
expect(r.added.ignore).toContain("node_modules/");
|
|
52
|
+
expect(r.added.ignore).toContain(".env");
|
|
53
|
+
expect(r.conflicts).toEqual([]);
|
|
54
|
+
|
|
55
|
+
const written = JSON.parse(
|
|
56
|
+
await readFile(join(projectDir, ".claude", "gitignore.json"), "utf8"),
|
|
57
|
+
);
|
|
58
|
+
expect(written.ignore).toContain("node_modules/");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("does not duplicate entries on second apply", async () => {
|
|
62
|
+
await applyTemplate(projectDir, "common-dev");
|
|
63
|
+
const r2 = await applyTemplate(projectDir, "common-dev");
|
|
64
|
+
expect(r2.added.ignore).toEqual([]);
|
|
65
|
+
expect(r2.added.track).toEqual([]);
|
|
66
|
+
|
|
67
|
+
const written = JSON.parse(
|
|
68
|
+
await readFile(join(projectDir, ".claude", "gitignore.json"), "utf8"),
|
|
69
|
+
);
|
|
70
|
+
const occurrences = written.ignore.filter(
|
|
71
|
+
(p: string) => p === "node_modules/",
|
|
72
|
+
).length;
|
|
73
|
+
expect(occurrences).toBe(1);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("reports conflicts when an existing pattern would flip action", async () => {
|
|
77
|
+
await writeFile(
|
|
78
|
+
join(projectDir, ".claude", "gitignore.json"),
|
|
79
|
+
JSON.stringify({ ignore: [], track: ["node_modules/"] }),
|
|
80
|
+
);
|
|
81
|
+
const r = await applyTemplate(projectDir, "common-dev");
|
|
82
|
+
expect(r.conflicts).toContain("node_modules/");
|
|
83
|
+
// node_modules/ should NOT have been added to ignore
|
|
84
|
+
const written = JSON.parse(
|
|
85
|
+
await readFile(join(projectDir, ".claude", "gitignore.json"), "utf8"),
|
|
86
|
+
);
|
|
87
|
+
expect(written.ignore).not.toContain("node_modules/");
|
|
88
|
+
expect(written.track).toContain("node_modules/");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("throws on unknown template name", async () => {
|
|
92
|
+
await expect(applyTemplate(projectDir, "does-not-exist")).rejects.toThrow(
|
|
93
|
+
/Unknown template/,
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("ensureProjectManifestForEdit", () => {
|
|
99
|
+
let projectDir: string;
|
|
100
|
+
|
|
101
|
+
beforeEach(async () => {
|
|
102
|
+
projectDir = await mkdtemp(join(tmpdir(), "gitig-edit-"));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
afterEach(async () => {
|
|
106
|
+
await rm(projectDir, { recursive: true, force: true });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("creates an editable project manifest from resolved rules", async () => {
|
|
110
|
+
const path = await ensureProjectManifestForEdit(projectDir, {
|
|
111
|
+
rules: [
|
|
112
|
+
{ pattern: ".env", action: "ignore", source: "builtin" },
|
|
113
|
+
{ pattern: ".mcp.json", action: "track", source: "builtin" },
|
|
114
|
+
],
|
|
115
|
+
conflicts: [],
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(path).toBe(join(projectDir, ".claude", "gitignore.json"));
|
|
119
|
+
const written = JSON.parse(await readFile(path, "utf8"));
|
|
120
|
+
expect(written.ignore).toEqual([".env"]);
|
|
121
|
+
expect(written.track).toEqual([".mcp.json"]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("keeps an existing non-empty project manifest", async () => {
|
|
125
|
+
const path = join(projectDir, ".claude", "gitignore.json");
|
|
126
|
+
await mkdir(join(projectDir, ".claude"), { recursive: true });
|
|
127
|
+
await writeFile(path, JSON.stringify({ ignore: ["custom"], track: [] }));
|
|
128
|
+
|
|
129
|
+
await ensureProjectManifestForEdit(projectDir, {
|
|
130
|
+
rules: [{ pattern: ".env", action: "ignore", source: "builtin" }],
|
|
131
|
+
conflicts: [],
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const written = JSON.parse(await readFile(path, "utf8"));
|
|
135
|
+
expect(written.ignore).toEqual(["custom"]);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* goBinaryName: the one rule for turning a Go module path into the binary
|
|
3
|
+
* `go install` produces. Both consumers (plugin-setup's Go path and
|
|
4
|
+
* plugin-requires' `setup.go` branch) are tested through their own surfaces;
|
|
5
|
+
* this file pins the rule itself.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
import { goBinaryName } from "../utils/go-module.js";
|
|
10
|
+
|
|
11
|
+
describe("goBinaryName", () => {
|
|
12
|
+
test("takes the last segment of a v0/v1 module path", () => {
|
|
13
|
+
expect(goBinaryName("github.com/MadAppGang/tmux-mcp")).toBe("tmux-mcp");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("strips a trailing /v2 major-version suffix before taking the name", () => {
|
|
17
|
+
expect(goBinaryName("github.com/MadAppGang/tmux-mcp/v2")).toBe("tmux-mcp");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("strips multi-digit suffixes (/v10, /v42)", () => {
|
|
21
|
+
expect(goBinaryName("github.com/x/tool/v10")).toBe("tool");
|
|
22
|
+
expect(goBinaryName("github.com/x/tool/v42")).toBe("tool");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("a segment that merely starts with v2 is a name, not a suffix", () => {
|
|
26
|
+
expect(goBinaryName("github.com/x/v2tool")).toBe("v2tool");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("v0 and v1 are never path suffixes, so they stay as names", () => {
|
|
30
|
+
// Nobody names a binary this, but the rule must not invent an exception.
|
|
31
|
+
expect(goBinaryName("github.com/x/v1")).toBe("v1");
|
|
32
|
+
expect(goBinaryName("github.com/x/v0")).toBe("v0");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("a suffix in the middle of the path is not trailing and is kept", () => {
|
|
36
|
+
expect(goBinaryName("github.com/x/repo/v2/cmd/cli")).toBe("cli");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("single-segment and empty inputs pass through", () => {
|
|
40
|
+
expect(goBinaryName("mytool")).toBe("mytool");
|
|
41
|
+
expect(goBinaryName("")).toBe("");
|
|
42
|
+
});
|
|
43
|
+
});
|