@wyxos/zephyr 0.2.16 → 0.2.17

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/zephyr.mjs CHANGED
@@ -1,24 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { main } from '../src/index.mjs'
3
- import { checkAndUpdateVersion } from '../src/version-checker.mjs'
4
- import inquirer from 'inquirer'
2
+ import process from 'node:process'
3
+ import { logError, main } from '../src/index.mjs'
5
4
 
6
5
  // Parse --type flag from command line arguments
7
6
  const args = process.argv.slice(2)
8
7
  const typeFlag = args.find(arg => arg.startsWith('--type='))
9
8
  const releaseType = typeFlag ? typeFlag.split('=')[1] : null
10
9
 
11
- // Check for updates and re-execute if user confirms
12
- checkAndUpdateVersion((questions) => inquirer.prompt(questions), args)
13
- .then((reExecuted) => {
14
- if (reExecuted) {
15
- // Version was updated and script re-executed, exit this process
16
- process.exit(0)
17
- }
18
- // No update or user declined, continue with normal execution
19
- return main(releaseType)
20
- })
21
- .catch((error) => {
22
- console.error(error.message)
23
- process.exit(1)
24
- })
10
+ try {
11
+ await main(releaseType)
12
+ } catch (error) {
13
+ logError(error?.message || String(error))
14
+ process.exitCode = 1
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wyxos/zephyr",
3
- "version": "0.2.16",
3
+ "version": "0.2.17",
4
4
  "description": "A streamlined deployment tool for web applications with intelligent Laravel project detection",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -13,6 +13,7 @@
13
13
  },
14
14
  "scripts": {
15
15
  "test": "vitest run",
16
+ "lint": "eslint .",
16
17
  "release": "node bin/zephyr.mjs --type=node"
17
18
  },
18
19
  "keywords": [
@@ -48,6 +49,9 @@
48
49
  "semver": "^7.6.3"
49
50
  },
50
51
  "devDependencies": {
52
+ "@eslint/js": "^9.39.2",
53
+ "eslint": "^9.39.2",
54
+ "globals": "^17.0.0",
51
55
  "vitest": "^2.1.8"
52
56
  }
53
57
  }
@@ -2,6 +2,7 @@ import { readFile, writeFile } from 'node:fs/promises'
2
2
  import path from 'node:path'
3
3
  import { spawn } from 'node:child_process'
4
4
  import process from 'node:process'
5
+ import chalk from 'chalk'
5
6
 
6
7
  const IS_WINDOWS = process.platform === 'win32'
7
8
 
@@ -147,7 +148,7 @@ async function fetchLatestNpmVersion(packageName) {
147
148
  }
148
149
  const data = await response.json()
149
150
  return data.version || null
150
- } catch (error) {
151
+ } catch (_error) {
151
152
  return null
152
153
  }
153
154
  }
@@ -166,7 +167,7 @@ async function fetchLatestPackagistVersion(packageName) {
166
167
  return latest.version || null
167
168
  }
168
169
  return null
169
- } catch (error) {
170
+ } catch (_error) {
170
171
  return null
171
172
  }
172
173
  }
@@ -262,7 +263,7 @@ async function getGitStatus(rootDir) {
262
263
  try {
263
264
  const result = await runCommand('git', ['status', '--porcelain'], { capture: true, cwd: rootDir })
264
265
  return result.stdout || ''
265
- } catch (error) {
266
+ } catch (_error) {
266
267
  return ''
267
268
  }
268
269
  }
@@ -280,7 +281,7 @@ function hasStagedChanges(statusOutput) {
280
281
  })
281
282
  }
282
283
 
