free-coding-models 0.5.4 → 0.5.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 +23 -8
- package/changelog/v0.5.5.md +15 -0
- package/package.json +1 -1
- package/src/core/updater.js +174 -10
- package/src/tui/app.js +11 -31
- package/src/tui/render-table.js +9 -3
- package/src/tui/tui-state.js +6 -0
- package/web/dist/assets/{index-BoWmUveV.js → index-uD3faN3G.js} +2 -2
- package/web/dist/assets/{index-BrpHevg4.css → index-uTifVKX1.css} +1 -1
- package/web/dist/index.html +2 -2
- package/web/server.js +12 -1
- package/web/src/App.jsx +7 -1
- package/web/src/global.css +15 -0
- package/web/src/hooks/useSocket.js +3 -0
|
@@ -11,11 +11,12 @@ if (process.argv.includes('--dev')) {
|
|
|
11
11
|
|
|
12
12
|
import chalk from 'chalk';
|
|
13
13
|
import { parseArgs, TIER_LETTER_MAP } from '../src/core/utils.js';
|
|
14
|
-
import { loadConfig } from '../src/core/config.js';
|
|
14
|
+
import { loadConfig, saveConfig } from '../src/core/config.js';
|
|
15
15
|
import { ensureTelemetryConfig } from '../src/core/telemetry.js';
|
|
16
16
|
import { ensureFavoritesConfig } from '../src/core/favorites.js';
|
|
17
17
|
import { buildCliHelpText } from '../src/tui/cli-help.js';
|
|
18
18
|
import { ALT_LEAVE } from '../src/core/constants.js';
|
|
19
|
+
import { enforceMandatoryStartupUpdate, isPackageDevMode } from '../src/core/updater.js';
|
|
19
20
|
import { runApp } from '../src/tui/app.js';
|
|
20
21
|
|
|
21
22
|
// Global error handlers to ensure terminal is restored if something crashes catastrophically
|
|
@@ -53,13 +54,32 @@ async function main() {
|
|
|
53
54
|
process.exit(0);
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
// Load JSON config before operational modes so the mandatory update policy can
|
|
58
|
+
// 📖 persist failure counters for TUI, Web Dashboard, Docker daemon, and Desktop sidecar launches.
|
|
59
|
+
const config = loadConfig();
|
|
60
|
+
ensureTelemetryConfig(config);
|
|
61
|
+
ensureFavoritesConfig(config);
|
|
62
|
+
|
|
63
|
+
const isDevMode = isPackageDevMode();
|
|
64
|
+
const shouldEnforceUpdate = !cliArgs.daemonStopMode && !cliArgs.daemonStatusMode;
|
|
65
|
+
const startupUpdate = shouldEnforceUpdate
|
|
66
|
+
? await enforceMandatoryStartupUpdate(config, {
|
|
67
|
+
saveConfig,
|
|
68
|
+
isDevMode,
|
|
69
|
+
surface: cliArgs.webMode ? 'web dashboard' : cliArgs.daemonMode ? 'router daemon' : 'TUI',
|
|
70
|
+
})
|
|
71
|
+
: { latestVersion: null, allowedOutdated: false, warningMessage: null, failures: 0, checked: false, updated: false, blocked: false };
|
|
72
|
+
|
|
73
|
+
if (startupUpdate.updated) return;
|
|
74
|
+
if (startupUpdate.blocked) process.exit(1);
|
|
75
|
+
|
|
56
76
|
// 📖 Standalone web dashboard: same full-catalog ping UI as the TUI, served
|
|
57
77
|
// 📖 locally with Socket.IO/SSE/REST realtime updates.
|
|
58
78
|
if (cliArgs.webMode) {
|
|
59
79
|
const { startWebServer } = await import('../web/server.js');
|
|
60
80
|
const parsedPort = Number.parseInt(process.env.FCM_WEB_PORT || process.env.FCM_PORT || '3333', 10);
|
|
61
81
|
const port = Number.isFinite(parsedPort) && parsedPort > 0 ? parsedPort : 3333;
|
|
62
|
-
await startWebServer(port, { open: true, startPingLoop: true });
|
|
82
|
+
await startWebServer(port, { open: true, startPingLoop: true, updateStatus: startupUpdate });
|
|
63
83
|
return;
|
|
64
84
|
}
|
|
65
85
|
|
|
@@ -102,12 +122,7 @@ async function main() {
|
|
|
102
122
|
process.exit(1);
|
|
103
123
|
}
|
|
104
124
|
|
|
105
|
-
|
|
106
|
-
const config = loadConfig();
|
|
107
|
-
ensureTelemetryConfig(config);
|
|
108
|
-
ensureFavoritesConfig(config);
|
|
109
|
-
|
|
110
|
-
await runApp(cliArgs, config);
|
|
125
|
+
await runApp(cliArgs, config, { startupUpdate, isDevMode });
|
|
111
126
|
}
|
|
112
127
|
|
|
113
128
|
main().catch((err) => {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog v0.5.5 - 2026-06-01
|
|
2
|
+
|
|
3
|
+
### Changed
|
|
4
|
+
- ⬆️ **deps-dev**: bump `vite` from `8.0.14` → `8.0.16` (#105)
|
|
5
|
+
- ⬆️ **deps-dev**: bump `vite-plus` from `0.1.20` → `0.1.23` (#106)
|
|
6
|
+
- ⬆️ **deps-dev**: bump `@vitejs/plugin-react` from `6.0.1` → `6.0.2` (#107)
|
|
7
|
+
|
|
8
|
+
### CI
|
|
9
|
+
- ⬆️ bump `docker/setup-buildx-action` from `3` → `4` (#104)
|
|
10
|
+
- ⬆️ bump `docker/login-action` from `3` → `4` (#103)
|
|
11
|
+
- ⬆️ bump `pnpm/action-setup` from `4` → `6` (#102)
|
|
12
|
+
|
|
13
|
+
### Notes
|
|
14
|
+
- All 6 dependabot PRs merged in a single batch with conflict resolution on `pnpm-lock.yaml`.
|
|
15
|
+
- 440/440 tests passing.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.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/core/updater.js
CHANGED
|
@@ -32,22 +32,29 @@
|
|
|
32
32
|
* → getManualInstallCmd(pm, version) — Human-readable install command string for error messages
|
|
33
33
|
* → checkForUpdateDetailed() — Fetch npm latest with explicit error info
|
|
34
34
|
* → checkForUpdate() — Startup wrapper, returns version string or null
|
|
35
|
+
* → isPackageDevMode() — Detect git/dev checkouts that must not self-update
|
|
36
|
+
* → enforceMandatoryStartupUpdate() — Mandatory startup self-update with two-failure fallback
|
|
35
37
|
* → runUpdate(latestVersion) — Install new version via detected PM + relaunch
|
|
36
38
|
* @exports
|
|
37
39
|
* detectPackageManager, getInstallArgs, getManualInstallCmd,
|
|
38
|
-
* checkForUpdateDetailed, checkForUpdate,
|
|
40
|
+
* checkForUpdateDetailed, checkForUpdate, isPackageDevMode,
|
|
41
|
+
* enforceMandatoryStartupUpdate, runUpdate, fetchLastReleaseDate
|
|
39
42
|
*
|
|
40
43
|
* @see bin/free-coding-models.js — calls checkForUpdate() at startup and runUpdate() on confirm
|
|
41
44
|
*/
|
|
42
45
|
|
|
43
46
|
import chalk from 'chalk'
|
|
44
47
|
import { createRequire } from 'module'
|
|
45
|
-
import {
|
|
48
|
+
import { fileURLToPath } from 'url'
|
|
49
|
+
import { dirname, join } from 'path'
|
|
50
|
+
import { accessSync, constants, existsSync } from 'fs'
|
|
46
51
|
|
|
47
52
|
const require = createRequire(import.meta.url)
|
|
48
53
|
const readline = require('readline')
|
|
49
54
|
const pkg = require('../../package.json')
|
|
50
55
|
const LOCAL_VERSION = pkg.version
|
|
56
|
+
const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..')
|
|
57
|
+
export const UPDATE_FAILURE_THRESHOLD = 2
|
|
51
58
|
|
|
52
59
|
/**
|
|
53
60
|
* 📖 detectPackageManager: figure out which package manager owns the current installation.
|
|
@@ -65,6 +72,70 @@ export function detectPackageManager() {
|
|
|
65
72
|
return 'npm'
|
|
66
73
|
}
|
|
67
74
|
|
|
75
|
+
/**
|
|
76
|
+
* 📖 isPackageDevMode: true for repo checkouts and explicit --dev runs.
|
|
77
|
+
* 📖 Self-updating a git checkout creates noisy loops during local development,
|
|
78
|
+
* 📖 while published npm installs do not ship a .git directory and can update safely.
|
|
79
|
+
* @returns {boolean}
|
|
80
|
+
*/
|
|
81
|
+
export function isPackageDevMode() {
|
|
82
|
+
return process.env.FCM_DEV === '1' || existsSync(join(PACKAGE_ROOT, '.git'))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 📖 getUpdateInstallFailureCount: sanitized persistent failure counter.
|
|
87
|
+
* @param {object} config
|
|
88
|
+
* @returns {number}
|
|
89
|
+
*/
|
|
90
|
+
export function getUpdateInstallFailureCount(config) {
|
|
91
|
+
const raw = Number(config?.settings?.updateInstallFailures || 0)
|
|
92
|
+
if (!Number.isFinite(raw) || raw < 0) return 0
|
|
93
|
+
return Math.floor(raw)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function ensureUpdateSettings(config) {
|
|
97
|
+
if (!config.settings || typeof config.settings !== 'object') config.settings = {}
|
|
98
|
+
return config.settings
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function persistUpdateSettings(config, saveConfig) {
|
|
102
|
+
if (typeof saveConfig !== 'function') return
|
|
103
|
+
try { saveConfig(config) } catch {}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resetUpdateInstallFailures(config, saveConfig) {
|
|
107
|
+
const settings = ensureUpdateSettings(config)
|
|
108
|
+
if (!settings.updateInstallFailures && !settings.updateLastFailureAt && !settings.updateLastFailureMessage) return
|
|
109
|
+
settings.updateInstallFailures = 0
|
|
110
|
+
delete settings.updateLastFailureAt
|
|
111
|
+
delete settings.updateLastFailureMessage
|
|
112
|
+
delete settings.updateInstallFailureVersion
|
|
113
|
+
persistUpdateSettings(config, saveConfig)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function recordUpdateInstallFailure(config, latestVersion, error, saveConfig) {
|
|
117
|
+
const settings = ensureUpdateSettings(config)
|
|
118
|
+
const nextFailures = Math.min(getUpdateInstallFailureCount(config) + 1, UPDATE_FAILURE_THRESHOLD)
|
|
119
|
+
settings.updateInstallFailures = nextFailures
|
|
120
|
+
settings.updateInstallFailureVersion = latestVersion
|
|
121
|
+
settings.updateLastFailureAt = new Date().toISOString()
|
|
122
|
+
settings.updateLastFailureMessage = error instanceof Error ? error.message : String(error || 'Unknown update error')
|
|
123
|
+
persistUpdateSettings(config, saveConfig)
|
|
124
|
+
return nextFailures
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 📖 buildOutdatedWarningMessage: one-line message shown when mandatory updates
|
|
129
|
+
* 📖 failed twice and FCM must let the UI start instead of trapping the user.
|
|
130
|
+
* @param {string|null} latestVersion
|
|
131
|
+
* @param {number} failures
|
|
132
|
+
* @returns {string}
|
|
133
|
+
*/
|
|
134
|
+
export function buildOutdatedWarningMessage(latestVersion, failures = UPDATE_FAILURE_THRESHOLD) {
|
|
135
|
+
const target = latestVersion ? `v${LOCAL_VERSION} → v${latestVersion}` : `v${LOCAL_VERSION}`
|
|
136
|
+
return `⚠️ OUTDATED VERSION (${target}) — automatic update failed ${failures} times. Models and free quotas change often; update as soon as possible for the freshest catalog.`
|
|
137
|
+
}
|
|
138
|
+
|
|
68
139
|
/**
|
|
69
140
|
* 📖 getInstallArgs: return the correct binary and argument list for a given PM.
|
|
70
141
|
* 📖 Each PM has different syntax for global install — this normalises them.
|
|
@@ -120,6 +191,89 @@ export async function checkForUpdate() {
|
|
|
120
191
|
return latestVersion
|
|
121
192
|
}
|
|
122
193
|
|
|
194
|
+
/**
|
|
195
|
+
* 📖 enforceMandatoryStartupUpdate: startup policy for every user-facing surface.
|
|
196
|
+
* 📖 If npm has a newer release, FCM installs it immediately without asking.
|
|
197
|
+
* 📖 The first failed install blocks startup so the next launch retries. After two
|
|
198
|
+
* 📖 consecutive install failures, startup is allowed with a loud outdated warning
|
|
199
|
+
* 📖 so offline/proxy/permission users are not permanently locked out.
|
|
200
|
+
*
|
|
201
|
+
* @param {object} config
|
|
202
|
+
* @param {{ saveConfig?: Function, isDevMode?: boolean, surface?: string }} [options]
|
|
203
|
+
* @returns {Promise<{ latestVersion: string|null, allowedOutdated: boolean, warningMessage: string|null, failures: number, checked: boolean, updated: boolean, blocked: boolean }>}
|
|
204
|
+
*/
|
|
205
|
+
export async function enforceMandatoryStartupUpdate(config, options = {}) {
|
|
206
|
+
const { saveConfig, surface = 'app' } = options
|
|
207
|
+
const devMode = typeof options.isDevMode === 'boolean' ? options.isDevMode : isPackageDevMode()
|
|
208
|
+
const base = {
|
|
209
|
+
latestVersion: null,
|
|
210
|
+
allowedOutdated: false,
|
|
211
|
+
warningMessage: null,
|
|
212
|
+
failures: getUpdateInstallFailureCount(config),
|
|
213
|
+
checked: false,
|
|
214
|
+
updated: false,
|
|
215
|
+
blocked: false,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (devMode) return base
|
|
219
|
+
|
|
220
|
+
const { latestVersion, error } = await checkForUpdateDetailed()
|
|
221
|
+
base.checked = true
|
|
222
|
+
|
|
223
|
+
if (error) {
|
|
224
|
+
const settings = ensureUpdateSettings(config)
|
|
225
|
+
settings.updateCheckFailures = Math.min(Number(settings.updateCheckFailures || 0) + 1, UPDATE_FAILURE_THRESHOLD)
|
|
226
|
+
persistUpdateSettings(config, saveConfig)
|
|
227
|
+
return base
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const settings = ensureUpdateSettings(config)
|
|
231
|
+
if (settings.updateCheckFailures) {
|
|
232
|
+
settings.updateCheckFailures = 0
|
|
233
|
+
persistUpdateSettings(config, saveConfig)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!latestVersion) {
|
|
237
|
+
resetUpdateInstallFailures(config, saveConfig)
|
|
238
|
+
return base
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
base.latestVersion = latestVersion
|
|
242
|
+
const failuresBeforeInstall = getUpdateInstallFailureCount(config)
|
|
243
|
+
if (failuresBeforeInstall >= UPDATE_FAILURE_THRESHOLD) {
|
|
244
|
+
base.allowedOutdated = true
|
|
245
|
+
base.failures = failuresBeforeInstall
|
|
246
|
+
base.warningMessage = buildOutdatedWarningMessage(latestVersion, failuresBeforeInstall)
|
|
247
|
+
return base
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
console.log(chalk.dim(` ⬆ New version v${latestVersion} detected for ${surface}; updating automatically...`))
|
|
251
|
+
const updateResult = runUpdate(latestVersion, { exitOnFailure: false })
|
|
252
|
+
if (updateResult?.ok) {
|
|
253
|
+
resetUpdateInstallFailures(config, saveConfig)
|
|
254
|
+
base.updated = true
|
|
255
|
+
return base
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const failures = recordUpdateInstallFailure(config, latestVersion, updateResult?.error, saveConfig)
|
|
259
|
+
base.failures = failures
|
|
260
|
+
|
|
261
|
+
if (failures >= UPDATE_FAILURE_THRESHOLD) {
|
|
262
|
+
base.allowedOutdated = true
|
|
263
|
+
base.warningMessage = buildOutdatedWarningMessage(latestVersion, failures)
|
|
264
|
+
console.log(chalk.red(` ${base.warningMessage}`))
|
|
265
|
+
console.log(chalk.dim(` Manual update: ${getManualInstallCmd(detectPackageManager(), latestVersion)}`))
|
|
266
|
+
console.log()
|
|
267
|
+
return base
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
base.blocked = true
|
|
271
|
+
console.log(chalk.red(' ✖ Mandatory update failed. FCM will retry on the next launch.'))
|
|
272
|
+
console.log(chalk.dim(` Attempt ${failures}/${UPDATE_FAILURE_THRESHOLD}. Manual update: ${getManualInstallCmd(detectPackageManager(), latestVersion)}`))
|
|
273
|
+
console.log()
|
|
274
|
+
return base
|
|
275
|
+
}
|
|
276
|
+
|
|
123
277
|
/**
|
|
124
278
|
* 📖 fetchLastReleaseDate: Get the human-readable publish date of the latest npm release.
|
|
125
279
|
* 📖 Used in the TUI footer to show users how fresh the package is.
|
|
@@ -265,10 +419,15 @@ function installUpdateCommand(latestVersion, useSudo) {
|
|
|
265
419
|
/**
|
|
266
420
|
* 📖 runUpdate: Run npm global install to update to latestVersion.
|
|
267
421
|
* 📖 Retries with sudo on permission errors.
|
|
268
|
-
* 📖 Relaunches the process on success
|
|
422
|
+
* 📖 Relaunches the process on success. Manual update actions keep the historic
|
|
423
|
+
* 📖 behavior and exit on failure; mandatory startup checks pass exitOnFailure=false
|
|
424
|
+
* 📖 so they can persist failure counters and decide whether to let the UI start.
|
|
269
425
|
* @param {string} latestVersion
|
|
426
|
+
* @param {{ exitOnFailure?: boolean, relaunchOnSuccess?: boolean }} [options]
|
|
427
|
+
* @returns {{ ok: boolean, error?: unknown }}
|
|
270
428
|
*/
|
|
271
|
-
export function runUpdate(latestVersion) {
|
|
429
|
+
export function runUpdate(latestVersion, options = {}) {
|
|
430
|
+
const { exitOnFailure = true, relaunchOnSuccess = true } = options
|
|
272
431
|
console.log()
|
|
273
432
|
console.log(chalk.bold.cyan(' ⬆ Updating free-coding-models to v' + latestVersion + '...'))
|
|
274
433
|
console.log()
|
|
@@ -276,6 +435,7 @@ export function runUpdate(latestVersion) {
|
|
|
276
435
|
const pm = detectPackageManager()
|
|
277
436
|
const { needsSudo, checkedPath } = detectGlobalInstallPermission(pm)
|
|
278
437
|
const sudoAvailable = process.platform !== 'win32' && hasSudoCommand()
|
|
438
|
+
let lastError = null
|
|
279
439
|
|
|
280
440
|
if (needsSudo && checkedPath && sudoAvailable) {
|
|
281
441
|
console.log(chalk.yellow(` ⚠ Global ${pm} path is not writable: ${checkedPath}`))
|
|
@@ -288,9 +448,10 @@ export function runUpdate(latestVersion) {
|
|
|
288
448
|
console.log()
|
|
289
449
|
console.log(chalk.green(` ✅ Update complete! Version ${latestVersion} installed.`))
|
|
290
450
|
console.log()
|
|
291
|
-
relaunchCurrentProcess()
|
|
292
|
-
return
|
|
451
|
+
if (relaunchOnSuccess) relaunchCurrentProcess()
|
|
452
|
+
return { ok: true }
|
|
293
453
|
} catch (err) {
|
|
454
|
+
lastError = err
|
|
294
455
|
const manualCmd = getManualInstallCmd(pm, latestVersion)
|
|
295
456
|
console.log()
|
|
296
457
|
if (isPermissionError(err) && !needsSudo && sudoAvailable) {
|
|
@@ -301,9 +462,10 @@ export function runUpdate(latestVersion) {
|
|
|
301
462
|
console.log()
|
|
302
463
|
console.log(chalk.green(` ✅ Update complete with sudo! Version ${latestVersion} installed.`))
|
|
303
464
|
console.log()
|
|
304
|
-
relaunchCurrentProcess()
|
|
305
|
-
return
|
|
306
|
-
} catch {
|
|
465
|
+
if (relaunchOnSuccess) relaunchCurrentProcess()
|
|
466
|
+
return { ok: true }
|
|
467
|
+
} catch (sudoErr) {
|
|
468
|
+
lastError = sudoErr
|
|
307
469
|
console.log()
|
|
308
470
|
console.log(chalk.red(' ✖ Update failed even with sudo. Try manually:'))
|
|
309
471
|
console.log(chalk.dim(` sudo ${manualCmd}`))
|
|
@@ -318,7 +480,9 @@ export function runUpdate(latestVersion) {
|
|
|
318
480
|
console.log()
|
|
319
481
|
}
|
|
320
482
|
}
|
|
321
|
-
|
|
483
|
+
|
|
484
|
+
if (exitOnFailure) process.exit(1)
|
|
485
|
+
return { ok: false, error: lastError }
|
|
322
486
|
}
|
|
323
487
|
|
|
324
488
|
|
package/src/tui/app.js
CHANGED
|
@@ -91,11 +91,7 @@
|
|
|
91
91
|
|
|
92
92
|
import chalk from 'chalk'
|
|
93
93
|
import { createRequire } from 'module'
|
|
94
|
-
import { fileURLToPath } from 'url'
|
|
95
|
-
import { readFileSync, writeFileSync, existsSync, copyFileSync, mkdirSync } from 'fs'
|
|
96
94
|
import { randomUUID } from 'crypto'
|
|
97
|
-
import { homedir } from 'os'
|
|
98
|
-
import { join, dirname } from 'path'
|
|
99
95
|
import { MODELS, sources } from '../../sources.js'
|
|
100
96
|
import { getAvg, getVerdict, getUptime, getP95, getJitter, getStabilityScore, sortResults, filterByTier, findBestModel, parseArgs, TIER_ORDER, VERDICT_ORDER, TIER_LETTER_MAP, scoreModelForTask, getTopRecommendations, TASK_TYPES, PRIORITY_TYPES, CONTEXT_BUDGETS, formatCtxWindow, labelFromId, formatResultsAsJSON } from '../core/utils.js'
|
|
101
97
|
import { loadConfig, saveConfig, getApiKey, resolveApiKeys, addApiKey, removeApiKey, isProviderEnabled, persistApiKeysForProvider } from '../core/config.js'
|
|
@@ -181,7 +177,7 @@ const LOCAL_VERSION = pkg.version
|
|
|
181
177
|
// ─── OpenCode integration ──────────────────────────────────────────────────────
|
|
182
178
|
// 📖 OpenCode helpers are imported from ../src/opencode.js
|
|
183
179
|
|
|
184
|
-
export async function runApp(cliArgs, config) {
|
|
180
|
+
export async function runApp(cliArgs, config, startupOptions = {}) {
|
|
185
181
|
|
|
186
182
|
// 📖 Detect user active terminal theme
|
|
187
183
|
detectActiveTheme(config.settings?.theme || 'auto')
|
|
@@ -278,32 +274,12 @@ export async function runApp(cliArgs, config) {
|
|
|
278
274
|
},
|
|
279
275
|
})
|
|
280
276
|
|
|
281
|
-
// 📖
|
|
282
|
-
// 📖
|
|
283
|
-
// 📖
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
latestVersion = await checkForUpdate()
|
|
288
|
-
// 📖 Reset failure counter on successful check
|
|
289
|
-
if (config.settings?.updateCheckFailures) {
|
|
290
|
-
config.settings.updateCheckFailures = 0
|
|
291
|
-
saveConfig(config)
|
|
292
|
-
}
|
|
293
|
-
} catch (err) {
|
|
294
|
-
const failures = (config.settings?.updateCheckFailures || 0) + 1
|
|
295
|
-
if (!config.settings) config.settings = {}
|
|
296
|
-
config.settings.updateCheckFailures = Math.min(failures, 3)
|
|
297
|
-
saveConfig(config)
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// 📖 Auto-update: if a new version is available, install it immediately (skip in dev mode)
|
|
301
|
-
// 📖 runUpdate() will relaunch the process with the new version after install completes
|
|
302
|
-
if (latestVersion && !isDevMode) {
|
|
303
|
-
console.log(chalk.dim(` ⬆ New version v${latestVersion} detected, updating...`))
|
|
304
|
-
runUpdate(latestVersion)
|
|
305
|
-
return // 📖 runUpdate relaunches the process — this line is a safety guard
|
|
306
|
-
}
|
|
277
|
+
// 📖 Mandatory startup update is enforced in the bin entry before any user-facing
|
|
278
|
+
// 📖 surface starts. runApp receives the result so the TUI can display the
|
|
279
|
+
// 📖 two-failure fallback warning without repeating npm registry/install work.
|
|
280
|
+
const startupUpdate = startupOptions.startupUpdate || {}
|
|
281
|
+
const latestVersion = startupUpdate.latestVersion || null
|
|
282
|
+
const isDevMode = startupOptions.isDevMode === true
|
|
307
283
|
|
|
308
284
|
// 📖 Dynamic OpenRouter free model discovery — fetch live free models from API
|
|
309
285
|
// 📖 Replaces static openrouter entries in MODELS with fresh data.
|
|
@@ -363,6 +339,8 @@ export async function runApp(cliArgs, config) {
|
|
|
363
339
|
sessionId,
|
|
364
340
|
latestVersion,
|
|
365
341
|
isDevMode,
|
|
342
|
+
updateWarningMessage: startupUpdate.warningMessage || null,
|
|
343
|
+
updateInstallFailures: startupUpdate.failures || 0,
|
|
366
344
|
})
|
|
367
345
|
|
|
368
346
|
// 📖 Apply the pre-fetched last release date now that state is initialized
|
|
@@ -823,6 +801,7 @@ export async function runApp(cliArgs, config) {
|
|
|
823
801
|
settingsUpdateLatestVersion: state.settingsUpdateLatestVersion,
|
|
824
802
|
startupLatestVersion: state.startupLatestVersion,
|
|
825
803
|
versionAlertsEnabled: state.versionAlertsEnabled,
|
|
804
|
+
updateWarningMessage: state.updateWarningMessage,
|
|
826
805
|
favoritesPinnedAndSticky: state.favoritesPinnedAndSticky,
|
|
827
806
|
customTextFilter: state.customTextFilter,
|
|
828
807
|
lastReleaseDate: state.lastReleaseDate,
|
|
@@ -914,6 +893,7 @@ export async function runApp(cliArgs, config) {
|
|
|
914
893
|
settingsUpdateLatestVersion: state.settingsUpdateLatestVersion,
|
|
915
894
|
startupLatestVersion: state.startupLatestVersion,
|
|
916
895
|
versionAlertsEnabled: state.versionAlertsEnabled,
|
|
896
|
+
updateWarningMessage: state.updateWarningMessage,
|
|
917
897
|
favoritesPinnedAndSticky: state.favoritesPinnedAndSticky,
|
|
918
898
|
customTextFilter: state.customTextFilter,
|
|
919
899
|
lastReleaseDate: state.lastReleaseDate,
|
package/src/tui/render-table.js
CHANGED
|
@@ -136,6 +136,7 @@ export const PROVIDER_COLOR = new Proxy({}, {
|
|
|
136
136
|
* settingsUpdateLatestVersion: string|null,
|
|
137
137
|
* startupLatestVersion: string|null,
|
|
138
138
|
* versionAlertsEnabled: boolean,
|
|
139
|
+
* updateWarningMessage?: string|null,
|
|
139
140
|
* favoritesPinnedAndSticky: boolean,
|
|
140
141
|
* customTextFilter: string|null,
|
|
141
142
|
* lastReleaseDate: string|null,
|
|
@@ -174,6 +175,7 @@ export function renderTable({
|
|
|
174
175
|
settingsUpdateLatestVersion = null,
|
|
175
176
|
startupLatestVersion = null,
|
|
176
177
|
versionAlertsEnabled = true,
|
|
178
|
+
updateWarningMessage = null,
|
|
177
179
|
favoritesPinnedAndSticky = false,
|
|
178
180
|
customTextFilter = null,
|
|
179
181
|
lastReleaseDate = null,
|
|
@@ -1073,15 +1075,19 @@ export function renderTable({
|
|
|
1073
1075
|
)
|
|
1074
1076
|
|
|
1075
1077
|
if (versionStatus.isOutdated) {
|
|
1076
|
-
const updateMsg =
|
|
1078
|
+
const updateMsg = updateWarningMessage
|
|
1079
|
+
? ` ${updateWarningMessage} • Press Shift+U to retry update `
|
|
1080
|
+
: ` 🚀⬆️ UPDATE AVAILABLE — v${LOCAL_VERSION} → v${versionStatus.latestVersion} • Click here or press Shift+U to update 🚀⬆️ `
|
|
1077
1081
|
const paddedBanner = terminalCols > 0
|
|
1078
1082
|
? updateMsg + ' '.repeat(Math.max(0, terminalCols - displayWidth(updateMsg)))
|
|
1079
1083
|
: updateMsg
|
|
1080
|
-
const
|
|
1084
|
+
const updateBanner = updateWarningMessage
|
|
1085
|
+
? chalk.bgRed.white.bold(paddedBanner)
|
|
1086
|
+
: chalk.bgRgb(57, 255, 20).rgb(0, 0, 0).bold(paddedBanner)
|
|
1081
1087
|
const updateBannerRow = lines.length + 1
|
|
1082
1088
|
_lastLayout.updateBannerRow = updateBannerRow
|
|
1083
1089
|
footerHotkeys.push({ key: 'update-click', row: updateBannerRow, xStart: 1, xEnd: Math.max(terminalCols, displayWidth(updateMsg)) })
|
|
1084
|
-
lines.push(
|
|
1090
|
+
lines.push(updateBanner)
|
|
1085
1091
|
} else {
|
|
1086
1092
|
_lastLayout.updateBannerRow = 0
|
|
1087
1093
|
}
|
package/src/tui/tui-state.js
CHANGED
|
@@ -70,6 +70,8 @@ export function intervalToPingMode(intervalMs) {
|
|
|
70
70
|
* sessionId: string,
|
|
71
71
|
* latestVersion: string|null,
|
|
72
72
|
* isDevMode: boolean,
|
|
73
|
+
* updateWarningMessage?: string|null,
|
|
74
|
+
* updateInstallFailures?: number,
|
|
73
75
|
* }} opts
|
|
74
76
|
* @returns {object} The complete TUI state (mutable)
|
|
75
77
|
*/
|
|
@@ -80,6 +82,8 @@ export function createTuiState({
|
|
|
80
82
|
sessionId,
|
|
81
83
|
latestVersion,
|
|
82
84
|
isDevMode,
|
|
85
|
+
updateWarningMessage = null,
|
|
86
|
+
updateInstallFailures = 0,
|
|
83
87
|
}) {
|
|
84
88
|
const now = Date.now()
|
|
85
89
|
|
|
@@ -110,6 +114,8 @@ export function createTuiState({
|
|
|
110
114
|
|
|
111
115
|
// 📖 Version tracking — startup auto-check + periodic re-check
|
|
112
116
|
startupLatestVersion: latestVersion,
|
|
117
|
+
updateWarningMessage,
|
|
118
|
+
updateInstallFailures,
|
|
113
119
|
lastReleaseDate: null,
|
|
114
120
|
versionAlertsEnabled: !isDevMode,
|
|
115
121
|
|