free-coding-models 0.1.86 → 0.1.89

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/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
- try {
93
- // 📖 Force install from npm registry (ignore local cache)
94
- // 📖 Use --prefer-online to ensure we get the latest published version
95
- execSync(`npm i -g free-coding-models@${latestVersion} --prefer-online`, { stdio: 'inherit' })
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
- console.log(chalk.green(' ✅ Update complete! Version ' + latestVersion + ' installed.'))
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.dim(' 🔄 Restarting with new version...'))
207
+ console.log(chalk.green(` Update complete! Version ${latestVersion} installed.`))
100
208
  console.log()
101
-
102
- // 📖 Relaunch automatically with the same arguments
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
- // 📖 Check if error is permission-related (EACCES or EPERM)
109
- const isPermissionError = err.code === 'EACCES' || err.code === 'EPERM' ||
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
- execSync(`sudo npm i -g free-coding-models@${latestVersion} --prefer-online`, { stdio: 'inherit' })
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.dim(' 🔄 Restarting with new version...'))
219
+ console.log(chalk.green(` Update complete with sudo! Version ${latestVersion} installed.`))
123
220
  console.log()
124
-
125
- // 📖 Relaunch automatically with the same arguments
126
- const args = process.argv.slice(2)
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, --no-telemetry (case-insensitive)
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, noTelemetry, tierFilter }
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 { apiKey, bestMode, fiableMode, openCodeMode, openCodeDesktopMode, openClawMode, noTelemetry, tierFilter, profileName, recommendMode }
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 ─────────────────────────────────────────