magus-cli 7.13.0 → 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.
Files changed (43) hide show
  1. package/README.md +7 -4
  2. package/package.json +4 -4
  3. package/src/__tests__/market-r2-adapters.test.ts +190 -0
  4. package/src/__tests__/market-r2-company-cli.test.ts +350 -0
  5. package/src/__tests__/market-r2-company-unit.test.ts +224 -0
  6. package/src/__tests__/market-r2-local-items.test.ts +197 -0
  7. package/src/__tests__/market-r2-plugin-add.test.ts +470 -0
  8. package/src/__tests__/market-r2-plugin-publish.test.ts +568 -0
  9. package/src/__tests__/market-r2-routing.test.ts +127 -0
  10. package/src/__tests__/market-r2-skill-create-plugin.test.ts +315 -0
  11. package/src/__tests__/market-r2b-display-path.test.ts +180 -0
  12. package/src/__tests__/market-r2b-fit-row.test.ts +287 -0
  13. package/src/__tests__/market-r2b-local-items.test.ts +253 -0
  14. package/src/__tests__/market-r2b-plugin-add.test.ts +130 -0
  15. package/src/__tests__/market-router.test.ts +4 -4
  16. package/src/cli/market-view.ts +59 -18
  17. package/src/cli/market.ts +128 -44
  18. package/src/cli/router.ts +42 -10
  19. package/src/data/marketplaces.ts +1 -0
  20. package/src/main.tsx +1 -1
  21. package/src/market-tui.tsx +1 -1
  22. package/src/services/local-marketplace.ts +8 -0
  23. package/src/services/market-change.ts +6 -6
  24. package/src/services/market-local.ts +93 -0
  25. package/src/services/market-ops-plugin.ts +366 -47
  26. package/src/services/market-ops-publish.ts +363 -15
  27. package/src/services/market-ops-repo.ts +94 -13
  28. package/src/services/market-ops-skill.ts +93 -11
  29. package/src/services/market-ops.ts +72 -8
  30. package/src/services/market-publish.ts +10 -1
  31. package/src/services/market-types.ts +23 -0
  32. package/src/tui-runtime.tsx +2 -2
  33. package/src/types/index.ts +2 -0
  34. package/src/ui/adapters/marketAdapter.ts +123 -4
  35. package/src/ui/adapters/pluginsAdapter.ts +25 -1
  36. package/src/ui/components/primitives/ListCategoryRow.tsx +22 -2
  37. package/src/ui/market/MarketplaceScreen.tsx +55 -24
  38. package/src/ui/market/MarketplacesDetail.snowflake.tsx +60 -4
  39. package/src/ui/market/MarketplacesScreen.tsx +120 -29
  40. package/src/ui/market/PluginDetail.snowflake.tsx +12 -1
  41. package/src/ui/market/marketKeymap.ts +29 -2
  42. package/src/ui/market/useMarketState.ts +146 -5
  43. package/src/utils/display-path.ts +33 -0
