byteplan-cli 1.3.2 → 1.3.4

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 CHANGED
@@ -12,8 +12,37 @@ npm install -g byteplan-cli
12
12
 
13
13
  ### Login
14
14
 
15
+ 登录并保存凭据(一次登录,后续无需重复):
16
+
15
17
  ```bash
18
+ # 首次登录(需要提供账号密码)
16
19
  byteplan login -u <phone> -p <password>
20
+
21
+ # 使用已保存的凭据刷新 Token(无需账号密码)
22
+ byteplan login
23
+
24
+ # 覆盖已有凭据(直接传新账号密码即可)
25
+ byteplan login -u <new_phone> -p <new_password>
26
+ ```
27
+
28
+ ### Config
29
+
30
+ 检查当前配置状态:
31
+
32
+ ```bash
33
+ byteplan config
34
+ ```
35
+
36
+ 输出示例:
37
+ ```json
38
+ {
39
+ "configured": true,
40
+ "user": "18256485741",
41
+ "env": "uat",
42
+ "hasToken": true,
43
+ "tokenExpired": false,
44
+ "message": "Configuration is valid."
45
+ }
17
46
  ```
18
47
 
19
48
  ### User Commands
@@ -45,71 +74,74 @@ byteplan data query <modelCode>
45
74
 
46
75
  ### Skills Commands
47
76
 
48
- BytePlan CLI 提供了一系列 Claude Code Skills,可以安装到多个 AI CLI 平台。
77
+ BytePlan CLI 提供了一系列 Skills,可以安装到多个 AI CLI 平台。
49
78
 
50
- ### 查看可用 Skills
79
+ **命令列表**:
51
80
 
52
81
  ```bash
82
+ # 查看可用的 Skills
53
83
  byteplan skills list
84
+
85
+ # 安装所有 Skills
86
+ byteplan skills install -p claude-code
87
+
88
+ # 查看已安装的 Skills
89
+ byteplan skills installed -p claude-code
54
90
  ```
55
91
 
56
- ### 安装 Skills
92
+ **支持的平台**:
57
93
 
58
- 支持多平台安装,使用 `-p` `--platform` 参数指定目标平台:
94
+ | Platform | 目录 |
95
+ |----------|------|
96
+ | `claude-code` (alias: `claude`) | `~/.claude/commands` |
97
+ | `codex` | `~/.codex` |
98
+ | `openclaw` | `~/.openclaw/skills` |
99
+
100
+ **安装示例**:
59
101
 
60
102
  ```bash
61
- # 安装到 Claude Code
62
- byteplan skills install byteplan-api -p claude-code
103
+ # 安装到单个平台
104
+ byteplan skills install -p claude-code
63
105
 
64
106
  # 安装到多个平台
65
107
  byteplan skills install -p claude-code,codex,openclaw
66
108
 
67
- # 安装所有 Skills
68
- byteplan skills install-all -p claude-code
109
+ # 使用自定义目录
110
+ byteplan skills install -d ~/.claude/commands
69
111
  ```
70
112
 
71
- ### 支持的平台
113
+ **Windows 用户**:`~` 路径会自动展开,也可使用 `%USERPROFILE%` 环境变量。
72
114
 
73
- | Platform | 目录 | 说明 |
74
- |----------|------|------|
75
- | `claude-code` (alias: `claude`) | `~/.claude/commands` | Claude Code CLI |
76
- | `codex` | `~/.codex` | OpenAI Codex CLI |
77
- | `openclaw` | `~/.openclaw/skills` | OpenClaw |
78
-
79
- ### 自定义目录
115
+ ## Options
80
116
 
81
- 也可以使用 `-d` 参数指定自定义目录:
117
+ - `-e, --env <environment>` - Environment (uat, prod), default: uat
82
118
 
83
- ```bash
84
- # 向后兼容:指定自定义目录
85
- byteplan skills install byteplan-api -d ~/.claude/commands
119
+ ## Configuration
86
120
 
87
- # 组合使用:覆盖平台默认目录
88
- byteplan skills install byteplan-api -p claude-code -d /custom/path
89
- ```
121
+ `byteplan login` 会将凭据持久化保存到 `~/.byteplan/.env`,后续命令无需重复登录。
90
122
 
