@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 +1 -1
- package/src/deploy/preflight.mjs +10 -1
- package/src/main.mjs +24 -8
- package/src/release-node.mjs +2 -59
- package/src/utils/command.mjs +67 -5
package/package.json
CHANGED
package/src/deploy/preflight.mjs
CHANGED
|
@@ -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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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 {
|
package/src/release-node.mjs
CHANGED
|
@@ -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:')
|
package/src/utils/command.mjs
CHANGED
|
@@ -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 =
|
|
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',
|
|
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 =
|
|
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',
|
|
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 })
|