@2api-store/activate 0.5.2 → 0.6.0
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/dist/activate.mjs +2 -0
- package/dist/lib/api.mjs +1 -0
- package/dist/lib/backup.mjs +1 -0
- package/dist/lib/cmds.mjs +1 -0
- package/dist/lib/detect.mjs +1 -0
- package/dist/lib/device.mjs +1 -0
- package/dist/lib/install.mjs +1 -0
- package/dist/lib/platforms.mjs +1 -0
- package/dist/lib/pow.mjs +1 -0
- package/dist/lib/prompt.mjs +1 -0
- package/dist/lib/render.mjs +1 -0
- package/dist/lib/ui.mjs +1 -0
- package/dist/lib/write.mjs +1 -0
- package/dist/package.json +5 -0
- package/package.json +10 -5
- package/activate.mjs +0 -540
- package/lib/api.mjs +0 -184
- package/lib/backup.mjs +0 -109
- package/lib/cmds.mjs +0 -546
- package/lib/detect.mjs +0 -122
- package/lib/install.mjs +0 -110
- package/lib/platforms.mjs +0 -123
- package/lib/prompt.mjs +0 -160
- package/lib/render.mjs +0 -94
- package/lib/ui.mjs +0 -90
- package/lib/write.mjs +0 -57
package/lib/cmds.mjs
DELETED
|
@@ -1,546 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 子命令实现:restore / reset / refresh-groups
|
|
3
|
-
*
|
|
4
|
-
* 设计原则:
|
|
5
|
-
* - 每个子命令是独立 async 函数,接收 argv 数组(不含子命令名本身)
|
|
6
|
-
* - 返回退出码(0 成功 / 1 输入错误 / 2 权限 / 3 写入失败)
|
|
7
|
-
* - 自包含:解析自己的 argv + 走完整流程
|
|
8
|
-
* - 操作配置文件前都备份(reset 删前备份)
|
|
9
|
-
* - 不复用 activate.mjs 的 main()(避免耦合,main 包含 redeem/CLI 多选/安装等复杂流程)
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import fs from 'node:fs'
|
|
13
|
-
import path from 'node:path'
|
|
14
|
-
import os from 'node:os'
|
|
15
|
-
|
|
16
|
-
import { backupFile, listBackups, restoreBackup } from './backup.mjs'
|
|
17
|
-
import { cliActivate, listGroups } from './api.mjs'
|
|
18
|
-
import { writeFileWithPermission, PermissionDeniedError } from './write.mjs'
|
|
19
|
-
import { renderContent, expandPath, listModels } from './render.mjs'
|
|
20
|
-
import { getCliMeta, isDefaultSelected } from './platforms.mjs'
|
|
21
|
-
import { detectAll } from './detect.mjs'
|
|
22
|
-
import {
|
|
23
|
-
ask,
|
|
24
|
-
askSecret,
|
|
25
|
-
multiSelect,
|
|
26
|
-
select,
|
|
27
|
-
confirm,
|
|
28
|
-
closePrompt,
|
|
29
|
-
} from './prompt.mjs'
|
|
30
|
-
import { banner, ok, fail, warn, info, step, plain, guide, chalk } from './ui.mjs'
|
|
31
|
-
|
|
32
|
-
// ============================================================
|
|
33
|
-
// 常量
|
|
34
|
-
// ============================================================
|
|
35
|
-
|
|
36
|
-
const DEFAULT_API_BASE = 'https://2api.store'
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* 2api.store 写入的配置文件清单(reset 用)
|
|
40
|
-
* 注意:~/.claude/settings.json 整文件删除(如用户有自定义设置,先备份后删,
|
|
41
|
-
* 若存在 .bak.* 则恢复到删除前的官方状态)
|
|
42
|
-
*/
|
|
43
|
-
const MANAGED_CONFIG_FILES_UNIX = [
|
|
44
|
-
'~/.codex/config.toml',
|
|
45
|
-
'~/.codex/auth.json',
|
|
46
|
-
'~/.claude/settings.json',
|
|
47
|
-
'~/.gemini/settings.json',
|
|
48
|
-
]
|
|
49
|
-
|
|
50
|
-
const MANAGED_CONFIG_FILES_WIN = [
|
|
51
|
-
'%userprofile%\\.codex\\config.toml',
|
|
52
|
-
'%userprofile%\\.codex\\auth.json',
|
|
53
|
-
'%userprofile%\\.claude\\settings.json',
|
|
54
|
-
'%userprofile%\\.gemini\\settings.json',
|
|
55
|
-
]
|
|
56
|
-
|
|
57
|
-
/** 扫描备份文件的目录 */
|
|
58
|
-
const BACKUP_SCAN_DIRS_UNIX = ['~/.codex', '~/.claude', '~/.gemini']
|
|
59
|
-
const BACKUP_SCAN_DIRS_WIN = [
|
|
60
|
-
'%userprofile%\\.codex',
|
|
61
|
-
'%userprofile%\\.claude',
|
|
62
|
-
'%userprofile%\\.gemini',
|
|
63
|
-
]
|
|
64
|
-
|
|
65
|
-
// ============================================================
|
|
66
|
-
// 工具:解析子命令自己的 argv(--api-base / --dry-run / --help)
|
|
67
|
-
// ============================================================
|
|
68
|
-
|
|
69
|
-
function parseSubArgs(argv) {
|
|
70
|
-
const args = { dryRun: false, _: [] }
|
|
71
|
-
for (let i = 0; i < argv.length; i++) {
|
|
72
|
-
const a = argv[i]
|
|
73
|
-
if (a === '--dry-run' || a === '-n') {
|
|
74
|
-
args.dryRun = true
|
|
75
|
-
} else if (a === '--api-base') {
|
|
76
|
-
args.apiBase = argv[++i]
|
|
77
|
-
} else if (a === '--help' || a === '-h') {
|
|
78
|
-
args.help = true
|
|
79
|
-
} else if (a.startsWith('--') && a.includes('=')) {
|
|
80
|
-
const [k, v] = a.slice(2).split(/=(.*)/, 2)
|
|
81
|
-
args[k] = v
|
|
82
|
-
} else if (a.startsWith('--')) {
|
|
83
|
-
args[a.replace(/^--/, '')] = argv[++i]
|
|
84
|
-
} else {
|
|
85
|
-
args._.push(a)
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return args
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function resolveHome(p, isWindows) {
|
|
92
|
-
return expandPath(p, { isWindows })
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function getManagedFiles(isWindows) {
|
|
96
|
-
return (isWindows ? MANAGED_CONFIG_FILES_WIN : MANAGED_CONFIG_FILES_UNIX).map((p) =>
|
|
97
|
-
resolveHome(p, isWindows),
|
|
98
|
-
)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function getBackupScanDirs(isWindows) {
|
|
102
|
-
return (isWindows ? BACKUP_SCAN_DIRS_WIN : BACKUP_SCAN_DIRS_UNIX).map((p) =>
|
|
103
|
-
resolveHome(p, isWindows),
|
|
104
|
-
)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** 格式化时间为本地可读格式 */
|
|
108
|
-
function fmtTime(ms) {
|
|
109
|
-
try {
|
|
110
|
-
const d = new Date(ms)
|
|
111
|
-
const pad = (n) => String(n).padStart(2, '0')
|
|
112
|
-
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
|
113
|
-
} catch {
|
|
114
|
-
return new Date(ms).toISOString()
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** 格式化文件大小 */
|
|
119
|
-
function fmtSize(bytes) {
|
|
120
|
-
if (bytes < 1024) return `${bytes}B`
|
|
121
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`
|
|
122
|
-
return `${(bytes / 1024 / 1024).toFixed(2)}MB`
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const SUB_HELP = `
|
|
126
|
-
子命令:
|
|
127
|
-
restore 从备份恢复(扫描 ~/.codex / ~/.claude / ~/.gemini 下的 .bak.* 文件)
|
|
128
|
-
reset 恢复官方默认(删 2api.store 写入的配置,并从最新备份恢复)
|
|
129
|
-
refresh-groups 刷新分组列表(只展示,不改 key/配置)
|
|
130
|
-
|
|
131
|
-
所有子命令都支持 --api-base <url> 覆盖端点,restore/reset 支持 --dry-run。
|
|
132
|
-
`.trim()
|
|
133
|
-
|
|
134
|
-
// ============================================================
|
|
135
|
-
// restore:从备份恢复
|
|
136
|
-
// ============================================================
|
|
137
|
-
|
|
138
|
-
async function cmdRestore(argv) {
|
|
139
|
-
const args = parseSubArgs(argv)
|
|
140
|
-
if (args.help) {
|
|
141
|
-
console.log(`npx @2api-store/activate restore [options]\n\n${SUB_HELP}`)
|
|
142
|
-
return 0
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
banner()
|
|
146
|
-
const env = detectAll()
|
|
147
|
-
if (!env.node.supported) {
|
|
148
|
-
fail(`需要 node >= 18,当前 ${env.node.version}`)
|
|
149
|
-
return 1
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
step('扫描备份文件')
|
|
153
|
-
const dirs = getBackupScanDirs(env.os.isWindows)
|
|
154
|
-
if (args.dryRun) warn('【试运行模式 --dry-run】不会真实恢复文件')
|
|
155
|
-
info(`扫描目录:${dirs.join(', ')}`)
|
|
156
|
-
|
|
157
|
-
const backups = listBackups(dirs)
|
|
158
|
-
if (backups.length === 0) {
|
|
159
|
-
info('未找到任何备份文件(.bak.*)')
|
|
160
|
-
info('提示:只有运行过 npx @2api-store/activate 后才会产生备份文件')
|
|
161
|
-
return 0
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
ok(`找到 ${backups.length} 个备份文件`)
|
|
165
|
-
|
|
166
|
-
// 按应用分组(根据 origPath 的父目录)
|
|
167
|
-
const APP_LABELS = {
|
|
168
|
-
'.codex': 'Codex CLI',
|
|
169
|
-
'.claude': 'Claude Code',
|
|
170
|
-
'.gemini': 'Gemini CLI',
|
|
171
|
-
}
|
|
172
|
-
function appLabel(origPath) {
|
|
173
|
-
const home = process.env.HOME || process.env.USERPROFILE || ''
|
|
174
|
-
const rel = path.relative(home, origPath).replace(/\\/g, '/')
|
|
175
|
-
const firstSeg = rel.split('/')[0]
|
|
176
|
-
return APP_LABELS[firstSeg] || firstSeg
|
|
177
|
-
}
|
|
178
|
-
function appDir(origPath) {
|
|
179
|
-
const home = process.env.HOME || process.env.USERPROFILE || ''
|
|
180
|
-
return path.dirname(origPath)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// 按应用(目录)分组
|
|
184
|
-
const byApp = {}
|
|
185
|
-
for (const b of backups) {
|
|
186
|
-
const dir = appDir(b.origPath)
|
|
187
|
-
if (!byApp[dir]) byApp[dir] = []
|
|
188
|
-
byApp[dir].push(b)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const appDirs = Object.keys(byApp)
|
|
192
|
-
if (appDirs.length === 0) {
|
|
193
|
-
info('无可恢复的备份')
|
|
194
|
-
return 0
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
console.log()
|
|
198
|
-
step('选择要恢复的应用')
|
|
199
|
-
const appItems = appDirs.map((dir) => {
|
|
200
|
-
const label = appLabel(path.join(dir, 'dummy'))
|
|
201
|
-
const count = byApp[dir].length
|
|
202
|
-
return { label: `${chalk.cyan(label)}(${count} 个备份)`, dir }
|
|
203
|
-
})
|
|
204
|
-
const appIdx = await select('选择应用(回车=第一个)', appItems, { defaultIndex: 0 })
|
|
205
|
-
if (appIdx < 0 || appIdx >= appItems.length) {
|
|
206
|
-
fail('未选择应用')
|
|
207
|
-
return 1
|
|
208
|
-
}
|
|
209
|
-
const chosenDir = appItems[appIdx].dir
|
|
210
|
-
const chosenAppLabel = appLabel(path.join(chosenDir, 'dummy'))
|
|
211
|
-
|
|
212
|
-
// 该应用的备份按时间戳分组(截取到秒:T08-23-03)
|
|
213
|
-
const appBackups = byApp[chosenDir].sort((a, b) => b.mtime - a.mtime)
|
|
214
|
-
const byTime = {}
|
|
215
|
-
const timeOrder = []
|
|
216
|
-
for (const b of appBackups) {
|
|
217
|
-
// 从 bakPath 提取时间戳前缀(到秒)
|
|
218
|
-
const m = path.basename(b.bakPath).match(/\.bak\.(.+)$/)
|
|
219
|
-
const fullTs = m ? m[1] : ''
|
|
220
|
-
// 截取到秒:去毫秒后缀(最后 4 字符:-XXXZ)
|
|
221
|
-
const tsKey = fullTs.replace(/-\d{3}Z$/, 'Z')
|
|
222
|
-
if (!byTime[tsKey]) {
|
|
223
|
-
byTime[tsKey] = []
|
|
224
|
-
timeOrder.push(tsKey)
|
|
225
|
-
}
|
|
226
|
-
byTime[tsKey].push(b)
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// 同时间组内按 origPath 去重(同 origPath 只保留 mtime 最大的)
|
|
230
|
-
for (const ts of timeOrder) {
|
|
231
|
-
const group = byTime[ts]
|
|
232
|
-
const byOrig = {}
|
|
233
|
-
for (const b of group) {
|
|
234
|
-
if (!byOrig[b.origPath] || b.mtime > byOrig[b.origPath].mtime) {
|
|
235
|
-
byOrig[b.origPath] = b
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
byTime[ts] = Object.values(byOrig).sort((a, b) => b.mtime - a.mtime)
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
console.log()
|
|
242
|
-
step(`选择恢复时间点(${chosenAppLabel})`)
|
|
243
|
-
const timeItems = timeOrder.map((ts) => {
|
|
244
|
-
const group = byTime[ts]
|
|
245
|
-
const files = group.map((b) => path.basename(b.origPath)).join(', ')
|
|
246
|
-
const tsDisplay = fmtTime(group[0].mtime)
|
|
247
|
-
// 从备份内容提取关键信息(key 尾号 + base_url + model),一行展示
|
|
248
|
-
const parts = []
|
|
249
|
-
for (const b of group) {
|
|
250
|
-
try {
|
|
251
|
-
const content = fs.readFileSync(b.bakPath, 'utf8')
|
|
252
|
-
// key:先试 sk- 格式,再试常见 auth 字段名(支持非 sk- 的 token 如 BigModel)
|
|
253
|
-
if (!parts.find((p) => p.includes('…'))) {
|
|
254
|
-
// 方式1: sk- 前缀
|
|
255
|
-
const mSk = content.match(/sk-([a-zA-Z0-9]{10,})/)
|
|
256
|
-
if (mSk) { parts.push(`sk-…${mSk[1].slice(-6)}`) }
|
|
257
|
-
else {
|
|
258
|
-
// 方式2: 常见 auth 字段名 → 取值尾 6 位(支持 BigModel/GLM 等非 sk- token)
|
|
259
|
-
const mField = content.match(/(?:API_KEY|AUTH_TOKEN|OPENAI_API_KEY|ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN)["':\s]+["']([a-zA-Z0-9._-]{10,})["']/)
|
|
260
|
-
if (mField) { parts.push(`key:…${mField[1].slice(-6)}`) }
|
|
261
|
-
else {
|
|
262
|
-
// 方式3: export 环境变量格式
|
|
263
|
-
const mExport = content.match(/export\s+(?:API_KEY|AUTH_TOKEN|OPENAI_API_KEY|ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN)=(["']?)([a-zA-Z0-9._-]{10,})/)
|
|
264
|
-
if (mExport) { parts.push(`key:…${mExport[2].slice(-6)}`) }
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
// base_url / apiBase
|
|
269
|
-
if (!parts.find((p) => p.startsWith('http'))) {
|
|
270
|
-
const m2 = content.match(/(https?:\/\/[^\s"'`}]+)/)
|
|
271
|
-
if (m2) parts.push(m2[1].replace(/\/+$/, ''))
|
|
272
|
-
}
|
|
273
|
-
// model(如果有)
|
|
274
|
-
if (!parts.find((p) => p.startsWith('model'))) {
|
|
275
|
-
const m3 = content.match(/(?:^|\n)\s*model\s*[=:]\s*["']?([a-zA-Z0-9][\w.-]+)/)
|
|
276
|
-
if (m3) parts.push(`model:${m3[1]}`)
|
|
277
|
-
}
|
|
278
|
-
} catch { /* ignore */ }
|
|
279
|
-
}
|
|
280
|
-
const hint = parts.length > 0 ? ` ${chalk.yellow(parts.join(' | '))}` : ''
|
|
281
|
-
return { label: `${chalk.dim(tsDisplay)}(${group.length} 文件:${files})${hint}`, ts, group }
|
|
282
|
-
})
|
|
283
|
-
const timeIdx = await select('选择时间点(回车=最新)', timeItems, { defaultIndex: 0 })
|
|
284
|
-
if (timeIdx < 0 || timeIdx >= timeItems.length) {
|
|
285
|
-
fail('未选择时间点')
|
|
286
|
-
return 1
|
|
287
|
-
}
|
|
288
|
-
const chosenGroup = timeItems[timeIdx].group
|
|
289
|
-
|
|
290
|
-
console.log()
|
|
291
|
-
info(`将恢复 ${chosenAppLabel}(${chosenGroup.length} 个文件):`)
|
|
292
|
-
for (const b of chosenGroup) {
|
|
293
|
-
info(` ${path.basename(b.origPath)} ← ${path.basename(b.bakPath)}`)
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
if (args.dryRun) {
|
|
297
|
-
warn('[试运行] 不真实恢复')
|
|
298
|
-
return 0
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// 恢复前安全备份当前文件(方便撤销)
|
|
302
|
-
let restored = 0
|
|
303
|
-
for (const b of chosenGroup) {
|
|
304
|
-
if (fs.existsSync(b.origPath)) {
|
|
305
|
-
try {
|
|
306
|
-
const safetyBak = backupFile(b.origPath)
|
|
307
|
-
if (safetyBak) info(`安全备份:${path.basename(b.origPath)} → ${path.basename(safetyBak)}`)
|
|
308
|
-
} catch {
|
|
309
|
-
// 安全备份失败不阻塞
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
try {
|
|
313
|
-
restoreBackup(b.bakPath, b.origPath)
|
|
314
|
-
ok(`已恢复:${b.origPath}`)
|
|
315
|
-
restored++
|
|
316
|
-
} catch (err) {
|
|
317
|
-
fail(`恢复失败:${b.origPath}(${err.message})`)
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
info(`共恢复 ${restored} 个文件`)
|
|
321
|
-
return restored > 0 ? 0 : 3
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// ============================================================
|
|
325
|
-
// reset:恢复官方默认
|
|
326
|
-
// ============================================================
|
|
327
|
-
|
|
328
|
-
async function cmdReset(argv) {
|
|
329
|
-
const args = parseSubArgs(argv)
|
|
330
|
-
if (args.help) {
|
|
331
|
-
console.log(`npx @2api-store/activate reset [options]\n\n${SUB_HELP}`)
|
|
332
|
-
return 0
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
banner()
|
|
336
|
-
const env = detectAll()
|
|
337
|
-
if (!env.node.supported) {
|
|
338
|
-
fail(`需要 node >= 18,当前 ${env.node.version}`)
|
|
339
|
-
return 1
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
step('定位 2api.store 写入的配置')
|
|
343
|
-
const managedFiles = getManagedFiles(env.os.isWindows)
|
|
344
|
-
const existing = managedFiles.filter((p) => {
|
|
345
|
-
try {
|
|
346
|
-
return fs.existsSync(p)
|
|
347
|
-
} catch {
|
|
348
|
-
return false
|
|
349
|
-
}
|
|
350
|
-
})
|
|
351
|
-
|
|
352
|
-
if (existing.length === 0) {
|
|
353
|
-
warn('未发现 2api.store 写入的配置文件(可能已 reset 或从未激活过)')
|
|
354
|
-
info('如需从备份恢复,请运行:npx @2api-store/activate restore')
|
|
355
|
-
return 0
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
info('将处理以下文件:')
|
|
359
|
-
existing.forEach((p) => plain(` - ${chalk.yellow(p)}`))
|
|
360
|
-
console.log()
|
|
361
|
-
|
|
362
|
-
// 扫描最新备份
|
|
363
|
-
const dirs = getBackupScanDirs(env.os.isWindows)
|
|
364
|
-
const allBackups = listBackups(dirs)
|
|
365
|
-
const hasBackups = allBackups.length > 0
|
|
366
|
-
if (hasBackups) {
|
|
367
|
-
ok(`发现 ${allBackups.length} 个备份文件(reset 后将尝试恢复每个配置的最新备份)`)
|
|
368
|
-
} else {
|
|
369
|
-
warn('未找到任何备份文件(reset 后这些文件将直接删除,无备份可恢复)')
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
console.log()
|
|
373
|
-
step('确认操作')
|
|
374
|
-
warn('⚠️ 这是破坏性操作:将删除上述配置文件并尝试从备份恢复。')
|
|
375
|
-
warn(' 删除前会再备份一次(.bak.reset.<时间戳>),可手动恢复。')
|
|
376
|
-
const sure = await confirm('确认 reset 吗?', { defaultValue: false })
|
|
377
|
-
if (!sure) {
|
|
378
|
-
plain('已取消')
|
|
379
|
-
return 0
|
|
380
|
-
}
|
|
381
|
-
console.log()
|
|
382
|
-
|
|
383
|
-
if (args.dryRun) {
|
|
384
|
-
warn('【试运行模式 --dry-run】不会真实删除/恢复文件')
|
|
385
|
-
info('将执行:')
|
|
386
|
-
existing.forEach((p) => plain(` - 删除 ${p}`))
|
|
387
|
-
if (hasBackups) {
|
|
388
|
-
info('并从最新备份恢复:')
|
|
389
|
-
allBackups.slice(0, 5).forEach((b) => plain(` - ${b.bakPath} → ${b.origPath}`))
|
|
390
|
-
if (allBackups.length > 5) plain(` ...(共 ${allBackups.length} 个备份)`)
|
|
391
|
-
}
|
|
392
|
-
return 0
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// 1) 删除前再备份一次(.bak.reset.<ts>),保险可撤销
|
|
396
|
-
step('安全备份当前文件')
|
|
397
|
-
const safetyBackups = []
|
|
398
|
-
for (const p of existing) {
|
|
399
|
-
try {
|
|
400
|
-
// 用自定义后缀,避免与 listBackups 的常规 .bak.<ts> 混淆
|
|
401
|
-
const ts = new Date().toISOString().replace(/[:.]/g, '-')
|
|
402
|
-
const safetyPath = `${p}.bak.reset.${ts}`
|
|
403
|
-
fs.copyFileSync(p, safetyPath)
|
|
404
|
-
safetyBackups.push(safetyPath)
|
|
405
|
-
plain(` ✓ ${p} → ${safetyPath}`)
|
|
406
|
-
} catch (err) {
|
|
407
|
-
fail(`安全备份失败 ${p}:${err.message}(跳过删除此文件避免误伤)`)
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
if (safetyBackups.length > 0) {
|
|
411
|
-
ok(`已安全备份 ${safetyBackups.length} 个文件`)
|
|
412
|
-
}
|
|
413
|
-
console.log()
|
|
414
|
-
|
|
415
|
-
// 2) 删除 managed 文件
|
|
416
|
-
step('删除 2api.store 配置')
|
|
417
|
-
const deleted = []
|
|
418
|
-
for (const p of existing) {
|
|
419
|
-
try {
|
|
420
|
-
fs.unlinkSync(p)
|
|
421
|
-
ok(`已删除:${p}`)
|
|
422
|
-
deleted.push(p)
|
|
423
|
-
} catch (err) {
|
|
424
|
-
if (err.code === 'EACCES' || err.code === 'EPERM' || err.code === 'EROFS') {
|
|
425
|
-
fail(`权限不足,无法删除 ${p}(请用 sudo / 管理员 PowerShell 重跑)`)
|
|
426
|
-
} else {
|
|
427
|
-
fail(`删除失败 ${p}:${err.message}`)
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
console.log()
|
|
432
|
-
|
|
433
|
-
// 3) 从最新备份恢复(按 origPath 分组,取每个 origPath 的最新备份)
|
|
434
|
-
if (hasBackups) {
|
|
435
|
-
step('从最新备份恢复')
|
|
436
|
-
// 按 origPath 分组,取每组最新的
|
|
437
|
-
const byOrig = new Map()
|
|
438
|
-
for (const b of allBackups) {
|
|
439
|
-
const cur = byOrig.get(b.origPath)
|
|
440
|
-
if (!cur || b.mtime > cur.mtime) byOrig.set(b.origPath, b)
|
|
441
|
-
}
|
|
442
|
-
let restoredCount = 0
|
|
443
|
-
for (const [origPath, b] of byOrig) {
|
|
444
|
-
try {
|
|
445
|
-
restoreBackup(b.bakPath, origPath)
|
|
446
|
-
ok(`已恢复:${b.bakPath} → ${origPath}`)
|
|
447
|
-
restoredCount++
|
|
448
|
-
} catch (err) {
|
|
449
|
-
warn(`恢复失败 ${origPath}:${err.message}(文件已删除,未恢复)`)
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
if (restoredCount > 0) info(`从备份恢复 ${restoredCount} 个文件`)
|
|
453
|
-
console.log()
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// 报告
|
|
457
|
-
step('reset 报告')
|
|
458
|
-
ok(`已删除 ${deleted.length} 个 2api.store 配置文件`)
|
|
459
|
-
if (safetyBackups.length > 0) {
|
|
460
|
-
info('安全备份位置(如需撤销 reset,手动恢复这些文件):')
|
|
461
|
-
safetyBackups.forEach((b) => plain(` - ${b}`))
|
|
462
|
-
}
|
|
463
|
-
info('下一步:如需重新激活,运行 npx @2api-store/activate')
|
|
464
|
-
|
|
465
|
-
return deleted.length > 0 ? 0 : 1
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// ============================================================
|
|
469
|
-
// refresh-groups:刷新分组列表(只展示)
|
|
470
|
-
// ============================================================
|
|
471
|
-
|
|
472
|
-
async function cmdRefreshGroups(argv) {
|
|
473
|
-
const args = parseSubArgs(argv)
|
|
474
|
-
if (args.help) {
|
|
475
|
-
console.log(`npx @2api-store/activate refresh-groups [options]\n\n${SUB_HELP}`)
|
|
476
|
-
return 0
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
banner()
|
|
480
|
-
const env = detectAll()
|
|
481
|
-
if (!env.node.supported) {
|
|
482
|
-
fail(`需要 node >= 18,当前 ${env.node.version}`)
|
|
483
|
-
return 1
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const apiBase = args.apiBase || process.env.ACTIVATE_API_BASE || DEFAULT_API_BASE
|
|
487
|
-
info(`API 端点:${apiBase}`)
|
|
488
|
-
console.log()
|
|
489
|
-
|
|
490
|
-
step('获取可用分组')
|
|
491
|
-
let groups = []
|
|
492
|
-
try {
|
|
493
|
-
groups = await listGroups({ apiBase })
|
|
494
|
-
} catch (err) {
|
|
495
|
-
fail(`获取分组失败:${err.message}`)
|
|
496
|
-
return 1
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
if (groups.length === 0) {
|
|
500
|
-
warn('未查询到可用分组')
|
|
501
|
-
info('可能原因:服务端未配置分组 / 网络错误 / 端点错误')
|
|
502
|
-
info(`当前端点:${apiBase}(可通过 --api-base 或 ACTIVATE_API_BASE 覆盖)`)
|
|
503
|
-
return 0
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
ok(`查询到 ${groups.length} 个可用分组`)
|
|
507
|
-
console.log()
|
|
508
|
-
|
|
509
|
-
// 彩色表格输出
|
|
510
|
-
step('分组列表')
|
|
511
|
-
const idWidth = Math.max(4, ...groups.map((g) => String(g.id ?? '').length))
|
|
512
|
-
const nameWidth = Math.max(6, ...groups.map((g) => String(g.name ?? '').length))
|
|
513
|
-
const header = ` ${'ID'.padEnd(idWidth)} ${'名称'.padEnd(nameWidth)} 说明`
|
|
514
|
-
plain(chalk.bold.cyan(header))
|
|
515
|
-
plain(chalk.dim(' ' + '─'.repeat(idWidth + nameWidth + 15)))
|
|
516
|
-
groups.forEach((g, i) => {
|
|
517
|
-
const id = String(g.id ?? '').padEnd(idWidth)
|
|
518
|
-
const name = String(g.name ?? '').padEnd(nameWidth)
|
|
519
|
-
const row = ` ${chalk.yellow(id)} ${chalk.green(name)}`
|
|
520
|
-
plain(row)
|
|
521
|
-
})
|
|
522
|
-
console.log()
|
|
523
|
-
|
|
524
|
-
info('💡 如需切换分组:')
|
|
525
|
-
plain(' • 已有 API Key 绑组:登录管理后台修改,或重新激活(npx @2api-store/activate)')
|
|
526
|
-
plain(' • 使用兑换码:重新激活时选目标分组即可')
|
|
527
|
-
info('(本命令仅展示分组,不修改 API Key 或配置文件)')
|
|
528
|
-
|
|
529
|
-
return 0
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// ============================================================
|
|
533
|
-
// 子命令注册表
|
|
534
|
-
// ============================================================
|
|
535
|
-
|
|
536
|
-
export const SUBCOMMANDS = {
|
|
537
|
-
restore: { handler: cmdRestore, description: '从备份恢复配置' },
|
|
538
|
-
reset: { handler: cmdReset, description: '恢复官方默认(删 2api.store 配置并从备份恢复)' },
|
|
539
|
-
'refresh-groups': { handler: cmdRefreshGroups, description: '查看可用分组(不改配置)' },
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
/** 子命令名集合(用于 argv[2] 分发判断) */
|
|
543
|
-
export const SUBCOMMAND_NAMES = new Set(Object.keys(SUBCOMMANDS))
|
|
544
|
-
|
|
545
|
-
/** 帮助文本中的子命令清单 */
|
|
546
|
-
export { SUB_HELP }
|
package/lib/detect.mjs
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* 检测操作系统
|
|
5
|
-
*/
|
|
6
|
-
export function detectOS() {
|
|
7
|
-
const platform = process.platform
|
|
8
|
-
const isWindows = platform === 'win32'
|
|
9
|
-
const isMac = platform === 'darwin'
|
|
10
|
-
const isLinux = platform === 'linux'
|
|
11
|
-
return {
|
|
12
|
-
platform,
|
|
13
|
-
isWindows,
|
|
14
|
-
isMac,
|
|
15
|
-
isLinux,
|
|
16
|
-
osType: isWindows ? 'windows' : 'unix',
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* 检测 node 版本
|
|
22
|
-
*/
|
|
23
|
-
export function detectNode() {
|
|
24
|
-
const version = process.versions.node
|
|
25
|
-
const major = parseInt(version.split('.')[0], 10)
|
|
26
|
-
return { version, major, supported: major >= 18 }
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* 已知 CLI 列表(含 codex-ws 共享 codex 二进制)
|
|
31
|
-
* grok 仅检测(无官方 npm CLI 包,不参与自动安装)
|
|
32
|
-
*/
|
|
33
|
-
const KNOWN_CLIS = ['codex', 'claude', 'gemini', 'grok', 'opencode']
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* 用 which/where 查找二进制路径
|
|
37
|
-
*/
|
|
38
|
-
function findBinary(name, isWindows) {
|
|
39
|
-
const cmd = isWindows ? 'where' : 'which'
|
|
40
|
-
try {
|
|
41
|
-
const result = spawnSync(cmd, [name], {
|
|
42
|
-
encoding: 'utf8',
|
|
43
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
44
|
-
timeout: 3000,
|
|
45
|
-
})
|
|
46
|
-
if (result.status === 0 && result.stdout) {
|
|
47
|
-
const first = result.stdout.trim().split(/\r?\n/)[0]
|
|
48
|
-
return first || null
|
|
49
|
-
}
|
|
50
|
-
} catch {
|
|
51
|
-
// spawn 失败(如命令不存在),忽略
|
|
52
|
-
}
|
|
53
|
-
return null
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* 查询二进制版本(--version 输出第一行)
|
|
58
|
-
* @param {string} bin 二进制名
|
|
59
|
-
* @param {boolean} isWindows
|
|
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 cmdBin = isWindows ? process.env.COMSPEC || 'cmd.exe' : bin
|
|
66
|
-
const cmdArgs = isWindows ? ['/c', bin, '--version'] : ['--version']
|
|
67
|
-
const result = spawnSync(cmdBin, cmdArgs, {
|
|
68
|
-
encoding: 'utf8',
|
|
69
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
70
|
-
timeout: 5000,
|
|
71
|
-
// Windows: CLI 是 <bin>.cmd batch,必须经 cmd.exe /c 解析;不用 shell:true(DEP0190)
|
|
72
|
-
})
|
|
73
|
-
if (result.status === 0 && result.stdout) {
|
|
74
|
-
const firstLine = result.stdout.trim().split(/\r?\n/)[0]
|
|
75
|
-
// 提取常见版本格式:可能形如 "codex 0.1.2505162003" / "1.2.3" / "claude 2.1.0"
|
|
76
|
-
const match = firstLine.match(/(\d+\.\d+(?:\.\d+)?(?:[-.\w]*)?)/)
|
|
77
|
-
return match ? match[1] : firstLine.slice(0, 40)
|
|
78
|
-
}
|
|
79
|
-
} catch {
|
|
80
|
-
// 二进制存在但 --version 不支持 / 超时 → 不阻塞,返回 null
|
|
81
|
-
}
|
|
82
|
-
return null
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* 检测单个二进制是否已安装(含版本)
|
|
87
|
-
* @param {string} bin 二进制名
|
|
88
|
-
* @returns {{installed: boolean, path: string|null, version: string|null}}
|
|
89
|
-
*/
|
|
90
|
-
export function detectInstalled(bin) {
|
|
91
|
-
const os = detectOS()
|
|
92
|
-
const path = findBinary(bin, os.isWindows)
|
|
93
|
-
if (!path) return { installed: false, path: null, version: null }
|
|
94
|
-
const version = getVersion(bin, os.isWindows)
|
|
95
|
-
return { installed: true, path, version }
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* 检测已安装的 CLI(批量)
|
|
100
|
-
* @param {boolean} isWindows
|
|
101
|
-
* @returns {Record<string, {path: string, version: string|null}>} name → info
|
|
102
|
-
*/
|
|
103
|
-
export function detectInstalledCLIs(isWindows) {
|
|
104
|
-
const installed = {}
|
|
105
|
-
for (const name of KNOWN_CLIS) {
|
|
106
|
-
const path = findBinary(name, isWindows)
|
|
107
|
-
if (path) {
|
|
108
|
-
installed[name] = { path, version: getVersion(name, isWindows) }
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return installed
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* 一次性检测全部环境(含已装 CLI 的版本)
|
|
116
|
-
*/
|
|
117
|
-
export function detectAll() {
|
|
118
|
-
const os = detectOS()
|
|
119
|
-
const node = detectNode()
|
|
120
|
-
const installed = detectInstalledCLIs(os.isWindows)
|
|
121
|
-
return { os, node, installed }
|
|
122
|
-
}
|