mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-22 22:44:13 +08:00
feat: add plugin_marketplaces input for dynamic marketplace installation (#642)
- Added plugin_marketplaces input to both main and base-action action.yml files - Updated install-plugins.ts to support multiple marketplace URLs (newline-separated) - Added validation for marketplace URLs to prevent security issues - Updated installPlugins function to dynamically add marketplaces instead of hardcoding - Defaults to official Claude Code marketplace when no marketplaces are specified - Updated base-action index.ts to pass plugin_marketplaces to installPlugins 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -105,6 +105,10 @@ inputs:
|
|||||||
description: "Comma-separated list of Claude Code plugin names to install (e.g., 'plugin1,plugin2,plugin3')"
|
description: "Comma-separated list of Claude Code plugin names to install (e.g., 'plugin1,plugin2,plugin3')"
|
||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ""
|
||||||
|
plugin_marketplaces:
|
||||||
|
description: "Newline-separated list of Claude Code plugin marketplace Git URLs to install from (e.g., 'https://github.com/user/marketplace1.git\nhttps://github.com/user/marketplace2.git')"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
execution_file:
|
execution_file:
|
||||||
@@ -218,6 +222,7 @@ runs:
|
|||||||
INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
|
INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
|
||||||
INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}
|
INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}
|
||||||
INPUT_PLUGINS: ${{ inputs.plugins }}
|
INPUT_PLUGINS: ${{ inputs.plugins }}
|
||||||
|
INPUT_PLUGIN_MARKETPLACES: ${{ inputs.plugin_marketplaces }}
|
||||||
|
|
||||||
# Model configuration
|
# Model configuration
|
||||||
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ inputs:
|
|||||||
description: "Comma-separated list of Claude Code plugin names to install (e.g., 'plugin1,plugin2,plugin3')"
|
description: "Comma-separated list of Claude Code plugin names to install (e.g., 'plugin1,plugin2,plugin3')"
|
||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ""
|
||||||
|
plugin_marketplaces:
|
||||||
|
description: "Newline-separated list of Claude Code plugin marketplace Git URLs to install from (e.g., 'https://github.com/user/marketplace1.git\nhttps://github.com/user/marketplace2.git')"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
conclusion:
|
conclusion:
|
||||||
@@ -131,6 +135,7 @@ runs:
|
|||||||
INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
|
INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
|
||||||
INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}
|
INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}
|
||||||
INPUT_PLUGINS: ${{ inputs.plugins }}
|
INPUT_PLUGINS: ${{ inputs.plugins }}
|
||||||
|
INPUT_PLUGIN_MARKETPLACES: ${{ inputs.plugin_marketplaces }}
|
||||||
|
|
||||||
# Provider configuration
|
# Provider configuration
|
||||||
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
|
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
|
||||||
|
|||||||
196
base-action/package-lock.json
generated
Normal file
196
base-action/package-lock.json
generated
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
{
|
||||||
|
"name": "@anthropic-ai/claude-code-base-action",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@anthropic-ai/claude-code-base-action",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@actions/core": "^1.10.1",
|
||||||
|
"shell-quote": "^1.8.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "^1.2.12",
|
||||||
|
"@types/node": "^20.0.0",
|
||||||
|
"@types/shell-quote": "^1.7.5",
|
||||||
|
"prettier": "3.5.3",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@actions/core": {
|
||||||
|
"version": "1.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz",
|
||||||
|
"integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@actions/exec": "^1.1.1",
|
||||||
|
"@actions/http-client": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@actions/exec": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@actions/io": "^1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@actions/http-client": {
|
||||||
|
"version": "2.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz",
|
||||||
|
"integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tunnel": "^0.0.6",
|
||||||
|
"undici": "^5.25.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@actions/io": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@fastify/busboy": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/bun": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bun-types": "1.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "20.19.23",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.23.tgz",
|
||||||
|
"integrity": "sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/react": {
|
||||||
|
"version": "19.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||||
|
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"csstype": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/shell-quote": {
|
||||||
|
"version": "1.7.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/shell-quote/-/shell-quote-1.7.5.tgz",
|
||||||
|
"integrity": "sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/bun-types": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/csstype": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "3.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
||||||
|
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin/prettier.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/shell-quote": {
|
||||||
|
"version": "1.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
|
||||||
|
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tunnel": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici": {
|
||||||
|
"version": "5.29.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz",
|
||||||
|
"integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@fastify/busboy": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ async function run() {
|
|||||||
await installPlugins(
|
await installPlugins(
|
||||||
process.env.INPUT_PLUGINS,
|
process.env.INPUT_PLUGINS,
|
||||||
process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE,
|
process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE,
|
||||||
|
process.env.INPUT_PLUGIN_MARKETPLACES,
|
||||||
);
|
);
|
||||||
|
|
||||||
const promptConfig = await preparePrompt({
|
const promptConfig = await preparePrompt({
|
||||||
|
|||||||
@@ -2,10 +2,34 @@ import { spawn, ChildProcess } from "child_process";
|
|||||||
|
|
||||||
const PLUGIN_NAME_REGEX = /^[@a-zA-Z0-9_\-\/\.]+$/;
|
const PLUGIN_NAME_REGEX = /^[@a-zA-Z0-9_\-\/\.]+$/;
|
||||||
const MAX_PLUGIN_NAME_LENGTH = 512;
|
const MAX_PLUGIN_NAME_LENGTH = 512;
|
||||||
const CLAUDE_CODE_MARKETPLACE_URL =
|
|
||||||
"https://github.com/anthropics/claude-code.git";
|
|
||||||
const PATH_TRAVERSAL_REGEX =
|
const PATH_TRAVERSAL_REGEX =
|
||||||
/\.\.\/|\/\.\.|\.\/|\/\.|(?:^|\/)\.\.$|(?:^|\/)\.$|\.\.(?![0-9])/;
|
/\.\.\/|\/\.\.|\.\/|\/\.|(?:^|\/)\.\.$|(?:^|\/)\.$|\.\.(?![0-9])/;
|
||||||
|
const MARKETPLACE_URL_REGEX =
|
||||||
|
/^https:\/\/[a-zA-Z0-9\-._~:/?#[\]@!$&'()*+,;=%]+\.git$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a marketplace URL for security issues
|
||||||
|
* @param url - The marketplace URL to validate
|
||||||
|
* @throws {Error} If the URL is invalid
|
||||||
|
*/
|
||||||
|
function validateMarketplaceUrl(url: string): void {
|
||||||
|
const normalized = url.trim();
|
||||||
|
|
||||||
|
if (!normalized) {
|
||||||
|
throw new Error("Marketplace URL cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MARKETPLACE_URL_REGEX.test(normalized)) {
|
||||||
|
throw new Error(`Invalid marketplace URL format: ${url}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional check for valid URL structure
|
||||||
|
try {
|
||||||
|
new URL(normalized);
|
||||||
|
} catch {
|
||||||
|
throw new Error(`Invalid marketplace URL: ${url}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates a plugin name for security issues
|
* Validates a plugin name for security issues
|
||||||
@@ -30,6 +54,30 @@ function validatePluginName(pluginName: string): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a newline-separated list of marketplace URLs and return an array of validated URLs
|
||||||
|
* @param marketplaces - Newline-separated list of marketplace Git URLs
|
||||||
|
* @returns Array of validated marketplace URLs (empty array if none provided)
|
||||||
|
*/
|
||||||
|
function parseMarketplaces(marketplaces?: string): string[] {
|
||||||
|
const trimmed = marketplaces?.trim();
|
||||||
|
|
||||||
|
if (!trimmed) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split by newline and process each URL
|
||||||
|
return trimmed
|
||||||
|
.split("\n")
|
||||||
|
.map((url) => url.trim())
|
||||||
|
.filter((url) => {
|
||||||
|
if (url.length === 0) return false;
|
||||||
|
|
||||||
|
validateMarketplaceUrl(url);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a comma-separated list of plugin names and return an array of trimmed, non-empty plugin names
|
* Parse a comma-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
|
* Validates plugin names to prevent command injection and path traversal attacks
|
||||||
@@ -104,18 +152,22 @@ async function installPlugin(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the Claude Code marketplace
|
* Adds a Claude Code plugin marketplace
|
||||||
* @param claudeExecutable - Path to the Claude executable
|
* @param claudeExecutable - Path to the Claude executable
|
||||||
|
* @param marketplaceUrl - The marketplace Git URL to add
|
||||||
* @returns Promise that resolves when the marketplace add command completes
|
* @returns Promise that resolves when the marketplace add command completes
|
||||||
* @throws {Error} If the command fails to execute
|
* @throws {Error} If the command fails to execute
|
||||||
*/
|
*/
|
||||||
async function addMarketplace(claudeExecutable: string): Promise<void> {
|
async function addMarketplace(
|
||||||
console.log("Adding Claude Code marketplace...");
|
claudeExecutable: string,
|
||||||
|
marketplaceUrl: string,
|
||||||
|
): Promise<void> {
|
||||||
|
console.log(`Adding marketplace: ${marketplaceUrl}`);
|
||||||
|
|
||||||
return executeClaudeCommand(
|
return executeClaudeCommand(
|
||||||
claudeExecutable,
|
claudeExecutable,
|
||||||
["plugin", "marketplace", "add", CLAUDE_CODE_MARKETPLACE_URL],
|
["plugin", "marketplace", "add", marketplaceUrl],
|
||||||
"Failed to add marketplace",
|
`Failed to add marketplace '${marketplaceUrl}'`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,12 +175,14 @@ async function addMarketplace(claudeExecutable: string): Promise<void> {
|
|||||||
* Installs Claude Code plugins from a comma-separated list
|
* Installs Claude Code plugins from a comma-separated list
|
||||||
* @param pluginsInput - Comma-separated list of plugin names, or undefined/empty to skip installation
|
* @param pluginsInput - Comma-separated list of plugin names, or undefined/empty to skip installation
|
||||||
* @param claudeExecutable - Path to the Claude executable (defaults to "claude")
|
* @param claudeExecutable - Path to the Claude executable (defaults to "claude")
|
||||||
|
* @param marketplacesInput - Newline-separated list of marketplace Git URLs
|
||||||
* @returns Promise that resolves when all plugins are installed
|
* @returns Promise that resolves when all plugins are installed
|
||||||
* @throws {Error} If any plugin fails validation or installation (stops on first error)
|
* @throws {Error} If any plugin fails validation or installation (stops on first error)
|
||||||
*/
|
*/
|
||||||
export async function installPlugins(
|
export async function installPlugins(
|
||||||
pluginsInput: string | undefined,
|
pluginsInput: string | undefined,
|
||||||
claudeExecutable?: string,
|
claudeExecutable?: string,
|
||||||
|
marketplacesInput?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const plugins = parsePlugins(pluginsInput);
|
const plugins = parsePlugins(pluginsInput);
|
||||||
|
|
||||||
@@ -140,8 +194,18 @@ export async function installPlugins(
|
|||||||
// Resolve executable path with explicit fallback
|
// Resolve executable path with explicit fallback
|
||||||
const resolvedExecutable = claudeExecutable || "claude";
|
const resolvedExecutable = claudeExecutable || "claude";
|
||||||
|
|
||||||
// Add marketplace before installing plugins
|
// Parse and add all marketplaces before installing plugins
|
||||||
await addMarketplace(resolvedExecutable);
|
const marketplaces = parseMarketplaces(marketplacesInput);
|
||||||
|
|
||||||
|
if (marketplaces.length > 0) {
|
||||||
|
console.log(`Adding ${marketplaces.length} marketplace(s)...`);
|
||||||
|
for (const marketplace of marketplaces) {
|
||||||
|
await addMarketplace(resolvedExecutable, marketplace);
|
||||||
|
console.log(`✓ Successfully added marketplace: ${marketplace}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("No marketplaces specified, skipping marketplace setup");
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`Installing ${plugins.length} plugin(s)...`);
|
console.log(`Installing ${plugins.length} plugin(s)...`);
|
||||||
|
|
||||||
|
|||||||
@@ -59,23 +59,11 @@ describe("installPlugins", () => {
|
|||||||
const spy = createMockSpawn();
|
const spy = createMockSpawn();
|
||||||
await installPlugins("test-plugin");
|
await installPlugins("test-plugin");
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledTimes(2);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
// First call: add marketplace
|
// Only call: install plugin (no marketplace without explicit marketplace input)
|
||||||
expect(spy).toHaveBeenNthCalledWith(
|
expect(spy).toHaveBeenNthCalledWith(
|
||||||
1,
|
1,
|
||||||
"claude",
|
"claude",
|
||||||
[
|
|
||||||
"plugin",
|
|
||||||
"marketplace",
|
|
||||||
"add",
|
|
||||||
"https://github.com/anthropics/claude-code.git",
|
|
||||||
],
|
|
||||||
{ stdio: "inherit" },
|
|
||||||
);
|
|
||||||
// Second call: install plugin
|
|
||||||
expect(spy).toHaveBeenNthCalledWith(
|
|
||||||
2,
|
|
||||||
"claude",
|
|
||||||
["plugin", "install", "test-plugin"],
|
["plugin", "install", "test-plugin"],
|
||||||
{ stdio: "inherit" },
|
{ stdio: "inherit" },
|
||||||
);
|
);
|
||||||
@@ -85,34 +73,22 @@ describe("installPlugins", () => {
|
|||||||
const spy = createMockSpawn();
|
const spy = createMockSpawn();
|
||||||
await installPlugins("plugin1,plugin2,plugin3");
|
await installPlugins("plugin1,plugin2,plugin3");
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledTimes(4);
|
expect(spy).toHaveBeenCalledTimes(3);
|
||||||
// First call: add marketplace
|
// Install plugins (no marketplace without explicit marketplace input)
|
||||||
expect(spy).toHaveBeenNthCalledWith(
|
expect(spy).toHaveBeenNthCalledWith(
|
||||||
1,
|
1,
|
||||||
"claude",
|
"claude",
|
||||||
[
|
|
||||||
"plugin",
|
|
||||||
"marketplace",
|
|
||||||
"add",
|
|
||||||
"https://github.com/anthropics/claude-code.git",
|
|
||||||
],
|
|
||||||
{ stdio: "inherit" },
|
|
||||||
);
|
|
||||||
// Subsequent calls: install plugins
|
|
||||||
expect(spy).toHaveBeenNthCalledWith(
|
|
||||||
2,
|
|
||||||
"claude",
|
|
||||||
["plugin", "install", "plugin1"],
|
["plugin", "install", "plugin1"],
|
||||||
{ stdio: "inherit" },
|
{ stdio: "inherit" },
|
||||||
);
|
);
|
||||||
expect(spy).toHaveBeenNthCalledWith(
|
expect(spy).toHaveBeenNthCalledWith(
|
||||||
3,
|
2,
|
||||||
"claude",
|
"claude",
|
||||||
["plugin", "install", "plugin2"],
|
["plugin", "install", "plugin2"],
|
||||||
{ stdio: "inherit" },
|
{ stdio: "inherit" },
|
||||||
);
|
);
|
||||||
expect(spy).toHaveBeenNthCalledWith(
|
expect(spy).toHaveBeenNthCalledWith(
|
||||||
4,
|
3,
|
||||||
"claude",
|
"claude",
|
||||||
["plugin", "install", "plugin3"],
|
["plugin", "install", "plugin3"],
|
||||||
{ stdio: "inherit" },
|
{ stdio: "inherit" },
|
||||||
@@ -123,23 +99,11 @@ describe("installPlugins", () => {
|
|||||||
const spy = createMockSpawn();
|
const spy = createMockSpawn();
|
||||||
await installPlugins("test-plugin", "/custom/path/to/claude");
|
await installPlugins("test-plugin", "/custom/path/to/claude");
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledTimes(2);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
// First call: add marketplace
|
// Only call: install plugin (no marketplace without explicit marketplace input)
|
||||||
expect(spy).toHaveBeenNthCalledWith(
|
expect(spy).toHaveBeenNthCalledWith(
|
||||||
1,
|
1,
|
||||||
"/custom/path/to/claude",
|
"/custom/path/to/claude",
|
||||||
[
|
|
||||||
"plugin",
|
|
||||||
"marketplace",
|
|
||||||
"add",
|
|
||||||
"https://github.com/anthropics/claude-code.git",
|
|
||||||
],
|
|
||||||
{ stdio: "inherit" },
|
|
||||||
);
|
|
||||||
// Second call: install plugin
|
|
||||||
expect(spy).toHaveBeenNthCalledWith(
|
|
||||||
2,
|
|
||||||
"/custom/path/to/claude",
|
|
||||||
["plugin", "install", "test-plugin"],
|
["plugin", "install", "test-plugin"],
|
||||||
{ stdio: "inherit" },
|
{ stdio: "inherit" },
|
||||||
);
|
);
|
||||||
@@ -149,27 +113,16 @@ describe("installPlugins", () => {
|
|||||||
const spy = createMockSpawn();
|
const spy = createMockSpawn();
|
||||||
await installPlugins(" plugin1 , plugin2 ");
|
await installPlugins(" plugin1 , plugin2 ");
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledTimes(3);
|
expect(spy).toHaveBeenCalledTimes(2);
|
||||||
// First call: add marketplace
|
// Install plugins (no marketplace without explicit marketplace input)
|
||||||
expect(spy).toHaveBeenNthCalledWith(
|
expect(spy).toHaveBeenNthCalledWith(
|
||||||
1,
|
1,
|
||||||
"claude",
|
"claude",
|
||||||
[
|
|
||||||
"plugin",
|
|
||||||
"marketplace",
|
|
||||||
"add",
|
|
||||||
"https://github.com/anthropics/claude-code.git",
|
|
||||||
],
|
|
||||||
{ stdio: "inherit" },
|
|
||||||
);
|
|
||||||
expect(spy).toHaveBeenNthCalledWith(
|
|
||||||
2,
|
|
||||||
"claude",
|
|
||||||
["plugin", "install", "plugin1"],
|
["plugin", "install", "plugin1"],
|
||||||
{ stdio: "inherit" },
|
{ stdio: "inherit" },
|
||||||
);
|
);
|
||||||
expect(spy).toHaveBeenNthCalledWith(
|
expect(spy).toHaveBeenNthCalledWith(
|
||||||
3,
|
2,
|
||||||
"claude",
|
"claude",
|
||||||
["plugin", "install", "plugin2"],
|
["plugin", "install", "plugin2"],
|
||||||
{ stdio: "inherit" },
|
{ stdio: "inherit" },
|
||||||
@@ -180,27 +133,16 @@ describe("installPlugins", () => {
|
|||||||
const spy = createMockSpawn();
|
const spy = createMockSpawn();
|
||||||
await installPlugins("plugin1,,plugin2");
|
await installPlugins("plugin1,,plugin2");
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledTimes(3);
|
expect(spy).toHaveBeenCalledTimes(2);
|
||||||
// First call: add marketplace
|
// Install plugins (no marketplace without explicit marketplace input)
|
||||||
expect(spy).toHaveBeenNthCalledWith(
|
expect(spy).toHaveBeenNthCalledWith(
|
||||||
1,
|
1,
|
||||||
"claude",
|
"claude",
|
||||||
[
|
|
||||||
"plugin",
|
|
||||||
"marketplace",
|
|
||||||
"add",
|
|
||||||
"https://github.com/anthropics/claude-code.git",
|
|
||||||
],
|
|
||||||
{ stdio: "inherit" },
|
|
||||||
);
|
|
||||||
expect(spy).toHaveBeenNthCalledWith(
|
|
||||||
2,
|
|
||||||
"claude",
|
|
||||||
["plugin", "install", "plugin1"],
|
["plugin", "install", "plugin1"],
|
||||||
{ stdio: "inherit" },
|
{ stdio: "inherit" },
|
||||||
);
|
);
|
||||||
expect(spy).toHaveBeenNthCalledWith(
|
expect(spy).toHaveBeenNthCalledWith(
|
||||||
3,
|
2,
|
||||||
"claude",
|
"claude",
|
||||||
["plugin", "install", "plugin2"],
|
["plugin", "install", "plugin2"],
|
||||||
{ stdio: "inherit" },
|
{ stdio: "inherit" },
|
||||||
@@ -211,7 +153,7 @@ describe("installPlugins", () => {
|
|||||||
createMockSpawn(1, false); // Exit code 1
|
createMockSpawn(1, false); // Exit code 1
|
||||||
|
|
||||||
await expect(installPlugins("failing-plugin")).rejects.toThrow(
|
await expect(installPlugins("failing-plugin")).rejects.toThrow(
|
||||||
"Failed to add marketplace (exit code: 1)",
|
"Failed to install plugin 'failing-plugin' (exit code: 1)",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -219,7 +161,7 @@ describe("installPlugins", () => {
|
|||||||
createMockSpawn(null, false); // Exit code null (terminated by signal)
|
createMockSpawn(null, false); // Exit code null (terminated by signal)
|
||||||
|
|
||||||
await expect(installPlugins("terminated-plugin")).rejects.toThrow(
|
await expect(installPlugins("terminated-plugin")).rejects.toThrow(
|
||||||
"Failed to add marketplace: process terminated by signal",
|
"Failed to install plugin 'terminated-plugin': process terminated by signal",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -227,10 +169,10 @@ describe("installPlugins", () => {
|
|||||||
const spy = createMockSpawn(1, false); // Exit code 1
|
const spy = createMockSpawn(1, false); // Exit code 1
|
||||||
|
|
||||||
await expect(installPlugins("plugin1,plugin2,plugin3")).rejects.toThrow(
|
await expect(installPlugins("plugin1,plugin2,plugin3")).rejects.toThrow(
|
||||||
"Failed to add marketplace (exit code: 1)",
|
"Failed to install plugin 'plugin1' (exit code: 1)",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Should only try to add marketplace before failing
|
// Should only try to install first plugin before failing
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -238,27 +180,16 @@ describe("installPlugins", () => {
|
|||||||
const spy = createMockSpawn();
|
const spy = createMockSpawn();
|
||||||
await installPlugins("org/plugin-name,@scope/plugin");
|
await installPlugins("org/plugin-name,@scope/plugin");
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledTimes(3);
|
expect(spy).toHaveBeenCalledTimes(2);
|
||||||
// First call: add marketplace
|
// Install plugins (no marketplace without explicit marketplace input)
|
||||||
expect(spy).toHaveBeenNthCalledWith(
|
expect(spy).toHaveBeenNthCalledWith(
|
||||||
1,
|
1,
|
||||||
"claude",
|
"claude",
|
||||||
[
|
|
||||||
"plugin",
|
|
||||||
"marketplace",
|
|
||||||
"add",
|
|
||||||
"https://github.com/anthropics/claude-code.git",
|
|
||||||
],
|
|
||||||
{ stdio: "inherit" },
|
|
||||||
);
|
|
||||||
expect(spy).toHaveBeenNthCalledWith(
|
|
||||||
2,
|
|
||||||
"claude",
|
|
||||||
["plugin", "install", "org/plugin-name"],
|
["plugin", "install", "org/plugin-name"],
|
||||||
{ stdio: "inherit" },
|
{ stdio: "inherit" },
|
||||||
);
|
);
|
||||||
expect(spy).toHaveBeenNthCalledWith(
|
expect(spy).toHaveBeenNthCalledWith(
|
||||||
3,
|
2,
|
||||||
"claude",
|
"claude",
|
||||||
["plugin", "install", "@scope/plugin"],
|
["plugin", "install", "@scope/plugin"],
|
||||||
{ stdio: "inherit" },
|
{ stdio: "inherit" },
|
||||||
@@ -269,7 +200,7 @@ describe("installPlugins", () => {
|
|||||||
createMockSpawn(0, true); // Trigger error event
|
createMockSpawn(0, true); // Trigger error event
|
||||||
|
|
||||||
await expect(installPlugins("test-plugin")).rejects.toThrow(
|
await expect(installPlugins("test-plugin")).rejects.toThrow(
|
||||||
"Failed to add marketplace: spawn error",
|
"Failed to install plugin 'test-plugin': spawn error",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -277,27 +208,16 @@ describe("installPlugins", () => {
|
|||||||
const spy = createMockSpawn();
|
const spy = createMockSpawn();
|
||||||
await installPlugins("plugin-a,plugin-b", "/usr/local/bin/claude-custom");
|
await installPlugins("plugin-a,plugin-b", "/usr/local/bin/claude-custom");
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledTimes(3);
|
expect(spy).toHaveBeenCalledTimes(2);
|
||||||
// First call: add marketplace
|
// Install plugins (no marketplace without explicit marketplace input)
|
||||||
expect(spy).toHaveBeenNthCalledWith(
|
expect(spy).toHaveBeenNthCalledWith(
|
||||||
1,
|
1,
|
||||||
"/usr/local/bin/claude-custom",
|
"/usr/local/bin/claude-custom",
|
||||||
[
|
|
||||||
"plugin",
|
|
||||||
"marketplace",
|
|
||||||
"add",
|
|
||||||
"https://github.com/anthropics/claude-code.git",
|
|
||||||
],
|
|
||||||
{ stdio: "inherit" },
|
|
||||||
);
|
|
||||||
expect(spy).toHaveBeenNthCalledWith(
|
|
||||||
2,
|
|
||||||
"/usr/local/bin/claude-custom",
|
|
||||||
["plugin", "install", "plugin-a"],
|
["plugin", "install", "plugin-a"],
|
||||||
{ stdio: "inherit" },
|
{ stdio: "inherit" },
|
||||||
);
|
);
|
||||||
expect(spy).toHaveBeenNthCalledWith(
|
expect(spy).toHaveBeenNthCalledWith(
|
||||||
3,
|
2,
|
||||||
"/usr/local/bin/claude-custom",
|
"/usr/local/bin/claude-custom",
|
||||||
["plugin", "install", "plugin-b"],
|
["plugin", "install", "plugin-b"],
|
||||||
{ stdio: "inherit" },
|
{ stdio: "inherit" },
|
||||||
@@ -360,22 +280,11 @@ describe("installPlugins", () => {
|
|||||||
const spy = createMockSpawn();
|
const spy = createMockSpawn();
|
||||||
await installPlugins("plugin-v1.0.2");
|
await installPlugins("plugin-v1.0.2");
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledTimes(2);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
// First call: add marketplace
|
// Only call: install plugin (no marketplace without explicit marketplace input)
|
||||||
expect(spy).toHaveBeenNthCalledWith(
|
expect(spy).toHaveBeenNthCalledWith(
|
||||||
1,
|
1,
|
||||||
"claude",
|
"claude",
|
||||||
[
|
|
||||||
"plugin",
|
|
||||||
"marketplace",
|
|
||||||
"add",
|
|
||||||
"https://github.com/anthropics/claude-code.git",
|
|
||||||
],
|
|
||||||
{ stdio: "inherit" },
|
|
||||||
);
|
|
||||||
expect(spy).toHaveBeenNthCalledWith(
|
|
||||||
2,
|
|
||||||
"claude",
|
|
||||||
["plugin", "install", "plugin-v1.0.2"],
|
["plugin", "install", "plugin-v1.0.2"],
|
||||||
{ stdio: "inherit" },
|
{ stdio: "inherit" },
|
||||||
);
|
);
|
||||||
@@ -385,22 +294,11 @@ describe("installPlugins", () => {
|
|||||||
const spy = createMockSpawn();
|
const spy = createMockSpawn();
|
||||||
await installPlugins("@scope/plugin-v1.0.0-beta.1");
|
await installPlugins("@scope/plugin-v1.0.0-beta.1");
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledTimes(2);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
// First call: add marketplace
|
// Only call: install plugin (no marketplace without explicit marketplace input)
|
||||||
expect(spy).toHaveBeenNthCalledWith(
|
expect(spy).toHaveBeenNthCalledWith(
|
||||||
1,
|
1,
|
||||||
"claude",
|
"claude",
|
||||||
[
|
|
||||||
"plugin",
|
|
||||||
"marketplace",
|
|
||||||
"add",
|
|
||||||
"https://github.com/anthropics/claude-code.git",
|
|
||||||
],
|
|
||||||
{ stdio: "inherit" },
|
|
||||||
);
|
|
||||||
expect(spy).toHaveBeenNthCalledWith(
|
|
||||||
2,
|
|
||||||
"claude",
|
|
||||||
["plugin", "install", "@scope/plugin-v1.0.0-beta.1"],
|
["plugin", "install", "@scope/plugin-v1.0.0-beta.1"],
|
||||||
{ stdio: "inherit" },
|
{ stdio: "inherit" },
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user