free-coding-models 0.3.0 → 0.3.2

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.
@@ -19,15 +19,22 @@
19
19
  * 📖 Crush: writes crush.json with provider config + models.large/small defaults
20
20
  * 📖 Pi: uses --provider/--model CLI flags for guaranteed auto-selection
21
21
  * 📖 Aider: writes ~/.aider.conf.yml + passes --model flag
22
- * 📖 Claude Code: uses ANTHROPIC_BASE_URL env + --model flag (proxy translates Anthropic ↔ OpenAI)
22
+ * 📖 Claude Code: uses ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN only (mirrors free-claude-code)
23
+ * 📖 Codex CLI: uses a custom model_provider override so Codex stays in explicit API-provider mode
24
+ * 📖 Gemini CLI: proxy mode is capability-gated because older builds do not support custom base URL routing cleanly
23
25
  *
24
26
  * @functions
25
27
  * → `resolveLauncherModelId` — choose the provider-specific id or proxy slug for a launch
28
+ * → `applyClaudeCodeModelOverrides` — force Claude Code auxiliary model slots onto the chosen proxy model
29
+ * → `buildClaudeProxyAuthToken` — encode the proxy token + selected model hint for Claude-only fallback routing
30
+ * → `buildCodexProxyArgs` — force Codex into a proxy-backed custom provider config
31
+ * → `inspectGeminiCliSupport` — detect whether the installed Gemini CLI can use proxy mode safely
26
32
  * → `writeGooseConfig` — install provider + set GOOSE_PROVIDER/GOOSE_MODEL in config.yaml
27
33
  * → `writeCrushConfig` — write provider + models.large/small to crush.json
28
34
  * → `startExternalTool` — configure and launch the selected external tool mode
29
35
  *
30
- * @exports resolveLauncherModelId, startExternalTool
36
+ * @exports resolveLauncherModelId, applyClaudeCodeModelOverrides, buildClaudeProxyAuthToken
37
+ * @exports buildCodexProxyArgs, inspectGeminiCliSupport, startExternalTool
31
38
  *
32
39
  * @see src/tool-metadata.js
33
40
  * @see src/provider-metadata.js
@@ -35,10 +42,10 @@
35
42
  */
36
43
 
37
44
  import chalk from 'chalk'
38
- import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from 'fs'
45
+ import { accessSync, constants, existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync, copyFileSync } from 'fs'
39
46
  import { homedir } from 'os'
40
- import { dirname, join } from 'path'
41
- import { spawn } from 'child_process'
47
+ import { delimiter, dirname, join } from 'path'
48
+ import { spawn, spawnSync } from 'child_process'
42
49
  import { sources } from '../sources.js'
43
50
  import { PROVIDER_COLOR } from './render-table.js'
44
51
  import { getApiKey, getProxySettings } from './config.js'
@@ -47,6 +54,36 @@ import { getToolMeta } from './tool-metadata.js'
47
54
  import { ensureProxyRunning, resolveProxyModelId } from './opencode.js'
48
55
  import { PROVIDER_METADATA } from './provider-metadata.js'
49
56
 
