istarshine 1.0.2 → 1.1.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/bin/cli.js CHANGED
@@ -10,6 +10,7 @@ import { install_skill } from '../lib/install.js';
10
10
  import { search_skills } from '../lib/search.js';
11
11
  import { list_skills } from '../lib/list.js';
12
12
  import { find_skills } from '../lib/find.js';
13
+ import { config_action } from '../lib/config.js';
13
14
 
14
15
  const program = new Command();
15
16
 
@@ -22,8 +23,9 @@ program
22
23
  program
23
24
  .command('install <skill-name>')
24
25
  .description('安装指定名称的 Skill 到当前项目')
25
- .option('-d, --dir <path>', '安装目标目录', '.kiro/skills')
26
+ .option('-d, --dir <path>', '安装目标目录', './skills')
26
27
  .option('--server <url>', 'Skills Hub 服务器地址', 'https://skills.istarshine.com')
28
+ .option('--api-key <value>', '指定 API Key(覆盖环境变量和配置文件)')
27
29
  .action(async (skill_name, options) => {
28
30
  await install_skill(skill_name, options);
29
31
  });
@@ -56,4 +58,15 @@ program
56
58
  await find_skills(keyword, options);
57
59
  });
58
60
 
61
+ // config 子命令
62
+ program
63
+ .command('config')
64
+ .description('管理 CLI 配置(API Key 等)')
65
+ .option('--api-key <value>', '设置 API Key')
66
+ .option('--show', '查看当前配置')
67
+ .option('--clear', '清除配置文件')
68
+ .action(async (options) => {
69
+ await config_action(options);
70
+ });
71
+
59
72
  program.parse();
package/lib/api.js CHANGED
@@ -9,17 +9,27 @@ import { request as http_request } from 'http';
9
9
  /**
10
10
  * 发送 GET 请求,返回 JSON
11
11
  * @param {string} url - 完整请求 URL
12
+ * @param {object} headers - 可选的自定义请求头
12
13
  * @returns {Promise<object>} 解析后的 JSON 对象
13
14
  */
