claudeup 6.2.0 → 6.3.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 +22 -0
- package/bin/claudeup.js +6 -1
- package/package.json +6 -6
- package/src/__tests__/alias-adopt.test.ts +331 -329
- package/src/__tests__/alias-parser.test.ts +384 -376
- package/src/__tests__/alias-shell-writer.test.ts +638 -625
- package/src/__tests__/alias-store.test.ts +143 -143
- package/src/__tests__/catalog-cache-store.test.ts +265 -265
- package/src/__tests__/catalog-notice.test.ts +123 -123
- package/src/__tests__/cli-apply-seams.test.ts +12 -8
- package/src/__tests__/cli-router.test.ts +132 -3
- package/src/__tests__/cli-update-view.test.ts +27 -8
- package/src/__tests__/community-staleness.test.ts +0 -2
- package/src/__tests__/component-badge.test.ts +4 -4
- package/src/__tests__/content-drift.test.ts +5 -1
- package/src/__tests__/conventions-integration.test.ts +774 -689
- package/src/__tests__/conventions-manager.test.ts +1216 -1152
- package/src/__tests__/doctor-bins.test.ts +14 -6
- package/src/__tests__/doctor.test.ts +438 -425
- package/src/__tests__/dual-write-prevention.test.ts +277 -282
- package/src/__tests__/enabled-not-installed.test.ts +71 -63
- package/src/__tests__/file-locking.test.ts +196 -196
- package/src/__tests__/gap-fill-versions.test.ts +318 -311
- package/src/__tests__/github-budget.test.ts +170 -164
- package/src/__tests__/gitignore-detector.test.ts +160 -152
- package/src/__tests__/gitignore-fixer.test.ts +285 -247
- package/src/__tests__/gitignore-prerun.test.ts +57 -57
- package/src/__tests__/gitignore-resolver.test.ts +173 -146
- package/src/__tests__/gitignore-service.test.ts +121 -119
- package/src/__tests__/go-module.test.ts +43 -0
- package/src/__tests__/install-command.test.ts +3 -3
- package/src/__tests__/install-plan.test.ts +13 -7
- package/src/__tests__/manifest.test.ts +16 -5
- package/src/__tests__/markdown-renderer.test.ts +0 -1
- package/src/__tests__/marketplace-badge.test.ts +111 -90
- package/src/__tests__/marketplace-version.test.ts +64 -61
- package/src/__tests__/plugin-manager-fallback.test.ts +249 -239
- package/src/__tests__/plugin-requires.test.ts +55 -1
- package/src/__tests__/plugin-setup.test.ts +111 -9
- package/src/__tests__/profile-command.test.ts +1 -7
- package/src/__tests__/profile-materializer.test.ts +31 -8
- package/src/__tests__/profile-sync.test.ts +23 -5
- package/src/__tests__/registry-version-resolution.test.ts +61 -47
- package/src/__tests__/resolve-executable.test.ts +1 -1
- package/src/__tests__/resolver.test.ts +38 -13
- package/src/__tests__/scope-action.test.ts +52 -50
- package/src/__tests__/scope-squares.test.tsx +4 -5
- package/src/__tests__/skill-install-files.test.ts +111 -103
- package/src/__tests__/symlink-manager.test.ts +27 -10
- package/src/__tests__/theme-env.test.ts +281 -0
- package/src/__tests__/theme-resolve.test.ts +380 -0
- package/src/__tests__/update-apply.test.ts +1 -3
- package/src/__tests__/useGitignoreModal.test.ts +69 -61
- package/src/__tests__/version-snapshot.test.ts +183 -165
- package/src/cli/ansi.ts +23 -15
- package/src/cli/doctor.ts +10 -9
- package/src/cli/install.ts +3 -3
- package/src/cli/profile.ts +5 -4
- package/src/cli/prompt.ts +0 -1
- package/src/cli/router.ts +69 -2
- package/src/cli/update-view.ts +11 -6
- package/src/cli/update.ts +9 -4
- package/src/cli/upgrade.ts +6 -4
- package/src/data/alias-flags.ts +260 -260
- package/src/data/gitignore-defaults.ts +20 -20
- package/src/data/gitignore-reasons.ts +86 -92
- package/src/data/gitignore-templates.ts +14 -14
- package/src/data/predefined-profiles.ts +1 -2
- package/src/data/skill-repos.ts +43 -35
- package/src/main.tsx +29 -6
- package/src/opentui.d.ts +1 -3
- package/src/prerunner/index.ts +7 -8
- package/src/services/alias-settings.ts +20 -23
- package/src/services/alias-shell-writer.ts +800 -803
- package/src/services/alias-store.ts +77 -77
- package/src/services/claude-settings.ts +27 -18
- package/src/services/content-drift.ts +14 -4
- package/src/services/conventions-manager.ts +692 -617
- package/src/services/doctor.ts +464 -403
- package/src/services/gitignore-detector.ts +113 -103
- package/src/services/gitignore-fixer.ts +204 -189
- package/src/services/gitignore-prerun.ts +26 -26
- package/src/services/gitignore-resolver.ts +162 -144
- package/src/services/gitignore-service.ts +120 -117
- package/src/services/install-plan.ts +4 -4
- package/src/services/local-marketplace.ts +7 -5
- package/src/services/manifest.ts +1 -1
- package/src/services/marketplace-catalog-git.ts +5 -2
- package/src/services/marketplace-fetcher.ts +241 -235
- package/src/services/plugin-manager.ts +17 -20
- package/src/services/plugin-mcp-config.ts +2 -2
- package/src/services/plugin-requires.ts +16 -8
- package/src/services/plugin-setup.ts +32 -15
- package/src/services/plugin-version-check.ts +4 -4
- package/src/services/profile-materializer.ts +9 -5
- package/src/services/profile-sync.ts +12 -4
- package/src/services/profiles.ts +3 -3
- package/src/services/resolver.ts +10 -6
- package/src/services/settings-manager.ts +82 -46
- package/src/services/skills-manager.ts +11 -9
- package/src/services/symlink-manager.ts +2 -5
- package/src/services/toolchain.ts +3 -3
- package/src/services/update-cache.ts +1 -1
- package/src/services/update-engine.ts +4 -4
- package/src/services/update-plan.ts +9 -11
- package/src/services/version-snapshot.ts +88 -88
- package/src/types/gitignore.ts +28 -28
- package/src/ui/App.tsx +36 -36
- package/src/ui/adapters/settingsAdapter.ts +2 -2
- package/src/ui/components/CategoryHeader.tsx +1 -1
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScopeIndicator.tsx +3 -1
- package/src/ui/components/ScrollableList.tsx +2 -1
- package/src/ui/components/StyledText.tsx +1 -1
- package/src/ui/components/layout/FooterHints.tsx +5 -3
- package/src/ui/components/layout/Panel.tsx +2 -2
- package/src/ui/components/layout/ScreenLayout.tsx +5 -4
- package/src/ui/components/modals/InputModal.tsx +7 -1
- package/src/ui/components/modals/ModalContainer.tsx +3 -3
- package/src/ui/components/modals/SelectModal.tsx +1 -1
- package/src/ui/components/primitives/DetailSection.tsx +1 -1
- package/src/ui/components/primitives/KeyValueLine.tsx +1 -1
- package/src/ui/components/primitives/ScopeSquares.tsx +2 -2
- package/src/ui/hooks/index.ts +0 -1
- package/src/ui/hooks/useGitignoreModal.ts +1 -5
- package/src/ui/hooks/useMismatchModal.ts +4 -10
- package/src/ui/renderers/cliToolRenderers.tsx +2 -2
- package/src/ui/renderers/gitignoreRenderers.tsx +2 -2
- package/src/ui/renderers/mcpRenderers.tsx +1 -1
- package/src/ui/renderers/pluginRenderers.tsx +3 -9
- package/src/ui/renderers/profileRenderers.tsx +9 -8
- package/src/ui/renderers/settingsRenderers.tsx +4 -4
- package/src/ui/renderers/skillRenderers.tsx +14 -14
- package/src/ui/screens/AliasScreen.tsx +31 -34
- package/src/ui/screens/CliToolsScreen.tsx +14 -13
- package/src/ui/screens/EnvVarsScreen.tsx +8 -8
- package/src/ui/screens/GitignoreScreen.tsx +15 -14
- package/src/ui/screens/McpRegistryScreen.tsx +10 -7
- package/src/ui/screens/McpScreen.tsx +10 -10
- package/src/ui/screens/PluginsScreen.tsx +17 -21
- package/src/ui/screens/ProfilesScreen.tsx +25 -24
- package/src/ui/screens/SkillsScreen.tsx +16 -14
- package/src/ui/state/AppContext.tsx +4 -8
- package/src/ui/state/DimensionsContext.tsx +3 -2
- package/src/ui/theme-mode.ts +20 -38
- package/src/ui/theme-resolve.ts +148 -0
- package/src/utils/clipboard.ts +5 -5
- package/src/utils/command-utils.ts +2 -2
- package/src/utils/config-dir.ts +1 -3
- package/src/utils/file-locking.ts +88 -86
- package/src/utils/go-module.ts +36 -0
- package/src/ui/hooks/useAsyncData.ts +0 -127
- package/src/ui/state/AnimationContext.tsx +0 -76
|
@@ -1,135 +1,137 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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";
|
|
3
4
|
import { tmpdir } from "node:os";
|
|
4
5
|
import { join } from "node:path";
|
|
5
|
-
import { spawnSync } from "node:child_process";
|
|
6
6
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
applyTemplate,
|
|
8
|
+
ensureProjectManifestForEdit,
|
|
9
|
+
loadGitignoreState,
|
|
10
10
|
} from "../services/gitignore-service";
|
|
11
11
|
|
|
12
12
|
function git(cwd: string, ...args: string[]): void {
|
|
13
|
-
|
|
13
|
+
spawnSync("git", ["-c", "core.excludesFile=/dev/null", ...args], { cwd });
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
describe("loadGitignoreState", () => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
35
|
});
|
|
36
36
|
|
|
37
37
|
describe("applyTemplate", () => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
+
});
|
|
94
96
|
});
|
|
95
97
|
|
|
96
98
|
describe("ensureProjectManifestForEdit", () => {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
+
});
|
|
135
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
|
+
});
|
|
@@ -18,9 +18,7 @@ import { runInstallCommand } from "../cli/install.js";
|
|
|
18
18
|
|
|
19
19
|
let logs: string[];
|
|
20
20
|
let errs: string[];
|
|
21
|
-
// biome-ignore lint/suspicious/noExplicitAny: saved console refs
|
|
22
21
|
let origLog: any;
|
|
23
|
-
// biome-ignore lint/suspicious/noExplicitAny: saved console refs
|
|
24
22
|
let origErr: any;
|
|
25
23
|
beforeEach(() => {
|
|
26
24
|
logs = [];
|
|
@@ -69,7 +67,9 @@ describe("runInstallCommand — early-return paths", () => {
|
|
|
69
67
|
test("unknown profile → exit 1", async () => {
|
|
70
68
|
dir = await project({
|
|
71
69
|
version: 2,
|
|
72
|
-
profiles: {
|
|
70
|
+
profiles: {
|
|
71
|
+
frontend: { name: "Frontend", plugins: { "dev@magus": "latest" } },
|
|
72
|
+
},
|
|
73
73
|
});
|
|
74
74
|
expect(await runInstallCommand(["ghost", "--check"], dir)).toBe(1);
|
|
75
75
|
});
|
|
@@ -22,9 +22,7 @@ function closure(overrides: Partial<ResolvedClosure> = {}): ResolvedClosure {
|
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function registry(
|
|
26
|
-
plugins: Record<string, string[]>,
|
|
27
|
-
): InstalledPluginsRegistry {
|
|
25
|
+
function registry(plugins: Record<string, string[]>): InstalledPluginsRegistry {
|
|
28
26
|
const out: InstalledPluginsRegistry = { version: 2, plugins: {} };
|
|
29
27
|
for (const [id, versions] of Object.entries(plugins)) {
|
|
30
28
|
out.plugins[id] = versions.map((v) => ({
|
|
@@ -41,12 +39,16 @@ function registry(
|
|
|
41
39
|
describe("computeVersionDrift", () => {
|
|
42
40
|
test("no drift when installed matches the pin", () => {
|
|
43
41
|
const c = closure({ plugins: { "dev@magus": "2.9.0" } });
|
|
44
|
-
expect(
|
|
42
|
+
expect(
|
|
43
|
+
computeVersionDrift(c, registry({ "dev@magus": ["2.9.0"] })),
|
|
44
|
+
).toEqual([]);
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
test("'latest' pins are never drift-checked", () => {
|
|
48
48
|
const c = closure({ plugins: { "dev@magus": "latest" } });
|
|
49
|
-
expect(
|
|
49
|
+
expect(
|
|
50
|
+
computeVersionDrift(c, registry({ "dev@magus": ["1.0.0"] })),
|
|
51
|
+
).toEqual([]);
|
|
50
52
|
});
|
|
51
53
|
|
|
52
54
|
test("flags a mismatch, reporting the highest installed version", () => {
|
|
@@ -75,7 +77,9 @@ describe("computeEnvStatus", () => {
|
|
|
75
77
|
});
|
|
76
78
|
const status = computeEnvStatus(c, { SET_VAR: "x", OPT: "" });
|
|
77
79
|
expect(status.missingRequired).toEqual(["FIGMA_TOKEN"]);
|
|
78
|
-
expect(status.required.find((e) => e.name === "SET_VAR")!.present).toBe(
|
|
80
|
+
expect(status.required.find((e) => e.name === "SET_VAR")!.present).toBe(
|
|
81
|
+
true,
|
|
82
|
+
);
|
|
79
83
|
// empty string counts as missing
|
|
80
84
|
expect(status.optional[0]!.present).toBe(false);
|
|
81
85
|
});
|
|
@@ -86,7 +90,9 @@ describe("summarizeClosure", () => {
|
|
|
86
90
|
const c = closure({
|
|
87
91
|
marketplaces: { magus: { source: "github", repo: "MadAppGang/magus" } },
|
|
88
92
|
plugins: { "dev@magus": "latest", "terminal@magus": "4.1.0" },
|
|
89
|
-
bins: [
|
|
93
|
+
bins: [
|
|
94
|
+
{ name: "claudish", via: "bun", version: "1.2.0", sources: ["x"] },
|
|
95
|
+
],
|
|
90
96
|
env: { required: ["FIGMA_TOKEN"], optional: [] },
|
|
91
97
|
});
|
|
92
98
|
const lines = summarizeClosure(c);
|
|
@@ -46,9 +46,14 @@ describe("migrateToV2", () => {
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
test("strictVersions is carried through as a boolean", () => {
|
|
49
|
-
expect(
|
|
49
|
+
expect(
|
|
50
|
+
migrateToV2({ version: 2, strictVersions: true, profiles: {} })
|
|
51
|
+
.strictVersions,
|
|
52
|
+
).toBe(true);
|
|
50
53
|
// Omitted (not written as false) to keep a committed manifest minimal.
|
|
51
|
-
expect(
|
|
54
|
+
expect(
|
|
55
|
+
migrateToV2({ version: 2, profiles: {} }).strictVersions,
|
|
56
|
+
).toBeUndefined();
|
|
52
57
|
});
|
|
53
58
|
});
|
|
54
59
|
|
|
@@ -94,8 +99,12 @@ describe("validateManifest", () => {
|
|
|
94
99
|
},
|
|
95
100
|
},
|
|
96
101
|
});
|
|
97
|
-
expect(errors.some((e) => e.path === "profiles.p.marketplaces.m")).toBe(
|
|
98
|
-
|
|
102
|
+
expect(errors.some((e) => e.path === "profiles.p.marketplaces.m")).toBe(
|
|
103
|
+
true,
|
|
104
|
+
);
|
|
105
|
+
expect(errors.some((e) => e.path.startsWith("profiles.p.skills"))).toBe(
|
|
106
|
+
true,
|
|
107
|
+
);
|
|
99
108
|
});
|
|
100
109
|
});
|
|
101
110
|
|
|
@@ -119,7 +128,9 @@ describe("readManifest / writeManifest", () => {
|
|
|
119
128
|
profiles: { p: { name: "P", plugins: { "dev@magus": "1.0.0" } } },
|
|
120
129
|
};
|
|
121
130
|
await writeManifest(manifest, dir);
|
|
122
|
-
expect(await fs.pathExists(join(dir, ".claude", "profiles.json"))).toBe(
|
|
131
|
+
expect(await fs.pathExists(join(dir, ".claude", "profiles.json"))).toBe(
|
|
132
|
+
true,
|
|
133
|
+
);
|
|
123
134
|
expect(await readManifest(dir)).toEqual(manifest);
|
|
124
135
|
});
|
|
125
136
|
|
|
@@ -8,7 +8,6 @@ import { renderMarkdown } from "../ui/renderers/markdownRenderer.js";
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
/** Flatten a rendered node tree to the text a reader would see. */
|
|
11
|
-
// biome-ignore lint/suspicious/noExplicitAny: walking opaque element trees
|
|
12
11
|
function textOf(node: any): string {
|
|
13
12
|
if (node === null || node === undefined || typeof node === "boolean") {
|
|
14
13
|
return "";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { buildPluginBrowserItems } from "../ui/adapters/pluginsAdapter.js";
|
|
3
2
|
import type { Marketplace } from "../types/index.js";
|
|
3
|
+
import { buildPluginBrowserItems } from "../ui/adapters/pluginsAdapter.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The marketplace category badge is the line users read on every claudeup launch,
|
|
@@ -9,111 +9,132 @@ import type { Marketplace } from "../types/index.js";
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const mp = (over: Partial<Marketplace> & { name: string }): Marketplace => ({
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
displayName: over.displayName ?? over.name,
|
|
13
|
+
source: { source: "github", repo: `someone/${over.name}` },
|
|
14
|
+
description: "",
|
|
15
|
+
...over,
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
/** Badge for a marketplace when it has plugins and is expanded. */
|
|
19
19
|
function badgeFor(marketplace: Marketplace): string | undefined {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
20
|
+
const items = buildPluginBrowserItems({
|
|
21
|
+
marketplaces: [marketplace],
|
|
22
|
+
plugins: [
|
|
23
|
+
{
|
|
24
|
+
id: `demo@${marketplace.name}`,
|
|
25
|
+
name: "demo",
|
|
26
|
+
marketplace: marketplace.name,
|
|
27
|
+
// Only the fields the adapter groups on matter here.
|
|
28
|
+
} as never,
|
|
29
|
+
],
|
|
30
|
+
collapsedMarketplaces: new Set(),
|
|
31
|
+
});
|
|
32
|
+
const category = items.find((i) => i.kind === "category");
|
|
33
|
+
return category && "badge" in category ? category.badge : undefined;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/** Tone for a marketplace when it has plugins and is expanded. */
|
|
37
37
|
function toneFor(marketplace: Marketplace): string | undefined {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
38
|
+
const items = buildPluginBrowserItems({
|
|
39
|
+
marketplaces: [marketplace],
|
|
40
|
+
plugins: [
|
|
41
|
+
{
|
|
42
|
+
id: `demo@${marketplace.name}`,
|
|
43
|
+
name: "demo",
|
|
44
|
+
marketplace: marketplace.name,
|
|
45
|
+
} as never,
|
|
46
|
+
],
|
|
47
|
+
collapsedMarketplaces: new Set(),
|
|
48
|
+
});
|
|
49
|
+
const category = items.find((i) => i.kind === "category");
|
|
50
|
+
return category && "tone" in category ? category.tone : undefined;
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
describe("marketplace category badge", () => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
test("the byline uses a low-key tone, not an attention-grabbing one", () => {
|
|
55
|
+
// It is a signature, not a status. Green reads as "action succeeded" and
|
|
56
|
+
// competes with the plugin rows for attention.
|
|
57
|
+
expect(
|
|
58
|
+
toneFor(
|
|
59
|
+
mp({
|
|
60
|
+
name: "magus",
|
|
61
|
+
source: { source: "github", repo: "MadAppGang/magus" },
|
|
62
|
+
}),
|
|
63
|
+
),
|
|
64
|
+
).toBe("byline");
|
|
56
65
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
// A third-party marketplace still uses the green "Added" status tone.
|
|
67
|
+
expect(
|
|
68
|
+
toneFor(
|
|
69
|
+
mp({
|
|
70
|
+
name: "sp",
|
|
71
|
+
source: { source: "github", repo: "obra/superpowers-marketplace" },
|
|
72
|
+
}),
|
|
73
|
+
),
|
|
74
|
+
).toBe("green");
|
|
75
|
+
});
|
|
64
76
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
77
|
+
test("MadAppGang marketplaces carry the byline instead of a generic label", () => {
|
|
78
|
+
for (const name of ["magus", "magus-marketing", "magus-alpha"]) {
|
|
79
|
+
const badge = badgeFor(
|
|
80
|
+
mp({ name, source: { source: "github", repo: `MadAppGang/${name}` } }),
|
|
81
|
+
);
|
|
82
|
+
expect(badge).toBe("by MadAppGang");
|
|
83
|
+
}
|
|
84
|
+
});
|
|
73
85
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
86
|
+
test("owner match is case-insensitive", () => {
|
|
87
|
+
// The npm/GitHub casing of the org has varied in config; don't depend on it.
|
|
88
|
+
const badge = badgeFor(
|
|
89
|
+
mp({
|
|
90
|
+
name: "magus",
|
|
91
|
+
source: { source: "github", repo: "madappgang/magus" },
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
expect(badge).toBe("by MadAppGang");
|
|
95
|
+
});
|
|
81
96
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
97
|
+
test("someone else's marketplace does NOT claim the byline", () => {
|
|
98
|
+
const badge = badgeFor(
|
|
99
|
+
mp({
|
|
100
|
+
name: "superpowers-marketplace",
|
|
101
|
+
source: { source: "github", repo: "obra/superpowers-marketplace" },
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
expect(badge).toBe("✓ Added");
|
|
105
|
+
});
|
|
91
106
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
107
|
+
test("a repo merely containing 'madappgang' downstream does not match", () => {
|
|
108
|
+
// Guards against a substring check — only the owner segment counts.
|
|
109
|
+
const badge = badgeFor(
|
|
110
|
+
mp({
|
|
111
|
+
name: "impostor",
|
|
112
|
+
source: { source: "github", repo: "evil/madappgang-magus" },
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
expect(badge).toBe("✓ Added");
|
|
116
|
+
});
|
|
99
117
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
118
|
+
test("Official and Deprecated still take precedence", () => {
|
|
119
|
+
expect(
|
|
120
|
+
badgeFor(
|
|
121
|
+
mp({
|
|
122
|
+
name: "claude-plugins-official",
|
|
123
|
+
source: {
|
|
124
|
+
source: "github",
|
|
125
|
+
repo: "anthropics/claude-plugins-official",
|
|
126
|
+
},
|
|
127
|
+
}),
|
|
128
|
+
),
|
|
129
|
+
).toBe("★ Official");
|
|
109
130
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
131
|
+
expect(
|
|
132
|
+
badgeFor(
|
|
133
|
+
mp({
|
|
134
|
+
name: "claude-code-plugins",
|
|
135
|
+
source: { source: "github", repo: "anthropics/claude-code" },
|
|
136
|
+
}),
|
|
137
|
+
),
|
|
138
|
+
).toBe("⚠ Deprecated");
|
|
139
|
+
});
|
|
119
140
|
});
|