@yankeeinlondon/promptly
Version:
An automation tool for prompting your favorite LLMs
205 lines (187 loc) • 6.58 kB
text/typescript
import type { Switches } from ".";
import type { Prompt } from "./types";
import { writeFileSync } from "node:fs";
import { relative } from "node:path";
import { cwd, exit } from "node:process";
import { ask, ask as q } from "@yankeeinlondon/ask";
import chalk from "chalk";
import FastGlob from "fast-glob";
import { resolve } from "pathe";
import { PROMPTS_GLOB } from "./constants";
import {
confirm,
fail,
info,
infoIndent,
log,
prettyFile,
processPrompt,
success,
toClipboard,
updatePromptFile,
} from "./utils";
/**
* replaces all requests for `file` or `web` resources
*/
async function processPrompts(
files: string[],
s: Switches,
): Promise<Prompt[]> {
const wait = files.map(i => processPrompt(i, s));
const results = await Promise.all(wait) as Prompt[];
return results;
}
/**
* Combine one or more **prompt** files and replace all `file` and `web` references
* with the appropriate content.
*
* Output is always placed on the clipboard but if `-o <filename>` is provided then
* it will also write to a file.
*/
export async function createPrompt(args: string[], s: Switches) {
const promptFiles: string[] = [];
const _promptContent = [];
log();
const candidates = await FastGlob(PROMPTS_GLOB);
if (candidates.length === 0) {
fail(`no prompts found; run ${chalk.blue.bold("prompt --env")} to better understand the current configuration of the CLI`);
exit(1);
}
else {
info(`${chalk.bold.yellow(candidates.length)} candidate ${chalk.bold("prompt")} files found in the path`);
}
let freeFormQuestion = "";
let requiresConfirmation = false;
for (const [idx, arg] of args.entries()) {
/** matched prompt files */
const found = candidates.filter(c => c.includes(arg));
if (found.length > 0) {
const exactMatches = found.filter(i => arg === i);
if (exactMatches.length === 1) {
success(`Matched the prompt file "${arg}" -> ${resolve(cwd(), exactMatches[0])}`);
}
else if (exactMatches.length > 1) {
confirm(`We found more than one prompt file which matches "${arg}"\n`);
promptFiles.push(
await q.select("select", "Choose which file to use", exactMatches)(),
);
}
else if (found.length === 1) {
success(`Matched the prompt file "${arg}" -> ${resolve(cwd(), found[0])}`);
promptFiles.push(found[0]);
}
else if (found.length > 1) {
requiresConfirmation = true;
confirm(`We found more than one prompt file which matches "${arg}"\n`);
promptFiles.push(
await q.select("select", "Choose which file to use", exactMatches)(),
);
}
}
else {
// no matches found with available prompts
const remainingHaveExt = args.slice(idx).some(
i => i.endsWith(".md") || i.endsWith(".txt"),
);
if (remainingHaveExt) {
requiresConfirmation = true;
fail(`the text "${chalk.bold(arg)}" ${chalk.dim.italic(`-- as well as the variant "${chalk.bold(`${arg}.md`)}" --`)} found no match for prompt files`);
console.log();
const action = q.select(
"action",
`What action should we take?`,
{
"skip this text and continue with the rest": "skip",
"quit for now to restate the CLI command": "quit",
},
);
const answer = await action();
if (answer === "quit") {
exit(1);
}
}
else {
if (
arg.length > 15 && arg.includes(" ")
) {
requiresConfirmation = true;
freeFormQuestion = args.slice(idx).join(" ");
success("set freeform text to be added to end of prompt");
break;
}
else {
requiresConfirmation = true;
fail(`the parameter ${chalk.bold.red(arg)} was not matched to a template and will be dropped.`);
}
}
}
} // end prompt files
if (requiresConfirmation) {
log();
const promptMsg = promptFiles.length > 0
? `The prompt files, ${chalk.italic("in order")}, are:\n${promptFiles.map(i => ` - ${prettyFile(i)}`).join("\n")}`
: `${chalk.bold.italic("No")} prompt files where found in the params!`;
const freeform = freeFormQuestion === ""
? ""
: promptFiles.length > 0
? `The following ${chalk.bold("freeform question")} will be appended to the end of the prompt chain:\n\n ${chalk.italic(freeFormQuestion)}\n`
: `The following ${chalk.bold("freeform question")} will be the full extent of the prompt as no prompt\n references were found:\n\n ${chalk.italic(freeFormQuestion)}\n`;
info(promptMsg);
if (freeform) {
info(freeform);
}
if (
await q.confirm("confirm", `Continue?`)()
) {
// no-op
}
else {
exit(1);
}
}
let prompts: Prompt[] = [];
if (promptFiles.length > 0) {
info(
`processing prompts [${chalk.bold.yellow(promptFiles.length)}] for inline references`,
);
/**
* the `code`, `doc` and `web` resource requests embedded in the
* prompt file(s)
*/
prompts = await processPrompts(promptFiles, s);
}
const prompt = `${prompts.map(i => i.content).join("\n")}\n${freeFormQuestion}\n`;
const copied = await toClipboard(prompts[0].content);
if (copied) {
log();
success(`the prompt has been copied to the clipboard!`);
}
else {
fail(`failed to copy prompt to the clipboard: ${(copied as Error).message}`);
}
if (s.output) {
writeFileSync(prompt, s.output, "utf-8");
success(`the prompt was saved to ${chalk.blue.bold(s.output)}`);
}
if (s.replace) {
for (const p of prompts) {
if (p.isCached && !s.refresh) {
info(`the "${prettyFile(relative(cwd(), p.promptFile))}" prompt file was ${chalk.italic("already")} in a cached state.`);
infoIndent(`this file will be left unchanged as it was ${chalk.italic("not")} re-evaluated for this prompt`);
infoIndent(`if you want to have the files and web references ${chalk.italic("re-evaluated")} add the ${chalk.blue(`--refresh`)} flag to your command`);
}
else {
if (
s.yes
|| ask.confirm(
`confirm`,
`Save the "${prettyFile(relative(cwd(), p.promptFile))}" with the ${chalk.italic("interpolated")} results?`,
)
) {
await updatePromptFile(p);
success(`prompt file ${chalk.bold.blue(relative(cwd(), p.promptFile))} saved backed to source`);
}
}
}
}
}