@botskill/cli 1.0.5 → 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/pack.js +53 -0
- package/src/commands/publish.js +35 -4
- package/src/commands/push.js +37 -5
- package/src/index.js +2 -0
- package/src/lib/packSkill.js +95 -0
- package/src/lib/uploadSkill.js +48 -0
package/package.json
CHANGED
|
@@ -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,11 +4,13 @@ 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
16
|
.action(async (options, command) => {
|
|
@@ -32,14 +34,37 @@ publishCommand
|
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
|
|
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) {
|
|
36
61
|
console.log('[DRY RUN] Would publish:', path.resolve(filePath));
|
|
37
62
|
return;
|
|
38
63
|
}
|
|
39
64
|
|
|
40
|
-
console.log(`Publishing skill from ${path.basename(
|
|
65
|
+
console.log(`Publishing skill from ${path.basename(uploadPath)}...`);
|
|
41
66
|
try {
|
|
42
|
-
const skill = await uploadSkillFile(
|
|
67
|
+
const skill = await uploadSkillFile(uploadPath, { apiUrl, configDir });
|
|
43
68
|
console.log('Skill published successfully!');
|
|
44
69
|
console.log(`Name: ${skill?.name}`);
|
|
45
70
|
console.log(`Version: ${skill?.version || (skill?.versions?.[0]?.version)}`);
|
|
@@ -48,13 +73,19 @@ publishCommand
|
|
|
48
73
|
if (err.message === 'NOT_LOGGED_IN') {
|
|
49
74
|
printSimpleError('Not logged in', 'Run "skm login" first');
|
|
50
75
|
} else if (err.message === 'FILE_NOT_FOUND') {
|
|
51
|
-
printSimpleError('File not found',
|
|
76
|
+
printSimpleError('File not found', uploadPath);
|
|
52
77
|
} else {
|
|
53
78
|
const msg = err.response?.data?.error || err.response?.data?.details?.[0];
|
|
54
79
|
if (msg) err._overrideMsg = msg;
|
|
55
80
|
if (err.response?.status === 401) err._overrideMsg = 'Token expired or invalid. Run "skm login" first.';
|
|
56
81
|
printApiError(err, { prefix: 'Publish failed' });
|
|
57
82
|
}
|
|
83
|
+
} finally {
|
|
84
|
+
if (tempPackPath) {
|
|
85
|
+
try {
|
|
86
|
+
await fs.remove(path.dirname(tempPackPath));
|
|
87
|
+
} catch (_) {}
|
|
88
|
+
}
|
|
58
89
|
}
|
|
59
90
|
});
|
|
60
91
|
|
package/src/commands/push.js
CHANGED
|
@@ -3,12 +3,14 @@ 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
16
|
.action(async (options, command) => {
|
|
@@ -32,14 +34,38 @@ pushCommand
|
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
|
|
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) {
|
|
36
62
|
console.log('[DRY RUN] Would upload:', path.resolve(filePath));
|
|
37
63
|
return;
|
|
38
64
|
}
|
|
39
65
|
|
|
40
|
-
console.log(`Pushing skill from ${path.basename(
|
|
66
|
+
console.log(`Pushing skill from ${path.basename(uploadPath)}...`);
|
|
41
67
|
try {
|
|
42
|
-
const skill = await uploadSkillFile(
|
|
68
|
+
const skill = await uploadSkillFile(uploadPath, { apiUrl, configDir });
|
|
43
69
|
console.log('Skill uploaded successfully!');
|
|
44
70
|
console.log(`Name: ${skill?.name}`);
|
|
45
71
|
console.log(`Version: ${skill?.version || (skill?.versions?.[0]?.version)}`);
|
|
@@ -48,13 +74,19 @@ pushCommand
|
|
|
48
74
|
if (err.message === 'NOT_LOGGED_IN') {
|
|
49
75
|
printSimpleError('Not logged in', 'Run "skm login" first');
|
|
50
76
|
} else if (err.message === 'FILE_NOT_FOUND') {
|
|
51
|
-
printSimpleError('File not found',
|
|
77
|
+
printSimpleError('File not found', uploadPath);
|
|
52
78
|
} else {
|
|
53
79
|
const msg = err.response?.data?.error || err.response?.data?.details?.[0];
|
|
54
80
|
if (msg) err._overrideMsg = msg;
|
|
55
81
|
if (err.response?.status === 401) err._overrideMsg = 'Token expired or invalid. Run "skm login" first.';
|
|
56
82
|
printApiError(err, { prefix: 'Push failed' });
|
|
57
83
|
}
|
|
84
|
+
} finally {
|
|
85
|
+
if (tempPackPath) {
|
|
86
|
+
try {
|
|
87
|
+
await fs.remove(path.dirname(tempPackPath));
|
|
88
|
+
} catch (_) {}
|
|
89
|
+
}
|
|
58
90
|
}
|
|
59
91
|
});
|
|
60
92
|
|
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';
|
|
@@ -36,6 +37,7 @@ program.addCommand(loginCommand);
|
|
|
36
37
|
program.addCommand(logoutCommand);
|
|
37
38
|
program.addCommand(configCommand);
|
|
38
39
|
program.addCommand(getCommand);
|
|
40
|
+
program.addCommand(packCommand);
|
|
39
41
|
program.addCommand(pushCommand);
|
|
40
42
|
program.addCommand(publishCommand);
|
|
41
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(),
|