@wyxos/zephyr 0.2.24 → 0.2.26
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 +0 -11
- package/bin/zephyr.mjs +0 -5
- package/package.json +1 -1
- package/src/deploy/remote-exec.mjs +1 -1
- package/src/main.mjs +0 -19
- package/src/utils/php-version.mjs +94 -31
- package/src/version-checker.mjs +0 -162
package/README.md
CHANGED
|
@@ -33,9 +33,6 @@ Common flags:
|
|
|
33
33
|
```bash
|
|
34
34
|
# Run a release workflow
|
|
35
35
|
zephyr --type node
|
|
36
|
-
|
|
37
|
-
# Skip the best-effort update check for this run
|
|
38
|
-
zephyr --skip-version-check
|
|
39
36
|
```
|
|
40
37
|
|
|
41
38
|
Follow the interactive prompts to configure your deployment target:
|
|
@@ -46,14 +43,6 @@ Follow the interactive prompts to configure your deployment target:
|
|
|
46
43
|
|
|
47
44
|
Configuration is saved automatically for future deployments.
|
|
48
45
|
|
|
49
|
-
## Update Checks
|
|
50
|
-
|
|
51
|
-
When run via `npx`, Zephyr can prompt to re-run itself using the latest published version.
|
|
52
|
-
|
|
53
|
-
- **Skip update check**:
|
|
54
|
-
- Set `ZEPHYR_SKIP_VERSION_CHECK=1`, or
|
|
55
|
-
- Use `zephyr --skip-version-check`
|
|
56
|
-
|
|
57
46
|
## Features
|
|
58
47
|
|
|
59
48
|
- Automated Git operations (branch switching, commits, pushes)
|
package/bin/zephyr.mjs
CHANGED
|
@@ -13,15 +13,10 @@ program
|
|
|
13
13
|
.name('zephyr')
|
|
14
14
|
.description('A streamlined deployment tool for web applications with intelligent Laravel project detection')
|
|
15
15
|
.option('--type <type>', 'Release type (node|vue|packagist)')
|
|
16
|
-
.option('--skip-version-check', 'Skip the version check for this run')
|
|
17
16
|
|
|
18
17
|
program.parse(process.argv)
|
|
19
18
|
const options = program.opts()
|
|
20
19
|
|
|
21
|
-
if (options.skipVersionCheck) {
|
|
22
|
-
process.env.ZEPHYR_SKIP_VERSION_CHECK = '1'
|
|
23
|
-
}
|
|
24
|
-
|
|
25
20
|
try {
|
|
26
21
|
await main(options.type ?? null)
|
|
27
22
|
} catch (error) {
|
package/package.json
CHANGED
package/src/main.mjs
CHANGED
|
@@ -8,7 +8,6 @@ import { NodeSSH } from 'node-ssh'
|
|
|
8
8
|
import { releaseNode } from './release-node.mjs'
|
|
9
9
|
import { releasePackagist } from './release-packagist.mjs'
|
|
10
10
|
import { validateLocalDependencies } from './dependency-scanner.mjs'
|
|
11
|
-
import { checkAndUpdateVersion } from './version-checker.mjs'
|
|
12
11
|
import { createChalkLogger, writeStderrLine, writeStdoutLine } from './utils/output.mjs'
|
|
13
12
|
import { runCommand as runCommandBase, runCommandCapture as runCommandCaptureBase } from './utils/command.mjs'
|
|
14
13
|
import { planLaravelDeploymentTasks } from './utils/task-planner.mjs'
|
|
@@ -429,24 +428,6 @@ async function selectPreset(projectConfig, servers) {
|
|
|
429
428
|
}
|
|
430
429
|
|
|
431
430
|
async function main(releaseType = null) {
|
|
432
|
-
// Best-effort update check (skip during tests or when explicitly disabled)
|
|
433
|
-
// If an update is accepted, the process will re-execute via npx @latest and we should exit early.
|
|
434
|
-
if (
|
|
435
|
-
process.env.ZEPHYR_SKIP_VERSION_CHECK !== '1' &&
|
|
436
|
-
process.env.NODE_ENV !== 'test' &&
|
|
437
|
-
process.env.VITEST !== 'true'
|
|
438
|
-
) {
|
|
439
|
-
try {
|
|
440
|
-
const args = process.argv.slice(2)
|
|
441
|
-
const reExecuted = await checkAndUpdateVersion(runPrompt, args)
|
|
442
|
-
if (reExecuted) {
|
|
443
|
-
return
|
|
444
|
-
}
|
|
445
|
-
} catch (_error) {
|
|
446
|
-
// Never block execution due to update check issues
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
431
|
// Handle node/vue package release
|
|
451
432
|
if (releaseType === 'node' || releaseType === 'vue') {
|
|
452
433
|
try {
|
|
@@ -56,67 +56,130 @@ export async function getPhpVersionRequirement(rootDir) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
const RUNCLOUD_PACKAGES = '/RunCloud/Packages'
|
|
60
|
+
|
|
61
|
+
function satisfiesVersion(actualVersionStr, requiredVersion) {
|
|
62
|
+
const normalized = semver.coerce(actualVersionStr)
|
|
63
|
+
const required = semver.coerce(requiredVersion)
|
|
64
|
+
return normalized && required && semver.gte(normalized, required)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function tryPhpPath(ssh, remoteCwd, pathOrCommand) {
|
|
68
|
+
const versionCheck = await ssh.execCommand(`${pathOrCommand} -r "echo PHP_VERSION;"`, { cwd: remoteCwd })
|
|
69
|
+
return versionCheck.code === 0 ? versionCheck.stdout.trim() : null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Discovers PHP binaries under RunCloud Packages (e.g. /RunCloud/Packages/php84rc/bin/php).
|
|
74
|
+
* Lists the directory and tries each php*rc/bin/php, returning the path that satisfies the version.
|
|
75
|
+
*/
|
|
76
|
+
async function findRunCloudPhp(ssh, remoteCwd, requiredVersion) {
|
|
77
|
+
const listResult = await ssh.execCommand(`ls -1 ${RUNCLOUD_PACKAGES} 2>/dev/null || true`, { cwd: remoteCwd })
|
|
78
|
+
if (listResult.code !== 0 || !listResult.stdout.trim()) {
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
const entries = listResult.stdout.trim().split(/\r?\n/).map((s) => s.trim()).filter(Boolean)
|
|
82
|
+
// e.g. php74rc, php80rc, php84rc
|
|
83
|
+
const phpDirs = entries.filter((name) => /^php\d+rc$/.test(name))
|
|
84
|
+
const majorMinor = semver.major(requiredVersion) + '.' + semver.minor(requiredVersion)
|
|
85
|
+
const targetSuffix = `php${majorMinor.replace('.', '')}rc` // php84rc for 8.4
|
|
86
|
+
|
|
87
|
+
// Prefer exact match (php84rc for 8.4), then try any that might satisfy
|
|
88
|
+
const toTry = phpDirs.filter((d) => d === targetSuffix).concat(phpDirs.filter((d) => d !== targetSuffix))
|
|
89
|
+
|
|
90
|
+
for (const dir of toTry) {
|
|
91
|
+
const binPath = `${RUNCLOUD_PACKAGES}/${dir}/bin/php`
|
|
92
|
+
const actualVersion = await tryPhpPath(ssh, remoteCwd, binPath)
|
|
93
|
+
if (actualVersion && satisfiesVersion(actualVersion, requiredVersion)) {
|
|
94
|
+
return binPath
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
|
|
59
100
|
/**
|
|
60
|
-
*
|
|
61
|
-
|
|
101
|
+
* Resolves a command (e.g. php84) via login shell so aliases are expanded; returns the path if it runs and satisfies version.
|
|
102
|
+
*/
|
|
103
|
+
async function resolveViaLoginShell(ssh, remoteCwd, commandName, requiredVersion) {
|
|
104
|
+
const whichResult = await ssh.execCommand(`bash -lc 'command -v ${commandName}' 2>/dev/null || true`, { cwd: remoteCwd })
|
|
105
|
+
if (whichResult.code !== 0 || !whichResult.stdout.trim()) {
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
const pathOrCommand = whichResult.stdout.trim()
|
|
109
|
+
const actualVersion = await tryPhpPath(ssh, remoteCwd, pathOrCommand)
|
|
110
|
+
if (actualVersion && satisfiesVersion(actualVersion, requiredVersion)) {
|
|
111
|
+
return pathOrCommand
|
|
112
|
+
}
|
|
113
|
+
return null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Finds the appropriate PHP binary command for a given version.
|
|
118
|
+
* Tries RunCloud paths, login-shell alias resolution, then common names (php8.4, php84), then default php.
|
|
62
119
|
* @param {object} ssh - SSH client instance
|
|
63
120
|
* @param {string} remoteCwd - Remote working directory
|
|
64
121
|
* @param {string} requiredVersion - Required PHP version (e.g., "8.4.0")
|
|
65
|
-
* @returns {Promise<string>} - PHP command
|
|
122
|
+
* @returns {Promise<string>} - PHP command or path (e.g., "php8.4", "/RunCloud/Packages/php84rc/bin/php", or "php")
|
|
66
123
|
*/
|
|
67
124
|
export async function findPhpBinary(ssh, remoteCwd, requiredVersion) {
|
|
68
125
|
if (!requiredVersion) {
|
|
69
126
|
return 'php'
|
|
70
127
|
}
|
|
71
128
|
|
|
72
|
-
// Extract major.minor version (e.g., "8.4" from "8.4.0")
|
|
73
129
|
const majorMinor = semver.major(requiredVersion) + '.' + semver.minor(requiredVersion)
|
|
74
130
|
const versionedPhp = `php${majorMinor.replace('.', '')}` // e.g., "php84"
|
|
75
131
|
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
132
|
+
// 1. RunCloud: discover /RunCloud/Packages/php*rc/bin/php
|
|
133
|
+
try {
|
|
134
|
+
const runcloudPath = await findRunCloudPhp(ssh, remoteCwd, requiredVersion)
|
|
135
|
+
if (runcloudPath) {
|
|
136
|
+
return runcloudPath
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// Ignore
|
|
140
|
+
}
|
|
82
141
|
|
|
142
|
+
// 2. Resolve alias via login shell (e.g. php84 -> real path)
|
|
143
|
+
try {
|
|
144
|
+
const resolved = await resolveViaLoginShell(ssh, remoteCwd, versionedPhp, requiredVersion)
|
|
145
|
+
if (resolved) {
|
|
146
|
+
return resolved
|
|
147
|
+
}
|
|
148
|
+
const resolvedDot = await resolveViaLoginShell(ssh, remoteCwd, `php${majorMinor}`, requiredVersion)
|
|
149
|
+
if (resolvedDot) {
|
|
150
|
+
return resolvedDot
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
// Ignore
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 3. Try common names in current PATH (non-login shell)
|
|
157
|
+
const candidates = [`php${majorMinor}`, versionedPhp]
|
|
83
158
|
for (const candidate of candidates) {
|
|
84
159
|
try {
|
|
85
160
|
const result = await ssh.execCommand(`command -v ${candidate}`, { cwd: remoteCwd })
|
|
86
161
|
if (result.code === 0 && result.stdout.trim()) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const actualVersion = versionCheck.stdout.trim()
|
|
91
|
-
// Normalize version and check if it satisfies the requirement
|
|
92
|
-
const normalizedVersion = semver.coerce(actualVersion)
|
|
93
|
-
if (normalizedVersion && semver.gte(normalizedVersion, semver.coerce(requiredVersion))) {
|
|
94
|
-
return candidate
|
|
95
|
-
}
|
|
162
|
+
const actualVersion = await tryPhpPath(ssh, remoteCwd, candidate)
|
|
163
|
+
if (actualVersion && satisfiesVersion(actualVersion, requiredVersion)) {
|
|
164
|
+
return candidate
|
|
96
165
|
}
|
|
97
166
|
}
|
|
98
167
|
} catch {
|
|
99
|
-
// Continue
|
|
168
|
+
// Continue
|
|
100
169
|
}
|
|
101
170
|
}
|
|
102
171
|
|
|
103
|
-
//
|
|
172
|
+
// 4. Default php
|
|
104
173
|
try {
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
const normalizedVersion = semver.coerce(actualVersion)
|
|
109
|
-
if (normalizedVersion && semver.gte(normalizedVersion, semver.coerce(requiredVersion))) {
|
|
110
|
-
return 'php'
|
|
111
|
-
}
|
|
174
|
+
const actualVersion = await tryPhpPath(ssh, remoteCwd, 'php')
|
|
175
|
+
if (actualVersion && satisfiesVersion(actualVersion, requiredVersion)) {
|
|
176
|
+
return 'php'
|
|
112
177
|
}
|
|
113
178
|
} catch {
|
|
114
179
|
// Ignore
|
|
115
180
|
}
|
|
116
181
|
|
|
117
|
-
|
|
118
|
-
// The error will be clearer when the command fails
|
|
119
|
-
return `php${majorMinor}`
|
|
182
|
+
return 'php'
|
|
120
183
|
}
|
|
121
184
|
|
|
122
185
|
/**
|
package/src/version-checker.mjs
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises'
|
|
2
|
-
import { fileURLToPath } from 'node:url'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import { spawn } from 'node:child_process'
|
|
5
|
-
import process from 'node:process'
|
|
6
|
-
import https from 'node:https'
|
|
7
|
-
import semver from 'semver'
|
|
8
|
-
|
|
9
|
-
const IS_WINDOWS = process.platform === 'win32'
|
|
10
|
-
const ZEPHYR_SKIP_VERSION_CHECK_ENV = 'ZEPHYR_SKIP_VERSION_CHECK'
|
|
11
|
-
|
|
12
|
-
async function getCurrentVersion() {
|
|
13
|
-
try {
|
|
14
|
-
// Try to get version from package.json
|
|
15
|
-
// When running via npx, the package.json is in the installed package directory
|
|
16
|
-
const packageJsonPath = path.resolve(
|
|
17
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
18
|
-
'..',
|
|
19
|
-
'package.json'
|
|
20
|
-
)
|
|
21
|
-
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'))
|
|
22
|
-
return packageJson.version
|
|
23
|
-
} catch (_error) {
|
|
24
|
-
// If we can't read package.json, return null
|
|
25
|
-
return null
|
|
26
|
-
}
|
|
27
|
-
}
|
|
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
|
-
|
|
66
|
-
async function getLatestVersion() {
|
|
67
|
-
try {
|
|
68
|
-
const data = await httpsGetJson('https://registry.npmjs.org/@wyxos/zephyr/latest')
|
|
69
|
-
if (!data) {
|
|
70
|
-
return null
|
|
71
|
-
}
|
|
72
|
-
return data.version || null
|
|
73
|
-
} catch (_error) {
|
|
74
|
-
return null
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function isNewerVersionAvailable(current, latest) {
|
|
79
|
-
if (!current || !latest) {
|
|
80
|
-
return false
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Use semver to properly compare versions
|
|
84
|
-
try {
|
|
85
|
-
return semver.gt(latest, current)
|
|
86
|
-
} catch (_error) {
|
|
87
|
-
// If semver comparison fails, fall back to simple string comparison
|
|
88
|
-
return latest !== current
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function reExecuteWithLatest(args) {
|
|
93
|
-
// Re-execute with npx @wyxos/zephyr@latest
|
|
94
|
-
const command = IS_WINDOWS ? 'npx.cmd' : 'npx'
|
|
95
|
-
const npxArgs = ['@wyxos/zephyr@latest', ...args]
|
|
96
|
-
|
|
97
|
-
return new Promise((resolve, reject) => {
|
|
98
|
-
const child = spawn(command, npxArgs, {
|
|
99
|
-
stdio: 'inherit',
|
|
100
|
-
env: {
|
|
101
|
-
...process.env,
|
|
102
|
-
[ZEPHYR_SKIP_VERSION_CHECK_ENV]: '1'
|
|
103
|
-
}
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
child.on('error', reject)
|
|
107
|
-
child.on('close', (code) => {
|
|
108
|
-
if (code === 0) {
|
|
109
|
-
resolve()
|
|
110
|
-
} else {
|
|
111
|
-
reject(new Error(`Command exited with code ${code}`))
|
|
112
|
-
}
|
|
113
|
-
})
|
|
114
|
-
})
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export async function checkAndUpdateVersion(promptFn, args) {
|
|
118
|
-
try {
|
|
119
|
-
if (process.env[ZEPHYR_SKIP_VERSION_CHECK_ENV] === '1') {
|
|
120
|
-
return false
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const currentVersion = await getCurrentVersion()
|
|
124
|
-
if (!currentVersion) {
|
|
125
|
-
// Can't determine current version, skip check
|
|
126
|
-
return false
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const latestVersion = await getLatestVersion()
|
|
130
|
-
if (!latestVersion) {
|
|
131
|
-
// Can't fetch latest version, skip check
|
|
132
|
-
return false
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (!isNewerVersionAvailable(currentVersion, latestVersion)) {
|
|
136
|
-
// Already on latest or newer
|
|
137
|
-
return false
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Newer version available, prompt user
|
|
141
|
-
const { shouldUpdate } = await promptFn([
|
|
142
|
-
{
|
|
143
|
-
type: 'confirm',
|
|
144
|
-
name: 'shouldUpdate',
|
|
145
|
-
message: `A new version of @wyxos/zephyr is available (${latestVersion}). You are currently on ${currentVersion}. Update and continue?`,
|
|
146
|
-
default: true
|
|
147
|
-
}
|
|
148
|
-
])
|
|
149
|
-
|
|
150
|
-
if (!shouldUpdate) {
|
|
151
|
-
return false
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// User confirmed, re-execute with latest version
|
|
155
|
-
await reExecuteWithLatest(args)
|
|
156
|
-
return true // Indicates we've re-executed, so the current process should exit
|
|
157
|
-
} catch (_error) {
|
|
158
|
-
// If version check fails, just continue with current version
|
|
159
|
-
// Don't block the user from using the tool
|
|
160
|
-
return false
|
|
161
|
-
}
|
|
162
|
-
}
|