playbook/outfitter-agents/plugins/outfitter/skills/docs-audit/scripts/report-path.ts

312 lines
9.3 KiB
TypeScript
Executable File

#!/usr/bin/env bun
/**
* report-path.ts - Generate report paths with timestamp and session ID
*
* Constructs standardized report filenames following the pattern:
* {timestamp}-{type}-{sessionShort}.md
* {timestamp}-{type}/ (for multi-file)
*
* Usage:
* ./report-path.ts # Uses CLAUDE_SESSION_ID env var
* ./report-path.ts --session abc123-def456 # Explicit session ID
* ./report-path.ts --type docs-audit # Report type (default: docs-audit)
* ./report-path.ts --multi # Output directory path instead of file
* ./report-path.ts --base .pack/reports # Base directory (default: .pack/reports)
* ./report-path.ts --json # Output as JSON with all components
* ./report-path.ts --scaffold # Create directory structure
* ./report-path.ts --scaffold --multi # Scaffold multi-file with placeholders
*/
import { parseArgs } from "util";
import { mkdir, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";
/**
* Components of a report path.
*/
interface ReportPathComponents {
/** Timestamp as YYYYMMDDhhmm */
timestamp: string;
/** ISO 8601 timestamp for frontmatter */
timestampISO: string;
/** Report type (e.g., "docs-audit") */
type: string;
/** Full session ID (or empty) */
sessionFull: string;
/** First 8 chars of session ID (or empty) */
sessionShort: string;
/** Just the filename */
filename: string;
/** Full path including base */
path: string;
/** Whether this is a directory mode report */
isMulti: boolean;
}
/**
* Gets current timestamp in both formatted and ISO formats.
* @returns Object with formatted (YYYYMMDDhhmm) and ISO timestamps
*/
function getTimestamp(): { formatted: string; iso: string } {
const now = new Date();
const formatted = [
now.getFullYear(),
String(now.getMonth() + 1).padStart(2, "0"),
String(now.getDate()).padStart(2, "0"),
String(now.getHours()).padStart(2, "0"),
String(now.getMinutes()).padStart(2, "0"),
].join("");
return {
formatted,
iso: now.toISOString(),
};
}
/**
* Truncates session ID to first 8 characters.
* @param sessionId - Full session ID
* @returns First 8 characters of session ID
*/
function getShortSessionId(sessionId: string): string {
if (!sessionId) return "";
// Handle UUIDs (take first segment) or just first 8 chars
const firstSegment = sessionId.split("-")[0];
return firstSegment.length >= 8 ? firstSegment.slice(0, 8) : firstSegment;
}
/**
* Builds a filename from components.
* @param timestamp - Formatted timestamp
* @param type - Report type
* @param sessionShort - Short session ID
* @param isMulti - Whether this is a multi-file report
* @returns Filename or directory name
*/
function buildFilename(
timestamp: string,
type: string,
sessionShort: string,
isMulti: boolean
): string {
// Multi-file mode: no session in directory name (session tracked in frontmatter)
// Single-file mode: session suffix for parallel disambiguation
if (isMulti) {
return `${timestamp}-${type}`;
}
const parts = [timestamp, type];
if (sessionShort) {
parts.push(sessionShort);
}
return `${parts.join("-")}.md`;
}
// Multi-file report structure
const MULTI_FILE_STRUCTURE = [
{ name: "summary.md", description: "Overall findings and links to other reports" },
{ name: "markdown-docs.md", description: "Analysis of docs/, README, etc." },
{ name: "docstrings.md", description: "TSDoc/JSDoc/docstring coverage" },
{ name: "recommendations.md", description: "Prioritized actionable recommendations" },
{ name: "meta.json", description: "Session metadata (structured)" },
] as const;
/**
* Generates YAML frontmatter for report file.
* @param components - Report path components
* @param fileType - Type of file within report
* @returns Frontmatter string
*/
function generateFrontmatter(components: ReportPathComponents, fileType: string): string {
return `---
type: ${components.type}
file_type: ${fileType}
generated: ${components.timestampISO}
timestamp: "${components.timestamp}"
session: "${components.sessionFull}"
session_short: "${components.sessionShort}"
status: pending
---
`;
}
/**
* Generates meta.json content for multi-file reports.
* @param components - Report path components
* @returns JSON string
*/
function generateMetaJson(components: ReportPathComponents): string {
return JSON.stringify(
{
type: components.type,
generated: components.timestampISO,
timestamp: components.timestamp,
session: components.sessionFull,
session_short: components.sessionShort,
path: components.path,
status: "pending",
files: MULTI_FILE_STRUCTURE.map((f) => f.name),
},
null,
2
);
}
/**
* Creates directory structure and placeholder files for report.
* @param components - Report path components
* @returns Array of created paths
*/
async function scaffoldReport(components: ReportPathComponents): Promise<string[]> {
const created: string[] = [];
if (components.isMulti) {
// Create directory and placeholder files
await mkdir(components.path, { recursive: true });
created.push(components.path);
for (const file of MULTI_FILE_STRUCTURE) {
const filePath = `${components.path}/${file.name}`;
if (!existsSync(filePath)) {
let content: string;
if (file.name.endsWith(".json")) {
// JSON files get structured metadata
content = generateMetaJson(components);
} else {
// Markdown files get frontmatter + placeholder
const fileType = file.name.replace(".md", "");
const frontmatter = generateFrontmatter(components, fileType);
content = `${frontmatter}# ${file.description}\n\n<!-- TODO: Populate during audit -->\n`;
}
await writeFile(filePath, content);
created.push(filePath);
}
}
} else {
// Create base directory only
const baseDir = components.path.substring(0, components.path.lastIndexOf("/"));
await mkdir(baseDir, { recursive: true });
created.push(baseDir);
}
return created;
}
/**
* Generates report path components from options.
* @param options - Report path options
* @returns Report path components
*/
function generateReportPath(options: {
session?: string;
type?: string;
base?: string;
multi?: boolean;
}): ReportPathComponents {
const sessionFull = options.session || process.env.CLAUDE_SESSION_ID || "";
const sessionShort = getShortSessionId(sessionFull);
const type = options.type || "docs-audit";
const base = options.base || ".pack/reports";
const isMulti = options.multi || false;
const { formatted: timestamp, iso: timestampISO } = getTimestamp();
const filename = buildFilename(timestamp, type, sessionShort, isMulti);
const path = `${base}/${filename}`;
return {
timestamp,
timestampISO,
type,
sessionFull,
sessionShort,
filename,
path,
isMulti,
};
}
// Main
async function main() {
const { values } = parseArgs({
args: Bun.argv.slice(2),
options: {
session: { type: "string", short: "s" },
type: { type: "string", short: "t", default: "docs-audit" },
base: { type: "string", short: "b", default: ".pack/reports" },
multi: { type: "boolean", short: "m" },
scaffold: { type: "boolean" },
json: { type: "boolean", short: "j" },
help: { type: "boolean", short: "h" },
},
allowPositionals: true,
});
if (values.help) {
console.log(`
report-path.ts - Generate report paths with timestamp and session ID
Usage:
./report-path.ts [options]
Options:
--session, -s <id> Session ID (defaults to CLAUDE_SESSION_ID env var)
--type, -t <type> Report type (default: docs-audit)
--base, -b <dir> Base directory (default: .pack/reports)
--multi, -m Directory mode (no session suffix, files inside)
--scaffold Create directory structure (with placeholders for --multi)
--json, -j Output as JSON with all components
--help, -h Show this help
Examples:
./report-path.ts
# Output: .pack/reports/202601251900-docs-audit.md
./report-path.ts --session abc12345-def-ghi
# Output: .pack/reports/202601251900-docs-audit-abc12345.md
./report-path.ts --multi
# Output: .pack/reports/202601251900-docs-audit
./report-path.ts --scaffold --multi --session abc123
# Creates: .pack/reports/202601251900-docs-audit/
# .pack/reports/202601251900-docs-audit/summary.md
# .pack/reports/202601251900-docs-audit/markdown-docs.md
# ...
./report-path.ts --json
# Output: {"timestamp":"202601251900","sessionShort":"abc12345",...}
`);
process.exit(0);
}
const result = generateReportPath({
session: values.session,
type: values.type,
base: values.base,
multi: values.multi,
});
if (values.scaffold) {
const created = await scaffoldReport(result);
if (values.json) {
console.log(JSON.stringify({ ...result, scaffolded: created }, null, 2));
} else {
console.log(`Scaffolded: ${result.path}`);
for (const path of created) {
console.log(` ${path}`);
}
}
} else if (values.json) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log(result.path);
}
}
main().catch((err) => {
console.error("Error:", err.message);
process.exit(1);
});