@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@take-out/scripts",
3
- "version": "0.1.19",
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
- "@take-out/helpers": "0.1.19",
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(ctx: SSHContext): Promise<{
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
- loopingServices.push(name)
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
 
@@ -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 or sequence
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', // medium gray
19
- '\x1b[38;5;240m', // darker gray
20
- '\x1b[38;5;250m', // lighter gray
21
- '\x1b[38;5;243m', // medium-dark gray
22
- '\x1b[38;5;248m', // medium-light gray
23
- '\x1b[38;5;238m', // dark gray
24
- '\x1b[38;5;252m', // light gray
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
- // handle flags
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++ // skip the value in next iteration
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') // just attempts to restart a failed process up to MAX_RESTARTS times
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
- // parse --stdin=<script-name> to specify which script receives keyboard input
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
- log.error('Please provide at least one script name to run')
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
- const subdirResults = await findPackageJsonDirs(path, maxDepth - 1)
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 (error) {
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
- let normalized = path.replace(/\\/g, '/')
158
+ const normalized = path.replace(/\\/g, '/')
196
159
  return normalized.startsWith('./') ? normalized.substring(2) : normalized
197
160
  }
198
161
 
199
- const workspaceDirs = allPackageDirs.filter((dir) => {
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: ['pipe', 'pipe', 'pipe'],
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) log.output(`${getPrefix(index)} ${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 = dataStr.split('\n')
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) log.error(`${getPrefix(index)} ${line}`)
282
+ if (line) console.error(`${getPrefix(index)} ${line}`)
338
283
  }
339
284
  })
340
285
 
341
286
  proc.on('error', (error) => {
342
- log.error(`${getPrefix(index)} Failed to start: ${error.message}`)
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
- log.error(`${getPrefix(index)} Process exited with code ${code}`)
294
+ console.error(`${getPrefix(index)} Process exited with code ${code}`)
354
295
 
355
- if (code === 1) {
356
- console.error('\x1b[31m❌ Run Failed\x1b[0m')
357
- console.error(
358
- `\x1b[31mProcess "${prefixLabel}" failed with exit code ${code}\x1b[0m`
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
- log.error(err.message)
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
- // --flags=last: only forward args to last script
643
- const args = !flagsLast || name === lastScript ? forwardArgs : []
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
- // --flags=last: only forward args to last script
658
- const args = !flagsLast || scriptName === lastScript ? forwardArgs : []
659
- return runScript(scriptName, workspace, `${packageName} ${scriptName}`, 0, args)
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
- log.error(`Error running scripts: ${error}`)
402
+ console.error(`Error running scripts: ${error}`)
675
403
  exit(1)
676
404
  }
677
405
  }
678
406
 
679
407
  main().catch((error) => {
680
- log.error(`Error running scripts: ${error}`)
408
+ console.error(`Error running scripts: ${error}`)
681
409
  exit(1)
682
410
  })