feat: ship slash commands with GitHub Action (#381)

* feat: add slash command shipping infrastructure

- Created /slash-commands/ directory to store bundled slash commands
- Added code-review.md slash command for automated PR reviews
- Modified setup-claude-code-settings.ts to copy slash commands to ~/.claude/
- Added test coverage for slash command installation
- Commands are automatically installed when the GitHub Action runs

* fix: simplify slash command implementation to match codebase patterns

- Reverted to using Bun's $ shell syntax consistently with the rest of the codebase
- Simplified slash command copying to basic shell commands
- Removed unnecessary fs/promises complexity
- Maintained all functionality and test coverage
- More appropriate for GitHub Action context where inputs are trusted

* remove test slash command

* fix: rename slash_commands_dir to experimental_slash_commands_dir

- Added 'experimental' prefix as suggested by Ashwin
- Updated all references in action.yml and base-action
- Restored accidentally removed code-review.md file

---------

Co-authored-by: km-anthropic <km-anthropic@users.noreply.github.com>
This commit is contained in:
km-anthropic
2025-08-03 21:05:33 -07:00
committed by GitHub
parent d66adfb7fa
commit 458e4b9e7f
5 changed files with 93 additions and 2 deletions

View File

@@ -61,6 +61,9 @@ inputs:
description: "Timeout in minutes for Claude Code execution"
required: false
default: "10"
experimental_slash_commands_dir:
description: "Experimental: Directory containing slash command files to install"
required: false
# Authentication settings
anthropic_api_key:
@@ -143,6 +146,7 @@ runs:
INPUT_TIMEOUT_MINUTES: ${{ inputs.timeout_minutes }}
INPUT_CLAUDE_ENV: ${{ inputs.claude_env }}
INPUT_FALLBACK_MODEL: ${{ inputs.fallback_model }}
INPUT_EXPERIMENTAL_SLASH_COMMANDS_DIR: ${{ inputs.experimental_slash_commands_dir }}
# Provider configuration
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}

View File

@@ -10,7 +10,11 @@ async function run() {
try {
validateEnvironmentVariables();
await setupClaudeCodeSettings(process.env.INPUT_SETTINGS);
await setupClaudeCodeSettings(
process.env.INPUT_SETTINGS,
undefined, // homeDir
process.env.INPUT_EXPERIMENTAL_SLASH_COMMANDS_DIR,
);
const promptConfig = await preparePrompt({
prompt: process.env.INPUT_PROMPT || "",

View File

@@ -5,6 +5,7 @@ import { readFile } from "fs/promises";
export async function setupClaudeCodeSettings(
settingsInput?: string,
homeDir?: string,
slashCommandsDir?: string,
) {
const home = homeDir ?? homedir();
const settingsPath = `${home}/.claude/settings.json`;
@@ -65,4 +66,17 @@ export async function setupClaudeCodeSettings(
await $`echo ${JSON.stringify(settings, null, 2)} > ${settingsPath}`.quiet();
console.log(`Settings saved successfully`);
if (slashCommandsDir) {
console.log(
`Copying slash commands from ${slashCommandsDir} to ${home}/.claude/`,
);
try {
await $`test -d ${slashCommandsDir}`.quiet();
await $`cp ${slashCommandsDir}/*.md ${home}/.claude/ 2>/dev/null || true`.quiet();
console.log(`Slash commands copied successfully`);
} catch (e) {
console.log(`Slash commands directory not found or error copying: ${e}`);
}
}
}

View File

@@ -3,7 +3,7 @@
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
import { setupClaudeCodeSettings } from "../src/setup-claude-code-settings";
import { tmpdir } from "os";
import { mkdir, writeFile, readFile, rm } from "fs/promises";
import { mkdir, writeFile, readFile, rm, readdir } from "fs/promises";
import { join } from "path";
const testHomeDir = join(
@@ -147,4 +147,72 @@ describe("setupClaudeCodeSettings", () => {
expect(settings.newKey).toBe("newValue");
expect(settings.model).toBe("claude-opus-4-20250514");
});
test("should copy slash commands to .claude directory when path provided", async () => {
const testSlashCommandsDir = join(testHomeDir, "test-slash-commands");
await mkdir(testSlashCommandsDir, { recursive: true });
await writeFile(
join(testSlashCommandsDir, "test-command.md"),
"---\ndescription: Test command\n---\nTest content",
);
await setupClaudeCodeSettings(undefined, testHomeDir, testSlashCommandsDir);
const testCommandPath = join(testHomeDir, ".claude", "test-command.md");
const content = await readFile(testCommandPath, "utf-8");
expect(content).toContain("Test content");
});
test("should skip slash commands when no directory provided", async () => {
await setupClaudeCodeSettings(undefined, testHomeDir);
const settingsContent = await readFile(settingsPath, "utf-8");
const settings = JSON.parse(settingsContent);
expect(settings.enableAllProjectMcpServers).toBe(true);
});
test("should handle missing slash commands directory gracefully", async () => {
const nonExistentDir = join(testHomeDir, "non-existent");
await setupClaudeCodeSettings(undefined, testHomeDir, nonExistentDir);
const settingsContent = await readFile(settingsPath, "utf-8");
expect(JSON.parse(settingsContent).enableAllProjectMcpServers).toBe(true);
});
test("should skip non-.md files in slash commands directory", async () => {
const testSlashCommandsDir = join(testHomeDir, "test-slash-commands");
await mkdir(testSlashCommandsDir, { recursive: true });
await writeFile(join(testSlashCommandsDir, "not-markdown.txt"), "ignored");
await writeFile(join(testSlashCommandsDir, "valid.md"), "copied");
await writeFile(join(testSlashCommandsDir, "another.md"), "also copied");
await setupClaudeCodeSettings(undefined, testHomeDir, testSlashCommandsDir);
const copiedFiles = await readdir(join(testHomeDir, ".claude"));
expect(copiedFiles).toContain("valid.md");
expect(copiedFiles).toContain("another.md");
expect(copiedFiles).not.toContain("not-markdown.txt");
expect(copiedFiles).toContain("settings.json"); // Settings should also exist
});
test("should handle slash commands path that is a file not directory", async () => {
const testFile = join(testHomeDir, "not-a-directory.txt");
await writeFile(testFile, "This is a file, not a directory");
await setupClaudeCodeSettings(undefined, testHomeDir, testFile);
const settingsContent = await readFile(settingsPath, "utf-8");
expect(JSON.parse(settingsContent).enableAllProjectMcpServers).toBe(true);
});
test("should handle empty slash commands directory", async () => {
const emptyDir = join(testHomeDir, "empty-slash-commands");
await mkdir(emptyDir, { recursive: true });
await setupClaudeCodeSettings(undefined, testHomeDir, emptyDir);
const settingsContent = await readFile(settingsPath, "utf-8");
expect(JSON.parse(settingsContent).enableAllProjectMcpServers).toBe(true);
});
});