@@ -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 localCopyLine(result: MarketResult): string[] {
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
- ok(
99
- `removed ${whose} copy of ${bold(copy.name)}${copy.scope === "project" ? ` — commit the deletion (.claude/skills/${copy.name})` : ""}`,
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 [ok(`${whose} copy of ${bold(copy.name)} is ready to edit`)];
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
- info(`kept ${whose} copy of ${bold(copy.name)}: ${copy.reason ?? ""}`),
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
- lines.push(` ${bold(m.name)} ${dim(m.repo)} ${scopeBadge(m.scope)}`);
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>] [--description <t>]
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
- connect <owner/repo|git-url|path> [--name <m>]
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 no marketplace.
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
- skill add <skill> --plugin <p@m> [--from project|global] [--keep-local]
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
 
package/src/cli/market.ts CHANGED
@@ -7,17 +7,18 @@
7
7
  * only that plan. Under `--json` one MarketResult is printed, on the last
8
8
  * line of stdout, and nothing is asked.
9
9
  *
10
- * Bare `magus market` is the interactive screen; this module answers "tui"
11
- * for it and the router decides.
10
+ * Bare `magus market` prints the help; the interactive screen is `magus
11
+ * edit`.
12
12
  */
13
13
 
14
14
  import {
15
+ planPluginAdd,
15
16
  planPluginCreate,
16
17
  planPluginDelete,
17
18
  } from "../services/market-ops-plugin.js";
18
19
  import {
19
- type PublishArgs,
20
20
  marketStatus,
21
+ planPluginPublish,
21
22
  planPublish,
22
23
  provisionalPublish,
23
24
  } from "../services/market-ops-publish.js";
@@ -61,6 +62,7 @@ import { askYesNo, promptValue } from "./prompt.js";
61
62
  /** Flags that take a value, and flags that stand alone. */
62
63
  const VALUE_FLAGS = new Set([
63
64
  "--name",
65
+ "--company",
64
66
  "--description",
65
67
  "--plugin",
66
68
  "--from",
@@ -70,6 +72,7 @@ const BOOL_FLAGS = new Set([
70
72
  "--private",
71
73
  "--public",
72
74
  "--keep-local",
75
+ "--create-plugin",
73
76
  "--force",
74
77
  "--dry-run",
75
78
  "--json",
@@ -276,15 +279,15 @@ function warnBlock(text: string): string {
276
279
 
277
280
  /**
278
281
  * `publish --dry-run`: the plan, or the answer a publish would give, and
279
- * nothing written. On state older than GitHub's, a provisional plan with no
280
- * compare, still exit 0.
282
+ * nothing written. On state older than GitHub's, a provisional answer with
283
+ * no compare (`provisional`), still exit 0; without one, that error.
281
284
  */
282
285
  async function dryRunPublish(
283
- args: PublishArgs,
286
+ plan: () => Promise<PlanAnswer>,
284
287
  io: Io,
285
- cwd: string,
288
+ provisional?: (error: MarketError) => MarketResult,
286
289
  ): Promise<number> {
287
- const answer = await planPublish(args, cwd);
290
+ const answer = await plan();
288
291
  if (answer.kind === "plan") {
289
292
  const p = answer.plan;
290
293
  const result: MarketResult = {
@@ -307,8 +310,11 @@ async function dryRunPublish(
307
310
  emit(answer.result, io);
308
311
  return exitCodeFor(answer.result);
309
312
  }
310
- if (answer.error.code === "stale" || answer.error.code === "no-network") {
311
- emit(provisionalPublish(args, cwd, answer.error), io);
313
+ if (
314
+ provisional &&
315
+ (answer.error.code === "stale" || answer.error.code === "no-network")
316
+ ) {
317
+ emit(provisional(answer.error), io);
312
318
  return 0;
313
319
  }
314
320
  const result = errorResult(answer.command, answer.error);
@@ -316,15 +322,12 @@ async function dryRunPublish(
316
322
  return answer.error.exit ?? exitCodeFor(result);
317
323
  }
318
324
 
319
- /**
320
- * Run `magus market <argv>`. Returns the exit code, or "tui" for a bare
321
- * `magus market`.
322
- */
325
+ /** Run `magus market <argv>`. Returns the exit code. */
323
326
  export async function runMarketCommand(
324
327
  argv: string[],
325
328
  version: string,
326
329
  cwd: string = process.cwd(),
327
- ): Promise<number | "tui"> {
330
+ ): Promise<number> {
328
331
  if (argv.includes("--help") || argv.includes("-h")) {
329
332
  console.log(marketHelp());
330
333
  return 0;
@@ -333,7 +336,11 @@ export async function runMarketCommand(
333
336
  console.log(`magus v${version}`);
334
337
  return 0;
335
338
  }
336
- if (argv.length === 0) return "tui";
339
+ if (argv.length === 0) {
340
+ console.log(marketHelp());
341
+ console.log("Open the screen with `magus edit`.");
342
+ return 0;
343
+ }
337
344
 
338
345
  const json = argv.includes("--json");
339
346
  const io: Io = {
@@ -348,7 +355,7 @@ export async function runMarketCommand(
348
355
  switch (verb) {
349
356
  case "list": {
350
357
  expect(p, [], { min: 0, max: 0, usage: "magus market list [--json]" });
351
- const result = await marketList();
358
+ const result = await marketList(cwd);
352
359
  emit(result, io);
353
360
  return exitCodeFor(result);
354
361
  }
@@ -367,12 +374,16 @@ export async function runMarketCommand(
367
374
  }
368
375
  case "create": {
369
376
  const usage =
370
- "magus market create <owner>/<repo> --private|--public [--name <m>] [--description <t>]";
371
- expect(p, ["--private", "--public", "--name", "--description"], {
372
- min: 1,
373
- max: 1,
374
- usage,
375
- });
377
+ "magus market create <owner>/<repo> --private|--public [--name <m>] [--company <name>] [--description <t>]";
378
+ expect(
379
+ p,
380
+ ["--private", "--public", "--name", "--company", "--description"],
381
+ {
382
+ min: 1,
383
+ max: 1,
384
+ usage,
385
+ },
386
+ );
376
387
  const priv = p.flags.has("--private");
377
388
  const pub = p.flags.has("--public");
378
389
  if (priv === pub) {
@@ -386,6 +397,7 @@ export async function runMarketCommand(
386
397
  repo: p.positional[0] as string,
387
398
  visibility: priv ? "private" : "public",
388
399
  name: str(p, "--name"),
400
+ company: str(p, "--company"),
389
401
  description: str(p, "--description"),
390
402
  }),
391
403
  io,
@@ -394,15 +406,20 @@ export async function runMarketCommand(
394
406
  );
395
407
  }
396
408
  case "connect": {
397
- expect(p, ["--name"], {
409
+ expect(p, ["--name", "--company"], {
398
410
  min: 1,
399
411
  max: 1,
400
- usage: "magus market connect <owner/repo|git-url|path> [--name <m>]",
412
+ usage:
413
+ "magus market connect <owner/repo|git-url|path> [--name <m>] [--company <name>]",
401
414
  });
402
415
  return await runWrite(
403
416
  () =>
404
417
  planConnect(
405
- { source: p.positional[0] as string, name: str(p, "--name") },
418
+ {
419
+ source: p.positional[0] as string,
420
+ name: str(p, "--name"),
421
+ company: str(p, "--company"),
422
+ },
406
423
  cwd,
407
424
  ),
408
425
  io,
@@ -435,6 +452,27 @@ export async function runMarketCommand(
435
452
  cwd,
436
453
  );
437
454
  }
455
+ if (sub === "add") {
456
+ expect(sp, ["--to", "--name"], {
457
+ min: 1,
458
+ max: 1,
459
+ usage: "magus market plugin add <path> [--to <m>] [--name <p>]",
460
+ });
461
+ return await runWrite(
462
+ () =>
463
+ planPluginAdd(
464
+ {
465
+ path: ref as string,
466
+ to: str(p, "--to"),
467
+ name: str(p, "--name"),
468
+ },
469
+ cwd,
470
+ ),
471
+ io,
472
+ yes,
473
+ cwd,
474
+ );
475
+ }
438
476
  if (sub === "delete") {
439
477
  expect(sp, [], {
440
478
  min: 1,
@@ -448,22 +486,40 @@ export async function runMarketCommand(
448
486
  cwd,
449
487
  );
450
488
  }
451
- throw new UsageError("usage: magus market plugin create|delete <p@m>");
489
+ throw new UsageError(
490
+ "usage: magus market plugin create|delete <p@m>, or magus market plugin add <path>",
491
+ );
452
492
  }
453
493
  case "skill": {
454
494
  const [sub, skill, ...extra] = p.positional;
455
495
  const sp = { ...p, positional: skill ? [skill, ...extra] : [] };
456
496
  if (sub === "add") {
457
497
  const usage =
458
- "magus market skill add <skill> --plugin <p@m> [--from project|global] [--keep-local]";
459
- expect(sp, ["--plugin", "--from", "--keep-local"], {
460
- min: 1,
461
- max: 1,
462
- usage,
463
- });
498
+ "magus market skill add <skill> --plugin <p@m> [--create-plugin [--description <t>]] [--from project|global] [--keep-local]";
499
+ expect(
500
+ sp,
501
+ [
502
+ "--plugin",
503
+ "--from",
504
+ "--keep-local",
505
+ "--create-plugin",
506
+ "--description",
507
+ ],
508
+ {
509
+ min: 1,
510
+ max: 1,
511
+ usage,
512
+ },
513
+ );
464
514
  const plugin = str(p, "--plugin");
465
515
  if (!plugin)
466
516
  throw new UsageError(`--plugin is required — usage: ${usage}`);
517
+ const createPlugin = p.flags.has("--create-plugin");
518
+ if (p.flags.has("--description") && !createPlugin) {
519
+ throw new UsageError(
520
+ `--description describes a plugin --create-plugin creates — usage: ${usage}`,
521
+ );
522
+ }
467
523
  const from = choice(
468
524
  p,
469
525
  "--from",
@@ -478,6 +534,12 @@ export async function runMarketCommand(
478
534
  plugin,
479
535
  from,
480
536
  keepLocal: p.flags.has("--keep-local"),
537
+ ...(createPlugin
538
+ ? {
539
+ createPlugin: true,
540
+ description: str(p, "--description"),
541
+ }
542
+ : {}),
481
543
  },
482
544
  cwd,
483
545
  ),
@@ -538,25 +600,47 @@ export async function runMarketCommand(
538
600
  return exitCodeFor(result);
539
601
  }
540
602
  case "publish": {
603
+ const usage =
604
+ "magus market publish <skill> [--plugin <p@m>] [--from project|global] [--keep-local] [--force] [--dry-run], or magus market publish --plugin <p@m> [--keep-local] [--dry-run]";
541
605
  expect(
542
606
  p,
543
607
  ["--plugin", "--from", "--keep-local", "--force", "--dry-run"],
544
- {
545
- min: 1,
546
- max: 1,
547
- usage:
548
- "magus market publish <skill> [--plugin <p@m>] [--from project|global] [--keep-local] [--force] [--dry-run]",
549
- },
608
+ { min: 0, max: 1, usage },
550
609
  );
551
- const args: PublishArgs = {
552
- skill: p.positional[0] as string,
610
+ const skill = p.positional[0];
611
+ const keepLocal = p.flags.has("--keep-local");
612
+ if (skill === undefined) {
613
+ const plugin = str(p, "--plugin");
614
+ if (!plugin) {
615
+ throw new UsageError(
616
+ `name a skill, or a plugin with --plugin — usage: ${usage}`,
617
+ );
618
+ }
619
+ for (const flag of ["--from", "--force"]) {
620
+ if (p.flags.has(flag)) {
621
+ throw new UsageError(
622
+ `${flag} applies to one named skill: \`magus market publish <skill> ${flag} …\``,
623
+ );
624
+ }
625
+ }
626
+ const plan = () => planPluginPublish({ plugin, keepLocal }, cwd);
627
+ if (p.flags.has("--dry-run")) return await dryRunPublish(plan, io);
628
+ return await runWrite(plan, io, yes, cwd);
629
+ }
630
+ const args = {
631
+ skill,
553
632
  plugin: str(p, "--plugin"),
554
633
  from: choice(p, "--from", ["project", "global"] as const, "project"),
555
- keepLocal: p.flags.has("--keep-local"),
634
+ keepLocal,
556
635
  force: p.flags.has("--force"),
557
636
  };
558
- if (p.flags.has("--dry-run")) return await dryRunPublish(args, io, cwd);
559
- return await runWrite(() => planPublish(args, cwd), io, yes, cwd);
637
+ const plan = () => planPublish(args, cwd);
638
+ if (p.flags.has("--dry-run")) {
639
+ return await dryRunPublish(plan, io, (error) =>
640
+ provisionalPublish(args, cwd, error),
641
+ );
642
+ }
643
+ return await runWrite(plan, io, yes, cwd);
560
644
  }
561
645
  default:
562
646
  throw new UsageError(
package/src/cli/router.ts CHANGED
@@ -30,8 +30,8 @@ export interface RouteOutcome {
30
30
  theme?: ThemeMode;
31
31
  /**
32
32
  * Which interactive app to launch when `launchTui` is true: the dependency
33
- * TUI (bare `magus`, the default) or the company marketplace screens (bare
34
- * `magus market`).
33
+ * TUI (bare `magus`, the default) or the company marketplace screens
34
+ * (`magus edit`).
35
35
  */
36
36
  app?: "main" | "market";
37
37
  }
@@ -93,6 +93,21 @@ const HANDLED = (exitCode: number): RouteOutcome => ({
93
93
  });
94
94
  const LAUNCH_TUI: RouteOutcome = { handled: false, launchTui: true };
95
95
 
96
+ /** `magus edit --help`. */
97
+ function editHelp(): string {
98
+ return `magus edit — open the company marketplace screens
99
+
100
+ Usage: magus edit
101
+
102
+ Lists the company marketplaces on this machine, their plugins and skills, and
103
+ creates, connects and changes them: publishes edited skills (one skill, or a
104
+ whole plugin as one new version), and adds this project's and your USER skills
105
+ and plugin folders to a marketplace. Needs a terminal.
106
+
107
+ \`magus market --help\` lists the commands that do the same without one.
108
+ `;
109
+ }
110
+
96
111
  /**
97
112
  * Every name the switch below handles.
98
113
  *
@@ -108,6 +123,7 @@ const SUBCOMMANDS = new Set([
108
123
  "models",
109
124
  "doctor",
110
125
  "market",
126
+ "edit",
111
127
  ]);
112
128
 
113
129
  /**
@@ -130,10 +146,10 @@ export async function route(
130
146
  themeEnv?: ThemeEnv,
131
147
  ): Promise<RouteOutcome> {
132
148
  // Top-level flags take precedence over any command token — for every
133
- // command but `market`, whose own verbs answer --help and --version (and
134
- // write nothing doing so). `magus upgrade --help` must still print help
135
- // rather than upgrade, so this exception is exactly argv[0] === "market".
136
- const marketFirst = argv[0] === "market";
149
+ // command but `market` and `edit`, which answer --help and --version
150
+ // themselves (and write nothing doing so). `magus upgrade --help` must still
151
+ // print help rather than upgrade, so this exception is exactly argv[0].
152
+ const marketFirst = argv[0] === "market" || argv[0] === "edit";
137
153
  if (!marketFirst && (argv.includes("--version") || argv.includes("-v"))) {
138
154
  console.log(`magus v${version}`);
139
155
  const result = await checkForUpdates();
@@ -229,8 +245,23 @@ export async function route(
229
245
  return HANDLED(await runDoctorCommand(rest));
230
246
  case "market": {
231
247
  const { runMarketCommand } = await import("./market.js");
232
- const outcome = await runMarketCommand(rest, version);
233
- if (outcome !== "tui") return HANDLED(outcome);
248
+ return HANDLED(await runMarketCommand(rest, version));
249
+ }
250
+ case "edit": {
251
+ if (rest.includes("--help") || rest.includes("-h")) {
252
+ console.log(editHelp());
253
+ return HANDLED(0);
254
+ }
255
+ if (rest.includes("--version") || rest.includes("-v")) {
256
+ console.log(`magus v${version}`);
257
+ return HANDLED(0);
258
+ }
259
+ if (rest.length > 0) {
260
+ console.error(
261
+ "magus edit: takes no arguments — the commands are `magus market <verb>` (`magus market --help` lists them)",
262
+ );
263
+ return HANDLED(2);
264
+ }
234
265
  return {
235
266
  handled: false,
236
267
  launchTui: true,
@@ -327,14 +358,15 @@ applied by a PreToolUse hook magus registers at user scope.
327
358
 
328
359
  Company marketplaces — your team's own Claude Code plugin marketplace on
329
360
  GitHub, listed first in magus. Every change prints its plan and asks first.
330
- market Open the company marketplace screens
361
+ edit Open the company marketplace screens
331
362
  market list Company marketplaces on this machine, with their plugins
332
363
  market create Create one on GitHub; market connect use an existing one
333
364
  market remove <m> Remove one from this machine (the repository stays)
334
- market plugin create|delete <p@m>
365
+ market plugin create|delete <p@m>; market plugin add <path>
335
366
  market skill add|edit|remove <skill>
336
367
  market status How your local copies of company skills stand
337
368
  market publish <skill> Send a local skill's edits back to its plugin
369
+ market publish --plugin <p@m> Every edited skill of a plugin, one version
338
370
  market --help Every market command, its flags and exit codes
339
371
 
340
372
  Other commands:
@@ -250,6 +250,7 @@ export function getAllMarketplaces(
250
250
  deprecated: defaultMp?.deprecated,
251
251
  owned: defaultMp?.owned,
252
252
  ...(local.company ? { company: true } : {}),
253
+ ...(local.ownerName ? { ownerName: local.ownerName } : {}),
253
254
  });
254
255
  }
255
256
  }
package/src/main.tsx CHANGED
@@ -97,7 +97,7 @@ async function main(): Promise<void> {
97
97
  if (outcome.app === "market") {
98
98
  if (!isInteractive) {
99
99
  console.error(
100
- "magus market opens an interactive screen, which needs a terminal. `magus market --help` lists the commands that run without one.",
100
+ "magus edit opens an interactive screen, which needs a terminal. `magus market --help` lists the commands that run without one.",
101
101
  );
102
102
  process.exit(2);
103
103
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The interactive screens behind bare `magus market`: the company
2
+ * The interactive screens behind `magus edit`: the company
3
3
  * marketplaces on this machine, and the plugins and skills of one of them.
4
4
  *
5
5
  * Imported dynamically from `main.tsx`, like `tui.tsx`, so no other command
@@ -55,6 +55,8 @@ export interface LocalMarketplace {
55
55
  * and the name and source are on this machine's trust list.
56
56
  */
57
57
  company?: boolean;
58
+ /** `owner.name` from the manifest, trimmed; absent when empty or missing. */
59
+ ownerName?: string;
58
60
  }
59
61
 
60
62
  /** Claude Code's marketplace clones, resolved per call. */
@@ -322,6 +324,11 @@ async function scanSingleMarketplace(
322
324
  ? manifest.metadata.pluginRoot
323
325
  : undefined;
324
326
 
327
+ const ownerName: string | undefined =
328
+ typeof manifest.owner?.name === "string" && manifest.owner.name.trim()
329
+ ? manifest.owner.name.trim()
330
+ : undefined;
331
+
325
332
  const plugins: LocalMarketplacePlugin[] = [];
326
333
 
327
334
  if (manifest.plugins && Array.isArray(manifest.plugins)) {
@@ -398,6 +405,7 @@ async function scanSingleMarketplace(
398
405
  company:
399
406
  readMarker(manifest) &&
400
407
  isTrustedIn(trusted, marketplaceName, remoteSource),
408
+ ...(ownerName ? { ownerName } : {}),
401
409
  };
402
410
  } catch {
403
411
  return null;
@@ -140,8 +140,8 @@ export interface ChangeSpec {
140
140
  /** Names a pull-request branch. */
141
141
  slug: string;
142
142
  prBody: string;
143
- /** A skill directory whose committed files must equal these. */
144
- verify?: { dir: string; files: readonly SkillFile[] };
143
+ /** Directories whose committed files must equal these, each. */
144
+ verify?: ReadonlyArray<{ dir: string; files: readonly SkillFile[] }>;
145
145
  /** Set when the commit is made, for later steps. */
146
146
  state: { commit?: string; pushed?: boolean };
147
147
  }
@@ -186,15 +186,15 @@ export function changeSteps(spec: ChangeSpec): MarketStep[] {
186
186
  }
187
187
  const made = await gitCommit(dir, spec.message);
188
188
  if (!made.ok) throw gitError(made, spec.label);
189
- if (spec.verify) {
190
- const tree = await lsTree(dir, made.value, spec.verify.dir);
189
+ for (const check of spec.verify ?? []) {
190
+ const tree = await lsTree(dir, made.value, check.dir);
191
191
  if (!tree.ok) throw gitError(tree, spec.label);
192
- const prefix = `${spec.verify.dir}/`;
192
+ const prefix = `${check.dir}/`;
193
193
  const committed = tree.value.map((e) => ({
194
194
  path: e.path.slice(prefix.length),
195
195
  oid: e.oid,
196
196
  }));
197
- if (!sameFiles(committed, spec.verify.files)) {
197
+ if (!sameFiles(committed, check.files)) {
198
198
  throw new MarketError(
199
199
  "other",
200
200
  "the commit did not hold exactly the planned files — nothing was pushed",