claudeup 4.40.1 → 4.41.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeup",
3
- "version": "4.40.1",
3
+ "version": "4.41.0",
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": "4.40.1",
68
- "claudeup-darwin-x64": "4.40.1",
69
- "claudeup-linux-x64": "4.40.1"
67
+ "claudeup-darwin-arm64": "4.41.0",
68
+ "claudeup-darwin-x64": "4.41.0",
69
+ "claudeup-linux-x64": "4.41.0"
70
70
  }
71
71
  }
@@ -38,3 +38,87 @@ describe("marketplaces", () => {
38
38
  expect(names).not.toContain("superpowers");
39
39
  });
40
40
  });
41
+
42
+ /**
43
+ * The community marketplaces added alongside the Magus channels.
44
+ *
45
+ * `name` is not cosmetic: plugin ids are built as `${plugin.name}@${mpName}`
46
+ * (plugin-manager.ts), so it MUST equal the upstream
47
+ * .claude-plugin/marketplace.json "name" field. A mismatch fails at install
48
+ * time with "not found in marketplace", nowhere near the typo that caused it.
49
+ *
50
+ * Verify against upstream with:
51
+ * gh api repos/<repo>/contents/.claude-plugin/marketplace.json \
52
+ * -H "Accept: application/vnd.github.raw" | jq -r '.name'
53
+ */
54
+ const COMMUNITY_MARKETPLACES: ReadonlyArray<[name: string, repo: string]> = [
55
+ ["mattpocock", "mattpocock/skills"],
56
+ ["ecc", "affaan-m/everything-claude-code"],
57
+ ["ponytail", "DietrichGebert/ponytail"],
58
+ ["addy-agent-skills", "addyosmani/agent-skills"],
59
+ ["humanizer", "blader/humanizer"],
60
+ ["opendesign", "manalkaff/opendesign"],
61
+ ];
62
+
63
+ describe("community marketplaces", () => {
64
+ it.each(COMMUNITY_MARKETPLACES)(
65
+ "%s points at the repo whose manifest declares that name",
66
+ (name, repo) => {
67
+ const mp = defaultMarketplaces.find((m) => m.name === name);
68
+ expect(mp).toBeDefined();
69
+ expect(mp?.source.repo).toBe(repo);
70
+ },
71
+ );
72
+
73
+ it.each(COMMUNITY_MARKETPLACES)(
74
+ "%s is visible by default via the always-show gate",
75
+ (name) => {
76
+ const mp = defaultMarketplaces.find((m) => m.name === name);
77
+ // plugin-manager's gate: configured || official || featured || owned.
78
+ // Without one of these the entry is only a name-to-repo mapping and
79
+ // never appears, which is a silent no-op rather than a failure.
80
+ expect(mp?.official || mp?.featured || mp?.owned).toBe(true);
81
+ },
82
+ );
83
+
84
+ it.each(COMMUNITY_MARKETPLACES)(
85
+ "%s claims neither Anthropic nor MadAppGang ownership",
86
+ (name) => {
87
+ const mp = defaultMarketplaces.find((m) => m.name === name);
88
+ // `official` badges it as Anthropic-managed; `owned` sorts it above
89
+ // everything as a MadAppGang channel. Both would be a false claim.
90
+ expect(mp?.official).toBeUndefined();
91
+ expect(mp?.owned).toBeUndefined();
92
+ },
93
+ );
94
+
95
+ it("introduces no duplicate marketplace name or repo", () => {
96
+ const names = defaultMarketplaces.map((m) => m.name);
97
+ const repos = defaultMarketplaces.map((m) => m.source.repo.toLowerCase());
98
+
99
+ expect(new Set(names).size).toBe(names.length);
100
+ expect(new Set(repos).size).toBe(repos.length);
101
+ });
102
+
103
+ it("surfaces every community marketplace through getAllMarketplaces", () => {
104
+ const names = getAllMarketplaces().map((mp) => mp.name);
105
+
106
+ for (const [name] of COMMUNITY_MARKETPLACES) {
107
+ expect(names).toContain(name);
108
+ }
109
+ });
110
+
111
+ it("keeps the Magus channels sorted above the community ones", () => {
112
+ const order = getAllMarketplaces().map((mp) => mp.name);
113
+ const lastMagus = Math.max(
114
+ order.indexOf("magus"),
115
+ order.indexOf("magus-marketing"),
116
+ order.indexOf("magus-alpha"),
117
+ );
118
+ const firstCommunity = Math.min(
119
+ ...COMMUNITY_MARKETPLACES.map(([name]) => order.indexOf(name)),
120
+ );
121
+
122
+ expect(lastMagus).toBeLessThan(firstCommunity);
123
+ });
124
+ });
@@ -87,6 +87,80 @@ export const defaultMarketplaces: Marketplace[] = [
87
87
  description: "Curated Claude Code plugins for skills and workflows",
88
88
  featured: true,
89
89
  },