283
- async function commitDependencyUpdates(rootDir, updatedFiles, logFn) {
284
+ async function commitDependencyUpdates(rootDir, updatedFiles, promptFn, logFn) {
284
285
  try {
285
286
  // Check if we're in a git repository
286
287
  await runCommand('git', ['rev-parse', '--is-inside-work-tree'], { capture: true, cwd: rootDir })
@@ -289,7 +290,30 @@ async function commitDependencyUpdates(rootDir, updatedFiles, logFn) {
289
290
  return false
290
291
  }
291
292
 
292
- const status = await getGitStatus(rootDir)
293
+ const statusBefore = await getGitStatus(rootDir)
294
+
295
+ // Avoid accidentally committing unrelated staged changes
296
+ if (hasStagedChanges(statusBefore)) {
297
+ if (logFn) {
298
+ logFn('Staged changes detected. Skipping auto-commit of dependency updates.')
299
+ }
300
+ return false
301
+ }
302
+
303
+ const fileList = updatedFiles.map((f) => path.basename(f)).join(', ')
304
+
305
+ const { shouldCommit } = await promptFn([
306
+ {
307
+ type: 'confirm',
308
+ name: 'shouldCommit',
309
+ message: `Commit dependency updates now? (${fileList})`,
310
+ default: true
311
+ }
312
+ ])
313
+
314
+ if (!shouldCommit) {
315
+ return false
316
+ }
293
317
 
294
318
  // Stage the updated files
295
319
  for (const file of updatedFiles) {
@@ -306,7 +330,6 @@ async function commitDependencyUpdates(rootDir, updatedFiles, logFn) {
306
330
  }
307
331
 
308
332
  // Build commit message
309
- const fileList = updatedFiles.map(f => path.basename(f)).join(', ')
310
333
  const commitMessage = `chore: update local file dependencies to online versions (${fileList})`
311
334
 
312
335
  if (logFn) {
@@ -353,20 +376,27 @@ async function validateLocalDependencies(rootDir, promptFn, logFn = null) {
353
376
  })
354
377
  )
355
378
 
356
- // Build warning messages
379
+ // Build warning messages with colored output (danger color for package name and version)
357
380
  const messages = depsWithVersions.map((dep) => {
381
+ const packageNameColored = chalk.red(dep.packageName)
382
+ const pathColored = chalk.dim(dep.path)
358
383
  const versionInfo = dep.latestVersion
359
- ? ` Latest version available: ${dep.latestVersion}.`
384
+ ? ` Latest version available: ${chalk.red(dep.latestVersion)}.`
360
385
  : ' Latest version could not be determined.'
361
- return `Dependency '${dep.packageName}' is pointing to a local path outside the repository: ${dep.path}.${versionInfo}`
386
+ return `Dependency ${packageNameColored} is pointing to a local path outside the repository: ${pathColored}.${versionInfo}`
362
387
  })
363
388
 
389
+ // Build the prompt message with colored count (danger color)
390
+ const countColored = chalk.red(allDeps.length)
391
+ const countText = allDeps.length === 1 ? 'dependency' : 'dependencies'
392
+ const promptMessage = `Found ${countColored} local file ${countText} pointing outside the repository:\n\n${messages.join('\n\n')}\n\nUpdate to latest version?`
393
+
364
394
  // Prompt user
365
395
  const { shouldUpdate } = await promptFn([
366
396
  {
367
397
  type: 'confirm',
368
398
  name: 'shouldUpdate',
369
- message: `Found ${allDeps.length} local file dependency/dependencies pointing outside the repository:\n\n${messages.join('\n\n')}\n\nUpdate to latest version?`,
399
+ message: promptMessage,
370
400
  default: true
371
401
  }
372
402
  ])
@@ -441,7 +471,7 @@ async function validateLocalDependencies(rootDir, promptFn, logFn = null) {
441
471
 
442
472
  // Commit the changes if any files were updated
443
473
  if (updatedFiles.size > 0) {
444
- await commitDependencyUpdates(rootDir, Array.from(updatedFiles), logFn)
474
+ await commitDependencyUpdates(rootDir, Array.from(updatedFiles), promptFn, logFn)
445
475
  }
446
476
  }
447
477
 
package/src/index.mjs CHANGED
@@ -10,6 +10,7 @@ import { NodeSSH } from 'node-ssh'
10
10
  import { releaseNode } from './release-node.mjs'
11
11
  import { releasePackagist } from './release-packagist.mjs'
12
12
  import { validateLocalDependencies } from './dependency-scanner.mjs'
13
+ import { checkAndUpdateVersion } from './version-checker.mjs'
13
14
 
14
15
  const IS_WINDOWS = process.platform === 'win32'
15
16
 
@@ -89,7 +90,7 @@ async function cleanupOldLogs(rootDir) {
89
90
  for (const file of filesToDelete) {
90
91
  try {
91
92
  await fs.unlink(file.path)
92
- } catch (error) {
93
+ } catch (_error) {
93
94
  // Ignore errors when deleting old logs
94
95
  }
95
96
  }
@@ -414,7 +415,7 @@ async function ensureProjectReleaseScript(rootDir) {
414
415
  let packageJson
415
416
  try {
416
417
  packageJson = JSON.parse(raw)
417
- } catch (error) {
418
+ } catch (_error) {
418
419
  logWarning('Unable to parse package.json; skipping release script injection.')
419
420
  return false
420
421
  }
@@ -453,7 +454,7 @@ async function ensureProjectReleaseScript(rootDir) {
453
454
  try {
454
455
  await runCommand('git', ['rev-parse', '--is-inside-work-tree'], { cwd: rootDir, silent: true })
455
456
  isGitRepo = true
456
- } catch (error) {
457
+ } catch (_error) {
457
458
  logWarning('Not a git repository; skipping commit for release script addition.')
458
459
  }
459
460
 
@@ -541,7 +542,7 @@ async function readRemoteLock(ssh, remoteCwd) {
541
542
  if (checkResult.stdout && checkResult.stdout.trim() !== 'LOCK_NOT_FOUND' && checkResult.stdout.trim() !== '') {
542
543
  try {
543
544
  return JSON.parse(checkResult.stdout.trim())
544
- } catch (error) {
545
+ } catch (_error) {
545
546
  return { raw: checkResult.stdout.trim() }
546
547
  }
547
548
  }
@@ -605,7 +606,7 @@ async function acquireRemoteLock(ssh, remoteCwd, rootDir) {
605
606
  let details = {}
606
607
  try {
607
608
  details = JSON.parse(checkResult.stdout.trim())
608
- } catch (error) {
609
+ } catch (_error) {
609
610
  details = { raw: checkResult.stdout.trim() }
610
611
  }
611
612
 
@@ -620,7 +621,7 @@ async function acquireRemoteLock(ssh, remoteCwd, rootDir) {
620
621
  let details = {}
621
622
  try {
622
623
  details = JSON.parse(checkResult.stdout.trim())
623
- } catch (error) {
624
+ } catch (_error) {
624
625
  details = { raw: checkResult.stdout.trim() }
625
626
  }
626
627
 
@@ -727,7 +728,7 @@ async function ensureGitignoreEntry(rootDir) {
727
728
  cwd: rootDir
728
729
  })
729
730
  isGitRepo = true
730
- } catch (error) {
731
+ } catch (_error) {
731
732
  logWarning('Not a git repository; skipping commit for .gitignore update.')
732
733
  }
733
734
 
@@ -949,7 +950,7 @@ async function listGitBranches(currentDir) {
949
950
  .filter(Boolean)
950
951
 
951
952
  return branches.length ? branches : ['master']
952
- } catch (error) {
953
+ } catch (_error) {
953
954
  logWarning('Unable to read git branches; defaulting to master.')
954
955
  return ['master']
955
956
  }
@@ -1002,7 +1003,7 @@ async function isPrivateKeyFile(filePath) {
1002
1003
  try {
1003
1004
  const content = await fs.readFile(filePath, 'utf8')
1004
1005
  return /-----BEGIN [A-Z ]*PRIVATE KEY-----/.test(content)
1005
- } catch (error) {
1006
+ } catch (_error) {
1006
1007
  return false
1007
1008
  }
1008
1009
  }
@@ -1091,7 +1092,7 @@ async function resolveSshKeyPath(targetPath) {
1091
1092
 
1092
1093
  try {
1093
1094
  await fs.access(expanded)
1094
- } catch (error) {
1095
+ } catch (_error) {
1095
1096
  throw new Error(`SSH key not accessible at ${expanded}`)
1096
1097
  }
1097
1098
 
@@ -1247,14 +1248,14 @@ async function runRemoteTasks(config, options = {}) {
1247
1248
  }
1248
1249
 
1249
1250
  // Run tests for Laravel projects
1250
- if (isLaravel) {
1251
- logProcessing('Running Laravel tests locally...')
1252
- try {
1253
- await runCommand('php', ['artisan', 'test', '--compact'], { cwd: rootDir })
1254
- logSuccess('Local tests passed.')
1255
- } catch (error) {
1256
- throw new Error(`Local tests failed. Fix test failures before deploying. ${error.message}`)
1257
- }
1251
+ if (isLaravel) {
1252
+ logProcessing('Running Laravel tests locally...')
1253
+ try {
1254
+ await runCommand('php', ['artisan', 'test', '--compact'], { cwd: rootDir })
1255
+ logSuccess('Local tests passed.')
1256
+ } catch (error) {
1257
+ throw new Error(`Local tests failed. Fix test failures before deploying. ${error.message}`)
1258
+ }
1258
1259
  }
1259
1260
  } else {
1260
1261
  logProcessing('Pre-push git hook detected. Skipping local linting and test execution.')
@@ -1314,7 +1315,7 @@ async function runRemoteTasks(config, options = {}) {
1314
1315
  const escapeForDoubleQuotes = (value) => value.replace(/(["\\$`])/g, '\\$1')
1315
1316
 
1316
1317
  const executeRemote = async (label, command, options = {}) => {
1317
- const { cwd = remoteCwd, allowFailure = false, printStdout = true, bootstrapEnv = true } = options
1318
+ const { cwd = remoteCwd, allowFailure = false, bootstrapEnv = true } = options
1318
1319
  logProcessing(`\n→ ${label}`)
1319
1320
 
1320
1321
  let wrappedCommand = command
@@ -1583,7 +1584,7 @@ async function runRemoteTasks(config, options = {}) {
1583
1584
  const remoteHome = remoteHomeResult.stdout.trim() || `/home/${sshUser}`
1584
1585
  const remoteCwd = resolveRemotePath(config.projectPath, remoteHome)
1585
1586
  await compareLocksAndPrompt(rootDir, ssh, remoteCwd)
1586
- } catch (lockError) {
1587
+ } catch (_lockError) {
1587
1588
  // Ignore lock comparison errors during error handling
1588
1589
  }
1589
1590
  }
@@ -1739,9 +1740,9 @@ async function selectApp(projectConfig, server, currentDir) {
1739
1740
  if (apps.length > 0) {
1740
1741
  const availableServers = [...new Set(apps.map((app) => app.serverName).filter(Boolean))]
1741
1742
  if (availableServers.length > 0) {
1742
- logWarning(
1743
- `No applications configured for server "${server.serverName}". Available servers: ${availableServers.join(', ')}`
1744
- )
1743
+ logWarning(
1744
+ `No applications configured for server "${server.serverName}". Available servers: ${availableServers.join(', ')}`
1745
+ )
1745
1746
  }
1746
1747
  }
1747
1748
  logProcessing(`No applications configured for ${server.serverName}. Let's create one.`)
@@ -1758,7 +1759,7 @@ async function selectApp(projectConfig, server, currentDir) {
1758
1759
  return appConfig
1759
1760
  }
1760
1761
 
1761
- const choices = matches.map(({ app, index }, matchIndex) => ({
1762
+ const choices = matches.map(({ app }, matchIndex) => ({
1762
1763
  name: `${app.projectPath} (${app.branch})`,
1763
1764
  value: matchIndex
1764
1765
  }))
@@ -1796,23 +1797,6 @@ async function selectApp(projectConfig, server, currentDir) {
1796
1797
  return chosen
1797
1798
  }
1798
1799
 
1799
- async function promptPresetName() {
1800
- const { presetName } = await runPrompt([
1801
- {
1802
- type: 'input',
1803
- name: 'presetName',
1804
- message: 'Enter a name for this preset',
1805
- validate: (value) => (value && value.trim().length > 0 ? true : 'Preset name cannot be empty.')
1806
- }
1807
- ])
1808
-
1809
- return presetName.trim()
1810
- }
1811
-
1812
- function generatePresetKey(serverName, projectPath) {
1813
- return `${serverName}:${projectPath}`
1814
- }
1815
-
1816
1800
  async function selectPreset(projectConfig, servers) {
1817
1801
  const presets = projectConfig.presets ?? []
1818
1802
  const apps = projectConfig.apps ?? []
@@ -1844,7 +1828,7 @@ async function selectPreset(projectConfig, servers) {
1844
1828
 
1845
1829
  return {
1846
1830
  name: displayName,
1847
- value: index
1831
+ value: index
1848
1832
  }
1849
1833
  })
1850
1834
 
@@ -1871,6 +1855,24 @@ async function selectPreset(projectConfig, servers) {
1871
1855
  }
1872
1856
 
1873
1857
  async function main(releaseType = null) {
1858
+ // Best-effort update check (skip during tests or when explicitly disabled)
1859
+ // If an update is accepted, the process will re-execute via npx @latest and we should exit early.
1860
+ if (
1861
+ process.env.ZEPHYR_SKIP_VERSION_CHECK !== '1' &&
1862
+ process.env.NODE_ENV !== 'test' &&
1863
+ process.env.VITEST !== 'true'
1864
+ ) {
1865
+ try {
1866
+ const args = process.argv.slice(2)
1867
+ const reExecuted = await checkAndUpdateVersion(runPrompt, args)
1868
+ if (reExecuted) {
1869
+ return
1870
+ }
1871
+ } catch (_error) {
1872
+ // Never block execution due to update check issues
1873
+ }
1874
+ }
1875
+
1874
1876
  // Handle node/vue package release
1875
1877
  if (releaseType === 'node' || releaseType === 'vue') {
1876
1878
  try {
@@ -2041,11 +2043,11 @@ async function main(releaseType = null) {
2041
2043
  } else {
2042
2044
  // Check if preset with this appId already exists
2043
2045
  const existingIndex = presets.findIndex((p) => p.appId === appId)
2044
- if (existingIndex >= 0) {
2046
+ if (existingIndex >= 0) {
2045
2047
  presets[existingIndex].name = trimmedName
2046
2048
  presets[existingIndex].branch = deploymentConfig.branch
2047
- } else {
2048
- presets.push({
2049
+ } else {
2050
+ presets.push({
2049
2051
  name: trimmedName,
2050
2052
  appId: appId,
2051
2053
  branch: deploymentConfig.branch
@@ -1,6 +1,5 @@
1
1
  import { spawn, exec } from 'node:child_process'
2
- import { fileURLToPath } from 'node:url'
3
- import { dirname, join } from 'node:path'
2
+ import { join } from 'node:path'
4
3
  import { readFile } from 'node:fs/promises'
5
4
  import fs from 'node:fs'
6
5
  import path from 'node:path'
@@ -489,7 +488,7 @@ function extractDomainFromHomepage(homepage) {
489
488
  return url.hostname
490
489
  } catch {
491
490
  // If it's not a valid URL, try to extract domain from string
492
- const match = homepage.match(/(?:https?:\/\/)?([^\/]+)/)
491
+ const match = homepage.match(/(?:https?:\/\/)?([^/]+)/)
493
492
  return match ? match[1] : null
494
493
  }
495
494
  }
@@ -537,7 +536,9 @@ async function deployGHPages(skipDeploy, pkg, rootDir = process.cwd()) {
537
536
  try {
538
537
  try {
539
538
  await runCommand('git', ['worktree', 'remove', worktreeDir, '-f'], { capture: true, cwd: rootDir })
540
- } catch { }
539
+ } catch (_error) {
540
+ // Ignore if worktree doesn't exist
541
+ }
541
542
 
542
543
  try {
543
544
  await runCommand('git', ['worktree', 'add', worktreeDir, 'gh-pages'], { capture: true, cwd: rootDir })
@@ -1,9 +1,7 @@
1
1
  import { spawn } from 'node:child_process'
2
- import { fileURLToPath } from 'node:url'
3
- import { dirname, join } from 'node:path'
2
+ import { join } from 'node:path'
4
3
  import { readFile, writeFile } from 'node:fs/promises'
5
4
  import fs from 'node:fs'
6
- import path from 'node:path'
7
5
  import process from 'node:process'
8
6
  import semver from 'semver'
9
7
  import inquirer from 'inquirer'
package/src/ssh-utils.mjs CHANGED
@@ -25,7 +25,7 @@ async function resolveSshKeyPath(targetPath) {
25
25
  const expanded = expandHomePath(targetPath)
26
26
  try {
27
27
  await fs.access(expanded)
28
- } catch (error) {
28
+ } catch (_error) {
29
29
  throw new Error(`SSH key not accessible at ${expanded}`)
30
30
  }
31
31
  return expanded
@@ -64,7 +64,7 @@ const createSshClient = () => {
64
64
  * @param {string} rootDir - Local root directory for logging
65
65
  * @returns {Promise<{ssh: NodeSSH, remoteCwd: string, remoteHome: string}>}
66
66
  */
67
- export async function connectToServer(config, rootDir) {
67
+ export async function connectToServer(config, _rootDir) {
68
68
  const ssh = createSshClient()
69
69
  const sshUser = config.sshUser || os.userInfo().username
70
70
  const privateKeyPath = await resolveSshKeyPath(config.sshKey)
@@ -96,7 +96,7 @@ export async function connectToServer(config, rootDir) {
96
96
  * @returns {Promise<Object>} Command result
97
97
  */
98
98
  export async function executeRemoteCommand(ssh, label, command, options = {}) {
99
- const { cwd, allowFailure = false, printStdout = true, bootstrapEnv = true, rootDir = null, writeToLogFile = null, env = {} } = options
99
+ const { cwd, allowFailure = false, bootstrapEnv = true, rootDir = null, writeToLogFile = null, env = {} } = options
100
100
 
101
101
  logProcessing(`\n→ ${label}`)
102
102
 
@@ -3,9 +3,11 @@ import { fileURLToPath } from 'node:url'
3
3
  import path from 'node:path'
4
4
  import { spawn } from 'node:child_process'
5
5
  import process from 'node:process'
6
+ import https from 'node:https'
6
7
  import semver from 'semver'
7
8
 
8
9
  const IS_WINDOWS = process.platform === 'win32'
10
+ const ZEPHYR_SKIP_VERSION_CHECK_ENV = 'ZEPHYR_SKIP_VERSION_CHECK'
9
11
 
10
12
  async function getCurrentVersion() {
11
13
  try {
@@ -18,21 +20,57 @@ async function getCurrentVersion() {
18
20
  )
19
21
  const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'))
20
22
  return packageJson.version
21
- } catch (error) {
23
+ } catch (_error) {
22
24
  // If we can't read package.json, return null
23
25
  return null
24
26
  }
25
27
  }
26
28
 
29
+ function httpsGetJson(url) {
30
+ return new Promise((resolve, reject) => {
31
+ const request = https.get(
32
+ url,
33
+ {
34
+ headers: {
35
+ accept: 'application/json'
36
+ }
37
+ },
38
+ (response) => {
39
+ const { statusCode } = response
40
+ if (!statusCode || statusCode < 200 || statusCode >= 300) {
41
+ response.resume()
42
+ resolve(null)
43
+ return
44
+ }
45
+
46
+ response.setEncoding('utf8')
47
+ let raw = ''
48
+ response.on('data', (chunk) => {
49
+ raw += chunk
50
+ })
51
+ response.on('end', () => {
52
+ try {
53
+ resolve(JSON.parse(raw))
54
+ } catch (error) {
55
+ reject(error)
56
+ }
57
+ })
58
+ }
59
+ )
60
+
61
+ request.on('error', reject)
62
+ request.end()
63
+ })
64
+ }
65
+
27
66
  async function getLatestVersion() {
28
67
  try {
29
- const response = await fetch('https://registry.npmjs.org/@wyxos/zephyr/latest')
30
- if (!response.ok) {
68
+ const data = await httpsGetJson('https://registry.npmjs.org/@wyxos/zephyr/latest')
69
+ if (!data) {
31
70
  return null
32
71
  }
33
- const data = await response.json()
34
72
  return data.version || null
35
- } catch (error) {
73
+ } catch (_error) {
36
74
  return null
37
75
  }
38
76
  }
@@ -45,7 +83,7 @@ function isNewerVersionAvailable(current, latest) {
45
83
  // Use semver to properly compare versions
46
84
  try {
47
85
  return semver.gt(latest, current)
48
- } catch (error) {
86
+ } catch (_error) {
49
87
  // If semver comparison fails, fall back to simple string comparison
50
88
  return latest !== current
51
89
  }
@@ -59,7 +97,11 @@ async function reExecuteWithLatest(args) {
59
97
  return new Promise((resolve, reject) => {
60
98
  const child = spawn(command, npxArgs, {
61
99
  stdio: 'inherit',
62
- shell: IS_WINDOWS
100
+ shell: IS_WINDOWS,
101
+ env: {
102
+ ...process.env,
103
+ [ZEPHYR_SKIP_VERSION_CHECK_ENV]: '1'
104
+ }
63
105
  })
64
106
 
65
107
  child.on('error', reject)
@@ -75,12 +117,7 @@ async function reExecuteWithLatest(args) {
75
117
 
76
118
  export async function checkAndUpdateVersion(promptFn, args) {
77
119
  try {
78
- // Skip check if already running @latest (detected via environment or process)
79
- // When npx runs @latest, the version should already be latest
80
- const isRunningLatest = process.env.npm_config_user_config?.includes('@latest') ||
81
- process.argv.some(arg => arg.includes('@latest'))
82
-
83
- if (isRunningLatest) {
120
+ if (process.env[ZEPHYR_SKIP_VERSION_CHECK_ENV] === '1') {
84
121
  return false
85
122
  }
86
123
 
@@ -118,7 +155,7 @@ export async function checkAndUpdateVersion(promptFn, args) {
118
155
  // User confirmed, re-execute with latest version
119
156
  await reExecuteWithLatest(args)
120
157
  return true // Indicates we've re-executed, so the current process should exit
121
- } catch (error) {
158
+ } catch (_error) {
122
159
  // If version check fails, just continue with current version
123
160
  // Don't block the user from using the tool
124
161
  return false