@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.
Files changed (33) hide show
  1. package/README.md +55 -2
  2. package/bin/zephyr.mjs +3 -1
  3. package/package.json +7 -2
  4. package/src/application/configuration/app-details.mjs +89 -0
  5. package/src/application/configuration/app-selection.mjs +87 -0
  6. package/src/application/configuration/preset-selection.mjs +59 -0
  7. package/src/application/configuration/select-deployment-target.mjs +165 -0
  8. package/src/application/configuration/server-selection.mjs +87 -0
  9. package/src/application/configuration/service.mjs +109 -0
  10. package/src/application/deploy/build-remote-deployment-plan.mjs +174 -0
  11. package/src/application/deploy/bump-local-package-version.mjs +81 -0
  12. package/src/application/deploy/execute-remote-deployment-plan.mjs +61 -0
  13. package/src/{utils/task-planner.mjs → application/deploy/plan-laravel-deployment-tasks.mjs} +5 -4
  14. package/src/application/deploy/prepare-local-deployment.mjs +52 -0
  15. package/src/application/deploy/resolve-local-deployment-context.mjs +17 -0
  16. package/src/application/deploy/resolve-pending-snapshot.mjs +45 -0
  17. package/src/application/deploy/run-deployment.mjs +147 -0
  18. package/src/application/deploy/run-local-deployment-checks.mjs +80 -0
  19. package/src/application/release/release-node-package.mjs +340 -0
  20. package/src/application/release/release-packagist-package.mjs +223 -0
  21. package/src/config/project.mjs +13 -0
  22. package/src/deploy/local-repo.mjs +187 -67
  23. package/src/deploy/remote-exec.mjs +2 -3
  24. package/src/index.mjs +27 -85
  25. package/src/main.mjs +78 -641
  26. package/src/release/shared.mjs +104 -0
  27. package/src/release-node.mjs +20 -424
  28. package/src/release-packagist.mjs +20 -291
  29. package/src/runtime/app-context.mjs +36 -0
  30. package/src/targets/index.mjs +24 -0
  31. package/src/utils/output.mjs +41 -16
  32. package/src/utils/config-flow.mjs +0 -284
  33. /package/src/{utils/php-version.mjs → infrastructure/php/version.mjs} +0 -0
@@ -1,296 +1,25 @@
1
- import { join } from 'node:path'
2
- import { readFile, writeFile } from 'node:fs/promises'
3
- import fs from 'node:fs'
4
1
  import process from 'node:process'
5
- import semver from 'semver'
6
- import inquirer from 'inquirer'
7
- import { validateLocalDependencies } from './dependency-scanner.mjs'
8
- import { writeStderr, writeStderrLine, writeStdoutLine } from './utils/output.mjs'
9
- import { runCommand as runCommandBase, runCommandCapture as runCommandCaptureBase } from './utils/command.mjs'
10
- import { ensureUpToDateWithUpstream, getCurrentBranch as getGitCurrentBranch, getUpstreamRef } from './utils/git.mjs'
2
+ import chalk from 'chalk'
3
+ import {createChalkLogger} from './utils/output.mjs'
4
+ import {
5
+ parseReleaseArgs,
6
+ } from './release/shared.mjs'
7
+ import {releasePackagistPackage} from './application/release/release-packagist-package.mjs'
11
8
 
