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.
Files changed (46) 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/__tests__/scope-uninstall-wrong-scope.test.ts +341 -0
  17. package/src/cli/market-view.ts +59 -18
  18. package/src/cli/market.ts +128 -44
  19. package/src/cli/router.ts +42 -10
  20. package/src/data/marketplaces.ts +1 -0
  21. package/src/main.tsx +1 -1
  22. package/src/market-tui.tsx +1 -1
  23. package/src/services/claude-cli.ts +13 -0
  24. package/src/services/local-marketplace.ts +8 -0
  25. package/src/services/market-change.ts +6 -6
  26. package/src/services/market-local.ts +93 -0
  27. package/src/services/market-ops-plugin.ts +366 -47
  28. package/src/services/market-ops-publish.ts +363 -15
  29. package/src/services/market-ops-repo.ts +94 -13
  30. package/src/services/market-ops-skill.ts +93 -11
  31. package/src/services/market-ops.ts +72 -8
  32. package/src/services/market-publish.ts +10 -1
  33. package/src/services/market-types.ts +23 -0
  34. package/src/services/update-engine.ts +97 -1
  35. package/src/tui-runtime.tsx +2 -2
  36. package/src/types/index.ts +2 -0
  37. package/src/ui/adapters/marketAdapter.ts +123 -4
  38. package/src/ui/adapters/pluginsAdapter.ts +25 -1
  39. package/src/ui/components/primitives/ListCategoryRow.tsx +22 -2
  40. package/src/ui/market/MarketplaceScreen.tsx +55 -24
  41. package/src/ui/market/MarketplacesDetail.snowflake.tsx +60 -4
  42. package/src/ui/market/MarketplacesScreen.tsx +120 -29
  43. package/src/ui/market/PluginDetail.snowflake.tsx +12 -1
  44. package/src/ui/market/marketKeymap.ts +29 -2
  45. package/src/ui/market/useMarketState.ts +146 -5
  46. package/src/utils/display-path.ts +33 -0
@@ -88,10 +88,17 @@ import {
88
88
  filesUnder,
89
89
  installLines,
90
90
  installStep,
91
+ pluginViewAt,
91
92
  readPlugin,
92
93
  versionFiles,
93
94
  versionLine,
94
95
  } from "./market-ops-content.js";
96
+ import {
97
+ NEW_PLUGIN_VERSION,
98
+ newPluginLayout,
99
+ refreshStep,
100
+ writeNewPlugin,
101
+ } from "./market-ops-plugin.js";
95
102
 
96
103
  // ─── skill add ───────────────────────────────────────────────────────────────
97
104
 
@@ -100,6 +107,13 @@ export interface SkillAddArgs {
100
107
  plugin: string;
101
108
  from: "project" | "global";
102
109
  keepLocal: boolean;
110
+ /**
111
+ * Create the plugin, in the same commit, when the marketplace has none of
112
+ * that name. Without it a missing plugin is `not-found`.
113
+ */
114
+ createPlugin?: boolean;
115
+ /** The new plugin's description; read only when it is created. */
116
+ description?: string;
103
117
  }
104
118
 
105
119
  export function skillFolder(
@@ -169,7 +183,34 @@ export async function planSkillAdd(
169
183
  `${where} has no skill named ${args.skill}`,
170
184
  );
171
185
  }
172
- const view = await readPlugin(args.plugin);
186
+ const ref = await resolvePluginRef(args.plugin);
187
+ const snapNow = await freshSnapshot(ref.m);
188
+ const manifestNow = await manifestAt(snapNow, ref.m.name);
189
+ const exists = pluginEntries(manifestNow).some(
190
+ (p) => p.name === ref.plugin,
191
+ );
192
+ if (!exists && !args.createPlugin) {
193
+ throw new MarketError(
194
+ "not-found",
195
+ `${ref.m.name} has no plugin named ${ref.plugin}`,
196
+ "`magus market list` shows its plugins; add --create-plugin to create it with this skill",
197
+ );
198
+ }
199
+ // A plugin created here starts at its first version: nothing to bump.
200
+ const layout = exists
201
+ ? null
202
+ : newPluginLayout(ref.m, snapNow, manifestNow, ref.plugin);
203
+ const view: PluginView = layout
204
+ ? {
205
+ m: ref.m,
206
+ snap: snapNow,
207
+ manifest: manifestNow,
208
+ plugin: ref.plugin,
209
+ dir: layout.dir,
210
+ pluginJsonPath: layout.pluginJsonPath,
211
+ pluginJson: null,
212
+ }
213
+ : await pluginViewAt(ref.m, snapNow, manifestNow, ref.plugin);
173
214
  const { m, snap, plugin, dir } = view;
