Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .runseal/deno.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
{
"imports": {
"@/lib/": "./lib/",
"@deno.land/": "https://deno.land/",
"@std/cli/parse-args": "jsr:@std/cli@1.0.30/parse-args"
},
"compilerOptions": {
"strict": true
},
Expand Down
7 changes: 7 additions & 0 deletions .runseal/hooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,11 @@ set -eu
root=$(git rev-parse --show-toplevel)
cd "$root"

branch=$(git symbolic-ref --quiet --short HEAD || true)
if [ "$branch" = "main" ]; then
printf '%s\n' "runseal pre-commit: refusing to commit directly on main" >&2
printf '%s\n' "create a feature branch first, then commit again" >&2
exit 1
fi

runseal :guard
68 changes: 68 additions & 0 deletions .runseal/lib/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { parseArgs as parseStdArgs } from "@std/cli/parse-args";
import type { Args, ParseOptions } from "@std/cli/parse-args";

import { io } from "@/lib/std/io.ts";

type CliParseOptions = Omit<ParseOptions, "unknown" | "--"> & {
unknownOptionMessage?: (arg: string) => string;
};

export function parseArgs(args: string[], options: CliParseOptions = {}): Args {
const { unknownOptionMessage, ...parseOptions } = options;
requireStringValues(args, Array.isArray(parseOptions.string) ? parseOptions.string : []);
return parseStdArgs(args, {
"--": true,
...parseOptions,
unknown: (arg) => {
if (arg.startsWith("-")) {
io.fail(unknownOptionMessage?.(arg) ?? `unknown option: ${arg}`);
}
return true;
},
});
}

function requireStringValues(args: string[], names: string[]): void {
const stringOptions = new Set(names);
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--") {
return;
}
if (!arg.startsWith("--")) {
continue;
}
const [name, value] = arg.slice(2).split("=", 2);
if (!stringOptions.has(name) || value !== undefined) {
continue;
}
const next = args[index + 1];
if (next === undefined || next.startsWith("-")) {
io.fail(`missing value for --${name}`);
}
}
}

export function helpRequested(args: Args): boolean {
return args.help === true || args.h === true || args._.includes("help");
}

export function requireNoPositionals(
args: Args,
context: string,
options: { allowHelp?: boolean } = {},
): void {
const extra = args._.find((arg) => !(options.allowHelp === true && arg === "help"));
if (extra !== undefined) {
io.fail(`${context}: unexpected argument: ${extra}`);
}
}

export function stringOption(args: Args, name: string, fallback = ""): string {
const value = args[name];
return typeof value === "string" ? value : fallback;
}

export function booleanOption(args: Args, name: string): boolean {
return args[name] === true;
}
77 changes: 77 additions & 0 deletions .runseal/lib/hash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
const encoder = new TextEncoder();

type TreeEntry = {
label: string;
file: string;
};

export async function treeHash(paths: string[]): Promise<string> {
if (paths.length === 0) {
throw new Error("treeHash requires at least one path");
}
const entries: TreeEntry[] = [];
for (const path of paths) {
await collectTree(path, path, entries);
}
entries.sort((left, right) => left.label.localeCompare(right.label));

const zero = new Uint8Array([0]);
const parts: Uint8Array[] = [];
for (const entry of entries) {
parts.push(
encoder.encode(normalizePath(entry.label)),
zero,
await Deno.readFile(entry.file),
zero,
);
}
const payload = concatBytes(parts);
const digestInput = new ArrayBuffer(payload.byteLength);
new Uint8Array(digestInput).set(payload);
const digest = await crypto.subtle.digest("SHA-256", digestInput);
return Array.from(new Uint8Array(digest))
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("");
}

async function collectTree(path: string, label: string, entries: TreeEntry[]): Promise<void> {
const stat = await Deno.stat(path);
if (stat.isFile) {
entries.push({ label, file: path });
return;
}
if (stat.isDirectory) {
for await (const entry of Deno.readDir(path)) {
await collectTree(pathJoin(path, entry.name), pathJoin(label, entry.name), entries);
}
return;
}
throw new Error(`unsupported path for treeHash: ${path}`);
}

function pathJoin(...parts: string[]): string {
const separator = Deno.build.os === "windows" ? "\\" : "/";
const joined = parts
.filter((part) => part !== "")
.map((part, index) =>
index === 0 ? part.replace(/[\\/]+$/g, "") : part.replace(/^[\\/]+|[\\/]+$/g, "")
)
.filter((part) => part !== "")
.join(separator);
return joined === "" ? "." : joined;
}

function normalizePath(path: string): string {
return path.replace(/\\/g, "/").replace(/\/+/g, "/");
}

function concatBytes(parts: Uint8Array[]): Uint8Array {
const total = parts.reduce((sum, part) => sum + part.length, 0);
const output = new Uint8Array(total);
let offset = 0;
for (const part of parts) {
output.set(part, offset);
offset += part.length;
}
return output;
}
201 changes: 0 additions & 201 deletions .runseal/lib/runseal.ts

This file was deleted.

Loading
Loading