12
- const STEP_PREFIX = '→'
13
- const OK_PREFIX = '✔'
14
- const WARN_PREFIX = '⚠'
15
-
16
- const IS_WINDOWS = process.platform === 'win32'
17
-
18
- function logStep(message) {
19
- writeStdoutLine(`${STEP_PREFIX} ${message}`)
20
- }
21
-
22
- function logSuccess(message) {
23
- writeStdoutLine(`${OK_PREFIX} ${message}`)
24
- }
25
-
26
- function logWarning(message) {
27
- writeStderrLine(`${WARN_PREFIX} ${message}`)
28
- }
29
-
30
- async function runCommand(command, args, { cwd = process.cwd(), capture = false } = {}) {
31
- if (capture) {
32
- const { stdout, stderr } = await runCommandCaptureBase(command, args, { cwd })
33
- return { stdout: stdout.trim(), stderr: stderr.trim() }
34
- }
35
-
36
- await runCommandBase(command, args, { cwd })
37
- return undefined
38
- }
39
-
40
- async function readComposer(rootDir = process.cwd()) {
41
- const composerPath = join(rootDir, 'composer.json')
42
- const raw = await readFile(composerPath, 'utf8')
43
- return JSON.parse(raw)
44
- }
45
-
46
- async function writeComposer(rootDir, composer, composerPath = null) {
47
- const pathToUse = composerPath || join(rootDir, 'composer.json')
48
- const content = JSON.stringify(composer, null, 2) + '\n'
49
- await writeFile(pathToUse, content, 'utf8')
50
- }
51
-
52
- function hasComposerScript(composer, scriptName) {
53
- return composer?.scripts?.[scriptName] !== undefined
54
- }
55
-
56
- async function hasLaravelPint(rootDir = process.cwd()) {
57
- const pintPath = join(rootDir, 'vendor', 'bin', 'pint')
58
- try {
59
- await fs.promises.access(pintPath)
60
- const stats = await fs.promises.stat(pintPath)
61
- return stats.isFile()
62
- } catch {
63
- return false
64
- }
65
- }
66
-
67
- async function hasArtisan(rootDir = process.cwd()) {
68
- const artisanPath = join(rootDir, 'artisan')
69
- try {
70
- await fs.promises.access(artisanPath)
71
- const stats = await fs.promises.stat(artisanPath)
72
- return stats.isFile()
73
- } catch {
74
- return false
75
- }
76
- }
77
-
78
- async function ensureCleanWorkingTree(rootDir = process.cwd()) {
79
- const { stdout } = await runCommand('git', ['status', '--porcelain'], { capture: true, cwd: rootDir })
80
-
81
- if (stdout.length > 0) {
82
- throw new Error('Working tree has uncommitted changes. Commit or stash them before releasing.')
83
- }
84
- }
85
-
86
- // Git helpers imported from src/utils/git.mjs
87
-
88
- function parseArgs() {
89
- const args = process.argv.slice(2)
90
- // Filter out --type flag as it's handled by zephyr CLI
91
- const filteredArgs = args.filter((arg) => !arg.startsWith('--type='))
92
- const positionals = filteredArgs.filter((arg) => !arg.startsWith('--'))
93
- const flags = new Set(filteredArgs.filter((arg) => arg.startsWith('--')))
94
-
95
- const releaseType = positionals[0] ?? 'patch'
96
- const skipTests = flags.has('--skip-tests')
97
- const skipLint = flags.has('--skip-lint')
98
-
99
- const allowedTypes = new Set([
100
- 'major',
101
- 'minor',
102
- 'patch',
103
- 'premajor',
104
- 'preminor',
105
- 'prepatch',
106
- 'prerelease'
107
- ])
108
-
109
- if (!allowedTypes.has(releaseType)) {
110
- throw new Error(
111
- `Invalid release type "${releaseType}". Use one of: ${Array.from(allowedTypes).join(', ')}.`
112
- )
113
- }
114
-
115
- return { releaseType, skipTests, skipLint }
116
- }
117
-
118
- async function runLint(skipLint, rootDir = process.cwd()) {
119
- if (skipLint) {
120
- logWarning('Skipping lint because --skip-lint flag was provided.')
121
- return
122
- }
123
-
124
- const hasPint = await hasLaravelPint(rootDir)
125
- if (!hasPint) {
126
- logStep('Skipping lint (Laravel Pint not found).')
127
- return
128
- }
129
-
130
- logStep('Running Laravel Pint...')
131
- const pintPath = IS_WINDOWS ? 'vendor\\bin\\pint' : 'vendor/bin/pint'
132
-
133
- let dotInterval = null
134
- try {
135
- // Capture output and show dots as progress
136
- process.stdout.write(' ')
137
- dotInterval = setInterval(() => {
138
- process.stdout.write('.')
139
- }, 200)
140
-
141
- await runCommand('php', [pintPath], { capture: true, cwd: rootDir })
142
-
143
- if (dotInterval) {
144
- clearInterval(dotInterval)
145
- dotInterval = null
146
- }
147
- process.stdout.write('\n')
148
- logSuccess('Lint passed.')
149
- } catch (error) {
150
- // Clear dots and show error output
151
- if (dotInterval) {
152
- clearInterval(dotInterval)
153
- dotInterval = null
154
- }
155
- process.stdout.write('\n')
156
- if (error.stdout) {
157
- writeStderr(error.stdout)
158
- }
159
- if (error.stderr) {
160
- writeStderr(error.stderr)
161
- }
162
- throw error
163
- }
164
- }
165
-
166
- async function runTests(skipTests, composer, rootDir = process.cwd()) {
167
- if (skipTests) {
168
- logWarning('Skipping tests because --skip-tests flag was provided.')
169
- return
170
- }
171
-
172
- const hasArtisanFile = await hasArtisan(rootDir)
173
- const hasTestScript = hasComposerScript(composer, 'test')
174
-
175
- if (!hasArtisanFile && !hasTestScript) {
176
- logStep('Skipping tests (no artisan file or test script found).')
177
- return
178
- }
179
-
180
- logStep('Running test suite...')
181
-
182
- let dotInterval = null
183
- try {
184
- // Capture output and show dots as progress
185
- process.stdout.write(' ')
186
- dotInterval = setInterval(() => {
187
- process.stdout.write('.')
188
- }, 200)
189
-
190
- if (hasArtisanFile) {
191
- await runCommand('php', ['artisan', 'test', '--compact'], { capture: true, cwd: rootDir })
192
- } else if (hasTestScript) {
193
- await runCommand('composer', ['test'], { capture: true, cwd: rootDir })
194
- }
195
-
196
- if (dotInterval) {
197
- clearInterval(dotInterval)
198
- dotInterval = null
199
- }
200
- process.stdout.write('\n')
201
- logSuccess('Tests passed.')
202
- } catch (error) {
203
- // Clear dots and show error output
204
- if (dotInterval) {
205
- clearInterval(dotInterval)
206
- dotInterval = null
207
- }
208
- process.stdout.write('\n')
209
- if (error.stdout) {
210
- writeStderr(error.stdout)
211
- }
212
- if (error.stderr) {
213
- writeStderr(error.stderr)
214
- }
215
- throw error
216
- }
217
- }
218
-
219
- async function bumpVersion(releaseType, rootDir = process.cwd()) {
220
- logStep(`Bumping composer version...`)
221
-
222
- const composer = await readComposer(rootDir)
223
- const currentVersion = composer.version || '0.0.0'
224
-
225
- if (!semver.valid(currentVersion)) {
226
- throw new Error(`Invalid current version "${currentVersion}" in composer.json. Must be a valid semver.`)
227
- }
228
-
229
- const newVersion = semver.inc(currentVersion, releaseType)
230
- if (!newVersion) {
231
- throw new Error(`Failed to calculate next ${releaseType} version from ${currentVersion}`)
232
- }
233
-
234
- composer.version = newVersion
235
- await writeComposer(rootDir, composer)
236
-
237
- logStep('Staging composer.json...')
238
- await runCommand('git', ['add', 'composer.json'], { cwd: rootDir })
239
-
240
- const commitMessage = `chore: release ${newVersion}`
241
- logStep('Committing version bump...')
242
- await runCommand('git', ['commit', '-m', commitMessage], { cwd: rootDir })
243
-
244
- logStep('Creating git tag...')
245
- await runCommand('git', ['tag', `v${newVersion}`], { cwd: rootDir })
246
-
247
- logSuccess(`Version updated to ${newVersion}.`)
248
- return { ...composer, version: newVersion }
249
- }
250
-
251
- async function pushChanges(rootDir = process.cwd()) {
252
- logStep('Pushing commits to origin...')
253
- await runCommand('git', ['push'], { cwd: rootDir })
254
-
255
- logStep('Pushing tags to origin...')
256
- await runCommand('git', ['push', 'origin', '--tags'], { cwd: rootDir })
257
-
258
- logSuccess('Git push completed.')
259
- }
9
+ const {logProcessing: logStep, logSuccess, logWarning} = createChalkLogger(chalk)
260
10
 
