diff --git a/index.ts b/index.ts index b708aa3..afa23bc 100644 --- a/index.ts +++ b/index.ts @@ -26,6 +26,8 @@ import { getPatternTool, getIssueTool, getRepositoryPullRequestTool, + installCLITool, + cliAnalysisTool, listOrganizationsTool, } from './src/tools/index.js'; import { @@ -47,6 +49,8 @@ import { listRepositoryToolsHandler, listToolsHandler, getPatternHandler, + cliAnalysisHandler, + installCliHandler, listOrganizationsHandler, } from './src/handlers/index.js'; import { validateOrganization } from './src/middleware/validation.js'; @@ -115,7 +119,10 @@ type toolKeys = | 'codacy_get_file_with_analysis' | 'codacy_get_repository_pull_request' | 'codacy_get_issue' - | 'codacy_get_pattern'; + | 'codacy_get_pattern' + | 'codacy_install_cli' + | 'codacy_cli_analysis'; + interface ToolDefinition { tool: Tool; handler: (args: any) => Promise; @@ -182,6 +189,14 @@ const toolDefinitions: { [key in toolKeys]: ToolDefinition } = { tool: listRepositoryToolsTool, handler: listRepositoryToolsHandler, }, + codacy_install_cli: { + tool: installCLITool, + handler: installCliHandler, + }, + codacy_cli_analysis: { + tool: cliAnalysisTool, + handler: cliAnalysisHandler, + }, codacy_list_organization: { tool: listOrganizationsTool, handler: listOrganizationsHandler, diff --git a/src/handlers/cliAnalysis.ts b/src/handlers/cliAnalysis.ts new file mode 100644 index 0000000..34d4a83 --- /dev/null +++ b/src/handlers/cliAnalysis.ts @@ -0,0 +1,27 @@ +import { exec } from 'child_process'; +import { getCodacyCliPath } from './installCLI.js'; + +export async function cliAnalysisHandler(args: { + tool: string; + format: string; + output: string; +}): Promise<{ + message: string; +}> { + const codacyCliPath = await getCodacyCliPath(); + + return new Promise((resolve, reject) => { + const command = `${codacyCliPath} analyze --tool ${args.tool} --format ${args.format} -o ${args.output}`; + + exec(command, (err, stdout) => { + if (err) { + console.error(`Analysis error: ${err}`); + reject({ message: `Analysis failed: ${err.message}` }); + return; + } + + console.log(`Analysis completed: ${stdout}`); + resolve({ message: `Analysis completed successfully. Output saved to ${args.output}` }); + }); + }); +} diff --git a/src/handlers/index.ts b/src/handlers/index.ts index ff14e02..6e9292b 100644 --- a/src/handlers/index.ts +++ b/src/handlers/index.ts @@ -4,4 +4,6 @@ export * from './analysis.js'; export * from './organization.js'; export * from './security.js'; export * from './tools.js'; +export * from './installCLI.js'; +export * from './cliAnalysis.js'; export * from './account.js'; diff --git a/src/handlers/installCLI.ts b/src/handlers/installCLI.ts new file mode 100644 index 0000000..cc7ee08 --- /dev/null +++ b/src/handlers/installCLI.ts @@ -0,0 +1,138 @@ +import { exec } from 'child_process'; +import https from 'https'; +import os from 'os'; +import path from 'node:path'; +import { promisify } from 'node:util'; +import * as fs from 'node:fs'; + +const MAC_OS_PATH = path.join(os.homedir(), 'Library/Caches/Codacy/codacy-cli-v2/'); +const LINUX_PATH = path.join(os.homedir(), '.cache/Codacy/codacy-cli-v2/'); +const GITHUB_LATEST_RELEASE_URL = + 'https://api.github.com/repos/codacy/codacy-cli-v2/releases/latest'; +const GITHUB_INSTALL_SCRIPT_URL = + 'https://raw.githubusercontent.com/codacy/codacy-cli-v2/main/codacy-cli.sh'; + +export const installCliHandler = async (): Promise<{ message: string }> => { + const isCliInstalled = await isCodacyCliInstalled(); + const isConfigPresent = isCodacyConfigPresent(); + + if (!isCliInstalled) { + const downloadSuccessful = await downloadCliTool(); + if (!downloadSuccessful) { + return { + message: 'Failed to download Codacy CLI', + }; + } + } + + const cliPath = await getCodacyCliPath(); + + if (isConfigPresent) { + return { + message: 'Codacy CLI is already installed and configured', + }; + } + + const initSuccessful = await execPromise(`${cliPath} init`); + if (!initSuccessful) { + return { + message: 'Failed to initialize Codacy CLI', + }; + } + + const installSuccessful = await execPromise(`${cliPath} install`); + if (!installSuccessful) { + return { + message: 'Failed to install Codacy CLI', + }; + } + return { + message: 'Codacy CLI installed successfully', + }; +}; + +export const getCodacyCliPath: () => Promise = async () => { + const latestReleaseTag = await getLatestReleaseTag(); + + if (os.platform() === 'darwin') { + return path.join(MAC_OS_PATH, latestReleaseTag, 'codacy-cli-v2'); + } else if (os.platform() === 'linux') { + return path.join(LINUX_PATH, latestReleaseTag, 'codacy-cli-v2'); + } else { + throw new Error('Unsupported OS'); + } +}; + +const getLatestReleaseTag = (): Promise => { + return new Promise((resolve, reject) => { + https + .get( + GITHUB_LATEST_RELEASE_URL, + { + headers: { 'User-Agent': 'node.js' }, + }, + res => { + let data = ''; + + res.on('data', chunk => { + data += chunk; + }); + + res.on('end', () => { + try { + const json = JSON.parse(data); + const tagName = json.tag_name; + resolve(tagName); + } catch (error) { + reject(new Error('Failed to parse response')); + } + }); + } + ) + .on('error', error => { + reject(new Error(`Request failed: ${error.message}`)); + }); + }); +}; + +const execAsync = promisify(exec); + +const execPromise = async (command: string): Promise => { + try { + await execAsync(command); + return true; + } catch (error) { + console.error(`Error executing command: ${command}, reason: ${error}`); + return false; + } +}; + +const downloadCliTool = (): Promise => { + return new Promise((resolve, _reject) => { + exec( + `bash <(curl -Ls ${GITHUB_INSTALL_SCRIPT_URL})`, + { shell: '/bin/bash' }, + (error, _stdout, _stderr) => { + if (error == null) { + resolve(true); + return; + } else { + resolve(false); + return; + } + } + ); + }); +}; + +const isCodacyCliInstalled: () => Promise = async () => { + const codacyCliPath = await getCodacyCliPath(); + return fs.existsSync(codacyCliPath); +}; + +const isCodacyConfigPresent = () => { + return ( + fs.existsSync(path.join(process.cwd(), '.codacy', 'codacy.yml')) || + fs.existsSync(path.join(process.cwd(), '.codacy', 'codacy.yaml')) + ); +}; diff --git a/src/tools/CliAnalysisTool.ts b/src/tools/CliAnalysisTool.ts new file mode 100644 index 0000000..85b2967 --- /dev/null +++ b/src/tools/CliAnalysisTool.ts @@ -0,0 +1,26 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js'; + +export const cliAnalysisTool: Tool = { + name: 'codacy_cli_analysis', + description: 'Run analysis using Codacy CLI', + inputSchema: { + type: 'object', + properties: { + tool: { + type: 'string', + description: 'Tool to use for analysis (e.g., eslint)', + default: 'eslint', + }, + format: { + type: 'string', + description: 'Output format (e.g., sarif)', + default: 'sarif', + }, + output: { + type: 'string', + description: 'Output file path', + default: 'results.sarif', + }, + }, + }, +}; diff --git a/src/tools/index.ts b/src/tools/index.ts index 618d349..4d8119d 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -16,4 +16,6 @@ export * from './getRepositoryPullRequestTool.js'; export * from './listRepositoryToolPatternsTool.js'; export * from './listRepositoryToolsTool.js'; export * from './listToolsTool.js'; +export * from './installCLITool.js'; +export * from './cliAnalysisTool.js'; export * from './listOrganizationsTool.js'; diff --git a/src/tools/installCLITool.ts b/src/tools/installCLITool.ts new file mode 100644 index 0000000..5784a78 --- /dev/null +++ b/src/tools/installCLITool.ts @@ -0,0 +1,15 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js'; + +export const installCLITool: Tool = { + name: 'codacy_install_cli', + description: 'Install and configure the Codacy CLI', + inputSchema: { + type: 'object', + properties: { + token: { + type: 'string', + description: 'The Codacy account token', + }, + }, + }, +};