Files
vercel__vercel-plugin/scripts/prompt-signals-explain.ts

191 lines
6.0 KiB
TypeScript

#!/usr/bin/env bun
/**
* Dry-run CLI for prompt-signal analysis.
*
* Usage:
* bun run scripts/prompt-signals-explain.ts --prompt 'add markdown formatting to streamed text'
* echo 'stream markdown in terminal' | bun run scripts/prompt-signals-explain.ts
* bun run scripts/prompt-signals-explain.ts --prompt '...' --json --seen-skills nextjs,ai-sdk
*/
import { resolve } from "node:path";
import { existsSync } from "node:fs";
import { analyzePrompt } from "../hooks/src/prompt-analysis.mjs";
import type { PromptAnalysisReport, PerSkillResult } from "../hooks/src/prompt-analysis.mjs";
import { loadValidatedSkillMap } from "../src/shared/skill-map-loader.ts";
// ---------------------------------------------------------------------------
// Arg parsing
// ---------------------------------------------------------------------------
const args = process.argv.slice(2);
let prompt = "";
let jsonOutput = false;
let seenSkills = "";
let budgetBytes = 8000;
let maxSkills = 2;
let wantHelp = false;
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === "--help" || arg === "-h") {
wantHelp = true;
} else if (arg === "--json") {
jsonOutput = true;
} else if (arg === "--prompt" || arg === "-p") {
i++;
prompt = args[i] ?? "";
} else if (arg === "--seen-skills") {
i++;
seenSkills = args[i] ?? "";
} else if (arg === "--budget-bytes") {
i++;
budgetBytes = parseInt(args[i], 10);
} else if (arg === "--max-skills") {
i++;
maxSkills = parseInt(args[i], 10);
}
}
// ---------------------------------------------------------------------------
// Help
// ---------------------------------------------------------------------------
if (wantHelp) {
console.log(`Usage: bun run scripts/prompt-signals-explain.ts [options]
Options:
--prompt, -p <text> Prompt to analyze (or pipe via stdin)
--json Output PromptAnalysisReport as JSON
--seen-skills <s1,s2> Comma-separated skills to exclude (dedup)
--budget-bytes <n> Budget in bytes (default: 8000)
--max-skills <n> Max skills to select (default: 2)
--help, -h Show this help
Examples:
bun run scripts/prompt-signals-explain.ts --prompt 'add markdown formatting to streamed text'
bun run scripts/prompt-signals-explain.ts --prompt '...' --json
echo 'stream markdown in terminal' | bun run scripts/prompt-signals-explain.ts`);
process.exit(0);
}
// ---------------------------------------------------------------------------
// Stdin fallback
// ---------------------------------------------------------------------------
if (!prompt) {
const isTTY = process.stdin.isTTY;
if (!isTTY) {
// Read from stdin
const chunks: Buffer[] = [];
for await (const chunk of process.stdin) {
chunks.push(chunk as Buffer);
}
prompt = Buffer.concat(chunks).toString("utf-8").trim();
}
}
if (!prompt) {
console.error("Error: no prompt provided. Use --prompt <text> or pipe via stdin.");
process.exit(1);
}
// ---------------------------------------------------------------------------
// Validate numeric args
// ---------------------------------------------------------------------------
if (!Number.isFinite(budgetBytes) || budgetBytes <= 0) {
console.error("Error: --budget-bytes must be a positive number.");
process.exit(1);
}
if (!Number.isFinite(maxSkills) || maxSkills <= 0) {
console.error("Error: --max-skills must be a positive integer.");
process.exit(1);
}
// ---------------------------------------------------------------------------
// Load skill map
// ---------------------------------------------------------------------------
const projectRoot = resolve(import.meta.dir, "..");
const skillsDir = resolve(projectRoot, "skills");
if (!existsSync(skillsDir)) {
console.error(`Error: no skills/ directory found at ${projectRoot}`);
process.exit(2);
}
const { skills } = loadValidatedSkillMap(skillsDir);
// ---------------------------------------------------------------------------
// Analyze
// ---------------------------------------------------------------------------
const report: PromptAnalysisReport = analyzePrompt(
prompt,
skills,
seenSkills,
budgetBytes,
maxSkills,
);
// ---------------------------------------------------------------------------
// Output
// ---------------------------------------------------------------------------
if (jsonOutput) {
console.log(JSON.stringify(report, null, 2));
process.exit(0);
}
// Human-readable table
console.log(`Prompt: "${report.normalizedPrompt}"`);
console.log(`Budget: ${report.budgetBytes} bytes | Max skills: ${maxSkills}`);
console.log(`Dedup: strategy=${report.dedupState.strategy}, seen=[${report.dedupState.seenSkills.join(",")}]`);
console.log();
const entries = Object.entries(report.perSkillResults);
if (entries.length === 0) {
console.log("No skills with promptSignals found.");
process.exit(0);
}
// Table header
const skillColWidth = Math.max(10, ...entries.map(([s]) => s.length)) + 2;
const header = [
"Skill".padEnd(skillColWidth),
"Score".padStart(7),
" Match",
" Suppr",
" Reason",
].join("");
console.log(header);
console.log("-".repeat(header.length + 20));
for (const [skill, r] of entries) {
const scoreStr = r.score === -Infinity ? " -Inf" : String(r.score).padStart(7);
const matchStr = r.matched ? " yes" : " no";
const supprStr = r.suppressed ? " yes" : " no";
console.log(
`${skill.padEnd(skillColWidth)}${scoreStr}${matchStr}${supprStr} ${r.reason}`,
);
}
console.log();
if (report.selectedSkills.length > 0) {
console.log(`Selected: ${report.selectedSkills.join(", ")}`);
}
if (report.droppedByCap.length > 0) {
console.log(`Dropped (cap): ${report.droppedByCap.join(", ")}`);
}
if (report.droppedByBudget.length > 0) {
console.log(`Dropped (budget): ${report.droppedByBudget.join(", ")}`);
}
if (report.dedupState.filteredByDedup.length > 0) {
console.log(`Filtered (dedup): ${report.dedupState.filteredByDedup.join(", ")}`);
}
console.log(`\nTiming: ${report.timingMs}ms`);
process.exit(0);