ba9e0c0787
Co-authored-by: Danny Kim <temeddix@gmail.com> Co-committed-by: Danny Kim <temeddix@gmail.com>
148 lines
3.8 KiB
TypeScript
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;
|
|
}
|