90
+ // Community marketplaces. Each `name` MUST equal the upstream
91
+ // .claude-plugin/marketplace.json "name" field — plugin ids are built as
92
+ // `${plugin.name}@${marketplaceName}`, so a mismatch here breaks install
93
+ // with a "not found in marketplace" error rather than anything obvious.
94
+ //
95
+ // All carry `featured` because that is the only flag that makes a
96
+ // third-party marketplace visible: the gate in plugin-manager is
97
+ // `configured || official || featured || owned`, and an unflagged entry is
98
+ // just a name→repo mapping for auto-recovery. `official` means Anthropic
99
+ // and `owned` means MadAppGang, so neither applies.
100
+ {
101
+ name: "mattpocock",
102
+ displayName: "Matt Pocock",
103
+ source: {
104
+ source: "github",
105
+ repo: "mattpocock/skills",
106
+ },
107
+ description:
108
+ "Agent skills for real engineering — grilling, spec and ticket flows, TDD, code review, domain modelling",
109
+ featured: true,
110
+ },
111
+ {
112
+ name: "ecc",
113
+ displayName: "Everything Claude Code",
114
+ source: {
115
+ source: "github",
116
+ repo: "affaan-m/everything-claude-code",
117
+ },
118
+ description:
119
+ "Operator layer for agent harnesses — 68 agents, 286 skills, and command shims",
120
+ featured: true,
121
+ },
122
+ {
123
+ name: "ponytail",
124
+ displayName: "Ponytail",
125
+ source: {
126
+ source: "github",
127
+ repo: "DietrichGebert/ponytail",
128
+ },
129
+ description:
130
+ "Pushes the agent toward the smallest solution that works — YAGNI, and the standard library before custom code",
131
+ featured: true,
132
+ },
133
+ {
134
+ name: "addy-agent-skills",
135
+ displayName: "Addy Osmani Skills",
136
+ source: {
137
+ source: "github",
138
+ repo: "addyosmani/agent-skills",
139
+ },
140
+ description: "Production-grade engineering skills for AI coding agents",
141
+ featured: true,
142
+ },
143
+ {
144
+ name: "humanizer",
145
+ displayName: "Humanizer",
146
+ source: {
147
+ source: "github",
148
+ repo: "blader/humanizer",
149
+ },
150
+ description: "Removes signs of AI-generated writing from text",
151
+ featured: true,
152
+ },
153
+ {
154
+ name: "opendesign",
155
+ displayName: "OpenDesign",
156
+ source: {
157
+ source: "github",
158
+ repo: "manalkaff/opendesign",
159
+ },
160
+ description:
161
+ "Designs and redesigns frontend UI and marketing graphics, routing to specialist skills per artifact type",
162
+ featured: true,
163
+ },
90
164
  {
91
165
  name: "claude-code-plugins",
92
166
  displayName: "Anthropic Deprecated",
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  import { readFile, writeFile, stat, rename, unlink, open } from 'node:fs/promises';
17
+ import { existsSync } from 'node:fs';
17
18
  import { randomUUID } from 'node:crypto';
18
19
  import { dirname, basename, join, resolve } from 'node:path';
19
20
  import { homedir } from 'node:os';
@@ -692,7 +693,11 @@ export async function resolveGlobalGitignorePath(): Promise<string | null> {
692
693
  export async function resolvePluginConventions(
693
694
  pluginPath: string,
694
695
  ): Promise<PluginConventions | null> {
695
- const manifestPath = join(pluginPath, 'plugin.json');
696
+ // `.claude-plugin/plugin.json` is the location Claude Code reads; a root-level
697
+ // manifest is inert to the runtime but still worth reading for a third-party plugin
698
+ // whose author put it there.
699
+ const nested = join(pluginPath, '.claude-plugin', 'plugin.json');
700
+ const manifestPath = existsSync(nested) ? nested : join(pluginPath, 'plugin.json');
696
701
 
697
702
  try {
698
703
  const raw = await readFile(manifestPath, 'utf-8');
@@ -156,15 +156,19 @@ export function parsePluginRequires(pluginJson: unknown): PluginRequires {
156
156
  export async function readPluginRequires(
157
157
  pluginPath: string,
158
158
  ): Promise<PluginRequires> {
159
- const manifestPath = path.join(pluginPath, "plugin.json");
159
+ // `.claude-plugin/plugin.json` FIRST that is the only location Claude Code's
160
+ // runtime loader reads, and the only one its documentation describes. The root
161
+ // fallback stays for third-party plugins whose authors put it there; such a manifest
162
+ // is inert as far as Claude Code is concerned, but claudeup can still read metadata
163
+ // out of it rather than reporting nothing.
164
+ const manifestPath = path.join(pluginPath, ".claude-plugin", "plugin.json");
160
165
  try {
161
166
  if (await fs.pathExists(manifestPath)) {
162
167
  return parsePluginRequires(await fs.readJson(manifestPath));
163
168
  }
164
- // Some plugins nest the manifest under .claude-plugin/plugin.json.
165
- const nested = path.join(pluginPath, ".claude-plugin", "plugin.json");
166
- if (await fs.pathExists(nested)) {
167
- return parsePluginRequires(await fs.readJson(nested));
169
+ const rootLevel = path.join(pluginPath, "plugin.json");
170
+ if (await fs.pathExists(rootLevel)) {
171
+ return parsePluginRequires(await fs.readJson(rootLevel));
168
172
  }
169
173
  } catch {
170
174
  // Unreadable/malformed — treat as no requirements.