@take-out/scripts 0.1.20 → 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 +2 -2
- package/src/run-pty.mjs +126 -113
- 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,
|
|
@@ -30,7 +30,7 @@
|
|
|
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.
|
|
33
|
+
"@take-out/helpers": "0.1.21",
|
|
34
34
|
"picocolors": "^1.1.1"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
package/src/run-pty.mjs
CHANGED
|
@@ -6,12 +6,12 @@ import { join, relative, resolve } from 'node:path'
|
|
|
6
6
|
import pty from '@lydell/node-pty'
|
|
7
7
|
|
|
8
8
|
const colors = [
|
|
9
|
-
'\x1b[38;5;
|
|
10
|
-
'\x1b[38;5;
|
|
11
|
-
'\x1b[38;5;
|
|
12
|
-
'\x1b[38;5;
|
|
13
|
-
'\x1b[38;5;
|
|
14
|
-
'\x1b[38;5;
|
|
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
15
|
]
|
|
16
16
|
const reset = '\x1b[0m'
|
|
17
17
|
const dim = '\x1b[2m'
|
|
@@ -43,14 +43,13 @@ const runBun = args.includes('--bun')
|
|
|
43
43
|
const flagsLast = args.includes('--flags=last')
|
|
44
44
|
|
|
45
45
|
const processes = []
|
|
46
|
-
let
|
|
47
|
-
let selectedIndex = 0
|
|
46
|
+
let focusedIndex = -1 // -1 = interleaved/dashboard
|
|
48
47
|
|
|
49
48
|
function getPrefix(index) {
|
|
50
49
|
const p = processes[index]
|
|
51
50
|
if (!p) return ''
|
|
52
51
|
const color = colors[index % colors.length]
|
|
53
|
-
return `${color}${p.shortcut}
|
|
52
|
+
return `${color}${p.shortcut}${reset}`
|
|
54
53
|
}
|
|
55
54
|
|
|
56
55
|
if (runCommands.length === 0) {
|
|
@@ -97,10 +96,8 @@ async function findPackageJsonDirs(base, depth = 3) {
|
|
|
97
96
|
async function findWorkspaceScripts(scripts) {
|
|
98
97
|
const patterns = await getWorkspacePatterns()
|
|
99
98
|
if (!patterns.length) return new Map()
|
|
100
|
-
|
|
101
99
|
const dirs = await findPackageJsonDirs('.')
|
|
102
100
|
const result = new Map()
|
|
103
|
-
|
|
104
101
|
for (const dir of dirs) {
|
|
105
102
|
if (dir === '.') continue
|
|
106
103
|
const rel = relative('.', dir).replace(/\\/g, '/')
|
|
@@ -111,7 +108,6 @@ async function findWorkspaceScripts(scripts) {
|
|
|
111
108
|
: rel === np || rel.startsWith(np + '/')
|
|
112
109
|
})
|
|
113
110
|
if (!matches) continue
|
|
114
|
-
|
|
115
111
|
const pkg = await readPackageJson(dir)
|
|
116
112
|
if (!pkg?.scripts) continue
|
|
117
113
|
const available = scripts.filter((s) => typeof pkg.scripts[s] === 'string')
|
|
@@ -124,7 +120,6 @@ function spawnScript(name, cwd, label, extraArgs, index) {
|
|
|
124
120
|
const runArgs = ['run', '--silent', runBun ? '--bun' : '', name, ...extraArgs].filter(
|
|
125
121
|
Boolean
|
|
126
122
|
)
|
|
127
|
-
|
|
128
123
|
const terminal = pty.spawn('bun', runArgs, {
|
|
129
124
|
cwd: resolve(cwd),
|
|
130
125
|
cols: process.stdout.columns || 80,
|
|
@@ -144,26 +139,34 @@ function spawnScript(name, cwd, label, extraArgs, index) {
|
|
|
144
139
|
killed: false,
|
|
145
140
|
}
|
|
146
141
|
|
|
147
|
-
if (index !== undefined)
|
|
148
|
-
|
|
149
|
-
} else {
|
|
150
|
-
processes.push(managed)
|
|
151
|
-
}
|
|
142
|
+
if (index !== undefined) processes[index] = managed
|
|
143
|
+
else processes.push(managed)
|
|
152
144
|
|
|
153
145
|
terminal.onData((data) => {
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
158
154
|
process.stdout.write(`${getPrefix(idx)} ${line}\n`)
|
|
159
155
|
}
|
|
156
|
+
} else if (focusedIndex === idx) {
|
|
157
|
+
// focused - raw output
|
|
158
|
+
process.stdout.write(data)
|
|
160
159
|
}
|
|
161
160
|
})
|
|
162
161
|
|
|
163
162
|
terminal.onExit(({ exitCode }) => {
|
|
164
163
|
if (managed.killed) return
|
|
164
|
+
if (focusedIndex === idx) {
|
|
165
|
+
focusedIndex = -1
|
|
166
|
+
showDashboard()
|
|
167
|
+
}
|
|
165
168
|
if (exitCode !== 0) {
|
|
166
|
-
|
|
169
|
+
console.error(`${getPrefix(idx)} exited ${exitCode}`)
|
|
167
170
|
}
|
|
168
171
|
})
|
|
169
172
|
|
|
@@ -171,115 +174,122 @@ function spawnScript(name, cwd, label, extraArgs, index) {
|
|
|
171
174
|
}
|
|
172
175
|
|
|
173
176
|
function computeShortcuts() {
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
176
181
|
.toLowerCase()
|
|
177
182
|
.split(/[^a-z]+/)
|
|
178
183
|
.filter(Boolean)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
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)
|
|
199
195
|
}
|
|
196
|
+
p.shortcut = shortcut
|
|
200
197
|
}
|
|
201
|
-
processes.forEach(
|
|
202
|
-
(p, i) => (p.shortcut = initials[i]?.slice(0, lengths[i]) || String(i + 1))
|
|
203
|
-
)
|
|
204
198
|
}
|
|
205
199
|
|
|
206
|
-
function
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
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
211
|
}
|
|
224
212
|
|
|
225
|
-
|
|
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
|
-
}
|
|
213
|
+
let pendingAction = null // 'r' or 'k'
|
|
232
214
|
|
|
233
215
|
function handleInput(data) {
|
|
234
216
|
const str = data.toString()
|
|
235
217
|
|
|
218
|
+
// ctrl+c exit
|
|
236
219
|
if (str === '\x03') {
|
|
237
220
|
cleanup()
|
|
238
221
|
return
|
|
239
222
|
}
|
|
240
223
|
|
|
224
|
+
// ctrl+z toggle focus
|
|
241
225
|
if (str === '\x1a') {
|
|
242
|
-
if (
|
|
243
|
-
|
|
226
|
+
if (focusedIndex >= 0) {
|
|
227
|
+
focusedIndex = -1
|
|
228
|
+
showDashboard()
|
|
244
229
|
} else {
|
|
245
|
-
|
|
246
|
-
renderDashboard()
|
|
230
|
+
showDashboard()
|
|
247
231
|
}
|
|
248
232
|
return
|
|
249
233
|
}
|
|
250
234
|
|
|
251
|
-
if
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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`)
|
|
274
275
|
}
|
|
275
|
-
} else
|
|
276
|
-
|
|
276
|
+
} else {
|
|
277
|
+
// focus
|
|
278
|
+
focusedIndex = match.index
|
|
279
|
+
console.log(`${dim}focused: ${match.label} (ctrl+z to unfocus)${reset}\n`)
|
|
277
280
|
}
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// escape cancels pending
|
|
285
|
+
if (str === '\x1b' && pendingAction) {
|
|
286
|
+
pendingAction = null
|
|
287
|
+
console.log('cancelled')
|
|
278
288
|
}
|
|
279
289
|
}
|
|
280
290
|
|
|
281
291
|
function cleanup() {
|
|
282
|
-
|
|
292
|
+
console.log()
|
|
283
293
|
for (const p of processes) {
|
|
284
294
|
if (!p.killed) {
|
|
285
295
|
p.killed = true
|
|
@@ -297,8 +307,12 @@ async function main() {
|
|
|
297
307
|
if (pkg?.scripts) {
|
|
298
308
|
for (const name of runCommands) {
|
|
299
309
|
if (typeof pkg.scripts[name] === 'string') {
|
|
300
|
-
|
|
301
|
-
|
|
310
|
+
spawnScript(
|
|
311
|
+
name,
|
|
312
|
+
'.',
|
|
313
|
+
name,
|
|
314
|
+
!flagsLast || name === lastScript ? forwardArgs : []
|
|
315
|
+
)
|
|
302
316
|
}
|
|
303
317
|
}
|
|
304
318
|
}
|
|
@@ -307,8 +321,12 @@ async function main() {
|
|
|
307
321
|
const wsScripts = await findWorkspaceScripts(runCommands)
|
|
308
322
|
for (const [dir, { scripts, name }] of wsScripts) {
|
|
309
323
|
for (const script of scripts) {
|
|
310
|
-
|
|
311
|
-
|
|
324
|
+
spawnScript(
|
|
325
|
+
script,
|
|
326
|
+
dir,
|
|
327
|
+
`${name} ${script}`,
|
|
328
|
+
!flagsLast || script === lastScript ? forwardArgs : []
|
|
329
|
+
)
|
|
312
330
|
}
|
|
313
331
|
}
|
|
314
332
|
|
|
@@ -318,9 +336,7 @@ async function main() {
|
|
|
318
336
|
}
|
|
319
337
|
|
|
320
338
|
computeShortcuts()
|
|
321
|
-
|
|
322
|
-
`${dim} running ${processes.length} processes (ctrl+z dashboard, ctrl+c exit)${reset}\n\n`
|
|
323
|
-
)
|
|
339
|
+
showDashboard()
|
|
324
340
|
|
|
325
341
|
if (process.stdin.isTTY) {
|
|
326
342
|
process.stdin.setRawMode(true)
|
|
@@ -329,12 +345,9 @@ async function main() {
|
|
|
329
345
|
}
|
|
330
346
|
|
|
331
347
|
process.stdout.on('resize', () => {
|
|
332
|
-
const cols = process.stdout.columns || 80
|
|
333
|
-
const rows = process.stdout.rows || 24
|
|
334
348
|
for (const p of processes) {
|
|
335
|
-
if (!p.killed)
|
|
336
|
-
p.terminal.resize(
|
|
337
|
-
}
|
|
349
|
+
if (!p.killed)
|
|
350
|
+
p.terminal.resize(process.stdout.columns || 80, process.stdout.rows || 24)
|
|
338
351
|
}
|
|
339
352
|
})
|
|
340
353
|
|
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
|
})
|