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.
Files changed (111) hide show
  1. package/README.md +21 -0
  2. package/package.json +4 -4
  3. package/src/__tests__/market-bases.test.ts +112 -0
  4. package/src/__tests__/market-config.test.ts +84 -0
  5. package/src/__tests__/market-conflicts.test.ts +175 -0
  6. package/src/__tests__/market-consent.test.ts +185 -0
  7. package/src/__tests__/market-content.test.ts +183 -0
  8. package/src/__tests__/market-delivery.test.ts +308 -0
  9. package/src/__tests__/market-fixture.ts +1167 -0
  10. package/src/__tests__/market-git.test.ts +130 -0
  11. package/src/__tests__/market-identity.test.ts +127 -0
  12. package/src/__tests__/market-install.test.ts +143 -0
  13. package/src/__tests__/market-layout.test.ts +206 -0
  14. package/src/__tests__/market-publish.test.ts +263 -0
  15. package/src/__tests__/market-r2-adapters.test.ts +190 -0
  16. package/src/__tests__/market-r2-company-cli.test.ts +350 -0
  17. package/src/__tests__/market-r2-company-unit.test.ts +224 -0
  18. package/src/__tests__/market-r2-local-items.test.ts +197 -0
  19. package/src/__tests__/market-r2-plugin-add.test.ts +470 -0
  20. package/src/__tests__/market-r2-plugin-publish.test.ts +568 -0
  21. package/src/__tests__/market-r2-routing.test.ts +127 -0
  22. package/src/__tests__/market-r2-skill-create-plugin.test.ts +315 -0
  23. package/src/__tests__/market-r2b-display-path.test.ts +180 -0
  24. package/src/__tests__/market-r2b-fit-row.test.ts +287 -0
  25. package/src/__tests__/market-r2b-local-items.test.ts +253 -0
  26. package/src/__tests__/market-r2b-plugin-add.test.ts +130 -0
  27. package/src/__tests__/market-readonly.test.ts +162 -0
  28. package/src/__tests__/market-repositories.test.ts +263 -0
  29. package/src/__tests__/market-results.test.ts +198 -0
  30. package/src/__tests__/market-router.test.ts +65 -0
  31. package/src/__tests__/market-safety.test.ts +357 -0
  32. package/src/__tests__/market-teammates.test.ts +304 -0
  33. package/src/cli/install.ts +64 -5
  34. package/src/cli/market-view.ts +329 -0
  35. package/src/cli/market.ts +664 -0
  36. package/src/cli/router.ts +69 -3
  37. package/src/cli/update-view.ts +56 -0
  38. package/src/cli/update.ts +111 -9
  39. package/src/data/marketplaces.ts +8 -2
  40. package/src/main.tsx +20 -2
  41. package/src/market-tui.tsx +21 -0
  42. package/src/services/active-closure.ts +71 -9
  43. package/src/services/claude-cli.ts +140 -10
  44. package/src/services/claude-settings.ts +19 -25
  45. package/src/services/doctor.ts +5 -5
  46. package/src/services/git-write.ts +615 -0
  47. package/src/services/github-cli.ts +309 -0
  48. package/src/services/local-marketplace.ts +112 -50
  49. package/src/services/market-bases.ts +191 -0
  50. package/src/services/market-change.ts +242 -0
  51. package/src/services/market-clone.ts +572 -0
  52. package/src/services/market-fs.ts +138 -0
  53. package/src/services/market-install.ts +172 -0
  54. package/src/services/market-layout.ts +311 -0
  55. package/src/services/market-local.ts +93 -0
  56. package/src/services/market-marker.ts +49 -0
  57. package/src/services/market-ops-content.ts +277 -0
  58. package/src/services/market-ops-plugin.ts +655 -0
  59. package/src/services/market-ops-publish.ts +1227 -0
  60. package/src/services/market-ops-repo.ts +916 -0
  61. package/src/services/market-ops-skill.ts +788 -0
  62. package/src/services/market-ops.ts +686 -0
  63. package/src/services/market-publish.ts +468 -0
  64. package/src/services/market-registry.ts +382 -0
  65. package/src/services/market-types.ts +268 -0
  66. package/src/services/marketplace-conflicts.ts +127 -0
  67. package/src/services/marketplace-refresh.ts +33 -3
  68. package/src/services/marketplace-sync.ts +253 -22
  69. package/src/services/plugin-dependencies.ts +17 -0
  70. package/src/services/plugin-manager.ts +1 -1
  71. package/src/services/plugin-mcp-config.ts +6 -9
  72. package/src/services/plugin-requires.ts +20 -8
  73. package/src/services/profile-absorb.ts +88 -3
  74. package/src/services/profile-adopt.ts +10 -1
  75. package/src/services/profiles.ts +64 -1
  76. package/src/services/resolver.ts +17 -5
  77. package/src/services/settings-manager.ts +2 -2
  78. package/src/services/skills-manager.ts +2 -2
  79. package/src/services/update-plan.ts +67 -8
  80. package/src/tui-runtime.tsx +142 -0
  81. package/src/tui.tsx +7 -124
  82. package/src/types/index.ts +7 -0
  83. package/src/ui/adapters/marketAdapter.ts +366 -0
  84. package/src/ui/adapters/pluginsAdapter.ts +45 -3
  85. package/src/ui/components/layout/ScreenLayout.tsx +43 -17
  86. package/src/ui/components/modals/ConfirmModal.tsx +145 -8
  87. package/src/ui/components/modals/HelpOverlay.tsx +10 -3
  88. package/src/ui/components/modals/ModalContainer.tsx +47 -5
  89. package/src/ui/components/primitives/ListCategoryRow.tsx +22 -2
  90. package/src/ui/components/primitives/ScopeBadge.tsx +28 -0
  91. package/src/ui/components/primitives/ScopeVersionLine.tsx +27 -0
  92. package/src/ui/components/primitives/SelectableRow.tsx +14 -1
  93. package/src/ui/components/primitives/ToneLine.tsx +25 -0
  94. package/src/ui/components/primitives/index.ts +5 -0
  95. package/src/ui/keymap.ts +21 -5
  96. package/src/ui/market/MarketApp.tsx +59 -0
  97. package/src/ui/market/MarketplaceScreen.tsx +280 -0
  98. package/src/ui/market/MarketplacesDetail.snowflake.tsx +161 -0
  99. package/src/ui/market/MarketplacesScreen.tsx +264 -0
  100. package/src/ui/market/PluginDetail.snowflake.tsx +61 -0
  101. package/src/ui/market/SkillDetail.snowflake.tsx +71 -0
  102. package/src/ui/market/marketKeymap.ts +243 -0
  103. package/src/ui/market/useMarketState.ts +694 -0
  104. package/src/ui/renderers/pluginRenderers.tsx +31 -3
  105. package/src/ui/renderers/pluginStatusText.ts +6 -0
  106. package/src/ui/screens/PluginsScreen.tsx +49 -5
  107. package/src/ui/screens/SkillsScreen.tsx +2 -2
  108. package/src/ui/state/AppContext.tsx +14 -2
  109. package/src/ui/state/types.ts +9 -2
  110. package/src/utils/config-dir.ts +29 -9
  111. package/src/utils/display-path.ts +33 -0
