// 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"; // 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 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; } getStdoutAsStringThenTrim(): string { return this.stdout.trim(); } getStdoutAsJson(): any { return JSON.parse(this.stdout); } getStderrAsJson(): any { return JSON.parse(this.stderr); } } export async function execCommand( command: string, args?: string[], options?: Deno.CommandOptions, ): Promise { 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), ); } export async function execCommandShell( command: string, args?: string[], options?: Deno.CommandOptions, ): Promise { 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; } 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 = getHomeDir(); if ((homeDir !== null) && envKey) { const envValue = readFileToStringSync( `${homeDir}/.config/envs/${envKey}`, ); if (envValue !== null) { return envValue.trim(); } } return Deno.env.get(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) { await Deno.stdout.write( new TextEncoder().encode( `\x1b[1000D${line}\x1b[K`, ), ); } 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 chars: string[] = []; const messageLength = message.length; for (let i = 0; i < messageLength; i++) { const c = message.charAt(i); const nextC = (i + 1) < messageLength ? message.charAt(i + 1) : null; 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 { inColorStart = true; } if (chars.length > 0) { tokens.push({ type: "text", content: chars.join(""), }); chars = []; } break; case "]": if (inColorStart || inColorEnd) { if (chars.length > 0) { tokens.push({ type: "color", colorStart: inColorStart, color: chars.join(""), }); 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", under: "4", red: "31", green: "32", yellow: "33", blue: "34", pink: "35", cyan: "36", }; 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 = COLOR_MAP[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 = COLOR_MAP[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 { const homeDir = getHomeDir(); if (homeDir === null) { throw new Error("Cannot find home dir"); } return homeDir; } export function getHomeDir(): 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 Deno.env.get("HOME") || null; } export function resolveFilename(filename: string): string { if (filename.startsWith("~/")) { return getHomeDir() + 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 { try { const stat = await Deno.stat(path); return stat != null; } catch { return false; } } 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 function getKeyRingPassword( service: string, user: string, ): string | null { const command = new Deno.Command("keyring.rs", { args: ["-g", "--json", "-S", service, "-U", user], }); const { code, stdout, stderr } = command.outputSync(); const stdoutString = new TextDecoder().decode(stdout); const stderrString = new TextDecoder().decode(stderr); if (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 function setKeyRingPassword( service: string, user: string, password: string, ): void { const command = new Deno.Command("keyring.rs", { args: ["-s", "-S", service, "-U", user, "-P", password], }); const { code, stdout, stderr } = command.outputSync(); const stdoutString = new TextDecoder().decode(stdout); const stderrString = new TextDecoder().decode(stderr); if (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"); } } } 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 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); }