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}