Project Files
src / validation / generator.ts
import type { GenerateInput, GenerateOutput, TemplateStyle } from "./types.js";
const ALLOWED_AGENTS = ["build", "plan", "general", "explore", "oracle"];
export function generateCommand(input: GenerateInput): GenerateOutput {
const warnings: string[] = [];
const opts = input.options || {};
const text = input.text.trim();
const template: TemplateStyle = opts.template || "basic";
// Derive filename
const baseName = opts.name || extractName(text);
const filename =
baseName
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "") + ".md";
// Derive description
let description = opts.description || "";
if (!description) {
description = extractDescription(text);
}
// Detect features in text
const hasArgs = /\$ARGUMENTS\b/.test(text) || /\$\d\b/.test(text);
const hasFileRefs = /@[^\s`"')\]}>]+/.test(text);
const hasBashCmd = /!`[^`]+`/.test(text);
// Generate frontmatter
let frontmatter = "---\n";
frontmatter += `description: ${escapeYamlValue(description)}\n`;
const agent = opts.agent || undefined;
if (agent && ALLOWED_AGENTS.includes(agent)) {
frontmatter += `agent: ${agent}\n`;
}
if (opts.model) {
frontmatter += `model: ${opts.model}\n`;
}
if (opts.subtask !== undefined) {
frontmatter += `subtask: ${String(opts.subtask)}\n`;
}
frontmatter += "---\n";
// Generate body based on template style
let body: string;
switch (template) {
case "minimal":
body = generateMinimalBody(text, hasArgs);
break;
case "detailed":
body = generateDetailedBody(
text,
description,
hasArgs,
hasFileRefs,
hasBashCmd,
);
break;
case "basic":
default:
body = generateBasicBody(text, hasArgs);
break;
}
const fullContent = frontmatter + body;
// Post-generation checks
const lineCount = fullContent.split("\n").length;
if (lineCount > 150) {
warnings.push(
`Generated file exceeds 150 lines (${lineCount} lines). Consider splitting or using --template minimal.`,
);
}
if (!description || description.length < 5) {
warnings.push(
"Description is very short. Use --description to provide a better one.",
);
}
return { filename, content: fullContent, warnings };
}
// ---- Private helpers ----
function extractName(text: string): string {
const cleaned = text.replace(/^["']/, "").replace(/["']$/, "").trim();
const words = cleaned.split(/\s+/).slice(0, 4);
if (words.length === 0) return "command";
return words
.join("-")
.toLowerCase()
.replace(/[^a-z0-9-]/g, "");
}
function extractDescription(text: string): string {
const lines = text.split("\n").filter((l) => l.trim().length > 0);
if (lines.length === 0) return "";
let firstLine = lines[0].trim();
firstLine = firstLine.replace(/^#+\s*/, "");
const dotIndex = firstLine.indexOf(".");
let desc = dotIndex > 5 ? firstLine.slice(0, dotIndex + 1) : firstLine;
if (desc.length > 80) {
desc = desc.slice(0, 77) + "...";
}
return desc.trim();
}
function escapeYamlValue(val: string): string {
const needsQuoting =
val.length > 50 ||
val.includes(": ") ||
val.includes("#") ||
val.includes("'") ||
val.includes('"') ||
val.includes("\n");
if (!needsQuoting) return val;
if (val.includes("\n")) {
return (
"|\n" +
val
.split("\n")
.map((l) => ` ${l}`)
.join("\n") +
"\n"
);
}
const escaped = val.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
return `"${escaped}"`;
}
function generateBasicBody(text: string, hasArgs: boolean): string {
let body = cleanBody(text);
body += "\n";
if (hasArgs) {
body += "\n## Usage\n";
body += "\n```\n/command <arguments>\n```\n";
}
return body;
}
function generateDetailedBody(
text: string,
_description: string,
hasArgs: boolean,
hasFileRefs: boolean,
hasBashCmd: boolean,
): string {
let body = "";
body += "\n" + cleanBody(text) + "\n";
if (hasArgs || hasFileRefs || hasBashCmd) {
body += "\n---\n\n";
if (hasArgs) {
body += "## Usage\n\n";
body += "| Argument | Description |\n";
body += "|----------|-------------|\n";
body += "| `$ARGUMENTS` | All arguments passed to the command |\n";
}
if (hasFileRefs) {
body += "\n## Required files\n\n";
body += "- Files referenced via `@path/to/file` syntax\n";
}
if (hasBashCmd) {
body += "\n## Required tools\n\n";
body += "- Commands executed via `` !`command` `` syntax\n";
}
}
if (hasArgs) {
body += "\n## Examples\n\n";
body +=
"```\n# Example 1\n/command <arg1>\n\n# Example 2\n/command <arg1> <arg2>\n```\n";
}
body += "\n## Notes\n\n";
body += "- Follow the instructions precisely.\n";
body += "- Do not modify files outside the specified scope.\n";
return body;
}
function generateMinimalBody(text: string, _hasArgs: boolean): string {
const cleaned = cleanBody(text);
const firstLine = cleaned.split("\n")[0] || cleaned;
return "\n" + firstLine.trim() + "\n";
}
function cleanBody(text: string): string {
return text
.split("\n")
.map((l) => l.trimEnd())
.join("\n")
.trim();
}