@wyxos/zephyr 0.7.5 → 0.8.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
CHANGED
|
@@ -49,6 +49,9 @@ zephyr minor --skip-checks
|
|
|
49
49
|
# Deploy a configured app non-interactively
|
|
50
50
|
zephyr --non-interactive --preset wyxos-release --maintenance off
|
|
51
51
|
|
|
52
|
+
# Configure a Laravel app target and verify SSH without deploying
|
|
53
|
+
zephyr --setup
|
|
54
|
+
|
|
52
55
|
# Deploy a configured app non-interactively and auto-commit dirty changes
|
|
53
56
|
zephyr --non-interactive --preset wyxos-release --auto-commit
|
|
54
57
|
|
|
@@ -106,6 +109,8 @@ then non-interactive mode stops immediately with a clear error instead.
|
|
|
106
109
|
|
|
107
110
|
For Laravel app deployments, `--maintenance on|off` overrides both the saved preset preference and the maintenance prompt when you want an explicit choice for the current run.
|
|
108
111
|
|
|
112
|
+
`--setup` is Laravel-only. It first verifies that the current project is a local Laravel app, then runs the normal local configuration prompts, tests SSH authentication to the selected server, and exits before local deploy preparation, pending snapshot handling, maintenance-mode decisions, locks, or remote deployment commands. On non-Laravel projects it fails before local setup changes are written.
|
|
113
|
+
|
|
109
114
|
`--auto-commit` is available for app deployments and tells Zephyr to let local Codex inspect the repo and generate the dirty-tree commit message instead of prompting for one.
|
|
110
115
|
|
|
111
116
|
`--skip-versioning` keeps Zephyr from mutating `package.json` or `composer.json`. On app deploys it skips the local npm version bump step. On package release workflows it releases the version already present in the manifest and creates the release tag from the current `HEAD`.
|
package/package.json
CHANGED
|
@@ -15,6 +15,7 @@ import {resolveRemotePath} from '../../utils/remote-path.mjs'
|
|
|
15
15
|
import {buildRemoteDeploymentPlan, resolveRemoteDeploymentState} from './build-remote-deployment-plan.mjs'
|
|
16
16
|
import {executeRemoteDeploymentPlan} from './execute-remote-deployment-plan.mjs'
|
|
17
17
|
import {prepareLocalDeployment} from './prepare-local-deployment.mjs'
|
|
18
|
+
import {verifyLaravelSetup} from './verify-laravel-setup.mjs'
|
|
18
19
|
|
|
19
20
|
async function resolveRemoteHome(ssh, sshUser) {
|
|
20
21
|
const remoteHomeResult = await ssh.execCommand('printf "%s" "$HOME"')
|
|
@@ -263,9 +264,22 @@ export async function runDeployment(config, options = {}) {
|
|
|
263
264
|
executionMode
|
|
264
265
|
} = context
|
|
265
266
|
|
|
267
|
+
const sshUser = config.sshUser || os.userInfo().username
|
|
268
|
+
|
|
269
|
+
if (executionMode?.setup === true) {
|
|
270
|
+
await verifyLaravelSetup({
|
|
271
|
+
config,
|
|
272
|
+
rootDir,
|
|
273
|
+
createSshClient,
|
|
274
|
+
sshUser,
|
|
275
|
+
logProcessing,
|
|
276
|
+
logSuccess
|
|
277
|
+
})
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
|
|
266
281
|
await cleanupOldLogs(rootDir)
|
|
267
282
|
|
|
268
|
-
const sshUser = config.sshUser || os.userInfo().username
|
|
269
283
|
const privateKeyPath = await resolveSshKeyPath(config.sshKey)
|
|
270
284
|
const privateKey = await fs.readFile(privateKeyPath, 'utf8')
|
|
271
285
|
let ssh = null
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
|
|
3
|
+
import {isLocalLaravelProject} from '../../deploy/preflight.mjs'
|
|
4
|
+
import {ZephyrError} from '../../runtime/errors.mjs'
|
|
5
|
+
import {resolveSshKeyPath} from '../../ssh/keys.mjs'
|
|
6
|
+
|
|
7
|
+
export async function assertLaravelSetupProject(rootDir) {
|
|
8
|
+
const isLaravel = await isLocalLaravelProject(rootDir)
|
|
9
|
+
|
|
10
|
+
if (!isLaravel) {
|
|
11
|
+
throw new ZephyrError(
|
|
12
|
+
'Zephyr setup is only supported for Laravel app projects.',
|
|
13
|
+
{code: 'ZEPHYR_SETUP_REQUIRES_LARAVEL'}
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function verifyLaravelSetup({
|
|
19
|
+
config,
|
|
20
|
+
rootDir,
|
|
21
|
+
createSshClient,
|
|
22
|
+
sshUser,
|
|
23
|
+
logProcessing,
|
|
24
|
+
logSuccess
|
|
25
|
+
} = {}) {
|
|
26
|
+
await assertLaravelSetupProject(rootDir)
|
|
27
|
+
|
|
28
|
+
const privateKeyPath = await resolveSshKeyPath(config.sshKey)
|
|
29
|
+
const privateKey = await fs.readFile(privateKeyPath, 'utf8')
|
|
30
|
+
const ssh = createSshClient()
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
logProcessing?.(`\nConnecting to ${config.serverIp} as ${sshUser} to verify SSH setup...`)
|
|
34
|
+
await ssh.connect({
|
|
35
|
+
host: config.serverIp,
|
|
36
|
+
username: sshUser,
|
|
37
|
+
privateKey
|
|
38
|
+
})
|
|
39
|
+
logSuccess?.('Setup verified. SSH connection succeeded for this Laravel app.')
|
|
40
|
+
} finally {
|
|
41
|
+
ssh.dispose()
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/cli/options.mjs
CHANGED
|
@@ -36,6 +36,7 @@ export function parseCliOptions(args = process.argv.slice(2)) {
|
|
|
36
36
|
.option('--type <type>', 'Workflow type (node|vue|packagist). Omit for normal app deployments.')
|
|
37
37
|
.option('--non-interactive', 'Fail instead of prompting when Zephyr needs user input.')
|
|
38
38
|
.option('--json', 'Emit NDJSON events to stdout. Requires --non-interactive.')
|
|
39
|
+
.option('--setup', 'Configure an app deployment target and verify SSH connectivity without deploying.')
|
|
39
40
|
.option('--preset <name>', 'Preset name to use for non-interactive app deployments.')
|
|
40
41
|
.option('--resume-pending', 'Resume a saved pending deployment snapshot without prompting.')
|
|
41
42
|
.option('--discard-pending', 'Discard a saved pending deployment snapshot without prompting.')
|
|
@@ -72,6 +73,7 @@ export function parseCliOptions(args = process.argv.slice(2)) {
|
|
|
72
73
|
versionArg: program.args[0] ?? null,
|
|
73
74
|
nonInteractive: Boolean(options.nonInteractive),
|
|
74
75
|
json: Boolean(options.json),
|
|
76
|
+
setup: Boolean(options.setup),
|
|
75
77
|
presetName: options.preset ?? null,
|
|
76
78
|
resumePending: Boolean(options.resumePending),
|
|
77
79
|
discardPending: Boolean(options.discardPending),
|
|
@@ -99,14 +101,19 @@ export function validateCliOptions(options = {}) {
|
|
|
99
101
|
workflowType = null,
|
|
100
102
|
nonInteractive = false,
|
|
101
103
|
json = false,
|
|
104
|
+
setup = false,
|
|
102
105
|
presetName = null,
|
|
103
106
|
resumePending = false,
|
|
104
107
|
discardPending = false,
|
|
105
108
|
maintenanceMode = null,
|
|
106
109
|
autoCommit = false,
|
|
107
110
|
skipVersioning = false,
|
|
111
|
+
skipChecks = false,
|
|
112
|
+
skipTests = false,
|
|
113
|
+
skipLint = false,
|
|
108
114
|
skipBuild = false,
|
|
109
|
-
skipDeploy = false
|
|
115
|
+
skipDeploy = false,
|
|
116
|
+
versionArg = null
|
|
110
117
|
} = options
|
|
111
118
|
|
|
112
119
|
if (json && !nonInteractive) {
|
|
@@ -120,6 +127,10 @@ export function validateCliOptions(options = {}) {
|
|
|
120
127
|
const isPackageRelease = workflowType === 'node' || workflowType === 'vue' || workflowType === 'packagist'
|
|
121
128
|
|
|
122
129
|
if (isPackageRelease) {
|
|
130
|
+
if (setup) {
|
|
131
|
+
throw new InvalidCliOptionsError('--setup is only valid for app deployments.')
|
|
132
|
+
}
|
|
133
|
+
|
|
123
134
|
if (presetName) {
|
|
124
135
|
throw new InvalidCliOptionsError('--preset is only valid for app deployments.')
|
|
125
136
|
}
|
|
@@ -140,12 +151,34 @@ export function validateCliOptions(options = {}) {
|
|
|
140
151
|
throw new InvalidCliOptionsError('--skip-build and --skip-deploy are only valid for node/vue release workflows.')
|
|
141
152
|
}
|
|
142
153
|
|
|
154
|
+
if (setup) {
|
|
155
|
+
if (versionArg) {
|
|
156
|
+
throw new InvalidCliOptionsError('--setup cannot be used with a version or bump argument.')
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (resumePending || discardPending) {
|
|
160
|
+
throw new InvalidCliOptionsError('--setup cannot be used with pending deployment snapshot flags.')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (maintenanceMode !== null) {
|
|
164
|
+
throw new InvalidCliOptionsError('--setup cannot be used with --maintenance.')
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (autoCommit) {
|
|
168
|
+
throw new InvalidCliOptionsError('--setup cannot be used with --auto-commit.')
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (skipVersioning || skipChecks || skipTests || skipLint) {
|
|
172
|
+
throw new InvalidCliOptionsError('--setup cannot be used with deployment skip flags.')
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
143
176
|
if (nonInteractive && !presetName) {
|
|
144
177
|
throw new InvalidCliOptionsError('--non-interactive app deployments require --preset <name>.')
|
|
145
178
|
}
|
|
146
179
|
}
|
|
147
180
|
|
|
148
|
-
if (skipVersioning &&
|
|
181
|
+
if (skipVersioning && versionArg) {
|
|
149
182
|
throw new InvalidCliOptionsError('--skip-versioning cannot be used together with an explicit version or bump argument.')
|
|
150
183
|
}
|
|
151
184
|
}
|
package/src/main.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import {createConfigurationService} from './application/configuration/service.mj
|
|
|
17
17
|
import {selectDeploymentTarget} from './application/configuration/select-deployment-target.mjs'
|
|
18
18
|
import {resolvePendingSnapshot} from './application/deploy/resolve-pending-snapshot.mjs'
|
|
19
19
|
import {runDeployment} from './application/deploy/run-deployment.mjs'
|
|
20
|
+
import {assertLaravelSetupProject} from './application/deploy/verify-laravel-setup.mjs'
|
|
20
21
|
import {SKIP_GIT_HOOKS_WARNING} from './utils/git-hooks.mjs'
|
|
21
22
|
import {notifyWorkflowResult} from './utils/notifications.mjs'
|
|
22
23
|
|
|
@@ -32,6 +33,7 @@ function normalizeMainOptions(firstArg = null, secondArg = null) {
|
|
|
32
33
|
versionArg: firstArg.versionArg ?? null,
|
|
33
34
|
nonInteractive: firstArg.nonInteractive === true,
|
|
34
35
|
json: firstArg.json === true,
|
|
36
|
+
setup: firstArg.setup === true,
|
|
35
37
|
presetName: firstArg.presetName ?? null,
|
|
36
38
|
resumePending: firstArg.resumePending === true,
|
|
37
39
|
discardPending: firstArg.discardPending === true,
|
|
@@ -60,6 +62,7 @@ function normalizeMainOptions(firstArg = null, secondArg = null) {
|
|
|
60
62
|
versionArg: secondArg ?? null,
|
|
61
63
|
nonInteractive: false,
|
|
62
64
|
json: false,
|
|
65
|
+
setup: false,
|
|
63
66
|
presetName: null,
|
|
64
67
|
resumePending: false,
|
|
65
68
|
discardPending: false,
|
|
@@ -127,6 +130,7 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
|
|
|
127
130
|
interactive: !options.nonInteractive,
|
|
128
131
|
json: options.json === true && options.nonInteractive === true,
|
|
129
132
|
workflow: resolveWorkflowName(options.workflowType),
|
|
133
|
+
setup: options.setup === true,
|
|
130
134
|
presetName: options.presetName,
|
|
131
135
|
maintenanceMode: options.maintenanceMode,
|
|
132
136
|
autoCommit: options.autoCommit === true,
|
|
@@ -157,7 +161,8 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
|
|
|
157
161
|
} = appContext
|
|
158
162
|
let currentExecutionMode = {
|
|
159
163
|
...executionMode,
|
|
160
|
-
...(appContext.executionMode ?? {})
|
|
164
|
+
...(appContext.executionMode ?? {}),
|
|
165
|
+
setup: executionMode.setup === true || appContext.executionMode?.setup === true
|
|
161
166
|
}
|
|
162
167
|
appContext.executionMode = currentExecutionMode
|
|
163
168
|
const configurationService = createConfigurationService(appContext)
|
|
@@ -171,6 +176,7 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
|
|
|
171
176
|
data: {
|
|
172
177
|
version: ZEPHYR_VERSION,
|
|
173
178
|
workflow: currentExecutionMode.workflow,
|
|
179
|
+
setup: currentExecutionMode.setup === true,
|
|
174
180
|
nonInteractive: currentExecutionMode.interactive === false,
|
|
175
181
|
presetName: currentExecutionMode.presetName,
|
|
176
182
|
maintenanceMode: currentExecutionMode.maintenanceMode,
|
|
@@ -198,6 +204,10 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
|
|
|
198
204
|
appContext
|
|
199
205
|
})
|
|
200
206
|
|
|
207
|
+
if (currentExecutionMode.setup) {
|
|
208
|
+
await assertLaravelSetupProject(rootDir)
|
|
209
|
+
}
|
|
210
|
+
|
|
201
211
|
if (options.workflowType === 'node' || options.workflowType === 'vue') {
|
|
202
212
|
await releaseNode({
|
|
203
213
|
releaseType: options.versionArg,
|
|
@@ -277,7 +287,7 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
|
|
|
277
287
|
const hasPackageJson = await fs.access(packageJsonPath).then(() => true).catch(() => false)
|
|
278
288
|
const hasComposerJson = await fs.access(composerJsonPath).then(() => true).catch(() => false)
|
|
279
289
|
|
|
280
|
-
if (hasPackageJson || hasComposerJson) {
|
|
290
|
+
if (!currentExecutionMode.setup && (hasPackageJson || hasComposerJson)) {
|
|
281
291
|
logProcessing('Validating dependencies...')
|
|
282
292
|
await validateLocalDependencies(rootDir, runPrompt, logSuccess, {
|
|
283
293
|
interactive: currentExecutionMode.interactive,
|
|
@@ -292,7 +302,8 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
|
|
|
292
302
|
logSuccess,
|
|
293
303
|
logWarning,
|
|
294
304
|
emitEvent,
|
|
295
|
-
executionMode: currentExecutionMode
|
|
305
|
+
executionMode: currentExecutionMode,
|
|
306
|
+
promptPresetOptions: currentExecutionMode.setup !== true
|
|
296
307
|
})
|
|
297
308
|
|
|
298
309
|
if (presetState) {
|
|
@@ -308,12 +319,14 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
|
|
|
308
319
|
await presetState.applyExecutionMode(currentExecutionMode)
|
|
309
320
|
}
|
|
310
321
|
|
|
311
|
-
const snapshotToUse =
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
322
|
+
const snapshotToUse = currentExecutionMode.setup
|
|
323
|
+
? null
|
|
324
|
+
: await resolvePendingSnapshot(rootDir, deploymentConfig, {
|
|
325
|
+
runPrompt,
|
|
326
|
+
logProcessing,
|
|
327
|
+
logWarning,
|
|
328
|
+
executionMode: currentExecutionMode
|
|
329
|
+
})
|
|
317
330
|
|
|
318
331
|
await runRemoteTasks(deploymentConfig, {
|
|
319
332
|
rootDir,
|
|
@@ -22,6 +22,7 @@ export function createAppContext({
|
|
|
22
22
|
interactive: executionMode.interactive !== false,
|
|
23
23
|
json: executionMode.json === true,
|
|
24
24
|
workflow: executionMode.workflow ?? 'deploy',
|
|
25
|
+
setup: executionMode.setup === true,
|
|
25
26
|
presetName: executionMode.presetName ?? null,
|
|
26
27
|
maintenanceMode: executionMode.maintenanceMode ?? null,
|
|
27
28
|
autoCommit: executionMode.autoCommit === true,
|