@o/run 0.16.19
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 +49 -0
- package/src/helpers/handleProcessExit.ts +230 -0
- package/src/helpers/process-cleanup.test.ts +150 -0
- package/src/helpers/process-state.ts +26 -0
- package/src/helpers/resolveScript.test.ts +340 -0
- package/src/helpers/resolveScript.ts +225 -0
- package/src/node-version-check.ts +55 -0
- package/src/process-cleanup.test.ts +255 -0
- package/src/run-pty-supervision.test.ts +110 -0
- package/src/run-pty.mjs +455 -0
- package/src/run-supervision.test.ts +94 -0
- package/src/run.ts +546 -0
package/src/run.ts
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @description Run multiple scripts in parallel
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn } from 'node:child_process'
|
|
8
|
+
import fs from 'node:fs'
|
|
9
|
+
import { join, relative, resolve } from 'node:path'
|
|
10
|
+
|
|
11
|
+
import { handleProcessExit } from './helpers/handleProcessExit'
|
|
12
|
+
import { getIsExiting } from './helpers/process-state'
|
|
13
|
+
import { flattenScripts } from './helpers/resolveScript'
|
|
14
|
+
import { checkNodeVersion } from './node-version-check'
|
|
15
|
+
|
|
16
|
+
const colors = [
|
|
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',
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
const reset = '\x1b[0m'
|
|
27
|
+
|
|
28
|
+
// eslint-disable-next-line no-control-regex
|
|
29
|
+
const ansiPattern = /\x1b\[[0-9;]*m/g
|
|
30
|
+
|
|
31
|
+
interface ManagedProcess {
|
|
32
|
+
proc: ReturnType<typeof spawn>
|
|
33
|
+
name: string
|
|
34
|
+
cwd: string
|
|
35
|
+
prefixLabel: string
|
|
36
|
+
extraArgs: string[]
|
|
37
|
+
index: number
|
|
38
|
+
shortcut: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function readPackageJson(directoryPath: string) {
|
|
42
|
+
try {
|
|
43
|
+
const packageJsonPath = join(directoryPath, 'package.json')
|
|
44
|
+
const content = await fs.promises.readFile(packageJsonPath, 'utf8')
|
|
45
|
+
return JSON.parse(content)
|
|
46
|
+
} catch {
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function getWorkspacePatterns(): Promise<string[]> {
|
|
52
|
+
try {
|
|
53
|
+
const packageJson = await readPackageJson('.')
|
|
54
|
+
if (!packageJson || !packageJson.workspaces) return []
|
|
55
|
+
|
|
56
|
+
return Array.isArray(packageJson.workspaces)
|
|
57
|
+
? packageJson.workspaces
|
|
58
|
+
: packageJson.workspaces.packages || []
|
|
59
|
+
} catch {
|
|
60
|
+
return []
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function hasPackageJson(path: string): Promise<boolean> {
|
|
65
|
+
try {
|
|
66
|
+
await fs.promises.access(join(path, 'package.json'))
|
|
67
|
+
return true
|
|
68
|
+
} catch {
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function findPackageJsonDirs(basePath: string, maxDepth = 3): Promise<string[]> {
|
|
74
|
+
if (maxDepth <= 0) return []
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const entries = await fs.promises.readdir(basePath, { withFileTypes: true })
|
|
78
|
+
const results: string[] = []
|
|
79
|
+
|
|
80
|
+
if (await hasPackageJson(basePath)) {
|
|
81
|
+
results.push(basePath)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const subDirPromises = entries
|
|
85
|
+
.filter(
|
|
86
|
+
(entry) =>
|
|
87
|
+
entry.isDirectory() &&
|
|
88
|
+
!entry.name.startsWith('.') &&
|
|
89
|
+
entry.name !== 'node_modules'
|
|
90
|
+
)
|
|
91
|
+
.map(async (dir) => {
|
|
92
|
+
const path = join(basePath, dir.name)
|
|
93
|
+
return findPackageJsonDirs(path, maxDepth - 1)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const subdirResults = await Promise.all(subDirPromises)
|
|
97
|
+
return [...results, ...subdirResults.flat()]
|
|
98
|
+
} catch {
|
|
99
|
+
return []
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function findWorkspaceDirectories(): Promise<string[]> {
|
|
104
|
+
const patterns = await getWorkspacePatterns()
|
|
105
|
+
if (!patterns.length) return []
|
|
106
|
+
|
|
107
|
+
const allPackageDirs = await findPackageJsonDirs('.')
|
|
108
|
+
|
|
109
|
+
const normalizePath = (path: string): string => {
|
|
110
|
+
const normalized = path.replace(/\\/g, '/')
|
|
111
|
+
return normalized.startsWith('./') ? normalized.substring(2) : normalized
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return allPackageDirs.filter((dir) => {
|
|
115
|
+
if (dir === '.') return false
|
|
116
|
+
|
|
117
|
+
const relativePath = relative('.', dir)
|
|
118
|
+
return patterns.some((pattern) => {
|
|
119
|
+
const normalizedPattern = normalizePath(pattern)
|
|
120
|
+
const normalizedPath = normalizePath(relativePath)
|
|
121
|
+
|
|
122
|
+
if (normalizedPattern.endsWith('/*')) {
|
|
123
|
+
const prefix = normalizedPattern.slice(0, -1)
|
|
124
|
+
return normalizedPath.startsWith(prefix)
|
|
125
|
+
}
|
|
126
|
+
return (
|
|
127
|
+
normalizedPath === normalizedPattern ||
|
|
128
|
+
normalizedPath.startsWith(normalizedPattern + '/')
|
|
129
|
+
)
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function findAvailableScripts(
|
|
135
|
+
directoryPath: string,
|
|
136
|
+
scriptNames: string[]
|
|
137
|
+
): Promise<string[]> {
|
|
138
|
+
const packageJson = await readPackageJson(directoryPath)
|
|
139
|
+
|
|
140
|
+
if (!packageJson || !packageJson.scripts) {
|
|
141
|
+
return []
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return scriptNames.filter(
|
|
145
|
+
(scriptName) => typeof packageJson.scripts?.[scriptName] === 'string'
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function mapWorkspacesToScripts(
|
|
150
|
+
scriptNames: string[]
|
|
151
|
+
): Promise<Map<string, { scripts: string[]; packageName: string }>> {
|
|
152
|
+
const workspaceDirs = await findWorkspaceDirectories()
|
|
153
|
+
const workspaceScriptMap = new Map<string, { scripts: string[]; packageName: string }>()
|
|
154
|
+
|
|
155
|
+
for (const dir of workspaceDirs) {
|
|
156
|
+
const availableScripts = await findAvailableScripts(dir, scriptNames)
|
|
157
|
+
|
|
158
|
+
if (availableScripts.length > 0) {
|
|
159
|
+
const packageJson = await readPackageJson(dir)
|
|
160
|
+
const packageName = packageJson?.name || dir
|
|
161
|
+
workspaceScriptMap.set(dir, {
|
|
162
|
+
scripts: availableScripts,
|
|
163
|
+
packageName,
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return workspaceScriptMap
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function computeShortcuts(managedProcesses: ManagedProcess[]) {
|
|
172
|
+
const initials = managedProcesses.map((p) => {
|
|
173
|
+
const words = p.prefixLabel
|
|
174
|
+
.toLowerCase()
|
|
175
|
+
.split(/[^a-z]+/)
|
|
176
|
+
.filter(Boolean)
|
|
177
|
+
return words.map((w) => w[0]).join('')
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
const lengths = new Array(managedProcesses.length).fill(1) as number[]
|
|
181
|
+
|
|
182
|
+
for (let round = 0; round < 5; round++) {
|
|
183
|
+
const shortcuts = initials.map((init, i) => init.slice(0, lengths[i]) || init)
|
|
184
|
+
|
|
185
|
+
let hasCollision = false
|
|
186
|
+
const groups = new Map<string, number[]>()
|
|
187
|
+
for (let i = 0; i < shortcuts.length; i++) {
|
|
188
|
+
const key = shortcuts[i]!
|
|
189
|
+
if (!groups.has(key)) groups.set(key, [])
|
|
190
|
+
groups.get(key)!.push(i)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
for (const [, indices] of groups) {
|
|
194
|
+
if (indices.length <= 1) continue
|
|
195
|
+
hasCollision = true
|
|
196
|
+
for (const idx of indices) {
|
|
197
|
+
lengths[idx]!++
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!hasCollision) {
|
|
202
|
+
for (let i = 0; i < managedProcesses.length; i++) {
|
|
203
|
+
managedProcesses[i]!.shortcut = shortcuts[i]!
|
|
204
|
+
}
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (let i = 0; i < managedProcesses.length; i++) {
|
|
210
|
+
const sc = initials[i]!.slice(0, lengths[i]) || initials[i]!
|
|
211
|
+
managedProcesses[i]!.shortcut = sc || String(i + 1)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// --- exported interface for CLI direct-import ---
|
|
216
|
+
|
|
217
|
+
export interface RunParallelScriptsOptions {
|
|
218
|
+
commands: string[]
|
|
219
|
+
noRoot?: boolean
|
|
220
|
+
flagsLast?: boolean
|
|
221
|
+
runBun?: boolean
|
|
222
|
+
watch?: boolean
|
|
223
|
+
forwardArgs?: string[]
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function runParallelScripts(
|
|
227
|
+
options: RunParallelScriptsOptions
|
|
228
|
+
): Promise<void> {
|
|
229
|
+
const {
|
|
230
|
+
commands: runCommands,
|
|
231
|
+
noRoot = false,
|
|
232
|
+
flagsLast = false,
|
|
233
|
+
runBun = false,
|
|
234
|
+
watch = false,
|
|
235
|
+
forwardArgs = [],
|
|
236
|
+
} = options
|
|
237
|
+
|
|
238
|
+
const MAX_RESTARTS = 3
|
|
239
|
+
|
|
240
|
+
const parentRunningScripts = process.env.BUN_RUN_SCRIPTS
|
|
241
|
+
? process.env.BUN_RUN_SCRIPTS.split(',')
|
|
242
|
+
: []
|
|
243
|
+
|
|
244
|
+
const managedProcesses: ManagedProcess[] = []
|
|
245
|
+
const { addChildProcess, exit } = handleProcessExit()
|
|
246
|
+
|
|
247
|
+
function getPrefix(index: number): string {
|
|
248
|
+
const managed = managedProcesses[index]
|
|
249
|
+
if (!managed) return ''
|
|
250
|
+
const color = colors[index % colors.length]
|
|
251
|
+
const sc = managed.shortcut || String(index + 1)
|
|
252
|
+
return `${color}${sc} ${managed.prefixLabel}${reset}`
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (runCommands.length === 0) {
|
|
256
|
+
console.error('Please provide at least one script name to run')
|
|
257
|
+
exit(1)
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// spawns a single script process, optionally running a command directly via bash
|
|
262
|
+
const runScript = async (
|
|
263
|
+
name: string,
|
|
264
|
+
cwd = '.',
|
|
265
|
+
prefixLabel: string = name,
|
|
266
|
+
restarts = 0,
|
|
267
|
+
extraArgs: string[] = [],
|
|
268
|
+
managedIndex?: number,
|
|
269
|
+
directCommand?: string,
|
|
270
|
+
deferredChildren?: import('./helpers/resolveScript').FlattenedScript[]
|
|
271
|
+
) => {
|
|
272
|
+
const index = managedIndex ?? managedProcesses.length
|
|
273
|
+
|
|
274
|
+
const allRunningScripts = [...parentRunningScripts, ...runCommands].join(',')
|
|
275
|
+
|
|
276
|
+
let proc: ReturnType<typeof spawn>
|
|
277
|
+
|
|
278
|
+
if (directCommand) {
|
|
279
|
+
// run the resolved command directly via bash, skipping the bun run layer
|
|
280
|
+
const command = extraArgs.length
|
|
281
|
+
? `${directCommand} ${extraArgs.join(' ')}`
|
|
282
|
+
: directCommand
|
|
283
|
+
proc = spawn('bash', ['-c', command], {
|
|
284
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
285
|
+
shell: false,
|
|
286
|
+
detached: true,
|
|
287
|
+
env: {
|
|
288
|
+
...process.env,
|
|
289
|
+
FORCE_COLOR: '3',
|
|
290
|
+
BUN_RUN_PARENT_SCRIPT: name,
|
|
291
|
+
BUN_RUN_SCRIPTS: allRunningScripts,
|
|
292
|
+
TKO_SILENT: '1',
|
|
293
|
+
} as any,
|
|
294
|
+
cwd: resolve(cwd),
|
|
295
|
+
})
|
|
296
|
+
} else {
|
|
297
|
+
const runArgs = [
|
|
298
|
+
'run',
|
|
299
|
+
'--silent',
|
|
300
|
+
runBun ? '--bun' : '',
|
|
301
|
+
name,
|
|
302
|
+
...extraArgs,
|
|
303
|
+
].filter(Boolean)
|
|
304
|
+
proc = spawn('bun', runArgs, {
|
|
305
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
306
|
+
shell: false,
|
|
307
|
+
detached: true,
|
|
308
|
+
env: {
|
|
309
|
+
...process.env,
|
|
310
|
+
FORCE_COLOR: '3',
|
|
311
|
+
BUN_RUN_PARENT_SCRIPT: name,
|
|
312
|
+
BUN_RUN_SCRIPTS: allRunningScripts,
|
|
313
|
+
TKO_SILENT: '1',
|
|
314
|
+
} as any,
|
|
315
|
+
cwd: resolve(cwd),
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// when this process spawned — used to tell a crash-loop (rapid re-crashes)
|
|
320
|
+
// from an occasional crash after a long stable run.
|
|
321
|
+
const startedAt = Date.now()
|
|
322
|
+
|
|
323
|
+
const managed: ManagedProcess = {
|
|
324
|
+
proc,
|
|
325
|
+
name,
|
|
326
|
+
cwd,
|
|
327
|
+
prefixLabel,
|
|
328
|
+
extraArgs,
|
|
329
|
+
index,
|
|
330
|
+
shortcut: '',
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (managedIndex !== undefined) {
|
|
334
|
+
managedProcesses[managedIndex] = managed
|
|
335
|
+
} else {
|
|
336
|
+
managedProcesses.push(managed)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
addChildProcess(proc)
|
|
340
|
+
|
|
341
|
+
proc.stdout!.on('data', (data) => {
|
|
342
|
+
if (getIsExiting()) return
|
|
343
|
+
const lines = data.toString().split('\n')
|
|
344
|
+
for (const line of lines) {
|
|
345
|
+
const stripped = line.replace(ansiPattern, '')
|
|
346
|
+
if (stripped.startsWith('$ ')) continue
|
|
347
|
+
if (line) console.info(`${getPrefix(index)} ${line}`)
|
|
348
|
+
}
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
proc.stderr!.on('data', (data) => {
|
|
352
|
+
if (getIsExiting()) return
|
|
353
|
+
const lines = data.toString().split('\n')
|
|
354
|
+
for (const line of lines) {
|
|
355
|
+
const stripped = line.replace(ansiPattern, '')
|
|
356
|
+
if (stripped.startsWith('$ ')) continue
|
|
357
|
+
if (line) console.error(`${getPrefix(index)} ${line}`)
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
proc.on('error', (error) => {
|
|
362
|
+
console.error(`${getPrefix(index)} Failed to start: ${error.message}`)
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
proc.on('close', (code, signal) => {
|
|
366
|
+
if (getIsExiting()) return
|
|
367
|
+
|
|
368
|
+
if (code !== 0 || signal !== null) {
|
|
369
|
+
const result = signal ? `signal ${signal}` : `code ${code}`
|
|
370
|
+
console.error(`${getPrefix(index)} Process exited with ${result}`)
|
|
371
|
+
|
|
372
|
+
if (watch) {
|
|
373
|
+
// a process that ran stably for a while before crashing isn't a
|
|
374
|
+
// crash-loop — reset the counter so an occasional crash over a long
|
|
375
|
+
// dev session recovers, instead of permanently giving up after
|
|
376
|
+
// MAX_RESTARTS lifetime hits. the cap then only catches RAPID re-crashes.
|
|
377
|
+
const ranMs = Date.now() - startedAt
|
|
378
|
+
const STABLE_MS = 20_000
|
|
379
|
+
const effectiveRestarts = ranMs >= STABLE_MS ? 0 : restarts
|
|
380
|
+
if (effectiveRestarts < MAX_RESTARTS) {
|
|
381
|
+
const newRestarts = effectiveRestarts + 1
|
|
382
|
+
// exponential backoff so a process crash-looping under CPU/memory
|
|
383
|
+
// pressure doesn't instantly re-boot and spin the box at 100% —
|
|
384
|
+
// wait 1s, 2s, 4s … capped at 30s before the next attempt.
|
|
385
|
+
const delayMs = Math.min(1_000 * 2 ** effectiveRestarts, 30_000)
|
|
386
|
+
console.info(
|
|
387
|
+
`Restarting process ${name} (${newRestarts}/${MAX_RESTARTS}) in ${Math.round(delayMs / 1000)}s`
|
|
388
|
+
)
|
|
389
|
+
setTimeout(() => {
|
|
390
|
+
if (getIsExiting()) return
|
|
391
|
+
runScript(
|
|
392
|
+
name,
|
|
393
|
+
cwd,
|
|
394
|
+
prefixLabel,
|
|
395
|
+
newRestarts,
|
|
396
|
+
extraArgs,
|
|
397
|
+
index,
|
|
398
|
+
directCommand,
|
|
399
|
+
deferredChildren
|
|
400
|
+
)
|
|
401
|
+
}, delayMs)
|
|
402
|
+
} else {
|
|
403
|
+
console.error(
|
|
404
|
+
`${getPrefix(index)} gave up after ${MAX_RESTARTS} rapid restarts`
|
|
405
|
+
)
|
|
406
|
+
exit(1)
|
|
407
|
+
}
|
|
408
|
+
} else {
|
|
409
|
+
exit(1)
|
|
410
|
+
}
|
|
411
|
+
} else if (deferredChildren && deferredChildren.length > 0) {
|
|
412
|
+
// command succeeded — spawn deferred parallel scripts
|
|
413
|
+
for (const deferred of deferredChildren) {
|
|
414
|
+
runScript(
|
|
415
|
+
deferred.name,
|
|
416
|
+
'.',
|
|
417
|
+
deferred.name,
|
|
418
|
+
0,
|
|
419
|
+
[],
|
|
420
|
+
undefined,
|
|
421
|
+
!deferred.isRaw ? deferred.command : undefined
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
return proc
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
checkNodeVersion().catch((err: unknown) => {
|
|
432
|
+
console.error((err as Error).message)
|
|
433
|
+
exit(1)
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
if (runCommands.length > 0) {
|
|
437
|
+
const lastScript = runCommands[runCommands.length - 1]
|
|
438
|
+
|
|
439
|
+
if (!noRoot) {
|
|
440
|
+
// read package.json scripts for flattening
|
|
441
|
+
const packageJson = await readPackageJson('.')
|
|
442
|
+
const pkgScripts = packageJson?.scripts ?? {}
|
|
443
|
+
|
|
444
|
+
const filteredCommands = runCommands.filter(
|
|
445
|
+
(name) => !parentRunningScripts.includes(name)
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
// flatten tko run/run-all chains into leaf commands
|
|
449
|
+
const flattened = flattenScripts(filteredCommands, pkgScripts, {
|
|
450
|
+
runningScripts: parentRunningScripts,
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
const lastLeaf = flattened[flattened.length - 1]
|
|
454
|
+
const scriptPromises = flattened.map((leaf) => {
|
|
455
|
+
const scriptArgs = !flagsLast || leaf === lastLeaf ? forwardArgs : []
|
|
456
|
+
// known scripts: run command directly via bash (skip bun run layer)
|
|
457
|
+
// unknown scripts: fall through to bun run --silent
|
|
458
|
+
const directCommand = !leaf.isRaw ? leaf.command : undefined
|
|
459
|
+
return runScript(
|
|
460
|
+
leaf.name,
|
|
461
|
+
'.',
|
|
462
|
+
leaf.name,
|
|
463
|
+
0,
|
|
464
|
+
scriptArgs,
|
|
465
|
+
undefined,
|
|
466
|
+
directCommand,
|
|
467
|
+
leaf.deferredScripts
|
|
468
|
+
)
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
await Promise.all(scriptPromises)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// workspace scripts still use bun run (they need workspace cwd)
|
|
475
|
+
const workspaceScriptMap = await mapWorkspacesToScripts(runCommands)
|
|
476
|
+
|
|
477
|
+
for (const [workspace, { scripts, packageName }] of workspaceScriptMap.entries()) {
|
|
478
|
+
const filteredScripts = scripts.filter(
|
|
479
|
+
(scriptName) => !parentRunningScripts.includes(scriptName)
|
|
480
|
+
)
|
|
481
|
+
const workspaceScriptPromises = filteredScripts.map((scriptName) => {
|
|
482
|
+
const scriptArgs = !flagsLast || scriptName === lastScript ? forwardArgs : []
|
|
483
|
+
return runScript(
|
|
484
|
+
scriptName,
|
|
485
|
+
workspace,
|
|
486
|
+
`${packageName} ${scriptName}`,
|
|
487
|
+
0,
|
|
488
|
+
scriptArgs
|
|
489
|
+
)
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
await Promise.all(workspaceScriptPromises)
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (managedProcesses.length === 0) {
|
|
497
|
+
exit(0)
|
|
498
|
+
} else {
|
|
499
|
+
computeShortcuts(managedProcesses)
|
|
500
|
+
}
|
|
501
|
+
} catch (error) {
|
|
502
|
+
console.error(`Error running scripts: ${error}`)
|
|
503
|
+
exit(1)
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// --- CLI entry point (only runs when executed directly, not when imported) ---
|
|
508
|
+
|
|
509
|
+
const ownFlags = ['--no-root', '--bun', '--watch', '--flags=last']
|
|
510
|
+
|
|
511
|
+
export function parseRunArgs(args: string[]): RunParallelScriptsOptions {
|
|
512
|
+
const commands: string[] = []
|
|
513
|
+
const forwardArgs: string[] = []
|
|
514
|
+
|
|
515
|
+
for (let i = 0; i < args.length; i++) {
|
|
516
|
+
const arg = args[i]!
|
|
517
|
+
|
|
518
|
+
if (arg.startsWith('--')) {
|
|
519
|
+
if (ownFlags.includes(arg)) continue
|
|
520
|
+
forwardArgs.push(arg)
|
|
521
|
+
const nextArg = args[i + 1]
|
|
522
|
+
if (nextArg && !nextArg.startsWith('--')) {
|
|
523
|
+
forwardArgs.push(nextArg)
|
|
524
|
+
i++
|
|
525
|
+
}
|
|
526
|
+
} else {
|
|
527
|
+
commands.push(arg)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return {
|
|
532
|
+
commands,
|
|
533
|
+
noRoot: args.includes('--no-root'),
|
|
534
|
+
runBun: args.includes('--bun'),
|
|
535
|
+
watch: args.includes('--watch'),
|
|
536
|
+
flagsLast: args.includes('--flags=last'),
|
|
537
|
+
forwardArgs,
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (typeof Bun !== 'undefined' && Bun.main === import.meta.path) {
|
|
542
|
+
runParallelScripts(parseRunArgs(process.argv.slice(2))).catch((error: unknown) => {
|
|
543
|
+
console.error(`Error running scripts: ${error}`)
|
|
544
|
+
process.exit(1)
|
|
545
|
+
})
|
|
546
|
+
}
|