free-coding-models 0.3.25 → 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 +62 -2
- package/README.md +23 -1
- package/package.json +1 -1
- package/sources.js +37 -19
- package/src/app.js +33 -6
- package/src/command-palette.js +1 -0
- package/src/installed-models-manager.js +636 -0
- package/src/key-handler.js +187 -14
- package/src/overlays.js +111 -2
- package/src/render-table.js +26 -10
- package/src/updater.js +127 -36
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()
|