57
+ const OPENAI_COMPAT_ENV_KEYS = [
58
+ 'OPENAI_API_KEY',
59
+ 'OPENAI_BASE_URL',
60
+ 'OPENAI_API_BASE',
61
+ 'OPENAI_MODEL',
62
+ 'LLM_API_KEY',
63
+ 'LLM_BASE_URL',
64
+ 'LLM_MODEL',
65
+ ]
66
+ const ANTHROPIC_ENV_KEYS = [
67
+ 'ANTHROPIC_API_KEY',
68
+ 'ANTHROPIC_AUTH_TOKEN',
69
+ 'ANTHROPIC_BASE_URL',
70
+ 'ANTHROPIC_MODEL',
71
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL',
72
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL',
73
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
74
+ 'ANTHROPIC_SMALL_FAST_MODEL',
75
+ 'CLAUDE_CODE_SUBAGENT_MODEL',
76
+ ]
77
+ const GEMINI_ENV_KEYS = [
78
+ 'GEMINI_API_KEY',
79
+ 'GOOGLE_API_KEY',
80
+ 'GOOGLE_GEMINI_BASE_URL',
81
+ 'GOOGLE_VERTEX_BASE_URL',
82
+ ]
83
+ const PROXY_SANITIZED_ENV_KEYS = [...OPENAI_COMPAT_ENV_KEYS, ...ANTHROPIC_ENV_KEYS, ...GEMINI_ENV_KEYS]
84
+ const GEMINI_PROXY_MIN_VERSION = '0.34.0'
85
+ const EXPERIMENTAL_PROXY_TOOLS_NOTE = 'FCM Proxy V2 support for external tools is still in beta, so some launch and authentication flows can remain flaky while the integration stabilizes.'
86
+
50
87
  function ensureDir(filePath) {
51
88
  const dir = dirname(filePath)
52
89
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
@@ -82,6 +119,16 @@ function getProviderBaseUrl(providerKey) {
82
119
  .replace(/\/predictions$/i, '')
83
120
  }
84
121
 
122
+ function deleteEnvKeys(env, keys) {
123
+ for (const key of keys) delete env[key]
124
+ }
125
+
126
+ function cloneInheritedEnv(inheritedEnv = process.env, sanitizeKeys = []) {
127
+ const env = { ...inheritedEnv }
128
+ deleteEnvKeys(env, sanitizeKeys)
129
+ return env
130
+ }
131
+
85
132
  function applyOpenAiCompatEnv(env, apiKey, baseUrl, modelId) {
86
133
  if (!apiKey || !baseUrl || !modelId) return env
87
134
  env.OPENAI_API_KEY = apiKey
@@ -107,17 +154,45 @@ export function resolveLauncherModelId(model, useProxy = false) {
107
154
  return model?.modelId ?? ''
108
155
  }
109
156
 
110
- function buildToolEnv(mode, model, config) {
157
+ export function applyClaudeCodeModelOverrides(env, modelId) {
158
+ const resolvedModelId = typeof modelId === 'string' ? modelId.trim() : ''
159
+ if (!resolvedModelId) return env
160
+
161
+ // 📖 Claude Code still uses auxiliary model slots (opus/sonnet/haiku/subagents)
162
+ // 📖 even when a custom primary model is selected. Pin them all to the same slug.
163
+ env.ANTHROPIC_MODEL = resolvedModelId
164
+ env.ANTHROPIC_DEFAULT_OPUS_MODEL = resolvedModelId
165
+ env.ANTHROPIC_DEFAULT_SONNET_MODEL = resolvedModelId
166
+ env.ANTHROPIC_DEFAULT_HAIKU_MODEL = resolvedModelId
167
+ env.ANTHROPIC_SMALL_FAST_MODEL = resolvedModelId
168
+ env.CLAUDE_CODE_SUBAGENT_MODEL = resolvedModelId
169
+ return env
170
+ }
171
+
172
+ export function buildClaudeProxyAuthToken(proxyToken, modelId) {
173
+ const resolvedProxyToken = typeof proxyToken === 'string' ? proxyToken.trim() : ''
174
+ const resolvedModelId = typeof modelId === 'string' ? modelId.trim() : ''
175
+ if (!resolvedProxyToken) return ''
176
+ return resolvedModelId ? `${resolvedProxyToken}:${resolvedModelId}` : resolvedProxyToken
177
+ }
178
+
179
+ export function buildToolEnv(mode, model, config, options = {}) {
180
+ const {
181
+ sanitize = false,
182
+ includeCompatDefaults = true,
183
+ includeProviderEnv = true,
184
+ inheritedEnv = process.env,
185
+ } = options
111
186
  const providerKey = model.providerKey
112
187
  const providerUrl = sources[providerKey]?.url || ''
113
188
  const baseUrl = getProviderBaseUrl(providerKey)
114
189
  const apiKey = getApiKey(config, providerKey)
115
- const env = { ...process.env }
190
+ const env = cloneInheritedEnv(inheritedEnv, sanitize ? PROXY_SANITIZED_ENV_KEYS : [])
116
191
  const providerEnvName = ENV_VAR_NAMES[providerKey]
117
- if (providerEnvName && apiKey) env[providerEnvName] = apiKey
192
+ if (includeProviderEnv && providerEnvName && apiKey) env[providerEnvName] = apiKey
118
193
 
119
194
  // 📖 OpenAI-compatible defaults reused by multiple CLIs.
120
- if (apiKey && baseUrl) {
195
+ if (includeCompatDefaults && apiKey && baseUrl) {
121
196
  env.OPENAI_API_KEY = apiKey
122
197
  env.OPENAI_BASE_URL = baseUrl
123
198
  env.OPENAI_API_BASE = baseUrl
@@ -135,6 +210,7 @@ function buildToolEnv(mode, model, config) {
135
210
  }
136
211
 
137
212
  if (mode === 'gemini' && apiKey && baseUrl) {
213
+ env.GEMINI_API_KEY = apiKey
138
214
  env.GOOGLE_API_KEY = apiKey
139
215
  env.GOOGLE_GEMINI_BASE_URL = baseUrl
140
216
  }
@@ -142,6 +218,114 @@ function buildToolEnv(mode, model, config) {
142
218
  return { env, apiKey, baseUrl, providerUrl }
143
219
  }
144
220
 
221
+ export function buildCodexProxyArgs(baseUrl) {
222
+ return [
223
+ '-c', 'model_provider="fcm_proxy"',
224
+ '-c', 'model_providers.fcm_proxy.name="FCM Proxy V2"',
225
+ '-c', `model_providers.fcm_proxy.base_url=${JSON.stringify(baseUrl)}`,
226
+ '-c', 'model_providers.fcm_proxy.env_key="FCM_PROXY_API_KEY"',
227
+ '-c', 'model_providers.fcm_proxy.wire_api="responses"',
228
+ ]
229
+ }
230
+
231
+ function compareSemver(a, b) {
232
+ const left = String(a || '').split('.').map(part => Number.parseInt(part, 10) || 0)
233
+ const right = String(b || '').split('.').map(part => Number.parseInt(part, 10) || 0)
234
+ const length = Math.max(left.length, right.length)
235
+ for (let idx = 0; idx < length; idx++) {
236
+ const lhs = left[idx] || 0
237
+ const rhs = right[idx] || 0
238
+ if (lhs > rhs) return 1
239
+ if (lhs < rhs) return -1
240
+ }
241
+ return 0
242
+ }
243
+
244
+ function findExecutableOnPath(command) {
245
+ const pathValue = process.env.PATH || ''
246
+ const candidates = process.platform === 'win32'
247
+ ? [command, `${command}.cmd`, `${command}.exe`]
248
+ : [command]
249
+
250
+ for (const dir of pathValue.split(delimiter).filter(Boolean)) {
251
+ for (const candidate of candidates) {
252
+ const fullPath = join(dir, candidate)
253
+ try {
254
+ accessSync(fullPath, constants.X_OK)
255
+ return fullPath
256
+ } catch { /* not executable */ }
257
+ }
258
+ }
259
+ return null
260
+ }
261
+
262
+ function findPackageJsonUpwards(startPath) {
263
+ let current = dirname(startPath)
264
+ while (current && current !== dirname(current)) {
265
+ const packageJsonPath = join(current, 'package.json')
266
+ if (existsSync(packageJsonPath)) return packageJsonPath
267
+ current = dirname(current)
268
+ }
269
+ return null
270
+ }
271
+
272
+ function detectGeminiCliVersion(binaryPath) {
273
+ if (!binaryPath) return null
274
+ try {
275
+ const realPath = realpathSync(binaryPath)
276
+ const versionMatch = realPath.match(/gemini-cli[\\/](\d+\.\d+\.\d+)(?:[\\/]|$)/)
277
+ if (versionMatch?.[1]) return versionMatch[1]
278
+
279
+ const packageJsonPath = findPackageJsonUpwards(realPath)
280
+ if (!packageJsonPath) return null
281
+ const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
282
+ if (typeof pkg?.version === 'string' && pkg.version.length > 0) {
283
+ return pkg.version
284
+ }
285
+ } catch { /* best effort */ }
286
+ return null
287
+ }
288
+
289
+ export function extractGeminiConfigError(output) {
290
+ const text = String(output || '').trim()
291
+ if (!text.includes('Invalid configuration in ')) return null
292
+ const lines = text.split(/\r?\n/).filter(Boolean)
293
+ return lines.slice(0, 8).join('\n')
294
+ }
295
+
296
+ export function inspectGeminiCliSupport(options = {}) {
297
+ const binaryPath = options.binaryPath || findExecutableOnPath(options.command || 'gemini')
298
+ if (!binaryPath) {
299
+ return {
300
+ installed: false,
301
+ version: null,
302
+ supportsProxyBaseUrl: false,
303
+ configError: null,
304
+ reason: 'Gemini CLI is not installed in PATH.',
305
+ }
306
+ }
307
+
308
+ const version = options.version || detectGeminiCliVersion(binaryPath)
309
+ const helpResult = options.helpResult || spawnSync(binaryPath, ['--help'], {
310
+ encoding: 'utf8',
311
+ timeout: 5000,
312
+ env: options.inheritedEnv || process.env,
313
+ })
314
+ const helpOutput = `${helpResult.stdout || ''}\n${helpResult.stderr || ''}`.trim()
315
+ const configError = extractGeminiConfigError(helpOutput)
316
+ const supportsProxyBaseUrl = version ? compareSemver(version, GEMINI_PROXY_MIN_VERSION) >= 0 : false
317
+
318
+ return {
319
+ installed: true,
320
+ version,
321
+ supportsProxyBaseUrl,
322
+ configError,
323
+ reason: supportsProxyBaseUrl
324
+ ? null
325
+ : `Gemini CLI ${version || '(unknown version)'} does not expose stable custom base URL support for proxy mode yet.`,
326
+ }
327
+ }
328
+
145
329
  function spawnCommand(command, args, env) {
146
330
  return new Promise((resolve, reject) => {
147
331
  const child = spawn(command, args, {
@@ -347,6 +531,10 @@ function printConfigResult(toolName, result) {
347
531
  if (result.backupPath) console.log(chalk.dim(` 💾 Backup: ${result.backupPath}`))
348
532
  }
349
533
 
534
+ function printExperimentalProxyNote() {
535
+ console.log(chalk.dim(` ${EXPERIMENTAL_PROXY_TOOLS_NOTE}`))
536
+ }
537
+
350
538
  export async function startExternalTool(mode, model, config) {
351
539
  const meta = getToolMeta(mode)
352
540
  const { env, apiKey, baseUrl } = buildToolEnv(mode, model, config)
@@ -429,6 +617,7 @@ export async function startExternalTool(mode, model, config) {
429
617
  console.log()
430
618
  console.log(chalk.yellow(' The proxy translates between provider protocols and handles key rotation,'))
431
619
  console.log(chalk.yellow(' which is required for this tool to connect.'))
620
+ console.log(chalk.dim(` ${EXPERIMENTAL_PROXY_TOOLS_NOTE}`))
432
621
  console.log()
433
622
  console.log(chalk.white(' To enable it:'))
434
623
  console.log(chalk.dim(' 1. Press ') + chalk.bold.white('J') + chalk.dim(' to open FCM Proxy V2 settings'))
@@ -441,33 +630,71 @@ export async function startExternalTool(mode, model, config) {
441
630
 
442
631
  if (mode === 'claude-code') {
443
632
  // 📖 Claude Code needs Anthropic-compatible wire format (POST /v1/messages).
444
- // 📖 The FCM proxy natively translates Anthropic OpenAI.
633
+ // 📖 Mirror free-claude-code: one auth env only (`ANTHROPIC_AUTH_TOKEN`) plus base URL.
445
634
  const started = await ensureProxyRunning(config)
635
+ const { env: proxyEnv } = buildToolEnv(mode, model, config, {
636
+ sanitize: true,
637
+ includeCompatDefaults: false,
638
+ includeProviderEnv: false,
639
+ })
446
640
  const proxyBase = `http://127.0.0.1:${started.port}`
447
- env.ANTHROPIC_BASE_URL = proxyBase
448
- env.ANTHROPIC_API_KEY = started.proxyToken
449
641
  const launchModelId = resolveLauncherModelId(model, true)
642
+ proxyEnv.ANTHROPIC_BASE_URL = proxyBase
643
+ proxyEnv.ANTHROPIC_AUTH_TOKEN = buildClaudeProxyAuthToken(started.proxyToken, launchModelId)
644
+ applyClaudeCodeModelOverrides(proxyEnv, launchModelId)
450
645
  console.log(chalk.dim(` 📖 Claude Code routed through FCM proxy on :${started.port} (Anthropic translation enabled)`))
451
- return spawnCommand('claude', ['--model', launchModelId], env)
646
+ return spawnCommand('claude', ['--model', launchModelId], proxyEnv)
452
647
  }
453
648
 
454
649
  if (mode === 'codex') {
455
650
  const started = await ensureProxyRunning(config)
456
- env.OPENAI_API_KEY = started.proxyToken
457
- env.OPENAI_BASE_URL = `http://127.0.0.1:${started.port}/v1`
651
+ const { env: proxyEnv } = buildToolEnv(mode, model, config, {
652
+ sanitize: true,
653
+ includeCompatDefaults: false,
654
+ includeProviderEnv: false,
655
+ })
458
656
  const launchModelId = resolveLauncherModelId(model, true)
657
+ const proxyBaseUrl = `http://127.0.0.1:${started.port}/v1`
658
+ proxyEnv.FCM_PROXY_API_KEY = started.proxyToken
459
659
  console.log(chalk.dim(` 📖 Codex routed through FCM proxy on :${started.port}`))
460
- return spawnCommand('codex', ['--model', launchModelId], env)
660
+ return spawnCommand('codex', [...buildCodexProxyArgs(proxyBaseUrl), '--model', launchModelId], proxyEnv)
461
661
  }
462
662
 
463
663
  if (mode === 'gemini') {
664
+ const geminiSupport = inspectGeminiCliSupport()
665
+ if (geminiSupport.configError) {
666
+ console.log()
667
+ console.log(chalk.red(' ✖ Gemini CLI configuration is invalid, so the proxy launch is blocked before auth.'))
668
+ console.log(chalk.dim(` ${geminiSupport.configError.split('\n').join('\n ')}`))
669
+ printExperimentalProxyNote()
670
+ console.log(chalk.dim(' Fix ~/.gemini/settings.json, then try again.'))
671
+ console.log()
672
+ return 1
673
+ }
674
+ if (!geminiSupport.supportsProxyBaseUrl) {
675
+ console.log()
676
+ const versionLabel = geminiSupport.version ? `v${geminiSupport.version}` : 'this installed version'
677
+ console.log(chalk.red(` ✖ Gemini CLI ${versionLabel} is not proxy-compatible in FCM yet.`))
678
+ console.log(chalk.yellow(' This build does not expose the custom base-URL contract we need, so launching it through the proxy would be misleading.'))
679
+ printExperimentalProxyNote()
680
+ console.log(chalk.dim(` Expected: Gemini CLI ${GEMINI_PROXY_MIN_VERSION}+ with stable proxy base URL support.`))
681
+ console.log()
682
+ return 1
683
+ }
684
+
464
685
  const started = await ensureProxyRunning(config)
465
- env.OPENAI_API_KEY = started.proxyToken
466
- env.OPENAI_BASE_URL = `http://127.0.0.1:${started.port}/v1`
467
686
  const launchModelId = resolveLauncherModelId(model, true)
687
+ const { env: proxyEnv } = buildToolEnv(mode, model, config, {
688
+ sanitize: true,
689
+ includeCompatDefaults: false,
690
+ includeProviderEnv: false,
691
+ })
692
+ proxyEnv.GEMINI_API_KEY = started.proxyToken
693
+ proxyEnv.GOOGLE_API_KEY = started.proxyToken
694
+ proxyEnv.GOOGLE_GEMINI_BASE_URL = `http://127.0.0.1:${started.port}/v1`
468
695
  printConfigResult(meta.label, writeGeminiConfig({ ...model, modelId: launchModelId }))
469
696
  console.log(chalk.dim(` 📖 Gemini routed through FCM proxy on :${started.port}`))
470
- return spawnCommand('gemini', ['--model', launchModelId], env)
697
+ return spawnCommand('gemini', ['--model', launchModelId], proxyEnv)
471
698
  }
472
699
 
473
700
  if (mode === 'qwen') {
package/src/utils.js CHANGED
@@ -386,16 +386,16 @@ export function findBestModel(results) {
386
386
  // 📖 Slices from index 2 to get user-provided arguments only.
387
387
  //
388
388
  // 📖 Argument types:
389
- // - API key: first positional arg that doesn't start with "--" (e.g., "nvapi-xxx")
389
+ // - API key: first positional arg that does not look like a CLI flag (e.g., "nvapi-xxx")
390
390
  // - Boolean flags: --best, --fiable, --opencode, --opencode-desktop, --openclaw,
391
391
  // --aider, --crush, --goose, --claude-code, --codex, --gemini, --qwen,
392
- // --openhands, --amp, --pi, --no-telemetry, --json (case-insensitive)
392
+ // --openhands, --amp, --pi, --no-telemetry, --json, --help/-h (case-insensitive)
393
393
  // - Value flag: --tier <letter> (the next non-flag arg is the tier value)
394
394
  //
395
395
  // 📖 Returns:
396
396
  // { apiKey, bestMode, fiableMode, openCodeMode, openCodeDesktopMode, openClawMode,
397
397
  // aiderMode, crushMode, gooseMode, claudeCodeMode, codexMode, geminiMode,
398
- // qwenMode, openHandsMode, ampMode, piMode, noTelemetry, jsonMode, tierFilter }
398
+ // qwenMode, openHandsMode, ampMode, piMode, noTelemetry, jsonMode, helpMode, tierFilter }
399
399
  //
400
400
  // 📖 Note: apiKey may be null here — the main CLI falls back to env vars and saved config.
401
401
  export function parseArgs(argv) {
@@ -420,7 +420,7 @@ export function parseArgs(argv) {
420
420
  if (profileValueIdx !== -1) skipIndices.add(profileValueIdx)
421
421
 
422
422
  for (const [i, arg] of args.entries()) {
423
- if (arg.startsWith('--')) {
423
+ if (arg.startsWith('--') || arg === '-h') {
424
424
  flags.push(arg.toLowerCase())
425
425
  } else if (skipIndices.has(i)) {
426
426
  // 📖 Skip — this is a value for --tier or --profile, not an API key
@@ -447,6 +447,7 @@ export function parseArgs(argv) {
447
447
  const noTelemetry = flags.includes('--no-telemetry')
448
448
  const cleanProxyMode = flags.includes('--clean-proxy') || flags.includes('--proxy-clean')
449
449
  const jsonMode = flags.includes('--json')
450
+ const helpMode = flags.includes('--help') || flags.includes('-h')
450
451
 
451
452
  let tierFilter = tierValueIdx !== -1 ? args[tierValueIdx].toUpperCase() : null
452
453
 
@@ -475,6 +476,7 @@ export function parseArgs(argv) {
475
476
  noTelemetry,
476
477
  cleanProxyMode,
477
478
  jsonMode,
479
+ helpMode,
478
480
  tierFilter,
479
481
  profileName,
480
482
  recommendMode
@@ -707,17 +709,31 @@ export function getProxyStatusInfo(proxyStartupStatus, isProxyActive, isProxyEna
707
709
  }
708
710
 
709
711
  /**
710
- * 📖 getVersionStatusInfo turns the settings update-check state into a compact,
712
+ * 📖 getVersionStatusInfo turns startup + manual update-check state into a compact,
711
713
  * 📖 render-friendly footer descriptor for the main table.
712
714
  *
713
- * 📖 Only an explicit `available` state should mark the local install as outdated.
714
- * 📖 This avoids showing a scary warning before the user has actually checked npm.
715
+ * 📖 Priority:
716
+ * 📖 1. Manual Settings check found an update (`available`)
717
+ * 📖 2. Startup auto-check already found a newer npm version
718
+ * 📖 3. Otherwise stay quiet
719
+ * 📖
720
+ * 📖 `versionAlertsEnabled` lets the CLI suppress npm-specific warnings in dev checkouts,
721
+ * 📖 where telling contributors to run a global npm update would be bogus.
715
722
  *
716
723
  * @param {'idle'|'checking'|'available'|'up-to-date'|'error'|'installing'} updateState
717
724
  * @param {string|null} latestVersion
725
+ * @param {string|null} [startupLatestVersion=null]
726
+ * @param {boolean} [versionAlertsEnabled=true]
718
727
  * @returns {{ isOutdated: boolean, latestVersion: string|null }}
719
728
  */
720
- export function getVersionStatusInfo(updateState, latestVersion) {
729
+ export function getVersionStatusInfo(updateState, latestVersion, startupLatestVersion = null, versionAlertsEnabled = true) {
730
+ if (!versionAlertsEnabled) {
731
+ return {
732
+ isOutdated: false,
733
+ latestVersion: null,
734
+ }
735
+ }
736
+
721
737
  if (updateState === 'available' && typeof latestVersion === 'string' && latestVersion.trim()) {
722
738
  return {
723
739
  isOutdated: true,
@@ -725,6 +741,13 @@ export function getVersionStatusInfo(updateState, latestVersion) {
725
741
  }
726
742
  }
727
743
 
744
+ if (typeof startupLatestVersion === 'string' && startupLatestVersion.trim()) {
745
+ return {
746
+ isOutdated: true,
747
+ latestVersion: startupLatestVersion.trim(),
748
+ }
749
+ }
750
+
728
751
  return {
729
752
  isOutdated: false,
730
753
  latestVersion: null,