import { join } from "jsr:@std/path@1"; export type BotType = "claude" | "codex"; export type InstallCommand = { command: string; args: string[]; }; export type SuperpowersReviewGuide = { version: string; skillPath: string; templatePath: string; skill: string; template: string; }; export type SuperpowersFileSystem = { readDir(path: string): AsyncIterable; readTextFile(path: string): Promise; }; const systemFileSystem: SuperpowersFileSystem = { readDir: Deno.readDir, readTextFile: Deno.readTextFile, }; export function parseBotType(value: string): BotType { if (value === "claude" || value === "codex") return value; throw new Error(`unsupported bot type: ${value}`); } export function superpowersInstallCommands( bot: BotType, ): InstallCommand[] { if (bot === "claude") { return [ { command: "claude", args: [ "plugin", "marketplace", "add", "obra/superpowers-marketplace", ], }, { command: "claude", args: [ "plugin", "install", "-y", "superpowers@superpowers-marketplace", ], }, ]; } return [{ command: "codex", args: ["plugin", "add", "superpowers@openai-curated-remote"], }]; } type Candidate = SuperpowersReviewGuide & { directory: string }; async function readCandidate( directory: string, fileSystem: SuperpowersFileSystem, ): Promise { if (!directory.split(/[\\/]/).includes("superpowers")) return null; const skillPath = join(directory, "SKILL.md"); const templatePath = join(directory, "code-reviewer.md"); try { const [skill, template] = await Promise.all([ fileSystem.readTextFile(skillPath), fileSystem.readTextFile(templatePath), ]); return { directory, version: directory.split(/[\\/]/).at(-3) ?? "unknown", skillPath, templatePath, skill, template, }; } catch (error) { if (error instanceof Deno.errors.NotFound) return null; throw error; } } async function findCandidates( directory: string, candidates: Candidate[], fileSystem: SuperpowersFileSystem, ): Promise { let entries: Deno.DirEntry[]; try { entries = []; for await (const entry of fileSystem.readDir(directory)) { entries.push(entry); } } catch (error) { if (error instanceof Deno.errors.NotFound) return; throw error; } for (const entry of entries) { if (!entry.isDirectory) continue; const child = join(directory, entry.name); if (entry.name === "requesting-code-review") { const candidate = await readCandidate(child, fileSystem); if (candidate !== null) candidates.push(candidate); } else { await findCandidates(child, candidates, fileSystem); } } } // Load the installed files rather than trusting skill discovery in a later // non-interactive agent process. The newest cached plugin version is the one // the installers activate, and the exact paths are reported by the caller. export async function loadSuperpowersReviewGuide( bot: BotType, home: string, fileSystem: SuperpowersFileSystem = systemFileSystem, ): Promise { const root = join( home, bot === "codex" ? ".codex" : ".claude", "plugins", "cache", ); const candidates: Candidate[] = []; await findCandidates(root, candidates, fileSystem); candidates.sort((left, right) => left.version.localeCompare(right.version, undefined, { numeric: true }) || left.directory.localeCompare(right.directory) ); const guide = candidates.at(-1); if (guide === undefined) { throw new Error( `installed Superpowers requesting-code-review files not found under ${root}`, ); } const { directory: _, ...result } = guide; return result; }