blyuecode 0.4.0 → 0.6.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 +12 -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++ });
|
|
@@ -107,6 +108,8 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
107
108
|
setOverlay('model');
|
|
108
109
|
if (key.ctrl && inputChar === 'o')
|
|
109
110
|
setOverlay('settings');
|
|
111
|
+
if (key.ctrl && inputChar === 'k')
|
|
112
|
+
setOverlay('skillstore');
|
|
110
113
|
});
|
|
111
114
|
useEffect(() => { try {
|
|
112
115
|
ensureSkillsDir();
|
|
@@ -137,6 +140,11 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
137
140
|
setOverlay('mcp');
|
|
138
141
|
return;
|
|
139
142
|
}
|
|
143
|
+
if (trimmed === '/skillstore') {
|
|
144
|
+
setInput('');
|
|
145
|
+
setOverlay('skillstore');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
140
148
|
if (trimmed === '/skills') {
|
|
141
149
|
setInput('');
|
|
142
150
|
setOverlay('skills');
|
|
@@ -209,6 +217,7 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
209
217
|
mkItem('tool', ' /settings open settings (Ctrl+O)'),
|
|
210
218
|
mkItem('tool', ' /mcp show MCP servers and tools'),
|
|
211
219
|
mkItem('tool', ' /skills list skills'),
|
|
220
|
+
mkItem('tool', ' /skillstore browse and install community skills'),
|
|
212
221
|
mkItem('tool', ' /skill <n> force-activate a skill for next msg'),
|
|
213
222
|
mkItem('tool', ' /compact summarize history to free context'),
|
|
214
223
|
mkItem('tool', ' /debug show last request to provider'),
|
|
@@ -304,6 +313,9 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
304
313
|
if (overlay === 'mcp') {
|
|
305
314
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(McpPanel, { onClose: () => setOverlay('none') }) }));
|
|
306
315
|
}
|
|
316
|
+
if (overlay === 'skillstore') {
|
|
317
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsx(SkillStorePanel, { onClose: () => setOverlay('none') }) }));
|
|
318
|
+
}
|
|
307
319
|
if (overlay === 'skills') {
|
|
308
320
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(SkillsPanel, { onClose: () => setOverlay('none') }) }));
|
|
309
321
|
}
|
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
|
+
};
|