@wyxos/zephyr 0.2.21 → 0.2.22
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 +144 -144
- package/bin/zephyr.mjs +29 -29
- package/package.json +58 -58
- package/src/config/project.mjs +118 -118
- package/src/config/servers.mjs +57 -57
- package/src/dependency-scanner.mjs +412 -433
- package/src/deploy/local-repo.mjs +215 -215
- package/src/deploy/locks.mjs +171 -171
- package/src/deploy/preflight.mjs +117 -117
- package/src/deploy/remote-exec.mjs +99 -99
- package/src/deploy/snapshots.mjs +35 -35
- package/src/index.mjs +91 -91
- package/src/main.mjs +677 -652
- package/src/project/bootstrap.mjs +147 -147
- package/src/runtime/local-command.mjs +18 -18
- package/src/runtime/prompt.mjs +14 -14
- package/src/runtime/ssh-client.mjs +14 -14
- package/src/ssh/index.mjs +8 -8
- package/src/ssh/keys.mjs +146 -146
- package/src/ssh/ssh.mjs +134 -134
- package/src/utils/command.mjs +92 -92
- package/src/utils/config-flow.mjs +284 -284
- package/src/utils/git.mjs +91 -91
- package/src/utils/id.mjs +6 -6
- package/src/utils/log-file.mjs +76 -76
- package/src/utils/output.mjs +29 -29
- package/src/utils/paths.mjs +28 -28
- package/src/utils/php-version.mjs +137 -0
- package/src/utils/remote-path.mjs +23 -23
- package/src/utils/task-planner.mjs +99 -96
- package/src/version-checker.mjs +162 -162
|
@@ -1,96 +1,99 @@
|
|
|
1
|
-
export function planLaravelDeploymentTasks({
|
|
2
|
-
branch,
|
|
3
|
-
isLaravel,
|
|
4
|
-
changedFiles,
|
|
5
|
-
horizonConfigured = false
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
file === 'composer.
|
|
15
|
-
file
|
|
16
|
-
file.endsWith('/composer.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
file === 'package
|
|
31
|
-
file
|
|
32
|
-
file.endsWith('/package
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
1
|
+
export function planLaravelDeploymentTasks({
|
|
2
|
+
branch,
|
|
3
|
+
isLaravel,
|
|
4
|
+
changedFiles,
|
|
5
|
+
horizonConfigured = false,
|
|
6
|
+
phpCommand = 'php'
|
|
7
|
+
}) {
|
|
8
|
+
const safeChangedFiles = Array.isArray(changedFiles) ? changedFiles : []
|
|
9
|
+
|
|
10
|
+
const shouldRunComposer =
|
|
11
|
+
isLaravel &&
|
|
12
|
+
safeChangedFiles.some(
|
|
13
|
+
(file) =>
|
|
14
|
+
file === 'composer.json' ||
|
|
15
|
+
file === 'composer.lock' ||
|
|
16
|
+
file.endsWith('/composer.json') ||
|
|
17
|
+
file.endsWith('/composer.lock')
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
const shouldRunMigrations =
|
|
21
|
+
isLaravel &&
|
|
22
|
+
safeChangedFiles.some((file) => file.startsWith('database/migrations/') && file.endsWith('.php'))
|
|
23
|
+
|
|
24
|
+
const hasPhpChanges = isLaravel && safeChangedFiles.some((file) => file.endsWith('.php'))
|
|
25
|
+
|
|
26
|
+
const shouldRunNpmInstall =
|
|
27
|
+
isLaravel &&
|
|
28
|
+
safeChangedFiles.some(
|
|
29
|
+
(file) =>
|
|
30
|
+
file === 'package.json' ||
|
|
31
|
+
file === 'package-lock.json' ||
|
|
32
|
+
file.endsWith('/package.json') ||
|
|
33
|
+
file.endsWith('/package-lock.json')
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const hasFrontendChanges =
|
|
37
|
+
isLaravel &&
|
|
38
|
+
safeChangedFiles.some((file) =>
|
|
39
|
+
['.vue', '.css', '.scss', '.js', '.ts', '.tsx', '.less'].some((ext) => file.endsWith(ext))
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const shouldRunBuild = isLaravel && (hasFrontendChanges || shouldRunNpmInstall)
|
|
43
|
+
const shouldClearCaches = hasPhpChanges
|
|
44
|
+
const shouldRestartQueues = hasPhpChanges
|
|
45
|
+
|
|
46
|
+
const steps = [
|
|
47
|
+
{
|
|
48
|
+
label: `Pull latest changes for ${branch}`,
|
|
49
|
+
command: `git pull origin ${branch}`
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
if (shouldRunComposer) {
|
|
54
|
+
// Composer is a PHP script, so we need to run it with the correct PHP version
|
|
55
|
+
// Try composer.phar first, then system composer, ensuring it uses the correct PHP
|
|
56
|
+
steps.push({
|
|
57
|
+
label: 'Update Composer dependencies',
|
|
58
|
+
command: `COMPOSER_ALLOW_SUPERUSER=1 if [ -f composer.phar ]; then ${phpCommand} composer.phar update --no-dev --no-interaction --prefer-dist; elif command -v composer >/dev/null 2>&1; then ${phpCommand} $(command -v composer) update --no-dev --no-interaction --prefer-dist; else ${phpCommand} composer update --no-dev --no-interaction --prefer-dist; fi`
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (shouldRunMigrations) {
|
|
63
|
+
steps.push({
|
|
64
|
+
label: 'Run database migrations',
|
|
65
|
+
command: `${phpCommand} artisan migrate --force`
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (shouldRunNpmInstall) {
|
|
70
|
+
steps.push({
|
|
71
|
+
label: 'Install Node dependencies',
|
|
72
|
+
command: 'npm install'
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (shouldRunBuild) {
|
|
77
|
+
steps.push({
|
|
78
|
+
label: 'Compile frontend assets',
|
|
79
|
+
command: 'npm run build'
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (shouldClearCaches) {
|
|
84
|
+
steps.push({
|
|
85
|
+
label: 'Clear Laravel caches',
|
|
86
|
+
command: `${phpCommand} artisan cache:clear && ${phpCommand} artisan config:clear && ${phpCommand} artisan view:clear`
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (shouldRestartQueues) {
|
|
91
|
+
steps.push({
|
|
92
|
+
label: horizonConfigured ? 'Restart Horizon workers' : 'Restart queue workers',
|
|
93
|
+
command: horizonConfigured ? `${phpCommand} artisan horizon:terminate` : `${phpCommand} artisan queue:restart`
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return steps
|
|
98
|
+
}
|
|
99
|
+
|
package/src/version-checker.mjs
CHANGED
|
@@ -1,162 +1,162 @@
|
|
|
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
|
-
}
|
|
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
|
+
}
|