istarshine 1.0.0
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 +39 -0
- package/bin/cli.js +48 -0
- package/lib/api.js +71 -0
- package/lib/install.js +65 -0
- package/lib/list.js +46 -0
- package/lib/search.js +46 -0
- package/lib/zip.js +92 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# istarshine
|
|
2
|
+
|
|
3
|
+
iStarshine Skills Hub CLI - 一键安装和管理 Agent Skills。
|
|
4
|
+
|
|
5
|
+
## 用法
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# 安装 skill 到当前项目
|
|
9
|
+
npx istarshine install <skill-name>
|
|
10
|
+
|
|
11
|
+
# 搜索 skill
|
|
12
|
+
npx istarshine search <keyword>
|
|
13
|
+
|
|
14
|
+
# 查看热门 skills
|
|
15
|
+
npx istarshine list
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 选项
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# 指定安装目录(默认 .kiro/skills)
|
|
22
|
+
npx istarshine install my-skill --dir ./custom/path
|
|
23
|
+
|
|
24
|
+
# 指定服务器地址
|
|
25
|
+
npx istarshine install my-skill --server https://your-server.com
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 示例
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# 安装一个搜索相关的 skill
|
|
32
|
+
npx istarshine install web-search
|
|
33
|
+
|
|
34
|
+
# 搜索所有包含 "search" 的 skill
|
|
35
|
+
npx istarshine search search
|
|
36
|
+
|
|
37
|
+
# 列出热门 skills
|
|
38
|
+
npx istarshine list
|
|
39
|
+
```
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* istarshine CLI 入口文件
|
|
5
|
+
* 用法: npx istarshine install <skill-name>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import { install_skill } from '../lib/install.js';
|
|
10
|
+
import { search_skills } from '../lib/search.js';
|
|
11
|
+
import { list_skills } from '../lib/list.js';
|
|
12
|
+
|
|
13
|
+
const program = new Command();
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.name('istarshine')
|
|
17
|
+
.description('iStarshine Skills Hub CLI - 一键安装和管理 Skills')
|
|
18
|
+
.version('1.0.0');
|
|
19
|
+
|
|
20
|
+
// install 子命令
|
|
21
|
+
program
|
|
22
|
+
.command('install <skill-name>')
|
|
23
|
+
.description('安装指定名称的 Skill 到当前项目')
|
|
24
|
+
.option('-d, --dir <path>', '安装目标目录', '.kiro/skills')
|
|
25
|
+
.option('--server <url>', 'Skills Hub 服务器地址', 'https://xgzn.istarshine.com')
|
|
26
|
+
.action(async (skill_name, options) => {
|
|
27
|
+
await install_skill(skill_name, options);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// search 子命令
|
|
31
|
+
program
|
|
32
|
+
.command('search <keyword>')
|
|
33
|
+
.description('搜索 Skills Hub 中的 Skill')
|
|
34
|
+
.option('--server <url>', 'Skills Hub 服务器地址', 'https://xgzn.istarshine.com')
|
|
35
|
+
.action(async (keyword, options) => {
|
|
36
|
+
await search_skills(keyword, options);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// list 子命令
|
|
40
|
+
program
|
|
41
|
+
.command('list')
|
|
42
|
+
.description('列出热门 Skills')
|
|
43
|
+
.option('--server <url>', 'Skills Hub 服务器地址', 'https://xgzn.istarshine.com')
|
|
44
|
+
.action(async (options) => {
|
|
45
|
+
await list_skills(options);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
program.parse();
|
package/lib/api.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API 请求封装模块
|
|
3
|
+
* 负责与 Skills Hub 后端通信
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { request } from 'https';
|
|
7
|
+
import { request as http_request } from 'http';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 发送 GET 请求,返回 JSON
|
|
11
|
+
* @param {string} url - 完整请求 URL
|
|
12
|
+
* @returns {Promise<object>} 解析后的 JSON 对象
|
|
13
|
+
*/
|
|
14
|
+
export function fetch_json(url) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const is_https = url.startsWith('https');
|
|
17
|
+
const do_request = is_https ? request : http_request;
|
|
18
|
+
|
|
19
|
+
do_request(url, (res) => {
|
|
20
|
+
// 处理重定向
|
|
21
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
22
|
+
return fetch_json(res.headers.location).then(resolve).catch(reject);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (res.statusCode !== 200) {
|
|
26
|
+
reject(new Error(`请求失败,状态码: ${res.statusCode}`));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let data = '';
|
|
31
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
32
|
+
res.on('end', () => {
|
|
33
|
+
try {
|
|
34
|
+
resolve(JSON.parse(data));
|
|
35
|
+
} catch (e) {
|
|
36
|
+
reject(new Error('响应解析失败: ' + e.message));
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}).on('error', reject).end();
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 下载二进制文件,返回 Buffer
|
|
45
|
+
* @param {string} url - 完整请求 URL
|
|
46
|
+
* @returns {Promise<Buffer>} 文件二进制数据
|
|
47
|
+
*/
|
|
48
|
+
export function download_file(url) {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const is_https = url.startsWith('https');
|
|
51
|
+
const do_request = is_https ? request : http_request;
|
|
52
|
+
|
|
53
|
+
do_request(url, (res) => {
|
|
54
|
+
// 处理重定向
|
|
55
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
56
|
+
return download_file(res.headers.location).then(resolve).catch(reject);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (res.statusCode !== 200) {
|
|
60
|
+
reject(new Error(`下载失败,状态码: ${res.statusCode}`));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const chunks = [];
|
|
65
|
+
res.on('data', (chunk) => { chunks.push(chunk); });
|
|
66
|
+
res.on('end', () => {
|
|
67
|
+
resolve(Buffer.concat(chunks));
|
|
68
|
+
});
|
|
69
|
+
}).on('error', reject).end();
|
|
70
|
+
});
|
|
71
|
+
}
|
package/lib/install.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* install 命令实现
|
|
3
|
+
* 根据 skill 名称搜索、下载 zip 并解压到本地目录
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { fetch_json, download_file } from './api.js';
|
|
7
|
+
import { createWriteStream, mkdirSync, existsSync } from 'fs';
|
|
8
|
+
import { join, resolve } from 'path';
|
|
9
|
+
import { extract_zip } from './zip.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 安装指定名称的 skill
|
|
13
|
+
* 流程: 按名称搜索 -> 找到精确匹配 -> 下载 zip -> 解压到目标目录
|
|
14
|
+
* @param {string} skill_name - skill 名称
|
|
15
|
+
* @param {object} options - 命令行选项 { dir, server }
|
|
16
|
+
*/
|
|
17
|
+
export async function install_skill(skill_name, options) {
|
|
18
|
+
const { dir, server } = options;
|
|
19
|
+
// 动态导入 chalk 和 ora(ESM 模块)
|
|
20
|
+
const chalk = (await import('chalk')).default;
|
|
21
|
+
const ora = (await import('ora')).default;
|
|
22
|
+
|
|
23
|
+
const spinner = ora(`正在搜索 Skill: ${skill_name}`).start();
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// 1. 按名称搜索 skill
|
|
27
|
+
const search_url = `${server}/api/skills/search?keyword=${encodeURIComponent(skill_name)}`;
|
|
28
|
+
const results = await fetch_json(search_url);
|
|
29
|
+
|
|
30
|
+
// 精确匹配名称
|
|
31
|
+
const skill = results.find(s => s.name === skill_name);
|
|
32
|
+
if (!skill) {
|
|
33
|
+
spinner.fail(chalk.red(`未找到名为 "${skill_name}" 的 Skill`));
|
|
34
|
+
if (results.length > 0) {
|
|
35
|
+
console.log(chalk.yellow('\n你是不是想找:'));
|
|
36
|
+
results.slice(0, 5).forEach(s => {
|
|
37
|
+
console.log(` ${chalk.cyan(s.name)} ${s.display_name || ''}`);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 2. 下载 zip 压缩包
|
|
44
|
+
spinner.text = `正在下载 ${skill_name}@${skill.version || 'latest'}...`;
|
|
45
|
+
const download_url = `${server}/api/skills/${skill.id}/download`;
|
|
46
|
+
const zip_buffer = await download_file(download_url);
|
|
47
|
+
|
|
48
|
+
// 3. 解压到目标目录
|
|
49
|
+
const target_dir = resolve(process.cwd(), dir, skill_name);
|
|
50
|
+
spinner.text = `正在解压到 ${target_dir}...`;
|
|
51
|
+
|
|
52
|
+
if (!existsSync(target_dir)) {
|
|
53
|
+
mkdirSync(target_dir, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await extract_zip(zip_buffer, target_dir);
|
|
57
|
+
|
|
58
|
+
spinner.succeed(chalk.green(`${skill_name}@${skill.version || 'latest'} 安装成功`));
|
|
59
|
+
console.log(chalk.gray(` 安装路径: ${target_dir}`));
|
|
60
|
+
|
|
61
|
+
} catch (err) {
|
|
62
|
+
spinner.fail(chalk.red(`安装失败: ${err.message}`));
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
package/lib/list.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* list 命令实现
|
|
3
|
+
* 列出 Skills Hub 中的热门 Skills
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { fetch_json } from './api.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 列出热门 skills
|
|
10
|
+
* @param {object} options - 命令行选项 { server }
|
|
11
|
+
*/
|
|
12
|
+
export async function list_skills(options) {
|
|
13
|
+
const { server } = options;
|
|
14
|
+
const chalk = (await import('chalk')).default;
|
|
15
|
+
const ora = (await import('ora')).default;
|
|
16
|
+
|
|
17
|
+
const spinner = ora('正在获取热门 Skills...').start();
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const url = `${server}/api/skills/popular`;
|
|
21
|
+
const results = await fetch_json(url);
|
|
22
|
+
|
|
23
|
+
if (!results || results.length === 0) {
|
|
24
|
+
spinner.info(chalk.yellow('暂无热门 Skill'));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
spinner.succeed(chalk.green(`热门 Skills:\n`));
|
|
29
|
+
|
|
30
|
+
results.forEach((skill, index) => {
|
|
31
|
+
const rank = chalk.gray(`${index + 1}.`);
|
|
32
|
+
const name = chalk.cyan(skill.name);
|
|
33
|
+
const version = chalk.gray(`v${skill.version || '?'}`);
|
|
34
|
+
const downloads = chalk.gray(`↓${skill.downloads || 0}`);
|
|
35
|
+
const likes = chalk.gray(`♥${skill.likes || 0}`);
|
|
36
|
+
const display = skill.display_name ? chalk.white(` - ${skill.display_name}`) : '';
|
|
37
|
+
console.log(` ${rank} ${name} ${version} ${downloads} ${likes}${display}`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
console.log(chalk.gray(`\n安装命令: npx istarshine install <skill-name>`));
|
|
41
|
+
|
|
42
|
+
} catch (err) {
|
|
43
|
+
spinner.fail(chalk.red(`获取列表失败: ${err.message}`));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
}
|
package/lib/search.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* search 命令实现
|
|
3
|
+
* 搜索 Skills Hub 中的 Skill 并展示结果
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { fetch_json } from './api.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 搜索并展示 skill 列表
|
|
10
|
+
* @param {string} keyword - 搜索关键词
|
|
11
|
+
* @param {object} options - 命令行选项 { server }
|
|
12
|
+
*/
|
|
13
|
+
export async function search_skills(keyword, options) {
|
|
14
|
+
const { server } = options;
|
|
15
|
+
const chalk = (await import('chalk')).default;
|
|
16
|
+
const ora = (await import('ora')).default;
|
|
17
|
+
|
|
18
|
+
const spinner = ora(`正在搜索: ${keyword}`).start();
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const url = `${server}/api/skills/search?keyword=${encodeURIComponent(keyword)}`;
|
|
22
|
+
const results = await fetch_json(url);
|
|
23
|
+
|
|
24
|
+
if (!results || results.length === 0) {
|
|
25
|
+
spinner.info(chalk.yellow(`未找到与 "${keyword}" 相关的 Skill`));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
spinner.succeed(chalk.green(`找到 ${results.length} 个结果:\n`));
|
|
30
|
+
|
|
31
|
+
results.forEach(skill => {
|
|
32
|
+
const name = chalk.cyan(skill.name);
|
|
33
|
+
const version = chalk.gray(`v${skill.version || '?'}`);
|
|
34
|
+
const downloads = chalk.gray(`↓${skill.downloads || 0}`);
|
|
35
|
+
const likes = chalk.gray(`♥${skill.likes || 0}`);
|
|
36
|
+
const display = skill.display_name ? chalk.white(` - ${skill.display_name}`) : '';
|
|
37
|
+
console.log(` ${name} ${version} ${downloads} ${likes}${display}`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
console.log(chalk.gray(`\n安装命令: npx istarshine install <skill-name>`));
|
|
41
|
+
|
|
42
|
+
} catch (err) {
|
|
43
|
+
spinner.fail(chalk.red(`搜索失败: ${err.message}`));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
}
|
package/lib/zip.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zip 解压模块
|
|
3
|
+
* 使用 Node.js 内置 zlib 实现,无需额外依赖
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
7
|
+
import { join, dirname } from 'path';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 解析 zip Buffer,返回所有文件条目
|
|
11
|
+
* @param {Buffer} zip_buffer - zip 文件的二进制数据
|
|
12
|
+
* @returns {Array<{name: string, data: Buffer}>} 文件条目列表
|
|
13
|
+
*/
|
|
14
|
+
async function parse_zip_entries(zip_buffer) {
|
|
15
|
+
const { inflateRawSync } = await import('node:zlib');
|
|
16
|
+
const entries = [];
|
|
17
|
+
let offset = 0;
|
|
18
|
+
|
|
19
|
+
while (offset < zip_buffer.length) {
|
|
20
|
+
const signature = zip_buffer.readUInt32LE(offset);
|
|
21
|
+
// local file header 签名: 0x04034b50
|
|
22
|
+
if (signature !== 0x04034b50) break;
|
|
23
|
+
|
|
24
|
+
const compression_method = zip_buffer.readUInt16LE(offset + 8);
|
|
25
|
+
const compressed_size = zip_buffer.readUInt32LE(offset + 18);
|
|
26
|
+
const name_length = zip_buffer.readUInt16LE(offset + 26);
|
|
27
|
+
const extra_length = zip_buffer.readUInt16LE(offset + 28);
|
|
28
|
+
|
|
29
|
+
const name_start = offset + 30;
|
|
30
|
+
const file_name = zip_buffer.subarray(name_start, name_start + name_length).toString('utf8');
|
|
31
|
+
const data_start = name_start + name_length + extra_length;
|
|
32
|
+
const raw_data = zip_buffer.subarray(data_start, data_start + compressed_size);
|
|
33
|
+
|
|
34
|
+
// 跳过目录条目
|
|
35
|
+
if (!file_name.endsWith('/')) {
|
|
36
|
+
let content;
|
|
37
|
+
if (compression_method === 0) {
|
|
38
|
+
content = raw_data;
|
|
39
|
+
} else if (compression_method === 8) {
|
|
40
|
+
content = inflateRawSync(raw_data);
|
|
41
|
+
} else {
|
|
42
|
+
throw new Error(`不支持的压缩方式: ${compression_method},文件: ${file_name}`);
|
|
43
|
+
}
|
|
44
|
+
entries.push({ name: file_name, data: content });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
offset = data_start + compressed_size;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return entries;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 检测并去除 zip 内的公共顶层目录前缀
|
|
55
|
+
* 例如所有文件都在 "auto-ops/" 下,则去掉这个前缀
|
|
56
|
+
* @param {Array<{name: string, data: Buffer}>} entries - 文件条目列表
|
|
57
|
+
* @returns {Array<{name: string, data: Buffer}>} 处理后的条目列表
|
|
58
|
+
*/
|
|
59
|
+
function strip_common_prefix(entries) {
|
|
60
|
+
if (entries.length === 0) return entries;
|
|
61
|
+
|
|
62
|
+
// 找第一个路径段作为候选前缀
|
|
63
|
+
const first_part = entries[0].name.split('/')[0] + '/';
|
|
64
|
+
|
|
65
|
+
// 检查是否所有文件都在这个前缀下
|
|
66
|
+
const all_match = entries.every(e => e.name.startsWith(first_part));
|
|
67
|
+
if (!all_match) return entries;
|
|
68
|
+
|
|
69
|
+
// 去掉公共前缀
|
|
70
|
+
return entries.map(e => ({
|
|
71
|
+
name: e.name.slice(first_part.length),
|
|
72
|
+
data: e.data,
|
|
73
|
+
})).filter(e => e.name.length > 0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 解压 zip Buffer 到指定目录
|
|
78
|
+
* 自动去除 zip 内的公共顶层目录前缀
|
|
79
|
+
* @param {Buffer} zip_buffer - zip 文件的二进制数据
|
|
80
|
+
* @param {string} target_dir - 解压目标目录
|
|
81
|
+
*/
|
|
82
|
+
export async function extract_zip(zip_buffer, target_dir) {
|
|
83
|
+
let entries = await parse_zip_entries(zip_buffer);
|
|
84
|
+
entries = strip_common_prefix(entries);
|
|
85
|
+
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
const file_path = join(target_dir, entry.name);
|
|
88
|
+
const file_dir = dirname(file_path);
|
|
89
|
+
mkdirSync(file_dir, { recursive: true });
|
|
90
|
+
writeFileSync(file_path, entry.data);
|
|
91
|
+
}
|
|
92
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "istarshine",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "iStarshine Skills Hub CLI - 一键安装和管理 Skills",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"istarshine": "./bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"istarshine",
|
|
12
|
+
"skills",
|
|
13
|
+
"cli",
|
|
14
|
+
"agent"
|
|
15
|
+
],
|
|
16
|
+
"author": "istarshine",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"commander": "^12.1.0",
|
|
20
|
+
"chalk": "^5.3.0",
|
|
21
|
+
"ora": "^8.1.0"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"bin/",
|
|
28
|
+
"lib/",
|
|
29
|
+
"README.md"
|
|
30
|
+
]
|
|
31
|
+
}
|