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.
@@ -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",
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
- * 📖 The migration reads the old file as a plain nvidia API key and writes
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
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 })
229
- } catch {
230
- // 📖 Silently fail — the app is still usable, keys just won't persist
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