@haolin-ai/skillman 1.0.6 → 1.0.8
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 +16 -1
- package/package.json +2 -2
- package/src/cli.js +297 -52
- package/src/i18n.js +39 -0
- package/src/installer.js +21 -1
- package/src/scanner.js +42 -14
- package/src/version.js +215 -0
package/README.md
CHANGED
|
@@ -47,9 +47,22 @@ skillman install git@github.com:org/repo.git
|
|
|
47
47
|
### Commands
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
|
+
# Initialize a new skill template
|
|
51
|
+
skillman init [skill-name]
|
|
52
|
+
skillman init my-skill -v 2.0.0 -d "Description" -a "Author"
|
|
53
|
+
|
|
50
54
|
# List all available agents
|
|
51
55
|
skillman agents
|
|
52
56
|
|
|
57
|
+
# List installed skills
|
|
58
|
+
skillman list
|
|
59
|
+
|
|
60
|
+
# Update a skill
|
|
61
|
+
skillman update my-skill
|
|
62
|
+
|
|
63
|
+
# Uninstall a skill
|
|
64
|
+
skillman uninstall my-skill
|
|
65
|
+
|
|
53
66
|
# Preview installation without making changes
|
|
54
67
|
skillman --dry-run
|
|
55
68
|
|
|
@@ -63,6 +76,8 @@ skillman --version
|
|
|
63
76
|
## Features
|
|
64
77
|
|
|
65
78
|
- **Multi-Agent Support**: Works with Claude Code, OpenClaw, Qoder, Codex, Cursor, and more
|
|
79
|
+
- **Skill Template Generator**: Quickly create new skills with `skillman init`
|
|
80
|
+
- **Version Management**: Track and manage skill versions with `list`, `update`, and `uninstall` commands
|
|
66
81
|
- **Workspace History**: Remembers previously used workspace paths for quick selection
|
|
67
82
|
- **Bilingual Support**: Automatically switches between English and Chinese based on system language
|
|
68
83
|
- **Dry-Run Mode**: Preview installations before applying changes
|
|
@@ -91,7 +106,7 @@ git clone <repo-url>
|
|
|
91
106
|
cd skillman
|
|
92
107
|
|
|
93
108
|
# Install dependencies
|
|
94
|
-
|
|
109
|
+
npm install
|
|
95
110
|
|
|
96
111
|
# Run locally
|
|
97
112
|
node bin/skillman.mjs
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haolin-ai/skillman",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "A CLI tool to install AI agent skills across multiple platforms",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"skillman": "
|
|
7
|
+
"skillman": "bin/skillman.mjs"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin",
|
package/src/cli.js
CHANGED
|
@@ -7,16 +7,27 @@ import path from 'path';
|
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
8
|
import { select, confirm, input, Separator, checkbox } from '@inquirer/prompts';
|
|
9
9
|
import pkg from '../package.json' with { type: 'json' };
|
|
10
|
-
import { scanSkills } from './scanner.js';
|
|
10
|
+
import { scanSkills, parseSkillFile } from './scanner.js';
|
|
11
11
|
import { installSkill } from './installer.js';
|
|
12
12
|
import { loadAgents } from './config.js';
|
|
13
13
|
import { t } from './i18n.js';
|
|
14
14
|
import { loadHistory, addWorkspace, saveLastUsed } from './history.js';
|
|
15
15
|
import { downloadSkill, parseUrl, cleanupDownloads } from './downloader.js';
|
|
16
|
+
import { InstalledSkillRegistry, formatInstalledSkills, uninstallSkill, updateSkill } from './version.js';
|
|
16
17
|
|
|
17
18
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
19
|
const VERSION = pkg.version || '1.0.0';
|
|
19
20
|
|
|
21
|
+
// ASCII Art Logo
|
|
22
|
+
const LOGO = `
|
|
23
|
+
███████╗██╗ ██╗██╗██╗ ██╗ ███╗ ███╗ █████╗ ███╗ ██╗
|
|
24
|
+
██╔════╝██║ ██╔╝██║██║ ██║ ████╗ ████║██╔══██╗████╗ ██║
|
|
25
|
+
███████╗█████╔╝ ██║██║ ██║ ██╔████╔██║███████║██╔██╗ ██║
|
|
26
|
+
╚════██║██╔═██╗ ██║██║ ██║ ██║╚██╔╝██║██╔══██║██║╚██╗██║
|
|
27
|
+
███████║██║ ██╗██║███████╗███████╗██║ ╚═╝ ██║██║ ██║██║ ╚████║
|
|
28
|
+
╚══════╝╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝
|
|
29
|
+
`;
|
|
30
|
+
|
|
20
31
|
// ANSI colors
|
|
21
32
|
const c = {
|
|
22
33
|
reset: '\x1b[0m',
|
|
@@ -41,22 +52,51 @@ const log = {
|
|
|
41
52
|
function parseArgs(args) {
|
|
42
53
|
const result = {
|
|
43
54
|
command: null,
|
|
55
|
+
subcommand: null,
|
|
44
56
|
dryRun: false,
|
|
45
57
|
help: false,
|
|
46
58
|
version: false,
|
|
59
|
+
initVersion: '1.0.0',
|
|
60
|
+
initDescription: '',
|
|
61
|
+
initAuthor: '',
|
|
62
|
+
initDir: true,
|
|
47
63
|
positional: []
|
|
48
64
|
};
|
|
49
65
|
|
|
50
|
-
for (
|
|
66
|
+
for (let i = 0; i < args.length; i++) {
|
|
67
|
+
const arg = args[i];
|
|
51
68
|
if (arg === '--dry-run' || arg === '-n') {
|
|
52
69
|
result.dryRun = true;
|
|
53
70
|
} else if (arg === '--help' || arg === '-h') {
|
|
54
71
|
result.help = true;
|
|
55
72
|
} else if (arg === '--version' || arg === '-v') {
|
|
56
|
-
|
|
73
|
+
// Check if next arg is a value (for init command)
|
|
74
|
+
if (args[i + 1] && !args[i + 1].startsWith('-')) {
|
|
75
|
+
result.initVersion = args[i + 1];
|
|
76
|
+
i++;
|
|
77
|
+
} else {
|
|
78
|
+
result.version = true;
|
|
79
|
+
}
|
|
80
|
+
} else if (arg === '--description' || arg === '-d') {
|
|
81
|
+
if (args[i + 1] && !args[i + 1].startsWith('-')) {
|
|
82
|
+
result.initDescription = args[i + 1];
|
|
83
|
+
i++;
|
|
84
|
+
}
|
|
85
|
+
} else if (arg === '--author' || arg === '-a') {
|
|
86
|
+
if (args[i + 1] && !args[i + 1].startsWith('-')) {
|
|
87
|
+
result.initAuthor = args[i + 1];
|
|
88
|
+
i++;
|
|
89
|
+
}
|
|
90
|
+
} else if (arg === '--dir') {
|
|
91
|
+
if (args[i + 1] && !args[i + 1].startsWith('-')) {
|
|
92
|
+
result.initDir = args[i + 1].toLowerCase() !== 'false';
|
|
93
|
+
i++;
|
|
94
|
+
}
|
|
57
95
|
} else if (!arg.startsWith('-')) {
|
|
58
96
|
if (!result.command) {
|
|
59
97
|
result.command = arg;
|
|
98
|
+
} else if (!result.subcommand) {
|
|
99
|
+
result.subcommand = arg;
|
|
60
100
|
} else {
|
|
61
101
|
result.positional.push(arg);
|
|
62
102
|
}
|
|
@@ -72,8 +112,13 @@ function showHelp() {
|
|
|
72
112
|
|
|
73
113
|
${c.cyan}${t('help.usage')}:${c.reset}
|
|
74
114
|
skillman ${t('help.cmd.interactive')}
|
|
115
|
+
skillman init [name] ${t('help.cmd.init')}
|
|
75
116
|
skillman install <path> ${t('help.cmd.install')}
|
|
76
117
|
skillman i <path> ${t('help.cmd.install')}
|
|
118
|
+
skillman list ${t('help.cmd.list')}
|
|
119
|
+
skillman update <skill> ${t('help.cmd.update')}
|
|
120
|
+
skillman u <skill> ${t('help.cmd.update')}
|
|
121
|
+
skillman uninstall <skill> ${t('help.cmd.uninstall')}
|
|
77
122
|
skillman agents ${t('help.cmd.agents')}
|
|
78
123
|
|
|
79
124
|
${c.cyan}${t('help.options')}:${c.reset}
|
|
@@ -83,9 +128,15 @@ ${c.cyan}${t('help.options')}:${c.reset}
|
|
|
83
128
|
|
|
84
129
|
${c.cyan}${t('help.examples')}:${c.reset}
|
|
85
130
|
skillman # ${t('help.cmd.interactive')}
|
|
131
|
+
skillman init # Create skill with default name
|
|
132
|
+
skillman init my-skill # Create skill with custom name
|
|
133
|
+
skillman init -v 2.0.0 # Create skill with specific version
|
|
86
134
|
skillman --dry-run # ${t('help.opt.dry_run')}
|
|
87
135
|
skillman install ./my-skill # ${t('help.cmd.install')}
|
|
88
136
|
skillman i github.com/owner/repo # ${t('help.cmd.install')}
|
|
137
|
+
skillman list # ${t('help.cmd.list')}
|
|
138
|
+
skillman update my-skill # ${t('help.cmd.update')}
|
|
139
|
+
skillman uninstall my-skill # ${t('help.cmd.uninstall')}
|
|
89
140
|
skillman agents # ${t('help.cmd.agents')}
|
|
90
141
|
`);
|
|
91
142
|
}
|
|
@@ -109,11 +160,75 @@ async function listAgents() {
|
|
|
109
160
|
}
|
|
110
161
|
}
|
|
111
162
|
|
|
112
|
-
//
|
|
163
|
+
// Initialize a new skill template
|
|
164
|
+
export async function initSkill(skillName, options) {
|
|
165
|
+
const name = skillName || 'my-skill';
|
|
166
|
+
const version = options.initVersion || '1.0.0';
|
|
167
|
+
const description = options.initDescription || '';
|
|
168
|
+
const author = options.initAuthor || '';
|
|
169
|
+
const createDir = options.initDir !== false;
|
|
170
|
+
|
|
171
|
+
console.log(`${c.cyan}${LOGO}${c.reset}`);
|
|
172
|
+
log.step(t('msg.init_skill') || 'Initializing skill template');
|
|
173
|
+
|
|
174
|
+
const targetDir = createDir ? path.join(process.cwd(), name) : process.cwd();
|
|
175
|
+
const skillFile = path.join(targetDir, 'SKILL.md');
|
|
176
|
+
|
|
177
|
+
// Check if already exists
|
|
178
|
+
try {
|
|
179
|
+
await fs.access(skillFile);
|
|
180
|
+
log.error(t('error.skill_exists') || 'SKILL.md already exists');
|
|
181
|
+
process.exit(1);
|
|
182
|
+
} catch {
|
|
183
|
+
// File doesn't exist, proceed
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Create directory if needed
|
|
187
|
+
if (createDir) {
|
|
188
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Build metadata section
|
|
192
|
+
let metadataSection = `metadata:\n version: ${version}`;
|
|
193
|
+
if (author) {
|
|
194
|
+
metadataSection += `\n author: ${author}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Generate SKILL.md content
|
|
198
|
+
const skillContent = `---
|
|
199
|
+
name: ${name}
|
|
200
|
+
description: ${description}
|
|
201
|
+
${metadataSection}
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
# ${name}
|
|
205
|
+
|
|
206
|
+
## Purpose
|
|
207
|
+
|
|
208
|
+
## Responsibilities
|
|
209
|
+
|
|
210
|
+
## Decision Rules
|
|
211
|
+
|
|
212
|
+
## Output Contract
|
|
213
|
+
`;
|
|
214
|
+
|
|
215
|
+
await fs.writeFile(skillFile, skillContent);
|
|
216
|
+
|
|
217
|
+
log.success(`${t('msg.created') || 'Created'}: ${skillFile}`);
|
|
218
|
+
console.log(`\n${c.gray}${t('msg.init_hint') || 'Edit SKILL.md to customize your skill'}${c.reset}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Install from URL or local path with optional version
|
|
113
222
|
async function installFromUrl(url, dryRun) {
|
|
114
|
-
|
|
223
|
+
// Parse version from URL (e.g., skill@1.2.3 or github.com/owner/repo@1.2.3)
|
|
224
|
+
const versionMatch = url.match(/@([^@\/]+)$/);
|
|
225
|
+
const requestedVersion = versionMatch ? versionMatch[1] : null;
|
|
226
|
+
const cleanUrl = requestedVersion ? url.slice(0, -versionMatch[0].length) : url;
|
|
227
|
+
|
|
228
|
+
console.log(`${c.cyan}${LOGO}${c.reset}`);
|
|
229
|
+
console.log(`${c.gray}${t('app.description')}${dryRun ? c.yellow + ' [DRY-RUN]' + c.reset : ''}\n`);
|
|
115
230
|
|
|
116
|
-
const parsed = parseUrl(
|
|
231
|
+
const parsed = parseUrl(cleanUrl);
|
|
117
232
|
const isRemote = parsed.type !== 'local';
|
|
118
233
|
|
|
119
234
|
// Step 1: Download/resolve path
|
|
@@ -142,35 +257,22 @@ async function installFromUrl(url, dryRun) {
|
|
|
142
257
|
|
|
143
258
|
log.success(t('msg.found_skills', { count: skills.length }));
|
|
144
259
|
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}));
|
|
157
|
-
|
|
158
|
-
selectedSkills = await checkbox({
|
|
159
|
-
message: t('step.select_skills') + ':',
|
|
160
|
-
choices: skillChoices,
|
|
161
|
-
pageSize: 10,
|
|
162
|
-
loop: false,
|
|
163
|
-
validate: (selected) => {
|
|
164
|
-
if (selected.length === 0) {
|
|
165
|
-
return t('error.no_selection') || 'Please select at least one skill';
|
|
166
|
-
}
|
|
167
|
-
return true;
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
log.success(`${t('msg.selected_count', { count: selectedSkills.length })}`);
|
|
260
|
+
// Check version if specified
|
|
261
|
+
if (requestedVersion) {
|
|
262
|
+
const matchingSkills = skills.filter(s => s.version === requestedVersion);
|
|
263
|
+
if (matchingSkills.length === 0) {
|
|
264
|
+
const availableVersions = [...new Set(skills.map(s => s.version).filter(Boolean))];
|
|
265
|
+
log.error(`Version ${requestedVersion} not found. Available versions: ${availableVersions.join(', ') || 'none'}`);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
skills.length = 0;
|
|
269
|
+
skills.push(...matchingSkills);
|
|
270
|
+
log.success(`Found ${skills.length} skill(s) matching version ${requestedVersion}`);
|
|
172
271
|
}
|
|
173
272
|
|
|
273
|
+
// Step 3: Select skills
|
|
274
|
+
const selectedSkills = await selectSkills(skills);
|
|
275
|
+
|
|
174
276
|
// Continue with rest of interactive flow
|
|
175
277
|
await continueInstallMultiple(selectedSkills, dryRun);
|
|
176
278
|
|
|
@@ -329,7 +431,13 @@ async function continueInstallMultiple(selectedSkills, dryRun) {
|
|
|
329
431
|
let shouldInstall = true;
|
|
330
432
|
try {
|
|
331
433
|
await fs.access(targetDir);
|
|
332
|
-
|
|
434
|
+
// Get existing skill version
|
|
435
|
+
const existingSkillFile = path.join(targetDir, 'SKILL.md');
|
|
436
|
+
const existingSkill = await parseSkillFile(existingSkillFile);
|
|
437
|
+
const currentVer = existingSkill?.version || '?';
|
|
438
|
+
const newVer = skill.version || '?';
|
|
439
|
+
log.warn(`${skill.name} ${t('msg.already_exists') || 'already exists'}`);
|
|
440
|
+
console.log(` ${c.gray}Current:${c.reset} v${currentVer} ${c.gray}→${c.reset} ${c.gray}Installing:${c.reset} v${newVer}`);
|
|
333
441
|
const overwrite = await confirm({ message: t('prompt.overwrite') + '?', default: false });
|
|
334
442
|
if (!overwrite) {
|
|
335
443
|
log.info(t('msg.skipped') || 'Skipped');
|
|
@@ -341,7 +449,12 @@ async function continueInstallMultiple(selectedSkills, dryRun) {
|
|
|
341
449
|
}
|
|
342
450
|
|
|
343
451
|
if (shouldInstall) {
|
|
344
|
-
await installSkill(skill.path, targetDir
|
|
452
|
+
await installSkill(skill.path, targetDir, {
|
|
453
|
+
name: skill.name,
|
|
454
|
+
version: skill.version,
|
|
455
|
+
agent: agent.name,
|
|
456
|
+
scope: scope
|
|
457
|
+
});
|
|
345
458
|
log.success(`${t('msg.target')}: ${targetDir}`);
|
|
346
459
|
installedCount++;
|
|
347
460
|
}
|
|
@@ -358,9 +471,133 @@ async function continueInstallMultiple(selectedSkills, dryRun) {
|
|
|
358
471
|
console.log();
|
|
359
472
|
}
|
|
360
473
|
|
|
474
|
+
// Helper: Select skills from list
|
|
475
|
+
async function selectSkills(skills, message = null) {
|
|
476
|
+
if (skills.length === 1) {
|
|
477
|
+
log.success(`${t('msg.selected')}: ${skills[0].name}`);
|
|
478
|
+
return [skills[0]];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const skillChoices = skills.map(s => {
|
|
482
|
+
const versionStr = s.version ? `@${s.version}` : '';
|
|
483
|
+
const descStr = s.description
|
|
484
|
+
? ` ${c.gray}(${s.description.slice(0, 40)}${s.description.length > 40 ? '...' : ''})${c.reset}`
|
|
485
|
+
: '';
|
|
486
|
+
return {
|
|
487
|
+
name: `${s.name}${versionStr}${descStr}`,
|
|
488
|
+
value: s
|
|
489
|
+
};
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
const selectedSkills = await checkbox({
|
|
493
|
+
message: (message || t('step.select_skills')) + ':',
|
|
494
|
+
choices: skillChoices,
|
|
495
|
+
pageSize: 10,
|
|
496
|
+
loop: false,
|
|
497
|
+
validate: (selected) => {
|
|
498
|
+
if (selected.length === 0) {
|
|
499
|
+
return t('error.no_selection') || 'Please select at least one skill';
|
|
500
|
+
}
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
log.success(`${t('msg.selected_count', { count: selectedSkills.length })}`);
|
|
506
|
+
return selectedSkills;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// List installed skills
|
|
510
|
+
async function listSkills() {
|
|
511
|
+
const registry = new InstalledSkillRegistry();
|
|
512
|
+
const skills = await registry.load();
|
|
513
|
+
|
|
514
|
+
console.log(`\n${c.cyan}${t('msg.installed_skills')}:${c.reset}\n`);
|
|
515
|
+
|
|
516
|
+
const lines = formatInstalledSkills(skills, t);
|
|
517
|
+
for (const line of lines) {
|
|
518
|
+
console.log(line);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Uninstall a skill
|
|
523
|
+
async function uninstallCommand(skillName, dryRun) {
|
|
524
|
+
if (!skillName) {
|
|
525
|
+
log.error(t('error.no_skill_name') || 'Please specify a skill name');
|
|
526
|
+
process.exit(1);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const registry = new InstalledSkillRegistry();
|
|
530
|
+
const skill = await registry.find(skillName);
|
|
531
|
+
|
|
532
|
+
if (!skill) {
|
|
533
|
+
log.error(t('error.skill_not_installed'));
|
|
534
|
+
process.exit(1);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (dryRun) {
|
|
538
|
+
log.dry(`${t('msg.uninstalling') || 'Would uninstall'}: ${skillName}`);
|
|
539
|
+
log.dry(` ${t('msg.target')}: ${skill.targetPath}`);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const confirmed = await confirm({
|
|
544
|
+
message: `${t('prompt.uninstall')} ${skillName}?`,
|
|
545
|
+
default: false
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
if (!confirmed) {
|
|
549
|
+
log.info(t('msg.cancelled') || 'Cancelled');
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const success = await uninstallSkill(skillName, registry);
|
|
554
|
+
|
|
555
|
+
if (success) {
|
|
556
|
+
log.success(`${t('msg.uninstalled')}: ${skillName}`);
|
|
557
|
+
} else {
|
|
558
|
+
log.error(t('error.unknown'));
|
|
559
|
+
process.exit(1);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Update a skill
|
|
564
|
+
async function updateCommand(skillName, dryRun) {
|
|
565
|
+
if (!skillName) {
|
|
566
|
+
log.error(t('error.no_skill_name') || 'Please specify a skill name');
|
|
567
|
+
process.exit(1);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const registry = new InstalledSkillRegistry();
|
|
571
|
+
const skill = await registry.find(skillName);
|
|
572
|
+
|
|
573
|
+
if (!skill) {
|
|
574
|
+
log.error(t('error.skill_not_installed'));
|
|
575
|
+
process.exit(1);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (dryRun) {
|
|
579
|
+
log.dry(`${t('msg.updating') || 'Would update'}: ${skillName}`);
|
|
580
|
+
log.dry(` ${t('msg.source')}: ${skill.sourcePath}`);
|
|
581
|
+
log.dry(` ${t('msg.target')}: ${skill.targetPath}`);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
log.step(`${t('msg.updating') || 'Updating'}: ${skillName}`);
|
|
586
|
+
|
|
587
|
+
const result = await updateSkill(skillName, registry);
|
|
588
|
+
|
|
589
|
+
if (result.success) {
|
|
590
|
+
log.success(`${t('msg.updated')}: ${skillName}`);
|
|
591
|
+
} else {
|
|
592
|
+
log.error(result.message);
|
|
593
|
+
process.exit(1);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
361
597
|
// Interactive install flow
|
|
362
598
|
async function interactiveInstall(dryRun) {
|
|
363
|
-
console.log(`${c.
|
|
599
|
+
console.log(`${c.cyan}${LOGO}${c.reset}`);
|
|
600
|
+
console.log(`${c.gray}${t('app.description')}${dryRun ? c.yellow + ' [DRY-RUN]' + c.reset : ''}\n`);
|
|
364
601
|
|
|
365
602
|
// Step 1: Scan skills
|
|
366
603
|
log.step(t('step.scan'));
|
|
@@ -373,24 +610,11 @@ async function interactiveInstall(dryRun) {
|
|
|
373
610
|
|
|
374
611
|
log.success(t('msg.found_skills', { count: skills.length }));
|
|
375
612
|
|
|
376
|
-
// Step 2: Select
|
|
377
|
-
const
|
|
378
|
-
name: s.description
|
|
379
|
-
? `${s.name} ${c.gray}(${s.description.slice(0, 40)}${s.description.length > 40 ? '...' : ''})${c.reset}`
|
|
380
|
-
: s.name,
|
|
381
|
-
value: s
|
|
382
|
-
}));
|
|
383
|
-
|
|
384
|
-
const selectedSkill = await select({
|
|
385
|
-
message: t('step.select_skill') + ':',
|
|
386
|
-
choices: skillChoices,
|
|
387
|
-
pageSize: 10
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
log.success(`${t('msg.selected')}: ${selectedSkill.name}`);
|
|
613
|
+
// Step 2: Select skills
|
|
614
|
+
const selectedSkills = await selectSkills(skills);
|
|
391
615
|
|
|
392
616
|
// Continue with agent selection and installation
|
|
393
|
-
await
|
|
617
|
+
await continueInstallMultiple(selectedSkills, dryRun);
|
|
394
618
|
}
|
|
395
619
|
|
|
396
620
|
// Main CLI function
|
|
@@ -413,12 +637,33 @@ export async function cli() {
|
|
|
413
637
|
return;
|
|
414
638
|
}
|
|
415
639
|
|
|
640
|
+
if (options.command === 'init') {
|
|
641
|
+
const skillName = options.subcommand || options.positional[0];
|
|
642
|
+
await initSkill(skillName, options);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
416
646
|
if (options.command === 'install' || options.command === 'i') {
|
|
417
647
|
const url = options.positional[0] || process.cwd();
|
|
418
648
|
await installFromUrl(url, options.dryRun);
|
|
419
649
|
return;
|
|
420
650
|
}
|
|
421
651
|
|
|
652
|
+
if (options.command === 'list') {
|
|
653
|
+
await listSkills();
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (options.command === 'uninstall') {
|
|
658
|
+
await uninstallCommand(options.subcommand, options.dryRun);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (options.command === 'update' || options.command === 'u') {
|
|
663
|
+
await updateCommand(options.subcommand, options.dryRun);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
422
667
|
// Default: interactive install
|
|
423
668
|
await interactiveInstall(options.dryRun);
|
|
424
669
|
}
|
package/src/i18n.js
CHANGED
|
@@ -64,12 +64,25 @@ const translations = {
|
|
|
64
64
|
'msg.installing': '正在安装',
|
|
65
65
|
'msg.skipped': '已跳过',
|
|
66
66
|
'msg.installed': '已安装',
|
|
67
|
+
'msg.no_installed_skills': '没有已安装的技能',
|
|
68
|
+
'msg.installed_skills': '已安装的技能',
|
|
69
|
+
'msg.skill_not_found': '未找到技能',
|
|
70
|
+
'msg.uninstalled': '已卸载',
|
|
71
|
+
'msg.updated': '已更新',
|
|
72
|
+
'msg.updating': '正在更新',
|
|
73
|
+
'msg.uninstalling': '正在卸载',
|
|
74
|
+
'msg.cancelled': '已取消',
|
|
75
|
+
'msg.already_exists': '已存在',
|
|
76
|
+
'msg.init_skill': '初始化 Skill 模板',
|
|
77
|
+
'msg.created': '已创建',
|
|
78
|
+
'msg.init_hint': '编辑 SKILL.md 来自定义你的 skill',
|
|
67
79
|
|
|
68
80
|
// Prompts
|
|
69
81
|
'prompt.workspace_path': '输入 Workspace 路径',
|
|
70
82
|
'prompt.overwrite': '是否覆盖',
|
|
71
83
|
'prompt.select_workspace': '选择 Workspace 路径',
|
|
72
84
|
'prompt.new_path': '输入新路径...',
|
|
85
|
+
'prompt.uninstall': '确认卸载',
|
|
73
86
|
|
|
74
87
|
// Options
|
|
75
88
|
'option.global': '全局',
|
|
@@ -80,13 +93,19 @@ const translations = {
|
|
|
80
93
|
'error.empty_path': '路径不能为空',
|
|
81
94
|
'error.not_implemented': '该命令尚未实现,请使用交互模式',
|
|
82
95
|
'error.no_selection': '请至少选择一个 skill',
|
|
96
|
+
'error.skill_not_installed': '技能未安装',
|
|
97
|
+
'error.no_skill_name': '请指定技能名称',
|
|
83
98
|
|
|
84
99
|
// Help
|
|
85
100
|
'help.usage': '用法',
|
|
86
101
|
'help.options': '选项',
|
|
87
102
|
'help.examples': '示例',
|
|
88
103
|
'help.cmd.interactive': '交互式安装 (扫描当前目录)',
|
|
104
|
+
'help.cmd.init': '初始化新的 skill 模板',
|
|
89
105
|
'help.cmd.install': '从指定路径安装 skill',
|
|
106
|
+
'help.cmd.list': '列出已安装的技能',
|
|
107
|
+
'help.cmd.update': '更新技能',
|
|
108
|
+
'help.cmd.uninstall': '卸载技能',
|
|
90
109
|
'help.cmd.agents': '列出可用的 agents',
|
|
91
110
|
'help.opt.dry_run': '预览安装而不执行更改',
|
|
92
111
|
'help.opt.version': '显示版本号',
|
|
@@ -126,18 +145,32 @@ const translations = {
|
|
|
126
145
|
'msg.preview_summary': 'Preview Summary',
|
|
127
146
|
'msg.dry_run_hint': 'Running with --dry-run, no changes made',
|
|
128
147
|
'msg.skill_exists': 'Skill already exists',
|
|
148
|
+
'msg.already_exists': 'already exists',
|
|
129
149
|
'msg.install_cancelled': 'Installation cancelled',
|
|
130
150
|
'msg.downloading': 'Downloading...',
|
|
131
151
|
'msg.downloaded': 'Downloaded',
|
|
132
152
|
'msg.installing': 'Installing',
|
|
133
153
|
'msg.skipped': 'Skipped',
|
|
134
154
|
'msg.installed': 'Installed',
|
|
155
|
+
'msg.no_installed_skills': 'No skills installed',
|
|
156
|
+
'msg.installed_skills': 'Installed Skills',
|
|
157
|
+
'msg.skill_not_found': 'Skill not found',
|
|
158
|
+
'msg.uninstalled': 'Uninstalled',
|
|
159
|
+
'msg.updated': 'Updated',
|
|
160
|
+
'msg.updating': 'Updating',
|
|
161
|
+
'msg.uninstalling': 'Uninstalling',
|
|
162
|
+
'msg.cancelled': 'Cancelled',
|
|
163
|
+
'msg.already_exists': 'already exists',
|
|
164
|
+
'msg.init_skill': 'Initializing skill template',
|
|
165
|
+
'msg.created': 'Created',
|
|
166
|
+
'msg.init_hint': 'Edit SKILL.md to customize your skill',
|
|
135
167
|
|
|
136
168
|
// Prompts
|
|
137
169
|
'prompt.workspace_path': 'Enter workspace path',
|
|
138
170
|
'prompt.overwrite': 'Overwrite existing',
|
|
139
171
|
'prompt.select_workspace': 'Select workspace path',
|
|
140
172
|
'prompt.new_path': 'Enter new path...',
|
|
173
|
+
'prompt.uninstall': 'Confirm uninstall',
|
|
141
174
|
|
|
142
175
|
// Options
|
|
143
176
|
'option.global': 'Global',
|
|
@@ -148,13 +181,19 @@ const translations = {
|
|
|
148
181
|
'error.empty_path': 'Path cannot be empty',
|
|
149
182
|
'error.not_implemented': 'Command not implemented, use interactive mode',
|
|
150
183
|
'error.no_selection': 'Please select at least one skill',
|
|
184
|
+
'error.skill_not_installed': 'Skill is not installed',
|
|
185
|
+
'error.no_skill_name': 'Please specify a skill name',
|
|
151
186
|
|
|
152
187
|
// Help
|
|
153
188
|
'help.usage': 'Usage',
|
|
154
189
|
'help.options': 'Options',
|
|
155
190
|
'help.examples': 'Examples',
|
|
156
191
|
'help.cmd.interactive': 'Interactive install (scan current directory)',
|
|
192
|
+
'help.cmd.init': 'Initialize new skill template',
|
|
157
193
|
'help.cmd.install': 'Install skill from path',
|
|
194
|
+
'help.cmd.list': 'List installed skills',
|
|
195
|
+
'help.cmd.update': 'Update a skill',
|
|
196
|
+
'help.cmd.uninstall': 'Uninstall a skill',
|
|
158
197
|
'help.cmd.agents': 'List available agents',
|
|
159
198
|
'help.opt.dry_run': 'Preview installation without making changes',
|
|
160
199
|
'help.opt.version': 'Show version number',
|
package/src/installer.js
CHANGED
|
@@ -5,14 +5,34 @@
|
|
|
5
5
|
|
|
6
6
|
import fs from 'fs/promises';
|
|
7
7
|
import path from 'path';
|
|
8
|
+
import { InstalledSkillRegistry } from './version.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Install skill by copying to target directory
|
|
11
12
|
* @param {string} srcPath - Source skill directory
|
|
12
13
|
* @param {string} targetDir - Target installation directory
|
|
14
|
+
* @param {Object} metadata - Installation metadata
|
|
15
|
+
* @param {string} metadata.name - Skill name
|
|
16
|
+
* @param {string} metadata.version - Skill version
|
|
17
|
+
* @param {string} metadata.agent - Target agent name
|
|
18
|
+
* @param {string} metadata.scope - Installation scope (global|workspace)
|
|
13
19
|
*/
|
|
14
|
-
export async function installSkill(srcPath, targetDir) {
|
|
20
|
+
export async function installSkill(srcPath, targetDir, metadata = {}) {
|
|
15
21
|
await copyDir(srcPath, targetDir);
|
|
22
|
+
|
|
23
|
+
// Record installation if metadata provided
|
|
24
|
+
if (metadata.name) {
|
|
25
|
+
const registry = new InstalledSkillRegistry();
|
|
26
|
+
await registry.add({
|
|
27
|
+
name: metadata.name,
|
|
28
|
+
version: metadata.version,
|
|
29
|
+
installedAt: new Date().toISOString(),
|
|
30
|
+
agent: metadata.agent,
|
|
31
|
+
scope: metadata.scope,
|
|
32
|
+
sourcePath: srcPath,
|
|
33
|
+
targetPath: targetDir
|
|
34
|
+
});
|
|
35
|
+
}
|
|
16
36
|
}
|
|
17
37
|
|
|
18
38
|
/**
|
package/src/scanner.js
CHANGED
|
@@ -10,6 +10,42 @@ import path from 'path';
|
|
|
10
10
|
// Common skill container directories
|
|
11
11
|
const SKILL_CONTAINERS = ['skills', '.agents/skills', '.claude/skills'];
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Parse a single SKILL.md file to extract metadata
|
|
15
|
+
* @param {string} skillFile - Path to SKILL.md
|
|
16
|
+
* @returns {Promise<Object|null>} Parsed skill info or null if invalid
|
|
17
|
+
*/
|
|
18
|
+
export async function parseSkillFile(skillFile) {
|
|
19
|
+
try {
|
|
20
|
+
const content = await fs.readFile(skillFile, 'utf-8');
|
|
21
|
+
const nameMatch = content.match(/^name:\s*(.+)$/m);
|
|
22
|
+
const descMatch = content.match(/^description:\s*(.+)$/m);
|
|
23
|
+
|
|
24
|
+
// Parse version from metadata block (supports quoted and unquoted)
|
|
25
|
+
const metadataMatch = content.match(/metadata:[\s\S]*?(?=\n\w|$)/);
|
|
26
|
+
let version;
|
|
27
|
+
if (metadataMatch) {
|
|
28
|
+
const versionInMeta = metadataMatch[0].match(/^\s+version:\s*(.+)$/m);
|
|
29
|
+
if (versionInMeta) {
|
|
30
|
+
// Remove quotes if present (both single and double)
|
|
31
|
+
version = versionInMeta[1].trim().replace(/^["']|["']$/g, '');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (nameMatch) {
|
|
36
|
+
return {
|
|
37
|
+
name: nameMatch[1].trim(),
|
|
38
|
+
description: descMatch ? descMatch[1].trim() : '',
|
|
39
|
+
version: version
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// File doesn't exist or can't be read
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
13
49
|
/**
|
|
14
50
|
* Scan a single directory for skills
|
|
15
51
|
* @param {string} dir - Directory to scan
|
|
@@ -28,20 +64,12 @@ async function scanSingleDir(dir) {
|
|
|
28
64
|
const skillPath = path.join(dir, entry.name);
|
|
29
65
|
const skillFile = path.join(skillPath, 'SKILL.md');
|
|
30
66
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
skills.push({
|
|
38
|
-
name: nameMatch[1].trim(),
|
|
39
|
-
path: skillPath,
|
|
40
|
-
description: descMatch ? descMatch[1].trim() : ''
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
} catch {
|
|
44
|
-
// No SKILL.md or parse error, skip
|
|
67
|
+
const skillInfo = await parseSkillFile(skillFile);
|
|
68
|
+
if (skillInfo) {
|
|
69
|
+
skills.push({
|
|
70
|
+
...skillInfo,
|
|
71
|
+
path: skillPath
|
|
72
|
+
});
|
|
45
73
|
}
|
|
46
74
|
}
|
|
47
75
|
} catch {
|
package/src/version.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version Management Module
|
|
3
|
+
* Handles tracking, listing, updating, and uninstalling skills
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
|
|
10
|
+
const CONFIG_DIR = path.join(os.homedir(), '.config', 'skillman');
|
|
11
|
+
const INSTALLED_FILE = path.join(CONFIG_DIR, 'installed.json');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get the path to the installed skills registry file
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
export function getInstalledSkillsPath() {
|
|
18
|
+
return INSTALLED_FILE;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Registry for tracking installed skills
|
|
23
|
+
*/
|
|
24
|
+
export class InstalledSkillRegistry {
|
|
25
|
+
constructor(registryPath = INSTALLED_FILE) {
|
|
26
|
+
this.registryPath = registryPath;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Load all installed skills from registry
|
|
31
|
+
* @returns {Promise<Array>}
|
|
32
|
+
*/
|
|
33
|
+
async load() {
|
|
34
|
+
try {
|
|
35
|
+
const content = await fs.readFile(this.registryPath, 'utf-8');
|
|
36
|
+
const data = JSON.parse(content);
|
|
37
|
+
return data.skills || [];
|
|
38
|
+
} catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Save skills to registry
|
|
45
|
+
* @param {Array} skills
|
|
46
|
+
*/
|
|
47
|
+
async save(skills) {
|
|
48
|
+
await fs.mkdir(path.dirname(this.registryPath), { recursive: true });
|
|
49
|
+
const data = {
|
|
50
|
+
version: 1,
|
|
51
|
+
updatedAt: new Date().toISOString(),
|
|
52
|
+
skills
|
|
53
|
+
};
|
|
54
|
+
await fs.writeFile(this.registryPath, JSON.stringify(data, null, 2));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Add a skill to the registry
|
|
59
|
+
* @param {Object} skill
|
|
60
|
+
*/
|
|
61
|
+
async add(skill) {
|
|
62
|
+
const skills = await this.load();
|
|
63
|
+
const existingIndex = skills.findIndex(s => s.name === skill.name);
|
|
64
|
+
|
|
65
|
+
if (existingIndex >= 0) {
|
|
66
|
+
skills[existingIndex] = skill;
|
|
67
|
+
} else {
|
|
68
|
+
skills.push(skill);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await this.save(skills);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Remove a skill from the registry
|
|
76
|
+
* @param {string} skillName
|
|
77
|
+
*/
|
|
78
|
+
async remove(skillName) {
|
|
79
|
+
const skills = await this.load();
|
|
80
|
+
const filtered = skills.filter(s => s.name !== skillName);
|
|
81
|
+
await this.save(filtered);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Find a skill by name
|
|
86
|
+
* @param {string} skillName
|
|
87
|
+
* @returns {Promise<Object|undefined>}
|
|
88
|
+
*/
|
|
89
|
+
async find(skillName) {
|
|
90
|
+
const skills = await this.load();
|
|
91
|
+
return skills.find(s => s.name === skillName);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Format installed skills for display
|
|
97
|
+
* @param {Array} skills
|
|
98
|
+
* @param {Function} t - Translation function
|
|
99
|
+
* @returns {Array} Formatted lines
|
|
100
|
+
*/
|
|
101
|
+
export function formatInstalledSkills(skills, t = (key) => key) {
|
|
102
|
+
if (skills.length === 0) {
|
|
103
|
+
return [t('msg.no_installed_skills')];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const lines = [];
|
|
107
|
+
lines.push('');
|
|
108
|
+
|
|
109
|
+
// Group by agent
|
|
110
|
+
const byAgent = skills.reduce((acc, skill) => {
|
|
111
|
+
const agent = skill.agent || 'unknown';
|
|
112
|
+
if (!acc[agent]) acc[agent] = [];
|
|
113
|
+
acc[agent].push(skill);
|
|
114
|
+
return acc;
|
|
115
|
+
}, {});
|
|
116
|
+
|
|
117
|
+
for (const [agent, agentSkills] of Object.entries(byAgent)) {
|
|
118
|
+
lines.push(`${agent}:`);
|
|
119
|
+
for (const skill of agentSkills) {
|
|
120
|
+
const scope = skill.scope === 'global' ? 'G' : 'W';
|
|
121
|
+
const version = skill.version || 'unknown';
|
|
122
|
+
lines.push(` ${skill.name}@${version} [${scope}]`);
|
|
123
|
+
}
|
|
124
|
+
lines.push('');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return lines;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Uninstall a skill
|
|
132
|
+
* @param {string} skillName
|
|
133
|
+
* @param {InstalledSkillRegistry} registry
|
|
134
|
+
* @returns {Promise<boolean>} - true if uninstalled, false if not found
|
|
135
|
+
*/
|
|
136
|
+
export async function uninstallSkill(skillName, registry = new InstalledSkillRegistry()) {
|
|
137
|
+
const skill = await registry.find(skillName);
|
|
138
|
+
|
|
139
|
+
if (!skill) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Remove from filesystem
|
|
144
|
+
if (skill.targetPath) {
|
|
145
|
+
try {
|
|
146
|
+
await fs.rm(skill.targetPath, { recursive: true, force: true });
|
|
147
|
+
} catch (error) {
|
|
148
|
+
// Log but continue - we still want to remove from registry
|
|
149
|
+
console.warn(`Warning: Could not remove skill directory: ${error.message}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Remove from registry
|
|
154
|
+
await registry.remove(skillName);
|
|
155
|
+
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Update a skill by reinstalling from source
|
|
161
|
+
* @param {string} skillName
|
|
162
|
+
* @param {InstalledSkillRegistry} registry
|
|
163
|
+
* @returns {Promise<{success: boolean, message: string}>}
|
|
164
|
+
*/
|
|
165
|
+
export async function updateSkill(skillName, registry = new InstalledSkillRegistry()) {
|
|
166
|
+
const skill = await registry.find(skillName);
|
|
167
|
+
|
|
168
|
+
if (!skill) {
|
|
169
|
+
return { success: false, message: 'Skill not installed' };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!skill.sourcePath) {
|
|
173
|
+
return { success: false, message: 'Cannot update: source path not recorded' };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
await fs.access(skill.sourcePath);
|
|
178
|
+
} catch {
|
|
179
|
+
return { success: false, message: 'Source no longer available' };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Reinstall
|
|
183
|
+
await copyDir(skill.sourcePath, skill.targetPath);
|
|
184
|
+
|
|
185
|
+
// Update registry with new timestamp
|
|
186
|
+
await registry.add({
|
|
187
|
+
...skill,
|
|
188
|
+
updatedAt: new Date().toISOString()
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return { success: true, message: 'Updated successfully' };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Copy directory recursively
|
|
196
|
+
* @param {string} src - Source directory
|
|
197
|
+
* @param {string} dest - Destination directory
|
|
198
|
+
*/
|
|
199
|
+
async function copyDir(src, dest) {
|
|
200
|
+
await fs.mkdir(dest, { recursive: true });
|
|
201
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
202
|
+
|
|
203
|
+
for (const entry of entries) {
|
|
204
|
+
const srcPath = path.join(src, entry.name);
|
|
205
|
+
const destPath = path.join(dest, entry.name);
|
|
206
|
+
|
|
207
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
|
|
208
|
+
|
|
209
|
+
if (entry.isDirectory()) {
|
|
210
|
+
await copyDir(srcPath, destPath);
|
|
211
|
+
} else {
|
|
212
|
+
await fs.copyFile(srcPath, destPath);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|