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
@@ -1,6 +1,7 @@
1
- import type React from "react";
1
+ import React from "react";
2
2
  import {
3
3
  type MarketplaceItem,
4
+ fitRow,
4
5
  freshnessWords,
5
6
  marketplaceItems,
6
7
  plural,
@@ -40,12 +41,36 @@ const SKILL_INDENT = 6;
40
41
  /** Cells a plugin row starts in. */
41
42
  const PLUGIN_INDENT = 2;
42
43
 
43
- function renderRow(
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(
44
52
  item: MarketplaceItem,
45
- _index: number,
46
53
  selected: boolean,
54
+ width: number,
47
55
  ): React.ReactNode {
48
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
+ );
49
74
  return (
50
75
  <SelectableRow selected={selected} indent={PLUGIN_INDENT}>
51
76
  <ScopeSquares
@@ -55,31 +80,32 @@ function renderRow(
55
80
  selected={selected}
56
81
  />
57
82
  {item.expanded ? " ▼ " : " ▶ "}
58
- <strong>{item.name}</strong>
59
- {" "}
60
- <MetaText text={item.version} selected={selected} />
61
- {" "}
62
- <MetaText
63
- text={plural(item.skills.length, "skill")}
64
- selected={selected}
65
- />
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
+ ))}
66
90
  </SelectableRow>
67
91
  );
68
92
  }
69
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
+ );
70
100
  return (
71
101
  <SelectableRow selected={selected} indent={SKILL_INDENT}>
72
- {item.skill}
73
- {mark ? (
74
- <>
102
+ {fit.name}
103
+ {fit.secondary.map((p) => (
104
+ <React.Fragment key={p.text}>
75
105
  {" "}
76
- <MetaText
77
- text={`${mark.glyph} ${mark.words}`}
78
- tone={mark.tone}
79
- selected={selected}
80
- />
81
- </>
82
- ) : null}
106
+ <MetaText text={p.text} tone={p.tone} selected={selected} />
107
+ </React.Fragment>
108
+ ))}
83
109
  </SelectableRow>
84
110
  );
85
111
  }
