@2api-store/activate 0.2.1 → 0.3.1
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/activate.mjs +174 -115
- package/lib/detect.mjs +48 -5
- package/lib/install.mjs +105 -0
- package/lib/platforms.mjs +109 -0
- package/lib/ui.mjs +91 -0
- package/package.json +4 -3
package/activate.mjs
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// 抑制 node DeprecationWarning(DEP0190 spawnSync shell 等)+ 透传子进程
|
|
3
|
+
process.removeAllListeners('warning')
|
|
4
|
+
process.env.NODE_NO_WARNINGS = '1'
|
|
2
5
|
/**
|
|
3
|
-
* @2api-store/activate — TokenShop CLI
|
|
6
|
+
* @2api-store/activate — TokenShop CLI 一键激活(v0.3.0)
|
|
4
7
|
*
|
|
5
|
-
*
|
|
6
|
-
* 1.
|
|
7
|
-
* 2. 输入凭据(sk-
|
|
8
|
-
* 3.
|
|
9
|
-
* 4.
|
|
10
|
-
* 5.
|
|
11
|
-
* 6.
|
|
12
|
-
* 7. 备份 →
|
|
13
|
-
* 8.
|
|
14
|
-
* 9.
|
|
8
|
+
* 主流程(薄编排,逻辑在各 lib 模块):
|
|
9
|
+
* 1. banner + 检测环境 + 列已装/未装 CLI
|
|
10
|
+
* 2. 输入凭据(sk- → API Key;否则兑换码 + email)
|
|
11
|
+
* 3. POST /api/v1/cli/activate → 配置包
|
|
12
|
+
* 4. 多选 CLI(主 CLI 默认选;opencode 默认不选;未装的标 ✗)
|
|
13
|
+
* 5. 未装 CLI → ensureInstalled(询问确认 → npm i -g)
|
|
14
|
+
* 6. 选默认模型
|
|
15
|
+
* 7. 备份 → 渲染 → 写入(每个文件彩色 ok/fail)
|
|
16
|
+
* 8. 验证 API Key(GET /v1/models)
|
|
17
|
+
* 9. 报告 + 打印启动/使用指南(每个激活 CLI 的 startCmd + usage)
|
|
15
18
|
*/
|
|
16
19
|
|
|
17
20
|
import { detectAll } from './lib/detect.mjs'
|
|
@@ -27,18 +30,16 @@ import {
|
|
|
27
30
|
confirm,
|
|
28
31
|
closePrompt,
|
|
29
32
|
} from './lib/prompt.mjs'
|
|
33
|
+
import { banner, ok, fail, warn, info, step, guide, plain } from './lib/ui.mjs'
|
|
34
|
+
import { getCliMeta, isDefaultSelected } from './lib/platforms.mjs'
|
|
35
|
+
import { ensureInstalled } from './lib/install.mjs'
|
|
30
36
|
|
|
31
|
-
const VERSION = '0.
|
|
37
|
+
const VERSION = '0.3.0'
|
|
32
38
|
const DEFAULT_API_BASE = 'https://2api.store'
|
|
33
39
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
| |_| | | | | || | _ / _ \ | _ \ | \| |
|
|
38
|
-
| _ | | |_| || |_| |/ ___ \| |_) | | |\ |
|
|
39
|
-
|_| |_| \___/ \____/_/ \_\____/ |_| \_|
|
|
40
|
-
TokenShop CLI 一键激活 v${VERSION}
|
|
41
|
-
`
|
|
40
|
+
// ============================================================
|
|
41
|
+
// 参数解析 / 帮助 / 凭据分流
|
|
42
|
+
// ============================================================
|
|
42
43
|
|
|
43
44
|
function parseArgs(argv) {
|
|
44
45
|
const args = { dryRun: false, platform: null, _: [] }
|
|
@@ -74,7 +75,7 @@ function printHelp() {
|
|
|
74
75
|
npx @2api-store/activate [options]
|
|
75
76
|
|
|
76
77
|
选项:
|
|
77
|
-
--dry-run, -n
|
|
78
|
+
--dry-run, -n 不真写配置/不真装 CLI,只展示将做什么
|
|
78
79
|
--platform <name> 只激活指定平台(openai/anthropic/gemini/grok/antigravity)
|
|
79
80
|
--api-base <url> 覆盖 API 端点(默认 $ACTIVATE_API_BASE 或 ${DEFAULT_API_BASE})
|
|
80
81
|
-h, --help 显示帮助
|
|
@@ -98,22 +99,17 @@ function printHelp() {
|
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
/**
|
|
101
|
-
*
|
|
102
|
-
* sk- 前缀 → key
|
|
103
|
-
* 32 字符 [a-zA-Z0-9] → redeem
|
|
102
|
+
* 凭据格式分流:sk- → key;16-64 字符 [a-zA-Z0-9] → redeem
|
|
104
103
|
*/
|
|
105
104
|
function classifyCredential(cred) {
|
|
106
105
|
if (!cred) return 'empty'
|
|
107
106
|
if (cred.startsWith('sk-')) return 'key'
|
|
108
|
-
// 兑换码:16-64 字符 [a-zA-Z0-9]
|
|
109
107
|
if (/^[a-zA-Z0-9]{16,64}$/.test(cred)) return 'redeem'
|
|
110
108
|
return 'unknown'
|
|
111
109
|
}
|
|
112
110
|
|
|
113
111
|
/**
|
|
114
|
-
* 从模型 ID
|
|
115
|
-
* 即使生产端点尚未部署过滤(0.314.0),脚本也按 key 分组实际可用模型平台过滤 configs,
|
|
116
|
-
* 避免给 codex key 列出 gemini/claude 等。
|
|
112
|
+
* 从模型 ID 列表推断平台集合(脚本兜底,与端点 inferPlatforms 对应)
|
|
117
113
|
*/
|
|
118
114
|
function inferModelPlatforms(models) {
|
|
119
115
|
const set = new Set()
|
|
@@ -127,6 +123,10 @@ function inferModelPlatforms(models) {
|
|
|
127
123
|
return [...set]
|
|
128
124
|
}
|
|
129
125
|
|
|
126
|
+
// ============================================================
|
|
127
|
+
// 主流程
|
|
128
|
+
// ============================================================
|
|
129
|
+
|
|
130
130
|
async function main() {
|
|
131
131
|
const args = parseArgs(process.argv.slice(2))
|
|
132
132
|
if (args.help) {
|
|
@@ -138,9 +138,8 @@ async function main() {
|
|
|
138
138
|
return 0
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
// === 1. 检测环境 ===
|
|
141
|
+
// === 1. banner + 环境检测 ===
|
|
142
|
+
banner()
|
|
144
143
|
const env = detectAll()
|
|
145
144
|
const osLabel = env.os.isMac
|
|
146
145
|
? 'macOS'
|
|
@@ -149,74 +148,79 @@ async function main() {
|
|
|
149
148
|
: env.os.isWindows
|
|
150
149
|
? 'Windows'
|
|
151
150
|
: env.os.platform
|
|
152
|
-
|
|
151
|
+
info(`环境:${osLabel} · node v${env.node.version}`)
|
|
153
152
|
if (!env.node.supported) {
|
|
154
|
-
|
|
153
|
+
fail(`需要 node >= 18,当前 ${env.node.version}`)
|
|
155
154
|
return 1
|
|
156
155
|
}
|
|
156
|
+
|
|
157
|
+
// 列出已装/未装 CLI(彩色)
|
|
157
158
|
const installedNames = Object.keys(env.installed)
|
|
159
|
+
const installedList = installedNames.map((n) => {
|
|
160
|
+
const v = env.installed[n]?.version
|
|
161
|
+
return v ? `${n}@${v}` : n
|
|
162
|
+
})
|
|
158
163
|
if (installedNames.length > 0) {
|
|
159
|
-
|
|
164
|
+
ok(`已检测到 CLI:${installedList.join(', ')}`)
|
|
160
165
|
} else {
|
|
161
|
-
|
|
166
|
+
info('未检测到 codex/claude/gemini/grok/opencode(可在后续步骤按需自动安装)')
|
|
162
167
|
}
|
|
163
168
|
|
|
164
169
|
const apiBase = args.apiBase || process.env.ACTIVATE_API_BASE || DEFAULT_API_BASE
|
|
165
|
-
|
|
166
|
-
if (args.dryRun)
|
|
167
|
-
if (args.platform)
|
|
170
|
+
info(`API 端点:${apiBase}`)
|
|
171
|
+
if (args.dryRun) warn('【试运行模式 --dry-run】不会真写文件、不会真装 CLI')
|
|
172
|
+
if (args.platform) info(`平台过滤:${args.platform}`)
|
|
168
173
|
console.log()
|
|
169
174
|
|
|
170
175
|
// === 2. 输入凭据 ===
|
|
171
176
|
const credential = (await askSecret('请输入 API Key(sk-...)或兑换码')).trim()
|
|
172
177
|
if (!credential) {
|
|
173
|
-
|
|
178
|
+
fail('未输入凭据')
|
|
174
179
|
return 1
|
|
175
180
|
}
|
|
176
181
|
const kind = classifyCredential(credential)
|
|
177
182
|
if (kind === 'unknown') {
|
|
178
|
-
|
|
183
|
+
fail('凭据格式无法识别(应为 sk- 前缀的 API Key 或兑换码)')
|
|
179
184
|
return 1
|
|
180
185
|
}
|
|
181
|
-
|
|
186
|
+
info(`识别为:${kind === 'key' ? 'API Key' : '兑换码'}模式`)
|
|
182
187
|
|
|
183
188
|
// === 3. 兑换码 → 邮箱 ===
|
|
184
189
|
let email = ''
|
|
185
190
|
if (kind === 'redeem') {
|
|
186
|
-
email = (await ask('请输入邮箱(用于兑换码绑定 /
|
|
191
|
+
email = (await ask('请输入邮箱(用于兑换码绑定 / 新账号创建)')).trim()
|
|
187
192
|
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
188
|
-
|
|
193
|
+
fail('邮箱格式不正确')
|
|
189
194
|
return 1
|
|
190
195
|
}
|
|
191
196
|
}
|
|
192
197
|
|
|
193
198
|
// === 4. POST /cli/activate ===
|
|
194
|
-
|
|
195
|
-
console.log('正在请求激活服务…')
|
|
199
|
+
step('请求激活服务')
|
|
196
200
|
let bundle
|
|
197
201
|
try {
|
|
198
202
|
bundle = await cliActivate({ credential, email, platform: args.platform, apiBase })
|
|
199
203
|
} catch (err) {
|
|
200
|
-
|
|
201
|
-
if (err.statusCode)
|
|
204
|
+
fail(`激活失败:${err.message}`)
|
|
205
|
+
if (err.statusCode) fail(` HTTP ${err.statusCode}`)
|
|
202
206
|
return 1
|
|
203
207
|
}
|
|
204
208
|
|
|
205
|
-
|
|
209
|
+
ok(`模式:${bundle.mode}${bundle.user?.is_new ? '(新用户已创建)' : ''}`)
|
|
206
210
|
if (bundle.user?.is_new && bundle.user?.email) {
|
|
207
|
-
|
|
208
|
-
if (bundle.user.message)
|
|
211
|
+
info(`新账号:${bundle.user.email}`)
|
|
212
|
+
if (bundle.user.message) info(bundle.user.message)
|
|
209
213
|
}
|
|
210
214
|
console.log()
|
|
211
215
|
|
|
212
|
-
// === 5.
|
|
216
|
+
// === 5. 过滤配置(os_type + 平台) + 多选 CLI(主 CLI 默认选,opencode 默认不选)===
|
|
213
217
|
const allowedPlatforms = inferModelPlatforms(bundle.models || [])
|
|
214
|
-
if (args.platform
|
|
215
|
-
|
|
218
|
+
if (args.platform && !allowedPlatforms.includes(args.platform)) {
|
|
219
|
+
allowedPlatforms.push(args.platform)
|
|
220
|
+
}
|
|
221
|
+
const osConfigs = (bundle.configs || []).filter((c) => {
|
|
216
222
|
if (!c) return false
|
|
217
|
-
// 按 key 分组可用模型平台过滤(兜底,与端点 inferPlatforms 一致;codex key 只列 openai)
|
|
218
223
|
if (allowedPlatforms.length > 0 && !allowedPlatforms.includes(c.platform)) return false
|
|
219
|
-
// 客户端 --platform 二次过滤(防御性)
|
|
220
224
|
if (args.platform && c.platform !== args.platform) return false
|
|
221
225
|
if (env.os.isWindows) {
|
|
222
226
|
return c.os_type === 'windows' || c.os_type === 'all'
|
|
@@ -225,93 +229,140 @@ async function main() {
|
|
|
225
229
|
})
|
|
226
230
|
|
|
227
231
|
if (osConfigs.length === 0) {
|
|
228
|
-
|
|
232
|
+
fail('服务端未返回适配当前系统的配置模板')
|
|
229
233
|
return 1
|
|
230
234
|
}
|
|
231
235
|
|
|
232
|
-
// dedupe by platform + client_type
|
|
236
|
+
// dedupe by platform + client_type,构建多选项
|
|
233
237
|
const seen = new Set()
|
|
234
238
|
const cliItems = []
|
|
235
239
|
for (const c of osConfigs) {
|
|
236
240
|
const key = `${c.platform}/${c.client_type}`
|
|
237
241
|
if (seen.has(key)) continue
|
|
238
242
|
seen.add(key)
|
|
239
|
-
const
|
|
243
|
+
const meta = getCliMeta(c.client_type)
|
|
244
|
+
const displayName = c.display_name || c.displayName || meta?.name || `${c.platform} ${c.client_type}`
|
|
245
|
+
const installed = env.installed[c.client_type]?.path || (meta && env.installed[meta.detectBin]?.path)
|
|
246
|
+
const installMark = installed ? '✓' : '✗'
|
|
247
|
+
const primaryMark = isDefaultSelected(c.client_type) ? '' : '(独立 CLI)'
|
|
240
248
|
cliItems.push({
|
|
241
|
-
label:
|
|
249
|
+
label: `[${installMark}] ${displayName} [${c.platform}/${c.client_type}]${primaryMark}`,
|
|
242
250
|
config: c,
|
|
251
|
+
clientType: c.client_type,
|
|
252
|
+
meta,
|
|
253
|
+
alreadyInstalled: !!installed,
|
|
243
254
|
})
|
|
244
255
|
}
|
|
245
256
|
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
257
|
+
// 默认选中:主 CLI(isPrimary=true)。dry-run 也用此默认(不因 dry-run 改默认)
|
|
258
|
+
const defaultSelected = cliItems
|
|
259
|
+
.map((item, i) => ({ item, i }))
|
|
260
|
+
.filter(({ item }) => isDefaultSelected(item.clientType))
|
|
261
|
+
.map(({ i }) => i)
|
|
262
|
+
|
|
263
|
+
step('选择 CLI')
|
|
264
|
+
const selectedIdxes = await multiSelect(
|
|
265
|
+
'选择要激活的 CLI(✓=已装 ✗=未装;默认选主 CLI,独立 CLI 默认不选)',
|
|
266
|
+
cliItems,
|
|
267
|
+
{ defaultSelected },
|
|
268
|
+
)
|
|
250
269
|
if (selectedIdxes.length === 0) {
|
|
251
|
-
|
|
270
|
+
plain('未选择任何 CLI,退出。')
|
|
252
271
|
return 0
|
|
253
272
|
}
|
|
254
|
-
const
|
|
273
|
+
const selectedItems = selectedIdxes.map((i) => cliItems[i])
|
|
255
274
|
console.log()
|
|
256
275
|
|
|
257
|
-
// === 6.
|
|
276
|
+
// === 6. 未装 CLI → 询问确认 → npm i -g(dry-run 跳过真装)===
|
|
277
|
+
if (!args.dryRun) {
|
|
278
|
+
step('检查 CLI 安装状态')
|
|
279
|
+
for (const item of selectedItems) {
|
|
280
|
+
if (item.alreadyInstalled) {
|
|
281
|
+
const ver = env.installed[item.clientType]?.version || env.installed[item.meta?.detectBin]?.version
|
|
282
|
+
ok(`${item.meta?.name || item.clientType} 已安装${ver ? `(v${ver})` : ''}`)
|
|
283
|
+
continue
|
|
284
|
+
}
|
|
285
|
+
if (!item.meta || !item.meta.installPackage) {
|
|
286
|
+
// 未知 CLI(如 grok 无官方 npm 包)→ 只提示不装
|
|
287
|
+
warn(`${item.clientType} 无可自动安装的 npm 包,请手动安装后重跑(配置仍会写入)`)
|
|
288
|
+
continue
|
|
289
|
+
}
|
|
290
|
+
info(`未检测到 ${item.meta.name}(命令 ${item.meta.detectBin})`)
|
|
291
|
+
const r = await ensureInstalled(item.meta, { dryRun: args.dryRun })
|
|
292
|
+
if (r.installed) {
|
|
293
|
+
ok(`${item.meta.name} 安装成功${r.version ? `(v${r.version})` : ''}`)
|
|
294
|
+
} else if (r.skipped && r.reason === 'user-declined') {
|
|
295
|
+
warn(`已跳过 ${item.meta.name} 安装(配置仍会写入,但请手动安装后使用)`)
|
|
296
|
+
} else if (r.skipped) {
|
|
297
|
+
info(`已跳过 ${item.meta.name} 安装(${r.reason})`)
|
|
298
|
+
} else {
|
|
299
|
+
fail(`${item.meta.name} 安装失败:${r.error || '未知错误'}(配置仍会写入,但请手动安装后使用)`)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
console.log()
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// === 7. 选默认模型 ===
|
|
258
306
|
const modelItems = listModels(bundle.models || [])
|
|
259
307
|
let defaultModelId = ''
|
|
260
308
|
if (modelItems.length > 0) {
|
|
261
|
-
|
|
309
|
+
step('选择默认模型')
|
|
262
310
|
const selectItems = modelItems.map((m) => ({ label: `${m.label} (${m.id})` }))
|
|
263
311
|
const idx = await select('选择默认模型', selectItems, { defaultIndex: 0 })
|
|
264
312
|
defaultModelId = modelItems[idx].id
|
|
265
|
-
|
|
313
|
+
ok(`已选默认模型:${defaultModelId}`)
|
|
266
314
|
} else {
|
|
267
|
-
|
|
315
|
+
warn('服务端未返回模型列表,{{defaultModel}} 将留空')
|
|
268
316
|
}
|
|
269
317
|
console.log()
|
|
270
318
|
|
|
271
|
-
//
|
|
319
|
+
// 渲染值
|
|
272
320
|
const baseUrl = bundle.base_url || ''
|
|
273
321
|
const baseUrlBeta = bundle.base_url_beta || baseUrl
|
|
274
322
|
const renderValues = {
|
|
275
323
|
apiKey: bundle.api_key || '',
|
|
276
324
|
baseUrl,
|
|
277
|
-
apiBase: baseUrl,
|
|
325
|
+
apiBase: baseUrl,
|
|
278
326
|
apiBaseBeta: baseUrlBeta,
|
|
279
327
|
antigravityApiBase: `${baseUrl}/antigravity`,
|
|
280
328
|
antigravityApiBaseBeta: `${baseUrlBeta}/antigravity`,
|
|
281
329
|
defaultModel: defaultModelId,
|
|
282
330
|
}
|
|
283
331
|
|
|
284
|
-
// ===
|
|
332
|
+
// === 8. 每个选中 CLI 的每个 file:备份 → 渲染 → 写入 ===
|
|
333
|
+
step('备份并写入配置')
|
|
285
334
|
const report = []
|
|
286
335
|
let firstPermissionError = null
|
|
287
336
|
|
|
288
|
-
for (const
|
|
337
|
+
for (const item of selectedItems) {
|
|
289
338
|
const displayName =
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const files =
|
|
339
|
+
item.config.display_name || item.config.displayName || item.meta?.name || item.clientType
|
|
340
|
+
plain(`── ${displayName} ──`)
|
|
341
|
+
const files = item.config.files || []
|
|
293
342
|
for (const f of files) {
|
|
294
343
|
const rawPath = f.path || ''
|
|
295
344
|
const isShellOnly = rawPath === 'Terminal' || rawPath === 'PowerShell'
|
|
296
|
-
const targetPath = isShellOnly
|
|
345
|
+
const targetPath = isShellOnly
|
|
346
|
+
? rawPath
|
|
347
|
+
: expandPath(rawPath, { isWindows: env.os.isWindows })
|
|
297
348
|
const rendered = renderContent(f.content || '', renderValues)
|
|
298
349
|
|
|
299
350
|
if (isShellOnly) {
|
|
300
|
-
|
|
301
|
-
|
|
351
|
+
plain(' [shell 命令,请手动执行]')
|
|
352
|
+
plain(rendered.split('\n').map((l) => ' ' + l).join('\n'))
|
|
302
353
|
report.push({ cli: displayName, path: rawPath, action: 'shell-only' })
|
|
303
354
|
continue
|
|
304
355
|
}
|
|
305
356
|
|
|
306
|
-
|
|
357
|
+
plain(` 目标:${targetPath}`)
|
|
307
358
|
|
|
308
359
|
if (args.dryRun) {
|
|
309
|
-
|
|
360
|
+
warn(' [试运行] 不真实写入')
|
|
310
361
|
const lines = rendered.split('\n')
|
|
311
362
|
const preview = lines.slice(0, 8).join('\n')
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
if (lines.length > 8)
|
|
363
|
+
plain(' 内容预览:')
|
|
364
|
+
plain(preview.split('\n').map((l) => ' ' + l).join('\n'))
|
|
365
|
+
if (lines.length > 8) plain(` ...(共 ${lines.length} 行)`)
|
|
315
366
|
report.push({ cli: displayName, path: targetPath, action: 'dry-run' })
|
|
316
367
|
console.log()
|
|
317
368
|
continue
|
|
@@ -321,30 +372,25 @@ async function main() {
|
|
|
321
372
|
let backupPath = null
|
|
322
373
|
try {
|
|
323
374
|
backupPath = backupFile(targetPath)
|
|
324
|
-
if (backupPath)
|
|
375
|
+
if (backupPath) plain(` 备份:${backupPath}`)
|
|
325
376
|
} catch (err) {
|
|
326
|
-
|
|
377
|
+
fail(`备份失败:${err.message}(跳过此文件避免无备份覆盖)`)
|
|
327
378
|
report.push({ cli: displayName, path: targetPath, action: 'backup-failed', error: err.message })
|
|
328
379
|
continue
|
|
329
380
|
}
|
|
330
381
|
|
|
331
|
-
//
|
|
382
|
+
// 写入
|
|
332
383
|
try {
|
|
333
384
|
writeFileWithPermission(targetPath, rendered, { isWindows: env.os.isWindows })
|
|
334
|
-
|
|
385
|
+
ok('已写入')
|
|
335
386
|
report.push({ cli: displayName, path: targetPath, action: 'written', backup: backupPath })
|
|
336
387
|
} catch (err) {
|
|
337
388
|
if (err instanceof PermissionDeniedError) {
|
|
338
389
|
if (!firstPermissionError) firstPermissionError = err
|
|
339
|
-
|
|
340
|
-
report.push({
|
|
341
|
-
cli: displayName,
|
|
342
|
-
path: targetPath,
|
|
343
|
-
action: 'permission-denied',
|
|
344
|
-
error: err.message,
|
|
345
|
-
})
|
|
390
|
+
fail(err.message)
|
|
391
|
+
report.push({ cli: displayName, path: targetPath, action: 'permission-denied', error: err.message })
|
|
346
392
|
} else {
|
|
347
|
-
|
|
393
|
+
fail(`写入失败:${err.message}`)
|
|
348
394
|
report.push({ cli: displayName, path: targetPath, action: 'failed', error: err.message })
|
|
349
395
|
}
|
|
350
396
|
}
|
|
@@ -354,22 +400,21 @@ async function main() {
|
|
|
354
400
|
|
|
355
401
|
closePrompt()
|
|
356
402
|
|
|
357
|
-
// ===
|
|
403
|
+
// === 9. 验证 API Key ===
|
|
358
404
|
if (!args.dryRun && bundle.api_key && bundle.base_url) {
|
|
359
|
-
|
|
405
|
+
step('验证 API Key')
|
|
406
|
+
info('请求 GET /v1/models…')
|
|
360
407
|
const v = await verifyApiKey({ apiKey: bundle.api_key, baseUrl: bundle.base_url })
|
|
361
408
|
if (v.ok) {
|
|
362
|
-
|
|
409
|
+
ok('API Key 验证通过(GET /v1/models → 200)')
|
|
363
410
|
} else {
|
|
364
|
-
|
|
365
|
-
`⚠ 验证未通过:${v.error || `HTTP ${v.statusCode}`}(配置已写入,请手动检查)`,
|
|
366
|
-
)
|
|
411
|
+
warn(`验证未通过:${v.error || `HTTP ${v.statusCode}`}(配置已写入,请手动检查)`)
|
|
367
412
|
}
|
|
368
413
|
console.log()
|
|
369
414
|
}
|
|
370
415
|
|
|
371
|
-
// ===
|
|
372
|
-
|
|
416
|
+
// === 10. 报告 ===
|
|
417
|
+
step('激活报告')
|
|
373
418
|
const written = report.filter((r) => r.action === 'written')
|
|
374
419
|
const failed = report.filter(
|
|
375
420
|
(r) => r.action === 'failed' || r.action === 'permission-denied' || r.action === 'backup-failed',
|
|
@@ -378,33 +423,46 @@ async function main() {
|
|
|
378
423
|
const dryRunItems = report.filter((r) => r.action === 'dry-run')
|
|
379
424
|
|
|
380
425
|
if (args.dryRun) {
|
|
381
|
-
|
|
426
|
+
plain(`[试运行] 将写入 ${dryRunItems.length} 个文件(未真实写入)`)
|
|
382
427
|
} else {
|
|
383
|
-
|
|
428
|
+
ok(`已写入 ${written.length} 个文件`)
|
|
384
429
|
}
|
|
385
430
|
if (shellOnly.length > 0) {
|
|
386
|
-
|
|
431
|
+
info(`需手动执行的 shell 命令:${shellOnly.length} 项(见上方输出)`)
|
|
387
432
|
}
|
|
388
433
|
if (failed.length > 0) {
|
|
389
|
-
|
|
390
|
-
failed.forEach((f) =>
|
|
434
|
+
warn(`失败 ${failed.length} 项:`)
|
|
435
|
+
failed.forEach((f) => warn(` - ${f.cli} ${f.path}`))
|
|
391
436
|
}
|
|
392
437
|
|
|
393
438
|
const backups = report.filter((r) => r.backup).map((r) => r.backup)
|
|
394
439
|
if (backups.length > 0) {
|
|
395
|
-
|
|
396
|
-
backups.forEach((b) =>
|
|
440
|
+
info('备份位置:')
|
|
441
|
+
backups.forEach((b) => plain(` - ${b}`))
|
|
397
442
|
}
|
|
398
443
|
|
|
399
444
|
if (bundle.user?.is_new) {
|
|
400
445
|
console.log()
|
|
401
|
-
|
|
446
|
+
info(`📧 新账号 ${bundle.user.email || ''} 已创建,请查收邮件获取初始密码,登录后尽快修改。`)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// === 11. 启动/使用指南(每个激活 CLI)===
|
|
450
|
+
if (!args.dryRun) {
|
|
451
|
+
const guideItems = selectedItems
|
|
452
|
+
.map((item) => item.meta)
|
|
453
|
+
.filter(Boolean)
|
|
454
|
+
.map((meta) => ({
|
|
455
|
+
name: meta.name,
|
|
456
|
+
startCmd: meta.startCmd,
|
|
457
|
+
usage: meta.usage,
|
|
458
|
+
}))
|
|
459
|
+
guide(guideItems)
|
|
402
460
|
}
|
|
403
461
|
|
|
404
462
|
if (firstPermissionError) {
|
|
405
463
|
console.log()
|
|
406
|
-
|
|
407
|
-
|
|
464
|
+
fail('部分文件权限不足,请按提示用 sudo / 管理员 PowerShell 重跑:')
|
|
465
|
+
fail(' sudo npx -y @2api-store/activate')
|
|
408
466
|
return 2
|
|
409
467
|
}
|
|
410
468
|
|
|
@@ -417,7 +475,8 @@ main()
|
|
|
417
475
|
process.exit(code || 0)
|
|
418
476
|
})
|
|
419
477
|
.catch((err) => {
|
|
420
|
-
|
|
478
|
+
fail(`未捕获错误:${err?.message || err}`)
|
|
479
|
+
if (err?.stack) console.error(err.stack)
|
|
421
480
|
closePrompt()
|
|
422
481
|
process.exit(1)
|
|
423
482
|
})
|
package/lib/detect.mjs
CHANGED
|
@@ -27,7 +27,8 @@ export function detectNode() {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* 已知 CLI
|
|
30
|
+
* 已知 CLI 列表(含 codex-ws 共享 codex 二进制)
|
|
31
|
+
* grok 仅检测(无官方 npm CLI 包,不参与自动安装)
|
|
31
32
|
*/
|
|
32
33
|
const KNOWN_CLIS = ['codex', 'claude', 'gemini', 'grok', 'opencode']
|
|
33
34
|
|
|
@@ -53,21 +54,63 @@ function findBinary(name, isWindows) {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
/**
|
|
56
|
-
*
|
|
57
|
+
* 查询二进制版本(--version 输出第一行)
|
|
58
|
+
* @param {string} bin 二进制名
|
|
57
59
|
* @param {boolean} isWindows
|
|
58
|
-
* @returns {
|
|
60
|
+
* @returns {string|null} 版本字符串(如 "1.2.3"),失败返回 null
|
|
61
|
+
*/
|
|
62
|
+
function getVersion(bin, isWindows) {
|
|
63
|
+
// --version 是通用约定;codex/claude/gemini/opencode/grok 都支持
|
|
64
|
+
try {
|
|
65
|
+
const result = spawnSync(isWindows ? `${bin}.cmd` : bin, ['--version'], {
|
|
66
|
+
encoding: 'utf8',
|
|
67
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
68
|
+
timeout: 5000,
|
|
69
|
+
// 不用 shell:true(node 22+ DEP0190);Windows 用 <bin>.cmd 直调
|
|
70
|
+
})
|
|
71
|
+
if (result.status === 0 && result.stdout) {
|
|
72
|
+
const firstLine = result.stdout.trim().split(/\r?\n/)[0]
|
|
73
|
+
// 提取常见版本格式:可能形如 "codex 0.1.2505162003" / "1.2.3" / "claude 2.1.0"
|
|
74
|
+
const match = firstLine.match(/(\d+\.\d+(?:\.\d+)?(?:[-.\w]*)?)/)
|
|
75
|
+
return match ? match[1] : firstLine.slice(0, 40)
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
// 二进制存在但 --version 不支持 / 超时 → 不阻塞,返回 null
|
|
79
|
+
}
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 检测单个二进制是否已安装(含版本)
|
|
85
|
+
* @param {string} bin 二进制名
|
|
86
|
+
* @returns {{installed: boolean, path: string|null, version: string|null}}
|
|
87
|
+
*/
|
|
88
|
+
export function detectInstalled(bin) {
|
|
89
|
+
const os = detectOS()
|
|
90
|
+
const path = findBinary(bin, os.isWindows)
|
|
91
|
+
if (!path) return { installed: false, path: null, version: null }
|
|
92
|
+
const version = getVersion(bin, os.isWindows)
|
|
93
|
+
return { installed: true, path, version }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 检测已安装的 CLI(批量)
|
|
98
|
+
* @param {boolean} isWindows
|
|
99
|
+
* @returns {Record<string, {path: string, version: string|null}>} name → info
|
|
59
100
|
*/
|
|
60
101
|
export function detectInstalledCLIs(isWindows) {
|
|
61
102
|
const installed = {}
|
|
62
103
|
for (const name of KNOWN_CLIS) {
|
|
63
104
|
const path = findBinary(name, isWindows)
|
|
64
|
-
if (path)
|
|
105
|
+
if (path) {
|
|
106
|
+
installed[name] = { path, version: getVersion(name, isWindows) }
|
|
107
|
+
}
|
|
65
108
|
}
|
|
66
109
|
return installed
|
|
67
110
|
}
|
|
68
111
|
|
|
69
112
|
/**
|
|
70
|
-
*
|
|
113
|
+
* 一次性检测全部环境(含已装 CLI 的版本)
|
|
71
114
|
*/
|
|
72
115
|
export function detectAll() {
|
|
73
116
|
const os = detectOS()
|
package/lib/install.mjs
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI 自动安装模块
|
|
3
|
+
*
|
|
4
|
+
* installCLI(meta) 执行 npm install -g(stdio inherit 展示 npm 进度)
|
|
5
|
+
* ensureInstalled(meta) 检测 + 询问确认 + 按需安装
|
|
6
|
+
*
|
|
7
|
+
* 设计:
|
|
8
|
+
* - 不自动安装(用户必须确认,避免后台改全局环境)
|
|
9
|
+
* - 安装用 spawnSync + stdio inherit(npm 进度直接透传,体验与手动 npm i 一致)
|
|
10
|
+
* - dry-run 模式不真装,只打印「将运行 npm install -g <pkg>」
|
|
11
|
+
* - 未识别包(无 meta / 无 installPackage)→ 返回 skipped,不阻塞主流程
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { spawnSync } from 'node:child_process'
|
|
15
|
+
import { detectInstalled } from './detect.mjs'
|
|
16
|
+
import { confirm } from './prompt.mjs'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 执行 npm install -g
|
|
20
|
+
* @param {{installPackage: string}} meta CLI 元数据
|
|
21
|
+
* @param {{dryRun?: boolean}} opts
|
|
22
|
+
* @returns {{ok: boolean, skipped?: boolean, reason?: string, error?: string}}
|
|
23
|
+
* - ok=true 安装成功(或 dry-run 跳过)
|
|
24
|
+
* - ok=false+error 安装失败(npm exit !=0 / spawn 错误)
|
|
25
|
+
* - skipped=true 因 dryRun 跳过实际安装
|
|
26
|
+
*/
|
|
27
|
+
export function installCLI(meta, opts = {}) {
|
|
28
|
+
if (!meta || !meta.installPackage) {
|
|
29
|
+
return { ok: false, skipped: true, reason: 'no-install-package' }
|
|
30
|
+
}
|
|
31
|
+
const pkg = meta.installPackage
|
|
32
|
+
const cmd = `npm install -g ${pkg}`
|
|
33
|
+
|
|
34
|
+
if (opts.dryRun) {
|
|
35
|
+
console.log(` [试运行] 将运行:${cmd}(不真实安装)`)
|
|
36
|
+
return { ok: true, skipped: true, reason: 'dry-run' }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log(` 正在执行:${cmd}…`)
|
|
40
|
+
try {
|
|
41
|
+
const result = spawnSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['install', '-g', pkg], {
|
|
42
|
+
stdio: 'inherit', // npm 进度/输出直接透传到当前终端
|
|
43
|
+
// 不用 shell:true(node 22+ DEP0190 警告 + 安全风险);Windows 用 npm.cmd 直调
|
|
44
|
+
})
|
|
45
|
+
if (result.status === 0) {
|
|
46
|
+
return { ok: true }
|
|
47
|
+
}
|
|
48
|
+
const errText = `npm exit ${result.status}`
|
|
49
|
+
return { ok: false, error: errText }
|
|
50
|
+
} catch (err) {
|
|
51
|
+
return { ok: false, error: err.message }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 确保 CLI 已安装:检测 → 未装则询问确认 → 安装
|
|
57
|
+
*
|
|
58
|
+
* @param {{detectBin?: string, installPackage?: string, name?: string, installCmd?: string}} meta CLI 元数据
|
|
59
|
+
* @param {{dryRun?: boolean, autoInstall?: boolean}} opts
|
|
60
|
+
* - dryRun 不真装
|
|
61
|
+
* - autoInstall true=未装时直接装(不问);false/缺省=询问确认后才装
|
|
62
|
+
* @returns {{installed: boolean, skipped?: boolean, reason?: string, version?: string|null, error?: string}}
|
|
63
|
+
* - installed=true 已装(或本次成功安装)
|
|
64
|
+
* - installed=false 未装且用户拒绝安装/安装失败/未知 meta
|
|
65
|
+
*/
|
|
66
|
+
export async function ensureInstalled(meta, opts = {}) {
|
|
67
|
+
if (!meta || !meta.installPackage) {
|
|
68
|
+
return { installed: false, skipped: true, reason: 'no-meta' }
|
|
69
|
+
}
|
|
70
|
+
const bin = meta.detectBin || meta.installPackage.split('/').pop()
|
|
71
|
+
const detectResult = detectInstalled(bin)
|
|
72
|
+
if (detectResult.installed) {
|
|
73
|
+
return { installed: true, version: detectResult.version }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 未装:决定是否安装
|
|
77
|
+
const { dryRun, autoInstall } = opts
|
|
78
|
+
|
|
79
|
+
// dry-run:直接走 installCLI 的 dry-run 分支(不问,因为不会真装)
|
|
80
|
+
if (dryRun) {
|
|
81
|
+
const r = installCLI(meta, { dryRun: true })
|
|
82
|
+
return { installed: false, skipped: true, reason: r.reason || 'dry-run' }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 非 dry-run:按 autoInstall 决定是否询问
|
|
86
|
+
let shouldInstall = autoInstall === true
|
|
87
|
+
if (!shouldInstall) {
|
|
88
|
+
shouldInstall = await confirm(
|
|
89
|
+
`未检测到 ${meta.name || bin}(命令 ${bin}),是否现在安装?\n 运行:${meta.installCmd || `npm install -g ${meta.installPackage}`}`,
|
|
90
|
+
{ defaultValue: true },
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!shouldInstall) {
|
|
95
|
+
return { installed: false, skipped: true, reason: 'user-declined' }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const r = installCLI(meta, { dryRun: false })
|
|
99
|
+
if (r.ok) {
|
|
100
|
+
// 安装后复查(确认二进制可解析)
|
|
101
|
+
const recheck = detectInstalled(bin)
|
|
102
|
+
return { installed: recheck.installed, version: recheck.version, justInstalled: true }
|
|
103
|
+
}
|
|
104
|
+
return { installed: false, error: r.error }
|
|
105
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI 元数据(按 client_type 索引)
|
|
3
|
+
*
|
|
4
|
+
* 用于:detect(识别已装/未装)、install(按需 npm i -g)、guide(启动/使用指南)。
|
|
5
|
+
*
|
|
6
|
+
* 字段约定:
|
|
7
|
+
* name 展示名(UI 用)
|
|
8
|
+
* installPackage npm 包名(npm i -g <pkg>)
|
|
9
|
+
* installCmd 完整安装命令字符串(UI 展示用,含 npm i -g)
|
|
10
|
+
* startCmd 启动命令(guide 用)
|
|
11
|
+
* detectBin 检测二进制名(which/where 用)
|
|
12
|
+
* configFiles 典型配置文件路径(仅展示参考,实际写入路径由服务端 configs 驱动)
|
|
13
|
+
* usage 使用提示文案(guide 用)
|
|
14
|
+
* isPrimary true=主 CLI(默认选中);false=独立 CLI(默认不选,如 opencode)
|
|
15
|
+
*
|
|
16
|
+
* npm 包名经 WebSearch 查证(2026-08-08):
|
|
17
|
+
* - @openai/codex 官方包(npm view @openai/codex → 0.147.0)
|
|
18
|
+
* - @anthropic-ai/claude-code 官方包(避开 imposter 包 claude-ai)→ 2.1.226
|
|
19
|
+
* - @google/gemini-cli 官方包 → 0.54.4
|
|
20
|
+
* - opencode-ai SST 官方包(非 @opencode-ai/cli)→ 1.18.15
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export const CLI_META = {
|
|
24
|
+
codex: {
|
|
25
|
+
name: 'Codex CLI',
|
|
26
|
+
installPackage: '@openai/codex',
|
|
27
|
+
installCmd: 'npm install -g @openai/codex',
|
|
28
|
+
startCmd: 'codex',
|
|
29
|
+
detectBin: 'codex',
|
|
30
|
+
configFiles: ['~/.codex/config.toml', '~/.codex/auth.json'],
|
|
31
|
+
usage: [
|
|
32
|
+
'权限模式(默认每步确认):',
|
|
33
|
+
' codex --full-auto 全自动(sandbox 内自动批准,推荐)',
|
|
34
|
+
' codex --yolo 全跳过(无 sandbox 无 approval,仅信任仓库)',
|
|
35
|
+
' codex --sandbox read-only --ask-for-approval never 只读(最安全)',
|
|
36
|
+
' codex --help 查看所有选项',
|
|
37
|
+
'项目根放 AGENTS.md 让 Codex 了解你的代码库',
|
|
38
|
+
].join('\n'),
|
|
39
|
+
isPrimary: true,
|
|
40
|
+
},
|
|
41
|
+
'codex-ws': {
|
|
42
|
+
// WebSocket 变体:与 codex 同包同二进制,仅服务端配置不同
|
|
43
|
+
name: 'Codex CLI (WebSocket)',
|
|
44
|
+
installPackage: '@openai/codex',
|
|
45
|
+
installCmd: 'npm install -g @openai/codex',
|
|
46
|
+
startCmd: 'codex',
|
|
47
|
+
detectBin: 'codex',
|
|
48
|
+
configFiles: ['~/.codex/config.toml', '~/.codex/auth.json'],
|
|
49
|
+
usage: [
|
|
50
|
+
'权限模式(WebSocket 模式由服务端配置激活,默认每步确认):',
|
|
51
|
+
' codex --full-auto 全自动(sandbox 内自动批准,推荐)',
|
|
52
|
+
' codex --yolo 全跳过(无 sandbox 无 approval,仅信任仓库)',
|
|
53
|
+
' codex --sandbox read-only --ask-for-approval never 只读(最安全)',
|
|
54
|
+
' codex --help 查看所有选项',
|
|
55
|
+
].join('\n'),
|
|
56
|
+
isPrimary: true,
|
|
57
|
+
},
|
|
58
|
+
claude: {
|
|
59
|
+
name: 'Claude Code',
|
|
60
|
+
installPackage: '@anthropic-ai/claude-code',
|
|
61
|
+
installCmd: 'npm install -g @anthropic-ai/claude-code',
|
|
62
|
+
startCmd: 'claude',
|
|
63
|
+
detectBin: 'claude',
|
|
64
|
+
configFiles: ['~/.claude/', '~/.claude.json'],
|
|
65
|
+
usage: '运行 `claude` 开始;`claude --help` 查看选项;在项目根放 CLAUDE.md 让 Claude 了解你的代码库',
|
|
66
|
+
isPrimary: true,
|
|
67
|
+
},
|
|
68
|
+
'gemini-cli': {
|
|
69
|
+
name: 'Gemini CLI',
|
|
70
|
+
installPackage: '@google/gemini-cli',
|
|
71
|
+
installCmd: 'npm install -g @google/gemini-cli',
|
|
72
|
+
startCmd: 'gemini',
|
|
73
|
+
detectBin: 'gemini',
|
|
74
|
+
configFiles: ['~/.gemini/settings.json'],
|
|
75
|
+
usage: '运行 `gemini` 开始;`gemini --help` 查看选项;在项目根放 GEMINI.md 让 Gemini 了解你的代码库',
|
|
76
|
+
isPrimary: true,
|
|
77
|
+
},
|
|
78
|
+
opencode: {
|
|
79
|
+
// 独立 CLI(非主 CLI),默认不选
|
|
80
|
+
name: 'OpenCode',
|
|
81
|
+
installPackage: 'opencode-ai',
|
|
82
|
+
installCmd: 'npm install -g opencode-ai',
|
|
83
|
+
startCmd: 'opencode',
|
|
84
|
+
detectBin: 'opencode',
|
|
85
|
+
configFiles: ['~/.config/opencode/opencode.json', 'opencode.json'],
|
|
86
|
+
usage: '运行 `opencode` 开始;`opencode --help` 查看选项;在项目根放 AGENTS.md 让 OpenCode 了解你的代码库',
|
|
87
|
+
isPrimary: false,
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 按 client_type 查询元数据,未定义返回 null
|
|
93
|
+
* @param {string} clientType
|
|
94
|
+
* @returns {object|null}
|
|
95
|
+
*/
|
|
96
|
+
export function getCliMeta(clientType) {
|
|
97
|
+
return CLI_META[clientType] || null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 判断 client_type 是否默认应选中(主 CLI 默认选,独立 CLI 默认不选)
|
|
102
|
+
* @param {string} clientType
|
|
103
|
+
* @returns {boolean}
|
|
104
|
+
*/
|
|
105
|
+
export function isDefaultSelected(clientType) {
|
|
106
|
+
const meta = CLI_META[clientType]
|
|
107
|
+
if (!meta) return true // 未知类型保守全选(向后兼容)
|
|
108
|
+
return meta.isPrimary
|
|
109
|
+
}
|
package/lib/ui.mjs
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 彩色 UI 输出(chalk 驱动)
|
|
3
|
+
*
|
|
4
|
+
* 颜色规范:
|
|
5
|
+
* banner 粗体青色(品牌色)
|
|
6
|
+
* ✓ ok 绿色(成功)
|
|
7
|
+
* ✗ fail 红色(错误)
|
|
8
|
+
* ⚠ warn 黄色(警告)
|
|
9
|
+
* ℹ info 蓝色(信息)
|
|
10
|
+
* step 粗体青色(步骤标题)
|
|
11
|
+
* guide 绿色(启动/使用指南)
|
|
12
|
+
*
|
|
13
|
+
* chalk 在非 TTY(CI/管道/重定向)/ Windows cmd 自动降级为无色,无需手动处理。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import chalk from 'chalk'
|
|
17
|
+
|
|
18
|
+
const PKG_VERSION = '0.3.1'
|
|
19
|
+
|
|
20
|
+
const BANNER = String.raw`
|
|
21
|
+
_ _ _ _ ____ _ ____ _ _
|
|
22
|
+
| | | | | | | | / ___| / \ | __ ) | \ | |
|
|
23
|
+
| |_| | | | | || | _ / _ \ | _ \ | \| |
|
|
24
|
+
| _ | | |_| || |_| |/ ___ \| |_) | | |\ |
|
|
25
|
+
|_| |_| \___/ \____/_/ \_\____/ |_| \_|
|
|
26
|
+
TokenShop CLI 一键激活 v${PKG_VERSION}
|
|
27
|
+
`
|
|
28
|
+
|
|
29
|
+
/** 彩色 banner */
|
|
30
|
+
export function banner() {
|
|
31
|
+
// 去除模板字面量首尾换行,保留 ASCII art 结构
|
|
32
|
+
console.log(chalk.bold.cyan(BANNER.replace(/^\n/, '').trimEnd()))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 成功(✓ 绿色) */
|
|
36
|
+
export function ok(msg) {
|
|
37
|
+
console.log(chalk.green(`✓ ${msg}`))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 错误(✗ 红色 → stderr) */
|
|
41
|
+
export function fail(msg) {
|
|
42
|
+
console.error(chalk.red(`✗ ${msg}`))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 警告(⚠ 黄色) */
|
|
46
|
+
export function warn(msg) {
|
|
47
|
+
console.warn(chalk.yellow(`⚠ ${msg}`))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 信息(ℹ 蓝色) */
|
|
51
|
+
export function info(msg) {
|
|
52
|
+
console.log(chalk.blue(`ℹ ${msg}`))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 步骤标题(粗体青色 + 框线分隔)
|
|
57
|
+
* @param {string} title
|
|
58
|
+
*/
|
|
59
|
+
export function step(title) {
|
|
60
|
+
console.log()
|
|
61
|
+
console.log(chalk.bold.cyan(`■ ${title}`))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* CLI 启动/使用指南(彩色框)
|
|
66
|
+
* 渲染每个已激活 CLI 的 startCmd + usage
|
|
67
|
+
* @param {Array<{name: string, startCmd: string, usage: string}>} items
|
|
68
|
+
*/
|
|
69
|
+
export function guide(items) {
|
|
70
|
+
if (!items || items.length === 0) return
|
|
71
|
+
step('启动 / 使用指南')
|
|
72
|
+
for (const it of items) {
|
|
73
|
+
const head = chalk.bold.green(` ▶ ${it.name}`)
|
|
74
|
+
const start = chalk.green(` 启动:${it.startCmd}`)
|
|
75
|
+
const usage = chalk.dim(it.usage || '')
|
|
76
|
+
console.log(head)
|
|
77
|
+
console.log(start)
|
|
78
|
+
if (usage) console.log(usage)
|
|
79
|
+
console.log()
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 普通纯文本输出(无颜色修饰,用于已经内嵌颜色的字符串或纯文本)
|
|
85
|
+
*/
|
|
86
|
+
export function plain(msg) {
|
|
87
|
+
console.log(msg)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 暴露 chalk 给调用方(复杂场景按需用) */
|
|
91
|
+
export { chalk }
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@2api-store/activate",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "TokenShop CLI 一键激活:配置本地 codex/claude/gemini/grok/opencode 指向 2api.store
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "TokenShop CLI 一键激活:配置本地 codex/claude/gemini/grok/opencode 指向 2api.store 中转(彩色输出 + 自动安装 CLI)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"activate": "./activate.mjs"
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
],
|
|
33
33
|
"author": "TokenShop",
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@inquirer/prompts": "^7.0.0"
|
|
35
|
+
"@inquirer/prompts": "^7.0.0",
|
|
36
|
+
"chalk": "^5.4.0"
|
|
36
37
|
},
|
|
37
38
|
"license": "MIT",
|
|
38
39
|
"publishConfig": {
|