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,26 @@
1
+ // src/tui/SearchBox.js - Central Search Input Component
2
+
3
+ const React = require('react');
4
+ const { Box, Text } = require('ink');
5
+ const TextInput = require('ink-text-input').default;
6
+
7
+ function SearchBox({ value, onChange, onSubmit, placeholder, isFocused }) {
8
+ return React.createElement(Box, {
9
+ borderStyle: 'single',
10
+ borderColor: isFocused ? 'cyan' : 'gray',
11
+ paddingX: 1
12
+ },
13
+ React.createElement(Box, { flexGrow: 1 },
14
+ React.createElement(TextInput, {
15
+ value: value,
16
+ onChange: onChange,
17
+ onSubmit: onSubmit,
18
+ placeholder: placeholder || 'Type to search...',
19
+ focus: isFocused
20
+ })
21
+ ),
22
+ React.createElement(Text, { color: 'gray' }, ' [/] Commands [Esc] Quit')
23
+ );
24
+ }
25
+
26
+ module.exports = SearchBox;
@@ -0,0 +1,121 @@
1
+ // src/tui/SearchView.js - Search 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 { searchSkills } = require('../matcher');
8
+
9
+ function SearchView({ onSelect, onCancel }) {
10
+ const [query, setQuery] = useState('');
11
+ const [results, setResults] = useState([]);
12
+ const [selectedIndex, setSelectedIndex] = useState(0);
13
+ const [isInputMode, setIsInputMode] = useState(true);
14
+
15
+ useEffect(() => {
16
+ if (query.length >= 2) {
17
+ performSearch(query);
18
+ } else {
19
+ setResults([]);
20
+ }
21
+ }, [query]);
22
+
23
+ const performSearch = async (q) => {
24
+ const res = await searchSkills(q);
25
+ setResults(res);
26
+ setSelectedIndex(0);
27
+ };
28
+
29
+ useInput((input, key) => {
30
+ if (key.escape) {
31
+ if (!isInputMode) {
32
+ setIsInputMode(true);
33
+ } else {
34
+ onCancel();
35
+ }
36
+ return;
37
+ }
38
+
39
+ if (!isInputMode) {
40
+ if (key.upArrow) {
41
+ setSelectedIndex(Math.max(0, selectedIndex - 1));
42
+ } else if (key.downArrow) {
43
+ setSelectedIndex(Math.min(results.length - 1, selectedIndex + 1));
44
+ } else if (key.return && results.length > 0) {
45
+ onSelect(results[selectedIndex]);
46
+ }
47
+ } else {
48
+ if (key.downArrow && results.length > 0) {
49
+ setIsInputMode(false);
50
+ setSelectedIndex(0);
51
+ } else if (key.return && results.length > 0) {
52
+ setIsInputMode(false);
53
+ setSelectedIndex(0);
54
+ }
55
+ }
56
+ });
57
+
58
+ return React.createElement(Box, { flexDirection: 'column' },
59
+ React.createElement(Box, { marginBottom: 1 },
60
+ React.createElement(Text, { bold: true, color: 'cyan' }, '🔍 Search Skills')
61
+ ),
62
+
63
+ // Search input
64
+ React.createElement(Box, { marginBottom: 1 },
65
+ React.createElement(Text, { color: 'gray' }, '> '),
66
+ React.createElement(TextInput, {
67
+ value: query,
68
+ onChange: setQuery,
69
+ placeholder: 'Type to search...',
70
+ focus: isInputMode
71
+ })
72
+ ),
73
+
74
+ // Results
75
+ results.length > 0 && React.createElement(Box, { flexDirection: 'column' },
76
+ React.createElement(Text, { color: 'gray', dimColor: true },
77
+ `Found ${results.length} results:`
78
+ ),
79
+ React.createElement(Box, { marginTop: 1, flexDirection: 'column' },
80
+ results.slice(0, 10).map((skill, idx) => {
81
+ const isSelected = !isInputMode && idx === selectedIndex;
82
+ const score = Math.round((1 - (skill.score || 0)) * 100);
83
+
84
+ return React.createElement(Box, { key: skill.id },
85
+ React.createElement(Text, {
86
+ color: isSelected ? 'cyan' : 'white',
87
+ inverse: isSelected
88
+ },
89
+ isSelected ? '▶ ' : ' '
90
+ ),
91
+ React.createElement(Text, {
92
+ color: isSelected ? 'cyan' : 'green'
93
+ },
94
+ skill.id.padEnd(25)
95
+ ),
96
+ React.createElement(Text, { color: 'gray' },
97
+ (skill.name || '').slice(0, 30)
98
+ ),
99
+ React.createElement(Text, { color: 'gray', dimColor: true },
100
+ ` (${score}%)`
101
+ )
102
+ );
103
+ })
104
+ )
105
+ ),
106
+
107
+ results.length === 0 && query.length >= 2 && React.createElement(
108
+ Text, { color: 'yellow' }, `No results for "${query}"`
109
+ ),
110
+
111
+ React.createElement(Box, { marginTop: 1 },
112
+ React.createElement(Text, { color: 'gray', dimColor: true },
113
+ isInputMode
114
+ ? 'Down Navigate to results │ Esc Cancel'
115
+ : 'Up/Down Navigate │ Enter Select │ Esc Back to input'
116
+ )
117
+ )
118
+ );
119
+ }
120
+
121
+ module.exports = SearchView;
@@ -0,0 +1,102 @@
1
+ // src/tui/SkillList.js - Skills List View Component
2
+
3
+ const React = require('react');
4
+ const { useState, useEffect } = React;
5
+ const { Box, Text, useInput } = require('ink');
6
+ const { listSkills } = require('../matcher');
7
+ const store = require('../store');
8
+
9
+ function SkillList({ onSelect }) {
10
+ const [skills, setSkills] = useState([]);
11
+ const [selectedIndex, setSelectedIndex] = useState(0);
12
+ const [loading, setLoading] = useState(true);
13
+ const [scrollOffset, setScrollOffset] = useState(0);
14
+ const visibleCount = 15; // Number of visible items
15
+
16
+ useEffect(() => {
17
+ loadSkills();
18
+ }, []);
19
+
20
+ const loadSkills = async () => {
21
+ setLoading(true);
22
+ try {
23
+ const allSkills = await listSkills();
24
+ setSkills(allSkills);
25
+ } catch (err) {
26
+ setSkills([]);
27
+ }
28
+ setLoading(false);
29
+ };
30
+
31
+ useInput((input, key) => {
32
+ if (skills.length === 0) return;
33
+
34
+ if (key.upArrow) {
35
+ const newIndex = Math.max(0, selectedIndex - 1);
36
+ setSelectedIndex(newIndex);
37
+ if (newIndex < scrollOffset) {
38
+ setScrollOffset(newIndex);
39
+ }
40
+ } else if (key.downArrow) {
41
+ const newIndex = Math.min(skills.length - 1, selectedIndex + 1);
42
+ setSelectedIndex(newIndex);
43
+ if (newIndex >= scrollOffset + visibleCount) {
44
+ setScrollOffset(newIndex - visibleCount + 1);
45
+ }
46
+ } else if (key.return) {
47
+ onSelect(skills[selectedIndex]);
48
+ }
49
+ });
50
+
51
+ if (loading) {
52
+ return React.createElement(Box, { flexDirection: 'column' },
53
+ React.createElement(Text, { color: 'yellow' }, '⏳ Loading skills...')
54
+ );
55
+ }
56
+
57
+ if (skills.length === 0) {
58
+ return React.createElement(Box, { flexDirection: 'column' },
59
+ React.createElement(Text, { color: 'yellow' }, '⚠️ No local data found.'),
60
+ React.createElement(Text, { color: 'gray' }, 'Press "y" to open Sync view and sync data first.')
61
+ );
62
+ }
63
+
64
+ const visibleSkills = skills.slice(scrollOffset, scrollOffset + visibleCount);
65
+
66
+ return React.createElement(Box, { flexDirection: 'column' },
67
+ React.createElement(Box, { marginBottom: 1 },
68
+ React.createElement(Text, { bold: true, color: 'cyan' },
69
+ `📚 Skills (${skills.length} total)`
70
+ ),
71
+ scrollOffset > 0 && React.createElement(Text, { color: 'gray' }, ' ▲'),
72
+ scrollOffset + visibleCount < skills.length && React.createElement(Text, { color: 'gray' }, ' ▼')
73
+ ),
74
+
75
+ visibleSkills.map((skill, idx) => {
76
+ const actualIndex = scrollOffset + idx;
77
+ const isSelected = actualIndex === selectedIndex;
78
+ const hasDoc = store.getDoc(skill.id) !== null;
79
+
80
+ return React.createElement(Box, { key: skill.id },
81
+ React.createElement(Text, {
82
+ color: isSelected ? 'cyan' : 'white',
83
+ bold: isSelected,
84
+ inverse: isSelected
85
+ },
86
+ isSelected ? '▶ ' : ' '
87
+ ),
88
+ React.createElement(Text, {
89
+ color: isSelected ? 'cyan' : 'green'
90
+ },
91
+ skill.id.padEnd(25)
92
+ ),
93
+ React.createElement(Text, { color: 'gray' },
94
+ (skill.name || skill.description || '').slice(0, 35)
95
+ ),
96
+ hasDoc && React.createElement(Text, { color: 'green' }, ' ✓')
97
+ );
98
+ })
99
+ );
100
+ }
101
+
102
+ module.exports = SkillList;
@@ -0,0 +1,143 @@
1
+ // src/tui/SyncView.js - Sync View Component
2
+
3
+ const React = require('react');
4
+ const { useState } = React;
5
+ const { Box, Text, useInput } = require('ink');
6
+ const Spinner = require('ink-spinner').default;
7
+ const syncer = require('../syncer');
8
+
9
+ function SyncView({ onBack }) {
10
+ const [status, setStatus] = useState(null);
11
+ const [syncing, setSyncing] = useState(false);
12
+ const [progress, setProgress] = useState('');
13
+ const [result, setResult] = useState(null);
14
+ const [selectedOption, setSelectedOption] = useState(0);
15
+
16
+ const options = [
17
+ { key: 'local', label: 'Sync Local Skills', desc: 'Sync locally found skills to primary directory' },
18
+ { key: 'remote', label: 'Sync Remote Skills', desc: 'Download all remote skills to primary directory' },
19
+ { key: 'all', label: 'Sync All', desc: 'Sync both local and remote skills to primary directory' },
20
+ { key: 'status', label: 'View Status', desc: 'Check sync status' }
21
+ ];
22
+
23
+ React.useEffect(() => {
24
+ const currentStatus = syncer.getStatus();
25
+ setStatus(currentStatus);
26
+ }, []);
27
+
28
+ useInput((input, key) => {
29
+ if (syncing) return;
30
+
31
+ if (key.escape) {
32
+ onBack();
33
+ return;
34
+ }
35
+
36
+ if (key.upArrow) {
37
+ setSelectedOption(Math.max(0, selectedOption - 1));
38
+ } else if (key.downArrow) {
39
+ setSelectedOption(Math.min(options.length - 1, selectedOption + 1));
40
+ } else if (key.return) {
41
+ executeOption(options[selectedOption].key);
42
+ }
43
+ });
44
+
45
+ const executeOption = async (optionKey) => {
46
+ setResult(null);
47
+
48
+ if (optionKey === 'status') {
49
+ const s = syncer.getStatus();
50
+ setStatus(s);
51
+ setResult(s ? 'Status refreshed' : 'No sync history');
52
+ return;
53
+ }
54
+
55
+ setSyncing(true);
56
+ setProgress('Starting sync...');
57
+
58
+ try {
59
+ await syncer.sync({
60
+ mode: optionKey // 'local', 'remote', 'all'
61
+ });
62
+ setResult('✅ Sync completed successfully!');
63
+ setStatus(syncer.getStatus());
64
+ } catch (err) {
65
+ setResult(`❌ Sync failed: ${err.message}`);
66
+ }
67
+
68
+ setSyncing(false);
69
+ setProgress('');
70
+ };
71
+
72
+ return React.createElement(Box, { flexDirection: 'column' },
73
+ React.createElement(Box, { marginBottom: 1 },
74
+ React.createElement(Text, { bold: true, color: 'cyan' }, '🔄 Sync Manager')
75
+ ),
76
+
77
+ // Current status
78
+ status && React.createElement(Box, {
79
+ flexDirection: 'column',
80
+ marginBottom: 1,
81
+ marginBottom: 1,
82
+ // Removed borderStyle
83
+ // paddingX: 1
84
+ },
85
+ // Use dim color for label for consistency
86
+ React.createElement(Text, { color: 'gray', dimColor: true }, ' 📊 Current Status:'),
87
+ React.createElement(Text, null,
88
+ ` Last Sync: ${status.lastSync ? new Date(status.lastSync).toLocaleString() : 'Never'}`
89
+ ),
90
+ React.createElement(Text, null, ` Total Skills: ${status.totalSkills || 0}`),
91
+ React.createElement(Text, null, ` Cached Docs: ${status.syncedDocs || 0}`)
92
+ ),
93
+
94
+ !status && React.createElement(Text, { color: 'yellow', marginBottom: 1 },
95
+ '⚠️ No sync history. Please sync first.'
96
+ ),
97
+
98
+ // Options
99
+ React.createElement(Box, { flexDirection: 'column', marginBottom: 1 },
100
+ React.createElement(Text, { color: 'gray', marginBottom: 1 }, 'Select an action:'),
101
+ options.map((opt, idx) => {
102
+ const isSelected = idx === selectedOption;
103
+ return React.createElement(Box, { key: opt.key },
104
+ React.createElement(Text, {
105
+ color: isSelected ? 'cyan' : 'white',
106
+ inverse: isSelected
107
+ },
108
+ isSelected ? '▶ ' : ' '
109
+ ),
110
+ React.createElement(Text, {
111
+ color: isSelected ? 'cyan' : 'white',
112
+ bold: isSelected
113
+ },
114
+ opt.label
115
+ ),
116
+ React.createElement(Text, { color: 'gray' }, ` - ${opt.desc}`)
117
+ );
118
+ })
119
+ ),
120
+
121
+ // Progress
122
+ syncing && React.createElement(Box, { marginTop: 1 },
123
+ React.createElement(Spinner, { type: 'dots' }),
124
+ React.createElement(Text, { color: 'yellow' }, ` ${progress}`)
125
+ ),
126
+
127
+ // Result
128
+ result && React.createElement(Box, { marginTop: 1 },
129
+ React.createElement(Text, {
130
+ color: result.startsWith('✅') ? 'green' :
131
+ result.startsWith('❌') ? 'red' : 'gray'
132
+ }, result)
133
+ ),
134
+
135
+ React.createElement(Box, { marginTop: 1 },
136
+ React.createElement(Text, { color: 'gray', dimColor: true },
137
+ 'Up/Down Navigate │ Enter Execute'
138
+ )
139
+ )
140
+ );
141
+ }
142
+
143
+ module.exports = SyncView;
@@ -0,0 +1,116 @@
1
+ // src/tui/ThemeView.js - Theme Selection View Component
2
+
3
+ const React = require('react');
4
+ const { useState, useEffect } = React;
5
+ const { Box, Text, useInput } = require('ink');
6
+ const theme = require('../theme');
7
+
8
+ function ThemeView({ onBack, onThemeChange }) {
9
+ const [currentTheme, setCurrentTheme] = useState(theme.getThemeName());
10
+ const [selectedOption, setSelectedOption] = useState(0);
11
+ const [message, setMessage] = useState(null);
12
+
13
+ const availableThemes = theme.getAvailableThemes();
14
+ const t = theme.getTheme(); // Get current theme colors
15
+
16
+ useInput((input, key) => {
17
+ if (key.escape) {
18
+ onBack();
19
+ return;
20
+ }
21
+
22
+ if (key.upArrow) {
23
+ setSelectedOption(Math.max(0, selectedOption - 1));
24
+ } else if (key.downArrow) {
25
+ setSelectedOption(Math.min(availableThemes.length - 1, selectedOption + 1));
26
+ } else if (key.return) {
27
+ const selected = availableThemes[selectedOption];
28
+ if (selected.key !== currentTheme) {
29
+ theme.setThemeName(selected.key);
30
+ setCurrentTheme(selected.key);
31
+ setMessage(`✅ Theme changed to ${selected.name}. Restart for full effect.`);
32
+ if (onThemeChange) {
33
+ onThemeChange(selected.key);
34
+ }
35
+ } else {
36
+ setMessage(`Already using ${selected.name} theme.`);
37
+ }
38
+ }
39
+ });
40
+
41
+ return React.createElement(Box, { flexDirection: 'column' },
42
+ React.createElement(Box, { marginBottom: 1 },
43
+ React.createElement(Text, { bold: true, color: t.primary }, '🎨 Theme Settings')
44
+ ),
45
+
46
+ // Current theme display
47
+ React.createElement(Box, {
48
+ flexDirection: 'column',
49
+ marginBottom: 1
50
+ },
51
+ React.createElement(Text, { color: t.textDim }, ` Current Theme: ${theme.THEMES[currentTheme]?.name || currentTheme}`)
52
+ ),
53
+
54
+ // Theme options
55
+ React.createElement(Box, { flexDirection: 'column', marginBottom: 1 },
56
+ React.createElement(Text, { color: t.textDim, marginBottom: 1 }, 'Select a theme:'),
57
+ availableThemes.map((themeOption, idx) => {
58
+ const isSelected = idx === selectedOption;
59
+ const isCurrent = themeOption.key === currentTheme;
60
+
61
+ return React.createElement(Box, { key: themeOption.key },
62
+ React.createElement(Text, {
63
+ color: isSelected ? t.selected : t.text,
64
+ inverse: isSelected
65
+ },
66
+ isSelected ? '▶ ' : ' '
67
+ ),
68
+ React.createElement(Text, {
69
+ color: isSelected ? t.selected : t.text,
70
+ bold: isSelected
71
+ },
72
+ themeOption.name
73
+ ),
74
+ isCurrent && React.createElement(Text, { color: t.success }, ' (current)')
75
+ );
76
+ })
77
+ ),
78
+
79
+ // Preview box
80
+ React.createElement(Box, {
81
+ flexDirection: 'column',
82
+ marginTop: 1,
83
+ borderStyle: 'single',
84
+ borderColor: availableThemes[selectedOption]?.key === 'dark' ? 'cyan' : 'blue',
85
+ paddingX: 1,
86
+ paddingY: 0
87
+ },
88
+ React.createElement(Text, { bold: true }, `Preview: ${availableThemes[selectedOption]?.name} Theme`),
89
+ React.createElement(Text, {
90
+ color: availableThemes[selectedOption]?.key === 'dark' ? 'cyan' : 'blue'
91
+ }, 'Primary Text'),
92
+ React.createElement(Text, {
93
+ color: availableThemes[selectedOption]?.key === 'dark' ? 'green' : 'magenta'
94
+ }, 'Local Highlight'),
95
+ React.createElement(Text, {
96
+ color: availableThemes[selectedOption]?.key === 'dark' ? 'yellow' : 'red'
97
+ }, 'Remote Highlight'),
98
+ React.createElement(Text, { color: 'gray' }, 'Dimmed Text')
99
+ ),
100
+
101
+ // Message
102
+ message && React.createElement(Box, { marginTop: 1 },
103
+ React.createElement(Text, {
104
+ color: message.startsWith('✅') ? t.success : t.textDim
105
+ }, message)
106
+ ),
107
+
108
+ React.createElement(Box, { marginTop: 1 },
109
+ React.createElement(Text, { color: t.textDim, dimColor: true },
110
+ 'Up/Down Navigate │ Enter Select │ Esc Back'
111
+ )
112
+ )
113
+ );
114
+ }
115
+
116
+ module.exports = ThemeView;
package/src/utils.js ADDED
@@ -0,0 +1,83 @@
1
+ const { marked } = require('marked');
2
+ const { markedTerminal } = require('marked-terminal');
3
+ const chalk = require('chalk');
4
+ const clipboardy = require('clipboardy');
5
+
6
+ // Configure marked with terminal renderer (marked-terminal v6+ API)
7
+ marked.use(markedTerminal({
8
+ codespan: chalk.yellow,
9
+ code: chalk.yellow,
10
+ link: chalk.blue.underline,
11
+ strong: chalk.bold.cyan,
12
+ firstHeading: chalk.bold.magenta.underline,
13
+ heading: chalk.bold.green
14
+ }));
15
+
16
+ function formatMarkdown(content) {
17
+ if (!content) return '';
18
+ try {
19
+ return marked(content);
20
+ } catch (err) {
21
+ console.error(chalk.red('Markdown parsing failed'));
22
+ return content;
23
+ }
24
+ }
25
+
26
+ async function copyToClipboard(content) {
27
+ try {
28
+ await clipboardy.write(content);
29
+ return true;
30
+ } catch (err) {
31
+ console.error(chalk.red('Clipboard copy failed: ' + err.message));
32
+ return false;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Format byte size
38
+ */
39
+ function formatBytes(bytes, decimals = 2) {
40
+ if (bytes === 0) return '0 Bytes';
41
+
42
+ const k = 1024;
43
+ const dm = decimals < 0 ? 0 : decimals;
44
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
45
+
46
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
47
+
48
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
49
+ }
50
+
51
+ module.exports = {
52
+ formatMarkdown,
53
+ copyToClipboard,
54
+ formatBytes,
55
+ parseFrontMatter
56
+ };
57
+
58
+ /**
59
+ * Parse Front Matter from Markdown
60
+ */
61
+ function parseFrontMatter(content) {
62
+ if (!content) return {};
63
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
64
+ if (!match) return {};
65
+
66
+ const frontMatter = {};
67
+ const lines = match[1].split('\n');
68
+
69
+ lines.forEach(line => {
70
+ const parts = line.split(':');
71
+ if (parts.length >= 2) {
72
+ const key = parts[0].trim();
73
+ let value = parts.slice(1).join(':').trim();
74
+ // Remove quotes if present
75
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
76
+ value = value.slice(1, -1);
77
+ }
78
+ frontMatter[key] = value;
79
+ }
80
+ });
81
+
82
+ return frontMatter;
83
+ }