261
11
  export async function releasePackagist() {
262
- const { releaseType, skipTests, skipLint } = parseArgs()
263
- const rootDir = process.cwd()
264
-
265
- logStep('Reading composer metadata...')
266
- const composer = await readComposer(rootDir)
267
-
268
- if (!composer.version) {
269
- throw new Error('composer.json does not have a version field. Add "version": "0.0.0" to composer.json.')
270
- }
271
-
272
- logStep('Validating dependencies...')
273
- await validateLocalDependencies(rootDir, (questions) => inquirer.prompt(questions), logSuccess)
274
-
275
- logStep('Checking working tree status...')
276
- await ensureCleanWorkingTree(rootDir)
277
-
278
- const branch = await getGitCurrentBranch(rootDir, { method: 'show-current' })
279
- if (!branch) {
280
- throw new Error('Unable to determine current branch.')
281
- }
282
-
283
- logStep(`Current branch: ${branch}`)
284
- const upstreamRef = await getUpstreamRef(rootDir)
285
- await ensureUpToDateWithUpstream({ branch, upstreamRef, rootDir, logStep, logWarning })
286
-
287
- await runLint(skipLint, rootDir)
288
- await runTests(skipTests, composer, rootDir)
289
-
290
- const updatedComposer = await bumpVersion(releaseType, rootDir)
291
- await pushChanges(rootDir)
292
-
293
- logSuccess(`Release workflow completed for ${composer.name}@${updatedComposer.version}.`)
294
- logStep('Note: Packagist will automatically detect the new git tag and update the package.')
12
+ const {releaseType, skipTests, skipLint} = parseReleaseArgs({
13
+ booleanFlags: ['--skip-tests', '--skip-lint']
14
+ })
15
+ const rootDir = process.cwd()
16
+ await releasePackagistPackage({
17
+ releaseType,
18
+ skipTests,
19
+ skipLint,
20
+ rootDir,
21
+ logStep,
22
+ logSuccess,
23
+ logWarning
24
+ })
295
25
  }
