diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index 8a9c537..eb912c6 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -30,6 +30,15 @@ jobs: - runner: ubuntu-latest target: bun-windows-x64 artifact: github-code-search-windows-x64.exe + - runner: ubuntu-latest + target: bun-windows-x64-baseline + artifact: github-code-search-windows-x64-baseline.exe + - runner: ubuntu-latest + target: bun-windows-x64-modern + artifact: github-code-search-windows-x64-modern.exe + - runner: ubuntu-latest + target: bun-windows-arm64 + artifact: github-code-search-windows-arm64.exe # macOS — must run on macOS for ad-hoc codesigning - runner: macos-latest target: bun-darwin-x64 diff --git a/README.md b/README.md index e46c830..3f8320c 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,22 @@ keyboard-driven TUI, fine-grained extract selection, markdown/JSON output. ## Quick start +**macOS / Linux** + ```bash export GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx curl -fsSL https://raw.githubusercontent.com/fulll/github-code-search/main/install.sh | bash github-code-search query "TODO" --org my-org ``` +**Windows** (PowerShell) + +```powershell +$env:GITHUB_TOKEN = "ghp_xxxxxxxxxxxxxxxxxxxx" +powershell -c "irm https://raw.githubusercontent.com/fulll/github-code-search/main/install.ps1 | iex" +github-code-search query "TODO" --org my-org +``` + ## Features - **Org-wide search** — queries all repositories in a GitHub organization in one command, with automatic pagination up to 1 000 results diff --git a/build.test.ts b/build.test.ts new file mode 100644 index 0000000..6eb3338 --- /dev/null +++ b/build.test.ts @@ -0,0 +1,325 @@ +import { describe, it, expect } from "bun:test"; +import { + parseTarget, + parseTargetArg, + isWindowsTarget, + getOutfile, + getBuildCompileOptions, + buildLabel, + buildCopyrightLine, + type WindowsMeta, +} from "./build"; +// ─── parseTargetArg ───────────────────────────────────────────────────────────── + +describe("parseTargetArg", () => { + it("extracts the target value from argv", () => { + expect(parseTargetArg(["bun", "build.ts", "--target=bun-linux-x64"])).toBe("bun-linux-x64"); + }); + + it("returns null when no --target= flag is present", () => { + expect(parseTargetArg(["bun", "build.ts"])).toBeNull(); + }); + + it("returns null for empty argv", () => { + expect(parseTargetArg([])).toBeNull(); + }); + + it("ignores unrelated flags", () => { + expect(parseTargetArg(["bun", "--verbose", "--target=bun-windows-x64"])).toBe( + "bun-windows-x64", + ); + }); + + it("returns empty string when --target= has no value", () => { + expect(parseTargetArg(["bun", "--target="])).toBe(""); + }); +}); +// ─── parseTarget ───────────────────────────────────────────────────────────── + +describe("parseTarget", () => { + it("parses bun-linux-x64", () => { + expect(parseTarget("bun-linux-x64")).toEqual({ os: "linux", arch: "x64" }); + }); + + it("parses bun-linux-x64-baseline", () => { + expect(parseTarget("bun-linux-x64-baseline")).toEqual({ + os: "linux", + arch: "x64-baseline", + }); + }); + + it("parses bun-linux-arm64", () => { + expect(parseTarget("bun-linux-arm64")).toEqual({ + os: "linux", + arch: "arm64", + }); + }); + + it("parses bun-linux-arm64-musl", () => { + expect(parseTarget("bun-linux-arm64-musl")).toEqual({ + os: "linux", + arch: "arm64-musl", + }); + }); + + it("parses bun-darwin-x64", () => { + expect(parseTarget("bun-darwin-x64")).toEqual({ + os: "darwin", + arch: "x64", + }); + }); + + it("parses bun-darwin-arm64", () => { + expect(parseTarget("bun-darwin-arm64")).toEqual({ + os: "darwin", + arch: "arm64", + }); + }); + + it("parses bun-windows-x64", () => { + expect(parseTarget("bun-windows-x64")).toEqual({ + os: "windows", + arch: "x64", + }); + }); + + it("parses bun-windows-x64-baseline", () => { + expect(parseTarget("bun-windows-x64-baseline")).toEqual({ + os: "windows", + arch: "x64-baseline", + }); + }); + + it("parses bun-windows-x64-modern", () => { + expect(parseTarget("bun-windows-x64-modern")).toEqual({ + os: "windows", + arch: "x64-modern", + }); + }); + + it("parses bun-windows-arm64", () => { + expect(parseTarget("bun-windows-arm64")).toEqual({ + os: "windows", + arch: "arm64", + }); + }); + + it("returns native platform when target is null", () => { + const result = parseTarget(null); + // We can only assert the shape, not the exact values (depends on the runner) + expect(typeof result.os).toBe("string"); + expect(typeof result.arch).toBe("string"); + expect(result.os.length).toBeGreaterThan(0); + }); + + it("returns native platform when target is undefined", () => { + const result = parseTarget(undefined); + expect(typeof result.os).toBe("string"); + expect(typeof result.arch).toBe("string"); + }); + + // baseline must be parsed before plain x64 (order matters in the if-chain) + it("does not confuse windows-x64-baseline with windows-x64", () => { + expect(parseTarget("bun-windows-x64-baseline").arch).toBe("x64-baseline"); + expect(parseTarget("bun-windows-x64").arch).toBe("x64"); + }); + + it("does not confuse linux-x64-baseline with linux-x64", () => { + expect(parseTarget("bun-linux-x64-baseline").arch).toBe("x64-baseline"); + expect(parseTarget("bun-linux-x64").arch).toBe("x64"); + }); + + // Regression: on Windows, process.platform is "win32" which isWindowsTarget() + // does not recognise. parseTarget(null) must normalise it to "windows". + // We can't execute the null path on a non-Windows runner, but we can assert the + // downstream contract: "win32" alone must NOT satisfy isWindowsTarget (proving + // normalisation is necessary) and getOutfile("windows", null) must add .exe. + it("isWindowsTarget rejects bare win32 — normalisation in parseTarget is required", () => { + expect(isWindowsTarget("win32")).toBe(false); + expect(isWindowsTarget("windows")).toBe(true); + expect(getOutfile("windows", null)).toBe("./dist/github-code-search.exe"); + }); + + // Regression: the end-of-function fallback (unrecognised target string) must + // also normalise win32 → windows, not leak the raw Node.js platform alias. + it("never returns win32 as os for unknown target strings", () => { + const result = parseTarget("bun-completely-unknown-future-target"); + expect(result.os).not.toBe("win32"); + }); +}); + +// ─── isWindowsTarget ───────────────────────────────────────────────────────── + +describe("isWindowsTarget", () => { + it("returns true for windows", () => { + expect(isWindowsTarget("windows")).toBe(true); + }); + + it("returns false for linux", () => { + expect(isWindowsTarget("linux")).toBe(false); + }); + + it("returns false for darwin", () => { + expect(isWindowsTarget("darwin")).toBe(false); + }); +}); + +// ─── getOutfile ─────────────────────────────────────────────────────────────── + +describe("getOutfile", () => { + it("adds .exe suffix for windows targets", () => { + expect(getOutfile("windows", "bun-windows-x64")).toBe( + "./dist/github-code-search-windows-x64.exe", + ); + }); + + it("adds .exe suffix for windows-x64-modern", () => { + expect(getOutfile("windows", "bun-windows-x64-modern")).toBe( + "./dist/github-code-search-windows-x64-modern.exe", + ); + }); + + it("adds .exe suffix for windows-x64-baseline", () => { + expect(getOutfile("windows", "bun-windows-x64-baseline")).toBe( + "./dist/github-code-search-windows-x64-baseline.exe", + ); + }); + + it("adds .exe suffix for windows-arm64", () => { + expect(getOutfile("windows", "bun-windows-arm64")).toBe( + "./dist/github-code-search-windows-arm64.exe", + ); + }); + + it("does not add .exe for linux targets", () => { + expect(getOutfile("linux", "bun-linux-x64")).toBe("./dist/github-code-search-linux-x64"); + }); + + it("does not add .exe for darwin targets", () => { + expect(getOutfile("darwin", "bun-darwin-arm64")).toBe("./dist/github-code-search-darwin-arm64"); + }); + + it("omits suffix for native (no target)", () => { + const outfile = getOutfile("linux", null); + expect(outfile).toBe("./dist/github-code-search"); + }); + + it("strips the bun- prefix from the suffix", () => { + expect(getOutfile("linux", "bun-linux-arm64")).toBe("./dist/github-code-search-linux-arm64"); + }); +}); + +// ─── getBuildCompileOptions ─────────────────────────────────────────────────── + +const FULL_META: WindowsMeta = { + iconPath: "/abs/path/favicon.ico", + title: "github-code-search", + publisher: "fulll", + appVersion: "1.2.3", + description: "Interactive GitHub code search", + copyright: "Copyright © 2026 fulll — MIT", +}; + +describe("getBuildCompileOptions", () => { + it("returns only outfile for non-windows targets", () => { + const opts = getBuildCompileOptions("linux", "./dist/foo", FULL_META); + expect(opts).toEqual({ outfile: "./dist/foo" }); + }); + + it("returns only outfile for darwin", () => { + const opts = getBuildCompileOptions("darwin", "./dist/foo", FULL_META); + expect(opts).toEqual({ outfile: "./dist/foo" }); + }); + + it("includes hideConsole for windows target", () => { + const opts = getBuildCompileOptions("windows", "./dist/foo.exe", FULL_META); + expect(opts).toMatchObject({ + outfile: "./dist/foo.exe", + windows: { hideConsole: true }, + }); + }); + + it("sets icon from iconPath", () => { + const opts = getBuildCompileOptions("windows", "./dist/foo.exe", FULL_META) as { + windows: WindowsMeta & { icon: string; hideConsole: boolean }; + }; + expect(opts.windows.icon).toBe("/abs/path/favicon.ico"); + }); + + it("sets title", () => { + const opts = getBuildCompileOptions("windows", "./dist/foo.exe", FULL_META) as { + windows: { title: string }; + }; + expect(opts.windows.title).toBe("github-code-search"); + }); + + it("sets publisher", () => { + const opts = getBuildCompileOptions("windows", "./dist/foo.exe", FULL_META) as { + windows: { publisher: string }; + }; + expect(opts.windows.publisher).toBe("fulll"); + }); + + it("sets version from appVersion", () => { + const opts = getBuildCompileOptions("windows", "./dist/foo.exe", FULL_META) as { + windows: { version: string }; + }; + expect(opts.windows.version).toBe("1.2.3"); + }); + + it("sets description", () => { + const opts = getBuildCompileOptions("windows", "./dist/foo.exe", FULL_META) as { + windows: { description: string }; + }; + expect(opts.windows.description).toBe("Interactive GitHub code search"); + }); + + it("sets copyright", () => { + const opts = getBuildCompileOptions("windows", "./dist/foo.exe", FULL_META) as { + windows: { copyright: string }; + }; + expect(opts.windows.copyright).toBe("Copyright © 2026 fulll — MIT"); + }); + + it("works with empty meta (all windows fields undefined)", () => { + const opts = getBuildCompileOptions("windows", "./dist/foo.exe") as { + outfile: string; + windows: Record; + }; + expect(opts.outfile).toBe("./dist/foo.exe"); + expect(opts.windows.icon).toBeUndefined(); + expect(opts.windows.title).toBeUndefined(); + }); +}); + +// ─── buildLabel ────────────────────────────────────────────────────────────── + +describe("buildLabel", () => { + it("formats the label string correctly", () => { + expect(buildLabel("1.9.0", "abc1234", "linux", "x64")).toBe("1.9.0 (abc1234 · linux/x64)"); + }); + + it("works with windows target", () => { + expect(buildLabel("1.9.0", "abc1234", "windows", "x64-modern")).toBe( + "1.9.0 (abc1234 · windows/x64-modern)", + ); + }); + + it("works with dev commit", () => { + expect(buildLabel("1.9.0", "dev", "darwin", "arm64")).toBe("1.9.0 (dev · darwin/arm64)"); + }); +}); + +// ─── buildCopyrightLine ────────────────────────────────────────────────────── + +describe("buildCopyrightLine", () => { + it("formats the copyright string correctly", () => { + expect(buildCopyrightLine(2026, "fulll", "MIT")).toBe("Copyright © 2026 fulll — MIT"); + }); + + it("uses the provided year", () => { + expect(buildCopyrightLine(2030, "Acme Corp", "Apache-2.0")).toBe( + "Copyright © 2030 Acme Corp — Apache-2.0", + ); + }); +}); diff --git a/build.ts b/build.ts index 326cc03..c5484fe 100644 --- a/build.ts +++ b/build.ts @@ -3,118 +3,244 @@ * Build script – compiles github-code-search.ts into a standalone binary. * * Usage: - * bun run build.ts.ts # current platform - * bun run build.ts.ts --target=bun-linux-x64 # cross-compile + * bun run build.ts # current platform + * bun run build.ts --target=bun-linux-x64 # cross-compile * * Supported targets (Bun executables): * bun-linux-x64 bun-linux-x64-baseline bun-linux-arm64 * bun-darwin-x64 bun-darwin-arm64 - * bun-windows-x64 + * bun-windows-x64 bun-windows-x64-baseline bun-windows-x64-modern + * bun-windows-arm64 */ -import { version } from "./package.json" with { type: "json" }; +import { version, description, author, license } from "./package.json" with { type: "json" }; -// ─── CLI args ────────────────────────────────────────────────────────────────── +// ─── Pure helpers (exported for unit tests) ─────────────────────────────────── -const targetArg = process.argv.find((a) => a.startsWith("--target=")); -const target = targetArg?.slice("--target=".length) ?? null; +export type ParsedTarget = { + os: string; + arch: string; +}; -// ─── Derive OS / arch from target ───────────────────────────────────────────── +/** + * Derive a canonical { os, arch } pair from a Bun target string such as + * "bun-windows-x64-modern" or "bun-linux-arm64". + * Returns { os: process.platform, arch: process.arch } when `t` is null/undefined. + */ +export function parseTarget(t: string | null | undefined): ParsedTarget { + if (!t) + return { + // Normalize Node's "win32" platform name to the canonical "windows" used + // throughout this build script so isWindowsTarget() / getOutfile() work + // correctly when building natively on Windows without a --target flag. + os: process.platform === "win32" ? "windows" : process.platform, + arch: process.arch === "x64" ? "x64" : process.arch, + }; -function parseTarget(t: string): { os: string; arch: string } { const s = t.replace(/^bun-/, ""); + if (s.startsWith("linux-x64-baseline")) return { os: "linux", arch: "x64-baseline" }; if (s.startsWith("linux-x64")) return { os: "linux", arch: "x64" }; if (s.startsWith("linux-arm64-musl")) return { os: "linux", arch: "arm64-musl" }; if (s.startsWith("linux-arm64")) return { os: "linux", arch: "arm64" }; if (s.startsWith("darwin-x64")) return { os: "darwin", arch: "x64" }; if (s.startsWith("darwin-arm64")) return { os: "darwin", arch: "arm64" }; + if (s.startsWith("windows-x64-baseline")) return { os: "windows", arch: "x64-baseline" }; + if (s.startsWith("windows-x64-modern")) return { os: "windows", arch: "x64-modern" }; if (s.startsWith("windows-x64")) return { os: "windows", arch: "x64" }; - return { os: process.platform, arch: process.arch }; + if (s.startsWith("windows-arm64")) return { os: "windows", arch: "arm64" }; + + // Fix: normalize win32 → windows for unrecognised target strings (e.g. future targets). + return { os: process.platform === "win32" ? "windows" : process.platform, arch: process.arch }; } -const { os: targetOs, arch: targetArch } = target - ? parseTarget(target) - : { - os: process.platform, - arch: process.arch === "x64" ? "x64" : process.arch, - }; +/** + * Returns true when the given os value targets Windows. + */ +export function isWindowsTarget(targetOs: string): boolean { + return targetOs === "windows"; +} -// ─── Output path ─────────────────────────────────────────────────────────────── +/** + * Compute the output filename for the binary. + * + * target=bun-linux-x64 → dist/github-code-search-linux-x64 + * target=bun-windows-x64 → dist/github-code-search-windows-x64.exe + * target=bun-windows-x64-modern → dist/github-code-search-windows-x64-modern.exe + * target=null (native) → dist/github-code-search + */ +export function getOutfile(targetOs: string, target: string | null): string { + const ext = isWindowsTarget(targetOs) ? ".exe" : ""; + const suffix = target ? `-${target.replace(/^bun-/, "")}` : ""; + return `./dist/github-code-search${suffix}${ext}`; +} + +export type WindowsMeta = { + iconPath?: string; + title?: string; + publisher?: string; + appVersion?: string; + description?: string; + copyright?: string; +}; + +/** + * Build the `compile` options forwarded to Bun.build(). + * Windows binaries receive metadata-enriching flags so the resulting .exe + * is not misidentified as "bun" by the OS and shows correct file properties. + * See: https://bun.sh/docs/bundler/executables#windows-specific-flags + * + * @param meta.iconPath Absolute path to .ico — must be absolute so Bun + * resolves it correctly regardless of the caller's CWD. + */ +export function getBuildCompileOptions( + targetOs: string, + outfile: string, + meta: WindowsMeta = {}, +): NonNullable[0]["compile"]> { + if (isWindowsTarget(targetOs)) { + return { + outfile, + windows: { + // iconPath must be absolute — relative paths are resolved from the + // process CWD which may differ from the script location. + icon: meta.iconPath, + // hideConsole: true — prevents Windows from spawning a detached console + // window when the binary is launched from a GUI context (e.g. Explorer). + // The binary still runs correctly in any terminal emulator / cmd / pwsh. + hideConsole: true, + title: meta.title, + publisher: meta.publisher, + version: meta.appVersion, + description: meta.description, + copyright: meta.copyright, + }, + }; + } + return { outfile }; +} -const ext = targetOs === "windows" ? ".exe" : ""; -const suffix = target ? `-${target.replace(/^bun-/, "")}` : ""; -const outfile = `./dist/github-code-search${suffix}${ext}`; +/** + * Extract the --target= argument from a process argv array. + * Pure function — extracted so it can be unit-tested without touching process.argv. + */ +export function parseTargetArg(argv: string[]): string | null { + return argv.find((a) => a.startsWith("--target="))?.slice("--target=".length) ?? null; +} -// ─── Git commit hash ────────────────────────────────────────────────────────── +/** + * Format the human-readable version label printed during the build. + * Pure function — extracted so it can be unit-tested. + */ +export function buildLabel(ver: string, commit: string, os: string, arch: string): string { + return `${ver} (${commit} · ${os}/${arch})`; +} -let commit = "dev"; -try { - const proc = Bun.spawn(["git", "rev-parse", "--short", "HEAD"], { - stdout: "pipe", - stderr: "pipe", - }); - await proc.exited; - commit = (await new Response(proc.stdout).text()).trim() || "dev"; -} catch { - // Not a git repo or git not available +/** + * Format the copyright string embedded in Windows EXE metadata. + * Pure function — extracted so it can be unit-tested. + */ +export function buildCopyrightLine(year: number, authorName: string, lic: string): string { + return `Copyright © ${year} ${authorName} — ${lic}`; } -// ─── Build ──────────────────────────────────────────────────────────────────── - -const label = `${version} (${commit} · ${targetOs}/${targetArch})`; -console.log(`Building github-code-search v${label}… outfile=${outfile}`); -if (target) console.log(` Target: ${target}`); - -await Bun.$`mkdir -p dist`; -await Bun.build({ - entrypoints: ["./github-code-search.ts"], - minify: true, - bytecode: true, - compile: { - outfile, - }, - define: { - BUILD_VERSION: JSON.stringify(version), - BUILD_COMMIT: JSON.stringify(commit), - BUILD_TARGET_OS: JSON.stringify(targetOs), - BUILD_TARGET_ARCH: JSON.stringify(targetArch), - }, - target: target ? target : undefined, -}); - -console.log(` Built ${outfile}`); - -// ─── Ad-hoc codesign (macOS only) ──────────────────────────────────────────── - -if (targetOs === "darwin" && process.platform === "darwin") { - const sign = Bun.spawn( - [ - "codesign", - "--deep", - "--force", - "--sign", - "-", - "--entitlements", - `${import.meta.dir}/entitlements.plist`, - outfile, - ], - { stdout: "inherit", stderr: "inherit" }, - ); - const signCode = await sign.exited; - if (signCode !== 0) { - console.error(`codesign failed (exit ${signCode})`); - process.exit(signCode); +// ─── CLI args ───────────────────────────────────────────────────────────────── + +// Fix: guard with import.meta.main so this block does not run when build.ts is +// imported by unit tests — see issue #108. +if (import.meta.main) { + const target = parseTargetArg(process.argv); + + // ─── Derive OS / arch from target ──────────────────────────────────────── + + const { os: targetOs, arch: targetArch } = parseTarget(target); + + // ─── Output path ───────────────────────────────────────────────────────── + + const outfile = getOutfile(targetOs, target); + + // ─── Git commit hash ───────────────────────────────────────────────────── + + let commit = "dev"; + try { + const proc = Bun.spawn(["git", "rev-parse", "--short", "HEAD"], { + stdout: "pipe", + stderr: "pipe", + }); + await proc.exited; + commit = (await new Response(proc.stdout).text()).trim() || "dev"; + } catch { + // Not a git repo or git not available } - console.log(` Codesigned ${outfile}`); - const verify = Bun.spawn(["codesign", "--verify", "--verbose", outfile], { - stdout: "inherit", - stderr: "inherit", + // ─── Build ─────────────────────────────────────────────────────────────── + + const label = buildLabel(version, commit, targetOs, targetArch); + console.log(`Building github-code-search v${label}… outfile=${outfile}`); + if (target) console.log(` Target: ${target}`); + + // Absolute path to the Windows icon — must be absolute so Bun resolves it + // correctly when the script is invoked from any working directory. + const icoPath = `${import.meta.dir}/docs/public/icons/favicon.ico`; + + const currentYear = new Date().getFullYear(); + const authorName = typeof author === "string" ? author : author.name; + + await Bun.$`mkdir -p dist`; + await Bun.build({ + entrypoints: ["./github-code-search.ts"], + minify: true, + // Fix: bytecode: true causes the binary to fail on Windows — removed. + compile: getBuildCompileOptions(targetOs, outfile, { + iconPath: icoPath, + title: "github-code-search", + publisher: authorName, + appVersion: version, + description, + copyright: buildCopyrightLine(currentYear, authorName, license), + }), + define: { + BUILD_VERSION: JSON.stringify(version), + BUILD_COMMIT: JSON.stringify(commit), + BUILD_TARGET_OS: JSON.stringify(targetOs), + BUILD_TARGET_ARCH: JSON.stringify(targetArch), + }, + target: target ? (target as Parameters[0]["target"]) : undefined, }); - const verifyCode = await verify.exited; - if (verifyCode !== 0) { - console.error(`codesign verification failed (exit ${verifyCode})`); - process.exit(verifyCode); + + console.log(` Built ${outfile}`); + + // ─── Ad-hoc codesign (macOS only) ──────────────────────────────────────── + + if (targetOs === "darwin" && process.platform === "darwin") { + const sign = Bun.spawn( + [ + "codesign", + "--deep", + "--force", + "--sign", + "-", + "--entitlements", + `${import.meta.dir}/entitlements.plist`, + outfile, + ], + { stdout: "inherit", stderr: "inherit" }, + ); + const signCode = await sign.exited; + if (signCode !== 0) { + console.error(`codesign failed (exit ${signCode})`); + process.exit(signCode); + } + console.log(` Codesigned ${outfile}`); + + const verify = Bun.spawn(["codesign", "--verify", "--verbose", outfile], { + stdout: "inherit", + stderr: "inherit", + }); + const verifyCode = await verify.exited; + if (verifyCode !== 0) { + console.error(`codesign verification failed (exit ${verifyCode})`); + process.exit(verifyCode); + } } } diff --git a/bun.lock b/bun.lock index d27446f..758c50c 100644 --- a/bun.lock +++ b/bun.lock @@ -17,6 +17,7 @@ "oxfmt": "^0.36.0", "oxlint": "^1.51.0", "pa11y-ci": "^4.1.0", + "sharp": "^0.34.5", "vitepress": "^1.6.3", "vitepress-mermaid-renderer": "^1.1.18", }, @@ -157,6 +158,56 @@ "@iconify/utils": ["@iconify/utils@3.1.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="], + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], "@lhci/cli": ["@lhci/cli@0.15.1", "", { "dependencies": { "@lhci/utils": "0.15.1", "chrome-launcher": "^0.13.4", "compression": "^1.7.4", "debug": "^4.3.1", "express": "^4.17.1", "inquirer": "^6.3.1", "isomorphic-fetch": "^3.0.0", "lighthouse": "12.6.1", "lighthouse-logger": "1.2.0", "open": "^7.1.0", "proxy-agent": "^6.4.0", "tmp": "^0.1.0", "uuid": "^8.3.1", "yargs": "^15.4.1", "yargs-parser": "^13.1.2" }, "bin": { "lhci": "./src/cli.js" } }, "sha512-yhC0oXnXqGHYy1xl4D8YqaydMZ/khFAnXGY/o2m/J3PqPa/D0nj3V6TLoH02oVMFeEF2AQim7UbmdXMiXx2tOw=="], @@ -767,6 +818,8 @@ "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], "devtools-protocol": ["devtools-protocol@0.0.1467305", "", {}, "sha512-LxwMLqBoPPGpMdRL4NkLFRNy3QLp6Uqa7GNp1v6JaBheop2QrB9Q7q0A/q/CYYP9sBfZdHOyszVx4gc9zyk7ow=="], @@ -1257,7 +1310,7 @@ "search-insights": ["search-insights@2.17.3", "", {}, "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ=="], - "semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], @@ -1267,6 +1320,8 @@ "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + "shiki": ["shiki@2.5.0", "", { "dependencies": { "@shikijs/core": "2.5.0", "@shikijs/engine-javascript": "2.5.0", "@shikijs/engine-oniguruma": "2.5.0", "@shikijs/langs": "2.5.0", "@shikijs/themes": "2.5.0", "@shikijs/types": "2.5.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ=="], "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], @@ -1477,8 +1532,6 @@ "@paulirish/trace_engine/third-party-web": ["third-party-web@0.29.0", "", {}, "sha512-nBDSJw5B7Sl1YfsATG2XkW5qgUPODbJhXw++BKygi9w6O/NKS98/uY/nR/DxDq2axEjL6halHW1v+jhm/j1DBQ=="], - "@puppeteer/browsers/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "@puppeteer/browsers/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], "@tybys/wasm-util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -1531,6 +1584,8 @@ "lighthouse/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], + "lighthouse/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + "lighthouse/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], "lighthouse/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], @@ -1545,8 +1600,6 @@ "mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "pa11y/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "puppeteer/devtools-protocol": ["devtools-protocol@0.0.1581282", "", {}, "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ=="], diff --git a/bunfig.toml b/bunfig.toml index 0b1d4a9..0635736 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -4,4 +4,7 @@ coverageReporter = ["text", "lcov"] coverageThreshold = { lines = 0.75, functions = 0.8, statements = 0.8 } coverageSkipTestFiles = true preload = ["./src/test-setup.ts"] -coveragePathIgnorePatterns = ["src/test-setup.ts"] +# build.ts is an executable entry script (like tui.ts): its side-effectful main +# block (Bun.build, git, codesign) cannot be unit-tested. All pure helpers are +# exported and tested separately. +coveragePathIgnorePatterns = ["src/test-setup.ts", "build.ts"] diff --git a/docs/.vitepress/theme/InstallSection.vue b/docs/.vitepress/theme/InstallSection.vue index 118a988..050b37f 100644 --- a/docs/.vitepress/theme/InstallSection.vue +++ b/docs/.vitepress/theme/InstallSection.vue @@ -1,15 +1,27 @@