@wyxos/zephyr 0.2.17 → 0.2.18

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/README.md CHANGED
@@ -30,6 +30,12 @@ Follow the interactive prompts to configure your deployment target:
30
30
 
31
31
  Configuration is saved automatically for future deployments.
32
32
 
33
+ ## Update Checks
34
+
35
+ When run via `npx`, Zephyr can prompt to re-run itself using the latest published version.
36
+
37
+ - **Skip update check**: set `ZEPHYR_SKIP_VERSION_CHECK=1`
38
+
33
39
  ## Features
34
40
 
35
41
  - Automated Git operations (branch switching, commits, pushes)
@@ -49,11 +55,12 @@ Configuration is saved automatically for future deployments.
49
55
  Zephyr analyzes changed files and runs appropriate tasks:
50
56
 
51
57
  - **Always**: `git pull origin <branch>`
52
- - **Composer files changed**: `composer update --no-dev --no-interaction --prefer-dist`
53
- - **Migration files added**: `php artisan migrate --force`
54
- - **package.json changed**: `npm install`
55
- - **Frontend files changed**: `npm run build`
56
- - **PHP files changed**: Clear Laravel caches, restart queues
58
+ - **Composer files changed** (`composer.json` / `composer.lock`): `composer update --no-dev --no-interaction --prefer-dist`
59
+ - **Migrations changed** (`database/migrations/*.php`): `php artisan migrate --force`
60
+ - **Node dependency files changed** (`package.json` / `package-lock.json`, including nested): `npm install`
61
+ - **Frontend files changed** (`.vue/.js/.ts/.tsx/.css/.scss/.less`): `npm run build`
62
+ - Note: `npm run build` is also scheduled when `npm install` is scheduled.
63
+ - **PHP files changed**: clear caches + restart queue workers (Horizon if configured)
57
64
 
58
65
  ## Configuration
59
66
 
@@ -64,6 +71,7 @@ Servers are stored globally at `~/.config/zephyr/servers.json`:
64
71
  ```json
65
72
  [
66
73
  {
74
+ "id": "server_abc123",
67
75
  "serverName": "production",
68
76
  "serverIp": "192.168.1.100"
69
77
  }
@@ -76,8 +84,17 @@ Deployment targets are stored per-project at `.zephyr/config.json`:
76
84
 
77
85
  ```json