91
- ### 查看已安装 Skills
123
+ **保存的内容**:
92
124
 
93
- ```bash
94
- byteplan skills installed -p claude-code
125
+ ```env
126
+ BP_ENV=uat # 环境 (uat/prod)
127
+ BP_USER=<phone> # 手机号(用于自动重登)
128
+ BP_PASSWORD=<password> # 密码(用于自动重登)
129
+ ACCESS_TOKEN=<token> # 当前访问令牌
130
+ REFRESH_TOKEN=<refresh_token> # 刷新令牌
131
+ TOKEN_EXPIRES_IN=<timestamp> # Token 过期时间戳
95
132
  ```
96
133
 
97
- ## Options
134
+ **Token 自动刷新**:
98
135
 
99
- - `-e, --env <environment>` - Environment (uat, prod), default: uat
136
+ - Token 有效期默认 2 小时
137
+ - CLI 检测到 Token 即将过期(剩余 < 5 分钟)时,会自动用保存的账号密码重新登录
138
+ - 无需手动干预,命令会自动执行
100
139
 
101
- ## Configuration
140
+ **安全性说明**:
102
141
 
103
- Credentials are stored in `~/.byteplan/.env`:
104
-
105
- ```env
106
- BP_ENV=uat
107
- BP_USER=<phone>
108
- BP_PASSWORD=<password>
109
- ACCESS_TOKEN=<token>
110
- REFRESH_TOKEN=<refresh_token>
111
- TOKEN_EXPIRES_IN=<timestamp>
112
- ```
142
+ - 凭据以明文存储在本地 `.env` 文件中
143
+ - 请勿在共享环境中使用,或确保 `~/.byteplan/` 目录权限受限
144
+ - 如需清除凭据:`rm ~/.byteplan/.env`
113
145
 
114
146
  ## License
115
147
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "byteplan-cli",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "BytePlan CLI - Command line tool for BytePlan API",
5
5
  "keywords": [
6
6
  "byteplan",
package/src/cli.js CHANGED
@@ -6,6 +6,9 @@
6
6
 
7
7
  import { Command } from 'commander';
8
8
  import { createRequire } from 'module';
9
+ import { homedir } from 'os';
10
+ import { join } from 'path';
11
+ import { existsSync, readFileSync } from 'fs';
9
12
  import {
10
13
  login,
11
14
  loginWithEnv,
@@ -23,6 +26,9 @@ import { skillsCmd } from './commands/skills.js';
23
26
  const require = createRequire(import.meta.url);
24
27
  const { version } = require('../package.json');
25
28
 
29
+ const configDir = join(homedir(), '.byteplan');
30
+ const envFile = join(configDir, '.env');
31
+
26
32
  function printJSON(data) {
27
33
  console.log(JSON.stringify(data, null, 2));
28
34
  }
@@ -32,6 +38,36 @@ function printError(error) {
32
38
  process.exit(1);
33
39
  }
34
40
 
41
+ // 检查是否已有配置
42
+ function checkExistingConfig() {
43
+ if (!existsSync(envFile)) {
44
+ return null;
45
+ }
46
+
47
+ const content = readFileSync(envFile, 'utf-8');
48
+ const lines = content.split('\n');
49
+ const config = {};
50
+
51
+ for (const line of lines) {
52
+ if (line.startsWith('BP_USER=')) {
53
+ config.user = line.split('=')[1];
54
+ }
55
+ if (line.startsWith('BP_ENV=')) {
56
+ config.env = line.split('=')[1];
57
+ }
58
+ if (line.startsWith('ACCESS_TOKEN=')) {
59
+ config.hasToken = true;
60
+ }
61
+ if (line.startsWith('TOKEN_EXPIRES_IN=')) {
62
+ const expiresAt = parseInt(line.split('=')[1]);
63
+ config.tokenExpiresAt = expiresAt;
64
+ config.tokenExpired = expiresAt < Date.now();
65
+ }
66
+ }
67
+
68
+ return config;
69
+ }
70
+
35
71
  const program = new Command();
36
72
 
37
73
  program
@@ -40,20 +76,102 @@ program
40
76
  .version(version)
41
77
  .option('-e, --env <environment>', 'Environment (uat, prod)', 'uat');
42
78
 
79
+ // Config command - 检查配置状态
80
+ program
81
+ .command('config')
82
+ .description('Check current configuration status')
83
+ .action(() => {
84
+ try {
85
+ const config = checkExistingConfig();
86
+
87
+ if (!config) {
88
+ printJSON({
89
+ configured: false,
90
+ configFile: envFile,
91
+ message: 'No configuration found. Please run: byteplan login -u <phone> -p <password>'
92
+ });
93
+ return;
94
+ }
95
+
96
+ printJSON({
97
+ configured: true,
98
+ configFile: envFile,
99
+ user: config.user || '',
100
+ env: config.env || 'uat',
101
+ hasToken: config.hasToken || false,
102
+ tokenExpired: config.tokenExpired || false,
103
+ tokenExpiresAt: config.tokenExpiresAt ? new Date(config.tokenExpiresAt).toISOString() : null,
104
+ message: config.tokenExpired
105
+ ? 'Token expired. Will auto-refresh on next command.'
106
+ : 'Configuration is valid.'
107
+ });
108
+ } catch (error) {
109
+ printError(error);
110
+ }
111
+ });
112
+
43
113
  // Login command
44
114
  program
45
115
  .command('login')
46
- .description('Login to BytePlan')
47
- .requiredOption('-u, --user <phone>', 'Phone number')
48
- .requiredOption('-p, --password <password>', 'Password')
116
+ .description('Login to BytePlan (use saved credentials if not provided)')
117
+ .option('-u, --user <phone>', 'Phone number (optional if already configured)')
118
+ .option('-p, --password <password>', 'Password (optional if already configured)')
49
119
  .action(async (options) => {
50
120
  try {
121
+ const existingConfig = checkExistingConfig();
51
122
  const env = program.opts().env;
123
+
124
+ // 没有提供账号密码,使用已保存的配置
125
+ if (!options.user && !options.password) {
126
+ if (!existingConfig) {
127
+ printJSON({
128
+ error: true,
129
+ message: 'No saved credentials. Please provide user and password.',
130
+ hint: 'byteplan login -u <phone> -p <password>'
131
+ });
132
+ process.exit(1);
133
+ }
134
+
135
+ // 使用保存的凭据重新登录
136
+ const result = await loginWithEnv(true);
137
+ const userInfo = await getUserInfo();
138
+
139
+ printJSON({
140
+ success: true,
141
+ source: 'saved_credentials',
142
+ user: {
143
+ userId: userInfo.user?.id || '',
144
+ userName: userInfo.user?.userName || '',
145
+ userPhone: userInfo.user?.mobile || existingConfig.user,
146
+ email: userInfo.user?.email || '',
147
+ },
148
+ tenant: {
149
+ tenantId: userInfo.user?.tenantId || '',
150
+ tenantName: userInfo.user?.tenantName || '',
151
+ tenantCode: userInfo.user?.tenantCode || '',
152
+ },
153
+ expiresIn: result.expires_in,
154
+ });
155
+ return;
156
+ }
157
+
158
+ // 提供了账号密码(直接覆盖)
159
+ if (!options.user || !options.password) {
160
+ printJSON({
161
+ error: true,
162
+ message: 'Both user and password are required when providing credentials.',
163
+ hint: 'byteplan login -u <phone> -p <password>'
164
+ });
165
+ process.exit(1);
166
+ }
167
+
168
+ // 新登录(覆盖已有配置)
52
169
  const result = await login(options.user, options.password, env);
53
170
  const userInfo = await getUserInfo(result.access_token);
54
171
 
55
172
  printJSON({
56
173
  success: true,
174
+ source: 'new_credentials',
57
175
  user: {
58
176
  userId: userInfo.user?.id || '',
59
177
  userName: userInfo.user?.userName || '',
@@ -66,6 +184,7 @@ program
66
184
  tenantCode: userInfo.user?.tenantCode || '',
67
185
  },
68
186
  expiresIn: result.expires_in,
187
+ overwritten: existingConfig ? true : false,
69
188
  });
70
189
  } catch (error) {
71
190
  printError(error);
@@ -12,6 +12,44 @@ import { createRequire } from 'module';
12
12
 
13
13
  const require = createRequire(import.meta.url);
14
14
 
15
+ // 配置文件路径
16
+ const configDir = path.join(homedir(), '.byteplan');
17
+ const envFile = path.join(configDir, '.env');
18
+
19
+ // 检查配置状态
20
+ function checkConfig() {
21
+ if (!existsSync(envFile)) {
22
+ return { configured: false };
23
+ }
24
+
25
+ const content = readFileSync(envFile, 'utf-8');
26
+ const config = { configured: true };
27
+
28
+ for (const line of content.split('\n')) {
29
+ if (line.startsWith('BP_USER=')) config.user = line.split('=')[1];
30
+ if (line.startsWith('BP_ENV=')) config.env = line.split('=')[1];
31
+ }
32
+
33
+ return config;
34
+ }
35
+
36
+ // 展开 ~ 路径,支持跨平台
37
+ function expandHomeDir(dirPath) {
38
+ if (!dirPath) return dirPath;
39
+
40
+ // 处理 ~ 开头的路径
41
+ if (dirPath.startsWith('~')) {
42
+ return path.join(homedir(), dirPath.slice(1));
43
+ }
44
+
45
+ // Windows: 处理 %USERPROFILE% 环境变量
46
+ if (dirPath.includes('%USERPROFILE%')) {
47
+ return dirPath.replace('%USERPROFILE%', homedir());
48
+ }
49
+
50
+ return dirPath;
51
+ }
52
+
15
53
  // 平台默认目录映射
16
54
  const PLATFORM_DIRS = {
17
55
  'claude-code': path.join(homedir(), '.claude', 'commands'),
@@ -22,24 +60,20 @@ const PLATFORM_DIRS = {
22
60
 
23
61
  // Skills 源目录(在 npm 包中)
24
62
  const getSkillsSourceDir = () => {
25
- // 获取当前模块所在目录,然后向上找到项目根目录
26
63
  const currentDir = path.dirname(new URL(import.meta.url).pathname);
27
- // 从 src/commands/skills.js -> src -> 项目根目录 -> skills
28
64
  return path.join(currentDir, '..', '..', 'skills');
29
65
  };
30
66
 
31
- // 解析平台和目录参数,返回目标目录列表
67
+ // 解析平台和目录参数
32
68
  function resolveTargetDirs(platformArg, dirArg) {
33
69
  const targets = [];
70
+ const expandedDirArg = expandHomeDir(dirArg);
34
71
 
35
72
  if (platformArg) {
36
- // 解析平台列表(逗号分隔)
37
73
  const platforms = platformArg.split(',').map(p => p.trim());
38
-
39
74
  for (const platform of platforms) {
40
75
  const normalizedName = platform.toLowerCase();
41
76
  const defaultDir = PLATFORM_DIRS[normalizedName];
42
-
43
77
  if (!defaultDir) {
44
78
  return {
45
79
  error: true,
@@ -47,20 +81,17 @@ function resolveTargetDirs(platformArg, dirArg) {
47
81
  available: Object.keys(PLATFORM_DIRS).filter(k => !k.includes('-')),
48
82
  };
49
83
  }
50
-
51
84
  targets.push({
52
85
  platform: normalizedName,
53
- targetDir: dirArg || defaultDir, // 如果指定了 -d,覆盖所有平台的默认目录
86
+ targetDir: expandedDirArg || defaultDir,
54
87
  });
55
88
  }
56
- } else if (dirArg) {
57
- // 向后兼容:只指定 -d 的情况
89
+ } else if (expandedDirArg) {
58
90
  targets.push({
59
91
  platform: 'custom',
60
- targetDir: dirArg,
92
+ targetDir: expandedDirArg,
61
93
  });
62
94
  } else {
63
- // 都不指定,返回错误
64
95
  return {
65
96
  error: true,
66
97
  message: 'Must specify at least one of --platform or --dir',
@@ -82,17 +113,11 @@ function printError(error) {
82
113
  // 获取 skill 信息
83
114
  function getSkillInfo(skillPath) {
84
115
  const skillMdPath = path.join(skillPath, 'SKILL.md');
85
- if (!existsSync(skillMdPath)) {
86
- return null;
87
- }
116
+ if (!existsSync(skillMdPath)) return null;
88
117
 
89
118
  const content = readFileSync(skillMdPath, 'utf-8');
90
-
91
- // 解析 frontmatter
92
119
  const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
93
- if (!frontmatterMatch) {
94
- return null;
95
- }
120
+ if (!frontmatterMatch) return null;
96
121
 
97
122
  const frontmatter = frontmatterMatch[1];
98
123
  const nameMatch = frontmatter.match(/name:\s*(.+)/);
@@ -101,283 +126,213 @@ function getSkillInfo(skillPath) {
101
126
  return {
102
127
  name: nameMatch ? nameMatch[1].trim() : path.basename(skillPath),
103
128
  description: descMatch ? descMatch[1].trim() : '',
104
- path: skillPath,
105
129
  };
106
130
  }
107
131
 
108
132
  // 获取所有可用 skills
109
133
  function getAvailableSkills() {
110
134
  const sourceDir = getSkillsSourceDir();
111
- if (!existsSync(sourceDir)) {
112
- return [];
113
- }
135
+ if (!existsSync(sourceDir)) return [];
114
136
 
115
137
  const skills = [];
116
138
  const dirs = readdirSync(sourceDir, { withFileTypes: true });
117
-
118
139
  for (const dir of dirs) {
119
140
  if (dir.isDirectory()) {
120
- const skillPath = path.join(sourceDir, dir.name);
121
- const info = getSkillInfo(skillPath);
122
- if (info) {
123
- skills.push(info);
124
- }
141
+ const info = getSkillInfo(path.join(sourceDir, dir.name));
142
+ if (info) skills.push(info);
125
143
  }
126
144
  }
127
-
128
145
  return skills;
129
146
  }
130
147
 
131
- // 获取已安装的 skills(支持自定义目录)
148
+ // 获取已安装的 skills
132
149
  function getInstalledSkills(targetDir) {
133
- if (!existsSync(targetDir)) {
134
- return [];
135
- }
150
+ if (!existsSync(targetDir)) return [];
136
151
 
137
152
  const skills = [];
138
153
  const dirs = readdirSync(targetDir, { withFileTypes: true });
139
-
140
154
  for (const dir of dirs) {
141
155
  if (dir.isDirectory() && dir.name.startsWith('byteplan-')) {
142
- const skillPath = path.join(targetDir, dir.name);
143
- const info = getSkillInfo(skillPath);
144
- if (info) {
145
- skills.push(info);
146
- }
156
+ const info = getSkillInfo(path.join(targetDir, dir.name));
157
+ if (info) skills.push(info);
147
158
  }
148
159
  }
149
-
150
160
  return skills;
151
161
  }
152
162
 
163
+ // 安装所有 skills
164
+ function installAllSkills(targetDirs) {
165
+ const sourceDir = getSkillsSourceDir();
166
+ const availableSkills = getAvailableSkills();
167
+ const toInstall = availableSkills.map(s => s.name);
168
+ const results = {};
169
+
170
+ for (const target of targetDirs) {
171
+ if (!existsSync(target.targetDir)) {
172
+ mkdirSync(target.targetDir, { recursive: true });
173
+ }
174
+
175
+ const installed = [];
176
+ for (const skillName of toInstall) {
177
+ cpSync(
178
+ path.join(sourceDir, skillName),
179
+ path.join(target.targetDir, skillName),
180
+ { recursive: true }
181
+ );
182
+ installed.push(skillName);
183
+ }
184
+
185
+ results[target.platform] = {
186
+ targetDir: target.targetDir,
187
+ installed: installed,
188
+ count: installed.length,
189
+ };
190
+ }
191
+
192
+ return { results, totalSkills: toInstall.length };
193
+ }
194
+
153
195
  // Skills 命令
154
196
  const skillsCmd = new Command('skills')
155
- .description('Manage BytePlan Claude Code skills')
156
- .option('-d, --dir <directory>', 'Target installation directory (overrides platform default)')
157
- .option('-p, --platform <platforms>', 'Target platform(s): claude-code, codex, openclaw (comma-separated for multiple)')
197
+ .description('Manage BytePlan skills')
198
+ .option('-p, --platform <platforms>', 'Target platform(s): claude-code, codex, openclaw (comma-separated)')
199
+ .option('-d, --dir <directory>', 'Custom target directory')
200
+ .option('--check-config', 'Check configuration status before install', false)
158
201
  .addHelpText('after', `
