@2api-store/activate 0.3.7 → 0.4.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/README.md +66 -2
- package/activate.mjs +36 -7
- package/lib/backup.mjs +82 -5
- package/lib/cmds.mjs +702 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,12 +25,76 @@ npx @2api-store/activate
|
|
|
25
25
|
|
|
26
26
|
凭据格式自动识别:`sk-` 前缀走 Key 模式;否则按兑换码处理。
|
|
27
27
|
|
|
28
|
+
## 子命令
|
|
29
|
+
|
|
30
|
+
除了默认激活流程,还提供 4 个子命令用于后续维护:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx @2api-store/activate <subcommand> [options]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
| 子命令 | 作用 |
|
|
37
|
+
|--------|------|
|
|
38
|
+
| `restore` | 从备份恢复:扫描 `~/.codex/`、`~/.claude/`、`~/.gemini/` 下的 `.bak.*` 文件,选择一个恢复(覆盖当前) |
|
|
39
|
+
| `reset` | 恢复官方默认:删除 2api.store 写入的配置文件(`~/.codex/config.toml`、`~/.codex/auth.json`、`~/.claude/settings.json`、`~/.gemini/settings.json`),并尝试从最新备份恢复。**破坏性操作,需确认** |
|
|
40
|
+
| `reconfigure` | 重新配置:输入已有 `sk-` API Key → 重拉最新模板/模型 → 覆盖写入配置(写前备份)。同激活流程但不创建账号 |
|
|
41
|
+
| `refresh-groups` | 查看可用分组:`GET /cli/groups` → 彩色表格展示。只读,不改 key/配置 |
|
|
42
|
+
|
|
43
|
+
所有子命令支持 `--api-base <url>` 覆盖端点;`restore` / `reset` / `reconfigure` 支持 `--dry-run` 预演。
|
|
44
|
+
|
|
45
|
+
### restore — 从备份恢复
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx @2api-store/activate restore
|
|
49
|
+
npx @2api-store/activate restore --dry-run # 预演
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- 扫描三个 CLI 目录下的 `.bak.<时间戳>` 文件,按修改时间倒序列出
|
|
53
|
+
- 选择要恢复的备份,复制回原文件(覆盖当前)
|
|
54
|
+
- 恢复前会自动再产生一份安全备份(防止恢复后想撤销)
|
|
55
|
+
- 原 `.bak.*` 文件保留,可多次恢复
|
|
56
|
+
|
|
57
|
+
### reset — 恢复官方默认
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx @2api-store/activate reset
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- 列出 2api.store 写入的配置文件(仅现存的)
|
|
64
|
+
- **删除前再备份一次**(`.bak.reset.<时间戳>`,方便撤销)
|
|
65
|
+
- 删除后尝试从最新常规备份恢复(回到激活前的官方状态)
|
|
66
|
+
- 需要显式确认(默认 `N`,输入 `y` 才执行)
|
|
67
|
+
|
|
68
|
+
### reconfigure — 重新配置
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npx @2api-store/activate reconfigure
|
|
72
|
+
npx @2api-store/activate reconfigure --api-base http://127.0.0.1:5566
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- 输入现有 `sk-` API Key(不接受兑换码)
|
|
76
|
+
- POST `/cli/activate`(key 模式,不创建账号)→ 重拉最新模板/模型
|
|
77
|
+
- 多选 CLI(默认选主 CLI)→ 选默认模型 → 备份并写入
|
|
78
|
+
- 适合:换了机器、配置丢失、想刷新到最新模板
|
|
79
|
+
|
|
80
|
+
### refresh-groups — 查看可用分组
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npx @2api-store/activate refresh-groups
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- 彩色表格列出所有可用分组(ID + 名称)
|
|
87
|
+
- 只读,不改 key/配置
|
|
88
|
+
- 如需切组:已有 API Key 走管理后台,或重新激活(兑换码可重选分组)
|
|
89
|
+
|
|
28
90
|
## 命令行选项
|
|
29
91
|
|
|
30
92
|
```
|
|
31
|
-
npx @2api-store/activate [options]
|
|
93
|
+
npx @2api-store/activate [options] 默认激活流程
|
|
94
|
+
npx @2api-store/activate <subcommand> [options] 运行子命令
|
|
32
95
|
|
|
33
|
-
|
|
96
|
+
选项(默认激活流程 + 所有子命令通用):
|
|
97
|
+
--dry-run, -n 不真写配置,只展示将写入什么(不适用 refresh-groups)
|
|
34
98
|
--platform <name> 只激活指定平台(openai/anthropic/gemini/grok/antigravity)
|
|
35
99
|
--api-base <url> 覆盖 API 端点
|
|
36
100
|
-h, --help 显示帮助
|
package/activate.mjs
CHANGED
|
@@ -3,9 +3,14 @@
|
|
|
3
3
|
process.removeAllListeners('warning')
|
|
4
4
|
process.env.NODE_NO_WARNINGS = '1'
|
|
5
5
|
/**
|
|
6
|
-
* @2api-store/activate — TokenShop CLI 一键激活(v0.3.0
|
|
6
|
+
* @2api-store/activate — TokenShop CLI 一键激活(v0.3.0+)
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* 入口(argv[2] 分发):
|
|
9
|
+
* - 子命令(restore / reset / reconfigure / refresh-groups)→ 分发到 lib/cmds.mjs
|
|
10
|
+
* - 无子命令 / 选项(--help / --version / --dry-run / --platform / --api-base)
|
|
11
|
+
* → 走默认激活流程 main()
|
|
12
|
+
*
|
|
13
|
+
* 默认激活主流程(薄编排,逻辑在各 lib 模块):
|
|
9
14
|
* 1. banner + 检测环境 + 列已装/未装 CLI
|
|
10
15
|
* 2. 输入凭据(sk- → API Key;否则兑换码 + email)
|
|
11
16
|
* 3. POST /api/v1/cli/activate → 配置包
|
|
@@ -33,6 +38,7 @@ import {
|
|
|
33
38
|
import { banner, ok, fail, warn, info, step, guide, plain, chalk } from './lib/ui.mjs'
|
|
34
39
|
import { getCliMeta, isDefaultSelected } from './lib/platforms.mjs'
|
|
35
40
|
import { ensureInstalled } from './lib/install.mjs'
|
|
41
|
+
import { SUBCOMMANDS, SUBCOMMAND_NAMES, SUB_HELP } from './lib/cmds.mjs'
|
|
36
42
|
|
|
37
43
|
import { createRequire } from 'node:module'
|
|
38
44
|
const VERSION = createRequire(import.meta.url)('./package.json').version
|
|
@@ -73,22 +79,29 @@ function printHelp() {
|
|
|
73
79
|
@2api-store/activate v${VERSION} — TokenShop CLI 一键激活
|
|
74
80
|
|
|
75
81
|
用法:
|
|
76
|
-
npx @2api-store/activate [options]
|
|
82
|
+
npx @2api-store/activate [options] 默认激活流程
|
|
83
|
+
npx @2api-store/activate <subcommand> [options] 运行子命令
|
|
77
84
|
|
|
78
|
-
|
|
85
|
+
选项(默认激活流程):
|
|
79
86
|
--dry-run, -n 不真写配置/不真装 CLI,只展示将做什么
|
|
80
87
|
--platform <name> 只激活指定平台(openai/anthropic/gemini/grok/antigravity)
|
|
81
88
|
--api-base <url> 覆盖 API 端点(默认 $ACTIVATE_API_BASE 或 ${DEFAULT_API_BASE})
|
|
82
89
|
-h, --help 显示帮助
|
|
83
90
|
-v, --version 显示版本
|
|
84
91
|
|
|
92
|
+
${SUB_HELP}
|
|
93
|
+
|
|
85
94
|
环境变量:
|
|
86
95
|
ACTIVATE_API_BASE 覆盖 API 端点(同 --api-base)
|
|
87
96
|
|
|
88
97
|
示例:
|
|
89
|
-
npx @2api-store/activate
|
|
90
|
-
npx @2api-store/activate --dry-run
|
|
91
|
-
npx @2api-store/activate --platform openai
|
|
98
|
+
npx @2api-store/activate # 默认激活
|
|
99
|
+
npx @2api-store/activate --dry-run # 试运行
|
|
100
|
+
npx @2api-store/activate --platform openai # 只激活 openai
|
|
101
|
+
npx @2api-store/activate restore # 从备份恢复
|
|
102
|
+
npx @2api-store/activate reset # 恢复官方默认
|
|
103
|
+
npx @2api-store/activate reconfigure # 用已有 API Key 重新配置
|
|
104
|
+
npx @2api-store/activate refresh-groups # 查看可用分组
|
|
92
105
|
ACTIVATE_API_BASE=http://127.0.0.1:5566 npx @2api-store/activate
|
|
93
106
|
|
|
94
107
|
退出码:
|
|
@@ -129,6 +142,22 @@ function inferModelPlatforms(models) {
|
|
|
129
142
|
// ============================================================
|
|
130
143
|
|
|
131
144
|
async function main() {
|
|
145
|
+
// === 子命令分发:argv[2] 是子命令(restore/reset/reconfigure/refresh-groups)===
|
|
146
|
+
// 注意:必须在 parseArgs 之前判断,因为子命令有自己的 argv 解析(--api-base / --dry-run)
|
|
147
|
+
const firstArg = process.argv[2]
|
|
148
|
+
if (firstArg && !firstArg.startsWith('-') && SUBCOMMAND_NAMES.has(firstArg)) {
|
|
149
|
+
const subRest = process.argv.slice(3)
|
|
150
|
+
const entry = SUBCOMMANDS[firstArg]
|
|
151
|
+
try {
|
|
152
|
+
const code = await entry.handler(subRest)
|
|
153
|
+
return code || 0
|
|
154
|
+
} catch (err) {
|
|
155
|
+
fail(`子命令 ${firstArg} 异常:${err?.message || err}`)
|
|
156
|
+
if (err?.stack) console.error(err.stack)
|
|
157
|
+
return 1
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
132
161
|
const args = parseArgs(process.argv.slice(2))
|
|
133
162
|
if (args.help) {
|
|
134
163
|
printHelp()
|
package/lib/backup.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* 备份目标文件到同目录 .bak.<ISO时间戳>(非覆盖,保留多次)。
|
|
@@ -8,14 +9,14 @@ import fs from 'node:fs'
|
|
|
8
9
|
* - 文件存在且备份成功 → 返回备份路径
|
|
9
10
|
* - 文件存在但备份失败 → 抛错(调用方应中止写入,避免无备份覆盖)
|
|
10
11
|
*
|
|
11
|
-
* @param {string}
|
|
12
|
+
* @param {string} targetPath 目标文件绝对路径
|
|
12
13
|
* @returns {string|null} 备份路径,或 null(文件不存在)
|
|
13
14
|
* @throws {Error} 文件存在但 copyFileSync 失败
|
|
14
15
|
*/
|
|
15
|
-
export function backupFile(
|
|
16
|
+
export function backupFile(targetPath) {
|
|
16
17
|
let exists = false
|
|
17
18
|
try {
|
|
18
|
-
exists = fs.existsSync(
|
|
19
|
+
exists = fs.existsSync(targetPath)
|
|
19
20
|
} catch {
|
|
20
21
|
// 无法检查存在性(权限等)— 视为不存在,best effort
|
|
21
22
|
return null
|
|
@@ -24,9 +25,85 @@ export function backupFile(path) {
|
|
|
24
25
|
|
|
25
26
|
// ISO 时间戳,文件名安全化(: 和 . 替换为 -)
|
|
26
27
|
const ts = new Date().toISOString().replace(/[:.]/g, '-')
|
|
27
|
-
const backupPath = `${
|
|
28
|
+
const backupPath = `${targetPath}.bak.${ts}`
|
|
28
29
|
|
|
29
30
|
// copyFileSync 失败会抛出 — 调用方知道文件存在且备份失败
|
|
30
|
-
fs.copyFileSync(
|
|
31
|
+
fs.copyFileSync(targetPath, backupPath)
|
|
31
32
|
return backupPath
|
|
32
33
|
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 扫描多个目录下所有 .bak.<timestamp> 备份文件,按修改时间倒序排列。
|
|
37
|
+
*
|
|
38
|
+
* 文件名格式约定:<original>.bak.<ISO时间戳>(backupFile 写入)。
|
|
39
|
+
*
|
|
40
|
+
* @param {string[]} dirs 要扫描的目录绝对路径列表
|
|
41
|
+
* @returns {Array<{bakPath: string, origPath: string, mtime: number, dir: string, file: string, origName: string, size: number}>}
|
|
42
|
+
* 按修改时间倒序(最新在前)
|
|
43
|
+
*/
|
|
44
|
+
export function listBackups(dirs) {
|
|
45
|
+
const backups = []
|
|
46
|
+
for (const dir of dirs) {
|
|
47
|
+
if (!dir) continue
|
|
48
|
+
let entries
|
|
49
|
+
try {
|
|
50
|
+
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
51
|
+
} catch {
|
|
52
|
+
// 目录不存在或权限不足 — 跳过
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
if (!entry.isFile()) continue
|
|
57
|
+
// 匹配 <filename>.bak.<timestamp>(timestamp 含 T Z - 数字)
|
|
58
|
+
const match = entry.name.match(/^(.+)\.bak\.[\dTZ:-]+$/)
|
|
59
|
+
if (!match) continue
|
|
60
|
+
const origName = match[1]
|
|
61
|
+
const bakPath = path.join(dir, entry.name)
|
|
62
|
+
const origPath = path.join(dir, origName)
|
|
63
|
+
try {
|
|
64
|
+
const stat = fs.statSync(bakPath)
|
|
65
|
+
backups.push({
|
|
66
|
+
bakPath,
|
|
67
|
+
origPath,
|
|
68
|
+
mtime: stat.mtimeMs,
|
|
69
|
+
dir,
|
|
70
|
+
file: entry.name,
|
|
71
|
+
origName,
|
|
72
|
+
size: stat.size,
|
|
73
|
+
})
|
|
74
|
+
} catch {
|
|
75
|
+
// stat 失败 — 跳过
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// 修改时间倒序(最新在前)
|
|
80
|
+
backups.sort((a, b) => b.mtime - a.mtime)
|
|
81
|
+
return backups
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 从备份恢复(复制回原文件,覆盖当前)。
|
|
86
|
+
*
|
|
87
|
+
* 行为:
|
|
88
|
+
* - 备份文件不存在 → 抛错(调用方应预先校验)
|
|
89
|
+
* - 目标父目录不存在 → 自动 mkdir
|
|
90
|
+
* - 恢复后保留备份文件(不删,多次恢复可用)
|
|
91
|
+
*
|
|
92
|
+
* @param {string} bakPath 备份文件绝对路径
|
|
93
|
+
* @param {string} origPath 原文件目标绝对路径
|
|
94
|
+
* @returns {string} origPath(恢复后的路径)
|
|
95
|
+
* @throws {Error} 备份文件不存在或复制失败
|
|
96
|
+
*/
|
|
97
|
+
export function restoreBackup(bakPath, origPath) {
|
|
98
|
+
if (!fs.existsSync(bakPath)) {
|
|
99
|
+
throw new Error(`备份文件不存在:${bakPath}`)
|
|
100
|
+
}
|
|
101
|
+
const dir = path.dirname(origPath)
|
|
102
|
+
try {
|
|
103
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
104
|
+
} catch {
|
|
105
|
+
// 父目录已存在或创建失败忽略(copyFileSync 会再报)
|
|
106
|
+
}
|
|
107
|
+
fs.copyFileSync(bakPath, origPath)
|
|
108
|
+
return origPath
|
|
109
|
+
}
|
package/lib/cmds.mjs
ADDED
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 子命令实现:restore / reset / reconfigure / refresh-groups
|
|
3
|
+
*
|
|
4
|
+
* 设计原则:
|
|
5
|
+
* - 每个子命令是独立 async 函数,接收 argv 数组(不含子命令名本身)
|
|
6
|
+
* - 返回退出码(0 成功 / 1 输入错误 / 2 权限 / 3 写入失败)
|
|
7
|
+
* - 自包含:解析自己的 argv + 走完整流程
|
|
8
|
+
* - 操作配置文件前都备份(reset 删前备份;reconfigure 写前备份)
|
|
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
|
+
reconfigure 重新配置(输入已有 sk- API Key → 重拉模板/模型 → 覆盖写入)
|
|
130
|
+
refresh-groups 刷新分组列表(只展示,不改 key/配置)
|
|
131
|
+
|
|
132
|
+
所有子命令都支持 --api-base <url> 覆盖端点,restore/reset/reconfigure 支持 --dry-run。
|
|
133
|
+
`.trim()
|
|
134
|
+
|
|
135
|
+
// ============================================================
|
|
136
|
+
// restore:从备份恢复
|
|
137
|
+
// ============================================================
|
|
138
|
+
|
|
139
|
+
async function cmdRestore(argv) {
|
|
140
|
+
const args = parseSubArgs(argv)
|
|
141
|
+
if (args.help) {
|
|
142
|
+
console.log(`npx @2api-store/activate restore [options]\n\n${SUB_HELP}`)
|
|
143
|
+
return 0
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
banner()
|
|
147
|
+
const env = detectAll()
|
|
148
|
+
if (!env.node.supported) {
|
|
149
|
+
fail(`需要 node >= 18,当前 ${env.node.version}`)
|
|
150
|
+
return 1
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
step('扫描备份文件')
|
|
154
|
+
const dirs = getBackupScanDirs(env.os.isWindows)
|
|
155
|
+
if (args.dryRun) warn('【试运行模式 --dry-run】不会真实恢复文件')
|
|
156
|
+
info(`扫描目录:${dirs.join(', ')}`)
|
|
157
|
+
|
|
158
|
+
const backups = listBackups(dirs)
|
|
159
|
+
if (backups.length === 0) {
|
|
160
|
+
info('未找到任何备份文件(.bak.*)')
|
|
161
|
+
info('提示:只有运行过 npx @2api-store/activate 后才会产生备份文件')
|
|
162
|
+
return 0
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
ok(`找到 ${backups.length} 个备份文件(按时间倒序)`)
|
|
166
|
+
console.log()
|
|
167
|
+
|
|
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
|
+
}
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
step('选择要恢复的备份')
|
|
181
|
+
const idx = await select('选择备份(回车=最新)', items, { defaultIndex: 0 })
|
|
182
|
+
if (idx < 0 || idx >= items.length) {
|
|
183
|
+
fail('未选择备份')
|
|
184
|
+
return 1
|
|
185
|
+
}
|
|
186
|
+
const chosen = items[idx].backup
|
|
187
|
+
console.log()
|
|
188
|
+
info(`将恢复:${chosen.bakPath}`)
|
|
189
|
+
info(` → ${chosen.origPath}`)
|
|
190
|
+
|
|
191
|
+
if (args.dryRun) {
|
|
192
|
+
warn('[试运行] 不真实恢复')
|
|
193
|
+
return 0
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 恢复前再备份当前(如存在)— 双向保险:恢复后想撤销还能找回
|
|
197
|
+
if (fs.existsSync(chosen.origPath)) {
|
|
198
|
+
try {
|
|
199
|
+
const safetyBak = backupFile(chosen.origPath)
|
|
200
|
+
if (safetyBak) info(`安全备份当前文件:${safetyBak}(恢复前自动产生,方便撤销)`)
|
|
201
|
+
} catch (err) {
|
|
202
|
+
warn(`安全备份失败(继续恢复):${err.message}`)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
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
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ============================================================
|
|
218
|
+
// reset:恢复官方默认
|
|
219
|
+
// ============================================================
|
|
220
|
+
|
|
221
|
+
async function cmdReset(argv) {
|
|
222
|
+
const args = parseSubArgs(argv)
|
|
223
|
+
if (args.help) {
|
|
224
|
+
console.log(`npx @2api-store/activate reset [options]\n\n${SUB_HELP}`)
|
|
225
|
+
return 0
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
banner()
|
|
229
|
+
const env = detectAll()
|
|
230
|
+
if (!env.node.supported) {
|
|
231
|
+
fail(`需要 node >= 18,当前 ${env.node.version}`)
|
|
232
|
+
return 1
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
step('定位 2api.store 写入的配置')
|
|
236
|
+
const managedFiles = getManagedFiles(env.os.isWindows)
|
|
237
|
+
const existing = managedFiles.filter((p) => {
|
|
238
|
+
try {
|
|
239
|
+
return fs.existsSync(p)
|
|
240
|
+
} catch {
|
|
241
|
+
return false
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
if (existing.length === 0) {
|
|
246
|
+
warn('未发现 2api.store 写入的配置文件(可能已 reset 或从未激活过)')
|
|
247
|
+
info('如需从备份恢复,请运行:npx @2api-store/activate restore')
|
|
248
|
+
return 0
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
info('将处理以下文件:')
|
|
252
|
+
existing.forEach((p) => plain(` - ${chalk.yellow(p)}`))
|
|
253
|
+
console.log()
|
|
254
|
+
|
|
255
|
+
// 扫描最新备份
|
|
256
|
+
const dirs = getBackupScanDirs(env.os.isWindows)
|
|
257
|
+
const allBackups = listBackups(dirs)
|
|
258
|
+
const hasBackups = allBackups.length > 0
|
|
259
|
+
if (hasBackups) {
|
|
260
|
+
ok(`发现 ${allBackups.length} 个备份文件(reset 后将尝试恢复每个配置的最新备份)`)
|
|
261
|
+
} else {
|
|
262
|
+
warn('未找到任何备份文件(reset 后这些文件将直接删除,无备份可恢复)')
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
console.log()
|
|
266
|
+
step('确认操作')
|
|
267
|
+
warn('⚠️ 这是破坏性操作:将删除上述配置文件并尝试从备份恢复。')
|
|
268
|
+
warn(' 删除前会再备份一次(.bak.reset.<时间戳>),可手动恢复。')
|
|
269
|
+
const sure = await confirm('确认 reset 吗?', { defaultValue: false })
|
|
270
|
+
if (!sure) {
|
|
271
|
+
plain('已取消')
|
|
272
|
+
return 0
|
|
273
|
+
}
|
|
274
|
+
console.log()
|
|
275
|
+
|
|
276
|
+
if (args.dryRun) {
|
|
277
|
+
warn('【试运行模式 --dry-run】不会真实删除/恢复文件')
|
|
278
|
+
info('将执行:')
|
|
279
|
+
existing.forEach((p) => plain(` - 删除 ${p}`))
|
|
280
|
+
if (hasBackups) {
|
|
281
|
+
info('并从最新备份恢复:')
|
|
282
|
+
allBackups.slice(0, 5).forEach((b) => plain(` - ${b.bakPath} → ${b.origPath}`))
|
|
283
|
+
if (allBackups.length > 5) plain(` ...(共 ${allBackups.length} 个备份)`)
|
|
284
|
+
}
|
|
285
|
+
return 0
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// 1) 删除前再备份一次(.bak.reset.<ts>),保险可撤销
|
|
289
|
+
step('安全备份当前文件')
|
|
290
|
+
const safetyBackups = []
|
|
291
|
+
for (const p of existing) {
|
|
292
|
+
try {
|
|
293
|
+
// 用自定义后缀,避免与 listBackups 的常规 .bak.<ts> 混淆
|
|
294
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-')
|
|
295
|
+
const safetyPath = `${p}.bak.reset.${ts}`
|
|
296
|
+
fs.copyFileSync(p, safetyPath)
|
|
297
|
+
safetyBackups.push(safetyPath)
|
|
298
|
+
plain(` ✓ ${p} → ${safetyPath}`)
|
|
299
|
+
} catch (err) {
|
|
300
|
+
fail(`安全备份失败 ${p}:${err.message}(跳过删除此文件避免误伤)`)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (safetyBackups.length > 0) {
|
|
304
|
+
ok(`已安全备份 ${safetyBackups.length} 个文件`)
|
|
305
|
+
}
|
|
306
|
+
console.log()
|
|
307
|
+
|
|
308
|
+
// 2) 删除 managed 文件
|
|
309
|
+
step('删除 2api.store 配置')
|
|
310
|
+
const deleted = []
|
|
311
|
+
for (const p of existing) {
|
|
312
|
+
try {
|
|
313
|
+
fs.unlinkSync(p)
|
|
314
|
+
ok(`已删除:${p}`)
|
|
315
|
+
deleted.push(p)
|
|
316
|
+
} catch (err) {
|
|
317
|
+
if (err.code === 'EACCES' || err.code === 'EPERM' || err.code === 'EROFS') {
|
|
318
|
+
fail(`权限不足,无法删除 ${p}(请用 sudo / 管理员 PowerShell 重跑)`)
|
|
319
|
+
} else {
|
|
320
|
+
fail(`删除失败 ${p}:${err.message}`)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
console.log()
|
|
325
|
+
|
|
326
|
+
// 3) 从最新备份恢复(按 origPath 分组,取每个 origPath 的最新备份)
|
|
327
|
+
if (hasBackups) {
|
|
328
|
+
step('从最新备份恢复')
|
|
329
|
+
// 按 origPath 分组,取每组最新的
|
|
330
|
+
const byOrig = new Map()
|
|
331
|
+
for (const b of allBackups) {
|
|
332
|
+
const cur = byOrig.get(b.origPath)
|
|
333
|
+
if (!cur || b.mtime > cur.mtime) byOrig.set(b.origPath, b)
|
|
334
|
+
}
|
|
335
|
+
let restoredCount = 0
|
|
336
|
+
for (const [origPath, b] of byOrig) {
|
|
337
|
+
try {
|
|
338
|
+
restoreBackup(b.bakPath, origPath)
|
|
339
|
+
ok(`已恢复:${b.bakPath} → ${origPath}`)
|
|
340
|
+
restoredCount++
|
|
341
|
+
} catch (err) {
|
|
342
|
+
warn(`恢复失败 ${origPath}:${err.message}(文件已删除,未恢复)`)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (restoredCount > 0) info(`从备份恢复 ${restoredCount} 个文件`)
|
|
346
|
+
console.log()
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// 报告
|
|
350
|
+
step('reset 报告')
|
|
351
|
+
ok(`已删除 ${deleted.length} 个 2api.store 配置文件`)
|
|
352
|
+
if (safetyBackups.length > 0) {
|
|
353
|
+
info('安全备份位置(如需撤销 reset,手动恢复这些文件):')
|
|
354
|
+
safetyBackups.forEach((b) => plain(` - ${b}`))
|
|
355
|
+
}
|
|
356
|
+
info('下一步:如需重新激活,运行 npx @2api-store/activate')
|
|
357
|
+
|
|
358
|
+
return deleted.length > 0 ? 0 : 1
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ============================================================
|
|
362
|
+
// reconfigure:重新配置
|
|
363
|
+
// ============================================================
|
|
364
|
+
|
|
365
|
+
async function cmdReconfigure(argv) {
|
|
366
|
+
const args = parseSubArgs(argv)
|
|
367
|
+
if (args.help) {
|
|
368
|
+
console.log(`npx @2api-store/activate reconfigure [options]\n\n${SUB_HELP}`)
|
|
369
|
+
return 0
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
banner()
|
|
373
|
+
const env = detectAll()
|
|
374
|
+
const osLabel = env.os.isMac
|
|
375
|
+
? 'macOS'
|
|
376
|
+
: env.os.isLinux
|
|
377
|
+
? 'Linux'
|
|
378
|
+
: env.os.isWindows
|
|
379
|
+
? 'Windows'
|
|
380
|
+
: env.os.platform
|
|
381
|
+
info(`环境:${osLabel} · node v${env.node.version}`)
|
|
382
|
+
if (!env.node.supported) {
|
|
383
|
+
fail(`需要 node >= 18,当前 ${env.node.version}`)
|
|
384
|
+
return 1
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const apiBase = args.apiBase || process.env.ACTIVATE_API_BASE || DEFAULT_API_BASE
|
|
388
|
+
info(`API 端点:${apiBase}`)
|
|
389
|
+
if (args.dryRun) warn('【试运行模式 --dry-run】不会真写配置')
|
|
390
|
+
console.log()
|
|
391
|
+
|
|
392
|
+
// 输入 API Key
|
|
393
|
+
const apiKey = (await askSecret('请输入现有的 API Key (sk-...)')).trim()
|
|
394
|
+
if (!apiKey) {
|
|
395
|
+
fail('未输入 API Key')
|
|
396
|
+
return 1
|
|
397
|
+
}
|
|
398
|
+
if (!apiKey.startsWith('sk-')) {
|
|
399
|
+
fail('reconfigure 仅支持 API Key 模式(sk- 前缀)。如需使用兑换码,请运行默认激活流程:npx @2api-store/activate')
|
|
400
|
+
return 1
|
|
401
|
+
}
|
|
402
|
+
info('识别为:API Key 模式(不创建账号,仅重拉模板写入配置)')
|
|
403
|
+
console.log()
|
|
404
|
+
|
|
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 模式重拉模板)')
|
|
413
|
+
let bundle
|
|
414
|
+
try {
|
|
415
|
+
bundle = await cliActivate({ credential: apiKey, platform, apiBase })
|
|
416
|
+
} catch (err) {
|
|
417
|
+
fail(`请求失败:${err.message}`)
|
|
418
|
+
if (err.statusCode) fail(` HTTP ${err.statusCode}`)
|
|
419
|
+
return 1
|
|
420
|
+
}
|
|
421
|
+
if (bundle.mode && bundle.mode !== 'key') {
|
|
422
|
+
warn(`服务端返回模式:${bundle.mode}(预期 key 模式,但继续)`)
|
|
423
|
+
}
|
|
424
|
+
ok('已获取最新配置模板')
|
|
425
|
+
if (bundle.base_url) info(`base_url:${bundle.base_url}`)
|
|
426
|
+
console.log()
|
|
427
|
+
|
|
428
|
+
// 过滤配置
|
|
429
|
+
const allowedPlatforms = new Set()
|
|
430
|
+
for (const m of bundle.models || []) {
|
|
431
|
+
const id = (typeof m === 'string' ? m : m?.id || '').toLowerCase()
|
|
432
|
+
if (/^(gpt|o1|o3|codex|glm|minimax|deepseek)/.test(id)) allowedPlatforms.add('openai')
|
|
433
|
+
else if (id.startsWith('claude')) allowedPlatforms.add('anthropic')
|
|
434
|
+
else if (id.startsWith('gemini')) allowedPlatforms.add('gemini')
|
|
435
|
+
else if (id.startsWith('grok')) allowedPlatforms.add('grok')
|
|
436
|
+
}
|
|
437
|
+
if (platform) allowedPlatforms.add(platform)
|
|
438
|
+
|
|
439
|
+
const osConfigs = (bundle.configs || []).filter((c) => {
|
|
440
|
+
if (!c) return false
|
|
441
|
+
if (allowedPlatforms.size > 0 && !allowedPlatforms.has(c.platform)) return false
|
|
442
|
+
if (platform && c.platform !== platform) return false
|
|
443
|
+
if (env.os.isWindows) {
|
|
444
|
+
return c.os_type === 'windows' || c.os_type === 'all'
|
|
445
|
+
}
|
|
446
|
+
return c.os_type === 'unix' || c.os_type === 'all'
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
if (osConfigs.length === 0) {
|
|
450
|
+
fail('服务端未返回适配当前系统的配置模板')
|
|
451
|
+
return 1
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// dedupe by platform + client_type
|
|
455
|
+
const seen = new Set()
|
|
456
|
+
const cliItems = []
|
|
457
|
+
for (const c of osConfigs) {
|
|
458
|
+
const key = `${c.platform}/${c.client_type}`
|
|
459
|
+
if (seen.has(key)) continue
|
|
460
|
+
seen.add(key)
|
|
461
|
+
const meta = getCliMeta(c.client_type)
|
|
462
|
+
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}`,
|
|
468
|
+
config: c,
|
|
469
|
+
clientType: c.client_type,
|
|
470
|
+
meta,
|
|
471
|
+
alreadyInstalled: !!installed,
|
|
472
|
+
})
|
|
473
|
+
}
|
|
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])
|
|
491
|
+
console.log()
|
|
492
|
+
|
|
493
|
+
// 选默认模型
|
|
494
|
+
const modelItems = listModels(bundle.models || [])
|
|
495
|
+
let defaultModelId = ''
|
|
496
|
+
if (modelItems.length > 0) {
|
|
497
|
+
step('选择默认模型')
|
|
498
|
+
const TOP_N = 10
|
|
499
|
+
const topItems = modelItems.slice(0, TOP_N)
|
|
500
|
+
const hasMore = modelItems.length > TOP_N
|
|
501
|
+
const selectItems = [
|
|
502
|
+
...topItems.map((m) => ({ label: `${m.label} (${m.id})` })),
|
|
503
|
+
...(hasMore ? [{ label: chalk.dim('其他(手动输入)') }] : []),
|
|
504
|
+
]
|
|
505
|
+
const idx = await select('选择默认模型', selectItems, { defaultIndex: 0 })
|
|
506
|
+
if (hasMore && idx === topItems.length) {
|
|
507
|
+
defaultModelId = (await ask('输入模型 ID')).trim() || topItems[0].id
|
|
508
|
+
} else {
|
|
509
|
+
defaultModelId = topItems[idx].id
|
|
510
|
+
}
|
|
511
|
+
ok(`已选默认模型:${defaultModelId}`)
|
|
512
|
+
} else {
|
|
513
|
+
warn('服务端未返回模型列表,{{defaultModel}} 将留空')
|
|
514
|
+
}
|
|
515
|
+
console.log()
|
|
516
|
+
|
|
517
|
+
// 渲染
|
|
518
|
+
const baseUrl = bundle.base_url || ''
|
|
519
|
+
const baseUrlBeta = bundle.base_url_beta || baseUrl
|
|
520
|
+
const renderValues = {
|
|
521
|
+
apiKey: bundle.api_key || apiKey,
|
|
522
|
+
baseUrl,
|
|
523
|
+
apiBase: baseUrl,
|
|
524
|
+
apiBaseBeta: baseUrlBeta,
|
|
525
|
+
antigravityApiBase: `${baseUrl}/antigravity`,
|
|
526
|
+
antigravityApiBaseBeta: `${baseUrlBeta}/antigravity`,
|
|
527
|
+
defaultModel: defaultModelId,
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// 备份 + 写入
|
|
531
|
+
step('备份并写入配置')
|
|
532
|
+
const report = []
|
|
533
|
+
let firstPermissionError = null
|
|
534
|
+
|
|
535
|
+
for (const item of selectedItems) {
|
|
536
|
+
const displayName =
|
|
537
|
+
item.config.display_name || item.config.displayName || item.meta?.name || item.clientType
|
|
538
|
+
plain(`── ${displayName} ──`)
|
|
539
|
+
const files = item.config.files || []
|
|
540
|
+
for (const f of files) {
|
|
541
|
+
const rawPath = f.path || ''
|
|
542
|
+
const isShellOnly = rawPath === 'Terminal' || rawPath === 'PowerShell'
|
|
543
|
+
const targetPath = isShellOnly
|
|
544
|
+
? rawPath
|
|
545
|
+
: expandPath(rawPath, { isWindows: env.os.isWindows })
|
|
546
|
+
const rendered = renderContent(f.content || '', renderValues)
|
|
547
|
+
|
|
548
|
+
if (isShellOnly) {
|
|
549
|
+
plain(' [shell 命令,请手动执行]')
|
|
550
|
+
plain(rendered.split('\n').map((l) => ' ' + l).join('\n'))
|
|
551
|
+
report.push({ cli: displayName, path: rawPath, action: 'shell-only' })
|
|
552
|
+
continue
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
plain(` 目标:${targetPath}`)
|
|
556
|
+
|
|
557
|
+
if (args.dryRun) {
|
|
558
|
+
warn(' [试运行] 不真实写入')
|
|
559
|
+
report.push({ cli: displayName, path: targetPath, action: 'dry-run' })
|
|
560
|
+
console.log()
|
|
561
|
+
continue
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
let backupPath = null
|
|
565
|
+
try {
|
|
566
|
+
backupPath = backupFile(targetPath)
|
|
567
|
+
if (backupPath) plain(` 备份:${backupPath}`)
|
|
568
|
+
} catch (err) {
|
|
569
|
+
fail(`备份失败:${err.message}(跳过此文件避免无备份覆盖)`)
|
|
570
|
+
report.push({ cli: displayName, path: targetPath, action: 'backup-failed', error: err.message })
|
|
571
|
+
continue
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
try {
|
|
575
|
+
writeFileWithPermission(targetPath, rendered, { isWindows: env.os.isWindows })
|
|
576
|
+
ok('已写入')
|
|
577
|
+
report.push({ cli: displayName, path: targetPath, action: 'written', backup: backupPath })
|
|
578
|
+
} catch (err) {
|
|
579
|
+
if (err instanceof PermissionDeniedError) {
|
|
580
|
+
if (!firstPermissionError) firstPermissionError = err
|
|
581
|
+
fail(err.message)
|
|
582
|
+
report.push({ cli: displayName, path: targetPath, action: 'permission-denied', error: err.message })
|
|
583
|
+
} else {
|
|
584
|
+
fail(`写入失败:${err.message}`)
|
|
585
|
+
report.push({ cli: displayName, path: targetPath, action: 'failed', error: err.message })
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
console.log()
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// 报告
|
|
593
|
+
step('重配报告')
|
|
594
|
+
const written = report.filter((r) => r.action === 'written')
|
|
595
|
+
const failed = report.filter(
|
|
596
|
+
(r) => r.action === 'failed' || r.action === 'permission-denied' || r.action === 'backup-failed',
|
|
597
|
+
)
|
|
598
|
+
if (args.dryRun) {
|
|
599
|
+
plain(`[试运行] 将写入 ${report.filter((r) => r.action === 'dry-run').length} 个文件`)
|
|
600
|
+
} else {
|
|
601
|
+
ok(`已写入 ${written.length} 个文件`)
|
|
602
|
+
}
|
|
603
|
+
if (failed.length > 0) {
|
|
604
|
+
warn(`失败 ${failed.length} 项:`)
|
|
605
|
+
failed.forEach((f) => warn(` - ${f.cli} ${f.path}`))
|
|
606
|
+
}
|
|
607
|
+
const backups = report.filter((r) => r.backup).map((r) => r.backup)
|
|
608
|
+
if (backups.length > 0) {
|
|
609
|
+
info('备份位置:')
|
|
610
|
+
backups.forEach((b) => plain(` - ${b}`))
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (firstPermissionError) {
|
|
614
|
+
console.log()
|
|
615
|
+
fail('部分文件权限不足,请按提示用 sudo / 管理员 PowerShell 重跑:')
|
|
616
|
+
fail(' sudo npx -y @2api-store/activate reconfigure')
|
|
617
|
+
return 2
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return failed.length > 0 ? 3 : 0
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ============================================================
|
|
624
|
+
// refresh-groups:刷新分组列表(只展示)
|
|
625
|
+
// ============================================================
|
|
626
|
+
|
|
627
|
+
async function cmdRefreshGroups(argv) {
|
|
628
|
+
const args = parseSubArgs(argv)
|
|
629
|
+
if (args.help) {
|
|
630
|
+
console.log(`npx @2api-store/activate refresh-groups [options]\n\n${SUB_HELP}`)
|
|
631
|
+
return 0
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
banner()
|
|
635
|
+
const env = detectAll()
|
|
636
|
+
if (!env.node.supported) {
|
|
637
|
+
fail(`需要 node >= 18,当前 ${env.node.version}`)
|
|
638
|
+
return 1
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const apiBase = args.apiBase || process.env.ACTIVATE_API_BASE || DEFAULT_API_BASE
|
|
642
|
+
info(`API 端点:${apiBase}`)
|
|
643
|
+
console.log()
|
|
644
|
+
|
|
645
|
+
step('获取可用分组')
|
|
646
|
+
let groups = []
|
|
647
|
+
try {
|
|
648
|
+
groups = await listGroups({ apiBase })
|
|
649
|
+
} catch (err) {
|
|
650
|
+
fail(`获取分组失败:${err.message}`)
|
|
651
|
+
return 1
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (groups.length === 0) {
|
|
655
|
+
warn('未查询到可用分组')
|
|
656
|
+
info('可能原因:服务端未配置分组 / 网络错误 / 端点错误')
|
|
657
|
+
info(`当前端点:${apiBase}(可通过 --api-base 或 ACTIVATE_API_BASE 覆盖)`)
|
|
658
|
+
return 0
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
ok(`查询到 ${groups.length} 个可用分组`)
|
|
662
|
+
console.log()
|
|
663
|
+
|
|
664
|
+
// 彩色表格输出
|
|
665
|
+
step('分组列表')
|
|
666
|
+
const idWidth = Math.max(4, ...groups.map((g) => String(g.id ?? '').length))
|
|
667
|
+
const nameWidth = Math.max(6, ...groups.map((g) => String(g.name ?? '').length))
|
|
668
|
+
const header = ` ${'ID'.padEnd(idWidth)} ${'名称'.padEnd(nameWidth)} 说明`
|
|
669
|
+
plain(chalk.bold.cyan(header))
|
|
670
|
+
plain(chalk.dim(' ' + '─'.repeat(idWidth + nameWidth + 15)))
|
|
671
|
+
groups.forEach((g, i) => {
|
|
672
|
+
const id = String(g.id ?? '').padEnd(idWidth)
|
|
673
|
+
const name = String(g.name ?? '').padEnd(nameWidth)
|
|
674
|
+
const row = ` ${chalk.yellow(id)} ${chalk.green(name)}`
|
|
675
|
+
plain(row)
|
|
676
|
+
})
|
|
677
|
+
console.log()
|
|
678
|
+
|
|
679
|
+
info('💡 如需切换分组:')
|
|
680
|
+
plain(' • 已有 API Key 绑组:登录管理后台修改,或重新激活(npx @2api-store/activate)')
|
|
681
|
+
plain(' • 使用兑换码:重新激活时选目标分组即可')
|
|
682
|
+
info('(本命令仅展示分组,不修改 API Key 或配置文件)')
|
|
683
|
+
|
|
684
|
+
return 0
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// ============================================================
|
|
688
|
+
// 子命令注册表
|
|
689
|
+
// ============================================================
|
|
690
|
+
|
|
691
|
+
export const SUBCOMMANDS = {
|
|
692
|
+
restore: { handler: cmdRestore, description: '从备份恢复配置' },
|
|
693
|
+
reset: { handler: cmdReset, description: '恢复官方默认(删 2api.store 配置并从备份恢复)' },
|
|
694
|
+
reconfigure: { handler: cmdReconfigure, description: '用已有 API Key 重新拉取并写入配置' },
|
|
695
|
+
'refresh-groups': { handler: cmdRefreshGroups, description: '查看可用分组(不改配置)' },
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** 子命令名集合(用于 argv[2] 分发判断) */
|
|
699
|
+
export const SUBCOMMAND_NAMES = new Set(Object.keys(SUBCOMMANDS))
|
|
700
|
+
|
|
701
|
+
/** 帮助文本中的子命令清单 */
|
|
702
|
+
export { SUB_HELP }
|