@simonyea/holysheep-cli 1.0.2 → 1.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonyea/holysheep-cli",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
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
 
@@ -1,54 +1,43 @@
1
1
  /**
2
- * OpenAI Codex CLI 适配器
3
- * 配置文件: ~/.codex/config.yaml 或 ~/.codex/config.json
4
- * 环境变量: OPENAI_API_KEY, OPENAI_BASE_URL
2
+ * Codex CLI 适配器 (@openai/codex v0.46+)
5
3
  *
6
- * Codex 支持 custom provider 配置:
7
- * providers:
8
- * - name: HolySheep
9
- * baseURL: https://api.holysheep.ai/v1
10
- * envKey: OPENAI_API_KEY
4
+ * 配置文件: ~/.codex/config.json(JSON 格式,不是 yaml)
11
5
  *
12
- * 注意: Codex 用 OpenAI 兼容格式,baseURL 需带 /v1
6
+ * 正确格式:
7
+ * {
8
+ * "model": "claude-sonnet-4-5",
9
+ * "provider": "holysheep", // 指定默认 provider
10
+ * "providers": {
11
+ * "holysheep": {
12
+ * "name": "HolySheep",
13
+ * "baseURL": "https://api.holysheep.ai/v1",
14
+ * "envKey": "OPENAI_API_KEY"
15
+ * }
16
+ * }
17
+ * }
18
+ *
19
+ * 环境变量: OPENAI_API_KEY(通过 envKey 指定)
20
+ * 注意: Codex 会优先使用账号登录,需要设置 provider 才能绕过
13
21
  */
14
- const fs = require('fs')
22
+ const fs = require('fs')
15
23
  const path = require('path')
16
- const os = require('os')
24
+ const os = require('os')
17
25
 
18
26
  const CONFIG_DIR = path.join(os.homedir(), '.codex')
19
- const CONFIG_YAML = path.join(CONFIG_DIR, 'config.yaml')
20
- const CONFIG_JSON = path.join(CONFIG_DIR, 'config.json')
27
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
21
28
 
22
29
  function readConfig() {
23
30
  try {
24
- if (fs.existsSync(CONFIG_YAML)) return { type: 'yaml', content: fs.readFileSync(CONFIG_YAML, 'utf8') }
25
- if (fs.existsSync(CONFIG_JSON)) return { type: 'json', content: fs.readFileSync(CONFIG_JSON, 'utf8') }
31
+ if (fs.existsSync(CONFIG_FILE)) {
32
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'))
33
+ }
26
34
  } catch {}
27
- return { type: 'yaml', content: '' }
35
+ return {}
28
36
  }
29
37
 
30
- function removeHolysheepProvider(yamlContent) {
31
- // 简单移除已有的 holysheep provider
32
- const lines = yamlContent.split('\n')
33
- const result = []
34
- let skip = false
35
- for (const line of lines) {
36
- if (line.includes('HolySheep') || line.includes('holysheep')) {
37
- skip = true
38
- // 也移除上一行的 `- name:` 前缀
39
- if (result.length && result[result.length - 1].trim().startsWith('- name:')) {
40
- result.pop()
41
- }
42
- continue
43
- }
44
- if (skip && (line.startsWith(' ') || line.trim() === '')) {
45
- if (line.trim() === '') skip = false
46
- continue
47
- }
48
- skip = false
49
- result.push(line)
50
- }
51
- return result.join('\n')
38
+ function writeConfig(data) {
39
+ if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true })
40
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), 'utf8')
52
41
  }
53
42
 
