@botskill/cli 1.0.7 → 1.0.9
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/package.json +1 -1
- package/src/commands/get.js +4 -1
- package/src/commands/update.js +115 -0
- package/src/commands/upgrade.js +75 -0
- package/src/commands/versions.js +58 -0
- package/src/index.js +6 -0
- package/src/lib/constants.js +3 -0
- package/src/lib/updateSkill.js +101 -0
package/package.json
CHANGED
package/src/commands/get.js
CHANGED
|
@@ -27,7 +27,10 @@ getCommand
|
|
|
27
27
|
.option('--api-url <url>', 'API base URL (overrides config for this command)')
|
|
28
28
|
.action(async (specifier, options, command) => {
|
|
29
29
|
const apiUrl = command.optsWithGlobals().apiUrl;
|
|
30
|
-
const
|
|
30
|
+
const spec = (specifier || '').trim();
|
|
31
|
+
const parsed = parseSpecifier(spec);
|
|
32
|
+
const name = spec.startsWith('@') ? spec : parsed.name;
|
|
33
|
+
const version = spec.startsWith('@') ? undefined : parsed.version;
|
|
31
34
|
const outputDir = path.resolve(options.output || process.cwd());
|
|
32
35
|
|
|
33
36
|
if (!name) {
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import { createApiClient } from '../lib/auth.js';
|
|
5
|
+
import { printApiError, printSimpleError } from '../lib/formatError.js';
|
|
6
|
+
import { compareVersions, getLatestVersion, downloadSkillToDir } from '../lib/updateSkill.js';
|
|
7
|
+
|
|
8
|
+
const updateCommand = new Command('update');
|
|
9
|
+
updateCommand
|
|
10
|
+
.description('Update skill(s) to latest version')
|
|
11
|
+
.argument('[name]', 'Skill name to update (omit to update all in current/specified dir)')
|
|
12
|
+
.option('-d, --dir <path>', 'Directory containing skill(s) to update (default: current directory)')
|
|
13
|
+
.option('--dry-run', 'Show what would be updated without downloading')
|
|
14
|
+
.option('--api-url <url>', 'API base URL (overrides config for this command)')
|
|
15
|
+
.action(async (nameArg, options, command) => {
|
|
16
|
+
const apiUrl = command.optsWithGlobals().apiUrl;
|
|
17
|
+
const api = createApiClient(apiUrl);
|
|
18
|
+
const baseDir = path.resolve(options.dir || process.cwd());
|
|
19
|
+
|
|
20
|
+
if (!(await fs.pathExists(baseDir))) {
|
|
21
|
+
printSimpleError('Directory not found', baseDir);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let skillsToUpdate = [];
|
|
25
|
+
if (nameArg) {
|
|
26
|
+
const configPath = path.join(baseDir, nameArg, 'skill.config.json');
|
|
27
|
+
const skillMdPath = path.join(baseDir, nameArg, 'SKILL.md');
|
|
28
|
+
const dirPath = path.join(baseDir, nameArg);
|
|
29
|
+
if (!(await fs.pathExists(dirPath))) {
|
|
30
|
+
printSimpleError('Skill not found', `No directory: ${dirPath}`);
|
|
31
|
+
}
|
|
32
|
+
let name = nameArg;
|
|
33
|
+
let version = '0.0.0';
|
|
34
|
+
if (await fs.pathExists(configPath)) {
|
|
35
|
+
try {
|
|
36
|
+
const config = await fs.readJson(configPath);
|
|
37
|
+
name = config.name || nameArg;
|
|
38
|
+
version = config.version || '0.0.0';
|
|
39
|
+
} catch (_) {}
|
|
40
|
+
} else if (await fs.pathExists(skillMdPath)) {
|
|
41
|
+
try {
|
|
42
|
+
const content = await fs.readFile(skillMdPath, 'utf-8');
|
|
43
|
+
const match = content.match(/^name:\s*(.+)$/m);
|
|
44
|
+
const verMatch = content.match(/version:\s*["']?([\d.]+)/m);
|
|
45
|
+
name = match ? match[1].trim() : nameArg;
|
|
46
|
+
version = verMatch ? verMatch[1] : '0.0.0';
|
|
47
|
+
} catch (_) {}
|
|
48
|
+
}
|
|
49
|
+
skillsToUpdate = [{ name, version, dirPath, dirName: nameArg }];
|
|
50
|
+
} else {
|
|
51
|
+
let skillsToUpdateList = await findInstalledSkills(baseDir);
|
|
52
|
+
if (skillsToUpdateList.length === 0) {
|
|
53
|
+
const configPath = path.join(baseDir, 'skill.config.json');
|
|
54
|
+
const skillMdPath = path.join(baseDir, 'SKILL.md');
|
|
55
|
+
if (await fs.pathExists(configPath) || await fs.pathExists(skillMdPath)) {
|
|
56
|
+
let name = path.basename(baseDir);
|
|
57
|
+
let version = '0.0.0';
|
|
58
|
+
if (await fs.pathExists(configPath)) {
|
|
59
|
+
try {
|
|
60
|
+
const config = await fs.readJson(configPath);
|
|
61
|
+
name = config.name || name;
|
|
62
|
+
version = config.version || '0.0.0';
|
|
63
|
+
} catch (_) {}
|
|
64
|
+
} else if (await fs.pathExists(skillMdPath)) {
|
|
65
|
+
try {
|
|
66
|
+
const content = await fs.readFile(skillMdPath, 'utf-8');
|
|
67
|
+
const match = content.match(/^name:\s*(.+)$/m);
|
|
68
|
+
const verMatch = content.match(/version:\s*["']?([\d.]+)/m);
|
|
69
|
+
name = match ? match[1].trim() : name;
|
|
70
|
+
version = verMatch ? verMatch[1] : '0.0.0';
|
|
71
|
+
} catch (_) {}
|
|
72
|
+
}
|
|
73
|
+
skillsToUpdateList = [{ name, version, dirPath: baseDir, dirName: path.basename(baseDir) }];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
skillsToUpdate = skillsToUpdateList;
|
|
77
|
+
if (skillsToUpdate.length === 0) {
|
|
78
|
+
console.log('No skills found. Run from a skills directory or use -d <path>.');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (options.dryRun) {
|
|
84
|
+
console.log('[DRY RUN] Would check and update:');
|
|
85
|
+
skillsToUpdate.forEach((s) => console.log(` - ${s.name} (${s.version})`));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let updated = 0;
|
|
90
|
+
for (const skill of skillsToUpdate) {
|
|
91
|
+
try {
|
|
92
|
+
const latest = await getLatestVersion(api, skill.name);
|
|
93
|
+
if (!latest) {
|
|
94
|
+
console.log(`Skip ${skill.name}: not found`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (compareVersions(latest, skill.version) <= 0) {
|
|
98
|
+
console.log(`${skill.name}: already at ${skill.version}`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const parentDir = path.dirname(skill.dirPath);
|
|
102
|
+
await downloadSkillToDir(api, skill.name, latest, parentDir);
|
|
103
|
+
console.log(`${skill.name}: ${skill.version} → ${latest}`);
|
|
104
|
+
updated++;
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (err.response?.data?.error) err._overrideMsg = err.response.data.error;
|
|
107
|
+
printApiError(err, { prefix: `Update ${skill.name} failed` });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (updated > 0) {
|
|
111
|
+
console.log(`\nUpdated ${updated} skill(s).`);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
export { updateCommand };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import { createApiClient } from '../lib/auth.js';
|
|
6
|
+
import { printApiError, printSimpleError } from '../lib/formatError.js';
|
|
7
|
+
import { compareVersions, getLatestVersion, downloadSkillToDir, findInstalledSkills } from '../lib/updateSkill.js';
|
|
8
|
+
import { DEFAULT_SKILLS_DIR } from '../lib/constants.js';
|
|
9
|
+
|
|
10
|
+
function resolveSkillsDir(dir) {
|
|
11
|
+
const p = dir || DEFAULT_SKILLS_DIR;
|
|
12
|
+
return p.replace(/^~/, os.homedir());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const upgradeCommand = new Command('upgrade');
|
|
16
|
+
upgradeCommand
|
|
17
|
+
.description('Upgrade all skills in the skills directory to latest version')
|
|
18
|
+
.option('-d, --dir <path>', `Skills directory (default: ${DEFAULT_SKILLS_DIR})`)
|
|
19
|
+
.option('--dry-run', 'Show what would be upgraded without downloading')
|
|
20
|
+
.option('--api-url <url>', 'API base URL (overrides config for this command)')
|
|
21
|
+
.action(async (options, command) => {
|
|
22
|
+
const apiUrl = command.optsWithGlobals().apiUrl;
|
|
23
|
+
const api = createApiClient(apiUrl);
|
|
24
|
+
const skillsDir = path.resolve(resolveSkillsDir(options.dir));
|
|
25
|
+
|
|
26
|
+
if (!(await fs.pathExists(skillsDir))) {
|
|
27
|
+
printSimpleError('Skills directory not found', skillsDir);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const skills = await findInstalledSkills(skillsDir);
|
|
31
|
+
if (skills.length === 0) {
|
|
32
|
+
console.log('No skills found in', skillsDir);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (options.dryRun) {
|
|
37
|
+
console.log(`[DRY RUN] Would check ${skills.length} skill(s) in ${skillsDir}`);
|
|
38
|
+
for (const s of skills) {
|
|
39
|
+
try {
|
|
40
|
+
const latest = await getLatestVersion(api, s.name);
|
|
41
|
+
const needUpgrade = latest && compareVersions(latest, s.version) > 0;
|
|
42
|
+
console.log(` ${s.name}: ${s.version}${needUpgrade ? ` → ${latest}` : ' (latest)'}`);
|
|
43
|
+
} catch (_) {
|
|
44
|
+
console.log(` ${s.name}: ${s.version} (check failed)`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let updated = 0;
|
|
51
|
+
for (const skill of skills) {
|
|
52
|
+
try {
|
|
53
|
+
const latest = await getLatestVersion(api, skill.name);
|
|
54
|
+
if (!latest) {
|
|
55
|
+
console.log(`Skip ${skill.name}: not found`);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (compareVersions(latest, skill.version) <= 0) {
|
|
59
|
+
console.log(`${skill.name}: already at ${skill.version}`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
await downloadSkillToDir(api, skill.name, latest, skillsDir);
|
|
63
|
+
console.log(`${skill.name}: ${skill.version} → ${latest}`);
|
|
64
|
+
updated++;
|
|
65
|
+
} catch (err) {
|
|
66
|
+
if (err.response?.data?.error) err._overrideMsg = err.response.data.error;
|
|
67
|
+
printApiError(err, { prefix: `Upgrade ${skill.name} failed` });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (updated > 0) {
|
|
71
|
+
console.log(`\nUpgraded ${updated} skill(s).`);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export { upgradeCommand };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { createApiClient } from '../lib/auth.js';
|
|
3
|
+
import { printApiError } from '../lib/formatError.js';
|
|
4
|
+
|
|
5
|
+
function parseSpecifier(spec) {
|
|
6
|
+
const s = spec.trim();
|
|
7
|
+
const atIdx = s.lastIndexOf('@');
|
|
8
|
+
if (atIdx < 0) return { name: s, version: undefined };
|
|
9
|
+
return {
|
|
10
|
+
name: s.slice(0, atIdx).trim(),
|
|
11
|
+
version: s.slice(atIdx + 1).trim() || undefined,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const versionsCommand = new Command('versions');
|
|
16
|
+
versionsCommand
|
|
17
|
+
.description('List all versions of a skill')
|
|
18
|
+
.argument('<name>', 'Skill name (e.g. learn-skills)')
|
|
19
|
+
.option('--api-url <url>', 'API base URL (overrides config for this command)')
|
|
20
|
+
.action(async (nameArg, options, command) => {
|
|
21
|
+
const apiUrl = command.optsWithGlobals().apiUrl;
|
|
22
|
+
const name = nameArg?.startsWith('@') ? nameArg.trim() : parseSpecifier(nameArg).name;
|
|
23
|
+
|
|
24
|
+
if (!name) {
|
|
25
|
+
console.error('Error: skill name is required');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const api = createApiClient(apiUrl);
|
|
31
|
+
const resolveRes = await api.get(`/skills/by-name/${encodeURIComponent(name)}`);
|
|
32
|
+
const skill = resolveRes.data?.skill ?? resolveRes.data;
|
|
33
|
+
if (!skill?._id) {
|
|
34
|
+
console.error('Skill not found');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const versions = skill.versions || [];
|
|
39
|
+
const versionList = versions.length > 0 ? versions : (skill.version ? [{ version: skill.version }] : []);
|
|
40
|
+
|
|
41
|
+
if (versionList.length === 0) {
|
|
42
|
+
console.log(`No versions found for ${skill.name}`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log(`\n${skill.name} - ${versionList.length} version(s):`);
|
|
47
|
+
console.log('─'.repeat(40));
|
|
48
|
+
versionList.forEach((v) => {
|
|
49
|
+
const date = v.createdAt ? new Date(v.createdAt).toLocaleDateString() : '';
|
|
50
|
+
console.log(` ${v.version}${date ? ` (${date})` : ''}`);
|
|
51
|
+
});
|
|
52
|
+
console.log('\nUse "skm get ' + skill.name + '@<version>" to download.');
|
|
53
|
+
} catch (err) {
|
|
54
|
+
printApiError(err, { prefix: 'Versions failed' });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export { versionsCommand };
|
package/src/index.js
CHANGED
|
@@ -15,6 +15,9 @@ import { publishCommand } from './commands/publish.js';
|
|
|
15
15
|
import { listCommand } from './commands/list.js';
|
|
16
16
|
import { searchCommand } from './commands/search.js';
|
|
17
17
|
import { infoCommand } from './commands/info.js';
|
|
18
|
+
import { versionsCommand } from './commands/versions.js';
|
|
19
|
+
import { updateCommand } from './commands/update.js';
|
|
20
|
+
import { upgradeCommand } from './commands/upgrade.js';
|
|
18
21
|
import { helpCommand } from './commands/help.js';
|
|
19
22
|
|
|
20
23
|
// 读取 package.json 中的版本号
|
|
@@ -43,6 +46,9 @@ program.addCommand(publishCommand);
|
|
|
43
46
|
program.addCommand(listCommand);
|
|
44
47
|
program.addCommand(searchCommand);
|
|
45
48
|
program.addCommand(infoCommand);
|
|
49
|
+
program.addCommand(versionsCommand);
|
|
50
|
+
program.addCommand(updateCommand);
|
|
51
|
+
program.addCommand(upgradeCommand);
|
|
46
52
|
program.addCommand(helpCommand);
|
|
47
53
|
|
|
48
54
|
program.parse();
|
package/src/lib/constants.js
CHANGED
|
@@ -8,3 +8,6 @@ export const FALLBACK_API_URL = 'http://localhost:3000/api';
|
|
|
8
8
|
|
|
9
9
|
export const getDefaultApiUrl = () =>
|
|
10
10
|
DEFAULT_API_URL === '__DEFAULT_API_URL__' ? FALLBACK_API_URL : DEFAULT_API_URL;
|
|
11
|
+
|
|
12
|
+
/** Default skills directory for upgrade (Cursor) */
|
|
13
|
+
export const DEFAULT_SKILLS_DIR = '~/.cursor/skills';
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import AdmZip from 'adm-zip';
|
|
4
|
+
import { createApiClient } from './auth.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Compare semver versions: returns 1 if a > b, -1 if a < b, 0 if equal
|
|
8
|
+
*/
|
|
9
|
+
export function compareVersions(a, b) {
|
|
10
|
+
if (!a || !b) return 0;
|
|
11
|
+
const pa = String(a).split('.').map(Number);
|
|
12
|
+
const pb = String(b).split('.').map(Number);
|
|
13
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
14
|
+
const va = pa[i] || 0;
|
|
15
|
+
const vb = pb[i] || 0;
|
|
16
|
+
if (va > vb) return 1;
|
|
17
|
+
if (va < vb) return -1;
|
|
18
|
+
}
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get latest version from API for a skill (by semver)
|
|
24
|
+
*/
|
|
25
|
+
export async function getLatestVersion(api, name) {
|
|
26
|
+
const res = await api.get(`/skills/by-name/${encodeURIComponent(name)}`);
|
|
27
|
+
const skill = res.data?.skill ?? res.data;
|
|
28
|
+
if (!skill?._id) return null;
|
|
29
|
+
const versions = skill.versions || [];
|
|
30
|
+
if (versions.length === 0) return skill.version || null;
|
|
31
|
+
let latest = versions[0].version;
|
|
32
|
+
for (const v of versions) {
|
|
33
|
+
if (compareVersions(v.version, latest) > 0) latest = v.version;
|
|
34
|
+
}
|
|
35
|
+
return latest;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Download skill to directory (replaces existing)
|
|
40
|
+
*/
|
|
41
|
+
export async function downloadSkillToDir(api, name, version, outputDir) {
|
|
42
|
+
const fullSpec = version ? `${name}@${version}` : name;
|
|
43
|
+
const resolveRes = await api.get(`/skills/by-name/${encodeURIComponent(fullSpec)}`);
|
|
44
|
+
const skill = resolveRes.data?.skill ?? resolveRes.data;
|
|
45
|
+
if (!skill?._id) throw new Error('Skill not found');
|
|
46
|
+
|
|
47
|
+
const versionParam = version ? `?version=${encodeURIComponent(version)}` : '';
|
|
48
|
+
const url = `/skills/${encodeURIComponent(skill._id)}/download${versionParam}`;
|
|
49
|
+
const res = await api.get(url, { responseType: 'arraybuffer' });
|
|
50
|
+
const buffer = Buffer.from(res.data);
|
|
51
|
+
|
|
52
|
+
const zip = new AdmZip(buffer);
|
|
53
|
+
const entries = zip.getEntries();
|
|
54
|
+
const rootEntry = entries.find(e => e.isDirectory)?.entryName || entries[0]?.entryName?.split('/')[0] || 'skill';
|
|
55
|
+
const skillDirName = rootEntry.replace(/\/$/, '');
|
|
56
|
+
|
|
57
|
+
const targetPath = path.join(outputDir, skillDirName);
|
|
58
|
+
await fs.remove(targetPath).catch(() => {});
|
|
59
|
+
zip.extractAllTo(outputDir, true);
|
|
60
|
+
|
|
61
|
+
return path.join(outputDir, skillDirName);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Find installed skills in a directory (subdirs with skill.config.json or SKILL.md)
|
|
66
|
+
*/
|
|
67
|
+
export async function findInstalledSkills(skillsDir) {
|
|
68
|
+
if (!(await fs.pathExists(skillsDir))) return [];
|
|
69
|
+
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
|
70
|
+
const skills = [];
|
|
71
|
+
for (const e of entries) {
|
|
72
|
+
if (!e.isDirectory()) continue;
|
|
73
|
+
const dirPath = path.join(skillsDir, e.name);
|
|
74
|
+
const configPath = path.join(dirPath, 'skill.config.json');
|
|
75
|
+
const skillMdPath = path.join(dirPath, 'SKILL.md');
|
|
76
|
+
let name, version;
|
|
77
|
+
if (await fs.pathExists(configPath)) {
|
|
78
|
+
try {
|
|
79
|
+
const config = await fs.readJson(configPath);
|
|
80
|
+
name = config.name || e.name;
|
|
81
|
+
version = config.version || '0.0.0';
|
|
82
|
+
} catch (_) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
} else if (await fs.pathExists(skillMdPath)) {
|
|
86
|
+
try {
|
|
87
|
+
const content = await fs.readFile(skillMdPath, 'utf-8');
|
|
88
|
+
const match = content.match(/^name:\s*(.+)$/m);
|
|
89
|
+
const verMatch = content.match(/version:\s*["']?([\d.]+)/m);
|
|
90
|
+
name = match ? match[1].trim() : e.name;
|
|
91
|
+
version = verMatch ? verMatch[1] : '0.0.0';
|
|
92
|
+
} catch (_) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
skills.push({ name, version, dirPath, dirName: e.name });
|
|
99
|
+
}
|
|
100
|
+
return skills;
|
|
101
|
+
}
|