claudeup 4.34.0 → 4.35.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.34.0",
3
+ "version": "4.35.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.34.0",
68
- "claudeup-darwin-x64": "4.34.0",
69
- "claudeup-linux-x64": "4.34.0"
67
+ "claudeup-darwin-arm64": "4.35.0",
68
+ "claudeup-darwin-x64": "4.35.0",
69
+ "claudeup-linux-x64": "4.35.0"
70
70
  }
71
71
  }
@@ -216,3 +216,74 @@ describe("resolveAllProfiles — union", () => {
216
216
  expect(union.conflicts?.some((c) => c.includes("claudish"))).toBe(true);
217
217
  });
218
218
  });
219
+
220
+ describe("marketplaces derived from plugin ids", () => {
221
+ const noBins = { binResolver: fakeBinResolver({}) };
222
+
223
+ test("a plugin id registers the marketplace it names", async () => {
224
+ const manifest = manifestOf({
225
+ solo: { name: "Solo", plugins: { "dev@magus": "latest" } },
226
+ });
227
+ const c = await resolveProfile(manifest, "solo", noBins);
228
+ expect(c.marketplaces.magus).toEqual({
229
+ source: "github",
230
+ repo: "MadAppGang/magus",
231
+ });
232
+ });
233
+
234
+ test("extends alone is enough — the failure this fixes", async () => {
235
+ // A profile that only extends a predefined one inherits plugins like
236
+ // dev@magus and previously NO marketplaces, so install resolved every
237
+ // plugin against a marketplace that was never registered.
238
+ const manifest = manifestOf({
239
+ team: { name: "Team", extends: "developer-essentials" },
240
+ });
241
+ const c = await resolveProfile(manifest, "team", noBins);
242
+ expect(Object.keys(c.plugins).length).toBeGreaterThan(0);
243
+ expect(c.marketplaces.magus?.repo).toBe("MadAppGang/magus");
244
+ });
245
+
246
+ test("each distinct channel is derived, not just the first", async () => {
247
+ const manifest = manifestOf({
248
+ mixed: {
249
+ name: "Mixed",
250
+ plugins: {
251
+ "dev@magus": "latest",
252
+ "seo@magus-marketing": "latest",
253
+ "autolinear@magus-alpha": "latest",
254
+ },
255
+ },
256
+ });
257
+ const c = await resolveProfile(manifest, "mixed", noBins);
258
+ expect(c.marketplaces["magus-marketing"]?.repo).toBe("MadAppGang/magus-marketing");
259
+ expect(c.marketplaces["magus-alpha"]?.repo).toBe("MadAppGang/magus-alpha");
260
+ });
261
+
262
+ test("an explicit declaration is never overridden", async () => {
263
+ const manifest = manifestOf({
264
+ pinned: {
265
+ name: "Pinned",
266
+ marketplaces: { magus: { source: "github", repo: "MadAppGang/magus-fork" } },
267
+ plugins: { "dev@magus": "latest" },
268
+ },
269
+ });
270
+ const c = await resolveProfile(manifest, "pinned", noBins);
271
+ expect(c.marketplaces.magus?.repo).toBe("MadAppGang/magus-fork");
272
+ });
273
+
274
+ test("an unknown marketplace is left for the manifest to declare", async () => {
275
+ const manifest = manifestOf({
276
+ third: { name: "Third", plugins: { "thing@someones-marketplace": "latest" } },
277
+ });
278
+ const c = await resolveProfile(manifest, "third", noBins);
279
+ expect(c.marketplaces["someones-marketplace"]).toBeUndefined();
280
+ });
281
+
282
+ test("a bare plugin id contributes no marketplace", async () => {
283
+ const manifest = manifestOf({
284
+ bare: { name: "Bare", plugins: { local: "latest" } },
285
+ });
286
+ const c = await resolveProfile(manifest, "bare", noBins);
287
+ expect(Object.keys(c.marketplaces)).toHaveLength(0);
288
+ });
289
+ });
@@ -53,6 +53,19 @@ export const defaultMarketplaces: Marketplace[] = [
53
53
  owned: true, // Ours: always listed even when uninstalled, sorted to the top
54
54
  // Not featured: a niche channel, so keep it collapsed by default.
55
55
  },
