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/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)` — runs `npm i -g free-coding-models@<version> --prefer-online`,
16
- * retrying with `sudo` on EACCES/EPERM. On success, relaunches the process with the
17
- * same argv. On failure, prints manual instructions and exits with code 1.
18
- * Uses `require('child_process').execSync` inline because ESM dynamic import is async
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 npm global + relaunch
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
- * 📖 detectGlobalInstallPermission: check whether npm global install paths are writable.
83
- * 📖 On sudo-based systems (Arch, many Linux/macOS setups), `npm i -g` will fail with EACCES
84
- * 📖 if the current user cannot write to the resolved global root/prefix.
85
- * 📖 We probe those paths ahead of time so the updater can go straight to an interactive
86
- * 📖 `sudo npm i -g ...` instead of printing a wall of permission errors first.
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
- try {
94
- const npmRoot = execFileSync('npm', ['root', '-g'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
95
- if (npmRoot) candidates.push(npmRoot)
96
- } catch {}
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
- try {
99
- const npmPrefix = execFileSync('npm', ['prefix', '-g'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
100
- if (npmPrefix) candidates.push(npmPrefix)
101
- } catch {}
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 npm global install, optionally prefixed with sudo.
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 npmArgs = ['i', '-g', `free-coding-models@${latestVersion}`, '--prefer-online']
262
+ const pm = detectPackageManager()
263
+ const { bin, args } = getInstallArgs(pm, latestVersion)
172
264
 
173
265
  if (useSudo) {
174
- execFileSync('sudo', ['npm', ...npmArgs], { stdio: 'inherit', shell: false })
266
+ execFileSync('sudo', [bin, ...args], { stdio: 'inherit', shell: false })
175
267
  return
176
268
  }
177
269
 
178
- execFileSync('npm', npmArgs, { stdio: 'inherit', shell: false })
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 { needsSudo, checkedPath } = detectGlobalInstallPermission()
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 npm path is not writable: ${checkedPath}`))
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(' ⚠ Permission denied during npm global install. Retrying with sudo...'))
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(' sudo npm i -g free-coding-models@' + latestVersion))
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 with your system's privilege escalation tool for free-coding-models@${latestVersion}.`))
322
+ console.log(chalk.dim(` Try manually: ${manualCmd}`))
232
323
  console.log()
233
324
  } else {
234
- console.log(chalk.red(' ✖ Update failed. Try manually: npm i -g free-coding-models@' + latestVersion))
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: 'Use current version',
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.red(' UPDATE AVAILABLE'))
282
- console.log(centerPad + chalk.red(` Version ${latestVersion} is ready to install`))
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()