magus-cli 7.14.0 → 7.14.2

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 CHANGED
@@ -46,9 +46,10 @@ install [profile] Install a profile's plugins, binaries, skills and
46
46
  update [profile] Bring the ACTIVE profile up to date: install what it
47
47
  declares but this machine lacks, and advance every
48
48
  "latest" pin to the newest published version.
49
+ Applies without asking; it only moves forward.
50
+ --dry-run Show the plan and stop; writes nothing, exits 0
49
51
  --check Report only, write nothing (exits 1 if anything is
50
52
  missing or known to be behind)
51
- --yes, -y Skip the confirmation prompt
52
53
  profile list Show every profile; ● marks the active one
53
54
  profile show <name> Print a profile's fully resolved closure
54
55
  profile switch <name> Make <name> active: render its settings.json,
@@ -56,8 +57,6 @@ profile switch <name> Make <name> active: render its settings.json,
56
57
  profile init Write .claude/profiles.json from this project's setup
57
58
  doctor [--fix] Check binary deps, the active profile, conventions
58
59
 
59
- claude [args...] Check for plugin updates (1h cache), then run claude
60
- --force, -f Force the update check
61
60
  upgrade Update magus itself
62
61
  --version, -v Version + update check
63
62
  --help, -h This list
@@ -171,15 +170,16 @@ walks through it end to end, including conflicts and protected branches.
171
170
 
172
171
  | Screen | What it manages |
173
172
  |---|---|
174
- | Plugins | Install, enable, disable; shows version changes and newly installed plugins |
173
+ | Plugins | Install, uninstall and update at USER, PROJECT or LOCAL scope; shows version changes and newly installed plugins |
175
174
  | Skills | Browse and install skills from configured skill repos |
176
175
  | MCP | Add MCP servers from a curated catalog |
177
176
  | Settings | Claude Code settings, from a catalog of known keys |
178
- | Profiles | Saved plugin sets (the TUI's own, older profile concept) |
177
+ | Profiles | Switch between this project's profiles, apply a built-in one, rename, delete, copy or import |
179
178
  | CLI Tools | Install and update CLI tools via the right package manager; a tool a plugin installs from a release shows as managed by that plugin |
180
179
  | Git State | Gitignore conventions and repo hygiene |
181
180
  | Alias | Shell alias + flag management for launching `claude` |
182
181
  | Styles | Communication style presets, composed into one native output style |
182
+ | Models | Per-subagent model routing presets (`magus models`) |
183
183
 
184
184
  Navigate with `↑/↓` or `j/k`, `Enter` to select, `r` to refresh, `?` for help,
185
185
  `q`/`Escape` to go back. Number keys `1`–`9` jump straight to a screen.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magus-cli",
3
- "version": "7.14.0",
3
+ "version": "7.14.2",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -65,8 +65,8 @@
65
65
  "typescript": "^5.6.3"
66
66
  },
67
67
  "optionalDependencies": {
68
- "magus-cli-darwin-arm64": "7.14.0",
69
- "magus-cli-darwin-x64": "7.14.0",
70
- "magus-cli-linux-x64": "7.14.0"
68
+ "magus-cli-darwin-arm64": "7.14.2",
69
+ "magus-cli-darwin-x64": "7.14.2",
70
+ "magus-cli-linux-x64": "7.14.2"
71
71
  }
72
72
  }