54
43
  module.exports = {
@@ -56,62 +45,57 @@ module.exports = {
56
45
  id: 'codex',
57
46
  checkInstalled() {
58
47
  try {
59
- require('child_process').execSync('which codex', { stdio: 'ignore' })
48
+ require('child_process').execSync('codex --version', { stdio: 'ignore' })
60
49
  return true
61
- } catch { return false }
50
+ } catch {
51
+ try {
52
+ require('child_process').execSync('npx @openai/codex --version', { stdio: 'ignore' })
53
+ return true
54
+ } catch { return false }
55
+ }
62
56
  },
63
57
  isConfigured() {
64
- const { content } = readConfig()
65
- return content.includes('holysheep') || content.includes('HolySheep')
58
+ const c = readConfig()
59
+ return c.provider === 'holysheep' &&
60
+ !!c.providers?.holysheep?.baseURL?.includes('holysheep')
66
61
  },
67
- configure(apiKey, baseUrlOpenAI) {
68
- if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true })
62
+ configure(apiKey, _baseUrlAnthropicNoV1, baseUrlOpenAI) {
63
+ const config = readConfig()
69
64
 
70
- // 生成 YAML 格式配置(Codex 官方推荐)
71
- let content = ''
72
- if (fs.existsSync(CONFIG_YAML)) {
73
- content = fs.readFileSync(CONFIG_YAML, 'utf8')
74
- content = removeHolysheepProvider(content)
75
- }
65
+ // 设置 HolySheep 为默认 provider
66
+ config.provider = 'holysheep'
67
+ config.model = config.model || 'claude-sonnet-4-5'
76
68
 
77
- // 追加 holysheep provider + 设为默认 model provider
78
- const providerBlock = `
79
- # HolySheep API — https://shop.holysheep.ai
80
- providers:
81
- - name: HolySheep
82
- baseURL: ${baseUrlOpenAI}
83
- envKey: OPENAI_API_KEY
84
- model: claude-sonnet-4-5
85
- `
86
- // 如果已有 providers 块,改为追加到列表
87
- if (content.includes('providers:')) {
88
- content = content.replace('providers:', `providers:\n - name: HolySheep\n baseURL: ${baseUrlOpenAI}\n envKey: OPENAI_API_KEY`)
89
- } else {
90
- content += providerBlock
69
+ if (!config.providers) config.providers = {}
70
+ config.providers.holysheep = {
71
+ name: 'HolySheep',
72
+ baseURL: baseUrlOpenAI, // https://api.holysheep.ai/v1
73
+ envKey: 'OPENAI_API_KEY',
91
74
  }
92
75
 
93
- fs.writeFileSync(CONFIG_YAML, content.trim() + '\n', 'utf8')
76
+ writeConfig(config)
94
77
 
95
- // 同时写入环境变量(Codex 通过 envKey 读取)
96
78
  return {
97
- file: CONFIG_YAML,
98
- hot: false,
79
+ file: CONFIG_FILE,
80
+ hot: false,
81
+ // 需要同时设置环境变量,供 envKey 读取
99
82
  envVars: {
100
- OPENAI_API_KEY: apiKey,
101
- OPENAI_BASE_URL: baseUrlOpenAI,
83
+ OPENAI_API_KEY: apiKey,
84
+ OPENAI_BASE_URL: baseUrlOpenAI,
102
85
  },
103
86
  }
104
87
  },
105
88
  reset() {
106
- if (fs.existsSync(CONFIG_YAML)) {
107
- let content = fs.readFileSync(CONFIG_YAML, 'utf8')
108
- content = removeHolysheepProvider(content)
109
- fs.writeFileSync(CONFIG_YAML, content, 'utf8')
89
+ const config = readConfig()
90
+ if (config.provider === 'holysheep') {
91
+ delete config.provider
92
+ delete config.providers?.holysheep
110
93
  }
94
+ writeConfig(config)
111
95
  },
112
- getConfigPath() { return CONFIG_YAML },
113
- hint: '切换后需重启终端或新开 terminal',
96
+ getConfigPath() { return CONFIG_FILE },
97
+ hint: '切换后重开终端生效;用 codex --provider holysheep 指定',
114
98
  installCmd: 'npm install -g @openai/codex',
115
99
  docsUrl: 'https://github.com/openai/codex',
116
- envVarFormat: 'openai', // 告知 setup 命令写哪些 env vars
100
+ envVarFormat: 'openai',
117
101
  }
@@ -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