blyuecode 0.4.0 → 0.5.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/dist/core/config.js +8 -0
- package/dist/core/skillstore.js +118 -0
- package/dist/ui/Chat.js +10 -0
- package/dist/ui/Settings.js +24 -0
- package/dist/ui/SkillStorePanel.js +145 -0
- package/package.json +1 -1
package/dist/core/config.js
CHANGED
|
@@ -28,6 +28,7 @@ function parseEnv(file) {
|
|
|
28
28
|
model: map.MODEL || 'gpt-4o-mini',
|
|
29
29
|
n8nUrl: map.N8N_URL || undefined,
|
|
30
30
|
n8nApiKey: map.N8N_API_KEY || undefined,
|
|
31
|
+
skillsIndexUrl: map.SKILLS_INDEX_URL || undefined,
|
|
31
32
|
};
|
|
32
33
|
}
|
|
33
34
|
export function saveConfig(cfg) {
|
|
@@ -44,6 +45,8 @@ export function saveConfig(cfg) {
|
|
|
44
45
|
lines.push(`N8N_URL=${cfg.n8nUrl}`);
|
|
45
46
|
if (cfg.n8nApiKey)
|
|
46
47
|
lines.push(`N8N_API_KEY=${cfg.n8nApiKey}`);
|
|
48
|
+
if (cfg.skillsIndexUrl)
|
|
49
|
+
lines.push(`SKILLS_INDEX_URL=${cfg.skillsIndexUrl}`);
|
|
47
50
|
lines.push('');
|
|
48
51
|
fs.writeFileSync(GLOBAL_ENV, lines.join('\n'), { mode: 0o600 });
|
|
49
52
|
return GLOBAL_ENV;
|
|
@@ -63,6 +66,10 @@ export function applyConfig(cfg) {
|
|
|
63
66
|
process.env.N8N_API_KEY = cfg.n8nApiKey;
|
|
64
67
|
else
|
|
65
68
|
delete process.env.N8N_API_KEY;
|
|
69
|
+
if (cfg.skillsIndexUrl)
|
|
70
|
+
process.env.SKILLS_INDEX_URL = cfg.skillsIndexUrl;
|
|
71
|
+
else
|
|
72
|
+
delete process.env.SKILLS_INDEX_URL;
|
|
66
73
|
}
|
|
67
74
|
export function getConfig() {
|
|
68
75
|
const cfg = loadConfig();
|
|
@@ -74,6 +81,7 @@ export function getConfig() {
|
|
|
74
81
|
model: process.env.MODEL || 'gpt-4o-mini',
|
|
75
82
|
n8nUrl: process.env.N8N_URL,
|
|
76
83
|
n8nApiKey: process.env.N8N_API_KEY,
|
|
84
|
+
skillsIndexUrl: process.env.SKILLS_INDEX_URL,
|
|
77
85
|
};
|
|
78
86
|
}
|
|
79
87
|
export function hasN8n() {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { skillsDir } from './skills.js';
|
|
4
|
+
const DEFAULT_INDEX_URL = 'https://raw.githubusercontent.com/lumenlabsllc/blyue-skills/main/index.json';
|
|
5
|
+
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
6
|
+
let cache = null;
|
|
7
|
+
export function indexUrl() {
|
|
8
|
+
return process.env.SKILLS_INDEX_URL || DEFAULT_INDEX_URL;
|
|
9
|
+
}
|
|
10
|
+
function baseUrlFor(url) {
|
|
11
|
+
return url.replace(/\/index\.json.*$/, '');
|
|
12
|
+
}
|
|
13
|
+
export async function fetchStore(opts) {
|
|
14
|
+
const url = indexUrl();
|
|
15
|
+
if (!opts?.force &&
|
|
16
|
+
cache &&
|
|
17
|
+
cache.url === url &&
|
|
18
|
+
Date.now() - cache.at < CACHE_TTL_MS) {
|
|
19
|
+
return cache.skills;
|
|
20
|
+
}
|
|
21
|
+
let res;
|
|
22
|
+
try {
|
|
23
|
+
res = await fetch(url, { headers: { Accept: 'application/json' } });
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
throw new Error(`Cannot reach skill store: ${err.message}. ` +
|
|
27
|
+
`Check SKILLS_INDEX_URL in /settings.`);
|
|
28
|
+
}
|
|
29
|
+
if (!res.ok) {
|
|
30
|
+
throw new Error(`Skill store returned HTTP ${res.status} ${res.statusText} — ${url}`);
|
|
31
|
+
}
|
|
32
|
+
const body = (await res.json());
|
|
33
|
+
if (!body || !Array.isArray(body.skills)) {
|
|
34
|
+
throw new Error('Skill store index is malformed (expected { version, skills: [...] })');
|
|
35
|
+
}
|
|
36
|
+
cache = { at: Date.now(), url, skills: body.skills };
|
|
37
|
+
return body.skills;
|
|
38
|
+
}
|
|
39
|
+
export function clearStoreCache() {
|
|
40
|
+
cache = null;
|
|
41
|
+
}
|
|
42
|
+
export function isInstalled(name) {
|
|
43
|
+
return fs.existsSync(path.join(skillsDir(), name, 'SKILL.md'));
|
|
44
|
+
}
|
|
45
|
+
export function installedVersion(name) {
|
|
46
|
+
const file = path.join(skillsDir(), name, 'SKILL.md');
|
|
47
|
+
if (!fs.existsSync(file))
|
|
48
|
+
return null;
|
|
49
|
+
try {
|
|
50
|
+
const raw = fs.readFileSync(file, 'utf8');
|
|
51
|
+
const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
52
|
+
if (!m)
|
|
53
|
+
return null;
|
|
54
|
+
const v = m[1].match(/^version:\s*["']?([^"'\r\n]+)["']?/m);
|
|
55
|
+
return v ? v[1].trim() : null;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function installSkill(skill) {
|
|
62
|
+
const url = `${baseUrlFor(indexUrl())}/${skill.path.replace(/^\/+/, '')}`;
|
|
63
|
+
let res;
|
|
64
|
+
try {
|
|
65
|
+
res = await fetch(url, { headers: { Accept: 'text/plain' } });
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
throw new Error(`Download failed: ${err.message}`);
|
|
69
|
+
}
|
|
70
|
+
if (!res.ok) {
|
|
71
|
+
throw new Error(`Download failed: HTTP ${res.status} for ${url}`);
|
|
72
|
+
}
|
|
73
|
+
const text = await res.text();
|
|
74
|
+
if (!text.trim().startsWith('---')) {
|
|
75
|
+
throw new Error(`Downloaded SKILL.md for "${skill.name}" has no frontmatter`);
|
|
76
|
+
}
|
|
77
|
+
const dir = path.join(skillsDir(), skill.name);
|
|
78
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
79
|
+
let out = text;
|
|
80
|
+
if (skill.version && !/^version:/m.test(text.split('---')[1] || '')) {
|
|
81
|
+
out = text.replace(/^---\r?\n([\s\S]*?)\r?\n---/, (_m, inner) => `---\n${inner}\nversion: ${skill.version}\n---`);
|
|
82
|
+
}
|
|
83
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'), out, 'utf8');
|
|
84
|
+
}
|
|
85
|
+
export function uninstallSkill(name) {
|
|
86
|
+
const dir = path.join(skillsDir(), name);
|
|
87
|
+
if (!fs.existsSync(dir))
|
|
88
|
+
return;
|
|
89
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
export async function updateAll() {
|
|
92
|
+
const store = await fetchStore();
|
|
93
|
+
const results = [];
|
|
94
|
+
for (const skill of store) {
|
|
95
|
+
if (!isInstalled(skill.name))
|
|
96
|
+
continue;
|
|
97
|
+
const from = installedVersion(skill.name);
|
|
98
|
+
const to = skill.version ?? null;
|
|
99
|
+
if (from && to && from === to) {
|
|
100
|
+
results.push({ name: skill.name, from, to, ok: true });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
await installSkill(skill);
|
|
105
|
+
results.push({ name: skill.name, from, to, ok: true });
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
results.push({
|
|
109
|
+
name: skill.name,
|
|
110
|
+
from,
|
|
111
|
+
to,
|
|
112
|
+
ok: false,
|
|
113
|
+
error: err.message,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return results;
|
|
118
|
+
}
|
package/dist/ui/Chat.js
CHANGED
|
@@ -20,6 +20,7 @@ import { theme } from './theme.js';
|
|
|
20
20
|
import { getConfig, saveConfig, applyConfig } from '../core/config.js';
|
|
21
21
|
import { recordModelUse } from '../core/modelUsage.js';
|
|
22
22
|
import { SkillsPanel } from './SkillsPanel.js';
|
|
23
|
+
import { SkillStorePanel } from './SkillStorePanel.js';
|
|
23
24
|
import { listSkills, findSkill, matchSkills, ensureSkillsDir, } from '../core/skills.js';
|
|
24
25
|
let nextId = 0;
|
|
25
26
|
const mkItem = (kind, text) => ({ kind, text, id: nextId++ });
|
|
@@ -137,6 +138,11 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
137
138
|
setOverlay('mcp');
|
|
138
139
|
return;
|
|
139
140
|
}
|
|
141
|
+
if (trimmed === '/skillstore') {
|
|
142
|
+
setInput('');
|
|
143
|
+
setOverlay('skillstore');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
140
146
|
if (trimmed === '/skills') {
|
|
141
147
|
setInput('');
|
|
142
148
|
setOverlay('skills');
|
|
@@ -209,6 +215,7 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
209
215
|
mkItem('tool', ' /settings open settings (Ctrl+O)'),
|
|
210
216
|
mkItem('tool', ' /mcp show MCP servers and tools'),
|
|
211
217
|
mkItem('tool', ' /skills list skills'),
|
|
218
|
+
mkItem('tool', ' /skillstore browse and install community skills'),
|
|
212
219
|
mkItem('tool', ' /skill <n> force-activate a skill for next msg'),
|
|
213
220
|
mkItem('tool', ' /compact summarize history to free context'),
|
|
214
221
|
mkItem('tool', ' /debug show last request to provider'),
|
|
@@ -304,6 +311,9 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
304
311
|
if (overlay === 'mcp') {
|
|
305
312
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(McpPanel, { onClose: () => setOverlay('none') }) }));
|
|
306
313
|
}
|
|
314
|
+
if (overlay === 'skillstore') {
|
|
315
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsx(SkillStorePanel, { onClose: () => setOverlay('none') }) }));
|
|
316
|
+
}
|
|
307
317
|
if (overlay === 'skills') {
|
|
308
318
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(SkillsPanel, { onClose: () => setOverlay('none') }) }));
|
|
309
319
|
}
|
package/dist/ui/Settings.js
CHANGED
|
@@ -72,6 +72,14 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
72
72
|
})(),
|
|
73
73
|
action: () => { setMcpCursor(0); setScreen('mcp'); },
|
|
74
74
|
},
|
|
75
|
+
{
|
|
76
|
+
label: 'Skills store URL',
|
|
77
|
+
hint: process.env.SKILLS_INDEX_URL || 'default (github · lumenlabsllc)',
|
|
78
|
+
action: () => {
|
|
79
|
+
setDraft(process.env.SKILLS_INDEX_URL || '');
|
|
80
|
+
setScreen('skillstore-url');
|
|
81
|
+
},
|
|
82
|
+
},
|
|
75
83
|
{ label: 'Close', hint: 'esc', action: onClose },
|
|
76
84
|
];
|
|
77
85
|
useInput((_, key) => {
|
|
@@ -276,6 +284,22 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
276
284
|
if (screen === 'mcp-confirm-del') {
|
|
277
285
|
return (_jsxs(Box, { flexDirection: "column", children: [top('delete server'), row(_jsxs(Text, { color: theme.warning, children: ["Delete MCP server \"", mcpConfirmDel, "\"?"] })), row(_jsx(Text, { color: theme.muted, children: "This removes it from mcp.json." })), bot, _jsxs(Text, { children: [_jsx(Text, { color: theme.success, bold: true, children: " [y]" }), _jsx(Text, { color: theme.muted, children: " yes " }), _jsx(Text, { color: theme.error, bold: true, children: "[n]" }), _jsx(Text, { color: theme.muted, children: " no" })] })] }));
|
|
278
286
|
}
|
|
287
|
+
if (screen === 'skillstore-url') {
|
|
288
|
+
return (_jsxs(Box, { flexDirection: "column", children: [top('skills store url'), row(_jsx(Text, { color: theme.muted, children: "Leave empty to use the default LUMEN store" })), row(_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsx(KeyInput, { value: draft, onChange: setDraft, placeholder: "https://raw.githubusercontent.com/you/repo/main/index.json", onSubmit: () => {
|
|
289
|
+
const v = draft.trim();
|
|
290
|
+
const next = { ...cfg };
|
|
291
|
+
if (v)
|
|
292
|
+
next.skillsIndexUrl = v;
|
|
293
|
+
else
|
|
294
|
+
delete next.skillsIndexUrl;
|
|
295
|
+
persist(next);
|
|
296
|
+
if (v)
|
|
297
|
+
process.env.SKILLS_INDEX_URL = v;
|
|
298
|
+
else
|
|
299
|
+
delete process.env.SKILLS_INDEX_URL;
|
|
300
|
+
setScreen('main');
|
|
301
|
+
} })] })), bot, _jsx(Text, { color: theme.muted, children: "type \u00B7 \u21B5 save \u00B7 esc cancel" })] }));
|
|
302
|
+
}
|
|
279
303
|
if (screen === 'mcp-add-name') {
|
|
280
304
|
return (_jsxs(Box, { flexDirection: "column", children: [top('add mcp · step 1/4'), row(_jsx(Text, { color: theme.muted, children: "Name (unique, e.g. \"filesystem\")" })), row(_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsx(KeyInput, { value: mcpDraftName, onChange: setMcpDraftName, placeholder: "filesystem", onSubmit: () => setScreen('mcp-add-command') })] })), bot, _jsx(Text, { color: theme.muted, children: "type \u00B7 \u21B5 next \u00B7 esc cancel" })] }));
|
|
281
305
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
3
|
+
import { Box, Text, useInput, useStdout } from 'ink';
|
|
4
|
+
import { fetchStore, installSkill, uninstallSkill, updateAll, isInstalled, indexUrl, } from '../core/skillstore.js';
|
|
5
|
+
import { theme } from './theme.js';
|
|
6
|
+
export const SkillStorePanel = ({ onClose, onChanged }) => {
|
|
7
|
+
const { stdout } = useStdout();
|
|
8
|
+
const width = Math.max(40, stdout?.columns ?? 60);
|
|
9
|
+
const inner = width - 4;
|
|
10
|
+
const [skills, setSkills] = useState([]);
|
|
11
|
+
const [loading, setLoading] = useState(true);
|
|
12
|
+
const [error, setError] = useState(null);
|
|
13
|
+
const [idx, setIdx] = useState(0);
|
|
14
|
+
const [filter, setFilter] = useState('');
|
|
15
|
+
const [status, setStatus] = useState(null);
|
|
16
|
+
const [version, setVersion] = useState(0);
|
|
17
|
+
const reload = async (force = false) => {
|
|
18
|
+
setLoading(true);
|
|
19
|
+
setError(null);
|
|
20
|
+
try {
|
|
21
|
+
const list = await fetchStore({ force });
|
|
22
|
+
setSkills(list);
|
|
23
|
+
setIdx(0);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
setError(err.message);
|
|
27
|
+
}
|
|
28
|
+
setLoading(false);
|
|
29
|
+
};
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
void reload(false);
|
|
32
|
+
}, []);
|
|
33
|
+
const filtered = useMemo(() => {
|
|
34
|
+
const q = filter.toLowerCase();
|
|
35
|
+
if (!q)
|
|
36
|
+
return skills;
|
|
37
|
+
return skills.filter((s) => s.name.toLowerCase().includes(q) ||
|
|
38
|
+
s.description.toLowerCase().includes(q) ||
|
|
39
|
+
(s.tags || []).some((t) => t.toLowerCase().includes(q)));
|
|
40
|
+
}, [skills, filter]);
|
|
41
|
+
const current = filtered[idx];
|
|
42
|
+
const flash = (msg) => {
|
|
43
|
+
setStatus(msg);
|
|
44
|
+
setTimeout(() => setStatus(null), 2500);
|
|
45
|
+
};
|
|
46
|
+
const doInstall = async () => {
|
|
47
|
+
if (!current)
|
|
48
|
+
return;
|
|
49
|
+
try {
|
|
50
|
+
await installSkill(current);
|
|
51
|
+
flash('✓ installed ' + current.name);
|
|
52
|
+
setVersion((v) => v + 1);
|
|
53
|
+
onChanged?.();
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
flash('✗ ' + err.message);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const doUninstall = () => {
|
|
60
|
+
if (!current)
|
|
61
|
+
return;
|
|
62
|
+
if (!isInstalled(current.name))
|
|
63
|
+
return;
|
|
64
|
+
try {
|
|
65
|
+
uninstallSkill(current.name);
|
|
66
|
+
flash('✓ removed ' + current.name);
|
|
67
|
+
setVersion((v) => v + 1);
|
|
68
|
+
onChanged?.();
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
flash('✗ ' + err.message);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const doUpdateAll = async () => {
|
|
75
|
+
setStatus('updating…');
|
|
76
|
+
try {
|
|
77
|
+
const res = await updateAll();
|
|
78
|
+
const updated = res.filter((r) => r.from !== r.to).length;
|
|
79
|
+
const failed = res.filter((r) => !r.ok).length;
|
|
80
|
+
flash(failed > 0
|
|
81
|
+
? `updated ${updated} · ${failed} failed`
|
|
82
|
+
: `✓ updated ${updated} skill${updated === 1 ? '' : 's'}`);
|
|
83
|
+
setVersion((v) => v + 1);
|
|
84
|
+
onChanged?.();
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
flash('✗ ' + err.message);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
useInput((input, key) => {
|
|
91
|
+
if (key.escape)
|
|
92
|
+
return onClose();
|
|
93
|
+
if (key.upArrow && filtered.length)
|
|
94
|
+
setIdx((i) => (i - 1 + filtered.length) % filtered.length);
|
|
95
|
+
if (key.downArrow && filtered.length)
|
|
96
|
+
setIdx((i) => (i + 1) % filtered.length);
|
|
97
|
+
if (input === 'i' || input === 'I') {
|
|
98
|
+
void doInstall();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (input === 'd' || input === 'D') {
|
|
102
|
+
doUninstall();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (input === 'u' || input === 'U') {
|
|
106
|
+
void doUpdateAll();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (input === 'r' || input === 'R') {
|
|
110
|
+
void reload(true);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (key.backspace || key.delete) {
|
|
114
|
+
setFilter((f) => f.slice(0, -1));
|
|
115
|
+
setIdx(0);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (input && !key.ctrl && !key.meta) {
|
|
119
|
+
const clean = input.replace(/[\x00-\x1F\x7F]/g, '');
|
|
120
|
+
if (clean) {
|
|
121
|
+
setFilter((f) => f + clean);
|
|
122
|
+
setIdx(0);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
const label = filter ? `store · filter: ${filter}` : 'skill store';
|
|
127
|
+
const topPad = Math.max(1, inner - label.length - 4);
|
|
128
|
+
const maxVisible = 12;
|
|
129
|
+
const start = Math.max(0, Math.min(idx - Math.floor(maxVisible / 2), Math.max(0, filtered.length - maxVisible)));
|
|
130
|
+
const visible = filtered.slice(start, start + maxVisible);
|
|
131
|
+
const installedCount = skills.filter((s) => isInstalled(s.name)).length;
|
|
132
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '╭─ ' }), _jsx(Text, { color: theme.accent, bold: true, children: label }), _jsx(Text, { color: theme.muted, children: ' ' + '─'.repeat(topPad) + '╮' })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsxs(Text, { color: theme.muted, children: [skills.length, " skill", skills.length === 1 ? '' : 's', " \u00B7 ", installedCount, " installed"] }), status && (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.muted, children: " \u00B7 " }), _jsx(Text, { color: status.startsWith('✗') ? theme.error : theme.success, children: status })] }))] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: theme.muted, children: indexUrl().slice(0, inner - 4) })] }), _jsx(Text, { color: theme.muted, children: '│ ' + '─'.repeat(Math.max(1, inner - 2)) }), loading ? (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: theme.muted, children: "fetching index\u2026" })] })) : error ? (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsxs(Text, { color: theme.error, children: ["\u2717 ", error.slice(0, inner - 6)] })] })) : filtered.length === 0 ? (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: theme.warning, children: "no skills match" })] })) : (visible.map((s, i) => {
|
|
133
|
+
const realIdx = start + i;
|
|
134
|
+
const active = realIdx === idx;
|
|
135
|
+
const installed = isInstalled(s.name);
|
|
136
|
+
const marker = installed ? '✓ ' : ' ';
|
|
137
|
+
const markerColor = installed ? theme.success : theme.muted;
|
|
138
|
+
const tags = (s.tags || []).slice(0, 3).join(', ');
|
|
139
|
+
const right = installed ? 'installed' : (s.version ? 'v' + s.version : '');
|
|
140
|
+
const nameCap = Math.max(10, inner - right.length - 8);
|
|
141
|
+
const name = s.name.length > nameCap ? s.name.slice(0, nameCap - 1) + '…' : s.name;
|
|
142
|
+
const pad = Math.max(1, inner - name.length - right.length - 5);
|
|
143
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: active ? theme.accent : theme.muted, bold: active, children: active ? '❯ ' : ' ' }), _jsx(Text, { color: markerColor, children: marker }), _jsx(Text, { color: active ? theme.text : theme.muted, bold: active, children: name }), _jsx(Text, { color: theme.muted, children: ' '.repeat(pad) }), _jsx(Text, { color: theme.muted, children: right })] }), active && s.description && (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: theme.muted, children: s.description.slice(0, inner - 8) })] })), active && tags && (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ tags: ' }), _jsx(Text, { color: theme.muted, children: tags })] }))] }, s.name));
|
|
144
|
+
})), _jsx(Text, { color: theme.muted, children: '╰' + '─'.repeat(inner) + '╯' }), _jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: " [" }), _jsx(Text, { color: theme.accent, children: "i" }), _jsx(Text, { color: theme.muted, children: "] install \u00B7 [" }), _jsx(Text, { color: theme.accent, children: "d" }), _jsx(Text, { color: theme.muted, children: "] remove \u00B7 [" }), _jsx(Text, { color: theme.accent, children: "u" }), _jsx(Text, { color: theme.muted, children: "] update all \u00B7 [" }), _jsx(Text, { color: theme.accent, children: "r" }), _jsx(Text, { color: theme.muted, children: "] refresh \u00B7 [" }), _jsx(Text, { color: theme.accent, children: "esc" }), _jsx(Text, { color: theme.muted, children: "] close" })] })] }));
|
|
145
|
+
};
|