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