import { execSync } from "node:child_process"; import { readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; // ============================================================ // License Templates // ============================================================ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const LICENSE_DIR = join(__dirname ?? ".", "licenses"); const LICENSE_TYPES = [ "mit", "apache-2.0", "bsd-3", "isc", "gpl-3.0", "agpl-3.0", "lgpl-3.0", "mpl-2.0", "unlicense", ] as const; type LicenseType = (typeof LICENSE_TYPES)[number]; function loadTemplate(type: LicenseType): (name: string, year: string) => string { const raw = readFileSync(join(LICENSE_DIR, `${type}.txt`), "utf-8"); return (name, year) => raw.replaceAll("{name}", name).replaceAll("{year}", year); } // ============================================================ // Core Logic // ============================================================ function generateLicense(type: LicenseType, name: string, year: string, cwd: string): string { const template = loadTemplate(type); const filePath = join(cwd, "LICENSE"); writeFileSync(filePath, template(name, year), "utf-8"); return filePath; } function isValidLicenseType(type: string): type is LicenseType { return LICENSE_TYPES.includes(type as LicenseType); } function getGitUserName(cwd: string): string { try { return execSync("git config user.name", { cwd, encoding: "utf-8" }).trim(); } catch { return "Your Name"; } } // ============================================================ // Extension Entry // ============================================================ export default function (pi: ExtensionAPI) { // Tool - LLM calls automatically pi.registerTool({ name: "generate_license", label: "Generate License", description: "Generate a license file in the current directory. Supported types: mit, apache-2.0, bsd-3, isc, gpl-3.0, agpl-3.0, lgpl-3.0, mpl-2.0, unlicense.", parameters: Type.Object({ type: Type.Optional( Type.String({ description: "License type: mit, apache-2.0, bsd-3, isc, gpl-3.0, agpl-3.0, lgpl-3.0, mpl-2.0, unlicense", }), ), name: Type.Optional(Type.String({ description: "Copyright holder name" })), }), async execute(_id, params, _signal, _onUpdate, ctx) { const { type = "mit", name } = params as { type?: string; name?: string; }; const year = new Date().getFullYear().toString(); const licenseType = isValidLicenseType(type) ? type : "mit"; const authorName = name || getGitUserName(ctx.cwd); const filePath = generateLicense(licenseType, authorName, year, ctx.cwd); return { content: [ { type: "text", text: `✓ LICENSE created at ${filePath} (${licenseType}, ${authorName}, ${year})`, }, ], details: { type: licenseType, name: authorName, year }, }; }, }); // Command - User calls directly with /license pi.registerCommand("gen-license", { description: "Generate a license file (mit, apache-2.0, bsd-3, isc, gpl-3.0, agpl-3.0, lgpl-3.0, mpl-2.0, unlicense)", getArgumentCompletions: (prefix: string) => { const items = LICENSE_TYPES.map((t) => ({ value: t, label: t })); const filtered = items.filter((i) => i.value.startsWith(prefix)); return filtered.length > 0 ? filtered : null; }, handler: async (args, ctx) => { const parts = (args || "").trim().split(/\s+/); const type = (parts[0] || "mit").toLowerCase(); const name = parts[1] || getGitUserName(ctx.cwd); const year = new Date().getFullYear().toString(); if (!isValidLicenseType(type)) { ctx.ui.notify(`Unknown license: ${type}. Available: ${LICENSE_TYPES.join(", ")}`, "error"); return; } generateLicense(type, name, year, ctx.cwd); ctx.ui.notify(`✓ LICENSE created (${type}, ${name}, ${year})`, "info"); }, }); }