@botskill/cli 1.0.4 → 1.0.6
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 +3 -2
- package/src/commands/info.js +3 -2
- package/src/commands/init.js +34 -15
- package/src/commands/list.js +8 -4
- package/src/commands/login.js +2 -2
- package/src/commands/logout.js +2 -2
- package/src/commands/pack.js +53 -0
- package/src/commands/publish.js +37 -5
- package/src/commands/push.js +39 -6
- package/src/commands/search.js +8 -4
- package/src/index.js +4 -1
- package/src/lib/packSkill.js +95 -0
- package/src/lib/uploadSkill.js +48 -0
package/package.json
CHANGED
package/src/commands/get.js
CHANGED
|
@@ -25,7 +25,8 @@ getCommand
|
|
|
25
25
|
.option('-o, --output <dir>', 'Output directory (default: current directory)')
|
|
26
26
|
.option('--dry-run', 'Show what would be downloaded without actually downloading')
|
|
27
27
|
.option('--api-url <url>', 'API base URL (overrides config for this command)')
|
|
28
|
-
.action(async (specifier, options) => {
|
|
28
|
+
.action(async (specifier, options, command) => {
|
|
29
|
+
const apiUrl = command.optsWithGlobals().apiUrl;
|
|
29
30
|
const { name, version } = parseSpecifier(specifier);
|
|
30
31
|
const outputDir = path.resolve(options.output || process.cwd());
|
|
31
32
|
|
|
@@ -42,7 +43,7 @@ getCommand
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
try {
|
|
45
|
-
const api = createApiClient(
|
|
46
|
+
const api = createApiClient(apiUrl);
|
|
46
47
|
|
|
47
48
|
const fullSpec = version ? `${name}@${version}` : name;
|
|
48
49
|
console.log(`Downloading skill: ${fullSpec}`);
|
package/src/commands/info.js
CHANGED
|
@@ -17,7 +17,7 @@ infoCommand
|
|
|
17
17
|
.description('Show skill details from BotSkill')
|
|
18
18
|
.argument('<specifier>', 'Skill name or name@version')
|
|
19
19
|
.option('--api-url <url>', 'API base URL (overrides config for this command)')
|
|
20
|
-
.action(async (specifier, options
|
|
20
|
+
.action(async (specifier, options, command) => {
|
|
21
21
|
const { name, version } = parseSpecifier(specifier);
|
|
22
22
|
|
|
23
23
|
if (!name) {
|
|
@@ -25,8 +25,9 @@ infoCommand
|
|
|
25
25
|
process.exit(1);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
const apiUrl = command.optsWithGlobals().apiUrl;
|
|
28
29
|
try {
|
|
29
|
-
const api = createApiClient(
|
|
30
|
+
const api = createApiClient(apiUrl);
|
|
30
31
|
const fullSpec = version ? `${name}@${version}` : name;
|
|
31
32
|
|
|
32
33
|
const resolveRes = await api.get(`/skills/by-name/${encodeURIComponent(fullSpec)}`);
|
package/src/commands/init.js
CHANGED
|
@@ -3,22 +3,25 @@ import path from 'path';
|
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
4
|
import inquirer from 'inquirer';
|
|
5
5
|
|
|
6
|
+
function toSkillName(raw) {
|
|
7
|
+
const s = String(raw || 'my-skill').trim();
|
|
8
|
+
return s.toLowerCase()
|
|
9
|
+
.replace(/\s+/g, '-')
|
|
10
|
+
.replace(/[^a-z0-9-]/g, '')
|
|
11
|
+
.replace(/-+/g, '-')
|
|
12
|
+
.replace(/^-|-$/g, '') || 'my-skill';
|
|
13
|
+
}
|
|
14
|
+
|
|
6
15
|
const initCommand = new Command('init');
|
|
7
16
|
initCommand
|
|
8
17
|
.description('Initialize a new skill project')
|
|
18
|
+
.argument('[path]', 'Target directory: "." or path for current/specified dir; omit to create skill-named directory')
|
|
9
19
|
.option('-n, --name <name>', 'Project/skill name')
|
|
10
20
|
.option('-d, --description <description>', 'Skill description')
|
|
11
21
|
.option('-c, --category <category>', 'Category: ai, data, web, devops, security, tools')
|
|
12
22
|
.option('-y, --yes', 'Use defaults without prompting')
|
|
13
|
-
.action(async (options) => {
|
|
23
|
+
.action(async (pathArg, options) => {
|
|
14
24
|
const cwd = process.cwd();
|
|
15
|
-
const configPath = path.join(cwd, 'skill.config.json');
|
|
16
|
-
|
|
17
|
-
if (await fs.pathExists(configPath)) {
|
|
18
|
-
console.error('skill.config.json already exists in this directory.');
|
|
19
|
-
process.exit(1);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
25
|
const validCategories = ['ai', 'data', 'web', 'devops', 'security', 'tools'];
|
|
23
26
|
let answers = {};
|
|
24
27
|
|
|
@@ -68,6 +71,25 @@ initCommand
|
|
|
68
71
|
]);
|
|
69
72
|
}
|
|
70
73
|
|
|
74
|
+
const skillName = toSkillName(answers.name);
|
|
75
|
+
let targetDir;
|
|
76
|
+
|
|
77
|
+
if (pathArg && pathArg !== '.') {
|
|
78
|
+
targetDir = path.resolve(cwd, pathArg);
|
|
79
|
+
await fs.ensureDir(targetDir);
|
|
80
|
+
} else if (pathArg === '.') {
|
|
81
|
+
targetDir = cwd;
|
|
82
|
+
} else {
|
|
83
|
+
targetDir = path.join(cwd, skillName);
|
|
84
|
+
await fs.ensureDir(targetDir);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const configPath = path.join(targetDir, 'skill.config.json');
|
|
88
|
+
if (await fs.pathExists(configPath)) {
|
|
89
|
+
console.error(`skill.config.json already exists in ${targetDir}`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
71
93
|
const config = {
|
|
72
94
|
name: answers.name,
|
|
73
95
|
description: answers.description,
|
|
@@ -82,12 +104,6 @@ initCommand
|
|
|
82
104
|
|
|
83
105
|
await fs.writeJson(configPath, config, { spaces: 2 });
|
|
84
106
|
|
|
85
|
-
// Agent Skills spec: https://agentskills.io/specification
|
|
86
|
-
// name: required, 1-64 chars, lowercase + hyphens
|
|
87
|
-
// description: required, max 1024 chars
|
|
88
|
-
// metadata.version, metadata.author: optional
|
|
89
|
-
const rawName = String(answers.name || 'my-skill').trim();
|
|
90
|
-
const skillName = rawName.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '').replace(/-+/g, '-').replace(/^-|-$/g, '') || 'my-skill';
|
|
91
107
|
const skillMd = `---
|
|
92
108
|
name: ${skillName}
|
|
93
109
|
description: ${answers.description}
|
|
@@ -115,10 +131,13 @@ Add your usage documentation here. The Markdown body contains skill instructions
|
|
|
115
131
|
\`\`\`
|
|
116
132
|
`;
|
|
117
133
|
|
|
118
|
-
const skillMdPath = path.join(
|
|
134
|
+
const skillMdPath = path.join(targetDir, 'SKILL.md');
|
|
119
135
|
await fs.writeFile(skillMdPath, skillMd, 'utf-8');
|
|
120
136
|
|
|
121
137
|
console.log('Created skill.config.json and SKILL.md');
|
|
138
|
+
if (targetDir !== cwd) {
|
|
139
|
+
console.log(`Location: ${targetDir}`);
|
|
140
|
+
}
|
|
122
141
|
console.log('\nNext steps:');
|
|
123
142
|
console.log('1. Edit SKILL.md to add documentation (the content below the frontmatter)');
|
|
124
143
|
console.log('2. Run "skm login" to authenticate');
|
package/src/commands/list.js
CHANGED
|
@@ -8,7 +8,9 @@ function formatSkillDisplay(skill) {
|
|
|
8
8
|
const downloads = skill.downloads ?? 0;
|
|
9
9
|
const category = skill.category || '—';
|
|
10
10
|
const status = skill.status || '—';
|
|
11
|
-
|
|
11
|
+
const desc = (skill.description || '').trim();
|
|
12
|
+
const description = desc.length > 80 ? desc.slice(0, 77) + '...' : desc || '—';
|
|
13
|
+
return { name, version, downloads, category, status, description };
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
const listCommand = new Command('list');
|
|
@@ -21,8 +23,9 @@ listCommand
|
|
|
21
23
|
.option('-l, --limit <number>', 'Maximum number of results (default: 20)', '20')
|
|
22
24
|
.option('-p, --page <number>', 'Page number for pagination (default: 1)', '1')
|
|
23
25
|
.option('--api-url <url>', 'API base URL (overrides config for this command)')
|
|
24
|
-
.action(async (options) => {
|
|
25
|
-
const
|
|
26
|
+
.action(async (options, command) => {
|
|
27
|
+
const apiUrl = command.optsWithGlobals().apiUrl;
|
|
28
|
+
const api = createApiClient(apiUrl);
|
|
26
29
|
const limit = parseInt(options.limit, 10) || 20;
|
|
27
30
|
const page = parseInt(options.page, 10) || 1;
|
|
28
31
|
|
|
@@ -59,9 +62,10 @@ listCommand
|
|
|
59
62
|
console.log(`\nFound ${pagination.totalSkills ?? skills.length} skill(s):`);
|
|
60
63
|
console.log('─'.repeat(60));
|
|
61
64
|
skills.forEach((skill) => {
|
|
62
|
-
const { name, version, downloads, category, status } = formatSkillDisplay(skill);
|
|
65
|
+
const { name, version, downloads, category, status, description } = formatSkillDisplay(skill);
|
|
63
66
|
const statusStr = options.mine ? ` | ${status}` : '';
|
|
64
67
|
console.log(` ${name}`);
|
|
68
|
+
console.log(` ${description}`);
|
|
65
69
|
console.log(` Version: ${version} | Downloads: ${downloads} | Category: ${category}${statusStr}`);
|
|
66
70
|
});
|
|
67
71
|
if (pagination.totalPages > 1) {
|
package/src/commands/login.js
CHANGED
|
@@ -12,8 +12,8 @@ loginCommand
|
|
|
12
12
|
.option('-p, --password <password>', 'Password')
|
|
13
13
|
.option('-t, --token <token>', 'Use access token directly (from web)')
|
|
14
14
|
.option('--api-url <url>', 'API base URL (overrides config for this command)')
|
|
15
|
-
.action(async (options) => {
|
|
16
|
-
const apiUrl = normalizeApiUrl(
|
|
15
|
+
.action(async (options, command) => {
|
|
16
|
+
const apiUrl = normalizeApiUrl(command.optsWithGlobals().apiUrl || getApiUrl());
|
|
17
17
|
|
|
18
18
|
if (options.token) {
|
|
19
19
|
setAuth({ token: options.token });
|
package/src/commands/logout.js
CHANGED
|
@@ -6,8 +6,8 @@ const logoutCommand = new Command('logout');
|
|
|
6
6
|
logoutCommand
|
|
7
7
|
.description('Logout from BotSkill')
|
|
8
8
|
.option('--api-url <url>', 'API base URL (overrides config for this command)')
|
|
9
|
-
.action(async (options) => {
|
|
10
|
-
const apiUrl = normalizeApiUrl(
|
|
9
|
+
.action(async (options, command) => {
|
|
10
|
+
const apiUrl = normalizeApiUrl(command.optsWithGlobals().apiUrl || getApiUrl());
|
|
11
11
|
const refreshToken = getRefreshToken();
|
|
12
12
|
if (refreshToken) {
|
|
13
13
|
try {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import { packDirectory } from '../lib/packSkill.js';
|
|
5
|
+
import { printSimpleError } from '../lib/formatError.js';
|
|
6
|
+
|
|
7
|
+
const packCommand = new Command('pack');
|
|
8
|
+
packCommand
|
|
9
|
+
.description('Pack current or specified directory into upload format (skill.zip)')
|
|
10
|
+
.argument('[path]', 'Directory to pack (default: current directory)')
|
|
11
|
+
.option('-o, --output <file>', 'Output file path (default: skill.zip in source directory)')
|
|
12
|
+
.option('--dry-run', 'Validate only, do not create zip')
|
|
13
|
+
.action(async (pathArg, options) => {
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
const dirPath = pathArg ? path.resolve(cwd, pathArg) : cwd;
|
|
16
|
+
|
|
17
|
+
if (!(await fs.pathExists(dirPath))) {
|
|
18
|
+
printSimpleError('Directory not found', dirPath);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const stat = await fs.stat(dirPath);
|
|
22
|
+
if (!stat.isDirectory()) {
|
|
23
|
+
printSimpleError('Not a directory', dirPath);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const skillMdPath = path.join(dirPath, 'SKILL.md');
|
|
27
|
+
if (!(await fs.pathExists(skillMdPath))) {
|
|
28
|
+
printSimpleError('SKILL.md not found', 'Create SKILL.md in the directory first');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (options.dryRun) {
|
|
32
|
+
console.log('[DRY RUN] Would pack:', dirPath);
|
|
33
|
+
console.log('[DRY RUN] Output:', options.output || path.join(dirPath, 'skill.zip'));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const outputPath = await packDirectory(dirPath, { output: options.output });
|
|
39
|
+
console.log('Packed successfully!');
|
|
40
|
+
console.log(`Output: ${outputPath}`);
|
|
41
|
+
console.log('\nUse "skm push" or "skm push -f <path>" to upload.');
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (err.message === 'NO_SKILL_MD') {
|
|
44
|
+
printSimpleError('SKILL.md not found', 'Create SKILL.md in the directory first');
|
|
45
|
+
} else if (err.message === 'FILE_NOT_FOUND') {
|
|
46
|
+
printSimpleError('Directory not found', dirPath);
|
|
47
|
+
} else {
|
|
48
|
+
printSimpleError(err.message || 'Pack failed');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export { packCommand };
|
package/src/commands/publish.js
CHANGED
|
@@ -4,14 +4,17 @@ import fs from 'fs-extra';
|
|
|
4
4
|
import { getToken } from '../lib/auth.js';
|
|
5
5
|
import { printApiError, printSimpleError } from '../lib/formatError.js';
|
|
6
6
|
import { uploadSkillFile, findUploadFile } from '../lib/uploadSkill.js';
|
|
7
|
+
import { packDirectory } from '../lib/packSkill.js';
|
|
7
8
|
|
|
8
9
|
const publishCommand = new Command('publish');
|
|
9
10
|
publishCommand
|
|
10
11
|
.description('Publish a skill to BotSkill (alias for push)')
|
|
11
12
|
.option('-f, --file <path>', 'Path to SKILL.md, .zip, or .tar.gz')
|
|
13
|
+
.option('--config-dir <path>', 'Directory containing skill.config.json (default: same as file)')
|
|
12
14
|
.option('--dry-run', 'Validate without uploading')
|
|
13
15
|
.option('--api-url <url>', 'API base URL (overrides config for this command)')
|
|
14
|
-
.action(async (options) => {
|
|
16
|
+
.action(async (options, command) => {
|
|
17
|
+
const apiUrl = command.optsWithGlobals().apiUrl;
|
|
15
18
|
if (!getToken()) {
|
|
16
19
|
console.error('Not logged in. Run: skm login');
|
|
17
20
|
process.exit(1);
|
|
@@ -31,14 +34,37 @@ publishCommand
|
|
|
31
34
|
}
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
|
|
37
|
+
const configDir = options.configDir ? path.resolve(options.configDir) : path.dirname(path.resolve(filePath));
|
|
38
|
+
let tempPackPath = null;
|
|
39
|
+
let uploadPath = filePath;
|
|
40
|
+
|
|
41
|
+
const isSkillMd = filePath.toLowerCase().endsWith('.md') || path.basename(filePath) === 'SKILL.md';
|
|
42
|
+
if (isSkillMd) {
|
|
43
|
+
const dirPath = path.dirname(path.resolve(filePath));
|
|
44
|
+
if (options.dryRun) {
|
|
45
|
+
console.log('[DRY RUN] Would pack directory to temp (skill-{version}.zip) and upload');
|
|
46
|
+
console.log('[DRY RUN] Source:', path.resolve(filePath));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
tempPackPath = await packDirectory(dirPath, { useTempDir: true });
|
|
51
|
+
uploadPath = tempPackPath;
|
|
52
|
+
} catch (err) {
|
|
53
|
+
if (err.message === 'NO_SKILL_MD') {
|
|
54
|
+
printSimpleError('SKILL.md not found', 'Create SKILL.md in the directory first');
|
|
55
|
+
} else {
|
|
56
|
+
printSimpleError(err.message || 'Pack failed');
|
|
57
|
+
}
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
} else if (options.dryRun) {
|
|
35
61
|
console.log('[DRY RUN] Would publish:', path.resolve(filePath));
|
|
36
62
|
return;
|
|
37
63
|
}
|
|
38
64
|
|
|
39
|
-
console.log(`Publishing skill from ${path.basename(
|
|
65
|
+
console.log(`Publishing skill from ${path.basename(uploadPath)}...`);
|
|
40
66
|
try {
|
|
41
|
-
const skill = await uploadSkillFile(
|
|
67
|
+
const skill = await uploadSkillFile(uploadPath, { apiUrl, configDir });
|
|
42
68
|
console.log('Skill published successfully!');
|
|
43
69
|
console.log(`Name: ${skill?.name}`);
|
|
44
70
|
console.log(`Version: ${skill?.version || (skill?.versions?.[0]?.version)}`);
|
|
@@ -47,13 +73,19 @@ publishCommand
|
|
|
47
73
|
if (err.message === 'NOT_LOGGED_IN') {
|
|
48
74
|
printSimpleError('Not logged in', 'Run "skm login" first');
|
|
49
75
|
} else if (err.message === 'FILE_NOT_FOUND') {
|
|
50
|
-
printSimpleError('File not found',
|
|
76
|
+
printSimpleError('File not found', uploadPath);
|
|
51
77
|
} else {
|
|
52
78
|
const msg = err.response?.data?.error || err.response?.data?.details?.[0];
|
|
53
79
|
if (msg) err._overrideMsg = msg;
|
|
54
80
|
if (err.response?.status === 401) err._overrideMsg = 'Token expired or invalid. Run "skm login" first.';
|
|
55
81
|
printApiError(err, { prefix: 'Publish failed' });
|
|
56
82
|
}
|
|
83
|
+
} finally {
|
|
84
|
+
if (tempPackPath) {
|
|
85
|
+
try {
|
|
86
|
+
await fs.remove(path.dirname(tempPackPath));
|
|
87
|
+
} catch (_) {}
|
|
88
|
+
}
|
|
57
89
|
}
|
|
58
90
|
});
|
|
59
91
|
|
package/src/commands/push.js
CHANGED
|
@@ -3,15 +3,18 @@ import path from 'path';
|
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
4
|
import { getToken } from '../lib/auth.js';
|
|
5
5
|
import { printApiError, printSimpleError } from '../lib/formatError.js';
|
|
6
|
-
import { uploadSkillFile, findUploadFile
|
|
6
|
+
import { uploadSkillFile, findUploadFile } from '../lib/uploadSkill.js';
|
|
7
|
+
import { packDirectory } from '../lib/packSkill.js';
|
|
7
8
|
|
|
8
9
|
const pushCommand = new Command('push');
|
|
9
10
|
pushCommand
|
|
10
11
|
.description('Upload/push a skill to BotSkill (SKILL.md, .zip, or .tar.gz)')
|
|
11
12
|
.option('-f, --file <path>', 'Path to SKILL.md, .zip, or .tar.gz')
|
|
13
|
+
.option('--config-dir <path>', 'Directory containing skill.config.json (default: same as file)')
|
|
12
14
|
.option('--dry-run', 'Validate without uploading')
|
|
13
15
|
.option('--api-url <url>', 'API base URL')
|
|
14
|
-
.action(async (options) => {
|
|
16
|
+
.action(async (options, command) => {
|
|
17
|
+
const apiUrl = command.optsWithGlobals().apiUrl;
|
|
15
18
|
if (!getToken()) {
|
|
16
19
|
console.error('Not logged in. Run: skm login');
|
|
17
20
|
process.exit(1);
|
|
@@ -31,14 +34,38 @@ pushCommand
|
|
|
31
34
|
}
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
|
|
37
|
+
const configDir = options.configDir ? path.resolve(options.configDir) : path.dirname(path.resolve(filePath));
|
|
38
|
+
let tempPackPath = null;
|
|
39
|
+
let uploadPath = filePath;
|
|
40
|
+
|
|
41
|
+
// 当源是 SKILL.md 时,打包到临时目录并加上版本号
|
|
42
|
+
const isSkillMd = filePath.toLowerCase().endsWith('.md') || path.basename(filePath) === 'SKILL.md';
|
|
43
|
+
if (isSkillMd) {
|
|
44
|
+
const dirPath = path.dirname(path.resolve(filePath));
|
|
45
|
+
if (options.dryRun) {
|
|
46
|
+
console.log('[DRY RUN] Would pack directory to temp (skill-{version}.zip) and upload');
|
|
47
|
+
console.log('[DRY RUN] Source:', path.resolve(filePath));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
tempPackPath = await packDirectory(dirPath, { useTempDir: true });
|
|
52
|
+
uploadPath = tempPackPath;
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (err.message === 'NO_SKILL_MD') {
|
|
55
|
+
printSimpleError('SKILL.md not found', 'Create SKILL.md in the directory first');
|
|
56
|
+
} else {
|
|
57
|
+
printSimpleError(err.message || 'Pack failed');
|
|
58
|
+
}
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
} else if (options.dryRun) {
|
|
35
62
|
console.log('[DRY RUN] Would upload:', path.resolve(filePath));
|
|
36
63
|
return;
|
|
37
64
|
}
|
|
38
65
|
|
|
39
|
-
console.log(`Pushing skill from ${path.basename(
|
|
66
|
+
console.log(`Pushing skill from ${path.basename(uploadPath)}...`);
|
|
40
67
|
try {
|
|
41
|
-
const skill = await uploadSkillFile(
|
|
68
|
+
const skill = await uploadSkillFile(uploadPath, { apiUrl, configDir });
|
|
42
69
|
console.log('Skill uploaded successfully!');
|
|
43
70
|
console.log(`Name: ${skill?.name}`);
|
|
44
71
|
console.log(`Version: ${skill?.version || (skill?.versions?.[0]?.version)}`);
|
|
@@ -47,13 +74,19 @@ pushCommand
|
|
|
47
74
|
if (err.message === 'NOT_LOGGED_IN') {
|
|
48
75
|
printSimpleError('Not logged in', 'Run "skm login" first');
|
|
49
76
|
} else if (err.message === 'FILE_NOT_FOUND') {
|
|
50
|
-
printSimpleError('File not found',
|
|
77
|
+
printSimpleError('File not found', uploadPath);
|
|
51
78
|
} else {
|
|
52
79
|
const msg = err.response?.data?.error || err.response?.data?.details?.[0];
|
|
53
80
|
if (msg) err._overrideMsg = msg;
|
|
54
81
|
if (err.response?.status === 401) err._overrideMsg = 'Token expired or invalid. Run "skm login" first.';
|
|
55
82
|
printApiError(err, { prefix: 'Push failed' });
|
|
56
83
|
}
|
|
84
|
+
} finally {
|
|
85
|
+
if (tempPackPath) {
|
|
86
|
+
try {
|
|
87
|
+
await fs.remove(path.dirname(tempPackPath));
|
|
88
|
+
} catch (_) {}
|
|
89
|
+
}
|
|
57
90
|
}
|
|
58
91
|
});
|
|
59
92
|
|
package/src/commands/search.js
CHANGED
|
@@ -9,7 +9,9 @@ function formatSkillDisplay(skill) {
|
|
|
9
9
|
const version = skill.version || (skill.versions?.[0]?.version) || '—';
|
|
10
10
|
const downloads = skill.downloads ?? 0;
|
|
11
11
|
const category = skill.category || '—';
|
|
12
|
-
|
|
12
|
+
const desc = (skill.description || '').trim();
|
|
13
|
+
const description = desc.length > 80 ? desc.slice(0, 77) + '...' : desc || '—';
|
|
14
|
+
return { displayName, version, downloads, category, description };
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
const searchCommand = new Command('search');
|
|
@@ -20,8 +22,9 @@ searchCommand
|
|
|
20
22
|
.option('-l, --limit <number>', 'Maximum number of results (default: 20)', '20')
|
|
21
23
|
.option('-p, --page <number>', 'Page number for pagination (default: 1)', '1')
|
|
22
24
|
.option('--api-url <url>', 'API base URL (overrides config for this command)')
|
|
23
|
-
.action(async (query, options) => {
|
|
24
|
-
const
|
|
25
|
+
.action(async (query, options, command) => {
|
|
26
|
+
const apiUrl = command.optsWithGlobals().apiUrl;
|
|
27
|
+
const api = createApiClient(apiUrl);
|
|
25
28
|
const limit = parseInt(options.limit, 10) || 20;
|
|
26
29
|
const page = parseInt(options.page, 10) || 1;
|
|
27
30
|
|
|
@@ -41,8 +44,9 @@ searchCommand
|
|
|
41
44
|
console.log(`\nFound ${pagination.totalSkills ?? skills.length} skill(s) for "${query}":`);
|
|
42
45
|
console.log('─'.repeat(60));
|
|
43
46
|
skills.forEach((skill) => {
|
|
44
|
-
const { displayName, version, downloads, category } = formatSkillDisplay(skill);
|
|
47
|
+
const { displayName, version, downloads, category, description } = formatSkillDisplay(skill);
|
|
45
48
|
console.log(` ${displayName}`);
|
|
49
|
+
console.log(` ${description}`);
|
|
46
50
|
console.log(` Version: ${version} | Downloads: ${downloads} | Category: ${category}`);
|
|
47
51
|
});
|
|
48
52
|
if (pagination.totalPages > 1) {
|
package/src/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { loginCommand } from './commands/login.js';
|
|
|
9
9
|
import { logoutCommand } from './commands/logout.js';
|
|
10
10
|
import { configCommand } from './commands/config.js';
|
|
11
11
|
import { getCommand } from './commands/get.js';
|
|
12
|
+
import { packCommand } from './commands/pack.js';
|
|
12
13
|
import { pushCommand } from './commands/push.js';
|
|
13
14
|
import { publishCommand } from './commands/publish.js';
|
|
14
15
|
import { listCommand } from './commands/list.js';
|
|
@@ -28,13 +29,15 @@ const program = new Command();
|
|
|
28
29
|
program
|
|
29
30
|
.name('skm')
|
|
30
31
|
.description('CLI tool for managing BotSkill - a platform for AI agent skills')
|
|
31
|
-
.version(version)
|
|
32
|
+
.version(version)
|
|
33
|
+
.option('--api-url <url>', 'API base URL (overrides config for this command)');
|
|
32
34
|
|
|
33
35
|
program.addCommand(initCommand);
|
|
34
36
|
program.addCommand(loginCommand);
|
|
35
37
|
program.addCommand(logoutCommand);
|
|
36
38
|
program.addCommand(configCommand);
|
|
37
39
|
program.addCommand(getCommand);
|
|
40
|
+
program.addCommand(packCommand);
|
|
38
41
|
program.addCommand(pushCommand);
|
|
39
42
|
program.addCommand(publishCommand);
|
|
40
43
|
program.addCommand(listCommand);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import AdmZip from 'adm-zip';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_EXCLUDE = [
|
|
7
|
+
'node_modules',
|
|
8
|
+
'.git',
|
|
9
|
+
'.DS_Store',
|
|
10
|
+
'skill.zip',
|
|
11
|
+
'skill.tar.gz',
|
|
12
|
+
'dist.zip',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create filter that excludes common unwanted paths
|
|
17
|
+
*/
|
|
18
|
+
function createExcludeFilter(exclude = DEFAULT_EXCLUDE) {
|
|
19
|
+
const patterns = exclude.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
20
|
+
const re = new RegExp(`(^|/)(${patterns.join('|')})(/|$)`, 'i');
|
|
21
|
+
return (zipPath) => !re.test(zipPath.replace(/\\/g, '/'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get name and version from skill.config.json in directory
|
|
26
|
+
*/
|
|
27
|
+
async function getConfigFromDir(dirPath) {
|
|
28
|
+
const configPath = path.join(dirPath, 'skill.config.json');
|
|
29
|
+
const defaultValue = { name: 'skill', version: '1.0.0' };
|
|
30
|
+
if (!(await fs.pathExists(configPath))) return defaultValue;
|
|
31
|
+
try {
|
|
32
|
+
const config = await fs.readJson(configPath);
|
|
33
|
+
return {
|
|
34
|
+
name: config.name || 'skill',
|
|
35
|
+
version: config.version || '1.0.0',
|
|
36
|
+
};
|
|
37
|
+
} catch {
|
|
38
|
+
return defaultValue;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function toSafeId(str) {
|
|
43
|
+
return String(str || 'skill')
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.replace(/[^a-z0-9.-]/g, '-')
|
|
46
|
+
.replace(/-+/g, '-')
|
|
47
|
+
.replace(/^-|-$/g, '') || 'skill';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Pack a directory into skill.zip format for upload
|
|
52
|
+
* @param {string} dirPath - Directory to pack (must contain SKILL.md)
|
|
53
|
+
* @param {Object} [opts]
|
|
54
|
+
* @param {string} [opts.output] - Output file path (default: skill.zip in dirPath)
|
|
55
|
+
* @param {string} [opts.version] - Version for filename when using tempDir
|
|
56
|
+
* @param {boolean} [opts.useTempDir] - If true, output to temp dir with skill-{version}.zip
|
|
57
|
+
* @param {string[]} [opts.exclude] - Additional paths to exclude
|
|
58
|
+
* @returns {Promise<string>} Path to created zip file
|
|
59
|
+
*/
|
|
60
|
+
export async function packDirectory(dirPath, opts = {}) {
|
|
61
|
+
const resolved = path.resolve(dirPath);
|
|
62
|
+
if (!(await fs.pathExists(resolved))) {
|
|
63
|
+
throw new Error('FILE_NOT_FOUND');
|
|
64
|
+
}
|
|
65
|
+
const stat = await fs.stat(resolved);
|
|
66
|
+
if (!stat.isDirectory()) {
|
|
67
|
+
throw new Error('NOT_DIRECTORY');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const skillMdPath = path.join(resolved, 'SKILL.md');
|
|
71
|
+
if (!(await fs.pathExists(skillMdPath))) {
|
|
72
|
+
throw new Error('NO_SKILL_MD');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let outputPath;
|
|
76
|
+
if (opts.output) {
|
|
77
|
+
outputPath = path.resolve(opts.output);
|
|
78
|
+
} else if (opts.useTempDir) {
|
|
79
|
+
const { name, version } = await getConfigFromDir(resolved);
|
|
80
|
+
const safeName = toSafeId(opts.name ?? name);
|
|
81
|
+
const safeVersion = String(opts.version ?? version).replace(/[^a-zA-Z0-9.-]/g, '-');
|
|
82
|
+
const tmpDir = path.join(os.tmpdir(), `skm-pack-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
83
|
+
await fs.ensureDir(tmpDir);
|
|
84
|
+
outputPath = path.join(tmpDir, `${safeName}-${safeVersion}.zip`);
|
|
85
|
+
} else {
|
|
86
|
+
outputPath = path.join(resolved, 'skill.zip');
|
|
87
|
+
}
|
|
88
|
+
const excludeFilter = createExcludeFilter(opts.exclude);
|
|
89
|
+
|
|
90
|
+
const zip = new AdmZip();
|
|
91
|
+
zip.addLocalFolder(resolved, '', (p) => excludeFilter(p.replace(/\\/g, '/')));
|
|
92
|
+
zip.writeZip(outputPath);
|
|
93
|
+
|
|
94
|
+
return outputPath;
|
|
95
|
+
}
|
package/src/lib/uploadSkill.js
CHANGED
|
@@ -24,11 +24,50 @@ export async function findUploadFile(cwd = process.cwd()) {
|
|
|
24
24
|
return null;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Load skill.config.json from directory (same dir as file or parent)
|
|
29
|
+
*/
|
|
30
|
+
export async function loadSkillConfig(filePath) {
|
|
31
|
+
const dir = path.dirname(path.resolve(filePath));
|
|
32
|
+
const configPath = path.join(dir, 'skill.config.json');
|
|
33
|
+
if (!(await fs.pathExists(configPath))) return null;
|
|
34
|
+
try {
|
|
35
|
+
return await fs.readJson(configPath);
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Append skill.config.json fields to FormData (version, category, license, tags, urls)
|
|
43
|
+
*/
|
|
44
|
+
function appendConfigToForm(form, config) {
|
|
45
|
+
if (!config) return;
|
|
46
|
+
const fields = [
|
|
47
|
+
'version',
|
|
48
|
+
'category',
|
|
49
|
+
'license',
|
|
50
|
+
'repositoryUrl',
|
|
51
|
+
'documentationUrl',
|
|
52
|
+
'demoUrl',
|
|
53
|
+
];
|
|
54
|
+
for (const key of fields) {
|
|
55
|
+
const val = config[key];
|
|
56
|
+
if (val !== undefined && val !== null && val !== '') {
|
|
57
|
+
form.append(key, Array.isArray(val) ? JSON.stringify(val) : String(val));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(config.tags) && config.tags.length > 0) {
|
|
61
|
+
form.append('tags', JSON.stringify(config.tags));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
27
65
|
/**
|
|
28
66
|
* Upload skill file (SKILL.md, .zip, .tar.gz) to BotSkill
|
|
29
67
|
* @param {string} filePath - Path to file
|
|
30
68
|
* @param {Object} [opts] - 可选
|
|
31
69
|
* @param {string} [opts.apiUrl] - 覆盖 API 地址(来自 --api-url)
|
|
70
|
+
* @param {string} [opts.configDir] - 指定读取 skill.config.json 的目录(默认与文件同目录)
|
|
32
71
|
*/
|
|
33
72
|
export async function uploadSkillFile(filePath, opts = {}) {
|
|
34
73
|
const token = getToken();
|
|
@@ -46,6 +85,15 @@ export async function uploadSkillFile(filePath, opts = {}) {
|
|
|
46
85
|
filename: path.basename(filePath),
|
|
47
86
|
});
|
|
48
87
|
|
|
88
|
+
const configDir = opts.configDir ?? path.dirname(path.resolve(filePath));
|
|
89
|
+
const configPath = path.join(configDir, 'skill.config.json');
|
|
90
|
+
if (await fs.pathExists(configPath)) {
|
|
91
|
+
try {
|
|
92
|
+
const config = await fs.readJson(configPath);
|
|
93
|
+
appendConfigToForm(form, config);
|
|
94
|
+
} catch (_) {}
|
|
95
|
+
}
|
|
96
|
+
|
|
49
97
|
const api = createApiClient(opts.apiUrl);
|
|
50
98
|
const res = await api.post('/skills/upload', form, {
|
|
51
99
|
headers: form.getHeaders(),
|