Skip to content

Commit 2221eb1

Browse files
Merge pull request #47 from codacy/install-cli
feat: add install CLI functionality
2 parents e772229 + de15484 commit 2221eb1

File tree

5 files changed

+166
-1
lines changed

5 files changed

+166
-1
lines changed

package.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@
3939
"icon": "resources/icons/codacy-logo.svg",
4040
"when": "Codacy:RepositoryManagerStateContext == NeedsAuthentication"
4141
},
42+
{
43+
"id": "codacy:cli",
44+
"name": "Codacy CLI",
45+
"when": "!codacy:cliInstalled && codacy:canInstallCLI",
46+
"icon": "$(gear)",
47+
"initialSize": 2
48+
},
4249
{
4350
"id": "codacy:prSummary",
4451
"name": "Pull request",
@@ -82,6 +89,16 @@
8289
"contents": "You have not yet signed in with Codacy\n[Sign in](command:codacy.signIn)",
8390
"when": "Codacy:RepositoryManagerStateContext == NeedsAuthentication"
8491
},
92+
{
93+
"view": "codacy:cli",
94+
"contents": "[Install Codacy CLI](command:codacy.installCLI)",
95+
"when": "!codacy:cliInstalled && codacy:canInstallCLI && !codacy:cliInstalling"
96+
},
97+
{
98+
"view": "codacy:cli",
99+
"contents": "$(loading~spin) Installing Codacy CLI...",
100+
"when": "!codacy:cliInstalled && codacy:canInstallCLI && codacy:cliInstalling"
101+
},
85102
{
86103
"view": "codacy:statuses",
87104
"contents": "No repositories open.",
@@ -200,6 +217,12 @@
200217
"title": "Reset Codacy MCP Server",
201218
"category": "Codacy commands",
202219
"when": "Codacy:RepositoryManagerStateContext == Loaded && codacy:supportsMCP && codacy:mcpConfigured"
220+
},
221+
{
222+
"command": "codacy.installCLI",
223+
"title": "Install Codacy CLI",
224+
"category": "Codacy commands",
225+
"when": "!codacy:cliInstalled && codacy:canInstallCLI"
203226
}
204227
],
205228
"menus": {
@@ -223,6 +246,10 @@
223246
{
224247
"command": "codacy.configureMCP.reset",
225248
"when": "Codacy:RepositoryManagerStateContext == Loaded && codacy:supportsMCP && codacy:mcpConfigured"
249+
},
250+
{
251+
"command": "codacy.installCLI",
252+
"when": "!codacy:cliInstalled && codacy:canInstallCLI"
226253
}
227254
],
228255
"view/title": [

src/commands/configureMCP.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as os from 'os'
55
import { Config } from '../common/config'
66
import { get, set } from 'lodash'
77
import { Repository } from '../api/client'
8+
import { installCodacyCLI } from './installAnalysisCLI'
89

910
interface Rule {
1011
when: string
@@ -269,6 +270,7 @@ export async function configureMCP(repository: Repository) {
269270

270271
vscode.window.showInformationMessage('Codacy MCP server added successfully')
271272
await createRules(repository)
273+
await installCodacyCLI(repository)
272274
} catch (error: unknown) {
273275
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
274276
vscode.window.showErrorMessage(`Failed to configure MCP server: ${errorMessage}`)

src/commands/installAnalysisCLI.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import * as os from 'os'
2+
import * as vscode from 'vscode'
3+
import * as fs from 'fs'
4+
import * as path from 'path'
5+
import { exec } from 'child_process'
6+
import { promisify } from 'util'
7+
import { Config } from '../common/config'
8+
import { Repository } from '../api/client'
9+
10+
const execAsync = promisify(exec)
11+
12+
export async function isCLIInstalled(): Promise<boolean> {
13+
try {
14+
await execAsync('codacy-cli --help')
15+
return true
16+
} catch {
17+
return false
18+
}
19+
}
20+
21+
async function isBrewInstalled(): Promise<boolean> {
22+
try {
23+
await execAsync('brew --version')
24+
return true
25+
} catch {
26+
return false
27+
}
28+
}
29+
30+
async function initializeCLI(repository: Repository): Promise<void> {
31+
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || ''
32+
const codacyYamlPath = path.join(workspacePath, '.codacy', 'codacy.yaml')
33+
const apiToken = Config.apiToken
34+
35+
const { provider, owner: organization, name: repositoryName } = repository
36+
37+
try {
38+
if (!fs.existsSync(codacyYamlPath)) {
39+
await execAsync(
40+
`codacy-cli init --api-token ${apiToken} --provider ${provider} --organization ${organization} --repository ${repositoryName}`
41+
)
42+
}
43+
await execAsync('codacy-cli install')
44+
} catch (error) {
45+
if (error instanceof Error) {
46+
throw new Error(`Failed to initialize Codacy CLI: ${error.message}`)
47+
}
48+
throw error
49+
}
50+
}
51+
52+
export async function installCodacyCLI(repository: Repository): Promise<void> {
53+
const platform = os.platform()
54+
55+
if (await isCLIInstalled()) {
56+
await initializeCLI(repository)
57+
return
58+
}
59+
60+
try {
61+
switch (platform) {
62+
case 'darwin':
63+
if (!(await isBrewInstalled())) {
64+
throw new Error('Please install Homebrew first and then try installing the Codacy CLI again.')
65+
}
66+
await execAsync('brew install codacy/codacy-cli-v2/codacy-cli-v2')
67+
break
68+
69+
case 'linux':
70+
throw new Error(
71+
'Codacy CLI cannot be automatically installed on Linux yet. For manual installation, please refer to the [Codacy CLI documentation](https://github.com/codacy/codacy-cli-v2).'
72+
)
73+
break
74+
75+
case 'win32':
76+
throw new Error('Codacy CLI is not supported on Windows yet.')
77+
78+
default:
79+
throw new Error(`Unsupported operating system: ${platform}`)
80+
}
81+
82+
await initializeCLI(repository)
83+
} catch (error) {
84+
if (error instanceof Error) {
85+
throw new Error(`Failed to install Codacy CLI: ${error.message}`)
86+
}
87+
throw error
88+
}
89+
}

src/extension.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as vscode from 'vscode'
2+
import * as os from 'os'
23
import { CommandType, wrapCommandWithCatch } from './common/utils'
34
import Logger from './common/logger'
45
import { initializeApi } from './api'
@@ -18,6 +19,7 @@ import Telemetry from './common/telemetry'
1819
import { decorateWithCoverage } from './views/coverage'
1920
import { APIState, Repository as GitRepository } from './git/git'
2021
import { configureMCP, createRules, isMCPConfigured } from './commands/configureMCP'
22+
import { installCodacyCLI, isCLIInstalled } from './commands/installAnalysisCLI'
2123

2224
/**
2325
* Helper function to register all extension commands
@@ -96,6 +98,8 @@ export async function activate(context: vscode.ExtensionContext) {
9698
(vscode.env.appName.toLowerCase().includes('code') && !!vscode.extensions.getExtension('GitHub.copilot'))
9799
)
98100

101+
await vscode.commands.executeCommand('setContext', 'codacy:canInstallCLI', os.platform() === 'darwin')
102+
99103
Config.init(context)
100104

101105
initializeApi()
@@ -180,6 +184,46 @@ export async function activate(context: vscode.ExtensionContext) {
180184
item.onClick()
181185
})
182186

187+
// Register CLI installation commands
188+
const updateCLIState = async () => {
189+
const isInstalled = await isCLIInstalled()
190+
vscode.commands.executeCommand('setContext', 'codacy:cliInstalled', isInstalled)
191+
}
192+
193+
await updateCLIState()
194+
195+
context.subscriptions.push(
196+
vscode.commands.registerCommand('codacy.installCLI', async () => {
197+
await vscode.commands.executeCommand('setContext', 'codacy:cliInstalling', true)
198+
199+
await vscode.window.withProgress(
200+
{
201+
location: vscode.ProgressLocation.Window,
202+
title: 'Installing Codacy CLI',
203+
cancellable: false,
204+
},
205+
async () => {
206+
try {
207+
const repository = repositoryManager.repository
208+
if (repository) {
209+
await installCodacyCLI(repository)
210+
await updateCLIState()
211+
vscode.window.showInformationMessage('Codacy CLI installed successfully!')
212+
} else {
213+
throw new Error('No repository found')
214+
}
215+
} catch (error) {
216+
vscode.window.showErrorMessage(
217+
`Failed to install Codacy CLI: ${error instanceof Error ? error.message : 'Unknown error'}`
218+
)
219+
} finally {
220+
await vscode.commands.executeCommand('setContext', 'codacy:cliInstalling', false)
221+
}
222+
}
223+
)
224+
})
225+
)
226+
183227
// Register MCP commands
184228
const updateMCPState = () => {
185229
const isConfigured = isMCPConfigured()

src/views/ProblemsDiagnosticCollection.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { BranchIssue } from '../git/IssuesManager'
77
import { CommitIssue } from '../api/client'
88
import { ProcessedSarifResult, runCodacyAnalyze } from '../commands/runCodacyAnalyze'
99
import * as path from 'path'
10+
import { isCLIInstalled } from '../commands/installAnalysisCLI'
1011
// import * as os from 'os'
1112

1213
const patternSeverityToDiagnosticSeverity = (severity: 'Info' | 'Warning' | 'Error'): vscode.DiagnosticSeverity => {
@@ -169,7 +170,9 @@ export class ProblemsDiagnosticCollection implements vscode.Disposable {
169170
const codacyConfigExists =
170171
(await vscode.workspace.fs.stat(vscode.Uri.file(codacyConfigPath)).then(() => true)) || false
171172

172-
if (!codacyConfigExists) return
173+
const isCliInstalled = await isCLIInstalled()
174+
175+
if (!isCliInstalled || !codacyConfigExists) return
173176

174177
let pathToFile = document.uri.fsPath
175178

0 commit comments

Comments
 (0)