feat: change plugins input from comma-separated to newline-separated (#644)

* feat: change plugins input from comma-separated to newline-separated

Changes:
- Update parsePlugins() to split by newline instead of comma for consistency with marketplaces input
- Update action.yml and base-action/action.yml with newline-separated format and realistic plugin examples
- Add plugin_marketplaces documentation to docs/usage.md
- Update all unit tests to match new installPlugins() signature (marketplaces, plugins, executable)
- Improve JSDoc comments for parsePlugins() and installPlugin() functions
- All 25 install-plugins tests passing

Breaking change: Users must update their workflows to use newline-separated format:
  Before: plugins: "plugin1,plugin2"
  After: plugins: "plugin1\nplugin2"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* test: add comprehensive marketplace functionality tests

Critical fix: All previous tests passed undefined as marketplacesInput parameter,
leaving the entire marketplace functionality completely untested.

Added 13 new tests covering:
- Single marketplace installation
- Multiple marketplaces with newline separation
- Marketplace + plugin installation order verification
- Marketplace URL validation (format, protocol, .git extension)
- Whitespace and empty entry handling
- Error handling for marketplace operations
- Custom executable path for marketplace operations

Test coverage: 38 tests (was 25), 81 expect calls (was 50)
All tests passing 

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Wanghong Yuan
2025-10-27 09:01:34 -07:00
committed by GitHub
parent 8ad13bd20b
commit 29fe50368c
6 changed files with 319 additions and 61 deletions

View File

@@ -18,9 +18,9 @@ async function run() {
// Install Claude Code plugins if specified
await installPlugins(
process.env.INPUT_PLUGIN_MARKETPLACES,
process.env.INPUT_PLUGINS,
process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE,
process.env.INPUT_PLUGIN_MARKETPLACES,
);
const promptConfig = await preparePrompt({

View File

@@ -79,10 +79,13 @@ function parseMarketplaces(marketplaces?: string): string[] {
}
/**
* Parse a comma-separated list of plugin names and return an array of trimmed, non-empty plugin names
* Parse a newline-separated list of plugin names and return an array of trimmed, non-empty plugin names
* Validates plugin names to prevent command injection and path traversal attacks
* Allows: letters, numbers, @, -, _, /, . (common npm/scoped package characters)
* Disallows: path traversal (../, ./), shell metacharacters, and consecutive dots
* @param plugins - Newline-separated list of plugin names, or undefined/empty to return empty array
* @returns Array of validated plugin names (empty array if none provided)
* @throws {Error} If any plugin name fails validation
*/
function parsePlugins(plugins?: string): string[] {
const trimmedPlugins = plugins?.trim();
@@ -91,9 +94,9 @@ function parsePlugins(plugins?: string): string[] {
return [];
}
// Split by comma and process each plugin
// Split by newline and process each plugin
return trimmedPlugins
.split(",")
.split("\n")
.map((p) => p.trim())
.filter((p) => {
if (p.length === 0) return false;
@@ -139,11 +142,17 @@ async function executeClaudeCommand(
/**
* Installs a single Claude Code plugin
* @param pluginName - The name of the plugin to install
* @param claudeExecutable - Path to the Claude executable
* @returns Promise that resolves when the plugin is installed successfully
* @throws {Error} If the plugin installation fails
*/
async function installPlugin(
pluginName: string,
claudeExecutable: string,
): Promise<void> {
console.log(`Installing plugin: ${pluginName}`);
return executeClaudeCommand(
claudeExecutable,
["plugin", "install", pluginName],
@@ -172,25 +181,18 @@ async function addMarketplace(
}
/**
* Installs Claude Code plugins from a comma-separated list
* @param pluginsInput - Comma-separated list of plugin names, or undefined/empty to skip installation
* @param claudeExecutable - Path to the Claude executable (defaults to "claude")
* Installs Claude Code plugins from a newline-separated list
* @param marketplacesInput - Newline-separated list of marketplace Git URLs
* @param pluginsInput - Newline-separated list of plugin names
* @param claudeExecutable - Path to the Claude executable (defaults to "claude")
* @returns Promise that resolves when all plugins are installed
* @throws {Error} If any plugin fails validation or installation (stops on first error)
*/
export async function installPlugins(
pluginsInput: string | undefined,
claudeExecutable?: string,
marketplacesInput?: string,
pluginsInput?: string,
claudeExecutable?: string,
): Promise<void> {
const plugins = parsePlugins(pluginsInput);
if (plugins.length === 0) {
console.log("No plugins to install");
return;
}
// Resolve executable path with explicit fallback
const resolvedExecutable = claudeExecutable || "claude";
@@ -207,13 +209,14 @@ export async function installPlugins(
console.log("No marketplaces specified, skipping marketplace setup");
}
console.log(`Installing ${plugins.length} plugin(s)...`);
for (const plugin of plugins) {
console.log(`Installing plugin: ${plugin}`);
await installPlugin(plugin, resolvedExecutable);
console.log(`✓ Successfully installed: ${plugin}`);
const plugins = parsePlugins(pluginsInput);
if (plugins.length > 0) {
console.log(`Installing ${plugins.length} plugin(s)...`);
for (const plugin of plugins) {
await installPlugin(plugin, resolvedExecutable);
console.log(`✓ Successfully installed: ${plugin}`);
}
} else {
console.log("No plugins specified, skipping plugins installation");
}
console.log("All plugins installed successfully");
}