diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 0000000..24f71f8 --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,40 @@ +--- +# yamllint disable rule:truthy rule:line-length +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 1 + + - name: Run Claude Code + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + additional_permissions: | + actions: read diff --git a/package.json b/package.json index 4e3435a..7765836 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,15 @@ ] }, "default": [] + }, + "infrahub-vscode.showInfrahubctlWarnings": { + "type": "boolean", + "default": true, + "description": "Show warnings when infrahubctl is not available in the Python environment." + }, + "infrahub-vscode.infrahubctlPath": { + "type": "string", + "description": "Custom path to infrahubctl executable. If not specified, the extension will look for infrahubctl in the active Python environment." } } }, @@ -158,6 +167,11 @@ "title": "Visualize Schema", "icon": "$(graph)", "category": "Infrahub" + }, + { + "command": "infrahub.showInfrahubctlGuidance", + "title": "Show infrahubctl Installation Guidance", + "category": "Infrahub" } ], "viewsContainers": { diff --git a/src/common/commands.ts b/src/common/commands.ts index cb2fc8d..176d40c 100644 --- a/src/common/commands.ts +++ b/src/common/commands.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { InfrahubYamlTreeItem } from '../treeview/infrahubYamlTreeViewProvider'; import { promptForVariables, searchForConfigSchemaFiles } from '../common/infrahub'; import { BranchCreateInput } from 'infrahub-sdk/dist/graphql/branch'; -import { showError, showInfo, escapeHtml, showConfirm, promptBranchAndRunInfrahubctl, getBranchPrompt, getServerPrompt, getGraphQLResultHtml, runInfrahubctlInTerminal } from '../common/utilities'; +import { showError, showInfo, escapeHtml, showConfirm, promptBranchAndRunInfrahubctl, getBranchPrompt, getServerPrompt, getGraphQLResultHtml, runInfrahubctlInTerminal, checkInfrahubctlBeforeCommand } from '../common/utilities'; import { SchemaVisualizerPanel } from '../webview/SchemaVisualizerPanel'; @@ -231,6 +231,13 @@ export async function newBranchCommand(serverItem: any, provider: { refresh?: () * Finds all config schema files in the workspace and runs infrahubctl schema check on the first base path. */ export async function checkAllSchemaFiles() { + // Check infrahubctl availability first + const canProceed = await checkInfrahubctlBeforeCommand(); + if (!canProceed) { + showInfo('Schema check cancelled.'); + return; + } + const foundFiles = searchForConfigSchemaFiles(); const workspaceFolder = vscode.workspace.workspaceFolders?.[0].uri.fsPath; if (!workspaceFolder) { @@ -251,6 +258,13 @@ export async function checkAllSchemaFiles() { * Finds all config schema files in the workspace and runs infrahubctl schema load on the first base path. */ export async function loadAllSchemaFiles() { + // Check infrahubctl availability first + const canProceed = await checkInfrahubctlBeforeCommand(); + if (!canProceed) { + showInfo('Schema load cancelled.'); + return; + } + const foundFiles = searchForConfigSchemaFiles(); const workspaceFolder = vscode.workspace.workspaceFolders?.[0].uri.fsPath; if (!workspaceFolder) { @@ -271,6 +285,13 @@ export async function loadAllSchemaFiles() { * Runs infrahubctl schema check on a specific file. */ export async function checkSchemaFile(filePath: string) { + // Check infrahubctl availability first + const canProceed = await checkInfrahubctlBeforeCommand(); + if (!canProceed) { + showInfo('Schema check cancelled.'); + return; + } + await promptBranchAndRunInfrahubctl('check', filePath); } @@ -278,6 +299,13 @@ export async function checkSchemaFile(filePath: string) { * Runs infrahubctl schema load on a specific file. */ export async function loadSchemaFile(filePath: string) { + // Check infrahubctl availability first + const canProceed = await checkInfrahubctlBeforeCommand(); + if (!canProceed) { + showInfo('Schema load cancelled.'); + return; + } + await promptBranchAndRunInfrahubctl('load', filePath); } @@ -291,6 +319,13 @@ export async function runTransformCommand(item: InfrahubYamlTreeItem): Promise { + const now = Date.now(); + if (this.cachedResult && (now - this.lastCheckTime) < this.CACHE_DURATION) { + return this.cachedResult; + } + + try { + const infrahubctlPath = await this.getInfrahubctlPath(); + if (infrahubctlPath && await this.fileExists(infrahubctlPath)) { + this.cachedResult = { + isAvailable: true, + path: infrahubctlPath, + pythonEnvironment: await this.getPythonEnvironmentInfo() + }; + } else { + this.cachedResult = { + isAvailable: false, + errorMessage: 'infrahubctl not found in Python environment', + pythonEnvironment: await this.getPythonEnvironmentInfo() + }; + } + } catch (error) { + this.cachedResult = { + isAvailable: false, + errorMessage: `Failed to check infrahubctl: ${error instanceof Error ? error.message : 'Unknown error'}`, + pythonEnvironment: await this.getPythonEnvironmentInfo() + }; + } + + this.lastCheckTime = now; + return this.cachedResult; + } + + /** + * Gets the resolved path to infrahubctl in the current Python environment. + * Returns null if not found or if Python extension is not available. + */ + public async getInfrahubctlPath(): Promise { + try { + // Check if custom path is configured + const config = vscode.workspace.getConfiguration('infrahub-vscode'); + const customPath = config.get('infrahubctlPath'); + if (customPath) { + return customPath; + } + + // Get active Python environment and resolve infrahubctl path (same logic as utilities.ts) + const pythonApi: PythonExtension = await PythonExtension.api(); + const environmentPathObj = pythonApi.environments.getActiveEnvironmentPath(); + const pythonPath = environmentPathObj?.path || environmentPathObj?.id || ''; + + if (!pythonPath) { + return 'infrahubctl'; // Fallback to system PATH + } + + const infrahubctlPath = path.join(path.dirname(pythonPath), 'infrahubctl'); + return infrahubctlPath; + } catch (error) { + console.warn('Failed to resolve infrahubctl path:', error); + return 'infrahubctl'; // Fallback to system PATH + } + } + + /** + * Gets installation guidance for infrahubctl with environment context. + */ + public async getInstallationGuidance(): Promise { + const pythonEnv = await this.getPythonEnvironmentInfo(); + const envInfo = pythonEnv ? `Current Python environment: ${pythonEnv}` : 'Python environment: Not detected'; + + return `infrahubctl is required for schema validation and transform operations. + +${envInfo} + +Installation options: +1. Install via uv: uv add "infrahub-sdk[all]" +2. Visit: https://docs.infrahub.app/python-sdk/guides/installation for detailed instructions + +After installation, restart VS Code to refresh the extension.`; + } + + /** + * Invalidates the cached result to force a fresh check. + */ + public invalidateCache(): void { + this.cachedResult = null; + this.lastCheckTime = 0; + } + + /** + * Gets information about the current Python environment. + */ + private async getPythonEnvironmentInfo(): Promise { + try { + const pythonApi: PythonExtension = await PythonExtension.api(); + const environmentPathObj = pythonApi.environments.getActiveEnvironmentPath(); + const pythonPath = environmentPathObj?.path || environmentPathObj?.id; + + if (pythonPath) { + return path.dirname(pythonPath); + } + } catch (error) { + console.warn('Failed to get Python environment info:', error); + } + return undefined; + } + + /** + * Checks if a file exists (cross-platform). + */ + private async fileExists(filePath: string): Promise { + try { + await fs.promises.access(filePath, fs.constants.F_OK); + return true; + } catch { + // Try with .exe extension on Windows + if (process.platform === 'win32' && !filePath.endsWith('.exe')) { + try { + await fs.promises.access(`${filePath}.exe`, fs.constants.F_OK); + return true; + } catch { + return false; + } + } + return false; + } + } +} \ No newline at end of file diff --git a/src/common/utilities.ts b/src/common/utilities.ts index f39af31..c411bea 100644 --- a/src/common/utilities.ts +++ b/src/common/utilities.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import { PythonExtension } from '@vscode/python-extension'; import { InfrahubClient, InfrahubClientOptions } from 'infrahub-sdk'; import * as path from 'path'; +import { InfrahubctlChecker } from './infrahubctlChecker'; /** * Checks if a file exists at the given URI using the VS Code workspace API. @@ -128,7 +129,29 @@ export async function runInfrahubctlInTerminal( vscode.window.showInformationMessage(notification); } } catch (error) { - vscode.window.showErrorMessage('Failed to run infrahubctl command in terminal.'); + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + + // Check if this might be an infrahubctl not found error + if (errorMessage.includes('not found') || errorMessage.includes('command not found')) { + const checker = new InfrahubctlChecker(); + const guidance = await checker.getInstallationGuidance(); + + const installAction = 'Installation Guide'; + const result = await vscode.window.showErrorMessage( + `Failed to run infrahubctl command: ${errorMessage} + +This usually means infrahubctl is not installed or not available in your Python environment.`, + installAction, + 'Dismiss' + ); + + if (result === installAction) { + vscode.window.showInformationMessage(guidance, { modal: true }); + } + } else { + vscode.window.showErrorMessage(`Failed to run infrahubctl command in terminal: ${errorMessage}`); + } + console.error('Terminal error:', error); } } @@ -235,6 +258,65 @@ export async function getServerPrompt(): Promise }; } +/** + * Checks if infrahubctl is available before running commands. + * Shows a warning dialog with installation guidance if not available. + * Returns true if available or user chooses to continue anyway. + */ +export async function checkInfrahubctlBeforeCommand(): Promise { + const config = vscode.workspace.getConfiguration('infrahub-vscode'); + const showWarnings = config.get('showInfrahubctlWarnings', true); + + if (!showWarnings) { + return true; // Skip check if warnings are disabled + } + + const checker = new InfrahubctlChecker(); + const result = await checker.checkInfrahubctlAvailability(); + + if (result.isAvailable) { + return true; + } + + // Show warning dialog + const guidance = await checker.getInstallationGuidance(); + const installAction = 'Install Guide'; + const continueAction = 'Continue Anyway'; + const cancelAction = 'Cancel'; + + const choice = await vscode.window.showWarningMessage( + `infrahubctl is required for this operation but was not found. + +${result.errorMessage || 'Not found in Python environment'} + +This command may fail without infrahubctl installed.`, + { modal: true }, + installAction, + continueAction, + cancelAction + ); + + if (choice === installAction) { + // Show detailed guidance + const detailChoice = await vscode.window.showInformationMessage( + guidance, + { modal: true }, + 'Open Documentation', + 'Continue Anyway', + 'Cancel' + ); + + if (detailChoice === 'Open Documentation') { + vscode.env.openExternal(vscode.Uri.parse('https://docs.infrahub.app/python-sdk/guides/installation')); + return false; // Don't continue + } + + return detailChoice === 'Continue Anyway'; + } + + return choice === continueAction; +} + /** * Returns HTML for displaying GraphQL query results and variables. */ diff --git a/src/extension.ts b/src/extension.ts index 0bb968e..3834e6b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -14,7 +14,10 @@ import { openFileAtLocation, searchForConfigSchemaFiles } from './common/infrahu import { InfrahubClient, InfrahubClientOptions } from 'infrahub-sdk'; import { executeInfrahubGraphQLQuery, checkAllSchemaFiles, loadAllSchemaFiles, checkSchemaFile, loadSchemaFile, runTransformCommand, visualizeSchemaCommand } from './common/commands'; import { newBranchCommand, deleteBranchCommand } from './common/commands'; +import { InfrahubctlChecker } from './common/infrahubctlChecker'; let statusBar: vscode.StatusBarItem; +let infrahubctlStatusBar: vscode.StatusBarItem; +let infrahubctlChecker: InfrahubctlChecker; // Store the original NODE_TLS_REJECT_UNAUTHORIZED value to restore it later let originalTlsRejectUnauthorized: string | undefined; @@ -29,6 +32,12 @@ export function activate(context: vscode.ExtensionContext) { // Set NODE_TLS_REJECT_UNAUTHORIZED based on server configuration updateTlsEnvironment(); + // Initialize infrahubctl checker + infrahubctlChecker = new InfrahubctlChecker(); + + // Check infrahubctl availability and store in extension context + checkAndStoreInfrahubctlAvailability(context); + const schemaDirectory = vscode.workspace.getConfiguration().get('infrahub-vscode.schemaDirectory', ''); // =============================================== // Register the definition provider for yaml files @@ -160,6 +169,22 @@ export function activate(context: vscode.ExtensionContext) { await visualizeSchemaCommand(context.extensionUri, serverItem); }), ); + context.subscriptions.push( + vscode.commands.registerCommand('infrahub.showInfrahubctlGuidance', async () => { + const guidance = await infrahubctlChecker.getInstallationGuidance(); + const installAction = 'View Installation Guide'; + const dismissAction = 'Dismiss'; + const result = await vscode.window.showWarningMessage( + 'infrahubctl is not available in your Python environment', + { modal: false }, + installAction, + dismissAction + ); + if (result === installAction) { + vscode.env.openExternal(vscode.Uri.parse('https://docs.infrahub.app/python-sdk/guides/installation')); + } + }), + ); // =============================================== // Status Bar @@ -170,6 +195,13 @@ export function activate(context: vscode.ExtensionContext) { statusBar.show(); setInterval(() => updateServerInfo(), 10000); + // Create infrahubctl status bar + infrahubctlStatusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 99); + context.subscriptions.push(infrahubctlStatusBar); + updateInfrahubctlStatus(); + // Re-check infrahubctl status every 10 seconds (aligns with cache duration) + setInterval(() => updateInfrahubctlStatus(), 10000); + // Listen for configuration changes to update TLS environment context.subscriptions.push( vscode.workspace.onDidChangeConfiguration(e => { @@ -229,6 +261,61 @@ async function updateServerInfo(): Promise { } } +/** + * Checks infrahubctl availability and stores the result in extension context. + */ +async function checkAndStoreInfrahubctlAvailability(context: vscode.ExtensionContext): Promise { + try { + const result = await infrahubctlChecker.checkInfrahubctlAvailability(); + context.globalState.update('infrahubctlAvailable', result.isAvailable); + context.globalState.update('infrahubctlCheckResult', result); + console.log('Infrahubctl check result:', result); + } catch (error) { + console.error('Failed to check infrahubctl availability:', error); + context.globalState.update('infrahubctlAvailable', false); + } +} + +/** + * Updates the infrahubctl status bar based on availability. + */ +async function updateInfrahubctlStatus(): Promise { + const config = vscode.workspace.getConfiguration('infrahub-vscode'); + const showWarnings = config.get('showInfrahubctlWarnings', true); + + if (!showWarnings) { + infrahubctlStatusBar.hide(); + return; + } + + try { + const result = await infrahubctlChecker.checkInfrahubctlAvailability(); + + if (!result.isAvailable) { + infrahubctlStatusBar.text = '$(warning) infrahubctl missing'; + infrahubctlStatusBar.tooltip = `infrahubctl not found\n${result.errorMessage || ''}\n\nClick for installation guidance`; + infrahubctlStatusBar.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); + infrahubctlStatusBar.command = 'infrahub.showInfrahubctlGuidance'; + infrahubctlStatusBar.show(); + } else { + infrahubctlStatusBar.hide(); + } + } catch (error) { + console.error('Failed to update infrahubctl status:', error); + infrahubctlStatusBar.text = '$(warning) infrahubctl check failed'; + infrahubctlStatusBar.tooltip = 'Failed to check infrahubctl availability'; + infrahubctlStatusBar.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground'); + infrahubctlStatusBar.show(); + } +} + +/** + * Gets the global infrahubctl checker instance. + */ +export function getInfrahubctlChecker(): InfrahubctlChecker { + return infrahubctlChecker; +} + // This method is called when your extension is deactivated export function deactivate() { // Restore the original NODE_TLS_REJECT_UNAUTHORIZED value