free-coding-models 0.3.26 → 0.3.28
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/CHANGELOG.md +15 -0
- package/README.md +5 -1
- package/package.json +1 -1
- package/src/app.js +24 -5
- package/src/key-handler.js +49 -14
- package/src/overlays.js +2 -2
- package/src/render-table.js +26 -10
- package/src/updater.js +127 -36
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
---
|
|
3
3
|
|
|
4
|
+
## [0.3.27] - 2026-03-27
|
|
5
|
+
|
|
6
|
+
### Added
|
|
7
|
+
- **Fluorescent green UPDATE AVAILABLE banner** — impossible-to-miss fluo green (🚀⬆️) banner at the bottom of the TUI when a new version is detected; click it to update instantly
|
|
8
|
+
- **Command Palette update entry** — `⬆️ UPDATE NOW` is always the first result in Ctrl+P when an update is available
|
|
9
|
+
- **Shift+U hotkey** — press Shift+U from the main table to trigger an immediate update
|
|
10
|
+
- **Mouse-clickable update banner** — click the fluo green banner to install the latest version and relaunch
|
|
11
|
+
- **Background version re-check every 5 minutes** — if a new version is published while the TUI is open, the banner appears live without restarting
|
|
12
|
+
- **Aggressive pre-TUI update prompt** — fluorescent green header, and "Continue without update" warns that reminders will follow
|
|
13
|
+
- **Last release date in footer** — light pink `Last release: Mar 27, 2026, 09:42 PM` shows when the package was last published to npm, so users know how fresh the model data is
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **Auto-update now detects the correct package manager** — bun, pnpm, and yarn users no longer get duplicate npm installs (fixes #46)
|
|
17
|
+
- Update banner in footer shows the correct install command for your package manager
|
|
18
|
+
|
|
4
19
|
## [0.3.26] - 2026-03-27
|
|
5
20
|
|
|
6
21
|
### Added
|
package/README.md
CHANGED
|
@@ -245,6 +245,7 @@ When a tool mode is active (via `Z`), models incompatible with that tool are hig
|
|
|
245
245
|
| `G` | Cycle global theme (`Auto → Dark → Light`) |
|
|
246
246
|
| `Ctrl+P` | Open ⚡️ command palette (search + run actions) |
|
|
247
247
|
| `R/S/C/M/O/L/A/H/V/B/U` | Sort columns |
|
|
248
|
+
| `Shift+U` | Update to latest version (when update available) |
|
|
248
249
|
| `P` | Settings (API keys, providers, updates, theme) |
|
|
249
250
|
| `Q` | Smart Recommend overlay |
|
|
250
251
|
| `N` | Changelog |
|
|
@@ -265,6 +266,7 @@ When a tool mode is active (via `Z`), models incompatible with that tool are hig
|
|
|
265
266
|
| **Right-click model row** | Toggle favorite |
|
|
266
267
|
| **Scroll wheel** | Navigate table / overlays / palette |
|
|
267
268
|
| **Click footer hotkey** | Trigger that action |
|
|
269
|
+
| **Click update banner** | Install latest version and relaunch |
|
|
268
270
|
| **Click command palette item** | Select item (double-click to confirm) |
|
|
269
271
|
| **Click recommend option** | Select option (double-click to confirm) |
|
|
270
272
|
| **Click outside modal** | Close command palette |
|
|
@@ -290,8 +292,10 @@ When a tool mode is active (via `Z`), models incompatible with that tool are hig
|
|
|
290
292
|
- **OpenCode Zen models** — 8 free models exclusive to OpenCode CLI/Desktop, powered by the Zen AI gateway
|
|
291
293
|
- **Width guardrail** — shows a warning instead of a broken table in narrow terminals
|
|
292
294
|
- **Readable everywhere** — semantic theme palette keeps table rows, overlays, badges, and help screens legible in dark and light terminals
|
|
293
|
-
- **Global theme switch** — `G` cycles `auto`, `dark`,
|
|
295
|
+
- **Global theme switch** — `G` cycles `auto`, `dark`, + `light` live without restarting
|
|
294
296
|
- **Auto-retry** — timeout models keep getting retried
|
|
297
|
+
- **Aggressive update nudging** — fluorescent green banner when an update is available, impossible to miss, Shift+U hotkey, command palette entry, background re-check every 5 min, mid-session updates the banner live without restarting
|
|
298
|
+
- **Last release timestamp** — light pink footer shows `Last release: Mar 27, 2026, 09:42 PM` from npm so users know how fresh the data is
|
|
295
299
|
|
|
296
300
|
---
|
|
297
301
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.28",
|
|
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/app.js
CHANGED
|
@@ -112,7 +112,7 @@ import { runFiableMode, filterByTierOrExit, fetchOpenRouterFreeModels } from '..
|
|
|
112
112
|
import { PROVIDER_METADATA, ENV_VAR_NAMES, isWindows, isMac } from '../src/provider-metadata.js'
|
|
113
113
|
import { parseTelemetryEnv, isTelemetryDebugEnabled, telemetryDebug, ensureTelemetryConfig, getTelemetryDistinctId, getTelemetrySystem, getTelemetryTerminal, isTelemetryEnabled, sendUsageTelemetry, sendBugReport } from '../src/telemetry.js'
|
|
114
114
|
import { ensureFavoritesConfig, toFavoriteKey, syncFavoriteFlags, toggleFavoriteModel } from '../src/favorites.js'
|
|
115
|
-
import { checkForUpdateDetailed, checkForUpdate, runUpdate, promptUpdateNotification } from '
|
|
115
|
+
import { checkForUpdateDetailed, checkForUpdate, runUpdate, promptUpdateNotification, fetchLastReleaseDate } from './updater.js'
|
|
116
116
|
import { promptApiKey } from '../src/setup.js'
|
|
117
117
|
import { stripAnsi, maskApiKey, displayWidth, padEndDisplay, tintOverlayLines, keepOverlayTargetVisible, sliceOverlayLines, calculateViewport, sortResultsWithPinnedFavorites, adjustScrollOffset } from '../src/render-helpers.js'
|
|
118
118
|
import { renderTable, PROVIDER_COLOR } from '../src/render-table.js'
|
|
@@ -295,6 +295,8 @@ export async function runApp(cliArgs, config) {
|
|
|
295
295
|
// 📖 Dynamic OpenRouter free model discovery — fetch live free models from API
|
|
296
296
|
// 📖 Replaces static openrouter entries in MODELS with fresh data.
|
|
297
297
|
// 📖 Fallback: if fetch fails, the static list from sources.js stays intact + warning shown.
|
|
298
|
+
const lastReleaseDate = await fetchLastReleaseDate()
|
|
299
|
+
state.lastReleaseDate = lastReleaseDate
|
|
298
300
|
const dynamicModels = await fetchOpenRouterFreeModels()
|
|
299
301
|
if (dynamicModels) {
|
|
300
302
|
// 📖 Remove all existing openrouter entries from MODELS
|
|
@@ -381,6 +383,7 @@ export async function runApp(cliArgs, config) {
|
|
|
381
383
|
lastUserActivityAt: now, // 📖 Any keypress refreshes this timer; inactivity can force slow mode.
|
|
382
384
|
resumeSpeedOnActivity: false, // 📖 Set after idle slowdown so the next activity restarts a 60s speed burst.
|
|
383
385
|
startupLatestVersion: latestVersion, // 📖 Startup auto-check result reused by the footer banner after "skip update".
|
|
386
|
+
lastReleaseDate: null, // 📖 Human-readable last npm publish date (fetched asynchronously).
|
|
384
387
|
versionAlertsEnabled: !isDevMode, // 📖 Dev checkouts should not tell contributors to upgrade the global npm package.
|
|
385
388
|
mode, // 📖 'opencode' or 'openclaw' — controls Enter action
|
|
386
389
|
tierFilterMode: 0, // 📖 Index into TIER_CYCLE (0=All, 1=S+, 2=S, ...)
|
|
@@ -673,10 +676,10 @@ export async function runApp(cliArgs, config) {
|
|
|
673
676
|
|
|
674
677
|
// 📖 Ensure we always leave alt screen cleanly (Ctrl+C, crash, normal exit)
|
|
675
678
|
const exit = (code = 0) => {
|
|
676
|
-
// 📖 Save cache before exiting so next run starts faster
|
|
677
679
|
saveCache(state.results, state.pingMode)
|
|
678
680
|
clearInterval(ticker)
|
|
679
681
|
clearTimeout(state.pingIntervalObj)
|
|
682
|
+
clearInterval(state.versionRecheckTimer)
|
|
680
683
|
process.stdout.write(ALT_LEAVE)
|
|
681
684
|
if (process.stdout.isTTY) {
|
|
682
685
|
process.stdout.flush && process.stdout.flush()
|
|
@@ -743,6 +746,7 @@ export async function runApp(cliArgs, config) {
|
|
|
743
746
|
const stopUi = ({ resetRawMode = false } = {}) => {
|
|
744
747
|
if (ticker) clearInterval(ticker)
|
|
745
748
|
clearTimeout(state.pingIntervalObj)
|
|
749
|
+
clearInterval(state.versionRecheckTimer)
|
|
746
750
|
if (onKeyPress) process.stdin.removeListener('keypress', onKeyPress)
|
|
747
751
|
if (onMouseData) process.stdin.removeListener('data', onMouseData)
|
|
748
752
|
if (process.stdin.isTTY && resetRawMode) process.stdin.setRawMode(false)
|
|
@@ -996,7 +1000,8 @@ export async function runApp(cliArgs, config) {
|
|
|
996
1000
|
state.startupLatestVersion,
|
|
997
1001
|
state.versionAlertsEnabled,
|
|
998
1002
|
state.favoritesPinnedAndSticky,
|
|
999
|
-
state.customTextFilter
|
|
1003
|
+
state.customTextFilter,
|
|
1004
|
+
state.lastReleaseDate
|
|
1000
1005
|
)
|
|
1001
1006
|
}
|
|
1002
1007
|
tableContent = state.commandPaletteFrozenTable
|
|
@@ -1030,7 +1035,8 @@ export async function runApp(cliArgs, config) {
|
|
|
1030
1035
|
state.startupLatestVersion,
|
|
1031
1036
|
state.versionAlertsEnabled,
|
|
1032
1037
|
state.favoritesPinnedAndSticky,
|
|
1033
|
-
state.customTextFilter
|
|
1038
|
+
state.customTextFilter,
|
|
1039
|
+
state.lastReleaseDate
|
|
1034
1040
|
)
|
|
1035
1041
|
}
|
|
1036
1042
|
|
|
@@ -1074,7 +1080,7 @@ export async function runApp(cliArgs, config) {
|
|
|
1074
1080
|
pinFavorites: state.favoritesPinnedAndSticky,
|
|
1075
1081
|
})
|
|
1076
1082
|
|
|
1077
|
-
process.stdout.write(ALT_HOME + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, state.tierFilterMode, state.scrollOffset, state.terminalRows, state.terminalCols, state.originFilterMode, null, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.widthWarningShowCount, state.settingsUpdateState, state.settingsUpdateLatestVersion, false, state.startupLatestVersion, state.versionAlertsEnabled, state.favoritesPinnedAndSticky, state.customTextFilter))
|
|
1083
|
+
process.stdout.write(ALT_HOME + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, state.tierFilterMode, state.scrollOffset, state.terminalRows, state.terminalCols, state.originFilterMode, null, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.widthWarningShowCount, state.settingsUpdateState, state.settingsUpdateLatestVersion, false, state.startupLatestVersion, state.versionAlertsEnabled, state.favoritesPinnedAndSticky, state.customTextFilter, state.lastReleaseDate))
|
|
1078
1084
|
if (process.stdout.isTTY) {
|
|
1079
1085
|
process.stdout.flush && process.stdout.flush()
|
|
1080
1086
|
}
|
|
@@ -1149,6 +1155,19 @@ export async function runApp(cliArgs, config) {
|
|
|
1149
1155
|
// 📖 Save cache after initial pings complete for faster next startup
|
|
1150
1156
|
saveCache(state.results, state.pingMode)
|
|
1151
1157
|
|
|
1158
|
+
// 📖 Background version re-check: poll npm registry every 5 minutes.
|
|
1159
|
+
// 📖 If a new version appears (wasn't there at startup), update the banner live.
|
|
1160
|
+
const VERSION_RECHECK_INTERVAL_MS = 5 * 60 * 1000
|
|
1161
|
+
state.versionRecheckTimer = setInterval(async () => {
|
|
1162
|
+
if (isDevMode || !state.versionAlertsEnabled) return
|
|
1163
|
+
try {
|
|
1164
|
+
const fresh = await checkForUpdate()
|
|
1165
|
+
if (fresh) {
|
|
1166
|
+
state.startupLatestVersion = fresh
|
|
1167
|
+
}
|
|
1168
|
+
} catch {}
|
|
1169
|
+
}, VERSION_RECHECK_INTERVAL_MS)
|
|
1170
|
+
|
|
1152
1171
|
// 📖 Keep interface running forever - user can select anytime or Ctrl+C to exit
|
|
1153
1172
|
// 📖 The pings continue running in background with dynamic interval
|
|
1154
1173
|
// 📖 User can press W to decrease interval (faster pings) or = to increase (slower)
|
package/src/key-handler.js
CHANGED
|
@@ -843,6 +843,21 @@ export function createKeyHandler(ctx) {
|
|
|
843
843
|
})
|
|
844
844
|
}
|
|
845
845
|
|
|
846
|
+
// 📖 Inject a high-priority update entry at the top when a newer version is known.
|
|
847
|
+
const updateVersion = state.startupLatestVersion
|
|
848
|
+
if (updateVersion && state.versionAlertsEnabled) {
|
|
849
|
+
state.commandPaletteResults.unshift({
|
|
850
|
+
id: 'action-update-now',
|
|
851
|
+
label: `⬆️ UPDATE NOW — v${updateVersion} available (recommended!)`,
|
|
852
|
+
type: 'command',
|
|
853
|
+
depth: 0,
|
|
854
|
+
hasChildren: false,
|
|
855
|
+
isExpanded: false,
|
|
856
|
+
updateVersion,
|
|
857
|
+
keywords: ['update', 'upgrade', 'version', 'install'],
|
|
858
|
+
})
|
|
859
|
+
}
|
|
860
|
+
|
|
846
861
|
if (state.commandPaletteCursor >= state.commandPaletteResults.length) {
|
|
847
862
|
state.commandPaletteCursor = Math.max(0, state.commandPaletteResults.length - 1)
|
|
848
863
|
}
|
|
@@ -869,6 +884,14 @@ export function createKeyHandler(ctx) {
|
|
|
869
884
|
function executeCommandPaletteEntry(entry) {
|
|
870
885
|
if (!entry?.id) return
|
|
871
886
|
|
|
887
|
+
// 📖 Update action: stop TUI cleanly and run the npm update + relaunch.
|
|
888
|
+
if (entry.id === 'action-update-now' && entry.updateVersion) {
|
|
889
|
+
closeCommandPalette()
|
|
890
|
+
stopUi({ resetRawMode: true })
|
|
891
|
+
runUpdate(entry.updateVersion)
|
|
892
|
+
return
|
|
893
|
+
}
|
|
894
|
+
|
|
872
895
|
if (entry.id.startsWith('action-set-ping-') && entry.pingMode) {
|
|
873
896
|
setPingMode(entry.pingMode, 'manual')
|
|
874
897
|
return
|
|
@@ -982,6 +1005,10 @@ export function createKeyHandler(ctx) {
|
|
|
982
1005
|
if (!key) return
|
|
983
1006
|
noteUserActivity()
|
|
984
1007
|
|
|
1008
|
+
// 📖 Ctrl+C: always exit immediately, checked FIRST to prevent any other key binding from swallowing it.
|
|
1009
|
+
// 📖 Also handles the raw \x03 byte as a fallback for terminals where readline doesn't set key.ctrl properly.
|
|
1010
|
+
if ((key.ctrl && key.name === 'c') || str === '\x03') { exit(0); return }
|
|
1011
|
+
|
|
985
1012
|
// 📖 Ctrl+P toggles the command palette from the main table only.
|
|
986
1013
|
if (key.ctrl && key.name === 'p') {
|
|
987
1014
|
if (state.commandPaletteOpen) {
|
|
@@ -1555,12 +1582,12 @@ export function createKeyHandler(ctx) {
|
|
|
1555
1582
|
// 📖 Help overlay: full keyboard navigation + key swallowing while overlay is open.
|
|
1556
1583
|
if (state.helpVisible) {
|
|
1557
1584
|
const pageStep = Math.max(1, (state.terminalRows || 1) - 2)
|
|
1558
|
-
if (key.name === 'escape' || key.name === '
|
|
1585
|
+
if (key.name === 'escape' || (key.ctrl && key.name === 'h')) {
|
|
1559
1586
|
state.helpVisible = false
|
|
1560
1587
|
return
|
|
1561
1588
|
}
|
|
1562
|
-
if (key.name === 'up') { state.helpScrollOffset = Math.max(0, state.helpScrollOffset - 1); return }
|
|
1563
|
-
if (key.name === 'down') { state.helpScrollOffset += 1; return }
|
|
1589
|
+
if (key.name === 'up' || key.name === 'k') { state.helpScrollOffset = Math.max(0, state.helpScrollOffset - 1); return }
|
|
1590
|
+
if (key.name === 'down' || key.name === 'j') { state.helpScrollOffset += 1; return }
|
|
1564
1591
|
if (key.name === 'pageup') { state.helpScrollOffset = Math.max(0, state.helpScrollOffset - pageStep); return }
|
|
1565
1592
|
if (key.name === 'pagedown') { state.helpScrollOffset += pageStep; return }
|
|
1566
1593
|
if (key.name === 'home') { state.helpScrollOffset = 0; return }
|
|
@@ -2100,6 +2127,13 @@ export function createKeyHandler(ctx) {
|
|
|
2100
2127
|
return
|
|
2101
2128
|
}
|
|
2102
2129
|
|
|
2130
|
+
// 📖 Shift+U: trigger immediate update when a newer version is known.
|
|
2131
|
+
if (key.name === 'u' && key.shift && state.startupLatestVersion && state.versionAlertsEnabled) {
|
|
2132
|
+
stopUi({ resetRawMode: true })
|
|
2133
|
+
runUpdate(state.startupLatestVersion)
|
|
2134
|
+
return
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2103
2137
|
// 📖 Sorting keys: R=rank, O=origin, M=model, L=latest ping, A=avg ping, S=SWE-bench, C=context, H=health, V=verdict, B=stability, U=uptime, G=usage
|
|
2104
2138
|
// 📖 T is reserved for tier filter cycling. Y toggles favorites display mode.
|
|
2105
2139
|
// 📖 X clears the active custom text filter.
|
|
@@ -2169,8 +2203,8 @@ export function createKeyHandler(ctx) {
|
|
|
2169
2203
|
return
|
|
2170
2204
|
}
|
|
2171
2205
|
|
|
2172
|
-
// 📖 Help overlay key:
|
|
2173
|
-
if (key.name === '
|
|
2206
|
+
// 📖 Help overlay key: Ctrl+H = toggle help overlay
|
|
2207
|
+
if (key.ctrl && key.name === 'h') {
|
|
2174
2208
|
state.helpVisible = !state.helpVisible
|
|
2175
2209
|
if (state.helpVisible) state.helpScrollOffset = 0
|
|
2176
2210
|
return
|
|
@@ -2194,8 +2228,8 @@ export function createKeyHandler(ctx) {
|
|
|
2194
2228
|
return
|
|
2195
2229
|
}
|
|
2196
2230
|
|
|
2197
|
-
if (key.name === 'up') {
|
|
2198
|
-
// 📖 Main list wrap navigation: top -> bottom on Up.
|
|
2231
|
+
if (key.name === 'up' || key.name === 'k') {
|
|
2232
|
+
// 📖 Main list wrap navigation: top -> bottom on Up / K (vim-style).
|
|
2199
2233
|
const count = state.visibleSorted.length
|
|
2200
2234
|
if (count === 0) return
|
|
2201
2235
|
state.cursor = state.cursor > 0 ? state.cursor - 1 : count - 1
|
|
@@ -2203,8 +2237,8 @@ export function createKeyHandler(ctx) {
|
|
|
2203
2237
|
return
|
|
2204
2238
|
}
|
|
2205
2239
|
|
|
2206
|
-
if (key.name === 'down') {
|
|
2207
|
-
// 📖 Main list wrap navigation: bottom -> top on Down.
|
|
2240
|
+
if (key.name === 'down' || key.name === 'j') {
|
|
2241
|
+
// 📖 Main list wrap navigation: bottom -> top on Down / J (vim-style).
|
|
2208
2242
|
const count = state.visibleSorted.length
|
|
2209
2243
|
if (count === 0) return
|
|
2210
2244
|
state.cursor = state.cursor < count - 1 ? state.cursor + 1 : 0
|
|
@@ -2212,11 +2246,6 @@ export function createKeyHandler(ctx) {
|
|
|
2212
2246
|
return
|
|
2213
2247
|
}
|
|
2214
2248
|
|
|
2215
|
-
if (key.name === 'c' && key.ctrl) { // Ctrl+C
|
|
2216
|
-
exit(0)
|
|
2217
|
-
return
|
|
2218
|
-
}
|
|
2219
|
-
|
|
2220
2249
|
// 📖 Esc can dismiss the narrow-terminal warning immediately without quitting the app.
|
|
2221
2250
|
if (key.name === 'escape' && state.terminalCols > 0 && state.terminalCols < WIDTH_WARNING_MIN_COLS) {
|
|
2222
2251
|
state.widthWarningDismissed = true
|
|
@@ -2691,6 +2720,12 @@ export function createMouseEventHandler(ctx) {
|
|
|
2691
2720
|
if (layout.footerHotkeys && layout.footerHotkeys.length > 0) {
|
|
2692
2721
|
const zone = layout.footerHotkeys.find(z => y === z.row && x >= z.xStart && x <= z.xEnd)
|
|
2693
2722
|
if (zone) {
|
|
2723
|
+
// 📖 Update banner click: stop TUI and run the npm update + relaunch.
|
|
2724
|
+
if (zone.key === 'update-click' && state.startupLatestVersion && state.versionAlertsEnabled) {
|
|
2725
|
+
stopUi({ resetRawMode: true })
|
|
2726
|
+
runUpdate(state.startupLatestVersion)
|
|
2727
|
+
return
|
|
2728
|
+
}
|
|
2694
2729
|
// 📖 Map the footer zone key to a synthetic keypress.
|
|
2695
2730
|
// 📖 Most are single-character keys; special cases like ctrl+p need special handling.
|
|
2696
2731
|
if (zone.key === 'ctrl+p') {
|
package/src/overlays.js
CHANGED
|
@@ -904,7 +904,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
904
904
|
lines.push('')
|
|
905
905
|
lines.push(` ${heading('Main TUI')}`)
|
|
906
906
|
lines.push(` ${heading('Navigation')}`)
|
|
907
|
-
lines.push(` ${key('↑↓')}
|
|
907
|
+
lines.push(` ${key('↑↓ / J/K')} Navigate rows ${hint('(J/K = vim-style scroll)')}`)
|
|
908
908
|
lines.push(` ${key('Enter')} Select model and launch`)
|
|
909
909
|
lines.push(` ${hint('If the active CLI is missing, FCM offers a one-click install prompt first.')}`)
|
|
910
910
|
lines.push('')
|
|
@@ -923,7 +923,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
923
923
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
924
924
|
lines.push(` ${key('Shift+R')} Reset view settings ${hint('(tier filter, sort, provider filter → defaults)')}`)
|
|
925
925
|
lines.push(` ${key('N')} Changelog ${hint('(📋 browse all versions, Enter to view details)')}`)
|
|
926
|
-
lines.push(` ${key('
|
|
926
|
+
lines.push(` ${key('Ctrl+H')} / ${key('Esc')} Show/hide this help`)
|
|
927
927
|
lines.push(` ${key('Ctrl+C')} Exit`)
|
|
928
928
|
lines.push('')
|
|
929
929
|
lines.push(` ${heading('Settings (P)')}`)
|
package/src/render-table.js
CHANGED
|
@@ -52,6 +52,7 @@ import { usagePlaceholderForProvider } from './ping.js'
|
|
|
52
52
|
import { calculateViewport, sortResultsWithPinnedFavorites, padEndDisplay, displayWidth } from './render-helpers.js'
|
|
53
53
|
import { getToolMeta, TOOL_METADATA, TOOL_MODE_ORDER, isModelCompatibleWithTool } from './tool-metadata.js'
|
|
54
54
|
import { getColumnSpacing } from './ui-config.js'
|
|
55
|
+
import { detectPackageManager, getManualInstallCmd } from './updater.js'
|
|
55
56
|
|
|
56
57
|
const require = createRequire(import.meta.url)
|
|
57
58
|
const { version: LOCAL_VERSION } = require('../package.json')
|
|
@@ -71,6 +72,7 @@ let _lastLayout = {
|
|
|
71
72
|
hasAboveIndicator: false, // 📖 whether "... N more above ..." is shown
|
|
72
73
|
hasBelowIndicator: false, // 📖 whether "... N more below ..." is shown
|
|
73
74
|
footerHotkeys: [], // 📖 Array of { key, row, xStart, xEnd } for footer click zones
|
|
75
|
+
updateBannerRow: 0, // 📖 1-based terminal row of the fluorescent update banner (0 = none)
|
|
74
76
|
}
|
|
75
77
|
export function getLastLayout() { return _lastLayout }
|
|
76
78
|
|
|
@@ -102,7 +104,7 @@ export const PROVIDER_COLOR = new Proxy({}, {
|
|
|
102
104
|
})
|
|
103
105
|
|
|
104
106
|
// ─── renderTable: mode param controls footer hint text (opencode vs openclaw) ─────────
|
|
105
|
-
export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, legacyStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, widthWarningShowCount = 0, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, legacyFlag = false, startupLatestVersion = null, versionAlertsEnabled = true, favoritesPinnedAndSticky = false, customTextFilter = null) {
|
|
107
|
+
export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, legacyStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, widthWarningShowCount = 0, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, legacyFlag = false, startupLatestVersion = null, versionAlertsEnabled = true, favoritesPinnedAndSticky = false, customTextFilter = null, lastReleaseDate = null) {
|
|
106
108
|
// 📖 Filter out hidden models for display
|
|
107
109
|
const visibleResults = results.filter(r => !r.hidden)
|
|
108
110
|
|
|
@@ -741,7 +743,9 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
741
743
|
{ text: ' • ', key: null },
|
|
742
744
|
{ text: 'P Settings', key: 'p' },
|
|
743
745
|
{ text: ' • ', key: null },
|
|
744
|
-
{ text: 'K
|
|
746
|
+
{ text: 'J/K Navigate', key: null },
|
|
747
|
+
{ text: ' • ', key: null },
|
|
748
|
+
{ text: 'Ctrl+H Help', key: 'ctrl+h' },
|
|
745
749
|
]
|
|
746
750
|
const footerRow1 = lines.length + 1 // 📖 1-based terminal row (line hasn't been pushed yet)
|
|
747
751
|
let xPos = 1
|
|
@@ -769,7 +773,9 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
769
773
|
themeColors.dim(` • `) +
|
|
770
774
|
hotkey('P', ' Settings') +
|
|
771
775
|
themeColors.dim(` • `) +
|
|
772
|
-
|
|
776
|
+
themeColors.dim('J/K Navigate') +
|
|
777
|
+
themeColors.dim(` • `) +
|
|
778
|
+
themeColors.dim('Ctrl+H Help')
|
|
773
779
|
)
|
|
774
780
|
|
|
775
781
|
// 📖 Line 2: command palette, recommend, feedback, theme
|
|
@@ -803,7 +809,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
803
809
|
hotkey('G', ' Theme') + themeColors.dim(` • `) +
|
|
804
810
|
hotkey('I', ' Feedback, bugs & requests')
|
|
805
811
|
)
|
|
806
|
-
// 📖 Proxy status is now shown via the
|
|
812
|
+
// 📖 Proxy status is now shown via the badge in line 2 above — no need for a dedicated line
|
|
807
813
|
const footerLine =
|
|
808
814
|
themeColors.footerLove(' Made with 💖 & ☕ by \x1b]8;;https://github.com/vava-nessa\x1b\\vava-nessa\x1b]8;;\x1b\\') +
|
|
809
815
|
themeColors.dim(' • ') +
|
|
@@ -823,12 +829,17 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
823
829
|
lines.push(footerLine)
|
|
824
830
|
|
|
825
831
|
if (versionStatus.isOutdated) {
|
|
826
|
-
const
|
|
832
|
+
const updateMsg = ` 🚀⬆️ UPDATE AVAILABLE — v${LOCAL_VERSION} → v${versionStatus.latestVersion} • Click here or press Shift+U to update 🚀⬆️ `
|
|
827
833
|
const paddedBanner = terminalCols > 0
|
|
828
|
-
?
|
|
829
|
-
:
|
|
830
|
-
|
|
831
|
-
lines.
|
|
834
|
+
? updateMsg + ' '.repeat(Math.max(0, terminalCols - displayWidth(updateMsg)))
|
|
835
|
+
: updateMsg
|
|
836
|
+
const fluoGreenBanner = chalk.bgRgb(57, 255, 20).rgb(0, 0, 0).bold(paddedBanner)
|
|
837
|
+
const updateBannerRow = lines.length + 1
|
|
838
|
+
_lastLayout.updateBannerRow = updateBannerRow
|
|
839
|
+
footerHotkeys.push({ key: 'update-click', row: updateBannerRow, xStart: 1, xEnd: Math.max(terminalCols, displayWidth(updateMsg)) })
|
|
840
|
+
lines.push(fluoGreenBanner)
|
|
841
|
+
} else {
|
|
842
|
+
_lastLayout.updateBannerRow = 0
|
|
832
843
|
}
|
|
833
844
|
|
|
834
845
|
// 📖 Final footer line: changelog + optional active text-filter badge + exit hint.
|
|
@@ -872,13 +883,18 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
872
883
|
|
|
873
884
|
_lastLayout.footerHotkeys = footerHotkeys
|
|
874
885
|
|
|
886
|
+
const releaseLabel = lastReleaseDate
|
|
887
|
+
? chalk.rgb(255, 182, 193)(`Last release: ${lastReleaseDate}`)
|
|
888
|
+
: ''
|
|
889
|
+
|
|
875
890
|
lines.push(
|
|
876
891
|
' ' + themeColors.hotkey('N') + themeColors.dim(' Changelog') +
|
|
877
892
|
(filterBadge
|
|
878
893
|
? themeColors.dim(' • ') + filterBadge
|
|
879
894
|
: '') +
|
|
880
895
|
themeColors.dim(' • ') +
|
|
881
|
-
themeColors.dim('Ctrl+C Exit')
|
|
896
|
+
themeColors.dim('Ctrl+C Exit') +
|
|
897
|
+
(releaseLabel ? themeColors.dim(' • ') + releaseLabel : '')
|
|
882
898
|
)
|
|
883
899
|
|
|
884
900
|
// 📖 Append \x1b[K (erase to EOL) to each line so leftover chars from previous
|
package/src/updater.js
CHANGED
|
@@ -12,11 +12,10 @@
|
|
|
12
12
|
* - `checkForUpdate()` — thin backward-compatible wrapper used at startup for the
|
|
13
13
|
* auto-update guard. Returns `latestVersion` (string) or `null`.
|
|
14
14
|
*
|
|
15
|
-
* - `runUpdate(latestVersion)` —
|
|
16
|
-
* retrying with `sudo` on EACCES/EPERM.
|
|
17
|
-
* same argv. On failure, prints manual
|
|
18
|
-
*
|
|
19
|
-
* but `execSync` must block to give `stdio: 'inherit'` feedback in the terminal.
|
|
15
|
+
* - `runUpdate(latestVersion)` — detects the active package manager (npm/bun/pnpm/yarn),
|
|
16
|
+
* runs the correct global install command, retrying with `sudo` on EACCES/EPERM.
|
|
17
|
+
* On success, relaunches the process with the same argv. On failure, prints manual
|
|
18
|
+
* instructions (using the correct PM command) and exits with code 1.
|
|
20
19
|
*
|
|
21
20
|
* - `promptUpdateNotification(latestVersion)` — renders a small centered interactive menu
|
|
22
21
|
* that lets the user choose: Update Now / Read Changelogs / Continue without update.
|
|
@@ -29,14 +28,21 @@
|
|
|
29
28
|
* can be imported independently from the bin entry point.
|
|
30
29
|
* - The auto-update flow in `main()` skips update if `isDevMode` is detected (presence of
|
|
31
30
|
* a `.git` directory next to the package root) to avoid an infinite update loop in dev.
|
|
31
|
+
* - `detectPackageManager()` checks the install path, script path, and runtime binary
|
|
32
|
+
* to determine which package manager (npm/bun/pnpm/yarn) owns the installation.
|
|
33
|
+
* All install commands, permission probes, and error messages use the detected PM.
|
|
32
34
|
*
|
|
33
35
|
* @functions
|
|
36
|
+
* → detectPackageManager() — Detect which PM owns the current installation
|
|
37
|
+
* → getInstallArgs(pm, version) — Build correct { bin, args } per package manager
|
|
38
|
+
* → getManualInstallCmd(pm, version) — Human-readable install command string for error messages
|
|
34
39
|
* → checkForUpdateDetailed() — Fetch npm latest with explicit error info
|
|
35
40
|
* → checkForUpdate() — Startup wrapper, returns version string or null
|
|
36
|
-
* → runUpdate(latestVersion) — Install new version via
|
|
41
|
+
* → runUpdate(latestVersion) — Install new version via detected PM + relaunch
|
|
37
42
|
* → promptUpdateNotification(version) — Interactive pre-TUI update menu
|
|
38
43
|
*
|
|
39
44
|
* @exports
|
|
45
|
+
* detectPackageManager, getInstallArgs, getManualInstallCmd,
|
|
40
46
|
* checkForUpdateDetailed, checkForUpdate, runUpdate, promptUpdateNotification
|
|
41
47
|
*
|
|
42
48
|
* @see bin/free-coding-models.js — calls checkForUpdate() at startup and runUpdate() on confirm
|
|
@@ -51,6 +57,50 @@ const readline = require('readline')
|
|
|
51
57
|
const pkg = require('../package.json')
|
|
52
58
|
const LOCAL_VERSION = pkg.version
|
|
53
59
|
|
|
60
|
+
/**
|
|
61
|
+
* 📖 detectPackageManager: figure out which package manager owns the current installation.
|
|
62
|
+
* 📖 Checks import.meta.url (package install path), process.argv[1] (script entry),
|
|
63
|
+
* 📖 and process.execPath (runtime binary) for signatures of bun, pnpm, or yarn.
|
|
64
|
+
* 📖 Falls back to 'npm' when no other signature is found.
|
|
65
|
+
* @returns {'npm' | 'bun' | 'pnpm' | 'yarn'}
|
|
66
|
+
*/
|
|
67
|
+
export function detectPackageManager() {
|
|
68
|
+
const sources = [import.meta.url, process.argv[1] || '', process.execPath || '']
|
|
69
|
+
const combined = sources.join(' ').toLowerCase()
|
|
70
|
+
if (combined.includes('.bun')) return 'bun'
|
|
71
|
+
if (combined.includes('pnpm')) return 'pnpm'
|
|
72
|
+
if (combined.includes('yarn')) return 'yarn'
|
|
73
|
+
return 'npm'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 📖 getInstallArgs: return the correct binary and argument list for a given PM.
|
|
78
|
+
* 📖 Each PM has different syntax for global install — this normalises them.
|
|
79
|
+
* @param {'npm' | 'bun' | 'pnpm' | 'yarn'} pm
|
|
80
|
+
* @param {string} version
|
|
81
|
+
* @returns {{ bin: string, args: string[] }}
|
|
82
|
+
*/
|
|
83
|
+
export function getInstallArgs(pm, version) {
|
|
84
|
+
const pkg = `free-coding-models@${version}`
|
|
85
|
+
switch (pm) {
|
|
86
|
+
case 'bun': return { bin: 'bun', args: ['add', '-g', pkg] }
|
|
87
|
+
case 'pnpm': return { bin: 'pnpm', args: ['add', '-g', pkg] }
|
|
88
|
+
case 'yarn': return { bin: 'yarn', args: ['global', 'add', pkg] }
|
|
89
|
+
default: return { bin: 'npm', args: ['i', '-g', pkg, '--prefer-online'] }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 📖 getManualInstallCmd: human-readable command string for error / fallback messages.
|
|
95
|
+
* @param {'npm' | 'bun' | 'pnpm' | 'yarn'} pm
|
|
96
|
+
* @param {string} version
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
export function getManualInstallCmd(pm, version) {
|
|
100
|
+
const { bin, args } = getInstallArgs(pm, version)
|
|
101
|
+
return `${bin} ${args.join(' ')}`
|
|
102
|
+
}
|
|
103
|
+
|
|
54
104
|
/**
|
|
55
105
|
* 📖 checkForUpdateDetailed: Fetch npm latest version with explicit error details.
|
|
56
106
|
* 📖 Used by settings manual-check flow to display meaningful status in the UI.
|
|
@@ -79,26 +129,67 @@ export async function checkForUpdate() {
|
|
|
79
129
|
}
|
|
80
130
|
|
|
81
131
|
/**
|
|
82
|
-
* 📖
|
|
83
|
-
* 📖
|
|
84
|
-
*
|
|
85
|
-
|
|
86
|
-
|
|
132
|
+
* 📖 fetchLastReleaseDate: Get the human-readable publish date of the latest npm release.
|
|
133
|
+
* 📖 Used in the TUI footer to show users how fresh the package is.
|
|
134
|
+
* @returns {Promise<string|null>} e.g. "Mar 27, 2026, 09:42 PM" or null on failure
|
|
135
|
+
*/
|
|
136
|
+
export async function fetchLastReleaseDate() {
|
|
137
|
+
try {
|
|
138
|
+
const res = await fetch('https://registry.npmjs.org/free-coding-models', { signal: AbortSignal.timeout(5000) })
|
|
139
|
+
if (!res.ok) return null
|
|
140
|
+
const data = await res.json()
|
|
141
|
+
const timeMap = data?.time
|
|
142
|
+
if (!timeMap) return null
|
|
143
|
+
const latestKey = data?.['dist-tags']?.latest
|
|
144
|
+
if (!latestKey || !timeMap[latestKey]) return null
|
|
145
|
+
const d = new Date(timeMap[latestKey])
|
|
146
|
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
|
147
|
+
const hh = d.getHours()
|
|
148
|
+
const mm = String(d.getMinutes()).padStart(2, '0')
|
|
149
|
+
const ampm = hh >= 12 ? 'PM' : 'AM'
|
|
150
|
+
const h12 = hh % 12 || 12
|
|
151
|
+
return `${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}, ${h12}:${mm} ${ampm}`
|
|
152
|
+
} catch {
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 📖 detectGlobalInstallPermission: check whether the detected PM's global install paths are writable.
|
|
159
|
+
* 📖 Bun installs to ~/.bun/install/global/ (always user-writable) so sudo is never needed.
|
|
160
|
+
* 📖 For npm/pnpm/yarn we probe their global root/prefix paths and check writability.
|
|
161
|
+
* @param {'npm' | 'bun' | 'pnpm' | 'yarn'} pm
|
|
87
162
|
* @returns {{ needsSudo: boolean, checkedPath: string|null }}
|
|
88
163
|
*/
|
|
89
|
-
function detectGlobalInstallPermission() {
|
|
164
|
+
function detectGlobalInstallPermission(pm) {
|
|
165
|
+
if (pm === 'bun') {
|
|
166
|
+
return { needsSudo: false, checkedPath: null }
|
|
167
|
+
}
|
|
168
|
+
|
|
90
169
|
const { execFileSync } = require('child_process')
|
|
91
170
|
const candidates = []
|
|
92
171
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
172
|
+
if (pm === 'pnpm') {
|
|
173
|
+
try {
|
|
174
|
+
const root = execFileSync('pnpm', ['root', '-g'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
|
|
175
|
+
if (root) candidates.push(root)
|
|
176
|
+
} catch {}
|
|
177
|
+
} else if (pm === 'yarn') {
|
|
178
|
+
try {
|
|
179
|
+
const dir = execFileSync('yarn', ['global', 'dir'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
|
|
180
|
+
if (dir) candidates.push(dir)
|
|
181
|
+
} catch {}
|
|
182
|
+
} else {
|
|
183
|
+
try {
|
|
184
|
+
const npmRoot = execFileSync('npm', ['root', '-g'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
|
|
185
|
+
if (npmRoot) candidates.push(npmRoot)
|
|
186
|
+
} catch {}
|
|
97
187
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
188
|
+
try {
|
|
189
|
+
const npmPrefix = execFileSync('npm', ['prefix', '-g'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
|
|
190
|
+
if (npmPrefix) candidates.push(npmPrefix)
|
|
191
|
+
} catch {}
|
|
192
|
+
}
|
|
102
193
|
|
|
103
194
|
for (const candidate of candidates) {
|
|
104
195
|
try {
|
|
@@ -162,20 +253,21 @@ function relaunchCurrentProcess() {
|
|
|
162
253
|
}
|
|
163
254
|
|
|
164
255
|
/**
|
|
165
|
-
* 📖 installUpdateCommand: run
|
|
256
|
+
* 📖 installUpdateCommand: run global install using the detected package manager, optionally prefixed with sudo.
|
|
166
257
|
* @param {string} latestVersion
|
|
167
258
|
* @param {boolean} useSudo
|
|
168
259
|
*/
|
|
169
260
|
function installUpdateCommand(latestVersion, useSudo) {
|
|
170
261
|
const { execFileSync } = require('child_process')
|
|
171
|
-
const
|
|
262
|
+
const pm = detectPackageManager()
|
|
263
|
+
const { bin, args } = getInstallArgs(pm, latestVersion)
|
|
172
264
|
|
|
173
265
|
if (useSudo) {
|
|
174
|
-
execFileSync('sudo', [
|
|
266
|
+
execFileSync('sudo', [bin, ...args], { stdio: 'inherit', shell: false })
|
|
175
267
|
return
|
|
176
268
|
}
|
|
177
269
|
|
|
178
|
-
execFileSync(
|
|
270
|
+
execFileSync(bin, args, { stdio: 'inherit', shell: false })
|
|
179
271
|
}
|
|
180
272
|
|
|
181
273
|
/**
|
|
@@ -189,19 +281,17 @@ export function runUpdate(latestVersion) {
|
|
|
189
281
|
console.log(chalk.bold.cyan(' ⬆ Updating free-coding-models to v' + latestVersion + '...'))
|
|
190
282
|
console.log()
|
|
191
283
|
|
|
192
|
-
const
|
|
284
|
+
const pm = detectPackageManager()
|
|
285
|
+
const { needsSudo, checkedPath } = detectGlobalInstallPermission(pm)
|
|
193
286
|
const sudoAvailable = process.platform !== 'win32' && hasSudoCommand()
|
|
194
287
|
|
|
195
288
|
if (needsSudo && checkedPath && sudoAvailable) {
|
|
196
|
-
console.log(chalk.yellow(` ⚠ Global
|
|
289
|
+
console.log(chalk.yellow(` ⚠ Global ${pm} path is not writable: ${checkedPath}`))
|
|
197
290
|
console.log(chalk.dim(' Re-running update with sudo so you can enter your password once.'))
|
|
198
291
|
console.log()
|
|
199
292
|
}
|
|
200
293
|
|
|
201
294
|
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
295
|
installUpdateCommand(latestVersion, needsSudo && sudoAvailable)
|
|
206
296
|
console.log()
|
|
207
297
|
console.log(chalk.green(` ✅ Update complete! Version ${latestVersion} installed.`))
|
|
@@ -209,9 +299,10 @@ export function runUpdate(latestVersion) {
|
|
|
209
299
|
relaunchCurrentProcess()
|
|
210
300
|
return
|
|
211
301
|
} catch (err) {
|
|
302
|
+
const manualCmd = getManualInstallCmd(pm, latestVersion)
|
|
212
303
|
console.log()
|
|
213
304
|
if (isPermissionError(err) && !needsSudo && sudoAvailable) {
|
|
214
|
-
console.log(chalk.yellow(
|
|
305
|
+
console.log(chalk.yellow(` ⚠ Permission denied during ${pm} global install. Retrying with sudo...`))
|
|
215
306
|
console.log()
|
|
216
307
|
try {
|
|
217
308
|
installUpdateCommand(latestVersion, true)
|
|
@@ -223,15 +314,15 @@ export function runUpdate(latestVersion) {
|
|
|
223
314
|
} catch {
|
|
224
315
|
console.log()
|
|
225
316
|
console.log(chalk.red(' ✖ Update failed even with sudo. Try manually:'))
|
|
226
|
-
console.log(chalk.dim(
|
|
317
|
+
console.log(chalk.dim(` sudo ${manualCmd}`))
|
|
227
318
|
console.log()
|
|
228
319
|
}
|
|
229
320
|
} else if (isPermissionError(err) && !sudoAvailable && process.platform !== 'win32') {
|
|
230
321
|
console.log(chalk.red(' ✖ Update failed due to permissions and `sudo` is not available in PATH.'))
|
|
231
|
-
console.log(chalk.dim(` Try manually
|
|
322
|
+
console.log(chalk.dim(` Try manually: ${manualCmd}`))
|
|
232
323
|
console.log()
|
|
233
324
|
} else {
|
|
234
|
-
console.log(chalk.red(
|
|
325
|
+
console.log(chalk.red(` ✖ Update failed. Try manually: ${manualCmd}`))
|
|
235
326
|
console.log()
|
|
236
327
|
}
|
|
237
328
|
}
|
|
@@ -264,7 +355,7 @@ export async function promptUpdateNotification(latestVersion) {
|
|
|
264
355
|
{
|
|
265
356
|
label: 'Continue without update',
|
|
266
357
|
icon: '▶',
|
|
267
|
-
description: '
|
|
358
|
+
description: '⚠ You will be reminded again in the TUI',
|
|
268
359
|
},
|
|
269
360
|
]
|
|
270
361
|
|
|
@@ -278,8 +369,8 @@ export async function promptUpdateNotification(latestVersion) {
|
|
|
278
369
|
const centerPad = ' '.repeat(Math.max(0, Math.floor((terminalWidth - maxWidth) / 2)))
|
|
279
370
|
|
|
280
371
|
console.log()
|
|
281
|
-
console.log(centerPad + chalk.bold.
|
|
282
|
-
console.log(centerPad + chalk.
|
|
372
|
+
console.log(centerPad + chalk.bold.rgb(57, 255, 20)(' 🚀⬆️ UPDATE AVAILABLE'))
|
|
373
|
+
console.log(centerPad + chalk.rgb(57, 255, 20)(` Version ${latestVersion} is ready to install`))
|
|
283
374
|
console.log()
|
|
284
375
|
console.log(centerPad + chalk.bold(' ⚡ Free Coding Models') + chalk.dim(` v${LOCAL_VERSION}`))
|
|
285
376
|
console.log()
|