@@ -0,0 +1,341 @@
1
+ /**
2
+ * A scope key on the Plugins screen uninstalls through `uninstallPluginInScope`.
3
+ * When the scope's settings.json turns the plugin on but Claude Code's registry
4
+ * holds its row at ANOTHER scope, `claude plugin uninstall --scope <scope>`
5
+ * refuses with "is installed in <other> scope, not <scope>" and changes
6
+ * nothing. In a profiled project that is the normal state of a PROJECT enable:
7
+ * the profile render wrote it, so the profile is what must drop it. Where no
8
+ * profile owns the enable, the refusal must reach the user in magus's terms,
9
+ * never as Claude Code's raw text.
10
+ *
11
+ * Real files, real reconcile: a temp project, HOME pointed at a temp dir. Only
12
+ * the `claude` CLI is doubled, and its uninstall refuses the way Claude Code
13
+ * does, touching no file.
14
+ */
15
+
16
+ import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
17
+ import { mkdtemp, rm } from "node:fs/promises";
18
+ import { tmpdir } from "node:os";
19
+ import { join } from "node:path";
20
+ import fs from "fs-extra";
21
+ import { readManifest, writeManifest } from "../services/manifest.js";
22
+ import { activateProfile } from "../services/profile-activate.js";
23
+ import {
24
+ type PluginApplyDeps,
25
+ REAL_PLUGIN_DEPS,
26
+ REAL_REMOVE_DEPS,
27
+ uninstallPluginInScope,
28
+ } from "../services/update-engine.js";
29
+ import type { ProfileManifest } from "../types/index.js";
30
+
31
+ type Scope = "user" | "project" | "local";
32
+
33
+ let project: string;
34
+ let home: string;
35
+ let log: ReturnType<typeof spyOn>;
36
+ const saved = {
37
+ HOME: process.env.HOME,
38
+ CLAUDE_CONFIG_DIR: process.env.CLAUDE_CONFIG_DIR,
39
+ };
40
+
41
+ function manifest(): ProfileManifest {
42
+ return {
43
+ version: 2,
44
+ profiles: {
45
+ team: {
46
+ name: "Team",
47
+ plugins: { "dev@magus": "latest", "multimodel@magus": "latest" },
48
+ },
49
+ },
50
+ };
51
+ }
52
+
53
+ /** The settings.json `claude plugin … --scope <scope>` reads and writes. */
54
+ function settingsFor(scope: Scope): string {
55
+ if (scope === "user") return join(home, ".claude", "settings.json");
56
+ if (scope === "local") return join(project, ".claude", "settings.local.json");
57
+ return join(project, ".claude", "settings.json");
58
+ }
59
+
60
+ async function setEnabled(scope: Scope, pluginId: string): Promise<void> {
61
+ const file = settingsFor(scope);
62
+ const json = (await fs.pathExists(file))
63
+ ? ((await fs.readJson(file)) as Record<string, unknown>)
64
+ : {};
65
+ json.enabledPlugins = {
66
+ ...((json.enabledPlugins as Record<string, boolean>) ?? {}),
67
+ [pluginId]: true,
68
+ };
69
+ await fs.outputFile(file, JSON.stringify(json));
70
+ }
71
+
72
+ async function enabledIn(scope: Scope): Promise<Record<string, boolean>> {
73
+ const file = settingsFor(scope);
74
+ if (!(await fs.pathExists(file))) return {};
75
+ const json = (await fs.readJson(file)) as {
76
+ enabledPlugins?: Record<string, boolean>;
77
+ };
78
+ return json.enabledPlugins ?? {};
79
+ }
80
+
81
+ async function profilePlugins(): Promise<Record<string, unknown>> {
82
+ const { profiles } = await readManifest(project);
83
+ return (profiles.team?.plugins ?? {}) as Record<string, unknown>;
84
+ }
85
+
86
+ /** Claude Code's refusal when the registry row is at `rowAt`, not `asked`. */
87
+ function wrongScopeRefusal(pluginId: string, rowAt: Scope, asked: Scope) {
88
+ return new Error(
89
+ `✘ Failed to uninstall plugin "${pluginId}": Plugin "${pluginId}" is installed in ${rowAt} scope, not ${asked}. Use --scope ${rowAt} to uninstall.`,
90
+ );
91
+ }
92
+
93
+ /**
94
+ * The real engine deps with only the `claude` CLI and the version read-back
95
+ * doubled. `uninstall` is the given behaviour; it never touches a file.
96
+ */
97
+ function deps(
98
+ uninstall: (pluginId: string, scope: Scope) => Promise<void>,
99
+ ): PluginApplyDeps {
100
+ const {
101
+ loadedVersion: _loaded,
102
+ registryRows: _rows,
103
+ ...real
104
+ } = REAL_PLUGIN_DEPS;
105
+ return {
106
+ ...real,
107
+ install: async () => {
108
+ throw new Error("install must not run here");
109
+ },
110
+ update: async () => {},
111
+ repair: async () => {},
112
+ readInstalled: async () => "1.0.0",
113
+ saveInstalled: async () => {},
114
+ repairStaleLoad: async () => {
115
+ throw new Error("repairStaleLoad must not run here");
116
+ },
117
+ remove: { ...REAL_REMOVE_DEPS, uninstall },
118
+ };
119
+ }
120
+
121
+ /** Claude Code holding the plugin's only registry row at `rowAt`. */
122
+ function rowOnlyAt(rowAt: Scope): PluginApplyDeps {
123
+ return deps(async (pluginId, scope) => {
124
+ if (scope !== rowAt) throw wrongScopeRefusal(pluginId, rowAt, scope);
125
+ });
126
+ }
127
+
128
+ /** Claude Code's older refusal, which names no scope the plugin is at. */
129
+ function notInstalledRefusal(pluginId: string, asked: Scope) {
130
+ return new Error(
131
+ `✘ Failed to uninstall plugin "${pluginId}": Plugin "${pluginId}" is not installed in ${asked} scope.`,
132
+ );
133
+ }
134
+
135
+ async function rejection(promise: Promise<unknown>): Promise<string> {
136
+ try {
137
+ await promise;
138
+ } catch (error) {
139
+ return error instanceof Error ? error.message : String(error);
140
+ }
141
+ throw new Error("expected the uninstall to reject, but it resolved");
142
+ }
143
+
144
+ beforeEach(async () => {
145
+ project = await mkdtemp(join(tmpdir(), "scope-wrong-scope-"));
146
+ home = await mkdtemp(join(tmpdir(), "scope-wrong-scope-home-"));
147
+ process.env.HOME = home;
148
+ process.env.CLAUDE_CONFIG_DIR = join(home, ".claude");
149
+ log = spyOn(console, "log").mockImplementation(() => {});
150
+ });
151
+
152
+ afterEach(async () => {
153
+ log.mockRestore();
154
+ process.env.HOME = saved.HOME;
155
+ if (saved.CLAUDE_CONFIG_DIR === undefined) {
156
+ // biome-ignore lint/performance/noDelete: an unset env var must be absent, not "undefined"
157
+ delete process.env.CLAUDE_CONFIG_DIR;
158
+ } else process.env.CLAUDE_CONFIG_DIR = saved.CLAUDE_CONFIG_DIR;
159
+ await rm(project, { recursive: true, force: true });
160
+ await rm(home, { recursive: true, force: true });
161
+ });
162
+
163
+ describe("a scope uninstall Claude Code refuses as the wrong scope", () => {
164
+ test("PROJECT, enabled by the active profile: the profile drops it and the project no longer enables it", async () => {
165
+ await writeManifest(manifest(), project);
166
+ await activateProfile(manifest(), "team", project, {
167
+ onDisplace: () => {},
168
+ });
169
+ expect(await enabledIn("project")).toHaveProperty("multimodel@magus", true);
170
+
171
+ await uninstallPluginInScope(
172
+ "multimodel@magus",
173
+ "project",
174
+ project,
175
+ rowOnlyAt("user"),
176
+ "team",
177
+ );
178
+
179
+ expect((await enabledIn("project"))["multimodel@magus"]).not.toBe(true);
180
+ expect(await profilePlugins()).not.toHaveProperty("multimodel@magus");
181
+ });
182
+
183
+ test("PROJECT, enabled with no profile to own it: refused in magus's terms, naming USER", async () => {
184
+ await setEnabled("project", "multimodel@magus");
185
+
186
+ const message = await rejection(
187
+ uninstallPluginInScope(
188
+ "multimodel@magus",
189
+ "project",
190
+ project,
191
+ rowOnlyAt("user"),
192
+ ),
193
+ );
194
+
195
+ expect(message).toMatch(/USER/);
196
+ expect(message).not.toContain("Failed to uninstall plugin");
197
+ expect(message).not.toContain("--scope");
198
+ });
199
+
200
+ test("USER, enabled while the only row is PROJECT: refused in magus's terms, naming PROJECT", async () => {
201
+ await writeManifest(manifest(), project);
202
+ await activateProfile(manifest(), "team", project, {
203
+ onDisplace: () => {},
204
+ });
205
+ await setEnabled("user", "multimodel@magus");
206
+ const userBefore = await fs.readFile(settingsFor("user"), "utf8");
207
+
208
+ const message = await rejection(
209
+ uninstallPluginInScope(
210
+ "multimodel@magus",
211
+ "user",
212
+ project,
213
+ rowOnlyAt("project"),
214
+ "team",
215
+ ),
216
+ );
217
+
218
+ expect(message).toMatch(/PROJECT/);
219
+ expect(message).not.toContain("Failed to uninstall plugin");
220
+ expect(message).not.toContain("--scope");
221
+ expect(await fs.readFile(settingsFor("user"), "utf8")).toBe(userBefore);
222
+ expect(await profilePlugins()).toHaveProperty("multimodel@magus");
223
+ });
224
+
225
+ test("PROJECT, any other CLI failure: rejected with its own text, the profile untouched", async () => {
226
+ await writeManifest(manifest(), project);
227
+ await activateProfile(manifest(), "team", project, {
228
+ onDisplace: () => {},
229
+ });
230
+ const failing = deps(async () => {
231
+ throw new Error("claude timed out");
232
+ });
233
+
234
+ const message = await rejection(
235
+ uninstallPluginInScope(
236
+ "multimodel@magus",
237
+ "project",
238
+ project,
239
+ failing,
240
+ "team",
241
+ ),
242
+ );
243
+
244
+ expect(message).toBe("claude timed out");
245
+ expect(await profilePlugins()).toHaveProperty("multimodel@magus");
246
+ expect(await enabledIn("project")).toHaveProperty("multimodel@magus", true);
247
+ });
248
+
249
+ test("PROJECT, the older not-installed wording, no profile to own it: refused in magus's terms, naming PROJECT", async () => {
250
+ await setEnabled("project", "multimodel@magus");
251
+ const olderWording = deps(async (pluginId, scope) => {
252
+ throw notInstalledRefusal(pluginId, scope);
253
+ });
254
+
255
+ const message = await rejection(
256
+ uninstallPluginInScope(
257
+ "multimodel@magus",
258
+ "project",
259
+ project,
260
+ olderWording,
261
+ ),
262
+ );
263
+
264
+ expect(message).toMatch(/PROJECT/);
265
+ expect(message).not.toContain("Failed to uninstall plugin");
266
+ expect(message).not.toContain("--scope");
267
+ });
268
+
269
+ test("LOCAL, enabled while the only row is USER: refused naming USER, the local settings untouched", async () => {
270
+ await setEnabled("local", "multimodel@magus");
271
+ const localBefore = await fs.readFile(settingsFor("local"), "utf8");
272
+
273
+ const message = await rejection(
274
+ uninstallPluginInScope(
275
+ "multimodel@magus",
276
+ "local",
277
+ project,
278
+ rowOnlyAt("user"),
279
+ ),
280
+ );
281
+
282
+ expect(message).toMatch(/USER/);
283
+ expect(message).not.toContain("Failed to uninstall plugin");
284
+ expect(message).not.toContain("--scope");
285
+ expect(await fs.readFile(settingsFor("local"), "utf8")).toBe(localBefore);
286
+ });
287
+
288
+ test("USER, not enabled there while PROJECT installs it: resolves, the profile still lists it and the project still enables it", async () => {
289
+ await writeManifest(manifest(), project);
290
+ await activateProfile(manifest(), "team", project, {
291
+ onDisplace: () => {},
292
+ });
293
+ expect(await enabledIn("user")).not.toHaveProperty("multimodel@magus");
294
+
295
+ await uninstallPluginInScope(
296
+ "multimodel@magus",
297
+ "user",
298
+ project,
299
+ rowOnlyAt("project"),
300
+ "team",
301
+ );
302
+
303
+ expect(await profilePlugins()).toHaveProperty("multimodel@magus");
304
+ expect(await enabledIn("project")).toHaveProperty("multimodel@magus", true);
305
+ expect(await enabledIn("user")).not.toHaveProperty("multimodel@magus");
306
+ });
307
+
308
+ test("PROJECT, the profile lists it but the enable survives the drop: refused saying the profile no longer lists it, naming USER", async () => {
309
+ // The profile file lists the plugin; the remove double models a drop
310
+ // that succeeds while the PROJECT enable stays on.
311
+ await writeManifest(manifest(), project);
312
+ const base = rowOnlyAt("user");
313
+ const remove = base.remove;
314
+ if (!remove) throw new Error("the harness deps carry remove");
315
+ const surviving: PluginApplyDeps = {
316
+ ...base,
317
+ recordInProfile: async () => {},
318
+ remove: {
319
+ ...remove,
320
+ dependents: undefined,
321
+ dropFromProfile: async () => true,
322
+ isInstalled: async (_id, scope) => scope === "project",
323
+ },
324
+ };
325
+
326
+ const message = await rejection(
327
+ uninstallPluginInScope(
328
+ "multimodel@magus",
329
+ "project",
330
+ project,
331
+ surviving,
332
+ "team",
333
+ ),
334
+ );
335
+
336
+ expect(message).toContain("no longer in the profile");
337
+ expect(message).toMatch(/USER/);
338
+ expect(message).not.toContain("Failed to uninstall plugin");
339
+ expect(message).not.toContain("--scope");
340
+ });
341
+ });
package/src/cli/router.ts CHANGED
@@ -305,7 +305,8 @@ Usage: magus Open the interactive TUI
305
305
  Options:
