@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wyxos/zephyr",
3
- "version": "0.7.5",
3
+ "version": "0.8.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",
@@ -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
+ }
@@ -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 && options.versionArg) {
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 = await resolvePendingSnapshot(rootDir, deploymentConfig, {
312
- runPrompt,
313
- logProcessing,
314
- logWarning,
315
- executionMode: currentExecutionMode
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,