Files
temeddix ba9e0c0787 Load Superpowers review instructions (#13)
Co-authored-by: Danny Kim <temeddix@gmail.com>
Co-committed-by: Danny Kim <temeddix@gmail.com>
2026-09-16 12:16:09 +00:00

148 lines
3.8 KiB
TypeScript

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<Deno.DirEntry>;
readTextFile(path: string): Promise<string>;
};
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<Candidate | null> {
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<void> {
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<SuperpowersReviewGuide> {
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;
}