free-coding-models 0.5.28 → 0.5.29

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.
@@ -39,6 +39,7 @@ import { join, dirname } from 'node:path'
39
39
  import { fileURLToPath } from 'node:url'
40
40
  import { spawn } from 'node:child_process'
41
41
  import { cleanupLegacyProxyArtifacts } from '../core/legacy-proxy-cleanup.js'
42
+ import { sleep } from '../core/shared-helpers.js'
42
43
  import { getLastLayout, COLUMN_SORT_MAP } from './render-table.js'
43
44
  import { cycleThemeSetting, detectActiveTheme } from './theme.js'
44
45
  import { syncShellEnv, ensureShellRcSource, removeShellEnv } from '../core/shell-env.js'
@@ -53,6 +54,7 @@ import {
53
54
  cycleRouterDashboardProbeMode,
54
55
  openRouterDashboardOverlay,
55
56
  restartRouterDashboardDaemon,
57
+ setDashboardNotice,
56
58
  toggleRouterDashboardProbePause,
57
59
  } from '../core/router-dashboard.js'
58
60
  import {
@@ -61,6 +63,7 @@ import {
61
63
  handlePlaygroundKeypress,
62
64
  } from '../core/playground.js'
63
65
  import { benchmarkModel } from '../core/benchmark.js'
66
+ import { isPackageDevMode } from '../core/updater.js'
64
67
 
65
68
  // 📖 Some providers need an explicit probe model because the first catalog entry
66
69
  // 📖 is not guaranteed to be accepted by their chat endpoint.
@@ -76,6 +79,52 @@ const PROVIDER_TEST_MODEL_OVERRIDES = {
76
79
  const SETTINGS_TEST_MAX_ATTEMPTS = 10
77
80
  const SETTINGS_TEST_RETRY_DELAY_MS = 4000
78
81
 
82
+ // 📖 spawnDaemonCommand — spawns `--daemon-bg` or `--daemon-stop` and captures
83
+ // 📖 the JSON result from stdout so we can surface startup errors to the user.
84
+ // 📖 Falls back to a generic notice when stdout isn't parseable.
85
+ function spawnDaemonCommand(state, args) {
86
+ const binPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'free-coding-models.js')
87
+ // 📖 Inherit FCM_DEV so the spawned daemon matches the TUI's dev/prod mode.
88
+ // 📖 Without this, a dev-mode TUI (git checkout) spawns a production daemon
89
+ // 📖 that writes to the wrong PID/port files and can't be discovered later.
90
+ // 📖 isPackageDevMode() detects git checkouts even without --dev or FCM_DEV=1.
91
+ const env = { ...process.env }
92
+ if (isPackageDevMode() && !env.FCM_DEV) env.FCM_DEV = '1'
93
+ const child = spawn('node', [binPath, ...args], {
94
+ detached: true,
95
+ stdio: ['ignore', 'pipe', 'pipe'],
96
+ env,
97
+ })
98
+ child.unref()
99
+
100
+ // 📖 Only capture stdout for start commands — stop doesn't need error surfacing.
101
+ if (!args.includes('--daemon-bg')) return
102
+
103
+ let stdout = ''
104
+ let stderr = ''
105
+ child.stdout?.on('data', (chunk) => { stdout += chunk })
106
+ child.stderr?.on('data', (chunk) => { stderr += chunk })
107
+ child.on('close', (code) => {
108
+ if (code === 0) return
109
+ // 📖 Try to parse the JSON result from --daemon-bg for a precise error message.
110
+ let errorMsg = null
111
+ try {
112
+ const parsed = JSON.parse(stdout.trim())
113
+ if (parsed?.error) errorMsg = parsed.error
114
+ } catch {}
115
+ if (!errorMsg && stderr.trim()) {
116
+ // 📖 Common startup crash reasons: port in use, config corruption, etc.
117
+ const lines = stderr.trim().split('\n').filter(l => !l.startsWith('node:'))
118
+ errorMsg = lines.slice(0, 2).join(' — ') || stderr.trim().slice(0, 120)
119
+ }
120
+ if (errorMsg) {
121
+ setDashboardNotice(state, 'error', `Daemon failed to start: ${errorMsg}`, 8000)
122
+ } else if (code !== 0) {
123
+ setDashboardNotice(state, 'error', `Daemon failed to start (exit code ${code})`, 8000)
124
+ }
125
+ })
126
+ }
127
+
79
128
  // 📖 PROVIDER_AUTH_ENDPOINTS maps provider keys to their auth-check URL + method.
80
129
  // 📖 For most providers this is the /models endpoint (returns 200=valid, 401=invalid).
81
130
  // 📖 Providers without an auth-check endpoint use null (falls back to chat completion ping).
@@ -114,11 +163,7 @@ const PROVIDER_AUTH_ENDPOINTS = {
114
163
  'ollama-cloud': { url: 'https://ollama.com/v1/models', method: 'GET' },
115
164
  }
116
165
 
117
- // 📖 Sleep helper kept local to this module so the Settings key test flow can
118
- // 📖 back off between retries without leaking timer logic into the rest of the TUI.
119
- function sleep(ms) {
120
- return new Promise((resolve) => setTimeout(resolve, ms))
121
- }
166
+ // 📖 Sleep imported from shared-helpers.js
122
167
 
123
168
  // 📖 testProviderKeyDirect: Fast auth-only check using /v1/account or /v1/models.
124
169
  // 📖 Fires 3 parallel probes to get a fast decisive result (auth error vs timeout vs 200).
@@ -1613,6 +1658,7 @@ export function createKeyHandler(ctx) {
1613
1658
  case 'action-toggle-favorite-mode': return toggleFavoritesDisplayMode()
1614
1659
  case 'action-reset-view': return resetViewSettings()
1615
1660
  case 'action-probe-404': return runBrokenModelProbe(state)
1661
+ case 'action-toggle-auto-hide-broken': return toggleAutoHideBrokenModels()
1616
1662
  default:
1617
1663
  return
1618
1664
  }
@@ -1805,15 +1851,9 @@ export function createKeyHandler(ctx) {
1805
1851
  // 📖 S: Toggle daemon start/stop
1806
1852
  if (key.name === 's') {
1807
1853
  const isRunning = state.routerDashboardStatus === 'ready' || state.routerDashboardStatus === 'partial'
1808
- const binPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'free-coding-models.js')
1809
1854
  const args = isRunning ? ['--daemon-stop'] : ['--daemon-bg']
1810
-
1811
1855
  state.routerDashboardStatus = 'loading'
1812
- const child = spawn('node', [binPath, ...args], {
1813
- detached: true,
1814
- stdio: 'ignore',
1815
- })
1816
- child.unref()
1856
+ spawnDaemonCommand(state, args)
1817
1857
  return
1818
1858
  }
1819
1859
 
@@ -1824,15 +1864,9 @@ export function createKeyHandler(ctx) {
1824
1864
 
1825
1865
  if ((state.routerDashboardCursorIndex ?? 0) === btnCursor) {
1826
1866
  const isRunning = state.routerDashboardStatus === 'ready' || state.routerDashboardStatus === 'partial'
1827
- const binPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'free-coding-models.js')
1828
1867
  const args = isRunning ? ['--daemon-stop'] : ['--daemon-bg']
1829
-
1830
1868
  state.routerDashboardStatus = 'loading'
1831
- const child = spawn('node', [binPath, ...args], {
1832
- detached: true,
1833
- stdio: 'ignore',
1834
- })
1835
- child.unref()
1869
+ spawnDaemonCommand(state, args)
1836
1870
  } else if ((state.routerDashboardCursorIndex ?? 0) === installBtnCursor) {
1837
1871
  state.routerDashboardOpen = false
1838
1872
  state.installEndpointsOpen = true
@@ -2413,9 +2447,32 @@ export function createKeyHandler(ctx) {
2413
2447
  const binPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'free-coding-models.js')
2414
2448
  const child = spawn('node', [binPath, '--daemon-bg'], {
2415
2449
  detached: true,
2416
- stdio: 'ignore',
2450
+ stdio: ['ignore', 'pipe', 'pipe'],
2451
+ env: { ...process.env, ...(isPackageDevMode() && !process.env.FCM_DEV ? { FCM_DEV: '1' } : {}) },
2417
2452
  })
2418
2453
  child.unref()
2454
+
2455
+ // 📖 Capture daemon startup output so we can show the real error reason
2456
+ let onboardingStdout = ''
2457
+ let onboardingStderr = ''
2458
+ child.stdout?.on('data', (chunk) => { onboardingStdout += chunk })
2459
+ child.stderr?.on('data', (chunk) => { onboardingStderr += chunk })
2460
+ child.on('close', (code) => {
2461
+ if (code === 0) return
2462
+ let detail = null
2463
+ try {
2464
+ const parsed = JSON.parse(onboardingStdout.trim())
2465
+ if (parsed?.error) detail = parsed.error
2466
+ } catch {}
2467
+ if (!detail && onboardingStderr.trim()) {
2468
+ const lines = onboardingStderr.trim().split('\n').filter(l => !l.startsWith('node:'))
2469
+ detail = lines.slice(0, 2).join(' — ') || onboardingStderr.trim().slice(0, 120)
2470
+ }
2471
+ if (state.routerOnboardingPhase === 'loading') {
2472
+ state.routerOnboardingPhase = 'error'
2473
+ state.routerOnboardingError = detail || `Daemon exited with code ${code}`
2474
+ }
2475
+ })
2419
2476
  await new Promise((r) => setTimeout(r, 2000))
2420
2477
  if (state.routerOnboardingPhase === 'loading') {
2421
2478
  state.routerOnboardingPhase = 'success'
@@ -49,7 +49,7 @@ import {
49
49
  } from '../core/constants.js'
50
50
  import { themeColors, currentPalette, getProviderRgb, getTierRgb, getReadableTextRgb, getTheme, THEME_BG_RGB } from './theme.js'
51
51
  import { TIER_COLOR } from './tier-colors.js'
52
- import { getAvg, getVerdict, getUptime, getStabilityScore, getVersionStatusInfo, NEW_MODELS } from '../core/utils.js'
52
+ import { getAvg, getVerdict, getUptime, getStabilityScore, getVersionStatusInfo, isNewModel } from '../core/utils.js'
53
53
  import { usagePlaceholderForProvider } from '../core/ping.js'
54
54
  import { formatBenchmarkLatency, formatBenchmarkTps } from '../core/benchmark.js'
55
55
  import { calculateViewport, sortResultsWithPinnedFavorites, padEndDisplay, displayWidth, stripAnsi, fadedRow } from './render-helpers.js'
@@ -656,13 +656,13 @@ export function renderTable({
656
656
  : providerName
657
657
  const source = themeColors.provider(r.providerKey, providerDisplay.padEnd(wSource))
658
658
  // 📖 Prefix: ⭐ favorite > 🎯 recommended > 🆕 new — only one emoji, never shifts the line
659
- const isNewModel = NEW_MODELS.has(r.modelId) || NEW_MODELS.has(`${r.providerKey}/${r.modelId}`)
659
+ const modelIsNew = isNewModel(r.addedDate)
660
660
  let favoritePrefix = ''
661
661
  if (r.isRecommended) {
662
662
  favoritePrefix = '🎯 '
663
663
  } else if (r.isFavorite) {
664
664
  favoritePrefix = '⭐ '
665
- } else if (isNewModel) {
665
+ } else if (modelIsNew) {
666
666
  favoritePrefix = '🆕 '
667
667
  }
668
668
  const prefixDisplayWidth = displayWidth(favoritePrefix)