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.
- package/README.md +7 -4
- package/package.json +4 -4
- 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-router.test.ts +4 -4
- package/src/__tests__/scope-uninstall-wrong-scope.test.ts +341 -0
- package/src/cli/market-view.ts +59 -18
- package/src/cli/market.ts +128 -44
- package/src/cli/router.ts +42 -10
- package/src/data/marketplaces.ts +1 -0
- package/src/main.tsx +1 -1
- package/src/market-tui.tsx +1 -1
- package/src/services/claude-cli.ts +13 -0
- package/src/services/local-marketplace.ts +8 -0
- package/src/services/market-change.ts +6 -6
- package/src/services/market-local.ts +93 -0
- package/src/services/market-ops-plugin.ts +366 -47
- package/src/services/market-ops-publish.ts +363 -15
- package/src/services/market-ops-repo.ts +94 -13
- package/src/services/market-ops-skill.ts +93 -11
- package/src/services/market-ops.ts +72 -8
- package/src/services/market-publish.ts +10 -1
- package/src/services/market-types.ts +23 -0
- package/src/services/update-engine.ts +97 -1
- package/src/tui-runtime.tsx +2 -2
- package/src/types/index.ts +2 -0
- package/src/ui/adapters/marketAdapter.ts +123 -4
- package/src/ui/adapters/pluginsAdapter.ts +25 -1
- package/src/ui/components/primitives/ListCategoryRow.tsx +22 -2
- package/src/ui/market/MarketplaceScreen.tsx +55 -24
- package/src/ui/market/MarketplacesDetail.snowflake.tsx +60 -4
- package/src/ui/market/MarketplacesScreen.tsx +120 -29
- package/src/ui/market/PluginDetail.snowflake.tsx +12 -1
- package/src/ui/market/marketKeymap.ts +29 -2
- package/src/ui/market/useMarketState.ts +146 -5
- package/src/utils/display-path.ts +33 -0
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
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={
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
{
|
|
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(
|
|
81
|
-
|
|
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
|
|
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
|
|
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={
|
|
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"
|
|
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"
|
|
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
|
|
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(
|
|
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
|
-
{
|
|
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",
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* The market screens' state and actions.
|
|
3
3
|
*
|
|
4
4
|
* Reading is read-only: the overview (`marketOverview`), this project's skill
|
|
5
|
-
* copies (`marketStatus`)
|
|
5
|
+
* copies (`marketStatus`), the installs (`installedVersionAt`) and the
|
|
6
|
+
* skills and plugin folders here that no marketplace holds (`localItems`)
|
|
6
7
|
* write nothing, so opening the screens changes nothing on the machine.
|
|
7
8
|
*
|
|
8
9
|
* Every change is a planner from the market services, shown in a confirm
|
|
@@ -14,13 +15,16 @@
|
|
|
14
15
|
|
|
15
16
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
16
17
|
import { viewerLogin, viewerOrgs } from "../../services/github-cli.js";
|
|
18
|
+
import { localItems } from "../../services/market-local.js";
|
|
17
19
|
import {
|
|
20
|
+
planPluginAdd,
|
|
18
21
|
planPluginCreate,
|
|
19
22
|
planPluginDelete,
|
|
20
23
|
} from "../../services/market-ops-plugin.js";
|
|
21
24
|
import {
|
|
22
25
|
localSkillFolders,
|
|
23
26
|
marketStatus,
|
|
27
|
+
planPluginPublish,
|
|
24
28
|
planPublish,
|
|
25
29
|
} from "../../services/market-ops-publish.js";
|
|
26
30
|
import {
|
|
@@ -36,6 +40,8 @@ import {
|
|
|
36
40
|
import {
|
|
37
41
|
type MarketOverviewEntry,
|
|
38
42
|
applyMarketPlan,
|
|
43
|
+
companyPluginNames,
|
|
44
|
+
companySkillNames,
|
|
39
45
|
errorResult,
|
|
40
46
|
marketFetch,
|
|
41
47
|
marketOverview,
|
|
@@ -44,6 +50,7 @@ import {
|
|
|
44
50
|
import { installedVersionAt } from "../../services/market-publish.js";
|
|
45
51
|
import type {
|
|
46
52
|
InstallScope,
|
|
53
|
+
LocalItem,
|
|
47
54
|
MarketResult,
|
|
48
55
|
PlanAnswer,
|
|
49
56
|
SkillStatusRow,
|
|
@@ -51,10 +58,11 @@ import type {
|
|
|
51
58
|
import {
|
|
52
59
|
type ScopeVersions,
|
|
53
60
|
marketFailureReport,
|
|
61
|
+
plural,
|
|
54
62
|
resultNote,
|
|
55
63
|
} from "../adapters/marketAdapter.js";
|
|
56
64
|
import { useApp, useFailurePopup, useModal } from "../state/AppContext.js";
|
|
57
|
-
import type
|
|
65
|
+
import { type AsyncData, asyncValue } from "../state/types.js";
|
|
58
66
|
|
|
59
67
|
export type MarketRoute =
|
|
60
68
|
| { screen: "marketplaces" }
|
|
@@ -66,6 +74,8 @@ export interface MarketData {
|
|
|
66
74
|
status: SkillStatusRow[];
|
|
67
75
|
/** Installed versions by `plugin@marketplace`. */
|
|
68
76
|
installs: Record<string, ScopeVersions>;
|
|
77
|
+
/** Skills and plugin folders here that no company marketplace holds. */
|
|
78
|
+
local: LocalItem[];
|
|
69
79
|
}
|
|
70
80
|
|
|
71
81
|
const SCOPES: readonly InstallScope[] = ["user", "project", "local"];
|
|
@@ -95,11 +105,16 @@ async function readInstalls(
|
|
|
95
105
|
/** Everything the screens draw, read without writing anything. */
|
|
96
106
|
async function readMarketData(cwd: string): Promise<MarketData> {
|
|
97
107
|
const marketplaces = await marketOverview();
|
|
98
|
-
const [status, installs] = await Promise.all([
|
|
108
|
+
const [status, installs, local] = await Promise.all([
|
|
99
109
|
marketStatus(undefined, cwd).then((r) => r.skills ?? []),
|
|
100
110
|
readInstalls(marketplaces, cwd),
|
|
111
|
+
localItems(
|
|
112
|
+
cwd,
|
|
113
|
+
companySkillNames(marketplaces),
|
|
114
|
+
companyPluginNames(marketplaces),
|
|
115
|
+
),
|
|
101
116
|
]);
|
|
102
|
-
return { marketplaces, status, installs };
|
|
117
|
+
return { marketplaces, status, installs, local };
|
|
103
118
|
}
|
|
104
119
|
|
|
105
120
|
/** What a confirmed action is, for its dialog and its failure popup. */
|
|
@@ -302,6 +317,14 @@ export function useMarketState() {
|
|
|
302
317
|
0,
|
|
303
318
|
);
|
|
304
319
|
if (!owner) return;
|
|
320
|
+
const company = (
|
|
321
|
+
await modal.input(
|
|
322
|
+
"Create a company marketplace",
|
|
323
|
+
'Company name (shown as "by <name>" in magus):',
|
|
324
|
+
owner,
|
|
325
|
+
)
|
|
326
|
+
)?.trim();
|
|
327
|
+
if (!company) return;
|
|
305
328
|
const repo = (
|
|
306
329
|
await modal.input(
|
|
307
330
|
"Create a company marketplace",
|
|
@@ -325,7 +348,7 @@ export function useMarketState() {
|
|
|
325
348
|
title: `Create ${owner}/${repo}?`,
|
|
326
349
|
subject: `${owner}/${repo}`,
|
|
327
350
|
},
|
|
328
|
-
() => planCreate({ repo: `${owner}/${repo}`, visibility }),
|
|
351
|
+
() => planCreate({ repo: `${owner}/${repo}`, visibility, company }),
|
|
329
352
|
);
|
|
330
353
|
}, [modal, runPlanned, showFailures]);
|
|
331
354
|
|
|
@@ -352,8 +375,124 @@ export function useMarketState() {
|
|
|
352
375
|
[cwd, runPlanned],
|
|
353
376
|
);
|
|
354
377
|
|
|
378
|
+
/**
|
|
379
|
+
* Add a local skill or plugin folder to a company marketplace: pick the
|
|
380
|
+
* marketplace (when there is more than one), then for a skill the plugin
|
|
381
|
+
* it goes into, a new one included. One plan, one consent.
|
|
382
|
+
*/
|
|
383
|
+
const addLocal = useCallback(
|
|
384
|
+
async (item: LocalItem) => {
|
|
385
|
+
const marketplaces = asyncValue(data)?.marketplaces ?? [];
|
|
386
|
+
if (marketplaces.length === 0) {
|
|
387
|
+
await showFailures({
|
|
388
|
+
headline: `No company marketplace to add ${item.name} to`,
|
|
389
|
+
rows: [
|
|
390
|
+
{
|
|
391
|
+
subject: item.name,
|
|
392
|
+
reason: "this machine has no company marketplace yet",
|
|
393
|
+
},
|
|
394
|
+
],
|
|
395
|
+
fix: "Press c to create one, or o to connect one, then press a again",
|
|
396
|
+
});
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const title = `Add ${item.name} to a company marketplace`;
|
|
400
|
+
const marketplace =
|
|
401
|
+
marketplaces.length === 1
|
|
402
|
+
? marketplaces[0]?.name
|
|
403
|
+
: await modal.select(
|
|
404
|
+
title,
|
|
405
|
+
"Which marketplace?",
|
|
406
|
+
marketplaces.map((m) => ({
|
|
407
|
+
label: m.name,
|
|
408
|
+
value: m.name,
|
|
409
|
+
...(m.company ? { meta: `by ${m.company}` } : {}),
|
|
410
|
+
})),
|
|
411
|
+
0,
|
|
412
|
+
);
|
|
413
|
+
if (!marketplace) return;
|
|
414
|
+
if (item.kind === "plugin") {
|
|
415
|
+
await runPlanned(
|
|
416
|
+
{
|
|
417
|
+
title: `Add the plugin ${item.name} to ${marketplace}?`,
|
|
418
|
+
subject: item.name,
|
|
419
|
+
marketplace,
|
|
420
|
+
},
|
|
421
|
+
() => planPluginAdd({ path: item.path, to: marketplace }, cwd),
|
|
422
|
+
);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const entry = marketplaces.find((m) => m.name === marketplace);
|
|
426
|
+
const NEW = "+new";
|
|
427
|
+
const choice = await modal.select(
|
|
428
|
+
title,
|
|
429
|
+
`Which plugin in ${marketplace}?`,
|
|
430
|
+
[
|
|
431
|
+
...(entry?.plugins ?? []).map((p) => ({
|
|
432
|
+
label: p.name,
|
|
433
|
+
value: p.name,
|
|
434
|
+
meta: plural(p.skills.length, "skill"),
|
|
435
|
+
})),
|
|
436
|
+
{ label: "+ New plugin", value: NEW },
|
|
437
|
+
],
|
|
438
|
+
0,
|
|
439
|
+
);
|
|
440
|
+
if (!choice) return;
|
|
441
|
+
let plugin = choice;
|
|
442
|
+
if (choice === NEW) {
|
|
443
|
+
const name = (
|
|
444
|
+
await modal.input(
|
|
445
|
+
`New plugin in ${marketplace}`,
|
|
446
|
+
`Plugin name (it is created with ${item.name} in it):`,
|
|
447
|
+
item.name,
|
|
448
|
+
)
|
|
449
|
+
)?.trim();
|
|
450
|
+
if (!name) return;
|
|
451
|
+
plugin = name;
|
|
452
|
+
}
|
|
453
|
+
const ref = `${plugin}@${marketplace}`;
|
|
454
|
+
await runPlanned(
|
|
455
|
+
{
|
|
456
|
+
title:
|
|
457
|
+
choice === NEW
|
|
458
|
+
? `Create ${ref} with ${item.name}?`
|
|
459
|
+
: `Add ${item.name} to ${ref}?`,
|
|
460
|
+
subject: item.name,
|
|
461
|
+
marketplace,
|
|
462
|
+
},
|
|
463
|
+
() =>
|
|
464
|
+
planSkillAdd(
|
|
465
|
+
{
|
|
466
|
+
skill: item.name,
|
|
467
|
+
plugin: ref,
|
|
468
|
+
from: item.scope === "user" ? "global" : "project",
|
|
469
|
+
keepLocal: false,
|
|
470
|
+
...(choice === NEW ? { createPlugin: true } : {}),
|
|
471
|
+
},
|
|
472
|
+
cwd,
|
|
473
|
+
),
|
|
474
|
+
);
|
|
475
|
+
},
|
|
476
|
+
[cwd, data, modal, runPlanned, showFailures],
|
|
477
|
+
);
|
|
478
|
+
|
|
355
479
|
// ─── Screen 2 actions ───────────────────────────────────────────────────
|
|
356
480
|
|
|
481
|
+
/** Push every skill of a plugin edited here, as one new version. */
|
|
482
|
+
const publishPlugin = useCallback(
|
|
483
|
+
async (ref: string, marketplace: string) => {
|
|
484
|
+
await runPlanned(
|
|
485
|
+
{
|
|
486
|
+
title: `Push a new version of ${ref}?`,
|
|
487
|
+
subject: ref,
|
|
488
|
+
marketplace,
|
|
489
|
+
},
|
|
490
|
+
() => planPluginPublish({ plugin: ref, keepLocal: false }, cwd),
|
|
491
|
+
);
|
|
492
|
+
},
|
|
493
|
+
[cwd, runPlanned],
|
|
494
|
+
);
|
|
495
|
+
|
|
357
496
|
const newPlugin = useCallback(
|
|
358
497
|
async (marketplace: string) => {
|
|
359
498
|
const name = (
|
|
@@ -541,6 +680,8 @@ export function useMarketState() {
|
|
|
541
680
|
create,
|
|
542
681
|
connect,
|
|
543
682
|
remove,
|
|
683
|
+
addLocal,
|
|
684
|
+
publishPlugin,
|
|
544
685
|
newPlugin,
|
|
545
686
|
deletePlugin,
|
|
546
687
|
addSkill,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* display-path.ts — a folder as a person reads it: relative to the project
|
|
3
|
+
* when it is inside it, `~/…` when it is under the home folder, else as is.
|
|
4
|
+
* For display only; JSON and every file operation keep the absolute path.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
/** `child` is `parent` itself or somewhere under it. */
|
|
11
|
+
function within(parent: string, child: string): string | null {
|
|
12
|
+
const rel = path.relative(parent, child);
|
|
13
|
+
if (rel === "") return "";
|
|
14
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
|
|
15
|
+
return rel;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `abs` shortened for display: `.claude/skills/greet` inside `cwd`,
|
|
20
|
+
* `~/.claude/skills/team-tools` under the home folder, otherwise unchanged.
|
|
21
|
+
* The home folder is `$HOME` when set, as Claude Code and the shell read it.
|
|
22
|
+
*/
|
|
23
|
+
export function displayPath(
|
|
24
|
+
abs: string,
|
|
25
|
+
cwd: string,
|
|
26
|
+
home: string = process.env.HOME || os.homedir(),
|
|
27
|
+
): string {
|
|
28
|
+
const inProject = within(path.resolve(cwd), path.resolve(abs));
|
|
29
|
+
if (inProject !== null) return inProject === "" ? "." : inProject;
|
|
30
|
+
const inHome = home ? within(path.resolve(home), path.resolve(abs)) : null;
|
|
31
|
+
if (inHome !== null) return inHome === "" ? "~" : `~/${inHome}`;
|
|
32
|
+
return abs;
|
|
33
|
+
}
|