mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 06:54:13 +08:00
Add plugins input to GitHub Action
This commit adds support for installing Claude Code plugins via a new `plugins` input parameter.
Changes:
- Added `plugins` input to action.yml (comma-separated list)
- Created `install-plugins.ts` with plugin installation logic
- Added comprehensive tests in `install-plugins.test.ts`
- Updated base-action index.ts to call plugin installation
- Plugins are installed after settings setup but before Claude execution
Usage example:
```yaml
- uses: anthropic-ai/claude-code-action@main
with:
plugins: "feature-dev,test-coverage-reviewer"
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import { preparePrompt } from "./prepare-prompt";
|
||||
import { runClaude } from "./run-claude";
|
||||
import { setupClaudeCodeSettings } from "./setup-claude-code-settings";
|
||||
import { validateEnvironmentVariables } from "./validate-env";
|
||||
import { installPlugins } from "./install-plugins";
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
@@ -15,6 +16,12 @@ async function run() {
|
||||
undefined, // homeDir
|
||||
);
|
||||
|
||||
// Install plugins if specified
|
||||
await installPlugins(
|
||||
process.env.INPUT_PLUGINS,
|
||||
process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE || "claude",
|
||||
);
|
||||
|
||||
const promptConfig = await preparePrompt({
|
||||
prompt: process.env.INPUT_PROMPT || "",
|
||||
promptFile: process.env.INPUT_PROMPT_FILE || "",
|
||||
|
||||
80
base-action/src/install-plugins.ts
Normal file
80
base-action/src/install-plugins.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { spawn } from "child_process";
|
||||
|
||||
// Declare console as global for TypeScript
|
||||
declare const console: {
|
||||
log: (message: string) => void;
|
||||
error: (message: string) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a comma-separated list of plugin names and returns an array of trimmed plugin names
|
||||
*/
|
||||
export function parsePlugins(pluginsInput: string | undefined): string[] {
|
||||
if (!pluginsInput || pluginsInput.trim() === "") {
|
||||
return [];
|
||||
}
|
||||
|
||||
return pluginsInput
|
||||
.split(",")
|
||||
.map((plugin) => plugin.trim())
|
||||
.filter((plugin) => plugin.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs a single Claude Code plugin
|
||||
*/
|
||||
export async function installPlugin(
|
||||
pluginName: string,
|
||||
claudeExecutable: string = "claude",
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const process = spawn(claudeExecutable, ["plugin", "install", pluginName], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
process.on("close", (code: number | null) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
`Failed to install plugin '${pluginName}' (exit code: ${code})`,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
process.on("error", (err: Error) => {
|
||||
reject(
|
||||
new Error(`Failed to install plugin '${pluginName}': ${err.message}`),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs Claude Code plugins from a comma-separated list
|
||||
*/
|
||||
export async function installPlugins(
|
||||
pluginsInput: string | undefined,
|
||||
claudeExecutable: string = "claude",
|
||||
): Promise<void> {
|
||||
const plugins = parsePlugins(pluginsInput);
|
||||
|
||||
if (plugins.length === 0) {
|
||||
console.log("No plugins to install");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Installing ${plugins.length} plugin(s)...`);
|
||||
|
||||
for (const plugin of plugins) {
|
||||
console.log(`Installing plugin: ${plugin}`);
|
||||
await installPlugin(plugin, claudeExecutable);
|
||||
console.log(`✓ Successfully installed: ${plugin}`);
|
||||
}
|
||||
|
||||
console.log("All plugins installed successfully");
|
||||
}
|
||||
84
base-action/test/install-plugins.test.ts
Normal file
84
base-action/test/install-plugins.test.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { parsePlugins } from "../src/install-plugins";
|
||||
|
||||
describe("parsePlugins", () => {
|
||||
test("should return empty array for undefined input", () => {
|
||||
expect(parsePlugins(undefined)).toEqual([]);
|
||||
});
|
||||
|
||||
test("should return empty array for empty string", () => {
|
||||
expect(parsePlugins("")).toEqual([]);
|
||||
});
|
||||
|
||||
test("should return empty array for whitespace-only string", () => {
|
||||
expect(parsePlugins(" \n\t ")).toEqual([]);
|
||||
});
|
||||
|
||||
test("should parse single plugin", () => {
|
||||
expect(parsePlugins("feature-dev")).toEqual(["feature-dev"]);
|
||||
});
|
||||
|
||||
test("should parse multiple plugins", () => {
|
||||
expect(parsePlugins("feature-dev,test-coverage-reviewer")).toEqual([
|
||||
"feature-dev",
|
||||
"test-coverage-reviewer",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should trim whitespace around plugin names", () => {
|
||||
expect(parsePlugins(" feature-dev , test-coverage-reviewer ")).toEqual([
|
||||
"feature-dev",
|
||||
"test-coverage-reviewer",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should handle spaces between commas", () => {
|
||||
expect(
|
||||
parsePlugins(
|
||||
"feature-dev, test-coverage-reviewer, code-quality-reviewer",
|
||||
),
|
||||
).toEqual([
|
||||
"feature-dev",
|
||||
"test-coverage-reviewer",
|
||||
"code-quality-reviewer",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should filter out empty values from consecutive commas", () => {
|
||||
expect(parsePlugins("feature-dev,,test-coverage-reviewer")).toEqual([
|
||||
"feature-dev",
|
||||
"test-coverage-reviewer",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should handle trailing comma", () => {
|
||||
expect(parsePlugins("feature-dev,test-coverage-reviewer,")).toEqual([
|
||||
"feature-dev",
|
||||
"test-coverage-reviewer",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should handle leading comma", () => {
|
||||
expect(parsePlugins(",feature-dev,test-coverage-reviewer")).toEqual([
|
||||
"feature-dev",
|
||||
"test-coverage-reviewer",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should handle plugins with special characters", () => {
|
||||
expect(parsePlugins("@scope/plugin-name,plugin-name-2")).toEqual([
|
||||
"@scope/plugin-name",
|
||||
"plugin-name-2",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should handle complex whitespace patterns", () => {
|
||||
expect(
|
||||
parsePlugins(
|
||||
"\n feature-dev \n,\t test-coverage-reviewer\t, code-quality \n",
|
||||
),
|
||||
).toEqual(["feature-dev", "test-coverage-reviewer", "code-quality"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user