mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-22 14:24:13 +08:00
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:
@@ -205,6 +205,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: ${{ github.action_path }}/slash-commands
|
||||
|
||||
# Model configuration
|
||||
ANTHROPIC_MODEL: ${{ inputs.model || inputs.anthropic_model }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 || "",
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user