mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 06:54:13 +08:00
feat: implement Claude Code GitHub Action v1.0 with auto-detection and slash commands
Major features: - Mode auto-detection based on GitHub event type - Unified prompt field replacing override_prompt and direct_prompt - Slash command system with pre-built commands - Full backward compatibility with v0.x Key changes: - Add mode detector for automatic mode selection - Implement slash command loader with YAML frontmatter support - Update action.yml with new prompt input - Create pre-built slash commands for common tasks - Update all tests for v1.0 compatibility Breaking changes (with compatibility): - Mode input now optional (auto-detected) - override_prompt deprecated (use prompt) - direct_prompt deprecated (use prompt)
This commit is contained in:
167
src/slash-commands/loader.ts
Normal file
167
src/slash-commands/loader.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { readFile, readdir, stat } from "fs/promises";
|
||||
import { join, dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import * as yaml from "js-yaml";
|
||||
|
||||
export interface SlashCommandMetadata {
|
||||
tools?: string[];
|
||||
settings?: Record<string, any>;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface SlashCommand {
|
||||
name: string;
|
||||
metadata: SlashCommandMetadata;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ResolvedCommand {
|
||||
expandedPrompt: string;
|
||||
tools?: string[];
|
||||
settings?: Record<string, any>;
|
||||
}
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const COMMANDS_DIR = join(__dirname, "../../slash-commands");
|
||||
|
||||
export async function resolveSlashCommand(
|
||||
prompt: string,
|
||||
variables?: Record<string, string | undefined>,
|
||||
): Promise<ResolvedCommand> {
|
||||
if (!prompt.startsWith("/")) {
|
||||
return handleLegacyPrompts(prompt);
|
||||
}
|
||||
|
||||
const parts = prompt.slice(1).split(" ");
|
||||
const commandPath = parts[0];
|
||||
const args = parts.slice(1);
|
||||
|
||||
if (!commandPath) {
|
||||
return { expandedPrompt: prompt };
|
||||
}
|
||||
|
||||
const commandParts = commandPath.split("/");
|
||||
|
||||
try {
|
||||
const command = await loadCommand(commandParts);
|
||||
if (!command) {
|
||||
console.warn(`Slash command not found: ${commandPath}`);
|
||||
return { expandedPrompt: prompt };
|
||||
}
|
||||
|
||||
let expandedContent = command.content;
|
||||
|
||||
if (args.length > 0) {
|
||||
expandedContent = expandedContent.replace(
|
||||
/\{args\}/g,
|
||||
args.join(" "),
|
||||
);
|
||||
}
|
||||
|
||||
if (variables) {
|
||||
Object.entries(variables).forEach(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
const regex = new RegExp(`\\{${key}\\}`, "g");
|
||||
expandedContent = expandedContent.replace(regex, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
expandedPrompt: expandedContent,
|
||||
tools: command.metadata.tools,
|
||||
settings: command.metadata.settings,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error loading slash command: ${error}`);
|
||||
return { expandedPrompt: prompt };
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCommand(
|
||||
commandParts: string[],
|
||||
): Promise<SlashCommand | null> {
|
||||
const possiblePaths = [
|
||||
join(COMMANDS_DIR, ...commandParts) + ".md",
|
||||
join(COMMANDS_DIR, ...commandParts, "default.md"),
|
||||
join(COMMANDS_DIR, commandParts[0] + ".md"),
|
||||
];
|
||||
|
||||
for (const filePath of possiblePaths) {
|
||||
try {
|
||||
const fileContent = await readFile(filePath, "utf-8");
|
||||
return parseCommandFile(commandParts.join("/"), fileContent);
|
||||
} catch (error) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseCommandFile(name: string, content: string): SlashCommand {
|
||||
let metadata: SlashCommandMetadata = {};
|
||||
let commandContent = content;
|
||||
|
||||
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
||||
if (frontmatterMatch && frontmatterMatch[1]) {
|
||||
try {
|
||||
const parsedYaml = yaml.load(frontmatterMatch[1]);
|
||||
if (parsedYaml && typeof parsedYaml === 'object') {
|
||||
metadata = parsedYaml as SlashCommandMetadata;
|
||||
}
|
||||
commandContent = frontmatterMatch[2]?.trim() || content;
|
||||
} catch (error) {
|
||||
console.warn(`Failed to parse frontmatter for command ${name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
metadata,
|
||||
content: commandContent,
|
||||
};
|
||||
}
|
||||
|
||||
export async function listAvailableCommands(): Promise<string[]> {
|
||||
const commands: string[] = [];
|
||||
|
||||
async function scanDirectory(dir: string, prefix = ""): Promise<void> {
|
||||
try {
|
||||
const entries = await readdir(dir);
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry);
|
||||
const entryStat = await stat(fullPath);
|
||||
|
||||
if (entryStat.isDirectory()) {
|
||||
await scanDirectory(fullPath, prefix ? `${prefix}/${entry}` : entry);
|
||||
} else if (entry.endsWith(".md")) {
|
||||
const commandName = entry.replace(".md", "");
|
||||
if (commandName !== "default") {
|
||||
commands.push(prefix ? `${prefix}/${commandName}` : commandName);
|
||||
} else if (prefix) {
|
||||
commands.push(prefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error scanning directory ${dir}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
await scanDirectory(COMMANDS_DIR);
|
||||
return commands.sort();
|
||||
}
|
||||
|
||||
function handleLegacyPrompts(prompt: string): ResolvedCommand {
|
||||
const legacyKeys = ["override_prompt", "direct_prompt"];
|
||||
for (const key of legacyKeys) {
|
||||
const envValue = process.env[key.toUpperCase()];
|
||||
if (envValue) {
|
||||
console.log(`Using legacy ${key} as prompt`);
|
||||
return { expandedPrompt: envValue };
|
||||
}
|
||||
}
|
||||
return { expandedPrompt: prompt };
|
||||
}
|
||||
Reference in New Issue
Block a user