306
306
  -v, --version Show version and check for updates
307
307
  -h, --help Show this help message
308
- --no-refresh Skip auto-refresh of marketplaces on startup
308
+ --no-refresh Skip adding, at startup, marketplaces this project uses
309
+ but this machine lacks
309
310
  --theme light|dark Force the palette. Otherwise: MAGUS_THEME, then
310
311
  TERM_THEME, then a 250 ms terminal query, then COLORFGBG,
311
312
  then dark. Only the exact words "light"/"dark" count.
@@ -373,9 +374,9 @@ Other commands:
373
374
  upgrade Update magus ITSELF to the latest version
374
375
 
375
376
  Navigation (TUI):
376
- [1] Plugins [4] Settings [7] Git State
377
+ [1] Plugins [4] Settings [7] Git State [0] Models
377
378
  [2] Skills [5] Profiles [8] Alias
378
- [3] MCP [6] CLI Tools
379
+ [3] MCP [6] CLI Tools [9] Styles
379
380
 
380
381
  Keys:
381
382
  ↑/↓ or j/k Navigate
@@ -288,6 +288,19 @@ export function isNotInstalledAnswer(error: unknown): boolean {
288
288
  );
289
289
  }
290
290
 
291
+ /**
292
+ * The scope a wrong-scope answer says the plugin IS installed at — `user` from
293
+ * `is installed in user scope, not project` — or null when the answer names
294
+ * none, as the `not installed in <scope> scope` wording does.
295
+ */
296
+ export function installedScopeFromAnswer(error: unknown): PluginScope | null {
297
+ const message = error instanceof Error ? error.message : String(error);
298
+ const match = /is installed in (user|project|local) scope, not /i.exec(
299
+ message,
300
+ );
301
+ return match ? (match[1].toLowerCase() as PluginScope) : null;
302
+ }
303
+
291
304
  /**
292
305
  * Repair a plugin whose files drifted from the marketplace under an unchanged
293
306
  * version — uninstall, then install again at the same scope.
@@ -43,6 +43,7 @@ import { selectProfileId } from "./active-closure.js";
43
43
  import type { PluginScope } from "./claude-cli.js";
44
44
  import {
45
45
  installPlugin,
46
+ installedScopeFromAnswer,
46
47
  isNotInstalledAnswer,
47
48
  repairPlugin,
48
49
  uninstallPluginViaCli,
@@ -1508,7 +1509,25 @@ export async function uninstallPluginInScope(
1508
1509
  }
1509
1510
  }
1510
1511
 
1511
- await remove.uninstall(pluginId, scope, projectPath);
1512
+ try {
1513
+ await remove.uninstall(pluginId, scope, projectPath);
1514
+ } catch (error) {
1515
+ if (!isNotInstalledAnswer(error)) throw error;
1516
+ await settleScopeWithNoInstall(
1517
+ pluginId,
1518
+ scope,
1519
+ projectPath,
1520
+ deps,
1521
+ error,
1522
+ profile,
1523
+ );
1524
+ if (scope === "project") {
1525
+ await remove
1526
+ .forgetVersion?.(pluginId, scope, projectPath)
1527
+ .catch(() => {});
1528
+ return;
1529
+ }
1530
+ }
1512
1531
  if (await remove.isInstalled(pluginId, scope, projectPath)) {
1513
1532
  throw new Error(`still installed at ${scope.toUpperCase()} scope`);
1514
1533
  }
@@ -1531,6 +1550,83 @@ export async function uninstallPluginInScope(
1531
1550
  await recordInProfileOrExplain(deps, projectPath, done);
1532
1551
  }
1533
1552
 
1553
+ /**
1554
+ * The half of {@link uninstallPluginInScope} that runs when Claude Code answers
1555
+ * that it holds no install of the plugin at `scope` here, so its uninstall had
1556
+ * nothing to remove.
1557
+ *
1558
+ * At PROJECT the setting that turns the plugin on is the profile render's, so
1559
+ * the profile drops it and the live files are brought back in line with the
1560
+ * profile. USER and LOCAL settings belong to no profile, so nothing is changed
1561
+ * there. Either way the scope is read back afterwards: still installed means
1562
+ * nothing magus owns turns it on, and the refusal names the scope Claude Code
1563
+ * reported, in magus's terms rather than the CLI's text.
1564
+ */
1565
+ async function settleScopeWithNoInstall(
1566
+ pluginId: string,
1567
+ scope: PluginScope,
1568
+ projectPath: string,
1569
+ deps: PluginApplyDeps,
1570
+ answer: unknown,
1571
+ profile: string | null | undefined,
1572
+ ): Promise<void> {
1573
+ const remove = deps.remove;
1574
+ if (!remove) throw new Error("magus cannot remove plugins from here");
1575
+ const name = pluginName(pluginId);
1576
+ const here = scope.toUpperCase();
1577
+
1578
+ let wasListed = false;
1579
+ if (scope === "project") {
1580
+ wasListed = await profileListsPlugin(pluginId, projectPath, profile);
1581
+ if (!(await remove.dropFromProfile(pluginId, projectPath, profile))) {
1582
+ throw new Error(`${name} is still in the profile at ${here} scope`);
1583
+ }
1584
+ await recordInProfileOrExplain(
1585
+ deps,
1586
+ projectPath,
1587
+ `${name} is no longer in the profile`,
1588
+ );
1589
+ }
1590
+
1591
+ if (!(await remove.isInstalled(pluginId, scope, projectPath))) return;
1592
+ // The profile was changed above, so the refusal says so: the user must not
1593
+ // find a plugin gone from the profile with nothing telling them why.
1594
+ const lead = wasListed
1595
+ ? `${name} is no longer in the profile, but it is`
1596
+ : `${name} is`;
1597
+ const installedAt = installedScopeFromAnswer(answer);
1598
+ if (installedAt && installedAt !== scope) {
1599
+ const there = installedAt.toUpperCase();
1600
+ throw new Error(
1601
+ `${lead}${wasListed ? " still" : ""} installed at ${there} scope, not ${here} — remove it from ${there}`,
1602
+ );
1603
+ }
1604
+ throw new Error(
1605
+ `${lead} not installed at ${here} scope in this project — remove it from the scope that installed it`,
1606
+ );
1607
+ }
1608
+
1609
+ /**
1610
+ * Whether `profile` lists the plugin, the profile picked as
1611
+ * `removePluginFromProfile` picks it: undefined means the active one.
1612
+ */
1613
+ async function profileListsPlugin(
1614
+ pluginId: string,
1615
+ projectPath: string,
1616
+ profile: string | null | undefined,
1617
+ ): Promise<boolean> {
1618
+ const manifest = await readManifest(projectPath);
1619
+ const id =
1620
+ profile === undefined
1621
+ ? selectProfileId(
1622
+ Object.keys(manifest.profiles),
1623
+ undefined,
1624
+ await activeProfile(projectPath),
1625
+ )
1626
+ : profile;
1627
+ return id ? profileLists(manifest.profiles[id], pluginId) : false;
1628
+ }
1629
+
1534
1630
  // -- binaries ----------------------------------------------------------------
