From 67f4d8d185ab2a93904c25f03498676b377757dc Mon Sep 17 00:00:00 2001 From: Sven Palberg Date: Mon, 2 Dec 2024 07:10:46 +0100 Subject: [PATCH] add cli --- cli.ts | 87 ++++++++++++++++++++++++++ days/mod.ts | 18 ++++++ deno.json | 12 +++- deno.lock | 117 ++++++++++++++++++++++++++++++++++- utils/getLines.ts | 10 +++ utils/loadInput.ts | 5 +- utils/mod.ts | 5 ++ utils/readLinesFromStream.ts | 19 ++++++ 8 files changed, 268 insertions(+), 5 deletions(-) create mode 100644 cli.ts create mode 100644 days/mod.ts create mode 100644 utils/getLines.ts create mode 100644 utils/readLinesFromStream.ts diff --git a/cli.ts b/cli.ts new file mode 100644 index 0000000..bd73d39 --- /dev/null +++ b/cli.ts @@ -0,0 +1,87 @@ +import { colors } from "@cliffy/ansi/colors"; +import { Command } from "@cliffy/command"; +import { Input, Select } from "@cliffy/prompt"; +import { readLinesFromFile } from "utils"; +import { days, getSolution } from "./days/mod.ts"; +import denoJson from "./deno.json" with { type: "json" }; +import { + readLinesFromStdin, + readLinesFromUrl, +} from "./utils/readLinesFromStream.ts"; + +const list = new Command() + .description("List all available solutions") + .action(() => { + console.log("Available solutions:"); + for (const day of Object.keys(days)) { + console.log(`- ${day}`); + } + }); + +const main = new Command() + .name("Advent of Code - 2024") + .version(denoJson.version) + .description("...") + .option("-d, --day ", "Day to run") + .option("-i, --input ", "Input file, local path or remote URL") + .action(async (options) => { + let input: Array | null = null; + let stdinClosed = false; + if (options.input != null) { + input = await loadInput(options.input); + } else { + input = await readLinesFromStdin(); + stdinClosed = input != null; + input = input ?? await provideInput().then(loadInput); + } + if (input === null) { + console.error("No input provided"); + Deno.exit(1); + } + if (stdinClosed && options.day == null) { + console.error("Day not provided"); + Deno.exit(1); + } + const day = options.day ?? await selectDay(); + const solution = getSolution(day); + if (solution == null) { + console.error(`Day ${day} not found`); + Deno.exit(1); + } + console.log(colors.bold.yellow(`Day ${day}`)); + console.log(`Part 1: ${solution.part1(input)}`); + console.log(`Part 2: ${solution.part2(input)}`); + }); + +async function provideInput(): Promise { + return await Input.prompt({ + message: "Provide input (path or URL)", + files: true, + }); +} + +async function selectDay(): Promise { + return await Select.prompt({ + message: "Select day", + options: Object.keys(days).map((day) => ({ + name: day.toString().padStart(2, "0"), + value: day, + })), + }); +} + +async function loadInput(input: string) { + return isWebUrl(input) + ? await readLinesFromUrl(input) + : await readLinesFromFile(input); +} + +function isWebUrl(path: string): boolean { + return path.startsWith("http://") || path.startsWith("https://"); +} + +if (import.meta.main) { + await main + .command("list", list) + .parse(Deno.args); +} diff --git a/days/mod.ts b/days/mod.ts new file mode 100644 index 0000000..3dcc1ea --- /dev/null +++ b/days/mod.ts @@ -0,0 +1,18 @@ +type PartFunction = (input: Array) => number; +type Solution = { part1: PartFunction; part2: PartFunction }; + +const days: Record = {}; + +for (const day of [1]) { + days[pad(day)] = await import(`./${pad(day)}/main.ts`); +} + +export function getSolution(day: string | number): Solution | null { + return days[pad(day)]; +} + +export { days }; + +function pad(day: string | number) { + return day.toString().padStart(2, "0"); +} diff --git a/deno.json b/deno.json index 897e463..dab47a3 100644 --- a/deno.json +++ b/deno.json @@ -1,9 +1,19 @@ { - "tasks": {}, + "name": "@aoc/2024", + "version": "0.0.1-rc.1", + "license": "MIT", + "exports": "./cli.ts", + "tasks": { + "cli": "deno run --allow-read ./cli.ts" + }, "imports": { + "@cliffy/ansi": "jsr:@cliffy/ansi@1.0.0-rc.5", + "@cliffy/command": "jsr:@cliffy/command@1.0.0-rc.5", + "@cliffy/prompt": "jsr:@cliffy/prompt@1.0.0-rc.5", "@std/expect": "jsr:@std/expect@1", "@std/collections": "jsr:@std/collections@1", "@std/path": "jsr:@std/path@1", + "@std/streams": "jsr:@std/streams@^1.0.8", "utils": "./utils/mod.ts" } } diff --git a/deno.lock b/deno.lock index 69ff358..4bb0668 100644 --- a/deno.lock +++ b/deno.lock @@ -1,23 +1,103 @@ { "version": "4", "specifiers": { + "jsr:@cliffy/ansi@1.0.0-rc.5": "1.0.0-rc.5", + "jsr:@cliffy/command@1.0.0-rc.5": "1.0.0-rc.5", + "jsr:@cliffy/flags@1.0.0-rc.5": "1.0.0-rc.5", + "jsr:@cliffy/internal@1.0.0-rc.5": "1.0.0-rc.5", + "jsr:@cliffy/keycode@1.0.0-rc.5": "1.0.0-rc.5", + "jsr:@cliffy/prompt@1.0.0-rc.5": "1.0.0-rc.5", + "jsr:@cliffy/table@1.0.0-rc.5": "1.0.0-rc.5", "jsr:@std/assert@1": "1.0.8", + "jsr:@std/assert@1.0.0-rc.2": "1.0.0-rc.2", "jsr:@std/assert@^1.0.8": "1.0.8", + "jsr:@std/bytes@^1.0.3": "1.0.4", + "jsr:@std/cli@1.0.0-rc.2": "1.0.0-rc.2", "jsr:@std/collections@1": "1.0.9", + "jsr:@std/encoding@1.0.0-rc.2": "1.0.0-rc.2", "jsr:@std/expect@1": "1.0.8", + "jsr:@std/fmt@~0.225.4": "0.225.6", "jsr:@std/internal@^1.0.5": "1.0.5", - "jsr:@std/path@1": "1.0.8" + "jsr:@std/io@~0.224.2": "0.224.9", + "jsr:@std/path@1": "1.0.8", + "jsr:@std/path@1.0.0-rc.2": "1.0.0-rc.2", + "jsr:@std/streams@^1.0.8": "1.0.8", + "jsr:@std/text@1.0.0-rc.1": "1.0.0-rc.1", + "npm:@types/node@*": "22.5.4" }, "jsr": { + "@cliffy/ansi@1.0.0-rc.5": { + "integrity": "85a4dba4da5d8278dcdfeea98672cd15706c244833f82edc60c61f410d9fc1a9", + "dependencies": [ + "jsr:@cliffy/internal", + "jsr:@std/encoding", + "jsr:@std/fmt", + "jsr:@std/io" + ] + }, + "@cliffy/command@1.0.0-rc.5": { + "integrity": "55e00a1d0ae38152fb275a89494a81ffb9b144eb9060107c0be5af46e1ba736c", + "dependencies": [ + "jsr:@cliffy/flags", + "jsr:@cliffy/internal", + "jsr:@cliffy/table", + "jsr:@std/fmt", + "jsr:@std/text" + ] + }, + "@cliffy/flags@1.0.0-rc.5": { + "integrity": "bd33b7b399e0af353f5516d87a2d552d46ee7e7f4a6f0c0bc65fcce750710217", + "dependencies": [ + "jsr:@std/text" + ] + }, + "@cliffy/internal@1.0.0-rc.5": { + "integrity": "1e8dca4fcfba1815bf1a899bb880e09f8b45284c352465ef8fb015887c1fc126" + }, + "@cliffy/keycode@1.0.0-rc.5": { + "integrity": "2bcb3cb13873f0b758664394e003fc0cfa751af37a076ca9ec6e574df77aa3a8" + }, + "@cliffy/prompt@1.0.0-rc.5": { + "integrity": "3573a4c5c460fc84dcc554e548acfc2616157b60a61a9781833967c5a76da9f0", + "dependencies": [ + "jsr:@cliffy/ansi", + "jsr:@cliffy/internal", + "jsr:@cliffy/keycode", + "jsr:@std/assert@1.0.0-rc.2", + "jsr:@std/fmt", + "jsr:@std/io", + "jsr:@std/path@1.0.0-rc.2", + "jsr:@std/text" + ] + }, + "@cliffy/table@1.0.0-rc.5": { + "integrity": "2b3e1b4764bbb56b0c39aeba95bc0bb551d9bd4475fbb6d1ce368c08b7ef9eb3", + "dependencies": [ + "jsr:@std/cli", + "jsr:@std/fmt" + ] + }, + "@std/assert@1.0.0-rc.2": { + "integrity": "0484eab1d76b55fca1c3beaff485a274e67dd3b9f065edcbe70030dfc0b964d3" + }, "@std/assert@1.0.8": { "integrity": "ebe0bd7eb488ee39686f77003992f389a06c3da1bbd8022184804852b2fa641b", "dependencies": [ "jsr:@std/internal" ] }, + "@std/bytes@1.0.4": { + "integrity": "11a0debe522707c95c7b7ef89b478c13fb1583a7cfb9a85674cd2cc2e3a28abc" + }, + "@std/cli@1.0.0-rc.2": { + "integrity": "97dfae82b9f0e189768ebfa7a5da53375955b94bad0a1804f8e3b73563b03787" + }, "@std/collections@1.0.9": { "integrity": "4f58104ead08a04a2199374247f07befe50ba01d9cca8cbb23ab9a0419921e71" }, + "@std/encoding@1.0.0-rc.2": { + "integrity": "160d7674a20ebfbccdf610b3801fee91cf6e42d1c106dd46bbaf46e395cd35ef" + }, "@std/expect@1.0.8": { "integrity": "27e40d8f3aefb372fc6a703fb0b69e34560e72a2f78705178babdffa00119a5f", "dependencies": [ @@ -25,18 +105,51 @@ "jsr:@std/internal" ] }, + "@std/fmt@0.225.6": { + "integrity": "aba6aea27f66813cecfd9484e074a9e9845782ab0685c030e453a8a70b37afc8" + }, "@std/internal@1.0.5": { "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba" }, + "@std/io@0.224.9": { + "integrity": "4414664b6926f665102e73c969cfda06d2c4c59bd5d0c603fd4f1b1c840d6ee3" + }, + "@std/path@1.0.0-rc.2": { + "integrity": "39f20d37a44d1867abac8d91c169359ea6e942237a45a99ee1e091b32b921c7d" + }, "@std/path@1.0.8": { "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be" + }, + "@std/streams@1.0.8": { + "integrity": "b41332d93d2cf6a82fe4ac2153b930adf1a859392931e2a19d9fabfb6f154fb3", + "dependencies": [ + "jsr:@std/bytes" + ] + }, + "@std/text@1.0.0-rc.1": { + "integrity": "34c722203e87ee12792c8d4a0cd2ee0e001341cbce75b860fc21be19d62232b0" + } + }, + "npm": { + "@types/node@22.5.4": { + "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", + "dependencies": [ + "undici-types" + ] + }, + "undici-types@6.19.8": { + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" } }, "workspace": { "dependencies": [ + "jsr:@cliffy/ansi@1.0.0-rc.5", + "jsr:@cliffy/command@1.0.0-rc.5", + "jsr:@cliffy/prompt@1.0.0-rc.5", "jsr:@std/collections@1", "jsr:@std/expect@1", - "jsr:@std/path@1" + "jsr:@std/path@1", + "jsr:@std/streams@^1.0.8" ] } } diff --git a/utils/getLines.ts b/utils/getLines.ts new file mode 100644 index 0000000..2bc4870 --- /dev/null +++ b/utils/getLines.ts @@ -0,0 +1,10 @@ +import { TextLineStream } from "@std/streams"; + +export async function getLines( + input: ReadableStream, +): Promise> { + const stream = input + .pipeThrough(new TextDecoderStream()) + .pipeThrough(new TextLineStream({ allowCR: true })); + return await Array.fromAsync(stream); +} diff --git a/utils/loadInput.ts b/utils/loadInput.ts index 2ce8a45..42e46dd 100644 --- a/utils/loadInput.ts +++ b/utils/loadInput.ts @@ -1,11 +1,12 @@ import { dirname, fromFileUrl, resolve } from "@std/path"; +import { readLinesFromFile } from "./readLinesFromStream.ts"; -export function loadInput(day: number): Array { +export function loadInput(day: number): Promise> { const path = resolve( dirname(fromFileUrl(import.meta.url)), "..", "inputs", `${day.toString().padStart(2, "0")}.txt`, ); - return Deno.readTextFileSync(path).split("\n"); + return readLinesFromFile(path); } diff --git a/utils/mod.ts b/utils/mod.ts index 753b94f..09f8f5c 100644 --- a/utils/mod.ts +++ b/utils/mod.ts @@ -1 +1,6 @@ export { loadInput } from "./loadInput.ts"; +export { + readLinesFromFile, + readLinesFromStdin, + readLinesFromUrl, +} from "./readLinesFromStream.ts"; diff --git a/utils/readLinesFromStream.ts b/utils/readLinesFromStream.ts new file mode 100644 index 0000000..fc3abd5 --- /dev/null +++ b/utils/readLinesFromStream.ts @@ -0,0 +1,19 @@ +import { getLines } from "./getLines.ts"; + +export async function readLinesFromFile(path: string): Promise> { + using file = await Deno.open(path); + return await getLines(file.readable); +} + +export async function readLinesFromStdin(): Promise | null> { + if (Deno.stdin.isTerminal()) return null; + return await getLines(Deno.stdin.readable); +} + +export async function readLinesFromUrl(url: string): Promise> { + const response = await fetch(url); + if (!response.ok || response.body === null) { + throw new Error(`Failed to fetch ${url}`); + } + return await getLines(response.body); +}