diff --git a/.config/dictionary.txt b/.config/dictionary.txt index d6c0ee8b7..c08567506 100644 --- a/.config/dictionary.txt +++ b/.config/dictionary.txt @@ -48,6 +48,7 @@ devspaces eslintcache extest fqcn +genai haaaad highlightjs hostsvars diff --git a/.depcheckrc.yml b/.depcheckrc.yml index 4f5cc8403..58ed4d8b2 100644 --- a/.depcheckrc.yml +++ b/.depcheckrc.yml @@ -7,6 +7,7 @@ ignores: - "@vitest/coverage-v8" - "@vscode/test-electron" # used by @vscode/test-cli - "@vscode/vsce" + - "@vitest/coverage-v8" # used by vitest.config.ts coverage provider - cypress-multi-reporters - depcheck - electron @@ -24,3 +25,4 @@ ignores: - vscode - webpack-cli - yarn-audit-fix + - "@typescript-eslint/eslint-plugin" # used by eslint.config.mjs diff --git a/docs/llm-providers.md b/docs/llm-providers.md new file mode 100644 index 000000000..1dc262e97 --- /dev/null +++ b/docs/llm-providers.md @@ -0,0 +1,238 @@ +# LLM Provider Support for Ansible Lightspeed + +The Ansible VS Code extension supports multiple LLM providers including Red Hat's Ansible Lightspeed with watsonx Code Assistant (WCA) and Google Gemini for Ansible code generation and assistance. + +## Supported Features + +When using LLM providers, the following Ansible Lightspeed features are available: + +**Supported in Phase 1:** + +- Playbook Generation +- Role Generation +- Interactive Chat (if provider supports it) + +**Not Supported in Phase 1:** + +- Inline Task Suggestions +- Content Source Matching + +## Supported Providers + +### Google Gemini + +Direct access to Google Gemini models. + +**Configuration:** + +- Provider: `google` +- API Endpoint: `https://generativelanguage.googleapis.com/v1beta` (⚠️ **fixed, not configurable**) +- API Key: Your Google AI API key (starts with `AIza`) +- Model Name: e.g., `gemini-2.5-flash`, `gemini-1.5-pro` + +> **Note:** For Google provider, the API endpoint is automatically set and cannot be changed. + +## Setup Instructions + +### Method 1: Guided Configuration (Recommended) + +1. Open the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`) +2. Run: `Ansible Lightspeed: Configure LLM Provider` +3. Select your desired provider from the list (Google or WCA) +4. Enter the required configuration details when prompted +5. Test the connection when prompted + +### Quick Provider Selection + +1. Open VS Code Settings (`Ctrl+,` / `Cmd+,`) +2. Search for "Ansible Lightspeed Provider" +3. Select from the dropdown: `WCA` or `Google` +4. Configure the required settings based on your selection + +### Method 2: Manual Configuration + +1. Open VS Code Settings (`Ctrl+,` / `Cmd+,`) +2. Search for "Ansible Lightspeed" +3. Configure the following settings: + +**For Google Gemini:** + +```json +{ + "ansible.lightspeed.enabled": true, + "ansible.lightspeed.provider": "google", + // apiEndpoint is automatically set (not configurable) + "ansible.lightspeed.apiKey": "your-google-api-key", + "ansible.lightspeed.modelName": "gemini-2.5-flash" +} +``` + +**For WCA (default):** + +```json +{ + "ansible.lightspeed.enabled": true, + "ansible.lightspeed.provider": "wca", + "ansible.lightspeed.apiEndpoint": "https://c.ai.ansible.redhat.com" +} +``` + +### Method 3: Workspace Configuration + +Add to your workspace `.vscode/settings.json`: + +```json +{ + "ansible.lightspeed.enabled": true, + "ansible.lightspeed.provider": "google", + // apiEndpoint is automatically set (not configurable) + "ansible.lightspeed.apiKey": "${env:GOOGLE_API_KEY}", + "ansible.lightspeed.modelName": "gemini-2.5-flash" +} +``` + +## Configuration Settings + +| Setting | Description | Default | Applicable To | +| ------------------------------------ | ----------------------------------- | ------------------------------------- | ----------------------- | +| `ansible.lightspeed.enabled` | Enable/disable Ansible Lightspeed | `true` | All providers | +| `ansible.lightspeed.provider` | Provider selection | `wca` | All providers | +| `ansible.lightspeed.apiEndpoint` | API endpoint URL | `https://c.ai.ansible.redhat.com` | **WCA only** | +| `ansible.lightspeed.modelName` | Model name/ID to use | `""` | All providers | +| `ansible.lightspeed.apiKey` | API key for authentication | `""` | Google only (not WCA) | +| `ansible.lightspeed.timeout` | Request timeout in milliseconds | `30000` | All providers | +| `ansible.lightspeed.customHeaders` | Custom HTTP headers (JSON object) | `{}` | Third-party only | + +## Usage + +Once configured, LLM providers work seamlessly with existing Ansible Lightspeed features: + +### Playbook Generation + +1. Right-click in an Ansible file +2. Select "Generate Ansible Playbook with Lightspeed" +3. Enter your requirements +4. The configured LLM provider will generate the playbook + +### Role Generation + +1. Right-click in an Ansible file +2. Select "Generate Ansible Role with Lightspeed" +3. Enter your requirements +4. The configured LLM provider will generate the role structure + +### Interactive Chat + +1. Open the Ansible Lightspeed panel +2. Use the chat interface to ask Ansible-related questions +3. The LLM provider will provide Ansible-specific assistance + +## Provider Management Commands + +Access these commands via the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`): + +- `Ansible Lightspeed: Configure LLM Provider` - Guided setup +- `Ansible Lightspeed: Test Provider Connection` - Verify connectivity +- `Ansible Lightspeed: Show Provider Status` - View current configuration +- `Ansible Lightspeed: Switch Provider` - Change between WCA and LLM providers + +## Switching Between Providers + +You can easily switch between providers using the dropdown: + +1. **Open Settings:** `Ctrl+,` / `Cmd+,` +2. **Search:** "Ansible Lightspeed Provider" +3. **Select:** Choose from the dropdown (`WCA` or `Google`) +4. **Configure:** Update `apiKey` and `modelName` as needed for the selected provider + +**Or use the Command Palette:** + +1. `Ctrl+Shift+P` / `Cmd+Shift+P` +2. Run: `Ansible Lightspeed: Switch Provider` +3. Select your desired provider from the list + +## Security Considerations + +**Important Security Notes:** + +1. **API Key Storage:** API keys are stored in VS Code settings. Consider using environment variables for sensitive keys. + +2. **Data Privacy:** When using LLM providers, your Ansible code and prompts are sent to external services. Review each provider's privacy policy. + +3. **Workspace Settings:** For team projects, avoid committing API keys to version control. Use environment variables or user-specific settings. + +4. **Network Security:** Ensure your network allows HTTPS connections to the provider endpoints. + +## Troubleshooting + +### Connection Issues + +1. **Test Connection:** + + ```text + Command Palette > Ansible Lightspeed: Test Provider Connection + ``` + +2. **Check API Key:** Ensure your API key is valid and has sufficient credits/quota + +3. **Verify Endpoint:** Confirm the API endpoint URL is correct + +4. **Network Access:** Check firewall/proxy settings + +### Common Error Messages + +| Error | Solution | +| ------------------------- | -------------------------------------------------- | +| "Authentication failed" | Check your API key | +| "Rate limit exceeded" | Wait and try again, or check your quota | +| "Request timeout" | Increase timeout setting or check network | +| "Model not found" | Verify the model name is correct for your provider | + +### Debug Information + +Enable debug logging by setting: + +```json +{ + "ansible.lightspeed.debug": true +} +``` + +Check the "Ansible Support" output channel for detailed logs. + +## Model Recommendations + +### For Code Generation + +- **Google:** `gemini-2.5-flash` or `gemini-1.5-pro` + +### For Chat/Explanations + +- **Google:** `gemini-2.5-flash` (fast and cost-effective) + +## Limitations + +1. **Phase 1 Limitations:** + - No inline task suggestions + - No content source matching + - Limited to playbook and role generation + +2. **Provider-Specific:** + - Rate limits vary by provider + - Model capabilities differ + - Costs vary by usage + +3. **Authentication:** + - No Red Hat SSO integration + - No Ansible Automation Platform subscription validation + +## Support + +For issues with LLM provider integration: + +1. Check this documentation +2. Test your provider configuration +3. Review the troubleshooting section +4. File an issue on the [Ansible VS Code Extension repository](https://github.com/ansible/vscode-ansible/issues) + +For provider-specific issues (API keys, billing, model availability), contact your provider's support directly. diff --git a/mkdocs.yml b/mkdocs.yml index 7c2f550e6..0a5a8375b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -43,6 +43,7 @@ nav: - index: mcp/README.md - API Reference: mcp/api.md - Configuration: configuration.md + - LLM Providers: llm-providers.md - Developer Guide: - development/index.md - development/contributing.md diff --git a/package.json b/package.json index 05f45487a..90e357303 100644 --- a/package.json +++ b/package.json @@ -989,6 +989,7 @@ "dependencies": { "@ansible/ansible-language-server": "workspace:^", "@ansible/ansible-mcp-server": "workspace:^", + "@google/genai": "^1.29.0", "@highlightjs/vue-plugin": "2.1.0", "@primeuix/themes": "^1.2.5", "@redhat-developer/vscode-redhat-telemetry": "^0.10.2", @@ -1000,6 +1001,7 @@ "@vscode/webview-ui-toolkit": "^1.4.0", "highlight.js": "^11.11.1", "ini": "^6.0.0", + "js-yaml": "^4.1.0", "marked": "^17.0.1", "minimatch": "^10.1.1", "primevue": "4.4.1", @@ -1019,6 +1021,7 @@ "@types/chai": "^5.2.3", "@types/express": "^5.0.5", "@types/glob": "^9.0.0", + "@types/js-yaml": "^4.0.9", "@types/jsdom": "^27.0.0", "@types/lodash": "^4.17.20", "@types/minimatch": "^6.0.0", @@ -1031,6 +1034,7 @@ "@types/vscode": "^1.85.0", "@types/vscode-webview": "^1.57.5", "@types/yargs": "^17.0.35", + "@typescript-eslint/eslint-plugin": "^8.46.3", "@typescript-eslint/parser": "^8.48.0", "@vitest/coverage-v8": "^4.0.14", "@vitest/ui": "^4.0.4", diff --git a/packages/ansible-language-server/src/interfaces/extensionSettings.ts b/packages/ansible-language-server/src/interfaces/extensionSettings.ts index 615a23cd9..40316afcb 100644 --- a/packages/ansible-language-server/src/interfaces/extensionSettings.ts +++ b/packages/ansible-language-server/src/interfaces/extensionSettings.ts @@ -14,8 +14,7 @@ export interface ExtensionSettingsWithDescriptionBase { [key: string]: SettingsEntry | string | boolean; } -export interface ExtensionSettingsWithDescription - extends ExtensionSettingsWithDescriptionBase { +export interface ExtensionSettingsWithDescription extends ExtensionSettingsWithDescriptionBase { ansible: AnsibleSettingsWithDescription; completion: CompletionSettingsWithDescription; validation: ValidationSettingsWithDescription; diff --git a/packages/ansible-mcp-server/test/handlers.test.ts b/packages/ansible-mcp-server/test/handlers.test.ts index 6e6f486f2..36fc49d09 100644 --- a/packages/ansible-mcp-server/test/handlers.test.ts +++ b/packages/ansible-mcp-server/test/handlers.test.ts @@ -94,9 +94,8 @@ describe("MCP Handlers", () => { }); it("should return formatted environment information successfully", async () => { - const { getEnvironmentInfo, formatEnvironmentInfo } = await import( - "../src/tools/adeTools.js" - ); + const { getEnvironmentInfo, formatEnvironmentInfo } = + await import("../src/tools/adeTools.js"); vi.mocked(getEnvironmentInfo).mockResolvedValue({ virtualEnv: "venv", @@ -161,9 +160,8 @@ describe("MCP Handlers", () => { }); it("should setup environment successfully", async () => { - const { setupDevelopmentEnvironment } = await import( - "../src/tools/adeTools.js" - ); + const { setupDevelopmentEnvironment } = + await import("../src/tools/adeTools.js"); vi.mocked(setupDevelopmentEnvironment).mockResolvedValue({ success: true, @@ -188,9 +186,8 @@ describe("MCP Handlers", () => { }); it("should handle setup failures", async () => { - const { setupDevelopmentEnvironment } = await import( - "../src/tools/adeTools.js" - ); + const { setupDevelopmentEnvironment } = + await import("../src/tools/adeTools.js"); vi.mocked(setupDevelopmentEnvironment).mockResolvedValue({ success: false, @@ -208,9 +205,8 @@ describe("MCP Handlers", () => { }); it("should handle exceptions during setup", async () => { - const { setupDevelopmentEnvironment } = await import( - "../src/tools/adeTools.js" - ); + const { setupDevelopmentEnvironment } = + await import("../src/tools/adeTools.js"); vi.mocked(setupDevelopmentEnvironment).mockRejectedValue( new Error("Setup exception"), @@ -310,9 +306,8 @@ describe("MCP Handlers", () => { }); it("should return best practices content successfully", async () => { - const { getAgentsGuidelines } = await import( - "../src/resources/agents.js" - ); + const { getAgentsGuidelines } = + await import("../src/resources/agents.js"); const mockGuidelines = "# Ansible Coding Guidelines for AI Agents\n\n## Best Practices\n\nTest content."; @@ -328,9 +323,8 @@ describe("MCP Handlers", () => { }); it("should handle file reading errors gracefully", async () => { - const { getAgentsGuidelines } = await import( - "../src/resources/agents.js" - ); + const { getAgentsGuidelines } = + await import("../src/resources/agents.js"); vi.mocked(getAgentsGuidelines).mockRejectedValue( new Error("File not found"), @@ -348,9 +342,8 @@ describe("MCP Handlers", () => { }); it("should handle non-Error exceptions", async () => { - const { getAgentsGuidelines } = await import( - "../src/resources/agents.js" - ); + const { getAgentsGuidelines } = + await import("../src/resources/agents.js"); vi.mocked(getAgentsGuidelines).mockRejectedValue("String error"); @@ -366,9 +359,8 @@ describe("MCP Handlers", () => { }); it("should return consistent results on multiple calls", async () => { - const { getAgentsGuidelines } = await import( - "../src/resources/agents.js" - ); + const { getAgentsGuidelines } = + await import("../src/resources/agents.js"); const mockGuidelines = "# Test Guidelines\n\nContent here."; vi.mocked(getAgentsGuidelines).mockResolvedValue(mockGuidelines); @@ -382,9 +374,8 @@ describe("MCP Handlers", () => { }); it("should handle empty content", async () => { - const { getAgentsGuidelines } = await import( - "../src/resources/agents.js" - ); + const { getAgentsGuidelines } = + await import("../src/resources/agents.js"); vi.mocked(getAgentsGuidelines).mockResolvedValue(""); diff --git a/src/definitions/constants.ts b/src/definitions/constants.ts index ab844c0bc..86cef1ae5 100644 --- a/src/definitions/constants.ts +++ b/src/definitions/constants.ts @@ -63,3 +63,45 @@ export const DevcontainerImages = { export const DevcontainerRecommendedExtensions = { RECOMMENDED_EXTENSIONS: ["redhat.ansible", "redhat.vscode-redhat-account"], }; + +// Ansible-specific prompts and system instructions for LLM providers +export const ANSIBLE_SYSTEM_PROMPT_PLAYBOOK = `You are an Ansible expert. +Your role is to help Ansible developers write playbooks. +You answer with just an Ansible playbook.`; + +// For role generation (from backend: langchain/pipelines.py) +export const ANSIBLE_SYSTEM_PROMPT_ROLE = `You are an ansible expert optimized to generate Ansible roles. +First line the role name in a way: role_name. +After that the answer is a plain tasks/main.yml file for the user's request. +Prefix your comments with the hash character.`; + +// For chat/explanations +export const ANSIBLE_SYSTEM_PROMPT_CHAT = `You are Ansible Lightspeed Intelligent Assistant - an intelligent virtual assistant for question-answering tasks related to the Ansible Automation Platform (AAP). +You are an expert on all things Ansible. Provide helpful, accurate answers about Ansible. +If the context of the question is not clear, consider it to be Ansible. +Refuse to answer questions not about Ansible.`; + +// For task completion (inline suggestions) +export const ANSIBLE_SYSTEM_PROMPT_COMPLETION = + "You are an Ansible code completion assistant. Generate ONLY valid Ansible YAML task content to continue from where the input ends. Do not include explanations, markdown formatting, or complete playbooks. Only output the task YAML continuation."; + +// For playbook explanation +export const ANSIBLE_SYSTEM_PROMPT_EXPLANATION = `You're an Ansible expert. +You format your output with Markdown. +You only answer with text paragraphs. +Write one paragraph per Ansible task. +Markdown title starts with the '#' character. +Write a title before every paragraph. +Do not return any YAML or Ansible in the output. +Give a lot of details regarding the parameters of each Ansible plugin.`; + +// Template for playbook generation +export const ANSIBLE_PLAYBOOK_GENERATION_TEMPLATE = `This is what the playbook should do: {PROMPT}`; + +// Template for role generation +export const ANSIBLE_ROLE_GENERATION_TEMPLATE = `This is what the role should do: {PROMPT}`; + +// Template for playbook explanation +export const ANSIBLE_PLAYBOOK_EXPLANATION_TEMPLATE = `Please explain the following Ansible playbook: + +{PLAYBOOK}`; diff --git a/src/definitions/lightspeed.ts b/src/definitions/lightspeed.ts index 12cb9abea..ea1736daa 100644 --- a/src/definitions/lightspeed.ts +++ b/src/definitions/lightspeed.ts @@ -87,6 +87,17 @@ export const LIGHTSPEED_STATUS_BAR_CLICK_HANDLER = export const LIGHTSPEED_CLIENT_ID = "Vu2gClkeR5qUJTUGHoFAePmBznd6RZjDdy5FW2wy"; export const LIGHTSPEED_SERVICE_LOGIN_TIMEOUT = 120000; +// LLM Provider API Endpoints (fixed, not user-configurable) +export const GOOGLE_API_ENDPOINT = + "https://generativelanguage.googleapis.com/v1beta"; +export const WCA_API_ENDPOINT_DEFAULT = "https://c.ai.ansible.redhat.com"; + +// LLM Provider Default Model Names +export const GOOGLE_DEFAULT_MODEL = "gemini-2.5-flash"; + +// LLM Provider Types +export type ProviderType = "wca" | "google"; + export type LIGHTSPEED_SUGGESTION_TYPE = "SINGLE-TASK" | "MULTI-TASK"; export type LIGHTSPEED_USER_TYPE = "Licensed" | "Unlicensed" | "Not logged in"; diff --git a/src/features/lightspeed/ansibleContext.ts b/src/features/lightspeed/ansibleContext.ts new file mode 100644 index 000000000..3ba686e61 --- /dev/null +++ b/src/features/lightspeed/ansibleContext.ts @@ -0,0 +1,346 @@ +/** + * Ansible-specific context processing and prompt engineering + * Ported from ansible-ai-connect-service/ansible_ai_connect/ai/api/formatter.py + */ + +import * as yaml from "js-yaml"; + +export interface AnsibleFileType { + type: "playbook" | "tasks" | "handlers" | "vars" | "role" | "inventory"; +} + +export interface AnsibleContext { + fileType: AnsibleFileType["type"]; + documentUri?: string; + workspaceContext?: { + roles?: string[]; + collections?: string[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + variables?: Record; + }; +} + +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +export class AnsibleContextProcessor { + /** + * Apply Ansible-specific prompt engineering and context injection + */ + static enhancePromptForAnsible( + prompt: string, + context: string = "", + ansibleContext?: AnsibleContext, + ): string { + // Combine context and prompt + const fullInput = context ? `${context}\n${prompt}` : prompt; + + // Detect if this is a multi-task prompt (contains multiple task definitions) + const isMultiTask = this.isMultiTaskPrompt(prompt); + + // Apply Ansible-specific preprocessing + const processed = this.preprocessAnsibleContent( + fullInput, + ansibleContext?.fileType || "playbook", + ); + + // Add Ansible-specific system context + const systemContext = this.getAnsibleSystemContext( + ansibleContext?.fileType || "playbook", + ); + + // Handle multi-task vs single-task scenarios + if (isMultiTask) { + return `${systemContext}\n\n${processed}`; + } else { + // For single tasks, ensure proper formatting + const formattedPrompt = this.formatSingleTaskPrompt(processed); + return `${systemContext}\n\n${formattedPrompt}`; + } + } + + /** + * Get Ansible-specific system context based on file type + */ + private static getAnsibleSystemContext(fileType: string): string { + const baseContext = `You are an expert Ansible developer. Generate valid, idiomatic Ansible YAML following best practices. + +Key requirements: +- Use proper YAML syntax and indentation (2 spaces) +- Follow Ansible naming conventions +- Use fully qualified collection names (FQCN) when appropriate +- Include meaningful task names +- Use appropriate Ansible modules and parameters +- Follow security best practices`; + + const typeSpecificContext = { + playbook: ` +Generate Ansible playbook content with: +- Proper play structure with hosts, tasks, etc. +- Appropriate use of become, vars, handlers +- Well-structured task definitions`, + + tasks: ` +Generate Ansible task definitions with: +- Clear, descriptive task names +- Proper module usage and parameters +- Appropriate conditionals and loops +- Error handling where needed`, + + handlers: ` +Generate Ansible handler definitions with: +- Descriptive handler names +- Proper service/restart operations +- Appropriate listen directives`, + + role: ` +Generate Ansible role structure with: +- Proper directory organization +- Main tasks, defaults, handlers, meta +- Reusable and parameterized content`, + + vars: ` +Generate Ansible variable definitions with: +- Clear variable naming conventions +- Proper data structures +- Documentation comments`, + + inventory: ` +Generate Ansible inventory content with: +- Proper host and group definitions +- Appropriate variable assignments +- Clear organization structure`, + }; + + return ( + baseContext + + (typeSpecificContext[fileType as keyof typeof typeSpecificContext] || "") + ); + } + + /** + * Detect if prompt contains multiple task definitions + */ + private static isMultiTaskPrompt(prompt: string): boolean { + // Count occurrences of task indicators + const taskIndicators = [ + /^\s*-\s+name:/gm, // YAML list item with name + /^\s*-\s+\w+:/gm, // YAML list item with module + ]; + + let taskCount = 0; + for (const indicator of taskIndicators) { + while (indicator.exec(prompt) !== null) { + taskCount++; + } + } + + return taskCount > 1; + } + + /** + * Preprocess Ansible content for normalization + */ + private static preprocessAnsibleContent( + content: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _fileType: string, + ): string { + try { + // Parse and re-serialize to normalize YAML formatting + const parsed = yaml.load(content); + if (parsed === null || parsed === undefined) { + return content; + } + + // Re-serialize with Ansible-friendly options + const normalized = yaml.dump(parsed, { + indent: 2, + lineWidth: 120, + noRefs: true, + quotingType: '"', + forceQuotes: false, + sortKeys: false, + }); + + return normalized.trim(); + } catch (error) { + // If YAML parsing fails, return original content + console.warn("Failed to normalize Ansible YAML:", error); + return content; + } + } + + /** + * Format single task prompt with proper structure + */ + private static formatSingleTaskPrompt(prompt: string): string { + // Ensure prompt ends with proper task structure + const trimmed = prompt.trim(); + + // If it doesn't start with a task indicator, add one + const namePattern = /^\s*-\s+name:/m; + const modulePattern = /^\s*-\s+\w+:/m; + if (!namePattern.test(trimmed) && !modulePattern.test(trimmed)) { + // Check if it's just a task name or description + if (!trimmed.includes(":")) { + return `- name: ${trimmed}\n `; + } + } + + return trimmed; + } + + /** + * Extract task names from prompt for multi-task scenarios + */ + static extractTaskNames(prompt: string): string[] { + const taskNames: string[] = []; + const nameRegex = /^\s*-\s+name:\s*(.+)$/gm; + + let match; + while ((match = nameRegex.exec(prompt)) !== null) { + const name = match[1].trim(); + if (name) { + taskNames.push(name); + } + } + + return taskNames; + } + + /** + * Clean and validate Ansible output from LLM + */ + static cleanAnsibleOutput(output: string): string { + let cleaned = output.trim(); + + // Remove common LLM artifacts + cleaned = cleaned.replace(/^```ya?ml\s*/i, ""); + cleaned = cleaned.replace(/```\s*$/, ""); + cleaned = cleaned.replace(/^```\s*/, ""); + + // Remove explanatory text that might precede the YAML + const yamlStart = cleaned.search(/^\s*(-\s+name:|---|\w+:)/m); + if (yamlStart > 0) { + cleaned = cleaned.substring(yamlStart); + } + + // Ensure proper YAML structure + try { + const parsed = yaml.load(cleaned); + if (parsed !== null) { + // Re-serialize to ensure valid YAML + cleaned = yaml + .dump(parsed, { + indent: 2, + lineWidth: 120, + noRefs: true, + quotingType: '"', + forceQuotes: false, + }) + .trim(); + } + } catch (error) { + console.warn("Output validation failed, returning as-is:", error); + } + + return cleaned; + } + + /** + * Validate that output is valid Ansible content + */ + static validateAnsibleContent(content: string): { + valid: boolean; + errors: string[]; + } { + const errors: string[] = []; + + try { + const parsed = yaml.load(content); + + if (parsed === null || parsed === undefined) { + errors.push("Content is empty or invalid YAML"); + return { valid: false, errors }; + } + + // Basic Ansible structure validation + if (Array.isArray(parsed)) { + // Task list or playbook + for (const item of parsed) { + if (typeof item !== "object") { + errors.push("Invalid task or play structure"); + continue; + } + + // Check for required fields in tasks + if ( + "name" in item || + Object.keys(item).some( + (key) => key !== "name" && !key.startsWith("_"), + ) + ) { + // Looks like a task or play + continue; + } else { + errors.push("Task missing name or module"); + } + } + } else if (typeof parsed === "object") { + // Single play or role structure + if (!("hosts" in parsed || "tasks" in parsed || "main" in parsed)) { + errors.push("Invalid playbook or role structure"); + } + } + } catch (yamlError) { + errors.push(`YAML syntax error: ${yamlError}`); + } + + return { valid: errors.length === 0, errors }; + } + + /** + * Add Ansible-specific stop sequences for better completion + */ + static getAnsibleStopSequences(): string[] { + return [ + "\n\n---", // Document separator + "\n\n- hosts:", // New play + "\n\n- name:", // New task (with spacing) + "\n\nhandlers:", // Handlers section + "\nvars:", // Variables section + "\ntasks:", // Tasks section + ]; + } + + /** + * Get appropriate temperature settings for different Ansible content types + */ + static getTemperatureForFileType(fileType: string): number { + const temperatures = { + playbook: 0.1, // Low for structured content + tasks: 0.1, // Low for precise task definitions + handlers: 0.05, // Very low for handlers + vars: 0, // Deterministic for variables + role: 0.15, // Slightly higher for creative role structure + inventory: 0, // Deterministic for inventory + }; + + return temperatures[fileType as keyof typeof temperatures] || 0.1; + } + + /** + * Get max tokens based on content type + */ + static getMaxTokensForFileType(fileType: string): number { + const maxTokens = { + playbook: 2000, // Large for full playbooks + tasks: 800, // Medium for task lists + handlers: 400, // Small for handlers + vars: 600, // Medium for variable definitions + role: 2500, // Large for role structure + inventory: 1000, // Medium for inventory + }; + + return maxTokens[fileType as keyof typeof maxTokens] || 1000; + } +} diff --git a/src/features/lightspeed/explorerWebviewViewProvider.ts b/src/features/lightspeed/explorerWebviewViewProvider.ts index 620f9c09b..b73a61882 100644 --- a/src/features/lightspeed/explorerWebviewViewProvider.ts +++ b/src/features/lightspeed/explorerWebviewViewProvider.ts @@ -16,9 +16,7 @@ import { LightspeedUser } from "./lightspeedUser"; import { isPlaybook, isDocumentInRole } from "./utils/explanationUtils"; -export class LightspeedExplorerWebviewViewProvider - implements WebviewViewProvider -{ +export class LightspeedExplorerWebviewViewProvider implements WebviewViewProvider { public static readonly viewType = "lightspeed-explorer-webview"; //sessionInfo: LightspeedSessionInfo = {}; diff --git a/src/features/lightspeed/feedbackWebviewViewProvider.ts b/src/features/lightspeed/feedbackWebviewViewProvider.ts index 88cabe148..c7dab9f23 100644 --- a/src/features/lightspeed/feedbackWebviewViewProvider.ts +++ b/src/features/lightspeed/feedbackWebviewViewProvider.ts @@ -11,9 +11,7 @@ import { setWebviewMessageListener, } from "./utils/feedbackView"; -export class LightspeedFeedbackWebviewViewProvider - implements WebviewViewProvider -{ +export class LightspeedFeedbackWebviewViewProvider implements WebviewViewProvider { public static readonly viewType = "lightspeed-feedback-webview"; constructor(private readonly _extensionUri: Uri) { diff --git a/src/features/lightspeed/providers/base.ts b/src/features/lightspeed/providers/base.ts new file mode 100644 index 000000000..3177b51cd --- /dev/null +++ b/src/features/lightspeed/providers/base.ts @@ -0,0 +1,211 @@ +import { + CompletionRequestParams, + CompletionResponseParams, +} from "../../../interfaces/lightspeed"; + +export interface ProviderMetadata { + ansibleFileType?: + | "playbook" + | "tasks" + | "handlers" + | "vars" + | "role" + | "inventory"; + documentUri?: string; + workspaceContext?: string; + context?: string; + isExplanation?: boolean; +} + +export interface HttpError { + status?: number; + message?: string; +} + +export interface LLMProvider { + readonly name: string; + readonly displayName: string; + + /** + * Validate provider configuration and test connection + */ + validateConfig(): Promise; + + /** + * Get provider connection status + */ + getStatus(): Promise; + + /** + * Send completion request to the provider + */ + completionRequest( + params: CompletionRequestParams, + ): Promise; + + /** + * Send chat request to the provider + */ + chatRequest(params: ChatRequestParams): Promise; + + /** + * Generate playbook using the provider + */ + generatePlaybook( + params: GenerationRequestParams, + ): Promise; + + /** + * Generate role using the provider + */ + generateRole( + params: GenerationRequestParams, + ): Promise; +} + +export interface ProviderStatus { + connected: boolean; + error?: string; + modelInfo?: { + name: string; + version?: string; + capabilities: string[]; + }; +} + +export interface ChatRequestParams { + message: string; + conversationId?: string; + metadata?: ProviderMetadata; +} + +export interface ChatResponseParams { + message: string; + conversationId: string; + model?: string; +} + +export interface GenerationRequestParams { + prompt: string; + type: "playbook" | "role"; + createOutline?: boolean; // If true, generate outline from the result + outline?: string; // User-edited outline to refine generation + metadata?: ProviderMetadata; +} + +export interface GenerationResponseParams { + content: string; + outline?: string; // Generated outline (numbered list of steps) + model?: string; +} + +export abstract class BaseLLMProvider< + TConfig = unknown, +> implements LLMProvider { + protected config: TConfig; + protected timeout: number; + + constructor(config: TConfig, timeout: number = 30000) { + this.config = config; + this.timeout = timeout; + } + + abstract readonly name: string; + abstract readonly displayName: string; + + abstract validateConfig(): Promise; + abstract getStatus(): Promise; + abstract completionRequest( + params: CompletionRequestParams, + ): Promise; + abstract chatRequest(params: ChatRequestParams): Promise; + abstract generatePlaybook( + params: GenerationRequestParams, + ): Promise; + abstract generateRole( + params: GenerationRequestParams, + ): Promise; + + /** + * Apply Ansible-specific prompt engineering + */ + protected applyAnsibleContext( + prompt: string, + metadata?: ProviderMetadata, + ): string { + const { AnsibleContextProcessor } = require("../ansibleContext"); + + const ansibleContext = { + fileType: metadata?.ansibleFileType || "playbook", + documentUri: metadata?.documentUri, + workspaceContext: metadata?.workspaceContext, + }; + + return AnsibleContextProcessor.enhancePromptForAnsible( + prompt, + metadata?.context || "", + ansibleContext, + ); + } + + /** + * Clean and validate Ansible output + */ + protected cleanAnsibleOutput(output: string): string { + const { AnsibleContextProcessor } = require("../ansibleContext"); + return AnsibleContextProcessor.cleanAnsibleOutput(output); + } + + /** + * Handle HTTP status code errors with comprehensive error messages + * This method provides reusable error handling for common HTTP status codes + */ + + protected handleHttpError( + error: HttpError, + operation: string, + providerName: string = "", + ): Error { + const statusCode = error?.status || undefined; + + // Handle HTTP status codes + switch (statusCode) { + case 400: + return new Error( + `Bad request - invalid or malformed request parameters. Please verify your request. Operation: ${operation}. Details: ${error?.message || "Unknown error"}`, + ); + + case 403: + return new Error( + `Forbidden - ${providerName} API key does not have permission to access this resource. Please check your API key permissions. Operation: ${operation} and status code: ${statusCode}`, + ); + + case 429: + return new Error( + `Rate limit exceeded - too many requests or quota exceeded. Please wait and try again later. Operation: ${operation} and status code: ${statusCode}`, + ); + + case 500: + return new Error( + `${providerName} encountered an unexpected error. Please retry the request. Operation: ${operation}. Details: ${error?.message || "Unknown error"}`, + ); + + case 503: + return new Error( + `Service unavailable - ${providerName} is temporarily unavailable, possibly due to high load. Please wait and try again later. Operation: ${operation} and status code: ${statusCode}`, + ); + + case 504: + return new Error( + `Gateway timeout - request timed out at the gateway. Please reduce input size or retry the request. Operation: ${operation} and status code: ${statusCode}`, + ); + + default: { + const errorMessage = error?.message || "Unknown error"; + return new Error( + `${providerName} error: ${errorMessage}. Operation: ${operation}. Status: ${statusCode || "N/A"}`, + ); + } + } + } +} diff --git a/src/features/lightspeed/providers/factory.ts b/src/features/lightspeed/providers/factory.ts new file mode 100644 index 000000000..6004207ef --- /dev/null +++ b/src/features/lightspeed/providers/factory.ts @@ -0,0 +1,149 @@ +import { LLMProvider } from "./base"; +import { GoogleProvider, GoogleConfig } from "./google"; +import { LightSpeedServiceSettings } from "../../../interfaces/extensionSettings"; +import { + GOOGLE_API_ENDPOINT, + WCA_API_ENDPOINT_DEFAULT, + GOOGLE_DEFAULT_MODEL, + ProviderType, +} from "../../../definitions/lightspeed"; +import { ProviderFactory, ProviderInfo } from "../../../interfaces/lightspeed"; + +export class LLMProviderFactory implements ProviderFactory { + private static instance: LLMProviderFactory; + + public static getInstance(): LLMProviderFactory { + if (!LLMProviderFactory.instance) { + LLMProviderFactory.instance = new LLMProviderFactory(); + } + return LLMProviderFactory.instance; + } + + createProvider( + type: ProviderType, + config: LightSpeedServiceSettings, + ): LLMProvider { + switch (type) { + case "wca": + // WCA provider would be handled differently (using existing LightSpeedAPI) + throw new Error( + "WCA provider should be handled by existing LightSpeedAPI, not factory", + ); + + case "google": + if (!config.apiKey) { + throw new Error( + "API Key is required for Google Gemini. Please set 'ansible.lightspeed.apiKey' in your settings.", + ); + } + // Google doesn't support custom endpoints - uses SDK + if (config.apiEndpoint && config.apiEndpoint !== GOOGLE_API_ENDPOINT) { + throw new Error( + `Custom API endpoints are not supported for Google Gemini provider. The endpoint is automatically configured. Please remove 'ansible.lightspeed.apiEndpoint' from your settings.`, + ); + } + return new GoogleProvider({ + apiKey: config.apiKey, + modelName: config.modelName || GOOGLE_DEFAULT_MODEL, + timeout: config.timeout || 30000, + } as GoogleConfig); + + default: + throw new Error(`Unsupported provider type: ${type}`); + } + } + + getSupportedProviders(): ProviderInfo[] { + return [ + { + type: "wca", + name: "wca", + displayName: + "Red Hat Ansible Lightspeed with IBM watsonx Code Assistant", + description: + "Official Red Hat Ansible Lightspeed service with IBM watsonx Code Assistant", + defaultEndpoint: WCA_API_ENDPOINT_DEFAULT, + configSchema: [ + { + key: "apiEndpoint", + label: "Lightspeed URL", + type: "string", + required: true, + placeholder: WCA_API_ENDPOINT_DEFAULT, + description: "URL for Ansible Lightspeed service", + }, + { + key: "modelName", + label: "Model ID Override", + type: "string", + required: false, + placeholder: "Leave empty to use organization default", + description: + "Model ID to override your organization's default model (commercial users only)", + }, + ], + }, + { + type: "google", + name: "google", + displayName: "Google Gemini", + description: "Direct access to Google Gemini models", + defaultEndpoint: GOOGLE_API_ENDPOINT, + configSchema: [ + { + key: "apiKey", + label: "API Key", + type: "password", + required: true, + placeholder: "AIza...", + description: "Your Google AI API key", + }, + { + key: "modelName", + label: "Model Name", + type: "string", + required: false, + placeholder: "gemini-2.5-flash", + description: + "The Gemini model to use (optional, defaults to gemini-2.5-flash)", + }, + ], + }, + ]; + } + + validateProviderConfig( + type: ProviderType, + config: LightSpeedServiceSettings, + ): boolean { + const providerInfo = this.getSupportedProviders().find( + (p) => p.type === type, + ); + if (!providerInfo) { + return false; + } + + // Check required fields + for (const field of providerInfo.configSchema) { + if (field.required) { + const value = config[field.key as keyof LightSpeedServiceSettings]; + if (!value || (typeof value === "string" && value.trim() === "")) { + return false; + } + } + } + + // Special validation for WCA + if (type === "wca") { + // WCA requires valid endpoint but no API key + if (!config.apiEndpoint || config.apiEndpoint.trim() === "") { + return false; + } + return true; + } + + return true; + } +} + +export const providerFactory = LLMProviderFactory.getInstance(); diff --git a/src/features/lightspeed/providers/google.ts b/src/features/lightspeed/providers/google.ts new file mode 100644 index 000000000..89417f1ad --- /dev/null +++ b/src/features/lightspeed/providers/google.ts @@ -0,0 +1,316 @@ +import { GoogleGenAI } from "@google/genai"; +import { + BaseLLMProvider, + ChatRequestParams, + ChatResponseParams, + GenerationRequestParams, + GenerationResponseParams, + ProviderStatus, +} from "./base"; +import { + CompletionRequestParams, + CompletionResponseParams, +} from "../../../interfaces/lightspeed"; +import { + ANSIBLE_SYSTEM_PROMPT_PLAYBOOK, + ANSIBLE_SYSTEM_PROMPT_ROLE, + ANSIBLE_SYSTEM_PROMPT_CHAT, + ANSIBLE_SYSTEM_PROMPT_EXPLANATION, + ANSIBLE_SYSTEM_PROMPT_COMPLETION, + ANSIBLE_PLAYBOOK_GENERATION_TEMPLATE, + ANSIBLE_ROLE_GENERATION_TEMPLATE, +} from "../../../definitions/constants"; +import { getLightspeedLogger } from "../../../utils/logger"; +import { + generateOutlineFromPlaybook, + generateOutlineFromRole, +} from "../utils/outlineGenerator"; + +export interface GoogleConfig { + apiKey: string; + modelName: string; + timeout?: number; +} + +export class GoogleProvider extends BaseLLMProvider { + readonly name = "google"; + readonly displayName = "Google Gemini"; + + private readonly client: GoogleGenAI; + private readonly modelName: string; + private readonly logger = getLightspeedLogger(); + private lastValidationError?: string; + + constructor(config: GoogleConfig) { + super(config, config.timeout); + this.modelName = config.modelName; + + this.logger.info( + `[Google Provider] Initializing with model: ${this.modelName}`, + ); + + this.client = new GoogleGenAI({ apiKey: config.apiKey }); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private handleGeminiError(error: any, operation: string): Error { + // Use the reusable HTTP error handler from base class + return this.handleHttpError(error, operation, "Google Gemini"); + } + + async validateConfig(): Promise { + try { + // Try a minimal generation to validate + await this.client.models.generateContent({ + model: this.modelName, + contents: "test", + }); + this.lastValidationError = undefined; // Clear any previous error + return true; + } catch (error) { + const errorMsg = `Config validation failed: ${error instanceof Error ? error.message : "Unknown error"}`; + this.logger.error(`[Google Provider] ${errorMsg}`); + this.lastValidationError = errorMsg; + return false; + } + } + + async getStatus(): Promise { + try { + const isValid = await this.validateConfig(); + if (!isValid) { + return { + connected: false, + error: + this.lastValidationError || + "Failed to connect to Google Gemini API. Check your API key.", + }; + } + + return { + connected: true, + modelInfo: { + name: this.modelName, + capabilities: ["completion", "chat", "generation"], + }, + }; + } catch (error) { + return { + connected: false, + error: error instanceof Error ? error.message : "Unknown error", + }; + } + } + + async completionRequest( + params: CompletionRequestParams, + ): Promise { + try { + this.logger.info( + `[Google Provider] Request params: ${JSON.stringify(params, null, 2)}`, + ); + this.logger.info(`[Google Provider] Model: ${this.modelName}`); + this.logger.info(`[Google Provider] Prompt:\n${params.prompt}`); + // For inline completion, use a minimal system instruction to guide the model + // to generate only valid Ansible YAML without explanations + const result = await this.client.models.generateContent({ + model: this.modelName, + contents: params.prompt, + config: { + systemInstruction: ANSIBLE_SYSTEM_PROMPT_COMPLETION, + }, + }); + + const text = result.text || ""; + + this.logger.info(`[Google Provider] Raw response:\n${text}`); + + // For inline completion, keep the response as-is to preserve indentation + // Only remove common markdown code fences if present + let suggestion = text.trim(); + suggestion = suggestion.replace(/^```ya?ml\s*/i, ""); + suggestion = suggestion.replace(/```\s*$/, ""); + + const result_data = { + predictions: [suggestion], + model: this.modelName, + suggestionId: params.suggestionId || "google-" + Date.now().toString(), + }; + + return result_data; + } catch (error) { + this.logger.error( + `[Google Provider] Completion request failed: ${error}`, + ); + this.logger.error( + `[Google Provider] Error stack: ${error instanceof Error ? error.stack : "No stack trace"}`, + ); + throw new Error( + `Google completion failed: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } + } + + async chatRequest(params: ChatRequestParams): Promise { + try { + const enhancedMessage = this.applyAnsibleContext( + params.message, + params.metadata, + ); + + // Log full request + this.logger.info( + `[Google Provider] Full params: ${JSON.stringify(params, null, 2)}`, + ); + + // Use explanation-specific system prompt if this is an explanation request + const isExplanation = params.metadata?.isExplanation === true; + const systemInstruction = isExplanation + ? ANSIBLE_SYSTEM_PROMPT_EXPLANATION + : ANSIBLE_SYSTEM_PROMPT_CHAT; + + this.logger.info( + `[Google Provider] Using system prompt: ${isExplanation ? "EXPLANATION" : "CHAT"}`, + ); + + const result = await this.client.models.generateContent({ + model: this.modelName, + contents: enhancedMessage, + config: { + systemInstruction: systemInstruction, + }, + }); + + const message = result.text || ""; + + // Log full response + this.logger.info(`[Google Provider] Full response:\n${message}`); + + return { + message: message, + conversationId: params.conversationId || "default", + model: this.modelName, + }; + } catch (error) { + this.logger.error(`[Google Provider] Chat request failed: ${error}`); + throw this.handleGeminiError(error, "chat generation"); + } + } + + async generatePlaybook( + params: GenerationRequestParams, + ): Promise { + try { + let promptText = params.prompt; + + // If outline is provided (second call), incorporate it into the prompt + if (params.outline) { + promptText = `${params.prompt}\n\nGenerate the playbook with these specific steps:\n${params.outline}`; + } + + const playbookPrompt = ANSIBLE_PLAYBOOK_GENERATION_TEMPLATE.replace( + "{PROMPT}", + promptText, + ); + + // Log full request + this.logger.info( + `[Google Provider] Full params: ${JSON.stringify(params, null, 2)}`, + ); + if (params.outline) { + this.logger.info(`[Google Provider] User outline: ${params.outline}`); + } + + const enhancedPrompt = this.applyAnsibleContext(playbookPrompt, { + ansibleFileType: "playbook", + }); + + const result = await this.client.models.generateContent({ + model: this.modelName, + contents: enhancedPrompt, + config: { + systemInstruction: ANSIBLE_SYSTEM_PROMPT_PLAYBOOK, + temperature: 0.3, + maxOutputTokens: 4000, + }, + }); + + const content = result.text || ""; + const cleanedContent = this.cleanAnsibleOutput(content); + + this.logger.info( + `[Google Provider] Generated playbook (full):\n${cleanedContent}`, + ); + + // Generate outline from the playbook if requested + let outline = ""; + if (params.createOutline) { + outline = generateOutlineFromPlaybook(cleanedContent); + this.logger.info(`[Google Provider] Generated outline: ${outline}`); + } + + return { + content: cleanedContent, + outline: outline, + model: this.modelName, + }; + } catch (error) { + this.logger.error( + `[Google Provider] Playbook generation failed: ${error}`, + ); + throw this.handleGeminiError(error, "playbook generation"); + } + } + + async generateRole( + params: GenerationRequestParams, + ): Promise { + try { + let promptText = params.prompt; + + // If outline is provided (second call), incorporate it into the prompt + if (params.outline) { + promptText = `${params.prompt}\n\nGenerate the role with these specific steps:\n${params.outline}`; + } + + const rolePrompt = ANSIBLE_ROLE_GENERATION_TEMPLATE.replace( + "{PROMPT}", + promptText, + ); + + const result = await this.client.models.generateContent({ + model: this.modelName, + contents: this.applyAnsibleContext(rolePrompt, { + ansibleFileType: "tasks", + }), + config: { + systemInstruction: ANSIBLE_SYSTEM_PROMPT_ROLE, + temperature: 0.3, + maxOutputTokens: 4000, + }, + }); + + const content = result.text || ""; + const cleanedContent = this.cleanAnsibleOutput(content); + + this.logger.info( + `[Google Provider] Generated role (full):\n${cleanedContent}`, + ); + + // Generate outline from the role if requested + let outline = ""; + if (params.createOutline) { + outline = generateOutlineFromRole(cleanedContent); + this.logger.info(`[Google Provider] Generated outline: ${outline}`); + } + + return { + content: cleanedContent, + outline: outline, + model: this.modelName, + }; + } catch (error) { + this.logger.error(`[Google Provider] Role generation failed: ${error}`); + throw this.handleGeminiError(error, "role generation"); + } + } +} diff --git a/src/features/lightspeed/utils/outlineGenerator.ts b/src/features/lightspeed/utils/outlineGenerator.ts new file mode 100644 index 000000000..fb14217c9 --- /dev/null +++ b/src/features/lightspeed/utils/outlineGenerator.ts @@ -0,0 +1,110 @@ +import * as yaml from "yaml"; + +/** + * Extract task names from a task list + */ +function extractTaskNames(taskList: unknown[]): string[] { + const taskNames: string[] = []; + for (const task of taskList) { + if (task && typeof task === "object" && "name" in task && task.name) { + taskNames.push(task.name as string); + } + } + return taskNames; +} + +/** + * Extract tasks from a single play + */ +function extractTasksFromPlay(play: Record): string[] { + const tasks: string[] = []; + + // Extract from tasks + if (play.tasks && Array.isArray(play.tasks)) { + tasks.push(...extractTaskNames(play.tasks)); + } + + // Extract from pre_tasks + if (play.pre_tasks && Array.isArray(play.pre_tasks)) { + tasks.push(...extractTaskNames(play.pre_tasks)); + } + + // Extract from post_tasks + if (play.post_tasks && Array.isArray(play.post_tasks)) { + tasks.push(...extractTaskNames(play.post_tasks)); + } + + return tasks; +} + +/** + * Generate an outline (numbered list of tasks) from a playbook YAML + * This mimics the WCA backend behavior for createOutline + */ +export function generateOutlineFromPlaybook(playbookYaml: string): string { + try { + const parsed = yaml.parse(playbookYaml); + + if (!parsed || !Array.isArray(parsed)) { + return ""; + } + + const tasks: string[] = []; + + // Extract tasks from all plays + for (const play of parsed) { + if (play && typeof play === "object") { + tasks.push(...extractTasksFromPlay(play as Record)); + } + } + + // Format as numbered list + return tasks.map((task, index) => `${index + 1}. ${task}`).join("\n"); + } catch (error) { + console.error("[Outline Generator] Failed to parse playbook:", error); + return ""; + } +} + +/** + * Generate an outline from role tasks YAML + */ +export function generateOutlineFromRole(roleYaml: string): string { + try { + const parsed = yaml.parse(roleYaml); + + if (!parsed || !Array.isArray(parsed)) { + return ""; + } + + const tasks: string[] = []; + + // Extract task names + for (const task of parsed) { + if (task.name) { + tasks.push(task.name); + } + } + + // Format as numbered list + return tasks.map((task, index) => `${index + 1}. ${task}`).join("\n"); + } catch (error) { + console.error("[Outline Generator] Failed to parse role:", error); + return ""; + } +} + +/** + * Regenerate playbook with refined outline + * Extracts the task descriptions from the numbered outline + */ +export function parseOutlineToTaskList(outline: string): string[] { + return outline + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.length > 0) + .map((line) => { + // Remove leading number and dot (e.g., "1. Task name" -> "Task name") + return line.replace(/^\d+\.\s*/, ""); + }); +} diff --git a/src/interfaces/extensionSettings.ts b/src/interfaces/extensionSettings.ts index df2c18536..e77758452 100644 --- a/src/interfaces/extensionSettings.ts +++ b/src/interfaces/extensionSettings.ts @@ -2,6 +2,8 @@ export type IPullPolicy = "always" | "missing" | "never" | "tag"; export type IContainerEngine = "auto" | "podman" | "docker"; +export type ProviderType = "wca" | "google"; + export interface ExtensionSettings { activationScript: string | undefined; interpreterPath: string | undefined; @@ -42,9 +44,15 @@ export interface UserResponse { // Settings appear on VS Code Settings UI export interface LightSpeedServiceSettings { enabled: boolean; + provider: ProviderType; URL: string; + apiEndpoint: string; + modelName: string | undefined; + model: string | undefined; // Legacy field for backwards compatibility + apiKey: string; // For third-party providers like Google + timeout: number; // Request timeout in milliseconds + customHeaders: Record; // Custom headers for third-party providers suggestions: { enabled: boolean; waitWindow: number }; - model: string | undefined; playbookGenerationCustomPrompt: string | undefined; playbookExplanationCustomPrompt: string | undefined; } diff --git a/src/interfaces/lightspeed.ts b/src/interfaces/lightspeed.ts index 4f3fde046..cfcbc937f 100644 --- a/src/interfaces/lightspeed.ts +++ b/src/interfaces/lightspeed.ts @@ -4,7 +4,10 @@ import { WizardGenerationActionType, ThumbsUpDownAction, UserAction, + ProviderType, } from "../definitions/lightspeed"; +import type { LightSpeedServiceSettings } from "./extensionSettings"; +import type { LLMProvider } from "../features/lightspeed/providers/base"; export interface LightspeedAuthSession extends AuthenticationSession { rhOrgHasSubscription: boolean; @@ -288,3 +291,34 @@ export interface LightspeedSessionInfo { userInfo?: LightspeedSessionUserInfo; modelInfo?: LightspeedSessionModelInfo; } + +// LLM Provider Factory Interfaces +export interface ConfigField { + key: string; + label: string; + type: "string" | "password" | "number" | "boolean"; + required: boolean; + placeholder?: string; + description?: string; +} + +export interface ProviderInfo { + type: ProviderType; + name: string; + displayName: string; + description: string; + configSchema: ConfigField[]; + defaultEndpoint?: string; +} + +export interface ProviderFactory { + createProvider( + type: ProviderType, + config: LightSpeedServiceSettings, + ): LLMProvider; + getSupportedProviders(): ProviderInfo[]; + validateProviderConfig( + type: ProviderType, + config: LightSpeedServiceSettings, + ): boolean; +} diff --git a/src/settings.ts b/src/settings.ts index 43904dd08..32f107037 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -43,14 +43,20 @@ export class SettingsManager { }, lightSpeedService: { enabled: lightSpeedSettings.get("enabled", true), + provider: lightSpeedSettings.get("provider", "wca"), URL: lightSpeedSettings.get("URL", "https://c.ai.ansible.redhat.com"), + apiEndpoint: lightSpeedSettings.get("apiEndpoint", ""), + modelName: lightSpeedSettings.get("modelName", undefined), + model: lightSpeedSettings.get("modelIdOverride", undefined), + apiKey: lightSpeedSettings.get("apiKey", ""), + timeout: lightSpeedSettings.get("timeout", 30000), + customHeaders: lightSpeedSettings.get("customHeaders", {}), suggestions: { enabled: lightSpeedSettings.get("enabled") === true && lightSpeedSettings.get("suggestions.enabled", true), waitWindow: lightSpeedSettings.get("suggestions.waitWindow", 0), }, - model: lightSpeedSettings.get("modelIdOverride", undefined), playbookGenerationCustomPrompt: lightSpeedSettings.get( "playbookGenerationCustomPrompt", undefined, diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 0de662d18..303153b79 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -34,3 +34,11 @@ export class Log { this.output.warn(message); } } + +// Singleton instance for all Lightspeed features to share the same output channel +let lightspeedLoggerInstance: Log | null = null; + +export function getLightspeedLogger(): Log { + lightspeedLoggerInstance ??= new Log(); + return lightspeedLoggerInstance; +} diff --git a/test/unit/vitest/lightspeed/ansibleContext.test.ts b/test/unit/vitest/lightspeed/ansibleContext.test.ts new file mode 100644 index 000000000..562fdce32 --- /dev/null +++ b/test/unit/vitest/lightspeed/ansibleContext.test.ts @@ -0,0 +1,611 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { AnsibleContextProcessor } from "../../../../src/features/lightspeed/ansibleContext.js"; +import type { AnsibleContext } from "../../../../src/features/lightspeed/ansibleContext.js"; +import { ANSIBLE_CONTENT, TEST_PROMPTS } from "./testConstants.js"; + +describe("AnsibleContextProcessor", () => { + // Mock console.warn to avoid noise in test output + beforeEach(() => { + vi.spyOn(console, "warn").mockImplementation(() => { + // Intentionally empty - suppresses console.warn in tests + }); + }); + + describe("enhancePromptForAnsible", () => { + it("should enhance prompt with system context for playbook", () => { + const prompt = TEST_PROMPTS.INSTALL_NGINX; + const context: AnsibleContext = { + fileType: "playbook", + }; + + const result = AnsibleContextProcessor.enhancePromptForAnsible( + prompt, + "", + context, + ); + + expect(result).toContain("You are an expert Ansible developer"); + expect(result).toContain("Generate Ansible playbook content"); + expect(result).toContain(prompt); + }); + + it("should enhance prompt with system context for tasks", () => { + const prompt = TEST_PROMPTS.CREATE_TASK; + const context: AnsibleContext = { + fileType: "tasks", + }; + + const result = AnsibleContextProcessor.enhancePromptForAnsible( + prompt, + "", + context, + ); + + expect(result).toContain("Generate Ansible task definitions"); + expect(result).toContain(prompt); + }); + + it("should enhance prompt with system context for handlers", () => { + const prompt = "Create a handler"; + const context: AnsibleContext = { + fileType: "handlers", + }; + + const result = AnsibleContextProcessor.enhancePromptForAnsible( + prompt, + "", + context, + ); + + expect(result).toContain("Generate Ansible handler definitions"); + }); + + it("should enhance prompt with system context for role", () => { + const prompt = TEST_PROMPTS.CREATE_ROLE; + const context: AnsibleContext = { + fileType: "role", + }; + + const result = AnsibleContextProcessor.enhancePromptForAnsible( + prompt, + "", + context, + ); + + expect(result).toContain("Generate Ansible role structure"); + }); + + it("should enhance prompt with system context for vars", () => { + const prompt = "Define variables"; + const context: AnsibleContext = { + fileType: "vars", + }; + + const result = AnsibleContextProcessor.enhancePromptForAnsible( + prompt, + "", + context, + ); + + expect(result).toContain("Generate Ansible variable definitions"); + }); + + it("should enhance prompt with system context for inventory", () => { + const prompt = "Create inventory"; + const context: AnsibleContext = { + fileType: "inventory", + }; + + const result = AnsibleContextProcessor.enhancePromptForAnsible( + prompt, + "", + context, + ); + + expect(result).toContain("Generate Ansible inventory content"); + }); + + it("should use default playbook fileType when context is not provided", () => { + const prompt = TEST_PROMPTS.INSTALL_NGINX; + + const result = AnsibleContextProcessor.enhancePromptForAnsible( + prompt, + "", + undefined, + ); + + expect(result).toContain("Generate Ansible playbook content"); + }); + + it("should combine context and prompt when context is provided", () => { + const prompt = TEST_PROMPTS.INSTALL_NGINX; + const contextString = "Previous context here"; + const context: AnsibleContext = { + fileType: "playbook", + }; + + const result = AnsibleContextProcessor.enhancePromptForAnsible( + prompt, + contextString, + context, + ); + + expect(result).toContain(contextString); + expect(result).toContain(prompt); + }); + + it("should handle multi-task prompts correctly", () => { + const prompt = ANSIBLE_CONTENT.MULTI_TASK; + const context: AnsibleContext = { + fileType: "tasks", + }; + + const result = AnsibleContextProcessor.enhancePromptForAnsible( + prompt, + "", + context, + ); + + expect(result).toContain("Generate Ansible task definitions"); + expect(result).toContain("Task one"); + expect(result).toContain("Task two"); + }); + + it("should format single task prompt correctly", () => { + const prompt = "Install nginx"; + const context: AnsibleContext = { + fileType: "tasks", + }; + + const result = AnsibleContextProcessor.enhancePromptForAnsible( + prompt, + "", + context, + ); + + expect(result).toContain("- name: Install nginx"); + }); + + it("should handle prompt that already has task structure", () => { + const prompt = ANSIBLE_CONTENT.SINGLE_TASK; + const context: AnsibleContext = { + fileType: "tasks", + }; + + const result = AnsibleContextProcessor.enhancePromptForAnsible( + prompt, + "", + context, + ); + + expect(result).toContain("- name: Install nginx"); + }); + + it("should handle prompt that already has task structure with module pattern", () => { + const prompt = "- ansible.builtin.debug:\n msg: test"; + const context: AnsibleContext = { + fileType: "tasks", + }; + + const result = AnsibleContextProcessor.enhancePromptForAnsible( + prompt, + "", + context, + ); + + expect(result).toContain("- ansible.builtin.debug:"); + }); + + it("should handle empty context string", () => { + const prompt = TEST_PROMPTS.INSTALL_NGINX; + const context: AnsibleContext = { + fileType: "playbook", + }; + + const result = AnsibleContextProcessor.enhancePromptForAnsible( + prompt, + "", + context, + ); + + expect(result).toContain(prompt); + }); + + it("should handle YAML that parses to null", () => { + const prompt = "null"; + const context: AnsibleContext = { + fileType: "playbook", + }; + + const result = AnsibleContextProcessor.enhancePromptForAnsible( + prompt, + "", + context, + ); + + expect(result).toContain("null"); + }); + + it("should handle YAML parsing errors gracefully", () => { + const prompt = "invalid: [unclosed bracket"; + const context: AnsibleContext = { + fileType: "playbook", + }; + + const result = AnsibleContextProcessor.enhancePromptForAnsible( + prompt, + "", + context, + ); + + expect(result).toContain("invalid: [unclosed bracket"); + expect(console.warn).toHaveBeenCalled(); + }); + }); + + describe("extractTaskNames", () => { + it("should extract task names from multi-task prompt", () => { + const prompt = ANSIBLE_CONTENT.MULTI_TASK; + const taskNames = AnsibleContextProcessor.extractTaskNames(prompt); + + expect(taskNames).toHaveLength(2); + expect(taskNames).toContain("Task one"); + expect(taskNames).toContain("Task two"); + }); + + it("should extract single task name", () => { + const prompt = ANSIBLE_CONTENT.SINGLE_TASK; + const taskNames = AnsibleContextProcessor.extractTaskNames(prompt); + + expect(taskNames).toHaveLength(1); + expect(taskNames[0]).toBe("Install nginx"); + }); + + it("should return empty array when no task names found", () => { + const prompt = "Just some text without tasks"; + const taskNames = AnsibleContextProcessor.extractTaskNames(prompt); + + expect(taskNames).toHaveLength(0); + }); + + it("should handle task names with quotes", () => { + const prompt = + '- name: "Task with quotes"\n ansible.builtin.debug:\n msg: "test"'; + const taskNames = AnsibleContextProcessor.extractTaskNames(prompt); + + expect(taskNames).toHaveLength(1); + expect(taskNames[0]).toBe('"Task with quotes"'); + }); + + it("should handle task names with special characters", () => { + const prompt = + "- name: Task (with) [special] chars\n ansible.builtin.debug:\n msg: test"; + const taskNames = AnsibleContextProcessor.extractTaskNames(prompt); + + expect(taskNames).toHaveLength(1); + expect(taskNames[0]).toBe("Task (with) [special] chars"); + }); + + it("should handle empty string", () => { + const taskNames = AnsibleContextProcessor.extractTaskNames(""); + + expect(taskNames).toHaveLength(0); + }); + + it("should handle task names with indentation", () => { + const prompt = + " - name: Indented task\n ansible.builtin.debug:\n msg: test"; + const taskNames = AnsibleContextProcessor.extractTaskNames(prompt); + + expect(taskNames).toHaveLength(1); + expect(taskNames[0]).toBe("Indented task"); + }); + }); + + describe("cleanAnsibleOutput", () => { + it("should remove YAML code block markers", () => { + const output = ANSIBLE_CONTENT.YAML_WITH_CODE_BLOCK; + const cleaned = AnsibleContextProcessor.cleanAnsibleOutput(output); + + expect(cleaned).not.toContain("```yaml"); + expect(cleaned).not.toContain("```"); + expect(cleaned).toContain("- name: Test task"); + }); + + it("should remove explanatory text before YAML", () => { + const output = ANSIBLE_CONTENT.YAML_WITH_EXPLANATION; + const cleaned = AnsibleContextProcessor.cleanAnsibleOutput(output); + + expect(cleaned).not.toContain("Here's the playbook:"); + expect(cleaned).toContain("- name: Test task"); + }); + + it("should normalize YAML formatting", () => { + const output = "---\n- name: Test\n debug:\n msg: hello"; + const cleaned = AnsibleContextProcessor.cleanAnsibleOutput(output); + + expect(cleaned).toBeTruthy(); + expect(cleaned).toContain("- name: Test"); + }); + + it("should handle valid YAML without code blocks", () => { + const output = ANSIBLE_CONTENT.SINGLE_TASK; + const cleaned = AnsibleContextProcessor.cleanAnsibleOutput(output); + + expect(cleaned).toContain("- name: Install nginx"); + }); + + it("should handle empty string", () => { + const cleaned = AnsibleContextProcessor.cleanAnsibleOutput(""); + + expect(cleaned).toBe(""); + }); + + it("should handle whitespace-only string", () => { + const cleaned = AnsibleContextProcessor.cleanAnsibleOutput(" \n \t "); + + expect(cleaned).toBe(""); + }); + + it("should handle invalid YAML gracefully", () => { + const output = ANSIBLE_CONTENT.INVALID_YAML; + const cleaned = AnsibleContextProcessor.cleanAnsibleOutput(output); + + // Should return the original content (or cleaned version) even if YAML is invalid + expect(cleaned).toBeTruthy(); + }); + + it("should remove multiple code block markers", () => { + const output = "```yaml\n- name: Test\n```"; + const cleaned = AnsibleContextProcessor.cleanAnsibleOutput(output); + + // The implementation removes code blocks at start/end, not all occurrences + // After YAML parsing and re-serialization, code blocks should be removed + expect(cleaned).not.toContain("```yaml"); + expect(cleaned).not.toContain("```"); + expect(cleaned).toContain("- name: Test"); + }); + + it("should handle YAML with leading whitespace", () => { + const output = " \n - name: Test\n debug:\n msg: test"; + const cleaned = AnsibleContextProcessor.cleanAnsibleOutput(output); + + expect(cleaned).toContain("- name: Test"); + }); + }); + + describe("validateAnsibleContent", () => { + it("should validate valid playbook structure", () => { + const content = ANSIBLE_CONTENT.PLAYBOOK; + const result = AnsibleContextProcessor.validateAnsibleContent(content); + + expect(result.valid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it("should validate valid task list", () => { + const content = ANSIBLE_CONTENT.SINGLE_TASK; + const result = AnsibleContextProcessor.validateAnsibleContent(content); + + expect(result.valid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it("should validate valid multi-task list", () => { + const content = ANSIBLE_CONTENT.MULTI_TASK; + const result = AnsibleContextProcessor.validateAnsibleContent(content); + + expect(result.valid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it("should reject invalid YAML syntax", () => { + const content = ANSIBLE_CONTENT.INVALID_YAML; + const result = AnsibleContextProcessor.validateAnsibleContent(content); + + expect(result.valid).toBe(false); + expect(result.errors.length).toBeGreaterThan(0); + expect(result.errors[0]).toContain("YAML syntax error"); + }); + + it("should reject empty content", () => { + const content = ANSIBLE_CONTENT.EMPTY; + const result = AnsibleContextProcessor.validateAnsibleContent(content); + + expect(result.valid).toBe(false); + expect(result.errors.length).toBeGreaterThan(0); + expect(result.errors[0]).toContain("empty or invalid"); + }); + + it("should reject null YAML", () => { + const content = ANSIBLE_CONTENT.NULL_YAML; + const result = AnsibleContextProcessor.validateAnsibleContent(content); + + expect(result.valid).toBe(false); + expect(result.errors.length).toBeGreaterThan(0); + }); + + it("should validate playbook with hosts", () => { + const content = + "---\n- hosts: all\n tasks:\n - name: Test\n debug:\n msg: test"; + const result = AnsibleContextProcessor.validateAnsibleContent(content); + + expect(result.valid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it("should validate role structure", () => { + const content = "---\nmain: []"; + const result = AnsibleContextProcessor.validateAnsibleContent(content); + + expect(result.valid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it("should reject invalid task structure", () => { + const content = "---\n- invalid: task\n no_name: true"; + const result = AnsibleContextProcessor.validateAnsibleContent(content); + + // May or may not be valid depending on implementation + expect(result).toHaveProperty("valid"); + expect(result).toHaveProperty("errors"); + }); + + it("should handle array of non-objects", () => { + const content = "---\n- string\n- 123\n- true"; + const result = AnsibleContextProcessor.validateAnsibleContent(content); + + expect(result.valid).toBe(false); + expect(result.errors.length).toBeGreaterThan(0); + }); + + it("should reject task missing name or module", () => { + const content = "---\n- _meta: {}\n _some_other: value"; + const result = AnsibleContextProcessor.validateAnsibleContent(content); + + expect(result.valid).toBe(false); + expect(result.errors).toContain("Task missing name or module"); + }); + + it("should reject invalid playbook or role structure", () => { + const content = "---\ninvalid_key: value\nanother_key: test"; + const result = AnsibleContextProcessor.validateAnsibleContent(content); + + expect(result.valid).toBe(false); + expect(result.errors).toContain("Invalid playbook or role structure"); + }); + }); + + describe("getAnsibleStopSequences", () => { + it("should return array of stop sequences", () => { + const sequences = AnsibleContextProcessor.getAnsibleStopSequences(); + + expect(Array.isArray(sequences)).toBe(true); + expect(sequences.length).toBeGreaterThan(0); + }); + + it("should include document separator", () => { + const sequences = AnsibleContextProcessor.getAnsibleStopSequences(); + + expect(sequences).toContain("\n\n---"); + }); + + it("should include new play indicator", () => { + const sequences = AnsibleContextProcessor.getAnsibleStopSequences(); + + expect(sequences).toContain("\n\n- hosts:"); + }); + + it("should include new task indicator", () => { + const sequences = AnsibleContextProcessor.getAnsibleStopSequences(); + + expect(sequences).toContain("\n\n- name:"); + }); + + it("should include handlers section", () => { + const sequences = AnsibleContextProcessor.getAnsibleStopSequences(); + + expect(sequences).toContain("\n\nhandlers:"); + }); + + it("should include vars section", () => { + const sequences = AnsibleContextProcessor.getAnsibleStopSequences(); + + expect(sequences).toContain("\nvars:"); + }); + + it("should include tasks section", () => { + const sequences = AnsibleContextProcessor.getAnsibleStopSequences(); + + expect(sequences).toContain("\ntasks:"); + }); + + it("should return consistent results on multiple calls", () => { + const sequences1 = AnsibleContextProcessor.getAnsibleStopSequences(); + const sequences2 = AnsibleContextProcessor.getAnsibleStopSequences(); + + expect(sequences1).toEqual(sequences2); + }); + }); + + describe("getTemperatureForFileType", () => { + it("should return correct temperature for playbook", () => { + const temp = + AnsibleContextProcessor.getTemperatureForFileType("playbook"); + + expect(temp).toBe(0.1); + }); + + it("should return correct temperature for tasks", () => { + const temp = AnsibleContextProcessor.getTemperatureForFileType("tasks"); + + expect(temp).toBe(0.1); + }); + + it("should return correct temperature for handlers", () => { + const temp = + AnsibleContextProcessor.getTemperatureForFileType("handlers"); + + expect(temp).toBe(0.05); + }); + + it("should return correct temperature for role", () => { + const temp = AnsibleContextProcessor.getTemperatureForFileType("role"); + + expect(temp).toBe(0.15); + }); + + it("should return default temperature for unknown file type", () => { + const temp = AnsibleContextProcessor.getTemperatureForFileType("unknown"); + + expect(temp).toBe(0.1); + }); + }); + + describe("getMaxTokensForFileType", () => { + it("should return correct max tokens for playbook", () => { + const tokens = + AnsibleContextProcessor.getMaxTokensForFileType("playbook"); + + expect(tokens).toBe(2000); + }); + + it("should return correct max tokens for tasks", () => { + const tokens = AnsibleContextProcessor.getMaxTokensForFileType("tasks"); + + expect(tokens).toBe(800); + }); + + it("should return correct max tokens for handlers", () => { + const tokens = + AnsibleContextProcessor.getMaxTokensForFileType("handlers"); + + expect(tokens).toBe(400); + }); + + it("should return correct max tokens for vars", () => { + const tokens = AnsibleContextProcessor.getMaxTokensForFileType("vars"); + + expect(tokens).toBe(600); + }); + + it("should return correct max tokens for role", () => { + const tokens = AnsibleContextProcessor.getMaxTokensForFileType("role"); + + expect(tokens).toBe(2500); + }); + + it("should return correct max tokens for inventory", () => { + const tokens = + AnsibleContextProcessor.getMaxTokensForFileType("inventory"); + + expect(tokens).toBe(1000); + }); + + it("should return default max tokens for unknown file type", () => { + const tokens = AnsibleContextProcessor.getMaxTokensForFileType("unknown"); + + expect(tokens).toBe(1000); + }); + }); +}); diff --git a/test/unit/vitest/lightspeed/providers/base.test.ts b/test/unit/vitest/lightspeed/providers/base.test.ts new file mode 100644 index 000000000..3bf220820 --- /dev/null +++ b/test/unit/vitest/lightspeed/providers/base.test.ts @@ -0,0 +1,448 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import * as Module from "module"; + +// Create mock implementations +const mockEnhancePromptForAnsible = vi.fn( + (prompt: string, context?: string) => { + return `enhanced: ${prompt} with context: ${context || "none"}`; + }, +); + +const mockCleanAnsibleOutput = vi.fn((output: string) => { + return output + .trim() + .replace(/^```ya?ml\s*/i, "") + .replace(/```\s*$/, ""); +}); + +const mockAnsibleContextModule = { + AnsibleContextProcessor: { + enhancePromptForAnsible: mockEnhancePromptForAnsible, + cleanAnsibleOutput: mockCleanAnsibleOutput, + }, +}; + +// Store original require as the import uses require() +const originalRequire = Module.prototype.require.bind(Module.prototype); + +Module.prototype.require = function ( + this: Module, + id: string, +): ReturnType { + // Intercept the require call for "../ansibleContext" + const normalizedId = id.replace(/\\/g, "/"); + if ( + id === "../ansibleContext" || + normalizedId === "../ansibleContext" || + normalizedId.endsWith("/ansibleContext") || + normalizedId.endsWith("/ansibleContext.js") || + normalizedId.includes("/ansibleContext") || + normalizedId.includes("ansibleContext.js") + ) { + return mockAnsibleContextModule; + } + // For all other requires, use the original + return originalRequire.call(this, id); +}; + +// Reset mocks before each test +beforeEach(() => { + vi.clearAllMocks(); + // Reset the mock implementations + mockEnhancePromptForAnsible.mockImplementation( + (prompt: string, context?: string) => { + return `enhanced: ${prompt} with context: ${context || "none"}`; + }, + ); + mockCleanAnsibleOutput.mockImplementation((output: string) => { + return output + .trim() + .replace(/^```ya?ml\s*/i, "") + .replace(/```\s*$/, ""); + }); +}); + +import { + BaseLLMProvider, + ProviderStatus, + ChatRequestParams, + ChatResponseParams, + GenerationRequestParams, + GenerationResponseParams, +} from "../../../../../src/features/lightspeed/providers/base.js"; +import { + CompletionRequestParams, + CompletionResponseParams, +} from "../../../../../src/interfaces/lightspeed.js"; +import { + TEST_PROVIDER_INFO, + TEST_PROMPTS, + TEST_OPERATIONS, + HTTP_STATUS_CODES, + DEFAULT_TIMEOUTS, + TEST_CONFIGS, +} from "../testConstants.js"; + +// This is needed because BaseLLMProvider is abstract and cannot be instantiated directly +class TestProvider extends BaseLLMProvider { + readonly name = "test"; + readonly displayName = "Test Provider"; + + // Minimal implementations of abstract methods - not tested here, only needed for instantiation + async validateConfig(): Promise { + return true; + } + + async getStatus(): Promise { + return { connected: true }; + } + + async completionRequest( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _params: CompletionRequestParams, + ): Promise { + throw new Error("Not implemented in test provider"); + } + + async chatRequest( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _params: ChatRequestParams, + ): Promise { + throw new Error("Not implemented in test provider"); + } + + async generatePlaybook( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _params: GenerationRequestParams, + ): Promise { + throw new Error("Not implemented in test provider"); + } + + async generateRole( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _params: GenerationRequestParams, + ): Promise { + throw new Error("Not implemented in test provider"); + } + + // Public test helper methods to access protected members and test base class functionality + getConfig() { + return this.config; + } + + getTimeout() { + return this.timeout; + } + + testApplyAnsibleContext( + prompt: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + metadata?: Record, + ): string { + return this.applyAnsibleContext(prompt, metadata); + } + + testCleanAnsibleOutput(output: string): string { + return this.cleanAnsibleOutput(output); + } + + testHandleHttpError( + error: { status?: number; message?: string }, + operation: string, + providerName?: string, + ): Error { + return this.handleHttpError(error, operation, providerName); + } +} + +describe("BaseLLMProvider", () => { + describe("Constructor", () => { + it("should initialize with config and default timeout", () => { + const config = TEST_CONFIGS.BASE_TEST; + const provider = new TestProvider(config); + + expect(provider.getConfig()).toEqual(config); + expect(provider.getTimeout()).toBe(DEFAULT_TIMEOUTS.DEFAULT); + }); + + it("should initialize with custom timeout", () => { + const config = TEST_CONFIGS.BASE_TEST; + const provider = new TestProvider(config, DEFAULT_TIMEOUTS.CUSTOM); + + expect(provider.getTimeout()).toBe(DEFAULT_TIMEOUTS.CUSTOM); + }); + }); + + describe("applyAnsibleContext", () => { + it("should enhance prompt with Ansible context", () => { + const provider = new TestProvider(TEST_CONFIGS.BASE_TEST); + const prompt = TEST_PROMPTS.INSTALL_NGINX; + const metadata = { + ansibleFileType: "playbook", + context: "existing context", + }; + + const result = provider.testApplyAnsibleContext(prompt, metadata); + + expect(result).toContain("enhanced:"); + expect(result).toContain(prompt); + }); + + it("should handle prompt without metadata", () => { + const provider = new TestProvider(TEST_CONFIGS.BASE_TEST); + const prompt = TEST_PROMPTS.INSTALL_NGINX; + + const result = provider.testApplyAnsibleContext(prompt); + + expect(result).toBeDefined(); + expect(typeof result).toBe("string"); + }); + + it("should handle metadata with documentUri and workspaceContext", () => { + const provider = new TestProvider(TEST_CONFIGS.BASE_TEST); + const prompt = TEST_PROMPTS.CREATE_TASK; + const metadata = { + ansibleFileType: "role", + documentUri: "file:///test.yml", + workspaceContext: "workspace info", + context: "additional context", + }; + + const result = provider.testApplyAnsibleContext(prompt, metadata); + + expect(result).toBeDefined(); + expect(result).toContain(prompt); + }); + + it("should default fileType to playbook when not provided", () => { + const provider = new TestProvider(TEST_CONFIGS.BASE_TEST); + const prompt = TEST_PROMPTS.GENERIC; + const metadata = {}; + + const result = provider.testApplyAnsibleContext(prompt, metadata); + + expect(result).toBeDefined(); + }); + }); + + describe("cleanAnsibleOutput", () => { + it("should clean YAML code blocks from output", () => { + const provider = new TestProvider(TEST_CONFIGS.BASE_TEST); + const output = "```yaml\n---\n- name: test\n```"; + + const result = provider.testCleanAnsibleOutput(output); + + expect(result).not.toContain("```yaml"); + expect(result).not.toContain("```"); + }); + + it("should clean YML code blocks from output", () => { + const provider = new TestProvider(TEST_CONFIGS.BASE_TEST); + const output = "```yml\n---\n- name: test\n```"; + + const result = provider.testCleanAnsibleOutput(output); + + expect(result).not.toContain("```yml"); + }); + + it("should trim whitespace from output", () => { + const provider = new TestProvider(TEST_CONFIGS.BASE_TEST); + const output = " \n---\n- name: test\n "; + + const result = provider.testCleanAnsibleOutput(output); + + expect(result).toBeDefined(); + expect(typeof result).toBe("string"); + }); + + it("should handle empty output", () => { + const provider = new TestProvider(TEST_CONFIGS.BASE_TEST); + const output = ""; + + const result = provider.testCleanAnsibleOutput(output); + + expect(result).toBe(""); + }); + }); + + describe("handleHttpError", () => { + it("should handle 400 Bad Request", () => { + const provider = new TestProvider(TEST_CONFIGS.BASE_TEST); + const error = { + status: HTTP_STATUS_CODES.BAD_REQUEST, + message: "Invalid request", + }; + + const result = provider.testHandleHttpError( + error, + TEST_OPERATIONS.GENERIC, + TEST_PROVIDER_INFO.PROVIDER_NAME, + ); + + expect(result).toBeInstanceOf(Error); + expect(result.message).toContain("Bad request"); + expect(result.message).toContain(TEST_OPERATIONS.GENERIC); + }); + + it("should handle 403 Forbidden", () => { + const provider = new TestProvider(TEST_CONFIGS.BASE_TEST); + const error = { status: HTTP_STATUS_CODES.FORBIDDEN }; + + const result = provider.testHandleHttpError( + error, + TEST_OPERATIONS.GENERIC, + TEST_PROVIDER_INFO.PROVIDER_NAME, + ); + + expect(result).toBeInstanceOf(Error); + expect(result.message).toContain("Forbidden"); + expect(result.message).toContain("API key"); + expect(result.message).toContain(TEST_OPERATIONS.GENERIC); + expect(result.message).toContain(String(HTTP_STATUS_CODES.FORBIDDEN)); + }); + + it("should handle 429 Rate Limit", () => { + const provider = new TestProvider(TEST_CONFIGS.BASE_TEST); + const error = { status: HTTP_STATUS_CODES.RATE_LIMIT }; + + const result = provider.testHandleHttpError( + error, + TEST_OPERATIONS.GENERIC, + TEST_PROVIDER_INFO.PROVIDER_NAME, + ); + + expect(result).toBeInstanceOf(Error); + expect(result.message).toContain("Rate limit exceeded"); + expect(result.message).toContain(TEST_OPERATIONS.GENERIC); + expect(result.message).toContain(String(HTTP_STATUS_CODES.RATE_LIMIT)); + }); + + it("should handle 500 Internal Server Error", () => { + const provider = new TestProvider(TEST_CONFIGS.BASE_TEST); + const error = { + status: HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR, + message: "Server error", + }; + + const result = provider.testHandleHttpError( + error, + TEST_OPERATIONS.GENERIC, + TEST_PROVIDER_INFO.PROVIDER_NAME, + ); + + expect(result).toBeInstanceOf(Error); + expect(result.message).toContain(TEST_PROVIDER_INFO.PROVIDER_NAME); + expect(result.message).toContain("unexpected error"); + expect(result.message).toContain(TEST_OPERATIONS.GENERIC); + }); + + it("should handle 503 Service Unavailable", () => { + const provider = new TestProvider(TEST_CONFIGS.BASE_TEST); + const error = { status: HTTP_STATUS_CODES.SERVICE_UNAVAILABLE }; + + const result = provider.testHandleHttpError( + error, + TEST_OPERATIONS.GENERIC, + TEST_PROVIDER_INFO.PROVIDER_NAME, + ); + + expect(result).toBeInstanceOf(Error); + expect(result.message).toContain("Service unavailable"); + expect(result.message).toContain(TEST_PROVIDER_INFO.PROVIDER_NAME); + expect(result.message).toContain(TEST_OPERATIONS.GENERIC); + expect(result.message).toContain( + String(HTTP_STATUS_CODES.SERVICE_UNAVAILABLE), + ); + }); + + it("should handle 504 Gateway Timeout", () => { + const provider = new TestProvider(TEST_CONFIGS.BASE_TEST); + const error = { status: HTTP_STATUS_CODES.GATEWAY_TIMEOUT }; + + const result = provider.testHandleHttpError( + error, + TEST_OPERATIONS.GENERIC, + TEST_PROVIDER_INFO.PROVIDER_NAME, + ); + + expect(result).toBeInstanceOf(Error); + expect(result.message).toContain("Gateway timeout"); + expect(result.message).toContain(TEST_OPERATIONS.GENERIC); + expect(result.message).toContain( + String(HTTP_STATUS_CODES.GATEWAY_TIMEOUT), + ); + }); + + it("should handle unknown status codes", () => { + const provider = new TestProvider(TEST_CONFIGS.BASE_TEST); + const error = { + status: HTTP_STATUS_CODES.TEAPOT, + message: "I'm a teapot", + }; + + const result = provider.testHandleHttpError( + error, + TEST_OPERATIONS.GENERIC, + TEST_PROVIDER_INFO.PROVIDER_NAME, + ); + + expect(result).toBeInstanceOf(Error); + expect(result.message).toContain( + `${TEST_PROVIDER_INFO.PROVIDER_NAME} error`, + ); + expect(result.message).toContain(TEST_OPERATIONS.GENERIC); + expect(result.message).toContain(String(HTTP_STATUS_CODES.TEAPOT)); + }); + + it("should handle errors without status code", () => { + const provider = new TestProvider(TEST_CONFIGS.BASE_TEST); + const error = { message: "Network error" }; + + const result = provider.testHandleHttpError( + error, + TEST_OPERATIONS.GENERIC, + TEST_PROVIDER_INFO.PROVIDER_NAME, + ); + + expect(result).toBeInstanceOf(Error); + expect(result.message).toContain( + `${TEST_PROVIDER_INFO.PROVIDER_NAME} error`, + ); + expect(result.message).toContain("Network error"); + expect(result.message).toContain(TEST_OPERATIONS.GENERIC); + expect(result.message).toContain("N/A"); + }); + + it("should handle errors without message", () => { + const provider = new TestProvider(TEST_CONFIGS.BASE_TEST); + const error = { status: HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR }; + + const result = provider.testHandleHttpError( + error, + TEST_OPERATIONS.GENERIC, + TEST_PROVIDER_INFO.PROVIDER_NAME, + ); + + expect(result).toBeInstanceOf(Error); + expect(result.message).toContain(TEST_PROVIDER_INFO.PROVIDER_NAME); + expect(result.message).toContain("unexpected error"); + expect(result.message).toContain("Unknown error"); + }); + + it("should use default provider name when not provided", () => { + const provider = new TestProvider(TEST_CONFIGS.BASE_TEST); + const error = { + status: HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR, + message: "Error", + }; + + const result = provider.testHandleHttpError( + error, + TEST_OPERATIONS.GENERIC, + ); + + expect(result).toBeInstanceOf(Error); + expect(result.message).toContain(TEST_OPERATIONS.GENERIC); + }); + }); +}); diff --git a/test/unit/vitest/lightspeed/providers/factory.test.ts b/test/unit/vitest/lightspeed/providers/factory.test.ts new file mode 100644 index 000000000..681466a8d --- /dev/null +++ b/test/unit/vitest/lightspeed/providers/factory.test.ts @@ -0,0 +1,235 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { LLMProviderFactory } from "../../../../../src/features/lightspeed/providers/factory.js"; +import { ProviderType } from "../../../../../src/definitions/lightspeed.js"; +import { PROVIDER_TYPES, TEST_LIGHTSPEED_SETTINGS } from "../testConstants.js"; + +// Mock AnsibleContextProcessor for providers that extend BaseLLMProvider +const mockEnhancePromptForAnsible = vi.fn( + (prompt: string, context?: string) => { + return `enhanced: ${prompt} with context: ${context || "none"}`; + }, +); + +const mockCleanAnsibleOutput = vi.fn((output: string) => { + return output + .trim() + .replace(/^```ya?ml\s*/i, "") + .replace(/```\s*$/, ""); +}); + +// Use vi.mock for ES modules - these are hoisted +vi.mock("../../../../../src/features/lightspeed/ansibleContext", () => ({ + AnsibleContextProcessor: { + enhancePromptForAnsible: mockEnhancePromptForAnsible, + cleanAnsibleOutput: mockCleanAnsibleOutput, + }, +})); + +// Reset mocks before each test +beforeEach(() => { + vi.clearAllMocks(); + mockEnhancePromptForAnsible.mockImplementation( + (prompt: string, context?: string) => { + return `enhanced: ${prompt} with context: ${context || "none"}`; + }, + ); + mockCleanAnsibleOutput.mockImplementation((output: string) => { + return output + .trim() + .replace(/^```ya?ml\s*/i, "") + .replace(/```\s*$/, ""); + }); +}); + +describe("LLMProviderFactory", () => { + describe("Singleton pattern", () => { + it("should return the same instance on multiple calls", () => { + const instance1 = LLMProviderFactory.getInstance(); + const instance2 = LLMProviderFactory.getInstance(); + expect(instance1).toBe(instance2); + }); + + it("should return an instance of LLMProviderFactory", () => { + const instance = LLMProviderFactory.getInstance(); + expect(instance).toBeInstanceOf(LLMProviderFactory); + }); + }); + + describe("createProvider", () => { + describe("Google provider", () => { + it("should create Google provider with full config", () => { + const factory = LLMProviderFactory.getInstance(); + const config = TEST_LIGHTSPEED_SETTINGS.GOOGLE_FULL; + + const provider = factory.createProvider(PROVIDER_TYPES.GOOGLE, config); + + expect(provider).toBeDefined(); + expect(provider.name).toBe("google"); + }); + + it("should throw error when API key is missing", () => { + const factory = LLMProviderFactory.getInstance(); + const config = TEST_LIGHTSPEED_SETTINGS.GOOGLE_WITH_EMPTY_API_KEY; + + expect(() => { + factory.createProvider(PROVIDER_TYPES.GOOGLE, config); + }).toThrow("API Key is required for Google Gemini"); + }); + + it("should throw error when custom API endpoint is provided", () => { + const factory = LLMProviderFactory.getInstance(); + const config = { + ...TEST_LIGHTSPEED_SETTINGS.GOOGLE_MINIMAL, + apiEndpoint: "https://custom-endpoint.example.com", + }; + + expect(() => { + factory.createProvider(PROVIDER_TYPES.GOOGLE, config); + }).toThrow( + "Custom API endpoints are not supported for Google Gemini provider. The endpoint is automatically configured. Please remove 'ansible.lightspeed.apiEndpoint' from your settings.", + ); + }); + }); + + describe("WCA provider", () => { + it("should throw error when trying to create WCA provider", () => { + const factory = LLMProviderFactory.getInstance(); + const config = TEST_LIGHTSPEED_SETTINGS.WCA; + + expect(() => { + factory.createProvider(PROVIDER_TYPES.WCA, config); + }).toThrow("WCA provider should be handled by existing LightSpeedAPI"); + }); + }); + + describe("Unsupported provider", () => { + it("should throw error for unsupported provider type", () => { + const factory = LLMProviderFactory.getInstance(); + const config = TEST_LIGHTSPEED_SETTINGS.UNSUPPORTED; + + expect(() => { + factory.createProvider("unsupported" as ProviderType, config); + }).toThrow("Unsupported provider type: unsupported"); + }); + }); + }); + + describe("getSupportedProviders", () => { + it("should return array of supported providers", () => { + const factory = LLMProviderFactory.getInstance(); + const providers = factory.getSupportedProviders(); + + expect(providers).toBeInstanceOf(Array); + expect(providers.length).toBeGreaterThan(0); + }); + + it("should include WCA provider", () => { + const factory = LLMProviderFactory.getInstance(); + const providers = factory.getSupportedProviders(); + const wcaProvider = providers.find((p) => p.type === PROVIDER_TYPES.WCA); + + expect(wcaProvider).toBeDefined(); + expect(wcaProvider?.type).toBe(PROVIDER_TYPES.WCA); + expect(wcaProvider?.name).toBe("wca"); + expect(wcaProvider?.displayName).toContain("Red Hat Ansible Lightspeed"); + }); + + it("should include Google provider", () => { + const factory = LLMProviderFactory.getInstance(); + const providers = factory.getSupportedProviders(); + const googleProvider = providers.find( + (p) => p.type === PROVIDER_TYPES.GOOGLE, + ); + + expect(googleProvider).toBeDefined(); + expect(googleProvider?.type).toBe(PROVIDER_TYPES.GOOGLE); + expect(googleProvider?.name).toBe("google"); + expect(googleProvider?.displayName).toBe("Google Gemini"); + }); + + it("should have correct structure for each provider", () => { + const factory = LLMProviderFactory.getInstance(); + const providers = factory.getSupportedProviders(); + + providers.forEach((provider) => { + expect(provider).toHaveProperty("type"); + expect(provider).toHaveProperty("name"); + expect(provider).toHaveProperty("displayName"); + expect(provider).toHaveProperty("description"); + expect(provider).toHaveProperty("configSchema"); + expect(Array.isArray(provider.configSchema)).toBe(true); + }); + }); + + it("should have config schema with required fields", () => { + const factory = LLMProviderFactory.getInstance(); + const providers = factory.getSupportedProviders(); + + providers.forEach((provider) => { + provider.configSchema.forEach((field) => { + expect(field).toHaveProperty("key"); + expect(field).toHaveProperty("label"); + expect(field).toHaveProperty("type"); + expect(field).toHaveProperty("required"); + expect(typeof field.required).toBe("boolean"); + }); + }); + }); + }); + + describe("validateProviderConfig", () => { + describe("Google provider validation", () => { + it("should return true for valid Google config", () => { + const factory = LLMProviderFactory.getInstance(); + const config = TEST_LIGHTSPEED_SETTINGS.GOOGLE_MINIMAL; + + const isValid = factory.validateProviderConfig( + PROVIDER_TYPES.GOOGLE, + config, + ); + + expect(isValid).toBe(true); + }); + + it("should return false when API key is missing", () => { + const factory = LLMProviderFactory.getInstance(); + const config = TEST_LIGHTSPEED_SETTINGS.GOOGLE_WITH_EMPTY_API_KEY; + + const isValid = factory.validateProviderConfig( + PROVIDER_TYPES.GOOGLE, + config, + ); + + expect(isValid).toBe(false); + }); + }); + + describe("WCA provider validation", () => { + it("should return true for valid WCA config", () => { + const factory = LLMProviderFactory.getInstance(); + const config = TEST_LIGHTSPEED_SETTINGS.WCA; + + const isValid = factory.validateProviderConfig( + PROVIDER_TYPES.WCA, + config, + ); + + expect(isValid).toBe(true); + }); + }); + + describe("Unsupported provider validation", () => { + it("should return false for unsupported provider type", () => { + const factory = LLMProviderFactory.getInstance(); + const config = TEST_LIGHTSPEED_SETTINGS.UNSUPPORTED; + + const isValid = factory.validateProviderConfig( + "unsupported" as ProviderType, + config, + ); + + expect(isValid).toBe(false); + }); + }); + }); +}); diff --git a/test/unit/vitest/lightspeed/providers/google.test.ts b/test/unit/vitest/lightspeed/providers/google.test.ts new file mode 100644 index 000000000..53f24b03f --- /dev/null +++ b/test/unit/vitest/lightspeed/providers/google.test.ts @@ -0,0 +1,580 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import * as Module from "module"; + +// Mock AnsibleContextProcessor +const mockEnhancePromptForAnsible = vi.fn( + (prompt: string, context?: string) => { + return `enhanced: ${prompt} with context: ${context || "none"}`; + }, +); + +const mockCleanAnsibleOutput = vi.fn((output: string) => { + return output + .trim() + .replace(/^```ya?ml\s*/i, "") + .replace(/```\s*$/, ""); +}); + +const mockAnsibleContextModule = { + AnsibleContextProcessor: { + enhancePromptForAnsible: mockEnhancePromptForAnsible, + cleanAnsibleOutput: mockCleanAnsibleOutput, + }, +}; + +// Store original require +const originalRequire = Module.prototype.require.bind(Module.prototype); + +// Patch require() to intercept module imports - must happen before imports +Module.prototype.require = function ( + this: Module, + id: string, +): ReturnType { + const normalizedId = id.replace(/\\/g, "/"); + if ( + id === "../ansibleContext" || + normalizedId === "../ansibleContext" || + normalizedId.endsWith("/ansibleContext") || + normalizedId.endsWith("/ansibleContext.js") || + normalizedId.includes("/ansibleContext") || + normalizedId.includes("ansibleContext.js") + ) { + return mockAnsibleContextModule; + } + return originalRequire.call(this, id); +}; + +vi.mock("@google/genai", () => { + // Create a proper constructor class that can be instantiated with 'new' + const generateContentMock = vi.fn(); + + class MockGoogleGenAI { + models = { + generateContent: generateContentMock, + }; + } + + // Export the mock so we can access it in tests + ( + MockGoogleGenAI as unknown as { + __generateContentMock: typeof generateContentMock; + } + ).__generateContentMock = generateContentMock; + + return { + GoogleGenAI: MockGoogleGenAI, + }; +}); + +vi.mock("../../../../../src/utils/logger", () => { + // Create a shared logger mock inside the factory + const loggerMock = { + info: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + debug: vi.fn(), + trace: vi.fn(), + }; + + return { + getLightspeedLogger: vi.fn(() => loggerMock), + __loggerMock: loggerMock, // Export for test access + }; +}); + +vi.mock( + "../../../../../src/features/lightspeed/utils/outlineGenerator", + () => ({ + generateOutlineFromPlaybook: vi.fn(() => { + return "1. Task one\n2. Task two"; + }), + generateOutlineFromRole: vi.fn(() => { + return "1. Setup task\n2. Configure task"; + }), + }), +); + +// Reset mocks before each test +beforeEach(() => { + vi.clearAllMocks(); + mockEnhancePromptForAnsible.mockImplementation( + (prompt: string, context?: string) => { + return `enhanced: ${prompt} with context: ${context || "none"}`; + }, + ); + mockCleanAnsibleOutput.mockImplementation((output: string) => { + return output + .trim() + .replace(/^```ya?ml\s*/i, "") + .replace(/```\s*$/, ""); + }); + + // Setup shared generateContent mock with default response + sharedGenerateContent.mockResolvedValue({ + text: "---\n- name: test playbook\n hosts: all", + }); + + mockedGenerateOutlineFromPlaybook.mockReturnValue("1. Task one\n2. Task two"); + mockedGenerateOutlineFromRole.mockReturnValue( + "1. Setup task\n2. Configure task", + ); +}); + +import { GoogleProvider } from "../../../../../src/features/lightspeed/providers/google.js"; +import type { CompletionRequestParams } from "../../../../../src/interfaces/lightspeed.js"; +import type { + ChatRequestParams, + GenerationRequestParams, +} from "../../../../../src/features/lightspeed/providers/base.js"; +import { + TEST_API_KEYS, + MODEL_NAMES, + TEST_PROMPTS, + TEST_CONTENT, + GOOGLE_PROVIDER, + HTTP_STATUS_CODES, +} from "../testConstants.js"; + +// Get the mocked modules +import { GoogleGenAI } from "@google/genai"; +import { getLightspeedLogger } from "../../../../../src/utils/logger.js"; +import { + generateOutlineFromPlaybook, + generateOutlineFromRole, +} from "../../../../../src/features/lightspeed/utils/outlineGenerator.js"; + +// Access the actual mocks from the mocked modules using vi.mocked +const mockedGenerateOutlineFromPlaybook = vi.mocked( + generateOutlineFromPlaybook, +); +const mockedGenerateOutlineFromRole = vi.mocked(generateOutlineFromRole); + +// Get the logger mock - call getLightspeedLogger to get the mocked instance +const mockedGetLightspeedLogger = vi.mocked(getLightspeedLogger); +const mockedLogger = mockedGetLightspeedLogger(); + +// Get the generateContent mock from the GoogleGenAI mock +const sharedGenerateContent = ( + GoogleGenAI as unknown as { + __generateContentMock: ReturnType; + } +).__generateContentMock; + +describe("GoogleProvider", () => { + describe("Constructor", () => { + it("should initialize with config and log model name", () => { + const config = { + apiKey: TEST_API_KEYS.GOOGLE, + modelName: MODEL_NAMES.GEMINI_25_FLASH, + }; + const provider = new GoogleProvider(config); + + expect(provider.name).toBe(GOOGLE_PROVIDER.NAME); + expect(provider.displayName).toBe(GOOGLE_PROVIDER.DISPLAY_NAME); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockedLogger.info).toHaveBeenCalledWith( + expect.stringContaining(MODEL_NAMES.GEMINI_25_FLASH), + ); + expect(sharedGenerateContent).toBeDefined(); + }); + }); + + describe("validateConfig and getStatus", () => { + it("should validate config and return status", async () => { + sharedGenerateContent.mockResolvedValue({ + text: "test response", + }); + const provider = new GoogleProvider({ + apiKey: TEST_API_KEYS.GOOGLE, + modelName: MODEL_NAMES.GEMINI_PRO, + }); + + const isValid = await provider.validateConfig(); + const status = await provider.getStatus(); + + expect(isValid).toBe(true); + expect(status.connected).toBe(true); + expect(status.modelInfo?.name).toBe(MODEL_NAMES.GEMINI_PRO); + expect(status.modelInfo?.capabilities).toEqual([ + "completion", + "chat", + "generation", + ]); + expect(sharedGenerateContent).toHaveBeenCalledWith({ + model: MODEL_NAMES.GEMINI_PRO, + contents: "test", + }); + }); + + it("should return false and disconnected status when API call fails", async () => { + sharedGenerateContent.mockRejectedValue(new Error("Invalid API key")); + const provider = new GoogleProvider({ + apiKey: TEST_API_KEYS.GOOGLE, + modelName: MODEL_NAMES.GEMINI_25_FLASH, + }); + + const isValid = await provider.validateConfig(); + const status = await provider.getStatus(); + + expect(isValid).toBe(false); + expect(status.connected).toBe(false); + expect(status.error).toContain("API key"); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockedLogger.error).toHaveBeenCalled(); + }); + }); + + describe("completionRequest", () => { + it("should return completion predictions with proper formatting", async () => { + const mockResponse = + "```yaml\n - name: Install package\n package:"; + sharedGenerateContent.mockResolvedValue({ + text: mockResponse, + }); + const provider = new GoogleProvider({ + apiKey: TEST_API_KEYS.GOOGLE, + modelName: MODEL_NAMES.GEMINI_25_FLASH, + }); + const params: CompletionRequestParams = { + prompt: TEST_PROMPTS.INSTALL_NGINX, + suggestionId: "test-suggestion-123", + }; + + const result = await provider.completionRequest(params); + + expect(result.predictions).toBeDefined(); + expect(Array.isArray(result.predictions)).toBe(true); + expect(result.model).toBe(MODEL_NAMES.GEMINI_25_FLASH); + expect(result.suggestionId).toBe("test-suggestion-123"); + expect(result.predictions[0]).not.toContain("```yaml"); + expect(result.predictions[0]).not.toContain("```"); + expect(sharedGenerateContent).toHaveBeenCalledWith({ + model: MODEL_NAMES.GEMINI_25_FLASH, + contents: TEST_PROMPTS.INSTALL_NGINX, + config: { + systemInstruction: expect.stringContaining("Ansible code completion"), + }, + }); + }); + + it("should generate suggestionId when not provided", async () => { + sharedGenerateContent.mockResolvedValue({ + text: "test completion", + }); + const provider = new GoogleProvider({ + apiKey: TEST_API_KEYS.GOOGLE, + modelName: MODEL_NAMES.GEMINI_25_FLASH, + }); + const params: CompletionRequestParams = { + prompt: TEST_PROMPTS.TEST_PROMPT, + }; + + const result = await provider.completionRequest(params); + + expect(result.suggestionId).toBeDefined(); + expect(result.suggestionId).toContain("google-"); + }); + + it("should handle errors and throw with proper message", async () => { + const error = new Error("API error"); + sharedGenerateContent.mockRejectedValue(error); + const provider = new GoogleProvider({ + apiKey: TEST_API_KEYS.GOOGLE, + modelName: MODEL_NAMES.GEMINI_25_FLASH, + }); + const params: CompletionRequestParams = { + prompt: TEST_PROMPTS.TEST_PROMPT, + }; + + await expect(provider.completionRequest(params)).rejects.toThrow( + "Google completion failed", + ); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockedLogger.error).toHaveBeenCalled(); + }); + }); + + describe("chatRequest", () => { + it("should return chat response with message", async () => { + const mockResponse = "This is a helpful response"; + sharedGenerateContent.mockResolvedValue({ + text: mockResponse, + }); + const provider = new GoogleProvider({ + apiKey: TEST_API_KEYS.GOOGLE, + modelName: MODEL_NAMES.GEMINI_PRO, + }); + const params: ChatRequestParams = { + message: "How do I install nginx?", + conversationId: "conv-123", + }; + + const result = await provider.chatRequest(params); + + expect(result.message).toBe(mockResponse); + expect(result.conversationId).toBe("conv-123"); + expect(result.model).toBe(MODEL_NAMES.GEMINI_PRO); + expect(mockEnhancePromptForAnsible).toHaveBeenCalled(); + }); + + it("should use correct system prompt based on isExplanation flag", async () => { + sharedGenerateContent.mockResolvedValue({ + text: "Response", + }); + const provider = new GoogleProvider({ + apiKey: TEST_API_KEYS.GOOGLE, + modelName: MODEL_NAMES.GEMINI_PRO, + }); + + // Test explanation mode + await provider.chatRequest({ + message: "Explain this task", + metadata: { isExplanation: true }, + }); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockedLogger.info).toHaveBeenCalledWith( + expect.stringContaining("EXPLANATION"), + ); + + // Test chat mode + await provider.chatRequest({ + message: "Hello", + metadata: { isExplanation: false }, + }); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockedLogger.info).toHaveBeenCalledWith( + expect.stringContaining("CHAT"), + ); + }); + + it("should handle errors and throw with proper message", async () => { + const error = { status: HTTP_STATUS_CODES.FORBIDDEN }; + sharedGenerateContent.mockRejectedValue(error); + const provider = new GoogleProvider({ + apiKey: TEST_API_KEYS.GOOGLE, + modelName: MODEL_NAMES.GEMINI_25_FLASH, + }); + const params: ChatRequestParams = { + message: "Test message", + }; + + await expect(provider.chatRequest(params)).rejects.toThrow(); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockedLogger.error).toHaveBeenCalled(); + }); + }); + + describe("generatePlaybook", () => { + it("should generate playbook content with proper config", async () => { + const mockPlaybook = "---\n- name: Install nginx\n hosts: all"; + sharedGenerateContent.mockResolvedValue({ + text: mockPlaybook, + }); + const provider = new GoogleProvider({ + apiKey: TEST_API_KEYS.GOOGLE, + modelName: MODEL_NAMES.GEMINI_PRO, + }); + const params: GenerationRequestParams = { + prompt: TEST_PROMPTS.INSTALL_NGINX, + type: "playbook", + }; + + const result = await provider.generatePlaybook(params); + + expect(result.content).toBeDefined(); + expect(result.model).toBe(MODEL_NAMES.GEMINI_PRO); + expect(mockEnhancePromptForAnsible).toHaveBeenCalled(); + expect(mockCleanAnsibleOutput).toHaveBeenCalled(); + expect(sharedGenerateContent).toHaveBeenCalledWith({ + model: MODEL_NAMES.GEMINI_PRO, + contents: expect.any(String), + config: { + systemInstruction: expect.any(String), + temperature: 0.3, + maxOutputTokens: 4000, + }, + }); + }); + + it("should generate outline when createOutline is true, otherwise not", async () => { + const mockPlaybook = + "---\n- name: Install nginx\n hosts: all\n tasks:\n - name: Task one\n - name: Task two"; + sharedGenerateContent.mockResolvedValue({ + text: mockPlaybook, + }); + const provider = new GoogleProvider({ + apiKey: TEST_API_KEYS.GOOGLE, + modelName: MODEL_NAMES.GEMINI_25_FLASH, + }); + + // Test with outline + const resultWithOutline = await provider.generatePlaybook({ + prompt: TEST_PROMPTS.INSTALL_NGINX, + type: "playbook", + createOutline: true, + }); + expect(resultWithOutline.outline).toBeDefined(); + expect(mockedGenerateOutlineFromPlaybook).toHaveBeenCalled(); + + // Test without outline + const resultWithoutOutline = await provider.generatePlaybook({ + prompt: TEST_PROMPTS.INSTALL_NGINX, + type: "playbook", + createOutline: false, + }); + expect(resultWithoutOutline.outline).toBe(""); + }); + + it("should incorporate outline into prompt when provided", async () => { + const outline = "1. Setup\n2. Configure"; + const mockPlaybook = "---\n- name: Install nginx\n hosts: all"; + sharedGenerateContent.mockResolvedValue({ + text: mockPlaybook, + }); + const provider = new GoogleProvider({ + apiKey: TEST_API_KEYS.GOOGLE, + modelName: MODEL_NAMES.GEMINI_PRO, + }); + const params: GenerationRequestParams = { + prompt: TEST_PROMPTS.INSTALL_NGINX, + type: "playbook", + outline: outline, + }; + + await provider.generatePlaybook(params); + + expect(mockEnhancePromptForAnsible).toHaveBeenCalledWith( + expect.stringContaining(outline), + expect.any(String), + expect.objectContaining({ + fileType: "playbook", + }), + ); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockedLogger.info).toHaveBeenCalledWith( + expect.stringContaining(outline), + ); + }); + + it("should handle errors and throw with proper message", async () => { + const error = { status: HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR }; + sharedGenerateContent.mockRejectedValue(error); + const provider = new GoogleProvider({ + apiKey: TEST_API_KEYS.GOOGLE, + modelName: MODEL_NAMES.GEMINI_25_FLASH, + }); + const params: GenerationRequestParams = { + prompt: TEST_PROMPTS.INSTALL_NGINX, + type: "playbook", + }; + + await expect(provider.generatePlaybook(params)).rejects.toThrow(); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockedLogger.error).toHaveBeenCalled(); + }); + }); + + describe("generateRole", () => { + it("should generate role content with proper config", async () => { + const mockRole = "---\n- name: Setup role\n tasks:"; + sharedGenerateContent.mockResolvedValue({ + text: mockRole, + }); + const provider = new GoogleProvider({ + apiKey: TEST_API_KEYS.GOOGLE, + modelName: MODEL_NAMES.GEMINI_PRO, + }); + const params: GenerationRequestParams = { + prompt: TEST_PROMPTS.CREATE_ROLE, + type: "role", + }; + + const result = await provider.generateRole(params); + + expect(result.content).toBeDefined(); + expect(result.model).toBe(MODEL_NAMES.GEMINI_PRO); + expect(mockEnhancePromptForAnsible).toHaveBeenCalled(); + expect(mockCleanAnsibleOutput).toHaveBeenCalled(); + expect(sharedGenerateContent).toHaveBeenCalledWith({ + model: MODEL_NAMES.GEMINI_PRO, + contents: expect.any(String), + config: { + systemInstruction: expect.any(String), + temperature: 0.3, + maxOutputTokens: 4000, + }, + }); + }); + + it("should generate outline when createOutline is true, otherwise not", async () => { + const mockRole = "---\n- name: Setup task\n- name: Configure task"; + sharedGenerateContent.mockResolvedValue({ + text: mockRole, + }); + const provider = new GoogleProvider({ + apiKey: TEST_API_KEYS.GOOGLE, + modelName: MODEL_NAMES.GEMINI_25_FLASH, + }); + + // Test with outline + const resultWithOutline = await provider.generateRole({ + prompt: TEST_PROMPTS.CREATE_ROLE, + type: "role", + createOutline: true, + }); + expect(resultWithOutline.outline).toBeDefined(); + expect(mockedGenerateOutlineFromRole).toHaveBeenCalled(); + + // Test without outline + const resultWithoutOutline = await provider.generateRole({ + prompt: TEST_PROMPTS.CREATE_ROLE, + type: "role", + createOutline: false, + }); + expect(resultWithoutOutline.outline).toBe(""); + }); + + it("should incorporate outline into prompt when provided", async () => { + const outline = "1. Setup\n2. Configure"; + sharedGenerateContent.mockResolvedValue({ + text: TEST_CONTENT.ROLE, + }); + const provider = new GoogleProvider({ + apiKey: TEST_API_KEYS.GOOGLE, + modelName: MODEL_NAMES.GEMINI_PRO, + }); + const params: GenerationRequestParams = { + prompt: TEST_PROMPTS.CREATE_ROLE, + type: "role", + outline: outline, + }; + + await provider.generateRole(params); + + expect(mockEnhancePromptForAnsible).toHaveBeenCalledWith( + expect.stringContaining(outline), + expect.any(String), + expect.objectContaining({ + fileType: "tasks", + }), + ); + }); + + it("should handle errors and throw with proper message", async () => { + const error = { status: HTTP_STATUS_CODES.SERVICE_UNAVAILABLE }; + sharedGenerateContent.mockRejectedValue(error); + const provider = new GoogleProvider({ + apiKey: TEST_API_KEYS.GOOGLE, + modelName: MODEL_NAMES.GEMINI_25_FLASH, + }); + const params: GenerationRequestParams = { + prompt: TEST_PROMPTS.CREATE_ROLE, + type: "role", + }; + + await expect(provider.generateRole(params)).rejects.toThrow(); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockedLogger.error).toHaveBeenCalled(); + }); + }); +}); diff --git a/test/unit/vitest/lightspeed/testConstants.ts b/test/unit/vitest/lightspeed/testConstants.ts new file mode 100644 index 000000000..61f0fb28c --- /dev/null +++ b/test/unit/vitest/lightspeed/testConstants.ts @@ -0,0 +1,183 @@ +/** + * Test constants and mock data for provider factory and base provider tests + */ + +import type { LightSpeedServiceSettings } from "../../../../src/interfaces/extensionSettings.js"; + +// Model names +export const MODEL_NAMES = { + GEMINI_PRO: "gemini-1.5-pro", + GEMINI_FLASH: "gemini-1.5-flash", + GEMINI_25_FLASH: "gemini-2.5-flash", + TEST_MODEL: "test-model", +} as const; + +// Provider types (only WCA and Google are supported in factory) +export const PROVIDER_TYPES = { + GOOGLE: "google", + WCA: "wca", +} as const; + +// API endpoints +export const API_ENDPOINTS = { + GOOGLE: "https://generativelanguage.googleapis.com/v1beta", + WCA_DEFAULT: "https://c.ai.ansible.redhat.com", +} as const; + +// Test API keys +export const TEST_API_KEYS = { + GOOGLE: "AIzaSyTest-google-key-12345", + TEST_KEY: "test-key", +} as const; + +// Test provider information +export const TEST_PROVIDER_INFO = { + NAME: "test-provider", + DISPLAY_NAME: "Test Provider", + PROVIDER_NAME: "TestProvider", +} as const; + +// Test responses and IDs +export const TEST_RESPONSES = { + COMPLETION: "test completion", + MESSAGE: "test response", + SUGGESTION_ID: "test-suggestion-id", + CONVERSATION_ID_DEFAULT: "default-id", +} as const; + +// Test prompts +export const TEST_PROMPTS = { + INSTALL_NGINX: "Install nginx", + CREATE_TASK: "Create a task", + CREATE_ROLE: "Create a role", + TEST_PROMPT: "test prompt", + GENERIC: "Test prompt", +} as const; + +// Test content +export const TEST_CONTENT = { + PLAYBOOK: "---\n- name: test playbook", + ROLE: "---\n- name: test role", + OUTLINE_DEFAULT: "1. Test step", +} as const; + +// Test operations +export const TEST_OPERATIONS = { + GENERIC: "test-operation", +} as const; + +// HTTP status codes for error testing +export const HTTP_STATUS_CODES = { + BAD_REQUEST: 400, + FORBIDDEN: 403, + RATE_LIMIT: 429, + INTERNAL_SERVER_ERROR: 500, + SERVICE_UNAVAILABLE: 503, + GATEWAY_TIMEOUT: 504, + TEAPOT: 418, // For testing unknown status codes +} as const; + +// Default timeouts +export const DEFAULT_TIMEOUTS = { + DEFAULT: 30000, + CUSTOM: 60000, +} as const; + +// Partial test configuration objects (used by base provider tests) +export const TEST_CONFIGS = { + GOOGLE_MINIMAL: { + apiKey: TEST_API_KEYS.GOOGLE, + }, + GOOGLE_FULL: { + apiKey: TEST_API_KEYS.GOOGLE, + modelName: MODEL_NAMES.GEMINI_PRO, + timeout: 45000, + }, + GOOGLE_WITH_MODEL: { + apiKey: TEST_API_KEYS.GOOGLE, + modelName: MODEL_NAMES.GEMINI_25_FLASH, + timeout: DEFAULT_TIMEOUTS.DEFAULT, + }, + WCA: { + apiEndpoint: API_ENDPOINTS.WCA_DEFAULT, + }, + BASE_TEST: { + apiKey: TEST_API_KEYS.TEST_KEY, + }, +} as const; + +// Base LightSpeedServiceSettings with all required common properties +export const BASE_LIGHTSPEED_SETTINGS: Omit< + LightSpeedServiceSettings, + "provider" | "apiKey" | "apiEndpoint" +> = { + enabled: true, + URL: "", + modelName: undefined, + model: undefined, + timeout: DEFAULT_TIMEOUTS.DEFAULT, + customHeaders: {}, + suggestions: { enabled: true, waitWindow: 0 }, + playbookGenerationCustomPrompt: undefined, + playbookExplanationCustomPrompt: undefined, +}; + +// Complete LightSpeedServiceSettings for common test scenarios +export const TEST_LIGHTSPEED_SETTINGS = { + GOOGLE_MINIMAL: { + ...BASE_LIGHTSPEED_SETTINGS, + provider: PROVIDER_TYPES.GOOGLE, + apiKey: TEST_API_KEYS.GOOGLE, + apiEndpoint: "", + } as LightSpeedServiceSettings, + GOOGLE_FULL: { + ...BASE_LIGHTSPEED_SETTINGS, + provider: PROVIDER_TYPES.GOOGLE, + apiKey: TEST_API_KEYS.GOOGLE, + modelName: MODEL_NAMES.GEMINI_PRO, + timeout: 45000, + apiEndpoint: "", + } as LightSpeedServiceSettings, + GOOGLE_WITH_EMPTY_API_KEY: { + ...BASE_LIGHTSPEED_SETTINGS, + provider: PROVIDER_TYPES.GOOGLE, + apiKey: "", + apiEndpoint: "", + } as LightSpeedServiceSettings, + WCA: { + ...BASE_LIGHTSPEED_SETTINGS, + provider: PROVIDER_TYPES.WCA, + apiKey: "", + apiEndpoint: API_ENDPOINTS.WCA_DEFAULT, + } as LightSpeedServiceSettings, + UNSUPPORTED: { + ...BASE_LIGHTSPEED_SETTINGS, + provider: "unsupported" as string, + apiKey: "", + apiEndpoint: "", + } as LightSpeedServiceSettings, +}; + +// Google provider specific constants +export const GOOGLE_PROVIDER = { + NAME: "google", + DISPLAY_NAME: "Google Gemini", + PROVIDER_NAME: "Google Gemini", +} as const; + +// Ansible test content +export const ANSIBLE_CONTENT = { + SINGLE_TASK: + "- name: Install nginx\n ansible.builtin.package:\n name: nginx\n state: present", + MULTI_TASK: + "- name: Task one\n ansible.builtin.debug:\n msg: 'First task'\n- name: Task two\n ansible.builtin.debug:\n msg: 'Second task'", + PLAYBOOK: + "---\n- hosts: all\n tasks:\n - name: Install nginx\n ansible.builtin.package:\n name: nginx", + INVALID_YAML: "- name: Task\n invalid: [unclosed", + YAML_WITH_CODE_BLOCK: + "```yaml\n- name: Test task\n ansible.builtin.debug:\n msg: 'test'\n```", + YAML_WITH_EXPLANATION: + "Here's the playbook:\n- name: Test task\n ansible.builtin.debug:\n msg: 'test'", + EMPTY: "", + NULL_YAML: "null", +} as const; diff --git a/test/unit/vitest/lightspeed/utils/outlineGenerator.test.ts b/test/unit/vitest/lightspeed/utils/outlineGenerator.test.ts new file mode 100644 index 000000000..dd7882fe7 --- /dev/null +++ b/test/unit/vitest/lightspeed/utils/outlineGenerator.test.ts @@ -0,0 +1,304 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { + generateOutlineFromPlaybook, + generateOutlineFromRole, + parseOutlineToTaskList, +} from "../../../../../src/features/lightspeed/utils/outlineGenerator.js"; +import { ANSIBLE_CONTENT } from "../testConstants.js"; + +describe("outlineGenerator", () => { + beforeEach(() => { + vi.spyOn(console, "error").mockImplementation(() => { + // Intentionally empty - suppresses console.error in tests + }); + }); + + describe("generateOutlineFromPlaybook", () => { + it("should generate outline from playbook with tasks", () => { + const playbook = `--- +- hosts: all + tasks: + - name: Install nginx + - name: Start nginx + - name: Configure nginx`; + + const result = generateOutlineFromPlaybook(playbook); + + expect(result).toBe( + "1. Install nginx\n2. Start nginx\n3. Configure nginx", + ); + }); + + it("should generate outline from playbook with pre_tasks and post_tasks", () => { + const playbook = `--- +- hosts: all + pre_tasks: + - name: Pre-task one + - name: Pre-task two + tasks: + - name: Main task + post_tasks: + - name: Post-task one`; + + const result = generateOutlineFromPlaybook(playbook); + + expect(result).toBe( + "1. Main task\n2. Pre-task one\n3. Pre-task two\n4. Post-task one", + ); + }); + + it("should handle multiple plays in playbook", () => { + const playbook = `--- +- hosts: web + tasks: + - name: Task in first play +- hosts: db + tasks: + - name: Task in second play`; + + const result = generateOutlineFromPlaybook(playbook); + + expect(result).toBe("1. Task in first play\n2. Task in second play"); + }); + + it("should skip tasks without name field", () => { + const playbook = `--- +- hosts: all + tasks: + - name: Task with name + - debug: + msg: "Task without name" + - name: Another task with name`; + + const result = generateOutlineFromPlaybook(playbook); + + expect(result).toBe("1. Task with name\n2. Another task with name"); + }); + + it("should return empty string for empty playbook", () => { + const playbook = ""; + + const result = generateOutlineFromPlaybook(playbook); + + expect(result).toBe(""); + }); + + it("should return empty string for invalid YAML", () => { + const playbook = ANSIBLE_CONTENT.INVALID_YAML; + + const result = generateOutlineFromPlaybook(playbook); + + expect(result).toBe(""); + }); + + it("should return empty string for non-array playbook", () => { + const playbook = `--- +hosts: all +tasks: + - name: Task`; + + const result = generateOutlineFromPlaybook(playbook); + + expect(result).toBe(""); + }); + + it("should return empty string for playbook with no tasks", () => { + const playbook = `--- +- hosts: all + vars: + var1: value1`; + + const result = generateOutlineFromPlaybook(playbook); + + expect(result).toBe(""); + }); + + it("should handle playbook with only pre_tasks", () => { + const playbook = `--- +- hosts: all + pre_tasks: + - name: Pre-task one + - name: Pre-task two`; + + const result = generateOutlineFromPlaybook(playbook); + + expect(result).toBe("1. Pre-task one\n2. Pre-task two"); + }); + + it("should handle playbook with only post_tasks", () => { + const playbook = `--- +- hosts: all + post_tasks: + - name: Post-task one + - name: Post-task two`; + + const result = generateOutlineFromPlaybook(playbook); + + expect(result).toBe("1. Post-task one\n2. Post-task two"); + }); + + it("should skip pre_tasks when not an array", () => { + const playbook = `--- +- hosts: all + pre_tasks: not-an-array + tasks: + - name: Main task`; + + const result = generateOutlineFromPlaybook(playbook); + + expect(result).toBe("1. Main task"); + }); + + it("should skip post_tasks when not an array", () => { + const playbook = `--- +- hosts: all + tasks: + - name: Main task + post_tasks: not-an-array`; + + const result = generateOutlineFromPlaybook(playbook); + + expect(result).toBe("1. Main task"); + }); + + it("should skip tasks when not an array", () => { + const playbook = `--- +- hosts: all + tasks: not-an-array + pre_tasks: + - name: Pre-task`; + + const result = generateOutlineFromPlaybook(playbook); + + expect(result).toBe("1. Pre-task"); + }); + }); + + describe("generateOutlineFromRole", () => { + it("should generate outline from role tasks", () => { + const roleYaml = `--- +- name: Install package +- name: Configure service +- name: Start service`; + + const result = generateOutlineFromRole(roleYaml); + + expect(result).toBe( + "1. Install package\n2. Configure service\n3. Start service", + ); + }); + + it("should return empty string for invalid YAML", () => { + const roleYaml = ANSIBLE_CONTENT.INVALID_YAML; + + const result = generateOutlineFromRole(roleYaml); + + expect(result).toBe(""); + }); + + it("should return empty string for non-array role YAML", () => { + const roleYaml = `--- +name: Task +action: debug`; + + const result = generateOutlineFromRole(roleYaml); + + expect(result).toBe(""); + }); + + it("should return empty string for role with no tasks", () => { + const roleYaml = `--- []`; + + const result = generateOutlineFromRole(roleYaml); + + expect(result).toBe(""); + }); + + it("should handle role with single task", () => { + const roleYaml = `--- +- name: Single task`; + + const result = generateOutlineFromRole(roleYaml); + + expect(result).toBe("1. Single task"); + }); + + it("should skip tasks without name field", () => { + const roleYaml = `--- +- name: Task with name +- debug: + msg: "Task without name" +- name: Another task with name`; + + const result = generateOutlineFromRole(roleYaml); + + expect(result).toBe("1. Task with name\n2. Another task with name"); + }); + + it("should return empty string for empty role YAML", () => { + const roleYaml = ""; + + const result = generateOutlineFromRole(roleYaml); + + expect(result).toBe(""); + }); + }); + + describe("parseOutlineToTaskList", () => { + it("should parse numbered outline to task list", () => { + const outline = "1. First task\n2. Second task\n3. Third task"; + + const result = parseOutlineToTaskList(outline); + + expect(result).toEqual(["First task", "Second task", "Third task"]); + }); + + it("should handle outline with single task", () => { + const outline = "1. Single task"; + + const result = parseOutlineToTaskList(outline); + + expect(result).toEqual(["Single task"]); + }); + + it("should handle outline with multi-digit numbers", () => { + const outline = "1. Task one\n10. Task ten\n100. Task hundred"; + + const result = parseOutlineToTaskList(outline); + + expect(result).toEqual(["Task one", "Task ten", "Task hundred"]); + }); + + it("should handle outline with extra whitespace", () => { + const outline = " 1. Task one \n 2. Task two "; + + const result = parseOutlineToTaskList(outline); + + expect(result).toEqual(["Task one", "Task two"]); + }); + + it("should filter out empty lines", () => { + const outline = "1. Task one\n\n2. Task two\n\n3. Task three"; + + const result = parseOutlineToTaskList(outline); + + expect(result).toEqual(["Task one", "Task two", "Task three"]); + }); + + it("should return empty array for empty outline", () => { + const outline = ""; + + const result = parseOutlineToTaskList(outline); + + expect(result).toEqual([]); + }); + + it("should handle outline with tasks containing numbers", () => { + const outline = "1. Install version 2.0\n2. Configure port 8080"; + + const result = parseOutlineToTaskList(outline); + + expect(result).toEqual(["Install version 2.0", "Configure port 8080"]); + }); + }); +}); diff --git a/test/unit/vitestSetup.ts b/test/unit/vitestSetup.ts index e11710a59..e782e5ec9 100644 --- a/test/unit/vitestSetup.ts +++ b/test/unit/vitestSetup.ts @@ -1,2 +1,52 @@ import * as chai from "chai"; chai.config.truncateThreshold = 0; // disable truncating +import { vi } from "vitest"; + +vi.mock("vscode", () => ({ + commands: { + executeCommand: vi.fn(), + }, + ExtensionContext: vi.fn(), + window: { + showErrorMessage: vi.fn(), + showInformationMessage: vi.fn(), + createOutputChannel: vi.fn((name: string, options?: { log?: boolean }) => { + // If log option is true, return LogOutputChannel with logging methods + if (options?.log) { + return { + appendLine: vi.fn(), + show: vi.fn(), + dispose: vi.fn(), + info: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + debug: vi.fn(), + trace: vi.fn(), + }; + } + // Otherwise return regular OutputChannel + return { + appendLine: vi.fn(), + show: vi.fn(), + dispose: vi.fn(), + }; + }), + }, + workspace: { + workspaceFolders: [], + getConfiguration: vi.fn(), + }, + Uri: { + file: vi.fn(), + parse: vi.fn(), + }, + ViewColumn: { + One: 1, + Two: 2, + Three: 3, + }, + env: { + machineId: "test-machine-id", + sessionId: "test-session-id", + }, +})); diff --git a/yarn.lock b/yarn.lock index 3ae8a18d8..481ee35ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6,9 +6,9 @@ __metadata: cacheKey: 10 "@acemir/cssom@npm:^0.9.23": - version: 0.9.24 - resolution: "@acemir/cssom@npm:0.9.24" - checksum: 10/c5cfb02ca2aee74b907896706951cc9319b6267c5042f123c7e82432817460bf4975f17ac35a1594d665cb44bc1a97b8ed6b67aff7058dc930328936f05e6e6e + version: 0.9.25 + resolution: "@acemir/cssom@npm:0.9.25" + checksum: 10/d5ca0a02d3ba3a9beae3ec48ebd5afd74df5b1cc12344ac7cb1954b426828fa67b5bfa236a5f3c95ec393e5a05d47cec21410a3cf0bdd2bf02a6d2311a14572f languageName: node linkType: hard @@ -82,15 +82,15 @@ __metadata: linkType: hard "@asamuzakjp/dom-selector@npm:^6.7.4": - version: 6.7.4 - resolution: "@asamuzakjp/dom-selector@npm:6.7.4" + version: 6.7.5 + resolution: "@asamuzakjp/dom-selector@npm:6.7.5" dependencies: "@asamuzakjp/nwsapi": "npm:^2.3.9" bidi-js: "npm:^1.0.3" css-tree: "npm:^3.1.0" is-potential-custom-element-name: "npm:^1.0.1" lru-cache: "npm:^11.2.2" - checksum: 10/dc550a4e32b956bfc820795e73bb3cae963496101a6dc1ebbd0cad409e734dadbcfbac22a44034c28ec401ee1454a379c64cc02d11eb2186e74c638482d8b2e1 + checksum: 10/6fdd63a12384637a5faeddde328d1405c7a8b57e6e084c2194b7ccbada2768f9e93e91594d95c0d5cbcafe6f86d606d46a21c97a118f4dfe565cb0775165ed0c languageName: node linkType: hard @@ -217,29 +217,29 @@ __metadata: linkType: hard "@azure/msal-browser@npm:^4.2.0": - version: 4.26.2 - resolution: "@azure/msal-browser@npm:4.26.2" + version: 4.27.0 + resolution: "@azure/msal-browser@npm:4.27.0" dependencies: - "@azure/msal-common": "npm:15.13.2" - checksum: 10/a6e77371bb3109e27f47355e1459a733c8f3ca2a052f502abd59a2f130958c62c3f0573407b190ae56489685c2ed9b6d126f47b98289da2634adca703574a339 + "@azure/msal-common": "npm:15.13.3" + checksum: 10/bfd80ba019db53dd9e898243b4615343def4ed65afecc9c5b362b13e9fcb0daeca9d3489a9a3373113ec183764e9723204bfc2ad8220a2dc36d5a1c2c13dfc2d languageName: node linkType: hard -"@azure/msal-common@npm:15.13.2": - version: 15.13.2 - resolution: "@azure/msal-common@npm:15.13.2" - checksum: 10/b2eecb6502305942f4f171fffca8314b37809a297a7332d959d0786008dfa2eba70f44dca5069a557589cf05d60e024c233a97beeaab7944fd6aae3186343967 +"@azure/msal-common@npm:15.13.3": + version: 15.13.3 + resolution: "@azure/msal-common@npm:15.13.3" + checksum: 10/7d438e09f0216a282efaeafad88fa683c92719771a5c21b1f16c524a5a5667fb15014e41a69426c177a598405b606065a41de3151f331393a577eac5540934dd languageName: node linkType: hard "@azure/msal-node@npm:^3.5.0": - version: 3.8.3 - resolution: "@azure/msal-node@npm:3.8.3" + version: 3.8.4 + resolution: "@azure/msal-node@npm:3.8.4" dependencies: - "@azure/msal-common": "npm:15.13.2" + "@azure/msal-common": "npm:15.13.3" jsonwebtoken: "npm:^9.0.0" uuid: "npm:^8.3.0" - checksum: 10/545459106b2f97ea8847770aa26ab064d6e65a3507e27c6d9962f90348712fe2c5aba066373aad52593a07e4de519c3ee9b72023a883e7610edcbcca9f9d19a7 + checksum: 10/183c52f21d8c6d68e0b3e1a9364efd16eb0d0cbfd3897424990dd72f3e5751f422b1874cb0c0d44dfaf0f03d4df32f6b57549b496833f466c25744946614ef08 languageName: node linkType: hard @@ -405,9 +405,9 @@ __metadata: linkType: hard "@csstools/css-syntax-patches-for-csstree@npm:^1.0.14": - version: 1.0.17 - resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.0.17" - checksum: 10/4572dda78df84fb5723a1a16b96202f686c2fb8dd32acf1cad884aae95ad560ef8a91c802b4eef9d980b6b9cc1ff8cd001ff1f7fccb48eec573dc4334a192411 + version: 1.0.20 + resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.0.20" + checksum: 10/e13c96e1d1fdb4fe48ec959ee8dede817151d4d1ae44a9b36d56f6da69a3d4bb019ae684a102a32d8aab7de06634c4f7876f7e570c40e81bb93361cd8bbfb04a languageName: node linkType: hard @@ -471,9 +471,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.27.0": - version: 0.27.0 - resolution: "@esbuild/aix-ppc64@npm:0.27.0" +"@esbuild/aix-ppc64@npm:0.27.1": + version: 0.27.1 + resolution: "@esbuild/aix-ppc64@npm:0.27.1" conditions: os=aix & cpu=ppc64 languageName: node linkType: hard @@ -485,9 +485,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.27.0": - version: 0.27.0 - resolution: "@esbuild/android-arm64@npm:0.27.0" +"@esbuild/android-arm64@npm:0.27.1": + version: 0.27.1 + resolution: "@esbuild/android-arm64@npm:0.27.1" conditions: os=android & cpu=arm64 languageName: node linkType: hard @@ -499,9 +499,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.27.0": - version: 0.27.0 - resolution: "@esbuild/android-arm@npm:0.27.0" +"@esbuild/android-arm@npm:0.27.1": + version: 0.27.1 + resolution: "@esbuild/android-arm@npm:0.27.1" conditions: os=android & cpu=arm languageName: node linkType: hard @@ -513,9 +513,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-x64@npm:0.27.0": - version: 0.27.0 - resolution: "@esbuild/android-x64@npm:0.27.0" +"@esbuild/android-x64@npm:0.27.1": + version: 0.27.1 + resolution: "@esbuild/android-x64@npm:0.27.1" conditions: os=android & cpu=x64 languageName: node linkType: hard @@ -527,9 +527,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.27.0": - version: 0.27.0 - resolution: "@esbuild/darwin-arm64@npm:0.27.0" +"@esbuild/darwin-arm64@npm:0.27.1": + version: 0.27.1 + resolution: "@esbuild/darwin-arm64@npm:0.27.1" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard @@ -541,9 +541,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.27.0": - version: 0.27.0 - resolution: "@esbuild/darwin-x64@npm:0.27.0" +"@esbuild/darwin-x64@npm:0.27.1": + version: 0.27.1 + resolution: "@esbuild/darwin-x64@npm:0.27.1" conditions: os=darwin & cpu=x64 languageName: node linkType: hard @@ -555,9 +555,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.27.0": - version: 0.27.0 - resolution: "@esbuild/freebsd-arm64@npm:0.27.0" +"@esbuild/freebsd-arm64@npm:0.27.1": + version: 0.27.1 + resolution: "@esbuild/freebsd-arm64@npm:0.27.1" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard @@ -569,9 +569,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.27.0": - version: 0.27.0 - resolution: "@esbuild/freebsd-x64@npm:0.27.0" +"@esbuild/freebsd-x64@npm:0.27.1": + version: 0.27.1 + resolution: "@esbuild/freebsd-x64@npm:0.27.1" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard @@ -583,9 +583,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.27.0": - version: 0.27.0 - resolution: "@esbuild/linux-arm64@npm:0.27.0" +"@esbuild/linux-arm64@npm:0.27.1": + version: 0.27.1 + resolution: "@esbuild/linux-arm64@npm:0.27.1" conditions: os=linux & cpu=arm64 languageName: node linkType: hard @@ -597,9 +597,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.27.0": - version: 0.27.0 - resolution: "@esbuild/linux-arm@npm:0.27.0" +"@esbuild/linux-arm@npm:0.27.1": + version: 0.27.1 + resolution: "@esbuild/linux-arm@npm:0.27.1" conditions: os=linux & cpu=arm languageName: node linkType: hard @@ -611,9 +611,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.27.0": - version: 0.27.0 - resolution: "@esbuild/linux-ia32@npm:0.27.0" +"@esbuild/linux-ia32@npm:0.27.1": + version: 0.27.1 + resolution: "@esbuild/linux-ia32@npm:0.27.1" conditions: os=linux & cpu=ia32 languageName: node linkType: hard @@ -625,9 +625,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.27.0": - version: 0.27.0 - resolution: "@esbuild/linux-loong64@npm:0.27.0" +"@esbuild/linux-loong64@npm:0.27.1": + version: 0.27.1 + resolution: "@esbuild/linux-loong64@npm:0.27.1" conditions: os=linux & cpu=loong64 languageName: node linkType: hard @@ -639,9 +639,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.27.0": - version: 0.27.0 - resolution: "@esbuild/linux-mips64el@npm:0.27.0" +"@esbuild/linux-mips64el@npm:0.27.1": + version: 0.27.1 + resolution: "@esbuild/linux-mips64el@npm:0.27.1" conditions: os=linux & cpu=mips64el languageName: node linkType: hard @@ -653,9 +653,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.27.0": - version: 0.27.0 - resolution: "@esbuild/linux-ppc64@npm:0.27.0" +"@esbuild/linux-ppc64@npm:0.27.1": + version: 0.27.1 + resolution: "@esbuild/linux-ppc64@npm:0.27.1" conditions: os=linux & cpu=ppc64 languageName: node linkType: hard @@ -667,9 +667,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.27.0": - version: 0.27.0 - resolution: "@esbuild/linux-riscv64@npm:0.27.0" +"@esbuild/linux-riscv64@npm:0.27.1": + version: 0.27.1 + resolution: "@esbuild/linux-riscv64@npm:0.27.1" conditions: os=linux & cpu=riscv64 languageName: node linkType: hard @@ -681,9 +681,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.27.0": - version: 0.27.0 - resolution: "@esbuild/linux-s390x@npm:0.27.0" +"@esbuild/linux-s390x@npm:0.27.1": + version: 0.27.1 + resolution: "@esbuild/linux-s390x@npm:0.27.1" conditions: os=linux & cpu=s390x languageName: node linkType: hard @@ -695,9 +695,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.27.0": - version: 0.27.0 - resolution: "@esbuild/linux-x64@npm:0.27.0" +"@esbuild/linux-x64@npm:0.27.1": + version: 0.27.1 + resolution: "@esbuild/linux-x64@npm:0.27.1" conditions: os=linux & cpu=x64 languageName: node linkType: hard @@ -709,9 +709,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-arm64@npm:0.27.0": - version: 0.27.0 - resolution: "@esbuild/netbsd-arm64@npm:0.27.0" +"@esbuild/netbsd-arm64@npm:0.27.1": + version: 0.27.1 + resolution: "@esbuild/netbsd-arm64@npm:0.27.1" conditions: os=netbsd & cpu=arm64 languageName: node linkType: hard @@ -723,9 +723,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.27.0": - version: 0.27.0 - resolution: "@esbuild/netbsd-x64@npm:0.27.0" +"@esbuild/netbsd-x64@npm:0.27.1": + version: 0.27.1 + resolution: "@esbuild/netbsd-x64@npm:0.27.1" conditions: os=netbsd & cpu=x64 languageName: node linkType: hard @@ -737,9 +737,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-arm64@npm:0.27.0": - version: 0.27.0 - resolution: "@esbuild/openbsd-arm64@npm:0.27.0" +"@esbuild/openbsd-arm64@npm:0.27.1": + version: 0.27.1 + resolution: "@esbuild/openbsd-arm64@npm:0.27.1" conditions: os=openbsd & cpu=arm64 languageName: node linkType: hard @@ -751,9 +751,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.27.0": - version: 0.27.0 - resolution: "@esbuild/openbsd-x64@npm:0.27.0" +"@esbuild/openbsd-x64@npm:0.27.1": + version: 0.27.1 + resolution: "@esbuild/openbsd-x64@npm:0.27.1" conditions: os=openbsd & cpu=x64 languageName: node linkType: hard @@ -765,9 +765,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/openharmony-arm64@npm:0.27.0": - version: 0.27.0 - resolution: "@esbuild/openharmony-arm64@npm:0.27.0" +"@esbuild/openharmony-arm64@npm:0.27.1": + version: 0.27.1 + resolution: "@esbuild/openharmony-arm64@npm:0.27.1" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard @@ -779,9 +779,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.27.0": - version: 0.27.0 - resolution: "@esbuild/sunos-x64@npm:0.27.0" +"@esbuild/sunos-x64@npm:0.27.1": + version: 0.27.1 + resolution: "@esbuild/sunos-x64@npm:0.27.1" conditions: os=sunos & cpu=x64 languageName: node linkType: hard @@ -793,9 +793,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.27.0": - version: 0.27.0 - resolution: "@esbuild/win32-arm64@npm:0.27.0" +"@esbuild/win32-arm64@npm:0.27.1": + version: 0.27.1 + resolution: "@esbuild/win32-arm64@npm:0.27.1" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard @@ -807,9 +807,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.27.0": - version: 0.27.0 - resolution: "@esbuild/win32-ia32@npm:0.27.0" +"@esbuild/win32-ia32@npm:0.27.1": + version: 0.27.1 + resolution: "@esbuild/win32-ia32@npm:0.27.1" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard @@ -821,9 +821,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.27.0": - version: 0.27.0 - resolution: "@esbuild/win32-x64@npm:0.27.0" +"@esbuild/win32-x64@npm:0.27.1": + version: 0.27.1 + resolution: "@esbuild/win32-x64@npm:0.27.1" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -876,8 +876,8 @@ __metadata: linkType: hard "@eslint/eslintrc@npm:^3.3.1": - version: 3.3.1 - resolution: "@eslint/eslintrc@npm:3.3.1" + version: 3.3.3 + resolution: "@eslint/eslintrc@npm:3.3.3" dependencies: ajv: "npm:^6.12.4" debug: "npm:^4.3.2" @@ -885,10 +885,10 @@ __metadata: globals: "npm:^14.0.0" ignore: "npm:^5.2.0" import-fresh: "npm:^3.2.1" - js-yaml: "npm:^4.1.0" + js-yaml: "npm:^4.1.1" minimatch: "npm:^3.1.2" strip-json-comments: "npm:^3.1.1" - checksum: 10/cc240addbab3c5fceaa65b2c8d5d4fd77ddbbf472c2f74f0270b9d33263dc9116840b6099c46b64c9680301146250439b044ed79278a1bcc557da412a4e3c1bb + checksum: 10/b586a364ff15ce1b68993aefc051ca330b1fece15fb5baf4a708d00113f9a14895cffd84a5f24c5a97bd4b4321130ab2314f90aa462a250f6b859c2da2cba1f3 languageName: node linkType: hard @@ -925,6 +925,21 @@ __metadata: languageName: node linkType: hard +"@google/genai@npm:^1.29.0": + version: 1.31.0 + resolution: "@google/genai@npm:1.31.0" + dependencies: + google-auth-library: "npm:^10.3.0" + ws: "npm:^8.18.0" + peerDependencies: + "@modelcontextprotocol/sdk": ^1.20.1 + peerDependenciesMeta: + "@modelcontextprotocol/sdk": + optional: true + checksum: 10/49da13fc1fe65c05a8c863bf2274fcb862e93e67a2cc30f28054a5b771663df33d04e74d4d763b7f6efb97f27c08b567f05f4822e98e073f18d109ebbf4748dc + languageName: node + linkType: hard + "@highlightjs/vue-plugin@npm:2.1.0": version: 2.1.0 resolution: "@highlightjs/vue-plugin@npm:2.1.0" @@ -1824,55 +1839,55 @@ __metadata: languageName: node linkType: hard -"@textlint/ast-node-types@npm:15.4.0": - version: 15.4.0 - resolution: "@textlint/ast-node-types@npm:15.4.0" - checksum: 10/3ed3cde43701364725c3973ba1a9b9b362eb9a3f4a37649347de117c3ec56ae8a423fed1b34e9a58ea0290ef07f879c287f6a2ff525c07c55da1aadbae668495 +"@textlint/ast-node-types@npm:15.4.1": + version: 15.4.1 + resolution: "@textlint/ast-node-types@npm:15.4.1" + checksum: 10/bd28a8af5c9a0041a951e9d840c5b1c41b6aa18eecc4793a30a46a431dfb8f954ff7d9de7d8211d229e51bcc88441572fbf929f59198b49c6e9191a11d904a38 languageName: node linkType: hard "@textlint/linter-formatter@npm:^15.2.0": - version: 15.4.0 - resolution: "@textlint/linter-formatter@npm:15.4.0" + version: 15.4.1 + resolution: "@textlint/linter-formatter@npm:15.4.1" dependencies: "@azu/format-text": "npm:^1.0.2" "@azu/style-format": "npm:^1.0.1" - "@textlint/module-interop": "npm:15.4.0" - "@textlint/resolver": "npm:15.4.0" - "@textlint/types": "npm:15.4.0" + "@textlint/module-interop": "npm:15.4.1" + "@textlint/resolver": "npm:15.4.1" + "@textlint/types": "npm:15.4.1" chalk: "npm:^4.1.2" debug: "npm:^4.4.3" - js-yaml: "npm:^3.14.1" + js-yaml: "npm:^4.1.0" lodash: "npm:^4.17.21" pluralize: "npm:^2.0.0" string-width: "npm:^4.2.3" strip-ansi: "npm:^6.0.1" table: "npm:^6.9.0" text-table: "npm:^0.2.0" - checksum: 10/ec9afcb3ff71b754b6362b0945825c1cec0c393ac6dff6d89aae67afae6690f7f2e8a1b8f444c6e07617644ada41aa7416629e7eba6820530f0f7c435e130f23 + checksum: 10/94f9668d9acf07d0c10538b97dbc1aceace2ddd67c96716cd931b2de4fef2f364c19d8f30b94375691525e10fee6a22cb8734c773d874824124a0827056cc0fb languageName: node linkType: hard -"@textlint/module-interop@npm:15.4.0, @textlint/module-interop@npm:^15.2.0": - version: 15.4.0 - resolution: "@textlint/module-interop@npm:15.4.0" - checksum: 10/464cd4bbd22dd53da9e68f0200021c6fbb5b0739c3d5f97b2afed7b9cf6e3b7483bb00febe0c209482caf54461a734d72b98b410ae486c1b7c99ef5f89bc22b3 +"@textlint/module-interop@npm:15.4.1, @textlint/module-interop@npm:^15.2.0": + version: 15.4.1 + resolution: "@textlint/module-interop@npm:15.4.1" + checksum: 10/89f5877f50a033653ef153387905ba86e8baa7d37e1453f22626bb0be073f30e1d910448adcffd8fdab2d2f09237089ca0b3ee7eb089043b402fb8239d740041 languageName: node linkType: hard -"@textlint/resolver@npm:15.4.0": - version: 15.4.0 - resolution: "@textlint/resolver@npm:15.4.0" - checksum: 10/755243e10ace39031176387132c7ac6a453f8792897991ec4bcaff908184bcd6481646c30f8ceef1e24e216da015bd002f9d19c9d1a8fbc123de6117ea2397d6 +"@textlint/resolver@npm:15.4.1": + version: 15.4.1 + resolution: "@textlint/resolver@npm:15.4.1" + checksum: 10/a360a3e130931eca1036d354c0f9bd9016e0bb5132cf5c3b7be146275c7927d36b1326e304a55e82b672220561d97e77b427f7503bca042c3402529de8346abb languageName: node linkType: hard -"@textlint/types@npm:15.4.0, @textlint/types@npm:^15.2.0": - version: 15.4.0 - resolution: "@textlint/types@npm:15.4.0" +"@textlint/types@npm:15.4.1, @textlint/types@npm:^15.2.0": + version: 15.4.1 + resolution: "@textlint/types@npm:15.4.1" dependencies: - "@textlint/ast-node-types": "npm:15.4.0" - checksum: 10/8460004ab860c4509786ea0d9b2156c65aeee3e4eb58215f15deede43ee9e9d39def3c68aedabc4157b722cf76eafba3503d5a2e6fe5f091c97ce4ffc12cb0cc + "@textlint/ast-node-types": "npm:15.4.1" + checksum: 10/a5bdaae7f48443f3a24abb20cdd8dd54420a0da1ccda31db47e40746a00144ff8b94ffaf45ab82158096ac34c0dc3af04a313c8ac140d79931244036f8344850 languageName: node linkType: hard @@ -2034,13 +2049,13 @@ __metadata: linkType: hard "@types/express@npm:^5.0.5": - version: 5.0.5 - resolution: "@types/express@npm:5.0.5" + version: 5.0.6 + resolution: "@types/express@npm:5.0.6" dependencies: "@types/body-parser": "npm:*" "@types/express-serve-static-core": "npm:^5.0.0" - "@types/serve-static": "npm:^1" - checksum: 10/9e72410286fbc80bea8a57d1b374c25235f6019dabf8af67e5b72c2f9be548f36ccc09d048fc88172731450e80f06018f90c17e05beb5afc4dbdaf5f7200dbb3 + "@types/serve-static": "npm:^2" + checksum: 10/da2cc3de1b1a4d7f20ed3fb6f0a8ee08e99feb3c2eb5a8d643db77017d8d0e70fee9e95da38a73f51bcdf5eda3bb6435073c0271dc04fb16fda92e55daf911fa languageName: node linkType: hard @@ -2091,6 +2106,13 @@ __metadata: languageName: node linkType: hard +"@types/js-yaml@npm:^4.0.9": + version: 4.0.9 + resolution: "@types/js-yaml@npm:4.0.9" + checksum: 10/a0ce595db8a987904badd21fc50f9f444cb73069f4b95a76cc222e0a17b3ff180669059c763ec314bc4c3ce284379177a9da80e83c5f650c6c1310cafbfaa8e6 + languageName: node + linkType: hard + "@types/jsdom@npm:^27.0.0": version: 27.0.0 resolution: "@types/jsdom@npm:27.0.0" @@ -2134,13 +2156,6 @@ __metadata: languageName: node linkType: hard -"@types/mime@npm:^1": - version: 1.3.5 - resolution: "@types/mime@npm:1.3.5" - checksum: 10/e29a5f9c4776f5229d84e525b7cd7dd960b51c30a0fb9a028c0821790b82fca9f672dab56561e2acd9e8eed51d431bde52eafdfef30f643586c4162f1aecfc78 - languageName: node - linkType: hard - "@types/minimatch@npm:^3.0.3": version: 3.0.5 resolution: "@types/minimatch@npm:3.0.5" @@ -2243,24 +2258,13 @@ __metadata: languageName: node linkType: hard -"@types/send@npm:<1": - version: 0.17.6 - resolution: "@types/send@npm:0.17.6" - dependencies: - "@types/mime": "npm:^1" - "@types/node": "npm:*" - checksum: 10/4948ab32ab84a81a0073f8243dd48ee766bc80608d5391060360afd1249f83c08a7476f142669ac0b0b8831c89d909a88bcb392d1b39ee48b276a91b50f3d8d1 - languageName: node - linkType: hard - -"@types/serve-static@npm:^1": - version: 1.15.10 - resolution: "@types/serve-static@npm:1.15.10" +"@types/serve-static@npm:^2": + version: 2.2.0 + resolution: "@types/serve-static@npm:2.2.0" dependencies: "@types/http-errors": "npm:*" "@types/node": "npm:*" - "@types/send": "npm:<1" - checksum: 10/d9be72487540b9598e7d77260d533f241eb2e5db5181bb885ef2d6bc4592dad1c9e8c0e27f465d59478b2faf90edd2d535e834f20fbd9dd3c0928d43dc486404 + checksum: 10/f2bad1304c7d0d3b7221faff3e490c40129d3803f4fb1b2fb84f31f561071c5e6a4b876c41bbbe82d5645034eea936e946bcaaf993dac1093ce68b56effad6e0 languageName: node linkType: hard @@ -2363,40 +2367,40 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:8.48.0": - version: 8.48.0 - resolution: "@typescript-eslint/eslint-plugin@npm:8.48.0" +"@typescript-eslint/eslint-plugin@npm:8.48.1, @typescript-eslint/eslint-plugin@npm:^8.46.3": + version: 8.48.1 + resolution: "@typescript-eslint/eslint-plugin@npm:8.48.1" dependencies: "@eslint-community/regexpp": "npm:^4.10.0" - "@typescript-eslint/scope-manager": "npm:8.48.0" - "@typescript-eslint/type-utils": "npm:8.48.0" - "@typescript-eslint/utils": "npm:8.48.0" - "@typescript-eslint/visitor-keys": "npm:8.48.0" + "@typescript-eslint/scope-manager": "npm:8.48.1" + "@typescript-eslint/type-utils": "npm:8.48.1" + "@typescript-eslint/utils": "npm:8.48.1" + "@typescript-eslint/visitor-keys": "npm:8.48.1" graphemer: "npm:^1.4.0" ignore: "npm:^7.0.0" natural-compare: "npm:^1.4.0" ts-api-utils: "npm:^2.1.0" peerDependencies: - "@typescript-eslint/parser": ^8.48.0 + "@typescript-eslint/parser": ^8.48.1 eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 10/c9cd87c72da7bb7f6175fdb53a4c08a26e61a3d9d1024960d193276217b37ca1e8e12328a57751ed9380475e11e198f9715e172126ea7d3b3da9948d225db92b + checksum: 10/3ccf420805fb8adb2f3059fa26eb9c6211c0624966d8c8654a1bd586bf87f30be0c62524dfd785185ef573bedd91c42ec3c98c23aed5d60cb9ac583dd9334bc8 languageName: node linkType: hard -"@typescript-eslint/parser@npm:8.48.0, @typescript-eslint/parser@npm:^8.48.0": - version: 8.48.0 - resolution: "@typescript-eslint/parser@npm:8.48.0" +"@typescript-eslint/parser@npm:8.48.1, @typescript-eslint/parser@npm:^8.48.0": + version: 8.48.1 + resolution: "@typescript-eslint/parser@npm:8.48.1" dependencies: - "@typescript-eslint/scope-manager": "npm:8.48.0" - "@typescript-eslint/types": "npm:8.48.0" - "@typescript-eslint/typescript-estree": "npm:8.48.0" - "@typescript-eslint/visitor-keys": "npm:8.48.0" + "@typescript-eslint/scope-manager": "npm:8.48.1" + "@typescript-eslint/types": "npm:8.48.1" + "@typescript-eslint/typescript-estree": "npm:8.48.1" + "@typescript-eslint/visitor-keys": "npm:8.48.1" debug: "npm:^4.3.4" peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 10/5919642345c79a43e57a85e0e69d1f56b5756b3fdb3586ec6371969604f589adc188338c8f12a787456edc3b38c70586d8209cffcf45e35e5a5ebd497c5f4257 + checksum: 10/d8409c9ede4b1cd2ad0e10e94bb00c54f79352f7d54c97bf24419cb983c19b9f6097e6c31b217ce7ec5cfc9a48117e732d9f88ce0cb8c0ccf7fc3faecdf854a3 languageName: node linkType: hard @@ -2413,16 +2417,16 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/project-service@npm:8.48.0": - version: 8.48.0 - resolution: "@typescript-eslint/project-service@npm:8.48.0" +"@typescript-eslint/project-service@npm:8.48.1": + version: 8.48.1 + resolution: "@typescript-eslint/project-service@npm:8.48.1" dependencies: - "@typescript-eslint/tsconfig-utils": "npm:^8.48.0" - "@typescript-eslint/types": "npm:^8.48.0" + "@typescript-eslint/tsconfig-utils": "npm:^8.48.1" + "@typescript-eslint/types": "npm:^8.48.1" debug: "npm:^4.3.4" peerDependencies: typescript: ">=4.8.4 <6.0.0" - checksum: 10/5853a2f57bf8a26b70c1fe5a906c1890ad4f0fca127218a7805161fc9ad547af97f4a600f32f5acdf2f2312b156affca2bea84af9a433215cbcc2056b6a27c77 + checksum: 10/66ecc7ef9572748860517cde7fbfc335d05ca8c99dcf13ac6d728ac93388d90cdc3ebe2ff33a85c0a03487b3c1c4e36c6e3fe413ee16d8fb003621cb58e65e52 languageName: node linkType: hard @@ -2436,13 +2440,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.48.0": - version: 8.48.0 - resolution: "@typescript-eslint/scope-manager@npm:8.48.0" +"@typescript-eslint/scope-manager@npm:8.48.1": + version: 8.48.1 + resolution: "@typescript-eslint/scope-manager@npm:8.48.1" dependencies: - "@typescript-eslint/types": "npm:8.48.0" - "@typescript-eslint/visitor-keys": "npm:8.48.0" - checksum: 10/963af7af235e940467504969c565b359ca454a156eba0d5af2e4fd9cca4294947187e1a85107ff05801688ac85b5767d2566414cbef47a03c23f7b46527decca + "@typescript-eslint/types": "npm:8.48.1" + "@typescript-eslint/visitor-keys": "npm:8.48.1" + checksum: 10/5040246220f9872ec47633297b7896ed5587af3163e06ddcb7ca0dcf1e171f359bd4f1c82f794a6adfecbccfb5ef437d51b522321034603c93ba1993c407bdf2 languageName: node linkType: hard @@ -2455,37 +2459,28 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/tsconfig-utils@npm:8.48.0, @typescript-eslint/tsconfig-utils@npm:^8.48.0": - version: 8.48.0 - resolution: "@typescript-eslint/tsconfig-utils@npm:8.48.0" +"@typescript-eslint/tsconfig-utils@npm:8.48.1, @typescript-eslint/tsconfig-utils@npm:^8.46.4, @typescript-eslint/tsconfig-utils@npm:^8.48.1": + version: 8.48.1 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.48.1" peerDependencies: typescript: ">=4.8.4 <6.0.0" - checksum: 10/e480cd80498c4119a8c5bc413a22abf4bf365b3674ff95f5513292ede31e4fd8118f50d76a786de702696396a43c0c7a4d0c2ccd1c2c7db61bd941ba74495021 + checksum: 10/830bcd0e7628441f91899e8e24aaed66d32a239babcc205aba1d08c08ff5a636d8c04f96d9873578df59d7468fc4c5df032667764b3b2ee0a733af36fca21c4a languageName: node linkType: hard -"@typescript-eslint/tsconfig-utils@npm:^8.46.4": - version: 8.47.0 - resolution: "@typescript-eslint/tsconfig-utils@npm:8.47.0" - peerDependencies: - typescript: ">=4.8.4 <6.0.0" - checksum: 10/7f44441da3778928937419f8ebc62939538cf30087e56c0ca56f599ce98111b82f496902a9e15d713822b9cd14b17937d57b722468450a48748f8e50fd7161af - languageName: node - linkType: hard - -"@typescript-eslint/type-utils@npm:8.48.0": - version: 8.48.0 - resolution: "@typescript-eslint/type-utils@npm:8.48.0" +"@typescript-eslint/type-utils@npm:8.48.1": + version: 8.48.1 + resolution: "@typescript-eslint/type-utils@npm:8.48.1" dependencies: - "@typescript-eslint/types": "npm:8.48.0" - "@typescript-eslint/typescript-estree": "npm:8.48.0" - "@typescript-eslint/utils": "npm:8.48.0" + "@typescript-eslint/types": "npm:8.48.1" + "@typescript-eslint/typescript-estree": "npm:8.48.1" + "@typescript-eslint/utils": "npm:8.48.1" debug: "npm:^4.3.4" ts-api-utils: "npm:^2.1.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 10/dfda42624d534f9fed270bd5c76c9c0bb879cccd3dfbfc2977c84489860fbc204f10bca5c69f3ac856cc4342c12f8947293e7449d3391af289620d7ec79ced0d + checksum: 10/6cf9370ac5437e2d64c71964646aed9e6c1ea3c7bb473258b50ae422106461d290f4215b9435b892a2dd563e3c31feb3169532375513b56b7e48f4a425283091 languageName: node linkType: hard @@ -2496,17 +2491,10 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:8.48.0, @typescript-eslint/types@npm:^8.48.0": - version: 8.48.0 - resolution: "@typescript-eslint/types@npm:8.48.0" - checksum: 10/cd14a7ecd1cb6af94e059a713357b9521ffab08b2793a7d33abda7006816e77f634d49d1ec6f1b99b47257a605347d691bd02b2b11477c9c328f2a27f52a664f - languageName: node - linkType: hard - -"@typescript-eslint/types@npm:^8.46.4": - version: 8.47.0 - resolution: "@typescript-eslint/types@npm:8.47.0" - checksum: 10/fc42416c01c512cfe1533bdf521925bca999adc68ffefa246e48552783f1fe9d22487d912611c5cb35fca481604aae3cab88279a53ce76c7cd7510b76775c078 +"@typescript-eslint/types@npm:8.48.1, @typescript-eslint/types@npm:^8.46.4, @typescript-eslint/types@npm:^8.48.1": + version: 8.48.1 + resolution: "@typescript-eslint/types@npm:8.48.1" + checksum: 10/1aa1e3f25b429bcebd9eb45b5252d950f1b24dbc6014a47dff8d00547e2e1ac47f351846fb996b6ebd49da37a85394051d36191cbbbf2c431b8db9d95afd198d languageName: node linkType: hard @@ -2530,14 +2518,14 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.48.0": - version: 8.48.0 - resolution: "@typescript-eslint/typescript-estree@npm:8.48.0" +"@typescript-eslint/typescript-estree@npm:8.48.1": + version: 8.48.1 + resolution: "@typescript-eslint/typescript-estree@npm:8.48.1" dependencies: - "@typescript-eslint/project-service": "npm:8.48.0" - "@typescript-eslint/tsconfig-utils": "npm:8.48.0" - "@typescript-eslint/types": "npm:8.48.0" - "@typescript-eslint/visitor-keys": "npm:8.48.0" + "@typescript-eslint/project-service": "npm:8.48.1" + "@typescript-eslint/tsconfig-utils": "npm:8.48.1" + "@typescript-eslint/types": "npm:8.48.1" + "@typescript-eslint/visitor-keys": "npm:8.48.1" debug: "npm:^4.3.4" minimatch: "npm:^9.0.4" semver: "npm:^7.6.0" @@ -2545,22 +2533,22 @@ __metadata: ts-api-utils: "npm:^2.1.0" peerDependencies: typescript: ">=4.8.4 <6.0.0" - checksum: 10/8ee6b9e98dd72d567b8842a695578b2098bd8cdcf5628d2819407a52b533a5a139ba9a5620976641bc4553144a1b971d75f2df218a7c281fe674df25835e9e22 + checksum: 10/485aa44d22453396dbe61c560c6f583bf876f971d9e70773093cd729279f88184cf5793bf706033bbd8465cce6f9d045b63574727d58d5996519c29e1adbbfe5 languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.48.0": - version: 8.48.0 - resolution: "@typescript-eslint/utils@npm:8.48.0" +"@typescript-eslint/utils@npm:8.48.1": + version: 8.48.1 + resolution: "@typescript-eslint/utils@npm:8.48.1" dependencies: "@eslint-community/eslint-utils": "npm:^4.7.0" - "@typescript-eslint/scope-manager": "npm:8.48.0" - "@typescript-eslint/types": "npm:8.48.0" - "@typescript-eslint/typescript-estree": "npm:8.48.0" + "@typescript-eslint/scope-manager": "npm:8.48.1" + "@typescript-eslint/types": "npm:8.48.1" + "@typescript-eslint/typescript-estree": "npm:8.48.1" peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 10/980b9faeaae0357bd7c002b15ab3bbcb7d5e4558be5df7980cf5221b41570a1a7b7d71ea2fcc8b1387f6c0db948d01468e6dcb31230d6757e28ac2ee5d8be4cf + checksum: 10/34afe5cf78020b682473e6529d6268eb8015bdb020a3c5303c4abb230d4d7c39e6fc8b9df58d1f0f35a1ceeb5d6182e71e42fe7a28dde8ffc31f8560f2dacc7c languageName: node linkType: hard @@ -2589,13 +2577,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.48.0": - version: 8.48.0 - resolution: "@typescript-eslint/visitor-keys@npm:8.48.0" +"@typescript-eslint/visitor-keys@npm:8.48.1": + version: 8.48.1 + resolution: "@typescript-eslint/visitor-keys@npm:8.48.1" dependencies: - "@typescript-eslint/types": "npm:8.48.0" + "@typescript-eslint/types": "npm:8.48.1" eslint-visitor-keys: "npm:^4.2.1" - checksum: 10/f9eaff8225b3b00e486e0221bd596b08a3ed463f31fab88221256908f6208c48f745281b7b92e6358d25e1dbdc37c6c2f4b42503403c24b071165bafd9a35d52 + checksum: 10/63aa165c57e6b38700adf84da2e90537577cdeb69d05031e3e70785fa412d96d539dc4c1696a0b7bc93284613f8b92fb1bb40f6068bb75347a942120b246ac60 languageName: node linkType: hard @@ -2621,11 +2609,11 @@ __metadata: linkType: hard "@vitest/coverage-v8@npm:^4.0.14": - version: 4.0.14 - resolution: "@vitest/coverage-v8@npm:4.0.14" + version: 4.0.15 + resolution: "@vitest/coverage-v8@npm:4.0.15" dependencies: "@bcoe/v8-coverage": "npm:^1.0.2" - "@vitest/utils": "npm:4.0.14" + "@vitest/utils": "npm:4.0.15" ast-v8-to-istanbul: "npm:^0.3.8" istanbul-lib-coverage: "npm:^3.2.2" istanbul-lib-report: "npm:^3.0.1" @@ -2636,34 +2624,34 @@ __metadata: std-env: "npm:^3.10.0" tinyrainbow: "npm:^3.0.3" peerDependencies: - "@vitest/browser": 4.0.14 - vitest: 4.0.14 + "@vitest/browser": 4.0.15 + vitest: 4.0.15 peerDependenciesMeta: "@vitest/browser": optional: true - checksum: 10/9d6c6a65f223603d2b881664d9e47da6b0b610a84753358195728118be63b6ec977234f707000490cc41bfd80390d8ad4b5af58d9d74b8f302233421cea20d43 + checksum: 10/cdf5d26ba7f6f3895f72662549298e216f810a6cfce8a337d81d8b738df62f0766e0bb5c74f44b09d1282d4a83e14ac63e65c95cef461ac066f4b348c228f9a6 languageName: node linkType: hard -"@vitest/expect@npm:4.0.14": - version: 4.0.14 - resolution: "@vitest/expect@npm:4.0.14" +"@vitest/expect@npm:4.0.15": + version: 4.0.15 + resolution: "@vitest/expect@npm:4.0.15" dependencies: "@standard-schema/spec": "npm:^1.0.0" "@types/chai": "npm:^5.2.2" - "@vitest/spy": "npm:4.0.14" - "@vitest/utils": "npm:4.0.14" + "@vitest/spy": "npm:4.0.15" + "@vitest/utils": "npm:4.0.15" chai: "npm:^6.2.1" tinyrainbow: "npm:^3.0.3" - checksum: 10/7b8d8d1c8532358427a43a77e889131b038dca71021b208a15ec980b27acee7864a555cb3c58885a0302c179bf3c06698055c9fe89b476d477909c083cc0d9ea + checksum: 10/cfb1822012a7ba66d46224c94d2951a780668729199a81eed918103d74110333bd1296e8f598cf2345bac0998f01a71803146da97c8bb69d2775abf3918f02c9 languageName: node linkType: hard -"@vitest/mocker@npm:4.0.14": - version: 4.0.14 - resolution: "@vitest/mocker@npm:4.0.14" +"@vitest/mocker@npm:4.0.15": + version: 4.0.15 + resolution: "@vitest/mocker@npm:4.0.15" dependencies: - "@vitest/spy": "npm:4.0.14" + "@vitest/spy": "npm:4.0.15" estree-walker: "npm:^3.0.3" magic-string: "npm:^0.30.21" peerDependencies: @@ -2674,61 +2662,52 @@ __metadata: optional: true vite: optional: true - checksum: 10/2a5c1507007803972cc6b23e4e560479c13bd90d54b597ac4586a3fec836d417dea533901bfcaf36bb3ca725d9f0ad419c03a77116912efc33f16ba7a170f126 + checksum: 10/9f2aed963bd1bbe13f8acb5d05a95e3cf09d50e57708fb9e88cb4f18b0c0c9c854290bdffd8900914b64796ebdec4c068634487ec2fe55e7984941fff404601a languageName: node linkType: hard -"@vitest/pretty-format@npm:4.0.13": - version: 4.0.13 - resolution: "@vitest/pretty-format@npm:4.0.13" +"@vitest/pretty-format@npm:4.0.15": + version: 4.0.15 + resolution: "@vitest/pretty-format@npm:4.0.15" dependencies: tinyrainbow: "npm:^3.0.3" - checksum: 10/734c6a4d1f7c567bbff7a7855aa90360a46d4f0c221db66d9d63ff7a690a4fd89e36e9eb8db9acf5647bcab9eb3c6625e8d06cc7e1b46321bf7d514375524c8d + checksum: 10/c8ef240027ac340ae420a9b3eb77683a6399edd066832e27793eae19c189e567c5a225c1f26848aa2a2b7545dcc0c9019d6ff0a643cbf0eae004a05117fc2b05 languageName: node linkType: hard -"@vitest/pretty-format@npm:4.0.14": - version: 4.0.14 - resolution: "@vitest/pretty-format@npm:4.0.14" +"@vitest/runner@npm:4.0.15": + version: 4.0.15 + resolution: "@vitest/runner@npm:4.0.15" dependencies: - tinyrainbow: "npm:^3.0.3" - checksum: 10/c5e896640dc4fedfec9e9a7f088fcfeadee3697d11bd7e1f15af7c7d25251a6e3c4590e9f7a24dc67f558c70a7962771f1d1621e2e88dcb02852382dfd96c42f - languageName: node - linkType: hard - -"@vitest/runner@npm:4.0.14": - version: 4.0.14 - resolution: "@vitest/runner@npm:4.0.14" - dependencies: - "@vitest/utils": "npm:4.0.14" + "@vitest/utils": "npm:4.0.15" pathe: "npm:^2.0.3" - checksum: 10/9eb30c8dd84d6c2ae3fcc7bf7b0bb4ade85838cb8568148f15532d7e42aaee4ae43e1ff0d6d4dbece9dad593367b162785b0b3900e003e655070e01a4af04c0b + checksum: 10/682c070d00d0505bc4568e807a746238fe726290bcaea2695a009016ce2c396f8a3c090e5ed12795c1b65bcab4188d2fd8c513ce8324abf978272f319e445d19 languageName: node linkType: hard -"@vitest/snapshot@npm:4.0.14": - version: 4.0.14 - resolution: "@vitest/snapshot@npm:4.0.14" +"@vitest/snapshot@npm:4.0.15": + version: 4.0.15 + resolution: "@vitest/snapshot@npm:4.0.15" dependencies: - "@vitest/pretty-format": "npm:4.0.14" + "@vitest/pretty-format": "npm:4.0.15" magic-string: "npm:^0.30.21" pathe: "npm:^2.0.3" - checksum: 10/e5b7663652b605aeb9058591480ca5f32d1842d9372fa6c09b3a870c569953e04c510b26ae3eca31adebe9f8eef79c51ae2008432c776bbd2ba62c492e76ef9e + checksum: 10/f881257fc1c520541131296f9762d627ad61eb167a3d7129942a5c2dce46e870af1a8446fbf94d2fcdc5a31ab787ffff113f2b8dbd75b15d0494fe43db649682 languageName: node linkType: hard -"@vitest/spy@npm:4.0.14": - version: 4.0.14 - resolution: "@vitest/spy@npm:4.0.14" - checksum: 10/ab7dcef9c01456260c0f45e87d5250db2ce2480ce38412a8422df3c23c9f976ec35496d1593a5d3954552f3b737dbaea66bbddebb42c60621f98439638a296e1 +"@vitest/spy@npm:4.0.15": + version: 4.0.15 + resolution: "@vitest/spy@npm:4.0.15" + checksum: 10/700b06beb4fd33c1430bc5061e7c3055df9ad1e64500a0a02edba6a52e37ba3bf800eadfda1f617e1eeca53d7ab6941a69ba2812980347fcc3c3b736c5ae5a56 languageName: node linkType: hard "@vitest/ui@npm:^4.0.4": - version: 4.0.13 - resolution: "@vitest/ui@npm:4.0.13" + version: 4.0.15 + resolution: "@vitest/ui@npm:4.0.15" dependencies: - "@vitest/utils": "npm:4.0.13" + "@vitest/utils": "npm:4.0.15" fflate: "npm:^0.8.2" flatted: "npm:^3.3.3" pathe: "npm:^2.0.3" @@ -2736,28 +2715,18 @@ __metadata: tinyglobby: "npm:^0.2.15" tinyrainbow: "npm:^3.0.3" peerDependencies: - vitest: 4.0.13 - checksum: 10/b5f77fc3a50c66c14be830988a4e5f299f75ef4bf8a8982eebd89ccf37a56ae7b0113ce404f49b46031c2ceac6031c05f2dc170d3531dca749696603a4587d83 + vitest: 4.0.15 + checksum: 10/327a2723c58931028ef86ca33ffcf39a1be7453f5db57e524b14f0665f21d41edf7c5d3a7efb221f71309fee3dc1e16df4ae7db0f4914a1526576b6e776eaeed languageName: node linkType: hard -"@vitest/utils@npm:4.0.13": - version: 4.0.13 - resolution: "@vitest/utils@npm:4.0.13" +"@vitest/utils@npm:4.0.15": + version: 4.0.15 + resolution: "@vitest/utils@npm:4.0.15" dependencies: - "@vitest/pretty-format": "npm:4.0.13" + "@vitest/pretty-format": "npm:4.0.15" tinyrainbow: "npm:^3.0.3" - checksum: 10/0b0a5d4cc23a4b1cb481f1b31e51fe0f8021bc67478b494f08490f6690d866dedc026275850ce3104c34462ee7b415afb35a3692ced642dfa8339890fa88649e - languageName: node - linkType: hard - -"@vitest/utils@npm:4.0.14": - version: 4.0.14 - resolution: "@vitest/utils@npm:4.0.14" - dependencies: - "@vitest/pretty-format": "npm:4.0.14" - tinyrainbow: "npm:^3.0.3" - checksum: 10/5cd2fee6e5115fc84297c092a8a35b10f962b0c92f5c64669a3523795b2a649b008642fc3c00844be8e6d52abd2b84527c5a618ef156f0a3222004ec2bb0f3e5 + checksum: 10/54d3fd272e05ad43913d842a25dce705eb71db8591511f28fa4a6d0c28fd5eb109c580072e9f8dbc0f431425c890b74494c9d0b14f78d0be18ab87071f06d020 languageName: node linkType: hard @@ -2986,53 +2955,53 @@ __metadata: languageName: node linkType: hard -"@vue/compiler-core@npm:3.5.24": - version: 3.5.24 - resolution: "@vue/compiler-core@npm:3.5.24" +"@vue/compiler-core@npm:3.5.25": + version: 3.5.25 + resolution: "@vue/compiler-core@npm:3.5.25" dependencies: "@babel/parser": "npm:^7.28.5" - "@vue/shared": "npm:3.5.24" + "@vue/shared": "npm:3.5.25" entities: "npm:^4.5.0" estree-walker: "npm:^2.0.2" source-map-js: "npm:^1.2.1" - checksum: 10/c80a93cf178f2a0b824cc90bdf107604f1c7b4ed0f03101dc0fbde5d5933aab17eef99244c59d86798231c034a2e34b8e81fa39a1fb2d302cae3dc82bba56282 + checksum: 10/7a35088a6d25ff7cbfd777df7e61da72bad6c26ac59cb66774e3669745f185632c11acebec2412f6a3e91b713aad2262654a92ce8da20acd629d3a2244820374 languageName: node linkType: hard -"@vue/compiler-dom@npm:3.5.24, @vue/compiler-dom@npm:^3.5.0": - version: 3.5.24 - resolution: "@vue/compiler-dom@npm:3.5.24" +"@vue/compiler-dom@npm:3.5.25, @vue/compiler-dom@npm:^3.5.0": + version: 3.5.25 + resolution: "@vue/compiler-dom@npm:3.5.25" dependencies: - "@vue/compiler-core": "npm:3.5.24" - "@vue/shared": "npm:3.5.24" - checksum: 10/03bda9baabd2fa2959f4111143d1f5dbb39825d0359dc3634c0892ba8bd479328a21726de446da82f6a5e4b6218bde8a2f21a138907236acf6bb2dac979679a6 + "@vue/compiler-core": "npm:3.5.25" + "@vue/shared": "npm:3.5.25" + checksum: 10/aa04f8c87e5f3333375e546fb8dd77e5afba159965ba8f3c8b8216b95f2202bb08acdf014185ac81afdbf69c31c56cefda85a40a14233355ea1ac63aa9f146c2 languageName: node linkType: hard -"@vue/compiler-sfc@npm:3.5.24, @vue/compiler-sfc@npm:^3.3.4": - version: 3.5.24 - resolution: "@vue/compiler-sfc@npm:3.5.24" +"@vue/compiler-sfc@npm:3.5.25, @vue/compiler-sfc@npm:^3.3.4": + version: 3.5.25 + resolution: "@vue/compiler-sfc@npm:3.5.25" dependencies: "@babel/parser": "npm:^7.28.5" - "@vue/compiler-core": "npm:3.5.24" - "@vue/compiler-dom": "npm:3.5.24" - "@vue/compiler-ssr": "npm:3.5.24" - "@vue/shared": "npm:3.5.24" + "@vue/compiler-core": "npm:3.5.25" + "@vue/compiler-dom": "npm:3.5.25" + "@vue/compiler-ssr": "npm:3.5.25" + "@vue/shared": "npm:3.5.25" estree-walker: "npm:^2.0.2" magic-string: "npm:^0.30.21" postcss: "npm:^8.5.6" source-map-js: "npm:^1.2.1" - checksum: 10/617a4b9ed8dfe34aba0fb5da5e6126e4b3693249aa63b11d50c6a4edb453c4e32982d8afc3c2fa46ce3d57456d84c2dd4bbe468d840d47d3782d4207f8dd80b9 + checksum: 10/08bafc59929ba1841a94bd9e2cdd22d494a8db34d6fa1c00e66cb6ae437e3567387da778551ea3424b8aca6e2ddc0e17202288d473ea0c0554448da4ac8eafd6 languageName: node linkType: hard -"@vue/compiler-ssr@npm:3.5.24": - version: 3.5.24 - resolution: "@vue/compiler-ssr@npm:3.5.24" +"@vue/compiler-ssr@npm:3.5.25": + version: 3.5.25 + resolution: "@vue/compiler-ssr@npm:3.5.25" dependencies: - "@vue/compiler-dom": "npm:3.5.24" - "@vue/shared": "npm:3.5.24" - checksum: 10/027101c86d3542641f4c7c19e0fff571ae91d9588af1a68813065f26feb7e0ec0ab21018f17d334e9ab5686c91bf0929846ba3f135148657902238d9c498e09d + "@vue/compiler-dom": "npm:3.5.25" + "@vue/shared": "npm:3.5.25" + checksum: 10/9a485194747ebf38232453173d84eb3f69e59e378d6e81fba61f4999d72226ecc496c1cdbafc3b977786add9f7cab27e40e2a456089f6edbf2164b416375b43f languageName: node linkType: hard @@ -3056,53 +3025,53 @@ __metadata: languageName: node linkType: hard -"@vue/reactivity@npm:3.5.24": - version: 3.5.24 - resolution: "@vue/reactivity@npm:3.5.24" +"@vue/reactivity@npm:3.5.25": + version: 3.5.25 + resolution: "@vue/reactivity@npm:3.5.25" dependencies: - "@vue/shared": "npm:3.5.24" - checksum: 10/880d4edca954121ff9be91b0b1f41efab6a182a658250f7488b165929041ecf1c8a3ee1036ef7b675bce875565d06275861f339e5d176e9b586cd92077fb21dc + "@vue/shared": "npm:3.5.25" + checksum: 10/50d5cb1f4e0619fd77393a307b59cc1ba682e89a5052acb1703d33cb521ddc97702b8539fc1c7ebcc9ff38a145dccd469e7187bb7337dacb85c53329e72ea7df languageName: node linkType: hard -"@vue/runtime-core@npm:3.5.24": - version: 3.5.24 - resolution: "@vue/runtime-core@npm:3.5.24" +"@vue/runtime-core@npm:3.5.25": + version: 3.5.25 + resolution: "@vue/runtime-core@npm:3.5.25" dependencies: - "@vue/reactivity": "npm:3.5.24" - "@vue/shared": "npm:3.5.24" - checksum: 10/2cd1c1806fa119e1e8077fb5e15879be9e01bd21c3d2ee591a1bf1a3ad35ee8c3dc8787c318eab95b646b2267a5612680a4bc34c43a58f9a45f257e5646baa5d + "@vue/reactivity": "npm:3.5.25" + "@vue/shared": "npm:3.5.25" + checksum: 10/4491495fdbf2bc24e73aefbba6756ecf22d42bf5a611c1a84418328e724af3b1ae2a2083e7347a0215db8ebe9a07920286cafc1e66d3c56baa133091046df816 languageName: node linkType: hard -"@vue/runtime-dom@npm:3.5.24": - version: 3.5.24 - resolution: "@vue/runtime-dom@npm:3.5.24" +"@vue/runtime-dom@npm:3.5.25": + version: 3.5.25 + resolution: "@vue/runtime-dom@npm:3.5.25" dependencies: - "@vue/reactivity": "npm:3.5.24" - "@vue/runtime-core": "npm:3.5.24" - "@vue/shared": "npm:3.5.24" + "@vue/reactivity": "npm:3.5.25" + "@vue/runtime-core": "npm:3.5.25" + "@vue/shared": "npm:3.5.25" csstype: "npm:^3.1.3" - checksum: 10/54983b40535e85caffbcc3e9e0be8ce973f825a8114149166b2016136014b2dcb41ac58052de87a3dbc63889afc42e8e2edfc984765ced95a7b265ad43fb2c8a + checksum: 10/07eb65bff96c3bb9cab9f4b52896d804a2fe811d8056d3dc1ea1b8b751d0b72f5b9f6eeac65ce16dc0f593c633f6c31f54bc61698d83e3b888dbb7832a1807b3 languageName: node linkType: hard -"@vue/server-renderer@npm:3.5.24": - version: 3.5.24 - resolution: "@vue/server-renderer@npm:3.5.24" +"@vue/server-renderer@npm:3.5.25": + version: 3.5.25 + resolution: "@vue/server-renderer@npm:3.5.25" dependencies: - "@vue/compiler-ssr": "npm:3.5.24" - "@vue/shared": "npm:3.5.24" + "@vue/compiler-ssr": "npm:3.5.25" + "@vue/shared": "npm:3.5.25" peerDependencies: - vue: 3.5.24 - checksum: 10/e9c941dd89a99c887d2741253687963375fab92f07ab3297923ea7acc70846620adac3673ef71b7a8635799bb0bd0b3593661848544bd5a21edbe619c393173d + vue: 3.5.25 + checksum: 10/065d80301f891a49928aa60e3145748d5e1bfb967979d17c2417b5d1b746964c31121271d6ca24d0db978a51cfa66d96db0659da9da0cf863eda27b4b934826c languageName: node linkType: hard -"@vue/shared@npm:3.5.24, @vue/shared@npm:^3.5.0": - version: 3.5.24 - resolution: "@vue/shared@npm:3.5.24" - checksum: 10/f61f3f7987da680f3cdaa82436310db4d4d74c5e18761deecac7421367c6a15ec59a0944e68b3a2bf960d60715f6c3ffb6535dbeeac921eb834b7e018161d231 +"@vue/shared@npm:3.5.25, @vue/shared@npm:^3.5.0": + version: 3.5.25 + resolution: "@vue/shared@npm:3.5.25" + checksum: 10/8f5102be5e8dec105df6409e048549c7887bd09a23b4253e26bf14ddcafa9648783785c2334d243c6b54fbbfc297ce5c254c6646a7ef2b42899e3784155be277 languageName: node linkType: hard @@ -3447,9 +3416,9 @@ __metadata: linkType: hard "alien-signals@npm:^3.0.0": - version: 3.1.0 - resolution: "alien-signals@npm:3.1.0" - checksum: 10/59534a0df3b23d932cfb4012a9f563d595815be91c079103d9ba0cf9f6df5c0fcd33e632b5d2e694714a159e43e7ec30d98e4dd53529300c30015769b0311705 + version: 3.1.1 + resolution: "alien-signals@npm:3.1.1" + checksum: 10/90a3cbff5dbf11ad5603a51deff38913ae9b4d75f0d9e113b5d1fc43087b6da7673065fc2e25c9211a512e535c88bfa72bd72766c1d92aa595fed1d76a8b38ec languageName: node linkType: hard @@ -3499,6 +3468,7 @@ __metadata: "@ansible/ansible-language-server": "workspace:^" "@ansible/ansible-mcp-server": "workspace:^" "@eslint/js": "npm:^9.39.1" + "@google/genai": "npm:^1.29.0" "@highlightjs/vue-plugin": "npm:2.1.0" "@html-eslint/eslint-plugin": "npm:^0.50.0" "@primeuix/themes": "npm:^1.2.5" @@ -3509,6 +3479,7 @@ __metadata: "@types/express": "npm:^5.0.5" "@types/glob": "npm:^9.0.0" "@types/ini": "npm:^4.1.1" + "@types/js-yaml": "npm:^4.0.9" "@types/jsdom": "npm:^27.0.0" "@types/lodash": "npm:^4.17.20" "@types/minimatch": "npm:^6.0.0" @@ -3521,6 +3492,7 @@ __metadata: "@types/vscode": "npm:^1.85.0" "@types/vscode-webview": "npm:^1.57.5" "@types/yargs": "npm:^17.0.35" + "@typescript-eslint/eslint-plugin": "npm:^8.46.3" "@typescript-eslint/parser": "npm:^8.48.0" "@vitejs/plugin-vue": "npm:^5.2.4" "@vitest/coverage-v8": "npm:^4.0.14" @@ -3546,6 +3518,7 @@ __metadata: globals: "npm:^16.5.0" highlight.js: "npm:^11.11.1" ini: "npm:^6.0.0" + js-yaml: "npm:^4.1.0" jsdom: "npm:^27.2.0" lodash: "npm:^4.17.21" marked: "npm:^17.0.1" @@ -3731,19 +3704,19 @@ __metadata: languageName: node linkType: hard -"base64-js@npm:^1.3.1": +"base64-js@npm:^1.3.0, base64-js@npm:^1.3.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" checksum: 10/669632eb3745404c2f822a18fc3a0122d2f9a7a13f7fb8b5823ee19d1d2ff9ee5b52c53367176ea4ad093c332fd5ab4bd0ebae5a8e27917a4105a4cfc86b1005 languageName: node linkType: hard -"baseline-browser-mapping@npm:^2.8.25": - version: 2.8.31 - resolution: "baseline-browser-mapping@npm:2.8.31" +"baseline-browser-mapping@npm:^2.9.0": + version: 2.9.2 + resolution: "baseline-browser-mapping@npm:2.9.2" bin: baseline-browser-mapping: dist/cli.js - checksum: 10/aefad7523ab6e93a28d278c2faae08b934d6f8899f9b6868a50cccb2e2cbb12bad0f5eda3ccb70d7f2e2f9e58cc1973050523e867e6bb0793ab0c63e194956b6 + checksum: 10/6e42ae4aaaa0becd37a58b00aa4734063a59b752b5bb66776fc73858d8d7eecea007f4af67fba7c48a9022ec243ad6a77aee735d9e063b1220a7f39b8cd39f4b languageName: node linkType: hard @@ -3765,6 +3738,13 @@ __metadata: languageName: node linkType: hard +"bignumber.js@npm:^9.0.0": + version: 9.3.1 + resolution: "bignumber.js@npm:9.3.1" + checksum: 10/1be0372bf0d6d29d0a49b9e6a9cefbd54dad9918232ad21fcd4ec39030260773abf0c76af960c6b3b98d3115a3a71e61c6a111812d1395040a039cfa178e0245 + languageName: node + linkType: hard + "binary-extensions@npm:^2.0.0": version: 2.3.0 resolution: "binary-extensions@npm:2.3.0" @@ -3876,17 +3856,17 @@ __metadata: linkType: hard "browserslist@npm:^4.26.3": - version: 4.28.0 - resolution: "browserslist@npm:4.28.0" + version: 4.28.1 + resolution: "browserslist@npm:4.28.1" dependencies: - baseline-browser-mapping: "npm:^2.8.25" - caniuse-lite: "npm:^1.0.30001754" - electron-to-chromium: "npm:^1.5.249" + baseline-browser-mapping: "npm:^2.9.0" + caniuse-lite: "npm:^1.0.30001759" + electron-to-chromium: "npm:^1.5.263" node-releases: "npm:^2.0.27" - update-browserslist-db: "npm:^1.1.4" + update-browserslist-db: "npm:^1.2.0" bin: browserslist: cli.js - checksum: 10/59dc88f8d950e44a064361cb874f486e532a8ba932e0cf549aee8b36dd2b791da2bc11f36c1cf820ebb9c1f3250b100f8c56364dd6e86dbc90495af424100e19 + checksum: 10/64f2a97de4bce8473c0e5ae0af8d76d1ead07a5b05fc6bc87b848678bb9c3a91ae787b27aa98cdd33fc00779607e6c156000bed58fefb9cf8e4c5a183b994cdb languageName: node linkType: hard @@ -4116,10 +4096,10 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.30001754": - version: 1.0.30001756 - resolution: "caniuse-lite@npm:1.0.30001756" - checksum: 10/1aa412f539bf2f3b9120caa6a3552579914cc9de7bc075212d1196e625dc6243317005a45c6aa7675610e5e23d47418564b7b3e7700244005bd665b01625b0ae +"caniuse-lite@npm:^1.0.30001759": + version: 1.0.30001759 + resolution: "caniuse-lite@npm:1.0.30001759" + checksum: 10/da0ec28dd993dffa99402914903426b9466d2798d41c1dc9341fcb7dd10f58fdd148122e2c65001246c030ba1c939645b7b4597f6321e3246dc792323bb11541 languageName: node linkType: hard @@ -4624,6 +4604,13 @@ __metadata: languageName: node linkType: hard +"data-uri-to-buffer@npm:^4.0.0": + version: 4.0.1 + resolution: "data-uri-to-buffer@npm:4.0.1" + checksum: 10/0d0790b67ffec5302f204c2ccca4494f70b4e2d940fea3d36b09f0bb2b8539c2e86690429eb1f1dc4bcc9e4df0644193073e63d9ee48ac9fce79ec1506e4aa4c + languageName: node + linkType: hard + "data-urls@npm:^6.0.0": version: 6.0.0 resolution: "data-urls@npm:6.0.0" @@ -4908,7 +4895,7 @@ __metadata: languageName: node linkType: hard -"ecdsa-sig-formatter@npm:1.0.11": +"ecdsa-sig-formatter@npm:1.0.11, ecdsa-sig-formatter@npm:^1.0.11": version: 1.0.11 resolution: "ecdsa-sig-formatter@npm:1.0.11" dependencies: @@ -4933,10 +4920,10 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.5.249": - version: 1.5.259 - resolution: "electron-to-chromium@npm:1.5.259" - checksum: 10/259aa8b9c2a82e54c60b2c9af283aa9f618f42aad3c54613fcea61d8e12e4c8f0641d625ba2e6687fe69eda1fc2f3c747e8dafad1c789ac59ac47d4afceeaa23 +"electron-to-chromium@npm:^1.5.263": + version: 1.5.264 + resolution: "electron-to-chromium@npm:1.5.264" + checksum: 10/83722da6fec39a3b1561a3d1087e726f9366339330891786de0af87cbaacc5c8484cc70ea2687c2c0e7ea702d4f56c45ec28dc5a831315b8f40ebd0da839ffa8 languageName: node linkType: hard @@ -5035,11 +5022,11 @@ __metadata: linkType: hard "envinfo@npm:^7.14.0": - version: 7.20.0 - resolution: "envinfo@npm:7.20.0" + version: 7.21.0 + resolution: "envinfo@npm:7.21.0" bin: envinfo: dist/cli.js - checksum: 10/9dab1e64e0b6614243b72b07d782434e0984ff6104ef8906c3b5e1562f96e0cde8bf38a9dcf373494a020b9752a0edeeec2a4fdf7bcd41d5d704791f93097ffe + checksum: 10/2469a72802ded4e43c007dcd1c5dd44d8049b7d18276874dcc3f3f14a54bc72806fa35e82760974ca1442d82f5f9df3651048204e72791f81bcdd5f07422a561 languageName: node linkType: hard @@ -5122,7 +5109,7 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.25.0, esbuild@npm:~0.25.0": +"esbuild@npm:^0.25.0": version: 0.25.12 resolution: "esbuild@npm:0.25.12" dependencies: @@ -5211,36 +5198,36 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.27.0": - version: 0.27.0 - resolution: "esbuild@npm:0.27.0" - dependencies: - "@esbuild/aix-ppc64": "npm:0.27.0" - "@esbuild/android-arm": "npm:0.27.0" - "@esbuild/android-arm64": "npm:0.27.0" - "@esbuild/android-x64": "npm:0.27.0" - "@esbuild/darwin-arm64": "npm:0.27.0" - "@esbuild/darwin-x64": "npm:0.27.0" - "@esbuild/freebsd-arm64": "npm:0.27.0" - "@esbuild/freebsd-x64": "npm:0.27.0" - "@esbuild/linux-arm": "npm:0.27.0" - "@esbuild/linux-arm64": "npm:0.27.0" - "@esbuild/linux-ia32": "npm:0.27.0" - "@esbuild/linux-loong64": "npm:0.27.0" - "@esbuild/linux-mips64el": "npm:0.27.0" - "@esbuild/linux-ppc64": "npm:0.27.0" - "@esbuild/linux-riscv64": "npm:0.27.0" - "@esbuild/linux-s390x": "npm:0.27.0" - "@esbuild/linux-x64": "npm:0.27.0" - "@esbuild/netbsd-arm64": "npm:0.27.0" - "@esbuild/netbsd-x64": "npm:0.27.0" - "@esbuild/openbsd-arm64": "npm:0.27.0" - "@esbuild/openbsd-x64": "npm:0.27.0" - "@esbuild/openharmony-arm64": "npm:0.27.0" - "@esbuild/sunos-x64": "npm:0.27.0" - "@esbuild/win32-arm64": "npm:0.27.0" - "@esbuild/win32-ia32": "npm:0.27.0" - "@esbuild/win32-x64": "npm:0.27.0" +"esbuild@npm:^0.27.0, esbuild@npm:~0.27.0": + version: 0.27.1 + resolution: "esbuild@npm:0.27.1" + dependencies: + "@esbuild/aix-ppc64": "npm:0.27.1" + "@esbuild/android-arm": "npm:0.27.1" + "@esbuild/android-arm64": "npm:0.27.1" + "@esbuild/android-x64": "npm:0.27.1" + "@esbuild/darwin-arm64": "npm:0.27.1" + "@esbuild/darwin-x64": "npm:0.27.1" + "@esbuild/freebsd-arm64": "npm:0.27.1" + "@esbuild/freebsd-x64": "npm:0.27.1" + "@esbuild/linux-arm": "npm:0.27.1" + "@esbuild/linux-arm64": "npm:0.27.1" + "@esbuild/linux-ia32": "npm:0.27.1" + "@esbuild/linux-loong64": "npm:0.27.1" + "@esbuild/linux-mips64el": "npm:0.27.1" + "@esbuild/linux-ppc64": "npm:0.27.1" + "@esbuild/linux-riscv64": "npm:0.27.1" + "@esbuild/linux-s390x": "npm:0.27.1" + "@esbuild/linux-x64": "npm:0.27.1" + "@esbuild/netbsd-arm64": "npm:0.27.1" + "@esbuild/netbsd-x64": "npm:0.27.1" + "@esbuild/openbsd-arm64": "npm:0.27.1" + "@esbuild/openbsd-x64": "npm:0.27.1" + "@esbuild/openharmony-arm64": "npm:0.27.1" + "@esbuild/sunos-x64": "npm:0.27.1" + "@esbuild/win32-arm64": "npm:0.27.1" + "@esbuild/win32-ia32": "npm:0.27.1" + "@esbuild/win32-x64": "npm:0.27.1" dependenciesMeta: "@esbuild/aix-ppc64": optional: true @@ -5296,7 +5283,7 @@ __metadata: optional: true bin: esbuild: bin/esbuild - checksum: 10/17a34f3c7cf67f5903693d14401beb3025bd9ecbffbfdd3f0d504e299689679fdd8034c94d9a6814983d448afc7d9fce5b8dfe03ee24ba23ee6f144a9dd2f15d + checksum: 10/534148f01e85ca93ec3a4ae8bef133680f5659e639915cd3a453d6ec9ead94c9a2e9bfd61380301471447e182beb62841cb72e0fa18251cdce3454a2511d7cf4 languageName: node linkType: hard @@ -5631,8 +5618,8 @@ __metadata: linkType: hard "execa@npm:^9.6.0": - version: 9.6.0 - resolution: "execa@npm:9.6.0" + version: 9.6.1 + resolution: "execa@npm:9.6.1" dependencies: "@sindresorhus/merge-streams": "npm:^4.0.0" cross-spawn: "npm:^7.0.6" @@ -5646,7 +5633,7 @@ __metadata: signal-exit: "npm:^4.1.0" strip-final-newline: "npm:^4.0.0" yoctocolors: "npm:^2.1.1" - checksum: 10/53443be93d847ff5b52d31ed3714f77aab764fb6c1d72dc7019214ab1cb1a69888e2158ba846426a8ea51443c110fe7a86de61ffb9ee5687b00120fbd739b8a4 + checksum: 10/d0f7a2185152379f8772f6d780b188f2728a95b9a68d1a897f58805d7ba6bd55eaa5e128cb66a274251a6b5e4d9388332b1417bd7d46c25e020e4e55725cf79e languageName: node linkType: hard @@ -5732,6 +5719,13 @@ __metadata: languageName: node linkType: hard +"extend@npm:^3.0.2": + version: 3.0.2 + resolution: "extend@npm:3.0.2" + checksum: 10/59e89e2dc798ec0f54b36d82f32a27d5f6472c53974f61ca098db5d4648430b725387b53449a34df38fd0392045434426b012f302b3cc049a6500ccf82877e4e + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -5824,6 +5818,16 @@ __metadata: languageName: node linkType: hard +"fetch-blob@npm:^3.1.2, fetch-blob@npm:^3.1.4": + version: 3.2.0 + resolution: "fetch-blob@npm:3.2.0" + dependencies: + node-domexception: "npm:^1.0.0" + web-streams-polyfill: "npm:^3.0.3" + checksum: 10/5264ecceb5fdc19eb51d1d0359921f12730941e333019e673e71eb73921146dceabcb0b8f534582be4497312d656508a439ad0f5edeec2b29ab2e10c72a1f86b + languageName: node + linkType: hard + "fflate@npm:^0.8.2": version: 0.8.2 resolution: "fflate@npm:0.8.2" @@ -5859,8 +5863,8 @@ __metadata: linkType: hard "finalhandler@npm:^2.1.0": - version: 2.1.0 - resolution: "finalhandler@npm:2.1.0" + version: 2.1.1 + resolution: "finalhandler@npm:2.1.1" dependencies: debug: "npm:^4.4.0" encodeurl: "npm:^2.0.0" @@ -5868,7 +5872,7 @@ __metadata: on-finished: "npm:^2.4.1" parseurl: "npm:^1.3.3" statuses: "npm:^2.0.1" - checksum: 10/b2bd68c310e2c463df0ab747ab05f8defbc540b8c3f2442f86e7d084ac8acbc31f8cae079931b7f5a406521501941e3395e963de848a0aaf45dd414adeb5ff4e + checksum: 10/f4ba75c23408d8f9d393c3e875b9452e84d68c925411a6e67b7efa678b0bed5075ef33def4bb65ed8e0dd37c92a3ea354bcbde07303cd4dc2550e12b95885067 languageName: node linkType: hard @@ -6020,6 +6024,15 @@ __metadata: languageName: node linkType: hard +"formdata-polyfill@npm:^4.0.10": + version: 4.0.10 + resolution: "formdata-polyfill@npm:4.0.10" + dependencies: + fetch-blob: "npm:^3.1.2" + checksum: 10/9b5001d2edef3c9449ac3f48bd4f8cc92e7d0f2e7c1a5c8ba555ad4e77535cc5cf621fabe49e97f304067037282dd9093b9160a3cb533e46420b446c4e6bc06f + languageName: node + linkType: hard + "forwarded@npm:0.2.0": version: 0.2.0 resolution: "forwarded@npm:0.2.0" @@ -6094,6 +6107,29 @@ __metadata: languageName: node linkType: hard +"gaxios@npm:^7.0.0": + version: 7.1.3 + resolution: "gaxios@npm:7.1.3" + dependencies: + extend: "npm:^3.0.2" + https-proxy-agent: "npm:^7.0.1" + node-fetch: "npm:^3.3.2" + rimraf: "npm:^5.0.1" + checksum: 10/234ae4d622c41472a0f1be252a9a0f0a6f4f6ae2418671ef83fd715b9e315100c621818d721fdf0e7471ef49a460f196eaf318c2b36ed5f51a1cf0f6a2639111 + languageName: node + linkType: hard + +"gcp-metadata@npm:^8.0.0": + version: 8.1.2 + resolution: "gcp-metadata@npm:8.1.2" + dependencies: + gaxios: "npm:^7.0.0" + google-logging-utils: "npm:^1.0.0" + json-bigint: "npm:^1.0.0" + checksum: 10/b3a4674067692991d1b72ddb5ff8cc24d08756fac2cf9ba4b49d92d0062724eca111ba58656fac54343bae8f0a29c8d264fb655ca2d6570e156fbdc338c787d9 + languageName: node + linkType: hard + "generator-function@npm:^2.0.0": version: 2.0.1 resolution: "generator-function@npm:2.0.1" @@ -6233,7 +6269,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.3.10, glob@npm:^10.4.1, glob@npm:^10.4.5": +"glob@npm:^10.3.10, glob@npm:^10.3.7, glob@npm:^10.4.1, glob@npm:^10.4.5": version: 10.5.0 resolution: "glob@npm:10.5.0" dependencies: @@ -6327,6 +6363,28 @@ __metadata: languageName: node linkType: hard +"google-auth-library@npm:^10.3.0": + version: 10.5.0 + resolution: "google-auth-library@npm:10.5.0" + dependencies: + base64-js: "npm:^1.3.0" + ecdsa-sig-formatter: "npm:^1.0.11" + gaxios: "npm:^7.0.0" + gcp-metadata: "npm:^8.0.0" + google-logging-utils: "npm:^1.0.0" + gtoken: "npm:^8.0.0" + jws: "npm:^4.0.0" + checksum: 10/f9cec00f17f1082bc2e1e342043425063e347a06c4b4ae40c5d644f4491755b18690cb7f50daa9aa056539b14f993febdc009b0dada0c9b5bcf35c7e0caf78a3 + languageName: node + linkType: hard + +"google-logging-utils@npm:^1.0.0": + version: 1.1.3 + resolution: "google-logging-utils@npm:1.1.3" + checksum: 10/5a6c090399545e0f1f2c92fbda316479dc5d573b2f4b54f0deb570dc31d8b254537894fd4e7c275ce7d352482e40d5857fa4b960c1b3d869584b5216dc2076e2 + languageName: node + linkType: hard + "gopd@npm:^1.0.1, gopd@npm:^1.2.0": version: 1.2.0 resolution: "gopd@npm:1.2.0" @@ -6368,6 +6426,16 @@ __metadata: languageName: node linkType: hard +"gtoken@npm:^8.0.0": + version: 8.0.0 + resolution: "gtoken@npm:8.0.0" + dependencies: + gaxios: "npm:^7.0.0" + jws: "npm:^4.0.0" + checksum: 10/b921430395dcd06ee63c3fc5a5e339ca4d6dcb38b6d618beb0f260bae1088d53d130f86029a9d578f1601c64685f49a65dba57bbd617c4b14039180b67b6c5ce + languageName: node + linkType: hard + "handlebars@npm:^4.7.8": version: 4.7.8 resolution: "handlebars@npm:4.7.8" @@ -7115,7 +7183,7 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:^4.1.0": +"js-yaml@npm:^4.1.0, js-yaml@npm:^4.1.1": version: 4.1.1 resolution: "js-yaml@npm:4.1.1" dependencies: @@ -7168,6 +7236,15 @@ __metadata: languageName: node linkType: hard +"json-bigint@npm:^1.0.0": + version: 1.0.0 + resolution: "json-bigint@npm:1.0.0" + dependencies: + bignumber.js: "npm:^9.0.0" + checksum: 10/cd3973b88e5706f8f89d2a9c9431f206ef385bd5c584db1b258891a5e6642507c32316b82745239088c697f5ddfe967351e1731f5789ba7855aed56ad5f70e1f + languageName: node + linkType: hard + "json-buffer@npm:3.0.1": version: 3.0.1 resolution: "json-buffer@npm:3.0.1" @@ -7233,10 +7310,10 @@ __metadata: linkType: hard "jsonwebtoken@npm:^9.0.0": - version: 9.0.2 - resolution: "jsonwebtoken@npm:9.0.2" + version: 9.0.3 + resolution: "jsonwebtoken@npm:9.0.3" dependencies: - jws: "npm:^3.2.2" + jws: "npm:^4.0.1" lodash.includes: "npm:^4.3.0" lodash.isboolean: "npm:^3.0.3" lodash.isinteger: "npm:^4.0.4" @@ -7246,7 +7323,7 @@ __metadata: lodash.once: "npm:^4.0.0" ms: "npm:^2.1.1" semver: "npm:^7.5.4" - checksum: 10/6e9b6d879cec2b27f2f3a88a0c0973edc7ba956a5d9356b2626c4fddfda969e34a3832deaf79c3e1c6c9a525bc2c4f2c2447fa477f8ac660f0017c31a59ae96b + checksum: 10/a67a276db41fbfb458ebdc4938d5d7b01d4743e16bda0f25ac01996fe5b5819d66656153f6cfce19b4680b79ae9f9ca185965defc22e77e0abddf443573238d6 languageName: node linkType: hard @@ -7262,24 +7339,24 @@ __metadata: languageName: node linkType: hard -"jwa@npm:^1.4.1": - version: 1.4.2 - resolution: "jwa@npm:1.4.2" +"jwa@npm:^2.0.1": + version: 2.0.1 + resolution: "jwa@npm:2.0.1" dependencies: buffer-equal-constant-time: "npm:^1.0.1" ecdsa-sig-formatter: "npm:1.0.11" safe-buffer: "npm:^5.0.1" - checksum: 10/a46c9ddbcc226d9e85e13ef96328c7d331abddd66b5a55ec44bcf4350464a6125385ac9c1e64faa0fae8d586d90a14d6b5e96c73f0388970a3918d5252efb0f3 + checksum: 10/b04312a1de85f912b96aa3a7211717b8336945fab5b4f7cbc7800f4c80934060c0a3111576fad8d76e41ad62887d6da4b21fd4c47e45c174197f8be7dc0c1694 languageName: node linkType: hard -"jws@npm:^3.2.2": - version: 3.2.2 - resolution: "jws@npm:3.2.2" +"jws@npm:^4.0.0, jws@npm:^4.0.1": + version: 4.0.1 + resolution: "jws@npm:4.0.1" dependencies: - jwa: "npm:^1.4.1" + jwa: "npm:^2.0.1" safe-buffer: "npm:^5.0.1" - checksum: 10/70b016974af8a76d25030c80a0097b24ed5b17a9cf10f43b163c11cb4eb248d5d04a3fe48c0d724d2884c32879d878ccad7be0663720f46b464f662f7ed778fe + checksum: 10/75d7b157489fa9a72023712c58a7a7706c7e2b10eec27fabd3bb9cae0c9e492251ab72527d20a8a5f5726196f0508c320c643fddff7076657f6bca16d0ceeeeb languageName: node linkType: hard @@ -7304,11 +7381,11 @@ __metadata: linkType: hard "keyv@npm:^5.5.3, keyv@npm:^5.5.4": - version: 5.5.4 - resolution: "keyv@npm:5.5.4" + version: 5.5.5 + resolution: "keyv@npm:5.5.5" dependencies: "@keyv/serialize": "npm:^1.1.1" - checksum: 10/2ee2178657b3f220cc7130727a1f9e65d05f2115c924af82e6f53e2c8b0197795a55fe7d4e6041ba712a6748039e5a4f7f50ad708bffb8bcc407b0dd0678dfa4 + checksum: 10/4bbc2119151d67bfc04d3a08fc98f4efac5e171e8c99519fc7bc9ae9e05df4d1c6c835a5d133d563ed109a20db4688f3f9738400cbd3674f6303cdfc4490d429 languageName: node linkType: hard @@ -7603,9 +7680,9 @@ __metadata: linkType: hard "lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1, lru-cache@npm:^11.2.2": - version: 11.2.2 - resolution: "lru-cache@npm:11.2.2" - checksum: 10/fa7919fbf068a739f79a1ad461eb273514da7246cebb9dca68e3cd7ba19e3839e7e2aaecd9b72867e08038561eeb96941189e89b3d4091c75ced4f56c71c80db + version: 11.2.4 + resolution: "lru-cache@npm:11.2.4" + checksum: 10/3b2da74c0b6653767f8164c38c4c4f4d7f0cc10c62bfa512663d94a830191ae6a5af742a8d88a8b30d5f9974652d3adae53931f32069139ad24fa2a18a199aca languageName: node linkType: hard @@ -8221,6 +8298,13 @@ __metadata: languageName: node linkType: hard +"node-domexception@npm:^1.0.0": + version: 1.0.0 + resolution: "node-domexception@npm:1.0.0" + checksum: 10/e332522f242348c511640c25a6fc7da4f30e09e580c70c6b13cb0be83c78c3e71c8d4665af2527e869fc96848924a4316ae7ec9014c091e2156f41739d4fa233 + languageName: node + linkType: hard + "node-fetch@npm:^2.6.7": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" @@ -8235,6 +8319,17 @@ __metadata: languageName: node linkType: hard +"node-fetch@npm:^3.3.2": + version: 3.3.2 + resolution: "node-fetch@npm:3.3.2" + dependencies: + data-uri-to-buffer: "npm:^4.0.0" + fetch-blob: "npm:^3.1.4" + formdata-polyfill: "npm:^4.0.10" + checksum: 10/24207ca8c81231c7c59151840e3fded461d67a31cf3e3b3968e12201a42f89ce4a0b5fb7079b1fa0a4655957b1ca9257553200f03a9f668b45ebad265ca5593d + languageName: node + linkType: hard + "node-gyp@npm:latest": version: 12.1.0 resolution: "node-gyp@npm:12.1.0" @@ -8981,11 +9076,11 @@ __metadata: linkType: hard "prettier@npm:^3.6.2": - version: 3.6.2 - resolution: "prettier@npm:3.6.2" + version: 3.7.4 + resolution: "prettier@npm:3.7.4" bin: prettier: bin/prettier.cjs - checksum: 10/1213691706bcef1371d16ef72773c8111106c3533b660b1cc8ec158bd109cdf1462804125f87f981f23c4a3dba053b6efafda30ab0114cc5b4a725606bb9ff26 + checksum: 10/b4d00ea13baed813cb777c444506632fb10faaef52dea526cacd03085f01f6db11fc969ccebedf05bf7d93c3960900994c6adf1b150e28a31afd5cfe7089b313 languageName: node linkType: hard @@ -9012,9 +9107,9 @@ __metadata: linkType: hard "proc-log@npm:^6.0.0": - version: 6.0.0 - resolution: "proc-log@npm:6.0.0" - checksum: 10/98831f35d30f254f89836ff3eb89e5970ed8f88ad1bde2ce6c0baa70e0f53166408ba8d9c6a5e3c44d10b611bb415ac46d9b2c78277a397608890c044f9d5942 + version: 6.1.0 + resolution: "proc-log@npm:6.1.0" + checksum: 10/9033f30f168ed5a0991b773d0c50ff88384c4738e9a0a67d341de36bf7293771eed648ab6a0562f62276da12fde91f3bbfc75ffff6e71ad49aafd74fc646be66 languageName: node linkType: hard @@ -9157,9 +9252,9 @@ __metadata: linkType: hard "react@npm:^19.2.0": - version: 19.2.0 - resolution: "react@npm:19.2.0" - checksum: 10/e13bcdb8e994c3cfa922743cb75ca8deb60531bf02f584d2d8dab940a8132ce8a2e6ef16f8ed7f372b4072e7a7eeff589b2812dabbedfa73e6e46201dac8a9d0 + version: 19.2.1 + resolution: "react@npm:19.2.1" + checksum: 10/7c7ab0f40b98e87e1466bea8c28564c5f3c384506cbda93d24788d32b40bf6059c991687c7e90a396b11f09223a779a81b1188af9acd839aaa0a672987c13107 languageName: node linkType: hard @@ -9363,6 +9458,17 @@ __metadata: languageName: node linkType: hard +"rimraf@npm:^5.0.1": + version: 5.0.10 + resolution: "rimraf@npm:5.0.10" + dependencies: + glob: "npm:^10.3.7" + bin: + rimraf: dist/esm/bin.mjs + checksum: 10/f3b8ce81eecbde4628b07bdf9e2fa8b684e0caea4999acb1e3b0402c695cd41f28cd075609a808e61ce2672f528ca079f675ab1d8e8d5f86d56643a03e0b8d2e + languageName: node + linkType: hard + "rimraf@npm:^6.1.2": version: 6.1.2 resolution: "rimraf@npm:6.1.2" @@ -10339,6 +10445,13 @@ __metadata: languageName: node linkType: hard +"tinyexec@npm:^1.0.2": + version: 1.0.2 + resolution: "tinyexec@npm:1.0.2" + checksum: 10/cb709ed4240e873d3816e67f851d445f5676e0ae3a52931a60ff571d93d388da09108c8057b62351766133ee05ff3159dd56c3a0fbd39a5933c6639ce8771405 + languageName: node + linkType: hard + "tinyglobby@npm:^0.2.11, tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.15": version: 0.2.15 resolution: "tinyglobby@npm:0.2.15" @@ -10592,10 +10705,10 @@ __metadata: linkType: hard "tsx@npm:^4.20.6": - version: 4.20.6 - resolution: "tsx@npm:4.20.6" + version: 4.21.0 + resolution: "tsx@npm:4.21.0" dependencies: - esbuild: "npm:~0.25.0" + esbuild: "npm:~0.27.0" fsevents: "npm:~2.3.3" get-tsconfig: "npm:^4.7.5" dependenciesMeta: @@ -10603,7 +10716,7 @@ __metadata: optional: true bin: tsx: dist/cli.mjs - checksum: 10/16396df25c474d7526f7adf9cd0c1f0b71a8c42f70bb93c2399c561eae3998abc015e8fe36a1e149fd289472919fb02816c5b46d72cf9f4335932419ecf2de8b + checksum: 10/7afedeff855ba98c47dc28b33d7e8e253c4dc1f791938db402d79c174bdf806b897c1a5f91e5b1259c112520c816f826b4c5d98f0bad7e95b02dec66fedb64d2 languageName: node linkType: hard @@ -10687,17 +10800,17 @@ __metadata: linkType: hard "typescript-eslint@npm:^8.48.0": - version: 8.48.0 - resolution: "typescript-eslint@npm:8.48.0" + version: 8.48.1 + resolution: "typescript-eslint@npm:8.48.1" dependencies: - "@typescript-eslint/eslint-plugin": "npm:8.48.0" - "@typescript-eslint/parser": "npm:8.48.0" - "@typescript-eslint/typescript-estree": "npm:8.48.0" - "@typescript-eslint/utils": "npm:8.48.0" + "@typescript-eslint/eslint-plugin": "npm:8.48.1" + "@typescript-eslint/parser": "npm:8.48.1" + "@typescript-eslint/typescript-estree": "npm:8.48.1" + "@typescript-eslint/utils": "npm:8.48.1" peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 10/9be54df60faf3b5a6d255032b4478170b6f64e38b8396475a2049479d1e3c1f5a23a18bb4d2d6ff685ef92ff8f2af28215772fe33b48148a8cf83a724d0778d1 + checksum: 10/2b5318d74f9b8c4cd5d253b4d5249a184a0c5ed9eaf998a604c0d7b816acdc04f40570964d35fc5c93d40171ed3d95b8eef721578b7bedc8433607f4f7e59520 languageName: node linkType: hard @@ -10833,9 +10946,9 @@ __metadata: languageName: node linkType: hard -"update-browserslist-db@npm:^1.1.4": - version: 1.1.4 - resolution: "update-browserslist-db@npm:1.1.4" +"update-browserslist-db@npm:^1.2.0": + version: 1.2.2 + resolution: "update-browserslist-db@npm:1.2.2" dependencies: escalade: "npm:^3.2.0" picocolors: "npm:^1.1.1" @@ -10843,7 +10956,7 @@ __metadata: browserslist: ">= 4.21.0" bin: update-browserslist-db: cli.js - checksum: 10/79b2c0a31e9b837b49dc55d5cb7b77f44a69502847c7be352a44b1d35ac2032bf0e1bb7543f992809ed427bf9d32aa3f7ad41cef96198fa959c1666870174c06 + checksum: 10/ae2102d3c83fca35e9deb012d82bfde6f734998ced937e34a3bf239a4b67577108fdd144283aafc0e5e3cf38ca1aecd7714906ba6f562896c762d2f2fa391026 languageName: node linkType: hard @@ -10947,8 +11060,8 @@ __metadata: linkType: hard "vite@npm:^6.0.0 || ^7.0.0, vite@npm:^7.2.4": - version: 7.2.4 - resolution: "vite@npm:7.2.4" + version: 7.2.6 + resolution: "vite@npm:7.2.6" dependencies: esbuild: "npm:^0.25.0" fdir: "npm:^6.5.0" @@ -10997,21 +11110,21 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10/25962e65159e62fa6b643ac5bc79457c61c9b2cec9a1a17763b067b1e5ecac6483ad84247d6d457d81e25c5d1f8c553372bedf71dee09d8c1bd41af6c7b016a5 + checksum: 10/c640ed9c91957749287af2f483f5b5024a56718ba7cf32b0e2c398772fdf7fdb9fd5f97665c41712d4e9422c14009e283d0b69ac1b64b4f91474625bebe324de languageName: node linkType: hard "vitest@npm:^4.0.14": - version: 4.0.14 - resolution: "vitest@npm:4.0.14" - dependencies: - "@vitest/expect": "npm:4.0.14" - "@vitest/mocker": "npm:4.0.14" - "@vitest/pretty-format": "npm:4.0.14" - "@vitest/runner": "npm:4.0.14" - "@vitest/snapshot": "npm:4.0.14" - "@vitest/spy": "npm:4.0.14" - "@vitest/utils": "npm:4.0.14" + version: 4.0.15 + resolution: "vitest@npm:4.0.15" + dependencies: + "@vitest/expect": "npm:4.0.15" + "@vitest/mocker": "npm:4.0.15" + "@vitest/pretty-format": "npm:4.0.15" + "@vitest/runner": "npm:4.0.15" + "@vitest/snapshot": "npm:4.0.15" + "@vitest/spy": "npm:4.0.15" + "@vitest/utils": "npm:4.0.15" es-module-lexer: "npm:^1.7.0" expect-type: "npm:^1.2.2" magic-string: "npm:^0.30.21" @@ -11020,7 +11133,7 @@ __metadata: picomatch: "npm:^4.0.3" std-env: "npm:^3.10.0" tinybench: "npm:^2.9.0" - tinyexec: "npm:^0.3.2" + tinyexec: "npm:^1.0.2" tinyglobby: "npm:^0.2.15" tinyrainbow: "npm:^3.0.3" vite: "npm:^6.0.0 || ^7.0.0" @@ -11029,10 +11142,10 @@ __metadata: "@edge-runtime/vm": "*" "@opentelemetry/api": ^1.9.0 "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 - "@vitest/browser-playwright": 4.0.14 - "@vitest/browser-preview": 4.0.14 - "@vitest/browser-webdriverio": 4.0.14 - "@vitest/ui": 4.0.14 + "@vitest/browser-playwright": 4.0.15 + "@vitest/browser-preview": 4.0.15 + "@vitest/browser-webdriverio": 4.0.15 + "@vitest/ui": 4.0.15 happy-dom: "*" jsdom: "*" peerDependenciesMeta: @@ -11056,7 +11169,7 @@ __metadata: optional: true bin: vitest: vitest.mjs - checksum: 10/f3c6dee29f9882027c5c412e6ca503419c11bcebaecd84e9036ddeadb75a9fddc0f8cadcb785693f7e30492c8068d960a905d2c24094d57cc84070a86b0b4a11 + checksum: 10/b6df3d07b3f858ce1efc072d90753bc76d278a29245317b70e7bab0ded8bfaf81fc1e41aa30687365fef6d0e4110727867716440bc745e8948ae217a67f9d77b languageName: node linkType: hard @@ -11165,20 +11278,20 @@ __metadata: linkType: hard "vue@npm:^3.5.22": - version: 3.5.24 - resolution: "vue@npm:3.5.24" - dependencies: - "@vue/compiler-dom": "npm:3.5.24" - "@vue/compiler-sfc": "npm:3.5.24" - "@vue/runtime-dom": "npm:3.5.24" - "@vue/server-renderer": "npm:3.5.24" - "@vue/shared": "npm:3.5.24" + version: 3.5.25 + resolution: "vue@npm:3.5.25" + dependencies: + "@vue/compiler-dom": "npm:3.5.25" + "@vue/compiler-sfc": "npm:3.5.25" + "@vue/runtime-dom": "npm:3.5.25" + "@vue/server-renderer": "npm:3.5.25" + "@vue/shared": "npm:3.5.25" peerDependencies: typescript: "*" peerDependenciesMeta: typescript: optional: true - checksum: 10/ba83250b23ed2607b8fde26d345ba75b5adb77e8b4cd20db26a0e7be3be25908f48d15d6067a3e21bf1f6340e865e602e9127984e791c077c77a4a17224e50f8 + checksum: 10/3c3ec68747f31d52095365f37e548c8d7488ccd0d6a9ea0aabf2d619e6f62b9c0673766411b3574c1229e08640349f1eb6eab1f7766e269c8f221d1de5d1f06d languageName: node linkType: hard @@ -11210,6 +11323,13 @@ __metadata: languageName: node linkType: hard +"web-streams-polyfill@npm:^3.0.3": + version: 3.3.3 + resolution: "web-streams-polyfill@npm:3.3.3" + checksum: 10/8e7e13501b3834094a50abe7c0b6456155a55d7571312b89570012ef47ec2a46d766934768c50aabad10a9c30dd764a407623e8bfcc74fcb58495c29130edea9 + languageName: node + linkType: hard + "webidl-conversions@npm:^3.0.0": version: 3.0.1 resolution: "webidl-conversions@npm:3.0.1" @@ -11504,7 +11624,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.18.3": +"ws@npm:^8.18.0, ws@npm:^8.18.3": version: 8.18.3 resolution: "ws@npm:8.18.3" peerDependencies: @@ -11602,11 +11722,11 @@ __metadata: linkType: hard "yaml@npm:^2.8.1": - version: 2.8.1 - resolution: "yaml@npm:2.8.1" + version: 2.8.2 + resolution: "yaml@npm:2.8.2" bin: yaml: bin.mjs - checksum: 10/eae07b3947d405012672ec17ce27348aea7d1fa0534143355d24a43a58f5e05652157ea2182c4fe0604f0540be71f99f1173f9d61018379404507790dff17665 + checksum: 10/4eab0074da6bc5a5bffd25b9b359cf7061b771b95d1b3b571852098380db3b1b8f96e0f1f354b56cc7216aa97cea25163377ccbc33a2e9ce00316fe8d02f4539 languageName: node linkType: hard