174
215
  const skillDir = joinRel(dir, "skills", args.skill);
175
216
  if (filesUnder(snap, skillDir).length > 0) {
@@ -179,7 +220,9 @@ export async function planSkillAdd(
179
220
  "to send your edits to it, use `magus market publish`",
180
221
  );
181
222
  }
182
- const { from, to } = bumpFrom(view);
223
+ const { from, to } = layout
224
+ ? { from: null, to: NEW_PLUGIN_VERSION }
225
+ : bumpFrom(view);
183
226
  const sets = await readSourceSets(folder, snap.dir, skillDir);
184
227
  if (sets.published.length === 0) {
185
228
  throw new MarketError(
@@ -213,15 +256,40 @@ export async function planSkillAdd(
213
256
  reason: keepReason,
214
257
  }
215
258
  : { scope: copyScope, name: args.skill, action: "removed" };
259
+ const created = layout
260
+ ? [
261
+ { path: MANIFEST, change: "modified" as const },
262
+ { path: layout.pluginJsonPath, change: "added" as const },
263
+ ]
264
+ : versionFiles(view);
216
265
  const files = [
217
266
  ...sets.published.map((f) => ({
218
267
  path: joinRel(skillDir, f.path),
219
268
  change: "added" as const,
220
269
  })),
221
- ...versionFiles(view),
270
+ ...created,
222
271
  ];
223
272
  const state: ChangeSpec["state"] & { moved?: boolean } = {};
224
273
  const steps: MarketStep[] = [
274
+ ...(layout
275
+ ? [
276
+ {
277
+ id: "edit",
278
+ label: `wrote ${plugin}`,
279
+ outside: false,
280
+ async run(ctx: Parameters<MarketStep["run"]>[0]) {
281
+ writeNewPlugin(
282
+ cloneOf(ctx),
283
+ m,
284
+ view.manifest,
285
+ layout,
286
+ args.description,
287
+ );
288
+ return undefined;
289
+ },
290
+ } satisfies MarketStep,
291
+ ]
292
+ : []),
225
293
  {
226
294
  id: "copy",
227
295
  label: `copied ${args.skill} into ${plugin}`,
@@ -246,20 +314,27 @@ export async function planSkillAdd(
246
314
  return undefined;
247
315
  },
248
316
  },
249
- bumpStep(view, to),
317
+ ...(layout ? [] : [bumpStep(view, to)]),
250
318
  ...changeSteps({
251
319
  label: m.name,
252
320
  source: m.source,
253
321
  branch: snap.branch,
254
322
  target,
255
- stage: [skillDir, ...versionFiles(view).map((f) => f.path)],
323
+ stage: layout
324
+ ? [MANIFEST, dir]
325
+ : [skillDir, ...versionFiles(view).map((f) => f.path)],
256
326
  files,
257
- message: `${plugin}: add ${args.skill} skill (${to})`,
327
+ message: layout
328
+ ? `${plugin}: create plugin with ${args.skill} skill (${to})`
329
+ : `${plugin}: add ${args.skill} skill (${to})`,
258
330
  slug: `${plugin}-${to}`,
259
- prBody: `Adds the skill \`${args.skill}\` to \`${plugin}\` and bumps it to ${to}.`,
260
- verify: { dir: skillDir, files: sets.published },
331
+ prBody: layout
332
+ ? `Creates the plugin \`${plugin}\` (${to}) in ${m.name} with the skill \`${args.skill}\`.`
333
+ : `Adds the skill \`${args.skill}\` to \`${plugin}\` and bumps it to ${to}.`,
334
+ verify: [{ dir: skillDir, files: sets.published }],
261
335
  state,
262
336
  }),
337
+ ...(layout ? [refreshStep(m.name, state)] : []),
263
338
  installStep(view, installs, to, state),
264
339
  {
265
340
  id: "local-copy",
@@ -296,8 +371,14 @@ export async function planSkillAdd(
296
371
  },
297
372
  ];
298
373
  const lines = [
299
- `Adds the skill ${args.skill} from ${where} to ${pluginId}`,
300
- versionLine(plugin, from, to),
374
+ ...(layout
375
+ ? [
376
+ `Creates the plugin ${plugin} (${to}) in ${m.name}, with the skill ${args.skill} from ${where}, in one commit`,
377
+ ]
378
+ : [
379
+ `Adds the skill ${args.skill} from ${where} to ${pluginId}`,
380
+ versionLine(plugin, from, to),
381
+ ]),
301
382
  `Publishes ${sets.published.length} file${sets.published.length === 1 ? "" : "s"}: ${sets.published.map((f) => f.path).join(", ")}`,
302
383
  ...(sets.untrackedFolder
303
384
  ? [
@@ -324,6 +405,7 @@ export async function planSkillAdd(
324
405
  from: args.from,
325
406
  keepLocal: args.keepLocal,
326
407
  plugin: pluginId,
408
+ createPlugin: layout ? { description: args.description ?? null } : null,
327
409
  commit: snap.commit,
328
410
  hash: sets.hash,
329
411
  notPublished: sets.notPublished,
@@ -344,7 +426,7 @@ export async function planSkillAdd(
344
426
  steps,
345
427
  sync: { name: m.name, url: cloneUrl(m.source) },
346
428
  lock: m.name,
347
- replan: () => planSkillAdd(args, cwd),
429
+ replan: () => planSkillAdd({ ...args, plugin: pluginId }, cwd),
348
430
  doneMessage: `${args.skill} is in ${pluginId} ${to}. Takes effect in new sessions, or after /reload-plugins.`,
349
431
  };
350
432
  return { kind: "plan", plan };
@@ -31,8 +31,10 @@ import {
31
31
  pluginRelDir,
32
32
  pluginRootOf,
33
33
  } from "./market-layout.js";
34
+ import { localItems } from "./market-local.js";
34
35
  import {
35
36
  type TrustedRegistration,
37
+ isGithubSource,
36
38
  listCompanyMarketplaces,
37
39
  } from "./market-registry.js";
38
40
  import {
@@ -400,6 +402,28 @@ function readJsonFile(file: string): unknown {
400
402
  }
401
403
  }
402
404
 
405
+ /**
406
+ * The company a marketplace belongs to: its manifest's `owner.name`, else the
407
+ * GitHub owner of its source. Undefined for a non-GitHub source whose
408
+ * manifest names no owner.
409
+ */
410
+ export function companyOf(
411
+ manifest: unknown,
412
+ source: string,
413
+ ): string | undefined {
414
+ const owner =
415
+ manifest && typeof manifest === "object" && !Array.isArray(manifest)
416
+ ? (manifest as { owner?: unknown }).owner
417
+ : undefined;
418
+ const name =
419
+ owner && typeof owner === "object"
420
+ ? (owner as { name?: unknown }).name
421
+ : undefined;
422
+ if (typeof name === "string" && name.trim()) return name.trim();
423
+ if (isGithubSource(source)) return source.split("/")[0] || undefined;
424
+ return undefined;
425
+ }
426
+
403
427
  /** The plugins and skills of a marketplace as Claude Code's copy holds them. */
404
428
  function rowFor(m: TrustedRegistration): MarketplaceRow {
405
429
  const row: MarketplaceRow = {
@@ -408,10 +432,14 @@ function rowFor(m: TrustedRegistration): MarketplaceRow {
408
432
  scope: "user",
409
433
  plugins: [],
410
434
  };
435
+ const fallback = companyOf(null, m.source);
436
+ if (fallback) row.company = fallback;
411
437
  if (!m.location) return row;
412
438
  const manifest = readJsonFile(
413
439
  path.join(m.location, ".claude-plugin", "marketplace.json"),
414
440
  );
441
+ const company = companyOf(manifest, m.source);
442
+ if (company) row.company = company;
415
443
  const root = pluginRootOf(manifest);
416
444
  for (const entry of pluginEntries(manifest)) {
417
445
  const dir = pluginDirFor(m.location, entry.source, root);
@@ -443,9 +471,35 @@ function rowFor(m: TrustedRegistration): MarketplaceRow {
443
471
  return row;
444
472
  }
445
473
 
446
- /** `market list`: every company marketplace here with its plugins. Read-only. */
447
- export async function marketList(): Promise<MarketResult> {
474
+ /** Every plugin name among `marketplaces`. */
475
+ export function companyPluginNames(
476
+ marketplaces: ReadonlyArray<{ plugins: MarketplaceRow["plugins"] }>,
477
+ ): Set<string> {
478
+ return new Set(marketplaces.flatMap((m) => m.plugins.map((p) => p.name)));
479
+ }
480
+
481
+ /** Every skill name a company plugin among `marketplaces` holds. */
482
+ export function companySkillNames(
483
+ marketplaces: ReadonlyArray<{ plugins: MarketplaceRow["plugins"] }>,
484
+ ): Set<string> {
485
+ return new Set(
486
+ marketplaces.flatMap((m) => m.plugins.flatMap((p) => p.skills)),
487
+ );
488
+ }
489
+
490
+ /**
491
+ * `market list`: every company marketplace here with its plugins, and the
492
+ * skills and plugin folders here that none of them holds. Read-only.
493
+ */
494
+ export async function marketList(
495
+ cwd: string = process.cwd(),
496
+ ): Promise<MarketResult> {
448
497
  const marketplaces = (await listCompanyMarketplaces()).map(rowFor);
498
+ const local = await localItems(
499
+ cwd,
500
+ companySkillNames(marketplaces),
501
+ companyPluginNames(marketplaces),
502
+ );
449
503
  return {
450
504
  command: "market list",
451
505
  outcome: "ok",
@@ -454,6 +508,7 @@ export async function marketList(): Promise<MarketResult> {
454
508
  ? "No company marketplaces on this machine"
455
509
  : `${marketplaces.length} company marketplace${marketplaces.length === 1 ? "" : "s"} on this machine`,
456
510
  marketplaces,
511
+ local,
457
512
  steps: [],
458
513
  };
459
514
  }
@@ -480,15 +535,16 @@ export interface MarketOverviewEntry {
480
535
  fix: string;
481
536
  /** The commit the plugins were read at, when a local copy had one. */
482
537
  commit: string | null;
538
+ /** The company it belongs to, as {@link companyOf} reads it. */
539
+ company?: string;
483
540
  plugins: MarketplaceRow["plugins"];
484
541
  }
485
542
 
486
543
  /** The plugins of a marketplace snapshot, with their versions and skills. */
487
544
  async function pluginsAt(
488
545
  snap: Snapshot,
489
- label: string,
546
+ manifest: Record<string, unknown>,
490
547
  ): Promise<MarketplaceRow["plugins"]> {
491
- const manifest = await manifestAt(snap, label);
492
548
  const root = pluginRootOf(manifest);
493
549
  const out: MarketplaceRow["plugins"] = [];
494
550
  for (const entry of pluginEntries(manifest)) {
@@ -541,14 +597,22 @@ export async function marketOverview(): Promise<MarketOverviewEntry[]> {
541
597
  commit: state.snapshot?.commit ?? null,
542
598
  plugins: [],
543
599
  };
600
+ let fromSnapshot = false;
544
601
  if (state.snapshot) {
545
602
  try {
546
- entry.plugins = await pluginsAt(state.snapshot, m.name);
603
+ const manifest = await manifestAt(state.snapshot, m.name);
604
+ entry.plugins = await pluginsAt(state.snapshot, manifest);
605
+ const company = companyOf(manifest, m.source);
606
+ if (company) entry.company = company;
607
+ fromSnapshot = true;
547
608
  } catch {
548
- entry.plugins = rowFor(m).plugins;
609
+ fromSnapshot = false;
549
610
  }
550
- } else {
551
- entry.plugins = rowFor(m).plugins;
611
+ }
612
+ if (!fromSnapshot) {
613
+ const row = rowFor(m);
614
+ entry.plugins = row.plugins;
615
+ if (row.company) entry.company = row.company;
552
616
  }
553
617
  out.push(entry);
554
618
  }
@@ -87,12 +87,21 @@ export async function readSourceSets(
87
87
  folder: string,
88
88
  destRepo: string,
89
89
  destDir: string,
90
+ /**
91
+ * A top-level folder whose own dot-name does not exclude it: a plugin's
92
+ * `.claude-plugin`. What lies inside it is judged by the usual rules.
93
+ */
94
+ keepDotDir?: string,
90
95
  ): Promise<SourceSets> {
91
96
  const items = inventory(folder);
92
97
  const notPublished: SourceSets["notPublished"] = [];
93
98
  const candidates: string[] = [];
94
99
  for (const item of items) {
95
- const shape = excludedByShape(item.path, item.symlink);
100
+ const judged =
101
+ keepDotDir && item.path.startsWith(`${keepDotDir}/`)
102
+ ? item.path.slice(keepDotDir.length + 1)
103
+ : item.path;
104
+ const shape = excludedByShape(judged, item.symlink);
96
105
  if (shape) notPublished.push({ path: item.path, reason: shape });
97
106
  else candidates.push(item.path);
98
107
  }
@@ -60,10 +60,29 @@ export type InstallScope = "user" | "project" | "local";
60
60
  export interface MarketplaceRow {
61
61
  name: string;
62
62
  repo: string;
63
+ /**
64
+ * The company it belongs to: the manifest's `owner.name`, else the GitHub
65
+ * owner of its source. Absent when neither names one.
66
+ */
67
+ company?: string;
63
68
  scope: "user";
64
69
  plugins: Array<{ name: string; version: string; skills: string[] }>;
65
70
  }
66
71
 
72
+ /**
73
+ * A skill or plugin folder on this machine that is not part of any company
74
+ * marketplace: one of this project's or your USER skills, or a plugin folder
75
+ * among them (one holding `.claude-plugin/plugin.json`).
76
+ */
77
+ export interface LocalItem {
78
+ kind: "skill" | "plugin";
79
+ /** The folder's name; for a plugin, its `plugin.json` name when it has one. */
80
+ name: string;
81
+ scope: "project" | "user";
82
+ /** The folder, absolute. */
83
+ path: string;
84
+ }
85
+
67
86
  export interface SkillStatusRow {
68
87
  skill: string;
69
88
  from: "project" | "user";
@@ -128,7 +147,11 @@ export interface MarketResult {
128
147
  installs?: InstallRow[];
129
148
  otherProjects?: number;
130
149
  localCopy?: LocalCopy;
150
+ /** A plugin-wide publish settles one local copy per published skill. */
151
+ localCopies?: LocalCopy[];
131
152
  marketplaces?: MarketplaceRow[];
153
+ /** `market list`: skills and plugins on this machine that no marketplace holds. */
154
+ local?: LocalItem[];
132
155
  skills?: SkillStatusRow[];
133
156
  plan?: string[];
134
157
  steps: StepRecord[];
@@ -43,6 +43,7 @@ import { selectProfileId } from "./active-closure.js";
43
43
  import type { PluginScope } from "./claude-cli.js";
44
44
  import {
45
45
  installPlugin,
46
+ installedScopeFromAnswer,
46
47
  isNotInstalledAnswer,
47
48
  repairPlugin,
48
49
  uninstallPluginViaCli,
@@ -1508,7 +1509,25 @@ export async function uninstallPluginInScope(
1508
1509
  }
1509
1510
  }
1510
1511
 
1511
- await remove.uninstall(pluginId, scope, projectPath);
1512
+ try {
1513
+ await remove.uninstall(pluginId, scope, projectPath);
1514
+ } catch (error) {
1515
+ if (!isNotInstalledAnswer(error)) throw error;
1516
+ await settleScopeWithNoInstall(
1517
+ pluginId,
1518
+ scope,
1519
+ projectPath,
1520
+ deps,
1521
+ error,
1522
+ profile,
1523
+ );
1524
+ if (scope === "project") {
1525
+ await remove
1526
+ .forgetVersion?.(pluginId, scope, projectPath)
1527
+ .catch(() => {});
1528
+ return;
1529
+ }
1530
+ }
1512
1531
  if (await remove.isInstalled(pluginId, scope, projectPath)) {
1513
1532
  throw new Error(`still installed at ${scope.toUpperCase()} scope`);
1514
1533
  }
@@ -1531,6 +1550,83 @@ export async function uninstallPluginInScope(
1531
1550
  await recordInProfileOrExplain(deps, projectPath, done);
1532
1551
  }
1533
1552
 
1553
+ /**
1554
+ * The half of {@link uninstallPluginInScope} that runs when Claude Code answers
1555
+ * that it holds no install of the plugin at `scope` here, so its uninstall had
1556
+ * nothing to remove.
1557
+ *
1558
+ * At PROJECT the setting that turns the plugin on is the profile render's, so
1559
+ * the profile drops it and the live files are brought back in line with the
1560
+ * profile. USER and LOCAL settings belong to no profile, so nothing is changed
1561
+ * there. Either way the scope is read back afterwards: still installed means
1562
+ * nothing magus owns turns it on, and the refusal names the scope Claude Code
1563
+ * reported, in magus's terms rather than the CLI's text.
1564
+ */
1565
+ async function settleScopeWithNoInstall(
1566
+ pluginId: string,
1567
+ scope: PluginScope,
1568
+ projectPath: string,
1569
+ deps: PluginApplyDeps,
1570
+ answer: unknown,
1571
+ profile: string | null | undefined,
1572
+ ): Promise<void> {
1573
+ const remove = deps.remove;
1574
+ if (!remove) throw new Error("magus cannot remove plugins from here");
1575
+ const name = pluginName(pluginId);
1576
+ const here = scope.toUpperCase();
1577
+
1578
+ let wasListed = false;
1579
+ if (scope === "project") {
1580
+ wasListed = await profileListsPlugin(pluginId, projectPath, profile);
1581
+ if (!(await remove.dropFromProfile(pluginId, projectPath, profile))) {
1582
+ throw new Error(`${name} is still in the profile at ${here} scope`);
1583
+ }
1584
+ await recordInProfileOrExplain(
1585
+ deps,
1586
+ projectPath,
1587
+ `${name} is no longer in the profile`,
1588
+ );
1589
+ }
1590
+
1591
+ if (!(await remove.isInstalled(pluginId, scope, projectPath))) return;
1592
+ // The profile was changed above, so the refusal says so: the user must not
1593
+ // find a plugin gone from the profile with nothing telling them why.
1594
+ const lead = wasListed
1595
+ ? `${name} is no longer in the profile, but it is`
1596
+ : `${name} is`;
1597
+ const installedAt = installedScopeFromAnswer(answer);
1598
+ if (installedAt && installedAt !== scope) {
1599
+ const there = installedAt.toUpperCase();
1600
+ throw new Error(
1601
+ `${lead}${wasListed ? " still" : ""} installed at ${there} scope, not ${here} — remove it from ${there}`,
1602
+ );
1603
+ }
1604
+ throw new Error(
1605
+ `${lead} not installed at ${here} scope in this project — remove it from the scope that installed it`,
1606
+ );
1607
+ }
1608
+
1609
+ /**
1610
+ * Whether `profile` lists the plugin, the profile picked as
1611
+ * `removePluginFromProfile` picks it: undefined means the active one.
1612
+ */
1613
+ async function profileListsPlugin(
1614
+ pluginId: string,
1615
+ projectPath: string,
1616
+ profile: string | null | undefined,
1617
+ ): Promise<boolean> {
1618
+ const manifest = await readManifest(projectPath);
1619
+ const id =
1620
+ profile === undefined
1621
+ ? selectProfileId(
1622
+ Object.keys(manifest.profiles),
1623
+ undefined,
1624
+ await activeProfile(projectPath),
1625
+ )
1626
+ : profile;
1627
+ return id ? profileLists(manifest.profiles[id], pluginId) : false;
1628
+ }
1629
+
1534
1630
  // -- binaries ----------------------------------------------------------------
1535
1631
 
1536
1632
  /**
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * What every magus TUI shares before its first screen: the renderer, the one
3
3
  * shutdown owner, Ctrl+C, the OSC 52 copy of a mouse selection, and the light
4
- * or dark theme. `tui.tsx` (bare `magus`) and `market-tui.tsx` (bare
5
- * `magus market`) each hand `mountTui` the React tree they draw.
4
+ * or dark theme. `tui.tsx` (bare `magus`) and `market-tui.tsx` (`magus
5
+ * edit`) each hand `mountTui` the React tree they draw.
6
6
  *
7
7
  * Like those two, this module is only ever imported dynamically from
8
8
  * `main.tsx`, so nothing on a non-interactive path loads `@opentui/*`.
@@ -55,6 +55,8 @@ export interface Marketplace {
55
55
  * marker and this machine trusts it. Listed above every other marketplace.
56
56
  */
57
57
  company?: boolean;
58
+ /** `owner.name` from the marketplace's own manifest, when it names one. */
59
+ ownerName?: string;
58
60
  }
59
61
 
60
62
  export interface DiscoveredMarketplace {