@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.
Files changed (3) hide show
  1. package/package.json +2 -2
  2. package/src/run-pty.mjs +126 -113
  3. 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.20",
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.20",
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;245m',
10
- '\x1b[38;5;240m',
11
- '\x1b[38;5;243m',
12
- '\x1b[38;5;248m',
13
- '\x1b[38;5;238m',
14
- '\x1b[38;5;252m',
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 dashboardMode = false
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} ${p.label}${reset}`
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
- processes[index] = managed
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 (dashboardMode) return
155
- const lines = data.split('\n')
156
- for (const line of lines) {
157
- if (line && line !== '\r') {
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
- process.stdout.write(`${getPrefix(idx)} exited with code ${exitCode}\n`)
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
- const initials = processes.map((p) =>
175
- p.label
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
- .map((w) => w[0])
180
- .join('')
181
- )
182
- const lengths = new Array(processes.length).fill(1)
183
-
184
- for (let round = 0; round < 5; round++) {
185
- const shortcuts = initials.map((init, i) => init.slice(0, lengths[i]) || init)
186
- const groups = new Map()
187
- shortcuts.forEach((s, i) => groups.set(s, [...(groups.get(s) || []), i]))
188
-
189
- let collision = false
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 renderDashboard() {
207
- process.stdout.write('\x1b[2J\x1b[H')
208
- process.stdout.write(
209
- `${bold}Dashboard${reset} ${dim}(↑↓ navigate, r restart, k kill, esc back)${reset}\n\n`
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
- function exitDashboard() {
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 (dashboardMode) {
243
- exitDashboard()
226
+ if (focusedIndex >= 0) {
227
+ focusedIndex = -1
228
+ showDashboard()
244
229
  } else {
245
- dashboardMode = true
246
- renderDashboard()
230
+ showDashboard()
247
231
  }
248
232
  return
249
233
  }
250
234
 
251
- if (dashboardMode) {
252
- if (str === '\x1b[A') {
253
- selectedIndex = Math.max(0, selectedIndex - 1)
254
- renderDashboard()
255
- } else if (str === '\x1b[B') {
256
- selectedIndex = Math.min(processes.length - 1, selectedIndex + 1)
257
- renderDashboard()
258
- } else if (str === 'r' || str === 'R') {
259
- const p = processes[selectedIndex]
260
- if (p) {
261
- p.killed = true
262
- p.terminal.kill()
263
- setTimeout(() => {
264
- spawnScript(p.name, p.cwd, p.label, p.extraArgs, p.index)
265
- exitDashboard()
266
- }, 100)
267
- }
268
- } else if (str === 'k' || str === 'K') {
269
- const p = processes[selectedIndex]
270
- if (p && !p.killed) {
271
- p.killed = true
272
- p.terminal.kill()
273
- renderDashboard()
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 if (str === '\x1b' || str === '\x1b\x1b') {
276
- exitDashboard()
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
- process.stdout.write('\n')
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
- const scriptArgs = !flagsLast || name === lastScript ? forwardArgs : []
301
- spawnScript(name, '.', name, scriptArgs)
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
- const scriptArgs = !flagsLast || script === lastScript ? forwardArgs : []
311
- spawnScript(script, dir, `${name} ${script}`, scriptArgs)
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
- process.stdout.write(
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(cols, rows)
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 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
  })