@2api-store/activate 0.4.0 → 0.4.2

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.
Files changed (3) hide show
  1. package/README.md +5 -5
  2. package/lib/cmds.mjs +125 -80
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -37,10 +37,10 @@ npx @2api-store/activate <subcommand> [options]
37
37
  |--------|------|
38
38
  | `restore` | 从备份恢复:扫描 `~/.codex/`、`~/.claude/`、`~/.gemini/` 下的 `.bak.*` 文件,选择一个恢复(覆盖当前) |
39
39
  | `reset` | 恢复官方默认:删除 2api.store 写入的配置文件(`~/.codex/config.toml`、`~/.codex/auth.json`、`~/.claude/settings.json`、`~/.gemini/settings.json`),并尝试从最新备份恢复。**破坏性操作,需确认** |
40
- | `reconfigure` | 重新配置:输入已有 `sk-` API Key → 重拉最新模板/模型 → 覆盖写入配置(写前备份)。同激活流程但不创建账号 |
40
+ | `reconfig` | 重新配置:输入已有 `sk-` API Key → 重拉最新模板/模型 → 覆盖写入配置(写前备份)。同激活流程但不创建账号 |
41
41
  | `refresh-groups` | 查看可用分组:`GET /cli/groups` → 彩色表格展示。只读,不改 key/配置 |
42
42
 
43
- 所有子命令支持 `--api-base <url>` 覆盖端点;`restore` / `reset` / `reconfigure` 支持 `--dry-run` 预演。
43
+ 所有子命令支持 `--api-base <url>` 覆盖端点;`restore` / `reset` / `reconfig` 支持 `--dry-run` 预演。
44
44
 
45
45
  ### restore — 从备份恢复
46
46
 
@@ -65,11 +65,11 @@ npx @2api-store/activate reset
65
65
  - 删除后尝试从最新常规备份恢复(回到激活前的官方状态)
66
66
  - 需要显式确认(默认 `N`,输入 `y` 才执行)
67
67
 
68
- ### reconfigure — 重新配置
68
+ ### reconfig — 重新配置
69
69
 
70
70
  ```bash
71
- npx @2api-store/activate reconfigure
72
- npx @2api-store/activate reconfigure --api-base http://127.0.0.1:5566
71
+ npx @2api-store/activate reconfig
72
+ npx @2api-store/activate reconfig --api-base http://127.0.0.1:5566
73
73
  ```
74
74
 
75
75
  - 输入现有 `sk-` API Key(不接受兑换码)
package/lib/cmds.mjs CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
- * 子命令实现:restore / reset / reconfigure / refresh-groups
2
+ * 子命令实现:restore / reset / reconfig / refresh-groups
3
3
  *
4
4
  * 设计原则:
5
5
  * - 每个子命令是独立 async 函数,接收 argv 数组(不含子命令名本身)
6
6
  * - 返回退出码(0 成功 / 1 输入错误 / 2 权限 / 3 写入失败)
7
7
  * - 自包含:解析自己的 argv + 走完整流程
8
- * - 操作配置文件前都备份(reset 删前备份;reconfigure 写前备份)
8
+ * - 操作配置文件前都备份(reset 删前备份;reconfig 写前备份)
9
9
  * - 不复用 activate.mjs 的 main()(避免耦合,main 包含 redeem/CLI 多选/安装等复杂流程)
10
10
  */
11
11
 
@@ -126,10 +126,10 @@ const SUB_HELP = `
126
126
  子命令:
127
127
  restore 从备份恢复(扫描 ~/.codex / ~/.claude / ~/.gemini 下的 .bak.* 文件)
128
128
  reset 恢复官方默认(删 2api.store 写入的配置,并从最新备份恢复)
129
- reconfigure 重新配置(输入已有 sk- API Key → 重拉模板/模型 → 覆盖写入)
129
+ reconfig 重新配置(输入已有 sk- API Key → 重拉模板/模型 → 覆盖写入)
130
130
  refresh-groups 刷新分组列表(只展示,不改 key/配置)
131
131
 
132
- 所有子命令都支持 --api-base <url> 覆盖端点,restore/reset/reconfigure 支持 --dry-run。
132
+ 所有子命令都支持 --api-base <url> 覆盖端点,restore/reset/reconfig 支持 --dry-run。
133
133
  `.trim()
134
134
 
135
135
  // ============================================================
@@ -162,56 +162,130 @@ async function cmdRestore(argv) {
162
162
  return 0
163
163
  }
164
164
 