@@ -2,9 +2,14 @@ export { SELECTED_MARK, SelectableRow, rowLead } from "./SelectableRow.js";
2
2
  export { ListCategoryRow } from "./ListCategoryRow.js";
3
3
  export { SCOPE_OFF, SCOPE_ON, ScopeSquares } from "./ScopeSquares.js";
4
4
  export { ScopeDetail, ScopeDot, ScopeKeyChip } from "./ScopeDetail.js";
5
+ export { ScopeBadge } from "./ScopeBadge.js";
6
+ export type { ScopeWord } from "./ScopeBadge.js";
5
7
  export { ActionHints } from "./ActionHints.js";
6
8
  export type { Hint } from "./ActionHints.js";
7
9
  export { MetaText } from "./MetaText.js";
8
10
  export { KeyValueLine } from "./KeyValueLine.js";
9
11
  export { DetailSection } from "./DetailSection.js";
10
12
  export { ErrorNotice } from "./ErrorNotice.js";
13
+ export { ToneLine } from "./ToneLine.js";
14
+ export type { LineTone } from "./ToneLine.js";
15
+ export { ScopeVersionLine } from "./ScopeVersionLine.js";
package/src/ui/keymap.ts CHANGED
@@ -70,7 +70,7 @@ export type KeyHandler = (key: KeyPress) => unknown;
70
70
  /** A binding whose id is known to the type system. */
