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 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
+ }