296
-
@@ -0,0 +1,36 @@
1
+ import chalk from 'chalk'
2
+ import inquirer from 'inquirer'
3
+ import {NodeSSH} from 'node-ssh'
4
+
5
+ import {createChalkLogger} from '../utils/output.mjs'
6
+ import {runCommand as runCommandBase, runCommandCapture as runCommandCaptureBase} from '../utils/command.mjs'
7
+ import {createLocalCommandRunners} from './local-command.mjs'
8
+ import {createRunPrompt} from './prompt.mjs'
9
+ import {createSshClientFactory} from './ssh-client.mjs'
10
+
11
+ export function createAppContext({
12
+ chalkInstance = chalk,
13
+ inquirerInstance = inquirer,
14
+ NodeSSHClass = NodeSSH,
15
+ runCommandImpl = runCommandBase,
16
+ runCommandCaptureImpl = runCommandCaptureBase
17
+ } = {}) {
18
+ const {logProcessing, logSuccess, logWarning, logError} = createChalkLogger(chalkInstance)
19
+ const runPrompt = createRunPrompt({inquirer: inquirerInstance})
20
+ const createSshClient = createSshClientFactory({NodeSSH: NodeSSHClass})
21
+ const {runCommand, runCommandCapture} = createLocalCommandRunners({
22
+ runCommandBase: runCommandImpl,
23
+ runCommandCaptureBase: runCommandCaptureImpl
24
+ })
25
+
26
+ return {
27
+ logProcessing,
28
+ logSuccess,
29
+ logWarning,
30
+ logError,
31
+ runPrompt,
32
+ createSshClient,
33
+ runCommand,
34
+ runCommandCapture
35
+ }
36
+ }
@@ -0,0 +1,24 @@
1
+ import process from 'node:process'
2
+
3
+ import {createAppContext} from '../runtime/app-context.mjs'
4
+ import {createConfigurationService} from '../application/configuration/service.mjs'
5
+ import {selectDeploymentTarget as selectDeploymentTargetImpl} from '../application/configuration/select-deployment-target.mjs'
6
+
7
+ const appContext = createAppContext()
8
+ const {
9
+ logSuccess,
10
+ logWarning,
11
+ logProcessing,
12
+ runPrompt
13
+ } = appContext
14
+ const configurationService = createConfigurationService(appContext)
15
+
16
+ export async function selectDeploymentTarget({rootDir = process.cwd()} = {}) {
17
+ return selectDeploymentTargetImpl(rootDir, {
18
+ configurationService,
19
+ runPrompt,
20
+ logProcessing,
21
+ logSuccess,
22
+ logWarning
23
+ })
24
+ }
@@ -1,29 +1,54 @@
1
1
  import process from 'node:process'