1535
1631
 
1536
1632
  /**
@@ -789,28 +789,15 @@ export function PluginsScreen() {
789
789
 
790
790
  const handleShowTeamConfigHelp = async () => {
791
791
  const helpText =
792
- "TEAM CONFIGURATION FOR MARKETPLACES\n\n" +
793
- "To configure marketplaces for your entire team:\n\n" +
794
- "1. Add to .claude/settings.json (committed to git):\n\n" +
795
- " {\n" +
796
- ' "extraKnownMarketplaces": {\n' +
797
- ' "company-tools": {\n' +
798
- ' "source": {\n' +
799
- ' "source": "github",\n' +
800
- ' "repo": "your-org/claude-plugins"\n' +
801
- " }\n" +
802
- " }\n" +
803
- " },\n" +
804
- ' "enabledPlugins": {\n' +
805
- ' "code-formatter@company-tools": true\n' +
806
- " }\n" +
807
- " }\n\n" +
808
- "2. Team members get prompted to install when they trust the folder\n\n" +
809
- "3. Marketplaces are GLOBAL ONLY (managed by Claude Code)\n" +
810
- " - Not project-specific\n" +
811
- " - All team members share the same marketplace cache\n\n" +
812
- "NOTE: Individual plugin enablement is still project-specific.\n" +
813
- 'Use "Project" scope in magus to enable for the team.';
792
+ "TEAM CONFIGURATION\n\n" +
793
+ "To give your whole team the same plugins:\n\n" +
794
+ "1. Install each plugin at PROJECT scope (p).\n" +
795
+ " magus records it in this project's profile, together with\n" +
796
+ " where its marketplace lives.\n\n" +
797
+ "2. Commit .claude/profiles.json.\n\n" +
798
+ "3. Teammates run magus install (a fresh clone) or magus update.\n" +
799
+ " magus adds the marketplaces and installs the plugins.\n\n" +
800
+ "Your company's own marketplace: magus edit.";
814
801
 
815
802
  await modal.message("Team Configuration", helpText);
816
803
  };