@take-out/scripts 0.1.12 → 0.1.14
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 +4 -4
- package/src/run.ts +310 -31
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@take-out/scripts",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/run.ts",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -29,11 +29,11 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@clack/prompts": "^0.8.2",
|
|
32
|
-
"@take-out/helpers": "0.1.
|
|
32
|
+
"@take-out/helpers": "0.1.14",
|
|
33
33
|
"picocolors": "^1.1.1"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"vxrn": "^1.6.
|
|
36
|
+
"vxrn": "^1.6.13"
|
|
37
37
|
},
|
|
38
38
|
"peerDependenciesMeta": {
|
|
39
39
|
"vxrn": {
|
|
@@ -41,6 +41,6 @@
|
|
|
41
41
|
}
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"vxrn": "1.6.
|
|
44
|
+
"vxrn": "1.6.13"
|
|
45
45
|
}
|
|
46
46
|
}
|
package/src/run.ts
CHANGED
|
@@ -89,9 +89,30 @@ const parentRunningScripts = process.env.BUN_RUN_SCRIPTS
|
|
|
89
89
|
? process.env.BUN_RUN_SCRIPTS.split(',')
|
|
90
90
|
: []
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
interface ManagedProcess {
|
|
93
|
+
proc: ReturnType<typeof spawn>
|
|
94
|
+
name: string
|
|
95
|
+
cwd: string
|
|
96
|
+
prefixLabel: string
|
|
97
|
+
extraArgs: string[]
|
|
98
|
+
index: number
|
|
99
|
+
shortcut: string
|
|
100
|
+
restarting: boolean
|
|
101
|
+
killing: boolean
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const managedProcesses: ManagedProcess[] = []
|
|
93
105
|
const { addChildProcess, exit } = handleProcessExit()
|
|
94
106
|
|
|
107
|
+
// dynamic prefix using shortcut letter(s) — falls back to index before shortcuts are computed
|
|
108
|
+
function getPrefix(index: number): string {
|
|
109
|
+
const managed = managedProcesses[index]
|
|
110
|
+
if (!managed) return ''
|
|
111
|
+
const color = colors[index % colors.length]
|
|
112
|
+
const sc = managed.shortcut || String(index + 1)
|
|
113
|
+
return `${color}${sc} ${managed.prefixLabel}${reset}`
|
|
114
|
+
}
|
|
115
|
+
|
|
95
116
|
if (runCommands.length === 0) {
|
|
96
117
|
log.error('Please provide at least one script name to run')
|
|
97
118
|
log.error('Example: bun run.ts watch lint test')
|
|
@@ -239,59 +260,68 @@ const runScript = async (
|
|
|
239
260
|
cwd = '.',
|
|
240
261
|
prefixLabel: string = name,
|
|
241
262
|
restarts = 0,
|
|
242
|
-
extraArgs: string[] = []
|
|
263
|
+
extraArgs: string[] = [],
|
|
264
|
+
managedIndex?: number,
|
|
243
265
|
) => {
|
|
244
|
-
const
|
|
245
|
-
const color = colors[colorIndex]
|
|
266
|
+
const index = managedIndex ?? managedProcesses.length
|
|
246
267
|
|
|
247
|
-
//
|
|
268
|
+
// capture stderr for error reporting
|
|
248
269
|
let stderrBuffer = ''
|
|
249
270
|
|
|
250
|
-
// Construct command with arguments to forward
|
|
251
271
|
// --silent suppresses bun's "$ command" output
|
|
252
272
|
const runArgs = ['run', '--silent', runBun ? '--bun' : '', name, ...extraArgs].filter(
|
|
253
273
|
Boolean
|
|
254
274
|
)
|
|
255
275
|
|
|
256
|
-
// Log the exact command being run
|
|
257
276
|
const commandDisplay = `bun ${runArgs.join(' ')}`
|
|
277
|
+
log.info(`${getPrefix(index)} Running: ${commandDisplay} (in ${resolve(cwd)})`)
|
|
258
278
|
|
|
259
|
-
log.info(
|
|
260
|
-
`${color}${prefixLabel}${reset} Running: ${commandDisplay} (in ${resolve(cwd)})`
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
// Combine parent running scripts with current scripts to prevent recursion
|
|
264
279
|
const allRunningScripts = [...parentRunningScripts, ...runCommands].join(',')
|
|
265
280
|
|
|
266
|
-
|
|
281
|
+
// always pipe stdin - parent handles keyboard shortcuts and forwarding
|
|
267
282
|
const proc = spawn('bun', runArgs, {
|
|
268
|
-
stdio: [
|
|
283
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
269
284
|
shell: false,
|
|
270
285
|
env: {
|
|
271
286
|
...process.env,
|
|
272
287
|
FORCE_COLOR: '3',
|
|
273
288
|
BUN_RUN_PARENT_SCRIPT: name,
|
|
274
289
|
BUN_RUN_SCRIPTS: allRunningScripts,
|
|
275
|
-
// propagate silent mode to child scripts
|
|
276
290
|
TKO_SILENT: '1',
|
|
277
291
|
} as any,
|
|
278
292
|
cwd: resolve(cwd),
|
|
279
293
|
detached: true,
|
|
280
294
|
})
|
|
281
295
|
|
|
282
|
-
log.info(`${
|
|
296
|
+
log.info(`${getPrefix(index)} Process started with PID: ${proc.pid}`)
|
|
297
|
+
|
|
298
|
+
const managed: ManagedProcess = {
|
|
299
|
+
proc,
|
|
300
|
+
name,
|
|
301
|
+
cwd,
|
|
302
|
+
prefixLabel,
|
|
303
|
+
extraArgs,
|
|
304
|
+
index,
|
|
305
|
+
shortcut: '',
|
|
306
|
+
restarting: false,
|
|
307
|
+
killing: false,
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (managedIndex !== undefined) {
|
|
311
|
+
managedProcesses[managedIndex] = managed
|
|
312
|
+
} else {
|
|
313
|
+
managedProcesses.push(managed)
|
|
314
|
+
}
|
|
283
315
|
|
|
284
|
-
processes.push(proc)
|
|
285
316
|
addChildProcess(proc)
|
|
286
317
|
|
|
287
318
|
proc.stdout!.on('data', (data) => {
|
|
288
|
-
if (getIsExiting()) return
|
|
319
|
+
if (getIsExiting()) return
|
|
289
320
|
const lines = data.toString().split('\n')
|
|
290
321
|
for (const line of lines) {
|
|
291
|
-
// filter out bun's "$ command" echo lines in nested scripts
|
|
292
322
|
const stripped = line.replace(ansiPattern, '')
|
|
293
323
|
if (stripped.startsWith('$ ')) continue
|
|
294
|
-
if (line) log.output(`${
|
|
324
|
+
if (line) log.output(`${getPrefix(index)} ${line}`)
|
|
295
325
|
}
|
|
296
326
|
})
|
|
297
327
|
|
|
@@ -299,28 +329,28 @@ const runScript = async (
|
|
|
299
329
|
const dataStr = data.toString()
|
|
300
330
|
stderrBuffer += dataStr
|
|
301
331
|
|
|
302
|
-
if (getIsExiting()) return
|
|
332
|
+
if (getIsExiting()) return
|
|
303
333
|
const lines = dataStr.split('\n')
|
|
304
334
|
for (const line of lines) {
|
|
305
|
-
// filter out bun's "$ command" echo lines in nested scripts
|
|
306
335
|
const stripped = line.replace(ansiPattern, '')
|
|
307
336
|
if (stripped.startsWith('$ ')) continue
|
|
308
|
-
if (line) log.error(`${
|
|
337
|
+
if (line) log.error(`${getPrefix(index)} ${line}`)
|
|
309
338
|
}
|
|
310
339
|
})
|
|
311
340
|
|
|
312
341
|
proc.on('error', (error) => {
|
|
313
|
-
log.error(`${
|
|
342
|
+
log.error(`${getPrefix(index)} Failed to start: ${error.message}`)
|
|
314
343
|
})
|
|
315
344
|
|
|
316
345
|
proc.on('close', (code) => {
|
|
317
|
-
if (getIsExiting())
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
346
|
+
if (getIsExiting()) return
|
|
347
|
+
|
|
348
|
+
// intentionally killed or restarting - skip error handling
|
|
349
|
+
const currentManaged = managedProcesses[index]
|
|
350
|
+
if (currentManaged?.restarting || currentManaged?.killing) return
|
|
321
351
|
|
|
322
352
|
if (code && code !== 0) {
|
|
323
|
-
log.error(`${
|
|
353
|
+
log.error(`${getPrefix(index)} Process exited with code ${code}`)
|
|
324
354
|
|
|
325
355
|
if (code === 1) {
|
|
326
356
|
console.error('\x1b[31m❌ Run Failed\x1b[0m')
|
|
@@ -333,7 +363,7 @@ const runScript = async (
|
|
|
333
363
|
console.info(
|
|
334
364
|
`Restarting process ${name} (${newRestarts}/${MAX_RESTARTS} times)`
|
|
335
365
|
)
|
|
336
|
-
runScript(name, cwd, prefixLabel, newRestarts, extraArgs)
|
|
366
|
+
runScript(name, cwd, prefixLabel, newRestarts, extraArgs, index)
|
|
337
367
|
} else {
|
|
338
368
|
exit(1)
|
|
339
369
|
}
|
|
@@ -344,6 +374,251 @@ const runScript = async (
|
|
|
344
374
|
return proc
|
|
345
375
|
}
|
|
346
376
|
|
|
377
|
+
// compute unique letter-based shortcuts from process labels
|
|
378
|
+
// splits on non-letters, takes first char of each word, extends until unique
|
|
379
|
+
function computeShortcuts() {
|
|
380
|
+
const initials = managedProcesses.map((p) => {
|
|
381
|
+
const words = p.prefixLabel
|
|
382
|
+
.toLowerCase()
|
|
383
|
+
.split(/[^a-z]+/)
|
|
384
|
+
.filter(Boolean)
|
|
385
|
+
return words.map((w) => w[0]).join('')
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
// start each shortcut at 1 letter, extend collisions
|
|
389
|
+
const lengths = new Array(managedProcesses.length).fill(1) as number[]
|
|
390
|
+
|
|
391
|
+
for (let round = 0; round < 5; round++) {
|
|
392
|
+
const shortcuts = initials.map(
|
|
393
|
+
(init, i) => init.slice(0, lengths[i]) || init
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
let hasCollision = false
|
|
397
|
+
const groups = new Map<string, number[]>()
|
|
398
|
+
for (let i = 0; i < shortcuts.length; i++) {
|
|
399
|
+
const key = shortcuts[i]!
|
|
400
|
+
if (!groups.has(key)) groups.set(key, [])
|
|
401
|
+
groups.get(key)!.push(i)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
for (const [, indices] of groups) {
|
|
405
|
+
if (indices.length <= 1) continue
|
|
406
|
+
hasCollision = true
|
|
407
|
+
for (const idx of indices) {
|
|
408
|
+
lengths[idx]!++
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (!hasCollision) {
|
|
413
|
+
for (let i = 0; i < managedProcesses.length; i++) {
|
|
414
|
+
managedProcesses[i]!.shortcut = shortcuts[i]!
|
|
415
|
+
}
|
|
416
|
+
return
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// fallback: use whatever we have, append index if still colliding
|
|
421
|
+
for (let i = 0; i < managedProcesses.length; i++) {
|
|
422
|
+
const sc = initials[i]!.slice(0, lengths[i]) || initials[i]!
|
|
423
|
+
managedProcesses[i]!.shortcut = sc || String(i + 1)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function killProcessGroup(managed: ManagedProcess) {
|
|
428
|
+
if (managed.proc.pid) {
|
|
429
|
+
try {
|
|
430
|
+
process.kill(-managed.proc.pid, 'SIGTERM')
|
|
431
|
+
} catch {}
|
|
432
|
+
await new Promise((r) => setTimeout(r, 200))
|
|
433
|
+
try {
|
|
434
|
+
process.kill(-managed.proc.pid, 'SIGKILL')
|
|
435
|
+
} catch {}
|
|
436
|
+
}
|
|
437
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function restartProcess(index: number) {
|
|
441
|
+
const managed = managedProcesses[index]
|
|
442
|
+
if (!managed) return
|
|
443
|
+
|
|
444
|
+
const { name, cwd, prefixLabel, extraArgs } = managed
|
|
445
|
+
|
|
446
|
+
managed.restarting = true
|
|
447
|
+
managed.killing = false
|
|
448
|
+
console.info(`\x1b[2m restarting ${managed.shortcut} ${prefixLabel}...\x1b[0m`)
|
|
449
|
+
|
|
450
|
+
await killProcessGroup(managed)
|
|
451
|
+
await runScript(name, cwd, prefixLabel, 0, extraArgs, index)
|
|
452
|
+
console.info(`${getPrefix(index)} \x1b[32m↻ restarted\x1b[0m`)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function killProcess(index: number) {
|
|
456
|
+
const managed = managedProcesses[index]
|
|
457
|
+
if (!managed) return
|
|
458
|
+
|
|
459
|
+
if (managed.killing) {
|
|
460
|
+
console.info(`\x1b[2m ${managed.shortcut} ${managed.prefixLabel} already stopped\x1b[0m`)
|
|
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(`${dim} ${reset}${color}${managed.shortcut}${reset}${dim} ${managed.prefixLabel}${stopped}${reset}`)
|
|
522
|
+
}
|
|
523
|
+
console.info()
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function enterMode(newMode: InputMode, label: string) {
|
|
527
|
+
clearTimer()
|
|
528
|
+
mode = newMode
|
|
529
|
+
buffer = ''
|
|
530
|
+
showProcessList(label)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
process.stdin.on('data', (key: string) => {
|
|
534
|
+
// ctrl+c
|
|
535
|
+
if (key === '\x03') {
|
|
536
|
+
process.stdin.setRawMode(false)
|
|
537
|
+
exit(0)
|
|
538
|
+
return
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// escape cancels
|
|
542
|
+
if (key === '\x1b' && mode) {
|
|
543
|
+
clearTimer()
|
|
544
|
+
buffer = ''
|
|
545
|
+
mode = null
|
|
546
|
+
console.info('\x1b[2m cancelled\x1b[0m')
|
|
547
|
+
return
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// ctrl+r - restart mode
|
|
551
|
+
if (key === '\x12') {
|
|
552
|
+
enterMode('restart', 'restart')
|
|
553
|
+
return
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// ctrl+k - kill mode
|
|
557
|
+
if (key === '\x0b') {
|
|
558
|
+
enterMode('kill', 'kill')
|
|
559
|
+
return
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// ctrl+l - clear screen
|
|
563
|
+
if (key === '\x0c') {
|
|
564
|
+
process.stdout.write('\x1b[2J\x1b[H')
|
|
565
|
+
return
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (mode) {
|
|
569
|
+
const lower = key.toLowerCase()
|
|
570
|
+
if (/^[a-z]$/.test(lower)) {
|
|
571
|
+
buffer += lower
|
|
572
|
+
clearTimer()
|
|
573
|
+
|
|
574
|
+
// exact match → dispatch immediately
|
|
575
|
+
const exact = managedProcesses.find((p) => p.shortcut === buffer)
|
|
576
|
+
if (exact) {
|
|
577
|
+
const m = mode
|
|
578
|
+
mode = null
|
|
579
|
+
buffer = ''
|
|
580
|
+
dispatchMatch(m, exact.index)
|
|
581
|
+
return
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// no shortcuts start with buffer → no match
|
|
585
|
+
const hasPrefix = managedProcesses.some((p) => p.shortcut.startsWith(buffer))
|
|
586
|
+
if (!hasPrefix) {
|
|
587
|
+
console.info(`\x1b[2m no match for "${buffer}"\x1b[0m`)
|
|
588
|
+
buffer = ''
|
|
589
|
+
mode = null
|
|
590
|
+
return
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// ambiguous — wait 500ms for more input
|
|
594
|
+
timer = setTimeout(finishMatch, 500)
|
|
595
|
+
} else {
|
|
596
|
+
// non-letter cancels
|
|
597
|
+
clearTimer()
|
|
598
|
+
buffer = ''
|
|
599
|
+
mode = null
|
|
600
|
+
console.info('\x1b[2m cancelled\x1b[0m')
|
|
601
|
+
}
|
|
602
|
+
return
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// forward other input to the designated stdin process
|
|
606
|
+
const stdinProc = managedProcesses.find((p) => p.name === stdinScript)
|
|
607
|
+
if (stdinProc?.proc.stdin && !stdinProc.proc.stdin.destroyed) {
|
|
608
|
+
stdinProc.proc.stdin.write(key)
|
|
609
|
+
}
|
|
610
|
+
})
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function printShortcutHint() {
|
|
614
|
+
if (!process.stdin.isTTY) return
|
|
615
|
+
if (managedProcesses.length === 0) return
|
|
616
|
+
|
|
617
|
+
const dim = '\x1b[2m'
|
|
618
|
+
console.info(`${dim} ctrl+r restart · ctrl+k kill · ctrl+l clear · ctrl+c exit${reset}`)
|
|
619
|
+
console.info()
|
|
620
|
+
}
|
|
621
|
+
|
|
347
622
|
async function main() {
|
|
348
623
|
checkNodeVersion().catch((err) => {
|
|
349
624
|
log.error(err.message)
|
|
@@ -384,8 +659,12 @@ async function main() {
|
|
|
384
659
|
}
|
|
385
660
|
}
|
|
386
661
|
|
|
387
|
-
if (
|
|
662
|
+
if (managedProcesses.length === 0) {
|
|
388
663
|
exit(0)
|
|
664
|
+
} else {
|
|
665
|
+
computeShortcuts()
|
|
666
|
+
printShortcutHint()
|
|
667
|
+
setupKeyboardShortcuts()
|
|
389
668
|
}
|
|
390
669
|
} catch (error) {
|
|
391
670
|
log.error(`Error running scripts: ${error}`)
|