159
202
  Commands:
160
- skills list List all available skills
161
- skills installed List installed skills (requires -p or -d)
162
- skills install [names] Install skills (requires -p or -d)
163
- skills install-all Install all BytePlan skills (requires -p or -d)
203
+ skills install Install all BytePlan skills
204
+ skills list List all available skills
205
+ skills installed List installed skills
164
206
 
165
- Supported Platforms:
166
- claude-code (alias: claude) ~/.claude/commands
167
- codex~/.codex
168
- openclaw~/.openclaw/skills
207
+ Platforms:
208
+ claude-code (claude) ~/.claude/commands
209
+ codex ~/.codex
210
+ openclaw ~/.openclaw/skills
169
211
 
170
212
  Examples:
171
- $ byteplan skills list
172
- $ byteplan skills installed -p claude-code
173
- $ byteplan skills install byteplan-api -p claude-code
174
- $ byteplan skills install byteplan-api byteplan-analysis -p claude-code,codex
175
- $ byteplan skills install-all -p claude-code,openclaw
176
- $ byteplan skills install byteplan-api -d ~/.claude/commands
177
- $ byteplan skills install byteplan-api -p claude-code -d /custom/path
213
+ byteplan skills install -p claude-code
214
+ byteplan skills install -p claude-code,codex,openclaw
215
+ byteplan skills list
178
216
  `);
179
217
 
180
- // skills list 子命令
218
+ // skills install 子命令 - 安装全部
181
219
  skillsCmd
182
- .command('list')
183
- .description('List all available BytePlan skills')
184
- .action(() => {
185
- try {
186
- const skills = getAvailableSkills();
187
-
188
- printJSON({
189
- skills: skills.map(s => ({
190
- name: s.name,
191
- description: s.description,
192
- })),
193
- total: skills.length,
194
- sourceDir: getSkillsSourceDir(),
195
- });
196
- } catch (error) {
197
- printError(error);
198
- }
199
- });
220
+ .command('install')
221
+ .description('Install all BytePlan skills to target platform(s)')
222
+ .addHelpText('after', `
223
+ Options (from parent):
224
+ -p, --platform Target platform(s): claude-code, codex, openclaw
225
+ -d, --dir Custom target directory
226
+ --check-config Check configuration status first
200
227
 
