magus-cli 7.12.2 → 7.14.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/README.md +21 -0
- package/package.json +4 -4
- package/src/__tests__/market-bases.test.ts +112 -0
- package/src/__tests__/market-config.test.ts +84 -0
- package/src/__tests__/market-conflicts.test.ts +175 -0
- package/src/__tests__/market-consent.test.ts +185 -0
- package/src/__tests__/market-content.test.ts +183 -0
- package/src/__tests__/market-delivery.test.ts +308 -0
- package/src/__tests__/market-fixture.ts +1167 -0
- package/src/__tests__/market-git.test.ts +130 -0
- package/src/__tests__/market-identity.test.ts +127 -0
- package/src/__tests__/market-install.test.ts +143 -0
- package/src/__tests__/market-layout.test.ts +206 -0
- package/src/__tests__/market-publish.test.ts +263 -0
- 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-readonly.test.ts +162 -0
- package/src/__tests__/market-repositories.test.ts +263 -0
- package/src/__tests__/market-results.test.ts +198 -0
- package/src/__tests__/market-router.test.ts +65 -0
- package/src/__tests__/market-safety.test.ts +357 -0
- package/src/__tests__/market-teammates.test.ts +304 -0
- package/src/cli/install.ts +64 -5
- package/src/cli/market-view.ts +329 -0
- package/src/cli/market.ts +664 -0
- package/src/cli/router.ts +69 -3
- package/src/cli/update-view.ts +56 -0
- package/src/cli/update.ts +111 -9
- package/src/data/marketplaces.ts +8 -2
- package/src/main.tsx +20 -2
- package/src/market-tui.tsx +21 -0
- package/src/services/active-closure.ts +71 -9
- package/src/services/claude-cli.ts +140 -10
- package/src/services/claude-settings.ts +19 -25
- package/src/services/doctor.ts +5 -5
- package/src/services/git-write.ts +615 -0
- package/src/services/github-cli.ts +309 -0
- package/src/services/local-marketplace.ts +112 -50
- package/src/services/market-bases.ts +191 -0
- package/src/services/market-change.ts +242 -0
- package/src/services/market-clone.ts +572 -0
- package/src/services/market-fs.ts +138 -0
- package/src/services/market-install.ts +172 -0
- package/src/services/market-layout.ts +311 -0
- package/src/services/market-local.ts +93 -0
- package/src/services/market-marker.ts +49 -0
- package/src/services/market-ops-content.ts +277 -0
- package/src/services/market-ops-plugin.ts +655 -0
- package/src/services/market-ops-publish.ts +1227 -0
- package/src/services/market-ops-repo.ts +916 -0
- package/src/services/market-ops-skill.ts +788 -0
- package/src/services/market-ops.ts +686 -0
- package/src/services/market-publish.ts +468 -0
- package/src/services/market-registry.ts +382 -0
- package/src/services/market-types.ts +268 -0
- package/src/services/marketplace-conflicts.ts +127 -0
- package/src/services/marketplace-refresh.ts +33 -3
- package/src/services/marketplace-sync.ts +253 -22
- package/src/services/plugin-dependencies.ts +17 -0
- package/src/services/plugin-manager.ts +1 -1
- package/src/services/plugin-mcp-config.ts +6 -9
- package/src/services/plugin-requires.ts +20 -8
- package/src/services/profile-absorb.ts +88 -3
- package/src/services/profile-adopt.ts +10 -1
- package/src/services/profiles.ts +64 -1
- package/src/services/resolver.ts +17 -5
- package/src/services/settings-manager.ts +2 -2
- package/src/services/skills-manager.ts +2 -2
- package/src/services/update-plan.ts +67 -8
- package/src/tui-runtime.tsx +142 -0
- package/src/tui.tsx +7 -124
- package/src/types/index.ts +7 -0
- package/src/ui/adapters/marketAdapter.ts +366 -0
- package/src/ui/adapters/pluginsAdapter.ts +45 -3
- package/src/ui/components/layout/ScreenLayout.tsx +43 -17
- package/src/ui/components/modals/ConfirmModal.tsx +145 -8
- package/src/ui/components/modals/HelpOverlay.tsx +10 -3
- package/src/ui/components/modals/ModalContainer.tsx +47 -5
- package/src/ui/components/primitives/ListCategoryRow.tsx +22 -2
- package/src/ui/components/primitives/ScopeBadge.tsx +28 -0
- package/src/ui/components/primitives/ScopeVersionLine.tsx +27 -0
- package/src/ui/components/primitives/SelectableRow.tsx +14 -1
- package/src/ui/components/primitives/ToneLine.tsx +25 -0
- package/src/ui/components/primitives/index.ts +5 -0
- package/src/ui/keymap.ts +21 -5
- package/src/ui/market/MarketApp.tsx +59 -0
- package/src/ui/market/MarketplaceScreen.tsx +280 -0
- package/src/ui/market/MarketplacesDetail.snowflake.tsx +161 -0
- package/src/ui/market/MarketplacesScreen.tsx +264 -0
- package/src/ui/market/PluginDetail.snowflake.tsx +61 -0
- package/src/ui/market/SkillDetail.snowflake.tsx +71 -0
- package/src/ui/market/marketKeymap.ts +243 -0
- package/src/ui/market/useMarketState.ts +694 -0
- package/src/ui/renderers/pluginRenderers.tsx +31 -3
- package/src/ui/renderers/pluginStatusText.ts +6 -0
- package/src/ui/screens/PluginsScreen.tsx +49 -5
- package/src/ui/screens/SkillsScreen.tsx +2 -2
- package/src/ui/state/AppContext.tsx +14 -2
- package/src/ui/state/types.ts +9 -2
- package/src/utils/config-dir.ts +29 -9
- package/src/utils/display-path.ts +33 -0
|
@@ -0,0 +1,916 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* market-ops-repo.ts — plans for the marketplace itself: `create`, `connect`
|
|
3
|
+
* and `remove`.
|
|
4
|
+
*
|
|
5
|
+
* Each planner is read-only. The one write a plan may make is `connect`'s
|
|
6
|
+
* shallow clone into a new directory under the OS temp dir, for a repository
|
|
7
|
+
* gh cannot read; it is removed before the plan returns.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import {
|
|
13
|
+
addMarketplace,
|
|
14
|
+
listMarketplaces,
|
|
15
|
+
listPlugins,
|
|
16
|
+
marketplaceAddHint,
|
|
17
|
+
removeMarketplace,
|
|
18
|
+
} from "./claude-cli.js";
|
|
19
|
+
import {
|
|
20
|
+
type RemoteHead,
|
|
21
|
+
catFile,
|
|
22
|
+
cloneShallowTemp,
|
|
23
|
+
initWithRemote,
|
|
24
|
+
lsRemoteHead,
|
|
25
|
+
push,
|
|
26
|
+
} from "./git-write.js";
|
|
27
|
+
import {
|
|
28
|
+
authStatus,
|
|
29
|
+
ghAvailable,
|
|
30
|
+
isRepoSlug,
|
|
31
|
+
repoCreate,
|
|
32
|
+
repoExists,
|
|
33
|
+
repoFileRaw,
|
|
34
|
+
repoPermissions,
|
|
35
|
+
} from "./github-cli.js";
|
|
36
|
+
import { getManifestPath, readManifest } from "./manifest.js";
|
|
37
|
+
import { forgetBasesFor } from "./market-bases.js";
|
|
38
|
+
import {
|
|
39
|
+
type ChangeSpec,
|
|
40
|
+
changeSteps,
|
|
41
|
+
cloneOf,
|
|
42
|
+
deliveryTarget,
|
|
43
|
+
linkRefusal,
|
|
44
|
+
readJson,
|
|
45
|
+
targetLine,
|
|
46
|
+
writeJson,
|
|
47
|
+
} from "./market-change.js";
|
|
48
|
+
import { cloneUrl, gitError, workingClonePath } from "./market-clone.js";
|
|
49
|
+
import { refuseLinkUnder, writeFileUnder } from "./market-fs.js";
|
|
50
|
+
import { validPackageName } from "./market-layout.js";
|
|
51
|
+
import { readMarker, withMarker } from "./market-marker.js";
|
|
52
|
+
import { type MarketCommand, computePlanId, planError } from "./market-ops.js";
|
|
53
|
+
import {
|
|
54
|
+
isGithubSource,
|
|
55
|
+
listCompanyMarketplaces,
|
|
56
|
+
normaliseSource,
|
|
57
|
+
readTrust,
|
|
58
|
+
registeredSource,
|
|
59
|
+
sameSource,
|
|
60
|
+
trust,
|
|
61
|
+
untrust,
|
|
62
|
+
} from "./market-registry.js";
|
|
63
|
+
import {
|
|
64
|
+
MarketError,
|
|
65
|
+
type MarketPlan,
|
|
66
|
+
type MarketStep,
|
|
67
|
+
type PlanAnswer,
|
|
68
|
+
} from "./market-types.js";
|
|
69
|
+
import { fastForwardCloneIfPresent } from "./marketplace-refresh.js";
|
|
70
|
+
|
|
71
|
+
const MANIFEST = ".claude-plugin/marketplace.json";
|
|
72
|
+
const README = "README.md";
|
|
73
|
+
|
|
74
|
+
/** The longest company name a marketplace may carry, in characters. */
|
|
75
|
+
export const COMPANY_NAME_MAX = 64;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check a company name for `owner.name`: one line, not blank, at most
|
|
79
|
+
* {@link COMPANY_NAME_MAX} characters once trimmed. Returns it trimmed; a
|
|
80
|
+
* bad one is a usage error (exit 2).
|
|
81
|
+
*/
|
|
82
|
+
export function checkCompanyName(value: string): string {
|
|
83
|
+
const company = value.trim();
|
|
84
|
+
if (company === "") {
|
|
85
|
+
throw new MarketError(
|
|
86
|
+
"invalid-name",
|
|
87
|
+
"The company name is empty",
|
|
88
|
+
"pass --company <name>",
|
|
89
|
+
2,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
// Every control character (C0, DEL, C1 — NEL included) and the Unicode
|
|
93
|
+
// line and paragraph separators.
|
|
94
|
+
if (/[\p{Cc}\u2028\u2029]/u.test(company)) {
|
|
95
|
+
throw new MarketError(
|
|
96
|
+
"invalid-name",
|
|
97
|
+
"The company name must be a single line",
|
|
98
|
+
"pass --company <name>",
|
|
99
|
+
2,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if ([...company].length > COMPANY_NAME_MAX) {
|
|
103
|
+
throw new MarketError(
|
|
104
|
+
"invalid-name",
|
|
105
|
+
`The company name is longer than ${COMPANY_NAME_MAX} characters`,
|
|
106
|
+
"pass a shorter --company <name>",
|
|
107
|
+
2,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return company;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function companyLine(company: string): string {
|
|
114
|
+
return `Company name: ${company} (magus shows the marketplace as "by ${company}")`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function newManifest(name: string, company: string, description?: string) {
|
|
118
|
+
return withMarker({
|
|
119
|
+
name,
|
|
120
|
+
owner: { name: company },
|
|
121
|
+
metadata: {
|
|
122
|
+
description: description ?? `${name} company plugin marketplace`,
|
|
123
|
+
},
|
|
124
|
+
plugins: [],
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function readme(name: string, repo: string): string {
|
|
129
|
+
return `# ${name}
|
|
130
|
+
|
|
131
|
+
The ${name} company plugin marketplace for Claude Code, managed with \`magus market\`.
|
|
132
|
+
|
|
133
|
+
Add it on a machine with:
|
|
134
|
+
|
|
135
|
+
\`\`\`
|
|
136
|
+
claude plugin marketplace add ${repo}
|
|
137
|
+
\`\`\`
|
|
138
|
+
`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Name checks shared by create and connect. */
|
|
142
|
+
async function checkNameFree(name: string, source: string): Promise<boolean> {
|
|
143
|
+
const registered = await registeredSource(name);
|
|
144
|
+
if (registered && !sameSource(registered, source)) {
|
|
145
|
+
throw new MarketError(
|
|
146
|
+
"name-taken",
|
|
147
|
+
`A marketplace named ${name} is already on this machine, from ${registered}`,
|
|
148
|
+
"pick another name with --name, or remove the other one first",
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
const trusted = (await readTrust()).marketplaces[name];
|
|
152
|
+
if (trusted && !sameSource(trusted.source, source)) {
|
|
153
|
+
throw new MarketError(
|
|
154
|
+
"name-taken",
|
|
155
|
+
`${name} is already a company marketplace on this machine, from ${trusted.source}`,
|
|
156
|
+
"pick another name with --name",
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return registered !== null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Register at USER scope through Claude Code, then read the registration back. */
|
|
163
|
+
function registerStep(name: string, claudeSource: string): MarketStep {
|
|
164
|
+
return {
|
|
165
|
+
id: "register",
|
|
166
|
+
label: `added ${name} to this machine`,
|
|
167
|
+
outside: true,
|
|
168
|
+
async run() {
|
|
169
|
+
try {
|
|
170
|
+
await addMarketplace(claudeSource, "user");
|
|
171
|
+
} catch (error) {
|
|
172
|
+
const hint = marketplaceAddHint(error);
|
|
173
|
+
throw new MarketError(
|
|
174
|
+
hint ? "gh-auth" : "other",
|
|
175
|
+
`Claude Code could not add ${name}: ${error instanceof Error ? error.message : String(error)}`,
|
|
176
|
+
hint ?? undefined,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const rows = await listMarketplaces();
|
|
180
|
+
if (!rows.some((r) => r.name === name)) {
|
|
181
|
+
throw new MarketError(
|
|
182
|
+
"other",
|
|
183
|
+
`Claude Code added the marketplace, but not under the name ${name}`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
return undefined;
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function trustStep(
|
|
192
|
+
name: string,
|
|
193
|
+
source: string,
|
|
194
|
+
via: "create" | "connect",
|
|
195
|
+
): MarketStep {
|
|
196
|
+
return {
|
|
197
|
+
id: "trust",
|
|
198
|
+
label: `${name} is a company marketplace on this machine`,
|
|
199
|
+
outside: true,
|
|
200
|
+
async run() {
|
|
201
|
+
const r = await trust(name, source, via);
|
|
202
|
+
if (r === "conflict") {
|
|
203
|
+
throw new MarketError(
|
|
204
|
+
"name-taken",
|
|
205
|
+
`${name} is already trusted from another source`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
return r === "exists" ? { skipped: true } : undefined;
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Write a new marketplace into an empty working clone and commit it. */
|
|
214
|
+
function initSteps(
|
|
215
|
+
name: string,
|
|
216
|
+
source: string,
|
|
217
|
+
company: string,
|
|
218
|
+
branch: string,
|
|
219
|
+
description: string | undefined,
|
|
220
|
+
): MarketStep[] {
|
|
221
|
+
const state = {};
|
|
222
|
+
return [
|
|
223
|
+
{
|
|
224
|
+
id: "init",
|
|
225
|
+
label: "wrote the marketplace",
|
|
226
|
+
outside: false,
|
|
227
|
+
async run(ctx) {
|
|
228
|
+
const dir = cloneOf(ctx);
|
|
229
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
230
|
+
const r = await initWithRemote(dir, cloneUrl(source), branch);
|
|
231
|
+
if (!r.ok) throw gitError(r, name);
|
|
232
|
+
writeJson(dir, MANIFEST, newManifest(name, company, description), name);
|
|
233
|
+
writeFileUnder(dir, README, readme(name, source), linkRefusal(name));
|
|
234
|
+
return undefined;
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
...changeSteps({
|
|
238
|
+
label: name,
|
|
239
|
+
source,
|
|
240
|
+
branch,
|
|
241
|
+
target: "push-or-pr",
|
|
242
|
+
stage: [MANIFEST, README],
|
|
243
|
+
files: [
|
|
244
|
+
{ path: MANIFEST, change: "added" },
|
|
245
|
+
{ path: README, change: "added" },
|
|
246
|
+
],
|
|
247
|
+
message: `Create the ${name} marketplace`,
|
|
248
|
+
slug: name,
|
|
249
|
+
prBody: "",
|
|
250
|
+
state,
|
|
251
|
+
}).filter((s) => s.id !== "deliver"),
|
|
252
|
+
{
|
|
253
|
+
id: "push",
|
|
254
|
+
label: `pushed to ${source}`,
|
|
255
|
+
outside: true,
|
|
256
|
+
async run(ctx) {
|
|
257
|
+
const r = await push(cloneOf(ctx), branch, { setUpstream: true });
|
|
258
|
+
if (!r.ok) throw gitError(r, name);
|
|
259
|
+
return undefined;
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Every step after the repository exists says how to finish by hand. */
|
|
266
|
+
function afterCreate(step: MarketStep, repo: string): MarketStep {
|
|
267
|
+
return {
|
|
268
|
+
...step,
|
|
269
|
+
async run(ctx) {
|
|
270
|
+
try {
|
|
271
|
+
return await step.run(ctx);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
const e =
|
|
274
|
+
error instanceof MarketError
|
|
275
|
+
? error
|
|
276
|
+
: new MarketError("other", String(error));
|
|
277
|
+
throw new MarketError(
|
|
278
|
+
e.code,
|
|
279
|
+
`${repo} was created on GitHub, but ${e.message}`,
|
|
280
|
+
`run \`magus market connect ${repo}\` to finish`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* For a marketplace already on this machine: fast-forward Claude Code's copy
|
|
289
|
+
* after the connect, then check magus now finds it as a company marketplace.
|
|
290
|
+
* magus discovers company marketplaces by the marker in that copy, so a copy
|
|
291
|
+
* left behind would leave the connect invisible. A change that went out as a
|
|
292
|
+
* pull request has nothing on the branch yet: the marker arrives when it is
|
|
293
|
+
* merged.
|
|
294
|
+
*/
|
|
295
|
+
function discoveryStep(
|
|
296
|
+
name: string,
|
|
297
|
+
action: "init" | "add" | "mark" | "register",
|
|
298
|
+
delivered: ChangeSpec["state"],
|
|
299
|
+
): MarketStep {
|
|
300
|
+
return {
|
|
301
|
+
id: "refresh",
|
|
302
|
+
label: `brought Claude Code's copy of ${name} up to date`,
|
|
303
|
+
outside: true,
|
|
304
|
+
async run() {
|
|
305
|
+
if ((action === "mark" || action === "add") && !delivered.pushed) {
|
|
306
|
+
return {
|
|
307
|
+
skipped: true,
|
|
308
|
+
detail: "the marker arrives with the pull request",
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
const r = await fastForwardCloneIfPresent(name);
|
|
312
|
+
const found = (await listCompanyMarketplaces()).some(
|
|
313
|
+
(m) => m.name === name,
|
|
314
|
+
);
|
|
315
|
+
if (found) return r === "refreshed" ? undefined : { skipped: true };
|
|
316
|
+
throw new MarketError(
|
|
317
|
+
"other",
|
|
318
|
+
r === "skipped"
|
|
319
|
+
? `${name} is connected, but Claude Code's copy of it has local changes, so magus cannot see the marker yet`
|
|
320
|
+
: `${name} is connected, but Claude Code's copy of it could not be brought up to date, so magus cannot see the marker yet`,
|
|
321
|
+
r === "skipped"
|
|
322
|
+
? "discard those changes, then run `magus update`"
|
|
323
|
+
: "run `magus update`",
|
|
324
|
+
);
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ─── create ──────────────────────────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
export interface CreateArgs {
|
|
332
|
+
repo: string;
|
|
333
|
+
visibility: "private" | "public";
|
|
334
|
+
name?: string;
|
|
335
|
+
description?: string;
|
|
336
|
+
/**
|
|
337
|
+
* The company name written as the manifest's `owner.name`; the GitHub
|
|
338
|
+
* owner of `repo` when absent.
|
|
339
|
+
*/
|
|
340
|
+
company?: string;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* A gh failure before anything was created: a host gh could not reach is a
|
|
345
|
+
* network problem, never a sign-in one.
|
|
346
|
+
*/
|
|
347
|
+
function ghCreateError(r: { failure: string; message: string }): MarketError {
|
|
348
|
+
if (r.failure === "no-network" || r.failure === "timeout") {
|
|
349
|
+
return new MarketError(
|
|
350
|
+
"no-network",
|
|
351
|
+
"Could not reach GitHub — nothing was created",
|
|
352
|
+
"check your network connection, then run the command again",
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
if (r.failure === "gh-auth") {
|
|
356
|
+
return new MarketError(
|
|
357
|
+
"gh-auth",
|
|
358
|
+
"Not signed in to GitHub",
|
|
359
|
+
"run `gh auth login`, then `gh auth setup-git`",
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
return new MarketError("other", r.message);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export async function planCreate(args: CreateArgs): Promise<PlanAnswer> {
|
|
366
|
+
const command: MarketCommand = "market create";
|
|
367
|
+
try {
|
|
368
|
+
if (!isRepoSlug(args.repo)) {
|
|
369
|
+
throw new MarketError(
|
|
370
|
+
"invalid-name",
|
|
371
|
+
`${args.repo} is not a GitHub owner/repo`,
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
const [owner = "", repoName = ""] = args.repo.split("/");
|
|
375
|
+
const company = checkCompanyName(args.company ?? owner);
|
|
376
|
+
const name = args.name ?? repoName.toLowerCase();
|
|
377
|
+
if (!validPackageName(name)) {
|
|
378
|
+
throw new MarketError(
|
|
379
|
+
"invalid-name",
|
|
380
|
+
`${name} cannot be a marketplace name (lower case letters, digits and dashes)`,
|
|
381
|
+
"pass --name <name>",
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
const auth = await authStatus();
|
|
385
|
+
if (!auth.ok) {
|
|
386
|
+
if (auth.failure === "gh-missing") {
|
|
387
|
+
throw new MarketError(
|
|
388
|
+
"gh-missing",
|
|
389
|
+
"gh is needed to create a GitHub repository",
|
|
390
|
+
"install gh, then run `gh auth login`",
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
throw ghCreateError(auth);
|
|
394
|
+
}
|
|
395
|
+
const exists = await repoExists(args.repo);
|
|
396
|
+
if (!exists.ok) throw ghCreateError(exists);
|
|
397
|
+
if (exists.value) {
|
|
398
|
+
throw new MarketError(
|
|
399
|
+
"name-taken",
|
|
400
|
+
`${args.repo} already exists on GitHub`,
|
|
401
|
+
`use \`magus market connect ${args.repo}\``,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
const source = normaliseSource(args.repo);
|
|
405
|
+
if (await checkNameFree(name, source)) {
|
|
406
|
+
throw new MarketError(
|
|
407
|
+
"name-taken",
|
|
408
|
+
`A marketplace named ${name} is already on this machine`,
|
|
409
|
+
"pick another name with --name",
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
const branch = "main";
|
|
413
|
+
const steps: MarketStep[] = [
|
|
414
|
+
{
|
|
415
|
+
id: "repo",
|
|
416
|
+
label: `created ${args.repo} on GitHub`,
|
|
417
|
+
outside: true,
|
|
418
|
+
async run() {
|
|
419
|
+
const r = await repoCreate(
|
|
420
|
+
args.repo,
|
|
421
|
+
args.visibility,
|
|
422
|
+
args.description,
|
|
423
|
+
);
|
|
424
|
+
if (!r.ok) {
|
|
425
|
+
throw new MarketError(
|
|
426
|
+
r.failure === "gh-auth" ? "gh-auth" : "other",
|
|
427
|
+
`GitHub did not create ${args.repo}: ${r.message}`,
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
return undefined;
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
...[
|
|
434
|
+
...initSteps(name, source, company, branch, args.description),
|
|
435
|
+
registerStep(name, source),
|
|
436
|
+
trustStep(name, source, "create"),
|
|
437
|
+
].map((s) => afterCreate(s, args.repo)),
|
|
438
|
+
];
|
|
439
|
+
const plan: MarketPlan = {
|
|
440
|
+
command,
|
|
441
|
+
lines: [
|
|
442
|
+
`Creates the ${args.visibility} GitHub repository ${args.repo}`,
|
|
443
|
+
`Writes a company marketplace named ${name} into it and pushes it to ${branch}`,
|
|
444
|
+
companyLine(company),
|
|
445
|
+
`Adds ${name} to this machine at USER scope and marks it as a company marketplace here`,
|
|
446
|
+
],
|
|
447
|
+
planId: computePlanId({
|
|
448
|
+
op: "create",
|
|
449
|
+
...args,
|
|
450
|
+
name,
|
|
451
|
+
company,
|
|
452
|
+
branch,
|
|
453
|
+
}),
|
|
454
|
+
summary: { marketplace: name },
|
|
455
|
+
steps: steps.map((s) =>
|
|
456
|
+
s.id === "init" ||
|
|
457
|
+
s.id === "validate" ||
|
|
458
|
+
s.id === "commit" ||
|
|
459
|
+
s.id === "push"
|
|
460
|
+
? {
|
|
461
|
+
...s,
|
|
462
|
+
run: (ctx) => s.run({ ...ctx, clone: workingClonePath(name) }),
|
|
463
|
+
}
|
|
464
|
+
: s,
|
|
465
|
+
),
|
|
466
|
+
sync: null,
|
|
467
|
+
lock: name,
|
|
468
|
+
replan: () => planCreate(args),
|
|
469
|
+
doneMessage: `${name} is ready on GitHub and on this machine. ${teammateNote(name)}`,
|
|
470
|
+
};
|
|
471
|
+
return { kind: "plan", plan };
|
|
472
|
+
} catch (error) {
|
|
473
|
+
return planError(command, error);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function teammateNote(name: string): string {
|
|
478
|
+
return `For teammates: install one of its plugins at PROJECT scope in a project (or save a project profile in magus), then commit .claude/profiles.json; their \`magus update\` adds ${name}.`;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ─── connect ─────────────────────────────────────────────────────────────────
|
|
482
|
+
|
|
483
|
+
export interface ConnectArgs {
|
|
484
|
+
source: string;
|
|
485
|
+
name?: string;
|
|
486
|
+
/**
|
|
487
|
+
* The company name for a manifest connect writes (`init` and `add` only);
|
|
488
|
+
* the GitHub owner of the source when absent. A repository that already
|
|
489
|
+
* holds a marketplace keeps the `owner.name` its manifest gives.
|
|
490
|
+
*/
|
|
491
|
+
company?: string;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function readRemoteManifest(
|
|
495
|
+
source: string,
|
|
496
|
+
head: RemoteHead,
|
|
497
|
+
): Promise<unknown> {
|
|
498
|
+
if (!head.commit) return null;
|
|
499
|
+
let text: string | null = null;
|
|
500
|
+
let read = false;
|
|
501
|
+
if (isGithubSource(source) && (await ghAvailable())) {
|
|
502
|
+
const r = await repoFileRaw(source, MANIFEST, head.branch ?? undefined);
|
|
503
|
+
if (r.ok) {
|
|
504
|
+
text = r.value;
|
|
505
|
+
read = true;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (!read) {
|
|
509
|
+
const temp = await cloneShallowTemp(cloneUrl(source));
|
|
510
|
+
if (!temp.ok) throw gitError(temp, source);
|
|
511
|
+
try {
|
|
512
|
+
const r = await catFile(temp.value.dir, `HEAD:${MANIFEST}`);
|
|
513
|
+
text = r.ok ? r.value : null;
|
|
514
|
+
} finally {
|
|
515
|
+
temp.value.dispose();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (text === null) return null;
|
|
519
|
+
try {
|
|
520
|
+
return JSON.parse(text) as unknown;
|
|
521
|
+
} catch {
|
|
522
|
+
throw new MarketError(
|
|
523
|
+
"other",
|
|
524
|
+
`${source} holds a marketplace manifest that is not valid JSON`,
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export async function planConnect(
|
|
530
|
+
args: ConnectArgs,
|
|
531
|
+
cwd: string,
|
|
532
|
+
): Promise<PlanAnswer> {
|
|
533
|
+
const command: MarketCommand = "market connect";
|
|
534
|
+
try {
|
|
535
|
+
// The value's form is checked before any GitHub or git work.
|
|
536
|
+
if (args.company !== undefined) checkCompanyName(args.company);
|
|
537
|
+
const source = normaliseSource(args.source, cwd);
|
|
538
|
+
const github = isGithubSource(source);
|
|
539
|
+
const url = cloneUrl(source);
|
|
540
|
+
const head = await lsRemoteHead(url);
|
|
541
|
+
if (!head.ok) throw gitError(head, source);
|
|
542
|
+
const manifest = await readRemoteManifest(source, head.value);
|
|
543
|
+
// No manifest in an empty repository: a new history. No manifest in one
|
|
544
|
+
// with commits: the marketplace is committed on top of its history.
|
|
545
|
+
const action =
|
|
546
|
+
manifest === null
|
|
547
|
+
? head.value.commit === null
|
|
548
|
+
? "init"
|
|
549
|
+
: "add"
|
|
550
|
+
: readMarker(manifest)
|
|
551
|
+
? "register"
|
|
552
|
+
: "mark";
|
|
553
|
+
const fresh = action === "init" || action === "add";
|
|
554
|
+
if (args.name !== undefined && !fresh) {
|
|
555
|
+
throw new MarketError(
|
|
556
|
+
"other",
|
|
557
|
+
"--name applies only when connect sets up a repository with no marketplace in it; Claude Code registers this one under the name its manifest gives",
|
|
558
|
+
"run it again without --name",
|
|
559
|
+
2,
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
if (args.company !== undefined && !fresh) {
|
|
563
|
+
throw new MarketError(
|
|
564
|
+
"other",
|
|
565
|
+
"--company applies only when connect sets up a repository with no marketplace in it; this one keeps the company name its manifest gives",
|
|
566
|
+
"run it again without --company",
|
|
567
|
+
2,
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
const base = path
|
|
571
|
+
.basename(source)
|
|
572
|
+
.replace(/\.git$/, "")
|
|
573
|
+
.toLowerCase();
|
|
574
|
+
const name = fresh
|
|
575
|
+
? (args.name ?? base)
|
|
576
|
+
: String((manifest as { name?: unknown }).name ?? "");
|
|
577
|
+
if (!validPackageName(name)) {
|
|
578
|
+
throw new MarketError(
|
|
579
|
+
"invalid-name",
|
|
580
|
+
`${name || "(none)"} cannot be a marketplace name (lower case letters, digits and dashes)`,
|
|
581
|
+
fresh ? "pass --name <name>" : undefined,
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
const registered = await checkNameFree(name, source);
|
|
585
|
+
const trusted = (await readTrust()).marketplaces[name] !== undefined;
|
|
586
|
+
// Registered and trusted is not yet connected: magus finds a company
|
|
587
|
+
// marketplace by the marker in Claude Code's copy. A connect whose
|
|
588
|
+
// discovery failed, or a marker pull request merged since, leaves that
|
|
589
|
+
// copy behind; then the plan below brings it up to date instead.
|
|
590
|
+
const discovered =
|
|
591
|
+
registered &&
|
|
592
|
+
trusted &&
|
|
593
|
+
(await listCompanyMarketplaces()).some((m) => m.name === name);
|
|
594
|
+
if (action === "register" && discovered) {
|
|
595
|
+
return {
|
|
596
|
+
kind: "result",
|
|
597
|
+
result: {
|
|
598
|
+
command,
|
|
599
|
+
outcome: "unchanged",
|
|
600
|
+
message: `${name} is already a company marketplace on this machine`,
|
|
601
|
+
marketplace: name,
|
|
602
|
+
steps: [],
|
|
603
|
+
},
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
if (action !== "register" && github && (await ghAvailable())) {
|
|
607
|
+
const can = await repoPermissions(source);
|
|
608
|
+
if (!can.ok || !can.value) {
|
|
609
|
+
throw new MarketError(
|
|
610
|
+
"no-access",
|
|
611
|
+
`Your account cannot write to ${source}, and connecting it needs a commit`,
|
|
612
|
+
"ask its owner for write access",
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
const branch = head.value.branch ?? "main";
|
|
617
|
+
const claudeSource = github
|
|
618
|
+
? source
|
|
619
|
+
: args.source.includes("://")
|
|
620
|
+
? args.source
|
|
621
|
+
: source;
|
|
622
|
+
const steps: MarketStep[] = [];
|
|
623
|
+
const lines: string[] = [];
|
|
624
|
+
const company = fresh
|
|
625
|
+
? checkCompanyName(
|
|
626
|
+
args.company ?? (github ? (source.split("/")[0] ?? name) : name),
|
|
627
|
+
)
|
|
628
|
+
: null;
|
|
629
|
+
const delivered: ChangeSpec["state"] = {};
|
|
630
|
+
if (action === "init") {
|
|
631
|
+
lines.push(
|
|
632
|
+
`${source} holds no marketplace yet: writes a company marketplace named ${name} into it and pushes it to ${branch}`,
|
|
633
|
+
companyLine(company as string),
|
|
634
|
+
);
|
|
635
|
+
steps.push(
|
|
636
|
+
...initSteps(name, source, company as string, branch, undefined).map(
|
|
637
|
+
(s) => ({
|
|
638
|
+
...s,
|
|
639
|
+
run: (ctx: Parameters<MarketStep["run"]>[0]) =>
|
|
640
|
+
s.run({ ...ctx, clone: workingClonePath(name) }),
|
|
641
|
+
}),
|
|
642
|
+
),
|
|
643
|
+
);
|
|
644
|
+
} else if (action === "mark" || action === "add") {
|
|
645
|
+
const target = await deliveryTarget(source, name);
|
|
646
|
+
lines.push(
|
|
647
|
+
action === "mark"
|
|
648
|
+
? `${name} is a marketplace without the company marker: commits the marker to its manifest`
|
|
649
|
+
: `${source} holds no marketplace yet: commits a company marketplace named ${name} on top of its ${branch} history, leaving its files as they are`,
|
|
650
|
+
...(action === "add" ? [companyLine(company as string)] : []),
|
|
651
|
+
targetLine(target, branch),
|
|
652
|
+
);
|
|
653
|
+
const state = delivered;
|
|
654
|
+
steps.push(
|
|
655
|
+
{
|
|
656
|
+
id: "marker",
|
|
657
|
+
label:
|
|
658
|
+
action === "mark"
|
|
659
|
+
? "added the company marker"
|
|
660
|
+
: "wrote the marketplace",
|
|
661
|
+
outside: false,
|
|
662
|
+
async run(ctx) {
|
|
663
|
+
const clone = cloneOf(ctx);
|
|
664
|
+
// The repository's own history can commit a link at
|
|
665
|
+
// `.claude-plugin` or the manifest: refuse before reading
|
|
666
|
+
// or writing through it.
|
|
667
|
+
refuseLinkUnder(clone, MANIFEST, linkRefusal(name));
|
|
668
|
+
const file = path.join(clone, MANIFEST);
|
|
669
|
+
if (action === "add") {
|
|
670
|
+
if (fs.existsSync(file)) {
|
|
671
|
+
throw new MarketError(
|
|
672
|
+
"plan-changed",
|
|
673
|
+
`${source} gained a marketplace while this ran — nothing was pushed`,
|
|
674
|
+
"run the command again to see the new plan",
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
writeJson(
|
|
678
|
+
clone,
|
|
679
|
+
MANIFEST,
|
|
680
|
+
newManifest(name, company as string, undefined),
|
|
681
|
+
name,
|
|
682
|
+
);
|
|
683
|
+
return undefined;
|
|
684
|
+
}
|
|
685
|
+
const current = readJson(clone, MANIFEST, name);
|
|
686
|
+
writeJson(clone, MANIFEST, withMarker(current), name);
|
|
687
|
+
return undefined;
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
...changeSteps({
|
|
691
|
+
label: name,
|
|
692
|
+
source,
|
|
693
|
+
branch,
|
|
694
|
+
target,
|
|
695
|
+
stage: [MANIFEST],
|
|
696
|
+
files: [
|
|
697
|
+
{
|
|
698
|
+
path: MANIFEST,
|
|
699
|
+
change: action === "mark" ? "modified" : "added",
|
|
700
|
+
},
|
|
701
|
+
],
|
|
702
|
+
message:
|
|
703
|
+
action === "mark"
|
|
704
|
+
? `Mark ${name} as a company marketplace`
|
|
705
|
+
: `Create the ${name} marketplace`,
|
|
706
|
+
slug: `${name}-company-marker`,
|
|
707
|
+
prBody:
|
|
708
|
+
action === "mark"
|
|
709
|
+
? `Adds the \`metadata.magus\` marker, so \`magus market\` treats ${name} as a company marketplace.`
|
|
710
|
+
: `Adds a company marketplace manifest, so \`magus market\` treats ${name} as a company marketplace.`,
|
|
711
|
+
state,
|
|
712
|
+
}).map((s) =>
|
|
713
|
+
s.id === "deliver"
|
|
714
|
+
? {
|
|
715
|
+
...s,
|
|
716
|
+
run: async (ctx: Parameters<MarketStep["run"]>[0]) => {
|
|
717
|
+
const out = await s.run(ctx);
|
|
718
|
+
if (!out?.stop) return out;
|
|
719
|
+
// A marker pull request still connects: the manifest is on
|
|
720
|
+
// the branch and the marker arrives when it is merged. A new
|
|
721
|
+
// manifest is not on the branch yet, so Claude Code has
|
|
722
|
+
// nothing to add until the pull request is merged.
|
|
723
|
+
if (action === "mark") return { ...out, stop: false };
|
|
724
|
+
return {
|
|
725
|
+
...out,
|
|
726
|
+
patch: {
|
|
727
|
+
...out.patch,
|
|
728
|
+
message: `${out.patch?.message ?? "Opened a pull request"}. After it is merged, run \`magus market connect ${args.source}\` to add ${name} to this machine`,
|
|
729
|
+
},
|
|
730
|
+
};
|
|
731
|
+
},
|
|
732
|
+
}
|
|
733
|
+
: s,
|
|
734
|
+
),
|
|
735
|
+
);
|
|
736
|
+
} else {
|
|
737
|
+
lines.push(`${name} is already a company marketplace`);
|
|
738
|
+
}
|
|
739
|
+
if (!registered) {
|
|
740
|
+
lines.push(
|
|
741
|
+
action === "add"
|
|
742
|
+
? `Adds ${name} to this machine at USER scope once the marketplace is on ${branch}; if it goes out as a pull request, that waits until it is merged`
|
|
743
|
+
: `Adds ${name} to this machine at USER scope`,
|
|
744
|
+
);
|
|
745
|
+
steps.push(registerStep(name, claudeSource));
|
|
746
|
+
}
|
|
747
|
+
if (!trusted) {
|
|
748
|
+
lines.push(`Marks ${name} as a company marketplace on this machine`);
|
|
749
|
+
}
|
|
750
|
+
steps.push(trustStep(name, source, "connect"));
|
|
751
|
+
if (registered) {
|
|
752
|
+
lines.push(
|
|
753
|
+
`Brings Claude Code's copy of ${name} up to date, so magus finds it as a company marketplace`,
|
|
754
|
+
);
|
|
755
|
+
steps.push(discoveryStep(name, action, delivered));
|
|
756
|
+
}
|
|
757
|
+
if (!github) {
|
|
758
|
+
lines.push(
|
|
759
|
+
"Works on this machine only: a project profile cannot record a repository outside GitHub for teammates",
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
const plan: MarketPlan = {
|
|
763
|
+
command,
|
|
764
|
+
lines,
|
|
765
|
+
planId: computePlanId({
|
|
766
|
+
op: "connect",
|
|
767
|
+
source,
|
|
768
|
+
commit: head.value.commit,
|
|
769
|
+
branch,
|
|
770
|
+
action,
|
|
771
|
+
name,
|
|
772
|
+
company,
|
|
773
|
+
registered,
|
|
774
|
+
}),
|
|
775
|
+
summary: { marketplace: name },
|
|
776
|
+
steps,
|
|
777
|
+
sync: action === "mark" || action === "add" ? { name, url } : null,
|
|
778
|
+
lock: name,
|
|
779
|
+
replan: () => planConnect(args, cwd),
|
|
780
|
+
doneMessage:
|
|
781
|
+
`${name} is connected as a company marketplace on this machine. ${github ? teammateNote(name) : ""}`.trim(),
|
|
782
|
+
};
|
|
783
|
+
return { kind: "plan", plan };
|
|
784
|
+
} catch (error) {
|
|
785
|
+
return planError(command, error);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// ─── remove ──────────────────────────────────────────────────────────────────
|
|
790
|
+
|
|
791
|
+
const SCOPE_WORD = {
|
|
792
|
+
user: "USER",
|
|
793
|
+
project: "PROJECT",
|
|
794
|
+
local: "LOCAL",
|
|
795
|
+
} as const;
|
|
796
|
+
|
|
797
|
+
async function profileNames(cwd: string, name: string): Promise<boolean> {
|
|
798
|
+
try {
|
|
799
|
+
if (!fs.existsSync(getManifestPath(cwd))) return false;
|
|
800
|
+
const manifest = await readManifest(cwd);
|
|
801
|
+
return Object.values(manifest.profiles).some(
|
|
802
|
+
(p) =>
|
|
803
|
+
(p.marketplaces && Object.hasOwn(p.marketplaces, name)) ||
|
|
804
|
+
Object.keys(p.plugins ?? {}).some((id) => id.endsWith(`@${name}`)),
|
|
805
|
+
);
|
|
806
|
+
} catch {
|
|
807
|
+
// An unreadable profile file only loses this one line of the plan.
|
|
808
|
+
return false;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
export async function planRemove(
|
|
813
|
+
name: string,
|
|
814
|
+
cwd: string,
|
|
815
|
+
): Promise<PlanAnswer> {
|
|
816
|
+
const command: MarketCommand = "market remove";
|
|
817
|
+
try {
|
|
818
|
+
const source = await registeredSource(name);
|
|
819
|
+
if (!source) {
|
|
820
|
+
throw new MarketError(
|
|
821
|
+
"not-found",
|
|
822
|
+
`${name} is not a marketplace on this machine`,
|
|
823
|
+
"`magus market list` shows the company marketplaces here",
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
if (!(await readTrust()).marketplaces[name]) {
|
|
827
|
+
throw new MarketError(
|
|
828
|
+
"not-company",
|
|
829
|
+
`${name} is not a company marketplace on this machine; magus market removes only those`,
|
|
830
|
+
"remove other marketplaces with `claude plugin marketplace remove`",
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
const installs = (await listPlugins(cwd))
|
|
834
|
+
.filter((p) => p.id.endsWith(`@${name}`))
|
|
835
|
+
.map((p) => ({
|
|
836
|
+
id: p.id,
|
|
837
|
+
scope: p.scope,
|
|
838
|
+
version: p.version ?? null,
|
|
839
|
+
project: p.projectPath ?? null,
|
|
840
|
+
}));
|
|
841
|
+
const describe = (i: (typeof installs)[number]) => {
|
|
842
|
+
const where = i.project ? ` in ${path.basename(i.project)}` : "";
|
|
843
|
+
const version = i.version ? ` ${i.version}` : "";
|
|
844
|
+
return `${i.id.split("@")[0]}${version} (${SCOPE_WORD[i.scope]}${where})`;
|
|
845
|
+
};
|
|
846
|
+
const lines = [
|
|
847
|
+
installs.length > 0
|
|
848
|
+
? `Removing ${name} from this machine uninstalls every plugin from it wherever it is installed: ${installs.map(describe).join(", ")} — and deletes their saved settings and data`
|
|
849
|
+
: `Removes ${name} from this machine; no plugin from it is installed`,
|
|
850
|
+
"The GitHub repository is not touched",
|
|
851
|
+
];
|
|
852
|
+
if (await profileNames(cwd, name)) {
|
|
853
|
+
lines.push(
|
|
854
|
+
`This project's profile lists ${name}: the next \`magus update\` here adds it back`,
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
const steps: MarketStep[] = [
|
|
858
|
+
{
|
|
859
|
+
id: "unregister",
|
|
860
|
+
label: `removed ${name} from Claude Code`,
|
|
861
|
+
outside: true,
|
|
862
|
+
async run() {
|
|
863
|
+
await removeMarketplace(name);
|
|
864
|
+
const rows = await listMarketplaces();
|
|
865
|
+
if (rows.some((r) => r.name === name)) {
|
|
866
|
+
throw new MarketError(
|
|
867
|
+
"other",
|
|
868
|
+
`Claude Code still lists ${name} after removing it`,
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
return undefined;
|
|
872
|
+
},
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
id: "untrust",
|
|
876
|
+
label: `${name} is no longer a company marketplace here`,
|
|
877
|
+
outside: true,
|
|
878
|
+
async run() {
|
|
879
|
+
return (await untrust(name)) ? undefined : { skipped: true };
|
|
880
|
+
},
|
|
881
|
+
},
|
|
882
|
+
{
|
|
883
|
+
id: "clone",
|
|
884
|
+
label: "removed magus's working copy",
|
|
885
|
+
outside: false,
|
|
886
|
+
async run() {
|
|
887
|
+
fs.rmSync(workingClonePath(name), { recursive: true, force: true });
|
|
888
|
+
return undefined;
|
|
889
|
+
},
|
|
890
|
+
},
|
|
891
|
+
{
|
|
892
|
+
id: "bases",
|
|
893
|
+
label: "forgot where local skill copies came from",
|
|
894
|
+
outside: true,
|
|
895
|
+
async run() {
|
|
896
|
+
forgetBasesFor(name);
|
|
897
|
+
return undefined;
|
|
898
|
+
},
|
|
899
|
+
},
|
|
900
|
+
];
|
|
901
|
+
const plan: MarketPlan = {
|
|
902
|
+
command,
|
|
903
|
+
lines,
|
|
904
|
+
planId: computePlanId({ op: "remove", name, source, installs }),
|
|
905
|
+
summary: { marketplace: name },
|
|
906
|
+
steps,
|
|
907
|
+
sync: null,
|
|
908
|
+
lock: name,
|
|
909
|
+
replan: () => planRemove(name, cwd),
|
|
910
|
+
doneMessage: `${name} is no longer on this machine. Takes effect in new sessions, or after /reload-plugins.`,
|
|
911
|
+
};
|
|
912
|
+
return { kind: "plan", plan };
|
|
913
|
+
} catch (error) {
|
|
914
|
+
return planError(command, error);
|
|
915
|
+
}
|
|
916
|
+
}
|