@wyxos/zephyr 0.2.5 → 0.2.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wyxos/zephyr",
3
- "version": "0.2.5",
3
+ "version": "0.2.8",
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",
@@ -5,24 +5,14 @@ import { readFile } from 'node:fs/promises'
5
5
  import fs from 'node:fs'
6
6
  import path from 'node:path'
7
7
  import process from 'node:process'
8
-
9
- const STEP_PREFIX = '→'
10
- const OK_PREFIX = '✔'
11
- const WARN_PREFIX = '⚠'
8
+ import chalk from 'chalk'
12
9
 
13
10
  const IS_WINDOWS = process.platform === 'win32'
14
11
 
15
- function logStep(message) {
16
- console.log(`${STEP_PREFIX} ${message}`)
17
- }
18
-
19
- function logSuccess(message) {
20
- console.log(`${OK_PREFIX} ${message}`)
21
- }
22
-
23
- function logWarning(message) {
24
- console.warn(`${WARN_PREFIX} ${message}`)
25
- }
12
+ const logProcessing = (message = '') => console.log(chalk.yellow(message))
13
+ const logSuccess = (message = '') => console.log(chalk.green(message))
14
+ const logWarning = (message = '') => console.warn(chalk.yellow(message))
15
+ const logError = (message = '') => console.error(chalk.red(message))
26
16
 
