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}