14
- export function fetch_json(url) {
15
+ export function fetch_json(url, headers = {}) {
15
16
  return new Promise((resolve, reject) => {
16
- const is_https = url.startsWith('https');
17
+ const parsed = new URL(url);
18
+ const is_https = parsed.protocol === 'https:';
17
19
  const do_request = is_https ? request : http_request;
18
20
 
19
- do_request(url, (res) => {
21
+ const options = {
22
+ hostname: parsed.hostname,
23
+ port: parsed.port || (is_https ? 443 : 80),
24
+ path: parsed.pathname + parsed.search,
25
+ method: 'GET',
26
+ headers: { ...headers },
27
+ };
28
+
29
+ do_request(options, (res) => {
20
30
  // 处理重定向
21
31
  if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
22
- return fetch_json(res.headers.location).then(resolve).catch(reject);
32
+ return fetch_json(res.headers.location, headers).then(resolve).catch(reject);
23
33
  }
24
34
 
25
35
  if (res.statusCode !== 200) {
@@ -41,19 +51,29 @@ export function fetch_json(url) {
41
51
  }
42
52
 
43
53
  /**
44
- * 下载二进制文件,返回 Buffer
54
+ * 下载二进制文件,返回带 headers 属性的 Buffer
45
55
  * @param {string} url - 完整请求 URL
46
- * @returns {Promise<Buffer>} 文件二进制数据
56
+ * @param {object} headers - 可选的自定义请求头
57
+ * @returns {Promise<Buffer>} 文件二进制数据,附带 .headers 属性(响应头)
47
58
  */
48
- export function download_file(url) {
59
+ export function download_file(url, headers = {}) {
49
60
  return new Promise((resolve, reject) => {
50
- const is_https = url.startsWith('https');
61
+ const parsed = new URL(url);
62
+ const is_https = parsed.protocol === 'https:';
51
63
  const do_request = is_https ? request : http_request;
52
64
 
53
- do_request(url, (res) => {
65
+ const options = {
66
+ hostname: parsed.hostname,
67
+ port: parsed.port || (is_https ? 443 : 80),
68
+ path: parsed.pathname + parsed.search,
69
+ method: 'GET',
70
+ headers: { ...headers },
71
+ };
72
+
73
+ do_request(options, (res) => {
54
74
  // 处理重定向
55
75
  if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
56
- return download_file(res.headers.location).then(resolve).catch(reject);
76
+ return download_file(res.headers.location, headers).then(resolve).catch(reject);
57
77
  }
58
78
 
59
79
  if (res.statusCode !== 200) {
@@ -64,7 +84,10 @@ export function download_file(url) {
64
84
  const chunks = [];
65
85
  res.on('data', (chunk) => { chunks.push(chunk); });
66
86
  res.on('end', () => {
67
- resolve(Buffer.concat(chunks));
87
+ // 返回 Buffer 并附加响应头属性,便于调用方读取 X-Auth-Warning 等
88
+ const buffer = Buffer.concat(chunks);
89
+ Object.assign(buffer, { headers: res.headers });
90
+ resolve(buffer);
68
91
  });
69
92
  }).on('error', reject).end();
70
93
  });
package/lib/config.js ADDED
@@ -0,0 +1,172 @@
1
+ /**
2
+ * 配置管理模块
3
+ * 负责 Config_File 的读写和 API Key 多来源优先级解析
4
+ */
5
+
6
+ import { readFileSync, writeFileSync, mkdirSync, unlinkSync, existsSync, chmodSync, statSync } from 'fs';
7
+ import { join } from 'path';
8
+ import { homedir } from 'os';
9
+
10
+ // 配置目录和文件路径
11
+ const CONFIG_DIR = join(homedir(), '.istarshine');
12
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
13
+
14
+ /**
15
+ * 读取配置文件,返回解析后的对象
16
+ * 文件不存在或 JSON 无效时返回 null
17
+ * @returns {object|null}
18
+ */
19
+ export function read_config() {
20
+ try {
21
+ if (!existsSync(CONFIG_FILE)) return null;
22
+ const content = readFileSync(CONFIG_FILE, 'utf8');
23
+ return JSON.parse(content);
24
+ } catch {
25
+ // JSON 解析失败或读取失败,返回 null
26
+ return null;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * 写入配置文件(合并模式,保留已有字段)
32
+ * 自动创建 CONFIG_DIR,文件权限设为 0o600
33
+ * @param {object} updates - 要更新的字段
34
+ */
35
+ export function write_config(updates) {
36
+ // 确保目录存在
37
+ if (!existsSync(CONFIG_DIR)) {
38
+ mkdirSync(CONFIG_DIR, { recursive: true });
39
+ }
40
+ // 读取已有配置并合并
41
+ const existing = read_config() || {};
42
+ const merged = { ...existing, ...updates };
43
+ // 写入文件,2 空格缩进
44
+ writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), 'utf8');
45
+ // 设置文件权限为仅当前用户可读写
46
+ chmodSync(CONFIG_FILE, 0o600);
47
+ }
48
+
49
+ /**
50
+ * 删除配置文件
51
+ * @returns {boolean} 是否成功删除(文件不存在返回 false)
52
+ */
53
+ export function clear_config() {
54
+ try {
55
+ if (!existsSync(CONFIG_FILE)) return false;
56
+ unlinkSync(CONFIG_FILE);
57
+ return true;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * 按优先级解析 API Key
65
+ * 1. cli_key(命令行 --api-key 参数)
66
+ * 2. 环境变量 ISTARSHINE_API_KEY
67
+ * 3. Config_File 中的 api_key 字段
68
+ * @param {string|undefined} cli_key - 命令行传入的 key
69
+ * @returns {string|null}
70
+ */
71
+ export function resolve_api_key(cli_key) {
72
+ // 优先级 1:命令行参数
73
+ if (cli_key) return cli_key;
74
+ // 优先级 2:环境变量
75
+ const env_key = process.env.ISTARSHINE_API_KEY;
76
+ if (env_key) return env_key;
77
+ // 优先级 3:配置文件
78
+ const cfg = read_config();
79
+ if (cfg && cfg.api_key) return cfg.api_key;
80
+ // 所有来源均无 Key
81
+ return null;
82
+ }
83
+
84
+ /**
85
+ * 将 API Key 脱敏为前缀形式
86
+ * 如 "sk-acme-a1b2c3d4..." → "sk-acme-a1b2..."
87
+ * @param {string} key
88
+ * @returns {string}
89
+ */
90
+ export function mask_api_key(key) {
91
+ if (!key) return '';
92
+ // 格式: sk-{prefix}-{32hex}
93
+ // 找到最后一个 '-' 的位置,取前4位hex + '...'
94
+ const last_dash = key.lastIndexOf('-');
95
+ if (last_dash === -1) {
96
+ // 非标准格式,只显示前8字符
97
+ return key.slice(0, 8) + '...';
98
+ }
99
+ const before_hex = key.slice(0, last_dash + 1);
100
+ const hex_part = key.slice(last_dash + 1);
101
+ return before_hex + hex_part.slice(0, 4) + '...';
102
+ }
103
+
104
+ // 导出路径常量,供测试使用
105
+ export { CONFIG_DIR, CONFIG_FILE };
106
+
107
+ /**
108
+ * config 子命令处理函数
109
+ * 支持 --api-key、--show、--clear 三个选项
110
+ * @param {object} options - 命令行选项
111
+ */
112
+ export async function config_action(options) {
113
+ const chalk = (await import('chalk')).default;
114
+
115
+ // --clear:清除配置文件
116
+ if (options.clear) {
117
+ const removed = clear_config();
118
+ if (removed) {
119
+ console.log(chalk.green('✔ 配置文件已清除'));
120
+ } else {
121
+ console.log(chalk.yellow('配置文件不存在,无需清除'));
122
+ }
123
+ return;
124
+ }
125
+
126
+ // --api-key:写入 API Key
127
+ if (options.apiKey) {
128
+ try {
129
+ write_config({ api_key: options.apiKey });
130
+ const masked = mask_api_key(options.apiKey);
131
+ console.log(chalk.green(`✔ API Key 已保存: ${masked}`));
132
+ } catch (err) {
133
+ console.log(chalk.red(`✖ 写入配置失败: ${err.message}`));
134
+ process.exit(1);
135
+ }
136
+ return;
137
+ }
138
+
139
+ // --show:展示当前配置
140
+ if (options.show) {
141
+ const cfg = read_config();
142
+ const env_key = process.env.ISTARSHINE_API_KEY;
143
+
144
+ if (!cfg && !env_key) {
145
+ console.log(chalk.yellow('尚未配置。使用 istarshine config --api-key <value> 设置 API Key'));
146
+ return;
147
+ }
148
+
149
+ console.log(chalk.cyan('当前配置:'));
150
+ if (cfg && cfg.api_key) {
151
+ console.log(` API Key (配置文件): ${mask_api_key(cfg.api_key)}`);
152
+ }
153
+ if (env_key) {
154
+ console.log(` API Key (环境变量 ISTARSHINE_API_KEY): ${mask_api_key(env_key)}`);
155
+ }
156
+ if (cfg) {
157
+ // 展示其他非 api_key 字段
158
+ for (const [k, v] of Object.entries(cfg)) {
159
+ if (k !== 'api_key') {
160
+ console.log(` ${k}: ${v}`);
161
+ }
162
+ }
163
+ }
164
+ return;
165
+ }
166
+
167
+ // 无选项时显示帮助
168
+ console.log(chalk.yellow('请指定选项。用法:'));
169
+ console.log(' istarshine config --api-key <value> 设置 API Key');
170
+ console.log(' istarshine config --show 查看当前配置');
171
+ console.log(' istarshine config --clear 清除配置文件');
172
+ }
package/lib/install.js CHANGED
@@ -1,18 +1,20 @@
1
1
  /**
2
2
  * install 命令实现
3
3
  * 根据 skill 名称搜索、下载 zip 并解压到本地目录
4
+ * 支持自动携带 API Key 认证和 SKILL.md 声明检测
4
5
  */
5
6
 
6
7
  import { fetch_json, download_file } from './api.js';
7
- import { createWriteStream, mkdirSync, existsSync } from 'fs';
8
+ import { readFileSync, mkdirSync, existsSync } from 'fs';
8
9
  import { join, resolve } from 'path';
9
10
  import { extract_zip } from './zip.js';
11
+ import { resolve_api_key, mask_api_key } from './config.js';
10
12
 
11
13
  /**
12
14
  * 安装指定名称的 skill
13
- * 流程: 按名称搜索 -> 找到精确匹配 -> 下载 zip -> 解压到目标目录
15
+ * 流程: 按名称搜索 -> 找到精确匹配 -> 下载 zip(携带 API Key)-> 解压 -> 检查 SKILL.md 声明
14
16
  * @param {string} skill_name - skill 名称
15
- * @param {object} options - 命令行选项 { dir, server }
17
+ * @param {object} options - 命令行选项 { dir, server, apiKey }
16
18
  */
17
19
  export async function install_skill(skill_name, options) {
18
20
  const { dir, server } = options;
@@ -23,7 +25,14 @@ export async function install_skill(skill_name, options) {
23
25
  const spinner = ora(`正在搜索 Skill: ${skill_name}`).start();
24
26
 
25
27
  try {
26
- // 1. 按名称搜索 skill
28
+ // 解析 API Key(命令行 > 环境变量 > 配置文件)
29
+ const api_key = resolve_api_key(options.apiKey);
30
+ // 构建 auth headers,仅下载接口使用
31
+ const auth_headers = api_key
32
+ ? { 'Authorization': `Bearer ${api_key}` }
33
+ : {};
34
+
35
+ // 1. 按名称搜索 skill(搜索接口不携带 auth header)
27
36
  const search_url = `${server}/api/skills/search?keyword=${encodeURIComponent(skill_name)}`;
28
37
  const results = await fetch_json(search_url);
29
38
 
@@ -40,10 +49,16 @@ export async function install_skill(skill_name, options) {
40
49
  process.exit(1);
41
50
  }
42
51
 
43
- // 2. 下载 zip 压缩包
52
+ // 2. 下载 zip 压缩包(下载接口携带 auth header)
44
53
  spinner.text = `正在下载 ${skill_name}@${skill.version || 'latest'}...`;
45
54
  const download_url = `${server}/api/skills/${skill.id}/download`;
46
- const zip_buffer = await download_file(download_url);
55
+ const zip_buffer = await download_file(download_url, auth_headers);
56
+
57
+ // 检查响应头 X-Auth-Warning
58
+ if (zip_buffer.headers && zip_buffer.headers['x-auth-warning']) {
59
+ // 先不输出,等安装成功后再显示
60
+ var auth_warning = zip_buffer.headers['x-auth-warning'];
61
+ }
47
62
 
48
63
  // 3. 解压到目标目录
49
64
  const target_dir = resolve(process.cwd(), dir, skill_name);
@@ -58,8 +73,44 @@ export async function install_skill(skill_name, options) {
58
73
  spinner.succeed(chalk.green(`${skill_name}@${skill.version || 'latest'} 安装成功`));
59
74
  console.log(chalk.gray(` 安装路径: ${target_dir}`));
60
75
 
76
+ // 安装成功后输出 X-Auth-Warning 警告
77
+ if (auth_warning) {
78
+ console.log(chalk.yellow(` ⚠ ${auth_warning}`));
79
+ }
80
+
81
+ // 4. 检查 SKILL.md 中的认证声明
82
+ check_skill_auth_declaration(target_dir, api_key, chalk);
83
+
61
84
  } catch (err) {
62
85
  spinner.fail(chalk.red(`安装失败: ${err.message}`));
63
86
  process.exit(1);
64
87
  }
65
88
  }
89
+
90
+ /**
91
+ * 检查解压后的 SKILL.md 是否包含 api.auth 或 metadata.openclaw 声明
92
+ * 如果包含且未配置 API Key,输出配置引导提示
93
+ * @param {string} target_dir - 解压目标目录
94
+ * @param {string|null} api_key - 当前解析到的 API Key
95
+ * @param {object} chalk - chalk 实例
96
+ */
97
+ function check_skill_auth_declaration(target_dir, api_key, chalk) {
98
+ const skill_md_path = join(target_dir, 'SKILL.md');
99
+ if (!existsSync(skill_md_path)) return;
100
+
101
+ try {
102
+ const content = readFileSync(skill_md_path, 'utf8');
103
+ // 检查是否包含认证声明
104
+ const has_api_auth = content.includes('api.auth') || content.includes('auth:');
105
+ const has_openclaw = content.includes('metadata.openclaw') || content.includes('primaryEnv');
106
+
107
+ if ((has_api_auth || has_openclaw) && !api_key) {
108
+ console.log('');
109
+ console.log(chalk.cyan(' 💡 该 Skill 需要 API Key 认证,请配置:'));
110
+ console.log(chalk.gray(' 方式一: npx istarshine config --api-key <your-key>'));
111
+ console.log(chalk.gray(' 方式二: openclaw config set skills.entries.<skill>.apiKey "<your-key>"'));
112
+ }
113
+ } catch {
114
+ // 读取 SKILL.md 失败不影响安装流程
115
+ }
116
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "istarshine",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "iStarshine Skills Hub CLI - 一键安装和管理 Skills",
5
5
  "type": "module",
6
6
  "main": "index.js",