@@ -125,6 +151,8 @@ export function MarketplaceScreen({
125
151
  publish: () => {
126
152
  if (item?.kind === "skill" && item.local) {
127
153
  void market.publish(item.skill, item.ref, name, item.local);
154
+ } else if (item?.kind === "plugin") {
155
+ void market.publishPlugin(item.ref, name);
128
156
  }
129
157
  },
130
158
  add: () => {
@@ -167,7 +195,7 @@ export function MarketplaceScreen({
167
195
 
168
196
  return (
169
197
  <ScreenLayout
170
- title={`magus market › ${name}`}
198
+ title={`magus edit › ${name}`}
171
199
  subtitle={
172
200
  market.data.status === "loading"
173
201
  ? "reading…"
@@ -186,7 +214,7 @@ export function MarketplaceScreen({
186
214
  bindings: [...MARKETPLACE_KEYS, ...MARKETPLACE_APP_KEYS],
187
215
  pinned: [MARKET_HELP],
188
216
  }}
189
- listPanel={
217
+ listPanel={(width: number) =>
190
218
  failed ? (
191
219
  <ErrorNotice
192
220
  title={`Could not read ${name}.`}
@@ -200,6 +228,7 @@ export function MarketplaceScreen({
200
228
  count={entry?.plugins.length ?? 0}
201
229
  selected={false}
202
230
  expandable={false}
231
+ width={width}
203
232
  />
204
233
  {items.length === 0 && market.data.status !== "loading" ? (
205
234
  <ToneLine tone="muted">
@@ -211,7 +240,9 @@ export function MarketplaceScreen({
211
240
  <ScrollableList
212
241
  items={items}
213
242
  selectedIndex={index}
214
- renderItem={renderRow}
243
+ renderItem={(it, _i, selected) =>
244
+ renderMarketplaceRow(it, selected, width)
245
+ }
215
246
  maxHeight={Math.max(
216
247
  1,
217
248
  dimensions.listPanelHeight - HEADING_ROWS,
@@ -1,7 +1,9 @@
1
+ import { displayPath } from "../../utils/display-path.js";
1
2
  import {
2
3
  type MarketplacesItem,
3
4
  freshnessTone,
4
5
  freshnessWords,
6
+ localKindWords,
5
7
  plural,
6
8
  } from "../adapters/marketAdapter.js";
7
9
  import {
@@ -22,12 +24,53 @@ import {
22
24
  */
23
25
  export function MarketplacesDetail({
24
26
  item,
27
+ canAdd,
28
+ cwd,
25
29
  note,
26
30
  }: {
27
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;
28
36
  note: string | undefined;
29
37
  }) {
30
- if (!item) return null;
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
+ }
31
74
  if (item.kind === "create") {
32
75
  return (
33
76
  <DetailSection title="Create a company marketplace">
@@ -36,8 +79,8 @@ export function MarketplacesDetail({
36
79
  to this machine at USER scope.
37
80
  </ToneLine>
38
81
  <ToneLine tone="muted">
39
- Enter or c asks for the owner, the name and who can see it, then shows
40
- what it will do.
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.
41
84
  </ToneLine>
42
85
  </DetailSection>
43
86
  );
@@ -55,10 +98,13 @@ export function MarketplacesDetail({
55
98
  </DetailSection>
56
99
  );
57
100
  }
58
- const { entry } = item;
101
+ const { entry, toPush } = item;
59
102
  return (
60
103
  <box flexDirection="column">
61
104
  <DetailSection title={entry.name}>
105
+ {entry.company ? (
106
+ <KeyValueLine label="company" value={entry.company} />
107
+ ) : null}
62
108
  <KeyValueLine
63
109
  label="repository"
64
110
  value={<MetaText text={entry.repo} tone="info" />}
@@ -83,6 +129,16 @@ export function MarketplacesDetail({
83
129
  </ToneLine>
84
130
  ) : null}
85
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}
86
142
  {entry.plugins.length > 0 ? (
87
143
  <DetailSection title="Plugins">
88
144
  {entry.plugins.map((p) => (
@@ -1,8 +1,12 @@
1
- import type React from "react";
1
+ import React from "react";
2
2
  import {
3
3
  type MarketplacesItem,
4
+ fitRow,
5
+ localKindWords,
4
6
  marketplacesItems,
7
+ nextSelectable,
5
8
  plural,
9
+ selectable,
6
10
  } from "../adapters/marketAdapter.js";
7
11
  import { ScrollableList } from "../components/ScrollableList.js";
8
12
  import { ScreenLayout } from "../components/layout/index.js";
@@ -18,6 +22,7 @@ import { type KeyHandler, moveDirection, runBindings } from "../keymap.js";
18
22
  import { useModal } from "../state/AppContext.js";
19
23
  import { useDimensions } from "../state/DimensionsContext.js";
20
24
  import { asyncValue } from "../state/types.js";
25
+ import { truncate } from "../text-width.js";
21
26
  import { MarketplacesDetail } from "./MarketplacesDetail.snowflake.js";
22
27
  import {
23
28
  MARKETPLACES_APP_KEYS,
@@ -31,37 +36,99 @@ import type { MarketState } from "./useMarketState.js";
31
36
  /** Rows above the list: the heading. */
32
37
  const HEADING_ROWS = 1;
33
38
 
34
- function renderRow(
39
+ /** Cells a row starts in, the lead cell included. */
40
+ const ROW_INDENT = 2;
41
+
42
+ type Part = { text: string; tone: "muted" | "info" | "warning" };
43
+
44
+ /** The secondary texts after a row's name and badge, cut first when narrow. */
45
+ function Secondary({
46
+ parts,
47
+ selected,
48
+ }: {
49
+ parts: readonly Part[];
50
+ selected: boolean;
51
+ }) {
52
+ return (
53
+ <>
54
+ {parts.map((p) => (
55
+ <React.Fragment key={p.text}>
56
+ {" "}
57
+ <MetaText text={p.text} tone={p.tone} selected={selected} />
58
+ </React.Fragment>
59
+ ))}
60
+ </>
61
+ );
62
+ }
63
+
64
+ /**
65
+ * One row of screen 1 in `width` columns, never wrapped: the name and the
66
+ * scope badge always show, the rest is cut or dropped from the end.
67
+ */
68
+ export function renderMarketplacesRow(
35
69
  item: MarketplacesItem,
36
- _index: number,
37
70
  selected: boolean,
71
+ width: number,
38
72
  ): React.ReactNode {
73
+ const room = Math.max(1, width - ROW_INDENT);
39
74
  if (item.kind === "create" || item.kind === "connect") {
40
75
  return (
41
- <SelectableRow selected={selected} variant="action" indent={2}>
42
- {item.kind === "create" ? "+ Create…" : "+ Connect…"}
76
+ <SelectableRow selected={selected} variant="action" indent={ROW_INDENT}>
77
+ {truncate(item.kind === "create" ? "+ Create…" : "+ Connect…", room)}
43
78
  </SelectableRow>
44
79
  );
45
80
  }
46
- const { entry } = item;
47
- return (
48
- <SelectableRow selected={selected} indent={2}>
49
- <strong>{entry.name}</strong>
50
- {" "}
51
- <MetaText text={entry.repo} tone="info" selected={selected} />
52
- {" "}
53
- <MetaText
54
- text={plural(entry.plugins.length, "plugin")}
55
- selected={selected}
81
+ if (item.kind === "local-heading") {
82
+ return (
83
+ <ListCategoryRow
84
+ title="Here, in no marketplace"
85
+ count={item.count}
86
+ selected={false}
87
+ expandable={false}
88
+ width={width}
56
89
  />
90
+ );
91
+ }
92
+ if (item.kind === "local") {
93
+ const scope = item.item.scope === "user" ? "USER" : "PROJECT";
94
+ const fit = fitRow<Part>(room, item.item.name, scope.length + 4, [
95
+ { text: localKindWords(item.item), tone: "muted" },
96
+ ]);
97
+ return (
98
+ <SelectableRow selected={selected} indent={ROW_INDENT}>
99
+ <strong>{fit.name}</strong>
100
+ {" "}
101
+ <ScopeBadge scope={scope} selected={selected} />
102
+ <Secondary parts={fit.secondary} selected={selected} />
103
+ </SelectableRow>
104
+ );
105
+ }
106
+ const { entry, toPush } = item;
107
+ const parts: Part[] = [
108
+ ...(entry.company
109
+ ? [{ text: `by ${entry.company}`, tone: "muted" as const }]
110
+ : []),
111
+ ...(toPush.length > 0
112
+ ? [
113
+ {
114
+ text: `● ${plural(toPush.length, "plugin")} to push`,
115
+ tone: "warning" as const,
116
+ },
117
+ ]
118
+ : []),
119
+ ...(entry.freshness === "stale"
120
+ ? [{ text: "⚠ not current", tone: "warning" as const }]
121
+ : []),
122
+ { text: plural(entry.plugins.length, "plugin"), tone: "muted" },
123
+ { text: entry.repo, tone: "info" },
124
+ ];
125
+ const fit = fitRow(room, entry.name, "USER".length + 4, parts);
126
+ return (
127
+ <SelectableRow selected={selected} indent={ROW_INDENT}>
128
+ <strong>{fit.name}</strong>
57
129
  {" "}
58
130
  <ScopeBadge scope="USER" selected={selected} />
59
- {entry.freshness === "stale" ? (
60
- <>
61
- {" "}
62
- <MetaText text="⚠ not current" tone="warning" selected={selected} />
63
- </>
64
- ) : null}
131
+ <Secondary parts={fit.secondary} selected={selected} />
65
132
  </SelectableRow>
66
133
  );
67
134
  }
@@ -77,8 +144,15 @@ export function MarketplacesScreen({
77
144
  const modal = useModal();
78
145
  const dimensions = useDimensions();
79
146
  const value = asyncValue(market.data);
80
- const items = marketplacesItems(value?.marketplaces ?? []);
81
- const index = Math.min(market.selected.marketplaces, items.length - 1);
147
+ const items = marketplacesItems(
148
+ value?.marketplaces ?? [],
149
+ value?.status ?? [],
150
+ value?.local ?? [],
151
+ );
152
+ const clamped = Math.min(market.selected.marketplaces, items.length - 1);
153
+ const index = selectable(items[clamped])
154
+ ? clamped
155
+ : nextSelectable(items, clamped, -1);
82
156
  const item = items[index];
83
157
 
84
158
  const select = (next: number) =>
@@ -92,6 +166,10 @@ export function MarketplacesScreen({
92
166
  if (item?.kind === "marketplace") market.open(item.entry.name);
93
167
  else if (item?.kind === "create") void market.create();
94
168
  else if (item?.kind === "connect") void market.connect();
169
+ else if (item?.kind === "local") void market.addLocal(item.item);
170
+ },
171
+ add: () => {
172
+ if (item?.kind === "local") void market.addLocal(item.item);
95
173
  },
96
174
  create: () => void market.create(),
97
175
  connect: () => void market.connect(),
@@ -99,7 +177,7 @@ export function MarketplacesScreen({
99
177
  if (item?.kind === "marketplace") void market.remove(item.entry.name);
100
178
  },
101
179
  fetch: () => void market.fetch(),
102
- move: (key) => select(index + moveDirection(key)),
180
+ move: (key) => select(nextSelectable(items, index, moveDirection(key))),
103
181
  };
104
182
  useKeyLayer("screen", true, (key) =>
105
183
  runBindings(MARKETPLACES_KEYS, handlers, key),
@@ -122,13 +200,13 @@ export function MarketplacesScreen({
122
200
 
123
201
  return (
124
202
  <ScreenLayout
125
- title="magus market"
203
+ title="magus edit"
126
204
  subtitle={loading ? "reading…" : "company plugin marketplaces"}
127
205
  keymap={{
128
206
  bindings: [...MARKETPLACES_KEYS, ...MARKETPLACES_APP_KEYS],
129
207
  pinned: [MARKET_HELP],
130
208
  }}
131
- listPanel={
209
+ listPanel={(width: number) =>
132
210
  failed ? (
133
211
  <ErrorNotice
134
212
  title="Could not read the company marketplaces."
@@ -142,14 +220,21 @@ export function MarketplacesScreen({
142
220
  count={count}
143
221
  selected={false}
144
222
  expandable={false}
223
+ width={width}
145
224
  />
146
225
  <ScrollableList
147
226
  items={items}
148
227
  selectedIndex={index}
149
- renderItem={renderRow}
228
+ renderItem={(it, _i, selected) =>
229
+ renderMarketplacesRow(it, selected, width)
230
+ }
150
231
  maxHeight={Math.max(1, dimensions.listPanelHeight - HEADING_ROWS)}
151
232
  getKey={(it) =>
152
- it.kind === "marketplace" ? `m:${it.entry.name}` : it.kind
233
+ it.kind === "marketplace"
234
+ ? `m:${it.entry.name}`
235
+ : it.kind === "local"
236
+ ? `l:${it.item.scope}:${it.item.name}`
237
+ : it.kind
153
238
  }
154
239
  />
155
240
  </box>
@@ -158,6 +243,8 @@ export function MarketplacesScreen({
158
243
  detailPanel={
159
244
  <MarketplacesDetail
160
245
  item={item}
246
+ canAdd={count > 0}
247
+ cwd={market.cwd}
161
248
  note={
162
249
  item?.kind === "marketplace"
163
250
  ? market.notes[item.entry.name]
@@ -166,7 +253,11 @@ export function MarketplacesScreen({
166
253
  />
167
254
  }
168
255
  detailKey={
169
- item?.kind === "marketplace" ? `m:${item.entry.name}` : item?.kind
256
+ item?.kind === "marketplace"
257
+ ? `m:${item.entry.name}`
258
+ : item?.kind === "local"
259
+ ? `l:${item.item.scope}:${item.item.name}`
260
+ : item?.kind
170
261
  }
171
262
  />
172
263
  );
@@ -3,6 +3,7 @@ import {
3
3
  DetailSection,
4
4
  KeyValueLine,
5
5
  ScopeVersionLine,
6
+ ToneLine,
6
7
  } from "../components/primitives/index.js";
7
8
 
8
9
  const SCOPE_WORDS = [
@@ -12,7 +13,8 @@ const SCOPE_WORDS = [
12
13
  ] as const;
13
14
 
14
15
  /**
15
- * The detail panel for a company plugin's row: its version, skills and installs.
16
+ * The detail panel for a company plugin's row: its version, skills, the
17
+ * skills edited here and its installs.
16
18
  *
17
19
  * A snowflake: the Marketplace screen's own panel, over the market adapter's
18
20
  * row type and that screen's words. It is composed only from library
@@ -36,6 +38,15 @@ export function PluginDetail({
36
38
  }
37
39
  />
38
40
  </DetailSection>
41
+ {item.edited.length > 0 ? (
42
+ <DetailSection title="Edited here">
43
+ <ToneLine tone="warning">● {item.edited.join(", ")}</ToneLine>
44
+ <ToneLine>p push a new version</ToneLine>
45
+ <ToneLine tone="muted">
46
+ Every edited skill goes out together, as one new version.
47
+ </ToneLine>
48
+ </DetailSection>
49
+ ) : null}
39
50
  <DetailSection title="Installed">
40
51
  {SCOPE_WORDS.map(([scope, word]) => (
41
52
  <ScopeVersionLine
@@ -33,6 +33,13 @@ export const MARKETPLACES_KEYS = [
33
33
  "Connect an existing repository as a company marketplace",
34
34
  75,
35
35
  ),
36
+ bind(
37
+ "add",
38
+ ["a"],
39
+ "add",
40
+ "Add the skill or plugin folder here to a company marketplace",
41
+ 70,
42
+ ),
36
43
  bind(
37
44
  "remove",
38
45
  ["d"],
@@ -59,7 +66,13 @@ export const MARKETPLACE_KEYS = [
59
66
  "Show or hide the plugin's skills",
60
67
  90,
61
68
  ),
62
- bind("publish", ["p"], "publish", "Publish this project's edits", 88),
69
+ bind(
70
+ "publish",
71
+ ["p"],
72
+ "publish",
73
+ "Publish edits: the skill's, or on a plugin every edited skill as one new version",
74
+ 88,
75
+ ),
63
76
  bind(
64
77
  "add",
65
78
  ["a"],
@@ -134,6 +147,16 @@ export const MARKETPLACE_APP_KEYS = [
134
147
 
135
148
  export type AppKeyId = (typeof MARKETPLACES_APP_KEYS)[number]["id"];
136
149
 
150
+ /** What the marks on screen 1 mean. */
151
+ const MARKETPLACES_LEGEND = [
152
+ { symbol: "●", meaning: "plugins with skills edited here, ready to push" },
153
+ {
154
+ symbol: "USER",
155
+ meaning:
156
+ "under Here, in no marketplace: your USER skills; PROJECT is this project's",
157
+ },
158
+ ] as const;
159
+
137
160
  /** What the glyphs on screen 2 mean. */
138
161
  const MARKETPLACE_LEGEND = [
139
162
  {
@@ -141,7 +164,10 @@ const MARKETPLACE_LEGEND = [
141
164
  meaning: "installed at that scope; the squares are user, project, local",
142
165
  },
143
166
  { symbol: "□", meaning: "not installed at that scope" },
144
- { symbol: "●", meaning: "edited here, ready to publish" },
167
+ {
168
+ symbol: "●",
169
+ meaning: "edited here, ready to publish; on a plugin, how many skills",
170
+ },
145
171
  { symbol: "◇", meaning: "edited here, origin unknown; publish asks twice" },
146
172
  { symbol: "⚠", meaning: "changed here and on GitHub; publish asks twice" },
147
173
  { symbol: "↓", meaning: "the plugin has a newer version than this copy" },
@@ -155,6 +181,7 @@ export function marketHelpSections(
155
181
  const one: HelpSection = {
156
182
  title: "Company marketplaces",
157
183
  bindings: MARKETPLACES_KEYS,
184
+ legend: MARKETPLACES_LEGEND,
158
185
  };
159
186
  const two: HelpSection = {
160
187
  title: "A marketplace",