aoc-automation
Version:
Advent of Code tool to automate the repetitive parts of AoC.
269 lines (267 loc) • 8.76 kB
JavaScript
import fs from "fs";
import path from "path";
import { stripIndent } from "common-tags";
import chokidar from "chokidar";
import kleur from "kleur";
import buildSource from "./processes/buildSource.js";
import runSolution from "./processes/runSolution.js";
import getLatestVersion from "./processes/getLatestVersion.js";
import copy from "../io/copy.js";
import save from "../io/save.js";
import { aocAutomationDaysJSON } from "../configs/runnerJSON.js";
import { readConfig, saveConfig } from "../io/config.js";
import { getInput, getPuzzleInfo, sendSolution, Status } from "../io/api.js";
import readmeYearMD from "../configs/readmeYearMD.js";
import readmeDayMD from "../configs/readmeDayMD.js";
import asciiPrompt, { AsciiOptions } from "../prompts/asciiPrompt.js";
import commandPrompt from "../prompts/commandPrompt.js";
import updateReadmes from "./updateReadMe.js";
import version from "../version.js";
let latestVersion = null;
getLatestVersion().then((v) => {
latestVersion = v;
});
const boldMagenta = kleur.bold().magenta;
const showFullInfo = () => {
const updateInfo = latestVersion === null ? "" : latestVersion !== version ? `(update available: v${latestVersion})` : "(latest)";
console.log();
console.log(stripIndent`
AoC Runner v${version} ${updateInfo}
Type ${boldMagenta("fetch")} or ${boldMagenta("f")} - to fetch the input
Type ${boldMagenta("send")} or ${boldMagenta("s")} - to send the solutions
Type ${boldMagenta("help")} or ${boldMagenta("h")} - to show all commands
Type ${boldMagenta("clear")} or ${boldMagenta("c")} - to clear the console
Type ${boldMagenta("quit")} or ${boldMagenta("q")} - to close the runner
`);
console.log();
};
const showInfo = () => {
console.log();
console.log(stripIndent`
Type: ${boldMagenta("f")} - fetch input, ${boldMagenta(
"s"
)} - send solutions, ${boldMagenta("h")} - help, ${boldMagenta("q")} - quit
`);
if (latestVersion !== null && latestVersion !== version) {
console.log();
console.log(kleur.green(`Update available (v${latestVersion})!`));
console.log(
`To update, close the runner and run`,
kleur.bold().green("npm i aoc-automation")
);
}
console.log();
};
const send = async (config, yearNum, dayNum, part) => {
const yearConfig = config.years.find((y) => y.year == yearNum);
console.log(`
Part ${part}:`);
const dayData = part === 1 ? yearConfig.days[dayNum - 1].part1 : yearConfig.days[dayNum - 1].part2;
if (dayData.solved) {
console.log(kleur.green(`Already solved!`));
return true;
}
if (dayData.attempts.includes(dayData.result)) {
console.log(kleur.yellow("You already tried this solution, skipping."));
return false;
}
if (dayData.result === null) {
console.log(kleur.yellow(`Solution is undefined, skipping.`));
return false;
}
if (/\n/.test(dayData.result)) {
console.log(
kleur.yellow(stripIndent`
Solution to part ${part} is a multiline string,
and should probably be interpreted before sending.
`)
);
console.log();
const { choice, replacement } = await asciiPrompt(part);
switch (choice) {
case AsciiOptions.INTERPRETED:
if (replacement !== void 0 && replacement !== "") {
dayData.result = replacement;
} else {
console.log(
kleur.yellow(`Solution is undefined, skipping.`)
);
return false;
}
break;
case AsciiOptions.AS_IS:
break;
case AsciiOptions.CANCEL: {
console.log(kleur.yellow(`Skipping.`));
return false;
}
}
}
const status = await sendSolution(
yearConfig.year,
dayNum,
part,
dayData.result
);
if (status === Status["SOLVED"]) {
yearConfig.days[dayNum - 1][part === 1 ? "part1" : "part2"].solved = true;
saveConfig(config);
updateReadmes(yearConfig.year.toString(), dayNum);
return true;
}
if (status === Status["WRONG"]) {
yearConfig.days[dayNum - 1][part === 1 ? "part1" : "part2"].attempts.push(dayData.result);
saveConfig(config);
}
return false;
};
const dev = async (yearRaw, dayRaw) => {
let year = yearRaw && (yearRaw.match(/\d{4}/) ?? [])[0];
let day = dayRaw && (dayRaw.match(/\d+/) ?? [])[0];
const today = new Date();
if (year === void 0) {
year = today.getFullYear().toString();
}
if (day === void 0) {
day = today.getDate().toString();
}
const config = readConfig();
const yearNum = Number(year);
const dayNum = Number(day);
if (yearNum < 2015 || yearNum > today.getFullYear()) {
console.log(
kleur.red(
`Wrong year - choose year between 2015 and ${new Date().getFullYear()}.`
)
);
process.exit(1);
}
let configYear = config.years.find((y) => y.year == yearNum);
if (configYear == void 0) {
const yearDir2 = path.join("src", year);
const daysConfig = aocAutomationDaysJSON({ year: yearNum });
if (dayNum < 1 || dayNum > daysConfig.length) {
console.log(kleur.red(`Wrong day - choose day between 1 and ${daysConfig.length}.`));
process.exit(1);
}
fs.mkdirSync(yearDir2, { recursive: true });
config.years.push(
configYear = { year: yearNum, days: daysConfig }
);
config.years.sort((a, b) => a.year - b.year);
saveConfig(config);
save(
yearDir2,
"README.md",
readmeYearMD(
config.language,
config.years.find((y) => y.year === Number(year))
)
);
}
console.log(`Starting ${year} Day ${day}...`);
const dayDir = `day${String(dayNum).padStart(2, "0")}`;
const fromDir = path.join("src", "template");
const yearDir = path.join("src", yearRaw);
const toDir = path.join(yearDir, dayDir);
const indexFile = path.join(
config.language === "ts" ? "dist" : "src",
yearRaw,
dayDir,
"index.js"
);
const inputPath = path.join(toDir, "input.txt");
const dayReadmePath = path.join(toDir, "README.md");
if (!fs.existsSync(fromDir)) {
console.log(kleur.red("Template folder does not exist!"));
}
const toExists = fs.existsSync(toDir);
const puzzleInfo = await getPuzzleInfo(yearNum, dayNum);
const [title, _, __, ___, ____] = puzzleInfo;
if (!toExists) {
console.log("Creating from template...");
copy(fromDir, toDir);
fs.writeFileSync(inputPath, "");
if (!fs.existsSync(dayReadmePath)) {
if (title != null) {
configYear.days[dayNum - 1].title = title;
saveConfig(config);
}
fs.writeFileSync(
dayReadmePath,
readmeDayMD(configYear.year, dayNum, title)
);
}
}
const dayIndexFile = path.join(toDir, config.language == "ts" ? "index.ts" : "index.js");
const utilsIndex = path.join("src", "utils", "index.ts");
const utilsIndexOut = path.join("dist", "utils", "index.js");
getInput(yearNum, dayNum, inputPath, dayIndexFile, puzzleInfo);
if (toExists) {
if (config.language === "ts") {
console.log(`Building ${dayIndexFile}...`);
buildSource(year, dayIndexFile);
}
runSolution(dayNum, indexFile);
}
const reload = (file) => {
if (![".js", ".ts", ".mjs"].includes(path.parse(file).ext)) {
return;
}
console.clear();
if (config.language === "ts") {
if (!fs.existsSync(utilsIndexOut)) {
console.log(`Building ${utilsIndex}...`);
buildSource(year, utilsIndex);
}
console.log(`Building ${file}...`);
buildSource(year, file);
}
runSolution(dayNum, indexFile);
showInfo();
process.stdout.write(kleur.cyan("?") + kleur.gray(" \u203A "));
};
chokidar.watch([path.join("src", "utils"), path.join("src", year)], {
ignoreInitial: true
}).on("add", reload).on("change", reload);
const listenToInput = async (year2) => {
showInfo();
const { command } = await commandPrompt();
const config2 = readConfig();
const yearNum2 = Number(year2);
switch (command.toLowerCase()) {
case "fetch":
case "f":
const puzzleInfo2 = await getPuzzleInfo(yearNum2, dayNum);
getInput(yearNum2, dayNum, inputPath, dayIndexFile, puzzleInfo2);
break;
case "send":
case "s":
const solved = await send(config2, yearNum2, dayNum, 1);
if (solved) {
await send(config2, yearNum2, dayNum, 2);
}
break;
case "help":
case "h":
showFullInfo();
break;
case "clear":
case "c":
console.clear();
break;
case "quit":
case "q":
process.exit();
default:
console.log("Command not supported");
break;
}
listenToInput(year2);
};
listenToInput(year);
};
var dev_default = dev;
export {
dev_default as default
};