#!/usr/bin/env runts -- --allow-all import { args, execCommand, execCommandShell, existsPath, log, readFileToString, stringifyPretty, term, uint8ArrayToHexString, writeStringToFile, } from "https://script.hatter.ink/@39/deno-commons-mod.ts"; import {parseArgs} from "jsr:@std/cli/parse-args"; function handleHelp() { console.log(term.auto(`Build scripts to bundle and sign. Usage: [blue][bold]build.ts[//] --help [blue][bold]build.ts[//] [-M|--no-min] [-S|--skip-sign] [-F|--force] [bold]FILES[/]`)); } async function main() { const flags = parseArgs(args(), { boolean: [ "help", "no-min", "skip-sign", "force", ], alias: { h: "help", S: "skip-sign", M: "no-min", F: "force", }, }); if (flags.help) { handleHelp(); return; } if (flags._.length === 0) { log.error(`No files specified`); return; } flags.min = !flags["no-min"]; const files: string[] = flags._.filter((f) => { return f.endsWith(".ts") && !f.includes(".bundle."); }); const filesCount = files.length; log.info(`Total ${filesCount} file(s)`); for (let i = 0; i < filesCount; i++) { const file = files[i]; if (file.includes(".bundle.") || !file.endsWith(".ts")) { log.warn(`Skip bundle file: ${file} #${i + 1} of ${filesCount}`); continue; } log.info(`Building file: ${file} #${i + 1} of ${filesCount}`); await buildFile(file, flags); } } function getBundleFilename(file: string): string { const lastIndexOfDot = file.lastIndexOf("."); if (lastIndexOfDot === -1) { return `${file}.bundle.ts`; } return `${file.substring(0, lastIndexOfDot)}.bundle.${ file.substring(lastIndexOfDot + 1) }`; } async function sha256OfString(input: string): Promise { const data = new TextEncoder().encode(input); const hashBuffer = await crypto.subtle.digest("SHA-256", data); return uint8ArrayToHexString(new Uint8Array(hashBuffer)); } interface SourceFileMeta { sha256: string; builtTime: number; min: boolean; } async function writeBundleSourceSha256( sourceFile: string, bundleFile: string, flags: any, ) { const sourceFileSha256 = `${bundleFile}.source-meta`; const sourceFileContent = await readFileToString(sourceFile); const sourceFileContentSha256 = await sha256OfString(sourceFileContent); await writeStringToFile( sourceFileSha256, stringifyPretty({ sha256: sourceFileContentSha256, builtTime: Date.now(), min: flags.min, } as SourceFileMeta), ); } async function checkBundleSourceSha256( sourceFile: string, bundleFile: string, flags: any, ) { const sourceFileSha256 = `${bundleFile}.source-meta`; if (!await existsPath(sourceFileSha256)) { return false; } const sourceFileSha256Content = await readFileToString(sourceFileSha256); const sourceFileSha256Meta = JSON.parse( sourceFileSha256Content, ) as SourceFileMeta; if (sourceFileSha256Meta.min !== flags.min) { return false; } const sourceFileContent = await readFileToString(sourceFile); const sourceFileContentSha256 = await sha256OfString(sourceFileContent); return sourceFileContentSha256 === sourceFileSha256Meta.sha256; } async function buildFile(file: string, flags: any) { const denoCmd = "deno"; const bundleFile = getBundleFilename(file); const denoBundleArgs = ["bundle"]; if (flags.min) { denoBundleArgs.push("--minify"); } denoBundleArgs.push("--allow-import"); denoBundleArgs.push(file); denoBundleArgs.push("-o"); denoBundleArgs.push(bundleFile); const isSourceFileMatches = await checkBundleSourceSha256( file, bundleFile, flags, ); if (isSourceFileMatches && !flags.force) { log.info(`Check file ${file} sha256 matches, skip build`); return; } if (isSourceFileMatches) { log.info(`Force build file is on`); } log.debug("Run deno build: ", denoCmd, denoBundleArgs); await execCommandShell(denoCmd, denoBundleArgs); if (flags["skip-sign"]) { log.warn(`Skip signature for file: ${bundleFile}`); } else { const signScriptUserPinOutput = await execCommand("keyring.rs", [ "-gRU", "yubikey:script-sign", ]); const scriptSignArgs: string[] = []; if (signScriptUserPinOutput.code !== 0) { log.warn(`Read script sign PIN failed: `, signScriptUserPinOutput); } else { scriptSignArgs.push("--pin"); scriptSignArgs.push(signScriptUserPinOutput.stdoutThenTrim()); } scriptSignArgs.push(bundleFile); const ret = await execCommandShell("script-sign.rs", scriptSignArgs); if (ret !== 0) { log.error(`Sign script: ${bundleFile} failed, ret code: ${ret}`); return; } } await execCommandShell("chmod", ["+x", bundleFile]); await writeBundleSourceSha256(file, bundleFile, flags); } await main(); // @SCRIPT-SIGNATURE-V1: yk-r1.ES256.20260211T222821+08:00.MEQCIAweu8hlz6/t/UPWtHzB // 6fm4DZPTSfP7NBPSjad5qmB9AiAkpgAvpLAD1qKd+Cz0KBJT6GQq30yt1RKEbYtgKEQjHA==