@wyxos/zephyr 0.2.31 → 0.3.1
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 +42 -5
- package/bin/zephyr.mjs +7 -2
- 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 +341 -0
- package/src/application/release/release-packagist-package.mjs +223 -0
- package/src/config/project.mjs +13 -0
- package/src/deploy/local-repo.mjs +188 -68
- package/src/deploy/remote-exec.mjs +2 -3
- package/src/index.mjs +15 -84
- package/src/main.mjs +78 -641
- package/src/release/shared.mjs +120 -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
|
@@ -16,7 +16,13 @@ npx @wyxos/zephyr
|
|
|
16
16
|
|
|
17
17
|
## Usage
|
|
18
18
|
|
|
19
|
-
Navigate to your
|
|
19
|
+
Navigate to your app or package directory and run:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm run release
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or invoke Zephyr directly:
|
|
20
26
|
|
|
21
27
|
```bash
|
|
22
28
|
zephyr
|
|
@@ -28,13 +34,32 @@ See all flags:
|
|
|
28
34
|
zephyr --help
|
|
29
35
|
```
|
|
30
36
|
|
|
31
|
-
Common
|
|
37
|
+
Common workflows:
|
|
32
38
|
|
|
33
39
|
```bash
|
|
34
|
-
#
|
|
40
|
+
# Deploy an app using the saved preset or the interactive prompts
|
|
41
|
+
zephyr
|
|
42
|
+
|
|
43
|
+
# Deploy an app and bump the local npm package version first
|
|
44
|
+
zephyr minor
|
|
45
|
+
|
|
46
|
+
# Release a Node/Vue package (defaults to a patch bump)
|
|
35
47
|
zephyr --type node
|
|
48
|
+
|
|
49
|
+
# Release a Node/Vue package with an explicit bump
|
|
50
|
+
zephyr --type node minor
|
|
51
|
+
|
|
52
|
+
# Release a Packagist package
|
|
53
|
+
zephyr --type packagist patch
|
|
36
54
|
```
|
|
37
55
|
|
|
56
|
+
When `--type node` or `--type vue` is used without a bump argument, Zephyr defaults to `patch`.
|
|
57
|
+
|
|
58
|
+
On a first run inside a project with `package.json`, Zephyr can:
|
|
59
|
+
- add `.zephyr/` to `.gitignore`
|
|
60
|
+
- add a `release` script that runs `npx @wyxos/zephyr@latest`
|
|
61
|
+
- create global server config and per-project deployment config interactively
|
|
62
|
+
|
|
38
63
|
Follow the interactive prompts to configure your deployment target:
|
|
39
64
|
- Server name and IP address
|
|
40
65
|
- Project path on the remote server
|
|
@@ -43,6 +68,18 @@ Follow the interactive prompts to configure your deployment target:
|
|
|
43
68
|
|
|
44
69
|
Configuration is saved automatically for future deployments.
|
|
45
70
|
|
|
71
|
+
## Project Scripts
|
|
72
|
+
|
|
73
|
+
The recommended entrypoint in consumer projects is:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npm run release
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- `npm run release` is the recommended app/package entrypoint once the release script has been installed.
|
|
80
|
+
- For `--type node` workflows, Zephyr runs your project's `lint` script when present.
|
|
81
|
+
- For `--type node` workflows, Zephyr runs `test:run` or `test` when present.
|
|
82
|
+
|
|
46
83
|
## Features
|
|
47
84
|
|
|
48
85
|
- Automated Git operations (branch switching, commits, pushes)
|
|
@@ -62,7 +99,7 @@ Configuration is saved automatically for future deployments.
|
|
|
62
99
|
Zephyr analyzes changed files and runs appropriate tasks:
|
|
63
100
|
|
|
64
101
|
- **Always**: `git pull origin <branch>`
|
|
65
|
-
- **Composer files changed** (`composer.json` / `composer.lock`): `composer
|
|
102
|
+
- **Composer files changed** (`composer.json` / `composer.lock`): `composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader` (requires `composer.lock`)
|
|
66
103
|
- **Migrations changed** (`database/migrations/*.php`): `php artisan migrate --force`
|
|
67
104
|
- **Node dependency files changed** (`package.json` / `package-lock.json`, including nested): `npm install`
|
|
68
105
|
- **Frontend files changed** (`.vue/.js/.ts/.tsx/.css/.scss/.less`): `npm run build`
|
|
@@ -131,4 +168,4 @@ The `.zephyr/` directory is automatically added to `.gitignore`.
|
|
|
131
168
|
|
|
132
169
|
- Node.js 16+
|
|
133
170
|
- Git
|
|
134
|
-
- SSH access to target servers
|
|
171
|
+
- SSH access to target servers
|
package/bin/zephyr.mjs
CHANGED
|
@@ -12,13 +12,18 @@ const program = new Command()
|
|
|
12
12
|
program
|
|
13
13
|
.name('zephyr')
|
|
14
14
|
.description('A streamlined deployment tool for web applications with intelligent Laravel project detection')
|
|
15
|
-
.option('--type <type>', '
|
|
15
|
+
.option('--type <type>', 'Workflow type (node|vue|packagist). Omit for normal app deployments.')
|
|
16
|
+
.argument(
|
|
17
|
+
'[version]',
|
|
18
|
+
'Version or npm bump type for deployments (e.g. 1.2.3, patch, minor, major). --type node/vue/packagist workflows accept bump types only and default to patch.'
|
|
19
|
+
)
|
|
16
20
|
|
|
17
21
|
program.parse(process.argv)
|
|
18
22
|
const options = program.opts()
|
|
23
|
+
const versionArg = program.args[0] ?? null
|
|
19
24
|
|
|
20
25
|
try {
|
|
21
|
-
await main(options.type ?? null)
|
|
26
|
+
await main(options.type ?? null, versionArg)
|
|
22
27
|
} catch (error) {
|
|
23
28
|
logError(error?.message || String(error))
|
|
24
29
|
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.1",
|
|
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
|
+
}
|