v7
  1// Copyright 2022 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 assert from 'node:assert'
 16import * as globbyModule from 'globby'
 17import minimist from 'minimist'
 18import nodeFetch, { RequestInfo, RequestInit } from 'node-fetch'
 19import { createInterface } from 'node:readline'
 20import { $, within, ProcessOutput } from './core.js'
 21import { Duration, isString, parseDuration } from './util.js'
 22import chalk from 'chalk'
 23
 24export { default as chalk } from 'chalk'
 25export { default as fs } from 'fs-extra'
 26export { default as which } from 'which'
 27export { default as YAML } from 'yaml'
 28export { default as path } from 'node:path'
 29export { default as os } from 'node:os'
 30export { ssh } from 'webpod'
 31
 32export let argv = minimist(process.argv.slice(2))
 33export function updateArgv(args: string[]) {
 34  argv = minimist(args)
 35  ;(global as any).argv = argv
 36}
 37
 38export const globby = Object.assign(function globby(
 39  patterns: string | readonly string[],
 40  options?: globbyModule.Options
 41) {
 42  return globbyModule.globby(patterns, options)
 43}, globbyModule)
 44export const glob = globby
 45
 46export function sleep(duration: Duration) {
 47  return new Promise((resolve) => {
 48    setTimeout(resolve, parseDuration(duration))
 49  })
 50}
 51
 52export async function fetch(url: RequestInfo, init?: RequestInit) {
 53  $.log({ kind: 'fetch', url, init })
 54  return nodeFetch(url, init)
 55}
 56
 57export function echo(...args: any[]): void
 58export function echo(pieces: TemplateStringsArray, ...args: any[]) {
 59  let msg
 60  const lastIdx = pieces.length - 1
 61  if (
 62    Array.isArray(pieces) &&
 63    pieces.every(isString) &&
 64    lastIdx === args.length
 65  ) {
 66    msg =
 67      args.map((a, i) => pieces[i] + stringify(a)).join('') + pieces[lastIdx]
 68  } else {
 69    msg = [pieces, ...args].map(stringify).join(' ')
 70  }
 71  console.log(msg)
 72}
 73
 74function stringify(arg: ProcessOutput | any) {
 75  if (arg instanceof ProcessOutput) {
 76    return arg.toString().replace(/\n$/, '')
 77  }
 78  return `${arg}`
 79}
 80
 81export async function question(
 82  query?: string,
 83  options?: { choices: string[] }
 84): Promise<string> {
 85  let completer = undefined
 86  if (options && Array.isArray(options.choices)) {
 87    /* c8 ignore next 5 */
 88    completer = function completer(line: string) {
 89      const completions = options.choices
 90      const hits = completions.filter((c) => c.startsWith(line))
 91      return [hits.length ? hits : completions, line]
 92    }
 93  }
 94  const rl = createInterface({
 95    input: process.stdin,
 96    output: process.stdout,
 97    terminal: true,
 98    completer,
 99  })
100
101  return new Promise((resolve) =>
102    rl.question(query ?? '', (answer) => {
103      rl.close()
104      resolve(answer)
105    })
106  )
107}
108
109export async function stdin() {
110  let buf = ''
111  process.stdin.setEncoding('utf8')
112  for await (const chunk of process.stdin) {
113    buf += chunk
114  }
115  return buf
116}
117
118export async function retry<T>(count: number, callback: () => T): Promise<T>
119export async function retry<T>(
120  count: number,
121  duration: Duration | Generator<number>,
122  callback: () => T
123): Promise<T>
124export async function retry<T>(
125  count: number,
126  a: Duration | Generator<number> | (() => T),
127  b?: () => T
128): Promise<T> {
129  const total = count
130  let callback: () => T
131  let delayStatic = 0
132  let delayGen: Generator<number> | undefined
133  if (typeof a == 'function') {
134    callback = a
135  } else {
136    if (typeof a == 'object') {
137      delayGen = a
138    } else {
139      delayStatic = parseDuration(a)
140    }
141    assert(b)
142    callback = b
143  }
144  let lastErr: unknown
145  let attempt = 0
146  while (count-- > 0) {
147    attempt++
148    try {
149      return await callback()
150    } catch (err) {
151      let delay = 0
152      if (delayStatic > 0) delay = delayStatic
153      if (delayGen) delay = delayGen.next().value
154      $.log({
155        kind: 'retry',
156        error:
157          chalk.bgRed.white(' FAIL ') +
158          ` Attempt: ${attempt}${total == Infinity ? '' : `/${total}`}` +
159          (delay > 0 ? `; next in ${delay}ms` : ''),
160      })
161      lastErr = err
162      if (count == 0) break
163      if (delay) await sleep(delay)
164    }
165  }
166  throw lastErr
167}
168
169export function* expBackoff(
170  max: Duration = '60s',
171  delay: Duration = '100ms'
172): Generator<number, void, unknown> {
173  const maxMs = parseDuration(max)
174  const randMs = parseDuration(delay)
175  let n = 0
176  while (true) {
177    yield Math.min(randMs * 2 ** n++, maxMs)
178  }
179}
180
181export async function spinner<T>(callback: () => T): Promise<T>
182export async function spinner<T>(title: string, callback: () => T): Promise<T>
183export async function spinner<T>(
184  title: string | (() => T),
185  callback?: () => T
186): Promise<T> {
187  if (typeof title == 'function') {
188    callback = title
189    title = ''
190  }
191  let i = 0
192  const spin = () =>
193    process.stderr.write(`  ${'⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'[i++ % 10]} ${title}\r`)
194  return within(async () => {
195    $.verbose = false
196    const id = setInterval(spin, 100)
197    let result: T
198    try {
199      result = await callback!()
200    } finally {
201      clearInterval(id)
202      process.stderr.write(' '.repeat(process.stdout.columns - 1) + '\r')
203    }
204    return result
205  })
206}