free-coding-models 0.1.87 → 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 +58 -45
- package/bin/free-coding-models.js +65 -14
- package/package.json +1 -1
- package/src/config.js +42 -4
- package/src/key-handler.js +105 -13
- package/src/opencode-sync.js +41 -0
- package/src/opencode.js +23 -2
- package/src/overlays.js +74 -13
- package/src/render-table.js +72 -16
- package/src/token-usage-reader.js +5 -5
- package/src/tool-launchers.js +319 -0
- package/src/tool-metadata.js +63 -0
- package/src/updater.js +128 -30
- package/src/utils.js +40 -3
package/src/updater.js
CHANGED
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
|
|
45
45
|
import chalk from 'chalk'
|
|
46
46
|
import { createRequire } from 'module'
|
|
47
|
+
import { accessSync, constants } from 'fs'
|
|
47
48
|
|
|
48
49
|
const require = createRequire(import.meta.url)
|
|
49
50
|
const readline = require('readline')
|
|
@@ -77,6 +78,106 @@ export async function checkForUpdate() {
|
|
|
77
78
|
return latestVersion
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
/**
|
|
82
|
+
* 📖 detectGlobalInstallPermission: check whether npm global install paths are writable.
|
|
83
|
+
* 📖 On sudo-based systems (Arch, many Linux/macOS setups), `npm i -g` will fail with EACCES
|
|
84
|
+
* 📖 if the current user cannot write to the resolved global root/prefix.
|
|
85
|
+
* 📖 We probe those paths ahead of time so the updater can go straight to an interactive
|
|
86
|
+
* 📖 `sudo npm i -g ...` instead of printing a wall of permission errors first.
|
|
87
|
+
* @returns {{ needsSudo: boolean, checkedPath: string|null }}
|
|
88
|
+
*/
|
|
89
|
+
function detectGlobalInstallPermission() {
|
|
90
|
+
const { execFileSync } = require('child_process')
|
|
91
|
+
const candidates = []
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const npmRoot = execFileSync('npm', ['root', '-g'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
|
|
95
|
+
if (npmRoot) candidates.push(npmRoot)
|
|
96
|
+
} catch {}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const npmPrefix = execFileSync('npm', ['prefix', '-g'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
|
|
100
|
+
if (npmPrefix) candidates.push(npmPrefix)
|
|
101
|
+
} catch {}
|
|
102
|
+
|
|
103
|
+
for (const candidate of candidates) {
|
|
104
|
+
try {
|
|
105
|
+
accessSync(candidate, constants.W_OK)
|
|
106
|
+
} catch {
|
|
107
|
+
return { needsSudo: true, checkedPath: candidate }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { needsSudo: false, checkedPath: candidates[0] || null }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 📖 hasSudoCommand: lightweight guard so we don't suggest sudo on systems where it does not exist.
|
|
116
|
+
* @returns {boolean}
|
|
117
|
+
*/
|
|
118
|
+
function hasSudoCommand() {
|
|
119
|
+
const { spawnSync } = require('child_process')
|
|
120
|
+
const result = spawnSync('sudo', ['-n', 'true'], { stdio: 'ignore', shell: false })
|
|
121
|
+
return result.status === 0 || result.status === 1
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 📖 isPermissionError: normalize npm permission failures across platforms and child-process APIs.
|
|
126
|
+
* @param {unknown} err
|
|
127
|
+
* @returns {boolean}
|
|
128
|
+
*/
|
|
129
|
+
function isPermissionError(err) {
|
|
130
|
+
const message = err instanceof Error ? err.message : String(err || '')
|
|
131
|
+
const stderr = typeof err?.stderr === 'string' ? err.stderr : ''
|
|
132
|
+
const combined = `${message}\n${stderr}`.toLowerCase()
|
|
133
|
+
return (
|
|
134
|
+
err?.code === 'EACCES' ||
|
|
135
|
+
err?.code === 'EPERM' ||
|
|
136
|
+
combined.includes('eacces') ||
|
|
137
|
+
combined.includes('eperm') ||
|
|
138
|
+
combined.includes('permission denied') ||
|
|
139
|
+
combined.includes('operation not permitted')
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 📖 relaunchCurrentProcess: restart free-coding-models with the same user arguments.
|
|
145
|
+
* 📖 Uses spawn with inherited stdio so the new process is interactive and does not require shell escaping.
|
|
146
|
+
*/
|
|
147
|
+
function relaunchCurrentProcess() {
|
|
148
|
+
const { spawn } = require('child_process')
|
|
149
|
+
console.log(chalk.dim(' 🔄 Restarting with new version...'))
|
|
150
|
+
console.log()
|
|
151
|
+
|
|
152
|
+
const args = process.argv.slice(1)
|
|
153
|
+
const child = spawn(process.execPath, args, {
|
|
154
|
+
stdio: 'inherit',
|
|
155
|
+
detached: false,
|
|
156
|
+
shell: false,
|
|
157
|
+
env: process.env,
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
child.on('exit', (code) => process.exit(code ?? 0))
|
|
161
|
+
child.on('error', () => process.exit(0))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 📖 installUpdateCommand: run npm global install, optionally prefixed with sudo.
|
|
166
|
+
* @param {string} latestVersion
|
|
167
|
+
* @param {boolean} useSudo
|
|
168
|
+
*/
|
|
169
|
+
function installUpdateCommand(latestVersion, useSudo) {
|
|
170
|
+
const { execFileSync } = require('child_process')
|
|
171
|
+
const npmArgs = ['i', '-g', `free-coding-models@${latestVersion}`, '--prefer-online']
|
|
172
|
+
|
|
173
|
+
if (useSudo) {
|
|
174
|
+
execFileSync('sudo', ['npm', ...npmArgs], { stdio: 'inherit', shell: false })
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
execFileSync('npm', npmArgs, { stdio: 'inherit', shell: false })
|
|
179
|
+
}
|
|
180
|
+
|
|
80
181
|
/**
|
|
81
182
|
* 📖 runUpdate: Run npm global install to update to latestVersion.
|
|
82
183
|
* 📖 Retries with sudo on permission errors.
|
|
@@ -84,54 +185,51 @@ export async function checkForUpdate() {
|
|
|
84
185
|
* @param {string} latestVersion
|
|
85
186
|
*/
|
|
86
187
|
export function runUpdate(latestVersion) {
|
|
87
|
-
const { execSync } = require('child_process')
|
|
88
188
|
console.log()
|
|
89
189
|
console.log(chalk.bold.cyan(' ⬆ Updating free-coding-models to v' + latestVersion + '...'))
|
|
90
190
|
console.log()
|
|
91
191
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
192
|
+
const { needsSudo, checkedPath } = detectGlobalInstallPermission()
|
|
193
|
+
const sudoAvailable = process.platform !== 'win32' && hasSudoCommand()
|
|
194
|
+
|
|
195
|
+
if (needsSudo && checkedPath && sudoAvailable) {
|
|
196
|
+
console.log(chalk.yellow(` ⚠ Global npm path is not writable: ${checkedPath}`))
|
|
197
|
+
console.log(chalk.dim(' Re-running update with sudo so you can enter your password once.'))
|
|
96
198
|
console.log()
|
|
97
|
-
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
// 📖 Force install from npm registry (ignore local cache).
|
|
203
|
+
// 📖 If the global install path is not writable, go straight to sudo instead of
|
|
204
|
+
// 📖 letting npm print a long EACCES stack first.
|
|
205
|
+
installUpdateCommand(latestVersion, needsSudo && sudoAvailable)
|
|
98
206
|
console.log()
|
|
99
|
-
console.log(chalk.
|
|
207
|
+
console.log(chalk.green(` ✅ Update complete! Version ${latestVersion} installed.`))
|
|
100
208
|
console.log()
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const args = process.argv.slice(2)
|
|
104
|
-
execSync(`node ${process.argv[1]} ${args.join(' ')}`, { stdio: 'inherit' })
|
|
105
|
-
process.exit(0)
|
|
209
|
+
relaunchCurrentProcess()
|
|
210
|
+
return
|
|
106
211
|
} catch (err) {
|
|
107
212
|
console.log()
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
(err.stderr && (err.stderr.includes('EACCES') || err.stderr.includes('permission') ||
|
|
111
|
-
err.stderr.includes('EACCES'))) ||
|
|
112
|
-
(err.message && (err.message.includes('EACCES') || err.message.includes('permission')))
|
|
113
|
-
|
|
114
|
-
if (isPermissionError) {
|
|
115
|
-
console.log(chalk.yellow(' ⚠️ Permission denied. Retrying with sudo...'))
|
|
213
|
+
if (isPermissionError(err) && !needsSudo && sudoAvailable) {
|
|
214
|
+
console.log(chalk.yellow(' ⚠ Permission denied during npm global install. Retrying with sudo...'))
|
|
116
215
|
console.log()
|
|
117
216
|
try {
|
|
118
|
-
|
|
119
|
-
console.log()
|
|
120
|
-
console.log(chalk.green(' ✅ Update complete with sudo! Version ' + latestVersion + ' installed.'))
|
|
217
|
+
installUpdateCommand(latestVersion, true)
|
|
121
218
|
console.log()
|
|
122
|
-
console.log(chalk.
|
|
219
|
+
console.log(chalk.green(` ✅ Update complete with sudo! Version ${latestVersion} installed.`))
|
|
123
220
|
console.log()
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
execSync(`node ${process.argv[1]} ${args.join(' ')}`, { stdio: 'inherit' })
|
|
128
|
-
process.exit(0)
|
|
129
|
-
} catch (sudoErr) {
|
|
221
|
+
relaunchCurrentProcess()
|
|
222
|
+
return
|
|
223
|
+
} catch {
|
|
130
224
|
console.log()
|
|
131
225
|
console.log(chalk.red(' ✖ Update failed even with sudo. Try manually:'))
|
|
132
226
|
console.log(chalk.dim(' sudo npm i -g free-coding-models@' + latestVersion))
|
|
133
227
|
console.log()
|
|
134
228
|
}
|
|
229
|
+
} else if (isPermissionError(err) && !sudoAvailable && process.platform !== 'win32') {
|
|
230
|
+
console.log(chalk.red(' ✖ Update failed due to permissions and `sudo` is not available in PATH.'))
|
|
231
|
+
console.log(chalk.dim(` Try manually with your system's privilege escalation tool for free-coding-models@${latestVersion}.`))
|
|
232
|
+
console.log()
|
|
135
233
|
} else {
|
|
136
234
|
console.log(chalk.red(' ✖ Update failed. Try manually: npm i -g free-coding-models@' + latestVersion))
|
|
137
235
|
console.log()
|
package/src/utils.js
CHANGED
|
@@ -385,11 +385,15 @@ export function findBestModel(results) {
|
|
|
385
385
|
//
|
|
386
386
|
// 📖 Argument types:
|
|
387
387
|
// - API key: first positional arg that doesn't start with "--" (e.g., "nvapi-xxx")
|
|
388
|
-
// - Boolean flags: --best, --fiable, --opencode, --opencode-desktop, --openclaw,
|
|
388
|
+
// - Boolean flags: --best, --fiable, --opencode, --opencode-desktop, --openclaw,
|
|
389
|
+
// --aider, --crush, --goose, --claude-code, --codex, --gemini, --qwen,
|
|
390
|
+
// --openhands, --amp, --pi, --no-telemetry (case-insensitive)
|
|
389
391
|
// - Value flag: --tier <letter> (the next non-flag arg is the tier value)
|
|
390
392
|
//
|
|
391
393
|
// 📖 Returns:
|
|
392
|
-
// { apiKey, bestMode, fiableMode, openCodeMode, openCodeDesktopMode, openClawMode,
|
|
394
|
+
// { apiKey, bestMode, fiableMode, openCodeMode, openCodeDesktopMode, openClawMode,
|
|
395
|
+
// aiderMode, crushMode, gooseMode, claudeCodeMode, codexMode, geminiMode,
|
|
396
|
+
// qwenMode, openHandsMode, ampMode, piMode, noTelemetry, tierFilter }
|
|
393
397
|
//
|
|
394
398
|
// 📖 Note: apiKey may be null here — the main CLI falls back to env vars and saved config.
|
|
395
399
|
export function parseArgs(argv) {
|
|
@@ -428,7 +432,18 @@ export function parseArgs(argv) {
|
|
|
428
432
|
const openCodeMode = flags.includes('--opencode')
|
|
429
433
|
const openCodeDesktopMode = flags.includes('--opencode-desktop')
|
|
430
434
|
const openClawMode = flags.includes('--openclaw')
|
|
435
|
+
const aiderMode = flags.includes('--aider')
|
|
436
|
+
const crushMode = flags.includes('--crush')
|
|
437
|
+
const gooseMode = flags.includes('--goose')
|
|
438
|
+
const claudeCodeMode = flags.includes('--claude-code')
|
|
439
|
+
const codexMode = flags.includes('--codex')
|
|
440
|
+
const geminiMode = flags.includes('--gemini')
|
|
441
|
+
const qwenMode = flags.includes('--qwen')
|
|
442
|
+
const openHandsMode = flags.includes('--openhands')
|
|
443
|
+
const ampMode = flags.includes('--amp')
|
|
444
|
+
const piMode = flags.includes('--pi')
|
|
431
445
|
const noTelemetry = flags.includes('--no-telemetry')
|
|
446
|
+
const cleanProxyMode = flags.includes('--clean-proxy') || flags.includes('--proxy-clean')
|
|
432
447
|
|
|
433
448
|
let tierFilter = tierValueIdx !== -1 ? args[tierValueIdx].toUpperCase() : null
|
|
434
449
|
|
|
@@ -437,7 +452,29 @@ export function parseArgs(argv) {
|
|
|
437
452
|
// 📖 --recommend — launch directly into Smart Recommend mode (Q key equivalent)
|
|
438
453
|
const recommendMode = flags.includes('--recommend')
|
|
439
454
|
|
|
440
|
-
return {
|
|
455
|
+
return {
|
|
456
|
+
apiKey,
|
|
457
|
+
bestMode,
|
|
458
|
+
fiableMode,
|
|
459
|
+
openCodeMode,
|
|
460
|
+
openCodeDesktopMode,
|
|
461
|
+
openClawMode,
|
|
462
|
+
aiderMode,
|
|
463
|
+
crushMode,
|
|
464
|
+
gooseMode,
|
|
465
|
+
claudeCodeMode,
|
|
466
|
+
codexMode,
|
|
467
|
+
geminiMode,
|
|
468
|
+
qwenMode,
|
|
469
|
+
openHandsMode,
|
|
470
|
+
ampMode,
|
|
471
|
+
piMode,
|
|
472
|
+
noTelemetry,
|
|
473
|
+
cleanProxyMode,
|
|
474
|
+
tierFilter,
|
|
475
|
+
profileName,
|
|
476
|
+
recommendMode
|
|
477
|
+
}
|
|
441
478
|
}
|
|
442
479
|
|
|
443
480
|
// ─── Smart Recommend — Scoring Engine ─────────────────────────────────────────
|