magus-cli 7.12.2 → 7.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/package.json +4 -4
- package/src/__tests__/market-bases.test.ts +112 -0
- package/src/__tests__/market-config.test.ts +84 -0
- package/src/__tests__/market-conflicts.test.ts +175 -0
- package/src/__tests__/market-consent.test.ts +185 -0
- package/src/__tests__/market-content.test.ts +183 -0
- package/src/__tests__/market-delivery.test.ts +308 -0
- package/src/__tests__/market-fixture.ts +1167 -0
- package/src/__tests__/market-git.test.ts +130 -0
- package/src/__tests__/market-identity.test.ts +127 -0
- package/src/__tests__/market-install.test.ts +143 -0
- package/src/__tests__/market-layout.test.ts +206 -0
- package/src/__tests__/market-publish.test.ts +263 -0
- package/src/__tests__/market-r2-adapters.test.ts +190 -0
- package/src/__tests__/market-r2-company-cli.test.ts +350 -0
- package/src/__tests__/market-r2-company-unit.test.ts +224 -0
- package/src/__tests__/market-r2-local-items.test.ts +197 -0
- package/src/__tests__/market-r2-plugin-add.test.ts +470 -0
- package/src/__tests__/market-r2-plugin-publish.test.ts +568 -0
- package/src/__tests__/market-r2-routing.test.ts +127 -0
- package/src/__tests__/market-r2-skill-create-plugin.test.ts +315 -0
- package/src/__tests__/market-r2b-display-path.test.ts +180 -0
- package/src/__tests__/market-r2b-fit-row.test.ts +287 -0
- package/src/__tests__/market-r2b-local-items.test.ts +253 -0
- package/src/__tests__/market-r2b-plugin-add.test.ts +130 -0
- package/src/__tests__/market-readonly.test.ts +162 -0
- package/src/__tests__/market-repositories.test.ts +263 -0
- package/src/__tests__/market-results.test.ts +198 -0
- package/src/__tests__/market-router.test.ts +65 -0
- package/src/__tests__/market-safety.test.ts +357 -0
- package/src/__tests__/market-teammates.test.ts +304 -0
- package/src/cli/install.ts +64 -5
- package/src/cli/market-view.ts +329 -0
- package/src/cli/market.ts +664 -0
- package/src/cli/router.ts +69 -3
- package/src/cli/update-view.ts +56 -0
- package/src/cli/update.ts +111 -9
- package/src/data/marketplaces.ts +8 -2
- package/src/main.tsx +20 -2
- package/src/market-tui.tsx +21 -0
- package/src/services/active-closure.ts +71 -9
- package/src/services/claude-cli.ts +140 -10
- package/src/services/claude-settings.ts +19 -25
- package/src/services/doctor.ts +5 -5
- package/src/services/git-write.ts +615 -0
- package/src/services/github-cli.ts +309 -0
- package/src/services/local-marketplace.ts +112 -50
- package/src/services/market-bases.ts +191 -0
- package/src/services/market-change.ts +242 -0
- package/src/services/market-clone.ts +572 -0
- package/src/services/market-fs.ts +138 -0
- package/src/services/market-install.ts +172 -0
- package/src/services/market-layout.ts +311 -0
- package/src/services/market-local.ts +93 -0
- package/src/services/market-marker.ts +49 -0
- package/src/services/market-ops-content.ts +277 -0
- package/src/services/market-ops-plugin.ts +655 -0
- package/src/services/market-ops-publish.ts +1227 -0
- package/src/services/market-ops-repo.ts +916 -0
- package/src/services/market-ops-skill.ts +788 -0
- package/src/services/market-ops.ts +686 -0
- package/src/services/market-publish.ts +468 -0
- package/src/services/market-registry.ts +382 -0
- package/src/services/market-types.ts +268 -0
- package/src/services/marketplace-conflicts.ts +127 -0
- package/src/services/marketplace-refresh.ts +33 -3
- package/src/services/marketplace-sync.ts +253 -22
- package/src/services/plugin-dependencies.ts +17 -0
- package/src/services/plugin-manager.ts +1 -1
- package/src/services/plugin-mcp-config.ts +6 -9
- package/src/services/plugin-requires.ts +20 -8
- package/src/services/profile-absorb.ts +88 -3
- package/src/services/profile-adopt.ts +10 -1
- package/src/services/profiles.ts +64 -1
- package/src/services/resolver.ts +17 -5
- package/src/services/settings-manager.ts +2 -2
- package/src/services/skills-manager.ts +2 -2
- package/src/services/update-plan.ts +67 -8
- package/src/tui-runtime.tsx +142 -0
- package/src/tui.tsx +7 -124
- package/src/types/index.ts +7 -0
- package/src/ui/adapters/marketAdapter.ts +366 -0
- package/src/ui/adapters/pluginsAdapter.ts +45 -3
- package/src/ui/components/layout/ScreenLayout.tsx +43 -17
- package/src/ui/components/modals/ConfirmModal.tsx +145 -8
- package/src/ui/components/modals/HelpOverlay.tsx +10 -3
- package/src/ui/components/modals/ModalContainer.tsx +47 -5
- package/src/ui/components/primitives/ListCategoryRow.tsx +22 -2
- package/src/ui/components/primitives/ScopeBadge.tsx +28 -0
- package/src/ui/components/primitives/ScopeVersionLine.tsx +27 -0
- package/src/ui/components/primitives/SelectableRow.tsx +14 -1
- package/src/ui/components/primitives/ToneLine.tsx +25 -0
- package/src/ui/components/primitives/index.ts +5 -0
- package/src/ui/keymap.ts +21 -5
- package/src/ui/market/MarketApp.tsx +59 -0
- package/src/ui/market/MarketplaceScreen.tsx +280 -0
- package/src/ui/market/MarketplacesDetail.snowflake.tsx +161 -0
- package/src/ui/market/MarketplacesScreen.tsx +264 -0
- package/src/ui/market/PluginDetail.snowflake.tsx +61 -0
- package/src/ui/market/SkillDetail.snowflake.tsx +71 -0
- package/src/ui/market/marketKeymap.ts +243 -0
- package/src/ui/market/useMarketState.ts +694 -0
- package/src/ui/renderers/pluginRenderers.tsx +31 -3
- package/src/ui/renderers/pluginStatusText.ts +6 -0
- package/src/ui/screens/PluginsScreen.tsx +49 -5
- package/src/ui/screens/SkillsScreen.tsx +2 -2
- package/src/ui/state/AppContext.tsx +14 -2
- package/src/ui/state/types.ts +9 -2
- package/src/utils/config-dir.ts +29 -9
- package/src/utils/display-path.ts +33 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
type MarketplacesItem,
|
|
4
|
+
fitRow,
|
|
5
|
+
localKindWords,
|
|
6
|
+
marketplacesItems,
|
|
7
|
+
nextSelectable,
|
|
8
|
+
plural,
|
|
9
|
+
selectable,
|
|
10
|
+
} from "../adapters/marketAdapter.js";
|
|
11
|
+
import { ScrollableList } from "../components/ScrollableList.js";
|
|
12
|
+
import { ScreenLayout } from "../components/layout/index.js";
|
|
13
|
+
import {
|
|
14
|
+
ErrorNotice,
|
|
15
|
+
ListCategoryRow,
|
|
16
|
+
MetaText,
|
|
17
|
+
ScopeBadge,
|
|
18
|
+
SelectableRow,
|
|
19
|
+
} from "../components/primitives/index.js";
|
|
20
|
+
import { useKeyLayer } from "../input/KeyRouter.js";
|
|
21
|
+
import { type KeyHandler, moveDirection, runBindings } from "../keymap.js";
|
|
22
|
+
import { useModal } from "../state/AppContext.js";
|
|
23
|
+
import { useDimensions } from "../state/DimensionsContext.js";
|
|
24
|
+
import { asyncValue } from "../state/types.js";
|
|
25
|
+
import { truncate } from "../text-width.js";
|
|
26
|
+
import { MarketplacesDetail } from "./MarketplacesDetail.snowflake.js";
|
|
27
|
+
import {
|
|
28
|
+
MARKETPLACES_APP_KEYS,
|
|
29
|
+
MARKETPLACES_KEYS,
|
|
30
|
+
MARKET_HELP,
|
|
31
|
+
type MarketplacesKeyId,
|
|
32
|
+
marketHelpSections,
|
|
33
|
+
} from "./marketKeymap.js";
|
|
34
|
+
import type { MarketState } from "./useMarketState.js";
|
|
35
|
+
|
|
36
|
+
/** Rows above the list: the heading. */
|
|
37
|
+
const HEADING_ROWS = 1;
|
|
38
|
+
|
|
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(
|
|
69
|
+
item: MarketplacesItem,
|
|
70
|
+
selected: boolean,
|
|
71
|
+
width: number,
|
|
72
|
+
): React.ReactNode {
|
|
73
|
+
const room = Math.max(1, width - ROW_INDENT);
|
|
74
|
+
if (item.kind === "create" || item.kind === "connect") {
|
|
75
|
+
return (
|
|
76
|
+
<SelectableRow selected={selected} variant="action" indent={ROW_INDENT}>
|
|
77
|
+
{truncate(item.kind === "create" ? "+ Create…" : "+ Connect…", room)}
|
|
78
|
+
</SelectableRow>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
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}
|
|
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>
|
|
129
|
+
{" "}
|
|
130
|
+
<ScopeBadge scope="USER" selected={selected} />
|
|
131
|
+
<Secondary parts={fit.secondary} selected={selected} />
|
|
132
|
+
</SelectableRow>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Screen 1: the company marketplaces on this machine. */
|
|
137
|
+
export function MarketplacesScreen({
|
|
138
|
+
market,
|
|
139
|
+
onExit,
|
|
140
|
+
}: {
|
|
141
|
+
market: MarketState;
|
|
142
|
+
onExit: () => void;
|
|
143
|
+
}) {
|
|
144
|
+
const modal = useModal();
|
|
145
|
+
const dimensions = useDimensions();
|
|
146
|
+
const value = asyncValue(market.data);
|
|
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);
|
|
156
|
+
const item = items[index];
|
|
157
|
+
|
|
158
|
+
const select = (next: number) =>
|
|
159
|
+
market.setSelected((s) => ({
|
|
160
|
+
...s,
|
|
161
|
+
marketplaces: Math.max(0, Math.min(items.length - 1, next)),
|
|
162
|
+
}));
|
|
163
|
+
|
|
164
|
+
const handlers: Record<MarketplacesKeyId, KeyHandler> = {
|
|
165
|
+
open: () => {
|
|
166
|
+
if (item?.kind === "marketplace") market.open(item.entry.name);
|
|
167
|
+
else if (item?.kind === "create") void market.create();
|
|
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);
|
|
173
|
+
},
|
|
174
|
+
create: () => void market.create(),
|
|
175
|
+
connect: () => void market.connect(),
|
|
176
|
+
remove: () => {
|
|
177
|
+
if (item?.kind === "marketplace") void market.remove(item.entry.name);
|
|
178
|
+
},
|
|
179
|
+
fetch: () => void market.fetch(),
|
|
180
|
+
move: (key) => select(nextSelectable(items, index, moveDirection(key))),
|
|
181
|
+
};
|
|
182
|
+
useKeyLayer("screen", true, (key) =>
|
|
183
|
+
runBindings(MARKETPLACES_KEYS, handlers, key),
|
|
184
|
+
);
|
|
185
|
+
useKeyLayer("global", true, (key) =>
|
|
186
|
+
runBindings(
|
|
187
|
+
MARKETPLACES_APP_KEYS,
|
|
188
|
+
{
|
|
189
|
+
help: () => modal.helpSections(marketHelpSections("marketplaces")),
|
|
190
|
+
quit: onExit,
|
|
191
|
+
back: onExit,
|
|
192
|
+
},
|
|
193
|
+
key,
|
|
194
|
+
),
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const count = value?.marketplaces.length ?? 0;
|
|
198
|
+
const loading = market.data.status === "loading";
|
|
199
|
+
const failed = market.data.status === "error" ? market.data.error : null;
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<ScreenLayout
|
|
203
|
+
title="magus edit"
|
|
204
|
+
subtitle={loading ? "reading…" : "company plugin marketplaces"}
|
|
205
|
+
keymap={{
|
|
206
|
+
bindings: [...MARKETPLACES_KEYS, ...MARKETPLACES_APP_KEYS],
|
|
207
|
+
pinned: [MARKET_HELP],
|
|
208
|
+
}}
|
|
209
|
+
listPanel={(width: number) =>
|
|
210
|
+
failed ? (
|
|
211
|
+
<ErrorNotice
|
|
212
|
+
title="Could not read the company marketplaces."
|
|
213
|
+
detail={failed.message}
|
|
214
|
+
hint="r fetches from GitHub and reads them again."
|
|
215
|
+
/>
|
|
216
|
+
) : (
|
|
217
|
+
<box flexDirection="column">
|
|
218
|
+
<ListCategoryRow
|
|
219
|
+
title="Company marketplaces"
|
|
220
|
+
count={count}
|
|
221
|
+
selected={false}
|
|
222
|
+
expandable={false}
|
|
223
|
+
width={width}
|
|
224
|
+
/>
|
|
225
|
+
<ScrollableList
|
|
226
|
+
items={items}
|
|
227
|
+
selectedIndex={index}
|
|
228
|
+
renderItem={(it, _i, selected) =>
|
|
229
|
+
renderMarketplacesRow(it, selected, width)
|
|
230
|
+
}
|
|
231
|
+
maxHeight={Math.max(1, dimensions.listPanelHeight - HEADING_ROWS)}
|
|
232
|
+
getKey={(it) =>
|
|
233
|
+
it.kind === "marketplace"
|
|
234
|
+
? `m:${it.entry.name}`
|
|
235
|
+
: it.kind === "local"
|
|
236
|
+
? `l:${it.item.scope}:${it.item.name}`
|
|
237
|
+
: it.kind
|
|
238
|
+
}
|
|
239
|
+
/>
|
|
240
|
+
</box>
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
detailPanel={
|
|
244
|
+
<MarketplacesDetail
|
|
245
|
+
item={item}
|
|
246
|
+
canAdd={count > 0}
|
|
247
|
+
cwd={market.cwd}
|
|
248
|
+
note={
|
|
249
|
+
item?.kind === "marketplace"
|
|
250
|
+
? market.notes[item.entry.name]
|
|
251
|
+
: undefined
|
|
252
|
+
}
|
|
253
|
+
/>
|
|
254
|
+
}
|
|
255
|
+
detailKey={
|
|
256
|
+
item?.kind === "marketplace"
|
|
257
|
+
? `m:${item.entry.name}`
|
|
258
|
+
: item?.kind === "local"
|
|
259
|
+
? `l:${item.item.scope}:${item.item.name}`
|
|
260
|
+
: item?.kind
|
|
261
|
+
}
|
|
262
|
+
/>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { MarketplaceItem } from "../adapters/marketAdapter.js";
|
|
2
|
+
import {
|
|
3
|
+
DetailSection,
|
|
4
|
+
KeyValueLine,
|
|
5
|
+
ScopeVersionLine,
|
|
6
|
+
ToneLine,
|
|
7
|
+
} from "../components/primitives/index.js";
|
|
8
|
+
|
|
9
|
+
const SCOPE_WORDS = [
|
|
10
|
+
["user", "USER"],
|
|
11
|
+
["project", "PROJECT"],
|
|
12
|
+
["local", "LOCAL"],
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The detail panel for a company plugin's row: its version, skills, the
|
|
17
|
+
* skills edited here and its installs.
|
|
18
|
+
*
|
|
19
|
+
* A snowflake: the Marketplace screen's own panel, over the market adapter's
|
|
20
|
+
* row type and that screen's words. It is composed only from library
|
|
21
|
+
* primitives and sets no colour, border or text style of its own.
|
|
22
|
+
*/
|
|
23
|
+
export function PluginDetail({
|
|
24
|
+
item,
|
|
25
|
+
}: {
|
|
26
|
+
item: Extract<MarketplaceItem, { kind: "plugin" }>;
|
|
27
|
+
}) {
|
|
28
|
+
return (
|
|
29
|
+
<box flexDirection="column">
|
|
30
|
+
<DetailSection title={item.ref}>
|
|
31
|
+
<KeyValueLine label="version" value={item.version} />
|
|
32
|
+
<KeyValueLine
|
|
33
|
+
label="skills"
|
|
34
|
+
value={
|
|
35
|
+
item.skills.length > 0
|
|
36
|
+
? item.skills.join(", ")
|
|
37
|
+
: "none yet — a adds one"
|
|
38
|
+
}
|
|
39
|
+
/>
|
|
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}
|
|
50
|
+
<DetailSection title="Installed">
|
|
51
|
+
{SCOPE_WORDS.map(([scope, word]) => (
|
|
52
|
+
<ScopeVersionLine
|
|
53
|
+
key={scope}
|
|
54
|
+
scope={word}
|
|
55
|
+
version={item.installs[scope]}
|
|
56
|
+
/>
|
|
57
|
+
))}
|
|
58
|
+
</DetailSection>
|
|
59
|
+
</box>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type MarketplaceItem,
|
|
3
|
+
copyWords,
|
|
4
|
+
publishable,
|
|
5
|
+
skillMark,
|
|
6
|
+
} from "../adapters/marketAdapter.js";
|
|
7
|
+
import {
|
|
8
|
+
DetailSection,
|
|
9
|
+
KeyValueLine,
|
|
10
|
+
MetaText,
|
|
11
|
+
ScopeBadge,
|
|
12
|
+
ToneLine,
|
|
13
|
+
} from "../components/primitives/index.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The detail panel for a company skill's row: which plugin holds it, the
|
|
17
|
+
* local copy here and how it stands, and what publishing would do.
|
|
18
|
+
*
|
|
19
|
+
* A snowflake: the Marketplace screen's own panel, over the market adapter's
|
|
20
|
+
* row type and that screen's words. It is composed only from library
|
|
21
|
+
* primitives and sets no colour, border or text style of its own.
|
|
22
|
+
*/
|
|
23
|
+
export function SkillDetail({
|
|
24
|
+
item,
|
|
25
|
+
}: {
|
|
26
|
+
item: Extract<MarketplaceItem, { kind: "skill" }>;
|
|
27
|
+
}) {
|
|
28
|
+
const mark = skillMark(item.local);
|
|
29
|
+
const local = item.local;
|
|
30
|
+
return (
|
|
31
|
+
<box flexDirection="column">
|
|
32
|
+
<DetailSection title={item.skill}>
|
|
33
|
+
<KeyValueLine label="plugin" value={item.ref} />
|
|
34
|
+
<KeyValueLine
|
|
35
|
+
label="here"
|
|
36
|
+
value={
|
|
37
|
+
local ? (
|
|
38
|
+
<>
|
|
39
|
+
<ScopeBadge
|
|
40
|
+
scope={local.from === "user" ? "USER" : "PROJECT"}
|
|
41
|
+
/>{" "}
|
|
42
|
+
{copyWords(local)}
|
|
43
|
+
</>
|
|
44
|
+
) : (
|
|
45
|
+
<MetaText text="no copy in this project — e copies it here to edit" />
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
/>
|
|
49
|
+
{mark ? (
|
|
50
|
+
<ToneLine tone={mark.tone}>
|
|
51
|
+
{mark.glyph} {mark.words}
|
|
52
|
+
</ToneLine>
|
|
53
|
+
) : local ? (
|
|
54
|
+
<ToneLine tone="muted">the same as the plugin's</ToneLine>
|
|
55
|
+
) : null}
|
|
56
|
+
{local?.reason ? (
|
|
57
|
+
<ToneLine tone="muted">{local.reason}</ToneLine>
|
|
58
|
+
) : null}
|
|
59
|
+
</DetailSection>
|
|
60
|
+
{publishable(local) ? (
|
|
61
|
+
<DetailSection title="Publish">
|
|
62
|
+
<ToneLine tone="muted">
|
|
63
|
+
{local?.state === "edited"
|
|
64
|
+
? "p shows what publishing changes, then asks."
|
|
65
|
+
: "p shows both versions and asks twice before replacing the plugin's."}
|
|
66
|
+
</ToneLine>
|
|
67
|
+
</DetailSection>
|
|
68
|
+
) : null}
|
|
69
|
+
</box>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every key the market screens bind, in one place: the footers, the help
|
|
3
|
+
* overlay and the key handling all read this file, the way the main TUI reads
|
|
4
|
+
* ui/keymap.ts. Bindings are built with the same `bind` and matched by the
|
|
5
|
+
* same `runBindings`, so a key means the same thing in both apps.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
HELP_ONLY,
|
|
10
|
+
type HelpSection,
|
|
11
|
+
type KeyBinding,
|
|
12
|
+
SCROLL_BINDINGS,
|
|
13
|
+
bind,
|
|
14
|
+
runBindings,
|
|
15
|
+
} from "../keymap.js";
|
|
16
|
+
|
|
17
|
+
const MOVE = bind(
|
|
18
|
+
"move",
|
|
19
|
+
["↑", "↓", "j", "k"],
|
|
20
|
+
"move",
|
|
21
|
+
"Move the selection",
|
|
22
|
+
20,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
/** Screen 1: the company marketplaces on this machine. */
|
|
26
|
+
export const MARKETPLACES_KEYS = [
|
|
27
|
+
bind("open", ["Enter"], "open", "Open the marketplace, or start the row", 90),
|
|
28
|
+
bind("create", ["c"], "create", "Create a company marketplace on GitHub", 80),
|
|
29
|
+
bind(
|
|
30
|
+
"connect",
|
|
31
|
+
["o"],
|
|
32
|
+
"connect",
|
|
33
|
+
"Connect an existing repository as a company marketplace",
|
|
34
|
+
75,
|
|
35
|
+
),
|
|
36
|
+
bind(
|
|
37
|
+
"add",
|
|
38
|
+
["a"],
|
|
39
|
+
"add",
|
|
40
|
+
"Add the skill or plugin folder here to a company marketplace",
|
|
41
|
+
70,
|
|
42
|
+
),
|
|
43
|
+
bind(
|
|
44
|
+
"remove",
|
|
45
|
+
["d"],
|
|
46
|
+
"remove",
|
|
47
|
+
"Remove the marketplace from this machine (the repository stays)",
|
|
48
|
+
60,
|
|
49
|
+
),
|
|
50
|
+
bind(
|
|
51
|
+
"fetch",
|
|
52
|
+
["r"],
|
|
53
|
+
"fetch",
|
|
54
|
+
"Fetch what changed on GitHub in every company marketplace",
|
|
55
|
+
50,
|
|
56
|
+
),
|
|
57
|
+
MOVE,
|
|
58
|
+
] as const;
|
|
59
|
+
|
|
60
|
+
/** Screen 2: one marketplace's plugins, each opening to its skills. */
|
|
61
|
+
export const MARKETPLACE_KEYS = [
|
|
62
|
+
bind(
|
|
63
|
+
"expand",
|
|
64
|
+
["Enter", "→"],
|
|
65
|
+
"expand",
|
|
66
|
+
"Show or hide the plugin's skills",
|
|
67
|
+
90,
|
|
68
|
+
),
|
|
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
|
+
),
|
|
76
|
+
bind(
|
|
77
|
+
"add",
|
|
78
|
+
["a"],
|
|
79
|
+
"add skill",
|
|
80
|
+
"Add a skill from this project or your USER skills",
|
|
81
|
+
85,
|
|
82
|
+
),
|
|
83
|
+
bind(
|
|
84
|
+
"edit",
|
|
85
|
+
["e"],
|
|
86
|
+
"edit here",
|
|
87
|
+
"Copy the skill into this project to edit it",
|
|
88
|
+
80,
|
|
89
|
+
),
|
|
90
|
+
bind(
|
|
91
|
+
"remove-skill",
|
|
92
|
+
["x"],
|
|
93
|
+
"remove skill",
|
|
94
|
+
"Remove the skill from its plugin",
|
|
95
|
+
70,
|
|
96
|
+
),
|
|
97
|
+
bind("new", ["n"], "new plugin", "Create a plugin in this marketplace", 65),
|
|
98
|
+
bind(
|
|
99
|
+
"delete",
|
|
100
|
+
["D"],
|
|
101
|
+
"delete plugin",
|
|
102
|
+
"Delete the plugin from this marketplace",
|
|
103
|
+
55,
|
|
104
|
+
),
|
|
105
|
+
bind(
|
|
106
|
+
"fetch",
|
|
107
|
+
["r"],
|
|
108
|
+
"fetch",
|
|
109
|
+
"Fetch what changed on GitHub in this marketplace",
|
|
110
|
+
HELP_ONLY,
|
|
111
|
+
),
|
|
112
|
+
MOVE,
|
|
113
|
+
] as const;
|
|
114
|
+
|
|
115
|
+
export type MarketplacesKeyId = (typeof MARKETPLACES_KEYS)[number]["id"];
|
|
116
|
+
export type MarketplaceKeyId = (typeof MARKETPLACE_KEYS)[number]["id"];
|
|
117
|
+
|
|
118
|
+
export const MARKET_HELP = bind(
|
|
119
|
+
"help",
|
|
120
|
+
["?"],
|
|
121
|
+
"help",
|
|
122
|
+
"Show every key (this list)",
|
|
123
|
+
100,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const QUIT = bind(
|
|
127
|
+
"quit",
|
|
128
|
+
["q"],
|
|
129
|
+
"quit",
|
|
130
|
+
"Quit (Ctrl+C quits from anywhere, even a dialog)",
|
|
131
|
+
30,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
/** The app keys on screen 1: Esc quits, as `q` does. */
|
|
135
|
+
export const MARKETPLACES_APP_KEYS = [
|
|
136
|
+
MARKET_HELP,
|
|
137
|
+
QUIT,
|
|
138
|
+
bind("back", ["Esc"], "quit", "Quit", HELP_ONLY),
|
|
139
|
+
] as const;
|
|
140
|
+
|
|
141
|
+
/** The app keys on screen 2: Esc goes back to the marketplaces. */
|
|
142
|
+
export const MARKETPLACE_APP_KEYS = [
|
|
143
|
+
MARKET_HELP,
|
|
144
|
+
QUIT,
|
|
145
|
+
bind("back", ["Esc"], "back", "Back to the company marketplaces", 35),
|
|
146
|
+
] as const;
|
|
147
|
+
|
|
148
|
+
export type AppKeyId = (typeof MARKETPLACES_APP_KEYS)[number]["id"];
|
|
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
|
+
|
|
160
|
+
/** What the glyphs on screen 2 mean. */
|
|
161
|
+
const MARKETPLACE_LEGEND = [
|
|
162
|
+
{
|
|
163
|
+
symbol: "■",
|
|
164
|
+
meaning: "installed at that scope; the squares are user, project, local",
|
|
165
|
+
},
|
|
166
|
+
{ symbol: "□", meaning: "not installed at that scope" },
|
|
167
|
+
{
|
|
168
|
+
symbol: "●",
|
|
169
|
+
meaning: "edited here, ready to publish; on a plugin, how many skills",
|
|
170
|
+
},
|
|
171
|
+
{ symbol: "◇", meaning: "edited here, origin unknown; publish asks twice" },
|
|
172
|
+
{ symbol: "⚠", meaning: "changed here and on GitHub; publish asks twice" },
|
|
173
|
+
{ symbol: "↓", meaning: "the plugin has a newer version than this copy" },
|
|
174
|
+
{ symbol: "?", meaning: "not checked against GitHub; r fetches" },
|
|
175
|
+
] as const;
|
|
176
|
+
|
|
177
|
+
/** The help overlay for the screen that is showing, its own keys first. */
|
|
178
|
+
export function marketHelpSections(
|
|
179
|
+
screen: "marketplaces" | "marketplace",
|
|
180
|
+
): HelpSection[] {
|
|
181
|
+
const one: HelpSection = {
|
|
182
|
+
title: "Company marketplaces",
|
|
183
|
+
bindings: MARKETPLACES_KEYS,
|
|
184
|
+
legend: MARKETPLACES_LEGEND,
|
|
185
|
+
};
|
|
186
|
+
const two: HelpSection = {
|
|
187
|
+
title: "A marketplace",
|
|
188
|
+
bindings: MARKETPLACE_KEYS,
|
|
189
|
+
legend: MARKETPLACE_LEGEND,
|
|
190
|
+
};
|
|
191
|
+
const [first, second] = screen === "marketplaces" ? [one, two] : [two, one];
|
|
192
|
+
return [
|
|
193
|
+
{ ...first, title: `${first.title} (this screen)` },
|
|
194
|
+
second,
|
|
195
|
+
{
|
|
196
|
+
title: "Everywhere",
|
|
197
|
+
bindings:
|
|
198
|
+
screen === "marketplaces"
|
|
199
|
+
? MARKETPLACES_APP_KEYS
|
|
200
|
+
: MARKETPLACE_APP_KEYS,
|
|
201
|
+
},
|
|
202
|
+
{ title: "Scrolling the right-hand panel", bindings: SCROLL_BINDINGS },
|
|
203
|
+
];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Refuse a keymap that claims one key twice on a screen, counting the app
|
|
208
|
+
* keys and the scroll keys. Runs when the module loads.
|
|
209
|
+
*/
|
|
210
|
+
function assertMarketKeymap(): void {
|
|
211
|
+
const screens: Array<readonly KeyBinding[]> = [
|
|
212
|
+
[...MARKETPLACES_KEYS, ...MARKETPLACES_APP_KEYS],
|
|
213
|
+
[...MARKETPLACE_KEYS, ...MARKETPLACE_APP_KEYS],
|
|
214
|
+
];
|
|
215
|
+
const tokens = (b: KeyBinding) =>
|
|
216
|
+
b.keys.flatMap((k) => (k === "/" ? [k] : k.split("/")));
|
|
217
|
+
for (const bindings of screens) {
|
|
218
|
+
const seen = new Map<string, string>();
|
|
219
|
+
for (const binding of [...bindings, ...SCROLL_BINDINGS]) {
|
|
220
|
+
// Parsing every key once is what makes a typo fail at load.
|
|
221
|
+
runBindings([binding], {}, NO_KEY);
|
|
222
|
+
for (const token of tokens(binding)) {
|
|
223
|
+
const owner = seen.get(token);
|
|
224
|
+
if (owner) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`market keymap: "${token}" is claimed by ${owner} and ${binding.id}`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
seen.set(token, binding.id);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const NO_KEY = {
|
|
236
|
+
name: "",
|
|
237
|
+
sequence: "",
|
|
238
|
+
ctrl: false,
|
|
239
|
+
meta: false,
|
|
240
|
+
shift: false,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
assertMarketKeymap();
|