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/README.md +381 -0
- package/bin/skill.js +348 -0
- package/bin/tui.js +33 -0
- package/package.json +53 -0
- package/scripts/package-lock.json +6 -0
- package/scripts/setup-env.bat +58 -0
- package/scripts/test-scan.js +42 -0
- package/src/actions.js +216 -0
- package/src/api.js +306 -0
- package/src/cache.js +107 -0
- package/src/config.js +220 -0
- package/src/fallback-index.json +6 -0
- package/src/interactive.js +23 -0
- package/src/localCrawler.js +204 -0
- package/src/matcher.js +170 -0
- package/src/store.js +156 -0
- package/src/syncer.js +226 -0
- package/src/theme.js +191 -0
- package/src/tui/ActionModal.js +209 -0
- package/src/tui/AddDelView.js +212 -0
- package/src/tui/App.js +739 -0
- package/src/tui/AsciiHeader.js +35 -0
- package/src/tui/CommandPalette.js +64 -0
- package/src/tui/ConfigView.js +168 -0
- package/src/tui/DetailView.js +139 -0
- package/src/tui/DualPane.js +114 -0
- package/src/tui/PrimaryView.js +163 -0
- package/src/tui/SearchBox.js +26 -0
- package/src/tui/SearchView.js +121 -0
- package/src/tui/SkillList.js +102 -0
- package/src/tui/SyncView.js +143 -0
- package/src/tui/ThemeView.js +116 -0
- package/src/utils.js +83 -0
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,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();
|