Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ runs:
INPUT_TIMEOUT_MINUTES: ${{ inputs.timeout_minutes }}
INPUT_CLAUDE_ENV: ${{ inputs.claude_env }}
INPUT_FALLBACK_MODEL: ${{ inputs.fallback_model }}
INPUT_SLASH_COMMANDS_DIR: ${{ github.action_path }}/slash-commands

# Model configuration
ANTHROPIC_MODEL: ${{ inputs.model || inputs.anthropic_model }}
Expand Down
4 changes: 4 additions & 0 deletions base-action/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ inputs:
description: "Timeout in minutes for Claude Code execution"
required: false
default: "10"
slash_commands_dir:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

experimental for now?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah yeah nice catch

description: "Directory containing slash command files to install"
required: false

# Authentication settings
anthropic_api_key:
Expand Down Expand Up @@ -143,6 +146,7 @@ runs:
INPUT_TIMEOUT_MINUTES: ${{ inputs.timeout_minutes }}
INPUT_CLAUDE_ENV: ${{ inputs.claude_env }}
INPUT_FALLBACK_MODEL: ${{ inputs.fallback_model }}
INPUT_SLASH_COMMANDS_DIR: ${{ inputs.slash_commands_dir }}

# Provider configuration
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
Expand Down
6 changes: 5 additions & 1 deletion base-action/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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_SLASH_COMMANDS_DIR,
);

const promptConfig = await preparePrompt({
prompt: process.env.INPUT_PROMPT || "",
Expand Down
14 changes: 14 additions & 0 deletions base-action/src/setup-claude-code-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Expand Down Expand Up @@ -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}`);
}
}
}
70 changes: 69 additions & 1 deletion base-action/test/setup-claude-code-settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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);
});
});