56
+ {
57
+ name: "magus-alpha",
58
+ displayName: "Magus Alpha",
59
+ source: {
60
+ source: "github",
61
+ repo: "MadAppGang/magus-alpha",
62
+ },
63
+ description:
64
+ "Experimental plugins with evolving interfaces — may change or be withdrawn without notice",
65
+ official: false,
66
+ owned: true, // Ours: always listed even when uninstalled, sorted to the top
67
+ // Not featured: experimental, so keep it collapsed by default.
68
+ },
56
69
  {
57
70
  name: "claude-plugins-official",
58
71
  displayName: "Anthropic Official",
@@ -17,6 +17,7 @@ import type {
17
17
  ResolvedClosure,
18
18
  } from "../types/index.js";
19
19
  import { cliTools } from "../data/cli-tools.js";
20
+ import { getMarketplaceByName } from "../data/marketplaces.js";
20
21
  import { PREDEFINED_PROFILES } from "../data/predefined-profiles.js";
21
22
  import { resolvePluginBinRequirements } from "./plugin-requires.js";
22
23
 
@@ -147,6 +148,36 @@ async function resolveBins(
147
148
  }
148
149
 
149
150
  /** Resolve a single profile (applying `extends`) into an install closure. */
151
+ /**
152
+ * Add the marketplace every plugin id already names.
153
+ *
154
+ * A plugin id carries its marketplace — `dev@magus`, `seo@magus-marketing` —
155
+ * so requiring the manifest to ALSO declare that marketplace is redundant, and
156
+ * forgetting to is silent: the plugin resolves against a marketplace that was
157
+ * never registered. `extends` made this the default failure, because it
158
+ * inherits plugins from a predefined profile and no marketplaces at all.
159
+ *
160
+ * Only marketplaces claudeup already knows are derived, since a repo is needed
161
+ * to register one. An unknown suffix is left alone for the manifest to declare.
162
+ * An explicit entry always wins: deriving must never override a pin.
163
+ */
164
+ function withDerivedMarketplaces(
165
+ declared: ProfileManifestEntry["marketplaces"],
166
+ plugins: Record<string, string>,
167
+ ): NonNullable<ProfileManifestEntry["marketplaces"]> {
168
+ const out = { ...(declared ?? {}) };
169
+ for (const id of Object.keys(plugins)) {
170
+ const at = id.lastIndexOf("@");
171
+ if (at <= 0) continue;
172
+ const name = id.slice(at + 1);
173
+ if (out[name]) continue;
174
+ const known = getMarketplaceByName(name);
175
+ if (!known?.source.repo) continue;
176
+ out[name] = { source: "github", repo: known.source.repo };
177
+ }
178
+ return out;
179
+ }
180
+
150
181
  export async function resolveProfile(
151
182
  manifest: ProfileManifest,
152
183
  profileId: string,
@@ -161,7 +192,7 @@ export async function resolveProfile(
161
192
  const bins = await resolveBins(entry, Object.keys(plugins), binResolver);
162
193
 
163
194
  return {
164
- marketplaces: { ...(entry.marketplaces ?? {}) },
195
+ marketplaces: withDerivedMarketplaces(entry.marketplaces, plugins),
165
196
  plugins,
166
197
  mcpServers: { ...(entry.mcpServers ?? {}) },
167
198
  bins,
@@ -6,7 +6,7 @@ import { ScreenLayout } from "../components/layout/index.js";
6
6
  import { ScrollableList } from "../components/ScrollableList.js";
7
7
  import { EmptyFilterState } from "../components/EmptyFilterState.js";
8
8
  import { fuzzyFilter } from "../../utils/fuzzy-search.js";
9
- import { getAllMarketplaces } from "../../data/marketplaces.js";
9
+ import { defaultMarketplaces, getAllMarketplaces } from "../../data/marketplaces.js";
10
10
  import { clearContentDriftCache } from "../../services/content-drift.js";
11
11
  import {
12
12
  diffVersions,
@@ -33,6 +33,7 @@ import {
33
33
  } from "../../services/claude-settings.js";
34
34
  import { saveProfile } from "../../services/profiles.js";
35
35
  import {
36
+ addMarketplace as cliAddMarketplace,
36
37
  installPlugin as cliInstallPlugin,
37
38
  repairPlugin as cliRepairPlugin,
38
39
  uninstallPlugin as cliUninstallPlugin,
@@ -387,16 +388,60 @@ export function PluginsScreen() {
387
388
  }
388
389
  };
389
390
 
391
+ /**
392
+ * Add a marketplace.
393
+ *
394
+ * The known ones are offered as a menu and added here — that covers every
395
+ * marketplace claudeup ships with, including magus, so a first run does not
396
+ * have to leave the tool. An arbitrary owner/repo still needs the terminal:
397
+ * there is no text-input modal, only confirm/message/select.
398
+ */
390
399
  const handleShowAddMarketplaceInstructions = async () => {
400
+ const installed = new Set(
401
+ pluginsState.marketplaces.status === "success"
402
+ ? pluginsState.marketplaces.data.map((m) => m.name)
403
+ : [],
404
+ );
405
+ const addable = defaultMarketplaces.filter(
406
+ (m) => !installed.has(m.name) && !m.deprecated && m.source.repo,
407
+ );
408
+
409
+ if (addable.length > 0) {
410
+ const choice = await modal.select(
411
+ "Add Marketplace",
412
+ "Pick one to add now, or choose Other for a repo not listed.",
413
+ [
414
+ ...addable.map((m) => ({
415
+ label: m.displayName || m.name,
416
+ value: m.source.repo as string,
417
+ description: m.description,
418
+ })),
419
+ { label: "Other…", value: "", description: "Any owner/repo" },
420
+ ],
421
+ );
422
+ if (choice === null) return;
423
+ if (choice) {
424
+ try {
425
+ await cliAddMarketplace(choice);
426
+ await handleRefresh();
427
+ } catch (e) {
428
+ await modal.message(
429
+ "Could not add marketplace",
430
+ `${(e as Error).message}\n\nRun it yourself if this persists:\n\n` +
431
+ ` claude plugin marketplace add ${choice}`,
432
+ "error",
433
+ );
434
+ }
435
+ return;
436
+ }
437
+ }
438
+
391
439
  await modal.message(
392
440
  "Add Marketplace",
393
- "To add a marketplace, run this command in your terminal:\n\n" +
441
+ "For a marketplace that is not listed, run this in your terminal:\n\n" +
394
442
  " claude plugin marketplace add owner/repo\n\n" +
395
- "Examples:\n" +
396
- " claude plugin marketplace add MadAppGang/magus\n" +
397
- " claude plugin marketplace add anthropics/claude-plugins-official\n\n" +
398
443
  "Auto-update is enabled by default for new marketplaces.\n\n" +
399
- "After adding, refresh claudeup with 'r' to see the new marketplace.",
444
+ "After adding, refresh claudeup with 'r' to see it.",
400
445
  "info",
401
446
  );
402
447
  };
@@ -593,14 +638,30 @@ export function PluginsScreen() {
593
638
  );
594
639
  }
595
640
  } else {
596
- await modal.message(
641
+ // Add it here rather than printing the command for the user to run.
642
+ // Telling someone to leave the tool, paste a command, come back and
643
+ // press 'r' is a step claudeup can just take: it already knows the
644
+ // repo, and addMarketplace() is the same call they would make.
645
+ const repo = mp.source.repo || mp.name;
646
+ const wantAdd = await modal.confirm(
597
647
  `Add ${mp.displayName}?`,
598
- `To add this marketplace, run in your terminal:\n\n` +
599
- ` claude plugin marketplace add ${mp.source.repo || mp.name}\n\n` +
600
- `Auto-update is enabled by default.\n\n` +
601
- `After adding, refresh claudeup with 'r' to see it.`,
602
- "info",
648
+ `Clone ${repo} and register it as a marketplace?\n\n` +
649
+ `Auto-update is enabled by default.`,
603
650
  );
651
+ if (wantAdd) {
652
+ try {
653
+ await cliAddMarketplace(repo);
654
+ await handleRefresh();
655
+ } catch (e) {
656
+ await modal.message(
657
+ "Could not add marketplace",
658
+ `${(e as Error).message}\n\n` +
659
+ `Run it yourself if this persists:\n\n` +
660
+ ` claude plugin marketplace add ${repo}`,
661
+ "error",
662
+ );
663
+ }
664
+ }
604
665
  }
605
666
  } else if (item.kind === "plugin") {
606
667
  const plugin = item.plugin;