@wyxos/zephyr 0.2.16 → 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 +27 -5
- package/bin/zephyr.mjs +9 -18
- package/package.json +5 -1
- package/src/dependency-scanner.mjs +47 -17
- package/src/index.mjs +79 -69
- package/src/release-node.mjs +45 -26
- package/src/release-packagist.mjs +26 -10
- package/src/ssh-utils.mjs +18 -7
- package/src/version-checker.mjs +50 -14
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
|
|
53
|
-
- **
|
|
54
|
-
- **package.json
|
|
55
|
-
- **Frontend files changed
|
|
56
|
-
-
|
|
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,24 +1,15 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import inquirer from 'inquirer'
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from 'node:process'
|
|
3
|
+
import { logError, main } from '../src/index.mjs'
|
|
5
4
|
|
|
6
5
|
// Parse --type flag from command line arguments
|
|
7
6
|
const args = process.argv.slice(2)
|
|
8
7
|
const typeFlag = args.find(arg => arg.startsWith('--type='))
|
|
9
8
|
const releaseType = typeFlag ? typeFlag.split('=')[1] : null
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
// No update or user declined, continue with normal execution
|
|
19
|
-
return main(releaseType)
|
|
20
|
-
})
|
|
21
|
-
.catch((error) => {
|
|
22
|
-
console.error(error.message)
|
|
23
|
-
process.exit(1)
|
|
24
|
-
})
|
|
10
|
+
try {
|
|
11
|
+
await main(releaseType)
|
|
12
|
+
} catch (error) {
|
|
13
|
+
logError(error?.message || String(error))
|
|
14
|
+
process.exitCode = 1
|
|
15
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wyxos/zephyr",
|
|
3
|
-
"version": "0.2.
|
|
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",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
15
|
"test": "vitest run",
|
|
16
|
+
"lint": "eslint .",
|
|
16
17
|
"release": "node bin/zephyr.mjs --type=node"
|
|
17
18
|
},
|
|
18
19
|
"keywords": [
|
|
@@ -48,6 +49,9 @@
|
|
|
48
49
|
"semver": "^7.6.3"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
52
|
+
"@eslint/js": "^9.39.2",
|
|
53
|
+
"eslint": "^9.39.2",
|
|
54
|
+
"globals": "^17.0.0",
|
|
51
55
|
"vitest": "^2.1.8"
|
|
52
56
|
}
|
|
53
57
|
}
|
|
@@ -2,6 +2,7 @@ import { readFile, writeFile } from 'node:fs/promises'
|
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { spawn } from 'node:child_process'
|
|
4
4
|
import process from 'node:process'
|
|
5
|
+
import chalk from 'chalk'
|
|
5
6
|
|
|
6
7
|
const IS_WINDOWS = process.platform === 'win32'
|
|
7
8
|
|
|
@@ -147,7 +148,7 @@ async function fetchLatestNpmVersion(packageName) {
|
|
|
147
148
|
}
|
|
148
149
|
const data = await response.json()
|
|
149
150
|
return data.version || null
|
|
150
|
-
} catch (
|
|
151
|
+
} catch (_error) {
|
|
151
152
|
return null
|
|
152
153
|
}
|
|
153
154
|
}
|
|
@@ -166,7 +167,7 @@ async function fetchLatestPackagistVersion(packageName) {
|
|
|
166
167
|
return latest.version || null
|
|
167
168
|
}
|
|
168
169
|
return null
|
|
169
|
-
} catch (
|
|
170
|
+
} catch (_error) {
|
|
170
171
|
return null
|
|
171
172
|
}
|
|
172
173
|
}
|
|
@@ -218,16 +219,16 @@ async function updateComposerJsonDependency(rootDir, packageName, newVersion, fi
|
|
|
218
219
|
|
|
219
220
|
async function runCommand(command, args, { cwd = process.cwd(), capture = false } = {}) {
|
|
220
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
|
+
|
|
221
226
|
const spawnOptions = {
|
|
222
227
|
stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
223
228
|
cwd
|
|
224
229
|
}
|
|
225
230
|
|
|
226
|
-
|
|
227
|
-
spawnOptions.shell = true
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const child = spawn(command, args, spawnOptions)
|
|
231
|
+
const child = spawn(resolvedCommand, args, spawnOptions)
|
|
231
232
|
let stdout = ''
|
|
232
233
|
let stderr = ''
|
|
233
234
|
|
|
@@ -246,7 +247,7 @@ async function runCommand(command, args, { cwd = process.cwd(), capture = false
|
|
|
246
247
|
if (code === 0) {
|
|
247
248
|
resolve(capture ? { stdout: stdout.trim(), stderr: stderr.trim() } : undefined)
|
|
248
249
|
} else {
|
|
249
|
-
const error = new Error(`Command failed (${code}): ${
|
|
250
|
+
const error = new Error(`Command failed (${code}): ${resolvedCommand} ${args.join(' ')}`)
|
|
250
251
|
if (capture) {
|
|
251
252
|
error.stdout = stdout
|
|
252
253
|
error.stderr = stderr
|
|
@@ -262,7 +263,7 @@ async function getGitStatus(rootDir) {
|
|
|
262
263
|
try {
|
|
263
264
|
const result = await runCommand('git', ['status', '--porcelain'], { capture: true, cwd: rootDir })
|
|
264
265
|
return result.stdout || ''
|
|
265
|
-
} catch (
|
|
266
|
+
} catch (_error) {
|
|
266
267
|
return ''
|
|
267
268
|
}
|
|
268
269
|
}
|
|
@@ -280,7 +281,7 @@ function hasStagedChanges(statusOutput) {
|
|
|
280
281
|
})
|
|
281
282
|
}
|
|
282
283
|
|
|
283
|
-
async function commitDependencyUpdates(rootDir, updatedFiles, logFn) {
|
|
284
|
+
async function commitDependencyUpdates(rootDir, updatedFiles, promptFn, logFn) {
|
|
284
285
|
try {
|
|
285
286
|
// Check if we're in a git repository
|
|
286
287
|
await runCommand('git', ['rev-parse', '--is-inside-work-tree'], { capture: true, cwd: rootDir })
|
|
@@ -289,7 +290,30 @@ async function commitDependencyUpdates(rootDir, updatedFiles, logFn) {
|
|
|
289
290
|
return false
|
|
290
291
|
}
|
|
291
292
|
|
|
292
|
-
const
|
|
293
|
+
const statusBefore = await getGitStatus(rootDir)
|
|
294
|
+
|
|
295
|
+
// Avoid accidentally committing unrelated staged changes
|
|
296
|
+
if (hasStagedChanges(statusBefore)) {
|
|
297
|
+
if (logFn) {
|
|
298
|
+
logFn('Staged changes detected. Skipping auto-commit of dependency updates.')
|
|
299
|
+
}
|
|
300
|
+
return false
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const fileList = updatedFiles.map((f) => path.basename(f)).join(', ')
|
|
304
|
+
|
|
305
|
+
const { shouldCommit } = await promptFn([
|
|
306
|
+
{
|
|
307
|
+
type: 'confirm',
|
|
308
|
+
name: 'shouldCommit',
|
|
309
|
+
message: `Commit dependency updates now? (${fileList})`,
|
|
310
|
+
default: true
|
|
311
|
+
}
|
|
312
|
+
])
|
|
313
|
+
|
|
314
|
+
if (!shouldCommit) {
|
|
315
|
+
return false
|
|
316
|
+
}
|
|
293
317
|
|
|
294
318
|
// Stage the updated files
|
|
295
319
|
for (const file of updatedFiles) {
|
|
@@ -306,7 +330,6 @@ async function commitDependencyUpdates(rootDir, updatedFiles, logFn) {
|
|
|
306
330
|
}
|
|
307
331
|
|
|
308
332
|
// Build commit message
|
|
309
|
-
const fileList = updatedFiles.map(f => path.basename(f)).join(', ')
|
|
310
333
|
const commitMessage = `chore: update local file dependencies to online versions (${fileList})`
|
|
311
334
|
|
|
312
335
|
if (logFn) {
|
|
@@ -353,20 +376,27 @@ async function validateLocalDependencies(rootDir, promptFn, logFn = null) {
|
|
|
353
376
|
})
|
|
354
377
|
)
|
|
355
378
|
|
|
356
|
-
// Build warning messages
|
|
379
|
+
// Build warning messages with colored output (danger color for package name and version)
|
|
357
380
|
const messages = depsWithVersions.map((dep) => {
|
|
381
|
+
const packageNameColored = chalk.red(dep.packageName)
|
|
382
|
+
const pathColored = chalk.dim(dep.path)
|
|
358
383
|
const versionInfo = dep.latestVersion
|
|
359
|
-
? ` Latest version available: ${dep.latestVersion}.`
|
|
384
|
+
? ` Latest version available: ${chalk.red(dep.latestVersion)}.`
|
|
360
385
|
: ' Latest version could not be determined.'
|
|
361
|
-
return `Dependency
|
|
386
|
+
return `Dependency ${packageNameColored} is pointing to a local path outside the repository: ${pathColored}.${versionInfo}`
|
|
362
387
|
})
|
|
363
388
|
|
|
389
|
+
// Build the prompt message with colored count (danger color)
|
|
390
|
+
const countColored = chalk.red(allDeps.length)
|
|
391
|
+
const countText = allDeps.length === 1 ? 'dependency' : 'dependencies'
|
|
392
|
+
const promptMessage = `Found ${countColored} local file ${countText} pointing outside the repository:\n\n${messages.join('\n\n')}\n\nUpdate to latest version?`
|
|
393
|
+
|
|
364
394
|
// Prompt user
|
|
365
395
|
const { shouldUpdate } = await promptFn([
|
|
366
396
|
{
|
|
367
397
|
type: 'confirm',
|
|
368
398
|
name: 'shouldUpdate',
|
|
369
|
-
message:
|
|
399
|
+
message: promptMessage,
|
|
370
400
|
default: true
|
|
371
401
|
}
|
|
372
402
|
])
|
|
@@ -441,7 +471,7 @@ async function validateLocalDependencies(rootDir, promptFn, logFn = null) {
|
|
|
441
471
|
|
|
442
472
|
// Commit the changes if any files were updated
|
|
443
473
|
if (updatedFiles.size > 0) {
|
|
444
|
-
await commitDependencyUpdates(rootDir, Array.from(updatedFiles), logFn)
|
|
474
|
+
await commitDependencyUpdates(rootDir, Array.from(updatedFiles), promptFn, logFn)
|
|
445
475
|
}
|
|
446
476
|
}
|
|
447
477
|
|
package/src/index.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { NodeSSH } from 'node-ssh'
|
|
|
10
10
|
import { releaseNode } from './release-node.mjs'
|
|
11
11
|
import { releasePackagist } from './release-packagist.mjs'
|
|
12
12
|
import { validateLocalDependencies } from './dependency-scanner.mjs'
|
|
13
|
+
import { checkAndUpdateVersion } from './version-checker.mjs'
|
|
13
14
|
|
|
14
15
|
const IS_WINDOWS = process.platform === 'win32'
|
|
15
16
|
|
|
@@ -22,10 +23,20 @@ const PENDING_TASKS_FILE = 'pending-tasks.json'
|
|
|
22
23
|
const RELEASE_SCRIPT_NAME = 'release'
|
|
23
24
|
const RELEASE_SCRIPT_COMMAND = 'npx @wyxos/zephyr@latest'
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
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))
|
|
29
40
|
|
|
30
41
|
let logFilePath = null
|
|
31
42
|
|
|
@@ -89,7 +100,7 @@ async function cleanupOldLogs(rootDir) {
|
|
|
89
100
|
for (const file of filesToDelete) {
|
|
90
101
|
try {
|
|
91
102
|
await fs.unlink(file.path)
|
|
92
|
-
} catch (
|
|
103
|
+
} catch (_error) {
|
|
93
104
|
// Ignore errors when deleting old logs
|
|
94
105
|
}
|
|
95
106
|
}
|
|
@@ -119,25 +130,23 @@ const runPrompt = async (questions) => {
|
|
|
119
130
|
|
|
120
131
|
async function runCommand(command, args, { silent = false, cwd } = {}) {
|
|
121
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
|
+
|
|
122
137
|
const spawnOptions = {
|
|
123
138
|
stdio: silent ? 'ignore' : 'inherit',
|
|
124
139
|
cwd
|
|
125
140
|
}
|
|
126
141
|
|
|
127
|
-
|
|
128
|
-
// Git commands work fine without shell
|
|
129
|
-
if (IS_WINDOWS && command !== 'git') {
|
|
130
|
-
spawnOptions.shell = true
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const child = spawn(command, args, spawnOptions)
|
|
142
|
+
const child = spawn(resolvedCommand, args, spawnOptions)
|
|
134
143
|
|
|
135
144
|
child.on('error', reject)
|
|
136
145
|
child.on('close', (code) => {
|
|
137
146
|
if (code === 0) {
|
|
138
147
|
resolve()
|
|
139
148
|
} else {
|
|
140
|
-
const error = new Error(`${
|
|
149
|
+
const error = new Error(`${resolvedCommand} exited with code ${code}`)
|
|
141
150
|
error.exitCode = code
|
|
142
151
|
reject(error)
|
|
143
152
|
}
|
|
@@ -147,6 +156,10 @@ async function runCommand(command, args, { silent = false, cwd } = {}) {
|
|
|
147
156
|
|
|
148
157
|
async function runCommandCapture(command, args, { cwd } = {}) {
|
|
149
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
|
+
|
|
150
163
|
let stdout = ''
|
|
151
164
|
let stderr = ''
|
|
152
165
|
|
|
@@ -155,13 +168,7 @@ async function runCommandCapture(command, args, { cwd } = {}) {
|
|
|
155
168
|
cwd
|
|
156
169
|
}
|
|
157
170
|
|
|
158
|
-
|
|
159
|
-
// Git commands work fine without shell
|
|
160
|
-
if (IS_WINDOWS && command !== 'git') {
|
|
161
|
-
spawnOptions.shell = true
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const child = spawn(command, args, spawnOptions)
|
|
171
|
+
const child = spawn(resolvedCommand, args, spawnOptions)
|
|
165
172
|
|
|
166
173
|
child.stdout.on('data', (chunk) => {
|
|
167
174
|
stdout += chunk
|
|
@@ -176,7 +183,7 @@ async function runCommandCapture(command, args, { cwd } = {}) {
|
|
|
176
183
|
if (code === 0) {
|
|
177
184
|
resolve(stdout)
|
|
178
185
|
} else {
|
|
179
|
-
const error = new Error(`${
|
|
186
|
+
const error = new Error(`${resolvedCommand} exited with code ${code}: ${stderr.trim()}`)
|
|
180
187
|
error.exitCode = code
|
|
181
188
|
reject(error)
|
|
182
189
|
}
|
|
@@ -296,7 +303,9 @@ async function ensureCommittedChangesPushed(targetBranch, rootDir) {
|
|
|
296
303
|
const commitLabel = aheadCount === 1 ? 'commit' : 'commits'
|
|
297
304
|
logProcessing(`Found ${aheadCount} ${commitLabel} not yet pushed to ${upstreamRef}. Pushing before deployment...`)
|
|
298
305
|
|
|
299
|
-
|
|
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 })
|
|
300
309
|
logSuccess(`Pushed committed changes to ${upstreamRef}.`)
|
|
301
310
|
|
|
302
311
|
return { pushed: true, upstreamRef }
|
|
@@ -414,7 +423,7 @@ async function ensureProjectReleaseScript(rootDir) {
|
|
|
414
423
|
let packageJson
|
|
415
424
|
try {
|
|
416
425
|
packageJson = JSON.parse(raw)
|
|
417
|
-
} catch (
|
|
426
|
+
} catch (_error) {
|
|
418
427
|
logWarning('Unable to parse package.json; skipping release script injection.')
|
|
419
428
|
return false
|
|
420
429
|
}
|
|
@@ -453,7 +462,7 @@ async function ensureProjectReleaseScript(rootDir) {
|
|
|
453
462
|
try {
|
|
454
463
|
await runCommand('git', ['rev-parse', '--is-inside-work-tree'], { cwd: rootDir, silent: true })
|
|
455
464
|
isGitRepo = true
|
|
456
|
-
} catch (
|
|
465
|
+
} catch (_error) {
|
|
457
466
|
logWarning('Not a git repository; skipping commit for release script addition.')
|
|
458
467
|
}
|
|
459
468
|
|
|
@@ -541,7 +550,7 @@ async function readRemoteLock(ssh, remoteCwd) {
|
|
|
541
550
|
if (checkResult.stdout && checkResult.stdout.trim() !== 'LOCK_NOT_FOUND' && checkResult.stdout.trim() !== '') {
|
|
542
551
|
try {
|
|
543
552
|
return JSON.parse(checkResult.stdout.trim())
|
|
544
|
-
} catch (
|
|
553
|
+
} catch (_error) {
|
|
545
554
|
return { raw: checkResult.stdout.trim() }
|
|
546
555
|
}
|
|
547
556
|
}
|
|
@@ -605,7 +614,7 @@ async function acquireRemoteLock(ssh, remoteCwd, rootDir) {
|
|
|
605
614
|
let details = {}
|
|
606
615
|
try {
|
|
607
616
|
details = JSON.parse(checkResult.stdout.trim())
|
|
608
|
-
} catch (
|
|
617
|
+
} catch (_error) {
|
|
609
618
|
details = { raw: checkResult.stdout.trim() }
|
|
610
619
|
}
|
|
611
620
|
|
|
@@ -620,7 +629,7 @@ async function acquireRemoteLock(ssh, remoteCwd, rootDir) {
|
|
|
620
629
|
let details = {}
|
|
621
630
|
try {
|
|
622
631
|
details = JSON.parse(checkResult.stdout.trim())
|
|
623
|
-
} catch (
|
|
632
|
+
} catch (_error) {
|
|
624
633
|
details = { raw: checkResult.stdout.trim() }
|
|
625
634
|
}
|
|
626
635
|
|
|
@@ -727,7 +736,7 @@ async function ensureGitignoreEntry(rootDir) {
|
|
|
727
736
|
cwd: rootDir
|
|
728
737
|
})
|
|
729
738
|
isGitRepo = true
|
|
730
|
-
} catch (
|
|
739
|
+
} catch (_error) {
|
|
731
740
|
logWarning('Not a git repository; skipping commit for .gitignore update.')
|
|
732
741
|
}
|
|
733
742
|
|
|
@@ -949,7 +958,7 @@ async function listGitBranches(currentDir) {
|
|
|
949
958
|
.filter(Boolean)
|
|
950
959
|
|
|
951
960
|
return branches.length ? branches : ['master']
|
|
952
|
-
} catch (
|
|
961
|
+
} catch (_error) {
|
|
953
962
|
logWarning('Unable to read git branches; defaulting to master.')
|
|
954
963
|
return ['master']
|
|
955
964
|
}
|
|
@@ -1002,7 +1011,7 @@ async function isPrivateKeyFile(filePath) {
|
|
|
1002
1011
|
try {
|
|
1003
1012
|
const content = await fs.readFile(filePath, 'utf8')
|
|
1004
1013
|
return /-----BEGIN [A-Z ]*PRIVATE KEY-----/.test(content)
|
|
1005
|
-
} catch (
|
|
1014
|
+
} catch (_error) {
|
|
1006
1015
|
return false
|
|
1007
1016
|
}
|
|
1008
1017
|
}
|
|
@@ -1091,7 +1100,7 @@ async function resolveSshKeyPath(targetPath) {
|
|
|
1091
1100
|
|
|
1092
1101
|
try {
|
|
1093
1102
|
await fs.access(expanded)
|
|
1094
|
-
} catch (
|
|
1103
|
+
} catch (_error) {
|
|
1095
1104
|
throw new Error(`SSH key not accessible at ${expanded}`)
|
|
1096
1105
|
}
|
|
1097
1106
|
|
|
@@ -1247,14 +1256,14 @@ async function runRemoteTasks(config, options = {}) {
|
|
|
1247
1256
|
}
|
|
1248
1257
|
|
|
1249
1258
|
// Run tests for Laravel projects
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1259
|
+
if (isLaravel) {
|
|
1260
|
+
logProcessing('Running Laravel tests locally...')
|
|
1261
|
+
try {
|
|
1262
|
+
await runCommand('php', ['artisan', 'test', '--compact'], { cwd: rootDir })
|
|
1263
|
+
logSuccess('Local tests passed.')
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
throw new Error(`Local tests failed. Fix test failures before deploying. ${error.message}`)
|
|
1266
|
+
}
|
|
1258
1267
|
}
|
|
1259
1268
|
} else {
|
|
1260
1269
|
logProcessing('Pre-push git hook detected. Skipping local linting and test execution.')
|
|
@@ -1314,7 +1323,7 @@ async function runRemoteTasks(config, options = {}) {
|
|
|
1314
1323
|
const escapeForDoubleQuotes = (value) => value.replace(/(["\\$`])/g, '\\$1')
|
|
1315
1324
|
|
|
1316
1325
|
const executeRemote = async (label, command, options = {}) => {
|
|
1317
|
-
const { cwd = remoteCwd, allowFailure = false,
|
|
1326
|
+
const { cwd = remoteCwd, allowFailure = false, bootstrapEnv = true } = options
|
|
1318
1327
|
logProcessing(`\n→ ${label}`)
|
|
1319
1328
|
|
|
1320
1329
|
let wrappedCommand = command
|
|
@@ -1583,7 +1592,7 @@ async function runRemoteTasks(config, options = {}) {
|
|
|
1583
1592
|
const remoteHome = remoteHomeResult.stdout.trim() || `/home/${sshUser}`
|
|
1584
1593
|
const remoteCwd = resolveRemotePath(config.projectPath, remoteHome)
|
|
1585
1594
|
await compareLocksAndPrompt(rootDir, ssh, remoteCwd)
|
|
1586
|
-
} catch (
|
|
1595
|
+
} catch (_lockError) {
|
|
1587
1596
|
// Ignore lock comparison errors during error handling
|
|
1588
1597
|
}
|
|
1589
1598
|
}
|
|
@@ -1739,9 +1748,9 @@ async function selectApp(projectConfig, server, currentDir) {
|
|
|
1739
1748
|
if (apps.length > 0) {
|
|
1740
1749
|
const availableServers = [...new Set(apps.map((app) => app.serverName).filter(Boolean))]
|
|
1741
1750
|
if (availableServers.length > 0) {
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1751
|
+
logWarning(
|
|
1752
|
+
`No applications configured for server "${server.serverName}". Available servers: ${availableServers.join(', ')}`
|
|
1753
|
+
)
|
|
1745
1754
|
}
|
|
1746
1755
|
}
|
|
1747
1756
|
logProcessing(`No applications configured for ${server.serverName}. Let's create one.`)
|
|
@@ -1758,7 +1767,7 @@ async function selectApp(projectConfig, server, currentDir) {
|
|
|
1758
1767
|
return appConfig
|
|
1759
1768
|
}
|
|
1760
1769
|
|
|
1761
|
-
const choices = matches.map(({ app
|
|
1770
|
+
const choices = matches.map(({ app }, matchIndex) => ({
|
|
1762
1771
|
name: `${app.projectPath} (${app.branch})`,
|
|
1763
1772
|
value: matchIndex
|
|
1764
1773
|
}))
|
|
@@ -1796,23 +1805,6 @@ async function selectApp(projectConfig, server, currentDir) {
|
|
|
1796
1805
|
return chosen
|
|
1797
1806
|
}
|
|
1798
1807
|
|
|
1799
|
-
async function promptPresetName() {
|
|
1800
|
-
const { presetName } = await runPrompt([
|
|
1801
|
-
{
|
|
1802
|
-
type: 'input',
|
|
1803
|
-
name: 'presetName',
|
|
1804
|
-
message: 'Enter a name for this preset',
|
|
1805
|
-
validate: (value) => (value && value.trim().length > 0 ? true : 'Preset name cannot be empty.')
|
|
1806
|
-
}
|
|
1807
|
-
])
|
|
1808
|
-
|
|
1809
|
-
return presetName.trim()
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
function generatePresetKey(serverName, projectPath) {
|
|
1813
|
-
return `${serverName}:${projectPath}`
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
1808
|
async function selectPreset(projectConfig, servers) {
|
|
1817
1809
|
const presets = projectConfig.presets ?? []
|
|
1818
1810
|
const apps = projectConfig.apps ?? []
|
|
@@ -1844,7 +1836,7 @@ async function selectPreset(projectConfig, servers) {
|
|
|
1844
1836
|
|
|
1845
1837
|
return {
|
|
1846
1838
|
name: displayName,
|
|
1847
|
-
|
|
1839
|
+
value: index
|
|
1848
1840
|
}
|
|
1849
1841
|
})
|
|
1850
1842
|
|
|
@@ -1871,6 +1863,24 @@ async function selectPreset(projectConfig, servers) {
|
|
|
1871
1863
|
}
|
|
1872
1864
|
|
|
1873
1865
|
async function main(releaseType = null) {
|
|
1866
|
+
// Best-effort update check (skip during tests or when explicitly disabled)
|
|
1867
|
+
// If an update is accepted, the process will re-execute via npx @latest and we should exit early.
|
|
1868
|
+
if (
|
|
1869
|
+
process.env.ZEPHYR_SKIP_VERSION_CHECK !== '1' &&
|
|
1870
|
+
process.env.NODE_ENV !== 'test' &&
|
|
1871
|
+
process.env.VITEST !== 'true'
|
|
1872
|
+
) {
|
|
1873
|
+
try {
|
|
1874
|
+
const args = process.argv.slice(2)
|
|
1875
|
+
const reExecuted = await checkAndUpdateVersion(runPrompt, args)
|
|
1876
|
+
if (reExecuted) {
|
|
1877
|
+
return
|
|
1878
|
+
}
|
|
1879
|
+
} catch (_error) {
|
|
1880
|
+
// Never block execution due to update check issues
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1874
1884
|
// Handle node/vue package release
|
|
1875
1885
|
if (releaseType === 'node' || releaseType === 'vue') {
|
|
1876
1886
|
try {
|
|
@@ -1880,7 +1890,7 @@ async function main(releaseType = null) {
|
|
|
1880
1890
|
logError('\nRelease failed:')
|
|
1881
1891
|
logError(error.message)
|
|
1882
1892
|
if (error.stack) {
|
|
1883
|
-
|
|
1893
|
+
writeStderrLine(error.stack)
|
|
1884
1894
|
}
|
|
1885
1895
|
process.exit(1)
|
|
1886
1896
|
}
|
|
@@ -1895,7 +1905,7 @@ async function main(releaseType = null) {
|
|
|
1895
1905
|
logError('\nRelease failed:')
|
|
1896
1906
|
logError(error.message)
|
|
1897
1907
|
if (error.stack) {
|
|
1898
|
-
|
|
1908
|
+
writeStderrLine(error.stack)
|
|
1899
1909
|
}
|
|
1900
1910
|
process.exit(1)
|
|
1901
1911
|
}
|
|
@@ -2016,7 +2026,7 @@ async function main(releaseType = null) {
|
|
|
2016
2026
|
}
|
|
2017
2027
|
|
|
2018
2028
|
logProcessing('\nSelected deployment target:')
|
|
2019
|
-
|
|
2029
|
+
writeStdoutLine(JSON.stringify(deploymentConfig, null, 2))
|
|
2020
2030
|
|
|
2021
2031
|
if (isCreatingNewPreset || !preset) {
|
|
2022
2032
|
const { presetName } = await runPrompt([
|
|
@@ -2041,11 +2051,11 @@ async function main(releaseType = null) {
|
|
|
2041
2051
|
} else {
|
|
2042
2052
|
// Check if preset with this appId already exists
|
|
2043
2053
|
const existingIndex = presets.findIndex((p) => p.appId === appId)
|
|
2044
|
-
|
|
2054
|
+
if (existingIndex >= 0) {
|
|
2045
2055
|
presets[existingIndex].name = trimmedName
|
|
2046
2056
|
presets[existingIndex].branch = deploymentConfig.branch
|
|
2047
|
-
|
|
2048
|
-
|
|
2057
|
+
} else {
|
|
2058
|
+
presets.push({
|
|
2049
2059
|
name: trimmedName,
|
|
2050
2060
|
appId: appId,
|
|
2051
2061
|
branch: deploymentConfig.branch
|
package/src/release-node.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { spawn, exec } from 'node:child_process'
|
|
2
|
-
import {
|
|
3
|
-
import { dirname, join } from 'node:path'
|
|
2
|
+
import { join } from 'node:path'
|
|
4
3
|
import { readFile } from 'node:fs/promises'
|
|
5
4
|
import fs from 'node:fs'
|
|
6
5
|
import path from 'node:path'
|
|
@@ -11,16 +10,34 @@ import { validateLocalDependencies } from './dependency-scanner.mjs'
|
|
|
11
10
|
|
|
12
11
|
const IS_WINDOWS = process.platform === 'win32'
|
|
13
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
|
+
|
|
14
31
|
function logStep(message) {
|
|
15
|
-
|
|
32
|
+
writeStdoutLine(chalk.yellow(`→ ${message}`))
|
|
16
33
|
}
|
|
17
34
|
|
|
18
35
|
function logSuccess(message) {
|
|
19
|
-
|
|
36
|
+
writeStdoutLine(chalk.green(`✔ ${message}`))
|
|
20
37
|
}
|
|
21
38
|
|
|
22
39
|
function logWarning(message) {
|
|
23
|
-
|
|
40
|
+
writeStderrLine(chalk.yellow(`⚠ ${message}`))
|
|
24
41
|
}
|
|
25
42
|
|
|
26
43
|
function runCommand(command, args, { cwd = process.cwd(), capture = false, useShell = false } = {}) {
|
|
@@ -156,7 +173,7 @@ async function ensureUpToDateWithUpstream(branch, upstreamRef, rootDir = process
|
|
|
156
173
|
await runCommand('git', ['fetch', remoteName, remoteBranch], { capture: true, cwd: rootDir })
|
|
157
174
|
} catch (error) {
|
|
158
175
|
if (error.stderr) {
|
|
159
|
-
|
|
176
|
+
writeStderr(error.stderr)
|
|
160
177
|
}
|
|
161
178
|
throw new Error(`Failed to fetch ${upstreamRef}: ${error.message}`)
|
|
162
179
|
}
|
|
@@ -182,7 +199,7 @@ async function ensureUpToDateWithUpstream(branch, upstreamRef, rootDir = process
|
|
|
182
199
|
await runCommand('git', ['pull', '--ff-only', remoteName, remoteBranch], { capture: true, cwd: rootDir })
|
|
183
200
|
} catch (error) {
|
|
184
201
|
if (error.stderr) {
|
|
185
|
-
|
|
202
|
+
writeStderr(error.stderr)
|
|
186
203
|
}
|
|
187
204
|
throw new Error(
|
|
188
205
|
`Unable to fast-forward ${branch} with ${upstreamRef}. Resolve conflicts manually, then rerun the release.\n${error.message}`
|
|
@@ -252,10 +269,10 @@ async function runLint(skipLint, pkg, rootDir = process.cwd()) {
|
|
|
252
269
|
logSuccess('Lint passed.')
|
|
253
270
|
} catch (error) {
|
|
254
271
|
if (error.stdout) {
|
|
255
|
-
|
|
272
|
+
writeStderr(error.stdout)
|
|
256
273
|
}
|
|
257
274
|
if (error.stderr) {
|
|
258
|
-
|
|
275
|
+
writeStderr(error.stderr)
|
|
259
276
|
}
|
|
260
277
|
throw error
|
|
261
278
|
}
|
|
@@ -288,10 +305,10 @@ async function runTests(skipTests, pkg, rootDir = process.cwd()) {
|
|
|
288
305
|
logSuccess('Tests passed.')
|
|
289
306
|
} catch (error) {
|
|
290
307
|
if (error.stdout) {
|
|
291
|
-
|
|
308
|
+
writeStderr(error.stdout)
|
|
292
309
|
}
|
|
293
310
|
if (error.stderr) {
|
|
294
|
-
|
|
311
|
+
writeStderr(error.stderr)
|
|
295
312
|
}
|
|
296
313
|
throw error
|
|
297
314
|
}
|
|
@@ -315,10 +332,10 @@ async function runBuild(skipBuild, pkg, rootDir = process.cwd()) {
|
|
|
315
332
|
logSuccess('Build completed.')
|
|
316
333
|
} catch (error) {
|
|
317
334
|
if (error.stdout) {
|
|
318
|
-
|
|
335
|
+
writeStderr(error.stdout)
|
|
319
336
|
}
|
|
320
337
|
if (error.stderr) {
|
|
321
|
-
|
|
338
|
+
writeStderr(error.stderr)
|
|
322
339
|
}
|
|
323
340
|
throw error
|
|
324
341
|
}
|
|
@@ -342,10 +359,10 @@ async function runLibBuild(skipBuild, pkg, rootDir = process.cwd()) {
|
|
|
342
359
|
logSuccess('Library built.')
|
|
343
360
|
} catch (error) {
|
|
344
361
|
if (error.stdout) {
|
|
345
|
-
|
|
362
|
+
writeStderr(error.stdout)
|
|
346
363
|
}
|
|
347
364
|
if (error.stderr) {
|
|
348
|
-
|
|
365
|
+
writeStderr(error.stderr)
|
|
349
366
|
}
|
|
350
367
|
throw error
|
|
351
368
|
}
|
|
@@ -378,7 +395,7 @@ async function ensureNpmAuth(rootDir = process.cwd()) {
|
|
|
378
395
|
logSuccess('npm authenticated.')
|
|
379
396
|
} catch (error) {
|
|
380
397
|
if (error.stderr) {
|
|
381
|
-
|
|
398
|
+
writeStderr(error.stderr)
|
|
382
399
|
}
|
|
383
400
|
throw error
|
|
384
401
|
}
|
|
@@ -439,10 +456,10 @@ async function pushChanges(rootDir = process.cwd()) {
|
|
|
439
456
|
logSuccess('Git push completed.')
|
|
440
457
|
} catch (error) {
|
|
441
458
|
if (error.stdout) {
|
|
442
|
-
|
|
459
|
+
writeStderr(error.stdout)
|
|
443
460
|
}
|
|
444
461
|
if (error.stderr) {
|
|
445
|
-
|
|
462
|
+
writeStderr(error.stderr)
|
|
446
463
|
}
|
|
447
464
|
throw error
|
|
448
465
|
}
|
|
@@ -473,10 +490,10 @@ async function publishPackage(pkg, rootDir = process.cwd()) {
|
|
|
473
490
|
logSuccess('npm publish completed.')
|
|
474
491
|
} catch (error) {
|
|
475
492
|
if (error.stdout) {
|
|
476
|
-
|
|
493
|
+
writeStderr(error.stdout)
|
|
477
494
|
}
|
|
478
495
|
if (error.stderr) {
|
|
479
|
-
|
|
496
|
+
writeStderr(error.stderr)
|
|
480
497
|
}
|
|
481
498
|
throw error
|
|
482
499
|
}
|
|
@@ -489,7 +506,7 @@ function extractDomainFromHomepage(homepage) {
|
|
|
489
506
|
return url.hostname
|
|
490
507
|
} catch {
|
|
491
508
|
// If it's not a valid URL, try to extract domain from string
|
|
492
|
-
const match = homepage.match(/(?:https?:\/\/)?([
|
|
509
|
+
const match = homepage.match(/(?:https?:\/\/)?([^/]+)/)
|
|
493
510
|
return match ? match[1] : null
|
|
494
511
|
}
|
|
495
512
|
}
|
|
@@ -537,7 +554,9 @@ async function deployGHPages(skipDeploy, pkg, rootDir = process.cwd()) {
|
|
|
537
554
|
try {
|
|
538
555
|
try {
|
|
539
556
|
await runCommand('git', ['worktree', 'remove', worktreeDir, '-f'], { capture: true, cwd: rootDir })
|
|
540
|
-
} catch {
|
|
557
|
+
} catch (_error) {
|
|
558
|
+
// Ignore if worktree doesn't exist
|
|
559
|
+
}
|
|
541
560
|
|
|
542
561
|
try {
|
|
543
562
|
await runCommand('git', ['worktree', 'add', worktreeDir, 'gh-pages'], { capture: true, cwd: rootDir })
|
|
@@ -565,10 +584,10 @@ async function deployGHPages(skipDeploy, pkg, rootDir = process.cwd()) {
|
|
|
565
584
|
logSuccess('GitHub Pages deployment completed.')
|
|
566
585
|
} catch (error) {
|
|
567
586
|
if (error.stdout) {
|
|
568
|
-
|
|
587
|
+
writeStderr(error.stdout)
|
|
569
588
|
}
|
|
570
589
|
if (error.stderr) {
|
|
571
|
-
|
|
590
|
+
writeStderr(error.stderr)
|
|
572
591
|
}
|
|
573
592
|
throw error
|
|
574
593
|
}
|
|
@@ -610,8 +629,8 @@ export async function releaseNode() {
|
|
|
610
629
|
|
|
611
630
|
logSuccess(`Release workflow completed for ${updatedPkg.name}@${updatedPkg.version}.`)
|
|
612
631
|
} catch (error) {
|
|
613
|
-
|
|
614
|
-
|
|
632
|
+
writeStderrLine('\nRelease failed:')
|
|
633
|
+
writeStderrLine(error.message)
|
|
615
634
|
throw error
|
|
616
635
|
}
|
|
617
636
|
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
|
-
import {
|
|
3
|
-
import { dirname, join } from 'node:path'
|
|
2
|
+
import { join } from 'node:path'
|
|
4
3
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
5
4
|
import fs from 'node:fs'
|
|
6
|
-
import path from 'node:path'
|
|
7
5
|
import process from 'node:process'
|
|
8
6
|
import semver from 'semver'
|
|
9
7
|
import inquirer from 'inquirer'
|
|
@@ -15,16 +13,34 @@ const WARN_PREFIX = '⚠'
|
|
|
15
13
|
|
|
16
14
|
const IS_WINDOWS = process.platform === 'win32'
|
|
17
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
|
+
|
|
18
34
|
function logStep(message) {
|
|
19
|
-
|
|
35
|
+
writeStdoutLine(`${STEP_PREFIX} ${message}`)
|
|
20
36
|
}
|
|
21
37
|
|
|
22
38
|
function logSuccess(message) {
|
|
23
|
-
|
|
39
|
+
writeStdoutLine(`${OK_PREFIX} ${message}`)
|
|
24
40
|
}
|
|
25
41
|
|
|
26
42
|
function logWarning(message) {
|
|
27
|
-
|
|
43
|
+
writeStderrLine(`${WARN_PREFIX} ${message}`)
|
|
28
44
|
}
|
|
29
45
|
|
|
30
46
|
function runCommand(command, args, { cwd = process.cwd(), capture = false, useShell = false } = {}) {
|
|
@@ -271,10 +287,10 @@ async function runLint(skipLint, rootDir = process.cwd()) {
|
|
|
271
287
|
}
|
|
272
288
|
process.stdout.write('\n')
|
|
273
289
|
if (error.stdout) {
|
|
274
|
-
|
|
290
|
+
writeStderr(error.stdout)
|
|
275
291
|
}
|
|
276
292
|
if (error.stderr) {
|
|
277
|
-
|
|
293
|
+
writeStderr(error.stderr)
|
|
278
294
|
}
|
|
279
295
|
throw error
|
|
280
296
|
}
|
|
@@ -324,10 +340,10 @@ async function runTests(skipTests, composer, rootDir = process.cwd()) {
|
|
|
324
340
|
}
|
|
325
341
|
process.stdout.write('\n')
|
|
326
342
|
if (error.stdout) {
|
|
327
|
-
|
|
343
|
+
writeStderr(error.stdout)
|
|
328
344
|
}
|
|
329
345
|
if (error.stderr) {
|
|
330
|
-
|
|
346
|
+
writeStderr(error.stderr)
|
|
331
347
|
}
|
|
332
348
|
throw error
|
|
333
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
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
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) {
|
|
@@ -25,7 +36,7 @@ async function resolveSshKeyPath(targetPath) {
|
|
|
25
36
|
const expanded = expandHomePath(targetPath)
|
|
26
37
|
try {
|
|
27
38
|
await fs.access(expanded)
|
|
28
|
-
} catch (
|
|
39
|
+
} catch (_error) {
|
|
29
40
|
throw new Error(`SSH key not accessible at ${expanded}`)
|
|
30
41
|
}
|
|
31
42
|
return expanded
|
|
@@ -64,7 +75,7 @@ const createSshClient = () => {
|
|
|
64
75
|
* @param {string} rootDir - Local root directory for logging
|
|
65
76
|
* @returns {Promise<{ssh: NodeSSH, remoteCwd: string, remoteHome: string}>}
|
|
66
77
|
*/
|
|
67
|
-
export async function connectToServer(config,
|
|
78
|
+
export async function connectToServer(config, _rootDir) {
|
|
68
79
|
const ssh = createSshClient()
|
|
69
80
|
const sshUser = config.sshUser || os.userInfo().username
|
|
70
81
|
const privateKeyPath = await resolveSshKeyPath(config.sshKey)
|
|
@@ -96,7 +107,7 @@ export async function connectToServer(config, rootDir) {
|
|
|
96
107
|
* @returns {Promise<Object>} Command result
|
|
97
108
|
*/
|
|
98
109
|
export async function executeRemoteCommand(ssh, label, command, options = {}) {
|
|
99
|
-
const { cwd, allowFailure = false,
|
|
110
|
+
const { cwd, allowFailure = false, bootstrapEnv = true, rootDir = null, writeToLogFile = null, env = {} } = options
|
|
100
111
|
|
|
101
112
|
logProcessing(`\n→ ${label}`)
|
|
102
113
|
|
package/src/version-checker.mjs
CHANGED
|
@@ -3,9 +3,11 @@ import { fileURLToPath } from 'node:url'
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { spawn } from 'node:child_process'
|
|
5
5
|
import process from 'node:process'
|
|
6
|
+
import https from 'node:https'
|
|
6
7
|
import semver from 'semver'
|
|
7
8
|
|
|
8
9
|
const IS_WINDOWS = process.platform === 'win32'
|
|
10
|
+
const ZEPHYR_SKIP_VERSION_CHECK_ENV = 'ZEPHYR_SKIP_VERSION_CHECK'
|
|
9
11
|
|
|
10
12
|
async function getCurrentVersion() {
|
|
11
13
|
try {
|
|
@@ -18,21 +20,57 @@ async function getCurrentVersion() {
|
|
|
18
20
|
)
|
|
19
21
|
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'))
|
|
20
22
|
return packageJson.version
|
|
21
|
-
} catch (
|
|
23
|
+
} catch (_error) {
|
|
22
24
|
// If we can't read package.json, return null
|
|
23
25
|
return null
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
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
|
+
|
|
27
66
|
async function getLatestVersion() {
|
|
28
67
|
try {
|
|
29
|
-
const
|
|
30
|
-
if (!
|
|
68
|
+
const data = await httpsGetJson('https://registry.npmjs.org/@wyxos/zephyr/latest')
|
|
69
|
+
if (!data) {
|
|
31
70
|
return null
|
|
32
71
|
}
|
|
33
|
-
const data = await response.json()
|
|
34
72
|
return data.version || null
|
|
35
|
-
} catch (
|
|
73
|
+
} catch (_error) {
|
|
36
74
|
return null
|
|
37
75
|
}
|
|
38
76
|
}
|
|
@@ -45,7 +83,7 @@ function isNewerVersionAvailable(current, latest) {
|
|
|
45
83
|
// Use semver to properly compare versions
|
|
46
84
|
try {
|
|
47
85
|
return semver.gt(latest, current)
|
|
48
|
-
} catch (
|
|
86
|
+
} catch (_error) {
|
|
49
87
|
// If semver comparison fails, fall back to simple string comparison
|
|
50
88
|
return latest !== current
|
|
51
89
|
}
|
|
@@ -59,7 +97,10 @@ async function reExecuteWithLatest(args) {
|
|
|
59
97
|
return new Promise((resolve, reject) => {
|
|
60
98
|
const child = spawn(command, npxArgs, {
|
|
61
99
|
stdio: 'inherit',
|
|
62
|
-
|
|
100
|
+
env: {
|
|
101
|
+
...process.env,
|
|
102
|
+
[ZEPHYR_SKIP_VERSION_CHECK_ENV]: '1'
|
|
103
|
+
}
|
|
63
104
|
})
|
|
64
105
|
|
|
65
106
|
child.on('error', reject)
|
|
@@ -75,12 +116,7 @@ async function reExecuteWithLatest(args) {
|
|
|
75
116
|
|
|
76
117
|
export async function checkAndUpdateVersion(promptFn, args) {
|
|
77
118
|
try {
|
|
78
|
-
|
|
79
|
-
// When npx runs @latest, the version should already be latest
|
|
80
|
-
const isRunningLatest = process.env.npm_config_user_config?.includes('@latest') ||
|
|
81
|
-
process.argv.some(arg => arg.includes('@latest'))
|
|
82
|
-
|
|
83
|
-
if (isRunningLatest) {
|
|
119
|
+
if (process.env[ZEPHYR_SKIP_VERSION_CHECK_ENV] === '1') {
|
|
84
120
|
return false
|
|
85
121
|
}
|
|
86
122
|
|
|
@@ -118,7 +154,7 @@ export async function checkAndUpdateVersion(promptFn, args) {
|
|
|
118
154
|
// User confirmed, re-execute with latest version
|
|
119
155
|
await reExecuteWithLatest(args)
|
|
120
156
|
return true // Indicates we've re-executed, so the current process should exit
|
|
121
|
-
} catch (
|
|
157
|
+
} catch (_error) {
|
|
122
158
|
// If version check fails, just continue with current version
|
|
123
159
|
// Don't block the user from using the tool
|
|
124
160
|
return false
|