2
2
 
3
+ export const LOG_PREFIXES = Object.freeze({
4
+ processing: '→',
5
+ success: '✔',
6
+ warning: '⚠',
7
+ error: '✖'
8
+ })
9
+
3
10
  export function writeStdoutLine(message = '') {
4
- const text = message == null ? '' : String(message)
5
- process.stdout.write(`${text}\n`)
11
+ const text = message == null ? '' : String(message)
12
+ process.stdout.write(`${text}\n`)
6
13
  }
7
14
 
8
15
  export function writeStderrLine(message = '') {
9
- const text = message == null ? '' : String(message)
10
- process.stderr.write(`${text}\n`)
16
+ const text = message == null ? '' : String(message)
17
+ process.stderr.write(`${text}\n`)
11
18
  }
12
19
 
13
20
  export function writeStderr(message = '') {
14
- const text = message == null ? '' : String(message)
15
- process.stderr.write(text)
16
- if (text && !text.endsWith('\n')) {
17
- process.stderr.write('\n')
18
- }
21
+ const text = message == null ? '' : String(message)
22
+ process.stderr.write(text)
23
+ if (text && !text.endsWith('\n')) {
24
+ process.stderr.write('\n')
25
+ }
19
26
  }
20
27
 
21
- export function createChalkLogger(chalk) {
22
- return {
23
- logProcessing: (message = '') => writeStdoutLine(chalk.yellow(message)),
24
- logSuccess: (message = '') => writeStdoutLine(chalk.green(message)),
25
- logWarning: (message = '') => writeStderrLine(chalk.yellow(message)),
26
- logError: (message = '') => writeStderrLine(chalk.red(message))
27
- }
28
+ export function formatLogMessage(message = '', prefix = '') {
29
+ const text = message == null ? '' : String(message)
30
+
31
+ if (!prefix || text.length === 0) {
32
+ return text
33
+ }
34
+
35
+ const leadingNewlines = text.match(/^\n+/)?.[0] ?? ''
36
+ const body = text.slice(leadingNewlines.length)
37
+
38
+ if (body.length === 0) {
39
+ return `${leadingNewlines}${prefix}`
40
+ }
41
+
42
+ return `${leadingNewlines}${prefix} ${body}`
28
43
  }
29
44
 
45
+ export function createChalkLogger(chalk, {
46
+ prefixes = LOG_PREFIXES
47
+ } = {}) {
48
+ return {
49
+ logProcessing: (message = '') => writeStdoutLine(chalk.yellow(formatLogMessage(message, prefixes.processing))),
50
+ logSuccess: (message = '') => writeStdoutLine(chalk.green(formatLogMessage(message, prefixes.success))),
51
+ logWarning: (message = '') => writeStderrLine(chalk.yellow(formatLogMessage(message, prefixes.warning))),
52
+ logError: (message = '') => writeStderrLine(chalk.red(formatLogMessage(message, prefixes.error)))
53
+ }
54
+ }