27
17
  function runCommand(command, args, { cwd = process.cwd(), capture = false, useShell = false } = {}) {
28
18
  return new Promise((resolve, reject) => {
@@ -114,7 +104,7 @@ async function ensureUpToDateWithUpstream(branch, upstreamRef, rootDir = process
114
104
  const remoteBranch = branchParts.join('/')
115
105
 
116
106
  if (remoteName && remoteBranch) {
117
- logStep(`Fetching latest updates from ${remoteName}/${remoteBranch}...`)
107
+ logProcessing(`Fetching latest updates from ${remoteName}/${remoteBranch}...`)
118
108
  try {
119
109
  await runCommand('git', ['fetch', remoteName, remoteBranch], { cwd: rootDir })
120
110
  } catch (error) {
@@ -136,7 +126,7 @@ async function ensureUpToDateWithUpstream(branch, upstreamRef, rootDir = process
136
126
 
137
127
  if (Number.isFinite(behind) && behind > 0) {
138
128
  if (remoteName && remoteBranch) {
139
- logStep(`Fast-forwarding ${branch} with ${upstreamRef}...`)
129
+ logProcessing(`Fast-forwarding ${branch} with ${upstreamRef}...`)
140
130
 
141
131
  try {
142
132
  await runCommand('git', ['pull', '--ff-only', remoteName, remoteBranch], { cwd: rootDir })
@@ -198,11 +188,11 @@ async function runLint(skipLint, pkg, rootDir = process.cwd()) {
198
188
  }
199
189
 
200
190
  if (!hasScript(pkg, 'lint')) {
201
- logStep('Skipping lint (no lint script found in package.json).')
191
+ logProcessing('Skipping lint (no lint script found in package.json).')
202
192
  return
203
193
  }
204
194
 
205
- logStep('Running lint...')
195
+ logProcessing('Running lint...')
206
196
  await runCommand('npm', ['run', 'lint'], { cwd: rootDir })
207
197
  logSuccess('Lint passed.')
208
198
  }
@@ -215,21 +205,49 @@ async function runTests(skipTests, pkg, rootDir = process.cwd()) {
215
205
 
216
206
  // Check for test:run or test script
217
207
  if (!hasScript(pkg, 'test:run') && !hasScript(pkg, 'test')) {
218
- logStep('Skipping tests (no test or test:run script found in package.json).')
208
+ logProcessing('Skipping tests (no test or test:run script found in package.json).')
219
209
  return
220
210
  }
221
211
 
222
- logStep('Running test suite...')
212
+ logProcessing('Running test suite...')
223
213
 
224
- // Prefer test:run if available, otherwise use test with --run flag
225
- if (hasScript(pkg, 'test:run')) {
226
- await runCommand('npm', ['run', 'test:run'], { cwd: rootDir })
227
- } else {
228
- // For test script, try to pass --run flag (works with vitest)
229
- await runCommand('npm', ['test', '--', '--run'], { cwd: rootDir })
230
- }
214
+ let dotInterval = null
215
+ try {
216
+ // Capture output and show dots as progress
217
+ process.stdout.write(' ')
218
+ dotInterval = setInterval(() => {
219
+ process.stdout.write('.')
220
+ }, 200)
221
+
222
+ // Prefer test:run if available, otherwise use test with --run flag
223
+ if (hasScript(pkg, 'test:run')) {
224
+ await runCommand('npm', ['run', 'test:run'], { capture: true, cwd: rootDir })
225
+ } else {
226
+ // For test script, try to pass --run flag (works with vitest)
227
+ await runCommand('npm', ['test', '--', '--run'], { capture: true, cwd: rootDir })
228
+ }
231
229
 
232
- logSuccess('Tests passed.')
230
+ if (dotInterval) {
231
+ clearInterval(dotInterval)
232
+ dotInterval = null
233
+ }
234
+ process.stdout.write('\n')
235
+ logSuccess('Tests passed.')
236
+ } catch (error) {
237
+ // Clear dots and show error output
238
+ if (dotInterval) {
239
+ clearInterval(dotInterval)
240
+ dotInterval = null
241
+ }
242
+ process.stdout.write('\n')
243
+ if (error.stdout) {
244
+ logError(error.stdout)
245
+ }
246
+ if (error.stderr) {
247
+ logError(error.stderr)
248
+ }
249
+ throw error
250
+ }
233
251
  }
234
252
 
235
253
  async function runBuild(skipBuild, pkg, rootDir = process.cwd()) {
@@ -239,11 +257,11 @@ async function runBuild(skipBuild, pkg, rootDir = process.cwd()) {
239
257
  }
240
258
 
241
259
  if (!hasScript(pkg, 'build')) {
242
- logStep('Skipping build (no build script found in package.json).')
260
+ logProcessing('Skipping build (no build script found in package.json).')
243
261
  return
244
262
  }
245
263
 
246
- logStep('Building project...')
264
+ logProcessing('Building project...')
247
265
  await runCommand('npm', ['run', 'build'], { cwd: rootDir })
248
266
  logSuccess('Build completed.')
249
267
  }
@@ -255,11 +273,11 @@ async function runLibBuild(skipBuild, pkg, rootDir = process.cwd()) {
255
273
  }
256
274
 
257
275
  if (!hasScript(pkg, 'build:lib')) {
258
- logStep('Skipping library build (no build:lib script found in package.json).')
276
+ logProcessing('Skipping library build (no build:lib script found in package.json).')
259
277
  return false
260
278
  }
261
279
 
262
- logStep('Building library...')
280
+ logProcessing('Building library...')
263
281
  await runCommand('npm', ['run', 'build:lib'], { cwd: rootDir })
264
282
  logSuccess('Library built.')
265
283
 
@@ -271,7 +289,7 @@ async function runLibBuild(skipBuild, pkg, rootDir = process.cwd()) {
271
289
  })
272
290
 
273
291
  if (hasLibChanges) {
274
- logStep('Committing lib build artifacts...')
292
+ logProcessing('Committing lib build artifacts...')
275
293
  await runCommand('git', ['add', 'lib/'], { cwd: rootDir })
276
294
  await runCommand('git', ['commit', '-m', 'chore: build lib artifacts'], { cwd: rootDir })
277
295
  logSuccess('Lib build artifacts committed.')
@@ -281,13 +299,13 @@ async function runLibBuild(skipBuild, pkg, rootDir = process.cwd()) {
281
299
  }
282
300
 
283
301
  async function ensureNpmAuth(rootDir = process.cwd()) {
284
- logStep('Confirming npm authentication...')
302
+ logProcessing('Confirming npm authentication...')
285
303
  await runCommand('npm', ['whoami'], { cwd: rootDir })
286
304
  logSuccess('npm authenticated.')
287
305
  }
288
306
 
289
307
  async function bumpVersion(releaseType, rootDir = process.cwd()) {
290
- logStep(`Bumping package version...`)
308
+ logProcessing(`Bumping package version...`)
291
309
 
292
310
  // Lib changes should already be committed by runLibBuild, but check anyway
293
311
  const { stdout: statusBefore } = await runCommand('git', ['status', '--porcelain'], { capture: true, cwd: rootDir })
@@ -297,7 +315,7 @@ async function bumpVersion(releaseType, rootDir = process.cwd()) {
297
315
  })
298
316
 
299
317
  if (hasLibChanges) {
300
- logStep('Stashing lib build artifacts...')
318
+ logProcessing('Stashing lib build artifacts...')
301
319
  await runCommand('git', ['stash', 'push', '-u', '-m', 'temp: lib build artifacts', 'lib/'], { cwd: rootDir })
302
320
  }
303
321
 
@@ -307,7 +325,7 @@ async function bumpVersion(releaseType, rootDir = process.cwd()) {
307
325
  } finally {
308
326
  // Restore lib changes and ensure they're in the commit
309
327
  if (hasLibChanges) {
310
- logStep('Restoring lib build artifacts...')
328
+ logProcessing('Restoring lib build artifacts...')
311
329
  await runCommand('git', ['stash', 'pop'], { cwd: rootDir })
312
330
  await runCommand('git', ['add', 'lib/'], { cwd: rootDir })
313
331
  const { stdout: statusAfter } = await runCommand('git', ['status', '--porcelain'], { capture: true, cwd: rootDir })
@@ -328,7 +346,7 @@ async function bumpVersion(releaseType, rootDir = process.cwd()) {
328
346
  }
329
347
 
330
348
  async function pushChanges(rootDir = process.cwd()) {
331
- logStep('Pushing commits and tags to origin...')
349
+ logProcessing('Pushing commits and tags to origin...')
332
350
  await runCommand('git', ['push', '--follow-tags'], { cwd: rootDir })
333
351
  logSuccess('Git push completed.')
334
352
  }
@@ -337,10 +355,13 @@ async function publishPackage(pkg, rootDir = process.cwd()) {
337
355
  const publishArgs = ['publish', '--ignore-scripts'] // Skip prepublishOnly since we already built lib
338
356
 
339
357
  if (pkg.name.startsWith('@')) {
340
- publishArgs.push('--access', 'public')
358
+ // For scoped packages, determine access level from publishConfig
359
+ // Default to 'public' for scoped packages if not specified (free npm accounts require public for scoped packages)
360
+ const access = pkg.publishConfig?.access || 'public'
361
+ publishArgs.push('--access', access)
341
362
  }
342
363
 
343
- logStep(`Publishing ${pkg.name}@${pkg.version} to npm...`)
364
+ logProcessing(`Publishing ${pkg.name}@${pkg.version} to npm...`)
344
365
  await runCommand('npm', publishArgs, { cwd: rootDir })
345
366
  logSuccess('npm publish completed.')
346
367
  }
@@ -374,11 +395,11 @@ async function deployGHPages(skipDeploy, pkg, rootDir = process.cwd()) {
374
395
  }
375
396
 
376
397
  if (!distExists) {
377
- logStep('Skipping GitHub Pages deployment (no dist directory found).')
398
+ logProcessing('Skipping GitHub Pages deployment (no dist directory found).')
378
399
  return
379
400
  }
380
401
 
381
- logStep('Deploying to GitHub Pages...')
402
+ logProcessing('Deploying to GitHub Pages...')
382
403
 
383
404
  // Write CNAME file to dist if homepage is set
384
405
  const cnamePath = path.join(distPath, 'CNAME')
@@ -428,35 +449,41 @@ async function deployGHPages(skipDeploy, pkg, rootDir = process.cwd()) {
428
449
  }
429
450
 
430
451
  export async function releaseNode() {
431
- const { releaseType, skipTests, skipLint, skipBuild, skipDeploy } = parseArgs()
432
- const rootDir = process.cwd()
433
-
434
- logStep('Reading package metadata...')
435
- const pkg = await readPackage(rootDir)
436
-
437
- logStep('Checking working tree status...')
438
- await ensureCleanWorkingTree(rootDir)
439
-
440
- const branch = await getCurrentBranch(rootDir)
441
- if (!branch) {
442
- throw new Error('Unable to determine current branch.')
443
- }
452
+ try {
453
+ const { releaseType, skipTests, skipLint, skipBuild, skipDeploy } = parseArgs()
454
+ const rootDir = process.cwd()
444
455
 
445
- logStep(`Current branch: ${branch}`)
446
- const upstreamRef = await getUpstreamRef(rootDir)
447
- await ensureUpToDateWithUpstream(branch, upstreamRef, rootDir)
456
+ logProcessing('Reading package metadata...')
457
+ const pkg = await readPackage(rootDir)
448
458
 
449
- await runLint(skipLint, pkg, rootDir)
450
- await runTests(skipTests, pkg, rootDir)
451
- await runBuild(skipBuild, pkg, rootDir)
452
- await runLibBuild(skipBuild, pkg, rootDir)
453
- await ensureNpmAuth(rootDir)
459
+ logProcessing('Checking working tree status...')
460
+ await ensureCleanWorkingTree(rootDir)
454
461
 
455
- const updatedPkg = await bumpVersion(releaseType, rootDir)
456
- await pushChanges(rootDir)
457
- await publishPackage(updatedPkg, rootDir)
458
- await deployGHPages(skipDeploy, updatedPkg, rootDir)
462
+ const branch = await getCurrentBranch(rootDir)
463
+ if (!branch) {
464
+ throw new Error('Unable to determine current branch.')
465
+ }
459
466
 
460
- logSuccess(`Release workflow completed for ${updatedPkg.name}@${updatedPkg.version}.`)
467
+ logProcessing(`Current branch: ${branch}`)
468
+ const upstreamRef = await getUpstreamRef(rootDir)
469
+ await ensureUpToDateWithUpstream(branch, upstreamRef, rootDir)
470
+
471
+ await runLint(skipLint, pkg, rootDir)
472
+ await runTests(skipTests, pkg, rootDir)
473
+ await runBuild(skipBuild, pkg, rootDir)
474
+ await runLibBuild(skipBuild, pkg, rootDir)
475
+ await ensureNpmAuth(rootDir)
476
+
477
+ const updatedPkg = await bumpVersion(releaseType, rootDir)
478
+ await pushChanges(rootDir)
479
+ await publishPackage(updatedPkg, rootDir)
480
+ await deployGHPages(skipDeploy, updatedPkg, rootDir)
481
+
482
+ logSuccess(`Release workflow completed for ${updatedPkg.name}@${updatedPkg.version}.`)
483
+ } catch (error) {
484
+ logError('\nRelease failed:')
485
+ logError(error.message)
486
+ throw error
487
+ }
461
488
  }
462
489
 
package/src/ssh-utils.mjs CHANGED
@@ -92,11 +92,11 @@ export async function connectToServer(config, rootDir) {
92
92
  * @param {NodeSSH} ssh - SSH client instance
93
93
  * @param {string} label - Human-readable label for the command
94
94
  * @param {string} command - Command to execute
95
- * @param {Object} options - Options: { cwd, allowFailure, printStdout, bootstrapEnv, rootDir, writeToLogFile }
95
+ * @param {Object} options - Options: { cwd, allowFailure, printStdout, bootstrapEnv, rootDir, writeToLogFile, env }
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 } = options
99
+ const { cwd, allowFailure = false, printStdout = true, bootstrapEnv = true, rootDir = null, writeToLogFile = null, env = {} } = options
100
100
 
101
101
  logProcessing(`\n→ ${label}`)
102
102
 
@@ -120,14 +120,27 @@ export async function executeRemoteCommand(ssh, label, command, options = {}) {
120
120
  ].join('; ')
121
121
 
122
122
  const escapeForDoubleQuotes = (value) => value.replace(/(["\\$`])/g, '\\$1')
123
+ const escapeForSingleQuotes = (value) => value.replace(/'/g, "'\\''")
124
+
125
+ // Build environment variable exports
126
+ let envExports = ''
127
+ if (Object.keys(env).length > 0) {
128
+ const envPairs = Object.entries(env).map(([key, value]) => {
129
+ const escapedValue = escapeForSingleQuotes(String(value))
130
+ return `${key}='${escapedValue}'`
131
+ })
132
+ envExports = envPairs.join(' ') + ' '
133
+ }
123
134
 
124
135
  let wrappedCommand = command
125
136
  let execOptions = { cwd }
126
137
 
127
138
  if (bootstrapEnv && cwd) {
128
139
  const cwdForShell = escapeForDoubleQuotes(cwd)
129
- wrappedCommand = `${profileBootstrap}; cd "${cwdForShell}" && ${command}`
140
+ wrappedCommand = `${profileBootstrap}; cd "${cwdForShell}" && ${envExports}${command}`
130
141
  execOptions = {}
142
+ } else if (Object.keys(env).length > 0) {
143
+ wrappedCommand = `${envExports}${command}`
131
144
  }
132
145
 
133
146
  const result = await ssh.execCommand(wrappedCommand, execOptions)
@@ -191,7 +204,13 @@ export async function readRemoteFile(ssh, filePath, remoteCwd) {
191
204
  }
192
205
 
193
206
  /**
194
- * Download file from remote server via SFTP
207
+ * Download file from remote server via SFTP with progress
208
+ *
209
+ * Note: Currently uses single-stream download (most reliable).
210
+ * Multi-streaming is technically possible with ssh2-sftp-client's fastGet,
211
+ * but it's unreliable on many servers and can cause data corruption.
212
+ * Single-stream ensures data integrity at the cost of potentially slower speeds.
213
+ *
195
214
  * @param {NodeSSH} ssh - SSH client instance
196
215
  * @param {string} remotePath - Path to file on remote server
197
216
  * @param {string} localPath - Local path to save file
@@ -206,8 +225,26 @@ export async function downloadRemoteFile(ssh, remotePath, localPath, remoteCwd)
206
225
 
207
226
  logProcessing(`Downloading ${absoluteRemotePath} to ${localPath}...`)
208
227
 
209
- await ssh.getFile(localPath, absoluteRemotePath)
228
+ let transferred = 0
229
+ const startTime = Date.now()
230
+
231
+ // Single-stream download (most reliable for data integrity)
232
+ await ssh.getFile(localPath, absoluteRemotePath, null, {
233
+ step: (totalTransferred, chunk, total) => {
234
+ transferred = totalTransferred
235
+ const percent = total > 0 ? Math.round((transferred / total) * 100) : 0
236
+ const elapsed = (Date.now() - startTime) / 1000
237
+ const speed = elapsed > 0 ? (transferred / elapsed / 1024 / 1024).toFixed(2) : 0
238
+ const sizeMB = (transferred / 1024 / 1024).toFixed(2)
239
+ const totalMB = total > 0 ? (total / 1024 / 1024).toFixed(2) : '?'
240
+
241
+ // Update progress on same line
242
+ process.stdout.write(`\r Progress: ${percent}% (${sizeMB}MB / ${totalMB}MB) - ${speed} MB/s`)
243
+ }
244
+ })
210
245
 
246
+ // Clear progress line and show completion
247
+ process.stdout.write('\r' + ' '.repeat(80) + '\r')
211
248
  logSuccess(`Downloaded ${absoluteRemotePath} to ${localPath}`)
212
249
  }
213
250