@wyxos/zephyr 0.2.18 → 0.2.19
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 +19 -1
- package/bin/zephyr.mjs +21 -6
- package/package.json +5 -4
- package/src/config/project.mjs +118 -0
- package/src/config/servers.mjs +57 -0
- package/src/dependency-scanner.mjs +7 -41
- package/src/deploy/local-repo.mjs +215 -0
- package/src/deploy/locks.mjs +171 -0
- package/src/deploy/preflight.mjs +117 -0
- package/src/deploy/remote-exec.mjs +99 -0
- package/src/deploy/snapshots.mjs +35 -0
- package/src/main.mjs +652 -0
- package/src/project/bootstrap.mjs +147 -0
- package/src/release-node.mjs +13 -180
- package/src/release-packagist.mjs +13 -146
- package/src/runtime/local-command.mjs +18 -0
- package/src/runtime/prompt.mjs +14 -0
- package/src/runtime/ssh-client.mjs +14 -0
- package/src/ssh/index.mjs +8 -0
- package/src/ssh/keys.mjs +146 -0
- package/src/ssh/ssh.mjs +124 -0
- package/src/utils/command.mjs +92 -0
- package/src/utils/config-flow.mjs +284 -0
- package/src/utils/git.mjs +91 -0
- package/src/utils/id.mjs +6 -0
- package/src/utils/log-file.mjs +76 -0
- package/src/utils/output.mjs +29 -0
- package/src/utils/paths.mjs +28 -0
- package/src/utils/remote-path.mjs +23 -0
- package/src/utils/task-planner.mjs +96 -0
- package/src/index.mjs +0 -2143
- package/src/ssh-utils.mjs +0 -289
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
export async function ensureGitignoreEntry(rootDir, {
|
|
5
|
+
projectConfigDir = '.zephyr',
|
|
6
|
+
runCommand,
|
|
7
|
+
logSuccess,
|
|
8
|
+
logWarning
|
|
9
|
+
} = {}) {
|
|
10
|
+
const gitignorePath = path.join(rootDir, '.gitignore')
|
|
11
|
+
const targetEntry = `${projectConfigDir}/`
|
|
12
|
+
let existingContent = ''
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
existingContent = await fs.readFile(gitignorePath, 'utf8')
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (error.code !== 'ENOENT') {
|
|
18
|
+
throw error
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const hasEntry = existingContent
|
|
23
|
+
.split(/\r?\n/)
|
|
24
|
+
.some((line) => line.trim() === targetEntry)
|
|
25
|
+
|
|
26
|
+
if (hasEntry) {
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const updatedContent = existingContent
|
|
31
|
+
? `${existingContent.replace(/\s*$/, '')}\n${targetEntry}\n`
|
|
32
|
+
: `${targetEntry}\n`
|
|
33
|
+
|
|
34
|
+
await fs.writeFile(gitignorePath, updatedContent)
|
|
35
|
+
logSuccess?.('Added .zephyr/ to .gitignore')
|
|
36
|
+
|
|
37
|
+
let isGitRepo = false
|
|
38
|
+
try {
|
|
39
|
+
await runCommand('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
40
|
+
silent: true,
|
|
41
|
+
cwd: rootDir
|
|
42
|
+
})
|
|
43
|
+
isGitRepo = true
|
|
44
|
+
} catch (_error) {
|
|
45
|
+
logWarning?.('Not a git repository; skipping commit for .gitignore update.')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!isGitRepo) {
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await runCommand('git', ['add', '.gitignore'], { cwd: rootDir })
|
|
54
|
+
await runCommand('git', ['commit', '-m', 'chore: ignore zephyr config'], { cwd: rootDir })
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (error.exitCode === 1) {
|
|
57
|
+
logWarning?.('Git commit skipped: nothing to commit or pre-commit hook prevented commit.')
|
|
58
|
+
} else {
|
|
59
|
+
throw error
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function ensureProjectReleaseScript(rootDir, {
|
|
65
|
+
runPrompt,
|
|
66
|
+
runCommand,
|
|
67
|
+
logSuccess,
|
|
68
|
+
logWarning,
|
|
69
|
+
releaseScriptName = 'release',
|
|
70
|
+
releaseScriptCommand = 'npx @wyxos/zephyr@latest'
|
|
71
|
+
} = {}) {
|
|
72
|
+
const packageJsonPath = path.join(rootDir, 'package.json')
|
|
73
|
+
|
|
74
|
+
let raw
|
|
75
|
+
try {
|
|
76
|
+
raw = await fs.readFile(packageJsonPath, 'utf8')
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error.code === 'ENOENT') {
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
throw error
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let packageJson
|
|
86
|
+
try {
|
|
87
|
+
packageJson = JSON.parse(raw)
|
|
88
|
+
} catch (_error) {
|
|
89
|
+
logWarning?.('Unable to parse package.json; skipping release script injection.')
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const currentCommand = packageJson?.scripts?.[releaseScriptName]
|
|
94
|
+
|
|
95
|
+
if (currentCommand && currentCommand.includes('@wyxos/zephyr')) {
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const { installReleaseScript } = await runPrompt([
|
|
100
|
+
{
|
|
101
|
+
type: 'confirm',
|
|
102
|
+
name: 'installReleaseScript',
|
|
103
|
+
message: 'Add "release" script to package.json that runs "npx @wyxos/zephyr@latest"?',
|
|
104
|
+
default: true
|
|
105
|
+
}
|
|
106
|
+
])
|
|
107
|
+
|
|
108
|
+
if (!installReleaseScript) {
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!packageJson.scripts || typeof packageJson.scripts !== 'object') {
|
|
113
|
+
packageJson.scripts = {}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
packageJson.scripts[releaseScriptName] = releaseScriptCommand
|
|
117
|
+
|
|
118
|
+
const updatedPayload = `${JSON.stringify(packageJson, null, 2)}\n`
|
|
119
|
+
await fs.writeFile(packageJsonPath, updatedPayload)
|
|
120
|
+
logSuccess?.('Added release script to package.json.')
|
|
121
|
+
|
|
122
|
+
let isGitRepo = false
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
await runCommand('git', ['rev-parse', '--is-inside-work-tree'], { cwd: rootDir, silent: true })
|
|
126
|
+
isGitRepo = true
|
|
127
|
+
} catch (_error) {
|
|
128
|
+
logWarning?.('Not a git repository; skipping commit for release script addition.')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (isGitRepo) {
|
|
132
|
+
try {
|
|
133
|
+
await runCommand('git', ['add', 'package.json'], { cwd: rootDir, silent: true })
|
|
134
|
+
await runCommand('git', ['commit', '-m', 'chore: add zephyr release script'], { cwd: rootDir, silent: true })
|
|
135
|
+
logSuccess?.('Committed package.json release script addition.')
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (error.exitCode === 1) {
|
|
138
|
+
logWarning?.('Git commit skipped: nothing to commit or pre-commit hook prevented commit.')
|
|
139
|
+
} else {
|
|
140
|
+
throw error
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return true
|
|
146
|
+
}
|
|
147
|
+
|
package/src/release-node.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { spawn, exec } from 'node:child_process'
|
|
2
1
|
import { join } from 'node:path'
|
|
3
2
|
import { readFile } from 'node:fs/promises'
|
|
4
3
|
import fs from 'node:fs'
|
|
@@ -7,26 +6,9 @@ import process from 'node:process'
|
|
|
7
6
|
import chalk from 'chalk'
|
|
8
7
|
import inquirer from 'inquirer'
|
|
9
8
|
import { validateLocalDependencies } from './dependency-scanner.mjs'
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
function writeStdoutLine(message = '') {
|
|
14
|
-
const text = message == null ? '' : String(message)
|
|
15
|
-
process.stdout.write(`${text}\n`)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function writeStderrLine(message = '') {
|
|
19
|
-
const text = message == null ? '' : String(message)
|
|
20
|
-
process.stderr.write(`${text}\n`)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function writeStderr(message = '') {
|
|
24
|
-
const text = message == null ? '' : String(message)
|
|
25
|
-
process.stderr.write(text)
|
|
26
|
-
if (text && !text.endsWith('\n')) {
|
|
27
|
-
process.stderr.write('\n')
|
|
28
|
-
}
|
|
29
|
-
}
|
|
9
|
+
import { writeStderr, writeStderrLine, writeStdoutLine } from './utils/output.mjs'
|
|
10
|
+
import { runCommand as runCommandBase, runCommandCapture as runCommandCaptureBase } from './utils/command.mjs'
|
|
11
|
+
import { ensureUpToDateWithUpstream, getCurrentBranch, getUpstreamRef } from './utils/git.mjs'
|
|
30
12
|
|
|
31
13
|
function logStep(message) {
|
|
32
14
|
writeStdoutLine(chalk.yellow(`→ ${message}`))
|
|
@@ -40,86 +22,14 @@ function logWarning(message) {
|
|
|
40
22
|
writeStderrLine(chalk.yellow(`⚠ ${message}`))
|
|
41
23
|
}
|
|
42
24
|
|
|
43
|
-
function runCommand(command, args, { cwd = process.cwd(), capture = false
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (needsShell) {
|
|
50
|
-
// When using shell, use exec to avoid deprecation warning with spawn
|
|
51
|
-
// Properly escape arguments for Windows cmd.exe
|
|
52
|
-
const escapedArgs = args.map(arg => {
|
|
53
|
-
// If arg contains spaces or special chars, wrap in quotes and escape internal quotes
|
|
54
|
-
if (arg.includes(' ') || arg.includes('"') || arg.includes('&') || arg.includes('|')) {
|
|
55
|
-
return `"${arg.replace(/"/g, '\\"')}"`
|
|
56
|
-
}
|
|
57
|
-
return arg
|
|
58
|
-
})
|
|
59
|
-
const commandString = `${command} ${escapedArgs.join(' ')}`
|
|
60
|
-
|
|
61
|
-
exec(commandString, { cwd, encoding: 'utf8' }, (error, stdout, stderr) => {
|
|
62
|
-
if (error) {
|
|
63
|
-
const err = new Error(`Command failed (${error.code}): ${command} ${args.join(' ')}`)
|
|
64
|
-
if (capture) {
|
|
65
|
-
err.stdout = stdout || ''
|
|
66
|
-
err.stderr = stderr || ''
|
|
67
|
-
} else {
|
|
68
|
-
// When not capturing, exec still provides output, so show it
|
|
69
|
-
if (stdout) process.stdout.write(stdout)
|
|
70
|
-
if (stderr) process.stderr.write(stderr)
|
|
71
|
-
}
|
|
72
|
-
err.exitCode = error.code
|
|
73
|
-
reject(err)
|
|
74
|
-
return
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (capture) {
|
|
78
|
-
resolve({ stdout: stdout.trim(), stderr: stderr.trim() })
|
|
79
|
-
} else {
|
|
80
|
-
// When not capturing, exec still provides output, so show it
|
|
81
|
-
if (stdout) process.stdout.write(stdout)
|
|
82
|
-
if (stderr) process.stderr.write(stderr)
|
|
83
|
-
resolve(undefined)
|
|
84
|
-
}
|
|
85
|
-
})
|
|
86
|
-
} else {
|
|
87
|
-
// Use spawn for commands that don't need shell
|
|
88
|
-
const spawnOptions = {
|
|
89
|
-
cwd,
|
|
90
|
-
stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit'
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const child = spawn(command, args, spawnOptions)
|
|
94
|
-
let stdout = ''
|
|
95
|
-
let stderr = ''
|
|
96
|
-
|
|
97
|
-
if (capture) {
|
|
98
|
-
child.stdout.on('data', (chunk) => {
|
|
99
|
-
stdout += chunk
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
child.stderr.on('data', (chunk) => {
|
|
103
|
-
stderr += chunk
|
|
104
|
-
})
|
|
105
|
-
}
|
|
25
|
+
async function runCommand(command, args, { cwd = process.cwd(), capture = false } = {}) {
|
|
26
|
+
if (capture) {
|
|
27
|
+
const { stdout, stderr } = await runCommandCaptureBase(command, args, { cwd })
|
|
28
|
+
return { stdout: stdout.trim(), stderr: stderr.trim() }
|
|
29
|
+
}
|
|
106
30
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (code === 0) {
|
|
110
|
-
resolve(capture ? { stdout: stdout.trim(), stderr: stderr.trim() } : undefined)
|
|
111
|
-
} else {
|
|
112
|
-
const error = new Error(`Command failed (${code}): ${command} ${args.join(' ')}`)
|
|
113
|
-
if (capture) {
|
|
114
|
-
error.stdout = stdout
|
|
115
|
-
error.stderr = stderr
|
|
116
|
-
}
|
|
117
|
-
error.exitCode = code
|
|
118
|
-
reject(error)
|
|
119
|
-
}
|
|
120
|
-
})
|
|
121
|
-
}
|
|
122
|
-
})
|
|
31
|
+
await runCommandBase(command, args, { cwd })
|
|
32
|
+
return undefined
|
|
123
33
|
}
|
|
124
34
|
|
|
125
35
|
async function readPackage(rootDir = process.cwd()) {
|
|
@@ -140,84 +50,7 @@ async function ensureCleanWorkingTree(rootDir = process.cwd()) {
|
|
|
140
50
|
}
|
|
141
51
|
}
|
|
142
52
|
|
|
143
|
-
|
|
144
|
-
const { stdout } = await runCommand('git', ['branch', '--show-current'], { capture: true, cwd: rootDir })
|
|
145
|
-
return stdout || null
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async function getUpstreamRef(rootDir = process.cwd()) {
|
|
149
|
-
try {
|
|
150
|
-
const { stdout } = await runCommand('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], {
|
|
151
|
-
capture: true,
|
|
152
|
-
cwd: rootDir
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
return stdout || null
|
|
156
|
-
} catch {
|
|
157
|
-
return null
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async function ensureUpToDateWithUpstream(branch, upstreamRef, rootDir = process.cwd()) {
|
|
162
|
-
if (!upstreamRef) {
|
|
163
|
-
logWarning(`Branch ${branch} has no upstream configured; skipping ahead/behind checks.`)
|
|
164
|
-
return
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const [remoteName, ...branchParts] = upstreamRef.split('/')
|
|
168
|
-
const remoteBranch = branchParts.join('/')
|
|
169
|
-
|
|
170
|
-
if (remoteName && remoteBranch) {
|
|
171
|
-
logStep(`Fetching latest updates from ${remoteName}/${remoteBranch}...`)
|
|
172
|
-
try {
|
|
173
|
-
await runCommand('git', ['fetch', remoteName, remoteBranch], { capture: true, cwd: rootDir })
|
|
174
|
-
} catch (error) {
|
|
175
|
-
if (error.stderr) {
|
|
176
|
-
writeStderr(error.stderr)
|
|
177
|
-
}
|
|
178
|
-
throw new Error(`Failed to fetch ${upstreamRef}: ${error.message}`)
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const aheadResult = await runCommand('git', ['rev-list', '--count', `${upstreamRef}..HEAD`], {
|
|
183
|
-
capture: true,
|
|
184
|
-
cwd: rootDir
|
|
185
|
-
})
|
|
186
|
-
const behindResult = await runCommand('git', ['rev-list', '--count', `HEAD..${upstreamRef}`], {
|
|
187
|
-
capture: true,
|
|
188
|
-
cwd: rootDir
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
const ahead = Number.parseInt(aheadResult.stdout || '0', 10)
|
|
192
|
-
const behind = Number.parseInt(behindResult.stdout || '0', 10)
|
|
193
|
-
|
|
194
|
-
if (Number.isFinite(behind) && behind > 0) {
|
|
195
|
-
if (remoteName && remoteBranch) {
|
|
196
|
-
logStep(`Fast-forwarding ${branch} with ${upstreamRef}...`)
|
|
197
|
-
|
|
198
|
-
try {
|
|
199
|
-
await runCommand('git', ['pull', '--ff-only', remoteName, remoteBranch], { capture: true, cwd: rootDir })
|
|
200
|
-
} catch (error) {
|
|
201
|
-
if (error.stderr) {
|
|
202
|
-
writeStderr(error.stderr)
|
|
203
|
-
}
|
|
204
|
-
throw new Error(
|
|
205
|
-
`Unable to fast-forward ${branch} with ${upstreamRef}. Resolve conflicts manually, then rerun the release.\n${error.message}`
|
|
206
|
-
)
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return ensureUpToDateWithUpstream(branch, upstreamRef, rootDir)
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
throw new Error(
|
|
213
|
-
`Branch ${branch} is behind ${upstreamRef} by ${behind} commit${behind === 1 ? '' : 's'}. Pull or rebase first.`
|
|
214
|
-
)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (Number.isFinite(ahead) && ahead > 0) {
|
|
218
|
-
logWarning(`Branch ${branch} is ahead of ${upstreamRef} by ${ahead} commit${ahead === 1 ? '' : 's'}.`)
|
|
219
|
-
}
|
|
220
|
-
}
|
|
53
|
+
// Git helpers imported from src/utils/git.mjs
|
|
221
54
|
|
|
222
55
|
function parseArgs() {
|
|
223
56
|
const args = process.argv.slice(2)
|
|
@@ -607,14 +440,14 @@ export async function releaseNode() {
|
|
|
607
440
|
logStep('Checking working tree status...')
|
|
608
441
|
await ensureCleanWorkingTree(rootDir)
|
|
609
442
|
|
|
610
|
-
const branch = await getCurrentBranch(rootDir)
|
|
443
|
+
const branch = await getCurrentBranch(rootDir, { method: 'show-current' })
|
|
611
444
|
if (!branch) {
|
|
612
445
|
throw new Error('Unable to determine current branch.')
|
|
613
446
|
}
|
|
614
447
|
|
|
615
448
|
logStep(`Current branch: ${branch}`)
|
|
616
449
|
const upstreamRef = await getUpstreamRef(rootDir)
|
|
617
|
-
await ensureUpToDateWithUpstream(branch, upstreamRef, rootDir)
|
|
450
|
+
await ensureUpToDateWithUpstream({ branch, upstreamRef, rootDir, logStep, logWarning })
|
|
618
451
|
|
|
619
452
|
await runLint(skipLint, pkg, rootDir)
|
|
620
453
|
await runTests(skipTests, pkg, rootDir)
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process'
|
|
2
1
|
import { join } from 'node:path'
|
|
3
2
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
4
3
|
import fs from 'node:fs'
|
|
@@ -6,6 +5,9 @@ import process from 'node:process'
|
|
|
6
5
|
import semver from 'semver'
|
|
7
6
|
import inquirer from 'inquirer'
|
|
8
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'
|
|
9
11
|
|
|
10
12
|
const STEP_PREFIX = '→'
|
|
11
13
|
const OK_PREFIX = '✔'
|
|
@@ -13,24 +15,6 @@ const WARN_PREFIX = '⚠'
|
|
|
13
15
|
|
|
14
16
|
const IS_WINDOWS = process.platform === 'win32'
|
|
15
17
|
|
|
16
|
-
function writeStdoutLine(message = '') {
|
|
17
|
-
const text = message == null ? '' : String(message)
|
|
18
|
-
process.stdout.write(`${text}\n`)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function writeStderrLine(message = '') {
|
|
22
|
-
const text = message == null ? '' : String(message)
|
|
23
|
-
process.stderr.write(`${text}\n`)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function writeStderr(message = '') {
|
|
27
|
-
const text = message == null ? '' : String(message)
|
|
28
|
-
process.stderr.write(text)
|
|
29
|
-
if (text && !text.endsWith('\n')) {
|
|
30
|
-
process.stderr.write('\n')
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
18
|
function logStep(message) {
|
|
35
19
|
writeStdoutLine(`${STEP_PREFIX} ${message}`)
|
|
36
20
|
}
|
|
@@ -43,60 +27,14 @@ function logWarning(message) {
|
|
|
43
27
|
writeStderrLine(`${WARN_PREFIX} ${message}`)
|
|
44
28
|
}
|
|
45
29
|
|
|
46
|
-
function runCommand(command, args, { cwd = process.cwd(), capture = false
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
cwd,
|
|
52
|
-
stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit'
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
let child
|
|
56
|
-
if (needsShell) {
|
|
57
|
-
// When using shell, construct the command string to avoid deprecation warning
|
|
58
|
-
// Properly escape arguments for Windows cmd.exe
|
|
59
|
-
const escapedArgs = args.map(arg => {
|
|
60
|
-
// If arg contains spaces or special chars, wrap in quotes and escape internal quotes
|
|
61
|
-
if (arg.includes(' ') || arg.includes('"') || arg.includes('&') || arg.includes('|')) {
|
|
62
|
-
return `"${arg.replace(/"/g, '\\"')}"`
|
|
63
|
-
}
|
|
64
|
-
return arg
|
|
65
|
-
})
|
|
66
|
-
const commandString = `${command} ${escapedArgs.join(' ')}`
|
|
67
|
-
spawnOptions.shell = true
|
|
68
|
-
child = spawn(commandString, [], spawnOptions)
|
|
69
|
-
} else {
|
|
70
|
-
child = spawn(command, args, spawnOptions)
|
|
71
|
-
}
|
|
72
|
-
let stdout = ''
|
|
73
|
-
let stderr = ''
|
|
74
|
-
|
|
75
|
-
if (capture) {
|
|
76
|
-
child.stdout.on('data', (chunk) => {
|
|
77
|
-
stdout += chunk
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
child.stderr.on('data', (chunk) => {
|
|
81
|
-
stderr += chunk
|
|
82
|
-
})
|
|
83
|
-
}
|
|
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
|
+
}
|
|
84
35
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (code === 0) {
|
|
88
|
-
resolve(capture ? { stdout: stdout.trim(), stderr: stderr.trim() } : undefined)
|
|
89
|
-
} else {
|
|
90
|
-
const error = new Error(`Command failed (${code}): ${command} ${args.join(' ')}`)
|
|
91
|
-
if (capture) {
|
|
92
|
-
error.stdout = stdout
|
|
93
|
-
error.stderr = stderr
|
|
94
|
-
}
|
|
95
|
-
error.exitCode = code
|
|
96
|
-
reject(error)
|
|
97
|
-
}
|
|
98
|
-
})
|
|
99
|
-
})
|
|
36
|
+
await runCommandBase(command, args, { cwd })
|
|
37
|
+
return undefined
|
|
100
38
|
}
|
|
101
39
|
|
|
102
40
|
async function readComposer(rootDir = process.cwd()) {
|
|
@@ -145,78 +83,7 @@ async function ensureCleanWorkingTree(rootDir = process.cwd()) {
|
|
|
145
83
|
}
|
|
146
84
|
}
|
|
147
85
|
|
|
148
|
-
|
|
149
|
-
const { stdout } = await runCommand('git', ['branch', '--show-current'], { capture: true, cwd: rootDir })
|
|
150
|
-
return stdout || null
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
async function getUpstreamRef(rootDir = process.cwd()) {
|
|
154
|
-
try {
|
|
155
|
-
const { stdout } = await runCommand('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], {
|
|
156
|
-
capture: true,
|
|
157
|
-
cwd: rootDir
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
return stdout || null
|
|
161
|
-
} catch {
|
|
162
|
-
return null
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async function ensureUpToDateWithUpstream(branch, upstreamRef, rootDir = process.cwd()) {
|
|
167
|
-
if (!upstreamRef) {
|
|
168
|
-
logWarning(`Branch ${branch} has no upstream configured; skipping ahead/behind checks.`)
|
|
169
|
-
return
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const [remoteName, ...branchParts] = upstreamRef.split('/')
|
|
173
|
-
const remoteBranch = branchParts.join('/')
|
|
174
|
-
|
|
175
|
-
if (remoteName && remoteBranch) {
|
|
176
|
-
logStep(`Fetching latest updates from ${remoteName}/${remoteBranch}...`)
|
|
177
|
-
try {
|
|
178
|
-
await runCommand('git', ['fetch', remoteName, remoteBranch], { cwd: rootDir })
|
|
179
|
-
} catch (error) {
|
|
180
|
-
throw new Error(`Failed to fetch ${upstreamRef}: ${error.message}`)
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const aheadResult = await runCommand('git', ['rev-list', '--count', `${upstreamRef}..HEAD`], {
|
|
185
|
-
capture: true,
|
|
186
|
-
cwd: rootDir
|
|
187
|
-
})
|
|
188
|
-
const behindResult = await runCommand('git', ['rev-list', '--count', `HEAD..${upstreamRef}`], {
|
|
189
|
-
capture: true,
|
|
190
|
-
cwd: rootDir
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
const ahead = Number.parseInt(aheadResult.stdout || '0', 10)
|
|
194
|
-
const behind = Number.parseInt(behindResult.stdout || '0', 10)
|
|
195
|
-
|
|
196
|
-
if (Number.isFinite(behind) && behind > 0) {
|
|
197
|
-
if (remoteName && remoteBranch) {
|
|
198
|
-
logStep(`Fast-forwarding ${branch} with ${upstreamRef}...`)
|
|
199
|
-
|
|
200
|
-
try {
|
|
201
|
-
await runCommand('git', ['pull', '--ff-only', remoteName, remoteBranch], { cwd: rootDir })
|
|
202
|
-
} catch (error) {
|
|
203
|
-
throw new Error(
|
|
204
|
-
`Unable to fast-forward ${branch} with ${upstreamRef}. Resolve conflicts manually, then rerun the release.\n${error.message}`
|
|
205
|
-
)
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return ensureUpToDateWithUpstream(branch, upstreamRef, rootDir)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
throw new Error(
|
|
212
|
-
`Branch ${branch} is behind ${upstreamRef} by ${behind} commit${behind === 1 ? '' : 's'}. Pull or rebase first.`
|
|
213
|
-
)
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (Number.isFinite(ahead) && ahead > 0) {
|
|
217
|
-
logWarning(`Branch ${branch} is ahead of ${upstreamRef} by ${ahead} commit${ahead === 1 ? '' : 's'}.`)
|
|
218
|
-
}
|
|
219
|
-
}
|
|
86
|
+
// Git helpers imported from src/utils/git.mjs
|
|
220
87
|
|
|
221
88
|
function parseArgs() {
|
|
222
89
|
const args = process.argv.slice(2)
|
|
@@ -408,14 +275,14 @@ export async function releasePackagist() {
|
|
|
408
275
|
logStep('Checking working tree status...')
|
|
409
276
|
await ensureCleanWorkingTree(rootDir)
|
|
410
277
|
|
|
411
|
-
const branch = await
|
|
278
|
+
const branch = await getGitCurrentBranch(rootDir, { method: 'show-current' })
|
|
412
279
|
if (!branch) {
|
|
413
280
|
throw new Error('Unable to determine current branch.')
|
|
414
281
|
}
|
|
415
282
|
|
|
416
283
|
logStep(`Current branch: ${branch}`)
|
|
417
284
|
const upstreamRef = await getUpstreamRef(rootDir)
|
|
418
|
-
await ensureUpToDateWithUpstream(branch, upstreamRef, rootDir)
|
|
285
|
+
await ensureUpToDateWithUpstream({ branch, upstreamRef, rootDir, logStep, logWarning })
|
|
419
286
|
|
|
420
287
|
await runLint(skipLint, rootDir)
|
|
421
288
|
await runTests(skipTests, composer, rootDir)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function createLocalCommandRunners({ runCommandBase, runCommandCaptureBase }) {
|
|
2
|
+
if (!runCommandBase || !runCommandCaptureBase) {
|
|
3
|
+
throw new Error('createLocalCommandRunners requires runCommandBase and runCommandCaptureBase')
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const runCommand = async (command, args, { silent = false, cwd } = {}) => {
|
|
7
|
+
const stdio = silent ? 'ignore' : 'inherit'
|
|
8
|
+
return runCommandBase(command, args, { cwd, stdio })
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const runCommandCapture = async (command, args, { cwd } = {}) => {
|
|
12
|
+
const { stdout } = await runCommandCaptureBase(command, args, { cwd })
|
|
13
|
+
return stdout
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return { runCommand, runCommandCapture }
|
|
17
|
+
}
|
|
18
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function createRunPrompt({ inquirer }) {
|
|
2
|
+
if (!inquirer) {
|
|
3
|
+
throw new Error('createRunPrompt requires inquirer')
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
return async function runPrompt(questions) {
|
|
7
|
+
if (typeof globalThis !== 'undefined' && globalThis.__zephyrPrompt) {
|
|
8
|
+
return globalThis.__zephyrPrompt(questions)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return inquirer.prompt(questions)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function createSshClientFactory({ NodeSSH }) {
|
|
2
|
+
if (!NodeSSH) {
|
|
3
|
+
throw new Error('createSshClientFactory requires NodeSSH')
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
return function createSshClient() {
|
|
7
|
+
if (typeof globalThis !== 'undefined' && globalThis.__zephyrSSHFactory) {
|
|
8
|
+
return globalThis.__zephyrSSHFactory()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return new NodeSSH()
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|