initial cli api, some movement on tool selection

This commit is contained in:
2025-04-24 20:27:09 -06:00
parent 08bba857db
commit 7d42920dcb
18 changed files with 938 additions and 180 deletions

30
util/asciiArt.ts Normal file
View File

@@ -0,0 +1,30 @@
export async function getAsciiArt(art: string) {
const artFilePath = Deno.env.get("BEARMETAL_ASCII_PATH") ||
getBearmetalAsciiPath();
if (!artFilePath) return art;
let artFileText: string;
if (artFilePath.startsWith("http")) {
artFileText = await fetch(artFilePath).then((res) => res.text());
} else {
artFileText = await Deno.readTextFile(artFilePath);
}
const parserRX = /begin\s+(\w+)\s*\n([\s\S]*?)\s*end\s*/g;
let result = parserRX.exec(artFileText);
while (result !== null) {
const [_, name, artText] = result;
if (name === art) return artText;
result = parserRX.exec(artFileText);
}
return art;
}
function getBearmetalAsciiPath() {
const filenameRX = /asciiarts?\.txt$/;
for (const filename of Deno.readDirSync(".")) {
if (filename.isFile && filenameRX.test(filename.name)) {
return filename.name;
}
}
return null;
}

55
util/call.ts Normal file
View File

@@ -0,0 +1,55 @@
type transformer = (arg: string) => any;
interface IConfig {
multiTransform?: boolean;
args?: string[];
}
export async function call<T extends unknown[]>(
tool: ToolFunc<T>,
transforms: transformer[],
conf?: IConfig,
) {
const config: IConfig = {};
if (typeof conf === "object") {
Object.assign(config, conf);
}
const args = config.args || Deno.args;
const shouldPair = transforms.length === args.length;
const multiTransform = config.multiTransform ||
!shouldPair && transforms.length > 1;
const transformedArgs = args.map((arg, i) => {
if (shouldPair) return transforms[i](arg);
if (multiTransform) return transforms.reduce((a, b) => b(a), arg);
return transforms[0] ? transforms[0](arg) : arg;
});
await tool(...transformedArgs as T);
}
type prompt = [string, (v?: string) => boolean] | string;
export async function callWithArgPrompt<T extends unknown[]>(
tool: ToolFunc<T>,
prompts: prompt[],
args?: string[],
) {
function buildPromptTransform(p: prompt): transformer {
let validation = (v?: string) => !!v;
let pText = p as string;
if (Array.isArray(p)) {
[pText, validation] = p;
}
return (a: string) => {
while (!validation(a)) {
a = prompt(pText) || "";
}
};
}
await call(tool, prompts.map(buildPromptTransform), { args });
}

128
util/caseManagement.ts Normal file
View File

@@ -0,0 +1,128 @@
function lowerToPascalCase(str: string) {
return str.replace(/(?:^|\s)\w/g, (match) => match.toUpperCase()).replaceAll(
" ",
"",
);
}
function lowerToTrainCase(str: string) {
return str.replace(/(?:^|\s)\w/g, (match) => match.toUpperCase()).replaceAll(
" ",
"-",
);
}
function lowerToCamelCase(str: string) {
return str.trim().replace(/(?:\s)\w/g, (match) => match.toUpperCase())
.replaceAll(" ", "");
}
function lowerToSnakeCase(str: string) {
return str.replace(" ", "_");
}
function lowerToKebabCase(str: string) {
return str.replace(" ", "-");
}
function lowerToMacroCase(str: string) {
return str.replace(/\w\S*/g, (match) => match.toUpperCase()).replaceAll(
" ",
"_",
);
}
function lowerToTitleCase(str: string) {
return str.replace(/(?:^|\s)\w/g, (match) => match.toUpperCase());
}
type CaseType =
| "pascal"
| "camel"
| "snake"
| "kebab"
| "macro"
| "upper"
| "lower"
| "train"
| "title"
| "";
function parseCase(str: string) {
const isCaseMap = new Map<CaseType, (str: string) => boolean>([
["pascal", (str: string) => {
return /^[A-Z][a-zA-Z]*$/.test(str);
}],
["camel", (str: string) => {
return /^[a-z][a-zA-Z]*$/.test(str);
}],
["snake", (str: string) => {
return /^[a-z][a-z0-9_]*$/.test(str);
}],
["kebab", (str: string) => {
return /^[a-z][a-z0-9-]*$/.test(str);
}],
["macro", (str: string) => {
return /^[A-Z]*$/.test(str);
}],
["upper", (str: string) => {
return /^[A-Z]*$/.test(str);
}],
["lower", (str: string) => {
return /^[a-z]*$/.test(str);
}],
["train", (str: string) => {
return /([A-Z][a-z]*(?:-|$))+/.test(str);
}],
]);
for (const [key, value] of isCaseMap) {
if (value(str)) return key;
}
return "";
}
function coerceCaseToLower(str: string, caseType: CaseType) {
switch (caseType) {
case "pascal":
case "camel":
return str.replace(/[A-Z]/g, (match) => " " + match.toLowerCase().trim());
case "macro":
case "snake":
case "upper":
return str.replace("_", " ").toLowerCase();
case "train":
case "kebab":
return str.replace("-", " ").toLowerCase();
default:
return str.toLowerCase();
}
}
export function toCase(str: string, toCase: CaseType) {
const caseType = parseCase(str) || "";
console.log(caseType);
if (caseType === toCase) return str;
const lowerStr = coerceCaseToLower(str, caseType);
console.log(lowerStr);
switch (toCase) {
case "pascal":
return lowerToPascalCase(lowerStr);
case "camel":
return lowerToCamelCase(lowerStr);
case "snake":
return lowerToSnakeCase(lowerStr);
case "kebab":
return lowerToKebabCase(lowerStr);
case "macro":
return lowerToMacroCase(lowerStr);
case "upper":
return lowerStr.toUpperCase();
case "lower":
return lowerStr.toLowerCase();
case "train":
return lowerToTrainCase(lowerStr);
case "title":
return lowerToTitleCase(lowerStr);
default:
return str;
}
}

19
util/saveLoadPdf.ts Normal file
View File

@@ -0,0 +1,19 @@
import { PDFDocument } from "pdf-lib";
export async function loadPdfForm(path: string) {
const pdfDoc = await loadPdf(path);
const form = pdfDoc.getForm();
return form;
}
export async function loadPdf(path: string) {
const pdfBytes = await Deno.readFile(path);
const pdfDoc = await PDFDocument.load(pdfBytes);
return pdfDoc;
}
export async function savePdf(doc: PDFDocument, path: string) {
const pdfBytes = await doc.save();
if (Deno.env.get("DRYRUN") || path.includes("dryrun")) return;
await Deno.writeFile(path, pdfBytes);
}