// Reference: // - https://docs.deno.com/runtime/fundamentals/testing/ import {decodeBase64, encodeBase64} from "jsr:@std/encoding/base64"; import {dirname, fromFileUrl} from "https://deno.land/std/path/mod.ts"; import {spawn, SpawnOptionsWithoutStdio} from "node:child_process"; // reference: https://docs.deno.com/examples/hex_base64_encoding/ // import { decodeBase64, encodeBase64 } from "jsr:@std/encoding/base64"; // import { decodeHex, encodeHex } from "jsr:@std/encoding/hex"; export function isDeno(): boolean { return typeof Deno !== "undefined"; } export function args(): string[] { return isDeno() ? Deno.args : process.argv.slice(2); } export function osEnv(key: string): string | undefined { return isDeno() ? Deno.env.get(key) : process.env[key]; } export class ProcessOutput { code: number; stdout: string; stderr: string; constructor(code: number, stdout: string, stderr: string) { this.code = code; this.stdout = stdout; this.stderr = stderr; } assertSuccess(): ProcessOutput { if (this.code !== 0) { throw new Error( `Failed to execute command, exit code: ${this.code}\n- stdout: ${this.stdout}\n- stderr: ${this.stderr}\n`, ); } return this; } stdoutThenTrim(): string { return this.getStdoutAsStringThenTrim(); } getStdoutAsStringThenTrim(): string { return this.stdout.trim(); } stderrThenTrim(): string { return this.getStderrAsStringThenTrim(); } getStderrAsStringThenTrim(): string { return this.stderr.trim(); } stdoutAsJson(): any { return this.getStdoutAsJson(); } getStdoutAsJson(): any { return JSON.parse(this.stdout); } stderrAsJson(): any { return this.getStderrAsJson(); } getStderrAsJson(): any { return JSON.parse(this.stderr); } } export async function execCommandAndStdout( command: string, args?: string[], options?: Deno.CommandOptions | SpawnOptionsWithoutStdio, ): Promise { const processOutput = await execCommand(command, args, options); processOutput.assertSuccess(); return processOutput.stdout.trim(); } export async function execCommand( command: string, args?: string[], options?: Deno.CommandOptions | SpawnOptionsWithoutStdio, ): Promise { if (isDeno()) { const opts = options || {}; if (args) opts.args = args; const cmd = new Deno.Command(command, opts); const { code, stdout, stderr } = await cmd.output(); return new ProcessOutput( code, new TextDecoder().decode(stdout), new TextDecoder().decode(stderr), ); } return await execCommandSpawn(command, args, options); } async function execCommandSpawn( command: string, args: string[], options?: SpawnOptionsWithoutStdio, ): Promise { const ps = spawn(command, args, options); return new Promise((resolve, reject) => { let stdout = ""; let stderr = ""; ps.stdout.on("data", (data) => { stdout += data.toString(); }); ps.stderr.on("data", (data) => { stderr += data.toString(); }); ps.on("close", (code) => { try { const output = new ProcessOutput(code, stdout, stderr); ps.stdin.end(); resolve(output); } catch (e) { reject(e); } }); ps.on("error", (err) => { reject(err); }); }); } export async function execCommandShell( command: string, args?: string[], options?: Deno.CommandOptions | SpawnOptionsWithoutStdio, ): Promise { if (isDeno()) { const opts = options || {}; if (args) opts.args = args; opts.stdin = "inherit"; opts.stdout = "inherit"; opts.stderr = "inherit"; const cmd = new Deno.Command(command, opts); return (await cmd.spawn().status).code; } return await execCommandShellSpwan(command, args, options); } async function execCommandShellSpwan( command: string, args?: string[], options?: SpawnOptionsWithoutStdio, ): Promise { return new Promise((resolve, reject) => { const ps = spawn(command, args, { shell: false, stdio: ["inherit", "inherit", "inherit"], }); ps.on("close", (code) => { resolve(code); }); ps.on("error", (err) => { reject(err); }); }); } export async function sleep(timeoutMillis: number): Promise { await new Promise((resolve) => setTimeout(resolve, timeoutMillis)); } export function compareVersion(ver1: string, ver2: string): 0 | 1 | -1 { if (ver1 === ver2) return 0; const ver1Parts = ver1.split("."); const ver2Parts = ver2.split("."); const ver1Main = parseInt(ver1Parts[0]); const ver2Main = parseInt(ver2Parts[0]); if (ver1Main > ver2Main) return 1; if (ver1Main < ver2Main) return -1; const ver1Second = parseInt(ver1Parts[1]); const ver2Second = parseInt(ver2Parts[1]); if (ver1Second > ver2Second) return 1; if (ver1Second < ver2Second) return -1; const ver1Third = parseInt(ver1Parts[2]); const ver2Third = parseInt(ver2Parts[2]); if (ver1Third > ver2Third) return 1; if (ver1Third < ver2Third) return -1; return 0; } export function isOn(val: string | undefined | null): boolean { if ((val === null) || (val === undefined)) { return false; } const lowerVal = val.toLowerCase(); return lowerVal === "on" || lowerVal === "yes" || lowerVal === "1" || lowerVal === "true"; } export function isUndefined(val: any): boolean { return typeof val === "undefined"; } export function isUndefinedOrNull(val: any): boolean { return isUndefined(val) || (val === null); } export function parseIntVal(val: any, defaultVal: number): number { if (isUndefinedOrNull(val)) { return defaultVal; } const parsedVal = parseInt(val, 10); if (isNaN(parsedVal)) { return defaultVal; } return parsedVal; } export function getEnv(envKey: string): string | null { const homeDir = homeDir(); if ((homeDir !== null) && envKey) { const envValue = readFileToStringSync( `${homeDir}/.config/envs/${envKey}`, ); if (envValue !== null) { return envValue.trim(); } } return osEnv(envKey) || null; } export function isEnvOn(envKey: string): boolean { return isOn(getEnv(envKey)); } export function formatHumanTime(timeMillis: number): string { const times = []; if (timeMillis < 1000) { return `${timeMillis}ms`; } const timeSecs = Math.floor(timeMillis / 1000); const timeSecsLow = timeSecs % 60; if (timeSecsLow > 0) { times.push(`${timeSecsLow}s`); } const timeMinutes = Math.floor(timeSecs / 60); const timeMinutesLow = timeMinutes % 60; if (timeMinutesLow > 0) { times.push(`${timeMinutesLow}m`); } const timeHours = Math.floor(timeMinutes / 60); const timeHoursLow = timeHours % 24; if (timeHoursLow > 0) { times.push(`${timeHoursLow}h`); } const timeDays = Math.floor(timeHours / 24); if (timeDays > 0) { times.push(`${timeDays}d`); } return times.reverse().join(" "); } export function formatSize(size: number): string { if (size < 0) { return "N/A"; } if (size == 0) { return "0B"; } const sizes = []; const bytesLow = size % 1024; if (bytesLow > 0) { sizes.push(`${bytesLow}B`); } const kb = Math.floor(size / 1024); const kbLow = kb % 1024; if (kbLow > 0) { sizes.push(`${kbLow}KiB`); } const mb = Math.floor(kb / 1024); const mbLow = mb % 1024; if (mbLow > 0) { sizes.push(`${mbLow}MiB`); } const gb = Math.floor(mb / 1024); if (gb > 0) { sizes.push(`${gb}GiB`); } return sizes.reverse().join(" "); } export function formatSize2(size: number): string { if (size < 0) { return "N/A"; } if (size < 1024) { return `${size}B`; } if (size < 1024 * 1024) { return `${formatNumber(size / 1024)}KiB`; } return `${formatNumber(size / (1024 * 1024))}MiB`; } export function formatPercent(a: number, b: number): string { if (b == null || b <= 0) { return "N/A"; } return formatNumber((a * 100) / b) + "%"; } export function formatNumber(num: number): string { const p = num.toString(); const pointIndex = p.indexOf("."); if (pointIndex < 0) { return p + ".00"; } const decimal = p.substring(pointIndex + 1); const decimalPart = decimal.length == 1 ? (decimal + "0") : decimal.substring(0, 2); return p.substring(0, pointIndex) + "." + decimalPart; } export async function clearLastLine() { await printLastLine(""); } export async function printLastLine(line: string) { const encodedLineBytes = new TextEncoder().encode( `\x1b[1000D${line}\x1b[K`, ); if (isDeno()) { await Deno.stdout.write(encodedLineBytes); } else { process.stdout.write(encodedLineBytes); } } interface ColorToken { type: "color" | "text"; content?: string; colorStart?: boolean; color?: string; } function parseColorTokens(message: string, renderColor: boolean): ColorToken[] { const tokens: ColorToken[] = []; if (message) { let inColorStart = false; let inColorEnd = false; let noEscape = false; let chars: string[] = []; let startedColors: string[] = []; const messageLength = message.length; const _isAllSlashOrEmpty = function (chars: string): { result: boolean; count: number; } { for (const char of chars) { if (char !== "/") { return { result: false, count: -1 }; } } return { result: true, count: chars.length }; }; for (let i = 0; i < messageLength; i++) { const c = message.charAt(i); const nextC = (i + 1) < messageLength ? message.charAt(i + 1) : null; const nextNextC = (i + 2) < messageLength ? message.charAt(i + 2) : null; if (noEscape) { if (c === "]" && nextC === "]" && nextNextC === "]") { // end no escape noEscape = false; i += 2; } else { chars.push(c); } continue; } switch (c) { case "\\": if (nextC === null) { chars.push(c); } else { chars.push(nextC); i++; } break; case "[": if (inColorStart || inColorEnd) { // SHOULD NOT HAPPEN break; } else if (nextC == "/") { inColorEnd = true; i++; } else if (nextC == "[" && nextNextC == "[") { // now no escape noEscape = true; i += 2; break; } else { inColorStart = true; } if (chars.length > 0) { tokens.push({ type: "text", content: chars.join(""), }); chars = []; } break; case "]": if (inColorStart || inColorEnd) { const isAllSlashOrEmpty = _isAllSlashOrEmpty(chars); if (isAllSlashOrEmpty.result && inColorEnd) { const popCount = isAllSlashOrEmpty.count + 1; for (let _i = 0; _i < popCount; _i++) { const poppedColor = startedColors.pop(); if (poppedColor) { tokens.push({ type: "color", colorStart: false, color: poppedColor, }); } } chars = []; } else if (chars.length > 0) { tokens.push({ type: "color", colorStart: inColorStart, color: chars.join(""), }); if (inColorStart) { startedColors.push(chars.join("")); } else { startedColors.pop(); } chars = []; } inColorStart = false; inColorEnd = false; } else { chars.push(c); } break; default: chars.push(c); break; } } const inColor = inColorStart || inColorEnd; if (chars.length > 0 && !inColor) { tokens.push({ type: "text", content: chars.join(""), }); } } return tokens; } const COLOR_MAP: Record = { blink: "5", bold: "1", b: "1", under: "4", u: "4", strikeout: "9", s: "9", black: "30", red: "31", green: "32", yellow: "33", blue: "34", pink: "35", cyan: "36", white: "37", bg_black: "40", bg_red: "41", bg_green: "42", bg_yellow: "43", bg_blue: "44", bg_pink: "45", bg_cyan: "46", bg_white: "47", black_bright: "90", red_bright: "91", green_bright: "92", yellow_bright: "93", blue_bright: "94", pink_bright: "95", cyan_bright: "96", white_bright: "97", bg_black_bright: "100", bg_red_bright: "101", bg_green_bright: "102", bg_yellow_bright: "103", bg_blue_bright: "104", bg_pink_bright: "105", bg_cyan_bright: "106", bg_white_bright: "107", }; function getColorCode(color: string): string { if (color.startsWith("#")) { return color.substring(1); } return COLOR_MAP[color]; } function renderColorTokens(tokens: ColorToken[]): string { const text: string[] = []; const colorMapStack = new Map(); for (const token of tokens) { if (token.type === "color") { const color = token.color; if (color) { const colorCode = getColorCode(color); if (!colorCode) { text.push(`[${token.colorStart ? "" : "/"}${color}]`); continue; } const colorStack = colorMapStack.get(color) ?? []; if (colorStack.length == 0) { colorMapStack.set(color, colorStack); } if (token.colorStart) { colorStack.push(1); } else { colorStack.pop(); text.push("\x1b[0m"); } const colors: string[] = []; for (const [color, colorStack] of colorMapStack) { if (colorStack.length > 0) { const currentColorCode = getColorCode(color); if (currentColorCode) { colors.push(currentColorCode); } } } if (colors.length > 0) { text.push(`\x1b[${colors.join(";")}m`); } } } else { if (token.content) { text.push(token.content); } } } text.push("\x1b[0m"); // FINALLY END ALL COLOR return text.join(""); } export function supportColor(): boolean { try { if (process.env.FORCE_COLOR !== undefined) { return process.env.FORCE_COLOR !== "0"; } if (process.env.NO_COLOR !== undefined) { return false; } return process.stdout.isTTY && process.stderr.isTTY; } catch (e) { // check color support failed, default false return false; } } class Term { constructor() { } blink(message: string): string { return `\x1b[5m${message}\x1b[0m`; } bold(message: string): string { return `\x1b[1m${message}\x1b[0m`; } under(message: string): string { return `\x1b[4m${message}\x1b[0m`; } red(message: string): string { return `\x1b[31m${message}\x1b[0m`; } green(message: string): string { return `\x1b[32m${message}\x1b[0m`; } yellow(message: string): string { return `\x1b[33m${message}\x1b[0m`; } blue(message: string): string { return `\x1b[34m${message}\x1b[0m`; } pink(message: string): string { return `\x1b[35m${message}\x1b[0m`; } cyan(message: string): string { return `\x1b[36m${message}\x1b[0m`; } auto(message: string, renderColor?: boolean): string { return renderColorTokens( parseColorTokens(message), renderColor ?? supportColor(), ); } } export const term = new Term(); function pad(message: string, length: number): string { if (message.length >= length) { return message; } return message + " ".repeat(length - message.length); } const LOGGER_PREFIX_LEN: number = 8; class Logger { constructor() { } // deno-lint-ignore no-explicit-any success(...data: any[]) { this.log( term.bold(term.green(`[${pad("SUCCESS", LOGGER_PREFIX_LEN)}]`)), data, ); } // deno-lint-ignore no-explicit-any error(...data: any[]) { this.log( term.bold(term.red(`[${pad("ERROR", LOGGER_PREFIX_LEN)}]`)), data, ); } // deno-lint-ignore no-explicit-any warn(...data: any[]) { this.log( term.bold(term.yellow(`[${pad("WARN", LOGGER_PREFIX_LEN)}]`)), data, ); } // deno-lint-ignore no-explicit-any warning(...data: any[]) { this.log( term.blink( term.bold(term.yellow(`[${pad("WARN", LOGGER_PREFIX_LEN)}]`)), ), data, ); } // deno-lint-ignore no-explicit-any info(...data: any[]) { this.log(term.bold(`[${pad("INFO", LOGGER_PREFIX_LEN)}]`), data); } // deno-lint-ignore no-explicit-any debug(...data: any[]) { this.log(`[${pad("DEBUG", LOGGER_PREFIX_LEN)}]`, data); } // deno-lint-ignore no-explicit-any log(prefix: string, data: any[]) { const args = [prefix]; for (let i = 0; i < data.length; i++) { args.push(data[i]); } console.log.apply(console, args); } } export const log = new Logger(); export function getHomeDirOrDie(): string { return homeDirOrDie(); } export function homeDirOrDie(): string { const homeDir = homeDir(); if (homeDir === null) { throw new Error("Cannot find home dir"); } return homeDir; } export function getHomeDir(): string | null { return homeDir(); } export function homeDir(): string | null { // if (Deno.build.os === "windows") { // const userProfile = Deno.env.get("USERPROFILE"); // if (userProfile) { // return userProfile; // } // const homeDrive = Deno.env.get("HOMEDRIVE"); // const homePath = Deno.env.get("HOMEPATH"); // if (homeDrive && homePath) { // return homeDrive + homePath; // } // return null; // } return osEnv("HOME") ?? null; } export function resolveFilename(filename: string): string { if (filename.startsWith("~/")) { return homeDir() + filename.substring(1); } return filename; } export function joinPath(path1: string, ...paths: string[]): string { let basePath = path1; if (paths != null && paths.length > 0) { for (let i = 0; i < paths.length; i++) { const path2 = paths[i]; if (basePath.endsWith("/") && path2.startsWith("/")) { basePath += path2.substring(1); } else if (basePath.endsWith("/") || path2.startsWith("/")) { basePath += path2; } else { basePath += "/" + path2; } } } return basePath; } export async function existsPath(path: string): Promise { if (isDeno()) { try { return await Deno.stat(path) != null; } catch { return false; } } return new Promise((resolve) => { fs.stat(path, (err, stats) => { if (err) { resolve(false); } else { resolve(stats != null); } }); }); } export async function readFileToString( filename: string, ): Promise { try { return await Deno.readTextFile(resolveFilename(filename)); } catch (e) { if (e instanceof Error && e.name == "NotFound") { return null; } throw e; } } export function readFileToStringSync(filename: string): string | null { try { return Deno.readTextFileSync(resolveFilename(filename)); } catch (e) { if (e instanceof Error && e.name == "NotFound") { return null; } throw e; } } export async function writeStringToFile( filename: string, data: string | null, ): Promise { const newFilename = resolveFilename(filename); if (data == null) { if (await existsPath(newFilename)) { await Deno.remove(newFilename); } } else { const parentDirname = dirname(newFilename); if (!await existsPath(parentDirname)) { await Deno.mkdir(parentDirname, { recursive: true }); } await Deno.writeTextFile(newFilename, data); } } export function uint8ArrayToHexString(uint8: Uint8Array): string { return Array.from(uint8) .map((b) => b.toString(16).padStart(2, "0")) .join(""); } export function hexStringToUint8Array(hex: string): Uint8Array { hex = hex.trim(); if (hex.startsWith("0x") || hex.startsWith("0X")) { hex = hex.slice(2); } if (hex.length % 2 !== 0) { throw new Error("Hex string must have an even number of characters"); } if (!/^[0-9a-fA-F]*$/.test(hex)) { throw new Error("Invalid hex string"); } const byteLength = hex.length / 2; const uint8 = new Uint8Array(byteLength); for (let i = 0; i < byteLength; i++) { uint8[i] = parseInt(hex.substring(i * 2, (i + 1) * 2), 16); } return uint8; } export function decodeBase64Url(base64UrlString: string): Uint8Array { let standardBase64 = base64UrlString.replace(/-/g, "+").replace(/_/g, "/"); while (standardBase64.length % 4) { standardBase64 += "="; } return decodeBase64(standardBase64); } export function encodeBase64Url( input: ArrayBuffer | Uint8Array | string, ): string { let standardBased64 = encodeBase64(input); return standardBased64.replace(/\+/g, "-").replace(/\//g, "_").replace( /=/g, "", ); } export async function getKeyRingPassword( service: string, user: string, ): string | null { const keyRingArgs = ["-g", "--json", "-S", service, "-U", user]; const processOutput = await execCommand("keyring.rs", keyRingArgs); const stdoutString = processOutput.getStdoutAsStringThenTrim(); const stderrString = processOutput.getStderrAsStringThenTrim(); if (processOutput.code != 0) { if (stderrString && stderrString.includes("Error: NoEntry")) { return null; } throw new Error( `keyring.rs -g failed, code: ${code}, stdout: ${stdoutString}, stderr: ${stderrString}`, ); } const result = JSON.parse(stdoutString) as { password: string; }; return result.password; } export async function setKeyRingPassword( service: string, user: string, password: string, ): void { const keyRingArgs = ["-s", "-S", service, "-U", user, "-P", password]; const processOutput = await execCommand("keyring.rs", keyRingArgs); const stdoutString = processOutput.getStdoutAsStringThenTrim(); const stderrString = processOutput.getStderrAsStringThenTrim(); if (processOutput.code != 0) { throw new Error( `keyring.rs -s failed, code: ${code}, stdout: ${stdoutString}, stderr: ${stderrString}`, ); } return; } export class ProcessBar { interval?: number; message: string; constructor(message?: string) { this.message = message || "Processing"; } async call(cb: () => Promise, clearLine?: boolean): Promise { this.start(); try { return await cb(); } finally { this.stop(clearLine); } } start(): void { const startMs = new Date().getTime(); let count = 0; this.interval = setInterval(() => { const dots = ".".repeat(((count++) % 10) + 1); const costMs = new Date().getTime() - startMs; const time = `${Math.floor(costMs / 1000)}s`; process.stderr.write(`\r${this.message} ${time} ${dots}\x1b[K`); }, 500); } stop(clearLine?: boolean): void { if (this.interval) { clearInterval(this.interval); process.stderr.write(clearLine ? "\r\x1b[K" : "\n"); } } } /** * @deprecated Use {@link fetchDataWithTimeout} instead. */ export async function fetchWithTimeout( input: URL | Request | string, timeout?: number, initCallback?: (init: RequestInit) => RequestInit, ): Promise { const fetchTimeout = timeout || 10000; const abortController = new AbortController(); const timeoutHandler = setTimeout(() => { abortController.abort(`Timeout ${fetchTimeout} ms`); }, fetchTimeout); let init: RequestInit = {}; init.signal = abortController.signal; if (initCallback) { init = initCallback(init); } const response = await fetch(input, init); clearTimeout(timeoutHandler); return response; } export type RequestInitWithTimeout = RequestInit & { timeoutMillis?: number; }; export async function fetchDataWithTimeout( input: URL | Request | string, init: RequestInitWithTimeout = {}, ): Promise { const timeout = init.timeoutMillis ?? 10000; const abortController = new AbortController(); const timeoutHandler = setTimeout(() => { abortController.abort( `Fetch '${input}' timeout: ${timeout} ms`, ); }, timeout); try { return await fetch(input, { ...init, signal: abortController.signal, }); } finally { clearTimeout(timeoutHandler); } } export function getCurrentScriptFile(): string { return fromFileUrl(import.meta.url); } export function getCurrentScriptDirectory(): string { return dirname(getCurrentScriptFile()); } export function stringifySorted>( record: T, space?: string | number, ): string { return JSON.stringify(record, (key, value) => { if ( value !== null && typeof value === "object" && !Array.isArray(value) ) { const sortedKeys = Object.keys(value).sort(); const sortedObj: Record = {}; for (const k of sortedKeys) { sortedObj[k] = value[k]; } return sortedObj; } return value; }, space); } export function stringifyPretty(object: any): string { return JSON.stringify(object, null, 2); }