@wyxos/zephyr 0.2.27 → 0.2.31

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.27",
3
+ "version": "0.2.31",
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",
@@ -45,7 +45,7 @@ export async function hasLaravelPint(rootDir) {
45
45
  }
46
46
  }
47
47
 
48
- export async function runLinting(rootDir, { runCommand, logProcessing, logSuccess } = {}) {
48
+ export async function runLinting(rootDir, { runCommand, logProcessing, logSuccess, logWarning, commandExists } = {}) {
49
49
  const hasNpmLint = await hasLintScript(rootDir)
50
50
  const hasPint = await hasLaravelPint(rootDir)
51
51
 
@@ -57,6 +57,15 @@ export async function runLinting(rootDir, { runCommand, logProcessing, logSucces
57
57
  }
58
58
 
59
59
  if (hasPint) {
60
+ // Check if PHP is available before trying to run Pint
61
+ if (commandExists && !commandExists('php')) {
62
+ logWarning?.(
63
+ 'PHP is not available in PATH. Skipping Laravel Pint.\n' +
64
+ ' To run Pint locally, ensure PHP is installed and added to your PATH.'
65
+ )
66
+ return false
67
+ }
68
+
60
69
  logProcessing?.('Running Laravel Pint...')
61
70
  await runCommand('php', ['vendor/bin/pint'], { cwd: rootDir })
62
71
  logSuccess?.('Linting completed.')
package/src/main.mjs CHANGED
@@ -9,7 +9,7 @@ import { releaseNode } from './release-node.mjs'
9
9
  import { releasePackagist } from './release-packagist.mjs'
10
10
  import { validateLocalDependencies } from './dependency-scanner.mjs'
11
11
  import { createChalkLogger, writeStderrLine, writeStdoutLine } from './utils/output.mjs'
12
- import { runCommand as runCommandBase, runCommandCapture as runCommandCaptureBase } from './utils/command.mjs'
12
+ import { runCommand as runCommandBase, runCommandCapture as runCommandCaptureBase, commandExists } from './utils/command.mjs'
13
13
  import { planLaravelDeploymentTasks } from './utils/task-planner.mjs'
14
14
  import { getPhpVersionRequirement, findPhpBinary } from './utils/php-version.mjs'
15
15
  import {
@@ -122,7 +122,7 @@ async function resolveSshKeyPath(targetPath) {
122
122
  // resolveRemotePath moved to src/utils/remote-path.mjs
123
123
 
124
124
  async function runLinting(rootDir) {
125
- return await preflight.runLinting(rootDir, { runCommand, logProcessing, logSuccess })
125
+ return await preflight.runLinting(rootDir, { runCommand, logProcessing, logSuccess, logWarning, commandExists })
126
126
  }
127
127
 
128
128
  async function commitLintingChanges(rootDir) {
@@ -164,12 +164,28 @@ async function runRemoteTasks(config, options = {}) {
164
164
 
165
165
  // Run tests for Laravel projects
166
166
  if (isLaravel) {
167
- logProcessing('Running Laravel tests locally...')
168
- try {
169
- await runCommand('php', ['artisan', 'test', '--compact'], { cwd: rootDir })
170
- logSuccess('Local tests passed.')
171
- } catch (error) {
172
- throw new Error(`Local tests failed. Fix test failures before deploying. ${error.message}`)
167
+ // Check if PHP is available before trying to run tests
168
+ if (!commandExists('php')) {
169
+ logWarning(
170
+ 'PHP is not available in PATH. Skipping local Laravel tests.\n' +
171
+ ' To run tests locally, ensure PHP is installed and added to your PATH.\n' +
172
+ ' On Windows with Laravel Herd, you may need to add Herd\'s PHP to your system PATH.'
173
+ )
174
+ } else {
175
+ logProcessing('Running Laravel tests locally...')
176
+ try {
177
+ await runCommand('php', ['artisan', 'test', '--compact'], { cwd: rootDir })
178
+ logSuccess('Local tests passed.')
179
+ } catch (error) {
180
+ // Provide clearer error message based on error type
181
+ if (error.code === 'ENOENT') {
182
+ throw new Error(
183
+ 'Failed to run Laravel tests: PHP executable not found.\n' +
184
+ 'Make sure PHP is installed and available in your PATH.'
185
+ )
186
+ }
187
+ throw new Error(`Local tests failed. Fix test failures before deploying.\n${error.message}`)
188
+ }
173
189
  }
174
190
  }
175
191
  } else {
@@ -229,29 +229,6 @@ async function runLibBuild(skipBuild, pkg, rootDir = process.cwd()) {
229
229
  return hasLibChanges
230
230
  }
231
231
 
232
- async function ensureNpmAuth(pkg, rootDir = process.cwd()) {
233
- const isPrivate = pkg?.publishConfig?.access === 'restricted'
234
- if (isPrivate) {
235
- logStep('Skipping npm authentication check (package is private/restricted).')
236
- return
237
- }
238
-
239
- logStep('Confirming npm authentication...')
240
- try {
241
- const result = await runCommand('npm', ['whoami'], { capture: true, cwd: rootDir })
242
- // Only show username if we captured it, otherwise just show success
243
- if (result?.stdout) {
244
- // Silently authenticated - we don't need to show the username
245
- }
246
- logSuccess('npm authenticated.')
247
- } catch (error) {
248
- if (error.stderr) {
249
- writeStderr(error.stderr)
250
- }
251
- throw error
252
- }
253
- }
254
-
255
232
  async function bumpVersion(releaseType, rootDir = process.cwd()) {
256
233
  logStep(`Bumping package version...`)
257
234
 
@@ -316,40 +293,6 @@ async function pushChanges(rootDir = process.cwd()) {
316
293
  }
317
294
  }
318
295
 
319
- async function publishPackage(pkg, rootDir = process.cwd()) {
320
- // Check if package is configured as private/restricted
321
- const isPrivate = pkg.publishConfig?.access === 'restricted'
322
-
323
- if (isPrivate) {
324
- logWarning('Skipping npm publish (package is configured as private/restricted).')
325
- logWarning('Private packages require npm paid plan. Publish manually or use GitHub Packages.')
326
- return
327
- }
328
-
329
- const publishArgs = ['publish', '--ignore-scripts'] // Skip prepublishOnly since we already built lib
330
-
331
- if (pkg.name.startsWith('@')) {
332
- // For scoped packages, determine access level from publishConfig
333
- // Default to 'public' for scoped packages if not specified (free npm accounts require public for scoped packages)
334
- const access = pkg.publishConfig?.access || 'public'
335
- publishArgs.push('--access', access)
336
- }
337
-
338
- logStep(`Publishing ${pkg.name}@${pkg.version} to npm...`)
339
- try {
340
- await runCommand('npm', publishArgs, { capture: true, cwd: rootDir })
341
- logSuccess('npm publish completed.')
342
- } catch (error) {
343
- if (error.stdout) {
344
- writeStderr(error.stdout)
345
- }
346
- if (error.stderr) {
347
- writeStderr(error.stderr)
348
- }
349
- throw error
350
- }
351
- }
352
-
353
296
  function extractDomainFromHomepage(homepage) {
354
297
  if (!homepage) return null
355
298
  try {
@@ -470,14 +413,14 @@ export async function releaseNode() {
470
413
  await runLint(skipLint, pkg, rootDir)
471
414
  await runTests(skipTests, pkg, rootDir)
472
415
  await runLibBuild(skipBuild, pkg, rootDir)
473
- await ensureNpmAuth(pkg, rootDir)
474
416
 
475
417
  const updatedPkg = await bumpVersion(releaseType, rootDir)
476
418
  await runBuild(skipBuild, updatedPkg, rootDir)
477
419
  await pushChanges(rootDir)
478
- await publishPackage(updatedPkg, rootDir)
479
420
  await deployGHPages(skipDeploy, updatedPkg, rootDir)
480
421
 
422
+ logStep('Publishing will be handled by GitHub Actions via trusted publishing.')
423
+
481
424
  logSuccess(`Release workflow completed for ${updatedPkg.name}@${updatedPkg.version}.`)
482
425
  } catch (error) {
483
426
  writeStderrLine('\nRelease failed:')
@@ -1,8 +1,30 @@
1
- import { spawn } from 'node:child_process'
1
+ import { spawn, spawnSync } from 'node:child_process'
2
2
  import process from 'node:process'
3
3
 
4
4
  const DEFAULT_IS_WINDOWS = process.platform === 'win32'
5
5
 
6
+ /**
7
+ * Check if a command exists in PATH.
8
+ * @param {string} command - The command to check
9
+ * @returns {boolean} - True if the command exists
10
+ */
11
+ export function commandExists(command) {
12
+ const resolvedCommand = resolveCommandForPlatform(command)
13
+
14
+ // On Windows, use 'where', on Unix use 'which'
15
+ const checker = DEFAULT_IS_WINDOWS ? 'where' : 'which'
16
+
17
+ try {
18
+ const result = spawnSync(checker, [resolvedCommand], {
19
+ stdio: ['ignore', 'pipe', 'ignore'],
20
+ shell: DEFAULT_IS_WINDOWS
21
+ })
22
+ return result.status === 0
23
+ } catch {
24
+ return false
25
+ }
26
+ }
27
+
6
28
  export function resolveCommandForPlatform(command, { isWindows = DEFAULT_IS_WINDOWS } = {}) {
7
29
  if (!isWindows) {
8
30
  return command
@@ -16,6 +38,20 @@ export function resolveCommandForPlatform(command, { isWindows = DEFAULT_IS_WIND
16
38
  return command
17
39
  }
18
40
 
41
+ /**
42
+ * Check if a command should be run with shell: true on Windows.
43
+ * This is needed for commands that might be .bat or .cmd shims (like php via Herd).
44
+ */
45
+ export function shouldUseShellOnWindows(command) {
46
+ if (!DEFAULT_IS_WINDOWS) {
47
+ return false
48
+ }
49
+ // Commands that are commonly provided as batch file shims on Windows
50
+ // Note: git is NOT included because Git for Windows installs a proper git.exe
51
+ const shellCommands = ['php', 'composer']
52
+ return shellCommands.includes(command.toLowerCase())
53
+ }
54
+
19
55
  function isWindowsShellShim(command) {
20
56
  return DEFAULT_IS_WINDOWS && typeof command === 'string' && /\.(cmd|bat)$/i.test(command)
21
57
  }
@@ -33,13 +69,26 @@ function quoteForCmd(arg) {
33
69
 
34
70
  export async function runCommand(command, args, { cwd = process.cwd(), stdio = 'inherit' } = {}) {
35
71
  const resolvedCommand = resolveCommandForPlatform(command)
72
+ const useShell = isWindowsShellShim(resolvedCommand) || shouldUseShellOnWindows(command)
36
73
 
37
74
  return new Promise((resolve, reject) => {
38
- const child = isWindowsShellShim(resolvedCommand)
75
+ const child = useShell
39
76
  ? spawn([resolvedCommand, ...(args ?? []).map(quoteForCmd)].join(' '), { cwd, stdio, shell: true })
40
77
  : spawn(resolvedCommand, args, { cwd, stdio })
41
78
 
42
- child.on('error', reject)
79
+ child.on('error', (err) => {
80
+ if (err.code === 'ENOENT') {
81
+ const error = new Error(
82
+ `Command not found: "${resolvedCommand}". ` +
83
+ `Make sure "${command}" is installed and available in your PATH.`
84
+ )
85
+ error.code = 'ENOENT'
86
+ error.originalError = err
87
+ reject(error)
88
+ } else {
89
+ reject(err)
90
+ }
91
+ })
43
92
  child.on('close', (code) => {
44
93
  if (code === 0) {
45
94
  resolve()
@@ -54,12 +103,13 @@ export async function runCommand(command, args, { cwd = process.cwd(), stdio = '
54
103
 
55
104
  export async function runCommandCapture(command, args, { cwd = process.cwd() } = {}) {
56
105
  const resolvedCommand = resolveCommandForPlatform(command)
106
+ const useShell = isWindowsShellShim(resolvedCommand) || shouldUseShellOnWindows(command)
57
107
 
58
108
  return new Promise((resolve, reject) => {
59
109
  let stdout = ''
60
110
  let stderr = ''
61
111
 
62
- const child = isWindowsShellShim(resolvedCommand)
112
+ const child = useShell
63
113
  ? spawn([resolvedCommand, ...(args ?? []).map(quoteForCmd)].join(' '), {
64
114
  cwd,
65
115
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -75,7 +125,19 @@ export async function runCommandCapture(command, args, { cwd = process.cwd() } =
75
125
  stderr += chunk
76
126
  })
77
127
 
78
- child.on('error', reject)
128
+ child.on('error', (err) => {
129
+ if (err.code === 'ENOENT') {
130
+ const error = new Error(
131
+ `Command not found: "${resolvedCommand}". ` +
132
+ `Make sure "${command}" is installed and available in your PATH.`
133
+ )
134
+ error.code = 'ENOENT'
135
+ error.originalError = err
136
+ reject(error)
137
+ } else {
138
+ reject(err)
139
+ }
140
+ })
79
141
  child.on('close', (code) => {
80
142
  if (code === 0) {
81
143
  resolve({ stdout, stderr })