@wyxos/zephyr 0.2.31 → 0.3.0
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 +55 -2
- package/bin/zephyr.mjs +3 -1
- package/package.json +7 -2
- package/src/application/configuration/app-details.mjs +89 -0
- package/src/application/configuration/app-selection.mjs +87 -0
- package/src/application/configuration/preset-selection.mjs +59 -0
- package/src/application/configuration/select-deployment-target.mjs +165 -0
- package/src/application/configuration/server-selection.mjs +87 -0
- package/src/application/configuration/service.mjs +109 -0
- package/src/application/deploy/build-remote-deployment-plan.mjs +174 -0
- package/src/application/deploy/bump-local-package-version.mjs +81 -0
- package/src/application/deploy/execute-remote-deployment-plan.mjs +61 -0
- package/src/{utils/task-planner.mjs → application/deploy/plan-laravel-deployment-tasks.mjs} +5 -4
- package/src/application/deploy/prepare-local-deployment.mjs +52 -0
- package/src/application/deploy/resolve-local-deployment-context.mjs +17 -0
- package/src/application/deploy/resolve-pending-snapshot.mjs +45 -0
- package/src/application/deploy/run-deployment.mjs +147 -0
- package/src/application/deploy/run-local-deployment-checks.mjs +80 -0
- package/src/application/release/release-node-package.mjs +340 -0
- package/src/application/release/release-packagist-package.mjs +223 -0
- package/src/config/project.mjs +13 -0
- package/src/deploy/local-repo.mjs +187 -67
- package/src/deploy/remote-exec.mjs +2 -3
- package/src/index.mjs +27 -85
- package/src/main.mjs +78 -641
- package/src/release/shared.mjs +104 -0
- package/src/release-node.mjs +20 -424
- package/src/release-packagist.mjs +20 -291
- package/src/runtime/app-context.mjs +36 -0
- package/src/targets/index.mjs +24 -0
- package/src/utils/output.mjs +41 -16
- package/src/utils/config-flow.mjs +0 -284
- /package/src/{utils/php-version.mjs → infrastructure/php/version.mjs} +0 -0
package/README.md
CHANGED
|
@@ -18,6 +18,12 @@ npx @wyxos/zephyr
|
|
|
18
18
|
|
|
19
19
|
Navigate to your project directory and run:
|
|
20
20
|
|
|
21
|
+
```bash
|
|
22
|
+
npm run release
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or invoke Zephyr directly:
|
|
26
|
+
|
|
21
27
|
```bash
|
|
22
28
|
zephyr
|
|
23
29
|
```
|
|
@@ -35,6 +41,11 @@ Common flags:
|
|
|
35
41
|
zephyr --type node
|
|
36
42
|
```
|
|
37
43
|
|
|
44
|
+
On a first run inside a project with `package.json`, Zephyr can:
|
|
45
|
+
- add `.zephyr/` to `.gitignore`
|
|
46
|
+
- add a `release` script that runs `npx @wyxos/zephyr@latest`
|
|
47
|
+
- create global server config and per-project deployment config interactively
|
|
48
|
+
|
|
38
49
|
Follow the interactive prompts to configure your deployment target:
|
|
39
50
|
- Server name and IP address
|
|
40
51
|
- Project path on the remote server
|
|
@@ -43,6 +54,48 @@ Follow the interactive prompts to configure your deployment target:
|
|
|
43
54
|
|
|
44
55
|
Configuration is saved automatically for future deployments.
|
|
45
56
|
|
|
57
|
+
## Scripts
|
|
58
|
+
|
|
59
|
+
Zephyr exposes the following common project workflow:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm run lint
|
|
63
|
+
npm test
|
|
64
|
+
npm run release
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- `npm run lint` runs `eslint . --fix`, so it can rewrite tracked files when auto-fixes are available.
|
|
68
|
+
- `npm test` runs the Vitest suite.
|
|
69
|
+
- `npm run release` is the recommended project entrypoint once the release script has been installed.
|
|
70
|
+
|
|
71
|
+
## Package API
|
|
72
|
+
|
|
73
|
+
The package surface is intentionally small:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
import {
|
|
77
|
+
logProcessing,
|
|
78
|
+
logSuccess,
|
|
79
|
+
logWarning,
|
|
80
|
+
logError,
|
|
81
|
+
runCommand,
|
|
82
|
+
runCommandCapture,
|
|
83
|
+
writeToLogFile
|
|
84
|
+
} from '@wyxos/zephyr'
|
|
85
|
+
|
|
86
|
+
import {selectDeploymentTarget} from '@wyxos/zephyr/targets'
|
|
87
|
+
|
|
88
|
+
import {
|
|
89
|
+
connectToServer,
|
|
90
|
+
executeRemoteCommand,
|
|
91
|
+
readRemoteFile,
|
|
92
|
+
downloadRemoteFile,
|
|
93
|
+
deleteRemoteFile
|
|
94
|
+
} from '@wyxos/zephyr/ssh'
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The root package does not expose the old interactive configuration helper bag; target selection now lives under `@wyxos/zephyr/targets`.
|
|
98
|
+
|
|
46
99
|
## Features
|
|
47
100
|
|
|
48
101
|
- Automated Git operations (branch switching, commits, pushes)
|
|
@@ -62,7 +115,7 @@ Configuration is saved automatically for future deployments.
|
|
|
62
115
|
Zephyr analyzes changed files and runs appropriate tasks:
|
|
63
116
|
|
|
64
117
|
- **Always**: `git pull origin <branch>`
|
|
65
|
-
- **Composer files changed** (`composer.json` / `composer.lock`): `composer
|
|
118
|
+
- **Composer files changed** (`composer.json` / `composer.lock`): `composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader` (requires `composer.lock`)
|
|
66
119
|
- **Migrations changed** (`database/migrations/*.php`): `php artisan migrate --force`
|
|
67
120
|
- **Node dependency files changed** (`package.json` / `package-lock.json`, including nested): `npm install`
|
|
68
121
|
- **Frontend files changed** (`.vue/.js/.ts/.tsx/.css/.scss/.less`): `npm run build`
|
|
@@ -131,4 +184,4 @@ The `.zephyr/` directory is automatically added to `.gitignore`.
|
|
|
131
184
|
|
|
132
185
|
- Node.js 16+
|
|
133
186
|
- Git
|
|
134
|
-
- SSH access to target servers
|
|
187
|
+
- SSH access to target servers
|
package/bin/zephyr.mjs
CHANGED
|
@@ -13,12 +13,14 @@ 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
|
+
.argument('[version]', 'Version or npm bump type (e.g. 1.2.3, patch, minor, major)')
|
|
16
17
|
|
|
17
18
|
program.parse(process.argv)
|
|
18
19
|
const options = program.opts()
|
|
20
|
+
const versionArg = program.args[0] ?? null
|
|
19
21
|
|
|
20
22
|
try {
|
|
21
|
-
await main(options.type ?? null)
|
|
23
|
+
await main(options.type ?? null, versionArg)
|
|
22
24
|
} catch (error) {
|
|
23
25
|
logError(error?.message || String(error))
|
|
24
26
|
process.exitCode = 1
|
package/package.json
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wyxos/zephyr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
|
7
|
+
"imports": {
|
|
8
|
+
"#src/*": "./src/*",
|
|
9
|
+
"#tests/*": "./tests/*"
|
|
10
|
+
},
|
|
7
11
|
"exports": {
|
|
8
12
|
".": "./src/index.mjs",
|
|
13
|
+
"./targets": "./src/targets/index.mjs",
|
|
9
14
|
"./ssh": "./src/ssh/index.mjs"
|
|
10
15
|
},
|
|
11
16
|
"bin": {
|
|
@@ -13,7 +18,7 @@
|
|
|
13
18
|
},
|
|
14
19
|
"scripts": {
|
|
15
20
|
"test": "vitest run",
|
|
16
|
-
"lint": "eslint .",
|
|
21
|
+
"lint": "eslint . --fix",
|
|
17
22
|
"release": "node bin/zephyr.mjs --type=node"
|
|
18
23
|
},
|
|
19
24
|
"keywords": [
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import inquirer from 'inquirer'
|
|
3
|
+
|
|
4
|
+
export function defaultProjectPath(currentDir) {
|
|
5
|
+
return `~/webapps/${path.basename(currentDir)}`
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function listGitBranches({
|
|
9
|
+
currentDir,
|
|
10
|
+
runCommandCapture,
|
|
11
|
+
logWarning
|
|
12
|
+
} = {}) {
|
|
13
|
+
try {
|
|
14
|
+
const output = await runCommandCapture(
|
|
15
|
+
'git',
|
|
16
|
+
['branch', '--format', '%(refname:short)'],
|
|
17
|
+
{cwd: currentDir}
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
const branches = output
|
|
21
|
+
.split(/\r?\n/)
|
|
22
|
+
.map((line) => line.trim())
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
|
|
25
|
+
return branches.length ? branches : ['master']
|
|
26
|
+
} catch (_error) {
|
|
27
|
+
logWarning?.('Unable to read git branches; defaulting to master.')
|
|
28
|
+
return ['master']
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function promptAppDetails({
|
|
33
|
+
currentDir,
|
|
34
|
+
existing = {},
|
|
35
|
+
runPrompt,
|
|
36
|
+
listGitBranches,
|
|
37
|
+
resolveDefaultProjectPath = defaultProjectPath,
|
|
38
|
+
promptSshDetails
|
|
39
|
+
} = {}) {
|
|
40
|
+
const branches = await listGitBranches(currentDir)
|
|
41
|
+
const defaultBranch = existing.branch || (branches.includes('master') ? 'master' : branches[0])
|
|
42
|
+
const defaults = {
|
|
43
|
+
projectPath: existing.projectPath || resolveDefaultProjectPath(currentDir),
|
|
44
|
+
branch: defaultBranch
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const answers = await runPrompt([
|
|
48
|
+
{
|
|
49
|
+
type: 'input',
|
|
50
|
+
name: 'projectPath',
|
|
51
|
+
message: 'Remote project path',
|
|
52
|
+
default: defaults.projectPath
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
type: 'list',
|
|
56
|
+
name: 'branchSelection',
|
|
57
|
+
message: 'Branch to deploy',
|
|
58
|
+
choices: [
|
|
59
|
+
...branches.map((branch) => ({name: branch, value: branch})),
|
|
60
|
+
new inquirer.Separator(),
|
|
61
|
+
{name: 'Enter custom branch…', value: '__custom'}
|
|
62
|
+
],
|
|
63
|
+
default: defaults.branch
|
|
64
|
+
}
|
|
65
|
+
])
|
|
66
|
+
|
|
67
|
+
let branch = answers.branchSelection
|
|
68
|
+
|
|
69
|
+
if (branch === '__custom') {
|
|
70
|
+
const {customBranch} = await runPrompt([
|
|
71
|
+
{
|
|
72
|
+
type: 'input',
|
|
73
|
+
name: 'customBranch',
|
|
74
|
+
message: 'Custom branch name',
|
|
75
|
+
default: defaults.branch
|
|
76
|
+
}
|
|
77
|
+
])
|
|
78
|
+
|
|
79
|
+
branch = customBranch.trim() || defaults.branch
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const sshDetails = await promptSshDetails(currentDir, existing)
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
projectPath: answers.projectPath.trim() || defaults.projectPath,
|
|
86
|
+
branch,
|
|
87
|
+
...sshDetails
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import inquirer from 'inquirer'
|
|
2
|
+
|
|
3
|
+
import {generateId} from '../../utils/id.mjs'
|
|
4
|
+
import {saveProjectConfig} from '../../config/project.mjs'
|
|
5
|
+
|
|
6
|
+
export async function selectApp({
|
|
7
|
+
projectConfig,
|
|
8
|
+
server,
|
|
9
|
+
currentDir,
|
|
10
|
+
runPrompt,
|
|
11
|
+
logWarning,
|
|
12
|
+
logProcessing,
|
|
13
|
+
logSuccess,
|
|
14
|
+
persistProjectConfig = saveProjectConfig,
|
|
15
|
+
createId = generateId,
|
|
16
|
+
promptAppDetails
|
|
17
|
+
} = {}) {
|
|
18
|
+
const apps = projectConfig.apps ?? []
|
|
19
|
+
const matches = apps
|
|
20
|
+
.map((app, index) => ({app, index}))
|
|
21
|
+
.filter(({app}) => app.serverId === server.id || app.serverName === server.serverName)
|
|
22
|
+
|
|
23
|
+
if (matches.length === 0) {
|
|
24
|
+
if (apps.length > 0) {
|
|
25
|
+
const availableServers = [...new Set(apps.map((app) => app.serverName).filter(Boolean))]
|
|
26
|
+
if (availableServers.length > 0) {
|
|
27
|
+
logWarning?.(
|
|
28
|
+
`No applications configured for server "${server.serverName}". Available servers: ${availableServers.join(', ')}`
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
logProcessing?.(`No applications are configured for ${server.serverName}. Creating one now.`)
|
|
34
|
+
const appDetails = await promptAppDetails(currentDir)
|
|
35
|
+
const appConfig = {
|
|
36
|
+
id: createId(),
|
|
37
|
+
serverId: server.id,
|
|
38
|
+
serverName: server.serverName,
|
|
39
|
+
...appDetails
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
projectConfig.apps.push(appConfig)
|
|
43
|
+
await persistProjectConfig(currentDir, projectConfig)
|
|
44
|
+
logSuccess?.('Saved deployment configuration to .zephyr/config.json')
|
|
45
|
+
return appConfig
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const choices = matches.map(({app}, matchIndex) => ({
|
|
49
|
+
name: `${app.projectPath} (${app.branch})`,
|
|
50
|
+
value: matchIndex
|
|
51
|
+
}))
|
|
52
|
+
|
|
53
|
+
choices.push(
|
|
54
|
+
new inquirer.Separator(),
|
|
55
|
+
{
|
|
56
|
+
name: '➕ Create a new application for this server',
|
|
57
|
+
value: 'create'
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const {selection} = await runPrompt([
|
|
62
|
+
{
|
|
63
|
+
type: 'list',
|
|
64
|
+
name: 'selection',
|
|
65
|
+
message: `Select an application for ${server.serverName}`,
|
|
66
|
+
choices,
|
|
67
|
+
default: 0
|
|
68
|
+
}
|
|
69
|
+
])
|
|
70
|
+
|
|
71
|
+
if (selection === 'create') {
|
|
72
|
+
const appDetails = await promptAppDetails(currentDir)
|
|
73
|
+
const appConfig = {
|
|
74
|
+
id: createId(),
|
|
75
|
+
serverId: server.id,
|
|
76
|
+
serverName: server.serverName,
|
|
77
|
+
...appDetails
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
projectConfig.apps.push(appConfig)
|
|
81
|
+
await persistProjectConfig(currentDir, projectConfig)
|
|
82
|
+
logSuccess?.('Appended deployment configuration to .zephyr/config.json')
|
|
83
|
+
return appConfig
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return matches[selection].app
|
|
87
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import inquirer from 'inquirer'
|
|
2
|
+
|
|
3
|
+
export async function selectPreset({
|
|
4
|
+
projectConfig,
|
|
5
|
+
servers,
|
|
6
|
+
runPrompt
|
|
7
|
+
} = {}) {
|
|
8
|
+
const presets = projectConfig.presets ?? []
|
|
9
|
+
const apps = projectConfig.apps ?? []
|
|
10
|
+
|
|
11
|
+
if (presets.length === 0) {
|
|
12
|
+
return null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const choices = presets.map((preset, index) => {
|
|
16
|
+
let displayName = preset.name
|
|
17
|
+
|
|
18
|
+
if (preset.appId) {
|
|
19
|
+
const app = apps.find((entry) => entry.id === preset.appId)
|
|
20
|
+
if (app) {
|
|
21
|
+
const server = servers.find((entry) => entry.id === app.serverId || entry.serverName === app.serverName)
|
|
22
|
+
const serverName = server?.serverName || 'unknown'
|
|
23
|
+
const branch = preset.branch || app.branch || 'unknown'
|
|
24
|
+
displayName = `${preset.name} (${serverName} → ${app.projectPath} [${branch}])`
|
|
25
|
+
}
|
|
26
|
+
} else if (preset.key) {
|
|
27
|
+
const keyParts = preset.key.split(':')
|
|
28
|
+
const serverName = keyParts[0]
|
|
29
|
+
const projectPath = keyParts[1]
|
|
30
|
+
const branch = preset.branch || (keyParts.length === 3 ? keyParts[2] : 'unknown')
|
|
31
|
+
displayName = `${preset.name} (${serverName} → ${projectPath} [${branch}])`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
name: displayName,
|
|
36
|
+
value: index
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
choices.push(
|
|
41
|
+
new inquirer.Separator(),
|
|
42
|
+
{
|
|
43
|
+
name: '➕ Create a new preset',
|
|
44
|
+
value: 'create'
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
const {selection} = await runPrompt([
|
|
49
|
+
{
|
|
50
|
+
type: 'list',
|
|
51
|
+
name: 'selection',
|
|
52
|
+
message: 'Select a preset',
|
|
53
|
+
choices,
|
|
54
|
+
default: 0
|
|
55
|
+
}
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
return selection === 'create' ? 'create' : presets[selection]
|
|
59
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import {writeStdoutLine} from '../../utils/output.mjs'
|
|
2
|
+
import {loadServers} from '../../config/servers.mjs'
|
|
3
|
+
import {loadProjectConfig, removePreset, saveProjectConfig} from '../../config/project.mjs'
|
|
4
|
+
|
|
5
|
+
export async function selectDeploymentTarget(rootDir, {
|
|
6
|
+
configurationService,
|
|
7
|
+
runPrompt,
|
|
8
|
+
logProcessing,
|
|
9
|
+
logSuccess,
|
|
10
|
+
logWarning
|
|
11
|
+
} = {}) {
|
|
12
|
+
const servers = await loadServers({logSuccess, logWarning})
|
|
13
|
+
const projectConfig = await loadProjectConfig(rootDir, servers, {logSuccess, logWarning})
|
|
14
|
+
|
|
15
|
+
let server = null
|
|
16
|
+
let appConfig = null
|
|
17
|
+
let isCreatingNewPreset = false
|
|
18
|
+
|
|
19
|
+
const preset = await configurationService.selectPreset(projectConfig, servers)
|
|
20
|
+
|
|
21
|
+
const removeInvalidPreset = async () => {
|
|
22
|
+
if (!preset || preset === 'create') {
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const removedPreset = removePreset(projectConfig, preset)
|
|
27
|
+
if (!removedPreset) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await saveProjectConfig(rootDir, projectConfig)
|
|
32
|
+
const presetLabel = removedPreset.name ? `"${removedPreset.name}"` : 'selected preset'
|
|
33
|
+
logWarning?.(`Removed ${presetLabel} from .zephyr/config.json because it is invalid.`)
|
|
34
|
+
isCreatingNewPreset = true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (preset === 'create') {
|
|
38
|
+
isCreatingNewPreset = true
|
|
39
|
+
server = await configurationService.selectServer(servers)
|
|
40
|
+
appConfig = await configurationService.selectApp(projectConfig, server, rootDir)
|
|
41
|
+
} else if (preset) {
|
|
42
|
+
if (preset.appId) {
|
|
43
|
+
appConfig = projectConfig.apps?.find((app) => app.id === preset.appId)
|
|
44
|
+
|
|
45
|
+
if (!appConfig) {
|
|
46
|
+
logWarning?.('Preset references an application that no longer exists. Creating a new configuration instead.')
|
|
47
|
+
await removeInvalidPreset()
|
|
48
|
+
server = await configurationService.selectServer(servers)
|
|
49
|
+
appConfig = await configurationService.selectApp(projectConfig, server, rootDir)
|
|
50
|
+
} else {
|
|
51
|
+
server = servers.find((entry) => entry.id === appConfig.serverId || entry.serverName === appConfig.serverName)
|
|
52
|
+
|
|
53
|
+
if (!server) {
|
|
54
|
+
logWarning?.('Preset references a server that no longer exists. Creating a new configuration instead.')
|
|
55
|
+
await removeInvalidPreset()
|
|
56
|
+
server = await configurationService.selectServer(servers)
|
|
57
|
+
appConfig = await configurationService.selectApp(projectConfig, server, rootDir)
|
|
58
|
+
} else if (preset.branch && appConfig.branch !== preset.branch) {
|
|
59
|
+
appConfig.branch = preset.branch
|
|
60
|
+
await saveProjectConfig(rootDir, projectConfig)
|
|
61
|
+
logSuccess?.(`Updated branch to ${preset.branch} from preset.`)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} else if (preset.key) {
|
|
65
|
+
const keyParts = preset.key.split(':')
|
|
66
|
+
const serverName = keyParts[0]
|
|
67
|
+
const projectPath = keyParts[1]
|
|
68
|
+
const presetBranch = preset.branch || (keyParts.length === 3 ? keyParts[2] : null)
|
|
69
|
+
|
|
70
|
+
server = servers.find((entry) => entry.serverName === serverName)
|
|
71
|
+
|
|
72
|
+
if (!server) {
|
|
73
|
+
logWarning?.(`Preset references server "${serverName}" which no longer exists. Creating a new configuration instead.`)
|
|
74
|
+
await removeInvalidPreset()
|
|
75
|
+
server = await configurationService.selectServer(servers)
|
|
76
|
+
appConfig = await configurationService.selectApp(projectConfig, server, rootDir)
|
|
77
|
+
} else {
|
|
78
|
+
appConfig = projectConfig.apps?.find(
|
|
79
|
+
(app) => (app.serverId === server.id || app.serverName === serverName) && app.projectPath === projectPath
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if (!appConfig) {
|
|
83
|
+
logWarning?.('Preset references an application that no longer exists. Creating a new configuration instead.')
|
|
84
|
+
await removeInvalidPreset()
|
|
85
|
+
appConfig = await configurationService.selectApp(projectConfig, server, rootDir)
|
|
86
|
+
} else {
|
|
87
|
+
preset.appId = appConfig.id
|
|
88
|
+
if (presetBranch && appConfig.branch !== presetBranch) {
|
|
89
|
+
appConfig.branch = presetBranch
|
|
90
|
+
}
|
|
91
|
+
preset.branch = appConfig.branch
|
|
92
|
+
await saveProjectConfig(rootDir, projectConfig)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
logWarning?.('Preset format is invalid. Creating a new configuration instead.')
|
|
97
|
+
await removeInvalidPreset()
|
|
98
|
+
server = await configurationService.selectServer(servers)
|
|
99
|
+
appConfig = await configurationService.selectApp(projectConfig, server, rootDir)
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
server = await configurationService.selectServer(servers)
|
|
103
|
+
appConfig = await configurationService.selectApp(projectConfig, server, rootDir)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const updated = await configurationService.ensureSshDetails(appConfig, rootDir)
|
|
107
|
+
|
|
108
|
+
if (updated) {
|
|
109
|
+
await saveProjectConfig(rootDir, projectConfig)
|
|
110
|
+
logSuccess?.('Updated .zephyr/config.json with SSH details.')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const deploymentConfig = {
|
|
114
|
+
serverName: server.serverName,
|
|
115
|
+
serverIp: server.serverIp,
|
|
116
|
+
projectPath: appConfig.projectPath,
|
|
117
|
+
branch: appConfig.branch,
|
|
118
|
+
sshUser: appConfig.sshUser,
|
|
119
|
+
sshKey: appConfig.sshKey
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
logProcessing?.('\nSelected deployment target:')
|
|
123
|
+
writeStdoutLine(JSON.stringify(deploymentConfig, null, 2))
|
|
124
|
+
|
|
125
|
+
if (isCreatingNewPreset || !preset) {
|
|
126
|
+
const {presetName} = await runPrompt([
|
|
127
|
+
{
|
|
128
|
+
type: 'input',
|
|
129
|
+
name: 'presetName',
|
|
130
|
+
message: 'Enter a name for this preset (leave blank to skip)',
|
|
131
|
+
default: isCreatingNewPreset ? '' : undefined
|
|
132
|
+
}
|
|
133
|
+
])
|
|
134
|
+
|
|
135
|
+
const trimmedName = presetName?.trim()
|
|
136
|
+
|
|
137
|
+
if (trimmedName && trimmedName.length > 0) {
|
|
138
|
+
const presets = projectConfig.presets ?? []
|
|
139
|
+
const appId = appConfig.id
|
|
140
|
+
|
|
141
|
+
if (!appId) {
|
|
142
|
+
logWarning?.('Cannot save preset: app configuration missing ID.')
|
|
143
|
+
} else {
|
|
144
|
+
const existingIndex = presets.findIndex((entry) => entry.appId === appId)
|
|
145
|
+
|
|
146
|
+
if (existingIndex >= 0) {
|
|
147
|
+
presets[existingIndex].name = trimmedName
|
|
148
|
+
presets[existingIndex].branch = deploymentConfig.branch
|
|
149
|
+
} else {
|
|
150
|
+
presets.push({
|
|
151
|
+
name: trimmedName,
|
|
152
|
+
appId,
|
|
153
|
+
branch: deploymentConfig.branch
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
projectConfig.presets = presets
|
|
158
|
+
await saveProjectConfig(rootDir, projectConfig)
|
|
159
|
+
logSuccess?.(`Saved preset "${trimmedName}" to .zephyr/config.json`)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {deploymentConfig, projectConfig}
|
|
165
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import inquirer from 'inquirer'
|
|
2
|
+
|
|
3
|
+
import {generateId} from '../../utils/id.mjs'
|
|
4
|
+
import {saveServers} from '../../config/servers.mjs'
|
|
5
|
+
|
|
6
|
+
export async function promptServerDetails({
|
|
7
|
+
existingServers = [],
|
|
8
|
+
runPrompt,
|
|
9
|
+
createId = generateId
|
|
10
|
+
} = {}) {
|
|
11
|
+
const defaults = {
|
|
12
|
+
serverName: existingServers.length === 0 ? 'home' : `server-${existingServers.length + 1}`,
|
|
13
|
+
serverIp: '1.1.1.1'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const answers = await runPrompt([
|
|
17
|
+
{
|
|
18
|
+
type: 'input',
|
|
19
|
+
name: 'serverName',
|
|
20
|
+
message: 'Enter a server name',
|
|
21
|
+
default: defaults.serverName
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
type: 'input',
|
|
25
|
+
name: 'serverIp',
|
|
26
|
+
message: 'Enter the server IP address',
|
|
27
|
+
default: defaults.serverIp
|
|
28
|
+
}
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
id: createId(),
|
|
33
|
+
serverName: answers.serverName.trim() || defaults.serverName,
|
|
34
|
+
serverIp: answers.serverIp.trim() || defaults.serverIp
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function selectServer({
|
|
39
|
+
servers,
|
|
40
|
+
runPrompt,
|
|
41
|
+
logProcessing,
|
|
42
|
+
logSuccess,
|
|
43
|
+
persistServers = saveServers,
|
|
44
|
+
promptServerDetails
|
|
45
|
+
} = {}) {
|
|
46
|
+
if (servers.length === 0) {
|
|
47
|
+
logProcessing?.('No servers are configured yet. Creating one now.')
|
|
48
|
+
const server = await promptServerDetails()
|
|
49
|
+
servers.push(server)
|
|
50
|
+
await persistServers(servers)
|
|
51
|
+
logSuccess?.('Saved server configuration to ~/.config/zephyr/servers.json')
|
|
52
|
+
return server
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const choices = servers.map((server, index) => ({
|
|
56
|
+
name: `${server.serverName} (${server.serverIp})`,
|
|
57
|
+
value: index
|
|
58
|
+
}))
|
|
59
|
+
|
|
60
|
+
choices.push(
|
|
61
|
+
new inquirer.Separator(),
|
|
62
|
+
{
|
|
63
|
+
name: '➕ Create a new server',
|
|
64
|
+
value: 'create'
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
const {selection} = await runPrompt([
|
|
69
|
+
{
|
|
70
|
+
type: 'list',
|
|
71
|
+
name: 'selection',
|
|
72
|
+
message: 'Select a server',
|
|
73
|
+
choices,
|
|
74
|
+
default: 0
|
|
75
|
+
}
|
|
76
|
+
])
|
|
77
|
+
|
|
78
|
+
if (selection === 'create') {
|
|
79
|
+
const server = await promptServerDetails(servers)
|
|
80
|
+
servers.push(server)
|
|
81
|
+
await persistServers(servers)
|
|
82
|
+
logSuccess?.('Appended server configuration to ~/.config/zephyr/servers.json')
|
|
83
|
+
return server
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return servers[selection]
|
|
87
|
+
}
|