@take-out/scripts 0.1.19 → 0.1.20
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/package.json +3 -2
- package/src/helpers/deploy-health.ts +13 -2
- package/src/run-pty.mjs +348 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@take-out/scripts",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/run.ts",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@clack/prompts": "^0.8.2",
|
|
32
|
-
"@
|
|
32
|
+
"@lydell/node-pty": "^1.2.0-beta.3",
|
|
33
|
+
"@take-out/helpers": "0.1.20",
|
|
33
34
|
"picocolors": "^1.1.1"
|
|
34
35
|
},
|
|
35
36
|
"peerDependencies": {
|
|
@@ -24,12 +24,19 @@ export interface ContainerInfo {
|
|
|
24
24
|
/**
|
|
25
25
|
* check for containers stuck in restart loop
|
|
26
26
|
* fails fast if any container is crash-looping
|
|
27
|
+
* excludes init containers (minio-init, migrate) that are expected to exit
|
|
27
28
|
*/
|
|
28
|
-
export async function checkRestartLoops(
|
|
29
|
+
export async function checkRestartLoops(
|
|
30
|
+
ctx: SSHContext,
|
|
31
|
+
options?: { excludePatterns?: string[] }
|
|
32
|
+
): Promise<{
|
|
29
33
|
hasLoop: boolean
|
|
30
34
|
services: string[]
|
|
31
35
|
}> {
|
|
32
36
|
const { sshOpts, deployUser, deployHost, $ } = ctx
|
|
37
|
+
// default exclude patterns for init/oneshot containers
|
|
38
|
+
const excludePatterns = options?.excludePatterns ?? ['init', 'migrate']
|
|
39
|
+
|
|
33
40
|
try {
|
|
34
41
|
const sshCmd = `ssh ${sshOpts} ${deployUser}@${deployHost}`
|
|
35
42
|
const result =
|
|
@@ -40,7 +47,11 @@ export async function checkRestartLoops(ctx: SSHContext): Promise<{
|
|
|
40
47
|
for (const line of lines) {
|
|
41
48
|
if (line.includes('Restarting')) {
|
|
42
49
|
const name = line.split(' ')[0] || ''
|
|
43
|
-
|
|
50
|
+
// skip init/oneshot containers
|
|
51
|
+
const isExcluded = excludePatterns.some((pattern) => name.includes(pattern))
|
|
52
|
+
if (!isExcluded) {
|
|
53
|
+
loopingServices.push(name)
|
|
54
|
+
}
|
|
44
55
|
}
|
|
45
56
|
}
|
|
46
57
|
|
package/src/run-pty.mjs
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import { join, relative, resolve } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import pty from '@lydell/node-pty'
|
|
7
|
+
|
|
8
|
+
const colors = [
|
|
9
|
+
'\x1b[38;5;245m',
|
|
10
|
+
'\x1b[38;5;240m',
|
|
11
|
+
'\x1b[38;5;243m',
|
|
12
|
+
'\x1b[38;5;248m',
|
|
13
|
+
'\x1b[38;5;238m',
|
|
14
|
+
'\x1b[38;5;252m',
|
|
15
|
+
]
|
|
16
|
+
const reset = '\x1b[0m'
|
|
17
|
+
const dim = '\x1b[2m'
|
|
18
|
+
const bold = '\x1b[1m'
|
|
19
|
+
const inverse = '\x1b[7m'
|
|
20
|
+
|
|
21
|
+
const args = process.argv.slice(2)
|
|
22
|
+
const ownFlags = ['--no-root', '--bun', '--watch', '--flags=last']
|
|
23
|
+
const runCommands = []
|
|
24
|
+
const forwardArgs = []
|
|
25
|
+
|
|
26
|
+
for (let i = 0; i < args.length; i++) {
|
|
27
|
+
const arg = args[i]
|
|
28
|
+
if (arg.startsWith('--')) {
|
|
29
|
+
if (ownFlags.includes(arg)) continue
|
|
30
|
+
forwardArgs.push(arg)
|
|
31
|
+
const nextArg = args[i + 1]
|
|
32
|
+
if (nextArg && !nextArg.startsWith('--')) {
|
|
33
|
+
forwardArgs.push(nextArg)
|
|
34
|
+
i++
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
runCommands.push(arg)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const noRoot = args.includes('--no-root')
|
|
42
|
+
const runBun = args.includes('--bun')
|
|
43
|
+
const flagsLast = args.includes('--flags=last')
|
|
44
|
+
|
|
45
|
+
const processes = []
|
|
46
|
+
let dashboardMode = false
|
|
47
|
+
let selectedIndex = 0
|
|
48
|
+
|
|
49
|
+
function getPrefix(index) {
|
|
50
|
+
const p = processes[index]
|
|
51
|
+
if (!p) return ''
|
|
52
|
+
const color = colors[index % colors.length]
|
|
53
|
+
return `${color}${p.shortcut} ${p.label}${reset}`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (runCommands.length === 0) {
|
|
57
|
+
console.error('Usage: run-pty <script1> [script2] ...')
|
|
58
|
+
process.exit(1)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function readPackageJson(dir) {
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(await fs.promises.readFile(join(dir, 'package.json'), 'utf8'))
|
|
64
|
+
} catch {
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function getWorkspacePatterns() {
|
|
70
|
+
const pkg = await readPackageJson('.')
|
|
71
|
+
if (!pkg?.workspaces) return []
|
|
72
|
+
return Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages || []
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function findPackageJsonDirs(base, depth = 3) {
|
|
76
|
+
if (depth <= 0) return []
|
|
77
|
+
const results = []
|
|
78
|
+
try {
|
|
79
|
+
if (
|
|
80
|
+
await fs.promises
|
|
81
|
+
.access(join(base, 'package.json'))
|
|
82
|
+
.then(() => true)
|
|
83
|
+
.catch(() => false)
|
|
84
|
+
) {
|
|
85
|
+
results.push(base)
|
|
86
|
+
}
|
|
87
|
+
const entries = await fs.promises.readdir(base, { withFileTypes: true })
|
|
88
|
+
for (const e of entries) {
|
|
89
|
+
if (e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules') {
|
|
90
|
+
results.push(...(await findPackageJsonDirs(join(base, e.name), depth - 1)))
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch {}
|
|
94
|
+
return results
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function findWorkspaceScripts(scripts) {
|
|
98
|
+
const patterns = await getWorkspacePatterns()
|
|
99
|
+
if (!patterns.length) return new Map()
|
|
100
|
+
|
|
101
|
+
const dirs = await findPackageJsonDirs('.')
|
|
102
|
+
const result = new Map()
|
|
103
|
+
|
|
104
|
+
for (const dir of dirs) {
|
|
105
|
+
if (dir === '.') continue
|
|
106
|
+
const rel = relative('.', dir).replace(/\\/g, '/')
|
|
107
|
+
const matches = patterns.some((p) => {
|
|
108
|
+
const np = p.replace(/\\/g, '/').replace(/^\.\//, '')
|
|
109
|
+
return np.endsWith('/*')
|
|
110
|
+
? rel.startsWith(np.slice(0, -1))
|
|
111
|
+
: rel === np || rel.startsWith(np + '/')
|
|
112
|
+
})
|
|
113
|
+
if (!matches) continue
|
|
114
|
+
|
|
115
|
+
const pkg = await readPackageJson(dir)
|
|
116
|
+
if (!pkg?.scripts) continue
|
|
117
|
+
const available = scripts.filter((s) => typeof pkg.scripts[s] === 'string')
|
|
118
|
+
if (available.length) result.set(dir, { scripts: available, name: pkg.name || dir })
|
|
119
|
+
}
|
|
120
|
+
return result
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function spawnScript(name, cwd, label, extraArgs, index) {
|
|
124
|
+
const runArgs = ['run', '--silent', runBun ? '--bun' : '', name, ...extraArgs].filter(
|
|
125
|
+
Boolean
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
const terminal = pty.spawn('bun', runArgs, {
|
|
129
|
+
cwd: resolve(cwd),
|
|
130
|
+
cols: process.stdout.columns || 80,
|
|
131
|
+
rows: process.stdout.rows || 24,
|
|
132
|
+
env: { ...process.env, FORCE_COLOR: '3' },
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const idx = index ?? processes.length
|
|
136
|
+
const managed = {
|
|
137
|
+
terminal,
|
|
138
|
+
name,
|
|
139
|
+
cwd,
|
|
140
|
+
label,
|
|
141
|
+
extraArgs,
|
|
142
|
+
index: idx,
|
|
143
|
+
shortcut: '',
|
|
144
|
+
killed: false,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (index !== undefined) {
|
|
148
|
+
processes[index] = managed
|
|
149
|
+
} else {
|
|
150
|
+
processes.push(managed)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
terminal.onData((data) => {
|
|
154
|
+
if (dashboardMode) return
|
|
155
|
+
const lines = data.split('\n')
|
|
156
|
+
for (const line of lines) {
|
|
157
|
+
if (line && line !== '\r') {
|
|
158
|
+
process.stdout.write(`${getPrefix(idx)} ${line}\n`)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
terminal.onExit(({ exitCode }) => {
|
|
164
|
+
if (managed.killed) return
|
|
165
|
+
if (exitCode !== 0) {
|
|
166
|
+
process.stdout.write(`${getPrefix(idx)} exited with code ${exitCode}\n`)
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
return managed
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function computeShortcuts() {
|
|
174
|
+
const initials = processes.map((p) =>
|
|
175
|
+
p.label
|
|
176
|
+
.toLowerCase()
|
|
177
|
+
.split(/[^a-z]+/)
|
|
178
|
+
.filter(Boolean)
|
|
179
|
+
.map((w) => w[0])
|
|
180
|
+
.join('')
|
|
181
|
+
)
|
|
182
|
+
const lengths = new Array(processes.length).fill(1)
|
|
183
|
+
|
|
184
|
+
for (let round = 0; round < 5; round++) {
|
|
185
|
+
const shortcuts = initials.map((init, i) => init.slice(0, lengths[i]) || init)
|
|
186
|
+
const groups = new Map()
|
|
187
|
+
shortcuts.forEach((s, i) => groups.set(s, [...(groups.get(s) || []), i]))
|
|
188
|
+
|
|
189
|
+
let collision = false
|
|
190
|
+
for (const indices of groups.values()) {
|
|
191
|
+
if (indices.length > 1) {
|
|
192
|
+
collision = true
|
|
193
|
+
indices.forEach((i) => lengths[i]++)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (!collision) {
|
|
197
|
+
processes.forEach((p, i) => (p.shortcut = shortcuts[i]))
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
processes.forEach(
|
|
202
|
+
(p, i) => (p.shortcut = initials[i]?.slice(0, lengths[i]) || String(i + 1))
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function renderDashboard() {
|
|
207
|
+
process.stdout.write('\x1b[2J\x1b[H')
|
|
208
|
+
process.stdout.write(
|
|
209
|
+
`${bold}Dashboard${reset} ${dim}(↑↓ navigate, r restart, k kill, esc back)${reset}\n\n`
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
for (let i = 0; i < processes.length; i++) {
|
|
213
|
+
const p = processes[i]
|
|
214
|
+
const prefix = i === selectedIndex ? `${inverse} > ${reset}` : ' '
|
|
215
|
+
const status = p.killed
|
|
216
|
+
? `${dim}(killed)${reset}`
|
|
217
|
+
: p.terminal.pid
|
|
218
|
+
? `${dim}(pid ${p.terminal.pid})${reset}`
|
|
219
|
+
: ''
|
|
220
|
+
process.stdout.write(`${prefix} ${getPrefix(i)} ${status}\n`)
|
|
221
|
+
}
|
|
222
|
+
process.stdout.write(`\n${dim}ctrl+c to exit${reset}\n`)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function exitDashboard() {
|
|
226
|
+
dashboardMode = false
|
|
227
|
+
process.stdout.write('\x1b[2J\x1b[H')
|
|
228
|
+
process.stdout.write(
|
|
229
|
+
`${dim} running ${processes.length} processes (ctrl+z dashboard, ctrl+c exit)${reset}\n\n`
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function handleInput(data) {
|
|
234
|
+
const str = data.toString()
|
|
235
|
+
|
|
236
|
+
if (str === '\x03') {
|
|
237
|
+
cleanup()
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (str === '\x1a') {
|
|
242
|
+
if (dashboardMode) {
|
|
243
|
+
exitDashboard()
|
|
244
|
+
} else {
|
|
245
|
+
dashboardMode = true
|
|
246
|
+
renderDashboard()
|
|
247
|
+
}
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (dashboardMode) {
|
|
252
|
+
if (str === '\x1b[A') {
|
|
253
|
+
selectedIndex = Math.max(0, selectedIndex - 1)
|
|
254
|
+
renderDashboard()
|
|
255
|
+
} else if (str === '\x1b[B') {
|
|
256
|
+
selectedIndex = Math.min(processes.length - 1, selectedIndex + 1)
|
|
257
|
+
renderDashboard()
|
|
258
|
+
} else if (str === 'r' || str === 'R') {
|
|
259
|
+
const p = processes[selectedIndex]
|
|
260
|
+
if (p) {
|
|
261
|
+
p.killed = true
|
|
262
|
+
p.terminal.kill()
|
|
263
|
+
setTimeout(() => {
|
|
264
|
+
spawnScript(p.name, p.cwd, p.label, p.extraArgs, p.index)
|
|
265
|
+
exitDashboard()
|
|
266
|
+
}, 100)
|
|
267
|
+
}
|
|
268
|
+
} else if (str === 'k' || str === 'K') {
|
|
269
|
+
const p = processes[selectedIndex]
|
|
270
|
+
if (p && !p.killed) {
|
|
271
|
+
p.killed = true
|
|
272
|
+
p.terminal.kill()
|
|
273
|
+
renderDashboard()
|
|
274
|
+
}
|
|
275
|
+
} else if (str === '\x1b' || str === '\x1b\x1b') {
|
|
276
|
+
exitDashboard()
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function cleanup() {
|
|
282
|
+
process.stdout.write('\n')
|
|
283
|
+
for (const p of processes) {
|
|
284
|
+
if (!p.killed) {
|
|
285
|
+
p.killed = true
|
|
286
|
+
p.terminal.kill()
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
process.exit(0)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function main() {
|
|
293
|
+
const lastScript = runCommands[runCommands.length - 1]
|
|
294
|
+
|
|
295
|
+
if (!noRoot) {
|
|
296
|
+
const pkg = await readPackageJson('.')
|
|
297
|
+
if (pkg?.scripts) {
|
|
298
|
+
for (const name of runCommands) {
|
|
299
|
+
if (typeof pkg.scripts[name] === 'string') {
|
|
300
|
+
const scriptArgs = !flagsLast || name === lastScript ? forwardArgs : []
|
|
301
|
+
spawnScript(name, '.', name, scriptArgs)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const wsScripts = await findWorkspaceScripts(runCommands)
|
|
308
|
+
for (const [dir, { scripts, name }] of wsScripts) {
|
|
309
|
+
for (const script of scripts) {
|
|
310
|
+
const scriptArgs = !flagsLast || script === lastScript ? forwardArgs : []
|
|
311
|
+
spawnScript(script, dir, `${name} ${script}`, scriptArgs)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (processes.length === 0) {
|
|
316
|
+
console.error('No scripts found')
|
|
317
|
+
process.exit(1)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
computeShortcuts()
|
|
321
|
+
process.stdout.write(
|
|
322
|
+
`${dim} running ${processes.length} processes (ctrl+z dashboard, ctrl+c exit)${reset}\n\n`
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
if (process.stdin.isTTY) {
|
|
326
|
+
process.stdin.setRawMode(true)
|
|
327
|
+
process.stdin.resume()
|
|
328
|
+
process.stdin.on('data', handleInput)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
process.stdout.on('resize', () => {
|
|
332
|
+
const cols = process.stdout.columns || 80
|
|
333
|
+
const rows = process.stdout.rows || 24
|
|
334
|
+
for (const p of processes) {
|
|
335
|
+
if (!p.killed) {
|
|
336
|
+
p.terminal.resize(cols, rows)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
process.on('SIGINT', cleanup)
|
|
342
|
+
process.on('SIGTERM', cleanup)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
main().catch((e) => {
|
|
346
|
+
console.error(e)
|
|
347
|
+
process.exit(1)
|
|
348
|
+
})
|