@take-out/scripts 0.1.37 → 0.1.38-1772433507984

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/src/run.ts CHANGED
@@ -1,32 +1,14 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  /**
4
- * @description Run multiple scripts in parallel
4
+ * @description Run multiple scripts in parallel using run-group for proper process management
5
5
  */
6
6
 
7
- import { spawn } from 'node:child_process'
8
7
  import fs from 'node:fs'
9
- import { join, relative, resolve } from 'node:path'
8
+ import { dirname, join, relative, resolve } from 'node:path'
9
+ import { fileURLToPath } from 'node:url'
10
10
 
11
- import { handleProcessExit } from '@take-out/scripts/helpers/handleProcessExit'
12
-
13
- import { getIsExiting } from './helpers/run'
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
11
+ const __dirname = dirname(fileURLToPath(import.meta.url))
30
12
 
31
13
  const args = process.argv.slice(2)
32
14
  const ownFlags = ['--no-root', '--bun', '--watch', '--flags=last']
@@ -50,40 +32,22 @@ for (let i = 0; i < args.length; i++) {
50
32
  }
51
33
 
52
34
  const noRoot = args.includes('--no-root')
53
- const runBun = args.includes('--bun')
54
- const watch = args.includes('--watch')
55
35
  const flagsLast = args.includes('--flags=last')
56
36
 
57
- const MAX_RESTARTS = 3
58
-
59
37
  const parentRunningScripts = process.env.BUN_RUN_SCRIPTS
60
38
  ? process.env.BUN_RUN_SCRIPTS.split(',')
61
39
  : []
62
40
 
63
- interface ManagedProcess {
64
- proc: ReturnType<typeof spawn>
65
- name: string
66
- cwd: string
67
- prefixLabel: string
68
- extraArgs: string[]
69
- index: number
70
- shortcut: string
71
- }
72
-
73
- const managedProcesses: ManagedProcess[] = []
74
- const { addChildProcess, exit } = handleProcessExit()
75
-
76
- function getPrefix(index: number): string {
77
- const managed = managedProcesses[index]
78
- if (!managed) return ''
79
- const color = colors[index % colors.length]
80
- const sc = managed.shortcut || String(index + 1)
81
- return `${color}${sc} ${managed.prefixLabel}${reset}`
82
- }
83
-
84
41
  if (runCommands.length === 0) {
85
42
  console.error('Please provide at least one script name to run')
86
- exit(1)
43
+ process.exit(1)
44
+ }
45
+
46
+ interface ResolvedCommand {
47
+ name: string
48
+ command: string
49
+ cwd: string
50
+ args: string[]
87
51
  }
88
52
 
89
53
  async function readPackageJson(directoryPath: string) {
@@ -179,6 +143,15 @@ async function findWorkspaceDirectories(): Promise<string[]> {
179
143
  })
180
144
  }
181
145
 
146
+ async function getScriptCommand(
147
+ directoryPath: string,
148
+ scriptName: string
149
+ ): Promise<string | null> {
150
+ const packageJson = await readPackageJson(directoryPath)
151
+ if (!packageJson?.scripts?.[scriptName]) return null
152
+ return packageJson.scripts[scriptName]
153
+ }
154
+
182
155
  async function findAvailableScripts(
183
156
  directoryPath: string,
184
157
  scriptNames: string[]
@@ -194,218 +167,180 @@ async function findAvailableScripts(
194
167
  )
195
168
  }
196
169
 
