@take-out/scripts 0.1.39 → 0.1.41
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 +6 -5
- package/src/build-initial.ts +1 -1
- package/src/dev-tunnel.ts +1 -1
- package/src/helpers/run.ts +14 -24
- package/src/release.ts +1 -1
- package/src/helpers/handleProcessExit.ts +0 -171
- package/src/run-pty.mjs +0 -413
- package/src/run.ts +0 -411
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@take-out/scripts",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.41",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"main": "./src/
|
|
5
|
+
"main": "./src/cmd.ts",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"exports": {
|
|
8
8
|
".": {
|
|
9
|
-
"types": "./src/
|
|
10
|
-
"default": "./src/
|
|
9
|
+
"types": "./src/cmd.ts",
|
|
10
|
+
"default": "./src/cmd.ts"
|
|
11
11
|
},
|
|
12
12
|
"./package.json": "./package.json",
|
|
13
13
|
"./helpers/*": {
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@clack/prompts": "^0.8.2",
|
|
32
32
|
"@lydell/node-pty": "^1.2.0-beta.3",
|
|
33
|
-
"@take-out/helpers": "0.1.
|
|
33
|
+
"@take-out/helpers": "0.1.41",
|
|
34
|
+
"@take-out/run": "0.1.41",
|
|
34
35
|
"picocolors": "^1.1.1"
|
|
35
36
|
},
|
|
36
37
|
"peerDependencies": {
|
package/src/build-initial.ts
CHANGED
|
@@ -25,7 +25,7 @@ await cmd`bootstrap project workspace and build initial packages`.run(
|
|
|
25
25
|
await $`cd packages/helpers && bun run build`
|
|
26
26
|
|
|
27
27
|
// then build all other packages in parallel
|
|
28
|
-
await $`bun ./packages/
|
|
28
|
+
await $`bun ./packages/run/src/run.ts build --no-root`
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
package/src/dev-tunnel.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { cmd } from './cmd'
|
|
|
7
7
|
await cmd`set up cloudflare dev tunnel for local development`
|
|
8
8
|
.args('--port number')
|
|
9
9
|
.run(async ({ args, run, os, path }) => {
|
|
10
|
-
const { handleProcessExit } = await import('
|
|
10
|
+
const { handleProcessExit } = await import('@take-out/run/helpers/handleProcessExit')
|
|
11
11
|
|
|
12
12
|
handleProcessExit()
|
|
13
13
|
|
package/src/helpers/run.ts
CHANGED
|
@@ -1,23 +1,17 @@
|
|
|
1
|
-
import { spawn
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
2
|
import { cpus } from 'node:os'
|
|
3
3
|
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
let isInExitCleanup = false
|
|
11
|
-
|
|
12
|
-
export function setExitCleanupState(state: boolean) {
|
|
13
|
-
isInExitCleanup = state
|
|
14
|
-
}
|
|
4
|
+
import {
|
|
5
|
+
getIsExiting,
|
|
6
|
+
notifyProcessHandlers,
|
|
7
|
+
type ProcessHandler,
|
|
8
|
+
type ProcessType,
|
|
9
|
+
} from '@take-out/run/helpers/process-state'
|
|
15
10
|
|
|
16
|
-
|
|
17
|
-
return isInExitCleanup
|
|
18
|
-
}
|
|
11
|
+
import type { Timer } from '@take-out/helpers'
|
|
19
12
|
|
|
20
|
-
|
|
13
|
+
export type { ProcessHandler, ProcessType }
|
|
14
|
+
export { getIsExiting }
|
|
21
15
|
|
|
22
16
|
const colors = [
|
|
23
17
|
'\x1b[36m', // cyan
|
|
@@ -105,7 +99,7 @@ export async function run(
|
|
|
105
99
|
shell.unref()
|
|
106
100
|
}
|
|
107
101
|
|
|
108
|
-
|
|
102
|
+
notifyProcessHandlers(shell)
|
|
109
103
|
|
|
110
104
|
if (timeout) {
|
|
111
105
|
timeoutId = setTimeout(() => {
|
|
@@ -206,7 +200,7 @@ export async function run(
|
|
|
206
200
|
? `Command timed out after ${timeout}ms: ${command}`
|
|
207
201
|
: `Command failed with exit code ${exitCode}: ${command}`
|
|
208
202
|
|
|
209
|
-
if (!silent && !
|
|
203
|
+
if (!silent && !getIsExiting()) {
|
|
210
204
|
console.error(`run() error: ${errorMsg}: ${stderr || ''}`)
|
|
211
205
|
}
|
|
212
206
|
|
|
@@ -218,14 +212,14 @@ export async function run(
|
|
|
218
212
|
return { stdout, stderr, exitCode }
|
|
219
213
|
} catch (error) {
|
|
220
214
|
clearTimeout(timeoutId)
|
|
221
|
-
if (!silent && !
|
|
215
|
+
if (!silent && !getIsExiting()) {
|
|
222
216
|
// only show the error message, not the full object if it's our error
|
|
223
217
|
if (error instanceof Error && (error as any).cause?.exitCode !== undefined) {
|
|
224
218
|
// this is our controlled error, already logged above
|
|
225
219
|
} else {
|
|
226
220
|
console.error(`Error running command: ${command}`, error)
|
|
227
221
|
}
|
|
228
|
-
} else if (!silent &&
|
|
222
|
+
} else if (!silent && getIsExiting()) {
|
|
229
223
|
// simple message when being killed due to another error
|
|
230
224
|
const shortCmd = command.split(' ')[0]
|
|
231
225
|
console.error(`${shortCmd} exiting due to earlier error`)
|
|
@@ -235,10 +229,6 @@ export async function run(
|
|
|
235
229
|
}
|
|
236
230
|
}
|
|
237
231
|
|
|
238
|
-
export const addProcessHandler = (cb: ProcessHandler) => {
|
|
239
|
-
processHandlers.add(cb)
|
|
240
|
-
}
|
|
241
|
-
|
|
242
232
|
export async function waitForRun(name: string) {
|
|
243
233
|
if (running[name] === undefined) {
|
|
244
234
|
throw new Error(`Can't wait before task runs: ${name}`)
|
package/src/release.ts
CHANGED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
addProcessHandler,
|
|
3
|
-
setExitCleanupState,
|
|
4
|
-
type ProcessHandler,
|
|
5
|
-
type ProcessType,
|
|
6
|
-
} from './run'
|
|
7
|
-
|
|
8
|
-
type ExitCallback = (info: { signal: NodeJS.Signals | string }) => void | Promise<void>
|
|
9
|
-
|
|
10
|
-
interface HandleProcessExitReturn {
|
|
11
|
-
addChildProcess: ProcessHandler
|
|
12
|
-
cleanup: () => Promise<void>
|
|
13
|
-
exit: (code?: number) => Promise<void>
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// kill an entire process group (works because children are spawned with detached: true,
|
|
17
|
-
// which makes them process group leaders). negative pid = kill the whole group.
|
|
18
|
-
// this is synchronous, no pgrep needed, no races.
|
|
19
|
-
function killProcessGroup(
|
|
20
|
-
pid: number,
|
|
21
|
-
signal: NodeJS.Signals = 'SIGTERM',
|
|
22
|
-
forceful: boolean = false
|
|
23
|
-
): void {
|
|
24
|
-
// kill the process group (negative pid)
|
|
25
|
-
try {
|
|
26
|
-
process.kill(-pid, signal)
|
|
27
|
-
} catch (_) {
|
|
28
|
-
// group may already be gone, try the individual process
|
|
29
|
-
try {
|
|
30
|
-
process.kill(pid, signal)
|
|
31
|
-
} catch (_) {
|
|
32
|
-
// process already gone
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (forceful && signal !== 'SIGKILL') {
|
|
37
|
-
// schedule a SIGKILL followup
|
|
38
|
-
setTimeout(() => {
|
|
39
|
-
try {
|
|
40
|
-
process.kill(-pid, 'SIGKILL')
|
|
41
|
-
} catch (_) {}
|
|
42
|
-
try {
|
|
43
|
-
process.kill(pid, 'SIGKILL')
|
|
44
|
-
} catch (_) {}
|
|
45
|
-
}, 100)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
let isHandling = false
|
|
50
|
-
|
|
51
|
-
export function handleProcessExit({
|
|
52
|
-
onExit,
|
|
53
|
-
}: {
|
|
54
|
-
onExit?: ExitCallback
|
|
55
|
-
} = {}): HandleProcessExitReturn {
|
|
56
|
-
if (isHandling) {
|
|
57
|
-
throw new Error(`Only one handleProcessExit per process should be registered!`)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
isHandling = true
|
|
61
|
-
const processes: ProcessType[] = []
|
|
62
|
-
let cleanupPromise: Promise<void> | null = null
|
|
63
|
-
|
|
64
|
-
const cleanup = (signal: NodeJS.Signals | string = 'SIGTERM'): Promise<void> => {
|
|
65
|
-
// return existing cleanup promise if already running, so process.exit
|
|
66
|
-
// override waits for the real cleanup instead of exiting early
|
|
67
|
-
if (cleanupPromise) return cleanupPromise
|
|
68
|
-
|
|
69
|
-
cleanupPromise = doCleanup(signal)
|
|
70
|
-
return cleanupPromise
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const doCleanup = async (signal: NodeJS.Signals | string) => {
|
|
74
|
-
setExitCleanupState(true)
|
|
75
|
-
|
|
76
|
-
if (onExit) {
|
|
77
|
-
try {
|
|
78
|
-
await onExit({ signal })
|
|
79
|
-
} catch (error) {
|
|
80
|
-
console.error('Error in exit callback:', error)
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (processes.length === 0) {
|
|
85
|
-
return
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// kill process groups synchronously - no pgrep, no races
|
|
89
|
-
// detached: true makes each child a process group leader,
|
|
90
|
-
// so kill(-pid) gets the entire group in one syscall
|
|
91
|
-
const isInterrupt = signal === 'SIGINT'
|
|
92
|
-
const killSignal = isInterrupt ? 'SIGTERM' : (signal as NodeJS.Signals)
|
|
93
|
-
|
|
94
|
-
for (const proc of processes) {
|
|
95
|
-
if (proc.pid) {
|
|
96
|
-
killProcessGroup(proc.pid, killSignal, isInterrupt)
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// brief wait for graceful shutdown
|
|
101
|
-
await new Promise((res) => setTimeout(res, isInterrupt ? 80 : 200))
|
|
102
|
-
|
|
103
|
-
// force kill any remaining
|
|
104
|
-
for (const proc of processes) {
|
|
105
|
-
if (proc.pid && !proc.exitCode) {
|
|
106
|
-
killProcessGroup(proc.pid, 'SIGKILL')
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const addChildProcess = (proc: ProcessType) => {
|
|
112
|
-
processes.push(proc)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
addProcessHandler(addChildProcess)
|
|
116
|
-
|
|
117
|
-
let finalized = false
|
|
118
|
-
|
|
119
|
-
const finalizeWithSignal = (signal: NodeJS.Signals, fallbackCode: number) => {
|
|
120
|
-
if (finalized) return
|
|
121
|
-
finalized = true
|
|
122
|
-
process.off('SIGINT', sigintHandler)
|
|
123
|
-
process.off('SIGTERM', sigtermHandler)
|
|
124
|
-
process.off('beforeExit', beforeExitHandler)
|
|
125
|
-
process.exit = originalExit
|
|
126
|
-
|
|
127
|
-
try {
|
|
128
|
-
process.kill(process.pid, signal)
|
|
129
|
-
} catch {
|
|
130
|
-
originalExit(fallbackCode)
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const sigtermHandler = () => {
|
|
135
|
-
cleanup('SIGTERM').then(() => {
|
|
136
|
-
finalizeWithSignal('SIGTERM', 143)
|
|
137
|
-
})
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const sigintHandler = () => {
|
|
141
|
-
cleanup('SIGINT').then(() => {
|
|
142
|
-
finalizeWithSignal('SIGINT', 130)
|
|
143
|
-
})
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// intercept process.exit to ensure cleanup completes before exiting.
|
|
147
|
-
// if cleanup is already running, this awaits the SAME promise instead of
|
|
148
|
-
// early-returning and calling originalExit prematurely.
|
|
149
|
-
const originalExit = process.exit
|
|
150
|
-
process.exit = ((code?: number) => {
|
|
151
|
-
cleanup('SIGTERM').then(() => {
|
|
152
|
-
originalExit(code)
|
|
153
|
-
})
|
|
154
|
-
}) as typeof process.exit
|
|
155
|
-
|
|
156
|
-
const beforeExitHandler = () => cleanup('SIGTERM')
|
|
157
|
-
process.on('beforeExit', beforeExitHandler)
|
|
158
|
-
process.on('SIGINT', sigintHandler)
|
|
159
|
-
process.on('SIGTERM', sigtermHandler)
|
|
160
|
-
|
|
161
|
-
const exit = async (code: number = 0) => {
|
|
162
|
-
await cleanup('SIGTERM')
|
|
163
|
-
process.exit(code)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
addChildProcess,
|
|
168
|
-
cleanup,
|
|
169
|
-
exit,
|
|
170
|
-
}
|
|
171
|
-
}
|
package/src/run-pty.mjs
DELETED
|
@@ -1,413 +0,0 @@
|
|
|
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
|
-
|
|
19
|
-
const args = process.argv.slice(2)
|
|
20
|
-
const ownFlags = ['--no-root', '--bun', '--watch', '--flags=last']
|
|
21
|
-
const runCommands = []
|
|
22
|
-
const forwardArgs = []
|
|
23
|
-
|
|
24
|
-
for (let i = 0; i < args.length; i++) {
|
|
25
|
-
const arg = args[i]
|
|
26
|
-
if (arg.startsWith('--')) {
|
|
27
|
-
if (ownFlags.includes(arg)) continue
|
|
28
|
-
forwardArgs.push(arg)
|
|
29
|
-
const nextArg = args[i + 1]
|
|
30
|
-
if (nextArg && !nextArg.startsWith('--')) {
|
|
31
|
-
forwardArgs.push(nextArg)
|
|
32
|
-
i++
|
|
33
|
-
}
|
|
34
|
-
} else {
|
|
35
|
-
runCommands.push(arg)
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const noRoot = args.includes('--no-root')
|
|
40
|
-
const runBun = args.includes('--bun')
|
|
41
|
-
const flagsLast = args.includes('--flags=last')
|
|
42
|
-
|
|
43
|
-
const processes = []
|
|
44
|
-
let focusedIndex = -1 // -1 = interleaved/dashboard
|
|
45
|
-
|
|
46
|
-
function getPrefix(index) {
|
|
47
|
-
const p = processes[index]
|
|
48
|
-
if (!p) return ''
|
|
49
|
-
const color = colors[index % colors.length]
|
|
50
|
-
return `${color}${p.shortcut}${reset}`
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (runCommands.length === 0) {
|
|
54
|
-
console.error('Usage: run-pty <script1> [script2] ...')
|
|
55
|
-
process.exit(1)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function readPackageJson(dir) {
|
|
59
|
-
try {
|
|
60
|
-
return JSON.parse(await fs.promises.readFile(join(dir, 'package.json'), 'utf8'))
|
|
61
|
-
} catch {
|
|
62
|
-
return null
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function getWorkspacePatterns() {
|
|
67
|
-
const pkg = await readPackageJson('.')
|
|
68
|
-
if (!pkg?.workspaces) return []
|
|
69
|
-
return Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages || []
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async function findPackageJsonDirs(base, depth = 3) {
|
|
73
|
-
if (depth <= 0) return []
|
|
74
|
-
const results = []
|
|
75
|
-
try {
|
|
76
|
-
if (
|
|
77
|
-
await fs.promises
|
|
78
|
-
.access(join(base, 'package.json'))
|
|
79
|
-
.then(() => true)
|
|
80
|
-
.catch(() => false)
|
|
81
|
-
) {
|
|
82
|
-
results.push(base)
|
|
83
|
-
}
|
|
84
|
-
const entries = await fs.promises.readdir(base, { withFileTypes: true })
|
|
85
|
-
for (const e of entries) {
|
|
86
|
-
if (e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules') {
|
|
87
|
-
results.push(...(await findPackageJsonDirs(join(base, e.name), depth - 1)))
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
} catch {}
|
|
91
|
-
return results
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function findWorkspaceScripts(scripts) {
|
|
95
|
-
const patterns = await getWorkspacePatterns()
|
|
96
|
-
if (!patterns.length) return new Map()
|
|
97
|
-
const dirs = await findPackageJsonDirs('.')
|
|
98
|
-
const result = new Map()
|
|
99
|
-
for (const dir of dirs) {
|
|
100
|
-
if (dir === '.') continue
|
|
101
|
-
const rel = relative('.', dir).replace(/\\/g, '/')
|
|
102
|
-
const matches = patterns.some((p) => {
|
|
103
|
-
const np = p.replace(/\\/g, '/').replace(/^\.\//, '')
|
|
104
|
-
return np.endsWith('/*')
|
|
105
|
-
? rel.startsWith(np.slice(0, -1))
|
|
106
|
-
: rel === np || rel.startsWith(np + '/')
|
|
107
|
-
})
|
|
108
|
-
if (!matches) continue
|
|
109
|
-
const pkg = await readPackageJson(dir)
|
|
110
|
-
if (!pkg?.scripts) continue
|
|
111
|
-
const available = scripts.filter((s) => typeof pkg.scripts[s] === 'string')
|
|
112
|
-
if (available.length) result.set(dir, { scripts: available, name: pkg.name || dir })
|
|
113
|
-
}
|
|
114
|
-
return result
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function spawnScript(name, cwd, label, extraArgs, index) {
|
|
118
|
-
const runArgs = ['run', '--silent', runBun ? '--bun' : '', name, ...extraArgs].filter(
|
|
119
|
-
Boolean
|
|
120
|
-
)
|
|
121
|
-
const terminal = pty.spawn('bun', runArgs, {
|
|
122
|
-
cwd: resolve(cwd),
|
|
123
|
-
cols: process.stdout.columns || 80,
|
|
124
|
-
rows: process.stdout.rows || 24,
|
|
125
|
-
env: { ...process.env, FORCE_COLOR: '3' },
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
const idx = index ?? processes.length
|
|
129
|
-
const managed = {
|
|
130
|
-
terminal,
|
|
131
|
-
name,
|
|
132
|
-
cwd,
|
|
133
|
-
label,
|
|
134
|
-
extraArgs,
|
|
135
|
-
index: idx,
|
|
136
|
-
shortcut: '',
|
|
137
|
-
killed: false,
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (index !== undefined) processes[index] = managed
|
|
141
|
-
else processes.push(managed)
|
|
142
|
-
|
|
143
|
-
terminal.onData((data) => {
|
|
144
|
-
if (focusedIndex === -1) {
|
|
145
|
-
// interleaved - prefix each line, skip bun's $ command echo
|
|
146
|
-
const lines = data.split(/\r?\n/)
|
|
147
|
-
for (const line of lines) {
|
|
148
|
-
if (!line) continue
|
|
149
|
-
// eslint-disable-next-line no-control-regex
|
|
150
|
-
const stripped = line.replace(/\x1b\[[0-9;]*m/g, '')
|
|
151
|
-
if (stripped.startsWith('$ ')) continue
|
|
152
|
-
process.stdout.write(`${getPrefix(idx)} ${line}\n`)
|
|
153
|
-
}
|
|
154
|
-
} else if (focusedIndex === idx) {
|
|
155
|
-
// focused - raw output
|
|
156
|
-
process.stdout.write(data)
|
|
157
|
-
}
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
terminal.onExit(({ exitCode }) => {
|
|
161
|
-
if (managed.killed) return
|
|
162
|
-
if (focusedIndex === idx) {
|
|
163
|
-
focusedIndex = -1
|
|
164
|
-
showDashboard()
|
|
165
|
-
}
|
|
166
|
-
if (exitCode !== 0) {
|
|
167
|
-
console.error(`${getPrefix(idx)} exited ${exitCode}`)
|
|
168
|
-
}
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
return managed
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function computeShortcuts() {
|
|
175
|
-
// use last word of label
|
|
176
|
-
for (let i = 0; i < processes.length; i++) {
|
|
177
|
-
const p = processes[i]
|
|
178
|
-
const words = p.label
|
|
179
|
-
.toLowerCase()
|
|
180
|
-
.split(/[^a-z]+/)
|
|
181
|
-
.filter(Boolean)
|
|
182
|
-
const lastWord = words[words.length - 1] || words[0] || String(i)
|
|
183
|
-
|
|
184
|
-
// find unique shortcut
|
|
185
|
-
let shortcut = lastWord[0]
|
|
186
|
-
let len = 1
|
|
187
|
-
while (
|
|
188
|
-
processes.slice(0, i).some((q) => q.shortcut === shortcut) &&
|
|
189
|
-
len < lastWord.length
|
|
190
|
-
) {
|
|
191
|
-
len++
|
|
192
|
-
shortcut = lastWord.slice(0, len)
|
|
193
|
-
}
|
|
194
|
-
p.shortcut = shortcut
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function showDashboard() {
|
|
199
|
-
const tabs = processes
|
|
200
|
-
.map((p, i) => {
|
|
201
|
-
const color = colors[i % colors.length]
|
|
202
|
-
return `${color}[${p.shortcut}]${reset} ${p.label.split(' ').pop()}${p.killed ? dim + ' ✗' + reset : ''}`
|
|
203
|
-
})
|
|
204
|
-
.join(' ')
|
|
205
|
-
console.log(`\n${tabs}`)
|
|
206
|
-
console.log(
|
|
207
|
-
`${dim}press shortcut to focus, r+shortcut restart, k+shortcut kill, ctrl+c exit${reset}\n`
|
|
208
|
-
)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
let pendingAction = null // 'r' or 'k'
|
|
212
|
-
|
|
213
|
-
function handleInput(data) {
|
|
214
|
-
const str = data.toString()
|
|
215
|
-
|
|
216
|
-
// ctrl+c exit
|
|
217
|
-
if (str === '\x03') {
|
|
218
|
-
cleanup()
|
|
219
|
-
return
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// ctrl+z toggle focus
|
|
223
|
-
if (str === '\x1a') {
|
|
224
|
-
if (focusedIndex >= 0) {
|
|
225
|
-
focusedIndex = -1
|
|
226
|
-
showDashboard()
|
|
227
|
-
} else {
|
|
228
|
-
showDashboard()
|
|
229
|
-
}
|
|
230
|
-
return
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// if focused, forward to process
|
|
234
|
-
if (focusedIndex >= 0) {
|
|
235
|
-
const p = processes[focusedIndex]
|
|
236
|
-
if (p && !p.killed) {
|
|
237
|
-
p.terminal.write(str)
|
|
238
|
-
}
|
|
239
|
-
return
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// dashboard mode
|
|
243
|
-
if (str === 'r') {
|
|
244
|
-
pendingAction = 'restart'
|
|
245
|
-
process.stdout.write(`${dim}restart which? ${reset}`)
|
|
246
|
-
return
|
|
247
|
-
}
|
|
248
|
-
if (str === 'k') {
|
|
249
|
-
pendingAction = 'kill'
|
|
250
|
-
process.stdout.write(`${dim}kill which? ${reset}`)
|
|
251
|
-
return
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// check for shortcut match
|
|
255
|
-
const match = processes.find((p) => p.shortcut === str.toLowerCase())
|
|
256
|
-
if (match) {
|
|
257
|
-
if (pendingAction === 'restart') {
|
|
258
|
-
pendingAction = null
|
|
259
|
-
console.log(match.shortcut)
|
|
260
|
-
match.killed = true
|
|
261
|
-
killProcessTree(match.terminal.pid)
|
|
262
|
-
setTimeout(() => {
|
|
263
|
-
spawnScript(match.name, match.cwd, match.label, match.extraArgs, match.index)
|
|
264
|
-
console.log(`${getPrefix(match.index)} restarted`)
|
|
265
|
-
}, 100)
|
|
266
|
-
} else if (pendingAction === 'kill') {
|
|
267
|
-
pendingAction = null
|
|
268
|
-
console.log(match.shortcut)
|
|
269
|
-
if (!match.killed) {
|
|
270
|
-
match.killed = true
|
|
271
|
-
killProcessTree(match.terminal.pid)
|
|
272
|
-
console.log(`${getPrefix(match.index)} killed`)
|
|
273
|
-
}
|
|
274
|
-
} else {
|
|
275
|
-
// focus
|
|
276
|
-
focusedIndex = match.index
|
|
277
|
-
console.log(`${dim}focused: ${match.label} (ctrl+z to unfocus)${reset}\n`)
|
|
278
|
-
}
|
|
279
|
-
return
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// escape cancels pending
|
|
283
|
-
if (str === '\x1b' && pendingAction) {
|
|
284
|
-
pendingAction = null
|
|
285
|
-
console.log('cancelled')
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// kill entire process group to prevent orphans
|
|
290
|
-
function killProcessTree(pid) {
|
|
291
|
-
if (!pid) return
|
|
292
|
-
// try killing process group first (negative pid)
|
|
293
|
-
try {
|
|
294
|
-
process.kill(-pid, 'SIGTERM')
|
|
295
|
-
} catch {}
|
|
296
|
-
// also kill direct process
|
|
297
|
-
try {
|
|
298
|
-
process.kill(pid, 'SIGTERM')
|
|
299
|
-
} catch {}
|
|
300
|
-
// schedule force kill
|
|
301
|
-
setTimeout(() => {
|
|
302
|
-
try {
|
|
303
|
-
process.kill(-pid, 'SIGKILL')
|
|
304
|
-
} catch {}
|
|
305
|
-
try {
|
|
306
|
-
process.kill(pid, 'SIGKILL')
|
|
307
|
-
} catch {}
|
|
308
|
-
}, 100)
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function cleanup() {
|
|
312
|
-
// restore terminal to cooked mode before exiting
|
|
313
|
-
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
314
|
-
try {
|
|
315
|
-
process.stdin.setRawMode(false)
|
|
316
|
-
} catch {}
|
|
317
|
-
}
|
|
318
|
-
// reset terminal attributes (colors, etc)
|
|
319
|
-
process.stdout.write('\x1b[0m\n')
|
|
320
|
-
|
|
321
|
-
for (const p of processes) {
|
|
322
|
-
if (!p.killed) {
|
|
323
|
-
p.killed = true
|
|
324
|
-
killProcessTree(p.terminal.pid)
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// wait for kills to complete before exit
|
|
329
|
-
setTimeout(() => process.exit(0), 150)
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
async function main() {
|
|
333
|
-
const lastScript = runCommands[runCommands.length - 1]
|
|
334
|
-
|
|
335
|
-
if (!noRoot) {
|
|
336
|
-
const pkg = await readPackageJson('.')
|
|
337
|
-
if (pkg?.scripts) {
|
|
338
|
-
for (const name of runCommands) {
|
|
339
|
-
if (typeof pkg.scripts[name] === 'string') {
|
|
340
|
-
spawnScript(
|
|
341
|
-
name,
|
|
342
|
-
'.',
|
|
343
|
-
name,
|
|
344
|
-
!flagsLast || name === lastScript ? forwardArgs : []
|
|
345
|
-
)
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const wsScripts = await findWorkspaceScripts(runCommands)
|
|
352
|
-
for (const [dir, { scripts, name }] of wsScripts) {
|
|
353
|
-
for (const script of scripts) {
|
|
354
|
-
spawnScript(
|
|
355
|
-
script,
|
|
356
|
-
dir,
|
|
357
|
-
`${name} ${script}`,
|
|
358
|
-
!flagsLast || script === lastScript ? forwardArgs : []
|
|
359
|
-
)
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
if (processes.length === 0) {
|
|
364
|
-
console.error('No scripts found')
|
|
365
|
-
process.exit(1)
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
computeShortcuts()
|
|
369
|
-
showDashboard()
|
|
370
|
-
|
|
371
|
-
if (process.stdin.isTTY) {
|
|
372
|
-
process.stdin.setRawMode(true)
|
|
373
|
-
process.stdin.resume()
|
|
374
|
-
process.stdin.on('data', handleInput)
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
process.stdout.on('resize', () => {
|
|
378
|
-
for (const p of processes) {
|
|
379
|
-
if (!p.killed) {
|
|
380
|
-
try {
|
|
381
|
-
p.terminal.resize(process.stdout.columns || 80, process.stdout.rows || 24)
|
|
382
|
-
} catch {
|
|
383
|
-
// pty already closed
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
})
|
|
388
|
-
|
|
389
|
-
process.on('SIGINT', cleanup)
|
|
390
|
-
process.on('SIGTERM', cleanup)
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// restore terminal on any unexpected exit
|
|
394
|
-
function restoreTerminal() {
|
|
395
|
-
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
396
|
-
try {
|
|
397
|
-
process.stdin.setRawMode(false)
|
|
398
|
-
} catch {}
|
|
399
|
-
}
|
|
400
|
-
process.stdout.write('\x1b[0m')
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
process.on('uncaughtException', (e) => {
|
|
404
|
-
restoreTerminal()
|
|
405
|
-
console.error(e)
|
|
406
|
-
process.exit(1)
|
|
407
|
-
})
|
|
408
|
-
|
|
409
|
-
main().catch((e) => {
|
|
410
|
-
restoreTerminal()
|
|
411
|
-
console.error(e)
|
|
412
|
-
process.exit(1)
|
|
413
|
-
})
|
package/src/run.ts
DELETED
|
@@ -1,411 +0,0 @@
|
|
|
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 '@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
|
|
30
|
-
|
|
31
|
-
const args = process.argv.slice(2)
|
|
32
|
-
const ownFlags = ['--no-root', '--bun', '--watch', '--flags=last']
|
|
33
|
-
const runCommands: string[] = []
|
|
34
|
-
const forwardArgs: string[] = []
|
|
35
|
-
|
|
36
|
-
for (let i = 0; i < args.length; i++) {
|
|
37
|
-
const arg = args[i]!
|
|
38
|
-
|
|
39
|
-
if (arg.startsWith('--')) {
|
|
40
|
-
if (ownFlags.includes(arg)) continue
|
|
41
|
-
forwardArgs.push(arg)
|
|
42
|
-
const nextArg = args[i + 1]
|
|
43
|
-
if (nextArg && !nextArg.startsWith('--')) {
|
|
44
|
-
forwardArgs.push(nextArg)
|
|
45
|
-
i++
|
|
46
|
-
}
|
|
47
|
-
} else {
|
|
48
|
-
runCommands.push(arg)
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const noRoot = args.includes('--no-root')
|
|
53
|
-
const runBun = args.includes('--bun')
|
|
54
|
-
const watch = args.includes('--watch')
|
|
55
|
-
const flagsLast = args.includes('--flags=last')
|
|
56
|
-
|
|
57
|
-
const MAX_RESTARTS = 3
|
|
58
|
-
|
|
59
|
-
const parentRunningScripts = process.env.BUN_RUN_SCRIPTS
|
|
60
|
-
? process.env.BUN_RUN_SCRIPTS.split(',')
|
|
61
|
-
: []
|
|
62
|
-
|
|
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
|
-
if (runCommands.length === 0) {
|
|
85
|
-
console.error('Please provide at least one script name to run')
|
|
86
|
-
exit(1)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async function readPackageJson(directoryPath: string) {
|
|
90
|
-
try {
|
|
91
|
-
const packageJsonPath = join(directoryPath, 'package.json')
|
|
92
|
-
const content = await fs.promises.readFile(packageJsonPath, 'utf8')
|
|
93
|
-
return JSON.parse(content)
|
|
94
|
-
} catch {
|
|
95
|
-
return null
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async function getWorkspacePatterns(): Promise<string[]> {
|
|
100
|
-
try {
|
|
101
|
-
const packageJson = await readPackageJson('.')
|
|
102
|
-
if (!packageJson || !packageJson.workspaces) return []
|
|
103
|
-
|
|
104
|
-
return Array.isArray(packageJson.workspaces)
|
|
105
|
-
? packageJson.workspaces
|
|
106
|
-
: packageJson.workspaces.packages || []
|
|
107
|
-
} catch {
|
|
108
|
-
return []
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function hasPackageJson(path: string): Promise<boolean> {
|
|
113
|
-
try {
|
|
114
|
-
await fs.promises.access(join(path, 'package.json'))
|
|
115
|
-
return true
|
|
116
|
-
} catch {
|
|
117
|
-
return false
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async function findPackageJsonDirs(basePath: string, maxDepth = 3): Promise<string[]> {
|
|
122
|
-
if (maxDepth <= 0) return []
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
const entries = await fs.promises.readdir(basePath, { withFileTypes: true })
|
|
126
|
-
const results: string[] = []
|
|
127
|
-
|
|
128
|
-
if (await hasPackageJson(basePath)) {
|
|
129
|
-
results.push(basePath)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const subDirPromises = entries
|
|
133
|
-
.filter(
|
|
134
|
-
(entry) =>
|
|
135
|
-
entry.isDirectory() &&
|
|
136
|
-
!entry.name.startsWith('.') &&
|
|
137
|
-
entry.name !== 'node_modules'
|
|
138
|
-
)
|
|
139
|
-
.map(async (dir) => {
|
|
140
|
-
const path = join(basePath, dir.name)
|
|
141
|
-
return findPackageJsonDirs(path, maxDepth - 1)
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
const subdirResults = await Promise.all(subDirPromises)
|
|
145
|
-
return [...results, ...subdirResults.flat()]
|
|
146
|
-
} catch {
|
|
147
|
-
return []
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
async function findWorkspaceDirectories(): Promise<string[]> {
|
|
152
|
-
const patterns = await getWorkspacePatterns()
|
|
153
|
-
if (!patterns.length) return []
|
|
154
|
-
|
|
155
|
-
const allPackageDirs = await findPackageJsonDirs('.')
|
|
156
|
-
|
|
157
|
-
const normalizePath = (path: string): string => {
|
|
158
|
-
const normalized = path.replace(/\\/g, '/')
|
|
159
|
-
return normalized.startsWith('./') ? normalized.substring(2) : normalized
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return allPackageDirs.filter((dir) => {
|
|
163
|
-
if (dir === '.') return false
|
|
164
|
-
|
|
165
|
-
const relativePath = relative('.', dir)
|
|
166
|
-
return patterns.some((pattern) => {
|
|
167
|
-
const normalizedPattern = normalizePath(pattern)
|
|
168
|
-
const normalizedPath = normalizePath(relativePath)
|
|
169
|
-
|
|
170
|
-
if (normalizedPattern.endsWith('/*')) {
|
|
171
|
-
const prefix = normalizedPattern.slice(0, -1)
|
|
172
|
-
return normalizedPath.startsWith(prefix)
|
|
173
|
-
}
|
|
174
|
-
return (
|
|
175
|
-
normalizedPath === normalizedPattern ||
|
|
176
|
-
normalizedPath.startsWith(normalizedPattern + '/')
|
|
177
|
-
)
|
|
178
|
-
})
|
|
179
|
-
})
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async function findAvailableScripts(
|
|
183
|
-
directoryPath: string,
|
|
184
|
-
scriptNames: string[]
|
|
185
|
-
): Promise<string[]> {
|
|
186
|
-
const packageJson = await readPackageJson(directoryPath)
|
|
187
|
-
|
|
188
|
-
if (!packageJson || !packageJson.scripts) {
|
|
189
|
-
return []
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return scriptNames.filter(
|
|
193
|
-
(scriptName) => typeof packageJson.scripts?.[scriptName] === 'string'
|
|
194
|
-
)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
async function mapWorkspacesToScripts(
|
|
198
|
-
scriptNames: string[]
|
|
199
|
-
): Promise<Map<string, { scripts: string[]; packageName: string }>> {
|
|
200
|
-
const workspaceDirs = await findWorkspaceDirectories()
|
|
201
|
-
const workspaceScriptMap = new Map<string, { scripts: string[]; packageName: string }>()
|
|
202
|
-
|
|
203
|
-
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
|
-
})
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return workspaceScriptMap
|
|
217
|
-
}
|
|
218
|
-
|
|
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
|
-
)
|
|
232
|
-
|
|
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
|
-
})
|
|
248
|
-
|
|
249
|
-
const managed: ManagedProcess = {
|
|
250
|
-
proc,
|
|
251
|
-
name,
|
|
252
|
-
cwd,
|
|
253
|
-
prefixLabel,
|
|
254
|
-
extraArgs,
|
|
255
|
-
index,
|
|
256
|
-
shortcut: '',
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (managedIndex !== undefined) {
|
|
260
|
-
managedProcesses[managedIndex] = managed
|
|
261
|
-
} else {
|
|
262
|
-
managedProcesses.push(managed)
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
addChildProcess(proc)
|
|
266
|
-
|
|
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
|
-
})
|
|
276
|
-
|
|
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
|
-
})
|
|
286
|
-
|
|
287
|
-
proc.on('error', (error) => {
|
|
288
|
-
console.error(`${getPrefix(index)} Failed to start: ${error.message}`)
|
|
289
|
-
})
|
|
290
|
-
|
|
291
|
-
proc.on('close', (code) => {
|
|
292
|
-
if (getIsExiting()) return
|
|
293
|
-
|
|
294
|
-
if (code && code !== 0) {
|
|
295
|
-
console.error(`${getPrefix(index)} Process exited with code ${code}`)
|
|
296
|
-
|
|
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
|
-
})
|
|
306
|
-
|
|
307
|
-
return proc
|
|
308
|
-
}
|
|
309
|
-
|
|
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
|
-
})
|
|
318
|
-
|
|
319
|
-
const lengths = new Array(managedProcesses.length).fill(1) as number[]
|
|
320
|
-
|
|
321
|
-
for (let round = 0; round < 5; round++) {
|
|
322
|
-
const shortcuts = initials.map((init, i) => init.slice(0, lengths[i]) || init)
|
|
323
|
-
|
|
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
|
-
}
|
|
331
|
-
|
|
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
|
-
}
|
|
339
|
-
|
|
340
|
-
if (!hasCollision) {
|
|
341
|
-
for (let i = 0; i < managedProcesses.length; i++) {
|
|
342
|
-
managedProcesses[i]!.shortcut = shortcuts[i]!
|
|
343
|
-
}
|
|
344
|
-
return
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
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
|
-
})
|
|
359
|
-
|
|
360
|
-
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)
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
main().catch((error) => {
|
|
409
|
-
console.error(`Error running scripts: ${error}`)
|
|
410
|
-
exit(1)
|
|
411
|
-
})
|