@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.
- package/README.md +144 -144
- package/bin/zephyr.mjs +29 -29
- package/package.json +58 -58
- package/src/config/project.mjs +118 -118
- package/src/config/servers.mjs +57 -57
- package/src/dependency-scanner.mjs +412 -433
- package/src/deploy/local-repo.mjs +215 -215
- package/src/deploy/locks.mjs +171 -171
- package/src/deploy/preflight.mjs +117 -117
- package/src/deploy/remote-exec.mjs +99 -99
- package/src/deploy/snapshots.mjs +35 -35
- package/src/index.mjs +91 -91
- package/src/main.mjs +677 -652
- package/src/project/bootstrap.mjs +147 -147
- package/src/runtime/local-command.mjs +18 -18
- package/src/runtime/prompt.mjs +14 -14
- package/src/runtime/ssh-client.mjs +14 -14
- package/src/ssh/index.mjs +8 -8
- package/src/ssh/keys.mjs +146 -146
- package/src/ssh/ssh.mjs +134 -134
- package/src/utils/command.mjs +92 -92
- package/src/utils/config-flow.mjs +284 -284
- package/src/utils/git.mjs +91 -91
- package/src/utils/id.mjs +6 -6
- package/src/utils/log-file.mjs +76 -76
- package/src/utils/output.mjs +29 -29
- package/src/utils/paths.mjs +28 -28
- package/src/utils/php-version.mjs +137 -0
- package/src/utils/remote-path.mjs +23 -23
- package/src/utils/task-planner.mjs +99 -96
- package/src/version-checker.mjs +162 -162
package/src/deploy/locks.mjs
CHANGED
|
@@ -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
|
+
|
package/src/deploy/preflight.mjs
CHANGED
|
@@ -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
|
+
|