@take-out/scripts 0.0.28
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/LICENSE +21 -0
- package/package.json +27 -0
- package/src/bootstrap.ts +182 -0
- package/src/check-circular-deps.ts +113 -0
- package/src/clean.ts +15 -0
- package/src/dev-tunnel-if-exist.ts +166 -0
- package/src/dev-tunnel.ts +178 -0
- package/src/ensure-tunnel.ts +13 -0
- package/src/env-pull.ts +54 -0
- package/src/env-update.ts +126 -0
- package/src/exec-with-env.ts +57 -0
- package/src/helpers/check-port.ts +22 -0
- package/src/helpers/ensure-s3-bucket.ts +88 -0
- package/src/helpers/env-load.ts +26 -0
- package/src/helpers/get-docker-host.ts +37 -0
- package/src/helpers/get-test-env.ts +25 -0
- package/src/helpers/handleProcessExit.ts +254 -0
- package/src/helpers/run.ts +310 -0
- package/src/helpers/wait-for-port.ts +33 -0
- package/src/helpers/zero-get-version.ts +8 -0
- package/src/node-version-check.ts +49 -0
- package/src/release.ts +352 -0
- package/src/run.ts +358 -0
- package/src/sst-get-environment.ts +31 -0
- package/src/typescript.ts +16 -0
- package/src/update-deps.ts +336 -0
- package/src/wait-for-dev.ts +40 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process'
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
3
|
+
import {
|
|
4
|
+
addProcessHandler,
|
|
5
|
+
setExitCleanupState,
|
|
6
|
+
type ProcessHandler,
|
|
7
|
+
type ProcessType,
|
|
8
|
+
} from './run'
|
|
9
|
+
|
|
10
|
+
type ExitCallback = (info: { signal: NodeJS.Signals | string }) => void | Promise<void>
|
|
11
|
+
|
|
12
|
+
interface HandleProcessExitReturn {
|
|
13
|
+
addChildProcess: ProcessHandler
|
|
14
|
+
cleanup: () => Promise<void>
|
|
15
|
+
exit: (code?: number) => Promise<void>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function getChildPids(parentPid: number): Promise<number[]> {
|
|
19
|
+
try {
|
|
20
|
+
const proc = spawn('pgrep', ['-P', parentPid.toString()], {
|
|
21
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
let output = ''
|
|
26
|
+
proc.stdout?.on('data', (data) => {
|
|
27
|
+
output += data.toString()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
proc.on('close', () => {
|
|
31
|
+
const childPids = output.trim().split('\n').filter(Boolean).map(Number)
|
|
32
|
+
resolve(childPids)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error(`Error getting child PIDs for ${parentPid}: ${error}`)
|
|
37
|
+
return []
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function getAllDescendantPids(parentPid: number): Promise<number[]> {
|
|
42
|
+
const childPids = await getChildPids(parentPid)
|
|
43
|
+
const descendantPids = [...childPids]
|
|
44
|
+
|
|
45
|
+
for (const childPid of childPids) {
|
|
46
|
+
const grandchildren = await getAllDescendantPids(childPid)
|
|
47
|
+
descendantPids.push(...grandchildren)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return descendantPids
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function killProcessTree(
|
|
54
|
+
pid: number,
|
|
55
|
+
signal: NodeJS.Signals = 'SIGTERM',
|
|
56
|
+
forceful: boolean = false
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
try {
|
|
59
|
+
const descendants = await getAllDescendantPids(pid)
|
|
60
|
+
|
|
61
|
+
// first send the requested signal
|
|
62
|
+
for (const descendantPid of descendants.reverse()) {
|
|
63
|
+
try {
|
|
64
|
+
process.kill(descendantPid, signal)
|
|
65
|
+
} catch (_) {
|
|
66
|
+
// process may already be gone
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (pid && !Number.isNaN(pid)) {
|
|
71
|
+
try {
|
|
72
|
+
process.kill(pid, signal)
|
|
73
|
+
} catch (_) {
|
|
74
|
+
// process may already be gone
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// if forceful, wait briefly then send SIGKILL
|
|
79
|
+
if (forceful && signal !== 'SIGKILL') {
|
|
80
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
81
|
+
|
|
82
|
+
// send SIGKILL to any still-alive processes
|
|
83
|
+
for (const descendantPid of descendants.reverse()) {
|
|
84
|
+
try {
|
|
85
|
+
process.kill(descendantPid, 'SIGKILL')
|
|
86
|
+
} catch (_) {
|
|
87
|
+
// process may already be gone
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (pid && !Number.isNaN(pid)) {
|
|
92
|
+
try {
|
|
93
|
+
process.kill(pid, 'SIGKILL')
|
|
94
|
+
} catch (_) {
|
|
95
|
+
// process may already be gone
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error(`Error killing process tree for ${pid}: ${error}`)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isChildProcess(proc: ProcessType): proc is ChildProcess {
|
|
105
|
+
return 'killed' in proc && typeof (proc as any).on === 'function'
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let isHandling = false
|
|
109
|
+
|
|
110
|
+
export function handleProcessExit({
|
|
111
|
+
onExit,
|
|
112
|
+
}: {
|
|
113
|
+
onExit?: ExitCallback
|
|
114
|
+
} = {}): HandleProcessExitReturn {
|
|
115
|
+
if (isHandling) {
|
|
116
|
+
throw new Error(`Only one handleProcessExit per process should be registered!`)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
isHandling = true
|
|
120
|
+
const processes: ProcessType[] = []
|
|
121
|
+
const allChildPids = new Set<number>()
|
|
122
|
+
let cleaning = false
|
|
123
|
+
|
|
124
|
+
const cleanup = async (signal: NodeJS.Signals | string = 'SIGTERM') => {
|
|
125
|
+
if (cleaning) return
|
|
126
|
+
cleaning = true
|
|
127
|
+
|
|
128
|
+
// notify run.ts that we're in cleanup state
|
|
129
|
+
setExitCleanupState(true)
|
|
130
|
+
|
|
131
|
+
// suppress console output during cleanup for cleaner exit
|
|
132
|
+
if (signal === 'SIGINT') {
|
|
133
|
+
const noop = () => {}
|
|
134
|
+
console.log = noop
|
|
135
|
+
console.info = noop
|
|
136
|
+
console.warn = noop
|
|
137
|
+
// keep console.error for critical errors only
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// wrap entire cleanup in a timeout to prevent hanging
|
|
141
|
+
if (onExit) {
|
|
142
|
+
try {
|
|
143
|
+
await onExit({ signal })
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error('Error in exit callback:', error)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// skip cleanup if no processes to clean up
|
|
150
|
+
if (processes.length === 0) {
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// for SIGINT (Ctrl+C), be more aggressive with cleanup
|
|
155
|
+
const isInterrupt = signal === 'SIGINT'
|
|
156
|
+
const killSignal = isInterrupt ? 'SIGTERM' : (signal as NodeJS.Signals)
|
|
157
|
+
|
|
158
|
+
await Promise.all(
|
|
159
|
+
processes.map(async (proc) => {
|
|
160
|
+
if (proc.pid) {
|
|
161
|
+
try {
|
|
162
|
+
await killProcessTree(proc.pid, killSignal, isInterrupt)
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error(
|
|
165
|
+
`Error during graceful shutdown of process ${proc.pid}: ${error}`
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
// shorter wait for SIGINT
|
|
173
|
+
await new Promise((res) => setTimeout(res, isInterrupt ? 50 : 200))
|
|
174
|
+
|
|
175
|
+
// force kill any remaining processes
|
|
176
|
+
for (const proc of processes) {
|
|
177
|
+
try {
|
|
178
|
+
if (!proc.exitCode && proc.pid) {
|
|
179
|
+
await killProcessTree(proc.pid, 'SIGKILL', true)
|
|
180
|
+
}
|
|
181
|
+
} catch (err) {
|
|
182
|
+
// process already gone
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const addChildProcess = (proc: ProcessType) => {
|
|
188
|
+
processes.push(proc)
|
|
189
|
+
|
|
190
|
+
// track pid if available
|
|
191
|
+
const pid = (proc as any).pid
|
|
192
|
+
if (pid) {
|
|
193
|
+
allChildPids.add(pid)
|
|
194
|
+
|
|
195
|
+
// for child processes, capture descendants once after a brief delay
|
|
196
|
+
if (isChildProcess(proc)) {
|
|
197
|
+
setTimeout(async () => {
|
|
198
|
+
if (proc.pid) {
|
|
199
|
+
try {
|
|
200
|
+
const childPids = await getChildPids(proc.pid)
|
|
201
|
+
for (const childPid of childPids) {
|
|
202
|
+
allChildPids.add(childPid)
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
// ignore errors in background polling
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}, 300)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
addProcessHandler(addChildProcess)
|
|
214
|
+
|
|
215
|
+
const sigtermHandler = () => {
|
|
216
|
+
cleanup('SIGTERM').then(() => {
|
|
217
|
+
process.exit(0)
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const sigintHandler = () => {
|
|
222
|
+
// immediately print newline and reset cursor for clean terminal
|
|
223
|
+
process.stdout.write('\n')
|
|
224
|
+
// reset terminal attributes
|
|
225
|
+
process.stdout.write('\x1b[0m')
|
|
226
|
+
|
|
227
|
+
cleanup('SIGINT').then(() => {
|
|
228
|
+
process.exit(0)
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// intercept process.exit to ensure cleanup
|
|
233
|
+
const originalExit = process.exit
|
|
234
|
+
process.exit = ((code?: number) => {
|
|
235
|
+
cleanup('SIGTERM').then(() => {
|
|
236
|
+
originalExit(code)
|
|
237
|
+
})
|
|
238
|
+
}) as typeof process.exit
|
|
239
|
+
|
|
240
|
+
process.on('beforeExit', () => cleanup('SIGTERM'))
|
|
241
|
+
process.on('SIGINT', sigintHandler)
|
|
242
|
+
process.on('SIGTERM', sigtermHandler)
|
|
243
|
+
|
|
244
|
+
const exit = async (code: number = 0) => {
|
|
245
|
+
await cleanup('SIGTERM')
|
|
246
|
+
process.exit(code)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
addChildProcess,
|
|
251
|
+
cleanup,
|
|
252
|
+
exit,
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process'
|
|
2
|
+
import type { Timer } from '@take-out/helpers'
|
|
3
|
+
|
|
4
|
+
export type ProcessType = ChildProcess | Bun.Subprocess
|
|
5
|
+
export type ProcessHandler = (process: ProcessType) => void
|
|
6
|
+
|
|
7
|
+
// track if we're in cleanup state (another process failed)
|
|
8
|
+
let isInExitCleanup = false
|
|
9
|
+
|
|
10
|
+
export function setExitCleanupState(state: boolean) {
|
|
11
|
+
isInExitCleanup = state
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getIsExiting() {
|
|
15
|
+
return isInExitCleanup
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const processHandlers = new Set<ProcessHandler>()
|
|
19
|
+
|
|
20
|
+
const colors = [
|
|
21
|
+
'\x1b[36m', // cyan
|
|
22
|
+
'\x1b[35m', // magenta
|
|
23
|
+
'\x1b[32m', // green
|
|
24
|
+
'\x1b[33m', // yellow
|
|
25
|
+
'\x1b[34m', // blue
|
|
26
|
+
'\x1b[31m', // red
|
|
27
|
+
]
|
|
28
|
+
const reset = '\x1b[0m'
|
|
29
|
+
let colorIndex = 0
|
|
30
|
+
|
|
31
|
+
function getNextColor(): string {
|
|
32
|
+
const color = colors[colorIndex % colors.length]!
|
|
33
|
+
colorIndex++
|
|
34
|
+
return color
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const running: Record<string, Promise<unknown> | undefined | null> = {}
|
|
38
|
+
|
|
39
|
+
export async function runInline(name: string, cb: () => Promise<void>) {
|
|
40
|
+
const promise = cb()
|
|
41
|
+
running[name] = promise
|
|
42
|
+
return await promise
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function run(
|
|
46
|
+
command: string,
|
|
47
|
+
options?: {
|
|
48
|
+
env?: Record<string, string>
|
|
49
|
+
cwd?: string
|
|
50
|
+
silent?: boolean
|
|
51
|
+
captureOutput?: boolean
|
|
52
|
+
prefix?: string
|
|
53
|
+
detached?: boolean
|
|
54
|
+
timeout?: number
|
|
55
|
+
timing?: boolean | string
|
|
56
|
+
}
|
|
57
|
+
) {
|
|
58
|
+
const { env, cwd, silent, captureOutput, prefix, detached, timeout, timing } =
|
|
59
|
+
options || {}
|
|
60
|
+
|
|
61
|
+
if (timing) {
|
|
62
|
+
const name = typeof timing === 'string' ? timing : command
|
|
63
|
+
const startTime = Date.now()
|
|
64
|
+
try {
|
|
65
|
+
const promise = runInternal()
|
|
66
|
+
running[name] = promise
|
|
67
|
+
const result = await promise
|
|
68
|
+
const duration = Date.now() - startTime
|
|
69
|
+
console.info(
|
|
70
|
+
`\x1b[32m✓\x1b[0m \x1b[35m${name}\x1b[0m completed in \x1b[33m${formatDuration(duration)}\x1b[0m`
|
|
71
|
+
)
|
|
72
|
+
return result
|
|
73
|
+
} catch (error) {
|
|
74
|
+
const duration = Date.now() - startTime
|
|
75
|
+
console.error(`✗ ${name} failed after ${formatDuration(duration)}`)
|
|
76
|
+
throw error
|
|
77
|
+
} finally {
|
|
78
|
+
running[name] = null
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return runInternal()
|
|
83
|
+
|
|
84
|
+
async function runInternal() {
|
|
85
|
+
if (!silent) {
|
|
86
|
+
console.info(`$ ${command}${cwd ? ` (in ${cwd})` : ``}`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let timeoutId: Timer | undefined
|
|
90
|
+
let didTimeOut = false
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const shell = Bun.spawn(['bash', '-c', command], {
|
|
94
|
+
env: { ...process.env, ...(env || {}) },
|
|
95
|
+
cwd,
|
|
96
|
+
stdout: 'pipe',
|
|
97
|
+
stderr: 'pipe', // always pipe stderr so we can capture it for error messages
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
if (detached) {
|
|
101
|
+
shell.unref()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
processHandlers.forEach((cb) => cb(shell))
|
|
105
|
+
|
|
106
|
+
if (timeout) {
|
|
107
|
+
timeoutId = setTimeout(() => {
|
|
108
|
+
didTimeOut = true
|
|
109
|
+
console.error(`Command timed out after ${timeout}ms: ${command}`)
|
|
110
|
+
shell.kill()
|
|
111
|
+
}, timeout)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const color = prefix ? getNextColor() : ''
|
|
115
|
+
const coloredPrefix = prefix ? `${color}[${prefix}]${reset}` : ''
|
|
116
|
+
|
|
117
|
+
const writeOutput = (text: string, isStderr: boolean) => {
|
|
118
|
+
if (!silent) {
|
|
119
|
+
const output = prefix ? `${coloredPrefix} ${text}` : text
|
|
120
|
+
if (!prefix || !captureOutput) {
|
|
121
|
+
const stream = isStderr ? process.stderr : process.stdout
|
|
122
|
+
stream.write(output)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const processStream = async (
|
|
128
|
+
stream: ReadableStream<Uint8Array> | undefined,
|
|
129
|
+
isStderr: boolean
|
|
130
|
+
): Promise<string> => {
|
|
131
|
+
if (silent && !captureOutput) {
|
|
132
|
+
return ''
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!stream) return ''
|
|
136
|
+
|
|
137
|
+
let buffer = ''
|
|
138
|
+
let captured = ''
|
|
139
|
+
const decoder = new TextDecoder()
|
|
140
|
+
const reader = stream.getReader()
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
while (true) {
|
|
144
|
+
const { done, value } = await reader.read()
|
|
145
|
+
if (done) break
|
|
146
|
+
|
|
147
|
+
const text = buffer + decoder.decode(value, { stream: true })
|
|
148
|
+
const lines = text.split('\n')
|
|
149
|
+
|
|
150
|
+
// keep last partial line in buffer
|
|
151
|
+
buffer = lines.pop() || ''
|
|
152
|
+
|
|
153
|
+
// process complete lines
|
|
154
|
+
for (const line of lines) {
|
|
155
|
+
// always capture for potential error messages or captureOutput
|
|
156
|
+
captured += line + '\n'
|
|
157
|
+
|
|
158
|
+
// output if not silent and appropriate
|
|
159
|
+
if (!captureOutput || prefix) {
|
|
160
|
+
writeOutput(line + '\n', isStderr)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// output any remaining buffer
|
|
166
|
+
if (buffer) {
|
|
167
|
+
captured += buffer
|
|
168
|
+
if (!captureOutput || prefix) {
|
|
169
|
+
writeOutput(buffer + '\n', isStderr)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.error(`Error reading stream!`, err)
|
|
174
|
+
} finally {
|
|
175
|
+
reader.releaseLock()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return captured
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// always process both streams
|
|
182
|
+
const [stdout, stderr] = await Promise.all([
|
|
183
|
+
processStream(shell.stdout, false),
|
|
184
|
+
processStream(shell.stderr, true),
|
|
185
|
+
])
|
|
186
|
+
|
|
187
|
+
const exitCode = await shell.exited
|
|
188
|
+
|
|
189
|
+
if (timeoutId) {
|
|
190
|
+
clearTimeout(timeoutId)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (detached) {
|
|
194
|
+
return { stdout: '', stderr: '' }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (exitCode !== 0) {
|
|
198
|
+
const errorMsg =
|
|
199
|
+
exitCode === 143 && didTimeOut
|
|
200
|
+
? `Command timed out after ${timeout}ms: ${command}`
|
|
201
|
+
: `Command failed with exit code ${exitCode}: ${command}`
|
|
202
|
+
|
|
203
|
+
if (!silent && !isInExitCleanup) {
|
|
204
|
+
console.error(`run() error: ${errorMsg}: ${stderr || ''}`)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const error = new Error(errorMsg, { cause: { exitCode } })
|
|
208
|
+
Error.captureStackTrace(error, runInternal)
|
|
209
|
+
throw error
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { stdout, stderr, exitCode }
|
|
213
|
+
} catch (error) {
|
|
214
|
+
clearTimeout(timeoutId)
|
|
215
|
+
if (!silent && !isInExitCleanup) {
|
|
216
|
+
// only show the error message, not the full object if it's our error
|
|
217
|
+
if (error instanceof Error && (error as any).cause?.exitCode !== undefined) {
|
|
218
|
+
// this is our controlled error, already logged above
|
|
219
|
+
} else {
|
|
220
|
+
console.error(`Error running command: ${command}`, error)
|
|
221
|
+
}
|
|
222
|
+
} else if (!silent && isInExitCleanup) {
|
|
223
|
+
// simple message when being killed due to another error
|
|
224
|
+
const shortCmd = command.split(' ')[0]
|
|
225
|
+
console.error(`${shortCmd} exiting due to earlier error`)
|
|
226
|
+
}
|
|
227
|
+
throw error
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export const addProcessHandler = (cb: ProcessHandler) => {
|
|
233
|
+
processHandlers.add(cb)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function waitForRun(name: string) {
|
|
237
|
+
if (running[name] === undefined) {
|
|
238
|
+
throw new Error(`Can't wait before task runs: ${name}`)
|
|
239
|
+
}
|
|
240
|
+
await running[name]
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function formatDuration(ms: number): string {
|
|
244
|
+
const seconds = Math.floor(ms / 1000)
|
|
245
|
+
const minutes = Math.floor(seconds / 60)
|
|
246
|
+
const remainingSeconds = seconds % 60
|
|
247
|
+
|
|
248
|
+
if (minutes > 0) {
|
|
249
|
+
return `${minutes}m ${remainingSeconds}s`
|
|
250
|
+
}
|
|
251
|
+
return `${seconds}s`
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export async function printTiming<T>(name: string, fn: () => Promise<T>): Promise<T> {
|
|
255
|
+
const startTime = Date.now()
|
|
256
|
+
try {
|
|
257
|
+
const result = await fn()
|
|
258
|
+
const duration = Date.now() - startTime
|
|
259
|
+
console.info(
|
|
260
|
+
`\x1b[32m✓\x1b[0m \x1b[35m${name}\x1b[0m completed in \x1b[33m${formatDuration(duration)}\x1b[0m`
|
|
261
|
+
)
|
|
262
|
+
return result
|
|
263
|
+
} catch (error) {
|
|
264
|
+
const duration = Date.now() - startTime
|
|
265
|
+
console.error(`✗ ${name} failed after ${formatDuration(duration)}`)
|
|
266
|
+
throw error
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export async function runParallel(
|
|
271
|
+
tasks: Array<{ name: string; fn: () => Promise<void>; condition?: () => boolean }>
|
|
272
|
+
) {
|
|
273
|
+
const activeTasks = tasks.filter((task) => !task.condition || task.condition())
|
|
274
|
+
|
|
275
|
+
if (activeTasks.length === 0) {
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
console.info(`\nStarting parallel tasks: ${activeTasks.map((t) => t.name).join(', ')}`)
|
|
280
|
+
|
|
281
|
+
const taskStartTime = Date.now()
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
await Promise.all(
|
|
285
|
+
activeTasks.map(async (task) => {
|
|
286
|
+
const startTime = Date.now()
|
|
287
|
+
try {
|
|
288
|
+
await task.fn()
|
|
289
|
+
const duration = Date.now() - startTime
|
|
290
|
+
console.info(
|
|
291
|
+
`\x1b[32m✓\x1b[0m task: \x1b[35m${task.name}\x1b[0m completed in \x1b[33m${formatDuration(duration)}\x1b[0m`
|
|
292
|
+
)
|
|
293
|
+
} catch (error) {
|
|
294
|
+
const duration = Date.now() - startTime
|
|
295
|
+
console.error(`✗ task: ${task.name} failed after ${formatDuration(duration)}`)
|
|
296
|
+
throw error
|
|
297
|
+
}
|
|
298
|
+
})
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
const totalDuration = Date.now() - taskStartTime
|
|
302
|
+
console.info(
|
|
303
|
+
`\nAll parallel tasks completed successfully in ${formatDuration(totalDuration)}`
|
|
304
|
+
)
|
|
305
|
+
} catch (error) {
|
|
306
|
+
const totalDuration = Date.now() - taskStartTime
|
|
307
|
+
console.error(`\nCI fail after ${formatDuration(totalDuration)} failed`)
|
|
308
|
+
throw error
|
|
309
|
+
}
|
|
310
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { checkPort } from './check-port'
|
|
2
|
+
|
|
3
|
+
interface WaitForPortOptions {
|
|
4
|
+
host?: string
|
|
5
|
+
intervalMs?: number
|
|
6
|
+
timeoutMs?: number
|
|
7
|
+
retries?: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function waitForPort(
|
|
11
|
+
port: number,
|
|
12
|
+
options: WaitForPortOptions = {}
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
const {
|
|
15
|
+
host = '127.0.0.1',
|
|
16
|
+
intervalMs = 1000,
|
|
17
|
+
timeoutMs = 30000,
|
|
18
|
+
retries = Math.floor(timeoutMs / intervalMs),
|
|
19
|
+
} = options
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < retries; i++) {
|
|
22
|
+
const isOpen = await checkPort(port, host)
|
|
23
|
+
if (isOpen) {
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (i < retries - 1) {
|
|
28
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs))
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
throw new Error(`Port ${port} on ${host} did not become available after ${timeoutMs}ms`)
|
|
33
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
export function getZeroVersion() {
|
|
5
|
+
const packageJsonPath = join(process.cwd(), 'package.json')
|
|
6
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
|
|
7
|
+
return packageJson.dependencies?.['@rocicorp/zero']?.replace(/^[\^~]/, '')
|
|
8
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs/promises'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
|
|
6
|
+
function getCurrentNodeVersion() {
|
|
7
|
+
return process.version
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function getRequiredNodeVersion() {
|
|
11
|
+
// try .node-version file first
|
|
12
|
+
try {
|
|
13
|
+
const nodeVersionContent = await fs.readFile(
|
|
14
|
+
path.join(process.cwd(), '.node-version'),
|
|
15
|
+
'utf-8'
|
|
16
|
+
)
|
|
17
|
+
return `v${nodeVersionContent.trim()}`
|
|
18
|
+
} catch {}
|
|
19
|
+
|
|
20
|
+
// fallback to package.json engines.node
|
|
21
|
+
try {
|
|
22
|
+
const packageJson = JSON.parse(
|
|
23
|
+
await fs.readFile(path.join(process.cwd(), 'package.json'), 'utf-8')
|
|
24
|
+
)
|
|
25
|
+
return packageJson?.engines?.node ? `v${packageJson.engines.node}` : null
|
|
26
|
+
} catch {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function checkNodeVersion() {
|
|
32
|
+
const currentNodeVersion = getCurrentNodeVersion()
|
|
33
|
+
const requiredNodeVersion = await getRequiredNodeVersion()
|
|
34
|
+
|
|
35
|
+
if (requiredNodeVersion) {
|
|
36
|
+
if (currentNodeVersion !== requiredNodeVersion) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`\u001b[33mWarning: Incorrect Node.js version. Expected ${requiredNodeVersion} but got ${currentNodeVersion}\u001b[0m`
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (import.meta.main) {
|
|
45
|
+
checkNodeVersion().catch((e: any) => {
|
|
46
|
+
console.error(e.message)
|
|
47
|
+
process.exit(1)
|
|
48
|
+
})
|
|
49
|
+
}
|