71
71
  type Bound<Id extends string> = KeyBinding & { readonly id: Id };
72
72
 
73
- function bind<const Id extends string>(
73
+ export function bind<const Id extends string>(
74
74
  id: Id,
75
75
  keys: readonly string[],
76
76
  label: string,
@@ -81,7 +81,7 @@ function bind<const Id extends string>(
81
81
  }
82
82
 
83
83
  /** Listed in help, never in a footer. */
84
- const HELP_ONLY = 0;
84
+ export const HELP_ONLY = 0;
85
85
 
86
86
  // ─── Shared bindings ────────────────────────────────────────────────────────
87
87
 
@@ -755,11 +755,27 @@ export function footerHints(
755
755
  : [];
756
756
  const global =
757
757
  owner === "input" ? [] : GLOBAL_BINDINGS.filter((b) => b !== HELP);
758
- const pool = [...own, ...SCROLL_BINDINGS, ...global].filter(
759
- (b) => b.priority > HELP_ONLY,
758
+ return fitHints(
759
+ [...own, ...SCROLL_BINDINGS, ...global],
760
+ owner === "input" ? [] : [HELP],
761
+ width,
760
762
  );
763
+ }
761
764
 
762
- const pinned: KeyBinding[] = owner === "input" ? [] : [HELP];
765
+ /**
766
+ * The hints of `bindings` that fit a footer `width` cells wide, `pinned` kept
767
+ * whenever they fit and drawn last. The rest are taken in descending priority
768
+ * until one does not fit, then shown in declaration order — the rule
769
+ * `footerHints` applies to a tab of the main TUI, for any list of bindings.
770
+ */
771
+ export function fitHints(
772
+ bindings: readonly KeyBinding[],
773
+ pinned: readonly KeyBinding[],
774
+ width: number,
775
+ ): KeyBinding[] {
776
+ const pool = bindings.filter(
777
+ (b) => b.priority > HELP_ONLY && !pinned.includes(b),
778
+ );
763
779
  if (footerWidth(pinned) > width) return [];
764
780
 
765
781
  const kept = new Set<KeyBinding>(pinned);
@@ -0,0 +1,59 @@
1
+ import { ModalContainer } from "../components/modals/index.js";
2
+ import { KeyRouter } from "../input/KeyRouter.js";
3
+ import { AppProvider } from "../state/AppContext.js";
4
+ import {
5
+ DimensionsProvider,
6
+ useDimensions,
7
+ } from "../state/DimensionsContext.js";
8
+ import { MarketplaceScreen } from "./MarketplaceScreen.js";
9
+ import { MarketplacesScreen } from "./MarketplacesScreen.js";
10
+ import { useMarketState } from "./useMarketState.js";
11
+
12
+ /** The app frame's side padding, the same as the main TUI's. */
13
+ const FRAME_PADDING = 1;
14
+
15
+ function MarketContent({ onExit }: { onExit: () => void }) {
16
+ const market = useMarketState();
17
+ const dimensions = useDimensions();
18
+ const { route } = market;
19
+
20
+ return (
21
+ <box flexDirection="column" height={dimensions.terminalHeight}>
22
+ <box
23
+ flexDirection="column"
24
+ height={dimensions.contentHeight}
25
+ paddingLeft={FRAME_PADDING}
26
+ paddingRight={FRAME_PADDING}
27
+ >
28
+ {route.screen === "marketplace" ? (
29
+ <MarketplaceScreen
30
+ market={market}
31
+ name={route.name}
32
+ onExit={onExit}
33
+ />
34
+ ) : (
35
+ <MarketplacesScreen market={market} onExit={onExit} />
36
+ )}
37
+ </box>
38
+ <ModalContainer />
39
+ </box>
40
+ );
41
+ }
42
+
43
+ /**
44
+ * The market TUI's root: the main TUI's modal and progress state, key router
45
+ * and dimensions, around two screens of its own. No startup questions and
46
+ * nothing added on open — the screens read, and change only what a confirmed
47
+ * plan says.
48
+ */
49
+ export function MarketApp({ onExit }: { onExit: () => void }) {
50
+ return (
51
+ <AppProvider>
52
+ <KeyRouter>
53
+ <DimensionsProvider>
54
+ <MarketContent onExit={onExit} />
55
+ </DimensionsProvider>
56
+ </KeyRouter>
57
+ </AppProvider>
58
+ );
59
+ }
@@ -0,0 +1,280 @@
1
+ import React from "react";
2
+ import {
3
+ type MarketplaceItem,
4
+ fitRow,
5
+ freshnessWords,
6
+ marketplaceItems,
7
+ plural,
8
+ skillMark,
9
+ } from "../adapters/marketAdapter.js";
10
+ import { ScrollableList } from "../components/ScrollableList.js";
11
+ import { ScreenLayout } from "../components/layout/index.js";
12
+ import {
13
+ DetailSection,
14
+ ErrorNotice,
15
+ ListCategoryRow,
16
+ MetaText,
17
+ ScopeSquares,
18
+ SelectableRow,
19
+ ToneLine,
20
+ } from "../components/primitives/index.js";
21
+ import { useKeyLayer } from "../input/KeyRouter.js";
22
+ import { type KeyHandler, moveDirection, runBindings } from "../keymap.js";
23
+ import { useModal } from "../state/AppContext.js";
24
+ import { useDimensions } from "../state/DimensionsContext.js";
25
+ import { asyncValue } from "../state/types.js";
26
+ import { PluginDetail } from "./PluginDetail.snowflake.js";
27
+ import { SkillDetail } from "./SkillDetail.snowflake.js";
28
+ import {
29
+ MARKETPLACE_APP_KEYS,
30
+ MARKETPLACE_KEYS,
31
+ MARKET_HELP,
32
+ type MarketplaceKeyId,
33
+ marketHelpSections,
34
+ } from "./marketKeymap.js";
35
+ import type { MarketState } from "./useMarketState.js";
36
+
37
+ /** Rows above the list: the heading. */
38
+ const HEADING_ROWS = 1;
39
+ /** Cells a skill row is indented under its plugin. */
40
+ const SKILL_INDENT = 6;
41
+ /** Cells a plugin row starts in. */
42
+ const PLUGIN_INDENT = 2;
43
+
44
+ /** Columns the scope squares and the ▶ marker take before a plugin's name. */
45
+ const PLUGIN_PREFIX = 6;
46
+
47
+ /**
48
+ * One row of screen 2 in `width` columns, never wrapped: the name always
49
+ * shows; the version, the skill count and the marks are cut from the end.
50
+ */
51
+ export function renderMarketplaceRow(
52
+ item: MarketplaceItem,
53
+ selected: boolean,
54
+ width: number,
55
+ ): React.ReactNode {
56
+ if (item.kind === "plugin") {
57
+ const fit = fitRow(
58
+ Math.max(1, width - PLUGIN_INDENT),
59
+ item.name,
60
+ PLUGIN_PREFIX,
61
+ [
62
+ { text: item.version, tone: "muted" as const },
63
+ { text: plural(item.skills.length, "skill"), tone: "muted" as const },
64
+ ...(item.edited.length > 0
65
+ ? [
66
+ {
67
+ text: `● ${plural(item.edited.length, "skill")} edited here`,
68
+ tone: "warning" as const,
69
+ },
70
+ ]
71
+ : []),
72
+ ],
73
+ );
74
+ return (
75
+ <SelectableRow selected={selected} indent={PLUGIN_INDENT}>
76
+ <ScopeSquares
77
+ user={item.installs.user !== null}
78
+ project={item.installs.project !== null}
79
+ local={item.installs.local !== null}
80
+ selected={selected}
81
+ />
82
+ {item.expanded ? " ▼ " : " ▶ "}
83
+ <strong>{fit.name}</strong>
84
+ {fit.secondary.map((p) => (
85
+ <React.Fragment key={p.text}>
86
+ {" "}
87
+ <MetaText text={p.text} tone={p.tone} selected={selected} />
88
+ </React.Fragment>
89
+ ))}
90
+ </SelectableRow>
91
+ );
92
+ }
93
+ const mark = skillMark(item.local);
94
+ const fit = fitRow(
95
+ Math.max(1, width - SKILL_INDENT),
96
+ item.skill,
97
+ 0,
98
+ mark ? [{ text: `${mark.glyph} ${mark.words}`, tone: mark.tone }] : [],
99
+ );
100
+ return (
101
+ <SelectableRow selected={selected} indent={SKILL_INDENT}>
102
+ {fit.name}
103
+ {fit.secondary.map((p) => (
104
+ <React.Fragment key={p.text}>
105
+ {" "}
106
+ <MetaText text={p.text} tone={p.tone} selected={selected} />
107
+ </React.Fragment>
108
+ ))}
109
+ </SelectableRow>
110
+ );
111
+ }
112
+
113
+ /** Screen 2: one marketplace's plugins, each opening to its skill rows. */
114
+ export function MarketplaceScreen({
115
+ market,
116
+ name,
117
+ onExit,
118
+ }: {
119
+ market: MarketState;
120
+ name: string;
121
+ onExit: () => void;
122
+ }) {
123
+ const modal = useModal();
124
+ const dimensions = useDimensions();
125
+ const value = asyncValue(market.data);
126
+ const entry = value?.marketplaces.find((m) => m.name === name);
127
+ const items = entry
128
+ ? marketplaceItems(
129
+ entry,
130
+ value?.installs ?? {},
131
+ value?.status ?? [],
132
+ market.expanded,
133
+ )
134
+ : [];
135
+ const index = Math.max(
136
+ 0,
137
+ Math.min(market.selected.marketplace, items.length - 1),
138
+ );
139
+ const item = items[index];
140
+
141
+ const select = (next: number) =>
142
+ market.setSelected((s) => ({
143
+ ...s,
144
+ marketplace: Math.max(0, Math.min(items.length - 1, next)),
145
+ }));
146
+
147
+ const handlers: Record<MarketplaceKeyId, KeyHandler> = {
148
+ expand: () => {
149
+ if (item?.kind === "plugin") market.toggle(item.ref);
150
+ },
151
+ publish: () => {
152
+ if (item?.kind === "skill" && item.local) {
153
+ void market.publish(item.skill, item.ref, name, item.local);
154
+ } else if (item?.kind === "plugin") {
155
+ void market.publishPlugin(item.ref, name);
156
+ }
157
+ },
158
+ add: () => {
159
+ if (item) void market.addSkill(item.ref, name);
160
+ },
161
+ edit: () => {
162
+ if (item?.kind === "skill") {
163
+ void market.editSkill(item.skill, item.ref, name);
164
+ }
165
+ },
166
+ "remove-skill": () => {
167
+ if (item?.kind === "skill") {
168
+ void market.removeSkill(item.skill, item.ref, name);
169
+ }
170
+ },
171
+ new: () => void market.newPlugin(name),
172
+ delete: () => {
173
+ if (item) void market.deletePlugin(item.ref, name);
174
+ },
175
+ fetch: () => void market.fetchOne(name),
176
+ move: (key) => select(index + moveDirection(key)),
177
+ };
178
+ useKeyLayer("screen", true, (key) =>
179
+ runBindings(MARKETPLACE_KEYS, handlers, key),
180
+ );
181
+ useKeyLayer("global", true, (key) =>
182
+ runBindings(
183
+ MARKETPLACE_APP_KEYS,
184
+ {
185
+ help: () => modal.helpSections(marketHelpSections("marketplace")),
186
+ quit: onExit,
187
+ back: market.back,
188
+ },
189
+ key,
190
+ ),
191
+ );
192
+
193
+ const failed = market.data.status === "error" ? market.data.error : null;
194
+ const note = market.notes[name];
195
+
196
+ return (
197
+ <ScreenLayout
198
+ title={`magus edit › ${name}`}
199
+ subtitle={
200
+ market.data.status === "loading"
201
+ ? "reading…"
202
+ : entry
203
+ ? `${entry.repo} · ${freshnessWords(entry.freshness)}`
204
+ : undefined
205
+ }
206
+ notice={
207
+ entry?.freshness === "stale" ? (
208
+ <ToneLine tone="warning">
209
+ ⚠ {entry.reason} — r fetches from GitHub
210
+ </ToneLine>
211
+ ) : undefined
212
+ }
213
+ keymap={{
214
+ bindings: [...MARKETPLACE_KEYS, ...MARKETPLACE_APP_KEYS],
215
+ pinned: [MARKET_HELP],
216
+ }}
217
+ listPanel={(width: number) =>
218
+ failed ? (
219
+ <ErrorNotice
220
+ title={`Could not read ${name}.`}
221
+ detail={failed.message}
222
+ hint="r fetches from GitHub and reads it again."
223
+ />
224
+ ) : (
225
+ <box flexDirection="column">
226
+ <ListCategoryRow
227
+ title="Plugins"
228
+ count={entry?.plugins.length ?? 0}
229
+ selected={false}
230
+ expandable={false}
231
+ width={width}
232
+ />
233
+ {items.length === 0 && market.data.status !== "loading" ? (
234
+ <ToneLine tone="muted">
235
+ {entry
236
+ ? " No plugins yet — n creates one."
237
+ : ` ${name} is no longer a company marketplace here.`}
238
+ </ToneLine>
239
+ ) : (
240
+ <ScrollableList
241
+ items={items}
242
+ selectedIndex={index}
243
+ renderItem={(it, _i, selected) =>
244
+ renderMarketplaceRow(it, selected, width)
245
+ }
246
+ maxHeight={Math.max(
247
+ 1,
248
+ dimensions.listPanelHeight - HEADING_ROWS,
249
+ )}
250
+ getKey={(it) =>
251
+ it.kind === "plugin"
252
+ ? `p:${it.ref}`
253
+ : `s:${it.ref}:${it.skill}`
254
+ }
255
+ />
256
+ )}
257
+ </box>
258
+ )
259
+ }
260
+ detailPanel={
261
+ <box flexDirection="column">
262
+ {item?.kind === "plugin" ? <PluginDetail item={item} /> : null}
263
+ {item?.kind === "skill" ? <SkillDetail item={item} /> : null}
264
+ {note ? (
265
+ <DetailSection title="Last change">
266
+ <ToneLine>{note}</ToneLine>
267
+ </DetailSection>
268
+ ) : null}
269
+ </box>
270
+ }
271
+ detailKey={
272
+ item
273
+ ? item.kind === "plugin"
274
+ ? item.ref
275
+ : `${item.ref}:${item.skill}`
276
+ : undefined
277
+ }
278
+ />
279
+ );
280
+ }
@@ -0,0 +1,161 @@
1
+ import { displayPath } from "../../utils/display-path.js";
2
+ import {
3
+ type MarketplacesItem,
4
+ freshnessTone,
5
+ freshnessWords,
6
+ localKindWords,
7
+ plural,
8
+ } from "../adapters/marketAdapter.js";
9
+ import {
10
+ DetailSection,
11
+ KeyValueLine,
12
+ MetaText,
13
+ ScopeBadge,
14
+ ToneLine,
15
+ } from "../components/primitives/index.js";
16
+
17
+ /**
18
+ * The detail panel for the marketplaces list: what Create and Connect do, or
19
+ * one company marketplace's repository, freshness and plugins.
20
+ *
21
+ * A snowflake: the Marketplaces screen's own panel, over the market adapter's
22
+ * row type and that screen's words. It is composed only from library
23
+ * primitives and sets no colour, border or text style of its own.
24
+ */
25
+ export function MarketplacesDetail({
26
+ item,
27
+ canAdd,
28
+ cwd,
29
+ note,
30
+ }: {
31
+ item: MarketplacesItem | undefined;
32
+ /** Whether there is a company marketplace to add a local row to. */
33
+ canAdd: boolean;
34
+ /** The project, against which a local folder is shown. */
35
+ cwd: string;
36
+ note: string | undefined;
37
+ }) {
38
+ if (!item || item.kind === "local-heading") return null;
39
+ if (item.kind === "local") {
40
+ const local = item.item;
41
+ return (
42
+ <DetailSection title={local.name}>
43
+ <KeyValueLine label="kind" value={localKindWords(local)} />
44
+ <KeyValueLine
45
+ label="scope"
46
+ value={
47
+ <ScopeBadge scope={local.scope === "user" ? "USER" : "PROJECT"} />
48
+ }
49
+ />
50
+ <KeyValueLine
51
+ label="folder"
52
+ value={<MetaText text={displayPath(local.path, cwd)} />}
53
+ />
54
+ <ToneLine>
55
+ {local.kind === "plugin"
56
+ ? "A plugin folder of your own, in no company marketplace."
57
+ : "A skill of your own, in no company plugin."}
58
+ </ToneLine>
59
+ {canAdd ? (
60
+ <ToneLine tone="muted">
61
+ {local.kind === "plugin"
62
+ ? "a adds it to a company marketplace as a new plugin; the folder stays."
63
+ : "a adds it to a plugin of a company marketplace, or to a new one."}
64
+ </ToneLine>
65
+ ) : (
66
+ <ToneLine tone="muted">
67
+ To add it, first create a company marketplace (c) or connect one
68
+ (o).
69
+ </ToneLine>
70
+ )}
71
+ </DetailSection>
72
+ );
73
+ }
74
+ if (item.kind === "create") {
75
+ return (
76
+ <DetailSection title="Create a company marketplace">
77
+ <ToneLine>
78
+ Makes a new repository on GitHub for your team's plugins, and adds it
79
+ to this machine at USER scope.
80
+ </ToneLine>
81
+ <ToneLine tone="muted">
82
+ Enter or c asks for the owner, the company name magus shows it by, the
83
+ repository name and who can see it, then shows what it will do.
84
+ </ToneLine>
85
+ </DetailSection>
86
+ );
87
+ }
88
+ if (item.kind === "connect") {
89
+ return (
90
+ <DetailSection title="Connect a company marketplace">
91
+ <ToneLine>
92
+ Uses a repository your team already has, from GitHub, a git URL or a
93
+ folder, and adds it to this machine at USER scope.
94
+ </ToneLine>
95
+ <ToneLine tone="muted">
96
+ Enter or o asks for the repository, then shows what it will do.
97
+ </ToneLine>
98
+ </DetailSection>
99
+ );
100
+ }
101
+ const { entry, toPush } = item;
102
+ return (
103
+ <box flexDirection="column">
104
+ <DetailSection title={entry.name}>
105
+ {entry.company ? (
106
+ <KeyValueLine label="company" value={entry.company} />
107
+ ) : null}
108
+ <KeyValueLine
109
+ label="repository"
110
+ value={<MetaText text={entry.repo} tone="info" />}
111
+ />
112
+ <KeyValueLine label="scope" value={<ScopeBadge scope="USER" />} />
113
+ <KeyValueLine
114
+ label="plugins"
115
+ value={plural(entry.plugins.length, "plugin")}
116
+ />
117
+ <KeyValueLine
118
+ label="GitHub"
119
+ value={
120
+ <MetaText
121
+ text={freshnessWords(entry.freshness)}
122
+ tone={freshnessTone(entry.freshness)}
123
+ />
124
+ }
125
+ />
126
+ {entry.freshness === "stale" ? (
127
+ <ToneLine tone="muted">
128
+ {entry.reason} — r fetches from GitHub
129
+ </ToneLine>
130
+ ) : null}
131
+ </DetailSection>
132
+ {toPush.length > 0 ? (
133
+ <DetailSection title="To push">
134
+ <ToneLine tone="warning">
135
+ ● {toPush.join(", ")} — skills edited here
136
+ </ToneLine>
137
+ <ToneLine tone="muted">
138
+ Enter opens the marketplace; p on a plugin pushes a new version.
139
+ </ToneLine>
140
+ </DetailSection>
141
+ ) : null}
142
+ {entry.plugins.length > 0 ? (
143
+ <DetailSection title="Plugins">
144
+ {entry.plugins.map((p) => (
145
+ <ToneLine key={p.name}>
146
+ {p.name}{" "}
147
+ <MetaText
148
+ text={`${p.version} · ${plural(p.skills.length, "skill")}`}
149
+ />
150
+ </ToneLine>
151
+ ))}
152
+ </DetailSection>
153
+ ) : null}
154
+ {note ? (
155
+ <DetailSection title="Last change">
156
+ <ToneLine>{note}</ToneLine>
157
+ </DetailSection>
158
+ ) : null}
159
+ </box>
160
+ );
161
+ }