@wyxos/zephyr 0.2.21 → 0.2.22

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.
@@ -1,171 +1,171 @@
1
- import fs from 'node:fs/promises'
2
- import os from 'node:os'
3
- import process from 'node:process'
4
-
5
- import { PROJECT_LOCK_FILE, ensureDirectory, getLockFilePath, getProjectConfigDir } from '../utils/paths.mjs'
6
-
7
- function createLockPayload() {
8
- return {
9
- user: os.userInfo().username,
10
- pid: process.pid,
11
- hostname: os.hostname(),
12
- startedAt: new Date().toISOString()
13
- }
14
- }
15
-
16
- export async function acquireLocalLock(rootDir) {
17
- const lockPath = getLockFilePath(rootDir)
18
- const configDir = getProjectConfigDir(rootDir)
19
- await ensureDirectory(configDir)
20
-
21
- const payload = createLockPayload()
22
- const payloadJson = JSON.stringify(payload, null, 2)
23
- await fs.writeFile(lockPath, payloadJson, 'utf8')
24
-
25
- return payload
26
- }
27
-
28
- export async function releaseLocalLock(rootDir, { logWarning } = {}) {
29
- const lockPath = getLockFilePath(rootDir)
30
- try {
31
- await fs.unlink(lockPath)
32
- } catch (error) {
33
- if (error.code !== 'ENOENT') {
34
- logWarning?.(`Failed to remove local lock file: ${error.message}`)
35
- }
36
- }
37
- }
38
-
39
- export async function readLocalLock(rootDir) {
40
- const lockPath = getLockFilePath(rootDir)
41
- try {
42
- const content = await fs.readFile(lockPath, 'utf8')
43
- return JSON.parse(content)
44
- } catch (error) {
45
- if (error.code === 'ENOENT') {
46
- return null
47
- }
48
- throw error
49
- }
50
- }
51
-
52
- export async function readRemoteLock(ssh, remoteCwd) {
53
- const lockPath = `.zephyr/${PROJECT_LOCK_FILE}`
54
- const escapedLockPath = lockPath.replace(/'/g, "'\\''")
55
- const checkCommand = `mkdir -p .zephyr && if [ -f '${escapedLockPath}' ]; then cat '${escapedLockPath}'; else echo "LOCK_NOT_FOUND"; fi`
56
-
57
- const checkResult = await ssh.execCommand(checkCommand, { cwd: remoteCwd })
58
-
59
- if (checkResult.stdout && checkResult.stdout.trim() !== 'LOCK_NOT_FOUND' && checkResult.stdout.trim() !== '') {
60
- try {
61
- return JSON.parse(checkResult.stdout.trim())
62
- } catch (_error) {
63
- return { raw: checkResult.stdout.trim() }
64
- }
65
- }
66
-
67
- return null
68
- }
69
-
70
- export async function compareLocksAndPrompt(rootDir, ssh, remoteCwd, { runPrompt, logWarning } = {}) {
71
- const localLock = await readLocalLock(rootDir)
72
- const remoteLock = await readRemoteLock(ssh, remoteCwd)
73
-
74
- if (!localLock || !remoteLock) {
75
- return false
76
- }
77
-
78
- const localKey = `${localLock.user}@${localLock.hostname}:${localLock.pid}:${localLock.startedAt}`
79
- const remoteKey = `${remoteLock.user}@${remoteLock.hostname}:${remoteLock.pid}:${remoteLock.startedAt}`
80
-
81
- if (localKey === remoteKey) {
82
- const startedBy = remoteLock.user ? `${remoteLock.user}@${remoteLock.hostname ?? 'unknown'}` : 'unknown user'
83
- const startedAt = remoteLock.startedAt ? ` at ${remoteLock.startedAt}` : ''
84
- const { shouldRemove } = await runPrompt([
85
- {
86
- type: 'confirm',
87
- name: 'shouldRemove',
88
- message: `Stale lock detected on server (started by ${startedBy}${startedAt}). This appears to be from a failed deployment. Remove it?`,
89
- default: true
90
- }
91
- ])
92
-
93
- if (shouldRemove) {
94
- const lockPath = `.zephyr/${PROJECT_LOCK_FILE}`
95
- const escapedLockPath = lockPath.replace(/'/g, "'\\''")
96
- const removeCommand = `rm -f '${escapedLockPath}'`
97
- await ssh.execCommand(removeCommand, { cwd: remoteCwd })
98
- await releaseLocalLock(rootDir, { logWarning })
99
- return true
100
- }
101
- }
102
-
103
- return false
104
- }
105
-
106
- export async function acquireRemoteLock(ssh, remoteCwd, rootDir, { runPrompt, logWarning } = {}) {
107
- const lockPath = `.zephyr/${PROJECT_LOCK_FILE}`
108
- const escapedLockPath = lockPath.replace(/'/g, "'\\''")
109
- const checkCommand = `mkdir -p .zephyr && if [ -f '${escapedLockPath}' ]; then cat '${escapedLockPath}'; else echo "LOCK_NOT_FOUND"; fi`
110
-
111
- const checkResult = await ssh.execCommand(checkCommand, { cwd: remoteCwd })
112
-
113
- if (checkResult.stdout && checkResult.stdout.trim() !== 'LOCK_NOT_FOUND' && checkResult.stdout.trim() !== '') {
114
- const localLock = await readLocalLock(rootDir)
115
- if (localLock) {
116
- const removed = await compareLocksAndPrompt(rootDir, ssh, remoteCwd, { runPrompt, logWarning })
117
- if (!removed) {
118
- let details = {}
119
- try {
120
- details = JSON.parse(checkResult.stdout.trim())
121
- } catch (_error) {
122
- details = { raw: checkResult.stdout.trim() }
123
- }
124
-
125
- const startedBy = details.user ? `${details.user}@${details.hostname ?? 'unknown'}` : 'unknown user'
126
- const startedAt = details.startedAt ? ` at ${details.startedAt}` : ''
127
- throw new Error(
128
- `Another deployment is currently in progress on the server (started by ${startedBy}${startedAt}). Remove ${remoteCwd}/${lockPath} if you are sure it is stale.`
129
- )
130
- }
131
- } else {
132
- let details = {}
133
- try {
134
- details = JSON.parse(checkResult.stdout.trim())
135
- } catch (_error) {
136
- details = { raw: checkResult.stdout.trim() }
137
- }
138
-
139
- const startedBy = details.user ? `${details.user}@${details.hostname ?? 'unknown'}` : 'unknown user'
140
- const startedAt = details.startedAt ? ` at ${details.startedAt}` : ''
141
- throw new Error(
142
- `Another deployment is currently in progress on the server (started by ${startedBy}${startedAt}). Remove ${remoteCwd}/${lockPath} if you are sure it is stale.`
143
- )
144
- }
145
- }
146
-
147
- const payload = createLockPayload()
148
- const payloadJson = JSON.stringify(payload, null, 2)
149
- const payloadBase64 = Buffer.from(payloadJson).toString('base64')
150
- const createCommand = `mkdir -p .zephyr && echo '${payloadBase64}' | base64 --decode > '${escapedLockPath}'`
151
-
152
- const createResult = await ssh.execCommand(createCommand, { cwd: remoteCwd })
153
- if (createResult.code !== 0) {
154
- throw new Error(`Failed to create lock file on server: ${createResult.stderr}`)
155
- }
156
-
157
- await acquireLocalLock(rootDir)
158
- return lockPath
159
- }
160
-
161
- export async function releaseRemoteLock(ssh, remoteCwd, { logWarning } = {}) {
162
- const lockPath = `.zephyr/${PROJECT_LOCK_FILE}`
163
- const escapedLockPath = lockPath.replace(/'/g, "'\\''")
164
- const removeCommand = `rm -f '${escapedLockPath}'`
165
-
166
- const result = await ssh.execCommand(removeCommand, { cwd: remoteCwd })
167
- if (result.code !== 0 && result.code !== 1) {
168
- logWarning?.(`Failed to remove lock file: ${result.stderr}`)
169
- }
170
- }
171
-
1
+ import fs from 'node:fs/promises'
2
+ import os from 'node:os'
3
+ import process from 'node:process'
4
+
5
+ import { PROJECT_LOCK_FILE, ensureDirectory, getLockFilePath, getProjectConfigDir } from '../utils/paths.mjs'
6
+
7
+ function createLockPayload() {
8
+ return {
9
+ user: os.userInfo().username,
10
+ pid: process.pid,
11
+ hostname: os.hostname(),
12
+ startedAt: new Date().toISOString()
13
+ }
14
+ }
15
+
16
+ export async function acquireLocalLock(rootDir) {
17
+ const lockPath = getLockFilePath(rootDir)
18
+ const configDir = getProjectConfigDir(rootDir)
19
+ await ensureDirectory(configDir)
20
+
21
+ const payload = createLockPayload()
22
+ const payloadJson = JSON.stringify(payload, null, 2)
23
+ await fs.writeFile(lockPath, payloadJson, 'utf8')
24
+
25
+ return payload
26
+ }
27
+
28
+ export async function releaseLocalLock(rootDir, { logWarning } = {}) {
29
+ const lockPath = getLockFilePath(rootDir)
30
+ try {
31
+ await fs.unlink(lockPath)
32
+ } catch (error) {
33
+ if (error.code !== 'ENOENT') {
34
+ logWarning?.(`Failed to remove local lock file: ${error.message}`)
35
+ }
36
+ }
37
+ }
38
+
39
+ export async function readLocalLock(rootDir) {
40
+ const lockPath = getLockFilePath(rootDir)
41
+ try {
42
+ const content = await fs.readFile(lockPath, 'utf8')
43
+ return JSON.parse(content)
44
+ } catch (error) {
45
+ if (error.code === 'ENOENT') {
46
+ return null
47
+ }
48
+ throw error
49
+ }
50
+ }
51
+
52
+ export async function readRemoteLock(ssh, remoteCwd) {
53
+ const lockPath = `.zephyr/${PROJECT_LOCK_FILE}`
54
+ const escapedLockPath = lockPath.replace(/'/g, "'\\''")
55
+ const checkCommand = `mkdir -p .zephyr && if [ -f '${escapedLockPath}' ]; then cat '${escapedLockPath}'; else echo "LOCK_NOT_FOUND"; fi`
56
+
57
+ const checkResult = await ssh.execCommand(checkCommand, { cwd: remoteCwd })
58
+
59
+ if (checkResult.stdout && checkResult.stdout.trim() !== 'LOCK_NOT_FOUND' && checkResult.stdout.trim() !== '') {
60
+ try {
61
+ return JSON.parse(checkResult.stdout.trim())
62
+ } catch (_error) {
63
+ return { raw: checkResult.stdout.trim() }
64
+ }
65
+ }
66
+
67
+ return null
68
+ }
69
+
70
+ export async function compareLocksAndPrompt(rootDir, ssh, remoteCwd, { runPrompt, logWarning } = {}) {
71
+ const localLock = await readLocalLock(rootDir)
72
+ const remoteLock = await readRemoteLock(ssh, remoteCwd)
73
+
74
+ if (!localLock || !remoteLock) {
75
+ return false
76
+ }
77
+
78
+ const localKey = `${localLock.user}@${localLock.hostname}:${localLock.pid}:${localLock.startedAt}`
79
+ const remoteKey = `${remoteLock.user}@${remoteLock.hostname}:${remoteLock.pid}:${remoteLock.startedAt}`
80
+
81
+ if (localKey === remoteKey) {
82
+ const startedBy = remoteLock.user ? `${remoteLock.user}@${remoteLock.hostname ?? 'unknown'}` : 'unknown user'
83
+ const startedAt = remoteLock.startedAt ? ` at ${remoteLock.startedAt}` : ''
84
+ const { shouldRemove } = await runPrompt([
85
+ {
86
+ type: 'confirm',
87
+ name: 'shouldRemove',
88
+ message: `Stale lock detected on server (started by ${startedBy}${startedAt}). This appears to be from a failed deployment. Remove it?`,
89
+ default: true
90
+ }
91
+ ])
92
+
93
+ if (shouldRemove) {
94
+ const lockPath = `.zephyr/${PROJECT_LOCK_FILE}`
95
+ const escapedLockPath = lockPath.replace(/'/g, "'\\''")
96
+ const removeCommand = `rm -f '${escapedLockPath}'`
97
+ await ssh.execCommand(removeCommand, { cwd: remoteCwd })
98
+ await releaseLocalLock(rootDir, { logWarning })
99
+ return true
100
+ }
101
+ }
102
+
103
+ return false
104
+ }
105
+
106
+ export async function acquireRemoteLock(ssh, remoteCwd, rootDir, { runPrompt, logWarning } = {}) {
107
+ const lockPath = `.zephyr/${PROJECT_LOCK_FILE}`
108
+ const escapedLockPath = lockPath.replace(/'/g, "'\\''")
109
+ const checkCommand = `mkdir -p .zephyr && if [ -f '${escapedLockPath}' ]; then cat '${escapedLockPath}'; else echo "LOCK_NOT_FOUND"; fi`
110
+
111
+ const checkResult = await ssh.execCommand(checkCommand, { cwd: remoteCwd })
112
+
113
+ if (checkResult.stdout && checkResult.stdout.trim() !== 'LOCK_NOT_FOUND' && checkResult.stdout.trim() !== '') {
114
+ const localLock = await readLocalLock(rootDir)
115
+ if (localLock) {
116
+ const removed = await compareLocksAndPrompt(rootDir, ssh, remoteCwd, { runPrompt, logWarning })
117
+ if (!removed) {
118
+ let details = {}
119
+ try {
120
+ details = JSON.parse(checkResult.stdout.trim())
121
+ } catch (_error) {
122
+ details = { raw: checkResult.stdout.trim() }
123
+ }
124
+
125
+ const startedBy = details.user ? `${details.user}@${details.hostname ?? 'unknown'}` : 'unknown user'
126
+ const startedAt = details.startedAt ? ` at ${details.startedAt}` : ''
127
+ throw new Error(
128
+ `Another deployment is currently in progress on the server (started by ${startedBy}${startedAt}). Remove ${remoteCwd}/${lockPath} if you are sure it is stale.`
129
+ )
130
+ }
131
+ } else {
132
+ let details = {}
133
+ try {
134
+ details = JSON.parse(checkResult.stdout.trim())
135
+ } catch (_error) {
136
+ details = { raw: checkResult.stdout.trim() }
137
+ }
138
+
139
+ const startedBy = details.user ? `${details.user}@${details.hostname ?? 'unknown'}` : 'unknown user'
140
+ const startedAt = details.startedAt ? ` at ${details.startedAt}` : ''
141
+ throw new Error(
142
+ `Another deployment is currently in progress on the server (started by ${startedBy}${startedAt}). Remove ${remoteCwd}/${lockPath} if you are sure it is stale.`
143
+ )
144
+ }
145
+ }
146
+
147
+ const payload = createLockPayload()
148
+ const payloadJson = JSON.stringify(payload, null, 2)
149
+ const payloadBase64 = Buffer.from(payloadJson).toString('base64')
150
+ const createCommand = `mkdir -p .zephyr && echo '${payloadBase64}' | base64 --decode > '${escapedLockPath}'`
151
+
152
+ const createResult = await ssh.execCommand(createCommand, { cwd: remoteCwd })
153
+ if (createResult.code !== 0) {
154
+ throw new Error(`Failed to create lock file on server: ${createResult.stderr}`)
155
+ }
156
+
157
+ await acquireLocalLock(rootDir)
158
+ return lockPath
159
+ }
160
+
161
+ export async function releaseRemoteLock(ssh, remoteCwd, { logWarning } = {}) {
162
+ const lockPath = `.zephyr/${PROJECT_LOCK_FILE}`
163
+ const escapedLockPath = lockPath.replace(/'/g, "'\\''")
164
+ const removeCommand = `rm -f '${escapedLockPath}'`
165
+
166
+ const result = await ssh.execCommand(removeCommand, { cwd: remoteCwd })
167
+ if (result.code !== 0 && result.code !== 1) {
168
+ logWarning?.(`Failed to remove lock file: ${result.stderr}`)
169
+ }
170
+ }
171
+
@@ -1,117 +1,117 @@
1
- import fs from 'node:fs/promises'
2
- import path from 'node:path'
3
-
4
- export async function hasPrePushHook(rootDir) {
5
- const hookPaths = [
6
- path.join(rootDir, '.git', 'hooks', 'pre-push'),
7
- path.join(rootDir, '.husky', 'pre-push'),
8
- path.join(rootDir, '.githooks', 'pre-push')
9
- ]
10
-
11
- for (const hookPath of hookPaths) {
12
- try {
13
- await fs.access(hookPath)
14
- const stats = await fs.stat(hookPath)
15
- if (stats.isFile()) {
16
- return true
17
- }
18
- } catch {
19
- // Hook doesn't exist at this path, continue checking
20
- }
21
- }
22
-
23
- return false
24
- }
25
-
26
- export async function hasLintScript(rootDir) {
27
- try {
28
- const packageJsonPath = path.join(rootDir, 'package.json')
29
- const raw = await fs.readFile(packageJsonPath, 'utf8')
30
- const packageJson = JSON.parse(raw)
31
- return packageJson.scripts && typeof packageJson.scripts.lint === 'string'
32
- } catch {
33
- return false
34
- }
35
- }
36
-
37
- export async function hasLaravelPint(rootDir) {
38
- try {
39
- const pintPath = path.join(rootDir, 'vendor', 'bin', 'pint')
40
- await fs.access(pintPath)
41
- const stats = await fs.stat(pintPath)
42
- return stats.isFile()
43
- } catch {
44
- return false
45
- }
46
- }
47
-
48
- export async function runLinting(rootDir, { runCommand, logProcessing, logSuccess } = {}) {
49
- const hasNpmLint = await hasLintScript(rootDir)
50
- const hasPint = await hasLaravelPint(rootDir)
51
-
52
- if (hasNpmLint) {
53
- logProcessing?.('Running npm lint...')
54
- await runCommand('npm', ['run', 'lint'], { cwd: rootDir })
55
- logSuccess?.('Linting completed.')
56
- return true
57
- }
58
-
59
- if (hasPint) {
60
- logProcessing?.('Running Laravel Pint...')
61
- await runCommand('php', ['vendor/bin/pint'], { cwd: rootDir })
62
- logSuccess?.('Linting completed.')
63
- return true
64
- }
65
-
66
- return false
67
- }
68
-
69
- export function hasStagedChanges(statusOutput) {
70
- if (!statusOutput || statusOutput.length === 0) {
71
- return false
72
- }
73
-
74
- const lines = statusOutput.split('\n').filter((line) => line.trim().length > 0)
75
-
76
- return lines.some((line) => {
77
- const firstChar = line[0]
78
- return firstChar && firstChar !== ' ' && firstChar !== '?'
79
- })
80
- }
81
-
82
- export async function commitLintingChanges(rootDir, { getGitStatus, runCommand, logProcessing, logSuccess } = {}) {
83
- const status = await getGitStatus(rootDir)
84
-
85
- if (!hasStagedChanges(status)) {
86
- await runCommand('git', ['add', '-u'], { cwd: rootDir })
87
- const newStatus = await getGitStatus(rootDir)
88
- if (!hasStagedChanges(newStatus)) {
89
- return false
90
- }
91
- }
92
-
93
- logProcessing?.('Committing linting changes...')
94
- await runCommand('git', ['commit', '-m', 'style: apply linting fixes'], { cwd: rootDir })
95
- logSuccess?.('Linting changes committed.')
96
- return true
97
- }
98
-
99
- export async function isLocalLaravelProject(rootDir) {
100
- try {
101
- const artisanPath = path.join(rootDir, 'artisan')
102
- const composerPath = path.join(rootDir, 'composer.json')
103
-
104
- await fs.access(artisanPath)
105
- const composerContent = await fs.readFile(composerPath, 'utf8')
106
- const composerJson = JSON.parse(composerContent)
107
-
108
- return (
109
- composerJson.require &&
110
- typeof composerJson.require === 'object' &&
111
- 'laravel/framework' in composerJson.require
112
- )
113
- } catch {
114
- return false
115
- }
116
- }
117
-
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+
4
+ export async function hasPrePushHook(rootDir) {
5
+ const hookPaths = [
6
+ path.join(rootDir, '.git', 'hooks', 'pre-push'),
7
+ path.join(rootDir, '.husky', 'pre-push'),
8
+ path.join(rootDir, '.githooks', 'pre-push')
9
+ ]
10
+
11
+ for (const hookPath of hookPaths) {
12
+ try {
13
+ await fs.access(hookPath)
14
+ const stats = await fs.stat(hookPath)
15
+ if (stats.isFile()) {
16
+ return true
17
+ }
18
+ } catch {
19
+ // Hook doesn't exist at this path, continue checking
20
+ }
21
+ }
22
+
23
+ return false
24
+ }
25
+
26
+ export async function hasLintScript(rootDir) {
27
+ try {
28
+ const packageJsonPath = path.join(rootDir, 'package.json')
29
+ const raw = await fs.readFile(packageJsonPath, 'utf8')
30
+ const packageJson = JSON.parse(raw)
31
+ return packageJson.scripts && typeof packageJson.scripts.lint === 'string'
32
+ } catch {
33
+ return false
34
+ }
35
+ }
36
+
37
+ export async function hasLaravelPint(rootDir) {
38
+ try {
39
+ const pintPath = path.join(rootDir, 'vendor', 'bin', 'pint')
40
+ await fs.access(pintPath)
41
+ const stats = await fs.stat(pintPath)
42
+ return stats.isFile()
43
+ } catch {
44
+ return false
45
+ }
46
+ }
47
+
48
+ export async function runLinting(rootDir, { runCommand, logProcessing, logSuccess } = {}) {
49
+ const hasNpmLint = await hasLintScript(rootDir)
50
+ const hasPint = await hasLaravelPint(rootDir)
51
+
52
+ if (hasNpmLint) {
53
+ logProcessing?.('Running npm lint...')
54
+ await runCommand('npm', ['run', 'lint'], { cwd: rootDir })
55
+ logSuccess?.('Linting completed.')
56
+ return true
57
+ }
58
+
59
+ if (hasPint) {
60
+ logProcessing?.('Running Laravel Pint...')
61
+ await runCommand('php', ['vendor/bin/pint'], { cwd: rootDir })
62
+ logSuccess?.('Linting completed.')
63
+ return true
64
+ }
65
+
66
+ return false
67
+ }
68
+
69
+ export function hasStagedChanges(statusOutput) {
70
+ if (!statusOutput || statusOutput.length === 0) {
71
+ return false
72
+ }
73
+
74
+ const lines = statusOutput.split('\n').filter((line) => line.trim().length > 0)
75
+
76
+ return lines.some((line) => {
77
+ const firstChar = line[0]
78
+ return firstChar && firstChar !== ' ' && firstChar !== '?'
79
+ })
80
+ }
81
+
82
+ export async function commitLintingChanges(rootDir, { getGitStatus, runCommand, logProcessing, logSuccess } = {}) {
83
+ const status = await getGitStatus(rootDir)
84
+
85
+ if (!hasStagedChanges(status)) {
86
+ await runCommand('git', ['add', '-u'], { cwd: rootDir })
87
+ const newStatus = await getGitStatus(rootDir)
88
+ if (!hasStagedChanges(newStatus)) {
89
+ return false
90
+ }
91
+ }
92
+
93
+ logProcessing?.('Committing linting changes...')
94
+ await runCommand('git', ['commit', '-m', 'style: apply linting fixes'], { cwd: rootDir })
95
+ logSuccess?.('Linting changes committed.')
96
+ return true
97
+ }
98
+
99
+ export async function isLocalLaravelProject(rootDir) {
100
+ try {
101
+ const artisanPath = path.join(rootDir, 'artisan')
102
+ const composerPath = path.join(rootDir, 'composer.json')
103
+
104
+ await fs.access(artisanPath)
105
+ const composerContent = await fs.readFile(composerPath, 'utf8')
106
+ const composerJson = JSON.parse(composerContent)
107
+
108
+ return (
109
+ composerJson.require &&
110
+ typeof composerJson.require === 'object' &&
111
+ 'laravel/framework' in composerJson.require
112
+ )
113
+ } catch {
114
+ return false
115
+ }
116
+ }
117
+