@take-out/scripts 0.1.19 → 0.1.21
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 +361 -0
- package/src/run.ts +44 -316
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@take-out/scripts",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
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.21",
|
|
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,361 @@
|
|
|
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;81m',
|
|
10
|
+
'\x1b[38;5;209m',
|
|
11
|
+
'\x1b[38;5;156m',
|
|
12
|
+
'\x1b[38;5;183m',
|
|
13
|
+
'\x1b[38;5;222m',
|
|
14
|
+
'\x1b[38;5;117m',
|
|
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 focusedIndex = -1 // -1 = interleaved/dashboard
|
|
47
|
+
|
|
48
|
+
function getPrefix(index) {
|
|
49
|
+
const p = processes[index]
|
|
50
|
+
if (!p) return ''
|
|
51
|
+
const color = colors[index % colors.length]
|
|
52
|
+
return `${color}${p.shortcut}${reset}`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (runCommands.length === 0) {
|
|
56
|
+
console.error('Usage: run-pty <script1> [script2] ...')
|
|
57
|
+
process.exit(1)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function readPackageJson(dir) {
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(await fs.promises.readFile(join(dir, 'package.json'), 'utf8'))
|
|
63
|
+
} catch {
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function getWorkspacePatterns() {
|
|
69
|
+
const pkg = await readPackageJson('.')
|
|
70
|
+
if (!pkg?.workspaces) return []
|
|
71
|
+
return Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages || []
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function findPackageJsonDirs(base, depth = 3) {
|
|
75
|
+
if (depth <= 0) return []
|
|
76
|
+
const results = []
|
|
77
|
+
try {
|
|
78
|
+
if (
|
|
79
|
+
await fs.promises
|
|
80
|
+
.access(join(base, 'package.json'))
|
|
81
|
+
.then(() => true)
|
|
82
|
+
.catch(() => false)
|
|
83
|
+
) {
|
|
84
|
+
results.push(base)
|
|
85
|
+
}
|
|
86
|
+
const entries = await fs.promises.readdir(base, { withFileTypes: true })
|
|
87
|
+
for (const e of entries) {
|
|
88
|
+
if (e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules') {
|
|
89
|
+
results.push(...(await findPackageJsonDirs(join(base, e.name), depth - 1)))
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch {}
|
|
93
|
+
return results
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function findWorkspaceScripts(scripts) {
|
|
97
|
+
const patterns = await getWorkspacePatterns()
|
|
98
|
+
if (!patterns.length) return new Map()
|
|
99
|
+
const dirs = await findPackageJsonDirs('.')
|
|
100
|
+
const result = new Map()
|
|
101
|
+
for (const dir of dirs) {
|
|
102
|
+
if (dir === '.') continue
|
|
103
|
+
const rel = relative('.', dir).replace(/\\/g, '/')
|
|
104
|
+
const matches = patterns.some((p) => {
|
|
105
|
+
const np = p.replace(/\\/g, '/').replace(/^\.\//, '')
|
|
106
|
+
return np.endsWith('/*')
|
|
107
|
+
? rel.startsWith(np.slice(0, -1))
|
|
108
|
+
: rel === np || rel.startsWith(np + '/')
|
|
109
|
+
})
|
|
110
|
+
if (!matches) continue
|
|
111
|
+
const pkg = await readPackageJson(dir)
|
|
112
|
+
if (!pkg?.scripts) continue
|
|
113
|
+
const available = scripts.filter((s) => typeof pkg.scripts[s] === 'string')
|
|
114
|
+
if (available.length) result.set(dir, { scripts: available, name: pkg.name || dir })
|
|
115
|
+
}
|
|
116
|
+
return result
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function spawnScript(name, cwd, label, extraArgs, index) {
|
|
120
|
+
const runArgs = ['run', '--silent', runBun ? '--bun' : '', name, ...extraArgs].filter(
|
|
121
|
+
Boolean
|
|
122
|
+
)
|
|
123
|
+
const terminal = pty.spawn('bun', runArgs, {
|
|
124
|
+
cwd: resolve(cwd),
|
|
125
|
+
cols: process.stdout.columns || 80,
|
|
126
|
+
rows: process.stdout.rows || 24,
|
|
127
|
+
env: { ...process.env, FORCE_COLOR: '3' },
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const idx = index ?? processes.length
|
|
131
|
+
const managed = {
|
|
132
|
+
terminal,
|
|
133
|
+
name,
|
|
134
|
+
cwd,
|
|
135
|
+
label,
|
|
136
|
+
extraArgs,
|
|
137
|
+
index: idx,
|
|
138
|
+
shortcut: '',
|
|
139
|
+
killed: false,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (index !== undefined) processes[index] = managed
|
|
143
|
+
else processes.push(managed)
|
|
144
|
+
|
|
145
|
+
terminal.onData((data) => {
|
|
146
|
+
if (focusedIndex === -1) {
|
|
147
|
+
// interleaved - prefix each line, skip bun's $ command echo
|
|
148
|
+
const lines = data.split(/\r?\n/)
|
|
149
|
+
for (const line of lines) {
|
|
150
|
+
if (!line) continue
|
|
151
|
+
// eslint-disable-next-line no-control-regex
|
|
152
|
+
const stripped = line.replace(/\x1b\[[0-9;]*m/g, '')
|
|
153
|
+
if (stripped.startsWith('$ ')) continue
|
|
154
|
+
process.stdout.write(`${getPrefix(idx)} ${line}\n`)
|
|
155
|
+
}
|
|
156
|
+
} else if (focusedIndex === idx) {
|
|
157
|
+
// focused - raw output
|
|
158
|
+
process.stdout.write(data)
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
terminal.onExit(({ exitCode }) => {
|
|
163
|
+
if (managed.killed) return
|
|
164
|
+
if (focusedIndex === idx) {
|
|
165
|
+
focusedIndex = -1
|
|
166
|
+
showDashboard()
|
|
167
|
+
}
|
|
168
|
+
if (exitCode !== 0) {
|
|
169
|
+
console.error(`${getPrefix(idx)} exited ${exitCode}`)
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
return managed
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function computeShortcuts() {
|
|
177
|
+
// use last word of label
|
|
178
|
+
for (let i = 0; i < processes.length; i++) {
|
|
179
|
+
const p = processes[i]
|
|
180
|
+
const words = p.label
|
|
181
|
+
.toLowerCase()
|
|
182
|
+
.split(/[^a-z]+/)
|
|
183
|
+
.filter(Boolean)
|
|
184
|
+
const lastWord = words[words.length - 1] || words[0] || String(i)
|
|
185
|
+
|
|
186
|
+
// find unique shortcut
|
|
187
|
+
let shortcut = lastWord[0]
|
|
188
|
+
let len = 1
|
|
189
|
+
while (
|
|
190
|
+
processes.slice(0, i).some((q) => q.shortcut === shortcut) &&
|
|
191
|
+
len < lastWord.length
|
|
192
|
+
) {
|
|
193
|
+
len++
|
|
194
|
+
shortcut = lastWord.slice(0, len)
|
|
195
|
+
}
|
|
196
|
+
p.shortcut = shortcut
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function showDashboard() {
|
|
201
|
+
const tabs = processes
|
|
202
|
+
.map((p, i) => {
|
|
203
|
+
const color = colors[i % colors.length]
|
|
204
|
+
return `${color}[${p.shortcut}]${reset} ${p.label.split(' ').pop()}${p.killed ? dim + ' ✗' + reset : ''}`
|
|
205
|
+
})
|
|
206
|
+
.join(' ')
|
|
207
|
+
console.log(`\n${tabs}`)
|
|
208
|
+
console.log(
|
|
209
|
+
`${dim}press shortcut to focus, r+shortcut restart, k+shortcut kill, ctrl+c exit${reset}\n`
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let pendingAction = null // 'r' or 'k'
|
|
214
|
+
|
|
215
|
+
function handleInput(data) {
|
|
216
|
+
const str = data.toString()
|
|
217
|
+
|
|
218
|
+
// ctrl+c exit
|
|
219
|
+
if (str === '\x03') {
|
|
220
|
+
cleanup()
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ctrl+z toggle focus
|
|
225
|
+
if (str === '\x1a') {
|
|
226
|
+
if (focusedIndex >= 0) {
|
|
227
|
+
focusedIndex = -1
|
|
228
|
+
showDashboard()
|
|
229
|
+
} else {
|
|
230
|
+
showDashboard()
|
|
231
|
+
}
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// if focused, forward to process
|
|
236
|
+
if (focusedIndex >= 0) {
|
|
237
|
+
const p = processes[focusedIndex]
|
|
238
|
+
if (p && !p.killed) {
|
|
239
|
+
p.terminal.write(str)
|
|
240
|
+
}
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// dashboard mode
|
|
245
|
+
if (str === 'r') {
|
|
246
|
+
pendingAction = 'restart'
|
|
247
|
+
process.stdout.write(`${dim}restart which? ${reset}`)
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
if (str === 'k') {
|
|
251
|
+
pendingAction = 'kill'
|
|
252
|
+
process.stdout.write(`${dim}kill which? ${reset}`)
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// check for shortcut match
|
|
257
|
+
const match = processes.find((p) => p.shortcut === str.toLowerCase())
|
|
258
|
+
if (match) {
|
|
259
|
+
if (pendingAction === 'restart') {
|
|
260
|
+
pendingAction = null
|
|
261
|
+
console.log(match.shortcut)
|
|
262
|
+
match.killed = true
|
|
263
|
+
match.terminal.kill()
|
|
264
|
+
setTimeout(() => {
|
|
265
|
+
spawnScript(match.name, match.cwd, match.label, match.extraArgs, match.index)
|
|
266
|
+
console.log(`${getPrefix(match.index)} restarted`)
|
|
267
|
+
}, 100)
|
|
268
|
+
} else if (pendingAction === 'kill') {
|
|
269
|
+
pendingAction = null
|
|
270
|
+
console.log(match.shortcut)
|
|
271
|
+
if (!match.killed) {
|
|
272
|
+
match.killed = true
|
|
273
|
+
match.terminal.kill()
|
|
274
|
+
console.log(`${getPrefix(match.index)} killed`)
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
// focus
|
|
278
|
+
focusedIndex = match.index
|
|
279
|
+
console.log(`${dim}focused: ${match.label} (ctrl+z to unfocus)${reset}\n`)
|
|
280
|
+
}
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// escape cancels pending
|
|
285
|
+
if (str === '\x1b' && pendingAction) {
|
|
286
|
+
pendingAction = null
|
|
287
|
+
console.log('cancelled')
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function cleanup() {
|
|
292
|
+
console.log()
|
|
293
|
+
for (const p of processes) {
|
|
294
|
+
if (!p.killed) {
|
|
295
|
+
p.killed = true
|
|
296
|
+
p.terminal.kill()
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
process.exit(0)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function main() {
|
|
303
|
+
const lastScript = runCommands[runCommands.length - 1]
|
|
304
|
+
|
|
305
|
+
if (!noRoot) {
|
|
306
|
+
const pkg = await readPackageJson('.')
|
|
307
|
+
if (pkg?.scripts) {
|
|
308
|
+
for (const name of runCommands) {
|
|
309
|
+
if (typeof pkg.scripts[name] === 'string') {
|
|
310
|
+
spawnScript(
|
|
311
|
+
name,
|
|
312
|
+
'.',
|
|
313
|
+
name,
|
|
314
|
+
!flagsLast || name === lastScript ? forwardArgs : []
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const wsScripts = await findWorkspaceScripts(runCommands)
|
|
322
|
+
for (const [dir, { scripts, name }] of wsScripts) {
|
|
323
|
+
for (const script of scripts) {
|
|
324
|
+
spawnScript(
|
|
325
|
+
script,
|
|
326
|
+
dir,
|
|
327
|
+
`${name} ${script}`,
|
|
328
|
+
!flagsLast || script === lastScript ? forwardArgs : []
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (processes.length === 0) {
|
|
334
|
+
console.error('No scripts found')
|
|
335
|
+
process.exit(1)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
computeShortcuts()
|
|
339
|
+
showDashboard()
|
|
340
|
+
|
|
341
|
+
if (process.stdin.isTTY) {
|
|
342
|
+
process.stdin.setRawMode(true)
|
|
343
|
+
process.stdin.resume()
|
|
344
|
+
process.stdin.on('data', handleInput)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
process.stdout.on('resize', () => {
|
|
348
|
+
for (const p of processes) {
|
|
349
|
+
if (!p.killed)
|
|
350
|
+
p.terminal.resize(process.stdout.columns || 80, process.stdout.rows || 24)
|
|
351
|
+
}
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
process.on('SIGINT', cleanup)
|
|
355
|
+
process.on('SIGTERM', cleanup)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
main().catch((e) => {
|
|
359
|
+
console.error(e)
|
|
360
|
+
process.exit(1)
|
|
361
|
+
})
|
package/src/run.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @description Run multiple scripts in parallel
|
|
4
|
+
* @description Run multiple scripts in parallel
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { spawn } from 'node:child_process'
|
|
@@ -13,15 +13,14 @@ import { handleProcessExit } from '@take-out/scripts/helpers/handleProcessExit'
|
|
|
13
13
|
import { getIsExiting } from './helpers/run'
|
|
14
14
|
import { checkNodeVersion } from './node-version-check'
|
|
15
15
|
|
|
16
|
-
// 256-color grays for subtle differentiation (232=darkest, 255=lightest)
|
|
17
16
|
const colors = [
|
|
18
|
-
'\x1b[38;5;245m',
|
|
19
|
-
'\x1b[38;5;240m',
|
|
20
|
-
'\x1b[38;5;250m',
|
|
21
|
-
'\x1b[38;5;243m',
|
|
22
|
-
'\x1b[38;5;248m',
|
|
23
|
-
'\x1b[38;5;238m',
|
|
24
|
-
'\x1b[38;5;252m',
|
|
17
|
+
'\x1b[38;5;245m',
|
|
18
|
+
'\x1b[38;5;240m',
|
|
19
|
+
'\x1b[38;5;250m',
|
|
20
|
+
'\x1b[38;5;243m',
|
|
21
|
+
'\x1b[38;5;248m',
|
|
22
|
+
'\x1b[38;5;238m',
|
|
23
|
+
'\x1b[38;5;252m',
|
|
25
24
|
]
|
|
26
25
|
|
|
27
26
|
const reset = '\x1b[0m'
|
|
@@ -29,22 +28,6 @@ const reset = '\x1b[0m'
|
|
|
29
28
|
// eslint-disable-next-line no-control-regex
|
|
30
29
|
const ansiPattern = /\x1b\[[0-9;]*m/g
|
|
31
30
|
|
|
32
|
-
// Verbose logging flag - set to false to reduce logs
|
|
33
|
-
const verbose = false
|
|
34
|
-
|
|
35
|
-
// Helper function to conditionally log based on verbosity
|
|
36
|
-
const log = {
|
|
37
|
-
info: (message: string) => {
|
|
38
|
-
if (verbose) console.info(message)
|
|
39
|
-
},
|
|
40
|
-
error: (message: string) => console.error(message),
|
|
41
|
-
output: (message: string) => console.info(message),
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const MAX_RESTARTS = 3
|
|
45
|
-
|
|
46
|
-
// Separate command names from flags/arguments
|
|
47
|
-
// Handles --flag=value and --flag value styles, excluding flag values from commands
|
|
48
31
|
const args = process.argv.slice(2)
|
|
49
32
|
const ownFlags = ['--no-root', '--bun', '--watch', '--flags=last']
|
|
50
33
|
const runCommands: string[] = []
|
|
@@ -54,37 +37,25 @@ for (let i = 0; i < args.length; i++) {
|
|
|
54
37
|
const arg = args[i]!
|
|
55
38
|
|
|
56
39
|
if (arg.startsWith('--')) {
|
|
57
|
-
|
|
58
|
-
if (ownFlags.includes(arg) || arg.startsWith('--stdin=')) {
|
|
59
|
-
continue
|
|
60
|
-
}
|
|
40
|
+
if (ownFlags.includes(arg)) continue
|
|
61
41
|
forwardArgs.push(arg)
|
|
62
|
-
// if next arg exists and doesn't start with --, treat it as this flag's value
|
|
63
42
|
const nextArg = args[i + 1]
|
|
64
43
|
if (nextArg && !nextArg.startsWith('--')) {
|
|
65
44
|
forwardArgs.push(nextArg)
|
|
66
|
-
i++
|
|
45
|
+
i++
|
|
67
46
|
}
|
|
68
47
|
} else {
|
|
69
|
-
// non-flag arg is a command name
|
|
70
48
|
runCommands.push(arg)
|
|
71
49
|
}
|
|
72
50
|
}
|
|
73
51
|
|
|
74
52
|
const noRoot = args.includes('--no-root')
|
|
75
53
|
const runBun = args.includes('--bun')
|
|
76
|
-
const watch = args.includes('--watch')
|
|
77
|
-
// --flags=last forwards args only to last script, default forwards to all
|
|
54
|
+
const watch = args.includes('--watch')
|
|
78
55
|
const flagsLast = args.includes('--flags=last')
|
|
79
56
|
|
|
80
|
-
|
|
81
|
-
// if not specified, defaults to the last script in the list
|
|
82
|
-
const stdinArg = args.find((arg) => arg.startsWith('--stdin='))
|
|
83
|
-
const stdinScript = stdinArg
|
|
84
|
-
? stdinArg.replace('--stdin=', '')
|
|
85
|
-
: (runCommands[runCommands.length - 1] ?? null)
|
|
57
|
+
const MAX_RESTARTS = 3
|
|
86
58
|
|
|
87
|
-
// Get the list of scripts already being run by a parent process
|
|
88
59
|
const parentRunningScripts = process.env.BUN_RUN_SCRIPTS
|
|
89
60
|
? process.env.BUN_RUN_SCRIPTS.split(',')
|
|
90
61
|
: []
|
|
@@ -97,14 +68,11 @@ interface ManagedProcess {
|
|
|
97
68
|
extraArgs: string[]
|
|
98
69
|
index: number
|
|
99
70
|
shortcut: string
|
|
100
|
-
restarting: boolean
|
|
101
|
-
killing: boolean
|
|
102
71
|
}
|
|
103
72
|
|
|
104
73
|
const managedProcesses: ManagedProcess[] = []
|
|
105
74
|
const { addChildProcess, exit } = handleProcessExit()
|
|
106
75
|
|
|
107
|
-
// dynamic prefix using shortcut letter(s) — falls back to index before shortcuts are computed
|
|
108
76
|
function getPrefix(index: number): string {
|
|
109
77
|
const managed = managedProcesses[index]
|
|
110
78
|
if (!managed) return ''
|
|
@@ -114,8 +82,7 @@ function getPrefix(index: number): string {
|
|
|
114
82
|
}
|
|
115
83
|
|
|
116
84
|
if (runCommands.length === 0) {
|
|
117
|
-
|
|
118
|
-
log.error('Example: bun run.ts watch lint test')
|
|
85
|
+
console.error('Please provide at least one script name to run')
|
|
119
86
|
exit(1)
|
|
120
87
|
}
|
|
121
88
|
|
|
@@ -124,7 +91,7 @@ async function readPackageJson(directoryPath: string) {
|
|
|
124
91
|
const packageJsonPath = join(directoryPath, 'package.json')
|
|
125
92
|
const content = await fs.promises.readFile(packageJsonPath, 'utf8')
|
|
126
93
|
return JSON.parse(content)
|
|
127
|
-
} catch
|
|
94
|
+
} catch {
|
|
128
95
|
return null
|
|
129
96
|
}
|
|
130
97
|
}
|
|
@@ -137,8 +104,7 @@ async function getWorkspacePatterns(): Promise<string[]> {
|
|
|
137
104
|
return Array.isArray(packageJson.workspaces)
|
|
138
105
|
? packageJson.workspaces
|
|
139
106
|
: packageJson.workspaces.packages || []
|
|
140
|
-
} catch
|
|
141
|
-
log.error('Error reading workspace patterns')
|
|
107
|
+
} catch {
|
|
142
108
|
return []
|
|
143
109
|
}
|
|
144
110
|
}
|
|
@@ -172,14 +138,12 @@ async function findPackageJsonDirs(basePath: string, maxDepth = 3): Promise<stri
|
|
|
172
138
|
)
|
|
173
139
|
.map(async (dir) => {
|
|
174
140
|
const path = join(basePath, dir.name)
|
|
175
|
-
|
|
176
|
-
return subdirResults
|
|
141
|
+
return findPackageJsonDirs(path, maxDepth - 1)
|
|
177
142
|
})
|
|
178
143
|
|
|
179
144
|
const subdirResults = await Promise.all(subDirPromises)
|
|
180
145
|
return [...results, ...subdirResults.flat()]
|
|
181
|
-
} catch
|
|
182
|
-
log.error(`Error scanning directory ${basePath}: ${error}`)
|
|
146
|
+
} catch {
|
|
183
147
|
return []
|
|
184
148
|
}
|
|
185
149
|
}
|
|
@@ -190,13 +154,12 @@ async function findWorkspaceDirectories(): Promise<string[]> {
|
|
|
190
154
|
|
|
191
155
|
const allPackageDirs = await findPackageJsonDirs('.')
|
|
192
156
|
|
|
193
|
-
// normalize path separators to forward slashes for cross-platform support
|
|
194
157
|
const normalizePath = (path: string): string => {
|
|
195
|
-
|
|
158
|
+
const normalized = path.replace(/\\/g, '/')
|
|
196
159
|
return normalized.startsWith('./') ? normalized.substring(2) : normalized
|
|
197
160
|
}
|
|
198
161
|
|
|
199
|
-
|
|
162
|
+
return allPackageDirs.filter((dir) => {
|
|
200
163
|
if (dir === '.') return false
|
|
201
164
|
|
|
202
165
|
const relativePath = relative('.', dir)
|
|
@@ -214,8 +177,6 @@ async function findWorkspaceDirectories(): Promise<string[]> {
|
|
|
214
177
|
)
|
|
215
178
|
})
|
|
216
179
|
})
|
|
217
|
-
|
|
218
|
-
return workspaceDirs
|
|
219
180
|
}
|
|
220
181
|
|
|
221
182
|
async function findAvailableScripts(
|
|
@@ -265,22 +226,14 @@ const runScript = async (
|
|
|
265
226
|
) => {
|
|
266
227
|
const index = managedIndex ?? managedProcesses.length
|
|
267
228
|
|
|
268
|
-
// capture stderr for error reporting
|
|
269
|
-
let stderrBuffer = ''
|
|
270
|
-
|
|
271
|
-
// --silent suppresses bun's "$ command" output
|
|
272
229
|
const runArgs = ['run', '--silent', runBun ? '--bun' : '', name, ...extraArgs].filter(
|
|
273
230
|
Boolean
|
|
274
231
|
)
|
|
275
232
|
|
|
276
|
-
const commandDisplay = `bun ${runArgs.join(' ')}`
|
|
277
|
-
log.info(`${getPrefix(index)} Running: ${commandDisplay} (in ${resolve(cwd)})`)
|
|
278
|
-
|
|
279
233
|
const allRunningScripts = [...parentRunningScripts, ...runCommands].join(',')
|
|
280
234
|
|
|
281
|
-
// always pipe stdin - parent handles keyboard shortcuts and forwarding
|
|
282
235
|
const proc = spawn('bun', runArgs, {
|
|
283
|
-
stdio: ['
|
|
236
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
284
237
|
shell: false,
|
|
285
238
|
env: {
|
|
286
239
|
...process.env,
|
|
@@ -290,11 +243,8 @@ const runScript = async (
|
|
|
290
243
|
TKO_SILENT: '1',
|
|
291
244
|
} as any,
|
|
292
245
|
cwd: resolve(cwd),
|
|
293
|
-
detached: true,
|
|
294
246
|
})
|
|
295
247
|
|
|
296
|
-
log.info(`${getPrefix(index)} Process started with PID: ${proc.pid}`)
|
|
297
|
-
|
|
298
248
|
const managed: ManagedProcess = {
|
|
299
249
|
proc,
|
|
300
250
|
name,
|
|
@@ -303,8 +253,6 @@ const runScript = async (
|
|
|
303
253
|
extraArgs,
|
|
304
254
|
index,
|
|
305
255
|
shortcut: '',
|
|
306
|
-
restarting: false,
|
|
307
|
-
killing: false,
|
|
308
256
|
}
|
|
309
257
|
|
|
310
258
|
if (managedIndex !== undefined) {
|
|
@@ -321,52 +269,36 @@ const runScript = async (
|
|
|
321
269
|
for (const line of lines) {
|
|
322
270
|
const stripped = line.replace(ansiPattern, '')
|
|
323
271
|
if (stripped.startsWith('$ ')) continue
|
|
324
|
-
if (line)
|
|
272
|
+
if (line) console.info(`${getPrefix(index)} ${line}`)
|
|
325
273
|
}
|
|
326
274
|
})
|
|
327
275
|
|
|
328
276
|
proc.stderr!.on('data', (data) => {
|
|
329
|
-
const dataStr = data.toString()
|
|
330
|
-
stderrBuffer += dataStr
|
|
331
|
-
|
|
332
277
|
if (getIsExiting()) return
|
|
333
|
-
const lines =
|
|
278
|
+
const lines = data.toString().split('\n')
|
|
334
279
|
for (const line of lines) {
|
|
335
280
|
const stripped = line.replace(ansiPattern, '')
|
|
336
281
|
if (stripped.startsWith('$ ')) continue
|
|
337
|
-
if (line)
|
|
282
|
+
if (line) console.error(`${getPrefix(index)} ${line}`)
|
|
338
283
|
}
|
|
339
284
|
})
|
|
340
285
|
|
|
341
286
|
proc.on('error', (error) => {
|
|
342
|
-
|
|
287
|
+
console.error(`${getPrefix(index)} Failed to start: ${error.message}`)
|
|
343
288
|
})
|
|
344
289
|
|
|
345
290
|
proc.on('close', (code) => {
|
|
346
291
|
if (getIsExiting()) return
|
|
347
292
|
|
|
348
|
-
// intentionally killed or restarting - skip error handling
|
|
349
|
-
const currentManaged = managedProcesses[index]
|
|
350
|
-
if (currentManaged?.restarting || currentManaged?.killing) return
|
|
351
|
-
|
|
352
293
|
if (code && code !== 0) {
|
|
353
|
-
|
|
294
|
+
console.error(`${getPrefix(index)} Process exited with code ${code}`)
|
|
354
295
|
|
|
355
|
-
if (
|
|
356
|
-
|
|
357
|
-
console.
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
if (watch && restarts < MAX_RESTARTS) {
|
|
362
|
-
const newRestarts = restarts + 1
|
|
363
|
-
console.info(
|
|
364
|
-
`Restarting process ${name} (${newRestarts}/${MAX_RESTARTS} times)`
|
|
365
|
-
)
|
|
366
|
-
runScript(name, cwd, prefixLabel, newRestarts, extraArgs, index)
|
|
367
|
-
} else {
|
|
368
|
-
exit(1)
|
|
369
|
-
}
|
|
296
|
+
if (watch && restarts < MAX_RESTARTS) {
|
|
297
|
+
const newRestarts = restarts + 1
|
|
298
|
+
console.info(`Restarting process ${name} (${newRestarts}/${MAX_RESTARTS} times)`)
|
|
299
|
+
runScript(name, cwd, prefixLabel, newRestarts, extraArgs, index)
|
|
300
|
+
} else {
|
|
301
|
+
exit(1)
|
|
370
302
|
}
|
|
371
303
|
}
|
|
372
304
|
})
|
|
@@ -374,8 +306,6 @@ const runScript = async (
|
|
|
374
306
|
return proc
|
|
375
307
|
}
|
|
376
308
|
|
|
377
|
-
// compute unique letter-based shortcuts from process labels
|
|
378
|
-
// splits on non-letters, takes first char of each word, extends until unique
|
|
379
309
|
function computeShortcuts() {
|
|
380
310
|
const initials = managedProcesses.map((p) => {
|
|
381
311
|
const words = p.prefixLabel
|
|
@@ -385,7 +315,6 @@ function computeShortcuts() {
|
|
|
385
315
|
return words.map((w) => w[0]).join('')
|
|
386
316
|
})
|
|
387
317
|
|
|
388
|
-
// start each shortcut at 1 letter, extend collisions
|
|
389
318
|
const lengths = new Array(managedProcesses.length).fill(1) as number[]
|
|
390
319
|
|
|
391
320
|
for (let round = 0; round < 5; round++) {
|
|
@@ -415,217 +344,15 @@ function computeShortcuts() {
|
|
|
415
344
|
}
|
|
416
345
|
}
|
|
417
346
|
|
|
418
|
-
// fallback: use whatever we have, append index if still colliding
|
|
419
347
|
for (let i = 0; i < managedProcesses.length; i++) {
|
|
420
348
|
const sc = initials[i]!.slice(0, lengths[i]) || initials[i]!
|
|
421
349
|
managedProcesses[i]!.shortcut = sc || String(i + 1)
|
|
422
350
|
}
|
|
423
351
|
}
|
|
424
352
|
|
|
425
|
-
async function killProcessGroup(managed: ManagedProcess) {
|
|
426
|
-
if (managed.proc.pid) {
|
|
427
|
-
try {
|
|
428
|
-
process.kill(-managed.proc.pid, 'SIGTERM')
|
|
429
|
-
} catch {}
|
|
430
|
-
await new Promise((r) => setTimeout(r, 200))
|
|
431
|
-
try {
|
|
432
|
-
process.kill(-managed.proc.pid, 'SIGKILL')
|
|
433
|
-
} catch {}
|
|
434
|
-
}
|
|
435
|
-
await new Promise((r) => setTimeout(r, 100))
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
async function restartProcess(index: number) {
|
|
439
|
-
const managed = managedProcesses[index]
|
|
440
|
-
if (!managed) return
|
|
441
|
-
|
|
442
|
-
const { name, cwd, prefixLabel, extraArgs } = managed
|
|
443
|
-
|
|
444
|
-
managed.restarting = true
|
|
445
|
-
managed.killing = false
|
|
446
|
-
console.info(`\x1b[2m restarting ${managed.shortcut} ${prefixLabel}...\x1b[0m`)
|
|
447
|
-
|
|
448
|
-
await killProcessGroup(managed)
|
|
449
|
-
await runScript(name, cwd, prefixLabel, 0, extraArgs, index)
|
|
450
|
-
console.info(`${getPrefix(index)} \x1b[32m↻ restarted\x1b[0m`)
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
async function killProcess(index: number) {
|
|
454
|
-
const managed = managedProcesses[index]
|
|
455
|
-
if (!managed) return
|
|
456
|
-
|
|
457
|
-
if (managed.killing) {
|
|
458
|
-
console.info(
|
|
459
|
-
`\x1b[2m ${managed.shortcut} ${managed.prefixLabel} already stopped\x1b[0m`
|
|
460
|
-
)
|
|
461
|
-
return
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
managed.killing = true
|
|
465
|
-
managed.restarting = false
|
|
466
|
-
console.info(`\x1b[2m killing ${managed.shortcut} ${managed.prefixLabel}...\x1b[0m`)
|
|
467
|
-
|
|
468
|
-
await killProcessGroup(managed)
|
|
469
|
-
console.info(`${getPrefix(index)} \x1b[31m■ stopped\x1b[0m`)
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
type InputMode = 'restart' | 'kill' | null
|
|
473
|
-
|
|
474
|
-
function setupKeyboardShortcuts() {
|
|
475
|
-
if (!process.stdin.isTTY) return
|
|
476
|
-
if (managedProcesses.length === 0) return
|
|
477
|
-
|
|
478
|
-
process.stdin.setRawMode(true)
|
|
479
|
-
process.stdin.resume()
|
|
480
|
-
process.stdin.setEncoding('utf8')
|
|
481
|
-
|
|
482
|
-
let mode: InputMode = null
|
|
483
|
-
let buffer = ''
|
|
484
|
-
let timer: ReturnType<typeof setTimeout> | null = null
|
|
485
|
-
|
|
486
|
-
function clearTimer() {
|
|
487
|
-
if (timer) {
|
|
488
|
-
clearTimeout(timer)
|
|
489
|
-
timer = null
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function dispatchMatch(m: InputMode, index: number) {
|
|
494
|
-
if (m === 'restart') restartProcess(index)
|
|
495
|
-
else if (m === 'kill') killProcess(index)
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
function finishMatch() {
|
|
499
|
-
clearTimer()
|
|
500
|
-
if (!buffer) return
|
|
501
|
-
|
|
502
|
-
const currentMode = mode
|
|
503
|
-
const match = managedProcesses.find((p) => p.shortcut === buffer)
|
|
504
|
-
if (match) {
|
|
505
|
-
dispatchMatch(currentMode, match.index)
|
|
506
|
-
} else {
|
|
507
|
-
console.info(`\x1b[2m no match for "${buffer}"\x1b[0m`)
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
buffer = ''
|
|
511
|
-
mode = null
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
function showProcessList(label: string) {
|
|
515
|
-
const dim = '\x1b[2m'
|
|
516
|
-
console.info()
|
|
517
|
-
console.info(`${dim} ${label} which process?${reset}`)
|
|
518
|
-
for (const managed of managedProcesses) {
|
|
519
|
-
const color = colors[managed.index % colors.length]
|
|
520
|
-
const stopped = managed.killing ? `${dim} (stopped)` : ''
|
|
521
|
-
console.info(
|
|
522
|
-
`${dim} ${reset}${color}${managed.shortcut}${reset}${dim} ${managed.prefixLabel}${stopped}${reset}`
|
|
523
|
-
)
|
|
524
|
-
}
|
|
525
|
-
console.info()
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
function enterMode(newMode: InputMode, label: string) {
|
|
529
|
-
clearTimer()
|
|
530
|
-
mode = newMode
|
|
531
|
-
buffer = ''
|
|
532
|
-
showProcessList(label)
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
process.stdin.on('data', (key: string) => {
|
|
536
|
-
// ctrl+c
|
|
537
|
-
if (key === '\x03') {
|
|
538
|
-
process.stdin.setRawMode(false)
|
|
539
|
-
exit(0)
|
|
540
|
-
return
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
// escape cancels
|
|
544
|
-
if (key === '\x1b' && mode) {
|
|
545
|
-
clearTimer()
|
|
546
|
-
buffer = ''
|
|
547
|
-
mode = null
|
|
548
|
-
console.info('\x1b[2m cancelled\x1b[0m')
|
|
549
|
-
return
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// ctrl+r - restart mode
|
|
553
|
-
if (key === '\x12') {
|
|
554
|
-
enterMode('restart', 'restart')
|
|
555
|
-
return
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// ctrl+k - kill mode
|
|
559
|
-
if (key === '\x0b') {
|
|
560
|
-
enterMode('kill', 'kill')
|
|
561
|
-
return
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// ctrl+l - clear screen
|
|
565
|
-
if (key === '\x0c') {
|
|
566
|
-
process.stdout.write('\x1b[2J\x1b[H')
|
|
567
|
-
return
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
if (mode) {
|
|
571
|
-
const lower = key.toLowerCase()
|
|
572
|
-
if (/^[a-z]$/.test(lower)) {
|
|
573
|
-
buffer += lower
|
|
574
|
-
clearTimer()
|
|
575
|
-
|
|
576
|
-
// exact match → dispatch immediately
|
|
577
|
-
const exact = managedProcesses.find((p) => p.shortcut === buffer)
|
|
578
|
-
if (exact) {
|
|
579
|
-
const m = mode
|
|
580
|
-
mode = null
|
|
581
|
-
buffer = ''
|
|
582
|
-
dispatchMatch(m, exact.index)
|
|
583
|
-
return
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
// no shortcuts start with buffer → no match
|
|
587
|
-
const hasPrefix = managedProcesses.some((p) => p.shortcut.startsWith(buffer))
|
|
588
|
-
if (!hasPrefix) {
|
|
589
|
-
console.info(`\x1b[2m no match for "${buffer}"\x1b[0m`)
|
|
590
|
-
buffer = ''
|
|
591
|
-
mode = null
|
|
592
|
-
return
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// ambiguous — wait 500ms for more input
|
|
596
|
-
timer = setTimeout(finishMatch, 500)
|
|
597
|
-
} else {
|
|
598
|
-
// non-letter cancels
|
|
599
|
-
clearTimer()
|
|
600
|
-
buffer = ''
|
|
601
|
-
mode = null
|
|
602
|
-
console.info('\x1b[2m cancelled\x1b[0m')
|
|
603
|
-
}
|
|
604
|
-
return
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// forward other input to the designated stdin process
|
|
608
|
-
const stdinProc = managedProcesses.find((p) => p.name === stdinScript)
|
|
609
|
-
if (stdinProc?.proc.stdin && !stdinProc.proc.stdin.destroyed) {
|
|
610
|
-
stdinProc.proc.stdin.write(key)
|
|
611
|
-
}
|
|
612
|
-
})
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
function printShortcutHint() {
|
|
616
|
-
if (!process.stdin.isTTY) return
|
|
617
|
-
if (managedProcesses.length === 0) return
|
|
618
|
-
|
|
619
|
-
const dim = '\x1b[2m'
|
|
620
|
-
console.info(
|
|
621
|
-
`${dim} ctrl+r restart · ctrl+k kill · ctrl+l clear · ctrl+c exit${reset}`
|
|
622
|
-
)
|
|
623
|
-
console.info()
|
|
624
|
-
}
|
|
625
|
-
|
|
626
353
|
async function main() {
|
|
627
354
|
checkNodeVersion().catch((err) => {
|
|
628
|
-
|
|
355
|
+
console.error(err.message)
|
|
629
356
|
exit(1)
|
|
630
357
|
})
|
|
631
358
|
|
|
@@ -633,15 +360,13 @@ async function main() {
|
|
|
633
360
|
if (runCommands.length > 0) {
|
|
634
361
|
const lastScript = runCommands[runCommands.length - 1]
|
|
635
362
|
|
|
636
|
-
// Root package.json scripts first, if not disabled
|
|
637
363
|
if (!noRoot) {
|
|
638
364
|
const filteredCommands = runCommands.filter(
|
|
639
365
|
(name) => !parentRunningScripts.includes(name)
|
|
640
366
|
)
|
|
641
367
|
const scriptPromises = filteredCommands.map((name) => {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
return runScript(name, '.', name, 0, args)
|
|
368
|
+
const scriptArgs = !flagsLast || name === lastScript ? forwardArgs : []
|
|
369
|
+
return runScript(name, '.', name, 0, scriptArgs)
|
|
645
370
|
})
|
|
646
371
|
|
|
647
372
|
await Promise.all(scriptPromises)
|
|
@@ -654,9 +379,14 @@ async function main() {
|
|
|
654
379
|
(scriptName) => !parentRunningScripts.includes(scriptName)
|
|
655
380
|
)
|
|
656
381
|
const workspaceScriptPromises = filteredScripts.map((scriptName) => {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
382
|
+
const scriptArgs = !flagsLast || scriptName === lastScript ? forwardArgs : []
|
|
383
|
+
return runScript(
|
|
384
|
+
scriptName,
|
|
385
|
+
workspace,
|
|
386
|
+
`${packageName} ${scriptName}`,
|
|
387
|
+
0,
|
|
388
|
+
scriptArgs
|
|
389
|
+
)
|
|
660
390
|
})
|
|
661
391
|
|
|
662
392
|
await Promise.all(workspaceScriptPromises)
|
|
@@ -667,16 +397,14 @@ async function main() {
|
|
|
667
397
|
exit(0)
|
|
668
398
|
} else {
|
|
669
399
|
computeShortcuts()
|
|
670
|
-
printShortcutHint()
|
|
671
|
-
setupKeyboardShortcuts()
|
|
672
400
|
}
|
|
673
401
|
} catch (error) {
|
|
674
|
-
|
|
402
|
+
console.error(`Error running scripts: ${error}`)
|
|
675
403
|
exit(1)
|
|
676
404
|
}
|
|
677
405
|
}
|
|
678
406
|
|
|
679
407
|
main().catch((error) => {
|
|
680
|
-
|
|
408
|
+
console.error(`Error running scripts: ${error}`)
|
|
681
409
|
exit(1)
|
|
682
410
|
})
|