@2api-store/activate 0.5.1 → 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 -88
- package/lib/write.mjs +0 -57
package/lib/install.mjs
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
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 isWin = process.platform === 'win32'
|
|
42
|
-
const npmBin = isWin ? process.env.COMSPEC || 'cmd.exe' : 'npm'
|
|
43
|
-
const npmArgs = isWin ? ['/c', 'npm', 'install', '-g', pkg] : ['install', '-g', pkg]
|
|
44
|
-
const result = spawnSync(npmBin, npmArgs, {
|
|
45
|
-
stdio: 'inherit', // npm 进度/输出直接透传到当前终端
|
|
46
|
-
// Windows: npm 是 npm.cmd batch,必须经 cmd.exe /c 解析;不用 shell:true(node 22+ DEP0190)
|
|
47
|
-
})
|
|
48
|
-
if (result.error) {
|
|
49
|
-
return { ok: false, error: result.error.message }
|
|
50
|
-
}
|
|
51
|
-
if (result.status === 0) {
|
|
52
|
-
return { ok: true }
|
|
53
|
-
}
|
|
54
|
-
return { ok: false, error: `npm exit ${result.status}` }
|
|
55
|
-
} catch (err) {
|
|
56
|
-
return { ok: false, error: err.message }
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* 确保 CLI 已安装:检测 → 未装则询问确认 → 安装
|
|
62
|
-
*
|
|
63
|
-
* @param {{detectBin?: string, installPackage?: string, name?: string, installCmd?: string}} meta CLI 元数据
|
|
64
|
-
* @param {{dryRun?: boolean, autoInstall?: boolean}} opts
|
|
65
|
-
* - dryRun 不真装
|
|
66
|
-
* - autoInstall true=未装时直接装(不问);false/缺省=询问确认后才装
|
|
67
|
-
* @returns {{installed: boolean, skipped?: boolean, reason?: string, version?: string|null, error?: string}}
|
|
68
|
-
* - installed=true 已装(或本次成功安装)
|
|
69
|
-
* - installed=false 未装且用户拒绝安装/安装失败/未知 meta
|
|
70
|
-
*/
|
|
71
|
-
export async function ensureInstalled(meta, opts = {}) {
|
|
72
|
-
if (!meta || !meta.installPackage) {
|
|
73
|
-
return { installed: false, skipped: true, reason: 'no-meta' }
|
|
74
|
-
}
|
|
75
|
-
const bin = meta.detectBin || meta.installPackage.split('/').pop()
|
|
76
|
-
const detectResult = detectInstalled(bin)
|
|
77
|
-
if (detectResult.installed) {
|
|
78
|
-
return { installed: true, version: detectResult.version }
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// 未装:决定是否安装
|
|
82
|
-
const { dryRun, autoInstall } = opts
|
|
83
|
-
|
|
84
|
-
// dry-run:直接走 installCLI 的 dry-run 分支(不问,因为不会真装)
|
|
85
|
-
if (dryRun) {
|
|
86
|
-
const r = installCLI(meta, { dryRun: true })
|
|
87
|
-
return { installed: false, skipped: true, reason: r.reason || 'dry-run' }
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// 非 dry-run:按 autoInstall 决定是否询问
|
|
91
|
-
let shouldInstall = autoInstall === true
|
|
92
|
-
if (!shouldInstall) {
|
|
93
|
-
shouldInstall = await confirm(
|
|
94
|
-
`未检测到 ${meta.name || bin}(命令 ${bin}),是否现在安装?\n 运行:${meta.installCmd || `npm install -g ${meta.installPackage}`}`,
|
|
95
|
-
{ defaultValue: true },
|
|
96
|
-
)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (!shouldInstall) {
|
|
100
|
-
return { installed: false, skipped: true, reason: 'user-declined' }
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const r = installCLI(meta, { dryRun: false })
|
|
104
|
-
if (r.ok) {
|
|
105
|
-
// 安装后复查(确认二进制可解析)
|
|
106
|
-
const recheck = detectInstalled(bin)
|
|
107
|
-
return { installed: recheck.installed, version: recheck.version, justInstalled: true }
|
|
108
|
-
}
|
|
109
|
-
return { installed: false, error: r.error }
|
|
110
|
-
}
|
package/lib/platforms.mjs
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
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: false, // codex 的 WebSocket 变体(同 CLI 同 config.toml,默认不选避免覆盖 codex 模式)
|
|
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: [
|
|
66
|
-
'常用命令:',
|
|
67
|
-
' claude 交互模式',
|
|
68
|
-
' claude "你的问题" 带初始问题启动',
|
|
69
|
-
' claude -p "问题" 非交互模式(输出后退出,适合脚本)',
|
|
70
|
-
' claude -c 继续最近会话',
|
|
71
|
-
' claude -r 恢复历史会话(选择器)',
|
|
72
|
-
' claude --model sonnet 指定模型(sonnet/opus/haiku/fable)',
|
|
73
|
-
' claude mcp 管理 MCP 服务器',
|
|
74
|
-
' claude doctor 诊断安装/配置',
|
|
75
|
-
' claude update 更新到最新版',
|
|
76
|
-
'权限模式(Shift+Tab 切换):default / acceptEdits / plan / auto / bypassPermissions',
|
|
77
|
-
' claude --dangerously-skip-permissions 跳过所有权限提示(=bypassPermissions,谨慎用)',
|
|
78
|
-
'项目根放 CLAUDE.md 让 Claude 了解你的代码库',
|
|
79
|
-
].join('\n'),
|
|
80
|
-
isPrimary: true,
|
|
81
|
-
},
|
|
82
|
-
'gemini-cli': {
|
|
83
|
-
name: 'Gemini CLI',
|
|
84
|
-
installPackage: '@google/gemini-cli',
|
|
85
|
-
installCmd: 'npm install -g @google/gemini-cli',
|
|
86
|
-
startCmd: 'gemini',
|
|
87
|
-
detectBin: 'gemini',
|
|
88
|
-
configFiles: ['~/.gemini/settings.json'],
|
|
89
|
-
usage: '运行 `gemini` 开始;`gemini --help` 查看选项;在项目根放 GEMINI.md 让 Gemini 了解你的代码库',
|
|
90
|
-
isPrimary: true,
|
|
91
|
-
},
|
|
92
|
-
opencode: {
|
|
93
|
-
// 独立 CLI(非主 CLI),默认不选
|
|
94
|
-
name: 'OpenCode',
|
|
95
|
-
installPackage: 'opencode-ai',
|
|
96
|
-
installCmd: 'npm install -g opencode-ai',
|
|
97
|
-
startCmd: 'opencode',
|
|
98
|
-
detectBin: 'opencode',
|
|
99
|
-
configFiles: ['~/.config/opencode/opencode.json', 'opencode.json'],
|
|
100
|
-
usage: '运行 `opencode` 开始;`opencode --help` 查看选项;在项目根放 AGENTS.md 让 OpenCode 了解你的代码库',
|
|
101
|
-
isPrimary: false,
|
|
102
|
-
},
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* 按 client_type 查询元数据,未定义返回 null
|
|
107
|
-
* @param {string} clientType
|
|
108
|
-
* @returns {object|null}
|
|
109
|
-
*/
|
|
110
|
-
export function getCliMeta(clientType) {
|
|
111
|
-
return CLI_META[clientType] || null
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* 判断 client_type 是否默认应选中(主 CLI 默认选,独立 CLI 默认不选)
|
|
116
|
-
* @param {string} clientType
|
|
117
|
-
* @returns {boolean}
|
|
118
|
-
*/
|
|
119
|
-
export function isDefaultSelected(clientType) {
|
|
120
|
-
const meta = CLI_META[clientType]
|
|
121
|
-
if (!meta) return true // 未知类型保守全选(向后兼容)
|
|
122
|
-
return meta.isPrimary
|
|
123
|
-
}
|
package/lib/prompt.mjs
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 提示输入模块(TUI 版,学 Hermes Agent 交互)。
|
|
3
|
-
*
|
|
4
|
-
* TTY(交互式):用 @inquirer/prompts —— select 上下+Enter、checkbox 上下+空格+Enter、password 掩码。
|
|
5
|
-
* 用户要的「上下选择、空格选中/取消」即 checkbox 的标准行为。
|
|
6
|
-
* 非 TTY(管道/测试):退化为 readline 行输入(序号选择),保证 CI/测试/脚本可用。
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import readline from 'node:readline'
|
|
10
|
-
|
|
11
|
-
// @inquirer/prompts 动态导入(仅 TTY 用;非 TTY 不加载避免无关报错)
|
|
12
|
-
let _inquirer = null
|
|
13
|
-
async function getInquirer() {
|
|
14
|
-
if (!_inquirer) _inquirer = await import('@inquirer/prompts')
|
|
15
|
-
return _inquirer
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
let _rl = null
|
|
19
|
-
let _lineQueue = null
|
|
20
|
-
let _lineQueuePromise = null
|
|
21
|
-
|
|
22
|
-
function isInteractive() {
|
|
23
|
-
return process.stdin.isTTY === true
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// 非交互模式:把 stdin 全量读入按行缓冲,从队列消费(避免 readline 在异步期间吸走后续行)
|
|
27
|
-
function ensureLineQueue() {
|
|
28
|
-
if (_lineQueue) return _lineQueuePromise
|
|
29
|
-
_lineQueue = []
|
|
30
|
-
_lineQueuePromise = new Promise((resolve) => {
|
|
31
|
-
let data = ''
|
|
32
|
-
process.stdin.setEncoding('utf8')
|
|
33
|
-
process.stdin.on('data', (c) => {
|
|
34
|
-
data += c
|
|
35
|
-
})
|
|
36
|
-
process.stdin.on('end', () => {
|
|
37
|
-
const lines = data.split(/\r?\n/)
|
|
38
|
-
if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop()
|
|
39
|
-
_lineQueue.push(...lines)
|
|
40
|
-
resolve()
|
|
41
|
-
})
|
|
42
|
-
if (process.stdin.destroyed) resolve()
|
|
43
|
-
})
|
|
44
|
-
return _lineQueuePromise
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function getRl() {
|
|
48
|
-
if (!_rl || _rl.closed) {
|
|
49
|
-
_rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
50
|
-
}
|
|
51
|
-
return _rl
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function closePrompt() {
|
|
55
|
-
if (_rl && !_rl.closed) _rl.close()
|
|
56
|
-
_rl = null
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** 普通文本输入 */
|
|
60
|
-
export async function ask(question, { defaultValue } = {}) {
|
|
61
|
-
const suffix = defaultValue ? ` [${defaultValue}]` : ''
|
|
62
|
-
if (isInteractive()) {
|
|
63
|
-
const inq = await getInquirer()
|
|
64
|
-
const v = await inq.input({ message: `${question}${suffix}`, default: defaultValue ?? '' })
|
|
65
|
-
return v || (defaultValue ?? '')
|
|
66
|
-
}
|
|
67
|
-
await ensureLineQueue()
|
|
68
|
-
const line = _lineQueue.shift() ?? ''
|
|
69
|
-
process.stdout.write(`${question}${suffix}: ${line}\n`)
|
|
70
|
-
const v = line.trim()
|
|
71
|
-
return v || (defaultValue ?? '')
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** 隐藏输入(API Key / 敏感凭据),TTY 掩码,非 TTY 退化为可见 */
|
|
75
|
-
export async function askSecret(question) {
|
|
76
|
-
if (isInteractive()) {
|
|
77
|
-
const inq = await getInquirer()
|
|
78
|
-
return inq.password({ message: question, mask: '*' })
|
|
79
|
-
}
|
|
80
|
-
return ask(question)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* 多选(TUI:上下导航 + 空格选中/取消 + 回车确认)。
|
|
85
|
-
* 非 TTY:退化为序号输入(逗号/空格分隔)。
|
|
86
|
-
* @returns {Promise<number[]>} 选中的索引数组
|
|
87
|
-
*/
|
|
88
|
-
export async function multiSelect(question, items, { defaultSelected = [] } = {}) {
|
|
89
|
-
if (items.length === 0) return []
|
|
90
|
-
if (isInteractive()) {
|
|
91
|
-
const inq = await getInquirer()
|
|
92
|
-
const choices = items.map((item, i) => ({
|
|
93
|
-
name: item.label,
|
|
94
|
-
value: i,
|
|
95
|
-
checked: defaultSelected.includes(i),
|
|
96
|
-
}))
|
|
97
|
-
const selected = await inq.checkbox({
|
|
98
|
-
message: question,
|
|
99
|
-
choices,
|
|
100
|
-
loop: false,
|
|
101
|
-
required: false,
|
|
102
|
-
})
|
|
103
|
-
return selected
|
|
104
|
-
}
|
|
105
|
-
// 非 TTY fallback:序号输入
|
|
106
|
-
console.log(question)
|
|
107
|
-
items.forEach((item, i) => {
|
|
108
|
-
const selected = defaultSelected.includes(i)
|
|
109
|
-
console.log(` [${selected ? 'x' : ' '}] ${i + 1}. ${item.label}`)
|
|
110
|
-
})
|
|
111
|
-
console.log(' (输入序号,逗号/空格分隔;回车=默认;a=全选;n=全不选)')
|
|
112
|
-
const defaultStr = defaultSelected.map((i) => i + 1).join(',')
|
|
113
|
-
const answer = await ask('选择', { defaultValue: defaultStr })
|
|
114
|
-
const trimmed = answer.trim().toLowerCase()
|
|
115
|
-
if (trimmed === 'a' || trimmed === 'all') return items.map((_, i) => i)
|
|
116
|
-
if (trimmed === 'n' || trimmed === 'none') return []
|
|
117
|
-
const nums = answer
|
|
118
|
-
.split(/[,\s]+/)
|
|
119
|
-
.map((s) => parseInt(s, 10))
|
|
120
|
-
.filter((n) => !isNaN(n) && n >= 1 && n <= items.length)
|
|
121
|
-
.map((n) => n - 1)
|
|
122
|
-
return [...new Set(nums)]
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* 单选(TUI:上下导航 + 回车确认)。
|
|
127
|
-
* @returns {Promise<number>} 选中的索引(items.length===1 时直接返回 0)
|
|
128
|
-
*/
|
|
129
|
-
export async function select(question, items, { defaultIndex = 0 } = {}) {
|
|
130
|
-
if (items.length === 0) return -1
|
|
131
|
-
if (items.length === 1) return 0
|
|
132
|
-
if (isInteractive()) {
|
|
133
|
-
const inq = await getInquirer()
|
|
134
|
-
const choices = items.map((item, i) => ({ name: item.label, value: i }))
|
|
135
|
-
return inq.select({ message: question, choices, default: defaultIndex, loop: false })
|
|
136
|
-
}
|
|
137
|
-
// 非 TTY fallback
|
|
138
|
-
console.log(question)
|
|
139
|
-
items.forEach((item, i) => {
|
|
140
|
-
const marker = i === defaultIndex ? ' (默认)' : ''
|
|
141
|
-
console.log(` ${i + 1}. ${item.label}${marker}`)
|
|
142
|
-
})
|
|
143
|
-
const answer = await ask('选择序号', { defaultValue: String(defaultIndex + 1) })
|
|
144
|
-
const n = parseInt(answer, 10)
|
|
145
|
-
if (isNaN(n) || n < 1 || n > items.length) return defaultIndex
|
|
146
|
-
return n - 1
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** 是非确认(TUI:y/n;非 TTY:输入) */
|
|
150
|
-
export async function confirm(question, { defaultValue = true } = {}) {
|
|
151
|
-
if (isInteractive()) {
|
|
152
|
-
const inq = await getInquirer()
|
|
153
|
-
return inq.confirm({ message: question, default: defaultValue })
|
|
154
|
-
}
|
|
155
|
-
const hint = defaultValue ? 'Y/n' : 'y/N'
|
|
156
|
-
const answer = await ask(`${question} (${hint})`, { defaultValue: defaultValue ? 'y' : 'n' })
|
|
157
|
-
const v = answer.trim().toLowerCase()
|
|
158
|
-
if (!v) return defaultValue
|
|
159
|
-
return v === 'y' || v === 'yes'
|
|
160
|
-
}
|
package/lib/render.mjs
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 占位符替换工具
|
|
3
|
-
*
|
|
4
|
-
* 对照 frontend/src/utils/clientConfigDefaults.ts,已知占位符:
|
|
5
|
-
* {{apiKey}} — API Key
|
|
6
|
-
* {{baseUrl}} — 主基址(如 https://2api.store)
|
|
7
|
-
* {{apiBase}} — 等价于 baseUrl(OpenCode 用此名)
|
|
8
|
-
* {{apiBaseBeta}} — beta 基址(OpenCode Gemini 用)
|
|
9
|
-
* {{antigravityApiBase}} — antigravity 基址(= baseUrl + /antigravity)
|
|
10
|
-
* {{antigravityApiBaseBeta}} — antigravity beta 基址
|
|
11
|
-
* {{defaultModel}} — 用户选择的默认模型 id(由脚本注入,后端保留占位符)
|
|
12
|
-
*
|
|
13
|
-
* 后端预注入 apiKey/baseUrl(spec §4.3 step 4),脚本主要注入 defaultModel。
|
|
14
|
-
* 为防御性处理(后端漏注入或返回原始模板),本函数替换全部已知占位符。
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
export const PLACEHOLDERS = [
|
|
18
|
-
'apiKey',
|
|
19
|
-
'baseUrl',
|
|
20
|
-
'apiBase',
|
|
21
|
-
'apiBaseBeta',
|
|
22
|
-
'antigravityApiBase',
|
|
23
|
-
'antigravityApiBaseBeta',
|
|
24
|
-
'defaultModel',
|
|
25
|
-
]
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* 渲染内容:替换所有已知占位符
|
|
29
|
-
* @param {string} content
|
|
30
|
-
* @param {Record<string, string>} values
|
|
31
|
-
* @returns {string}
|
|
32
|
-
*/
|
|
33
|
-
export function renderContent(content, values) {
|
|
34
|
-
if (!content) return ''
|
|
35
|
-
let out = content
|
|
36
|
-
for (const key of PLACEHOLDERS) {
|
|
37
|
-
const v = values[key]
|
|
38
|
-
if (v !== undefined && v !== null) {
|
|
39
|
-
// 字面替换,避免 RegExp 特殊字符问题
|
|
40
|
-
out = out.split(`{{${key}}}`).join(String(v))
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return out
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* 展开路径占位符
|
|
48
|
-
* unix: ~/foo → $HOME/foo
|
|
49
|
-
* windows: %userprofile%\foo → %USERPROFILE%\foo
|
|
50
|
-
* @param {string} p 原始路径
|
|
51
|
-
* @param {{isWindows: boolean}} opts
|
|
52
|
-
* @returns {string}
|
|
53
|
-
*/
|
|
54
|
-
export function expandPath(p, { isWindows } = {}) {
|
|
55
|
-
if (!p) return p
|
|
56
|
-
if (isWindows) {
|
|
57
|
-
const userProfile = process.env.USERPROFILE || ''
|
|
58
|
-
return p.replace(/^%userprofile%/i, userProfile).replace(/^~/, userProfile)
|
|
59
|
-
}
|
|
60
|
-
const home = process.env.HOME || ''
|
|
61
|
-
if (p === '~') return home
|
|
62
|
-
if (p.startsWith('~/')) return home + p.slice(1)
|
|
63
|
-
return p
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// 模型版本解析(gpt-5.6-sol → [5,6],gpt-oss-safeguard → [0,0] 旧)用于倒序排序(最新在前)
|
|
67
|
-
function modelVersion(id) {
|
|
68
|
-
const m = String(id || '').toLowerCase().match(/(\d+)\.(\d+)/)
|
|
69
|
-
return m ? [parseInt(m[1], 10), parseInt(m[2], 10)] : [0, 0]
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* 将 models 数组规整为 {id, label} 列表,按版本倒序排序(最新在前),供选择展示用
|
|
74
|
-
* @param {Array<{id?: string, name?: string}|string>} models
|
|
75
|
-
* @returns {Array<{index: number, id: string, label: string, raw: any}>}
|
|
76
|
-
*/
|
|
77
|
-
export function listModels(models) {
|
|
78
|
-
if (!Array.isArray(models) || models.length === 0) return []
|
|
79
|
-
const items = models.map((m, i) => {
|
|
80
|
-
if (typeof m === 'string') {
|
|
81
|
-
return { index: i, id: m, label: m, raw: m }
|
|
82
|
-
}
|
|
83
|
-
const id = m.id || m.name || String(m)
|
|
84
|
-
const label = m.name || m.id || id
|
|
85
|
-
return { index: i, id, label, raw: m }
|
|
86
|
-
})
|
|
87
|
-
// 版本倒序(最新在前);同版本按 id 字母序
|
|
88
|
-
items.sort((a, b) => {
|
|
89
|
-
const va = modelVersion(a.id)
|
|
90
|
-
const vb = modelVersion(b.id)
|
|
91
|
-
return vb[0] - va[0] || vb[1] - va[1] || String(a.id).localeCompare(String(b.id))
|
|
92
|
-
})
|
|
93
|
-
return items
|
|
94
|
-
}
|
package/lib/ui.mjs
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
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
|
-
import { createRequire } from 'node:module'
|
|
19
|
-
const PKG_VERSION = createRequire(import.meta.url)('../package.json').version
|
|
20
|
-
|
|
21
|
-
const BANNER = ` ╭───────────────────────────────────╮
|
|
22
|
-
│ 2api.store · CLI 一键激活 │
|
|
23
|
-
│ v${PKG_VERSION} │
|
|
24
|
-
╰───────────────────────────────────╯`
|
|
25
|
-
|
|
26
|
-
/** 彩色 banner */
|
|
27
|
-
export function banner() {
|
|
28
|
-
// 去除模板字面量首尾换行,保留 ASCII art 结构
|
|
29
|
-
console.log(chalk.bold.cyan(BANNER.replace(/^\n/, '').trimEnd()))
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** 成功(✓ 绿色) */
|
|
33
|
-
export function ok(msg) {
|
|
34
|
-
console.log(chalk.green(`✓ ${msg}`))
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** 错误(✗ 红色 → stderr) */
|
|
38
|
-
export function fail(msg) {
|
|
39
|
-
console.error(chalk.red(`✗ ${msg}`))
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** 警告(⚠ 黄色) */
|
|
43
|
-
export function warn(msg) {
|
|
44
|
-
console.warn(chalk.yellow(`⚠ ${msg}`))
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** 信息(ℹ 蓝色) */
|
|
48
|
-
export function info(msg) {
|
|
49
|
-
console.log(chalk.blue(`ℹ ${msg}`))
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* 步骤标题(粗体青色 + 框线分隔)
|
|
54
|
-
* @param {string} title
|
|
55
|
-
*/
|
|
56
|
-
export function step(title) {
|
|
57
|
-
console.log()
|
|
58
|
-
console.log(chalk.bold.cyan(`■ ${title}`))
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* CLI 启动/使用指南(彩色框)
|
|
63
|
-
* 渲染每个已激活 CLI 的 startCmd + usage
|
|
64
|
-
* @param {Array<{name: string, startCmd: string, usage: string}>} items
|
|
65
|
-
*/
|
|
66
|
-
export function guide(items) {
|
|
67
|
-
if (!items || items.length === 0) return
|
|
68
|
-
step('启动 / 使用指南')
|
|
69
|
-
for (const it of items) {
|
|
70
|
-
const head = chalk.bold.green(` ▶ ${it.name}`)
|
|
71
|
-
const start = chalk.green(` 启动:${it.startCmd}`)
|
|
72
|
-
const usage = chalk.dim(it.usage || '')
|
|
73
|
-
console.log(head)
|
|
74
|
-
console.log(start)
|
|
75
|
-
if (usage) console.log(usage)
|
|
76
|
-
console.log()
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* 普通纯文本输出(无颜色修饰,用于已经内嵌颜色的字符串或纯文本)
|
|
82
|
-
*/
|
|
83
|
-
export function plain(msg) {
|
|
84
|
-
console.log(msg)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** 暴露 chalk 给调用方(复杂场景按需用) */
|
|
88
|
-
export { chalk }
|
package/lib/write.mjs
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* 权限相关错误码(EACCES/EPERM/EROFS)
|
|
6
|
-
*/
|
|
7
|
-
const PERM_CODES = new Set(['EACCES', 'EPERM', 'EROFS'])
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* 权限不足错误(含明确的重跑提示,绝不自动 sudo)
|
|
11
|
-
*/
|
|
12
|
-
export class PermissionDeniedError extends Error {
|
|
13
|
-
constructor(targetPath, cause, isWindows) {
|
|
14
|
-
const tip = isWindows
|
|
15
|
-
? '请用「管理员 PowerShell」重跑:\n Start-Process powershell -Verb RunAs -ArgumentList "npx,-y,@2api-store/activate"'
|
|
16
|
-
: '请用 sudo 重跑:\n sudo npx -y @2api-store/activate'
|
|
17
|
-
super(`写入失败:${targetPath}(${cause.code || cause.message})\n ${tip}`)
|
|
18
|
-
this.name = 'PermissionDeniedError'
|
|
19
|
-
this.code = cause.code
|
|
20
|
-
this.path = targetPath
|
|
21
|
-
this.cause = cause
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* 两阶段写入(spec §5.5):
|
|
27
|
-
* 1. 先 fs.mkdirSync(recursive) 确保父目录
|
|
28
|
-
* 2. fs.writeFile 试用户权限
|
|
29
|
-
* 3. 失败(EACCES/EPERM/EROFS)→ 抛 PermissionDeniedError(含提示,绝不自动 sudo)
|
|
30
|
-
*
|
|
31
|
-
* 文件权限 0o600(仅属主读写,因含 API Key)
|
|
32
|
-
*
|
|
33
|
-
* @param {string} targetPath 目标绝对路径
|
|
34
|
-
* @param {string} content 写入内容
|
|
35
|
-
* @param {{isWindows?: boolean}} opts
|
|
36
|
-
*/
|
|
37
|
-
export function writeFileWithPermission(targetPath, content, { isWindows = false } = {}) {
|
|
38
|
-
const dir = path.dirname(targetPath)
|
|
39
|
-
try {
|
|
40
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
41
|
-
} catch (err) {
|
|
42
|
-
if (PERM_CODES.has(err.code)) {
|
|
43
|
-
throw new PermissionDeniedError(targetPath, err, isWindows)
|
|
44
|
-
}
|
|
45
|
-
throw err
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
fs.writeFileSync(targetPath, content, { encoding: 'utf8', mode: 0o600 })
|
|
50
|
-
} catch (err) {
|
|
51
|
-
if (PERM_CODES.has(err.code)) {
|
|
52
|
-
throw new PermissionDeniedError(targetPath, err, isWindows)
|
|
53
|
-
}
|
|
54
|
-
throw err
|
|
55
|
-
}
|
|
56
|
-
return targetPath
|
|
57
|
-
}
|