@wyxos/zephyr 0.1.0 → 0.1.2
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 +75 -75
- package/bin/zephyr.mjs +7 -7
- package/package.json +20 -47
- package/publish.mjs +222 -0
- package/src/index.mjs +867 -781
- package/tests/index.test.js +369 -0
package/README.md
CHANGED
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
# @wyxos/zephyr
|
|
2
|
-
|
|
3
|
-
A streamlined deployment tool for web applications with intelligent Laravel project detection.
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install -g @wyxos/zephyr
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Or run directly with npx:
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
npx @wyxos/zephyr
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
## Usage
|
|
18
|
-
|
|
19
|
-
Navigate to your project directory and run:
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
zephyr
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
Follow the interactive prompts to configure your deployment target:
|
|
26
|
-
- Server name and IP address
|
|
27
|
-
- Project path on the remote server
|
|
28
|
-
- Git branch to deploy
|
|
29
|
-
- SSH user and private key
|
|
30
|
-
|
|
31
|
-
Configuration is saved to `release.json` for future deployments.
|
|
32
|
-
|
|
33
|
-
## Features
|
|
34
|
-
|
|
35
|
-
- Automated Git operations (branch switching, commits, pushes)
|
|
36
|
-
- SSH-based deployment to remote servers
|
|
37
|
-
- Laravel project detection with smart task execution
|
|
38
|
-
- Intelligent dependency management (Composer, npm)
|
|
39
|
-
- Database migrations when detected
|
|
40
|
-
- Frontend asset compilation
|
|
41
|
-
- Cache clearing and queue worker management
|
|
42
|
-
- SSH key validation and management
|
|
43
|
-
|
|
44
|
-
## Smart Task Execution
|
|
45
|
-
|
|
46
|
-
Zephyr analyzes changed files and runs appropriate tasks:
|
|
47
|
-
|
|
48
|
-
- **Always**: `git pull origin <branch>`
|
|
49
|
-
- **Composer files changed**: `composer update`
|
|
50
|
-
- **Migration files added**: `php artisan migrate`
|
|
51
|
-
- **package.json changed**: `npm install`
|
|
52
|
-
- **Frontend files changed**: `npm run build`
|
|
53
|
-
- **PHP files changed**: Clear Laravel caches, restart queues
|
|
54
|
-
|
|
55
|
-
## Configuration
|
|
56
|
-
|
|
57
|
-
Deployment targets are stored in `release.json`:
|
|
58
|
-
|
|
59
|
-
```json
|
|
60
|
-
[
|
|
61
|
-
{
|
|
62
|
-
"serverName": "production",
|
|
63
|
-
"serverIp": "192.168.1.100",
|
|
64
|
-
"projectPath": "~/webapps/myapp",
|
|
65
|
-
"branch": "main",
|
|
66
|
-
"sshUser": "forge",
|
|
67
|
-
"sshKey": "~/.ssh/id_rsa"
|
|
68
|
-
}
|
|
69
|
-
]
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
## Requirements
|
|
73
|
-
|
|
74
|
-
- Node.js 16+
|
|
75
|
-
- Git
|
|
1
|
+
# @wyxos/zephyr
|
|
2
|
+
|
|
3
|
+
A streamlined deployment tool for web applications with intelligent Laravel project detection.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @wyxos/zephyr
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run directly with npx:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @wyxos/zephyr
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
Navigate to your project directory and run:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
zephyr
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Follow the interactive prompts to configure your deployment target:
|
|
26
|
+
- Server name and IP address
|
|
27
|
+
- Project path on the remote server
|
|
28
|
+
- Git branch to deploy
|
|
29
|
+
- SSH user and private key
|
|
30
|
+
|
|
31
|
+
Configuration is saved to `release.json` for future deployments.
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- Automated Git operations (branch switching, commits, pushes)
|
|
36
|
+
- SSH-based deployment to remote servers
|
|
37
|
+
- Laravel project detection with smart task execution
|
|
38
|
+
- Intelligent dependency management (Composer, npm)
|
|
39
|
+
- Database migrations when detected
|
|
40
|
+
- Frontend asset compilation
|
|
41
|
+
- Cache clearing and queue worker management
|
|
42
|
+
- SSH key validation and management
|
|
43
|
+
|
|
44
|
+
## Smart Task Execution
|
|
45
|
+
|
|
46
|
+
Zephyr analyzes changed files and runs appropriate tasks:
|
|
47
|
+
|
|
48
|
+
- **Always**: `git pull origin <branch>`
|
|
49
|
+
- **Composer files changed**: `composer update`
|
|
50
|
+
- **Migration files added**: `php artisan migrate`
|
|
51
|
+
- **package.json changed**: `npm install`
|
|
52
|
+
- **Frontend files changed**: `npm run build`
|
|
53
|
+
- **PHP files changed**: Clear Laravel caches, restart queues
|
|
54
|
+
|
|
55
|
+
## Configuration
|
|
56
|
+
|
|
57
|
+
Deployment targets are stored in `release.json`:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
[
|
|
61
|
+
{
|
|
62
|
+
"serverName": "production",
|
|
63
|
+
"serverIp": "192.168.1.100",
|
|
64
|
+
"projectPath": "~/webapps/myapp",
|
|
65
|
+
"branch": "main",
|
|
66
|
+
"sshUser": "forge",
|
|
67
|
+
"sshKey": "~/.ssh/id_rsa"
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Requirements
|
|
73
|
+
|
|
74
|
+
- Node.js 16+
|
|
75
|
+
- Git
|
|
76
76
|
- SSH access to target servers
|
package/bin/zephyr.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { main } from '../src/index.mjs'
|
|
3
|
-
|
|
4
|
-
main().catch((error) => {
|
|
5
|
-
console.error(error.message)
|
|
6
|
-
process.exit(1)
|
|
7
|
-
})
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { main } from '../src/index.mjs'
|
|
3
|
+
|
|
4
|
+
main().catch((error) => {
|
|
5
|
+
console.error(error.message)
|
|
6
|
+
process.exit(1)
|
|
7
|
+
})
|
package/package.json
CHANGED
|
@@ -1,47 +1,20 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@wyxos/zephyr",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"author": "wyxos",
|
|
22
|
-
"license": "MIT",
|
|
23
|
-
"repository": {
|
|
24
|
-
"type": "git",
|
|
25
|
-
"url": "git+https://github.com/wyxos/zephyr.git"
|
|
26
|
-
},
|
|
27
|
-
"bugs": {
|
|
28
|
-
"url": "https://github.com/wyxos/zephyr/issues"
|
|
29
|
-
},
|
|
30
|
-
"homepage": "https://github.com/wyxos/zephyr#readme",
|
|
31
|
-
"engines": {
|
|
32
|
-
"node": ">=16.0.0"
|
|
33
|
-
},
|
|
34
|
-
"files": [
|
|
35
|
-
"bin/",
|
|
36
|
-
"src/",
|
|
37
|
-
"README.md"
|
|
38
|
-
],
|
|
39
|
-
"dependencies": {
|
|
40
|
-
"chalk": "5.3.0",
|
|
41
|
-
"inquirer": "^9.2.12",
|
|
42
|
-
"node-ssh": "^13.1.0"
|
|
43
|
-
},
|
|
44
|
-
"devDependencies": {
|
|
45
|
-
"vitest": "^2.1.8"
|
|
46
|
-
}
|
|
47
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@wyxos/zephyr",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "vitest",
|
|
7
|
+
"release": "node publish.mjs"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"zephyr": "bin/zephyr.mjs"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"chalk": "5.3.0",
|
|
14
|
+
"inquirer": "^9.2.12",
|
|
15
|
+
"node-ssh": "^13.1.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"vitest": "^2.1.8"
|
|
19
|
+
}
|
|
20
|
+
}
|
package/publish.mjs
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { dirname, join } from 'node:path'
|
|
5
|
+
import { readFile } from 'node:fs/promises'
|
|
6
|
+
|
|
7
|
+
const ROOT = dirname(fileURLToPath(import.meta.url))
|
|
8
|
+
const PACKAGE_PATH = join(ROOT, 'package.json')
|
|
9
|
+
|
|
10
|
+
const STEP_PREFIX = '→'
|
|
11
|
+
const OK_PREFIX = '✔'
|
|
12
|
+
const WARN_PREFIX = '⚠'
|
|
13
|
+
|
|
14
|
+
function logStep(message) {
|
|
15
|
+
console.log(`${STEP_PREFIX} ${message}`)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function logSuccess(message) {
|
|
19
|
+
console.log(`${OK_PREFIX} ${message}`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function logWarning(message) {
|
|
23
|
+
console.warn(`${WARN_PREFIX} ${message}`)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function runCommand(command, args, { cwd = ROOT, capture = false } = {}) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const spawnOptions = {
|
|
29
|
+
cwd,
|
|
30
|
+
stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const child = spawn(command, args, spawnOptions)
|
|
34
|
+
let stdout = ''
|
|
35
|
+
let stderr = ''
|
|
36
|
+
|
|
37
|
+
if (capture) {
|
|
38
|
+
child.stdout.on('data', (chunk) => {
|
|
39
|
+
stdout += chunk
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
child.stderr.on('data', (chunk) => {
|
|
43
|
+
stderr += chunk
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
child.on('error', reject)
|
|
48
|
+
child.on('close', (code) => {
|
|
49
|
+
if (code === 0) {
|
|
50
|
+
resolve(capture ? { stdout: stdout.trim(), stderr: stderr.trim() } : undefined)
|
|
51
|
+
} else {
|
|
52
|
+
const error = new Error(`Command failed (${code}): ${command} ${args.join(' ')}`)
|
|
53
|
+
if (capture) {
|
|
54
|
+
error.stdout = stdout
|
|
55
|
+
error.stderr = stderr
|
|
56
|
+
}
|
|
57
|
+
error.exitCode = code
|
|
58
|
+
reject(error)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function readPackage() {
|
|
65
|
+
const raw = await readFile(PACKAGE_PATH, 'utf8')
|
|
66
|
+
return JSON.parse(raw)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function ensureCleanWorkingTree() {
|
|
70
|
+
const { stdout } = await runCommand('git', ['status', '--porcelain'], { capture: true })
|
|
71
|
+
|
|
72
|
+
if (stdout.length > 0) {
|
|
73
|
+
throw new Error('Working tree has uncommitted changes. Commit or stash them before releasing.')
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function getCurrentBranch() {
|
|
78
|
+
const { stdout } = await runCommand('git', ['branch', '--show-current'], { capture: true })
|
|
79
|
+
return stdout || null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function getUpstreamRef() {
|
|
83
|
+
try {
|
|
84
|
+
const { stdout } = await runCommand('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], {
|
|
85
|
+
capture: true
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
return stdout || null
|
|
89
|
+
} catch {
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function ensureUpToDateWithUpstream(branch, upstreamRef) {
|
|
95
|
+
if (!upstreamRef) {
|
|
96
|
+
logWarning(`Branch ${branch} has no upstream configured; skipping ahead/behind checks.`)
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const aheadResult = await runCommand('git', ['rev-list', '--count', `${upstreamRef}..HEAD`], {
|
|
101
|
+
capture: true
|
|
102
|
+
})
|
|
103
|
+
const behindResult = await runCommand('git', ['rev-list', '--count', `HEAD..${upstreamRef}`], {
|
|
104
|
+
capture: true
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const ahead = Number.parseInt(aheadResult.stdout || '0', 10)
|
|
108
|
+
const behind = Number.parseInt(behindResult.stdout || '0', 10)
|
|
109
|
+
|
|
110
|
+
if (Number.isFinite(behind) && behind > 0) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`Branch ${branch} is behind ${upstreamRef} by ${behind} commit${behind === 1 ? '' : 's'}. Pull or rebase first.`
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (Number.isFinite(ahead) && ahead > 0) {
|
|
117
|
+
logWarning(`Branch ${branch} is ahead of ${upstreamRef} by ${ahead} commit${ahead === 1 ? '' : 's'}.`)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseArgs() {
|
|
122
|
+
const args = process.argv.slice(2)
|
|
123
|
+
const positionals = args.filter((arg) => !arg.startsWith('--'))
|
|
124
|
+
const flags = new Set(args.filter((arg) => arg.startsWith('--')))
|
|
125
|
+
|
|
126
|
+
const releaseType = positionals[0] ?? 'patch'
|
|
127
|
+
const skipTests = flags.has('--skip-tests')
|
|
128
|
+
|
|
129
|
+
const allowedTypes = new Set([
|
|
130
|
+
'major',
|
|
131
|
+
'minor',
|
|
132
|
+
'patch',
|
|
133
|
+
'premajor',
|
|
134
|
+
'preminor',
|
|
135
|
+
'prepatch',
|
|
136
|
+
'prerelease'
|
|
137
|
+
])
|
|
138
|
+
|
|
139
|
+
if (!allowedTypes.has(releaseType)) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Invalid release type "${releaseType}". Use one of: ${Array.from(allowedTypes).join(', ')}.`
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { releaseType, skipTests }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function runTests(skipTests) {
|
|
149
|
+
if (skipTests) {
|
|
150
|
+
logWarning('Skipping tests because --skip-tests flag was provided.')
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
logStep('Running test suite (vitest run)...')
|
|
155
|
+
await runCommand('npx', ['vitest', 'run'])
|
|
156
|
+
logSuccess('Tests passed.')
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function ensureNpmAuth() {
|
|
160
|
+
logStep('Confirming npm authentication...')
|
|
161
|
+
await runCommand('npm', ['whoami'])
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function bumpVersion(releaseType) {
|
|
165
|
+
logStep(`Bumping package version with "npm version ${releaseType}"...`)
|
|
166
|
+
await runCommand('npm', ['version', releaseType, '--message', 'chore: release %s'])
|
|
167
|
+
const pkg = await readPackage()
|
|
168
|
+
logSuccess(`Version updated to ${pkg.version}.`)
|
|
169
|
+
return pkg
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function pushChanges() {
|
|
173
|
+
logStep('Pushing commits and tags to origin...')
|
|
174
|
+
await runCommand('git', ['push', '--follow-tags'])
|
|
175
|
+
logSuccess('Git push completed.')
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function publishPackage(pkg) {
|
|
179
|
+
const publishArgs = ['publish']
|
|
180
|
+
|
|
181
|
+
if (pkg.name.startsWith('@')) {
|
|
182
|
+
publishArgs.push('--access', 'public')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
logStep(`Publishing ${pkg.name}@${pkg.version} to npm...`)
|
|
186
|
+
await runCommand('npm', publishArgs)
|
|
187
|
+
logSuccess('npm publish completed.')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function main() {
|
|
191
|
+
const { releaseType, skipTests } = parseArgs()
|
|
192
|
+
|
|
193
|
+
logStep('Reading package metadata...')
|
|
194
|
+
const pkg = await readPackage()
|
|
195
|
+
|
|
196
|
+
logStep('Checking working tree status...')
|
|
197
|
+
await ensureCleanWorkingTree()
|
|
198
|
+
|
|
199
|
+
const branch = await getCurrentBranch()
|
|
200
|
+
if (!branch) {
|
|
201
|
+
throw new Error('Unable to determine current branch.')
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
logStep(`Current branch: ${branch}`)
|
|
205
|
+
const upstreamRef = await getUpstreamRef()
|
|
206
|
+
await ensureUpToDateWithUpstream(branch, upstreamRef)
|
|
207
|
+
|
|
208
|
+
await runTests(skipTests)
|
|
209
|
+
await ensureNpmAuth()
|
|
210
|
+
|
|
211
|
+
const updatedPkg = await bumpVersion(releaseType)
|
|
212
|
+
await pushChanges()
|
|
213
|
+
await publishPackage(updatedPkg)
|
|
214
|
+
|
|
215
|
+
logSuccess(`Release workflow completed for ${updatedPkg.name}@${updatedPkg.version}.`)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
main().catch((error) => {
|
|
219
|
+
console.error('\nRelease failed:')
|
|
220
|
+
console.error(error.message)
|
|
221
|
+
process.exit(1)
|
|
222
|
+
})
|