@simonyea/holysheep-cli 1.0.2 → 1.0.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonyea/holysheep-cli",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "一键配置所有 AI 编程工具接入 HolySheep API — Claude Code / Codex / Gemini CLI / OpenCode / OpenClaw / Aider / Cursor",
5
5
  "keywords": [
6
6
  "claude",
@@ -4,15 +4,55 @@
4
4
  const inquirer = require('inquirer')
5
5
  const chalk = require('chalk')
6
6
  const ora = require('ora')
7
+ const { execSync, spawnSync } = require('child_process')
7
8
  const { saveConfig, getApiKey, BASE_URL_ANTHROPIC, BASE_URL_OPENAI, SHOP_URL } = require('../utils/config')
8
9
  const { writeEnvToShell } = require('../utils/shell')
9
10
  const TOOLS = require('../tools')
10
11
 
11
- const TOOL_CHOICES = TOOLS.map(t => ({
12
- name: `${t.checkInstalled() ? chalk.green('●') : chalk.gray('○')} ${t.name.padEnd(18)} ${t.checkInstalled() ? chalk.gray('(已安装)') : chalk.gray('(未安装)')}`,
13
- value: t.id,
14
- short: t.name,
15
- }))
12
+ // 工具的自动安装命令(npm/pip)
13
+ const AUTO_INSTALL = {
14
+ 'claude-code': { cmd: 'npm install -g @anthropic-ai/claude-code', mgr: 'npm' },
15
+ 'codex': { cmd: 'npm install -g @openai/codex', mgr: 'npm' },
16
+ 'gemini-cli': { cmd: 'npm install -g @google/gemini-cli', mgr: 'npm' },
17
+ 'opencode': { cmd: 'npm install -g opencode-ai', mgr: 'npm' },
18
+ 'aider': { cmd: 'pip install aider-chat', mgr: 'pip' },
19
+ }
20
+
21
+ function canAutoInstall(toolId) {
22
+ return !!AUTO_INSTALL[toolId]
23
+ }
24
+
25
+ async function tryAutoInstall(tool) {
26
+ const info = AUTO_INSTALL[tool.id]
27
+ if (!info) return false
28
+
29
+ // 检查 npm/pip 是否可用
30
+ try {
31
+ execSync(`${info.mgr} --version`, { stdio: 'ignore' })
32
+ } catch {
33
+ console.log(chalk.red(` ✗ 未找到 ${info.mgr},无法自动安装 ${tool.name}`))
34
+ return false
35
+ }
36
+
37
+ const spinner = ora(`正在安装 ${tool.name}...`).start()
38
+ try {
39
+ spawnSync(info.cmd.split(' ')[0], info.cmd.split(' ').slice(1), {
40
+ stdio: 'inherit',
41
+ shell: true,
42
+ })
43
+ // 安装后重新检测
44
+ if (tool.checkInstalled()) {
45
+ spinner.succeed(`${tool.name} 安装成功`)
46
+ return true
47
+ } else {
48
+ spinner.fail(`${tool.name} 安装后仍未检测到,请手动安装: ${chalk.cyan(info.cmd)}`)
49
+ return false
50
+ }
51
+ } catch (e) {
52
+ spinner.fail(`安装失败: ${e.message}`)
53
+ return false
54
+ }
55
+ }
16
56
 
17
57
  async function setup(options) {
18
58
  console.log()
@@ -38,18 +78,36 @@ async function setup(options) {
38
78
  console.log(`${chalk.green('✓')} 使用已保存的 API Key: ${chalk.cyan(maskKey(apiKey))}`)
39
79
  }
40
80
 
41
- // Step 2: 选择工具
81
+ // Step 2: 选择工具(已安装 + 未安装分组显示)
82
+ const installedTools = TOOLS.filter(t => t.checkInstalled())
83
+ const uninstalledTools = TOOLS.filter(t => !t.checkInstalled())
84
+
85
+ const choices = []
86
+ if (installedTools.length) {
87
+ choices.push(new inquirer.Separator(chalk.green('── 已安装 ──')))
88
+ installedTools.forEach(t => choices.push({
89
+ name: `${chalk.green('●')} ${t.name.padEnd(18)} ${chalk.gray('(已安装)')}`,
90
+ value: t.id,
91
+ short: t.name,
92
+ checked: true, // 已安装的默认全选
93
+ }))
94
+ }
95
+ if (uninstalledTools.length) {
96
+ choices.push(new inquirer.Separator(chalk.gray('── 未安装(可自动安装)──')))
97
+ uninstalledTools.forEach(t => choices.push({
98
+ name: `${chalk.gray('○')} ${t.name.padEnd(18)} ${canAutoInstall(t.id) ? chalk.cyan('(选中后自动安装)') : chalk.gray('(需手动安装)')}`,
99
+ value: t.id,
100
+ short: t.name,
101
+ checked: false,
102
+ }))
103
+ }
104
+
42
105
  const { toolIds } = await inquirer.prompt([{
43
106
  type: 'checkbox',
44
107
  name: 'toolIds',
45
108
  message: '选择要配置的工具(空格选中,回车确认):',
46
- choices: [
47
- new inquirer.Separator('── 已安装 ──'),
48
- ...TOOL_CHOICES.filter((_, i) => TOOLS[i].checkInstalled()),
49
- new inquirer.Separator('── 未安装 ──'),
50
- ...TOOL_CHOICES.filter((_, i) => !TOOLS[i].checkInstalled()),
51
- ],
52
- pageSize: 12,
109
+ choices,
110
+ pageSize: 14,
53
111
  }])
54
112
 
55
113
  if (toolIds.length === 0) {
@@ -59,12 +117,46 @@ async function setup(options) {
59
117
 
60
118
  console.log()
61
119
 
62
- // Step 3: 配置每个工具
120
+ // Step 3: 对未安装但被选中的工具,询问是否自动安装
63
121
  const selectedTools = TOOLS.filter(t => toolIds.includes(t.id))
122
+ const needInstall = selectedTools.filter(t => !t.checkInstalled() && canAutoInstall(t.id))
123
+ const cantInstall = selectedTools.filter(t => !t.checkInstalled() && !canAutoInstall(t.id))
124
+
125
+ // 提示不能自动安装的工具
126
+ if (cantInstall.length) {
127
+ console.log(chalk.yellow(`以下工具需要手动安装后再运行 hs setup:`))
128
+ cantInstall.forEach(t => console.log(` ${chalk.gray('→')} ${t.name}: ${chalk.cyan(t.installCmd)}`))
129
+ console.log()
130
+ }
131
+
132
+ // 自动安装可以安装的工具
133
+ if (needInstall.length) {
134
+ const { doInstall } = await inquirer.prompt([{
135
+ type: 'confirm',
136
+ name: 'doInstall',
137
+ message: `检测到 ${needInstall.map(t => chalk.cyan(t.name)).join('、')} 未安装,现在自动安装?`,
138
+ default: true,
139
+ }])
140
+
141
+ if (doInstall) {
142
+ for (const tool of needInstall) {
143
+ await tryAutoInstall(tool)
144
+ }
145
+ console.log()
146
+ }
147
+ }
148
+
149
+ // Step 4: 配置每个已安装的工具
64
150
  const envVarsToWrite = {}
65
151
  const results = []
152
+ const toConfigureTools = selectedTools.filter(t => t.checkInstalled())
66
153
 
67
- for (const tool of selectedTools) {
154
+ if (toConfigureTools.length === 0) {
155
+ console.log(chalk.yellow('没有可配置的工具(请先安装),退出。'))
156
+ return
157
+ }
158
+
159
+ for (const tool of toConfigureTools) {
68
160
  const spinner = ora(`配置 ${tool.name}...`).start()
69
161
  try {
70
162
  const result = tool.configure(apiKey, BASE_URL_ANTHROPIC, BASE_URL_OPENAI)
@@ -74,7 +166,6 @@ async function setup(options) {
74
166
  result.steps.forEach((s, i) => console.log(` ${chalk.gray(i + 1 + '.')} ${s}`))
75
167
  results.push({ tool, status: 'manual' })
76
168
  } else {
77
- // 收集需要写入 shell 的环境变量
78
169
  if (result.envVars) Object.assign(envVarsToWrite, result.envVars)
79
170
  spinner.succeed(`${chalk.green(tool.name)} ${chalk.gray(result.file ? `→ ${result.file}` : '')}`)
80
171
  results.push({ tool, status: 'ok', result })
@@ -85,16 +176,15 @@ async function setup(options) {
85
176
  }
86
177
  }
87
178
 
88
- // Step 4: 写入通用环境变量(如果有工具需要)
89
- const needsEnvVars = selectedTools.some(t => t.id === 'codex' || t.id === 'aider')
179
+ // Step 5: 写入通用环境变量
180
+ const needsEnvVars = toConfigureTools.some(t => t.id === 'codex' || t.id === 'aider')
90
181
  if (needsEnvVars || Object.keys(envVarsToWrite).length > 0) {
91
- const defaultEnv = {
182
+ Object.assign(envVarsToWrite, {
92
183
  ANTHROPIC_API_KEY: apiKey,
93
184
  ANTHROPIC_BASE_URL: BASE_URL_ANTHROPIC,
94
185
  OPENAI_API_KEY: apiKey,
95
186
  OPENAI_BASE_URL: BASE_URL_OPENAI,
96
- }
97
- Object.assign(envVarsToWrite, defaultEnv)
187
+ })
98
188
  }
99
189
 
100
190
  if (Object.keys(envVarsToWrite).length > 0) {
@@ -107,7 +197,7 @@ async function setup(options) {
107
197
  }
108
198
  }
109
199
 
110
- // Step 5: 保存 API Key 到本地
200
+ // Step 6: 保存 API Key
111
201
  saveConfig({ apiKey })
112
202
 
113
203
  // 摘要
@@ -116,7 +206,7 @@ async function setup(options) {
116
206
  console.log(chalk.green.bold('✅ 配置完成!'))
117
207
  console.log()
118
208
 
119
- const ok = results.filter(r => r.status === 'ok')
209
+ const ok = results.filter(r => r.status === 'ok')
120
210
  const manual = results.filter(r => r.status === 'manual')
121
211
  const errors = results.filter(r => r.status === 'error')
122
212
 
@@ -10,6 +10,10 @@ const MARKER_END = '# <<< holysheep-cli managed <<<'
10
10
 
11
11
  function getShellRcFiles() {
12
12
  const home = os.homedir()
13
+
14
+ // Windows:不写 shell rc,改用 setx 写系统环境变量
15
+ if (process.platform === 'win32') return []
16
+
13
17
  const shell = process.env.SHELL || ''
14
18
  const candidates = []
15
19
 
@@ -19,7 +23,7 @@ function getShellRcFiles() {
19
23
 
20
24
  // 默认兜底
21
25
  if (candidates.length === 0) {
22
- const zshrc = path.join(home, '.zshrc')
26
+ const zshrc = path.join(home, '.zshrc')
23
27
  const bashrc = path.join(home, '.bashrc')
24
28
  if (fs.existsSync(zshrc)) candidates.push(zshrc)
25
29
  if (fs.existsSync(bashrc)) candidates.push(bashrc)
@@ -47,6 +51,19 @@ function buildEnvBlock(envVars) {
47
51
  }
48
52
 
49
53
  function writeEnvToShell(envVars) {
54
+ // Windows: 用 setx 写入用户级环境变量
55
+ if (process.platform === 'win32') {
56
+ const { execSync } = require('child_process')
57
+ const written = []
58
+ for (const [k, v] of Object.entries(envVars)) {
59
+ try {
60
+ execSync(`setx ${k} "${v}"`, { stdio: 'ignore' })
61
+ written.push(`[System Env] ${k}`)
62
+ } catch {}
63
+ }
64
+ return written
65
+ }
66
+
50
67
  const files = getShellRcFiles()
51
68
  const written = []
52
69