import { colors, path, walkSync } from "./deps.ts";import { env } from "./env.ts";
const DRAKE_VERS = "1.6.0";
export function vers(): string { return DRAKE_VERS;}
export class DrakeError extends Error { constructor(message?: string) { super(message); this.name = "DrakeError"; }}
export function stat(path: string): Deno.FileInfo | null { try { return Deno.statSync(path); } catch (err) { if (err.code === "ENOENT") { return null; } else { throw err; } }}
export function pathExists(path: string): boolean { return stat(path) != null;}
export function isFile(path: string): boolean { return !!stat(path)?.isFile;}
export function isDirectory(path: string): boolean { return !!stat(path)?.isDirectory;}
export function abort(message: string): never { if (env("--abort-exits")) { message = `${colors.red(colors.bold("error"))}: ${message}`; if (env("--debug")) { const e = new Error(); if (e.stack) { message += `\n${e.stack}`; } } console.error(message); Deno.exit(1); } else { throw new DrakeError(message); }}
export function log(message: string): void { if (!env("--quiet")) { console.log(message); }}
export function logExecution(title: string, info: string, duration?: number) { if (env("--quiet")) { return; } if ((title == "sh" || title == "shCapture") && !env("--verbose")) { return; } info = `${colors.green(colors.bold(`${title}: `))}${info}`; if (duration !== undefined) { info += ` ${colors.brightWhite(colors.bold(`(${duration}ms)`))}`; } log(info);}
export function debug(title: string, message: any = ""): void { if (typeof message === "object") { message = JSON.stringify(message, null, 1); } if (env("--debug") && Deno.isatty(Deno.stderr.rid)) { if (title !== "") { message = `${colors.yellow(colors.bold(title + ":"))} ${message}`; } console.error(message); }}
export function quote(values: string[], sep = " "): string { values = values.map((value) => `"${value.replace(/"/g, '\\"')}"`); return values.join(sep);}
export async function sleep(ms: number): Promise<unknown> { return new Promise((resolve) => setTimeout(resolve, ms));}
export function readFile(filename: string): string { try { const result = Deno.readTextFileSync(filename); debug( "readFile", `${filename}: ${result.length} characters read`, ); return result; } catch (e) { abort(`readFile: ${filename}: ${e.message}`); }}
export function writeFile(filename: string, text: string): boolean { const exists = !!stat(filename); try { debug( "writeFile", `${filename}: ${text.length} characters written`, ); Deno.writeTextFileSync(filename, text); } catch (e) { abort(`writeFile: ${filename}: ${e.message}`); } return !exists;}
export function updateFile( filename: string, find: RegExp, replace: string,): boolean { debug( "updateFile", `${filename}: find: ${find}, replace: "${replace}"`, ); let changed = false; const text = readFile(filename); const updatedText = text.replace(find, replace); if (text !== updatedText) { writeFile(filename, updatedText); changed = true; } return changed;}
export function makeDir(dir: string): boolean { debug("makeDir", dir); const exists = !!stat(dir); if (exists) { if (!stat(dir)?.isDirectory) { abort(`file is not directory: ${dir}`); } } else { Deno.mkdirSync(dir, { recursive: true }); } return !exists;}
export function glob(...patterns: string[]): string[] { function glob1(pattern: string): string[] { const globOptions = { extended: true, globstar: true } as const; pattern = path.normalizeGlob(pattern, globOptions); let root = path.dirname(pattern); while (root !== "." && path.isGlob(root)) { root = path.dirname(root); } if (!stat(root)) { return []; } const regexp = path.globToRegExp(pattern, globOptions); const iter = walkSync(root, { match: [regexp], includeDirs: false }); return Array.from(iter, (info) => info.path); } debug("glob", `${quote(patterns, ", ")}`); let result: string[] = []; for (const pattern of patterns) { try { result = [...result, ...glob1(pattern)]; } catch (e) { abort(`${pattern}: ${e.message}`); } } result = [...new Set(result)].map((p) => path.normalize(p)).sort(); debug("", `${result.slice(0, 100).join("\n")}`); if (result.length > 100) { debug("", `... (${result.length} files)`); } return result;}
export function remove(...patterns: string[]): void { debug("remove", `${quote(patterns, ", ")}`); for (const f of glob(...patterns)) { Deno.removeSync(f); }}
function shArgs(command: string): string[] { if (Deno.build.os === "windows") { return ["PowerShell.exe", "-Command", command]; } else { let shellExe = Deno.env.get("SHELL")!; if (!shellExe) { shellExe = "/bin/bash"; if (!stat(shellExe)?.isFile) { abort( `cannot locate shell: no SHELL environment variable or ${shellExe} executable`, ); } } return [shellExe, "-c", command]; }}
export interface ShOpts { cwd?: string; env?: { [key: string]: string }; stdout?: "inherit" | "piped" | "null" | number; stderr?: "inherit" | "piped" | "null" | number;}
export async function sh(commands: string | string[], opts: ShOpts = {}) { if (typeof commands === "string") { commands = [commands]; } debug("sh", `${commands.join("\n")}\nopts: ${JSON.stringify(opts)}`); const startTime = new Date().getTime(); const processes: Deno.Process[] = []; const results: Deno.ProcessStatus[] = []; try { for (const cmd of commands) { const p = Deno.run({ cmd: shArgs(cmd), cwd: opts.cwd, env: opts.env, stdout: opts.stdout ?? "inherit", stderr: opts.stderr ?? "inherit", }); processes.push(p); } results.push(...await Promise.all(processes.map((p) => p.status()))); } finally { for (const p of processes) { p.close(); } } for (const i in results) { const cmd = commands[i]; const code = results[i].code; if (code === undefined) { abort(`sh: ${cmd}: undefined exit code`); } if (code !== 0) { abort(`sh: ${cmd}: error code: ${code}`); } } logExecution( "sh", `${commands.join("\n")}`, new Date().getTime() - startTime, );}
export type ShOutput = { code: number | undefined; output: string; error: string;};
export interface ShCaptureOpts extends ShOpts { input?: string;}
export async function shCapture( command: string, opts: ShCaptureOpts = {},): Promise<ShOutput> { const startTime = new Date().getTime(); const p = Deno.run({ cmd: shArgs(command), cwd: opts.cwd, env: opts.env, stdin: opts.input !== undefined ? "piped" : undefined, stdout: opts.stdout ?? "piped", stderr: opts.stderr ?? "inherit", }); let status: Deno.ProcessStatus; let outputBytes, errorBytes: Uint8Array; try { if (p.stdin) { await p.stdin.write( new TextEncoder().encode(opts.input), ); p.stdin.close(); } [status, outputBytes, errorBytes] = await Promise.all( [ p.status(), p.stdout ? p.output() : Promise.resolve(new Uint8Array()), p.stderr ? p.stderrOutput() : Promise.resolve(new Uint8Array()), ], ); } finally { p.close(); } const result = { code: status.code, output: new TextDecoder().decode(outputBytes), error: new TextDecoder().decode(errorBytes), } as const; debug( "shCapture", `${command}\nopts: ${JSON.stringify(opts)}\noutputs: ${ JSON.stringify(result) }`, ); logExecution("shCapture", command, new Date().getTime() - startTime); return result;}