197
- async function mapWorkspacesToScripts(
198
- scriptNames: string[]
199
- ): Promise<Map<string, { scripts: string[]; packageName: string }>> {
170
+ async function resolveAllCommands(): Promise<ResolvedCommand[]> {
171
+ const commands: ResolvedCommand[] = []
172
+ const lastScript = runCommands[runCommands.length - 1]
173
+
174
+ // root scripts
175
+ if (!noRoot) {
176
+ const filteredCommands = runCommands.filter(
177
+ (name) => !parentRunningScripts.includes(name)
178
+ )
179
+
180
+ for (const name of filteredCommands) {
181
+ const command = await getScriptCommand('.', name)
182
+ if (command) {
183
+ const scriptArgs = !flagsLast || name === lastScript ? forwardArgs : []
184
+ commands.push({
185
+ name,
186
+ command,
187
+ cwd: '.',
188
+ args: scriptArgs,
189
+ })
190
+ }
191
+ }
192
+ }
193
+
194
+ // workspace scripts
200
195
  const workspaceDirs = await findWorkspaceDirectories()
201
- const workspaceScriptMap = new Map<string, { scripts: string[]; packageName: string }>()
202
196
 
203
197
  for (const dir of workspaceDirs) {
204
- const availableScripts = await findAvailableScripts(dir, scriptNames)
205
-
206
- if (availableScripts.length > 0) {
207
- const packageJson = await readPackageJson(dir)
208
- const packageName = packageJson?.name || dir
209
- workspaceScriptMap.set(dir, {
210
- scripts: availableScripts,
211
- packageName,
212
- })
198
+ const availableScripts = await findAvailableScripts(dir, runCommands)
199
+ const filteredScripts = availableScripts.filter(
200
+ (scriptName) => !parentRunningScripts.includes(scriptName)
201
+ )
202
+
203
+ for (const scriptName of filteredScripts) {
204
+ const command = await getScriptCommand(dir, scriptName)
205
+ if (command) {
206
+ const packageJson = await readPackageJson(dir)
207
+ const packageName = packageJson?.name || dir
208
+ const scriptArgs = !flagsLast || scriptName === lastScript ? forwardArgs : []
209
+
210
+ commands.push({
211
+ name: `${packageName} ${scriptName}`,
212
+ command,
213
+ cwd: dir,
214
+ args: scriptArgs,
215
+ })
216
+ }
213
217
  }
214
218
  }
215
219
 
216
- return workspaceScriptMap
220
+ return commands
217
221
  }
218
222
 
219
- const runScript = async (
220
- name: string,
221
- cwd = '.',
222
- prefixLabel: string = name,
223
- restarts = 0,
224
- extraArgs: string[] = [],
225
- managedIndex?: number
226
- ) => {
227
- const index = managedIndex ?? managedProcesses.length
228
-
229
- const runArgs = ['run', '--silent', runBun ? '--bun' : '', name, ...extraArgs].filter(
230
- Boolean
231
- )
223
+ function findRunGroup(): string | null {
224
+ // check local first (in takeout monorepo)
225
+ const localPath = join(__dirname, 'run-group', 'run-group')
226
+ if (fs.existsSync(localPath)) return localPath
232
227
 
233
- const allRunningScripts = [...parentRunningScripts, ...runCommands].join(',')
234
-
235
- const proc = spawn('bun', runArgs, {
236
- stdio: ['ignore', 'pipe', 'pipe'],
237
- shell: false,
238
- detached: true,
239
- env: {
240
- ...process.env,
241
- FORCE_COLOR: '3',
242
- BUN_RUN_PARENT_SCRIPT: name,
243
- BUN_RUN_SCRIPTS: allRunningScripts,
244
- TKO_SILENT: '1',
245
- } as any,
246
- cwd: resolve(cwd),
247
- })
228
+ // check installed package
229
+ const installedPath = join(__dirname, '..', 'run-group', 'run-group')
230
+ if (fs.existsSync(installedPath)) return installedPath
248
231
 
249
- const managed: ManagedProcess = {
250
- proc,
251
- name,
252
- cwd,
253
- prefixLabel,
254
- extraArgs,
255
- index,
256
- shortcut: '',
257
- }
232
+ return null
233
+ }
258
234
 
259
- if (managedIndex !== undefined) {
260
- managedProcesses[managedIndex] = managed
261
- } else {
262
- managedProcesses.push(managed)
263
- }
235
+ // fallback: use node spawn when run-group binary not available
236
+ async function runWithNodeFallback(commands: ResolvedCommand[]) {
237
+ const { spawn } = await import('node:child_process')
238
+ const { handleProcessExit } = await import('@take-out/scripts/helpers/handleProcessExit')
239
+
240
+ const colors = [
241
+ '\x1b[36m', '\x1b[35m', '\x1b[32m', '\x1b[33m', '\x1b[34m', '\x1b[31m',
242
+ ]
243
+ const reset = '\x1b[0m'
244
+
245
+ const { addChildProcess } = handleProcessExit()
246
+ let exitCode = 0
247
+
248
+ const procs = commands.map((cmd, i) => {
249
+ const color = colors[i % colors.length]
250
+ const prefix = `${color}[${cmd.name}]${reset}`
251
+ const fullCwd = resolve(cmd.cwd)
252
+ const fullCommand = cmd.args.length > 0
253
+ ? `${cmd.command} ${cmd.args.join(' ')}`
254
+ : cmd.command
255
+
256
+ const proc = spawn('sh', ['-c', fullCommand], {
257
+ cwd: fullCwd,
258
+ stdio: ['ignore', 'pipe', 'pipe'],
259
+ env: {
260
+ ...process.env,
261
+ FORCE_COLOR: '3',
262
+ TKO_SILENT: '1',
263
+ BUN_RUN_SCRIPTS: runCommands.join(','),
264
+ },
265
+ })
264
266
 
265
- addChildProcess(proc)
267
+ addChildProcess(proc)
266
268
 
267
- proc.stdout!.on('data', (data) => {
268
- if (getIsExiting()) return
269
- const lines = data.toString().split('\n')
270
- for (const line of lines) {
271
- const stripped = line.replace(ansiPattern, '')
272
- if (stripped.startsWith('$ ')) continue
273
- if (line) console.info(`${getPrefix(index)} ${line}`)
274
- }
275
- })
269
+ proc.stdout?.on('data', (data) => {
270
+ for (const line of data.toString().split('\n')) {
271
+ if (line) console.log(`${prefix} ${line}`)
272
+ }
273
+ })
276
274
 
277
- proc.stderr!.on('data', (data) => {
278
- if (getIsExiting()) return
279
- const lines = data.toString().split('\n')
280
- for (const line of lines) {
281
- const stripped = line.replace(ansiPattern, '')
282
- if (stripped.startsWith('$ ')) continue
283
- if (line) console.error(`${getPrefix(index)} ${line}`)
284
- }
285
- })
275
+ proc.stderr?.on('data', (data) => {
276
+ for (const line of data.toString().split('\n')) {
277
+ if (line) console.error(`${prefix} ${line}`)
278
+ }
279
+ })
286
280
 
287
- proc.on('error', (error) => {
288
- console.error(`${getPrefix(index)} Failed to start: ${error.message}`)
281
+ return new Promise<number>((resolve) => {
282
+ proc.on('close', (code) => resolve(code || 0))
283
+ })
289
284
  })
290
285
 
291
- proc.on('close', (code) => {
292
- if (getIsExiting()) return
286
+ const codes = await Promise.all(procs)
287
+ exitCode = Math.max(...codes)
288
+ process.exit(exitCode)
289
+ }
293
290
 
294
- if (code && code !== 0) {
295
- console.error(`${getPrefix(index)} Process exited with code ${code}`)
291
+ async function main() {
292
+ const commands = await resolveAllCommands()
296
293
 
297
- if (watch && restarts < MAX_RESTARTS) {
298
- const newRestarts = restarts + 1
299
- console.info(`Restarting process ${name} (${newRestarts}/${MAX_RESTARTS} times)`)
300
- runScript(name, cwd, prefixLabel, newRestarts, extraArgs, index)
301
- } else {
302
- exit(1)
303
- }
304
- }
305
- })
294
+ if (commands.length === 0) {
295
+ console.error('No matching scripts found')
296
+ process.exit(0)
297
+ }
306
298
 
307
- return proc
308
- }
299
+ const runGroupPath = findRunGroup()
309
300
 
310
- function computeShortcuts() {
311
- const initials = managedProcesses.map((p) => {
312
- const words = p.prefixLabel
313
- .toLowerCase()
314
- .split(/[^a-z]+/)
315
- .filter(Boolean)
316
- return words.map((w) => w[0]).join('')
317
- })
301
+ // fallback to node if binary not available
302
+ if (!runGroupPath) {
303
+ return runWithNodeFallback(commands)
304
+ }
318
305
 
319
- const lengths = new Array(managedProcesses.length).fill(1) as number[]
306
+ // build run-group command
307
+ // format: run-group -p -t @name <cmd1> --- @name2 <cmd2> --- ...
308
+ const runGroupArgs: string[] = ['-p', '-t']
320
309
 
321
- for (let round = 0; round < 5; round++) {
322
- const shortcuts = initials.map((init, i) => init.slice(0, lengths[i]) || init)
310
+ for (let i = 0; i < commands.length; i++) {
311
+ const cmd = commands[i]!
312
+ const fullCwd = resolve(cmd.cwd)
323
313
 
324
- let hasCollision = false
325
- const groups = new Map<string, number[]>()
326
- for (let i = 0; i < shortcuts.length; i++) {
327
- const key = shortcuts[i]!
328
- if (!groups.has(key)) groups.set(key, [])
329
- groups.get(key)!.push(i)
330
- }
314
+ // wrap command with cd and env setup
315
+ // use sh -c to handle complex commands with &&, ||, etc
316
+ const envSetup = `FORCE_COLOR=3 TKO_SILENT=1 BUN_RUN_SCRIPTS=${runCommands.join(',')}`
317
+ const fullCommand = cmd.args.length > 0
318
+ ? `${cmd.command} ${cmd.args.join(' ')}`
319
+ : cmd.command
331
320
 
332
- for (const [, indices] of groups) {
333
- if (indices.length <= 1) continue
334
- hasCollision = true
335
- for (const idx of indices) {
336
- lengths[idx]!++
337
- }
338
- }
321
+ // use @name prefix for display name in parallel mode
322
+ runGroupArgs.push(`@${cmd.name}`)
323
+ runGroupArgs.push('sh', '-c', `cd "${fullCwd}" && ${envSetup} ${fullCommand}`)
339
324
 
340
- if (!hasCollision) {
341
- for (let i = 0; i < managedProcesses.length; i++) {
342
- managedProcesses[i]!.shortcut = shortcuts[i]!
343
- }
344
- return
325
+ if (i < commands.length - 1) {
326
+ runGroupArgs.push('---')
345
327
  }
346
328
  }
347
329
 
348
- for (let i = 0; i < managedProcesses.length; i++) {
349
- const sc = initials[i]!.slice(0, lengths[i]) || initials[i]!
350
- managedProcesses[i]!.shortcut = sc || String(i + 1)
351
- }
352
- }
353
-
354
- async function main() {
355
- checkNodeVersion().catch((err) => {
356
- console.error(err.message)
357
- exit(1)
358
- })
330
+ // exec run-group (replaces this process)
331
+ const { execFileSync } = await import('node:child_process')
359
332
 
360
333
  try {
361
- if (runCommands.length > 0) {
362
- const lastScript = runCommands[runCommands.length - 1]
363
-
364
- if (!noRoot) {
365
- const filteredCommands = runCommands.filter(
366
- (name) => !parentRunningScripts.includes(name)
367
- )
368
- const scriptPromises = filteredCommands.map((name) => {
369
- const scriptArgs = !flagsLast || name === lastScript ? forwardArgs : []
370
- return runScript(name, '.', name, 0, scriptArgs)
371
- })
372
-
373
- await Promise.all(scriptPromises)
374
- }
375
-
376
- const workspaceScriptMap = await mapWorkspacesToScripts(runCommands)
377
-
378
- for (const [workspace, { scripts, packageName }] of workspaceScriptMap.entries()) {
379
- const filteredScripts = scripts.filter(
380
- (scriptName) => !parentRunningScripts.includes(scriptName)
381
- )
382
- const workspaceScriptPromises = filteredScripts.map((scriptName) => {
383
- const scriptArgs = !flagsLast || scriptName === lastScript ? forwardArgs : []
384
- return runScript(
385
- scriptName,
386
- workspace,
387
- `${packageName} ${scriptName}`,
388
- 0,
389
- scriptArgs
390
- )
391
- })
392
-
393
- await Promise.all(workspaceScriptPromises)
394
- }
395
- }
396
-
397
- if (managedProcesses.length === 0) {
398
- exit(0)
399
- } else {
400
- computeShortcuts()
401
- }
402
- } catch (error) {
403
- console.error(`Error running scripts: ${error}`)
404
- exit(1)
334
+ execFileSync(runGroupPath, runGroupArgs, {
335
+ stdio: 'inherit',
336
+ env: process.env,
337
+ })
338
+ } catch (error: any) {
339
+ process.exit(error.status || 1)
405
340
  }
406
341
  }
407
342
 
408
343
  main().catch((error) => {
409
344
  console.error(`Error running scripts: ${error}`)
410
- exit(1)
345
+ process.exit(1)
411
346
  })