@wyxos/zephyr 0.2.15 → 0.2.17
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/bin/zephyr.mjs +8 -17
- package/package.json +5 -1
- package/src/dependency-scanner.mjs +158 -7
- package/src/index.mjs +48 -46
- package/src/release-node.mjs +6 -5
- package/src/release-packagist.mjs +2 -4
- package/src/ssh-utils.mjs +3 -3
- package/src/version-checker.mjs +51 -14
package/bin/zephyr.mjs
CHANGED
|
@@ -1,24 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import inquirer from 'inquirer'
|
|
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.17",
|
|
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
|
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
+
import { spawn } from 'node:child_process'
|
|
4
|
+
import process from 'node:process'
|
|
5
|
+
import chalk from 'chalk'
|
|
6
|
+
|
|
7
|
+
const IS_WINDOWS = process.platform === 'win32'
|
|
3
8
|
|
|
4
9
|
function isLocalPathOutsideRepo(depPath, rootDir) {
|
|
5
10
|
if (!depPath || typeof depPath !== 'string') {
|
|
@@ -143,7 +148,7 @@ async function fetchLatestNpmVersion(packageName) {
|
|
|
143
148
|
}
|
|
144
149
|
const data = await response.json()
|
|
145
150
|
return data.version || null
|
|
146
|
-
} catch (
|
|
151
|
+
} catch (_error) {
|
|
147
152
|
return null
|
|
148
153
|
}
|
|
149
154
|
}
|
|
@@ -162,7 +167,7 @@ async function fetchLatestPackagistVersion(packageName) {
|
|
|
162
167
|
return latest.version || null
|
|
163
168
|
}
|
|
164
169
|
return null
|
|
165
|
-
} catch (
|
|
170
|
+
} catch (_error) {
|
|
166
171
|
return null
|
|
167
172
|
}
|
|
168
173
|
}
|
|
@@ -212,7 +217,135 @@ async function updateComposerJsonDependency(rootDir, packageName, newVersion, fi
|
|
|
212
217
|
await writeFile(composerJsonPath, updatedContent, 'utf8')
|
|
213
218
|
}
|
|
214
219
|
|
|
215
|
-
async function
|
|
220
|
+
async function runCommand(command, args, { cwd = process.cwd(), capture = false } = {}) {
|
|
221
|
+
return new Promise((resolve, reject) => {
|
|
222
|
+
const spawnOptions = {
|
|
223
|
+
stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
224
|
+
cwd
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (IS_WINDOWS && command !== 'git') {
|
|
228
|
+
spawnOptions.shell = true
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const child = spawn(command, args, spawnOptions)
|
|
232
|
+
let stdout = ''
|
|
233
|
+
let stderr = ''
|
|
234
|
+
|
|
235
|
+
if (capture) {
|
|
236
|
+
child.stdout.on('data', (chunk) => {
|
|
237
|
+
stdout += chunk
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
child.stderr.on('data', (chunk) => {
|
|
241
|
+
stderr += chunk
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
child.on('error', reject)
|
|
246
|
+
child.on('close', (code) => {
|
|
247
|
+
if (code === 0) {
|
|
248
|
+
resolve(capture ? { stdout: stdout.trim(), stderr: stderr.trim() } : undefined)
|
|
249
|
+
} else {
|
|
250
|
+
const error = new Error(`Command failed (${code}): ${command} ${args.join(' ')}`)
|
|
251
|
+
if (capture) {
|
|
252
|
+
error.stdout = stdout
|
|
253
|
+
error.stderr = stderr
|
|
254
|
+
}
|
|
255
|
+
error.exitCode = code
|
|
256
|
+
reject(error)
|
|
257
|
+
}
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function getGitStatus(rootDir) {
|
|
263
|
+
try {
|
|
264
|
+
const result = await runCommand('git', ['status', '--porcelain'], { capture: true, cwd: rootDir })
|
|
265
|
+
return result.stdout || ''
|
|
266
|
+
} catch (_error) {
|
|
267
|
+
return ''
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function hasStagedChanges(statusOutput) {
|
|
272
|
+
if (!statusOutput || statusOutput.length === 0) {
|
|
273
|
+
return false
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const lines = statusOutput.split('\n').filter((line) => line.trim().length > 0)
|
|
277
|
+
|
|
278
|
+
return lines.some((line) => {
|
|
279
|
+
const firstChar = line[0]
|
|
280
|
+
return firstChar && firstChar !== ' ' && firstChar !== '?'
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function commitDependencyUpdates(rootDir, updatedFiles, promptFn, logFn) {
|
|
285
|
+
try {
|
|
286
|
+
// Check if we're in a git repository
|
|
287
|
+
await runCommand('git', ['rev-parse', '--is-inside-work-tree'], { capture: true, cwd: rootDir })
|
|
288
|
+
} catch {
|
|
289
|
+
// Not a git repository, skip commit
|
|
290
|
+
return false
|
|
291
|
+
}
|
|
292
|
+
|
|
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
|
+
}
|
|
317
|
+
|
|
318
|
+
// Stage the updated files
|
|
319
|
+
for (const file of updatedFiles) {
|
|
320
|
+
try {
|
|
321
|
+
await runCommand('git', ['add', file], { cwd: rootDir })
|
|
322
|
+
} catch {
|
|
323
|
+
// File might not exist or not be tracked, continue
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const newStatus = await getGitStatus(rootDir)
|
|
328
|
+
if (!hasStagedChanges(newStatus)) {
|
|
329
|
+
return false
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Build commit message
|
|
333
|
+
const commitMessage = `chore: update local file dependencies to online versions (${fileList})`
|
|
334
|
+
|
|
335
|
+
if (logFn) {
|
|
336
|
+
logFn('Committing dependency updates...')
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
await runCommand('git', ['commit', '-m', commitMessage], { cwd: rootDir })
|
|
340
|
+
|
|
341
|
+
if (logFn) {
|
|
342
|
+
logFn('Dependency updates committed.')
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return true
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function validateLocalDependencies(rootDir, promptFn, logFn = null) {
|
|
216
349
|
const packageDeps = await scanPackageJsonDependencies(rootDir)
|
|
217
350
|
const composerDeps = await scanComposerJsonDependencies(rootDir)
|
|
218
351
|
|
|
@@ -243,20 +376,27 @@ async function validateLocalDependencies(rootDir, promptFn) {
|
|
|
243
376
|
})
|
|
244
377
|
)
|
|
245
378
|
|
|
246
|
-
// Build warning messages
|
|
379
|
+
// Build warning messages with colored output (danger color for package name and version)
|
|
247
380
|
const messages = depsWithVersions.map((dep) => {
|
|
381
|
+
const packageNameColored = chalk.red(dep.packageName)
|
|
382
|
+
const pathColored = chalk.dim(dep.path)
|
|
248
383
|
const versionInfo = dep.latestVersion
|
|
249
|
-
? ` Latest version available: ${dep.latestVersion}.`
|
|
384
|
+
? ` Latest version available: ${chalk.red(dep.latestVersion)}.`
|
|
250
385
|
: ' Latest version could not be determined.'
|
|
251
|
-
return `Dependency
|
|
386
|
+
return `Dependency ${packageNameColored} is pointing to a local path outside the repository: ${pathColored}.${versionInfo}`
|
|
252
387
|
})
|
|
253
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
|
+
|
|
254
394
|
// Prompt user
|
|
255
395
|
const { shouldUpdate } = await promptFn([
|
|
256
396
|
{
|
|
257
397
|
type: 'confirm',
|
|
258
398
|
name: 'shouldUpdate',
|
|
259
|
-
message:
|
|
399
|
+
message: promptMessage,
|
|
260
400
|
default: true
|
|
261
401
|
}
|
|
262
402
|
])
|
|
@@ -265,6 +405,9 @@ async function validateLocalDependencies(rootDir, promptFn) {
|
|
|
265
405
|
throw new Error('Release cancelled: local file dependencies must be updated before release.')
|
|
266
406
|
}
|
|
267
407
|
|
|
408
|
+
// Track which files were updated
|
|
409
|
+
const updatedFiles = new Set()
|
|
410
|
+
|
|
268
411
|
// Update dependencies
|
|
269
412
|
for (const dep of depsWithVersions) {
|
|
270
413
|
if (!dep.latestVersion) {
|
|
@@ -273,8 +416,10 @@ async function validateLocalDependencies(rootDir, promptFn) {
|
|
|
273
416
|
|
|
274
417
|
if (dep.field === 'dependencies' || dep.field === 'devDependencies') {
|
|
275
418
|
await updatePackageJsonDependency(rootDir, dep.packageName, dep.latestVersion, dep.field)
|
|
419
|
+
updatedFiles.add('package.json')
|
|
276
420
|
} else if (dep.field === 'require' || dep.field === 'require-dev') {
|
|
277
421
|
await updateComposerJsonDependency(rootDir, dep.packageName, dep.latestVersion, dep.field)
|
|
422
|
+
updatedFiles.add('composer.json')
|
|
278
423
|
} else if (dep.field === 'repositories') {
|
|
279
424
|
// For repositories, we need to remove the repository entry
|
|
280
425
|
// But we still need to update the dependency version
|
|
@@ -319,9 +464,15 @@ async function validateLocalDependencies(rootDir, promptFn) {
|
|
|
319
464
|
const updatedContent = JSON.stringify(updatedComposer, null, 2) + '\n'
|
|
320
465
|
await writeFile(composerJsonPath, updatedContent, 'utf8')
|
|
321
466
|
}
|
|
467
|
+
updatedFiles.add('composer.json')
|
|
322
468
|
}
|
|
323
469
|
}
|
|
324
470
|
}
|
|
471
|
+
|
|
472
|
+
// Commit the changes if any files were updated
|
|
473
|
+
if (updatedFiles.size > 0) {
|
|
474
|
+
await commitDependencyUpdates(rootDir, Array.from(updatedFiles), promptFn, logFn)
|
|
475
|
+
}
|
|
325
476
|
}
|
|
326
477
|
|
|
327
478
|
export {
|
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
|
|
|
@@ -89,7 +90,7 @@ async function cleanupOldLogs(rootDir) {
|
|
|
89
90
|
for (const file of filesToDelete) {
|
|
90
91
|
try {
|
|
91
92
|
await fs.unlink(file.path)
|
|
92
|
-
} catch (
|
|
93
|
+
} catch (_error) {
|
|
93
94
|
// Ignore errors when deleting old logs
|
|
94
95
|
}
|
|
95
96
|
}
|
|
@@ -414,7 +415,7 @@ async function ensureProjectReleaseScript(rootDir) {
|
|
|
414
415
|
let packageJson
|
|
415
416
|
try {
|
|
416
417
|
packageJson = JSON.parse(raw)
|
|
417
|
-
} catch (
|
|
418
|
+
} catch (_error) {
|
|
418
419
|
logWarning('Unable to parse package.json; skipping release script injection.')
|
|
419
420
|
return false
|
|
420
421
|
}
|
|
@@ -453,7 +454,7 @@ async function ensureProjectReleaseScript(rootDir) {
|
|
|
453
454
|
try {
|
|
454
455
|
await runCommand('git', ['rev-parse', '--is-inside-work-tree'], { cwd: rootDir, silent: true })
|
|
455
456
|
isGitRepo = true
|
|
456
|
-
} catch (
|
|
457
|
+
} catch (_error) {
|
|
457
458
|
logWarning('Not a git repository; skipping commit for release script addition.')
|
|
458
459
|
}
|
|
459
460
|
|
|
@@ -541,7 +542,7 @@ async function readRemoteLock(ssh, remoteCwd) {
|
|
|
541
542
|
if (checkResult.stdout && checkResult.stdout.trim() !== 'LOCK_NOT_FOUND' && checkResult.stdout.trim() !== '') {
|
|
542
543
|
try {
|
|
543
544
|
return JSON.parse(checkResult.stdout.trim())
|
|
544
|
-
} catch (
|
|
545
|
+
} catch (_error) {
|
|
545
546
|
return { raw: checkResult.stdout.trim() }
|
|
546
547
|
}
|
|
547
548
|
}
|
|
@@ -605,7 +606,7 @@ async function acquireRemoteLock(ssh, remoteCwd, rootDir) {
|
|
|
605
606
|
let details = {}
|
|
606
607
|
try {
|
|
607
608
|
details = JSON.parse(checkResult.stdout.trim())
|
|
608
|
-
} catch (
|
|
609
|
+
} catch (_error) {
|
|
609
610
|
details = { raw: checkResult.stdout.trim() }
|
|
610
611
|
}
|
|
611
612
|
|
|
@@ -620,7 +621,7 @@ async function acquireRemoteLock(ssh, remoteCwd, rootDir) {
|
|
|
620
621
|
let details = {}
|
|
621
622
|
try {
|
|
622
623
|
details = JSON.parse(checkResult.stdout.trim())
|
|
623
|
-
} catch (
|
|
624
|
+
} catch (_error) {
|
|
624
625
|
details = { raw: checkResult.stdout.trim() }
|
|
625
626
|
}
|
|
626
627
|
|
|
@@ -727,7 +728,7 @@ async function ensureGitignoreEntry(rootDir) {
|
|
|
727
728
|
cwd: rootDir
|
|
728
729
|
})
|
|
729
730
|
isGitRepo = true
|
|
730
|
-
} catch (
|
|
731
|
+
} catch (_error) {
|
|
731
732
|
logWarning('Not a git repository; skipping commit for .gitignore update.')
|
|
732
733
|
}
|
|
733
734
|
|
|
@@ -949,7 +950,7 @@ async function listGitBranches(currentDir) {
|
|
|
949
950
|
.filter(Boolean)
|
|
950
951
|
|
|
951
952
|
return branches.length ? branches : ['master']
|
|
952
|
-
} catch (
|
|
953
|
+
} catch (_error) {
|
|
953
954
|
logWarning('Unable to read git branches; defaulting to master.')
|
|
954
955
|
return ['master']
|
|
955
956
|
}
|
|
@@ -1002,7 +1003,7 @@ async function isPrivateKeyFile(filePath) {
|
|
|
1002
1003
|
try {
|
|
1003
1004
|
const content = await fs.readFile(filePath, 'utf8')
|
|
1004
1005
|
return /-----BEGIN [A-Z ]*PRIVATE KEY-----/.test(content)
|
|
1005
|
-
} catch (
|
|
1006
|
+
} catch (_error) {
|
|
1006
1007
|
return false
|
|
1007
1008
|
}
|
|
1008
1009
|
}
|
|
@@ -1091,7 +1092,7 @@ async function resolveSshKeyPath(targetPath) {
|
|
|
1091
1092
|
|
|
1092
1093
|
try {
|
|
1093
1094
|
await fs.access(expanded)
|
|
1094
|
-
} catch (
|
|
1095
|
+
} catch (_error) {
|
|
1095
1096
|
throw new Error(`SSH key not accessible at ${expanded}`)
|
|
1096
1097
|
}
|
|
1097
1098
|
|
|
@@ -1247,14 +1248,14 @@ async function runRemoteTasks(config, options = {}) {
|
|
|
1247
1248
|
}
|
|
1248
1249
|
|
|
1249
1250
|
// Run tests for Laravel projects
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1251
|
+
if (isLaravel) {
|
|
1252
|
+
logProcessing('Running Laravel tests locally...')
|
|
1253
|
+
try {
|
|
1254
|
+
await runCommand('php', ['artisan', 'test', '--compact'], { cwd: rootDir })
|
|
1255
|
+
logSuccess('Local tests passed.')
|
|
1256
|
+
} catch (error) {
|
|
1257
|
+
throw new Error(`Local tests failed. Fix test failures before deploying. ${error.message}`)
|
|
1258
|
+
}
|
|
1258
1259
|
}
|
|
1259
1260
|
} else {
|
|
1260
1261
|
logProcessing('Pre-push git hook detected. Skipping local linting and test execution.')
|
|
@@ -1314,7 +1315,7 @@ async function runRemoteTasks(config, options = {}) {
|
|
|
1314
1315
|
const escapeForDoubleQuotes = (value) => value.replace(/(["\\$`])/g, '\\$1')
|
|
1315
1316
|
|
|
1316
1317
|
const executeRemote = async (label, command, options = {}) => {
|
|
1317
|
-
const { cwd = remoteCwd, allowFailure = false,
|
|
1318
|
+
const { cwd = remoteCwd, allowFailure = false, bootstrapEnv = true } = options
|
|
1318
1319
|
logProcessing(`\n→ ${label}`)
|
|
1319
1320
|
|
|
1320
1321
|
let wrappedCommand = command
|
|
@@ -1583,7 +1584,7 @@ async function runRemoteTasks(config, options = {}) {
|
|
|
1583
1584
|
const remoteHome = remoteHomeResult.stdout.trim() || `/home/${sshUser}`
|
|
1584
1585
|
const remoteCwd = resolveRemotePath(config.projectPath, remoteHome)
|
|
1585
1586
|
await compareLocksAndPrompt(rootDir, ssh, remoteCwd)
|
|
1586
|
-
} catch (
|
|
1587
|
+
} catch (_lockError) {
|
|
1587
1588
|
// Ignore lock comparison errors during error handling
|
|
1588
1589
|
}
|
|
1589
1590
|
}
|
|
@@ -1739,9 +1740,9 @@ async function selectApp(projectConfig, server, currentDir) {
|
|
|
1739
1740
|
if (apps.length > 0) {
|
|
1740
1741
|
const availableServers = [...new Set(apps.map((app) => app.serverName).filter(Boolean))]
|
|
1741
1742
|
if (availableServers.length > 0) {
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1743
|
+
logWarning(
|
|
1744
|
+
`No applications configured for server "${server.serverName}". Available servers: ${availableServers.join(', ')}`
|
|
1745
|
+
)
|
|
1745
1746
|
}
|
|
1746
1747
|
}
|
|
1747
1748
|
logProcessing(`No applications configured for ${server.serverName}. Let's create one.`)
|
|
@@ -1758,7 +1759,7 @@ async function selectApp(projectConfig, server, currentDir) {
|
|
|
1758
1759
|
return appConfig
|
|
1759
1760
|
}
|
|
1760
1761
|
|
|
1761
|
-
const choices = matches.map(({ app
|
|
1762
|
+
const choices = matches.map(({ app }, matchIndex) => ({
|
|
1762
1763
|
name: `${app.projectPath} (${app.branch})`,
|
|
1763
1764
|
value: matchIndex
|
|
1764
1765
|
}))
|
|
@@ -1796,23 +1797,6 @@ async function selectApp(projectConfig, server, currentDir) {
|
|
|
1796
1797
|
return chosen
|
|
1797
1798
|
}
|
|
1798
1799
|
|
|
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
1800
|
async function selectPreset(projectConfig, servers) {
|
|
1817
1801
|
const presets = projectConfig.presets ?? []
|
|
1818
1802
|
const apps = projectConfig.apps ?? []
|
|
@@ -1844,7 +1828,7 @@ async function selectPreset(projectConfig, servers) {
|
|
|
1844
1828
|
|
|
1845
1829
|
return {
|
|
1846
1830
|
name: displayName,
|
|
1847
|
-
|
|
1831
|
+
value: index
|
|
1848
1832
|
}
|
|
1849
1833
|
})
|
|
1850
1834
|
|
|
@@ -1871,6 +1855,24 @@ async function selectPreset(projectConfig, servers) {
|
|
|
1871
1855
|
}
|
|
1872
1856
|
|
|
1873
1857
|
async function main(releaseType = null) {
|
|
1858
|
+
// Best-effort update check (skip during tests or when explicitly disabled)
|
|
1859
|
+
// If an update is accepted, the process will re-execute via npx @latest and we should exit early.
|
|
1860
|
+
if (
|
|
1861
|
+
process.env.ZEPHYR_SKIP_VERSION_CHECK !== '1' &&
|
|
1862
|
+
process.env.NODE_ENV !== 'test' &&
|
|
1863
|
+
process.env.VITEST !== 'true'
|
|
1864
|
+
) {
|
|
1865
|
+
try {
|
|
1866
|
+
const args = process.argv.slice(2)
|
|
1867
|
+
const reExecuted = await checkAndUpdateVersion(runPrompt, args)
|
|
1868
|
+
if (reExecuted) {
|
|
1869
|
+
return
|
|
1870
|
+
}
|
|
1871
|
+
} catch (_error) {
|
|
1872
|
+
// Never block execution due to update check issues
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1874
1876
|
// Handle node/vue package release
|
|
1875
1877
|
if (releaseType === 'node' || releaseType === 'vue') {
|
|
1876
1878
|
try {
|
|
@@ -1915,7 +1917,7 @@ async function main(releaseType = null) {
|
|
|
1915
1917
|
|
|
1916
1918
|
if (hasPackageJson || hasComposerJson) {
|
|
1917
1919
|
logProcessing('Validating dependencies...')
|
|
1918
|
-
await validateLocalDependencies(rootDir, runPrompt)
|
|
1920
|
+
await validateLocalDependencies(rootDir, runPrompt, logSuccess)
|
|
1919
1921
|
}
|
|
1920
1922
|
|
|
1921
1923
|
// Load servers first (they may be migrated)
|
|
@@ -2041,11 +2043,11 @@ async function main(releaseType = null) {
|
|
|
2041
2043
|
} else {
|
|
2042
2044
|
// Check if preset with this appId already exists
|
|
2043
2045
|
const existingIndex = presets.findIndex((p) => p.appId === appId)
|
|
2044
|
-
|
|
2046
|
+
if (existingIndex >= 0) {
|
|
2045
2047
|
presets[existingIndex].name = trimmedName
|
|
2046
2048
|
presets[existingIndex].branch = deploymentConfig.branch
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
+
} else {
|
|
2050
|
+
presets.push({
|
|
2049
2051
|
name: trimmedName,
|
|
2050
2052
|
appId: appId,
|
|
2051
2053
|
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'
|
|
@@ -489,7 +488,7 @@ function extractDomainFromHomepage(homepage) {
|
|
|
489
488
|
return url.hostname
|
|
490
489
|
} catch {
|
|
491
490
|
// If it's not a valid URL, try to extract domain from string
|
|
492
|
-
const match = homepage.match(/(?:https?:\/\/)?([
|
|
491
|
+
const match = homepage.match(/(?:https?:\/\/)?([^/]+)/)
|
|
493
492
|
return match ? match[1] : null
|
|
494
493
|
}
|
|
495
494
|
}
|
|
@@ -537,7 +536,9 @@ async function deployGHPages(skipDeploy, pkg, rootDir = process.cwd()) {
|
|
|
537
536
|
try {
|
|
538
537
|
try {
|
|
539
538
|
await runCommand('git', ['worktree', 'remove', worktreeDir, '-f'], { capture: true, cwd: rootDir })
|
|
540
|
-
} catch {
|
|
539
|
+
} catch (_error) {
|
|
540
|
+
// Ignore if worktree doesn't exist
|
|
541
|
+
}
|
|
541
542
|
|
|
542
543
|
try {
|
|
543
544
|
await runCommand('git', ['worktree', 'add', worktreeDir, 'gh-pages'], { capture: true, cwd: rootDir })
|
|
@@ -583,7 +584,7 @@ export async function releaseNode() {
|
|
|
583
584
|
const pkg = await readPackage(rootDir)
|
|
584
585
|
|
|
585
586
|
logStep('Validating dependencies...')
|
|
586
|
-
await validateLocalDependencies(rootDir, (questions) => inquirer.prompt(questions))
|
|
587
|
+
await validateLocalDependencies(rootDir, (questions) => inquirer.prompt(questions), logSuccess)
|
|
587
588
|
|
|
588
589
|
logStep('Checking working tree status...')
|
|
589
590
|
await ensureCleanWorkingTree(rootDir)
|
|
@@ -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'
|
|
@@ -387,7 +385,7 @@ export async function releasePackagist() {
|
|
|
387
385
|
}
|
|
388
386
|
|
|
389
387
|
logStep('Validating dependencies...')
|
|
390
|
-
await validateLocalDependencies(rootDir, (questions) => inquirer.prompt(questions))
|
|
388
|
+
await validateLocalDependencies(rootDir, (questions) => inquirer.prompt(questions), logSuccess)
|
|
391
389
|
|
|
392
390
|
logStep('Checking working tree status...')
|
|
393
391
|
await ensureCleanWorkingTree(rootDir)
|
package/src/ssh-utils.mjs
CHANGED
|
@@ -25,7 +25,7 @@ async function resolveSshKeyPath(targetPath) {
|
|
|
25
25
|
const expanded = expandHomePath(targetPath)
|
|
26
26
|
try {
|
|
27
27
|
await fs.access(expanded)
|
|
28
|
-
} catch (
|
|
28
|
+
} catch (_error) {
|
|
29
29
|
throw new Error(`SSH key not accessible at ${expanded}`)
|
|
30
30
|
}
|
|
31
31
|
return expanded
|
|
@@ -64,7 +64,7 @@ const createSshClient = () => {
|
|
|
64
64
|
* @param {string} rootDir - Local root directory for logging
|
|
65
65
|
* @returns {Promise<{ssh: NodeSSH, remoteCwd: string, remoteHome: string}>}
|
|
66
66
|
*/
|
|
67
|
-
export async function connectToServer(config,
|
|
67
|
+
export async function connectToServer(config, _rootDir) {
|
|
68
68
|
const ssh = createSshClient()
|
|
69
69
|
const sshUser = config.sshUser || os.userInfo().username
|
|
70
70
|
const privateKeyPath = await resolveSshKeyPath(config.sshKey)
|
|
@@ -96,7 +96,7 @@ export async function connectToServer(config, rootDir) {
|
|
|
96
96
|
* @returns {Promise<Object>} Command result
|
|
97
97
|
*/
|
|
98
98
|
export async function executeRemoteCommand(ssh, label, command, options = {}) {
|
|
99
|
-
const { cwd, allowFailure = false,
|
|
99
|
+
const { cwd, allowFailure = false, bootstrapEnv = true, rootDir = null, writeToLogFile = null, env = {} } = options
|
|
100
100
|
|
|
101
101
|
logProcessing(`\n→ ${label}`)
|
|
102
102
|
|
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,11 @@ async function reExecuteWithLatest(args) {
|
|
|
59
97
|
return new Promise((resolve, reject) => {
|
|
60
98
|
const child = spawn(command, npxArgs, {
|
|
61
99
|
stdio: 'inherit',
|
|
62
|
-
shell: IS_WINDOWS
|
|
100
|
+
shell: IS_WINDOWS,
|
|
101
|
+
env: {
|
|
102
|
+
...process.env,
|
|
103
|
+
[ZEPHYR_SKIP_VERSION_CHECK_ENV]: '1'
|
|
104
|
+
}
|
|
63
105
|
})
|
|
64
106
|
|
|
65
107
|
child.on('error', reject)
|
|
@@ -75,12 +117,7 @@ async function reExecuteWithLatest(args) {
|
|
|
75
117
|
|
|
76
118
|
export async function checkAndUpdateVersion(promptFn, args) {
|
|
77
119
|
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) {
|
|
120
|
+
if (process.env[ZEPHYR_SKIP_VERSION_CHECK_ENV] === '1') {
|
|
84
121
|
return false
|
|
85
122
|
}
|
|
86
123
|
|
|
@@ -118,7 +155,7 @@ export async function checkAndUpdateVersion(promptFn, args) {
|
|
|
118
155
|
// User confirmed, re-execute with latest version
|
|
119
156
|
await reExecuteWithLatest(args)
|
|
120
157
|
return true // Indicates we've re-executed, so the current process should exit
|
|
121
|
-
} catch (
|
|
158
|
+
} catch (_error) {
|
|
122
159
|
// If version check fails, just continue with current version
|
|
123
160
|
// Don't block the user from using the tool
|
|
124
161
|
return false
|