@2api-store/activate 0.4.0 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +5 -5
  2. package/lib/cmds.mjs +106 -44
  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,118 @@ async function cmdRestore(argv) {
162
162
  return 0
163
163
  }
164
164
 
165
- ok(`找到 ${backups.length} 个备份文件(按时间倒序)`)
166
- console.log()
165
+ ok(`找到 ${backups.length} 个备份文件`)
167
166
 
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,
177
- }
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
+
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 }
178
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
+ }
179
229
 
180
- step('选择要恢复的备份')
181
- const idx = await select('选择备份(回车=最新)', items, { defaultIndex: 0 })
182
- if (idx < 0 || idx >= items.length) {
183
- fail('未选择备份')
230
+ console.log()
231
+ step(`选择恢复时间点(${chosenAppLabel})`)
232
+ const timeItems = timeOrder.map((ts) => {
233
+ const group = byTime[ts]
234
+ const files = group.map((b) => path.basename(b.origPath)).join(', ')
235
+ const tsDisplay = fmtTime(group[0].mtime)
236
+ return { label: `${chalk.dim(tsDisplay)}(${group.length} 文件:${files})`, ts, group }
237
+ })
238
+ const timeIdx = await select('选择时间点(回车=最新)', timeItems, { defaultIndex: 0 })
239
+ if (timeIdx < 0 || timeIdx >= timeItems.length) {
240
+ fail('未选择时间点')
184
241
  return 1
185
242
  }
186
- const chosen = items[idx].backup
243
+ const chosenGroup = timeItems[timeIdx].group
244
+
187
245
  console.log()
188
- info(`将恢复:${chosen.bakPath}`)
189
- info(` → ${chosen.origPath}`)
246
+ info(`将恢复 ${chosenAppLabel}(${chosenGroup.length} 个文件):`)
247
+ for (const b of chosenGroup) {
248
+ info(` ${path.basename(b.origPath)} ← ${path.basename(b.bakPath)}`)
249
+ }
190
250
 
191
251
  if (args.dryRun) {
192
252
  warn('[试运行] 不真实恢复')
193
253
  return 0
194
254
  }
195
255
 
196
- // 恢复前再备份当前(如存在)— 双向保险:恢复后想撤销还能找回
197
- if (fs.existsSync(chosen.origPath)) {
256
+ // 恢复前安全备份当前文件(方便撤销)
257
+ let restored = 0
258
+ for (const b of chosenGroup) {
259
+ if (fs.existsSync(b.origPath)) {
260
+ try {
261
+ const safetyBak = backupFile(b.origPath)
262
+ if (safetyBak) info(`安全备份:${path.basename(b.origPath)} → ${path.basename(safetyBak)}`)
263
+ } catch {
264
+ // 安全备份失败不阻塞
265
+ }
266
+ }
198
267
  try {
199
- const safetyBak = backupFile(chosen.origPath)
200
- if (safetyBak) info(`安全备份当前文件:${safetyBak}(恢复前自动产生,方便撤销)`)
268
+ restoreBackup(b.bakPath, b.origPath)
269
+ ok(`已恢复:${b.origPath}`)
270
+ restored++
201
271
  } catch (err) {
202
- warn(`安全备份失败(继续恢复):${err.message}`)
272
+ fail(`恢复失败:${b.origPath}(${err.message})`)
203
273
  }
204
274
  }
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
- }
275
+ info(`共恢复 ${restored} 个文件`)
276
+ return restored > 0 ? 0 : 3
215
277
  }
216
278
 
217
279
  // ============================================================
@@ -359,13 +421,13 @@ async function cmdReset(argv) {
359
421
  }
360
422
 
361
423
  // ============================================================
362
- // reconfigure:重新配置
424
+ // reconfig:重新配置
363
425
  // ============================================================
364
426
 
365
- async function cmdReconfigure(argv) {
427
+ async function cmdReconfig(argv) {
366
428
  const args = parseSubArgs(argv)
367
429
  if (args.help) {
368
- console.log(`npx @2api-store/activate reconfigure [options]\n\n${SUB_HELP}`)
430
+ console.log(`npx @2api-store/activate reconfig [options]\n\n${SUB_HELP}`)
369
431
  return 0
370
432
  }
371
433
 
@@ -396,7 +458,7 @@ async function cmdReconfigure(argv) {
396
458
  return 1
397
459
  }
398
460
  if (!apiKey.startsWith('sk-')) {
399
- fail('reconfigure 仅支持 API Key 模式(sk- 前缀)。如需使用兑换码,请运行默认激活流程:npx @2api-store/activate')
461
+ fail('reconfig 仅支持 API Key 模式(sk- 前缀)。如需使用兑换码,请运行默认激活流程:npx @2api-store/activate')
400
462
  return 1
401
463
  }
402
464
  info('识别为:API Key 模式(不创建账号,仅重拉模板写入配置)')
@@ -613,7 +675,7 @@ async function cmdReconfigure(argv) {
613
675
  if (firstPermissionError) {
614
676
  console.log()
615
677
  fail('部分文件权限不足,请按提示用 sudo / 管理员 PowerShell 重跑:')
616
- fail(' sudo npx -y @2api-store/activate reconfigure')
678
+ fail(' sudo npx -y @2api-store/activate reconfig')
617
679
  return 2
618
680
  }
619
681
 
@@ -691,7 +753,7 @@ async function cmdRefreshGroups(argv) {
691
753
  export const SUBCOMMANDS = {
692
754
  restore: { handler: cmdRestore, description: '从备份恢复配置' },
693
755
  reset: { handler: cmdReset, description: '恢复官方默认(删 2api.store 配置并从备份恢复)' },
694
- reconfigure: { handler: cmdReconfigure, description: '用已有 API Key 重新拉取并写入配置' },
756
+ reconfig: { handler: cmdReconfig, description: '用已有 API Key 重新拉取并写入配置' },
695
757
  'refresh-groups': { handler: cmdRefreshGroups, description: '查看可用分组(不改配置)' },
696
758
  }
697
759
 
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.1",
4
4
  "description": "TokenShop CLI 一键激活:配置本地 codex/claude/gemini/grok/opencode 指向 2api.store 中转(彩色输出 + 自动安装 CLI)",
5
5
  "type": "module",
6
6
  "bin": {