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.
package/bin/skill.js ADDED
@@ -0,0 +1,348 @@
1
+ #!/usr/bin/env node
2
+ // bin/skill.js
3
+
4
+ // Check for TUI mode first (before Commander parsing)
5
+ const args = process.argv.slice(2);
6
+ const shouldLaunchTUI = args.length === 0 || args.includes('--tui') || args.includes('-t');
7
+
8
+ if (shouldLaunchTUI && !args.includes('--help') && !args.includes('-h')) {
9
+ // Launch TUI mode
10
+ require('./tui.js');
11
+ } else {
12
+ // Original CLI mode
13
+ const { program } = require('commander');
14
+ const chalk = require('chalk');
15
+ const { exec } = require('child_process');
16
+ const path = require('path');
17
+ const api = require('../src/api');
18
+ const syncer = require('../src/syncer');
19
+ const store = require('../src/store');
20
+ const { matchSkill, listSkills, searchSkills } = require('../src/matcher');
21
+ const { formatMarkdown } = require('../src/utils');
22
+ const { selectSkill } = require('../src/interactive');
23
+ const pkg = require('../package.json');
24
+
25
+ program
26
+ .name('skill')
27
+ .description('Skill Search: Retrieve skill documentation from SkillsMP')
28
+ .version(pkg.version, '-v, --version')
29
+ .option('-t, --tui', 'Launch in TUI (Text User Interface) mode');
30
+
31
+ // Main command: Fetch skill documentation
32
+ program
33
+ .argument('[keyword]', 'Skill ID or keyword')
34
+ .option('-r, --remote', 'Force fetch from remote API')
35
+ .action(async (keyword, options) => {
36
+ try {
37
+ let skill;
38
+ let isRemoteFetch = false;
39
+
40
+ // 1. Remote Mode
41
+ if (options.remote) {
42
+ console.log(chalk.gray('Searching remote...'));
43
+ const results = await api.searchSkills(keyword, { limit: 5 });
44
+
45
+ if (results.skills.length === 0) {
46
+ throw new Error(`No results found for "${keyword}" (Remote)`);
47
+ }
48
+
49
+ if (results.skills.length === 1) {
50
+ skill = results.skills[0];
51
+ } else {
52
+ skill = await selectSkill(results.skills);
53
+ }
54
+ isRemoteFetch = true;
55
+ }
56
+ // 2. Local Mode
57
+ else {
58
+ const result = await matchSkill(keyword);
59
+
60
+ if (result.type === 'empty') {
61
+ return;
62
+ }
63
+
64
+ switch (result.type) {
65
+ case 'exact':
66
+ case 'keyword':
67
+ case 'fuzzy':
68
+ skill = result.skill;
69
+ break;
70
+ case 'multiple':
71
+ skill = await selectSkill(result.matches);
72
+ break;
73
+ case 'none':
74
+ console.log(chalk.red(`✗ No documentation found for "${keyword}"`));
75
+ console.log(chalk.gray('\nHints:'));
76
+ console.log(chalk.gray(' skill list List all available skills'));
77
+ console.log(chalk.gray(' skill sync Sync latest data'));
78
+ process.exit(1);
79
+ }
80
+ }
81
+
82
+ console.log(chalk.green(`✓ Selected: ${skill.name || skill.id}`));
83
+
84
+ // Handle content
85
+ let content;
86
+ let docPath;
87
+
88
+ // Check local cache first unless remote forced
89
+ if (!options.remote) {
90
+ content = store.getDoc(skill.id);
91
+ }
92
+
93
+ // Download if missing or remote mode
94
+ if (!content || isRemoteFetch) {
95
+ if (!skill.githubUrl) {
96
+ throw new Error('This skill has no associated GitHub repository.');
97
+ }
98
+
99
+ console.log(chalk.gray('Fetching document content...'));
100
+ content = await api.fetchSkillContent(skill.githubUrl);
101
+
102
+ // Save to local store
103
+ store.setDoc(skill.id, content);
104
+
105
+ // Also cache the skill info to index if it doesn't exist
106
+ const index = store.getIndex();
107
+ if (!index.skills.find(s => s.id === skill.id)) {
108
+ index.skills.push(skill);
109
+ store.setIndex(index);
110
+ }
111
+
112
+ console.log(chalk.green('✓ Saved to local storage.'));
113
+ }
114
+
115
+ // Implementation of #v26011801: Open option logic
116
+ docPath = path.join(store.getPaths().docsDir, `${skill.id}.md`);
117
+
118
+ if (options.remote) {
119
+ // In remote mode, offer to open instead of printing
120
+ const inquirer = require('inquirer');
121
+ const prompt = inquirer.createPromptModule();
122
+
123
+ const answer = await prompt([
124
+ {
125
+ type: 'list',
126
+ name: 'action',
127
+ message: 'Content saved. What would you like to do?',
128
+ choices: [
129
+ { name: 'View in Terminal', value: 'view' },
130
+ { name: 'Open in Editor', value: 'open' },
131
+ { name: 'Exit', value: 'exit' }
132
+ ]
133
+ }
134
+ ]);
135
+
136
+ if (answer.action === 'open') {
137
+ // Try to open with default editor
138
+ const cmd = process.platform === 'win32' ? 'start' : (process.platform === 'darwin' ? 'open' : 'xdg-open');
139
+ // Windows start command needs an extra pair of quotes for the title if path has spaces, but here path is safe-ish.
140
+ // Actually 'start "" "path"' is safer on Windows.
141
+ const finalCmd = process.platform === 'win32' ? `start "" "${docPath}"` : `${cmd} "${docPath}"`;
142
+
143
+ exec(finalCmd, (err) => {
144
+ if (err) console.error(chalk.red('Failed to open file:', err.message));
145
+ });
146
+ } else if (answer.action === 'view') {
147
+ console.log('\n' + formatMarkdown(content));
148
+ }
149
+ } else {
150
+ // Local mode default behavior: print to terminal
151
+ console.log('\n' + formatMarkdown(content));
152
+ }
153
+
154
+ } catch (error) {
155
+ console.error(chalk.red(`✗ ${error.message}`));
156
+ if (process.env.DEBUG) console.error(error);
157
+ process.exit(1);
158
+ }
159
+ });
160
+
161
+ // Sync command
162
+ program
163
+ .command('sync')
164
+ .description('Sync data from remote repository')
165
+ .option('-f, --force', 'Force full sync')
166
+ .option('--docs', 'Sync all document contents')
167
+ .option('--full', 'Sync full Skill folders')
168
+ .option('--id <id>', 'Sync specific Skill only')
169
+ .option('--status', 'View sync status')
170
+ .action(async (options) => {
171
+ try {
172
+ if (options.status) {
173
+ const status = syncer.getStatus();
174
+ if (!status) {
175
+ console.log(chalk.yellow('No sync history found.'));
176
+ } else {
177
+ console.log(chalk.cyan('📊 Sync Status:'));
178
+ console.log(` Last Sync: ${new Date(status.lastSync).toLocaleString()}`);
179
+ console.log(` Local Skills: ${status.totalSkills}`);
180
+ console.log(` Cached Docs: ${status.syncedDocs}`);
181
+ console.log(` Full Synced: ${status.fullSynced}`);
182
+ }
183
+ return;
184
+ }
185
+
186
+ await syncer.sync(options);
187
+ } catch (error) {
188
+ console.error(chalk.red(`✗ Sync failed: ${error.message}`));
189
+ }
190
+ });
191
+
192
+ // List command
193
+ program
194
+ .command('list')
195
+ .alias('ls')
196
+ .description('List all local skills')
197
+ .action(async () => {
198
+ try {
199
+ const skills = await listSkills();
200
+ if (skills.length === 0) {
201
+ console.log(chalk.yellow('No local data. Please run "skill sync" first.'));
202
+ return;
203
+ }
204
+
205
+ console.log(chalk.cyan.bold('\n📚 Available Skills:\n'));
206
+ skills.forEach(skill => {
207
+ const id = chalk.green(skill.id.padEnd(30));
208
+ const desc = (skill.description || '').slice(0, 50) + (skill.description?.length > 50 ? '...' : '');
209
+ console.log(` ${id} ${desc}`);
210
+ });
211
+ console.log(chalk.gray(`\nTotal: ${skills.length} skills`));
212
+ } catch (error) {
213
+ console.error(chalk.red(`✗ ${error.message}`));
214
+ }
215
+ });
216
+
217
+ // Search command
218
+ program
219
+ .command('search <query>')
220
+ .alias('s')
221
+ .description('Search skills locally')
222
+ .action(async (query) => {
223
+ try {
224
+ const results = await searchSkills(query);
225
+ if (results.length === 0) {
226
+ console.log(chalk.yellow(`No results found for "${query}"`));
227
+ return;
228
+ }
229
+
230
+ console.log(chalk.cyan.bold('\n🔍 Search Results:\n'));
231
+ results.forEach(r => {
232
+ const score = Math.round((1 - r.score) * 100);
233
+ const id = chalk.green(r.id.padEnd(30));
234
+ console.log(` ${id} ${r.name || ''} ${chalk.gray(`(${score}%)`)}`);
235
+ });
236
+ } catch (error) {
237
+ console.error(chalk.red(`✗ ${error.message}`));
238
+ }
239
+ });
240
+
241
+ // Config command
242
+ program
243
+ .command('config')
244
+ .description('Manage configuration')
245
+ .option('--api-key <key>', 'Set SkillsMP API Key')
246
+ .action((options) => {
247
+ const config = require('../src/config');
248
+ if (options.apiKey) {
249
+ config.setApiKey(options.apiKey);
250
+ console.log(chalk.green('✓ API Key saved'));
251
+ } else {
252
+ console.log(JSON.stringify(config.getUserConfig(), null, 2));
253
+ }
254
+ });
255
+
256
+ // Add Custom Path
257
+ program
258
+ .command('add <path>')
259
+ .description('Add a custom skill directory')
260
+ .action((dirPath) => {
261
+ const config = require('../src/config');
262
+ const path = require('path');
263
+ const absolutePath = path.resolve(dirPath);
264
+ if (config.addCustomPath(absolutePath)) {
265
+ console.log(chalk.green(`✓ Added custom path: ${absolutePath}`));
266
+ } else {
267
+ console.log(chalk.yellow(`! Path already exists: ${absolutePath}`));
268
+ }
269
+ });
270
+
271
+ // Remove Custom Path
272
+ program
273
+ .command('remove <path>')
274
+ .alias('rm')
275
+ .description('Remove a custom skill directory')
276
+ .action((dirPath) => {
277
+ const config = require('../src/config');
278
+ const path = require('path');
279
+ const absolutePath = path.resolve(dirPath);
280
+ const currentPaths = config.getCustomPaths();
281
+
282
+ // Try partial match first if it's unique? No, explicit is better.
283
+ // Try exact string first, then absolute path.
284
+ let target = dirPath;
285
+ if (!currentPaths.includes(target)) {
286
+ target = absolutePath;
287
+ }
288
+
289
+ if (config.removeCustomPath(target)) {
290
+ console.log(chalk.green(`✓ Removed custom path: ${target}`));
291
+ } else {
292
+ console.log(chalk.red(`✗ Path not found: ${dirPath}`));
293
+ console.log(chalk.gray('Use "skill config" to list all paths.'));
294
+ }
295
+ });
296
+
297
+ // Primary Directory
298
+ program
299
+ .command('primary [dir]')
300
+ .description('Set or list primary directory')
301
+ .action((dir) => {
302
+ const config = require('../src/config');
303
+ const available = config.getAvailablePrimaryDirs();
304
+
305
+ if (!dir) {
306
+ console.log(chalk.cyan.bold('\nPossible Primary Directories:'));
307
+ const current = config.getPrimaryDirName();
308
+ available.forEach(d => {
309
+ const isCurrent = d.key === current;
310
+ const prefix = isCurrent ? chalk.green('->') : ' ';
311
+ const name = isCurrent ? chalk.green(d.name.padEnd(20)) : d.name.padEnd(20);
312
+ console.log(`${prefix} ${name} ${chalk.gray(d.desc)}`);
313
+ });
314
+ return;
315
+ }
316
+
317
+ const match = available.find(d => d.key === dir || d.name === dir || d.name === '.' + dir);
318
+ if (match) {
319
+ config.setPrimaryDir(match.key);
320
+ console.log(chalk.green(`✓ Primary directory set to: ${match.name}`));
321
+ } else {
322
+ console.log(chalk.red(`✗ Invalid directory: ${dir}`));
323
+ console.log(chalk.gray('Use "skill primary" to see available options.'));
324
+ }
325
+ });
326
+
327
+ // Theme (TUI setting)
328
+ program
329
+ .command('theme [mode]')
330
+ .description('Set or toggle TUI theme (dark/light)')
331
+ .action((mode) => {
332
+ const theme = require('../src/theme');
333
+ if (!mode) {
334
+ const newTheme = theme.toggleTheme();
335
+ console.log(chalk.green(`✓ TUI Theme toggled to: ${newTheme}`));
336
+ } else {
337
+ const m = mode.toLowerCase();
338
+ if (['dark', 'light'].includes(m)) {
339
+ theme.setThemeName(m);
340
+ console.log(chalk.green(`✓ TUI Theme set to: ${m}`));
341
+ } else {
342
+ console.log(chalk.red('✗ Invalid theme. Use "dark" or "light".'));
343
+ }
344
+ }
345
+ });
346
+
347
+ program.parse();
348
+ }
package/bin/tui.js ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ // bin/tui.js - TUI Mode Entry Point (Alternate Buffer)
3
+
4
+ const React = require('react');
5
+ const { render } = require('ink');
6
+ const App = require('../src/tui/App');
7
+
8
+ // Enter Alternate Screen Buffer, Clear Screen, Move Cursor to Top-Left, Show Cursor
9
+ process.stdout.write('\x1b[?1049h\x1b[2J\x1b[H\x1b[?25h');
10
+
11
+ // Config Ink to use fullscreen mode which handles clearing automatically
12
+ const { waitUntilExit, clear, unmount } = render(React.createElement(App), {
13
+ exitOnCtrlC: true,
14
+ patchConsole: false,
15
+ debug: false,
16
+ // Note: 'fullscreen' option isn't directly exposed in render options like this in older Ink versions,
17
+ // but clearing screen manually is good. However, Ink v3+ handles full screen by taking over stdout.
18
+ // Let's rely on standard clearing.
19
+ });
20
+
21
+ // Capture exit to ensure we restore screen
22
+ process.on('SIGINT', () => {
23
+ unmount();
24
+ process.stdout.write('\x1b[?1049l\x1b[?25h'); // Restore buffer and cursor
25
+ process.exit(0);
26
+ });
27
+
28
+ waitUntilExit().then(() => {
29
+ unmount();
30
+ // Exit Alternate Screen Buffer and show cursor
31
+ process.stdout.write('\x1b[?1049l\x1b[?25h');
32
+ process.exit(0);
33
+ });
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "skill-search",
3
+ "version": "0.0.1",
4
+ "description": "Quickly retrieve skill documentation from GitHub",
5
+ "keywords": [
6
+ "cli",
7
+ "skill",
8
+ "documentation",
9
+ "markdown"
10
+ ],
11
+ "homepage": "https://github.com/LLMist/Skill-Search#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/LLMist/Skill-Search/issues"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/LLMist/Skill-Search.git"
18
+ },
19
+ "license": "ISC",
20
+ "author": "LLM.ist",
21
+ "type": "commonjs",
22
+ "main": "bin/skill.js",
23
+ "bin": {
24
+ "skill": "bin/skill.js"
25
+ },
26
+ "scripts": {
27
+ "test": "echo \"Error: no test specified\" && exit 1",
28
+ "build": "pkg . --out-path=dist --targets=node18-linux-x64,node18-macos-x64,node18-win-x64"
29
+ },
30
+ "dependencies": {
31
+ "axios": "^1.6.0",
32
+ "chalk": "^4.1.2",
33
+ "clipboardy": "^2.3.0",
34
+ "commander": "^11.1.0",
35
+ "fuse.js": "^7.0.0",
36
+ "ink": "^3.2.0",
37
+ "ink-select-input": "^4.2.2",
38
+ "ink-spinner": "^4.0.3",
39
+ "ink-text-input": "^4.0.3",
40
+ "inquirer": "^8.2.6",
41
+ "marked": "^11.0.0",
42
+ "marked-terminal": "^6.1.0",
43
+ "ora": "^5.4.1",
44
+ "react": "^17.0.2",
45
+ "terminal-link": "^5.0.0"
46
+ },
47
+ "devDependencies": {
48
+ "eslint": "^8.0.0",
49
+ "jest": "^29.0.0",
50
+ "pkg": "^5.8.1",
51
+ "prettier": "^3.0.0"
52
+ }
53
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "scripts",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {}
6
+ }
@@ -0,0 +1,58 @@
1
+ @echo off
2
+ setlocal
3
+
4
+ :: ==========================================
5
+ :: Skill Search 多设备开发环境配置脚本
6
+ :: ==========================================
7
+
8
+ echo [INFO] Detected Device Name: %COMPUTERNAME%
9
+
10
+ :: 1. 定义本地隔离路径
11
+ set "LOCAL_DIR=.local\%COMPUTERNAME%"
12
+ set "TARGET_MODULES=%LOCAL_DIR%\node_modules"
13
+
14
+ :: 2. 确保目录存在
15
+ if not exist ".local" (
16
+ mkdir ".local"
17
+ )
18
+ if not exist "%LOCAL_DIR%" (
19
+ mkdir "%LOCAL_DIR%"
20
+ )
21
+ if not exist "%TARGET_MODULES%" (
22
+ echo [INFO] Creating directory: %TARGET_MODULES%
23
+ mkdir "%TARGET_MODULES%"
24
+ )
25
+
26
+ :: 3. 处理根目录 node_modules
27
+ if exist "node_modules" (
28
+ :: 检查 node_modules 是否已经是链接
29
+ fsutil reparsepoint query "node_modules" >nul 2>&1
30
+ if %errorlevel% equ 0 (
31
+ echo [INFO] node_modules is already a symlink/junction.
32
+ rmdir "node_modules"
33
+ ) else (
34
+ echo [WARNING] Found existing standard node_modules folder.
35
+ echo [WARNING] Removing standard folder to replace with device-specific link...
36
+ rmdir /s /q "node_modules"
37
+ )
38
+ )
39
+
40
+ :: 4. 创建符号链接 (Junction)
41
+ :: node_modules -> .local/DEVICE_NAME/node_modules
42
+ echo [INFO] Linking node_modules -^> %TARGET_MODULES%
43
+ mklink /J "node_modules" "%TARGET_MODULES%"
44
+
45
+ if %errorlevel% neq 0 (
46
+ echo [ERROR] Failed to create symlink. Please run as Administrator.
47
+ pause
48
+ exit /b 1
49
+ )
50
+
51
+ :: 5. 安装依赖
52
+ echo [INFO] Installing dependencies via npm...
53
+ npm install
54
+
55
+ echo.
56
+ echo [SUCCESS] Environment setup complete for %COMPUTERNAME%!
57
+ echo [INFO] Dependencies are stored in: %TARGET_MODULES%
58
+ pause
@@ -0,0 +1,42 @@
1
+ const { scanForSkillDirectories } = require('../src/localCrawler');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+
6
+ async function run() {
7
+ console.log('Home dir:', os.homedir());
8
+
9
+ // Create a dummy dot-folder with skills to ensure it's found
10
+ const dummyDir = path.join(os.homedir(), '.test-auto-scan-dir');
11
+ const skillsDir = path.join(dummyDir, 'skills');
12
+
13
+ if (!fs.existsSync(skillsDir)) {
14
+ fs.mkdirSync(skillsDir, { recursive: true });
15
+ console.log('Created dummy dir:', dummyDir);
16
+ }
17
+
18
+ try {
19
+ const results = await scanForSkillDirectories();
20
+ console.log('Found directories:', results);
21
+
22
+ if (results.includes('.test-auto-scan-dir')) {
23
+ console.log('SUCCESS: Found the test directory.');
24
+ } else {
25
+ console.log('FAILURE: Did not find the test directory.');
26
+ }
27
+ } catch (err) {
28
+ console.error('Error:', err);
29
+ } finally {
30
+ // Cleanup
31
+ try {
32
+ if (fs.existsSync(dummyDir)) {
33
+ fs.rmSync(dummyDir, { recursive: true, force: true });
34
+ console.log('Cleaned up dummy dir');
35
+ }
36
+ } catch (e) {
37
+ console.error('Cleanup error:', e);
38
+ }
39
+ }
40
+ }
41
+
42
+ run();