165
- ok(`找到 ${backups.length} 个备份文件(按时间倒序)`)
165
+ ok(`找到 ${backups.length} 个备份文件`)
166
+
167
+ // 按应用分组(根据 origPath 的父目录)
168
+ const APP_LABELS = {
169
+ '.codex': 'Codex CLI',
170
+ '.claude': 'Claude Code',
171
+ '.gemini': 'Gemini CLI',
172
+ }
173
+ function appLabel(origPath) {
174
+ const home = process.env.HOME || process.env.USERPROFILE || ''
175
+ const rel = path.relative(home, origPath).replace(/\\/g, '/')
176
+ const firstSeg = rel.split('/')[0]
177
+ return APP_LABELS[firstSeg] || firstSeg
178
+ }
179
+ function appDir(origPath) {
180
+ const home = process.env.HOME || process.env.USERPROFILE || ''
181
+ return path.dirname(origPath)
182
+ }
183
+
184
+ // 按应用(目录)分组
185
+ const byApp = {}
186
+ for (const b of backups) {
187
+ const dir = appDir(b.origPath)
188
+ if (!byApp[dir]) byApp[dir] = []
189
+ byApp[dir].push(b)
190
+ }
191
+
192
+ const appDirs = Object.keys(byApp)
193
+ if (appDirs.length === 0) {
194
+ info('无可恢复的备份')
195
+ return 0
196
+ }
197
+
166
198
  console.log()
199
+ step('选择要恢复的应用')
200
+ const appItems = appDirs.map((dir) => {
201
+ const label = appLabel(path.join(dir, 'dummy'))
202
+ const count = byApp[dir].length
203
+ return { label: `${chalk.cyan(label)}(${count} 个备份)`, dir }
204
+ })
205
+ const appIdx = await select('选择应用(回车=第一个)', appItems, { defaultIndex: 0 })
206
+ if (appIdx < 0 || appIdx >= appItems.length) {
207
+ fail('未选择应用')
208
+ return 1
209
+ }
210
+ const chosenDir = appItems[appIdx].dir
211
+ const chosenAppLabel = appLabel(path.join(chosenDir, 'dummy'))
212
+
213
+ // 该应用的备份按时间戳分组(截取到秒:T08-23-03)
214
+ const appBackups = byApp[chosenDir].sort((a, b) => b.mtime - a.mtime)
215
+ const byTime = {}
216
+ const timeOrder = []
217
+ for (const b of appBackups) {
218
+ // 从 bakPath 提取时间戳前缀(到秒)
219
+ const m = path.basename(b.bakPath).match(/\.bak\.(.+)$/)
220
+ const fullTs = m ? m[1] : ''
221
+ // 截取到秒:去毫秒后缀(最后 4 字符:-XXXZ)
222
+ const tsKey = fullTs.replace(/-\d{3}Z$/, 'Z')
223
+ if (!byTime[tsKey]) {
224
+ byTime[tsKey] = []
225
+ timeOrder.push(tsKey)
226
+ }
227
+ byTime[tsKey].push(b)
228
+ }
167
229
 
