magus-cli 7.13.0 → 7.14.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/README.md +7 -4
- package/package.json +4 -4
- package/src/__tests__/market-r2-adapters.test.ts +190 -0
- package/src/__tests__/market-r2-company-cli.test.ts +350 -0
- package/src/__tests__/market-r2-company-unit.test.ts +224 -0
- package/src/__tests__/market-r2-local-items.test.ts +197 -0
- package/src/__tests__/market-r2-plugin-add.test.ts +470 -0
- package/src/__tests__/market-r2-plugin-publish.test.ts +568 -0
- package/src/__tests__/market-r2-routing.test.ts +127 -0
- package/src/__tests__/market-r2-skill-create-plugin.test.ts +315 -0
- package/src/__tests__/market-r2b-display-path.test.ts +180 -0
- package/src/__tests__/market-r2b-fit-row.test.ts +287 -0
- package/src/__tests__/market-r2b-local-items.test.ts +253 -0
- package/src/__tests__/market-r2b-plugin-add.test.ts +130 -0
- package/src/__tests__/market-router.test.ts +4 -4
- package/src/__tests__/scope-uninstall-wrong-scope.test.ts +341 -0
- package/src/cli/market-view.ts +59 -18
- package/src/cli/market.ts +128 -44
- package/src/cli/router.ts +42 -10
- package/src/data/marketplaces.ts +1 -0
- package/src/main.tsx +1 -1
- package/src/market-tui.tsx +1 -1
- package/src/services/claude-cli.ts +13 -0
- package/src/services/local-marketplace.ts +8 -0
- package/src/services/market-change.ts +6 -6
- package/src/services/market-local.ts +93 -0
- package/src/services/market-ops-plugin.ts +366 -47
- package/src/services/market-ops-publish.ts +363 -15
- package/src/services/market-ops-repo.ts +94 -13
- package/src/services/market-ops-skill.ts +93 -11
- package/src/services/market-ops.ts +72 -8
- package/src/services/market-publish.ts +10 -1
- package/src/services/market-types.ts +23 -0
- package/src/services/update-engine.ts +97 -1
- package/src/tui-runtime.tsx +2 -2
- package/src/types/index.ts +2 -0
- package/src/ui/adapters/marketAdapter.ts +123 -4
- package/src/ui/adapters/pluginsAdapter.ts +25 -1
- package/src/ui/components/primitives/ListCategoryRow.tsx +22 -2
- package/src/ui/market/MarketplaceScreen.tsx +55 -24
- package/src/ui/market/MarketplacesDetail.snowflake.tsx +60 -4
- package/src/ui/market/MarketplacesScreen.tsx +120 -29
- package/src/ui/market/PluginDetail.snowflake.tsx +12 -1
- package/src/ui/market/marketKeymap.ts +29 -2
- package/src/ui/market/useMarketState.ts +146 -5
- package/src/utils/display-path.ts +33 -0
|
@@ -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/market-view.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type {
|
|
11
|
+
LocalCopy,
|
|
11
12
|
MarketPlan,
|
|
12
13
|
MarketResult,
|
|
13
14
|
SkillStatusRow,
|
|
@@ -89,23 +90,24 @@ export function stepLine(step: StepRecord, label: string): string {
|
|
|
89
90
|
return fail(`${label}: ${step.detail ?? "failed"}`);
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
function
|
|
93
|
-
const copy = result.localCopy;
|
|
94
|
-
if (!copy) return [];
|
|
93
|
+
function copyLine(copy: LocalCopy): string {
|
|
95
94
|
const whose = copy.scope === "user" ? "your USER" : "this project's";
|
|
96
95
|
if (copy.action === "removed") {
|
|
97
|
-
return
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
),
|
|
101
|
-
];
|
|
96
|
+
return ok(
|
|
97
|
+
`removed ${whose} copy of ${bold(copy.name)}${copy.scope === "project" ? ` — commit the deletion (.claude/skills/${copy.name})` : ""}`,
|
|
98
|
+
);
|
|
102
99
|
}
|
|
103
100
|
if (copy.action === "created") {
|
|
104
|
-
return
|
|
101
|
+
return ok(`${whose} copy of ${bold(copy.name)} is ready to edit`);
|
|
105
102
|
}
|
|
103
|
+
return info(`kept ${whose} copy of ${bold(copy.name)}: ${copy.reason ?? ""}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function localCopyLine(result: MarketResult): string[] {
|
|
106
107
|
return [
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
...(result.localCopy ? [result.localCopy] : []),
|
|
109
|
+
...(result.localCopies ?? []),
|
|
110
|
+
].map(copyLine);
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
function installLines(result: MarketResult): string[] {
|
|
@@ -213,6 +215,22 @@ export function statusLines(result: MarketResult): string[] {
|
|
|
213
215
|
export function listLines(result: MarketResult): string[] {
|
|
214
216
|
const lines = headerFor("market list");
|
|
215
217
|
const rows = result.marketplaces ?? [];
|
|
218
|
+
const local = result.local ?? [];
|
|
219
|
+
const localLines =
|
|
220
|
+
local.length === 0
|
|
221
|
+
? []
|
|
222
|
+
: [
|
|
223
|
+
` ${bold("Here, in no marketplace")}`,
|
|
224
|
+
...local.map(
|
|
225
|
+
(l) => ` ${scopeBadge(l.scope)} ${bold(l.name)} ${dim(l.kind)}`,
|
|
226
|
+
),
|
|
227
|
+
note(
|
|
228
|
+
rows.length === 0
|
|
229
|
+
? "a company marketplace is needed before these can be added to one"
|
|
230
|
+
: "add one with `magus market skill add <skill> --plugin <p@m> --create-plugin` or `magus market plugin add <path>`",
|
|
231
|
+
),
|
|
232
|
+
"",
|
|
233
|
+
];
|
|
216
234
|
if (rows.length === 0) {
|
|
217
235
|
lines.push(info(result.message));
|
|
218
236
|
lines.push(
|
|
@@ -220,10 +238,13 @@ export function listLines(result: MarketResult): string[] {
|
|
|
220
238
|
"create one with `magus market create`, or `magus market connect` an existing one",
|
|
221
239
|
),
|
|
222
240
|
);
|
|
223
|
-
return [...lines, ""];
|
|
241
|
+
return [...lines, "", ...localLines];
|
|
224
242
|
}
|
|
225
243
|
for (const m of rows) {
|
|
226
|
-
|
|
244
|
+
const by = m.company ? ` by ${m.company}` : "";
|
|
245
|
+
lines.push(
|
|
246
|
+
` ${bold(m.name)}${by} ${dim(m.repo)} ${scopeBadge(m.scope)}`,
|
|
247
|
+
);
|
|
227
248
|
if (m.plugins.length === 0) lines.push(listItem(dim("no plugins yet")));
|
|
228
249
|
for (const p of m.plugins) {
|
|
229
250
|
lines.push(
|
|
@@ -235,7 +256,7 @@ export function listLines(result: MarketResult): string[] {
|
|
|
235
256
|
}
|
|
236
257
|
lines.push("");
|
|
237
258
|
}
|
|
238
|
-
return lines;
|
|
259
|
+
return [...lines, ...localLines];
|
|
239
260
|
}
|
|
240
261
|
|
|
241
262
|
/** `magus market --help`. */
|
|
@@ -244,23 +265,38 @@ export function marketHelp(): string {
|
|
|
244
265
|
|
|
245
266
|
Usage: magus market <command> [args]
|
|
246
267
|
|
|
268
|
+
The screen for all of this is \`magus edit\`; these commands run without one.
|
|
269
|
+
|
|
247
270
|
list Company marketplaces on this machine, with their
|
|
248
271
|
plugins and skills. Changes nothing.
|
|
249
272
|
fetch [<m>] Bring magus's copies of company marketplaces up to
|
|
250
273
|
date with GitHub. Changes nothing else.
|
|
251
|
-
create <owner>/<repo> --private|--public [--name <m>] [--
|
|
274
|
+
create <owner>/<repo> --private|--public [--name <m>] [--company <name>]
|
|
275
|
+
[--description <t>]
|
|
252
276
|
Create a GitHub repository holding a new company
|
|
253
277
|
marketplace and add it to this machine.
|
|
254
|
-
|
|
278
|
+
--company is the name magus shows it by ("by
|
|
279
|
+
<name>"); the GitHub owner when left out. One line,
|
|
280
|
+
at most 64 characters.
|
|
281
|
+
connect <owner/repo|git-url|path> [--name <m>] [--company <name>]
|
|
255
282
|
Use an existing repository as a company marketplace.
|
|
256
|
-
--name only when the repository holds
|
|
283
|
+
--name and --company only when the repository holds
|
|
284
|
+
no marketplace; otherwise its manifest names both.
|
|
257
285
|
remove <m> Remove a company marketplace from this machine,
|
|
258
286
|
uninstalling its plugins. The repository stays.
|
|
259
287
|
plugin create <p@m> [--description <t>]
|
|
260
288
|
plugin delete <p@m> Add or delete a plugin in the marketplace.
|
|
261
|
-
|
|
289
|
+
plugin add <path> [--to <m>] [--name <p>]
|
|
290
|
+
Copy a plugin folder of your own (one holding
|
|
291
|
+
.claude-plugin/plugin.json) into the marketplace
|
|
292
|
+
as a new plugin, at its own version (0.1.0 when it
|
|
293
|
+
has none). The folder stays where it is.
|
|
294
|
+
skill add <skill> --plugin <p@m> [--create-plugin [--description <t>]]
|
|
295
|
+
[--from project|global] [--keep-local]
|
|
262
296
|
Add a skill from this project (or your USER skills)
|
|
263
297
|
to a plugin, then use the plugin's version.
|
|
298
|
+
--create-plugin creates the plugin in the same
|
|
299
|
+
commit when the marketplace has none of that name.
|
|
264
300
|
skill edit <skill> [--plugin <p@m>] [--to project|global]
|
|
265
301
|
Copy a plugin's skill here to edit it.
|
|
266
302
|
skill remove <skill> --plugin <p@m>
|
|
@@ -274,6 +310,11 @@ Usage: magus market <command> [args]
|
|
|
274
310
|
then remove the local copy. --force replaces the
|
|
275
311
|
plugin's version when the copy's origin is unknown
|
|
276
312
|
or both sides changed. --dry-run shows the plan.
|
|
313
|
+
publish --plugin <p@m> [--keep-local] [--dry-run]
|
|
314
|
+
Send every skill of the plugin edited here as one
|
|
315
|
+
new patch version. A copy whose origin is unknown,
|
|
316
|
+
or that changed on both sides, is left out: publish
|
|
317
|
+
it on its own with --force.
|
|
277
318
|
|
|
278
319
|
@m may be left out when this machine has one company marketplace.
|
|
279
320
|
|