skill-search 0.0.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.
@@ -0,0 +1,35 @@
1
+ // src/tui/AsciiHeader.js - FIGlet-style ASCII Art Header
2
+
3
+ const React = require('react');
4
+ const { Box, Text } = require('ink');
5
+
6
+ // ASCII Art for "SKILL CLI" using a compact block style
7
+ const ASCII_ART = [
8
+ '███████╗██╗ ██╗██╗██╗ ██╗ ██████╗██╗ ██╗',
9
+ '██╔════╝██║ ██╔╝██║██║ ██║ ██╔════╝██║ ██║',
10
+ '███████╗█████╔╝ ██║██║ ██║ ██║ ██║ ██║',
11
+ '╚════██║██╔═██╗ ██║██║ ██║ ██║ ██║ ██║',
12
+ '███████║██║ ██╗██║███████╗███████╗ ╚██████╗███████╗██║',
13
+ '╚══════╝╚═╝ ╚═╝╚═╝╚══════╝╚══════╝ ╚═════╝╚══════╝╚═╝'
14
+ ];
15
+
16
+ // Gradient colors for the ASCII art (top to bottom)
17
+ const GRADIENT_COLORS = ['cyan', 'cyanBright', 'cyan', 'blue', 'blueBright', 'blue'];
18
+
19
+ function AsciiHeader() {
20
+ return React.createElement(Box, {
21
+ flexDirection: 'column',
22
+ alignItems: 'center',
23
+ paddingY: 1
24
+ },
25
+ ASCII_ART.map((line, index) =>
26
+ React.createElement(Text, {
27
+ key: index,
28
+ color: GRADIENT_COLORS[index] || 'cyan',
29
+ bold: true
30
+ }, line)
31
+ )
32
+ );
33
+ }
34
+
35
+ module.exports = AsciiHeader;
@@ -0,0 +1,64 @@
1
+ // src/tui/CommandPalette.js - Slash Command Menu
2
+
3
+ const React = require('react');
4
+ const { useState } = React;
5
+ const { Box, Text, useInput } = require('ink');
6
+
7
+ const COMMANDS = [
8
+ { key: 'sync', icon: '🔄', label: 'Sync', desc: 'Sync skills from remote' },
9
+ { key: 'sync-docs', icon: '📥', label: 'Sync Docs', desc: 'Download all SKILL.md files' },
10
+ { key: 'config', icon: '⚙️', label: 'Config', desc: 'Configure API settings' },
11
+ { key: 'clear', icon: '🗑️', label: 'Clear Cache', desc: 'Clear local cache' },
12
+ { key: 'help', icon: '❓', label: 'Help', desc: 'Show help information' },
13
+ { key: 'quit', icon: '👋', label: 'Quit', desc: 'Exit the application' }
14
+ ];
15
+
16
+ function CommandPalette({ isOpen, filter, selectedIndex, onSelect, onClose }) {
17
+ if (!isOpen) return null;
18
+
19
+ // Filter commands based on input after "/"
20
+ const filteredCommands = filter
21
+ ? COMMANDS.filter(cmd =>
22
+ cmd.key.toLowerCase().includes(filter.toLowerCase()) ||
23
+ cmd.label.toLowerCase().includes(filter.toLowerCase())
24
+ )
25
+ : COMMANDS;
26
+
27
+ return React.createElement(Box, {
28
+ flexDirection: 'column',
29
+ borderStyle: 'round',
30
+ borderColor: 'cyan',
31
+ paddingX: 1,
32
+ paddingY: 0,
33
+ marginX: 2
34
+ },
35
+ React.createElement(Box, { marginBottom: 1 },
36
+ React.createElement(Text, { bold: true, color: 'cyan' }, '/ Commands'),
37
+ React.createElement(Text, { color: 'gray' }, ' (Up/Down select, Enter run, Esc cancel)')
38
+ ),
39
+
40
+ filteredCommands.length === 0
41
+ ? React.createElement(Text, { color: 'yellow' }, 'No matching commands')
42
+ : filteredCommands.map((cmd, idx) => {
43
+ const isSelected = idx === selectedIndex;
44
+ return React.createElement(Box, { key: cmd.key },
45
+ React.createElement(Text, {
46
+ color: isSelected ? 'cyan' : 'white',
47
+ bold: isSelected,
48
+ inverse: isSelected
49
+ }, isSelected ? ' ▶ ' : ' '),
50
+ React.createElement(Text, null, `${cmd.icon} `),
51
+ React.createElement(Text, {
52
+ color: isSelected ? 'cyan' : 'white',
53
+ bold: isSelected
54
+ }, cmd.label.padEnd(15)),
55
+ React.createElement(Text, { color: 'gray' }, cmd.desc)
56
+ );
57
+ })
58
+ );
59
+ }
60
+
61
+ // Export commands for use in App.js
62
+ CommandPalette.COMMANDS = COMMANDS;
63
+
64
+ module.exports = CommandPalette;
@@ -0,0 +1,168 @@
1
+ // src/tui/ConfigView.js - Configuration View Component
2
+
3
+ const React = require('react');
4
+ const { useState, useEffect } = React;
5
+ const { Box, Text, useInput } = require('ink');
6
+ const TextInput = require('ink-text-input').default;
7
+ const config = require('../config');
8
+
9
+ function ConfigView({ onBack }) {
10
+ const [currentConfig, setCurrentConfig] = useState(null);
11
+ const [editingApiKey, setEditingApiKey] = useState(false);
12
+ const [apiKeyValue, setApiKeyValue] = useState('');
13
+ const [message, setMessage] = useState(null);
14
+ const [selectedOption, setSelectedOption] = useState(0);
15
+
16
+ const options = [
17
+ { key: 'view', label: 'View Current Config' },
18
+ { key: 'apikey', label: 'Set API Key' },
19
+ { key: 'paths', label: 'View Data Paths' }
20
+ ];
21
+
22
+ useEffect(() => {
23
+ loadConfig();
24
+ }, []);
25
+
26
+ const loadConfig = () => {
27
+ const cfg = config.getUserConfig();
28
+ setCurrentConfig(cfg);
29
+ setApiKeyValue(cfg.api?.apiKey || '');
30
+ };
31
+
32
+ useInput((input, key) => {
33
+ if (editingApiKey) {
34
+ if (key.escape) {
35
+ setEditingApiKey(false);
36
+ loadConfig();
37
+ } else if (key.return) {
38
+ saveApiKey();
39
+ }
40
+ return;
41
+ }
42
+
43
+ if (key.escape) {
44
+ onBack();
45
+ return;
46
+ }
47
+
48
+ if (key.upArrow) {
49
+ setSelectedOption(Math.max(0, selectedOption - 1));
50
+ } else if (key.downArrow) {
51
+ setSelectedOption(Math.min(options.length - 1, selectedOption + 1));
52
+ } else if (key.return) {
53
+ executeOption(options[selectedOption].key);
54
+ }
55
+ });
56
+
57
+ const executeOption = (optionKey) => {
58
+ setMessage(null);
59
+
60
+ if (optionKey === 'view') {
61
+ loadConfig();
62
+ setMessage('Config refreshed');
63
+ } else if (optionKey === 'apikey') {
64
+ setEditingApiKey(true);
65
+ } else if (optionKey === 'paths') {
66
+ const paths = config.getPaths();
67
+ setMessage(`Data: ${paths.dataDir}`);
68
+ }
69
+ };
70
+
71
+ const saveApiKey = () => {
72
+ try {
73
+ config.setApiKey(apiKeyValue);
74
+ setEditingApiKey(false);
75
+ setMessage('✅ API Key saved successfully!');
76
+ loadConfig();
77
+ } catch (err) {
78
+ setMessage(`❌ Error: ${err.message}`);
79
+ }
80
+ };
81
+
82
+ const maskApiKey = (key) => {
83
+ if (!key) return '(not set)';
84
+ if (key.length <= 8) return '****';
85
+ return key.slice(0, 4) + '****' + key.slice(-4);
86
+ };
87
+
88
+ return React.createElement(Box, { flexDirection: 'column' },
89
+ React.createElement(Box, { marginBottom: 1 },
90
+ React.createElement(Text, { bold: true, color: 'cyan' }, '⚙️ Configuration')
91
+ ),
92
+
93
+ // Current config display
94
+ currentConfig && React.createElement(Box, {
95
+ flexDirection: 'column',
96
+ marginBottom: 1,
97
+ marginBottom: 1,
98
+ // Removed borderStyle
99
+ // paddingX: 1
100
+ },
101
+ // Use dim color for label
102
+ React.createElement(Text, { color: 'gray', dimColor: true }, ' Current Settings:'),
103
+ React.createElement(Text, null,
104
+ ` API URL: ${currentConfig.api?.baseUrl || 'N/A'}`
105
+ ),
106
+ React.createElement(Text, null,
107
+ ` API Key: ${maskApiKey(currentConfig.api?.apiKey)}`
108
+ ),
109
+ React.createElement(Text, null,
110
+ ` Cache TTL: ${(currentConfig.cache?.ttl || 0) / 3600000}h`
111
+ )
112
+ ),
113
+
114
+ // API Key editing mode
115
+ editingApiKey && React.createElement(Box, { flexDirection: 'column', marginBottom: 1 },
116
+ React.createElement(Text, { color: 'yellow' }, 'Enter new API Key:'),
117
+ React.createElement(Box, null,
118
+ React.createElement(Text, { color: 'gray' }, '> '),
119
+ React.createElement(TextInput, {
120
+ value: apiKeyValue,
121
+ onChange: setApiKeyValue,
122
+ placeholder: 'sk-xxxxxxxx...',
123
+ focus: true
124
+ })
125
+ ),
126
+ React.createElement(Text, { color: 'gray', dimColor: true },
127
+ 'Enter to save │ Esc to cancel'
128
+ )
129
+ ),
130
+
131
+ // Options (when not editing)
132
+ !editingApiKey && React.createElement(Box, { flexDirection: 'column', marginBottom: 1 },
133
+ options.map((opt, idx) => {
134
+ const isSelected = idx === selectedOption;
135
+ return React.createElement(Box, { key: opt.key },
136
+ React.createElement(Text, {
137
+ color: isSelected ? 'cyan' : 'white',
138
+ inverse: isSelected
139
+ },
140
+ isSelected ? '▶ ' : ' '
141
+ ),
142
+ React.createElement(Text, {
143
+ color: isSelected ? 'cyan' : 'white',
144
+ bold: isSelected
145
+ },
146
+ opt.label
147
+ )
148
+ );
149
+ })
150
+ ),
151
+
152
+ // Message
153
+ message && React.createElement(Box, { marginTop: 1 },
154
+ React.createElement(Text, {
155
+ color: message.startsWith('✅') ? 'green' :
156
+ message.startsWith('❌') ? 'red' : 'gray'
157
+ }, message)
158
+ ),
159
+
160
+ !editingApiKey && React.createElement(Box, { marginTop: 1 },
161
+ React.createElement(Text, { color: 'gray', dimColor: true },
162
+ 'Up/Down Navigate │ Enter Select'
163
+ )
164
+ )
165
+ );
166
+ }
167
+
168
+ module.exports = ConfigView;
@@ -0,0 +1,139 @@
1
+ // src/tui/DetailView.js - Skill Detail View Component
2
+
3
+ const React = require('react');
4
+ const { useState, useEffect } = React;
5
+ const { Box, Text, useInput } = require('ink');
6
+ const store = require('../store');
7
+ const api = require('../api');
8
+
9
+ function DetailView({ skill, onBack }) {
10
+ const [content, setContent] = useState(null);
11
+ const [loading, setLoading] = useState(true);
12
+ const [error, setError] = useState(null);
13
+ const [scrollOffset, setScrollOffset] = useState(0);
14
+ const visibleLines = 20;
15
+
16
+ useEffect(() => {
17
+ if (skill) {
18
+ loadContent();
19
+ }
20
+ }, [skill]);
21
+
22
+ const loadContent = async () => {
23
+ setLoading(true);
24
+ setError(null);
25
+
26
+ try {
27
+ // Try local first
28
+ let doc = store.getDoc(skill.id);
29
+
30
+ if (!doc && skill.githubUrl) {
31
+ // Fetch from remote
32
+ doc = await api.fetchSkillContent(skill.githubUrl);
33
+ store.setDoc(skill.id, doc);
34
+ }
35
+
36
+ setContent(doc || 'No content available.');
37
+ } catch (err) {
38
+ setError(err.message);
39
+ setContent(null);
40
+ }
41
+ setLoading(false);
42
+ };
43
+
44
+ useInput((input, key) => {
45
+ if (key.escape || input === 'b') {
46
+ onBack();
47
+ return;
48
+ }
49
+
50
+ const lines = (content || '').split('\n');
51
+ const maxScroll = Math.max(0, lines.length - visibleLines);
52
+
53
+ if (key.upArrow) {
54
+ setScrollOffset(Math.max(0, scrollOffset - 1));
55
+ } else if (key.downArrow) {
56
+ setScrollOffset(Math.min(maxScroll, scrollOffset + 1));
57
+ } else if (key.pageUp) {
58
+ setScrollOffset(Math.max(0, scrollOffset - visibleLines));
59
+ } else if (key.pageDown) {
60
+ setScrollOffset(Math.min(maxScroll, scrollOffset + visibleLines));
61
+ }
62
+ });
63
+
64
+ if (!skill) {
65
+ return React.createElement(Text, { color: 'red' }, '❌ No skill selected');
66
+ }
67
+
68
+ if (loading) {
69
+ return React.createElement(Box, { flexDirection: 'column' },
70
+ React.createElement(Text, { color: 'yellow' },
71
+ `⏳ Loading ${skill.id}...`
72
+ )
73
+ );
74
+ }
75
+
76
+ if (error) {
77
+ return React.createElement(Box, { flexDirection: 'column' },
78
+ React.createElement(Text, { color: 'red' }, `❌ Error: ${error}`),
79
+ React.createElement(Text, { color: 'gray' }, 'Press Esc or "b" to go back')
80
+ );
81
+ }
82
+
83
+ const lines = content.split('\n');
84
+ const visibleContent = lines.slice(scrollOffset, scrollOffset + visibleLines);
85
+
86
+ return React.createElement(Box, { flexDirection: 'column' },
87
+ // Header
88
+ React.createElement(Box, { marginBottom: 1 },
89
+ React.createElement(Text, { bold: true, color: 'cyan' },
90
+ `📄 ${skill.name || skill.id}`
91
+ ),
92
+ React.createElement(Text, { color: 'gray' },
93
+ ` (${scrollOffset + 1}-${Math.min(scrollOffset + visibleLines, lines.length)}/${lines.length})`
94
+ )
95
+ ),
96
+
97
+ // Content
98
+ React.createElement(Box, { flexDirection: 'column' },
99
+ visibleContent.map((line, idx) => {
100
+ // Simple markdown highlighting
101
+ let color = 'white';
102
+ let bold = false;
103
+
104
+ if (line.startsWith('# ')) {
105
+ color = 'magenta';
106
+ bold = true;
107
+ } else if (line.startsWith('## ')) {
108
+ color = 'green';
109
+ bold = true;
110
+ } else if (line.startsWith('### ')) {
111
+ color = 'cyan';
112
+ bold = true;
113
+ } else if (line.startsWith('```')) {
114
+ color = 'yellow';
115
+ } else if (line.startsWith('- ') || line.startsWith('* ')) {
116
+ color = 'white';
117
+ } else if (line.startsWith('>')) {
118
+ color = 'gray';
119
+ }
120
+
121
+ return React.createElement(Text, {
122
+ key: idx,
123
+ color,
124
+ bold,
125
+ wrap: 'truncate-end'
126
+ }, line || ' ');
127
+ })
128
+ ),
129
+
130
+ // Footer
131
+ React.createElement(Box, { marginTop: 1 },
132
+ React.createElement(Text, { color: 'gray', dimColor: true },
133
+ 'Up/Down Scroll │ PgUp/PgDn │ Esc/b Back'
134
+ )
135
+ )
136
+ );
137
+ }
138
+
139
+ module.exports = DetailView;
@@ -0,0 +1,114 @@
1
+ // src/tui/DualPane.js - Dual Column Results Display
2
+
3
+ const React = require('react');
4
+ const { useState } = React;
5
+ const { Box, Text, useInput } = require('ink');
6
+ const store = require('../store');
7
+
8
+ function SkillItem({ skill, isSelected, isLocal }) {
9
+ const hasCache = isLocal && store.getDoc(skill.id) !== null;
10
+
11
+ return React.createElement(Box, null,
12
+ React.createElement(Text, {
13
+ color: isSelected ? 'cyan' : 'white',
14
+ bold: isSelected,
15
+ inverse: isSelected
16
+ }, isSelected ? ' ▶ ' : ' '),
17
+ React.createElement(Text, {
18
+ color: isSelected ? 'cyan' : (isLocal ? 'green' : 'yellow')
19
+ }, (skill.name || skill.id).slice(0, 25).padEnd(25)),
20
+ hasCache && React.createElement(Text, { color: 'green' }, ' ✓')
21
+ );
22
+ }
23
+
24
+ function DualPane({
25
+ localSkills,
26
+ remoteSkills,
27
+ activePane, // 'local' or 'remote'
28
+ localIndex,
29
+ remoteIndex,
30
+ onSelect,
31
+ onPaneChange,
32
+ isActive
33
+ }) {
34
+ const visibleCount = 8;
35
+
36
+ // Calculate scroll offsets
37
+ const localOffset = Math.max(0, localIndex - visibleCount + 1);
38
+ const remoteOffset = Math.max(0, remoteIndex - visibleCount + 1);
39
+
40
+ const visibleLocal = localSkills.slice(localOffset, localOffset + visibleCount);
41
+ const visibleRemote = remoteSkills.slice(remoteOffset, remoteOffset + visibleCount);
42
+
43
+ return React.createElement(Box, { flexDirection: 'row', flexGrow: 1 },
44
+ // Left Pane - Local Skills
45
+ React.createElement(Box, {
46
+ flexDirection: 'column',
47
+ width: '50%',
48
+ borderStyle: 'single',
49
+ borderColor: activePane === 'local' ? 'cyan' : 'gray',
50
+ paddingX: 1
51
+ },
52
+ React.createElement(Box, { marginBottom: 1 },
53
+ React.createElement(Text, {
54
+ bold: true,
55
+ color: activePane === 'local' ? 'cyan' : 'white'
56
+ }, '📚 LOCAL SKILLS'),
57
+ React.createElement(Text, { color: 'gray' }, ` (${localSkills.length})`)
58
+ ),
59
+
60
+ localSkills.length === 0
61
+ ? React.createElement(Text, { color: 'gray', dimColor: true }, 'No local skills found')
62
+ : visibleLocal.map((skill, idx) =>
63
+ React.createElement(SkillItem, {
64
+ key: skill.id,
65
+ skill: skill,
66
+ isSelected: activePane === 'local' && (localOffset + idx) === localIndex,
67
+ isLocal: true
68
+ })
69
+ ),
70
+
71
+ localSkills.length > visibleCount && React.createElement(Box, { marginTop: 1 },
72
+ React.createElement(Text, { color: 'gray', dimColor: true },
73
+ `${localIndex + 1}/${localSkills.length}`
74
+ )
75
+ )
76
+ ),
77
+
78
+ // Right Pane - Remote Skills
79
+ React.createElement(Box, {
80
+ flexDirection: 'column',
81
+ width: '50%',
82
+ borderStyle: 'single',
83
+ borderColor: activePane === 'remote' ? 'cyan' : 'gray',
84
+ paddingX: 1
85
+ },
86
+ React.createElement(Box, { marginBottom: 1 },
87
+ React.createElement(Text, {
88
+ bold: true,
89
+ color: activePane === 'remote' ? 'cyan' : 'white'
90
+ }, '🌐 REMOTE SKILLS'),
91
+ React.createElement(Text, { color: 'gray' }, ` (${remoteSkills.length})`)
92
+ ),
93
+
94
+ remoteSkills.length === 0
95
+ ? React.createElement(Text, { color: 'gray', dimColor: true }, 'Search to find remote skills')
96
+ : visibleRemote.map((skill, idx) =>
97
+ React.createElement(SkillItem, {
98
+ key: skill.id,
99
+ skill: skill,
100
+ isSelected: activePane === 'remote' && (remoteOffset + idx) === remoteIndex,
101
+ isLocal: false
102
+ })
103
+ ),
104
+
105
+ remoteSkills.length > visibleCount && React.createElement(Box, { marginTop: 1 },
106
+ React.createElement(Text, { color: 'gray', dimColor: true },
107
+ `${remoteIndex + 1}/${remoteSkills.length}`
108
+ )
109
+ )
110
+ )
111
+ );
112
+ }
113
+
114
+ module.exports = DualPane;
@@ -0,0 +1,163 @@
1
+ // src/tui/PrimaryView.js - Primary Directory Selection View Component
2
+
3
+ const React = require('react');
4
+ const { useState, useEffect } = React;
5
+ const { Box, Text, useInput } = require('ink');
6
+ const config = require('../config');
7
+ const theme = require('../theme');
8
+ const { scanForSkillDirectories } = require('../localCrawler');
9
+
10
+ function PrimaryView({ onBack, onDirChange }) {
11
+ const [currentDir, setCurrentDir] = useState(config.getPrimaryDirName());
12
+ const [selectedOption, setSelectedOption] = useState(0);
13
+ const [message, setMessage] = useState(null);
14
+ const [availableDirs, setAvailableDirs] = useState(config.getAvailablePrimaryDirs());
15
+
16
+ const t = theme.getTheme(); // Get current theme colors
17
+
18
+ // Load extra directories found on disk
19
+ useEffect(() => {
20
+ let mounted = true;
21
+
22
+ const loadDirs = async () => {
23
+ try {
24
+ const foundDirs = await scanForSkillDirectories();
25
+ if (!mounted) return;
26
+
27
+ setAvailableDirs(prev => {
28
+ const next = [...prev];
29
+ const existingKeys = new Set(prev.map(d => d.key));
30
+ let changed = false;
31
+
32
+ foundDirs.forEach(dirName => {
33
+ // config.js assumes primary dirs start with '.', so we only support those for now
34
+ if (!dirName.startsWith('.')) return;
35
+
36
+ const key = dirName.substring(1);
37
+ if (!existingKeys.has(key)) {
38
+ next.push({
39
+ key: key,
40
+ name: dirName,
41
+ desc: 'Found local directory'
42
+ });
43
+ changed = true;
44
+ }
45
+ });
46
+
47
+ return changed ? next : prev;
48
+ });
49
+ } catch (err) {
50
+ // Ignore errors
51
+ }
52
+ };
53
+
54
+ loadDirs();
55
+
56
+ return () => { mounted = false; };
57
+ }, []);
58
+
59
+ // Set initial selection to current directory
60
+ useEffect(() => {
61
+ const idx = availableDirs.findIndex(d => d.key === currentDir);
62
+
63
+ if (idx >= 0) {
64
+ setSelectedOption(idx);
65
+ }
66
+ }, [currentDir]);
67
+
68
+ useInput((input, key) => {
69
+ if (key.escape) {
70
+ onBack();
71
+ return;
72
+ }
73
+
74
+ if (key.upArrow) {
75
+ setSelectedOption(Math.max(0, selectedOption - 1));
76
+ } else if (key.downArrow) {
77
+ setSelectedOption(Math.min(availableDirs.length - 1, selectedOption + 1));
78
+ } else if (key.return) {
79
+ const selected = availableDirs[selectedOption];
80
+ if (selected.key !== currentDir) {
81
+ config.setPrimaryDir(selected.key);
82
+ setCurrentDir(selected.key);
83
+ setMessage(`✅ Primary directory changed to ${selected.name}`);
84
+ if (onDirChange) {
85
+ onDirChange(selected.key);
86
+ }
87
+ } else {
88
+ setMessage(`Already using ${selected.name} as primary directory.`);
89
+ }
90
+ }
91
+ });
92
+
93
+ return React.createElement(Box, { flexDirection: 'column' },
94
+ React.createElement(Box, { marginBottom: 1 },
95
+ React.createElement(Text, { bold: true, color: t.primary }, '📁 Primary Directory Settings')
96
+ ),
97
+
98
+ // Current directory display
99
+ React.createElement(Box, {
100
+ flexDirection: 'column',
101
+ marginBottom: 1
102
+ },
103
+ React.createElement(Text, { color: t.textDim },
104
+ ` Current: ~/${availableDirs.find(d => d.key === currentDir)?.name || '.' + currentDir}`
105
+ )
106
+ ),
107
+
108
+ // Info box
109
+ React.createElement(Box, {
110
+ flexDirection: 'column',
111
+ marginBottom: 1,
112
+ paddingX: 1
113
+ },
114
+ React.createElement(Text, { color: t.textDim },
115
+ 'The primary directory is where synced skills are stored and'
116
+ ),
117
+ React.createElement(Text, { color: t.textDim },
118
+ 'displayed first in the local list.'
119
+ )
120
+ ),
121
+
122
+ // Directory options
123
+ React.createElement(Box, { flexDirection: 'column', marginBottom: 1 },
124
+ React.createElement(Text, { color: t.textDim, marginBottom: 1 }, 'Select primary directory:'),
125
+ availableDirs.map((dirOption, idx) => {
126
+ const isSelected = idx === selectedOption;
127
+ const isCurrent = dirOption.key === currentDir;
128
+
129
+ return React.createElement(Box, { key: dirOption.key },
130
+ React.createElement(Text, {
131
+ color: isSelected ? t.selected : t.text,
132
+ inverse: isSelected
133
+ },
134
+ isSelected ? '▶ ' : ' '
135
+ ),
136
+ React.createElement(Text, {
137
+ color: isSelected ? t.selected : t.text,
138
+ bold: isSelected
139
+ },
140
+ dirOption.name.padEnd(16)
141
+ ),
142
+ React.createElement(Text, { color: t.textDim }, dirOption.desc),
143
+ isCurrent && React.createElement(Text, { color: t.success }, ' (current)')
144
+ );
145
+ })
146
+ ),
147
+
148
+ // Message
149
+ message && React.createElement(Box, { marginTop: 1 },
150
+ React.createElement(Text, {
151
+ color: message.startsWith('✅') ? t.success : t.textDim
152
+ }, message)
153
+ ),
154
+
155
+ React.createElement(Box, { marginTop: 1 },
156
+ React.createElement(Text, { color: t.textDim, dimColor: true },
157
+ 'Up/Down Navigate │ Enter Select │ Esc Back'
158
+ )
159
+ )
160
+ );
161
+ }
162
+
163
+ module.exports = PrimaryView;