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
|
@@ -11,30 +11,146 @@ import type {
|
|
|
11
11
|
} from "../../services/market-ops.js";
|
|
12
12
|
import type {
|
|
13
13
|
InstallScope,
|
|
14
|
+
LocalItem,
|
|
14
15
|
MarketResult,
|
|
15
16
|
SkillStatusRow,
|
|
16
17
|
} from "../../services/market-types.js";
|
|
17
18
|
import type { FailureReport, FailureRow } from "../state/types.js";
|
|
19
|
+
import { displayWidth, truncate } from "../text-width.js";
|
|
18
20
|
import type { theme } from "../theme.js";
|
|
19
21
|
|
|
20
22
|
// ─── Screen 1 ────────────────────────────────────────────────────────────────
|
|
21
23
|
|
|
22
24
|
export type MarketplacesItem =
|
|
23
|
-
| {
|
|
25
|
+
| {
|
|
26
|
+
kind: "marketplace";
|
|
27
|
+
entry: MarketOverviewEntry;
|
|
28
|
+
/** Its plugins with skills edited here, ready to push. */
|
|
29
|
+
toPush: readonly string[];
|
|
30
|
+
}
|
|
24
31
|
| { kind: "create" }
|
|
25
|
-
| { kind: "connect" }
|
|
32
|
+
| { kind: "connect" }
|
|
33
|
+
/** The heading over the local rows; never selected. */
|
|
34
|
+
| { kind: "local-heading"; count: number }
|
|
35
|
+
| { kind: "local"; item: LocalItem };
|
|
26
36
|
|
|
27
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* The marketplaces, the two rows that add one, then — when there are any —
|
|
39
|
+
* the skills and plugin folders here that no marketplace holds yet.
|
|
40
|
+
*/
|
|
28
41
|
export function marketplacesItems(
|
|
29
42
|
entries: readonly MarketOverviewEntry[],
|
|
43
|
+
status: readonly SkillStatusRow[] = [],
|
|
44
|
+
local: readonly LocalItem[] = [],
|
|
30
45
|
): MarketplacesItem[] {
|
|
31
46
|
return [
|
|
32
|
-
...entries.map((entry) => ({
|
|
47
|
+
...entries.map((entry) => ({
|
|
48
|
+
kind: "marketplace" as const,
|
|
49
|
+
entry,
|
|
50
|
+
toPush: pluginsToPush(entry.name, status),
|
|
51
|
+
})),
|
|
33
52
|
{ kind: "create" },
|
|
34
53
|
{ kind: "connect" },
|
|
54
|
+
...(local.length > 0
|
|
55
|
+
? [
|
|
56
|
+
{ kind: "local-heading" as const, count: local.length },
|
|
57
|
+
...local.map((item) => ({ kind: "local" as const, item })),
|
|
58
|
+
]
|
|
59
|
+
: []),
|
|
35
60
|
];
|
|
36
61
|
}
|
|
37
62
|
|
|
63
|
+
/** Whether a row can hold the selection: every row but a heading. */
|
|
64
|
+
export function selectable(item: MarketplacesItem | undefined): boolean {
|
|
65
|
+
return item !== undefined && item.kind !== "local-heading";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The next selectable index from `from` in direction `step` (±1), or `from`
|
|
70
|
+
* when there is none that way.
|
|
71
|
+
*/
|
|
72
|
+
export function nextSelectable(
|
|
73
|
+
items: readonly MarketplacesItem[],
|
|
74
|
+
from: number,
|
|
75
|
+
step: number,
|
|
76
|
+
): number {
|
|
77
|
+
for (let i = from + step; i >= 0 && i < items.length; i += step) {
|
|
78
|
+
if (selectable(items[i])) return i;
|
|
79
|
+
}
|
|
80
|
+
return from;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The skill rows `edited` here, by the plugin `ref` that owns them. */
|
|
84
|
+
export function editedSkills(
|
|
85
|
+
status: readonly SkillStatusRow[],
|
|
86
|
+
ref: string,
|
|
87
|
+
): string[] {
|
|
88
|
+
return [
|
|
89
|
+
...new Set(
|
|
90
|
+
status
|
|
91
|
+
.filter((r) => r.state === "edited" && r.owner === ref)
|
|
92
|
+
.map((r) => r.skill),
|
|
93
|
+
),
|
|
94
|
+
].sort();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The plugins of `marketplace` with at least one skill edited here. */
|
|
98
|
+
export function pluginsToPush(
|
|
99
|
+
marketplace: string,
|
|
100
|
+
status: readonly SkillStatusRow[],
|
|
101
|
+
): string[] {
|
|
102
|
+
const suffix = `@${marketplace}`;
|
|
103
|
+
return [
|
|
104
|
+
...new Set(
|
|
105
|
+
status
|
|
106
|
+
.filter(
|
|
107
|
+
(r) => r.state === "edited" && r.owner?.endsWith(suffix) === true,
|
|
108
|
+
)
|
|
109
|
+
.map((r) => (r.owner as string).slice(0, -suffix.length)),
|
|
110
|
+
),
|
|
111
|
+
].sort();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** What a local row is, in the user's words. */
|
|
115
|
+
export function localKindWords(item: LocalItem): string {
|
|
116
|
+
return item.kind === "plugin" ? "plugin" : "skill";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The narrowest a cut secondary text is still worth showing. */
|
|
120
|
+
const MIN_CUT = 4;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Fit one list row into `width` columns without wrapping. The name and the
|
|
124
|
+
* parts of `fixed` columns (a scope badge, the gaps around it) always show:
|
|
125
|
+
* the name is cut with "…" to leave them room. The secondary texts follow in
|
|
126
|
+
* order while each fits whole after a gap of `gap`; the first that does not
|
|
127
|
+
* is cut to what is left (when that is at least a few columns), and the rest
|
|
128
|
+
* are dropped.
|
|
129
|
+
*/
|
|
130
|
+
export function fitRow<T extends { text: string }>(
|
|
131
|
+
width: number,
|
|
132
|
+
name: string,
|
|
133
|
+
fixed: number,
|
|
134
|
+
secondary: readonly T[],
|
|
135
|
+
gap = 2,
|
|
136
|
+
): { name: string; secondary: T[] } {
|
|
137
|
+
const nameOut = truncate(name, Math.max(1, width - fixed));
|
|
138
|
+
let used = displayWidth(nameOut) + fixed;
|
|
139
|
+
const out: T[] = [];
|
|
140
|
+
for (const part of secondary) {
|
|
141
|
+
const need = gap + displayWidth(part.text);
|
|
142
|
+
if (used + need <= width) {
|
|
143
|
+
out.push(part);
|
|
144
|
+
used += need;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const room = width - used - gap;
|
|
148
|
+
if (room >= MIN_CUT) out.push({ ...part, text: truncate(part.text, room) });
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
return { name: nameOut, secondary: out };
|
|
152
|
+
}
|
|
153
|
+
|
|
38
154
|
/** "1 plugin", "3 plugins". */
|
|
39
155
|
export function plural(n: number, word: string): string {
|
|
40
156
|
return `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
@@ -73,6 +189,8 @@ export type MarketplaceItem =
|
|
|
73
189
|
skills: readonly string[];
|
|
74
190
|
installs: ScopeVersions;
|
|
75
191
|
expanded: boolean;
|
|
192
|
+
/** Its skills edited here, which `p` pushes as one new version. */
|
|
193
|
+
edited: readonly string[];
|
|
76
194
|
}
|
|
77
195
|
| {
|
|
78
196
|
kind: "skill";
|
|
@@ -119,6 +237,7 @@ export function marketplaceItems(
|
|
|
119
237
|
skills: p.skills,
|
|
120
238
|
installs: installs[ref] ?? NO_INSTALLS,
|
|
121
239
|
expanded: open,
|
|
240
|
+
edited: editedSkills(status, ref),
|
|
122
241
|
});
|
|
123
242
|
if (!open) continue;
|
|
124
243
|
for (const skill of p.skills) {
|
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
FailureRow,
|
|
16
16
|
PluginContentsEntry,
|
|
17
17
|
} from "../state/types.js";
|
|
18
|
+
import { truncate } from "../text-width.js";
|
|
18
19
|
import type { BadgeTone } from "../theme.js";
|
|
19
20
|
import {
|
|
20
21
|
failureReport,
|
|
@@ -215,6 +216,12 @@ function pushPluginItems(
|
|
|
215
216
|
}
|
|
216
217
|
}
|
|
217
218
|
|
|
219
|
+
/**
|
|
220
|
+
* The widest company name a group header shows. The header keeps its badge
|
|
221
|
+
* whole and clips the title instead, so a long name is clipped here first.
|
|
222
|
+
*/
|
|
223
|
+
const COMPANY_BADGE_COLUMNS = 24;
|
|
224
|
+
|
|
218
225
|
/**
|
|
219
226
|
* Derives tone and badge for a category item based on marketplace identity.
|
|
220
227
|
*/
|
|
@@ -244,7 +251,13 @@ function categoryStyling(
|
|
|
244
251
|
// MadAppGang, and being this machine's company marketplace is the stronger
|
|
245
252
|
// fact.
|
|
246
253
|
if (mp.company) {
|
|
247
|
-
|
|
254
|
+
const company = companyName(mp);
|
|
255
|
+
return {
|
|
256
|
+
tone: "info",
|
|
257
|
+
badge: company
|
|
258
|
+
? `by ${truncate(company, COMPANY_BADGE_COLUMNS)}`
|
|
259
|
+
: "◆ Company",
|
|
260
|
+
};
|
|
248
261
|
}
|
|
249
262
|
if (isMadAppGang(mp)) {
|
|
250
263
|
return { tone: "byline", badge: "by MadAppGang" };
|
|
@@ -262,6 +275,17 @@ function isMadAppGang(mp: Marketplace): boolean {
|
|
|
262
275
|
return (mp.source.repo ?? "").toLowerCase().startsWith("madappgang/");
|
|
263
276
|
}
|
|
264
277
|
|
|
278
|
+
/**
|
|
279
|
+
* The name a company marketplace is shown by: the manifest's `owner.name`,
|
|
280
|
+
* else the GitHub owner of its repository.
|
|
281
|
+
*/
|
|
282
|
+
export function companyName(mp: Marketplace): string | undefined {
|
|
283
|
+
if (mp.ownerName) return mp.ownerName;
|
|
284
|
+
const repo = mp.source.repo ?? "";
|
|
285
|
+
const slash = repo.indexOf("/");
|
|
286
|
+
return slash > 0 ? repo.slice(0, slash) : undefined;
|
|
287
|
+
}
|
|
288
|
+
|
|
265
289
|
// ─── "It failed, and here is why" ────────────────────────────────────────────
|
|
266
290
|
|
|
267
291
|
/** A version token — `v2.0.3`, `8.1.3`, `1.0.0-beta.2`. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { displayWidth, truncate } from "../../text-width.js";
|
|
2
3
|
import { type BadgeTone, theme } from "../../theme.js";
|
|
3
4
|
import { SelectableRow, rowLead } from "./SelectableRow.js";
|
|
4
5
|
|
|
@@ -17,6 +18,12 @@ interface ListCategoryRowProps {
|
|
|
17
18
|
* that is broken rather than as a section title.
|
|
18
19
|
*/
|
|
19
20
|
expandable?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Columns the row may use. When given, the title and count are cut with
|
|
23
|
+
* "…" to fit one line (the badge goes first), so a narrow panel never
|
|
24
|
+
* wraps the heading onto a second row.
|
|
25
|
+
*/
|
|
26
|
+
width?: number;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
export function ListCategoryRow({
|
|
@@ -27,10 +34,21 @@ export function ListCategoryRow({
|
|
|
27
34
|
tone = "neutral",
|
|
28
35
|
selected,
|
|
29
36
|
expandable = true,
|
|
37
|
+
width,
|
|
30
38
|
}: ListCategoryRowProps) {
|
|
31
39
|
const colors = theme.badge[tone];
|
|
32
40
|
const marker = expandable ? (expanded ? "▼ " : "▶ ") : "";
|
|
33
|
-
const
|
|
41
|
+
const full = `${marker}${title}${count !== undefined ? ` (${count})` : ""}`;
|
|
42
|
+
// The lead cell and the trailing space inside the chip take two columns.
|
|
43
|
+
const label =
|
|
44
|
+
width === undefined ? full : truncate(full, Math.max(1, width - 2));
|
|
45
|
+
const shownBadge =
|
|
46
|
+
width === undefined ||
|
|
47
|
+
(badge !== undefined &&
|
|
48
|
+
label === full &&
|
|
49
|
+
displayWidth(full) + 3 + displayWidth(badge) <= width)
|
|
50
|
+
? badge
|
|
51
|
+
: undefined;
|
|
34
52
|
|
|
35
53
|
// Both forms start with the row's lead cell — a blank inside the chip, the
|
|
36
54
|
// selection mark on the cursor — so the title never moves as it is selected.
|
|
@@ -50,7 +68,9 @@ export function ListCategoryRow({
|
|
|
50
68
|
{label}{" "}
|
|
51
69
|
</strong>
|
|
52
70
|
)}
|
|
53
|
-
{!selected &&
|
|
71
|
+
{!selected && shownBadge ? (
|
|
72
|
+
<span fg={colors.badgeFg}> {shownBadge}</span>
|
|
73
|
+
) : null}
|
|
54
74
|
</SelectableRow>
|
|
55
75
|
);
|
|
56
76
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
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>{
|
|
59
|
-
{
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
{
|
|
73
|
-
{
|
|
74
|
-
|
|
102
|
+
{fit.name}
|
|
103
|
+
{fit.secondary.map((p) => (
|
|
104
|
+
<React.Fragment key={p.text}>
|
|
75
105
|
{" "}
|
|
76
|
-
<MetaText
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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={
|
|
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
|
|
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) => (
|