From c8eaeea6ea78c0b8f46dd6cb589010700af11772 Mon Sep 17 00:00:00 2001 From: Test User Date: Mon, 19 Jan 2026 13:26:48 +0100 Subject: [PATCH] feat(frontend): add methodology installer and compatibility checking - Add methodology installer service for downloading and installing methodology packages - Add methodology IPC handlers for frontend-backend communication - Add methodology API module in preload for renderer access - Add useMethodologyCompatibility hook for checking project compatibility - Add MethodologyWarningDialog component for user notifications - Add methodology sources configuration for BMAD and other methodologies - Update AddProjectModal to support methodology selection - Update OnboardingWizard to include methodology setup - Extend useMethodologies hook with installation and verification logic - Add i18n translations for methodology-related UI text - Add methodology types and IPC channel definitions Co-Authored-By: Claude Opus 4.5 --- apps/backend/methodologies/sources.py | 171 +++++++++ .../main/ipc-handlers/claude-code-handlers.ts | 11 +- apps/frontend/src/main/ipc-handlers/index.ts | 7 +- .../main/ipc-handlers/methodology-handlers.ts | 144 ++++++++ .../main/ipc-handlers/settings-handlers.ts | 45 +++ apps/frontend/src/main/methodology/index.ts | 8 + .../src/main/methodology/installer.ts | 344 ++++++++++++++++++ apps/frontend/src/preload/api/index.ts | 11 +- .../preload/api/modules/methodology-api.ts | 79 ++++ .../src/preload/api/modules/shell-api.ts | 6 +- apps/frontend/src/renderer/App.tsx | 53 ++- .../renderer/components/AddProjectModal.tsx | 31 +- .../components/MethodologyWarningDialog.tsx | 90 +++++ .../onboarding/OnboardingWizard.tsx | 15 +- .../components/task-form/useMethodologies.ts | 157 +++++++- .../hooks/useMethodologyCompatibility.ts | 187 ++++++++++ .../frontend/src/renderer/lib/browser-mock.ts | 45 ++- .../renderer/lib/mocks/infrastructure-mock.ts | 5 + apps/frontend/src/shared/constants/index.ts | 3 + apps/frontend/src/shared/constants/ipc.ts | 11 +- .../shared/constants/methodology-sources.ts | 121 ++++++ .../src/shared/i18n/locales/en/tasks.json | 7 + .../src/shared/i18n/locales/fr/tasks.json | 7 + apps/frontend/src/shared/types/ipc.ts | 22 ++ apps/frontend/src/shared/types/methodology.ts | 61 ++++ apps/frontend/src/shared/types/project.ts | 2 + 26 files changed, 1612 insertions(+), 31 deletions(-) create mode 100644 apps/backend/methodologies/sources.py create mode 100644 apps/frontend/src/main/ipc-handlers/methodology-handlers.ts create mode 100644 apps/frontend/src/main/methodology/index.ts create mode 100644 apps/frontend/src/main/methodology/installer.ts create mode 100644 apps/frontend/src/preload/api/modules/methodology-api.ts create mode 100644 apps/frontend/src/renderer/components/MethodologyWarningDialog.tsx create mode 100644 apps/frontend/src/renderer/hooks/useMethodologyCompatibility.ts create mode 100644 apps/frontend/src/shared/constants/methodology-sources.ts diff --git a/apps/backend/methodologies/sources.py b/apps/backend/methodologies/sources.py new file mode 100644 index 00000000..e834524d --- /dev/null +++ b/apps/backend/methodologies/sources.py @@ -0,0 +1,171 @@ +"""Methodology plugin source handlers for multiple distribution types. + +This module defines the source types and verification statuses for methodology plugins, +enabling a flexible multi-source plugin distribution system. + +Plugin Classification: +- Native: The built-in AutoClaude methodology (not a plugin, always available) +- Verified: Plugins maintained and tested by Auto Claude team +- Community: Third-party/user-created plugins without verification guarantee + +Distribution Sources: +- NATIVE: Built-in, no installation required +- NPM: npm/pnpm/yarn packages +- GITHUB: GitHub releases or repositories +- LOCAL: Manual installation to plugins directory + +Architecture Source: This implements the methodology plugin system design +with project-level settings and multi-source versioning. +""" + +from dataclasses import dataclass +from enum import Enum + + +class SourceType(Enum): + """Distribution source types for methodology plugins.""" + + NATIVE = "native" # The built-in AutoClaude methodology (not a plugin) + NPM = "npm" # npm/pnpm/yarn packages + GITHUB = "github" # GitHub releases or repos + LOCAL = "local" # Manual copy to plugins directory + + +class VerificationStatus(Enum): + """Verification status for methodology plugins.""" + + NATIVE = "native" # The AutoClaude methodology itself + VERIFIED = "verified" # Maintained and tested by Auto Claude team + COMMUNITY = "community" # Third-party, no verification + + +@dataclass +class MethodologySource: + """Configuration for a methodology's distribution source. + + Attributes: + type: Distribution source type (native, npm, github, local) + verification: Verification status (native, verified, community) + package_name: Package identifier (NPM: package name, GitHub: owner/repo) + install_command: Override default install command (optional) + version_command: Command to check installed version (optional) + min_version: Minimum supported version for verified plugins + max_version: Maximum supported version (exclusive, optional) + """ + + type: SourceType + verification: VerificationStatus + package_name: str | None = None + install_command: str | None = None + version_command: str | None = None + min_version: str = "1.0.0" + max_version: str | None = None + + +# Registry of known methodologies and their sources +# Verified plugins are maintained by Auto Claude and tested for compatibility +METHODOLOGY_SOURCES: dict[str, MethodologySource] = { + "native": MethodologySource( + type=SourceType.NATIVE, + verification=VerificationStatus.NATIVE, + ), + "bmad": MethodologySource( + type=SourceType.NPM, + verification=VerificationStatus.VERIFIED, + package_name="bmad-method", + install_command="npx bmad-method@alpha install", + version_command="npx bmad-method@alpha --version", + min_version="1.0.0", + ), +} + + +def get_methodology_source(name: str) -> MethodologySource | None: + """Get the source configuration for a methodology. + + Args: + name: Name of the methodology (e.g., 'native', 'bmad') + + Returns: + MethodologySource if found, None otherwise + """ + return METHODOLOGY_SOURCES.get(name) + + +def is_verified_methodology(name: str) -> bool: + """Check if a methodology is verified (maintained by Auto Claude team). + + Args: + name: Name of the methodology + + Returns: + True if methodology is verified or native, False otherwise + """ + source = get_methodology_source(name) + if source is None: + return False + return source.verification in (VerificationStatus.NATIVE, VerificationStatus.VERIFIED) + + +def is_native_methodology(name: str) -> bool: + """Check if a methodology is the native (built-in) methodology. + + Args: + name: Name of the methodology + + Returns: + True if methodology is native, False otherwise + """ + source = get_methodology_source(name) + if source is None: + return False + return source.verification == VerificationStatus.NATIVE + + +def get_install_command(name: str) -> str | None: + """Get the install command for a methodology. + + Args: + name: Name of the methodology + + Returns: + Install command string if available, None for native or unknown + """ + source = get_methodology_source(name) + if source is None or source.type == SourceType.NATIVE: + return None + return source.install_command + + +def get_version_command(name: str) -> str | None: + """Get the version check command for a methodology. + + Args: + name: Name of the methodology + + Returns: + Version command string if available, None for native or unknown + """ + source = get_methodology_source(name) + if source is None or source.type == SourceType.NATIVE: + return None + return source.version_command + + +def list_available_methodologies() -> list[dict]: + """List all registered methodologies with their source info. + + Returns: + List of dictionaries with methodology source information + """ + return [ + { + "name": name, + "type": source.type.value, + "verification": source.verification.value, + "package_name": source.package_name, + "min_version": source.min_version, + "max_version": source.max_version, + } + for name, source in METHODOLOGY_SOURCES.items() + ] diff --git a/apps/frontend/src/main/ipc-handlers/claude-code-handlers.ts b/apps/frontend/src/main/ipc-handlers/claude-code-handlers.ts index 3116b644..6d2c1d6d 100644 --- a/apps/frontend/src/main/ipc-handlers/claude-code-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/claude-code-handlers.ts @@ -340,10 +340,13 @@ function getInstallCommand(isUpdate: boolean): string { } /** - * Escape single quotes in a string for use in AppleScript + * Escape a string for use in AppleScript double-quoted strings. + * AppleScript uses backslash to escape special characters inside double quotes. */ export function escapeAppleScriptString(str: string): string { - return str.replace(/'/g, "'\\''"); + // Escape backslashes first (to avoid double-escaping) + // Then escape double quotes + return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); } /** @@ -407,13 +410,15 @@ export async function openTerminalWithCommand(command: string): Promise { if (terminalId === 'iterm2') { // iTerm2 + // IMPORTANT: create window FIRST, then activate - this prevents opening a blank default window + // when iTerm2 isn't already running (same pattern as Terminal.app) script = ` tell application "iTerm" - activate create window with default profile tell current session of current window write text "${escapedCommand}" end tell + activate end tell `; } else if (terminalId === 'warp') { diff --git a/apps/frontend/src/main/ipc-handlers/index.ts b/apps/frontend/src/main/ipc-handlers/index.ts index 7b6f52e8..6a592911 100644 --- a/apps/frontend/src/main/ipc-handlers/index.ts +++ b/apps/frontend/src/main/ipc-handlers/index.ts @@ -34,6 +34,7 @@ import { registerMcpHandlers } from './mcp-handlers'; import { registerProfileHandlers } from './profile-handlers'; import { registerTerminalWorktreeIpcHandlers } from './terminal'; import { registerCheckpointHandlers } from './checkpoint-handlers'; +import { registerMethodologyHandlers } from './methodology-handlers'; import { notificationService } from '../notification-service'; /** @@ -122,6 +123,9 @@ export function setupIpcHandlers( // Checkpoint handlers (Semi-Auto execution mode - Story 5.4) registerCheckpointHandlers(agentManager, getMainWindow); + // Methodology plugin handlers (project-level methodology settings) + registerMethodologyHandlers(); + console.warn('[IPC] All handler modules registered successfully'); } @@ -149,5 +153,6 @@ export { registerClaudeCodeHandlers, registerMcpHandlers, registerProfileHandlers, - registerCheckpointHandlers + registerCheckpointHandlers, + registerMethodologyHandlers }; diff --git a/apps/frontend/src/main/ipc-handlers/methodology-handlers.ts b/apps/frontend/src/main/ipc-handlers/methodology-handlers.ts new file mode 100644 index 00000000..83d637c9 --- /dev/null +++ b/apps/frontend/src/main/ipc-handlers/methodology-handlers.ts @@ -0,0 +1,144 @@ +/** + * IPC handlers for methodology plugin operations. + * + * Handles methodology checking, installation, configuration, + * and version compatibility checks for project-level methodology settings. + */ + +import { ipcMain } from 'electron'; +import { IPC_CHANNELS } from '../../shared/constants'; +import type { IPCResult } from '../../shared/types'; +import type { + ProjectMethodologyConfig, + MethodologyInstallResult, + MethodologyCompatibilityResult +} from '../../shared/types/methodology'; +import { + checkInstalled, + install, + getConfig, + saveConfig, + checkCompatibility, + listAvailable +} from '../methodology'; + +/** + * Register all methodology-related IPC handlers. + */ +export function registerMethodologyHandlers(): void { + // Check if methodology is installed in project + ipcMain.handle( + IPC_CHANNELS.METHODOLOGY_CHECK_INSTALLED, + async ( + _, + projectPath: string, + name: string + ): Promise> => { + try { + const result = await checkInstalled(projectPath, name); + return { success: true, data: result }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to check installation' + }; + } + } + ); + + // Install methodology + ipcMain.handle( + IPC_CHANNELS.METHODOLOGY_INSTALL, + async ( + _, + projectPath: string, + name: string, + version?: string + ): Promise> => { + try { + const result = await install(projectPath, name, version); + return { success: true, data: result }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to install methodology' + }; + } + } + ); + + // Get project's methodology config + ipcMain.handle( + IPC_CHANNELS.METHODOLOGY_GET_CONFIG, + async ( + _, + projectPath: string + ): Promise> => { + try { + const config = await getConfig(projectPath); + return { success: true, data: config }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to get config' + }; + } + } + ); + + // Save project's methodology config + ipcMain.handle( + IPC_CHANNELS.METHODOLOGY_SAVE_CONFIG, + async ( + _, + projectPath: string, + config: ProjectMethodologyConfig + ): Promise> => { + try { + await saveConfig(projectPath, config); + return { success: true, data: undefined }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to save config' + }; + } + } + ); + + // List available methodologies with their sources + ipcMain.handle( + IPC_CHANNELS.METHODOLOGY_LIST_AVAILABLE, + async (): Promise>> => { + try { + const methodologies = listAvailable(); + return { success: true, data: methodologies }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to list methodologies' + }; + } + } + ); + + // Check version compatibility + ipcMain.handle( + IPC_CHANNELS.METHODOLOGY_CHECK_COMPATIBILITY, + async ( + _, + name: string, + version: string + ): Promise> => { + try { + const result = checkCompatibility(name, version); + return { success: true, data: result }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to check compatibility' + }; + } + } + ); +} diff --git a/apps/frontend/src/main/ipc-handlers/settings-handlers.ts b/apps/frontend/src/main/ipc-handlers/settings-handlers.ts index 532e1db4..95cfaa2c 100644 --- a/apps/frontend/src/main/ipc-handlers/settings-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/settings-handlers.ts @@ -21,6 +21,7 @@ import { setUpdateChannel, setUpdateChannelWithDowngradeCheck } from '../app-upd import { getSettingsPath, readSettingsFile } from '../settings-utils'; import { configureTools, getToolPath, getToolInfo, isPathFromWrongPlatform, preWarmToolCache } from '../cli-tool-manager'; import { parseEnvFile } from './utils'; +import { openTerminalWithCommand } from './claude-code-handlers'; const settingsPath = getSettingsPath(); @@ -526,6 +527,50 @@ export function registerSettingsHandlers( } ); + // Open terminal with a specific command using user's preferred terminal + ipcMain.handle( + IPC_CHANNELS.SHELL_OPEN_TERMINAL_WITH_COMMAND, + async (_, command: string, cwd?: string): Promise> => { + try { + // Validate command input + if (!command || typeof command !== 'string' || command.trim() === '') { + return { + success: false, + error: 'Command is required and must be a non-empty string' + }; + } + + // Build the full command with cd if cwd is provided + let fullCommand = command; + if (cwd) { + const resolvedCwd = path.resolve(cwd); + if (!existsSync(resolvedCwd)) { + return { + success: false, + error: `Directory does not exist: ${resolvedCwd}` + }; + } + // Prefix with cd command + fullCommand = process.platform === 'win32' + ? `cd /d "${resolvedCwd}" && ${command}` + : `cd "${resolvedCwd}" && ${command}`; + } + + console.log('[Shell] Opening terminal with command:', fullCommand); + await openTerminalWithCommand(fullCommand); + + return { success: true }; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : 'Unknown error'; + console.error('[Shell] Failed to open terminal with command:', errorMsg); + return { + success: false, + error: `Failed to open terminal with command: ${errorMsg}` + }; + } + } + ); + // ============================================ // Auto-Build Source Environment Operations // ============================================ diff --git a/apps/frontend/src/main/methodology/index.ts b/apps/frontend/src/main/methodology/index.ts new file mode 100644 index 00000000..5551e3ac --- /dev/null +++ b/apps/frontend/src/main/methodology/index.ts @@ -0,0 +1,8 @@ +/** + * Methodology plugin management module. + * + * Exports all methodology-related functionality for project-level + * methodology configuration and installation. + */ + +export * from './installer'; diff --git a/apps/frontend/src/main/methodology/installer.ts b/apps/frontend/src/main/methodology/installer.ts new file mode 100644 index 00000000..19cb082f --- /dev/null +++ b/apps/frontend/src/main/methodology/installer.ts @@ -0,0 +1,344 @@ +/** + * Methodology plugin installation manager. + * + * Handles checking installation status, installing methodologies from various sources, + * and verifying version compatibility. + */ + +import { exec } from 'child_process'; +import { promises as fs } from 'fs'; +import path from 'path'; +import { promisify } from 'util'; +import { logger } from '../app-logger'; +import { + METHODOLOGY_SOURCES, + getMethodologySource, + isNativeMethodology, + type MethodologySource +} from '../../shared/constants/methodology-sources'; +import type { + ProjectMethodologyConfig, + MethodologyInstallResult, + MethodologyCompatibilityResult +} from '../../shared/types/methodology'; + +const execAsync = promisify(exec); + +/** File name for project methodology configuration */ +const METHODOLOGY_CONFIG_FILE = 'methodology.json'; + +/** + * Get the path to the methodology config file for a project. + */ +function getConfigPath(projectPath: string): string { + return path.join(projectPath, '.auto-claude', METHODOLOGY_CONFIG_FILE); +} + +/** + * Check if a methodology is installed in a project. + * + * For native methodology, always returns installed. + * For npm methodologies, checks if the install artifacts exist. + */ +export async function checkInstalled( + projectPath: string, + name: string +): Promise { + try { + // Native methodology is always available + if (isNativeMethodology(name)) { + return { success: true, version: '1.0.0' }; + } + + const source = getMethodologySource(name); + if (!source) { + return { success: false, error: `Unknown methodology: ${name}` }; + } + + // Check if methodology config exists for this project + const configPath = getConfigPath(projectPath); + try { + const configContent = await fs.readFile(configPath, 'utf-8'); + const config: ProjectMethodologyConfig = JSON.parse(configContent); + + if (config.name === name && config.version) { + return { success: true, version: config.version }; + } + } catch { + // Config doesn't exist or is invalid - methodology not installed + } + + // For npm methodologies, try to get version from command + if (source.type === 'npm' && source.versionCommand) { + try { + const { stdout } = await execAsync(source.versionCommand, { + cwd: projectPath, + timeout: 30000 + }); + const version = stdout.trim(); + if (version) { + return { success: true, version }; + } + } catch { + // Version command failed - not installed + } + } + + return { success: false, error: 'Methodology not installed' }; + } catch (error) { + logger.error('Error checking methodology installation:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error' + }; + } +} + +/** + * Install a methodology in a project. + * + * For npm methodologies, runs the install command. + * For local methodologies, verifies the path exists. + */ +export async function install( + projectPath: string, + name: string, + version?: string +): Promise { + try { + // Native methodology doesn't need installation + if (isNativeMethodology(name)) { + // Just save the config + await saveConfig(projectPath, { + name: 'native', + version: '1.0.0', + source: 'native', + installedAt: new Date().toISOString() + }); + return { success: true, version: '1.0.0' }; + } + + const source = getMethodologySource(name); + if (!source) { + return { success: false, error: `Unknown methodology: ${name}` }; + } + + // Handle npm installation + if (source.type === 'npm' && source.installCommand) { + logger.info(`Installing methodology ${name} via npm...`); + + // Modify install command if version specified + let command = source.installCommand; + if (version && source.packageName) { + // Replace @alpha or @version with specific version + command = command.replace( + new RegExp(`${source.packageName}@\\S+`), + `${source.packageName}@${version}` + ); + } + + try { + const { stdout, stderr } = await execAsync(command, { + cwd: projectPath, + timeout: 120000 // 2 minute timeout for install + }); + + logger.debug('Install output:', stdout); + if (stderr) { + logger.debug('Install stderr:', stderr); + } + + // Get installed version + let installedVersion = version || source.minVersion; + if (source.versionCommand) { + try { + const { stdout: versionOutput } = await execAsync(source.versionCommand, { + cwd: projectPath, + timeout: 30000 + }); + installedVersion = versionOutput.trim() || installedVersion; + } catch { + // Version check failed, use default + } + } + + // Save configuration + await saveConfig(projectPath, { + name, + version: installedVersion, + source: source.type, + installedAt: new Date().toISOString(), + packageName: source.packageName + }); + + return { success: true, version: installedVersion }; + } catch (error) { + logger.error('Install command failed:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Install command failed' + }; + } + } + + // Handle local installation (verify path exists) + if (source.type === 'local') { + // For local, we just need to verify the methodology exists + // and save the config + await saveConfig(projectPath, { + name, + version: source.minVersion, + source: 'local', + installedAt: new Date().toISOString() + }); + return { success: true, version: source.minVersion }; + } + + return { success: false, error: `Unsupported source type: ${source.type}` }; + } catch (error) { + logger.error('Error installing methodology:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error' + }; + } +} + +/** + * Get the methodology configuration for a project. + */ +export async function getConfig( + projectPath: string +): Promise { + try { + const configPath = getConfigPath(projectPath); + const content = await fs.readFile(configPath, 'utf-8'); + return JSON.parse(content) as ProjectMethodologyConfig; + } catch { + // Config doesn't exist + return null; + } +} + +/** + * Save methodology configuration for a project. + */ +export async function saveConfig( + projectPath: string, + config: ProjectMethodologyConfig +): Promise { + const configPath = getConfigPath(projectPath); + + // Ensure .auto-claude directory exists + const autoClaudeDir = path.dirname(configPath); + await fs.mkdir(autoClaudeDir, { recursive: true }); + + // Write config + await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8'); + logger.info(`Saved methodology config for ${config.name} at ${configPath}`); +} + +/** + * Check version compatibility for a methodology. + */ +export function checkCompatibility( + name: string, + installedVersion: string +): MethodologyCompatibilityResult { + // Native is always compatible + if (isNativeMethodology(name)) { + return { compatible: true, installedVersion }; + } + + const source = getMethodologySource(name); + if (!source) { + return { + compatible: false, + warning: `Unknown methodology: ${name}`, + installedVersion + }; + } + + // Parse versions for comparison + const installed = parseVersion(installedVersion); + const min = parseVersion(source.minVersion); + const max = source.maxVersion ? parseVersion(source.maxVersion) : null; + + // Check minimum version + if (compareVersions(installed, min) < 0) { + return { + compatible: false, + warning: `Installed version ${installedVersion} is below minimum required ${source.minVersion}. Consider updating.`, + installedVersion, + minVersion: source.minVersion, + maxVersion: source.maxVersion + }; + } + + // Check maximum version (exclusive) + if (max && compareVersions(installed, max) >= 0) { + return { + compatible: false, + warning: `Installed version ${installedVersion} exceeds maximum supported ${source.maxVersion}. This may cause compatibility issues.`, + installedVersion, + minVersion: source.minVersion, + maxVersion: source.maxVersion + }; + } + + return { + compatible: true, + installedVersion, + minVersion: source.minVersion, + maxVersion: source.maxVersion + }; +} + +/** + * List all available methodologies with their source info. + */ +export function listAvailable(): Array<{ + name: string; + type: string; + verification: string; + packageName?: string; + minVersion: string; + maxVersion?: string; +}> { + return Object.entries(METHODOLOGY_SOURCES).map(([name, source]) => ({ + name, + type: source.type, + verification: source.verification, + packageName: source.packageName, + minVersion: source.minVersion, + maxVersion: source.maxVersion + })); +} + +/** + * Parse a semver version string into components. + */ +function parseVersion(version: string): { major: number; minor: number; patch: number } { + const match = version.match(/^(\d+)\.(\d+)\.(\d+)/); + if (!match) { + return { major: 0, minor: 0, patch: 0 }; + } + return { + major: parseInt(match[1], 10), + minor: parseInt(match[2], 10), + patch: parseInt(match[3], 10) + }; +} + +/** + * Compare two parsed versions. + * Returns: negative if a < b, 0 if a === b, positive if a > b + */ +function compareVersions( + a: { major: number; minor: number; patch: number }, + b: { major: number; minor: number; patch: number } +): number { + if (a.major !== b.major) return a.major - b.major; + if (a.minor !== b.minor) return a.minor - b.minor; + return a.patch - b.patch; +} diff --git a/apps/frontend/src/preload/api/index.ts b/apps/frontend/src/preload/api/index.ts index fb54a1df..841ad037 100644 --- a/apps/frontend/src/preload/api/index.ts +++ b/apps/frontend/src/preload/api/index.ts @@ -14,6 +14,7 @@ import { ClaudeCodeAPI, createClaudeCodeAPI } from './modules/claude-code-api'; import { McpAPI, createMcpAPI } from './modules/mcp-api'; import { ProfileAPI, createProfileAPI } from './profile-api'; import { CheckpointAPI, createCheckpointAPI } from './modules/checkpoint-api'; +import { MethodologyAPI, createMethodologyAPI } from './modules/methodology-api'; export interface ElectronAPI extends ProjectAPI, @@ -29,7 +30,8 @@ export interface ElectronAPI extends DebugAPI, ClaudeCodeAPI, McpAPI, - ProfileAPI { + ProfileAPI, + MethodologyAPI { github: GitHubAPI; checkpoints: CheckpointAPI; } @@ -46,6 +48,7 @@ export const createElectronAPI = (): ElectronAPI => ({ ...createClaudeCodeAPI(), ...createMcpAPI(), ...createProfileAPI(), + ...createMethodologyAPI(), github: createGitHubAPI(), checkpoints: createCheckpointAPI() }); @@ -65,7 +68,8 @@ export { createDebugAPI, createClaudeCodeAPI, createMcpAPI, - createCheckpointAPI + createCheckpointAPI, + createMethodologyAPI }; export type { @@ -84,5 +88,6 @@ export type { DebugAPI, ClaudeCodeAPI, McpAPI, - CheckpointAPI + CheckpointAPI, + MethodologyAPI }; diff --git a/apps/frontend/src/preload/api/modules/methodology-api.ts b/apps/frontend/src/preload/api/modules/methodology-api.ts new file mode 100644 index 00000000..a233731e --- /dev/null +++ b/apps/frontend/src/preload/api/modules/methodology-api.ts @@ -0,0 +1,79 @@ +/** + * Methodology Plugin API + * + * Exposes methodology installation, configuration, and compatibility checking + * to the renderer for project-level methodology settings. + */ + +import { ipcRenderer } from 'electron'; +import { IPC_CHANNELS } from '../../../shared/constants/ipc'; +import type { IPCResult } from '../../../shared/types/common'; +import type { + ProjectMethodologyConfig, + MethodologyInstallResult, + MethodologyCompatibilityResult +} from '../../../shared/types/methodology'; + +export interface MethodologyAPI { + /** Check if a methodology is installed in a project */ + checkMethodologyInstalled: ( + projectPath: string, + name: string + ) => Promise>; + + /** Install a methodology in a project */ + installMethodology: ( + projectPath: string, + name: string, + version?: string + ) => Promise>; + + /** Get project's methodology configuration */ + getMethodologyConfig: ( + projectPath: string + ) => Promise>; + + /** Save project's methodology configuration */ + saveMethodologyConfig: ( + projectPath: string, + config: ProjectMethodologyConfig + ) => Promise>; + + /** List available methodologies with their sources */ + listAvailableMethodologies: () => Promise>>; + + /** Check version compatibility for a methodology */ + checkMethodologyCompatibility: ( + name: string, + version: string + ) => Promise>; +} + +export function createMethodologyAPI(): MethodologyAPI { + return { + checkMethodologyInstalled: (projectPath: string, name: string) => + ipcRenderer.invoke(IPC_CHANNELS.METHODOLOGY_CHECK_INSTALLED, projectPath, name), + + installMethodology: (projectPath: string, name: string, version?: string) => + ipcRenderer.invoke(IPC_CHANNELS.METHODOLOGY_INSTALL, projectPath, name, version), + + getMethodologyConfig: (projectPath: string) => + ipcRenderer.invoke(IPC_CHANNELS.METHODOLOGY_GET_CONFIG, projectPath), + + saveMethodologyConfig: (projectPath: string, config: ProjectMethodologyConfig) => + ipcRenderer.invoke(IPC_CHANNELS.METHODOLOGY_SAVE_CONFIG, projectPath, config), + + listAvailableMethodologies: () => + ipcRenderer.invoke(IPC_CHANNELS.METHODOLOGY_LIST_AVAILABLE), + + checkMethodologyCompatibility: (name: string, version: string) => + ipcRenderer.invoke(IPC_CHANNELS.METHODOLOGY_CHECK_COMPATIBILITY, name, version), + }; +} diff --git a/apps/frontend/src/preload/api/modules/shell-api.ts b/apps/frontend/src/preload/api/modules/shell-api.ts index 1a395ffd..f805b8f2 100644 --- a/apps/frontend/src/preload/api/modules/shell-api.ts +++ b/apps/frontend/src/preload/api/modules/shell-api.ts @@ -8,6 +8,8 @@ import type { IPCResult } from '../../../shared/types'; export interface ShellAPI { openExternal: (url: string) => Promise; openTerminal: (dirPath: string) => Promise>; + /** Open user's preferred terminal with a command. Optionally cd to a directory first. */ + openTerminalWithCommand: (command: string, cwd?: string) => Promise>; } /** @@ -17,5 +19,7 @@ export const createShellAPI = (): ShellAPI => ({ openExternal: (url: string): Promise => invokeIpc(IPC_CHANNELS.SHELL_OPEN_EXTERNAL, url), openTerminal: (dirPath: string): Promise> => - invokeIpc(IPC_CHANNELS.SHELL_OPEN_TERMINAL, dirPath) + invokeIpc(IPC_CHANNELS.SHELL_OPEN_TERMINAL, dirPath), + openTerminalWithCommand: (command: string, cwd?: string): Promise> => + invokeIpc(IPC_CHANNELS.SHELL_OPEN_TERMINAL_WITH_COMMAND, command, cwd) }); diff --git a/apps/frontend/src/renderer/App.tsx b/apps/frontend/src/renderer/App.tsx index d5f4ed76..7318d493 100644 --- a/apps/frontend/src/renderer/App.tsx +++ b/apps/frontend/src/renderer/App.tsx @@ -51,6 +51,8 @@ import { OnboardingWizard } from './components/onboarding'; import { AppUpdateNotification } from './components/AppUpdateNotification'; import { ProactiveSwapListener } from './components/ProactiveSwapListener'; import { GitHubSetupModal } from './components/GitHubSetupModal'; +import { MethodologyWarningDialog } from './components/MethodologyWarningDialog'; +import { useMethodologyCompatibility } from './hooks/useMethodologyCompatibility'; import { useProjectStore, loadProjects, addProject, initializeProject, removeProject } from './stores/project-store'; import { useTaskStore, loadTasks } from './stores/task-store'; import { useSettingsStore, loadSettings, loadProfiles } from './stores/settings-store'; @@ -61,7 +63,7 @@ import { initDownloadProgressListener } from './stores/download-store'; import { GlobalDownloadIndicator } from './components/GlobalDownloadIndicator'; import { useIpcListeners } from './hooks/useIpc'; import { useGlobalTerminalListeners } from './hooks/useGlobalTerminalListeners'; -import { COLOR_THEMES, UI_SCALE_MIN, UI_SCALE_MAX, UI_SCALE_DEFAULT } from '../shared/constants'; +import { COLOR_THEMES, UI_SCALE_MIN, UI_SCALE_MAX, UI_SCALE_DEFAULT, getInstallCommand } from '../shared/constants'; import type { Task, Project, ColorTheme } from '../shared/types'; import { ProjectTabBar } from './components/ProjectTabBar'; import { AddProjectModal } from './components/AddProjectModal'; @@ -105,6 +107,14 @@ export function App() { // This ensures terminal output is captured even when the terminal component is not rendered useGlobalTerminalListeners(); + // Methodology compatibility check + const { + warning: methodologyWarning, + dismissWarning: dismissMethodologyWarning, + continueAnyway: continueWithMethodology, + switchToNative: switchToNativeMethodology, + } = useMethodologyCompatibility(); + // Stores const projects = useProjectStore((state) => state.projects); const selectedProjectId = useProjectStore((state) => state.selectedProjectId); @@ -665,6 +675,39 @@ export function App() { const updatedProject = useProjectStore.getState().projects.find(p => p.id === projectId); console.log('[InitDialog] Updated project:', updatedProject); + // Save methodology configuration AFTER initialization completes + // This ensures .auto-claude directory exists before we write to it + // Note: Use pendingProject for methodology since it was set during project creation + // (updatedProject from store may not have settings synced yet) + const projectPath = updatedProject?.path || pendingProject?.path; + if (projectPath) { + const methodology = pendingProject?.settings?.methodology || updatedProject?.settings?.methodology || 'native'; + console.log('[InitDialog] Saving methodology config:', methodology, 'from pendingProject:', pendingProject?.settings?.methodology); + try { + await window.electronAPI.saveMethodologyConfig(projectPath, { + name: methodology, + version: '1.0.0', + source: methodology === 'native' ? 'native' : 'npm', + installedAt: new Date().toISOString() + }); + + // If methodology requires installation, open user's preferred terminal with install command + const installCommand = getInstallCommand(methodology); + if (installCommand) { + console.log('[InitDialog] Opening terminal for methodology installation:', methodology, installCommand); + try { + await window.electronAPI.openTerminalWithCommand(installCommand, projectPath); + } catch (termErr) { + console.warn('[InitDialog] Failed to open terminal for methodology install:', termErr); + // Non-fatal - user can run the command manually + } + } + } catch (err) { + console.warn('[InitDialog] Failed to save methodology config:', err); + // Non-fatal - methodology can be configured later + } + } + // Mark as successful to prevent onOpenChange from treating this as a skip setInitSuccess(true); setIsInitializing(false); @@ -1036,6 +1079,14 @@ export function App() { /> )} + {/* Methodology Version Warning Dialog - shows when incompatible methodology version detected */} + + {/* Remove Project Confirmation Dialog */} { if (!open) handleCancelRemoveProject(); diff --git a/apps/frontend/src/renderer/components/AddProjectModal.tsx b/apps/frontend/src/renderer/components/AddProjectModal.tsx index 852f3feb..59d2fc41 100644 --- a/apps/frontend/src/renderer/components/AddProjectModal.tsx +++ b/apps/frontend/src/renderer/components/AddProjectModal.tsx @@ -14,6 +14,7 @@ import { } from './ui/dialog'; import { cn } from '../lib/utils'; import { addProject } from '../stores/project-store'; +import { MethodologySelector } from './task-form/MethodologySelector'; import type { Project } from '../../shared/types'; type ModalStep = 'choose' | 'create-form'; @@ -30,6 +31,7 @@ export function AddProjectModal({ open, onOpenChange, onProjectAdded }: AddProje const [projectName, setProjectName] = useState(''); const [projectLocation, setProjectLocation] = useState(''); const [initGit, setInitGit] = useState(true); + const [methodology, setMethodology] = useState('native'); const [isCreating, setIsCreating] = useState(false); const [error, setError] = useState(null); @@ -40,6 +42,7 @@ export function AddProjectModal({ open, onOpenChange, onProjectAdded }: AddProje setProjectName(''); setProjectLocation(''); setInitGit(true); + setMethodology('native'); setError(null); } }, [open]); @@ -139,7 +142,26 @@ export function AddProjectModal({ open, onOpenChange, onProjectAdded }: AddProje // Non-fatal - main branch can be set later in settings } } - onProjectAdded?.(project, true); // New projects always need init + + // Save methodology preference in project settings (NOT methodology.json yet) + // The methodology.json will be saved AFTER initialization completes + // to avoid creating .auto-claude directory prematurely + let projectWithSettings = project; + try { + await window.electronAPI.updateProjectSettings(project.id, { + methodology + }); + // Also update local project object so it's available in pendingProject + // The IPC call updates the main store, but we need it in the object we pass + projectWithSettings = { + ...project, + settings: { ...project.settings, methodology } + }; + } catch { + // Non-fatal - methodology can be set later + } + + onProjectAdded?.(projectWithSettings, true); // New projects always need init onOpenChange(false); } } catch (err) { @@ -273,6 +295,13 @@ export function AddProjectModal({ open, onOpenChange, onProjectAdded }: AddProje + {/* Methodology Selector */} + + {error && (
{error} diff --git a/apps/frontend/src/renderer/components/MethodologyWarningDialog.tsx b/apps/frontend/src/renderer/components/MethodologyWarningDialog.tsx new file mode 100644 index 00000000..32e42c13 --- /dev/null +++ b/apps/frontend/src/renderer/components/MethodologyWarningDialog.tsx @@ -0,0 +1,90 @@ +/** + * MethodologyWarningDialog - Displays version compatibility warnings + * + * Shows when a methodology's installed version may not be compatible + * with the current Auto Claude version. Provides options to: + * - Continue anyway (at user's own risk) + * - Switch to native methodology + */ + +import { useTranslation } from 'react-i18next'; +import { AlertTriangle, Play, Home, RefreshCw } from 'lucide-react'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from './ui/alert-dialog'; +import { Button } from './ui/button'; +import type { MethodologyWarningInfo } from '../hooks/useMethodologyCompatibility'; + +interface MethodologyWarningDialogProps { + /** Warning information to display */ + warning: MethodologyWarningInfo | null; + /** Called when user chooses to continue anyway */ + onContinue: () => void; + /** Called when user chooses to switch to native */ + onSwitchToNative: () => void; + /** Called when dialog is dismissed */ + onDismiss: () => void; +} + +export function MethodologyWarningDialog({ + warning, + onContinue, + onSwitchToNative, + onDismiss, +}: MethodologyWarningDialogProps) { + const { t } = useTranslation(['tasks', 'common']); + + if (!warning?.showWarning) { + return null; + } + + return ( + !open && onDismiss()}> + + + + + {t('tasks:methodology.versionWarning.title')} + + +

+ {t('tasks:methodology.versionWarning.description', { + methodology: warning.methodologyName.toUpperCase(), + version: warning.installedVersion, + minVersion: warning.minVersion || '1.0.0', + })} +

+ {warning.warningMessage && ( +

+ {warning.warningMessage} +

+ )} +

+ {t('tasks:methodology.versionWarning.riskNotice')} +

+
+
+ + + + {t('common:cancel')} + + + + {t('tasks:methodology.versionWarning.continueAnyway')} + + +
+
+ ); +} diff --git a/apps/frontend/src/renderer/components/onboarding/OnboardingWizard.tsx b/apps/frontend/src/renderer/components/onboarding/OnboardingWizard.tsx index 2d3c11f6..50a4d010 100644 --- a/apps/frontend/src/renderer/components/onboarding/OnboardingWizard.tsx +++ b/apps/frontend/src/renderer/components/onboarding/OnboardingWizard.tsx @@ -18,7 +18,7 @@ import { ClaudeCodeStep } from './ClaudeCodeStep'; import { DevToolsStep } from './DevToolsStep'; import { PrivacyStep } from './PrivacyStep'; import { GraphitiStep } from './GraphitiStep'; -import { MethodologyStep } from './MethodologyStep'; +// Note: MethodologyStep removed from onboarding - methodology is now a per-project setting import { CompletionStep } from './CompletionStep'; import { useSettingsStore } from '../../stores/settings-store'; @@ -30,7 +30,8 @@ interface OnboardingWizardProps { } // Wizard step identifiers -type WizardStepId = 'welcome' | 'auth-choice' | 'oauth' | 'claude-code' | 'devtools' | 'privacy' | 'graphiti' | 'methodology' | 'completion'; +// Note: 'methodology' step removed - methodology is now configured at project level +type WizardStepId = 'welcome' | 'auth-choice' | 'oauth' | 'claude-code' | 'devtools' | 'privacy' | 'graphiti' | 'completion'; // Step configuration with translation keys const WIZARD_STEPS: { id: WizardStepId; labelKey: string }[] = [ @@ -41,7 +42,6 @@ const WIZARD_STEPS: { id: WizardStepId; labelKey: string }[] = [ { id: 'devtools', labelKey: 'steps.devtools' }, { id: 'privacy', labelKey: 'steps.privacy' }, { id: 'graphiti', labelKey: 'steps.memory' }, - { id: 'methodology', labelKey: 'steps.methodology' }, { id: 'completion', labelKey: 'steps.done' } ]; @@ -231,14 +231,7 @@ export function OnboardingWizard({ onSkip={skipWizard} /> ); - case 'methodology': - return ( - - ); + // Note: 'methodology' case removed - methodology is now configured at project level case 'completion': return ( void; } +export interface UseMethodologiesWithStatusResult { + /** List of available methodologies with installation status */ + methodologies: MethodologyWithStatus[]; + /** Whether methodologies are currently loading */ + isLoading: boolean; + /** Error message if loading failed */ + error: string | null; + /** Refetch methodologies */ + refetch: () => void; +} + /** * Hook to fetch available methodology plugins * @@ -59,10 +75,27 @@ export function useMethodologies(): UseMethodologiesResult { setError(null); try { - // Epic 6 will replace this with: await window.electronAPI.getMethodologies() + // Try to load from registry via IPC + const result = await window.electronAPI.listAvailableMethodologies(); + if (result.success && result.data) { + // Map registry data to MethodologyInfo + const methodologyList: MethodologyInfo[] = result.data.map(m => ({ + name: m.name, + version: m.minVersion, + description: getMethodologyDescription(m.name), + author: getMethodologyAuthor(m.name), + complexity_levels: ['quick', 'standard', 'complex'], + execution_modes: ['full_auto', 'semi_auto'], + is_verified: m.verification === 'verified' || m.verification === 'native', + })); + setMethodologies(methodologyList); + } else { + // Fallback to static methodologies + setMethodologies(STATIC_METHODOLOGIES); + } + } catch { + // Fallback to static methodologies setMethodologies(STATIC_METHODOLOGIES); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to load methodologies'); } finally { setIsLoading(false); } @@ -79,3 +112,111 @@ export function useMethodologies(): UseMethodologiesResult { refetch: loadMethodologies, }; } + +/** + * Hook to fetch methodologies with installation status for a specific project + * + * @param projectPath - Path to the project to check installation status + * @returns Object containing methodologies with status, loading state, and error + */ +export function useMethodologiesWithStatus( + projectPath: string | null +): UseMethodologiesWithStatusResult { + const [methodologies, setMethodologies] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const loadMethodologies = useCallback(async () => { + setIsLoading(true); + setError(null); + + try { + // Load available methodologies from registry + const result = await window.electronAPI.listAvailableMethodologies(); + const methodologyList = result.success && result.data ? result.data : []; + + // Build methodology list with installation status + const methodologiesWithStatus: MethodologyWithStatus[] = await Promise.all( + methodologyList.map(async (m) => { + let isInstalled = false; + let version = m.minVersion; + + // Check installation status if project path provided + if (projectPath) { + if (isNativeMethodology(m.name)) { + isInstalled = true; + } else { + const installResult = await window.electronAPI.checkMethodologyInstalled( + projectPath, + m.name + ); + if (installResult.success && installResult.data?.success) { + isInstalled = true; + version = installResult.data.version || m.minVersion; + } + } + } + + return { + name: m.name, + version, + description: getMethodologyDescription(m.name), + author: getMethodologyAuthor(m.name), + complexity_levels: ['quick', 'standard', 'complex'], + execution_modes: ['full_auto', 'semi_auto'], + is_verified: m.verification === 'verified' || m.verification === 'native', + isInstalled, + sourceType: m.type as 'native' | 'npm' | 'github' | 'local', + verification: m.verification as 'native' | 'verified' | 'community', + }; + }) + ); + + setMethodologies(methodologiesWithStatus); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load methodologies'); + // Fallback with basic status + setMethodologies(STATIC_METHODOLOGIES.map(m => ({ + ...m, + isInstalled: m.name === 'native', + sourceType: 'native' as const, + verification: 'native' as const, + }))); + } finally { + setIsLoading(false); + } + }, [projectPath]); + + useEffect(() => { + loadMethodologies(); + }, [loadMethodologies]); + + return { + methodologies, + isLoading, + error, + refetch: loadMethodologies, + }; +} + +/** + * Get description for a methodology by name + */ +function getMethodologyDescription(name: string): string { + const descriptions: Record = { + native: 'Built-in methodology with spec creation and implementation phases', + bmad: 'BMAD (Business Model Agile Development) - structured approach with PRD, architecture, and story-driven development', + }; + return descriptions[name] || ''; +} + +/** + * Get author for a methodology by name + */ +function getMethodologyAuthor(name: string): string { + const authors: Record = { + native: 'Auto Claude', + bmad: 'BMad', + }; + return authors[name] || 'Community'; +} diff --git a/apps/frontend/src/renderer/hooks/useMethodologyCompatibility.ts b/apps/frontend/src/renderer/hooks/useMethodologyCompatibility.ts new file mode 100644 index 00000000..d1319d77 --- /dev/null +++ b/apps/frontend/src/renderer/hooks/useMethodologyCompatibility.ts @@ -0,0 +1,187 @@ +/** + * Hook for checking methodology version compatibility when a project is activated. + * + * This hook monitors project changes and checks if the installed methodology + * version is compatible with Auto Claude requirements. It returns warning + * information when incompatibilities are detected. + */ + +import { useState, useEffect, useCallback } from 'react'; +import { useProjectStore } from '../stores/project-store'; +import type { MethodologyCompatibilityResult, ProjectMethodologyConfig } from '../../shared/types/methodology'; +import { isNativeMethodology } from '../../shared/constants/methodology-sources'; + +export interface MethodologyWarningInfo { + /** Whether a warning should be displayed */ + showWarning: boolean; + /** The methodology name */ + methodologyName: string; + /** The installed version */ + installedVersion: string; + /** Warning message from compatibility check */ + warningMessage: string; + /** Minimum required version */ + minVersion?: string; + /** Maximum supported version */ + maxVersion?: string; +} + +export interface UseMethodologyCompatibilityResult { + /** Warning information if incompatibility detected */ + warning: MethodologyWarningInfo | null; + /** Whether compatibility check is in progress */ + isChecking: boolean; + /** Dismiss the current warning */ + dismissWarning: () => void; + /** Choose to continue anyway with current version */ + continueAnyway: () => void; + /** Switch to native methodology */ + switchToNative: () => Promise; + /** Recheck compatibility */ + recheckCompatibility: () => void; +} + +/** + * Hook to check methodology compatibility for the active project. + * + * Version checking occurs: + * - On project switch (when activeProjectId changes) + * - On component mount (app startup) + * - When explicitly triggered via recheckCompatibility + * + * Does NOT check: + * - On every task creation (too frequent) + */ +export function useMethodologyCompatibility(): UseMethodologyCompatibilityResult { + const [warning, setWarning] = useState(null); + const [isChecking, setIsChecking] = useState(false); + const [dismissedForSession, setDismissedForSession] = useState>(new Set()); + + const activeProjectId = useProjectStore((state) => state.activeProjectId); + const getActiveProject = useProjectStore((state) => state.getActiveProject); + + const checkCompatibility = useCallback(async () => { + const project = getActiveProject(); + if (!project) { + setWarning(null); + return; + } + + setIsChecking(true); + + try { + // Get methodology config for the project + const configResult = await window.electronAPI.getMethodologyConfig(project.path); + + if (!configResult.success || !configResult.data) { + // No methodology config - assume native, which is always compatible + setWarning(null); + setIsChecking(false); + return; + } + + const config: ProjectMethodologyConfig = configResult.data; + + // Native methodology is always compatible + if (isNativeMethodology(config.name)) { + setWarning(null); + setIsChecking(false); + return; + } + + // Check if already dismissed for this project+methodology combo in this session + const dismissKey = `${project.id}:${config.name}:${config.version}`; + if (dismissedForSession.has(dismissKey)) { + setWarning(null); + setIsChecking(false); + return; + } + + // Check version compatibility + const compatResult = await window.electronAPI.checkMethodologyCompatibility( + config.name, + config.version + ); + + if (!compatResult.success || !compatResult.data) { + // Error checking compatibility - don't show warning + setWarning(null); + setIsChecking(false); + return; + } + + const compatibility: MethodologyCompatibilityResult = compatResult.data; + + if (!compatibility.compatible && compatibility.warning) { + setWarning({ + showWarning: true, + methodologyName: config.name, + installedVersion: config.version, + warningMessage: compatibility.warning, + minVersion: compatibility.minVersion, + maxVersion: compatibility.maxVersion, + }); + } else { + setWarning(null); + } + } catch (error) { + console.error('Error checking methodology compatibility:', error); + setWarning(null); + } finally { + setIsChecking(false); + } + }, [getActiveProject, dismissedForSession]); + + // Check compatibility when active project changes + useEffect(() => { + checkCompatibility(); + }, [activeProjectId, checkCompatibility]); + + const dismissWarning = useCallback(() => { + const project = getActiveProject(); + if (project && warning) { + // Remember dismissal for this session + const dismissKey = `${project.id}:${warning.methodologyName}:${warning.installedVersion}`; + setDismissedForSession((prev) => new Set(prev).add(dismissKey)); + } + setWarning(null); + }, [getActiveProject, warning]); + + const continueAnyway = useCallback(() => { + dismissWarning(); + }, [dismissWarning]); + + const switchToNative = useCallback(async () => { + const project = getActiveProject(); + if (!project) return; + + try { + // Save native methodology config + await window.electronAPI.saveMethodologyConfig(project.path, { + name: 'native', + version: '1.0.0', + source: 'native', + installedAt: new Date().toISOString(), + }); + + // Update project settings + await window.electronAPI.updateProjectSettings(project.id, { + methodology: 'native', + }); + + // Clear warning + setWarning(null); + } catch (error) { + console.error('Error switching to native methodology:', error); + } + }, [getActiveProject]); + + return { + warning, + isChecking, + dismissWarning, + continueAnyway, + switchToNative, + recheckCompatibility: checkCompatibility, + }; +} diff --git a/apps/frontend/src/renderer/lib/browser-mock.ts b/apps/frontend/src/renderer/lib/browser-mock.ts index 34bd02d9..f76f09b8 100644 --- a/apps/frontend/src/renderer/lib/browser-mock.ts +++ b/apps/frontend/src/renderer/lib/browser-mock.ts @@ -350,7 +350,50 @@ const browserMockAPI: ElectronAPI = { openLogsFolder: async () => ({ success: false, error: 'Not available in browser mode' }), copyDebugInfo: async () => ({ success: false, error: 'Not available in browser mode' }), getRecentErrors: async () => [], - listLogFiles: async () => [] + listLogFiles: async () => [], + + // Methodology Plugin Operations + checkMethodologyInstalled: async (_projectPath: string, name: string) => ({ + success: true, + data: { + success: name === 'native', + version: name === 'native' ? '1.0.0' : undefined, + error: name === 'native' ? undefined : 'Not installed (browser mock)' + } + }), + installMethodology: async (_projectPath: string, _name: string, _version?: string) => ({ + success: false, + data: { + success: false, + error: 'Installation not available in browser mode' + } + }), + getMethodologyConfig: async (_projectPath: string) => ({ + success: true, + data: { + name: 'native', + version: '1.0.0', + source: 'native' as const, + installedAt: new Date().toISOString() + } + }), + saveMethodologyConfig: async (_projectPath: string, _config: any) => ({ + success: true, + data: undefined + }), + listAvailableMethodologies: async () => ({ + success: true, + data: [ + { name: 'native', type: 'native', verification: 'native', minVersion: '1.0.0' }, + { name: 'bmad', type: 'npm', verification: 'verified', packageName: 'bmad-method', minVersion: '1.0.0' } + ] + }), + checkMethodologyCompatibility: async (_name: string, _version: string) => ({ + success: true, + data: { + compatible: true + } + }) }; /** diff --git a/apps/frontend/src/renderer/lib/mocks/infrastructure-mock.ts b/apps/frontend/src/renderer/lib/mocks/infrastructure-mock.ts index 81168fa0..04bf3cdc 100644 --- a/apps/frontend/src/renderer/lib/mocks/infrastructure-mock.ts +++ b/apps/frontend/src/renderer/lib/mocks/infrastructure-mock.ts @@ -183,5 +183,10 @@ export const infrastructureMock = { openTerminal: async (dirPath: string) => { console.warn('[Browser Mock] openTerminal:', dirPath); return { success: true }; + }, + + openTerminalWithCommand: async (command: string, cwd?: string) => { + console.warn('[Browser Mock] openTerminalWithCommand:', command, 'cwd:', cwd); + return { success: true }; } }; diff --git a/apps/frontend/src/shared/constants/index.ts b/apps/frontend/src/shared/constants/index.ts index 5b3f4987..dd441f3b 100644 --- a/apps/frontend/src/shared/constants/index.ts +++ b/apps/frontend/src/shared/constants/index.ts @@ -35,3 +35,6 @@ export * from './api-profiles'; // Configuration and paths export * from './config'; + +// Methodology plugin sources +export * from './methodology-sources'; diff --git a/apps/frontend/src/shared/constants/ipc.ts b/apps/frontend/src/shared/constants/ipc.ts index 71be6f28..d91f3cbc 100644 --- a/apps/frontend/src/shared/constants/ipc.ts +++ b/apps/frontend/src/shared/constants/ipc.ts @@ -152,6 +152,7 @@ export const IPC_CHANNELS = { // Shell operations SHELL_OPEN_EXTERNAL: 'shell:openExternal', SHELL_OPEN_TERMINAL: 'shell:openTerminal', + SHELL_OPEN_TERMINAL_WITH_COMMAND: 'shell:openTerminalWithCommand', // Roadmap operations ROADMAP_GET: 'roadmap:get', @@ -538,5 +539,13 @@ export const IPC_CHANNELS = { // Sentry error reporting SENTRY_STATE_CHANGED: 'sentry:state-changed', // Notify main process when setting changes GET_SENTRY_DSN: 'sentry:get-dsn', // Get DSN from main process (env var) - GET_SENTRY_CONFIG: 'sentry:get-config' // Get full Sentry config (DSN + sample rates) + GET_SENTRY_CONFIG: 'sentry:get-config', // Get full Sentry config (DSN + sample rates) + + // Methodology plugin operations + METHODOLOGY_CHECK_INSTALLED: 'methodology:checkInstalled', // Check if methodology is installed in project + METHODOLOGY_INSTALL: 'methodology:install', // Install methodology + METHODOLOGY_GET_CONFIG: 'methodology:getConfig', // Get project's methodology config + METHODOLOGY_SAVE_CONFIG: 'methodology:saveConfig', // Save project's methodology config + METHODOLOGY_LIST_AVAILABLE: 'methodology:listAvailable', // List available methodologies with their sources + METHODOLOGY_CHECK_COMPATIBILITY: 'methodology:checkCompatibility' // Check version compatibility } as const; diff --git a/apps/frontend/src/shared/constants/methodology-sources.ts b/apps/frontend/src/shared/constants/methodology-sources.ts new file mode 100644 index 00000000..8862b7ff --- /dev/null +++ b/apps/frontend/src/shared/constants/methodology-sources.ts @@ -0,0 +1,121 @@ +/** + * Methodology plugin source definitions for multiple distribution types. + * + * This mirrors the backend sources.py configuration, enabling consistent + * handling of methodology sources across frontend and backend. + * + * Plugin Classification: + * - Native: The built-in AutoClaude methodology (not a plugin, always available) + * - Verified: Plugins maintained and tested by Auto Claude team + * - Community: Third-party/user-created plugins without verification guarantee + */ + +/** + * Distribution source types for methodology plugins. + */ +export type SourceType = 'native' | 'npm' | 'github' | 'local'; + +/** + * Verification status for methodology plugins. + */ +export type VerificationStatus = 'native' | 'verified' | 'community'; + +/** + * Configuration for a methodology's distribution source. + */ +export interface MethodologySource { + /** Distribution source type */ + type: SourceType; + /** Verification status */ + verification: VerificationStatus; + /** Package identifier (NPM: package name, GitHub: owner/repo) */ + packageName?: string; + /** Override default install command */ + installCommand?: string; + /** Command to check installed version */ + versionCommand?: string; + /** Minimum supported version for verified plugins */ + minVersion: string; + /** Maximum supported version (exclusive) */ + maxVersion?: string; +} + +/** + * Registry of known methodologies and their sources. + * Verified plugins are maintained by Auto Claude and tested for compatibility. + */ +export const METHODOLOGY_SOURCES: Record = { + native: { + type: 'native', + verification: 'native', + minVersion: '1.0.0', + }, + bmad: { + type: 'npm', + verification: 'verified', + packageName: 'bmad-method', + installCommand: 'npx bmad-method@alpha install', + versionCommand: 'npx bmad-method@alpha --version', + minVersion: '1.0.0', + }, +}; + +/** + * Get the source configuration for a methodology. + */ +export function getMethodologySource(name: string): MethodologySource | undefined { + return METHODOLOGY_SOURCES[name]; +} + +/** + * Check if a methodology is verified (maintained by Auto Claude team). + */ +export function isVerifiedMethodology(name: string): boolean { + const source = getMethodologySource(name); + if (!source) return false; + return source.verification === 'native' || source.verification === 'verified'; +} + +/** + * Check if a methodology is the native (built-in) methodology. + */ +export function isNativeMethodology(name: string): boolean { + const source = getMethodologySource(name); + if (!source) return false; + return source.verification === 'native'; +} + +/** + * Get the install command for a methodology. + */ +export function getInstallCommand(name: string): string | undefined { + const source = getMethodologySource(name); + if (!source || source.type === 'native') return undefined; + return source.installCommand; +} + +/** + * List all registered methodology names. + */ +export function listAvailableMethodologyNames(): string[] { + return Object.keys(METHODOLOGY_SOURCES); +} + +/** + * User-friendly display names for verification statuses. + */ +export const VERIFICATION_DISPLAY_NAMES: Record = { + native: 'Built-in', + verified: 'Verified', + community: 'Community', +}; + +/** + * User-friendly display names for source types. + */ +export const SOURCE_TYPE_DISPLAY_NAMES: Record = { + native: 'Built-in', + npm: 'NPM Package', + github: 'GitHub', + local: 'Local', +}; diff --git a/apps/frontend/src/shared/i18n/locales/en/tasks.json b/apps/frontend/src/shared/i18n/locales/en/tasks.json index 06fb7dfd..2930f3db 100644 --- a/apps/frontend/src/shared/i18n/locales/en/tasks.json +++ b/apps/frontend/src/shared/i18n/locales/en/tasks.json @@ -325,6 +325,13 @@ "badge": { "native": "Native", "bmad": "BMAD" + }, + "versionWarning": { + "title": "Methodology Version Warning", + "description": "The installed {{methodology}} version ({{version}}) may not be fully compatible with Auto Claude. Recommended: {{minVersion}}+", + "riskNotice": "This could cause unexpected behavior during task execution. You can continue anyway or switch to the native methodology.", + "continueAnyway": "Continue Anyway", + "useNative": "Use Native" } }, "bulkPR": { diff --git a/apps/frontend/src/shared/i18n/locales/fr/tasks.json b/apps/frontend/src/shared/i18n/locales/fr/tasks.json index 9dbabd7d..23d960b2 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/tasks.json +++ b/apps/frontend/src/shared/i18n/locales/fr/tasks.json @@ -325,6 +325,13 @@ "badge": { "native": "Native", "bmad": "BMAD" + }, + "versionWarning": { + "title": "Avertissement de version de méthodologie", + "description": "La version installée de {{methodology}} ({{version}}) peut ne pas être entièrement compatible avec Auto Claude. Recommandé : {{minVersion}}+", + "riskNotice": "Cela pourrait causer un comportement inattendu lors de l'exécution des tâches. Vous pouvez continuer quand même ou passer à la méthodologie native.", + "continueAnyway": "Continuer quand même", + "useNative": "Utiliser Native" } }, "bulkPR": { diff --git a/apps/frontend/src/shared/types/ipc.ts b/apps/frontend/src/shared/types/ipc.ts index 81c1fb7c..ea187f9b 100644 --- a/apps/frontend/src/shared/types/ipc.ts +++ b/apps/frontend/src/shared/types/ipc.ts @@ -25,6 +25,11 @@ import type { McpHealthCheckResult, McpTestConnectionResult } from './project'; +import type { + ProjectMethodologyConfig, + MethodologyInstallResult, + MethodologyCompatibilityResult +} from './methodology'; import type { Task, TaskStatus, @@ -626,6 +631,8 @@ export interface ElectronAPI { // Shell operations openExternal: (url: string) => Promise; openTerminal: (dirPath: string) => Promise>; + /** Open user's preferred terminal with a command. Optionally cd to a directory first. */ + openTerminalWithCommand: (command: string, cwd?: string) => Promise>; // Auto Claude source environment operations getSourceEnv: () => Promise>; @@ -814,6 +821,21 @@ export interface ElectronAPI { // MCP Server health check operations checkMcpHealth: (server: CustomMcpServer) => Promise>; testMcpConnection: (server: CustomMcpServer) => Promise>; + + // Methodology Plugin operations + checkMethodologyInstalled: (projectPath: string, name: string) => Promise>; + installMethodology: (projectPath: string, name: string, version?: string) => Promise>; + getMethodologyConfig: (projectPath: string) => Promise>; + saveMethodologyConfig: (projectPath: string, config: ProjectMethodologyConfig) => Promise>; + listAvailableMethodologies: () => Promise>>; + checkMethodologyCompatibility: (name: string, version: string) => Promise>; } declare global { diff --git a/apps/frontend/src/shared/types/methodology.ts b/apps/frontend/src/shared/types/methodology.ts index 43aa9616..0c474ee4 100644 --- a/apps/frontend/src/shared/types/methodology.ts +++ b/apps/frontend/src/shared/types/methodology.ts @@ -2,6 +2,8 @@ * Methodology plugin types for task execution */ +import type { SourceType, VerificationStatus } from '../constants/methodology-sources'; + /** * Information about an available methodology plugin */ @@ -23,3 +25,62 @@ export interface MethodologyInfo { /** Install path of the methodology */ install_path?: string; } + +/** + * Project-level methodology configuration. + * Stored in .auto-claude/methodology.json + */ +export interface ProjectMethodologyConfig { + /** Name of the methodology (e.g., 'native', 'bmad') */ + name: string; + /** Installed version of the methodology */ + version: string; + /** How the methodology was installed */ + source: SourceType; + /** ISO timestamp when the methodology was installed/configured */ + installedAt?: string; + /** Package identifier for npm/github sources */ + packageName?: string; +} + +/** + * Result of a methodology installation operation + */ +export interface MethodologyInstallResult { + /** Whether the installation was successful */ + success: boolean; + /** Installed version (on success) */ + version?: string; + /** Error message (on failure) */ + error?: string; +} + +/** + * Result of a methodology version compatibility check + */ +export interface MethodologyCompatibilityResult { + /** Whether the version is compatible */ + compatible: boolean; + /** Warning message if not fully compatible */ + warning?: string; + /** Installed version being checked */ + installedVersion?: string; + /** Minimum required version */ + minVersion?: string; + /** Maximum supported version */ + maxVersion?: string; +} + +/** + * Extended methodology info with installation status + */ +export interface MethodologyWithStatus extends MethodologyInfo { + /** Whether this methodology is installed in the current project */ + isInstalled: boolean; + /** Source type (native, npm, github, local) */ + sourceType: SourceType; + /** Verification status (native, verified, community) */ + verification: VerificationStatus; + /** Compatibility status if version checked */ + compatibility?: MethodologyCompatibilityResult; +} diff --git a/apps/frontend/src/shared/types/project.ts b/apps/frontend/src/shared/types/project.ts index fe464e62..b46b6d47 100644 --- a/apps/frontend/src/shared/types/project.ts +++ b/apps/frontend/src/shared/types/project.ts @@ -26,6 +26,8 @@ export interface ProjectSettings { mainBranch?: string; /** Include CLAUDE.md instructions in agent system prompt (default: true) */ useClaudeMd?: boolean; + /** Default methodology name for this project (default: 'native') */ + methodology?: string; } export interface NotificationSettings {