168
- // 构建选项
169
- const items = backups.map((b) => {
170
- const rel = path.relative(path.dirname(b.bakPath), b.bakPath)
171
- const ts = fmtTime(b.mtime)
172
- const sz = fmtSize(b.size)
173
- const orig = chalk.dim(`→ ${resolveHome('~/' + path.relative(process.env.HOME || '', b.origPath), env.os.isWindows)}`)
174
- return {
175
- label: `${chalk.cyan(rel)} ${chalk.dim(ts)} ${chalk.yellow(sz)} ${orig}`,
176
- backup: b,
230
+ // 同时间组内按 origPath 去重(同 origPath 只保留 mtime 最大的)
231
+ for (const ts of timeOrder) {
232
+ const group = byTime[ts]
233
+ const byOrig = {}
234
+ for (const b of group) {
235
+ if (!byOrig[b.origPath] || b.mtime > byOrig[b.origPath].mtime) {
236
+ byOrig[b.origPath] = b
237
+ }
177
238
  }
178
- })
239
+ byTime[ts] = Object.values(byOrig).sort((a, b) => b.mtime - a.mtime)
240
+ }
179
241
 
180
- step('选择要恢复的备份')
181
- const idx = await select('选择备份(回车=最新)', items, { defaultIndex: 0 })
182
- if (idx < 0 || idx >= items.length) {
183
- fail('未选择备份')
242
+ console.log()
243
+ step(`选择恢复时间点(${chosenAppLabel})`)
244
+ const timeItems = timeOrder.map((ts) => {
245
+ const group = byTime[ts]
246
+ const files = group.map((b) => path.basename(b.origPath)).join(', ')
247
+ const tsDisplay = fmtTime(group[0].mtime)
248
+ return { label: `${chalk.dim(tsDisplay)}(${group.length} 文件:${files})`, ts, group }
249
+ })
250
+ const timeIdx = await select('选择时间点(回车=最新)', timeItems, { defaultIndex: 0 })
251
+ if (timeIdx < 0 || timeIdx >= timeItems.length) {
252
+ fail('未选择时间点')
184
253
  return 1
185
254
  }
186
- const chosen = items[idx].backup
255
+ const chosenGroup = timeItems[timeIdx].group
256
+
187
257
  console.log()
188
- info(`将恢复:${chosen.bakPath}`)
189
- info(` → ${chosen.origPath}`)
258
+ info(`将恢复 ${chosenAppLabel}(${chosenGroup.length} 个文件):`)
259
+ for (const b of chosenGroup) {
260
+ info(` ${path.basename(b.origPath)} ← ${path.basename(b.bakPath)}`)
261
+ }
190
262
 
191
263
  if (args.dryRun) {
192
264
  warn('[试运行] 不真实恢复')
193
265
  return 0
194
266
  }
195
267
 
196
- // 恢复前再备份当前(如存在)— 双向保险:恢复后想撤销还能找回
197
- if (fs.existsSync(chosen.origPath)) {
268
+ // 恢复前安全备份当前文件(方便撤销)
269
+ let restored = 0
270
+ for (const b of chosenGroup) {
271
+ if (fs.existsSync(b.origPath)) {
272
+ try {
273
+ const safetyBak = backupFile(b.origPath)
274
+ if (safetyBak) info(`安全备份:${path.basename(b.origPath)} → ${path.basename(safetyBak)}`)
275
+ } catch {
276
+ // 安全备份失败不阻塞
277
+ }
278
+ }
198
279
  try {
199
- const safetyBak = backupFile(chosen.origPath)
200
- if (safetyBak) info(`安全备份当前文件:${safetyBak}(恢复前自动产生,方便撤销)`)
280
+ restoreBackup(b.bakPath, b.origPath)
281
+ ok(`已恢复:${b.origPath}`)
282
+ restored++
201
283
  } catch (err) {
202
- warn(`安全备份失败(继续恢复):${err.message}`)
284
+ fail(`恢复失败:${b.origPath}(${err.message})`)
203
285
  }
204
286
  }
205
-
206
- try {
207
- restoreBackup(chosen.bakPath, chosen.origPath)
208
- ok(`已恢复:${chosen.origPath}`)
209
- info('(原备份文件保留,可多次恢复)')
210
- return 0
211
- } catch (err) {
212
- fail(`恢复失败:${err.message}`)
213
- return 3
214
- }
287
+ info(`共恢复 ${restored} 个文件`)
288
+ return restored > 0 ? 0 : 3
215
289
  }
216
290
 
217
291
  // ============================================================
@@ -359,13 +433,13 @@ async function cmdReset(argv) {
359
433
  }
360
434
 
361
435
  // ============================================================
362
- // reconfigure:重新配置
436
+ // reconfig:重新配置
363
437
  // ============================================================
364
438
 
365
- async function cmdReconfigure(argv) {
439
+ async function cmdReconfig(argv) {
366
440
  const args = parseSubArgs(argv)
367
441
  if (args.help) {
368
- console.log(`npx @2api-store/activate reconfigure [options]\n\n${SUB_HELP}`)
442
+ console.log(`npx @2api-store/activate reconfig [options]\n\n${SUB_HELP}`)
369
443
  return 0
370
444
  }
371
445
 
@@ -396,23 +470,17 @@ async function cmdReconfigure(argv) {
396
470
  return 1
397
471
  }
398
472
  if (!apiKey.startsWith('sk-')) {
399
- fail('reconfigure 仅支持 API Key 模式(sk- 前缀)。如需使用兑换码,请运行默认激活流程:npx @2api-store/activate')
473
+ fail('reconfig 仅支持 API Key 模式(sk- 前缀)。如需使用兑换码,请运行默认激活流程:npx @2api-store/activate')
400
474
  return 1
401
475
  }
402
476
  info('识别为:API Key 模式(不创建账号,仅重拉模板写入配置)')
403
477
  console.log()
404
478
 
405
- // 询问是否限制平台(可选)
406
- const platformAnswer = (await ask('限制平台?(留空=全部,或输入 openai/anthropic/gemini/grok/antigravity)')).trim()
407
- const platform = platformAnswer || null
408
- if (platform) info(`平台过滤:${platform}`)
409
- console.log()
410
-
411
- // POST /cli/activate
412
- step('请求激活服务(key 模式重拉模板)')
479
+ // POST /cli/activate — 全平台,不问平台限制
480
+ step('请求激活服务(全量重拉模板)')
413
481
  let bundle
414
482
  try {
415
- bundle = await cliActivate({ credential: apiKey, platform, apiBase })
483
+ bundle = await cliActivate({ credential: apiKey, apiBase })
416
484
  } catch (err) {
417
485
  fail(`请求失败:${err.message}`)
418
486
  if (err.statusCode) fail(` HTTP ${err.statusCode}`)
@@ -425,7 +493,7 @@ async function cmdReconfigure(argv) {
425
493
  if (bundle.base_url) info(`base_url:${bundle.base_url}`)
426
494
  console.log()
427
495
 
428
- // 过滤配置
496
+ // 过滤配置 — 按模型平台推断 + OS 过滤(与默认激活流程一致)
429
497
  const allowedPlatforms = new Set()
430
498
  for (const m of bundle.models || []) {
431
499
  const id = (typeof m === 'string' ? m : m?.id || '').toLowerCase()
@@ -434,12 +502,10 @@ async function cmdReconfigure(argv) {
434
502
  else if (id.startsWith('gemini')) allowedPlatforms.add('gemini')
435
503
  else if (id.startsWith('grok')) allowedPlatforms.add('grok')
436
504
  }
437
- if (platform) allowedPlatforms.add(platform)
438
505
 
439
506
  const osConfigs = (bundle.configs || []).filter((c) => {
440
507
  if (!c) return false
441
508
  if (allowedPlatforms.size > 0 && !allowedPlatforms.has(c.platform)) return false
442
- if (platform && c.platform !== platform) return false
443
509
  if (env.os.isWindows) {
444
510
  return c.os_type === 'windows' || c.os_type === 'all'
445
511
  }
@@ -451,43 +517,22 @@ async function cmdReconfigure(argv) {
451
517
  return 1
452
518
  }
453
519
 
454
- // dedupe by platform + client_type
520
+ // reconfig: 全选所有配置,不问 CLI 选择
455
521
  const seen = new Set()
456
- const cliItems = []
522
+ const selectedItems = []
457
523
  for (const c of osConfigs) {
458
524
  const key = `${c.platform}/${c.client_type}`
459
525
  if (seen.has(key)) continue
460
526
  seen.add(key)
461
527
  const meta = getCliMeta(c.client_type)
462
528
  const displayName = c.display_name || c.displayName || meta?.name || `${c.platform} ${c.client_type}`
463
- const installed = env.installed[c.client_type]?.path || (meta && env.installed[meta.detectBin]?.path)
464
- const primaryMark = isDefaultSelected(c.client_type) ? '' : '(独立 CLI)'
465
- const nameColored = installed ? chalk.green(displayName) : chalk.gray(`${displayName} (未装)`)
466
- cliItems.push({
467
- label: `${nameColored} ${chalk.dim(`[${c.platform}/${c.client_type}]`)}${primaryMark}`,
529
+ selectedItems.push({
468
530
  config: c,
469
531
  clientType: c.client_type,
470
532
  meta,
471
- alreadyInstalled: !!installed,
472
533
  })
473
534
  }
474
-
475
- const defaultSelected = cliItems
476
- .map((item, i) => ({ item, i }))
477
- .filter(({ item }) => isDefaultSelected(item.clientType))
478
- .map(({ i }) => i)
479
-
480
- step('选择要重配的 CLI')
481
- const selectedIdxes = await multiSelect(
482
- '选择 CLI(绿色=已装,灰色=未装;默认选主 CLI)',
483
- cliItems,
484
- { defaultSelected },
485
- )
486
- if (selectedIdxes.length === 0) {
487
- plain('未选择任何 CLI,退出。')
488
- return 0
489
- }
490
- const selectedItems = selectedIdxes.map((i) => cliItems[i])
535
+ ok(`将覆盖 ${selectedItems.length} 个 CLI 配置`)
491
536
  console.log()
492
537
 
493
538
  // 选默认模型
@@ -613,7 +658,7 @@ async function cmdReconfigure(argv) {
613
658
  if (firstPermissionError) {
614
659
  console.log()
615
660
  fail('部分文件权限不足,请按提示用 sudo / 管理员 PowerShell 重跑:')
616
- fail(' sudo npx -y @2api-store/activate reconfigure')
661
+ fail(' sudo npx -y @2api-store/activate reconfig')
617
662
  return 2
618
663
  }
619
664
 
@@ -691,7 +736,7 @@ async function cmdRefreshGroups(argv) {
691
736
  export const SUBCOMMANDS = {
692
737
  restore: { handler: cmdRestore, description: '从备份恢复配置' },
693
738
  reset: { handler: cmdReset, description: '恢复官方默认(删 2api.store 配置并从备份恢复)' },
694
- reconfigure: { handler: cmdReconfigure, description: '用已有 API Key 重新拉取并写入配置' },
739
+ reconfig: { handler: cmdReconfig, description: '用已有 API Key 重新拉取并写入配置' },
695
740
  'refresh-groups': { handler: cmdRefreshGroups, description: '查看可用分组(不改配置)' },
696
741
  }
697
742
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2api-store/activate",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "TokenShop CLI 一键激活:配置本地 codex/claude/gemini/grok/opencode 指向 2api.store 中转(彩色输出 + 自动安装 CLI)",
5
5
  "type": "module",
6
6
  "bin": {