@2api-store/activate 0.1.0 → 0.2.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.
package/activate.mjs CHANGED
@@ -110,6 +110,23 @@ function classifyCredential(cred) {
110
110
  return 'unknown'
111
111
  }
112
112
 
113
+ /**
114
+ * 从模型 ID 列表推断平台集合(与端点 inferPlatforms 对应,脚本兜底过滤)。
115
+ * 即使生产端点尚未部署过滤(0.314.0),脚本也按 key 分组实际可用模型平台过滤 configs,
116
+ * 避免给 codex key 列出 gemini/claude 等。
117
+ */
118
+ function inferModelPlatforms(models) {
119
+ const set = new Set()
120
+ for (const m of models || []) {
121
+ const id = (typeof m === 'string' ? m : m?.id || '').toLowerCase()
122
+ if (/^(gpt|o1|o3|codex|glm|minimax|deepseek)/.test(id)) set.add('openai')
123
+ else if (id.startsWith('claude')) set.add('anthropic')
124
+ else if (id.startsWith('gemini')) set.add('gemini')
125
+ else if (id.startsWith('grok')) set.add('grok')
126
+ }
127
+ return [...set]
128
+ }
129
+
113
130
  async function main() {
114
131
  const args = parseArgs(process.argv.slice(2))
115
132
  if (args.help) {
@@ -192,10 +209,14 @@ async function main() {
192
209
  }
193
210
  console.log()
194
211
 
195
- // === 5. 按 os_type 过滤、展示可用 CLI ===
212
+ // === 5. 按 os_type + key 分组平台过滤、展示可用 CLI ===
213
+ const allowedPlatforms = inferModelPlatforms(bundle.models || [])
214
+ if (args.platform) allowedPlatforms.push(args.platform)
196
215
  let osConfigs = (bundle.configs || []).filter((c) => {
197
216
  if (!c) return false
198
- // 客户端 --platform 二次过滤(防御性,后端已过滤)
217
+ // 按 key 分组可用模型平台过滤(兜底,与端点 inferPlatforms 一致;codex key 只列 openai)
218
+ if (allowedPlatforms.length > 0 && !allowedPlatforms.includes(c.platform)) return false
219
+ // 客户端 --platform 二次过滤(防御性)
199
220
  if (args.platform && c.platform !== args.platform) return false
200
221
  if (env.os.isWindows) {
201
222
  return c.os_type === 'windows' || c.os_type === 'all'
package/lib/prompt.mjs CHANGED
@@ -1,26 +1,29 @@
1
1
  /**
2
- * 提示输入模块。
2
+ * 提示输入模块(TUI 版,学 Hermes Agent 交互)。
3
3
  *
4
- * 两种模式:
5
- * - TTY(交互式):readline.question,支持 askSecret 掩码。
6
- * - 非 TTY(管道/测试):一次性把 stdin 读入按行缓冲,从队列消费。
7
- * 避免 HTTP/异步操作期间 readline 把数据"吸走"导致后续 question 拿不到输入。
4
+ * TTY(交互式):用 @inquirer/prompts —— select 上下+Enter、checkbox 上下+空格+Enter、password 掩码。
5
+ * 用户要的「上下选择、空格选中/取消」即 checkbox 的标准行为。
6
+ * 非 TTY(管道/测试):退化为 readline 行输入(序号选择),保证 CI/测试/脚本可用。
8
7
  */
9
8
 
10
9
  import readline from 'node:readline'
11
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
+
12
18
  let _rl = null
13
- let _lineQueue = null // string[] | null(仅非 TTY 时使用)
19
+ let _lineQueue = null
14
20
  let _lineQueuePromise = null
15
21
 
16
22
  function isInteractive() {
17
23
  return process.stdin.isTTY === true
18
24
  }
19
25
 
20
- /**
21
- * 非交互模式:把 stdin 全量读入,按 \n 切成行队列。
22
- * 后续 ask 从队列取;队列空时返回 undefined(视作空字符串)。
23
- */
26
+ // 非交互模式:把 stdin 全量读入按行缓冲,从队列消费(避免 readline 在异步期间吸走后续行)
24
27
  function ensureLineQueue() {
25
28
  if (_lineQueue) return _lineQueuePromise
26
29
  _lineQueue = []
@@ -31,15 +34,11 @@ function ensureLineQueue() {
31
34
  data += c
32
35
  })
33
36
  process.stdin.on('end', () => {
34
- // 按 \n 切,去掉末尾空行
35
37
  const lines = data.split(/\r?\n/)
36
- // 如果末尾有 \n,split 会产生一个空字符串元素;保留中间空行(用户回车默认)
37
- // 去掉最后一个空元素(来自末尾 \n)
38
38
  if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop()
39
39
  _lineQueue.push(...lines)
40
40
  resolve()
41
41
  })
42
- // 如果 stdin 已结束(极端情况),也 resolve
43
42
  if (process.stdin.destroyed) resolve()
44
43
  })
45
44
  return _lineQueuePromise
@@ -47,173 +46,95 @@ function ensureLineQueue() {
47
46
 
48
47
  function getRl() {
49
48
  if (!_rl || _rl.closed) {
50
- _rl = readline.createInterface({
51
- input: process.stdin,
52
- output: process.stdout,
53
- })
49
+ _rl = readline.createInterface({ input: process.stdin, output: process.stdout })
54
50
  }
55
51
  return _rl
56
52
  }
57
53
 
58
- /**
59
- * 关闭提示接口(退出前调用)
60
- */
61
54
  export function closePrompt() {
62
- if (_rl && !_rl.closed) {
63
- _rl.close()
64
- }
55
+ if (_rl && !_rl.closed) _rl.close()
65
56
  _rl = null
66
57
  }
67
58
 
68
- /**
69
- * 普通文本输入
70
- * @param {string} question
71
- * @param {{defaultValue?: string}} [opts]
72
- * @returns {Promise<string>}
73
- */
59
+ /** 普通文本输入 */
74
60
  export async function ask(question, { defaultValue } = {}) {
75
61
  const suffix = defaultValue ? ` [${defaultValue}]` : ''
76
- // 非 TTY:从行队列取
77
- if (!isInteractive()) {
78
- await ensureLineQueue()
79
- const line = _lineQueue.shift() ?? ''
80
- // 模拟 prompt 输出(可读性)
81
- process.stdout.write(`${question}${suffix}: ${line}\n`)
82
- const v = line.trim()
62
+ if (isInteractive()) {
63
+ const inq = await getInquirer()
64
+ const v = await inq.input({ message: `${question}${suffix}`, default: defaultValue ?? '' })
83
65
  return v || (defaultValue ?? '')
84
66
  }
85
- // TTY:readline
86
- const rl = getRl()
87
- return new Promise((resolve) => {
88
- rl.question(`${question}${suffix}: `, (answer) => {
89
- const v = (answer || '').trim()
90
- resolve(v || (defaultValue ?? ''))
91
- })
92
- })
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 ?? '')
93
72
  }
94
73
 
95
- /**
96
- * 隐藏输入(API Key / 敏感凭据)
97
- * 非 TTY 退化为 ask(可见输入),便于测试。
98
- * @param {string} question
99
- * @returns {Promise<string>}
100
- */
74
+ /** 隐藏输入(API Key / 敏感凭据),TTY 掩码,非 TTY 退化为可见 */
101
75
  export async function askSecret(question) {
102
- // 非 TTY(含管道)→ ask
103
- if (!isInteractive()) {
104
- return ask(question)
76
+ if (isInteractive()) {
77
+ const inq = await getInquirer()
78
+ return inq.password({ message: question, mask: '*' })
105
79
  }
106
- // TTY:先关 rl(它独占 stdin),切原始模式逐字符读取
107
- if (_rl && !_rl.closed) _rl.close()
108
- _rl = null
109
-
110
- return new Promise((resolve) => {
111
- process.stdout.write(`${question}: `)
112
- let result = ''
113
- let resolved = false
114
-
115
- const cleanup = () => {
116
- if (resolved) return
117
- resolved = true
118
- process.stdin.removeListener('data', onData)
119
- try {
120
- process.stdin.setRawMode(false)
121
- } catch {
122
- // ignore
123
- }
124
- process.stdin.pause()
125
- process.stdout.write('\n')
126
- resolve(result)
127
- }
128
-
129
- const onData = (buf) => {
130
- for (let i = 0; i < buf.length; i++) {
131
- const byte = buf[i]
132
- if (byte === 13 || byte === 10) {
133
- // Enter
134
- cleanup()
135
- return
136
- } else if (byte === 3 || byte === 4) {
137
- // Ctrl+C / Ctrl+D
138
- cleanup()
139
- return
140
- } else if (byte === 127 || byte === 8) {
141
- // Backspace / Ctrl+H
142
- if (result.length > 0) {
143
- result = result.slice(0, -1)
144
- process.stdout.write('\b \b')
145
- }
146
- } else if (byte >= 32 && byte < 127) {
147
- result += String.fromCharCode(byte)
148
- process.stdout.write('*')
149
- }
150
- }
151
- }
152
-
153
- try {
154
- process.stdin.setRawMode(true)
155
- } catch {
156
- // 无法 raw mode — 退化为可见输入
157
- process.stdout.write('\n')
158
- resolve('')
159
- return
160
- }
161
- process.stdin.resume()
162
- process.stdin.on('data', onData)
163
- })
80
+ return ask(question)
164
81
  }
165
82
 
166
83
  /**
167
- * 多选(空格/逗号分隔序号)。
168
- * - 输入空 → defaultSelected
169
- * - 输入 a/all → 全选
170
- * - 输入 n/none → 全不选
171
- * - 输入 1,3 5 → 选 [0, 2, 4]
172
- *
173
- * @param {string} question
174
- * @param {Array<{label: string}>} items
175
- * @param {{defaultSelected?: number[]}} [opts]
84
+ * 多选(TUI:上下导航 + 空格选中/取消 + 回车确认)。
85
+ * 非 TTY:退化为序号输入(逗号/空格分隔)。
176
86
  * @returns {Promise<number[]>} 选中的索引数组
177
87
  */
178
88
  export async function multiSelect(question, items, { defaultSelected = [] } = {}) {
179
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:序号输入
180
106
  console.log(question)
181
107
  items.forEach((item, i) => {
182
108
  const selected = defaultSelected.includes(i)
183
109
  console.log(` [${selected ? 'x' : ' '}] ${i + 1}. ${item.label}`)
184
110
  })
185
111
  console.log(' (输入序号,逗号/空格分隔;回车=默认;a=全选;n=全不选)')
186
-
187
112
  const defaultStr = defaultSelected.map((i) => i + 1).join(',')
188
113
  const answer = await ask('选择', { defaultValue: defaultStr })
189
114
  const trimmed = answer.trim().toLowerCase()
190
-
191
- if (trimmed === 'a' || trimmed === 'all') {
192
- return items.map((_, i) => i)
193
- }
194
- if (trimmed === 'n' || trimmed === 'none') {
195
- return []
196
- }
197
-
115
+ if (trimmed === 'a' || trimmed === 'all') return items.map((_, i) => i)
116
+ if (trimmed === 'n' || trimmed === 'none') return []
198
117
  const nums = answer
199
118
  .split(/[,\s]+/)
200
119
  .map((s) => parseInt(s, 10))
201
120
  .filter((n) => !isNaN(n) && n >= 1 && n <= items.length)
202
121
  .map((n) => n - 1)
203
- // 去重
204
122
  return [...new Set(nums)]
205
123
  }
206
124
 
207
125
  /**
208
- * 单选
209
- * @param {string} question
210
- * @param {Array<{label: string}>} items
211
- * @param {{defaultIndex?: number}} [opts]
126
+ * 单选(TUI:上下导航 + 回车确认)。
212
127
  * @returns {Promise<number>} 选中的索引(items.length===1 时直接返回 0)
213
128
  */
214
129
  export async function select(question, items, { defaultIndex = 0 } = {}) {
215
130
  if (items.length === 0) return -1
216
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
217
138
  console.log(question)
218
139
  items.forEach((item, i) => {
219
140
  const marker = i === defaultIndex ? ' (默认)' : ''
@@ -225,17 +146,14 @@ export async function select(question, items, { defaultIndex = 0 } = {}) {
225
146
  return n - 1
226
147
  }
227
148
 
228
- /**
229
- * 是非确认
230
- * @param {string} question
231
- * @param {{defaultValue?: boolean}} [opts]
232
- * @returns {Promise<boolean>}
233
- */
149
+ /** 是非确认(TUI:y/n;非 TTY:输入) */
234
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
+ }
235
155
  const hint = defaultValue ? 'Y/n' : 'y/N'
236
- const answer = await ask(`${question} (${hint})`, {
237
- defaultValue: defaultValue ? 'y' : 'n',
238
- })
156
+ const answer = await ask(`${question} (${hint})`, { defaultValue: defaultValue ? 'y' : 'n' })
239
157
  const v = answer.trim().toLowerCase()
240
158
  if (!v) return defaultValue
241
159
  return v === 'y' || v === 'yes'
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@2api-store/activate",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "TokenShop CLI 一键激活:配置本地 codex/claude/gemini/grok/opencode 指向 2api.store 中转",
5
5
  "type": "module",
6
6
  "bin": {
7
- "activate": "activate.mjs"
7
+ "activate": "./activate.mjs"
8
8
  },
9
9
  "main": "./activate.mjs",
10
10
  "engines": {
@@ -31,6 +31,9 @@
31
31
  "opencode"
32
32
  ],
33
33
  "author": "TokenShop",
34
+ "dependencies": {
35
+ "@inquirer/prompts": "^7.0.0"
36
+ },
34
37
  "license": "MIT",
35
38
  "publishConfig": {
36
39
  "access": "public"