@wyxos/zephyr 0.2.15 → 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.15",
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
  }
@@ -1,5 +1,10 @@
1
1
  import { readFile, writeFile } from 'node:fs/promises'
2
2
  import path from 'node:path'
3
+ import { spawn } from 'node:child_process'
4
+ import process from 'node:process'
5
+ import chalk from 'chalk'
6
+
7
+ const IS_WINDOWS = process.platform === 'win32'
3
8
 
4
9
  function isLocalPathOutsideRepo(depPath, rootDir) {
5
10
  if (!depPath || typeof depPath !== 'string') {
@@ -143,7 +148,7 @@ async function fetchLatestNpmVersion(packageName) {
143
148
  }
144
149
  const data = await response.json()
145
150
  return data.version || null
146
- } catch (error) {
151
+ } catch (_error) {
147
152
  return null
148
153
  }
149
154
  }
@@ -162,7 +167,7 @@ async function fetchLatestPackagistVersion(packageName) {
162
167
  return latest.version || null
163
168
  }
164
169
  return null
165
- } catch (error) {
170
+ } catch (_error) {
166
171
  return null
167
172
  }
168
173
  }
@@ -212,7 +217,135 @@ async function updateComposerJsonDependency(rootDir, packageName, newVersion, fi
212
217
  await writeFile(composerJsonPath, updatedContent, 'utf8')
213
218
  }
214
219
 
215
- async function validateLocalDependencies(rootDir, promptFn) {
220
+ async function runCommand(command, args, { cwd = process.cwd(), capture = false } = {}) {
221
+ return new Promise((resolve, reject) => {
222
+ const spawnOptions = {
223
+ stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
224
+ cwd
225
+ }
226
+
227
+ if (IS_WINDOWS && command !== 'git') {
228
+ spawnOptions.shell = true
229
+ }
230
+
231
+ const child = spawn(command, args, spawnOptions)
232
+ let stdout = ''
233
+ let stderr = ''
234
+
235
+ if (capture) {
236
+ child.stdout.on('data', (chunk) => {
237
+ stdout += chunk
238
+ })
239
+
240
+ child.stderr.on('data', (chunk) => {
241
+ stderr += chunk
242
+ })
243
+ }
244
+
245
+ child.on('error', reject)
246
+ child.on('close', (code) => {
247
+ if (code === 0) {
248
+ resolve(capture ? { stdout: stdout.trim(), stderr: stderr.trim() } : undefined)
249
+ } else {
250
+ const error = new Error(`Command failed (${code}): ${command} ${args.join(' ')}`)
251
+ if (capture) {
252
+ error.stdout = stdout
253
+ error.stderr = stderr
254
+ }
255
+ error.exitCode = code
256
+ reject(error)
257
+ }
258
+ })
259
+ })
260
+ }
261
+
262
+ async function getGitStatus(rootDir) {
263
+ try {
264
+ const result = await runCommand('git', ['status', '--porcelain'], { capture: true, cwd: rootDir })
265
+ return result.stdout || ''
266
+ } catch (_error) {
267
+ return ''
268
+ }
269
+ }
270
+
271
+ function hasStagedChanges(statusOutput) {
272
+ if (!statusOutput || statusOutput.length === 0) {
273
+ return false
274
+ }
275
+
276
+ const lines = statusOutput.split('\n').filter((line) => line.trim().length > 0)
277
+
278
+ return lines.some((line) => {
279
+ const firstChar = line[0]
280
+ return firstChar && firstChar !== ' ' && firstChar !== '?'
281
+ })
282
+ }
283
+
284
+ async function commitDependencyUpdates(rootDir, updatedFiles, promptFn, logFn) {
285
+ try {
286
+ // Check if we're in a git repository
287
+ await runCommand('git', ['rev-parse', '--is-inside-work-tree'], { capture: true, cwd: rootDir })
288
+ } catch {
289
+ // Not a git repository, skip commit
290
+ return false
291
+ }
292
+
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
+ }
317
+
318
+ // Stage the updated files
319
+ for (const file of updatedFiles) {
320
+ try {
321
+ await runCommand('git', ['add', file], { cwd: rootDir })
322
+ } catch {
323
+ // File might not exist or not be tracked, continue
324
+ }
325
+ }
326
+
327
+ const newStatus = await getGitStatus(rootDir)
328
+ if (!hasStagedChanges(newStatus)) {
329
+ return false
330
+ }
331
+
332
+ // Build commit message
333
+ const commitMessage = `chore: update local file dependencies to online versions (${fileList})`
334
+
335
+ if (logFn) {
336
+ logFn('Committing dependency updates...')
337
+ }
338
+
339
+ await runCommand('git', ['commit', '-m', commitMessage], { cwd: rootDir })
340
+
341
+ if (logFn) {
342
+ logFn('Dependency updates committed.')
343
+ }
344
+
345
+ return true
346
+ }
347
+
348
+ async function validateLocalDependencies(rootDir, promptFn, logFn = null) {
216
349
  const packageDeps = await scanPackageJsonDependencies(rootDir)
217
350
  const composerDeps = await scanComposerJsonDependencies(rootDir)
218
351
 
@@ -243,20 +376,27 @@ async function validateLocalDependencies(rootDir, promptFn) {
243
376
  })