201
- // skills installed 子命令
202
- skillsCmd
203
- .command('installed')
204
- .description('List installed BytePlan skills in target directory')
228
+ Examples:
229
+ byteplan skills install -p claude-code
230
+ byteplan skills install -p claude-code,codex
231
+ byteplan skills install -d ~/.claude/commands
232
+ `)
205
233
  .action(() => {
206
234
  try {
235
+ // 检查配置状态
236
+ const config = checkConfig();
207
237
  const parentOptions = skillsCmd.opts();
208
- const platformArg = parentOptions.platform;
209
- const dirArg = parentOptions.dir;
210
238
 
211
- const resolved = resolveTargetDirs(platformArg, dirArg);
239
+ // 如果请求检查配置或未配置,显示配置状态
240
+ if (parentOptions.checkConfig || !config.configured) {
241
+ printJSON({
242
+ config: config.configured ? {
243
+ configured: true,
244
+ user: config.user,
245
+ env: config.env
246
+ } : {
247
+ configured: false,
248
+ configFile: envFile
249
+ },
250
+ hint: config.configured
251
+ ? 'Configuration found. Run without --check-config to install.'
252
+ : 'Please login first: byteplan login -u <phone> -p <password>'
253
+ });
254
+ if (!config.configured) process.exit(1);
255
+ if (parentOptions.checkConfig) process.exit(0);
256
+ }
257
+
258
+ const resolved = resolveTargetDirs(parentOptions.platform, parentOptions.dir);
259
+
212
260
  if (resolved.error) {
213
261
  printJSON(resolved);
214
262
  process.exit(1);
215
263
  }
216
264
 
217
- const results = {};
218
- for (const target of resolved.targets) {
219
- const skills = getInstalledSkills(target.targetDir);
220
- results[target.platform] = {
221
- targetDir: target.targetDir,
222
- skills: skills.map(s => ({
223
- name: s.name,
224
- description: s.description,
225
- })),
226
- count: skills.length,
227
- };
228
- }
265
+ const { results, totalSkills } = installAllSkills(resolved.targets);
229
266
 
230
267
  printJSON({
268
+ success: true,
269
+ config: { user: config.user, env: config.env },
231
270
  platforms: results,
232
271
  totalPlatforms: resolved.targets.length,
272
+ totalSkills: totalSkills,
233
273
  });
234
274
  } catch (error) {
235
275
  printError(error);
236
276
  }
237
277
  });
238
278
 
239
- // skills install 子命令
279
+ // skills list 子命令
240
280
  skillsCmd
241
- .command('install [names...]')
242
- .description('Install specified skills to target directory')
243
- .option('-a, --all', 'Install all BytePlan skills', false)
244
- .addHelpText('after', `
245
- Examples:
246
- $ byteplan skills install byteplan-api -p claude-code
247
- $ byteplan skills install byteplan-api byteplan-analysis -p claude-code,codex
248
- $ byteplan skills install --all -p claude-code
249
- $ byteplan skills install byteplan-api -d ~/.claude/commands
250
- $ byteplan skills install byteplan-api -p claude-code -d /custom/path
251
- `)
252
- .action(async (names, options) => {
281
+ .command('list')
282
+ .description('List all available BytePlan skills')
283
+ .action(() => {
253
284
  try {
254
- const parentOptions = skillsCmd.opts();
255
- const platformArg = parentOptions.platform;
256
- const dirArg = parentOptions.dir;
257
-
258
- const resolved = resolveTargetDirs(platformArg, dirArg);
259
- if (resolved.error) {
260
- printJSON(resolved);
261
- process.exit(1);
262
- }
263
-
264
- const sourceDir = getSkillsSourceDir();
265
-
266
- // 获取要安装的 skills
267
- let toInstall = names || [];
268
- if (options.all || toInstall.length === 0) {
269
- const available = getAvailableSkills();
270
- toInstall = available.map(s => s.name);
271
- }
272
-
273
- // 验证 skills 存在
274
- const availableSkills = getAvailableSkills();
275
- const availableNames = availableSkills.map(s => s.name);
276
-
277
- const invalid = toInstall.filter(n => !availableNames.includes(n));
278
- if (invalid.length > 0) {
279
- printJSON({
280
- error: true,
281
- message: `Unknown skills: ${invalid.join(', ')}`,
282
- available: availableNames,
283
- });
284
- process.exit(1);
285
- }
286
-
287
- // 安装到各目标目录
288
- const results = {};
289
- for (const target of resolved.targets) {
290
- const targetDir = target.targetDir;
291
-
292
- // 确保目标目录存在
293
- if (!existsSync(targetDir)) {
294
- mkdirSync(targetDir, { recursive: true });
295
- }
296
-
297
- // 安装 skills
298
- const installed = [];
299
- for (const skillName of toInstall) {
300
- const skillSourcePath = path.join(sourceDir, skillName);
301
- const skillTargetPath = path.join(targetDir, skillName);
302
-
303
- // 复制 skill 目录
304
- cpSync(skillSourcePath, skillTargetPath, { recursive: true });
305
- installed.push(skillName);
306
- }
307
-
308
- results[target.platform] = {
309
- targetDir: targetDir,
310
- installed: installed,
311
- count: installed.length,
312
- };
313
- }
285
+ const config = checkConfig();
286
+ const skills = getAvailableSkills();
314
287
 
315
288
  printJSON({
316
- success: true,
317
- platforms: results,
318
- totalPlatforms: resolved.targets.length,
319
- totalSkills: toInstall.length,
289
+ config: config.configured ? { user: config.user, env: config.env } : { configured: false },
290
+ skills: skills.map(s => ({ name: s.name, description: s.description })),
291
+ total: skills.length,
320
292
  });
321
293
  } catch (error) {
322
294
  printError(error);
323
295
  }
324
296
  });
325
297
 
326
- // skills install-all 子命令(快捷方式)
298
+ // skills installed 子命令
327
299
  skillsCmd
328
- .command('install-all')
329
- .description('Install all BytePlan skills to target directory')
330
- .action(async () => {
300
+ .command('installed')
301
+ .description('List installed BytePlan skills')
302
+ .addHelpText('after', `
303
+ Options (from parent):
304
+ -p, --platform Target platform(s): claude-code, codex, openclaw
305
+ -d, --dir Custom target directory
306
+
307
+ Examples:
308
+ byteplan skills installed -p claude-code
309
+ byteplan skills installed -d ~/.claude/commands
310
+ `)
311
+ .action(() => {
331
312
  try {
313
+ const config = checkConfig();
332
314
  const parentOptions = skillsCmd.opts();
333
- const platformArg = parentOptions.platform;
334
- const dirArg = parentOptions.dir;
315
+ const resolved = resolveTargetDirs(parentOptions.platform, parentOptions.dir);
335
316
 
336
- const resolved = resolveTargetDirs(platformArg, dirArg);
337
317
  if (resolved.error) {
338
318
  printJSON(resolved);
339
319
  process.exit(1);
340
320
  }
341
321
 
342
- const sourceDir = getSkillsSourceDir();
343
-
344
- // 获取所有 skills
345
- const availableSkills = getAvailableSkills();
346
- const toInstall = availableSkills.map(s => s.name);
347
-
348
- // 安装到各目标目录
349
322
  const results = {};
350
323
  for (const target of resolved.targets) {
351
- const targetDir = target.targetDir;
352
-
353
- // 确保目标目录存在
354
- if (!existsSync(targetDir)) {
355
- mkdirSync(targetDir, { recursive: true });
356
- }
357
-
358
- // 安装所有 skills
359
- const installed = [];
360
- for (const skillName of toInstall) {
361
- const skillSourcePath = path.join(sourceDir, skillName);
362
- const skillTargetPath = path.join(targetDir, skillName);
363
-
364
- // 复制 skill 目录
365
- cpSync(skillSourcePath, skillTargetPath, { recursive: true });
366
- installed.push(skillName);
367
- }
368
-
324
+ const skills = getInstalledSkills(target.targetDir);
369
325
  results[target.platform] = {
370
- targetDir: targetDir,
371
- installed: installed,
372
- count: installed.length,
326
+ targetDir: target.targetDir,
327
+ skills: skills.map(s => ({ name: s.name, description: s.description })),
328
+ count: skills.length,
373
329
  };
374
330
  }
375
331
 
376
332
  printJSON({
377
- success: true,
333
+ config: config.configured ? { user: config.user, env: config.env } : { configured: false },
378
334
  platforms: results,
379
335
  totalPlatforms: resolved.targets.length,
380
- totalSkills: toInstall.length,
381
336
  });
382
337
  } catch (error) {
383
338
  printError(error);