113 lines
2.5 KiB
TypeScript
113 lines
2.5 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
|
|
export interface SlashCommandContext {
|
|
cwd: string;
|
|
}
|
|
|
|
export type SlashCommandResult =
|
|
| { handled: true; message?: string }
|
|
| { handled: true; overlay: "diff"; content: string; truncated: boolean }
|
|
| { handled: false };
|
|
|
|
export interface SlashCommand {
|
|
name: string;
|
|
description: string;
|
|
run: (ctx: SlashCommandContext) => SlashCommandResult;
|
|
}
|
|
|
|
function isGitRepo(cwd: string): boolean {
|
|
const result = spawnSync(
|
|
"git",
|
|
[
|
|
"-c",
|
|
"safe.bareRepository=explicit",
|
|
"-c",
|
|
"core.fsmonitor=false",
|
|
"rev-parse",
|
|
"--is-inside-work-tree",
|
|
],
|
|
{
|
|
cwd,
|
|
stdio: ["ignore", "ignore", "ignore"],
|
|
},
|
|
);
|
|
return result.status === 0;
|
|
}
|
|
|
|
const MAX_DIFF_BYTES = 2_000_000;
|
|
|
|
function readDiff(cwd: string): { text: string; truncated: boolean } | null {
|
|
const result = spawnSync(
|
|
"git",
|
|
[
|
|
"-c",
|
|
"safe.bareRepository=explicit",
|
|
"-c",
|
|
"core.fsmonitor=false",
|
|
"--no-pager",
|
|
"diff",
|
|
"--no-color",
|
|
],
|
|
{
|
|
cwd,
|
|
encoding: "utf8",
|
|
maxBuffer: 32 * 1024 * 1024,
|
|
},
|
|
);
|
|
if (result.status !== 0 && result.status !== null) return null;
|
|
const stdout = result.stdout ?? "";
|
|
if (stdout.length > MAX_DIFF_BYTES) {
|
|
return { text: stdout.slice(0, MAX_DIFF_BYTES), truncated: true };
|
|
}
|
|
return { text: stdout, truncated: false };
|
|
}
|
|
|
|
const diffCommand: SlashCommand = {
|
|
name: "diff",
|
|
description: "show unstaged changes",
|
|
run: (ctx) => {
|
|
if (!isGitRepo(ctx.cwd)) {
|
|
return {
|
|
handled: true,
|
|
message: `not a git repository: ${ctx.cwd}`,
|
|
};
|
|
}
|
|
|
|
const diff = readDiff(ctx.cwd);
|
|
if (diff === null) {
|
|
return { handled: true, message: "failed to run `git diff`" };
|
|
}
|
|
|
|
if (diff.text.trim().length === 0) {
|
|
return { handled: true, message: "no unstaged changes" };
|
|
}
|
|
|
|
return {
|
|
handled: true,
|
|
overlay: "diff",
|
|
content: diff.text,
|
|
truncated: diff.truncated,
|
|
};
|
|
},
|
|
};
|
|
|
|
const COMMANDS: Record<string, SlashCommand> = {
|
|
diff: diffCommand,
|
|
};
|
|
|
|
export function tryRunSlashCommand(
|
|
input: string,
|
|
ctx: SlashCommandContext,
|
|
): SlashCommandResult {
|
|
const trimmed = input.trim();
|
|
if (!trimmed.startsWith("/")) return { handled: false };
|
|
const name = trimmed.slice(1).split(/\s+/)[0]?.toLowerCase() ?? "";
|
|
const cmd = COMMANDS[name];
|
|
if (!cmd) return { handled: false };
|
|
return cmd.run(ctx);
|
|
}
|
|
|
|
export function listSlashCommands(): SlashCommand[] {
|
|
return Object.values(COMMANDS);
|
|
}
|