78
86
  {
87
+ "presets": [
88
+ {
89
+ "name": "prod-main",
90
+ "appId": "app_def456",
91
+ "branch": "main"
92
+ }
93
+ ],
79
94
  "apps": [
80
95
  {
96
+ "id": "app_def456",
97
+ "serverId": "server_abc123",
81
98
  "serverName": "production",
82
99
  "projectPath": "~/webapps/myapp",
83
100
  "branch": "main",
@@ -98,6 +115,11 @@ Zephyr creates a `.zephyr/` directory in your project with:
98
115
 
99
116
  The `.zephyr/` directory is automatically added to `.gitignore`.
100
117
 
118
+ ## Notes
119
+
120
+ - If Zephyr reports **"No upstream file changes detected"**, it means the remote repository already matches `origin/<branch>` after `git fetch`. In that case, Zephyr will only run `git pull` and skip all conditional maintenance tasks.
121
+ - If Zephyr prompts to update local file dependencies (path-based deps outside the repo), it may also prompt to commit those updates before continuing.
122
+
101
123
  ## Requirements
102
124
 
103
125
  - Node.js 16+
package/bin/zephyr.mjs CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  import process from 'node:process'
3
3
  import { logError, main } from '../src/index.mjs'
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wyxos/zephyr",
3
- "version": "0.2.17",
3
+ "version": "0.2.18",
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",
@@ -219,16 +219,16 @@ async function updateComposerJsonDependency(rootDir, packageName, newVersion, fi
219
219
 
220
220
  async function runCommand(command, args, { cwd = process.cwd(), capture = false } = {}) {
221
221
  return new Promise((resolve, reject) => {
222
+ const resolvedCommand = IS_WINDOWS && (command === 'npm' || command === 'npx' || command === 'pnpm' || command === 'yarn')
223
+ ? `${command}.cmd`
224
+ : command
225
+
222
226
  const spawnOptions = {
223
227
  stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
224
228
  cwd
225
229
  }
226
230
 
227
- if (IS_WINDOWS && command !== 'git') {
228
- spawnOptions.shell = true
229
- }
230
-
231
- const child = spawn(command, args, spawnOptions)
231
+ const child = spawn(resolvedCommand, args, spawnOptions)
232
232
  let stdout = ''
233
233
  let stderr = ''
234
234
 
@@ -247,7 +247,7 @@ async function runCommand(command, args, { cwd = process.cwd(), capture = false
247
247
  if (code === 0) {
248
248
  resolve(capture ? { stdout: stdout.trim(), stderr: stderr.trim() } : undefined)
249
249
  } else {
250
- const error = new Error(`Command failed (${code}): ${command} ${args.join(' ')}`)
250
+ const error = new Error(`Command failed (${code}): ${resolvedCommand} ${args.join(' ')}`)
251
251
  if (capture) {
252
252
  error.stdout = stdout
253
253
  error.stderr = stderr
package/src/index.mjs CHANGED
@@ -23,10 +23,20 @@ const PENDING_TASKS_FILE = 'pending-tasks.json'
23
23
  const RELEASE_SCRIPT_NAME = 'release'
24
24
  const RELEASE_SCRIPT_COMMAND = 'npx @wyxos/zephyr@latest'
25
25
 
26
- const logProcessing = (message = '') => console.log(chalk.yellow(message))
27
- const logSuccess = (message = '') => console.log(chalk.green(message))
28
- const logWarning = (message = '') => console.warn(chalk.yellow(message))
29
- const logError = (message = '') => console.error(chalk.red(message))
26
+ function writeStdoutLine(message = '') {
27
+ const text = message == null ? '' : String(message)
28
+ process.stdout.write(`${text}\n`)
29
+ }
30
+
31
+ function writeStderrLine(message = '') {
32
+ const text = message == null ? '' : String(message)
33
+ process.stderr.write(`${text}\n`)
34
+ }
35
+
36
+ const logProcessing = (message = '') => writeStdoutLine(chalk.yellow(message))
37
+ const logSuccess = (message = '') => writeStdoutLine(chalk.green(message))
38
+ const logWarning = (message = '') => writeStderrLine(chalk.yellow(message))
39
+ const logError = (message = '') => writeStderrLine(chalk.red(message))
30
40
 
31
41
  let logFilePath = null
32
42
 
@@ -120,25 +130,23 @@ const runPrompt = async (questions) => {
120
130
 
121
131
  async function runCommand(command, args, { silent = false, cwd } = {}) {
122
132
  return new Promise((resolve, reject) => {
133
+ const resolvedCommand = IS_WINDOWS && (command === 'npm' || command === 'npx' || command === 'pnpm' || command === 'yarn')
134
+ ? `${command}.cmd`
135
+ : command
136
+
123
137
  const spawnOptions = {
124
138
  stdio: silent ? 'ignore' : 'inherit',
125
139
  cwd
126
140
  }
127
141
 
128
- // On Windows, use shell for commands that might need PATH resolution (php, composer, etc.)
129
- // Git commands work fine without shell
130
- if (IS_WINDOWS && command !== 'git') {
131
- spawnOptions.shell = true
132
- }
133
-
134
- const child = spawn(command, args, spawnOptions)
142
+ const child = spawn(resolvedCommand, args, spawnOptions)
135
143
 
136
144
  child.on('error', reject)
137
145
  child.on('close', (code) => {
138
146
  if (code === 0) {
139
147
  resolve()
140
148
  } else {
141
- const error = new Error(`${command} exited with code ${code}`)
149
+ const error = new Error(`${resolvedCommand} exited with code ${code}`)
142
150
  error.exitCode = code
143
151
  reject(error)
144
152
  }
@@ -148,6 +156,10 @@ async function runCommand(command, args, { silent = false, cwd } = {}) {
148
156
 
149
157
  async function runCommandCapture(command, args, { cwd } = {}) {
150
158
  return new Promise((resolve, reject) => {
159
+ const resolvedCommand = IS_WINDOWS && (command === 'npm' || command === 'npx' || command === 'pnpm' || command === 'yarn')
160
+ ? `${command}.cmd`
161
+ : command
162
+
151
163
  let stdout = ''
152
164
  let stderr = ''
153
165
 
@@ -156,13 +168,7 @@ async function runCommandCapture(command, args, { cwd } = {}) {
156
168
  cwd
157
169
  }
158
170
 
159
- // On Windows, use shell for commands that might need PATH resolution (php, composer, etc.)
160
- // Git commands work fine without shell
161
- if (IS_WINDOWS && command !== 'git') {
162
- spawnOptions.shell = true
163
- }
164
-
165
- const child = spawn(command, args, spawnOptions)
171
+ const child = spawn(resolvedCommand, args, spawnOptions)
166
172
 
167
173
  child.stdout.on('data', (chunk) => {
168
174
  stdout += chunk
@@ -177,7 +183,7 @@ async function runCommandCapture(command, args, { cwd } = {}) {
177
183
  if (code === 0) {
178
184
  resolve(stdout)
179
185
  } else {
180
- const error = new Error(`${command} exited with code ${code}: ${stderr.trim()}`)
186
+ const error = new Error(`${resolvedCommand} exited with code ${code}: ${stderr.trim()}`)
181
187
  error.exitCode = code
182
188
  reject(error)
183
189
  }
@@ -297,7 +303,9 @@ async function ensureCommittedChangesPushed(targetBranch, rootDir) {
297
303
  const commitLabel = aheadCount === 1 ? 'commit' : 'commits'
298
304
  logProcessing(`Found ${aheadCount} ${commitLabel} not yet pushed to ${upstreamRef}. Pushing before deployment...`)
299
305
 
300
- await runCommand('git', ['push', remoteName, `${targetBranch}:${upstreamBranch}`], { cwd: rootDir })
306
+ // Keep terminal output clean: suppress git push progress output (it is very noisy),
307
+ // but still surface errors via the thrown exception message.
308
+ await runCommandCapture('git', ['push', remoteName, `${targetBranch}:${upstreamBranch}`], { cwd: rootDir })
301
309
  logSuccess(`Pushed committed changes to ${upstreamRef}.`)
302
310
 
303
311
  return { pushed: true, upstreamRef }
@@ -1882,7 +1890,7 @@ async function main(releaseType = null) {
1882
1890
  logError('\nRelease failed:')
1883
1891
  logError(error.message)
1884
1892
  if (error.stack) {
1885
- console.error(error.stack)
1893
+ writeStderrLine(error.stack)
1886
1894
  }
1887
1895
  process.exit(1)
1888
1896
  }
@@ -1897,7 +1905,7 @@ async function main(releaseType = null) {
1897
1905
  logError('\nRelease failed:')
1898
1906
  logError(error.message)
1899
1907
  if (error.stack) {
1900
- console.error(error.stack)
1908
+ writeStderrLine(error.stack)
1901
1909
  }
1902
1910
  process.exit(1)
1903
1911
  }
@@ -2018,7 +2026,7 @@ async function main(releaseType = null) {
2018
2026
  }
2019
2027
 
2020
2028
  logProcessing('\nSelected deployment target:')
2021
- console.log(JSON.stringify(deploymentConfig, null, 2))
2029
+ writeStdoutLine(JSON.stringify(deploymentConfig, null, 2))
2022
2030
 
2023
2031
  if (isCreatingNewPreset || !preset) {
2024
2032
  const { presetName } = await runPrompt([
@@ -10,16 +10,34 @@ import { validateLocalDependencies } from './dependency-scanner.mjs'
10
10
 
11
11
  const IS_WINDOWS = process.platform === 'win32'
12
12
 
13
+ function writeStdoutLine(message = '') {
14
+ const text = message == null ? '' : String(message)
15
+ process.stdout.write(`${text}\n`)
16
+ }
17
+
18
+ function writeStderrLine(message = '') {
19
+ const text = message == null ? '' : String(message)
20
+ process.stderr.write(`${text}\n`)
21
+ }
22
+
23
+ function writeStderr(message = '') {
24
+ const text = message == null ? '' : String(message)
25
+ process.stderr.write(text)
26
+ if (text && !text.endsWith('\n')) {
27
+ process.stderr.write('\n')
28
+ }
29
+ }
30
+
13
31
  function logStep(message) {
14
- console.log(chalk.yellow(`→ ${message}`))
32
+ writeStdoutLine(chalk.yellow(`→ ${message}`))
15
33
  }
16
34
 
17
35
  function logSuccess(message) {
18
- console.log(chalk.green(`✔ ${message}`))
36
+ writeStdoutLine(chalk.green(`✔ ${message}`))
19
37
  }
20
38
 
21
39
  function logWarning(message) {
22
- console.warn(chalk.yellow(`⚠ ${message}`))
40
+ writeStderrLine(chalk.yellow(`⚠ ${message}`))
23
41
  }
24
42
 
25
43
  function runCommand(command, args, { cwd = process.cwd(), capture = false, useShell = false } = {}) {
@@ -155,7 +173,7 @@ async function ensureUpToDateWithUpstream(branch, upstreamRef, rootDir = process
155
173
  await runCommand('git', ['fetch', remoteName, remoteBranch], { capture: true, cwd: rootDir })
156
174
  } catch (error) {
157
175
  if (error.stderr) {
158
- console.error(error.stderr)
176
+ writeStderr(error.stderr)
159
177
  }
160
178
  throw new Error(`Failed to fetch ${upstreamRef}: ${error.message}`)
161
179
  }
@@ -181,7 +199,7 @@ async function ensureUpToDateWithUpstream(branch, upstreamRef, rootDir = process
181
199
  await runCommand('git', ['pull', '--ff-only', remoteName, remoteBranch], { capture: true, cwd: rootDir })
182
200
  } catch (error) {
183
201
  if (error.stderr) {
184
- console.error(error.stderr)
202
+ writeStderr(error.stderr)
185
203
  }
186
204
  throw new Error(
187
205
  `Unable to fast-forward ${branch} with ${upstreamRef}. Resolve conflicts manually, then rerun the release.\n${error.message}`
@@ -251,10 +269,10 @@ async function runLint(skipLint, pkg, rootDir = process.cwd()) {
251
269
  logSuccess('Lint passed.')
252
270
  } catch (error) {
253
271
  if (error.stdout) {
254
- console.error(error.stdout)
272
+ writeStderr(error.stdout)
255
273
  }
256
274
  if (error.stderr) {
257
- console.error(error.stderr)
275
+ writeStderr(error.stderr)
258
276
  }
259
277
  throw error
260
278
  }
@@ -287,10 +305,10 @@ async function runTests(skipTests, pkg, rootDir = process.cwd()) {
287
305
  logSuccess('Tests passed.')
288
306
  } catch (error) {
289
307
  if (error.stdout) {
290
- console.error(error.stdout)
308
+ writeStderr(error.stdout)
291
309
  }
292
310
  if (error.stderr) {
293
- console.error(error.stderr)
311
+ writeStderr(error.stderr)
294
312
  }
295
313
  throw error
296
314
  }
@@ -314,10 +332,10 @@ async function runBuild(skipBuild, pkg, rootDir = process.cwd()) {
314
332
  logSuccess('Build completed.')
315
333
  } catch (error) {
316
334
  if (error.stdout) {
317
- console.error(error.stdout)
335
+ writeStderr(error.stdout)
318
336
  }
319
337
  if (error.stderr) {
320
- console.error(error.stderr)
338
+ writeStderr(error.stderr)
321
339
  }
322
340
  throw error
323
341
  }
@@ -341,10 +359,10 @@ async function runLibBuild(skipBuild, pkg, rootDir = process.cwd()) {
341
359
  logSuccess('Library built.')
342
360
  } catch (error) {
343
361
  if (error.stdout) {
344
- console.error(error.stdout)
362
+ writeStderr(error.stdout)
345
363
  }
346
364
  if (error.stderr) {
347
- console.error(error.stderr)
365
+ writeStderr(error.stderr)
348
366
  }
349
367
  throw error
350
368
  }
@@ -377,7 +395,7 @@ async function ensureNpmAuth(rootDir = process.cwd()) {
377
395
  logSuccess('npm authenticated.')
378
396
  } catch (error) {
379
397
  if (error.stderr) {
380
- console.error(error.stderr)
398
+ writeStderr(error.stderr)
381
399
  }
382
400
  throw error
383
401
  }
@@ -438,10 +456,10 @@ async function pushChanges(rootDir = process.cwd()) {
438
456
  logSuccess('Git push completed.')
439
457
  } catch (error) {
440
458
  if (error.stdout) {
441
- console.error(error.stdout)
459
+ writeStderr(error.stdout)
442
460
  }
443
461
  if (error.stderr) {
444
- console.error(error.stderr)
462
+ writeStderr(error.stderr)
445
463
  }
446
464
  throw error
447
465
  }
@@ -472,10 +490,10 @@ async function publishPackage(pkg, rootDir = process.cwd()) {
472
490
  logSuccess('npm publish completed.')
473
491
  } catch (error) {
474
492
  if (error.stdout) {
475
- console.error(error.stdout)
493
+ writeStderr(error.stdout)
476
494
  }
477
495
  if (error.stderr) {
478
- console.error(error.stderr)
496
+ writeStderr(error.stderr)
479
497
  }
480
498
  throw error
481
499
  }
@@ -566,10 +584,10 @@ async function deployGHPages(skipDeploy, pkg, rootDir = process.cwd()) {
566
584
  logSuccess('GitHub Pages deployment completed.')
567
585
  } catch (error) {
568
586
  if (error.stdout) {
569
- console.error(error.stdout)
587
+ writeStderr(error.stdout)
570
588
  }
571
589
  if (error.stderr) {
572
- console.error(error.stderr)
590
+ writeStderr(error.stderr)
573
591
  }
574
592
  throw error
575
593
  }
@@ -611,8 +629,8 @@ export async function releaseNode() {
611
629
 
612
630
  logSuccess(`Release workflow completed for ${updatedPkg.name}@${updatedPkg.version}.`)
613
631
  } catch (error) {
614
- console.error('\nRelease failed:')
615
- console.error(error.message)
632
+ writeStderrLine('\nRelease failed:')
633
+ writeStderrLine(error.message)
616
634
  throw error
617
635
  }
618
636
  }
@@ -13,16 +13,34 @@ const WARN_PREFIX = '⚠'
13
13
 
14
14
  const IS_WINDOWS = process.platform === 'win32'
15
15
 
16
+ function writeStdoutLine(message = '') {
17
+ const text = message == null ? '' : String(message)
18
+ process.stdout.write(`${text}\n`)
19
+ }
20
+
21
+ function writeStderrLine(message = '') {
22
+ const text = message == null ? '' : String(message)
23
+ process.stderr.write(`${text}\n`)
24
+ }
25
+
26
+ function writeStderr(message = '') {
27
+ const text = message == null ? '' : String(message)
28
+ process.stderr.write(text)
29
+ if (text && !text.endsWith('\n')) {
30
+ process.stderr.write('\n')
31
+ }
32
+ }
33
+
16
34
  function logStep(message) {
17
- console.log(`${STEP_PREFIX} ${message}`)
35
+ writeStdoutLine(`${STEP_PREFIX} ${message}`)
18
36
  }
19
37
 
20
38
  function logSuccess(message) {
21
- console.log(`${OK_PREFIX} ${message}`)
39
+ writeStdoutLine(`${OK_PREFIX} ${message}`)
22
40
  }
23
41
 
24
42
  function logWarning(message) {
25
- console.warn(`${WARN_PREFIX} ${message}`)
43
+ writeStderrLine(`${WARN_PREFIX} ${message}`)
26
44
  }
27
45
 
28
46
  function runCommand(command, args, { cwd = process.cwd(), capture = false, useShell = false } = {}) {
@@ -269,10 +287,10 @@ async function runLint(skipLint, rootDir = process.cwd()) {
269
287
  }
270
288
  process.stdout.write('\n')
271
289
  if (error.stdout) {
272
- console.error(error.stdout)
290
+ writeStderr(error.stdout)
273
291
  }
274
292
  if (error.stderr) {
275
- console.error(error.stderr)
293
+ writeStderr(error.stderr)
276
294
  }
277
295
  throw error
278
296
  }
@@ -322,10 +340,10 @@ async function runTests(skipTests, composer, rootDir = process.cwd()) {
322
340
  }
323
341
  process.stdout.write('\n')
324
342
  if (error.stdout) {
325
- console.error(error.stdout)
343
+ writeStderr(error.stdout)
326
344
  }
327
345
  if (error.stderr) {
328
- console.error(error.stderr)
346
+ writeStderr(error.stderr)
329
347
  }
330
348
  throw error
331
349
  }
package/src/ssh-utils.mjs CHANGED
@@ -3,13 +3,24 @@ import os from 'node:os'
3
3
  import path from 'node:path'
4
4
  import { NodeSSH } from 'node-ssh'
5
5
  import chalk from 'chalk'
6
+ import process from 'node:process'
6
7
 
7
8
  // Import utility functions - these need to be passed in or redefined to avoid circular dependency
8
9
  // For now, we'll redefine the simple ones and accept others as parameters
9
- const logProcessing = (message = '') => console.log(chalk.yellow(message))
10
- const logSuccess = (message = '') => console.log(chalk.green(message))
11
- const logError = (message = '') => console.error(chalk.red(message))
12
- const logWarning = (message = '') => console.warn(chalk.yellow(message))
10
+ function writeStdoutLine(message = '') {
11
+ const text = message == null ? '' : String(message)
12
+ process.stdout.write(`${text}\n`)
13
+ }
14
+
15
+ function writeStderrLine(message = '') {
16
+ const text = message == null ? '' : String(message)
17
+ process.stderr.write(`${text}\n`)
18
+ }
19
+
20
+ const logProcessing = (message = '') => writeStdoutLine(chalk.yellow(message))
21
+ const logSuccess = (message = '') => writeStdoutLine(chalk.green(message))
22
+ const logError = (message = '') => writeStderrLine(chalk.red(message))
23
+ const logWarning = (message = '') => writeStderrLine(chalk.yellow(message))
13
24
 
14
25
  function expandHomePath(targetPath) {
15
26
  if (!targetPath) {
@@ -97,7 +97,6 @@ async function reExecuteWithLatest(args) {
97
97
  return new Promise((resolve, reject) => {
98
98
  const child = spawn(command, npxArgs, {
99
99
  stdio: 'inherit',
100
- shell: IS_WINDOWS,
101
100
  env: {
102
101
  ...process.env,
103
102
  [ZEPHYR_SKIP_VERSION_CHECK_ENV]: '1'