claudeup 6.2.0 → 6.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeup",
3
- "version": "6.2.0",
3
+ "version": "6.2.1",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -64,8 +64,8 @@
64
64
  "typescript": "^5.6.3"
65
65
  },
66
66
  "optionalDependencies": {
67
- "claudeup-darwin-arm64": "6.2.0",
68
- "claudeup-darwin-x64": "6.2.0",
69
- "claudeup-linux-x64": "6.2.0"
67
+ "claudeup-darwin-arm64": "6.2.1",
68
+ "claudeup-darwin-x64": "6.2.1",
69
+ "claudeup-linux-x64": "6.2.1"
70
70
  }
71
71
  }
@@ -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
+ });
@@ -69,6 +69,46 @@ describe("parsePluginRequires — setup block (the established convention)", ()
69
69
  expect(tmux.formula).toBe("tmux");
70
70
  });
71
71
 
72
+ // Negative control for the /vN fix: basename(base) yields "v2" here, so
73
+ // without the go-module helper this test FAILS on `name`.
74
+ test("terminal's v2 pin: /v2 is the module's major version, not the binary", () => {
75
+ const r = parsePluginRequires({
76
+ name: "terminal",
77
+ setup: { go: ["github.com/MadAppGang/tmux-mcp/v2@v2.0.0"] },
78
+ });
79
+ expect(r.bin).toEqual([
80
+ {
81
+ name: "tmux-mcp",
82
+ via: "go",
83
+ module: "github.com/MadAppGang/tmux-mcp/v2",
84
+ version: "v2.0.0",
85
+ check: "tmux-mcp --version",
86
+ },
87
+ ]);
88
+ });
89
+
90
+ // One manifest per spec: entries are keyed by binary name, so a v1 and a v2
91
+ // pin of the same module in ONE setup would (correctly) collapse to one.
92
+ test.each([
93
+ [
94
+ "github.com/MadAppGang/tmux-mcp@v1.7.1",
95
+ { name: "tmux-mcp", module: "github.com/MadAppGang/tmux-mcp", version: "v1.7.1" },
96
+ ],
97
+ [
98
+ "github.com/MadAppGang/tmux-mcp/v2",
99
+ { name: "tmux-mcp", module: "github.com/MadAppGang/tmux-mcp/v2", version: undefined },
100
+ ],
101
+ [
102
+ "github.com/x/v2tool@v1.0.0",
103
+ { name: "v2tool", module: "github.com/x/v2tool", version: "v1.0.0" },
104
+ ],
105
+ ])("go spec %s → name/module/version", (spec, expected) => {
106
+ const r = parsePluginRequires({ setup: { go: [spec] } });
107
+ expect(
108
+ r.bin!.map((b) => ({ name: b.name, module: b.module, version: b.version })),
109
+ ).toEqual([expected]);
110
+ });
111
+
72
112
  test("browser-use's setup: pip packages", () => {
73
113
  const r = parsePluginRequires({
74
114
  name: "browser-use",
@@ -86,6 +86,7 @@ const {
86
86
  extractGoVersion,
87
87
  extractBinaryName,
88
88
  parseAtSpec,
89
+ parseGoSpec,
89
90
  parsePipSpec,
90
91
  goDepNeedsInstall,
91
92
  goDepDecision,
@@ -127,6 +128,89 @@ describe("extractGoBinaryName", () => {
127
128
  it("handles empty string gracefully", () => {
128
129
  expect(extractGoBinaryName("")).toBe("");
129
130
  });
131
+
132
+ // Negative control for the /vN fix: a plain basename returns "v2" here.
133
+ // Without the go-module helper wired in, this test FAILS.
134
+ it("names the binary after the module, not its /v2 major-version suffix", () => {
135
+ expect(
136
+ extractGoBinaryName("github.com/MadAppGang/tmux-mcp/v2@v2.0.0"),
137
+ ).toBe("tmux-mcp");
138
+ });
139
+
140
+ it("strips the /v2 suffix from an unpinned module path too", () => {
141
+ expect(extractGoBinaryName("github.com/MadAppGang/tmux-mcp/v2")).toBe(
142
+ "tmux-mcp",
143
+ );
144
+ });
145
+
146
+ it("keeps a name that merely starts with v2", () => {
147
+ expect(extractGoBinaryName("github.com/x/v2tool@v1.0.0")).toBe("v2tool");
148
+ });
149
+ });
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // 1b. parseGoSpec — the Go-specific view over parseAtSpec
153
+ // ---------------------------------------------------------------------------
154
+
155
+ describe("parseGoSpec", () => {
156
+ it("v1 module: name and pinned version", () => {
157
+ expect(parseGoSpec("github.com/MadAppGang/tmux-mcp@v1.7.1")).toEqual({
158
+ spec: "github.com/MadAppGang/tmux-mcp@v1.7.1",
159
+ name: "tmux-mcp",
160
+ version: "v1.7.1",
161
+ });
162
+ });
163
+
164
+ it("v2 module: the /v2 suffix is not the name, the version survives", () => {
165
+ expect(parseGoSpec("github.com/MadAppGang/tmux-mcp/v2@v2.0.0")).toEqual({
166
+ spec: "github.com/MadAppGang/tmux-mcp/v2@v2.0.0",
167
+ name: "tmux-mcp",
168
+ version: "v2.0.0",
169
+ });
170
+ });
171
+
172
+ it("v2 module without a pin: name resolved, version null", () => {
173
+ expect(parseGoSpec("github.com/MadAppGang/tmux-mcp/v2")).toEqual({
174
+ spec: "github.com/MadAppGang/tmux-mcp/v2",
175
+ name: "tmux-mcp",
176
+ version: null,
177
+ });
178
+ });
179
+
180
+ it("a segment that starts with v2 is a real name", () => {
181
+ expect(parseGoSpec("github.com/x/v2tool@v1.0.0")).toEqual({
182
+ spec: "github.com/x/v2tool@v1.0.0",
183
+ name: "v2tool",
184
+ version: "v1.0.0",
185
+ });
186
+ });
187
+
188
+ it("@latest is unpinned, same as parseAtSpec", () => {
189
+ expect(parseGoSpec("github.com/MadAppGang/tmux-mcp/v2@latest")).toEqual({
190
+ spec: "github.com/MadAppGang/tmux-mcp/v2@latest",
191
+ name: "tmux-mcp",
192
+ version: null,
193
+ });
194
+ });
195
+
196
+ // Terminal 5.0.0 contract cases (test plan Area 3, C5 and C6). They live
197
+ // here rather than in their own file because any test file that imports
198
+ // plugin-setup.js ahead of the mock.module seam above bakes the real
199
+ // execFile into promisify() and fails the installPluginDeps suites.
200
+ it("C5: a /v10 suffix is stripped in full, not only its first digit", () => {
201
+ const { name, version } = parseGoSpec("github.com/x/tool/v10@v10.1.0");
202
+ expect({ name, version }).toEqual({ name: "tool", version: "v10.1.0" });
203
+ });
204
+
205
+ it("C6: /v1 is never a Go major-version suffix, so it stays the name", () => {
206
+ const { name, version } = parseGoSpec("github.com/x/tool/v1@v1.0.0");
207
+ expect({ name, version }).toEqual({ name: "v1", version: "v1.0.0" });
208
+ });
209
+
210
+ it("C6 control: the same shape at /v2 is a suffix and is stripped", () => {
211
+ const { name, version } = parseGoSpec("github.com/x/tool/v2@v2.0.0");
212
+ expect({ name, version }).toEqual({ name: "tool", version: "v2.0.0" });
213
+ });
130
214
  });
131
215
 
132
216
  // ---------------------------------------------------------------------------
@@ -295,6 +379,14 @@ describe("extractGoVersion", () => {
295
379
  it("returns null when unpinned", () => {
296
380
  expect(extractGoVersion("github.com/user/tool")).toBeNull();
297
381
  });
382
+ it("reads the pin behind a /v2 module path", () => {
383
+ expect(
384
+ extractGoVersion("github.com/MadAppGang/tmux-mcp/v2@v2.0.0"),
385
+ ).toBe("v2.0.0");
386
+ });
387
+ it("a /v2 module path with no pin is unpinned, not pinned to v2", () => {
388
+ expect(extractGoVersion("github.com/MadAppGang/tmux-mcp/v2")).toBeNull();
389
+ });
298
390
  });
299
391
 
300
392
  describe("goDepNeedsInstall — version awareness", () => {
@@ -666,6 +758,16 @@ describe("parseAtSpec", () => {
666
758
  expect(parseAtSpec("tool@latest").version).toBeNull();
667
759
  });
668
760
 
761
+ it("does NOT apply the Go /vN rule — an npm scope may be named v2", () => {
762
+ // The Go major-version strip lives in parseGoSpec only; folding it in
763
+ // here would turn this scoped npm package into "@scope".
764
+ expect(parseAtSpec("@scope/v2@1.0.0")).toEqual({
765
+ spec: "@scope/v2@1.0.0",
766
+ name: "v2",
767
+ version: "1.0.0",
768
+ });
769
+ });
770
+
669
771
  it("treats a bare name as unpinned", () => {
670
772
  expect(parseAtSpec("tool")).toEqual({
671
773
  spec: "tool",
@@ -7,7 +7,7 @@
7
7
  * invisible. No update is offered, and the only cure is a reinstall — which is
8
8
  * exactly the "plugin says installed but its skills are missing" report.
9
9
  *
10
- * This is not a hypothetical. `publish-dist.sh` force-pushes a rebuilt tree, so
10
+ * This is not a hypothetical. `publish-dist.sh` pushes a rebuilt tree, so
11
11
  * same-version-different-content is a normal outcome of the release process.
12
12
  * Measured on 2026-08-06: `cache/magus/dev/3.0.1/skills` held
13
13
  * {audit, tui-progress} while `marketplaces/magus/plugins/dev/skills` held
@@ -113,11 +113,13 @@ function getGitRemote(marketplacePath: string): string | undefined {
113
113
  * Names of the skills under `skillsDir`, recursing through grouping directories.
114
114
  *
115
115
  * A skill is a directory holding a SKILL.md, and it may sit one or more levels
116
- * down: dev groups its 43 skills under `skills/frontend/`, `skills/backend/`
117
- * and so on, and mattpocock groups its 37 under `skills/engineering/`. Reading
118
- * only the top level counted the *groups* — dev reported 9 skills against 43 on
119
- * disk, mattpocock 5 against 37 — and that count is what the plugin detail
120
- * panel prints.
116
+ * down: dev nests its skills by category under `skills/frontend/`,
117
+ * `skills/backend/` and so on, and mattpocock groups its 37 under
118
+ * `skills/engineering/`. Reading only the top level counted the *groups* — dev
119
+ * once reported 9 skills against 43 on disk, mattpocock 5 against 37 — and that
120
+ * count is what the plugin detail panel prints. dev also ships a `knowledge/`
121
+ * tree beside `skills/`: reference manuals reached by path, holding no SKILL.md
122
+ * and registering nothing, so this scan correctly never sees them.
121
123
  *
122
124
  * Depth is capped because this runs over every plugin of every cloned
123
125
  * marketplace on each list load, and a skill nested four levels below `skills/`
@@ -15,6 +15,7 @@ import type {
15
15
  BinRequirement,
16
16
  PluginRequires,
17
17
  } from "../types/index.js";
18
+ import { goBinaryName } from "../utils/go-module.js";
18
19
  import { parsePluginId } from "../utils/string-utils.js";
19
20
  import { getLocalMarketplacesInfo } from "./plugin-manager.js";
20
21
 
@@ -98,15 +99,19 @@ function parseSetup(setup: unknown): BinRequirement[] {
98
99
  pkgManager("npm");
99
100
  pkgManager("pip");
100
101
 
102
+ // Not basename(): a Go module's trailing /vN is its major version, not the
103
+ // binary. `tmux-mcp/v2` installs `tmux-mcp`; naming it `v2` here made every
104
+ // downstream presence check look for a binary that never exists.
101
105
  for (const spec of (s.go as string[] | undefined) ?? []) {
102
106
  if (typeof spec !== "string") continue;
103
107
  const { base, version } = splitVersion(spec);
108
+ const name = goBinaryName(base);
104
109
  bins.push({
105
- name: basename(base),
110
+ name,
106
111
  via: "go",
107
112
  module: base,
108
113
  version,
109
- check: `${basename(base)} --version`,
114
+ check: `${name} --version`,
110
115
  });
111
116
  }
112
117
  for (const spec of (s.brew as string[] | undefined) ?? []) {
@@ -33,6 +33,7 @@ import { constants as fsConstants } from "node:fs";
33
33
  import path from "node:path";
34
34
  import os from "node:os";
35
35
  import { which } from "../utils/command-utils.js";
36
+ import { goBinaryName } from "../utils/go-module.js";
36
37
 
37
38
  const execFileAsync = promisify(execFile);
38
39
 
@@ -392,14 +393,32 @@ async function installCargoPackages(
392
393
  }
393
394
  }
394
395
 
396
+ /**
397
+ * {@link parseAtSpec} for a Go module spec.
398
+ *
399
+ * Same split, but the name comes from {@link goBinaryName}: Go's `/vN`
400
+ * major-version suffix is part of the module path, not the binary, so
401
+ * "github.com/MadAppGang/tmux-mcp/v2@v2.0.0" installs `tmux-mcp`, not `v2`.
402
+ * The generic parser cannot do this — an npm scope may legitimately be `v2`.
403
+ */
404
+ export function parseGoSpec(spec: string): ParsedSpec {
405
+ const parsed = parseAtSpec(spec);
406
+ // Same separator rule as parseAtSpec: a leading `@` is never a version.
407
+ const at = spec.lastIndexOf("@");
408
+ const modulePath = at > 0 ? spec.slice(0, at) : spec;
409
+ return { ...parsed, name: goBinaryName(modulePath) };
410
+ }
411
+
395
412
  /**
396
413
  * Extract binary name from a Go module path.
397
- * The binary name is the last path segment before @version.
398
- * e.g., "github.com/MadAppGang/tmux-mcp@latest" → "tmux-mcp"
399
- * "github.com/user/tool" → "tool"
414
+ * The binary name is the last path segment before @version, minus any /vN
415
+ * major-version suffix.
416
+ * e.g., "github.com/MadAppGang/tmux-mcp@latest" → "tmux-mcp"
417
+ * "github.com/MadAppGang/tmux-mcp/v2@v2.0.0" → "tmux-mcp"
418
+ * "github.com/user/tool" → "tool"
400
419
  */
401
420
  export function extractGoBinaryName(pkg: string): string {
402
- return parseAtSpec(pkg).name;
421
+ return parseGoSpec(pkg).name;
403
422
  }
404
423
 
405
424
  /**
@@ -409,7 +428,7 @@ export function extractGoBinaryName(pkg: string): string {
409
428
  * "github.com/user/tool" → null
410
429
  */
411
430
  export function extractGoVersion(pkg: string): string | null {
412
- return parseAtSpec(pkg).version;
431
+ return parseGoSpec(pkg).version;
413
432
  }
414
433
 
415
434
  /** Normalize a semver-ish string for comparison: trim and drop a leading "v". */
@@ -641,7 +660,7 @@ export async function binaryDepDecision(
641
660
 
642
661
  /** {@link binaryDepDecision} for a Go module path. */
643
662
  export async function goDepDecision(pkg: string): Promise<GoDepDecision> {
644
- return binaryDepDecision(parseAtSpec(pkg), getGoInstallDir);
663
+ return binaryDepDecision(parseGoSpec(pkg), getGoInstallDir);
645
664
  }
646
665
 
647
666
  /**
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Go module path → installed binary name.
3
+ *
4
+ * `go install` names the binary after the last path element of the module
5
+ * path, EXCEPT when that element is a major-version suffix: the module
6
+ * `github.com/MadAppGang/tmux-mcp/v2` installs `tmux-mcp`, not `v2`. Taking a
7
+ * plain basename therefore points every later check at a binary that never
8
+ * exists — presence probes fail, so the module is reinstalled on every run,
9
+ * doctor reports it missing, and the pin comparison never runs at all.
10
+ *
11
+ * Only the Go rule lives here. npm and cargo share the `name@version` syntax
12
+ * but not the suffix convention — an npm scoped package can legitimately be
13
+ * called `@scope/v2` — so this must not be folded into the generic spec parser.
14
+ */
15
+
16
+ /**
17
+ * Trailing major-version suffix per the Go modules spec: `/vN` with N >= 2.
18
+ * Anchored to the END of the path with nothing after `vN`, so a segment that
19
+ * merely starts with "v2" (`github.com/x/v2tool`) is a real name, not a suffix.
20
+ * v0 and v1 are never written as a path suffix. The alternation, not a plain
21
+ * `[2-9][0-9]*`, because that shorthand rejects v10–v19 — a first digit of 1
22
+ * is only excluded when it is the ONLY digit.
23
+ */
24
+ const MAJOR_VERSION_SUFFIX = /\/v(?:[2-9]|[1-9][0-9]+)$/;
25
+
26
+ /**
27
+ * The binary `go install <modulePath>` produces.
28
+ *
29
+ * Takes the MODULE PATH — the part before any `@version` — so callers that
30
+ * hold a full `path@version` spec split it first. An empty or single-segment
31
+ * input is returned unchanged.
32
+ */
33
+ export function goBinaryName(modulePath: string): string {
34
+ const withoutSuffix = modulePath.replace(MAJOR_VERSION_SUFFIX, "");
35
+ return withoutSuffix.split("/").pop() || withoutSuffix;
36
+ }