@simonyea/holysheep-cli 1.0.1 → 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 +2 -2
- package/src/commands/setup.js +113 -23
- package/src/utils/shell.js +18 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simonyea/holysheep-cli",
|
|
3
|
-
"version": "1.0.
|
|
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",
|
|
@@ -39,4 +39,4 @@
|
|
|
39
39
|
"node-fetch": "^2.7.0",
|
|
40
40
|
"ora": "^5.4.1"
|
|
41
41
|
}
|
|
42
|
-
}
|
|
42
|
+
}
|
package/src/commands/setup.js
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
89
|
-
const needsEnvVars =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
package/src/utils/shell.js
CHANGED
|
@@ -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
|
|
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
|
|