lingyao-ai 0.2.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 +53 -0
- package/cli.js +871 -0
- package/package.json +18 -0
- package/project.config.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# lingyao-ai
|
|
2
|
+
|
|
3
|
+
A customizable setup CLI that behaves like `npx leishen-ai`, with your own configuration.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Configure Codex and/or Claude Code in one interactive flow.
|
|
8
|
+
- Optional auto-install checks for `@openai/codex` and `@anthropic-ai/claude-code`.
|
|
9
|
+
- Writes and backs up:
|
|
10
|
+
- `~/.claude/config.json`
|
|
11
|
+
- `~/.codex/config.toml`
|
|
12
|
+
- `~/.codex/auth.json`
|
|
13
|
+
- `~/.codex/.env`
|
|
14
|
+
- shell RC block (`.zshrc`, `.bashrc`, etc.)
|
|
15
|
+
- On Windows, optionally patch VSCode ChatGPT extension model order.
|
|
16
|
+
|
|
17
|
+
## 1) Customize your config
|
|
18
|
+
|
|
19
|
+
Edit `project.config.json`:
|
|
20
|
+
|
|
21
|
+
- `brand.*`: product name and shell marker.
|
|
22
|
+
- `apiKey.*`: API key format and env variable name.
|
|
23
|
+
- `endpoints.*`: your gateway domain and paths.
|
|
24
|
+
- `cli.*`: min Node version, npm mirror, package names.
|
|
25
|
+
- `claude.*` / `codex.*`: provider defaults and target files.
|
|
26
|
+
- `codex.windowsVscodePatch.*`: Windows extension patch behavior.
|
|
27
|
+
|
|
28
|
+
## 2) Run locally
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
cd /Users/espresso/Project/lingyao-ai
|
|
32
|
+
node cli.js
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Use a custom config file:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
node cli.js --config /absolute/path/to/project.config.json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## 3) Publish and use with npx
|
|
42
|
+
|
|
43
|
+
Package name is already set to `lingyao-ai`.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm publish --access public
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Then users can run:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npx lingyao-ai
|
|
53
|
+
```
|
package/cli.js
ADDED
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict'
|
|
4
|
+
|
|
5
|
+
const fs = require('fs')
|
|
6
|
+
const os = require('os')
|
|
7
|
+
const path = require('path')
|
|
8
|
+
const readline = require('readline')
|
|
9
|
+
const { spawnSync } = require('child_process')
|
|
10
|
+
|
|
11
|
+
const DEFAULT_CONFIG_PATH = path.join(__dirname, 'project.config.json')
|
|
12
|
+
|
|
13
|
+
function exitGracefully() {
|
|
14
|
+
console.log('\nCancelled.')
|
|
15
|
+
process.exit(0)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
process.on('SIGINT', exitGracefully)
|
|
19
|
+
|
|
20
|
+
function showHelp() {
|
|
21
|
+
console.log(`
|
|
22
|
+
Usage:
|
|
23
|
+
node cli.js [--config /abs/path/to/project.config.json]
|
|
24
|
+
|
|
25
|
+
Options:
|
|
26
|
+
-c, --config Use a custom configuration file.
|
|
27
|
+
-h, --help Show help.
|
|
28
|
+
`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseArgs(argv) {
|
|
32
|
+
let configPath = DEFAULT_CONFIG_PATH
|
|
33
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
34
|
+
const arg = argv[i]
|
|
35
|
+
if (arg === '-h' || arg === '--help') {
|
|
36
|
+
showHelp()
|
|
37
|
+
process.exit(0)
|
|
38
|
+
}
|
|
39
|
+
if (arg === '-c' || arg === '--config') {
|
|
40
|
+
const next = argv[i + 1]
|
|
41
|
+
if (!next) {
|
|
42
|
+
throw new Error('Missing value for --config')
|
|
43
|
+
}
|
|
44
|
+
configPath = path.resolve(next)
|
|
45
|
+
i += 1
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { configPath }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function loadConfig(configPath) {
|
|
52
|
+
if (!fs.existsSync(configPath)) {
|
|
53
|
+
throw new Error(`Config file not found: ${configPath}`)
|
|
54
|
+
}
|
|
55
|
+
const raw = fs.readFileSync(configPath, 'utf8')
|
|
56
|
+
const cfg = JSON.parse(raw)
|
|
57
|
+
validateConfig(cfg)
|
|
58
|
+
return cfg
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function validateConfig(cfg) {
|
|
62
|
+
const required = [
|
|
63
|
+
['brand.name', cfg?.brand?.name],
|
|
64
|
+
['brand.shellMarker', cfg?.brand?.shellMarker],
|
|
65
|
+
['apiKey.regex', cfg?.apiKey?.regex],
|
|
66
|
+
['apiKey.example', cfg?.apiKey?.example],
|
|
67
|
+
['apiKey.envKey', cfg?.apiKey?.envKey],
|
|
68
|
+
['endpoints.baseOrigin', cfg?.endpoints?.baseOrigin],
|
|
69
|
+
['endpoints.claudePath', cfg?.endpoints?.claudePath],
|
|
70
|
+
['endpoints.codexPath', cfg?.endpoints?.codexPath],
|
|
71
|
+
['cli.packages.codex', cfg?.cli?.packages?.codex],
|
|
72
|
+
['cli.packages.claude', cfg?.cli?.packages?.claude],
|
|
73
|
+
['claude.configPath', cfg?.claude?.configPath],
|
|
74
|
+
['claude.primaryApiKey', cfg?.claude?.primaryApiKey],
|
|
75
|
+
['claude.baseUrlEnvKey', cfg?.claude?.baseUrlEnvKey],
|
|
76
|
+
['claude.authTokenEnvKey', cfg?.claude?.authTokenEnvKey],
|
|
77
|
+
['codex.configDir', cfg?.codex?.configDir],
|
|
78
|
+
['codex.providerName', cfg?.codex?.providerName],
|
|
79
|
+
['codex.defaultModel', cfg?.codex?.defaultModel],
|
|
80
|
+
['codex.reasoningEffort', cfg?.codex?.reasoningEffort],
|
|
81
|
+
['codex.wireApi', cfg?.codex?.wireApi]
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
const missing = required.filter(([, value]) => !value).map(([key]) => key)
|
|
85
|
+
if (missing.length > 0) {
|
|
86
|
+
throw new Error(`Config missing required fields: ${missing.join(', ')}`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
new RegExp(cfg.apiKey.regex)
|
|
91
|
+
} catch (error) {
|
|
92
|
+
throw new Error(`Invalid apiKey.regex: ${error.message}`)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function joinUrl(base, subPath) {
|
|
97
|
+
const left = String(base || '').replace(/\/+$/, '')
|
|
98
|
+
const right = String(subPath || '').replace(/^\/+/, '')
|
|
99
|
+
return right ? `${left}/${right}` : left
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function ensureDir(dirPath) {
|
|
103
|
+
if (!fs.existsSync(dirPath)) {
|
|
104
|
+
fs.mkdirSync(dirPath, { recursive: true })
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function backupFileIfExists(filePath, suffix = '.bak') {
|
|
109
|
+
if (!fs.existsSync(filePath)) return null
|
|
110
|
+
const backupPath = `${filePath}${suffix}`
|
|
111
|
+
if (!fs.existsSync(backupPath)) {
|
|
112
|
+
fs.copyFileSync(filePath, backupPath)
|
|
113
|
+
}
|
|
114
|
+
return backupPath
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function writeJson(filePath, data) {
|
|
118
|
+
ensureDir(path.dirname(filePath))
|
|
119
|
+
const backupPath = backupFileIfExists(filePath)
|
|
120
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8')
|
|
121
|
+
return { filePath, backupPath }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function writeText(filePath, content) {
|
|
125
|
+
ensureDir(path.dirname(filePath))
|
|
126
|
+
const backupPath = backupFileIfExists(filePath)
|
|
127
|
+
fs.writeFileSync(filePath, content, 'utf8')
|
|
128
|
+
return { filePath, backupPath }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function upsertDotenvFile(filePath, envVars) {
|
|
132
|
+
ensureDir(path.dirname(filePath))
|
|
133
|
+
const backupPath = backupFileIfExists(filePath)
|
|
134
|
+
const content = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : ''
|
|
135
|
+
const lines = content.split(/\r?\n/)
|
|
136
|
+
const remaining = new Set(Object.keys(envVars))
|
|
137
|
+
|
|
138
|
+
const updatedLines = lines.map((line) => {
|
|
139
|
+
const trimmed = String(line || '').trim()
|
|
140
|
+
if (!trimmed || trimmed.startsWith('#')) return line
|
|
141
|
+
|
|
142
|
+
const eqIdx = String(line).indexOf('=')
|
|
143
|
+
if (eqIdx === -1) return line
|
|
144
|
+
|
|
145
|
+
const key = String(line)
|
|
146
|
+
.slice(0, eqIdx)
|
|
147
|
+
.trim()
|
|
148
|
+
if (!Object.prototype.hasOwnProperty.call(envVars, key)) return line
|
|
149
|
+
|
|
150
|
+
remaining.delete(key)
|
|
151
|
+
return `${key}=${envVars[key]}`
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
for (const key of remaining) {
|
|
155
|
+
updatedLines.push(`${key}=${envVars[key]}`)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fs.writeFileSync(filePath, updatedLines.join('\n'), 'utf8')
|
|
159
|
+
if (process.platform !== 'win32') {
|
|
160
|
+
try {
|
|
161
|
+
fs.chmodSync(filePath, 0o600)
|
|
162
|
+
} catch (_) {
|
|
163
|
+
// Ignore chmod failure.
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { filePath, backupPath }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function detectShellRcFiles() {
|
|
171
|
+
if (process.platform === 'win32') return []
|
|
172
|
+
|
|
173
|
+
const home = os.homedir()
|
|
174
|
+
const shell = String(process.env.SHELL || '').toLowerCase()
|
|
175
|
+
const priority = shell.includes('zsh')
|
|
176
|
+
? ['.zshrc', '.zprofile']
|
|
177
|
+
: shell.includes('bash')
|
|
178
|
+
? ['.bashrc', '.bash_profile']
|
|
179
|
+
: ['.zshrc', '.bashrc']
|
|
180
|
+
|
|
181
|
+
const defaults = ['.bashrc', '.bash_profile', '.profile', '.zshrc', '.zprofile']
|
|
182
|
+
const ordered = [...priority, ...defaults]
|
|
183
|
+
const seen = new Set()
|
|
184
|
+
const rcFiles = []
|
|
185
|
+
|
|
186
|
+
for (const fileName of ordered) {
|
|
187
|
+
if (seen.has(fileName)) continue
|
|
188
|
+
seen.add(fileName)
|
|
189
|
+
rcFiles.push(path.join(home, fileName))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return rcFiles
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function updateRcFile(rcPath, envVars, marker) {
|
|
196
|
+
if (!rcPath || Object.keys(envVars).length === 0) return null
|
|
197
|
+
|
|
198
|
+
const startMarker = `# >>> ${marker}`
|
|
199
|
+
const endMarker = `# <<< ${marker}`
|
|
200
|
+
const content = fs.existsSync(rcPath) ? fs.readFileSync(rcPath, 'utf8') : ''
|
|
201
|
+
const lines = content.split(/\r?\n/)
|
|
202
|
+
const startIdx = lines.findIndex((line) => line.trim() === startMarker)
|
|
203
|
+
const endIdx = lines.findIndex((line) => line.trim() === endMarker)
|
|
204
|
+
|
|
205
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx >= startIdx) {
|
|
206
|
+
lines.splice(startIdx, endIdx - startIdx + 1)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const block = [startMarker]
|
|
210
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
211
|
+
block.push(`export ${key}="${value}"`)
|
|
212
|
+
}
|
|
213
|
+
block.push(endMarker, '')
|
|
214
|
+
|
|
215
|
+
const trimmed = lines.join('\n').trimEnd()
|
|
216
|
+
const finalContent = trimmed ? `${trimmed}\n\n${block.join('\n')}` : block.join('\n')
|
|
217
|
+
fs.writeFileSync(rcPath, finalContent, 'utf8')
|
|
218
|
+
return rcPath
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function configureWindowsEnv(envVars) {
|
|
222
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
223
|
+
try {
|
|
224
|
+
spawnSync('setx', [key, String(value)], { stdio: 'ignore' })
|
|
225
|
+
} catch (_) {
|
|
226
|
+
// Best-effort only.
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function askLine(question) {
|
|
232
|
+
return new Promise((resolve) => {
|
|
233
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
234
|
+
rl.question(question, (answer) => {
|
|
235
|
+
rl.close()
|
|
236
|
+
resolve(String(answer || '').trim())
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function askYesNo(question, initialYes = true) {
|
|
242
|
+
const yesHint = initialYes ? 'Y/n' : 'y/N'
|
|
243
|
+
while (true) {
|
|
244
|
+
const answer = (await askLine(`${question} [${yesHint}]: `)).toLowerCase()
|
|
245
|
+
if (!answer) return initialYes
|
|
246
|
+
if (['y', 'yes'].includes(answer)) return true
|
|
247
|
+
if (['n', 'no'].includes(answer)) return false
|
|
248
|
+
console.log('Please type y or n.')
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function promptApiKey(apiKeyRegex, example) {
|
|
253
|
+
while (true) {
|
|
254
|
+
const key = await askLine(`Input API key (${example}): `)
|
|
255
|
+
if (apiKeyRegex.test(key)) return key
|
|
256
|
+
console.log('Invalid API key format. Try again.')
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function promptTargets() {
|
|
261
|
+
while (true) {
|
|
262
|
+
console.log('\nSelect target:')
|
|
263
|
+
console.log('1) Codex')
|
|
264
|
+
console.log('2) Claude Code')
|
|
265
|
+
console.log('3) Codex + Claude Code')
|
|
266
|
+
console.log('q) Quit')
|
|
267
|
+
|
|
268
|
+
const answer = (await askLine('Choice: ')).toLowerCase()
|
|
269
|
+
if (answer === '1') return ['codex']
|
|
270
|
+
if (answer === '2') return ['claude']
|
|
271
|
+
if (answer === '3') return ['codex', 'claude']
|
|
272
|
+
if (['q', 'quit', 'exit'].includes(answer)) process.exit(0)
|
|
273
|
+
|
|
274
|
+
console.log('Invalid choice.')
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function getNodeMajorVersion() {
|
|
279
|
+
const raw = (process.versions && process.versions.node) || '0.0.0'
|
|
280
|
+
const major = Number.parseInt(String(raw).split('.')[0], 10)
|
|
281
|
+
return Number.isFinite(major) ? major : 0
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function ensureMinimumNodeVersion(minMajor) {
|
|
285
|
+
const required = Number.isFinite(Number(minMajor)) ? Number(minMajor) : 16
|
|
286
|
+
const current = getNodeMajorVersion()
|
|
287
|
+
if (current > 0 && current < required) {
|
|
288
|
+
console.log(`Node.js ${required}+ required. Current: ${process.versions.node}`)
|
|
289
|
+
return false
|
|
290
|
+
}
|
|
291
|
+
return true
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function findExecutableInPath(binName) {
|
|
295
|
+
const rawPath = process.env.PATH || ''
|
|
296
|
+
if (!rawPath) return null
|
|
297
|
+
|
|
298
|
+
const dirs = rawPath.split(path.delimiter).filter(Boolean)
|
|
299
|
+
const exts =
|
|
300
|
+
process.platform === 'win32'
|
|
301
|
+
? (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';').filter(Boolean)
|
|
302
|
+
: ['']
|
|
303
|
+
|
|
304
|
+
for (const dir of dirs) {
|
|
305
|
+
for (const ext of exts) {
|
|
306
|
+
const fullPath =
|
|
307
|
+
process.platform === 'win32' ? path.join(dir, `${binName}${ext}`) : path.join(dir, binName)
|
|
308
|
+
try {
|
|
309
|
+
if (fs.existsSync(fullPath)) return fullPath
|
|
310
|
+
} catch (_) {
|
|
311
|
+
// Ignore path entries that cannot be read.
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return null
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function runCommandText(command, args) {
|
|
320
|
+
let result
|
|
321
|
+
try {
|
|
322
|
+
result = spawnSync(command, args, { encoding: 'utf8' })
|
|
323
|
+
} catch (error) {
|
|
324
|
+
return { ok: false, error, stdout: '', stderr: '' }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const stdout = String(result.stdout || '')
|
|
328
|
+
const stderr = String(result.stderr || '')
|
|
329
|
+
if (result.error) {
|
|
330
|
+
return { ok: false, error: result.error, stdout, stderr }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (result.status === 0) {
|
|
334
|
+
return { ok: true, text: stdout.trim(), stdout, stderr }
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return { ok: false, status: result.status, stdout, stderr }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function detectCli(binName, versionArgs, npmPackageName) {
|
|
341
|
+
const executablePath = findExecutableInPath(binName)
|
|
342
|
+
if (executablePath) {
|
|
343
|
+
const versionResult = runCommandText(binName, versionArgs)
|
|
344
|
+
if (versionResult.ok) {
|
|
345
|
+
const version = versionResult.text ? versionResult.text.split('\n')[0].trim() : null
|
|
346
|
+
return {
|
|
347
|
+
state: 'ready',
|
|
348
|
+
executablePath,
|
|
349
|
+
version
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
state: 'exists_but_failed',
|
|
355
|
+
executablePath,
|
|
356
|
+
stdout: versionResult.stdout,
|
|
357
|
+
stderr: versionResult.stderr
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (findExecutableInPath('npm')) {
|
|
362
|
+
const listResult = runCommandText('npm', ['ls', '-g', '--depth=0', npmPackageName])
|
|
363
|
+
if (listResult.ok) {
|
|
364
|
+
return { state: 'installed_not_on_path' }
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return { state: 'not_installed' }
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function showMirrorHint(pkgName, registry) {
|
|
372
|
+
if (!pkgName || !registry) return
|
|
373
|
+
console.log(`Hint: if install is slow, try:`)
|
|
374
|
+
console.log(`npm install -g ${pkgName} --registry=${registry}`)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function showInstallTroubleshoot(result) {
|
|
378
|
+
const out = `${String(result?.stdout || '')}\n${String(result?.stderr || '')}`
|
|
379
|
+
if (/EACCES|permission denied/i.test(out)) {
|
|
380
|
+
console.log('Install failed: permission issue.')
|
|
381
|
+
return
|
|
382
|
+
}
|
|
383
|
+
if (/ETARGET|E404|404 Not Found/i.test(out)) {
|
|
384
|
+
console.log('Install failed: npm registry may be out of sync.')
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
if (/ECONNRESET|ETIMEDOUT|ENOTFOUND|network/i.test(out)) {
|
|
388
|
+
console.log('Install failed: network problem detected.')
|
|
389
|
+
return
|
|
390
|
+
}
|
|
391
|
+
if (/unsupported engine|notsup|engine/i.test(out)) {
|
|
392
|
+
console.log('Install failed: Node.js version may be too low.')
|
|
393
|
+
return
|
|
394
|
+
}
|
|
395
|
+
console.log('Install failed. Check npm logs for details.')
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function installGlobalPackage(pkgName) {
|
|
399
|
+
const result = spawnSync('npm', ['install', '-g', pkgName], {
|
|
400
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
401
|
+
encoding: 'utf8'
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
if (result.error) {
|
|
405
|
+
return {
|
|
406
|
+
ok: false,
|
|
407
|
+
error: result.error,
|
|
408
|
+
stdout: '',
|
|
409
|
+
stderr: ''
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (result.status === 0) {
|
|
414
|
+
return {
|
|
415
|
+
ok: true,
|
|
416
|
+
status: 0,
|
|
417
|
+
stdout: String(result.stdout || ''),
|
|
418
|
+
stderr: String(result.stderr || '')
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
ok: false,
|
|
424
|
+
status: result.status,
|
|
425
|
+
stdout: String(result.stdout || ''),
|
|
426
|
+
stderr: String(result.stderr || '')
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function ensureCliInstalled(options) {
|
|
431
|
+
const {
|
|
432
|
+
label,
|
|
433
|
+
binName,
|
|
434
|
+
versionArgs,
|
|
435
|
+
npmPackageName,
|
|
436
|
+
mirrorRegistry,
|
|
437
|
+
minNodeMajorForInstall
|
|
438
|
+
} = options
|
|
439
|
+
|
|
440
|
+
const detected = detectCli(binName, versionArgs, npmPackageName)
|
|
441
|
+
if (detected.state === 'ready') {
|
|
442
|
+
const ver = detected.version ? ` (${detected.version})` : ''
|
|
443
|
+
console.log(`${label} already detected${ver}.`)
|
|
444
|
+
return true
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (
|
|
448
|
+
Number.isFinite(Number(minNodeMajorForInstall)) &&
|
|
449
|
+
getNodeMajorVersion() < Number(minNodeMajorForInstall)
|
|
450
|
+
) {
|
|
451
|
+
console.log(
|
|
452
|
+
`${label} auto install skipped: Node.js ${minNodeMajorForInstall}+ recommended. Current: ${process.versions.node}`
|
|
453
|
+
)
|
|
454
|
+
return false
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (!findExecutableInPath('npm')) {
|
|
458
|
+
console.log('npm command not found. Install Node.js/npm first.')
|
|
459
|
+
return false
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (detected.state === 'installed_not_on_path') {
|
|
463
|
+
console.log(`${label} package is installed globally, but command is not on PATH.`)
|
|
464
|
+
return false
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (detected.state === 'exists_but_failed') {
|
|
468
|
+
console.log(`${label} command exists but failed to run.`)
|
|
469
|
+
const details = String(detected.stderr || detected.stdout || '').trim()
|
|
470
|
+
if (details) {
|
|
471
|
+
console.log(details.split('\n').slice(0, 6).join('\n'))
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const shouldInstall = await askYesNo(`Run npm install -g ${npmPackageName} now?`, true)
|
|
476
|
+
if (!shouldInstall) {
|
|
477
|
+
showMirrorHint(npmPackageName, mirrorRegistry)
|
|
478
|
+
return false
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
showMirrorHint(npmPackageName, mirrorRegistry)
|
|
482
|
+
const installResult = installGlobalPackage(npmPackageName)
|
|
483
|
+
if (!installResult.ok) {
|
|
484
|
+
showInstallTroubleshoot(installResult)
|
|
485
|
+
return false
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const after = detectCli(binName, versionArgs, npmPackageName)
|
|
489
|
+
if (after.state === 'ready') {
|
|
490
|
+
const ver = after.version ? ` (${after.version})` : ''
|
|
491
|
+
console.log(`${label} installed successfully${ver}.`)
|
|
492
|
+
return true
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
console.log(`${label} installed, but command still not found on PATH.`)
|
|
496
|
+
return false
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function escapeRegExp(str) {
|
|
500
|
+
return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function parseExtensionSemver(name, extensionPrefix) {
|
|
504
|
+
const safePrefix = escapeRegExp(extensionPrefix)
|
|
505
|
+
const regex = new RegExp(`^${safePrefix}(\\d+)\\.(\\d+)\\.(\\d+)(?:[-.].*)?$`, 'i')
|
|
506
|
+
const match = String(name || '').trim().match(regex)
|
|
507
|
+
if (!match) return null
|
|
508
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])]
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function compareSemverTupleDesc(left, right) {
|
|
512
|
+
const l = Array.isArray(left) ? left : []
|
|
513
|
+
const r = Array.isArray(right) ? right : []
|
|
514
|
+
const max = Math.max(l.length, r.length, 0)
|
|
515
|
+
for (let i = 0; i < max; i += 1) {
|
|
516
|
+
const lv = Number.isFinite(l[i]) ? l[i] : -1
|
|
517
|
+
const rv = Number.isFinite(r[i]) ? r[i] : -1
|
|
518
|
+
if (lv > rv) return -1
|
|
519
|
+
if (lv < rv) return 1
|
|
520
|
+
}
|
|
521
|
+
return 0
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function listWindowsVscodeAssets(windowsPatchCfg) {
|
|
525
|
+
const home = os.homedir()
|
|
526
|
+
const extensionPrefix = String(windowsPatchCfg?.extensionPrefix || 'openai.chatgpt-')
|
|
527
|
+
const userDirs = Array.isArray(windowsPatchCfg?.userDirs)
|
|
528
|
+
? windowsPatchCfg.userDirs
|
|
529
|
+
: ['.vscode', '.vscode-insiders']
|
|
530
|
+
|
|
531
|
+
const candidates = []
|
|
532
|
+
|
|
533
|
+
for (const userDir of userDirs) {
|
|
534
|
+
const extensionsRoot = path.join(home, String(userDir), 'extensions')
|
|
535
|
+
if (!fs.existsSync(extensionsRoot)) continue
|
|
536
|
+
|
|
537
|
+
let entries = []
|
|
538
|
+
try {
|
|
539
|
+
entries = fs.readdirSync(extensionsRoot, { withFileTypes: true })
|
|
540
|
+
} catch (_) {
|
|
541
|
+
continue
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
for (const entry of entries) {
|
|
545
|
+
if (!entry?.isDirectory?.()) continue
|
|
546
|
+
if (!String(entry.name).startsWith(extensionPrefix)) continue
|
|
547
|
+
|
|
548
|
+
const extensionDir = path.join(extensionsRoot, entry.name)
|
|
549
|
+
const assetsDir = path.join(extensionDir, 'webview', 'assets')
|
|
550
|
+
if (!fs.existsSync(assetsDir)) continue
|
|
551
|
+
|
|
552
|
+
let assetEntries = []
|
|
553
|
+
try {
|
|
554
|
+
assetEntries = fs.readdirSync(assetsDir, { withFileTypes: true })
|
|
555
|
+
} catch (_) {
|
|
556
|
+
continue
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
for (const assetEntry of assetEntries) {
|
|
560
|
+
if (!assetEntry?.isFile?.()) continue
|
|
561
|
+
if (!/^index-.*\.js$/i.test(String(assetEntry.name))) continue
|
|
562
|
+
|
|
563
|
+
const filePath = path.join(assetsDir, assetEntry.name)
|
|
564
|
+
let mtimeMs = 0
|
|
565
|
+
try {
|
|
566
|
+
mtimeMs = Number(fs.statSync(filePath)?.mtimeMs || 0)
|
|
567
|
+
} catch (_) {
|
|
568
|
+
mtimeMs = 0
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
candidates.push({
|
|
572
|
+
filePath,
|
|
573
|
+
extensionName: entry.name,
|
|
574
|
+
version: parseExtensionSemver(entry.name, extensionPrefix),
|
|
575
|
+
mtimeMs
|
|
576
|
+
})
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
candidates.sort((a, b) => {
|
|
582
|
+
const versionCmp = compareSemverTupleDesc(a.version, b.version)
|
|
583
|
+
if (versionCmp !== 0) return versionCmp
|
|
584
|
+
if (a.mtimeMs > b.mtimeMs) return -1
|
|
585
|
+
if (a.mtimeMs < b.mtimeMs) return 1
|
|
586
|
+
return 0
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
return candidates
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function extractQuotedModels(expression) {
|
|
593
|
+
const models = []
|
|
594
|
+
const regex = /["']([^"']+)["']/g
|
|
595
|
+
let match = regex.exec(expression)
|
|
596
|
+
while (match) {
|
|
597
|
+
models.push(match[1])
|
|
598
|
+
match = regex.exec(expression)
|
|
599
|
+
}
|
|
600
|
+
return models
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function patchDefaultModelOrder(content, targetModel) {
|
|
604
|
+
const match = /DEFAULT_MODEL_ORDER\s*=\s*\[([\s\S]*?)\]/.exec(content)
|
|
605
|
+
if (!match) return { status: 'marker_not_found' }
|
|
606
|
+
|
|
607
|
+
const originalModels = extractQuotedModels(match[1])
|
|
608
|
+
if (originalModels.length === 0) return { status: 'parse_failed' }
|
|
609
|
+
|
|
610
|
+
const nextModels = [targetModel, ...originalModels.filter((m) => m !== targetModel)]
|
|
611
|
+
const alreadyUpToDate =
|
|
612
|
+
originalModels.length === nextModels.length &&
|
|
613
|
+
originalModels.every((model, idx) => model === nextModels[idx])
|
|
614
|
+
|
|
615
|
+
if (alreadyUpToDate) {
|
|
616
|
+
return { status: 'already', models: originalModels }
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const replacement = `DEFAULT_MODEL_ORDER=[${nextModels
|
|
620
|
+
.map((model) => JSON.stringify(model))
|
|
621
|
+
.join(',')}]`
|
|
622
|
+
|
|
623
|
+
const start = match.index
|
|
624
|
+
const end = start + match[0].length
|
|
625
|
+
const updatedContent = `${content.slice(0, start)}${replacement}${content.slice(end)}`
|
|
626
|
+
const verify = /DEFAULT_MODEL_ORDER\s*=\s*\[([\s\S]*?)\]/.exec(updatedContent)
|
|
627
|
+
if (!verify) return { status: 'verify_failed' }
|
|
628
|
+
|
|
629
|
+
const verifyModels = extractQuotedModels(verify[1])
|
|
630
|
+
if (verifyModels[0] !== targetModel) return { status: 'verify_failed' }
|
|
631
|
+
|
|
632
|
+
return { status: 'patched', content: updatedContent, models: verifyModels }
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function patchWindowsVscodeDefaultModel(targetModel, windowsPatchCfg) {
|
|
636
|
+
if (process.platform !== 'win32') return { status: 'not_windows' }
|
|
637
|
+
|
|
638
|
+
const candidates = listWindowsVscodeAssets(windowsPatchCfg)
|
|
639
|
+
if (candidates.length === 0) return { status: 'assets_not_found' }
|
|
640
|
+
|
|
641
|
+
let parseFailed = 0
|
|
642
|
+
for (const candidate of candidates) {
|
|
643
|
+
let content = ''
|
|
644
|
+
try {
|
|
645
|
+
content = fs.readFileSync(candidate.filePath, 'utf8')
|
|
646
|
+
} catch (_) {
|
|
647
|
+
continue
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const result = patchDefaultModelOrder(content, targetModel)
|
|
651
|
+
if (result.status === 'marker_not_found' || result.status === 'parse_failed') {
|
|
652
|
+
parseFailed += 1
|
|
653
|
+
continue
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (result.status === 'already') {
|
|
657
|
+
return {
|
|
658
|
+
status: 'already',
|
|
659
|
+
filePath: candidate.filePath,
|
|
660
|
+
extensionName: candidate.extensionName
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (result.status !== 'patched') {
|
|
665
|
+
continue
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const backupPath = backupFileIfExists(candidate.filePath, '.backup')
|
|
669
|
+
fs.writeFileSync(candidate.filePath, result.content, 'utf8')
|
|
670
|
+
|
|
671
|
+
return {
|
|
672
|
+
status: 'patched',
|
|
673
|
+
filePath: candidate.filePath,
|
|
674
|
+
backupPath,
|
|
675
|
+
extensionName: candidate.extensionName
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (parseFailed > 0) return { status: 'pattern_not_supported' }
|
|
680
|
+
return { status: 'assets_not_found' }
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function tomlString(value) {
|
|
684
|
+
return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function renderCodexToml(cfg) {
|
|
688
|
+
const codexBaseUrl = joinUrl(cfg.endpoints.baseOrigin, cfg.endpoints.codexPath)
|
|
689
|
+
const provider = cfg.codex.providerName
|
|
690
|
+
const migrations = Object.entries(cfg.codex.modelMigrations || {})
|
|
691
|
+
const migrationSection =
|
|
692
|
+
migrations.length === 0
|
|
693
|
+
? ''
|
|
694
|
+
: `\n[notice.model_migrations]\n${migrations
|
|
695
|
+
.map(([from, to]) => `"${tomlString(from)}" = "${tomlString(to)}"`)
|
|
696
|
+
.join('\n')}\n`
|
|
697
|
+
|
|
698
|
+
const envInstructionLine = cfg?.codex?.envKeyInstructions
|
|
699
|
+
? `\nenv_key_instructions = "${tomlString(cfg.codex.envKeyInstructions)}"`
|
|
700
|
+
: ''
|
|
701
|
+
|
|
702
|
+
return `model_provider = "${tomlString(provider)}"
|
|
703
|
+
model = "${tomlString(cfg.codex.defaultModel)}"
|
|
704
|
+
model_reasoning_effort = "${tomlString(cfg.codex.reasoningEffort)}"
|
|
705
|
+
disable_response_storage = true
|
|
706
|
+
preferred_auth_method = "apikey"
|
|
707
|
+
|
|
708
|
+
[model_providers.${provider}]
|
|
709
|
+
name = "${tomlString(provider)}"
|
|
710
|
+
base_url = "${tomlString(codexBaseUrl)}"
|
|
711
|
+
wire_api = "${tomlString(cfg.codex.wireApi)}"
|
|
712
|
+
requires_openai_auth = ${cfg.codex.requiresOpenAIAuth ? 'true' : 'false'}
|
|
713
|
+
env_key = "${tomlString(cfg.apiKey.envKey)}"${envInstructionLine}${migrationSection}`
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function configureClaude(cfg, apiKey) {
|
|
717
|
+
const home = os.homedir()
|
|
718
|
+
const claudeConfigPath = path.join(home, cfg.claude.configPath)
|
|
719
|
+
const info = writeJson(claudeConfigPath, { primaryApiKey: cfg.claude.primaryApiKey })
|
|
720
|
+
const claudeBaseUrl = joinUrl(cfg.endpoints.baseOrigin, cfg.endpoints.claudePath)
|
|
721
|
+
|
|
722
|
+
return {
|
|
723
|
+
env: {
|
|
724
|
+
[cfg.claude.baseUrlEnvKey]: claudeBaseUrl,
|
|
725
|
+
[cfg.claude.authTokenEnvKey]: apiKey
|
|
726
|
+
},
|
|
727
|
+
files: [
|
|
728
|
+
{
|
|
729
|
+
label: 'Claude config',
|
|
730
|
+
path: info.filePath,
|
|
731
|
+
backupPath: info.backupPath
|
|
732
|
+
}
|
|
733
|
+
]
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function configureCodex(cfg, apiKey) {
|
|
738
|
+
const home = os.homedir()
|
|
739
|
+
const codexDir = path.join(home, cfg.codex.configDir)
|
|
740
|
+
const tomlPath = path.join(codexDir, 'config.toml')
|
|
741
|
+
const authPath = path.join(codexDir, 'auth.json')
|
|
742
|
+
const envPath = path.join(codexDir, '.env')
|
|
743
|
+
|
|
744
|
+
const tomlInfo = writeText(tomlPath, renderCodexToml(cfg))
|
|
745
|
+
const authInfo = writeJson(authPath, cfg.codex.authJson || { OPENAI_API_KEY: null })
|
|
746
|
+
const envInfo = upsertDotenvFile(envPath, { [cfg.apiKey.envKey]: apiKey })
|
|
747
|
+
|
|
748
|
+
return {
|
|
749
|
+
env: {
|
|
750
|
+
[cfg.apiKey.envKey]: apiKey
|
|
751
|
+
},
|
|
752
|
+
files: [
|
|
753
|
+
{ label: 'Codex config.toml', path: tomlInfo.filePath, backupPath: tomlInfo.backupPath },
|
|
754
|
+
{ label: 'Codex auth.json', path: authInfo.filePath, backupPath: authInfo.backupPath },
|
|
755
|
+
{ label: 'Codex .env', path: envInfo.filePath, backupPath: envInfo.backupPath }
|
|
756
|
+
]
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function printSummary(fileSummaries) {
|
|
761
|
+
console.log('\nCompleted changes:')
|
|
762
|
+
for (const item of fileSummaries) {
|
|
763
|
+
console.log(`- ${item.label}: ${item.path}`)
|
|
764
|
+
console.log(` backup: ${item.backupPath || 'none (new file)'}`)
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
async function main() {
|
|
769
|
+
const { configPath } = parseArgs(process.argv.slice(2))
|
|
770
|
+
const cfg = loadConfig(configPath)
|
|
771
|
+
|
|
772
|
+
if (!ensureMinimumNodeVersion(cfg?.cli?.minNodeMajor)) {
|
|
773
|
+
process.exit(1)
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
console.log(`\n${cfg.brand.name}`)
|
|
777
|
+
console.log(`Config file: ${configPath}`)
|
|
778
|
+
console.log(`Base origin: ${cfg.endpoints.baseOrigin}`)
|
|
779
|
+
|
|
780
|
+
const apiKeyRegex = new RegExp(cfg.apiKey.regex)
|
|
781
|
+
const apiKey = await promptApiKey(apiKeyRegex, cfg.apiKey.example)
|
|
782
|
+
const targets = await promptTargets()
|
|
783
|
+
|
|
784
|
+
const npmMirrorRegistry = cfg?.cli?.npmMirrorRegistry || ''
|
|
785
|
+
const codexPackage = cfg.cli.packages.codex
|
|
786
|
+
const claudePackage = cfg.cli.packages.claude
|
|
787
|
+
|
|
788
|
+
if (targets.includes('codex')) {
|
|
789
|
+
await ensureCliInstalled({
|
|
790
|
+
label: 'Codex CLI',
|
|
791
|
+
binName: 'codex',
|
|
792
|
+
versionArgs: ['--version'],
|
|
793
|
+
npmPackageName: codexPackage,
|
|
794
|
+
mirrorRegistry: npmMirrorRegistry,
|
|
795
|
+
minNodeMajorForInstall: cfg?.cli?.codexMinNodeMajor
|
|
796
|
+
})
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (targets.includes('claude')) {
|
|
800
|
+
await ensureCliInstalled({
|
|
801
|
+
label: 'Claude CLI',
|
|
802
|
+
binName: 'claude',
|
|
803
|
+
versionArgs: ['--version'],
|
|
804
|
+
npmPackageName: claudePackage,
|
|
805
|
+
mirrorRegistry: npmMirrorRegistry,
|
|
806
|
+
minNodeMajorForInstall: cfg?.cli?.minNodeMajor
|
|
807
|
+
})
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
let envVars = {}
|
|
811
|
+
const fileSummaries = []
|
|
812
|
+
|
|
813
|
+
if (targets.includes('claude')) {
|
|
814
|
+
const result = configureClaude(cfg, apiKey)
|
|
815
|
+
envVars = { ...envVars, ...result.env }
|
|
816
|
+
fileSummaries.push(...result.files)
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
if (targets.includes('codex')) {
|
|
820
|
+
const result = configureCodex(cfg, apiKey)
|
|
821
|
+
envVars = { ...envVars, ...result.env }
|
|
822
|
+
fileSummaries.push(...result.files)
|
|
823
|
+
|
|
824
|
+
const windowsPatchCfg = cfg?.codex?.windowsVscodePatch
|
|
825
|
+
if (process.platform === 'win32' && windowsPatchCfg?.enabled) {
|
|
826
|
+
const shouldPatch = await askYesNo(
|
|
827
|
+
`Patch VSCode ChatGPT default model to ${cfg.codex.defaultModel}?`,
|
|
828
|
+
true
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
if (shouldPatch) {
|
|
832
|
+
const patchResult = patchWindowsVscodeDefaultModel(cfg.codex.defaultModel, windowsPatchCfg)
|
|
833
|
+
if (patchResult.status === 'patched') {
|
|
834
|
+
console.log(`VSCode model order patched: ${patchResult.filePath}`)
|
|
835
|
+
fileSummaries.push({
|
|
836
|
+
label: 'VSCode model order',
|
|
837
|
+
path: patchResult.filePath,
|
|
838
|
+
backupPath: patchResult.backupPath || null
|
|
839
|
+
})
|
|
840
|
+
} else if (patchResult.status === 'already') {
|
|
841
|
+
console.log(`VSCode model order already up to date: ${patchResult.filePath}`)
|
|
842
|
+
} else if (patchResult.status === 'assets_not_found') {
|
|
843
|
+
console.log('VSCode ChatGPT asset file not found, patch skipped.')
|
|
844
|
+
} else if (patchResult.status === 'pattern_not_supported') {
|
|
845
|
+
console.log('VSCode asset found but pattern not supported, patch skipped.')
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if (process.platform === 'win32') {
|
|
852
|
+
configureWindowsEnv(envVars)
|
|
853
|
+
console.log('Environment variables were written with setx. Open a new terminal to apply.')
|
|
854
|
+
} else {
|
|
855
|
+
const updatedRcFiles = detectShellRcFiles()
|
|
856
|
+
.map((rcPath) => updateRcFile(rcPath, envVars, cfg.brand.shellMarker))
|
|
857
|
+
.filter(Boolean)
|
|
858
|
+
|
|
859
|
+
if (updatedRcFiles.length > 0) {
|
|
860
|
+
console.log('\nUpdated shell rc files:')
|
|
861
|
+
updatedRcFiles.forEach((filePath) => console.log(`- ${filePath}`))
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
printSummary(fileSummaries)
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
main().catch((error) => {
|
|
869
|
+
console.error(`Failed: ${error.message}`)
|
|
870
|
+
process.exit(1)
|
|
871
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lingyao-ai",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Lingyao setup CLI for Codex and Claude Code.",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"bin": {
|
|
7
|
+
"lingyao-ai": "cli.js",
|
|
8
|
+
"lingyao": "cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node cli.js",
|
|
12
|
+
"lint": "node --check cli.js"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.0.0"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT"
|
|
18
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"brand": {
|
|
3
|
+
"name": "Lingyao AI",
|
|
4
|
+
"shellMarker": "lingyao-ai"
|
|
5
|
+
},
|
|
6
|
+
"apiKey": {
|
|
7
|
+
"regex": "^cr_[0-9a-f]{64}$",
|
|
8
|
+
"example": "cr_<64_hex>",
|
|
9
|
+
"envKey": "CRS_OAI_KEY"
|
|
10
|
+
},
|
|
11
|
+
"endpoints": {
|
|
12
|
+
"baseOrigin": "https://claude.chiddns.com",
|
|
13
|
+
"claudePath": "/",
|
|
14
|
+
"codexPath": "/v1"
|
|
15
|
+
},
|
|
16
|
+
"cli": {
|
|
17
|
+
"minNodeMajor": 16,
|
|
18
|
+
"codexMinNodeMajor": 18,
|
|
19
|
+
"npmMirrorRegistry": "https://registry.npmmirror.com",
|
|
20
|
+
"packages": {
|
|
21
|
+
"codex": "@openai/codex",
|
|
22
|
+
"claude": "@anthropic-ai/claude-code"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"claude": {
|
|
26
|
+
"configPath": ".claude/config.json",
|
|
27
|
+
"primaryApiKey": "custom-provider",
|
|
28
|
+
"baseUrlEnvKey": "ANTHROPIC_BASE_URL",
|
|
29
|
+
"authTokenEnvKey": "ANTHROPIC_AUTH_TOKEN"
|
|
30
|
+
},
|
|
31
|
+
"codex": {
|
|
32
|
+
"configDir": ".codex",
|
|
33
|
+
"providerName": "custom-provider",
|
|
34
|
+
"defaultModel": "gpt-5.4",
|
|
35
|
+
"reasoningEffort": "xhigh",
|
|
36
|
+
"wireApi": "responses",
|
|
37
|
+
"requiresOpenAIAuth": true,
|
|
38
|
+
"authJson": {
|
|
39
|
+
"OPENAI_API_KEY": null
|
|
40
|
+
},
|
|
41
|
+
"modelMigrations": {
|
|
42
|
+
"gpt-5.4": "gpt-5.4"
|
|
43
|
+
},
|
|
44
|
+
"windowsVscodePatch": {
|
|
45
|
+
"enabled": true,
|
|
46
|
+
"extensionPrefix": "openai.chatgpt-",
|
|
47
|
+
"userDirs": [
|
|
48
|
+
".vscode",
|
|
49
|
+
".vscode-insiders"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|