free-coding-models 0.2.3 → 0.2.5
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/bin/free-coding-models.js +3 -0
- package/package.json +1 -1
- package/src/config.js +228 -10
|
@@ -573,6 +573,9 @@ async function main() {
|
|
|
573
573
|
return state.results
|
|
574
574
|
}
|
|
575
575
|
|
|
576
|
+
// 📖 Apply initial filters so configured-only mode works on first render
|
|
577
|
+
applyTierFilter()
|
|
578
|
+
|
|
576
579
|
// ─── Overlay renderers + key handler ─────────────────────────────────────
|
|
577
580
|
const stopUi = ({ resetRawMode = false } = {}) => {
|
|
578
581
|
if (ticker) clearInterval(ticker)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Find the fastest coding LLM models in seconds — ping free models from multiple providers, pick the best one for OpenCode, Cursor, or any AI coding assistant.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nvidia",
|
package/src/config.js
CHANGED
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
* → getProxySettings(config) — Return normalized proxy settings from config
|
|
103
103
|
* → normalizeEndpointInstalls(endpointInstalls) — Keep tracked endpoint installs stable across app versions
|
|
104
104
|
*
|
|
105
|
-
* @exports loadConfig, saveConfig, getApiKey, isProviderEnabled
|
|
105
|
+
* @exports loadConfig, saveConfig, validateConfigFile, getApiKey, isProviderEnabled
|
|
106
106
|
* @exports addApiKey, removeApiKey, listApiKeys — multi-key management helpers
|
|
107
107
|
* @exports saveAsProfile, loadProfile, listProfiles, deleteProfile
|
|
108
108
|
* @exports getActiveProfileName, setActiveProfile, getProxySettings, normalizeEndpointInstalls
|
|
@@ -112,9 +112,9 @@
|
|
|
112
112
|
* @see sources.js — provider keys come from Object.keys(sources)
|
|
113
113
|
*/
|
|
114
114
|
|
|
115
|
-
import { readFileSync, writeFileSync, existsSync } from 'fs'
|
|
116
|
-
import { homedir } from 'os'
|
|
117
|
-
import { join } from 'path'
|
|
115
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'node:fs'
|
|
116
|
+
import { homedir } from 'node:os'
|
|
117
|
+
import { join } from 'node:path'
|
|
118
118
|
|
|
119
119
|
// 📖 New JSON config path — stores all providers' API keys + enabled state
|
|
120
120
|
export const CONFIG_PATH = join(homedir(), '.free-coding-models.json')
|
|
@@ -155,14 +155,26 @@ const ENV_VARS = {
|
|
|
155
155
|
* 2. If missing, check if ~/.free-coding-models (old plain-text) exists → migrate
|
|
156
156
|
* 3. If neither, return an empty default config
|
|
157
157
|
*
|
|
158
|
-
* 📖
|
|
159
|
-
* a proper JSON config. The old file is NOT deleted (safety first).
|
|
158
|
+
* 📖 Now includes automatic validation and repair from backups if config is corrupted.
|
|
160
159
|
*
|
|
161
160
|
* @returns {{ apiKeys: Record<string,string>, providers: Record<string,{enabled:boolean}>, favorites: string[], telemetry: { enabled: boolean | null, consentVersion: number, anonymousId: string | null } }}
|
|
162
161
|
*/
|
|
163
162
|
export function loadConfig() {
|
|
164
163
|
// 📖 Try new JSON config first
|
|
165
164
|
if (existsSync(CONFIG_PATH)) {
|
|
165
|
+
// 📖 Validate the config file first, try auto-repair if corrupted
|
|
166
|
+
const validation = validateConfigFile({ autoRepair: true })
|
|
167
|
+
|
|
168
|
+
if (!validation.valid && !validation.repaired) {
|
|
169
|
+
// 📖 Config is corrupted and repair failed - warn user but continue with empty config
|
|
170
|
+
console.error(`⚠️ Warning: Config file is corrupted and could not be repaired: ${validation.error}`)
|
|
171
|
+
console.error('⚠️ Starting with fresh config. Your backups are in ~/.free-coding-models.backups/')
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (validation.repaired) {
|
|
175
|
+
console.log('✅ Config file was corrupted but has been restored from backup.')
|
|
176
|
+
}
|
|
177
|
+
|
|
166
178
|
try {
|
|
167
179
|
const raw = readFileSync(CONFIG_PATH, 'utf8').trim()
|
|
168
180
|
const parsed = JSON.parse(raw)
|
|
@@ -204,7 +216,10 @@ export function loadConfig() {
|
|
|
204
216
|
const config = _emptyConfig()
|
|
205
217
|
config.apiKeys.nvidia = oldKey
|
|
206
218
|
// 📖 Auto-save migrated config so next launch is fast
|
|
207
|
-
saveConfig(config)
|
|
219
|
+
const result = saveConfig(config)
|
|
220
|
+
if (!result.success) {
|
|
221
|
+
console.error(`⚠️ Warning: Failed to save migrated config: ${result.error}`)
|
|
222
|
+
}
|
|
208
223
|
return config
|
|
209
224
|
}
|
|
210
225
|
} catch {
|
|
@@ -220,14 +235,217 @@ export function loadConfig() {
|
|
|
220
235
|
*
|
|
221
236
|
* 📖 Uses mode 0o600 so the file is only readable by the owning user (API keys!).
|
|
222
237
|
* 📖 Pretty-prints JSON for human readability.
|
|
238
|
+
* 📖 Now includes:
|
|
239
|
+
* - Automatic backup before overwriting (keeps last 5 versions)
|
|
240
|
+
* - Verification that write succeeded
|
|
241
|
+
* - Explicit error handling (no silent failures)
|
|
242
|
+
* - Post-write validation to ensure file is valid JSON
|
|
223
243
|
*
|
|
224
244
|
* @param {{ apiKeys: Record<string,string>, providers: Record<string,{enabled:boolean}>, favorites?: string[], telemetry?: { enabled?: boolean | null, consentVersion?: number, anonymousId?: string | null } }} config
|
|
245
|
+
* @returns {{ success: boolean, error?: string, backupCreated?: boolean }}
|
|
225
246
|
*/
|
|
226
247
|
export function saveConfig(config) {
|
|
248
|
+
// 📖 Create backup of existing config before overwriting
|
|
249
|
+
const backupCreated = createBackup()
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
// 📖 Write the new config
|
|
253
|
+
const json = JSON.stringify(config, null, 2)
|
|
254
|
+
writeFileSync(CONFIG_PATH, json, { mode: 0o600 })
|
|
255
|
+
|
|
256
|
+
// 📖 Verify the write succeeded by reading back and validating
|
|
257
|
+
try {
|
|
258
|
+
const written = readFileSync(CONFIG_PATH, 'utf8')
|
|
259
|
+
const parsed = JSON.parse(written)
|
|
260
|
+
|
|
261
|
+
// 📖 Basic sanity check - ensure apiKeys object exists
|
|
262
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
263
|
+
throw new Error('Written config is not a valid object')
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 📖 Verify critical data wasn't lost
|
|
267
|
+
if (config.apiKeys && Object.keys(config.apiKeys).length > 0) {
|
|
268
|
+
if (!parsed.apiKeys || Object.keys(parsed.apiKeys).length === 0) {
|
|
269
|
+
throw new Error('API keys were lost during write')
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return { success: true, backupCreated }
|
|
274
|
+
} catch (verifyError) {
|
|
275
|
+
// 📖 Verification failed - this is critical!
|
|
276
|
+
const errorMsg = `Config verification failed: ${verifyError.message}`
|
|
277
|
+
|
|
278
|
+
// 📖 Try to restore from backup if we have one
|
|
279
|
+
if (backupCreated) {
|
|
280
|
+
try {
|
|
281
|
+
restoreFromBackup()
|
|
282
|
+
errorMsg += ' (Restored from backup)'
|
|
283
|
+
} catch (restoreError) {
|
|
284
|
+
errorMsg += ` (Backup restoration failed: ${restoreError.message})`
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return { success: false, error: errorMsg, backupCreated }
|
|
289
|
+
}
|
|
290
|
+
} catch (writeError) {
|
|
291
|
+
// 📖 Write failed - explicit error instead of silent failure
|
|
292
|
+
const errorMsg = `Failed to write config: ${writeError.message}`
|
|
293
|
+
|
|
294
|
+
// 📖 Try to restore from backup if we have one
|
|
295
|
+
if (backupCreated) {
|
|
296
|
+
try {
|
|
297
|
+
restoreFromBackup()
|
|
298
|
+
errorMsg += ' (Restored from backup)'
|
|
299
|
+
} catch (restoreError) {
|
|
300
|
+
errorMsg += ` (Backup restoration failed: ${restoreError.message})`
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return { success: false, error: errorMsg, backupCreated }
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* 📖 createBackup: Creates a timestamped backup of the current config file.
|
|
310
|
+
* 📖 Keeps only the 5 most recent backups to avoid disk space issues.
|
|
311
|
+
* 📖 Backup files are stored in ~/.free-coding-models.backups/
|
|
312
|
+
*
|
|
313
|
+
* @returns {boolean} true if backup was created, false otherwise
|
|
314
|
+
*/
|
|
315
|
+
function createBackup() {
|
|
227
316
|
try {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
317
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
318
|
+
return false // No file to backup
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// 📖 Create backup directory if it doesn't exist
|
|
322
|
+
const backupDir = join(homedir(), '.free-coding-models.backups')
|
|
323
|
+
if (!existsSync(backupDir)) {
|
|
324
|
+
mkdirSync(backupDir, { mode: 0o700, recursive: true })
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// 📖 Create timestamped backup
|
|
328
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '').slice(0, -5) + 'Z'
|
|
329
|
+
const backupPath = join(backupDir, `config.${timestamp}.json`)
|
|
330
|
+
const backupContent = readFileSync(CONFIG_PATH, 'utf8')
|
|
331
|
+
writeFileSync(backupPath, backupContent, { mode: 0o600 })
|
|
332
|
+
|
|
333
|
+
// 📖 Clean up old backups (keep only 5 most recent)
|
|
334
|
+
const backups = readdirSync(backupDir)
|
|
335
|
+
.filter(f => f.startsWith('config.') && f.endsWith('.json'))
|
|
336
|
+
.map(f => ({
|
|
337
|
+
name: f,
|
|
338
|
+
path: join(backupDir, f),
|
|
339
|
+
time: statSync(join(backupDir, f)).mtime.getTime()
|
|
340
|
+
}))
|
|
341
|
+
.sort((a, b) => b.time - a.time)
|
|
342
|
+
|
|
343
|
+
// 📖 Delete old backups beyond the 5 most recent
|
|
344
|
+
if (backups.length > 5) {
|
|
345
|
+
for (const oldBackup of backups.slice(5)) {
|
|
346
|
+
try {
|
|
347
|
+
unlinkSync(oldBackup.path)
|
|
348
|
+
} catch {
|
|
349
|
+
// Ignore cleanup errors
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return true
|
|
355
|
+
} catch (error) {
|
|
356
|
+
// 📖 Log but don't fail if backup creation fails
|
|
357
|
+
console.error(`Warning: Backup creation failed: ${error.message}`)
|
|
358
|
+
return false
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* 📖 restoreFromBackup: Restores the most recent backup.
|
|
364
|
+
* 📖 Used when config write or verification fails.
|
|
365
|
+
*
|
|
366
|
+
* @throws {Error} if no backup exists or restoration fails
|
|
367
|
+
*/
|
|
368
|
+
function restoreFromBackup() {
|
|
369
|
+
const backupDir = join(homedir(), '.free-coding-models.backups')
|
|
370
|
+
|
|
371
|
+
if (!existsSync(backupDir)) {
|
|
372
|
+
throw new Error('No backup directory found')
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// 📖 Find the most recent backup
|
|
376
|
+
const backups = readdirSync(backupDir)
|
|
377
|
+
.filter(f => f.startsWith('config.') && f.endsWith('.json'))
|
|
378
|
+
.map(f => ({
|
|
379
|
+
name: f,
|
|
380
|
+
path: join(backupDir, f),
|
|
381
|
+
time: statSync(join(backupDir, f)).mtime.getTime()
|
|
382
|
+
}))
|
|
383
|
+
.sort((a, b) => b.time - a.time)
|
|
384
|
+
|
|
385
|
+
if (backups.length === 0) {
|
|
386
|
+
throw new Error('No backups available')
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const latestBackup = backups[0]
|
|
390
|
+
const backupContent = readFileSync(latestBackup.path, 'utf8')
|
|
391
|
+
|
|
392
|
+
// 📖 Verify backup is valid JSON before restoring
|
|
393
|
+
JSON.parse(backupContent)
|
|
394
|
+
|
|
395
|
+
// 📖 Restore the backup
|
|
396
|
+
writeFileSync(CONFIG_PATH, backupContent, { mode: 0o600 })
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* 📖 validateConfigFile: Checks if the config file is valid JSON.
|
|
401
|
+
* 📖 Returns validation result and can auto-repair from backups if needed.
|
|
402
|
+
*
|
|
403
|
+
* @param {{ autoRepair?: boolean }} options - If true, attempts to repair using backups
|
|
404
|
+
* @returns {{ valid: boolean, error?: string, repaired?: boolean }}
|
|
405
|
+
*/
|
|
406
|
+
export function validateConfigFile(options = {}) {
|
|
407
|
+
const { autoRepair = false } = options
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
411
|
+
return { valid: true } // No config file is valid (will be created)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const content = readFileSync(CONFIG_PATH, 'utf8')
|
|
415
|
+
|
|
416
|
+
// 📖 Check if file is empty
|
|
417
|
+
if (!content.trim()) {
|
|
418
|
+
throw new Error('Config file is empty')
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 📖 Try to parse JSON
|
|
422
|
+
const parsed = JSON.parse(content)
|
|
423
|
+
|
|
424
|
+
// 📖 Basic structure validation
|
|
425
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
426
|
+
throw new Error('Config is not a valid object')
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// 📖 Check for critical corruption (apiKeys should be an object if it exists)
|
|
430
|
+
if (parsed.apiKeys !== null && parsed.apiKeys !== undefined && typeof parsed.apiKeys !== 'object') {
|
|
431
|
+
throw new Error('apiKeys field is corrupted')
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return { valid: true }
|
|
435
|
+
} catch (error) {
|
|
436
|
+
const errorMsg = `Config validation failed: ${error.message}`
|
|
437
|
+
|
|
438
|
+
// 📖 Attempt auto-repair from backup if requested
|
|
439
|
+
if (autoRepair) {
|
|
440
|
+
try {
|
|
441
|
+
restoreFromBackup()
|
|
442
|
+
return { valid: false, error: errorMsg, repaired: true }
|
|
443
|
+
} catch (repairError) {
|
|
444
|
+
return { valid: false, error: `${errorMsg} (Repair failed: ${repairError.message})`, repaired: false }
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return { valid: false, error: errorMsg, repaired: false }
|
|
231
449
|
}
|
|
232
450
|
}
|
|
233
451
|
|