244
377
  )
245
378
 
246
- // Build warning messages
379
+ // Build warning messages with colored output (danger color for package name and version)
247
380
  const messages = depsWithVersions.map((dep) => {
381
+ const packageNameColored = chalk.red(dep.packageName)
382
+ const pathColored = chalk.dim(dep.path)
248
383
  const versionInfo = dep.latestVersion
249
- ? ` Latest version available: ${dep.latestVersion}.`
384
+ ? ` Latest version available: ${chalk.red(dep.latestVersion)}.`
250
385
  : ' Latest version could not be determined.'
251
- 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}`
252
387
  })
253
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
+
254
394
  // Prompt user
255
395
  const { shouldUpdate } = await promptFn([
256
396
  {
257
397
  type: 'confirm',
258
398
  name: 'shouldUpdate',
259
- 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,
260
400
  default: true
261
401
  }
262
402
  ])
@@ -265,6 +405,9 @@ async function validateLocalDependencies(rootDir, promptFn) {
265
405
  throw new Error('Release cancelled: local file dependencies must be updated before release.')
266
406
  }
267
407
 
408
+ // Track which files were updated
409
+ const updatedFiles = new Set()
410
+
268
411
  // Update dependencies
269
412
  for (const dep of depsWithVersions) {
270
413
  if (!dep.latestVersion) {
@@ -273,8 +416,10 @@ async function validateLocalDependencies(rootDir, promptFn) {
273
416
 
274
417
  if (dep.field === 'dependencies' || dep.field === 'devDependencies') {
275
418
  await updatePackageJsonDependency(rootDir, dep.packageName, dep.latestVersion, dep.field)
419
+ updatedFiles.add('package.json')
276
420
  } else if (dep.field === 'require' || dep.field === 'require-dev') {
277
421
  await updateComposerJsonDependency(rootDir, dep.packageName, dep.latestVersion, dep.field)
422
+ updatedFiles.add('composer.json')
278
423
  } else if (dep.field === 'repositories') {
279
424
  // For repositories, we need to remove the repository entry
280
425
  // But we still need to update the dependency version
@@ -319,9 +464,15 @@ async function validateLocalDependencies(rootDir, promptFn) {
319
464
  const updatedContent = JSON.stringify(updatedComposer, null, 2) + '\n'
320
465
  await writeFile(composerJsonPath, updatedContent, 'utf8')
321
466
  }
467
+ updatedFiles.add('composer.json')
322
468
  }
323
469
  }
324
470
  }
471
+
472
+ // Commit the changes if any files were updated
473
+ if (updatedFiles.size > 0) {
474
+ await commitDependencyUpdates(rootDir, Array.from(updatedFiles), promptFn, logFn)
475
+ }
325
476
  }
326
477
 
327
478
  export {
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 {
@@ -1915,7 +1917,7 @@ async function main(releaseType = null) {
1915
1917
 
1916
1918
  if (hasPackageJson || hasComposerJson) {
1917
1919
  logProcessing('Validating dependencies...')
1918
- await validateLocalDependencies(rootDir, runPrompt)
1920
+ await validateLocalDependencies(rootDir, runPrompt, logSuccess)
1919
1921
  }
1920
1922
 
1921
1923
  // Load servers first (they may be migrated)
@@ -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 })
@@ -583,7 +584,7 @@ export async function releaseNode() {
583
584
  const pkg = await readPackage(rootDir)
584
585
 
585
586
  logStep('Validating dependencies...')
586
- await validateLocalDependencies(rootDir, (questions) => inquirer.prompt(questions))
587
+ await validateLocalDependencies(rootDir, (questions) => inquirer.prompt(questions), logSuccess)
587
588
 
588
589
  logStep('Checking working tree status...')
589
590
  await ensureCleanWorkingTree(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'
@@ -387,7 +385,7 @@ export async function releasePackagist() {
387
385
  }
388
386
 
389
387
  logStep('Validating dependencies...')
390
- await validateLocalDependencies(rootDir, (questions) => inquirer.prompt(questions))
388
+ await validateLocalDependencies(rootDir, (questions) => inquirer.prompt(questions), logSuccess)
391
389
 
392
390
  logStep('Checking working tree status...')
393
391
  await ensureCleanWorkingTree(rootDir)
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