main
1// Copyright 2021 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import { type AsyncHook, AsyncLocalStorage, createHook } from 'node:async_hooks'
16import { Buffer } from 'node:buffer'
17import cp, {
18 type ChildProcess,
19 type IOType,
20 type StdioOptions,
21} from 'node:child_process'
22import { type Encoding } from 'node:crypto'
23import { EventEmitter } from 'node:events'
24import fs from 'node:fs'
25import { EOL as _EOL } from 'node:os'
26import process from 'node:process'
27import { type Readable, type Writable } from 'node:stream'
28import { inspect } from 'node:util'
29
30import { Fail } from './error.ts'
31import { log } from './log.ts'
32import {
33 exec,
34 buildCmd,
35 chalk,
36 which,
37 ps,
38 VoidStream,
39 type TSpawnStore,
40} from './vendor-core.ts'
41import {
42 type Duration,
43 isString,
44 isStringLiteral,
45 iteratorToArray,
46 getLast,
47 getLines,
48 noop,
49 once,
50 parseBool,
51 parseDuration,
52 preferLocalBin,
53 proxyOverride,
54 quote,
55 quotePowerShell,
56 toCamelCase,
57 randomId,
58 bufArrJoin,
59} from './util.ts'
60
61export { bus } from './internals.ts'
62export { default as path } from 'node:path'
63export * as os from 'node:os'
64export { Fail } from './error.ts'
65export { log, type LogEntry } from './log.ts'
66export { chalk, which, ps } from './vendor-core.ts'
67export { type Duration, quote, quotePowerShell } from './util.ts'
68
69const CWD = Symbol('processCwd')
70const SYNC = Symbol('syncExec')
71const EPF = Symbol('end-piped-from')
72const SHOT = Symbol('snapshot')
73const EOL = Buffer.from(_EOL)
74const BR_CC = '\n'.charCodeAt(0)
75const DLMTR = /\r?\n/
76const SIGTERM = 'SIGTERM'
77const ENV_PREFIX = 'ZX_'
78const ENV_OPTS: Set<string> = new Set([
79 'cwd',
80 'preferLocal',
81 'detached',
82 'verbose',
83 'quiet',
84 'timeout',
85 'timeoutSignal',
86 'killSignal',
87 'prefix',
88 'postfix',
89 'shell',
90])
91
92// prettier-ignore
93export interface Options {
94 [CWD]: string
95 [SYNC]: boolean
96 cwd?: string
97 ac?: AbortController
98 signal?: AbortSignal
99 input?: string | Buffer | Readable | ProcessOutput | ProcessPromise
100 timeout?: Duration
101 timeoutSignal?: NodeJS.Signals
102 stdio: StdioOptions
103 verbose: boolean
104 sync: boolean
105 env: NodeJS.ProcessEnv
106 shell: string | true
107 nothrow: boolean
108 prefix?: string
109 postfix?: string
110 quote?: typeof quote
111 quiet: boolean
112 detached: boolean
113 preferLocal: boolean | string | string[]
114 spawn: typeof cp.spawn
115 spawnSync: typeof cp.spawnSync
116 store?: TSpawnStore
117 log: typeof log
118 kill: typeof kill
119 killSignal?: NodeJS.Signals
120 halt?: boolean
121 delimiter?: string | RegExp
122}
123
124// prettier-ignore
125type Snapshot = Options & {
126 from: string
127 pieces: TemplateStringsArray
128 args: string[]
129 cmd: string
130 ee: EventEmitter
131 ac: AbortController
132}
133
134// prettier-ignore
135export const defaults: Options = resolveDefaults({
136 [CWD]: process.cwd(),
137 [SYNC]: false,
138 verbose: false,
139 env: process.env,
140 sync: false,
141 shell: true,
142 stdio: 'pipe',
143 nothrow: false,
144 quiet: false,
145 detached: false,
146 preferLocal: false,
147 spawn: cp.spawn,
148 spawnSync: cp.spawnSync,
149 log,
150 kill,
151 killSignal: SIGTERM,
152 timeoutSignal: SIGTERM,
153})
154
155const storage = new AsyncLocalStorage<Options>()
156
157const getStore = () => storage.getStore() || defaults
158
159const getSnapshot = (
160 opts: Options,
161 from: string,
162 pieces: TemplateStringsArray,
163 args: any[]
164): Snapshot => ({
165 ...opts,
166 ac: opts.ac || new AbortController(),
167 ee: new EventEmitter(),
168 from,
169 pieces,
170 args,
171 cmd: '',
172})
173
174export function within<R>(callback: () => R): R {
175 return storage.run({ ...getStore() }, callback)
176}
177
178// prettier-ignore
179export interface Shell<
180 S = false,
181 R = S extends true ? ProcessOutput : ProcessPromise,
182> {
183 (pieces: TemplateStringsArray, ...args: any[]): R
184 <O extends Partial<Options> = Partial<Options>, R = O extends { sync: true } ? Shell<true> : Shell>(opts: O): R
185 sync: {
186 (pieces: TemplateStringsArray, ...args: any[]): ProcessOutput
187 (opts: Partial<Omit<Options, 'sync'>>): Shell<true>
188 }
189}
190
191// The zx
192export type $ = Shell & Options
193
194export const $: $ = new Proxy<$>(
195 // prettier-ignore
196 function (pieces: TemplateStringsArray | Partial<Options>, ...args: any[]) {
197 const opts = getStore()
198 if (!Array.isArray(pieces)) {
199 return function (this: any, ...args: any) {
200 return within(() => Object.assign($, opts, pieces).apply(this, args))
201 }
202 }
203 const from = Fail.getCallerLocation()
204 const cb: PromiseCallback = () => (cb[SHOT] = getSnapshot(opts, from, pieces as TemplateStringsArray, args))
205 const pp = new ProcessPromise(cb)
206
207 if (!pp.isHalted()) pp.run()
208
209 return pp.sync ? pp.output : pp
210 } as $,
211 {
212 set(t, key, value) {
213 return Reflect.set(
214 key in Function.prototype ? t : getStore(),
215 key === 'sync' ? SYNC : key,
216 value
217 )
218 },
219 get(t, key) {
220 return key === 'sync'
221 ? $({ sync: true })
222 : Reflect.get(key in Function.prototype ? t : getStore(), key)
223 },
224 }
225)
226
227type ProcessStage = 'initial' | 'halted' | 'running' | 'fulfilled' | 'rejected'
228
229type Resolve = (out: ProcessOutput) => void
230
231type Reject = (error: ProcessOutput | Error) => void
232
233type PromiseCallback = {
234 (resolve: Resolve, reject: Reject): void
235 [SHOT]?: Snapshot
236}
237
238type PromisifiedStream<D extends Writable = Writable> = D &
239 PromiseLike<ProcessOutput & D> & { run(): void }
240
241type PipeAcceptor = Writable | ProcessPromise
242type PipeDest = PipeAcceptor | TemplateStringsArray | string
243type PipeMethod = {
244 (dest: TemplateStringsArray, ...args: any[]): ProcessPromise
245 (file: string): PromisifiedStream
246 <D extends Writable>(dest: D): PromisifiedStream<D>
247 <D extends ProcessPromise>(dest: D): D
248}
249
250export class ProcessPromise extends Promise<ProcessOutput> {
251 private _stage: ProcessStage = 'initial'
252 private _id = randomId()
253 private _snapshot!: Snapshot
254 private _timeoutId?: ReturnType<typeof setTimeout>
255 private _piped = false
256 private _stdin = new VoidStream()
257 private _zurk: ReturnType<typeof exec> | null = null
258 private _output: ProcessOutput | null = null
259 private _resolve!: Resolve
260 private _reject!: Reject
261
262 constructor(executor: PromiseCallback) {
263 let resolve: Resolve
264 let reject: Reject
265 super((...args) => {
266 ;[resolve = noop, reject = noop] = args
267 executor(...args)
268 })
269
270 const snapshot = executor[SHOT]
271 if (snapshot) {
272 this._snapshot = snapshot
273 this._resolve = resolve!
274 this._reject = reject!
275 if (snapshot.halt) this._stage = 'halted'
276 try {
277 this.build()
278 } catch (err) {
279 this.finalize(ProcessOutput.fromError(err as Error), true)
280 }
281 } else ProcessPromise.disarm(this)
282 }
283 // prettier-ignore
284 private build(): void {
285 const $ = this._snapshot
286 if (!$.shell)
287 throw new Fail(`No shell is available: ${Fail.DOCS_URL}/shell`)
288 if (!$.quote)
289 throw new Fail(`No quote function is defined: ${Fail.DOCS_URL}/quotes`)
290 if ($.pieces.some((p) => p == null))
291 throw new Fail(`Malformed command at ${$.from}`)
292
293 $.cmd = buildCmd(
294 $.quote!,
295 $.pieces as TemplateStringsArray,
296 $.args
297 ) as string
298 }
299 run(): this {
300 ProcessPromise.bus.runBack(this)
301 if (this.isRunning() || this.isSettled()) return this // The _run() can be called from a few places.
302 this._stage = 'running'
303
304 const self = this
305 const $ = self._snapshot
306 const id = self.id
307 const cwd = $.cwd || $[CWD]
308
309 if ($.preferLocal) {
310 const dirs =
311 $.preferLocal === true ? [$.cwd, $[CWD]] : [$.preferLocal].flat()
312 $.env = preferLocalBin($.env, ...dirs)
313 }
314
315 // prettier-ignore
316 this._zurk = exec({
317 cmd: self.fullCmd,
318 cwd,
319 input: ($.input as ProcessPromise | ProcessOutput)?.stdout ?? $.input,
320 stdin: self._stdin,
321 sync: self.sync,
322 signal: self.signal,
323 shell: isString($.shell) ? $.shell : true,
324 id,
325 env: $.env,
326 spawn: $.spawn,
327 spawnSync:$.spawnSync,
328 store: $.store,
329 stdio: $.stdio,
330 detached: $.detached,
331 ee: $.ee,
332 run(cb, ctx){
333 (self.cmd as unknown as Promise<string>).then?.(
334 cmd => {
335 $.cmd = cmd
336 ctx.cmd = self.fullCmd
337 cb()
338 },
339 error => self.finalize(ProcessOutput.fromError(error))
340 ) || cb()
341 },
342 on: {
343 start: () => {
344 $.log({ kind: 'cmd', cmd: $.cmd, cwd, verbose: self.isVerbose(), id })
345 self.timeout($.timeout, $.timeoutSignal)
346 },
347 stdout: (data) => {
348 // If the process is piped, don't print its output.
349 $.log({ kind: 'stdout', data, verbose: !self._piped && self.isVerbose(), id })
350 },
351 stderr: (data) => {
352 // Stderr should be printed regardless of piping.
353 $.log({ kind: 'stderr', data, verbose: !self.isQuiet(), id })
354 },
355 end: (data, c) => {
356 const { error: _error, status, signal: __signal, duration, ctx: { store }} = data
357 const { stdout, stderr } = store
358 const { cause, exitCode, signal: _signal } = self._breakerData || {}
359
360 const signal = _signal ?? __signal
361 const code = exitCode ?? status
362 const error = cause ?? _error
363 const output = new ProcessOutput({
364 code,
365 signal,
366 error,
367 duration,
368 store,
369 from: $.from,
370 })
371
372 $.log({ kind: 'end', signal, exitCode: code, duration, error, verbose: self.isVerbose(), id })
373
374 // Ensures EOL
375 if (stdout.length && getLast(getLast(stdout)) !== BR_CC) c.on.stdout!(EOL, c)
376 if (stderr.length && getLast(getLast(stderr)) !== BR_CC) c.on.stderr!(EOL, c)
377
378 self.finalize(output)
379 },
380 },
381 })
382
383 return this
384 }
385 private _breakerData?: Partial<
386 Pick<ProcessOutput, 'exitCode' | 'signal' | 'cause'>
387 >
388
389 private break(
390 exitCode?: ProcessOutput['exitCode'],
391 signal?: ProcessOutput['signal'],
392 cause?: ProcessOutput['cause']
393 ): void {
394 if (!this.isRunning()) return
395 this._breakerData = { exitCode, signal, cause }
396 this.kill(signal)
397 }
398
399 private finalize(output: ProcessOutput, legacy = false): void {
400 if (this.isSettled()) return
401 this._output = output
402 ProcessPromise.bus.unpipeBack(this)
403 if (output.ok || this.isNothrow()) {
404 this._stage = 'fulfilled'
405 this._resolve(output)
406 } else {
407 this._stage = 'rejected'
408 if (legacy) {
409 this._resolve(output) // to avoid unhandledRejection alerts
410 throw output.cause || output
411 }
412 this._reject(output)
413 if (this.sync) throw output
414 }
415 }
416
417 abort(reason?: string) {
418 if (this.isSettled()) throw new Fail('Too late to abort the process.')
419 if (this.signal !== this.ac.signal)
420 throw new Fail('The signal is controlled by another process.')
421 if (!this.child)
422 throw new Fail('Trying to abort a process without creating one.')
423
424 this.ac.abort(reason)
425 }
426
427 kill(signal?: NodeJS.Signals | null): Promise<void> {
428 if (this.isSettled()) throw new Fail('Too late to kill the process.')
429 if (!this.child)
430 throw new Fail('Trying to kill a process without creating one.')
431 if (!this.pid) throw new Fail('The process pid is undefined.')
432
433 return $.kill(this.pid, signal || this._snapshot.killSignal || $.killSignal)
434 }
435
436 // Configurators
437 stdio(
438 stdin: IOType | StdioOptions,
439 stdout: IOType = 'pipe',
440 stderr: IOType = 'pipe'
441 ): this {
442 this._snapshot.stdio = Array.isArray(stdin)
443 ? stdin
444 : [stdin, stdout, stderr]
445 return this
446 }
447
448 nothrow(v = true): this {
449 this._snapshot.nothrow = v
450 return this
451 }
452
453 quiet(v = true): this {
454 this._snapshot.quiet = v
455 return this
456 }
457
458 verbose(v = true): this {
459 this._snapshot.verbose = v
460 return this
461 }
462
463 timeout(d: Duration = 0, signal = $.timeoutSignal): this {
464 if (this.isSettled()) return this
465
466 const $ = this._snapshot
467 $.timeout = parseDuration(d)
468 $.timeoutSignal = signal
469
470 if (this._timeoutId) clearTimeout(this._timeoutId)
471 if ($.timeout && this.isRunning()) {
472 this._timeoutId = setTimeout(() => this.kill($.timeoutSignal), $.timeout)
473 this.finally(() => clearTimeout(this._timeoutId)).catch(noop)
474 }
475 return this
476 }
477 /**
478 * @deprecated Use $({halt: true})`cmd` instead.
479 */
480 halt(): this {
481 return this
482 }
483
484 // Getters
485 get id(): string {
486 return this._id
487 }
488
489 get pid(): number | undefined {
490 return this.child?.pid
491 }
492
493 get cmd(): string {
494 return this._snapshot.cmd
495 }
496
497 get fullCmd(): string {
498 const { prefix = '', postfix = '', cmd } = this._snapshot
499 return prefix + cmd + postfix
500 }
501
502 get child(): ChildProcess | undefined {
503 return this._zurk?.child
504 }
505
506 get stdin(): Writable {
507 return this.child?.stdin!
508 }
509
510 get stdout(): Readable {
511 return this.child?.stdout!
512 }
513
514 get stderr(): Readable {
515 return this.child?.stderr!
516 }
517
518 get exitCode(): Promise<number | null> {
519 return this.then(
520 (o) => o.exitCode,
521 (o) => o.exitCode
522 )
523 }
524
525 get signal(): AbortSignal {
526 return this._snapshot.signal || this.ac.signal
527 }
528
529 get ac(): AbortController {
530 return this._snapshot.ac
531 }
532
533 get output(): ProcessOutput | null {
534 return this._output
535 }
536
537 get stage(): ProcessStage {
538 return this._stage
539 }
540
541 get sync(): boolean {
542 return this._snapshot[SYNC]
543 }
544
545 override get [Symbol.toStringTag](): string {
546 return 'ProcessPromise'
547 }
548
549 [Symbol.toPrimitive](): string {
550 return this.toString()
551 }
552
553 // Output formatters
554 json<T = any>(): Promise<T> {
555 return this.then((o) => o.json<T>())
556 }
557
558 text(encoding?: Encoding): Promise<string> {
559 return this.then((o) => o.text(encoding))
560 }
561
562 lines(delimiter?: Options['delimiter']): Promise<string[]> {
563 return this.then((o) => o.lines(delimiter))
564 }
565
566 buffer(): Promise<Buffer> {
567 return this.then((o) => o.buffer())
568 }
569
570 blob(type?: string): Promise<Blob> {
571 return this.then((o) => o.blob(type))
572 }
573
574 // Status checkers
575 isQuiet(): boolean {
576 return this._snapshot.quiet
577 }
578
579 isVerbose(): boolean {
580 return this._snapshot.verbose && !this.isQuiet()
581 }
582
583 isNothrow(): boolean {
584 return this._snapshot.nothrow
585 }
586
587 isHalted(): boolean {
588 return this.stage === 'halted' && !this.sync
589 }
590
591 private isSettled(): boolean {
592 return !!this.output
593 }
594
595 private isRunning(): boolean {
596 return this.stage === 'running'
597 }
598
599 // Piping
600 // prettier-ignore
601 get pipe(): PipeMethod & {
602 [key in keyof TSpawnStore]: PipeMethod
603 } {
604 const getPipeMethod = (kind: keyof TSpawnStore) => this._pipe.bind(this, kind) as PipeMethod
605 const stdout = getPipeMethod('stdout')
606 const stderr = getPipeMethod('stderr')
607 const stdall = getPipeMethod('stdall')
608 return Object.assign(stdout, { stdout, stderr, stdall })
609 }
610
611 unpipe(to?: PipeAcceptor): this {
612 ProcessPromise.bus.unpipe(this, to)
613 return this
614 }
615
616 // prettier-ignore
617 private _pipe(source: keyof TSpawnStore, dest: PipeDest, ...args: any[]): PromisifiedStream | ProcessPromise {
618 if (isString(dest))
619 return this._pipe(source, fs.createWriteStream(dest))
620
621 if (isStringLiteral(dest, ...args))
622 return this._pipe(
623 source,
624 $({
625 halt: true,
626 signal: this.signal,
627 })(dest as TemplateStringsArray, ...args)
628 )
629
630 const isP = dest instanceof ProcessPromise
631 if (isP && dest.isSettled()) throw new Fail('Cannot pipe to a settled process.')
632 if (!isP && dest.writableEnded) throw new Fail('Cannot pipe to a closed stream.')
633
634 this._piped = true
635 ProcessPromise.bus.pipe(this, dest)
636
637 const { ee } = this._snapshot
638 const output = this.output
639 const from = new VoidStream()
640 const check = () => !!ProcessPromise.bus.refs.get(this)?.has(dest)
641 const end = () => {
642 if (!check()) return
643 setImmediate(() => {
644 ProcessPromise.bus.unpipe(this, dest)
645 ProcessPromise.bus.sources(dest).length === 0 && from.end()
646 })
647 }
648 const fill = () => {
649 for (const chunk of this._zurk!.store[source]) from.write(chunk)
650 }
651 const fillSettled = () => {
652 if (!output) return
653 if (isP && !output.ok) dest.break(output.exitCode, output.signal, output.cause)
654 fill()
655 end()
656 }
657
658 if (!output) {
659 const onData = (chunk: string | Buffer) => check() && from.write(chunk)
660 ee
661 .once(source, () => {
662 fill()
663 ee.on(source, onData)
664 })
665 .once('end', () => {
666 ee.removeListener(source, onData)
667 end()
668 })
669 }
670
671 if (isP) {
672 from.pipe(dest._stdin)
673 if (this.isHalted()) ee.once('start', () => dest.run())
674 else {
675 dest.run()
676 this.catch((e) => dest.break(e.exitCode, e.signal, e.cause))
677 }
678 fillSettled()
679 return dest
680 }
681
682 from.once('end', () => dest.emit(EPF)).pipe(dest)
683 fillSettled()
684 return ProcessPromise.promisifyStream(dest, this)
685 }
686
687 // prettier-ignore
688 private static bus = {
689 refs: new Map<ProcessPromise, Set<PipeAcceptor>>,
690 streams: new WeakMap<Writable, PromisifiedStream>(),
691 pipe(from: ProcessPromise, to: PipeAcceptor) {
692 const set = this.refs.get(from) || (this.refs.set(from, new Set())).get(from)!
693 set.add(to)
694 },
695 unpipe(from: ProcessPromise, to?: PipeAcceptor) {
696 const set = this.refs.get(from)
697 if (!set) return
698 if (to) set.delete(to)
699 if (set.size) return
700 this.refs.delete(from)
701 from._piped = false
702 },
703 unpipeBack(to: ProcessPromise, from?: ProcessPromise) {
704 if (from) return this.unpipe(from, to)
705 for (const _from of this.refs.keys()) {
706 this.unpipe(_from, to)
707 }
708 },
709 runBack(p: PipeAcceptor) {
710 for (const from of this.sources(p)) {
711 if (from instanceof ProcessPromise) from.run()
712 else this.streams.get(from)?.run()
713 }
714 },
715 sources(p: PipeAcceptor): PipeAcceptor[] {
716 const refs = []
717 for (const [from, set] of this.refs.entries()) {
718 set.has(p) && refs.push(from)
719 }
720 return refs
721 }
722 }
723
724 private static promisifyStream = <S extends Writable>(
725 stream: S,
726 from: ProcessPromise
727 ): PromisifiedStream<S> => {
728 const proxy =
729 ProcessPromise.bus.streams.get(stream) ||
730 proxyOverride(stream as PromisifiedStream<S>, {
731 then(res: any = noop, rej: any = noop) {
732 return new Promise((_res, _rej) => {
733 const end = () => _res(res(proxyOverride(stream, from.output)))
734 stream
735 .once('error', (e) => _rej(rej(e)))
736 .once('finish', end)
737 .once(EPF, end)
738 })
739 },
740 run() {
741 from.run()
742 },
743 pipe(...args: any) {
744 const dest = stream.pipe.apply(stream, args)
745 return dest instanceof ProcessPromise
746 ? dest
747 : ProcessPromise.promisifyStream(dest as Writable, from)
748 },
749 })
750
751 ProcessPromise.bus.streams.set(stream, proxy as any)
752 return proxy as PromisifiedStream<S>
753 }
754
755 // Promise API
756 override then<R = ProcessOutput, E = ProcessOutput>(
757 onfulfilled?:
758 | ((value: ProcessOutput) => PromiseLike<R> | R)
759 | undefined
760 | null,
761 onrejected?:
762 | ((reason: ProcessOutput) => PromiseLike<E> | E)
763 | undefined
764 | null
765 ): Promise<R | E> {
766 return super.then(onfulfilled, onrejected)
767 }
768
769 override catch<T = ProcessOutput>(
770 onrejected?:
771 | ((reason: ProcessOutput) => PromiseLike<T> | T)
772 | undefined
773 | null
774 ): Promise<ProcessOutput | T> {
775 return super.catch(onrejected)
776 }
777
778 // Async iterator API
779 async *[Symbol.asyncIterator](): AsyncIterator<string> {
780 const memo: (string | undefined)[] = []
781 const dlmtr = this._snapshot.delimiter || $.delimiter || DLMTR
782
783 for (const chunk of this._zurk!.store.stdout) {
784 yield* getLines(chunk, memo, dlmtr)
785 }
786
787 for await (const chunk of this.stdout || []) {
788 yield* getLines(chunk, memo, dlmtr)
789 }
790
791 if (memo[0]) yield memo[0]
792
793 await this
794 }
795
796 // Stream-like API
797 private writable = true
798 private emit(event: string, ...args: any[]) {
799 return this
800 }
801 private on(event: string, cb: any) {
802 this._stdin.on(event, cb)
803 return this
804 }
805 private once(event: string, cb: any) {
806 this._stdin.once(event, cb)
807 return this
808 }
809 private write(data: any, encoding: NodeJS.BufferEncoding, cb: any) {
810 this._stdin.write(data, encoding, cb)
811 return this
812 }
813 private end(chunk: any, cb: any) {
814 this._stdin.end(chunk, cb)
815 return this
816 }
817 private removeListener(event: string, cb: any) {
818 this._stdin.removeListener(event, cb)
819 return this
820 }
821
822 // prettier-ignore
823 private static disarm(p: ProcessPromise, toggle = true): void {
824 Object.getOwnPropertyNames(ProcessPromise.prototype).forEach(k => {
825 if (k in Promise.prototype) return
826 if (!toggle) { Reflect.deleteProperty(p, k); return }
827 Object.defineProperty(p, k, { configurable: true, get() {
828 throw new Fail('Inappropriate usage. Apply $ instead of direct instantiation.')
829 }})
830 })
831 }
832}
833
834type ProcessDto = {
835 code: number | null
836 signal: NodeJS.Signals | null
837 duration: number
838 error: any
839 from: string
840 store: TSpawnStore
841 delimiter?: string | RegExp
842}
843
844export class ProcessOutput extends Error {
845 private readonly _dto!: ProcessDto
846 cause!: Error | null
847 message!: string
848 stdout!: string
849 stderr!: string
850 stdall!: string
851 constructor(dto: ProcessDto)
852 constructor(
853 code?: number | null,
854 signal?: NodeJS.Signals | null,
855 stdout?: string,
856 stderr?: string,
857 stdall?: string,
858 message?: string,
859 duration?: number
860 )
861 // prettier-ignore
862 constructor(
863 code: number | null | ProcessDto = null,
864 signal: NodeJS.Signals | null = null,
865 stdout: string = '',
866 stderr: string = '',
867 stdall: string = '',
868 message: string = '',
869 duration: number = 0,
870 error: any = null,
871 from: string = '',
872 store: TSpawnStore = { stdout: [stdout], stderr: [stderr], stdall: [stdall], }
873 ) {
874 super(message)
875 const dto = code !== null && typeof code === 'object'
876 ? code
877 : { code, signal, duration, error, from, store }
878
879 Object.defineProperties(this, {
880 _dto: { value: dto, enumerable: false },
881 cause: { get() { return dto.error }, enumerable: false },
882 stdout: { get: once(() => bufArrJoin(dto.store.stdout)) },
883 stderr: { get: once(() => bufArrJoin(dto.store.stderr)) },
884 stdall: { get: once(() => bufArrJoin(dto.store.stdall)) },
885 message: { get: once(() =>
886 dto.error || message
887 ? ProcessOutput.getErrorMessage(dto.error || new Error(message), dto.from)
888 : ProcessOutput.getExitMessage(
889 dto.code,
890 dto.signal,
891 this.stderr,
892 dto.from,
893 this.stderr.trim() ? '' : ProcessOutput.getErrorDetails(this.lines())
894 )
895 )},
896 })
897 }
898
899 get exitCode(): number | null {
900 return this._dto.code
901 }
902
903 get signal(): NodeJS.Signals | null {
904 return this._dto.signal
905 }
906
907 get duration(): number {
908 return this._dto.duration
909 }
910
911 get [Symbol.toStringTag](): string {
912 return 'ProcessOutput'
913 }
914
915 get ok(): boolean {
916 return !this._dto.error && this.exitCode === 0
917 }
918
919 json<T = any>(): T {
920 return JSON.parse(this.stdall)
921 }
922
923 buffer(): Buffer {
924 return Buffer.from(this.stdall)
925 }
926
927 blob(type = 'text/plain'): Blob {
928 if (!globalThis.Blob)
929 throw new Fail(
930 'Blob is not supported in this environment. Provide a polyfill'
931 )
932 return new Blob([this.buffer()], { type })
933 }
934
935 text(encoding: Encoding = 'utf8'): string {
936 return encoding === 'utf8'
937 ? this.toString()
938 : this.buffer().toString(encoding)
939 }
940
941 lines(delimiter?: string | RegExp): string[] {
942 return iteratorToArray(this[Symbol.iterator](delimiter))
943 }
944
945 override toString(): string {
946 return this.stdall
947 }
948
949 override valueOf(): string {
950 return this.stdall.trim()
951 }
952
953 [Symbol.toPrimitive](): string {
954 return this.valueOf()
955 }
956 // prettier-ignore
957 *[Symbol.iterator](dlmtr: Options['delimiter'] = this._dto.delimiter || $.delimiter || DLMTR): Iterator<string> {
958 const memo: (string | undefined)[] = []
959 for (const chunk of this._dto.store.stdall) {
960 yield* getLines(chunk, memo, dlmtr)
961 }
962
963 if (memo[0]) yield memo[0]
964 }
965
966 [inspect.custom](): string {
967 const codeInfo = ProcessOutput.getExitCodeInfo(this.exitCode)
968
969 return `ProcessOutput {
970 stdout: ${chalk.green(inspect(this.stdout))},
971 stderr: ${chalk.red(inspect(this.stderr))},
972 signal: ${inspect(this.signal)},
973 exitCode: ${(this.ok ? chalk.green : chalk.red)(this.exitCode)}${
974 codeInfo ? chalk.grey(' (' + codeInfo + ')') : ''
975 },
976 duration: ${this.duration}
977}`
978 }
979
980 static getExitMessage = Fail.formatExitMessage
981 static getErrorMessage = Fail.formatErrorMessage
982 static getErrorDetails = Fail.formatErrorDetails
983 static getExitCodeInfo = Fail.getExitCodeInfo
984
985 static fromError(error: Error): ProcessOutput {
986 const output = new ProcessOutput()
987 output._dto.error = error
988 return output
989 }
990}
991
992export const useBash = (): void => setShell('bash', false)
993export const usePwsh = (): void => setShell('pwsh')
994export const usePowerShell = (): void => setShell('powershell.exe')
995function setShell(n: string, ps = true) {
996 $.shell = which.sync(n)
997 $.prefix = ps ? '' : 'set -euo pipefail;'
998 $.postfix = ps ? '; exit $LastExitCode' : ''
999 $.quote = ps ? quotePowerShell : quote
1000}
1001
1002try {
1003 const { shell, prefix, postfix } = $
1004 useBash()
1005 if (isString(shell)) $.shell = shell
1006 if (isString(prefix)) $.prefix = prefix
1007 if (isString(postfix)) $.postfix = postfix
1008} catch (err) {}
1009
1010let cwdSyncHook: AsyncHook
1011
1012export function syncProcessCwd(flag: boolean = true) {
1013 cwdSyncHook =
1014 cwdSyncHook ||
1015 createHook({
1016 init: syncCwd,
1017 before: syncCwd,
1018 promiseResolve: syncCwd,
1019 after: syncCwd,
1020 destroy: syncCwd,
1021 })
1022 if (flag) cwdSyncHook.enable()
1023 else cwdSyncHook.disable()
1024}
1025
1026function syncCwd() {
1027 if ($[CWD] != process.cwd()) process.chdir($[CWD])
1028}
1029
1030export function cd(dir: string | ProcessOutput) {
1031 if (dir instanceof ProcessOutput) {
1032 dir = dir.toString().trim()
1033 }
1034
1035 $.log({ kind: 'cd', dir, verbose: !$.quiet && $.verbose })
1036 process.chdir(dir)
1037 $[CWD] = process.cwd()
1038}
1039
1040export async function kill(
1041 pid: number | `${number}`,
1042 signal = $.killSignal || SIGTERM
1043) {
1044 if (
1045 (typeof pid !== 'number' && typeof pid !== 'string') ||
1046 !/^\d+$/.test(pid as string)
1047 )
1048 throw new Fail(`Invalid pid: ${pid}`)
1049
1050 $.log({ kind: 'kill', pid, signal, verbose: !$.quiet && $.verbose })
1051 if (
1052 process.platform === 'win32' &&
1053 (await new Promise((resolve) => {
1054 cp.exec(`taskkill /pid ${pid} /t /f`, (err) => resolve(!err))
1055 }))
1056 )
1057 return
1058
1059 for (const p of await ps.tree({ pid, recursive: true })) {
1060 try {
1061 process.kill(+p.pid, signal)
1062 } catch (e) {}
1063 }
1064 try {
1065 process.kill(-pid, signal)
1066 } catch (e) {
1067 try {
1068 process.kill(+pid, signal)
1069 } catch (e) {}
1070 }
1071}
1072
1073export function resolveDefaults(
1074 defs: Options = defaults,
1075 prefix: string = ENV_PREFIX,
1076 env = process.env,
1077 allowed = ENV_OPTS
1078): Options {
1079 return Object.entries(env).reduce<Options>((m, [k, v]) => {
1080 if (v && k.startsWith(prefix)) {
1081 const _k = toCamelCase(k.slice(prefix.length))
1082 const _v = parseBool(v)
1083 if (allowed.has(_k)) (m as any)[_k] = _v
1084 }
1085 return m
1086 }, defs)
1087}