diff --git a/registry/harleylrn/modules/kiro-cli/README.md b/registry/harleylrn/modules/kiro-cli/README.md index bcba4815dd..70fde10a14 100644 --- a/registry/harleylrn/modules/kiro-cli/README.md +++ b/registry/harleylrn/modules/kiro-cli/README.md @@ -1,350 +1,155 @@ --- display_name: Kiro CLI -description: Run Kiro CLI in your workspace to access AI coding assistant with MCP integration and task reporting. +description: Install and configure Kiro CLI in your workspace. icon: ../../../../.icons/kiro.svg verified: true -tags: [agent, ai, kiro, kiro-cli, tasks] +tags: [agent, ai, kiro, kiro-cli] --- # Kiro CLI -Run [Kiro CLI](https://kiro.dev/) in your workspace to access AI coding assistant. This module provides a complete integration with Coder workspaces, including automatic installation, MCP (Model Context Protocol) integration for task reporting, and support for custom pre/post install scripts. +Install and configure [Kiro CLI](https://kiro.dev/docs/cli/) in your workspace. ```tf module "kiro-cli" { source = "registry.coder.com/harleylrn/kiro-cli/coder" - version = "1.0.2" - agent_id = coder_agent.example.id - workdir = "/home/coder" - - # Required: Authentication tarball (see below for generation) - auth_tarball = <<-EOF -base64encoded-tarball -EOF + version = "2.0.0" + agent_id = coder_agent.main.id } ``` -![Kiro CLI in action](../../.images/kiro-cli.png) - -## Prerequisites - -- **zstd** - Required for compressing the authentication tarball - - **Ubuntu/Debian**: `sudo apt-get install zstd` - - **RHEL/CentOS/Fedora**: `sudo yum install zstd` or `sudo dnf install zstd` -- **auth_tarball** - Required for installation and authentication - -### Authentication Tarball - -You must generate an authenticated Kiro CLI tarball on another machine where you have successfully logged in: - -```bash -# 1. Install Kiro CLI and login on your local machine -kiro-cli login - -# 2. Generate the authentication tarball -cd ~/.local/share/kiro-cli -tar -c . | zstd | base64 -w 0 -``` - -Copy the output and use it as the `auth_tarball` variable. - -## Detailed Authentication Setup - -**Step 1: Install Kiro CLI locally** - -- Download from [Kiro CLI](https://kiro.dev/) -- Follow the installation instructions for your platform - -**Step 2: Authenticate** - -```bash -kiro-cli login -``` - -Complete the authentication process in your browser. +> [!WARNING] +> If upgrading from v1.x.x of this module: v2 is a major refactor that drops support for Coder Tasks and AgentAPI. The module now only installs and configures Kiro CLI; launch it with your own `coder_app`. `ai_prompt`, `system_prompt`, and `trust_all_tools` are removed in favor of `kiro-cli chat` flags in your launcher, and without `agent_config` Kiro's built-in default agent is used. `order`, `group`, `report_tasks`, `cli_app`, `web_app_display_name`, `cli_app_display_name`, `install_agentapi`, `agentapi_version`, `agentapi_chat_based_path`, `coder_mcp_instructions`, and the `task_app_id` output are removed, and `workdir` is optional. `kiro_install_url` now defaults to `null`: `latest` is installed from `https://cli.kiro.dev/install`, and pinned versions still come from `https://desktop-release.q.us-east-1.amazonaws.com`. Keep using v1.x.x if you depend on Coder Tasks. -**Step 3: Generate tarball** +## Examples -```bash -cd ~/.local/share/kiro-cli -tar -c . | zstd | base64 -w 0 > /tmp/kiro-cli-auth.txt -``` - -**Step 4: Use in Terraform** +### Standalone mode with a launcher app ```tf -variable "kiro_cli_auth_tarball" { - type = string - sensitive = true - default = "PASTE_YOUR_TARBALL_HERE" +locals { + kiro_workdir = "/home/coder/project" } -``` - -> [!IMPORTANT] -> -> - Regenerate the tarball if you logout or re-authenticate -> - Each user needs their own authentication tarball -> - Keep the tarball secure as it contains authentication credentials - -### Default System Prompt - -The module includes a simple system prompt that instructs Kiro CLI: - -``` -You are a helpful Coding assistant. Aim to autonomously investigate -and solve issues the user gives you and test your work, whenever possible. -Avoid shortcuts like mocking tests. When you get stuck, you can ask the user -but opt for autonomy. -``` - -You can customize this behavior by providing your own system prompt via the `system_prompt` variable. - -### Default Coder MCP Instructions - -The module includes specific instructions for the Coder MCP server integration that are separate from the system prompt: - -``` -YOU MUST REPORT ALL TASKS TO CODER. -When reporting tasks you MUST follow these EXACT instructions: -- IMMEDIATELY report status after receiving ANY user message -- Be granular If you are investigating with multiple steps report each step to coder. - -Task state MUST be one of the following: -- Use "state": "working" when actively processing WITHOUT needing additional user input -- Use "state": "complete" only when finished with a task -- Use "state": "failure" when you need ANY user input lack sufficient details or encounter blockers. - -Task summaries MUST: -- Include specifics about what you're doing -- Include clear and actionable steps for the user -- Be less than 160 characters in length -``` - -You can customize these instructions by providing your own via the `coder_mcp_instructions` variable. - -## Default Agent Configuration - -The module includes a default agent configuration template that provides a comprehensive setup for Kiro CLI integration: - -```json -{ - "name": "agent", - "description": "This is an default agent config", - "prompt": "${system_prompt}", - "mcpServers": {}, - "tools": ["read", "write", "shell", "aws", "@coder", "knowledge"], - "toolAliases": {}, - "allowedTools": ["read", "@coder"], - "resources": [ - "file://KiroQ.md", - "file://README.md", - "file://.kiro/steering/**/*.md" - ], - "hooks": {}, - "toolsSettings": {}, - "useLegacyMcpJson": true -} -``` - -### Configuration Details: - -- **Tools Available:** File operations (`read`, `write`), shell execution (`shell`), AWS CLI (`aws`), Coder MCP integration (`@coder`), and knowledge base access (`knowledge`) -- **@coder Tool:** Enables Coder MCP integration for task reporting (`coder_report_task` and related tools) -- **Allowed Tools:** By default, only `read` and `@coder` are allowed (can be customized for security) -- **Resources:** Access to documentation and rule files in the workspace -- **MCP Servers:** Empty by default, can be configured via `agent_config` variable -- **System Prompt:** Dynamically populated from the `system_prompt` variable -- **Legacy MCP:** Uses legacy MCP JSON format for compatibility - -You can override this configuration by providing your own JSON via the `agent_config` variable. - -### Agent Name Configuration - -The module automatically extracts the agent name from the `"name"` field in the `agent_config` JSON and uses it for: - -- **Configuration File:** Saves the agent config as `~/.kiro/agents/{agent_name}.json` -- **Default Agent:** Sets the agent as the default using `q settings chat.defaultAgent {agent_name}` -- **MCP Integration:** Associates the Coder MCP server with the specified agent name - -If no custom `agent_config` is provided, the default agent name "agent" is used. - -## Usage Examples -### Basic Usage - -```tf module "kiro-cli" { source = "registry.coder.com/harleylrn/kiro-cli/coder" - version = "1.0.2" - agent_id = coder_agent.example.id - workdir = "/home/coder" + version = "2.0.0" + agent_id = coder_agent.main.id + workdir = local.kiro_workdir auth_tarball = var.kiro_cli_auth_tarball } -``` - -This example will: - -1. Download and install Kiro CLI latest version -2. Extract authentication tarball to ~/.local/share/kiro-cli -3. Configure Coder MCP integration for task reporting -4. Create default agent configuration file -5. Start Kiro CLI in /home/coder directory -6. Provide web interface through AgentAPI - -> [!IMPORTANT] -> By default `write` tool is not allowed, which will pause the task execution -> and will wait for the prompt to approve its usage. -> To avoid this, and allow the normal task flow, user has two options: -> -> - Change the parameter `trust_all_tools` value to `true` (default to `false`) -> OR -> - Provide your own agent configuration with the tools of your choice allowed - -### With Custom AI Prompt - -```tf -module "kiro-cli" { - source = "registry.coder.com/harleylrn/kiro-cli/coder" - version = "1.0.2" - agent_id = coder_agent.example.id - workdir = "/home/coder" - auth_tarball = var.kiro_cli_auth_tarball - ai_prompt = "Help me set up a Python FastAPI project with proper testing structure" - trust_all_tools = true -} -``` - -> [!IMPORTANT] -> **_Security Notice_** -> In order to allow the tasks flow non-interactively all the tools are trusted -> This flag bypasses standard permission checks and allows Kiro CLI broader access to your system than normally permitted. -> While this enables more functionality, it also means Kiro CLI can potentially execute commands with the same privileges as the user running it. -> Use this module only in trusted environments and be aware of the security implications. - -### With Custom Pre/Post Install Scripts -```tf -module "kiro-cli" { - source = "registry.coder.com/harleylrn/kiro-cli/coder" - version = "1.0.2" - agent_id = coder_agent.example.id - workdir = "/home/coder" - auth_tarball = var.kiro_cli_auth_tarball - - pre_install_script = <<-EOT +resource "coder_app" "kiro_cli" { + agent_id = coder_agent.main.id + slug = "kiro-cli" + display_name = "Kiro CLI" + icon = "/icon/kiro.svg" + open_in = "slim-window" + command = <<-EOT #!/usr/bin/env bash - echo "Setting up custom environment..." - # Install additional dependencies - sudo apt-get update && sudo apt-get install -y zstd + set -e + cd "${local.kiro_workdir}" + exec kiro-cli chat EOT - - post_install_script = <<-EOT - #!/usr/bin/env bash - echo "Configuring Kiro CLI settings..." - # Custom configuration commands - kiro-cli settings chat.model claude-3-sonnet - EOT -} -``` - -### Specific Version Installation - -```tf -module "kiro-cli" { - source = "registry.coder.com/harleylrn/kiro-cli/coder" - version = "1.0.2" - agent_id = coder_agent.example.id - workdir = "/home/coder" - auth_tarball = var.kiro_cli_auth_tarball - kiro_cli_version = "1.14.0" # Specific version - install_kiro_cli = true } ``` -### Custom Agent Configuration +### Custom agent ```tf module "kiro-cli" { source = "registry.coder.com/harleylrn/kiro-cli/coder" - version = "1.0.2" - agent_id = coder_agent.example.id - workdir = "/home/coder" + version = "2.0.0" + agent_id = coder_agent.main.id + workdir = "/home/coder/project" auth_tarball = var.kiro_cli_auth_tarball - agent_config = <<-EOT - { - "name": "custom-agent", - "description": "Custom Kiro CLI agent for my workspace", - "prompt": "You are a specialized DevOps assistant...", - "tools": ["read", "write", "shell", "aws"] + agent_config = <<-EOF + { + "name": "coder-agent", + "description": "Coding agent for this workspace", + "prompt": "You are a helpful coding assistant.", + "tools": ["read", "write", "shell"], + "allowedTools": ["read"], + "mcpServers": { + "playwright": { + "command": "npx", + "args": ["-y", "@playwright/mcp@latest", "--headless", "--isolated", "--no-sandbox"] + } } - EOT + } + EOF } ``` -### With Custom AgentAPI Configuration +`agent_config` uses the [agent configuration](https://kiro.dev/docs/custom-agents/configuration-reference/) format and is set as `chat.defaultAgent`. + +### Pinned version or internal mirror ```tf module "kiro-cli" { - source = "registry.coder.com/harleylrn/kiro-cli/coder" - version = "1.0.2" - agent_id = coder_agent.example.id - workdir = "/home/coder" - auth_tarball = var.kiro_cli_auth_tarball - - # AgentAPI configuration for environments without wildcard access url. https://coder.com/docs/admin/setup#wildcard-access-url - agentapi_chat_based_path = true - agentapi_version = "v0.10.0" + source = "registry.coder.com/harleylrn/kiro-cli/coder" + version = "2.0.0" + agent_id = coder_agent.main.id + kiro_cli_version = "2.26.0" + kiro_install_url = "https://artifacts.internal.corp/kiro-cli-releases" } ``` -### Air-Gapped Installation +With the defaults, the module runs the official installer. Otherwise it installs `//kirocli--linux.zip`. See [Kiro CLI installation](https://kiro.dev/docs/getting-started/installation/). -For environments without direct internet access, you can host Kiro CLI installation files internally and configure the module to use your internal repository: +### Serialize a downstream `coder_script` after the install pipeline + +The module exposes the `scripts` output: an ordered list of `coder exp sync` names for the scripts this module creates (pre_install, install, post_install). Scripts that were not configured are absent. ```tf module "kiro-cli" { - source = "registry.coder.com/harleylrn/kiro-cli/coder" - version = "1.0.2" - agent_id = coder_agent.example.id - workdir = "/home/coder" - auth_tarball = var.kiro_cli_auth_tarball + source = "registry.coder.com/harleylrn/kiro-cli/coder" + version = "2.0.0" + agent_id = coder_agent.main.id +} - # Point to internal artifact repository - kiro_install_url = "https://artifacts.internal.corp/kiro-cli-releases" +resource "coder_script" "post_kiro_cli" { + agent_id = coder_agent.main.id + display_name = "Run after Kiro CLI install" + run_on_start = true + script = <<-EOT + #!/usr/bin/env bash + set -euo pipefail + trap 'coder exp sync complete post-kiro-cli' EXIT + coder exp sync want post-kiro-cli ${join(" ", module.kiro-cli.scripts)} + coder exp sync start post-kiro-cli - # Use specific version available in your repository - kiro_cli_version = "latest" + kiro-cli --version + EOT } ``` -**Prerequisites for Air-Gapped Setup:** +## Authentication -1. Download Kiro CLI installation files from the official source and host them internally -2. Maintain the same directory structure: `{base_url}/{version}/kirocli-{arch}-linux.zip` -3. Ensure both architectures are available: - - `kirocli-x86_64-linux.zip` for Intel/AMD systems - - `kirocli-aarch64-linux.zip` for ARM systems -4. Configure network access from Coder workspaces to your internal repository +Set `api_key` (exported as `KIRO_API_KEY`), pass a pre-authenticated `auth_tarball` (requires `zstd`), or run `kiro-cli login` in the workspace. See [Kiro authentication](https://kiro.dev/docs/getting-started/authentication/). -## Troubleshooting +To generate the tarball: -### Common Issues - -**Authentication issues:** +```bash +#!/usr/bin/env bash +kiro-cli login +cd ~/.local/share/kiro-cli +tar -c . | zstd | base64 -w 0 +``` -- Regenerate the auth tarball on your local machine -- Ensure the tarball is properly base64 encoded -- Check that the original authentication is still valid +## Troubleshooting -**MCP integration not working:** +Check the log files in `~/.coder-modules/harleylrn/kiro-cli/logs/` for detailed information. -- Verify that AgentAPI is installed (`install_agentapi = true`) -- Check that the Coder agent is properly configured -- Review the system prompt configuration +```bash +cat ~/.coder-modules/harleylrn/kiro-cli/logs/install.log +cat ~/.coder-modules/harleylrn/kiro-cli/logs/pre_install.log +cat ~/.coder-modules/harleylrn/kiro-cli/logs/post_install.log +``` -## Outputs +## References -| Name | Description | -| -------------------------------------------------------------------- | ----------- | -| [task_app_id](#output_task_app_id) | n/a | +- [Kiro CLI documentation](https://kiro.dev/docs/cli/) +- [Kiro CLI commands](https://kiro.dev/docs/reference/cli-commands/) +- [Kiro MCP configuration](https://kiro.dev/docs/mcp/configuration/) +- [Kiro settings reference](https://kiro.dev/docs/reference/settings/) diff --git a/registry/harleylrn/modules/kiro-cli/kiro-cli.tftest.hcl b/registry/harleylrn/modules/kiro-cli/kiro-cli.tftest.hcl deleted file mode 100644 index 0f183a69e8..0000000000 --- a/registry/harleylrn/modules/kiro-cli/kiro-cli.tftest.hcl +++ /dev/null @@ -1,372 +0,0 @@ -run "required_variables" { - command = plan - - variables { - agent_id = "test-agent-id" - workdir = "/tmp/test-workdir" - } -} - -run "minimal_config" { - command = plan - - variables { - agent_id = "test-agent-id" - workdir = "/tmp/test-workdir" - auth_tarball = "dGVzdA==" # base64 "test" - } - - assert { - condition = resource.coder_env.status_slug[0].name == "CODER_MCP_APP_STATUS_SLUG" - error_message = "Status slug environment variable not configured correctly" - } - - assert { - condition = resource.coder_env.status_slug[0].value == "kiro-cli" - error_message = "Status slug value should be 'kiro-cli'" - } -} - -# Test Case 1: Basic Usage – No Autonomous Use of Q -# Using vanilla Kubernetes Deployment Template configuration -run "test_case_1_basic_usage" { - command = plan - - variables { - agent_id = "test-agent-id" - workdir = "/tmp/test-workdir" - auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball" - } - - # Q is installed and authenticated - assert { - condition = resource.coder_env.status_slug[0].name == "CODER_MCP_APP_STATUS_SLUG" - error_message = "Status slug environment variable should be configured for basic usage" - } - - assert { - condition = resource.coder_env.status_slug[0].value == "kiro-cli" - error_message = "Status slug value should be 'kiro-cli' for basic usage" - } - - # AgentAPI is installed and configured (default behavior) - assert { - condition = length(resource.coder_env.auth_tarball) == 1 - error_message = "Auth tarball environment variable should be created for authentication" - } - - # Foundational configuration applied - assert { - condition = length(local.agent_config) > 0 - error_message = "Agent config should be generated with foundational configuration" - } - - # No additional parameters required (using defaults) - assert { - condition = local.agent_name == "agent" - error_message = "Default agent name should be 'agent' when no custom config provided" - } -} - -# Test Case 2: Autonomous Usage – Autonomous Use of Q -# AI prompt passed through from external source (Tasks interface or Issue Tracker CI) -run "test_case_2_autonomous_usage" { - command = plan - - variables { - agent_id = "test-agent-id" - workdir = "/tmp/test-workdir" - auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball" - ai_prompt = "Help me set up a Python FastAPI project with proper testing structure" - } - - # Q is installed and authenticated - assert { - condition = resource.coder_env.status_slug[0].name == "CODER_MCP_APP_STATUS_SLUG" - error_message = "Status slug environment variable should be configured for autonomous usage" - } - - assert { - condition = resource.coder_env.status_slug[0].value == "kiro-cli" - error_message = "Status slug value should be 'kiro-cli' for autonomous usage" - } - - # AgentAPI is installed and configured - assert { - condition = length(resource.coder_env.auth_tarball) == 1 - error_message = "Auth tarball environment variable should be created for autonomous usage" - } - - # Foundational configuration for all components applied - assert { - condition = length(local.agent_config) > 0 - error_message = "Agent config should be generated for autonomous usage" - } - - # AI prompt is configured - assert { - condition = local.full_prompt == "Help me set up a Python FastAPI project with proper testing structure" - error_message = "AI prompt should be configured correctly for autonomous usage" - } - - # Default agent name when no custom config - assert { - condition = local.agent_name == "agent" - error_message = "Default agent name should be 'agent' for autonomous usage" - } -} - -# Test Case 3: Extended Configuration – Parameter Validation and File Rendering -# Validates extended configuration options and parameter application -run "test_case_3_extended_configuration" { - command = plan - - variables { - agent_id = "test-agent-id" - workdir = "/tmp/test-workdir" - auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball" - kiro_cli_version = "1.14.1" - kiro_install_url = "https://desktop-release.q.us-east-1.amazonaws.com" - install_kiro_cli = true - install_agentapi = true - agentapi_version = "v0.6.0" - trust_all_tools = true - ai_prompt = "Help me create a production-grade TypeScript monorepo with testing and deployment" - system_prompt = "You are a helpful software assistant working in a secure enterprise environment" - pre_install_script = "echo 'Pre-install setup'" - post_install_script = "echo 'Post-install cleanup'" - agent_config = jsonencode({ - name = "production-agent" - description = "Production Kiro CLI agent for enterprise environment" - prompt = "You are a helpful software assistant working in a secure enterprise environment" - mcpServers = {} - tools = ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"] - toolAliases = {} - allowedTools = ["fs_read"] - resources = ["file://KiroQ.md", "file://README.md", "file://.kiro/steering/**/*.md"] - hooks = {} - toolsSettings = {} - useLegacyMcpJson = true - }) - } - - # All installation parameters are applied correctly - assert { - condition = resource.coder_env.status_slug[0].value == "kiro-cli" - error_message = "Status slug should be configured correctly with extended parameters" - } - - assert { - condition = resource.coder_env.auth_tarball[0].value == "dGVzdEF1dGhUYXJiYWxs" - error_message = "Auth tarball should be configured correctly with extended parameters" - } - - # Custom agent configuration is loaded and referenced correctly - assert { - condition = local.agent_name == "production-agent" - error_message = "Agent name should be extracted from custom agent config" - } - - assert { - condition = length(local.agent_config) > 0 - error_message = "Custom agent config should be processed correctly" - } - - # AI prompt and system prompt are configured - assert { - condition = local.full_prompt == "Help me create a production-grade TypeScript monorepo with testing and deployment" - error_message = "AI prompt should be configured correctly in extended configuration" - } - - # Pre-install and post-install scripts are provided - assert { - condition = length(local.agent_config) > 0 - error_message = "Agent config should be generated correctly for extended configuration" - } -} - -run "full_config" { - command = plan - - variables { - agent_id = "test-agent-id" - workdir = "/tmp/test-workdir" - install_kiro_cli = true - install_agentapi = true - agentapi_version = "v0.5.0" - kiro_cli_version = "latest" - trust_all_tools = true - ai_prompt = "Build a web application" - auth_tarball = "dGVzdA==" - order = 1 - group = "AI Tools" - icon = "/icon/custom-kiro-cli.svg" - pre_install_script = "echo 'pre-install'" - post_install_script = "echo 'post-install'" - agent_config = jsonencode({ - name = "test-agent" - description = "Test agent configuration" - prompt = "You are a helpful AI assistant for testing." - mcpServers = {} - tools = ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"] - toolAliases = {} - allowedTools = ["fs_read"] - resources = ["file://KiroQ.md", "file://README.md", "file://.kiro/steering/**/*.md"] - hooks = {} - toolsSettings = {} - useLegacyMcpJson = true - }) - } - - assert { - condition = resource.coder_env.status_slug[0].name == "CODER_MCP_APP_STATUS_SLUG" - error_message = "Status slug environment variable not configured correctly" - } - - assert { - condition = resource.coder_env.status_slug[0].value == "kiro-cli" - error_message = "Status slug value should be 'kiro-cli'" - } - - assert { - condition = length(resource.coder_env.auth_tarball) == 1 - error_message = "Auth tarball environment variable should be created when provided" - } -} - -run "auth_tarball_environment" { - command = plan - - variables { - agent_id = "test-agent-id" - workdir = "/tmp/test-workdir" - auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball" - } - - assert { - condition = resource.coder_env.auth_tarball[0].name == "KIRO_CLI_AUTH_TARBALL" - error_message = "Auth tarball environment variable name should be 'KIRO_CLI_AUTH_TARBALL'" - } - - assert { - condition = resource.coder_env.auth_tarball[0].value == "dGVzdEF1dGhUYXJiYWxs" - error_message = "Auth tarball environment variable value should match input" - } -} - -run "empty_auth_tarball" { - command = plan - - variables { - agent_id = "test-agent-id" - workdir = "/tmp/test-workdir" - auth_tarball = "" - } - - assert { - condition = length(resource.coder_env.auth_tarball) == 0 - error_message = "Auth tarball environment variable should not be created when empty" - } -} - -run "custom_system_prompt" { - command = plan - - variables { - agent_id = "test-agent-id" - workdir = "/tmp/test-workdir" - system_prompt = "Custom system prompt for testing" - } - - # Test that the system prompt is used in the agent config template - assert { - condition = length(local.agent_config) > 0 - error_message = "Agent config should be generated with custom system prompt" - } -} - -run "install_options" { - command = plan - - variables { - agent_id = "test-agent-id" - workdir = "/tmp/test-workdir" - install_kiro_cli = false - install_agentapi = false - } - - assert { - condition = resource.coder_env.status_slug[0].name == "CODER_MCP_APP_STATUS_SLUG" - error_message = "Status slug should still be configured even when install options are disabled" - } -} - -run "version_configuration" { - command = plan - - variables { - agent_id = "test-agent-id" - workdir = "/tmp/test-workdir" - kiro_cli_version = "2.15.0" - agentapi_version = "v0.4.0" - } - - assert { - condition = resource.coder_env.status_slug[0].value == "kiro-cli" - error_message = "Status slug value should remain 'kiro-cli' regardless of version" - } -} - -# Additional test for agent name extraction -run "agent_name_extraction" { - command = plan - - variables { - agent_id = "test-agent-id" - workdir = "/tmp/test-workdir" - agent_config = jsonencode({ - name = "custom-enterprise-agent" - description = "Custom enterprise agent configuration" - prompt = "You are a custom enterprise AI assistant." - mcpServers = {} - tools = ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"] - toolAliases = {} - allowedTools = ["fs_read", "fs_write"] - resources = ["file://README.md"] - hooks = {} - toolsSettings = {} - useLegacyMcpJson = true - }) - } - - assert { - condition = local.agent_name == "custom-enterprise-agent" - error_message = "Agent name should be extracted correctly from custom agent config" - } - - assert { - condition = length(local.agent_config) > 0 - error_message = "Agent config should be processed correctly" - } -} - -# Test for JSON encoding validation -run "json_encoding_validation" { - command = plan - - variables { - agent_id = "test-agent-id" - workdir = "/tmp/test-workdir" - system_prompt = "Multi-line\nsystem prompt\nwith newlines" - } - - assert { - condition = length(local.system_prompt) > 0 - error_message = "System prompt should be JSON encoded correctly" - } - - assert { - condition = length(local.agent_config) > 0 - error_message = "Agent config should be generated correctly with multi-line system prompt" - } -} diff --git a/registry/harleylrn/modules/kiro-cli/main.test.ts b/registry/harleylrn/modules/kiro-cli/main.test.ts index e0f9b28d5f..17ff4319c0 100644 --- a/registry/harleylrn/modules/kiro-cli/main.test.ts +++ b/registry/harleylrn/modules/kiro-cli/main.test.ts @@ -1,531 +1,459 @@ -import { describe, it, expect } from "bun:test"; import { + test, + afterEach, + describe, + setDefaultTimeout, + beforeAll, + expect, +} from "bun:test"; +import { + execContainer, + readFileContainer, + removeContainer, + runContainer, runTerraformApply, runTerraformInit, - findResourceInstance, + TerraformState, } from "~test"; +import { + extractCoderEnvVars, + writeExecutable, +} from "../../../coder/modules/agentapi/test-util"; import path from "path"; -const moduleDir = path.resolve(__dirname); - -// Always provide agent_config to bypass template parsing issues -const baseAgentConfig = JSON.stringify({ - name: "test-agent", - description: "Test agent configuration", - prompt: "You are a helpful AI assistant.", - mcpServers: {}, - tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"], - toolAliases: {}, - allowedTools: ["fs_read"], - resources: ["file://README.md", "file://.kiro/steering/**/*.md"], - hooks: {}, - toolsSettings: {}, - useLegacyMcpJson: true, -}); - -const requiredVars = { - agent_id: "dummy-agent-id", - agent_config: baseAgentConfig, - workdir: "/tmp/test-workdir", +interface ModuleScripts { + pre_install?: string; + install: string; + post_install?: string; +} + +const SCRIPT_SUFFIXES = [ + "Pre-Install Script", + "Install Script", + "Post-Install Script", +] as const; + +const collectScripts = (state: TerraformState): ModuleScripts => { + const byDisplayName: Record = {}; + for (const resource of state.resources) { + if (resource.type !== "coder_script") continue; + for (const instance of resource.instances) { + const attrs = instance.attributes as Record; + const displayName = attrs.display_name as string | undefined; + const script = attrs.script as string | undefined; + if (displayName && script) { + byDisplayName[displayName] = script; + } + } + } + const scripts: Partial = {}; + for (const suffix of SCRIPT_SUFFIXES) { + const key = `Kiro CLI: ${suffix}`; + if (!(key in byDisplayName)) continue; + switch (suffix) { + case "Pre-Install Script": + scripts.pre_install = byDisplayName[key]; + break; + case "Install Script": + scripts.install = byDisplayName[key]; + break; + case "Post-Install Script": + scripts.post_install = byDisplayName[key]; + break; + } + } + if (!scripts.install) { + throw new Error("install script not found in terraform state"); + } + return scripts as ModuleScripts; }; -const fullConfigVars = { - agent_id: "dummy-agent-id", - workdir: "/tmp/test-workdir", - install_kiro_cli: true, - install_agentapi: true, - agentapi_version: "v0.6.0", - kiro_cli_version: "1.14.1", - kiro_install_url: "https://desktop-release.q.us-east-1.amazonaws.com", - trust_all_tools: false, - ai_prompt: "Build a comprehensive test suite", - auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball" - order: 1, - group: "AI Tools", - icon: "/icon/custom-kiro-cli.svg", - pre_install_script: "echo 'Starting pre-install'", - post_install_script: "echo 'Completed post-install'", - agent_config: baseAgentConfig, +let cleanupFunctions: (() => Promise)[] = []; +const registerCleanup = (cleanup: () => Promise) => { + cleanupFunctions.push(cleanup); }; +afterEach(async () => { + const cleanupFnsCopy = cleanupFunctions.slice().reverse(); + cleanupFunctions = []; + for (const cleanup of cleanupFnsCopy) { + try { + await cleanup(); + } catch (error) { + console.error("Error during cleanup:", error); + } + } +}); -describe("kiro-cli module v1.0.0", async () => { - await runTerraformInit(moduleDir); - - // Test Case 1: Basic Usage – No Autonomous Use of Q - // Matches CDES-203 Test Case #1: Basic Usage - it("Test Case 1: Basic Usage - No Autonomous Use of Q", async () => { - const basicUsageVars = { - agent_id: "dummy-agent-id", - workdir: "/tmp/test-workdir", - auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball" - }; - - const state = await runTerraformApply(moduleDir, basicUsageVars); - - // Q is installed and authenticated - const statusSlugEnv = findResourceInstance( - state, - "coder_env", - "status_slug", - ); - expect(statusSlugEnv).toBeDefined(); - expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG"); - expect(statusSlugEnv.value).toBe("kiro-cli"); - - // AgentAPI is installed and configured (default behavior) - const authTarballEnv = findResourceInstance( - state, - "coder_env", - "auth_tarball", - ); - expect(authTarballEnv).toBeDefined(); - expect(authTarballEnv.name).toBe("KIRO_CLI_AUTH_TARBALL"); - expect(authTarballEnv.value).toBe("dGVzdEF1dGhUYXJiYWxs"); - - // Foundational configuration for all components is applied - // No additional parameters are required for the module to work - // Using the terminal application and Q chat returns a functional interface +interface SetupProps { + skipKiroMock?: boolean; + moduleVariables?: Record; +} + +const projectDir = "/home/coder/project"; + +const setup = async ( + props?: SetupProps, +): Promise<{ + id: string; + coderEnvVars: Record; + scripts: ModuleScripts; +}> => { + const moduleDir = path.resolve(import.meta.dir); + const state = await runTerraformApply(moduleDir, { + agent_id: "foo", + workdir: projectDir, + install_kiro_cli: "false", + ...props?.moduleVariables, }); - - // Test Case 2: Autonomous Usage – Autonomous Use of Q - // Matches CDES-203 Test Case 2: Autonomous Usage - it("Test Case 2: Autonomous Usage - Autonomous Use of Q", async () => { - const autonomousUsageVars = { - agent_id: "dummy-agent-id", - workdir: "/tmp/test-workdir", - auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball" - ai_prompt: - "Help me set up a Python FastAPI project with proper testing structure", - }; - - const state = await runTerraformApply(moduleDir, autonomousUsageVars); - - // Q is installed and authenticated - const statusSlugEnv = findResourceInstance( - state, - "coder_env", - "status_slug", - ); - expect(statusSlugEnv).toBeDefined(); - expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG"); - expect(statusSlugEnv.value).toBe("kiro-cli"); - - // AgentAPI is installed and configured - const authTarballEnv = findResourceInstance( - state, - "coder_env", - "auth_tarball", - ); - expect(authTarballEnv).toBeDefined(); - expect(authTarballEnv.name).toBe("KIRO_CLI_AUTH_TARBALL"); - - // AI prompt is passed through from external source - // The Chat interface functions as required - // The Tasks interface functions as required - // The template can be invoked from GitHub integration as expected + const scripts = collectScripts(state); + const coderEnvVars = extractCoderEnvVars(state); + + const id = await runContainer("codercom/enterprise-node:latest"); + registerCleanup(async () => { + if (process.env["DEBUG"] === "true" || process.env["DEBUG"] === "1") { + console.log(`Not removing container ${id} in debug mode`); + return; + } + await removeContainer(id); }); - // Test Case 3: Extended Configuration – Parameter Validation and File Rendering - // Matches CDES-203 Test Case 3: Extended Configuration - it("Test Case 3: Extended Configuration - Parameter Validation and File Rendering", async () => { - const extendedConfigVars = { - agent_id: "dummy-agent-id", - workdir: "/tmp/test-workdir", - auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball" - kiro_cli_version: "1.14.1", - kiro_install_url: "https://desktop-release.q.us-east-1.amazonaws.com", - install_kiro_cli: true, - install_agentapi: true, - agentapi_version: "v0.6.0", - trust_all_tools: true, - ai_prompt: - "Help me create a production-grade TypeScript monorepo with testing and deployment", - system_prompt: - "You are a helpful software assistant working in a secure enterprise environment", - pre_install_script: "echo 'Pre-install setup'", - post_install_script: "echo 'Post-install cleanup'", - agent_config: JSON.stringify({ - name: "production-agent", - description: "Production Kiro CLI agent for enterprise environment", - prompt: - "You are a helpful software assistant working in a secure enterprise environment", - mcpServers: {}, - tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"], - toolAliases: {}, - allowedTools: ["fs_read"], - resources: [ - "file://KiroQ.md", - "file://README.md", - "file://.kiro/steering/**/*.md", - ], - hooks: {}, - toolsSettings: {}, - useLegacyMcpJson: true, - }), - }; - - const state = await runTerraformApply(moduleDir, extendedConfigVars); - - // All installation steps execute in the correct order - const statusSlugEnv = findResourceInstance( - state, - "coder_env", - "status_slug", - ); - expect(statusSlugEnv).toBeDefined(); - expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG"); - expect(statusSlugEnv.value).toBe("kiro-cli"); - - // auth_tarball is unpacked and used as expected - const authTarballEnv = findResourceInstance( - state, - "coder_env", - "auth_tarball", - ); - expect(authTarballEnv).toBeDefined(); - expect(authTarballEnv.value).toBe("dGVzdEF1dGhUYXJiYWxs"); - - // agent_config is rendered correctly, and the name field is used as the agent's name - // The specified ai_prompt and system_prompt are respected by the Q agent - // Tools are trusted globally if trust_all_tools = true - // Files and scripts execute in proper sequence + await writeExecutable({ + containerId: id, + filePath: "/usr/bin/coder", + content: "#!/bin/bash\nexit 0\n", }); + if (!props?.skipKiroMock) { + await writeExecutable({ + containerId: id, + filePath: "/usr/bin/kiro-cli", + content: await Bun.file( + path.join(moduleDir, "testdata", "kiro-cli-mock.sh"), + ).text(), + }); + } + return { id, coderEnvVars, scripts }; +}; - // 1. Basic functionality test (replaces testRequiredVariables) - it("works with required variables", async () => { - const state = await runTerraformApply(moduleDir, requiredVars); +const envPrefix = (env?: Record) => + Object.entries(env ?? {}) + .map(([key, value]) => `export ${key}="${value.replace(/"/g, '\\"')}" && `) + .join(""); + +const runScript = async ( + id: string, + name: string, + script: string, + env?: Record, +) => { + const target = `/tmp/coder-utils-${name}.sh`; + await writeExecutable({ containerId: id, filePath: target, content: script }); + return execContainer(id, ["bash", "-c", `${envPrefix(env)}${target}`]); +}; - // Should create the basic resources - const statusSlugEnv = findResourceInstance( - state, - "coder_env", - "status_slug", - ); - expect(statusSlugEnv).toBeDefined(); - expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG"); - expect(statusSlugEnv.value).toBe("kiro-cli"); - }); +const runScripts = async ( + id: string, + scripts: ModuleScripts, + env?: Record, +) => { + const ordered: [string, string | undefined][] = [ + ["pre_install", scripts.pre_install], + ["install", scripts.install], + ["post_install", scripts.post_install], + ]; + for (const [name, script] of ordered) { + if (!script) continue; + const resp = await runScript(id, name, script, env); + if (resp.exitCode !== 0) { + console.log(`script ${name} failed:`); + console.log(resp.stdout); + console.log(resp.stderr); + throw new Error(`coder-utils ${name} script exited ${resp.exitCode}`); + } + } +}; - // 2. Environment variables are created correctly - it("creates required environment variables", async () => { - const state = await runTerraformApply(moduleDir, fullConfigVars); +// Records curl arguments and copies the given fixture to --output. +const writeCurlMock = async (id: string, fixture: string) => { + await writeExecutable({ + containerId: id, + filePath: "/usr/local/bin/curl", + content: [ + "#!/bin/bash", + "printf '%s\\n' \"$*\" >> /tmp/kiro-curl-args", + 'while [ $# -gt 0 ]; do if [ "$1" = "--output" ]; then out="$2"; fi; shift; done', + `cp ${fixture} "$out"`, + ].join("\n"), + }); +}; - // Check status slug environment variable - const statusSlugEnv = findResourceInstance( - state, - "coder_env", - "status_slug", - ); - expect(statusSlugEnv).toBeDefined(); - expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG"); - expect(statusSlugEnv.value).toBe("kiro-cli"); - - // Check auth tarball environment variable - const authTarballEnv = findResourceInstance( - state, - "coder_env", - "auth_tarball", - ); - expect(authTarballEnv).toBeDefined(); - expect(authTarballEnv.name).toBe("KIRO_CLI_AUTH_TARBALL"); - expect(authTarballEnv.value).toBe("dGVzdEF1dGhUYXJiYWxs"); +const fakeKiroInstall = (version: string) => + [ + 'mkdir -p "$HOME/.local/bin"', + `printf '#!/bin/sh\\necho "kiro-cli ${version}"\\n' > "$HOME/.local/bin/kiro-cli"`, + 'chmod +x "$HOME/.local/bin/kiro-cli"', + ].join("\n"); + +// Builds a release-archive fixture with the same layout as +// kirocli--linux.zip (kirocli/install.sh). +const writeArchiveFixture = async (id: string, version: string) => { + await execContainer(id, ["mkdir", "-p", "/tmp/kiro-fixture/kirocli"]); + await writeExecutable({ + containerId: id, + filePath: "/tmp/kiro-fixture/kirocli/install.sh", + content: [ + "#!/bin/sh", + 'echo "KIRO_CLI_SKIP_SETUP=$KIRO_CLI_SKIP_SETUP" > /tmp/kiro-archive-install-env', + fakeKiroInstall(version), + ].join("\n"), }); + const resp = await execContainer(id, [ + "bash", + "-c", + "cd /tmp/kiro-fixture && python3 -m zipfile -c /tmp/kiro-archive.zip kirocli", + ]); + expect(resp.exitCode).toBe(0); +}; - // 3. Empty auth tarball handling - it("handles empty auth tarball correctly", async () => { - const noAuthVars = { - ...requiredVars, - auth_tarball: "", - }; +const moduleLogDir = "/home/coder/.coder-modules/harleylrn/kiro-cli/logs"; +const installLog = (id: string) => + readFileContainer(id, `${moduleLogDir}/install.log`); - const state = await runTerraformApply(moduleDir, noAuthVars); +setDefaultTimeout(60 * 1000); - // Auth tarball environment variable should not be created when empty - const authTarballEnv = state.resources?.find( - (r) => r.type === "coder_env" && r.name === "auth_tarball", - ); - expect(authTarballEnv).toBeUndefined(); +describe("kiro-cli", async () => { + beforeAll(async () => { + await runTerraformInit(import.meta.dir); }); - // 4. Status slug is always created - it("creates status slug environment variable", async () => { - const state = await runTerraformApply(moduleDir, requiredVars); - - // Status slug should always be configured - const statusSlugEnv = findResourceInstance( - state, - "coder_env", - "status_slug", - ); - expect(statusSlugEnv).toBeDefined(); - expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG"); - expect(statusSlugEnv.value).toBe("kiro-cli"); + test("happy-path-validates-existing-binary", async () => { + const { id, scripts } = await setup(); + await runScripts(id, scripts); + const log = await installLog(id); + expect(log).toContain("Skipping Kiro CLI installation"); + expect(log).toContain("Validated existing Kiro CLI"); + expect(log).toContain("Kiro CLI module setup completed."); + expect(log).not.toContain("agentapi"); }); - // 5. Install options configuration - it("respects install option flags", async () => { - const noInstallVars = { - ...requiredVars, - install_kiro_cli: false, - install_agentapi: false, - }; - - const state = await runTerraformApply(moduleDir, noInstallVars); - - // Status slug should still be configured even when install options are disabled - const statusSlugEnv = findResourceInstance( - state, - "coder_env", - "status_slug", - ); - expect(statusSlugEnv).toBeDefined(); - expect(statusSlugEnv.value).toBe("kiro-cli"); + test("preinstalled-binary-required-when-install-disabled", async () => { + const { id, scripts } = await setup({ skipKiroMock: true }); + const resp = await runScript(id, "install", scripts.install); + expect(resp.exitCode).not.toBe(0); + const log = await installLog(id); + expect(log).toContain("was not found or is not executable"); }); - // 6. Configurable installation URL - it("uses configurable kiro_install_url parameter", async () => { - const customUrlVars = { - ...requiredVars, - kiro_install_url: "https://internal-mirror.company.com/kiro-cli", - }; - - const state = await runTerraformApply(moduleDir, customUrlVars); - - // Should create the basic resources - const statusSlugEnv = findResourceInstance( - state, - "coder_env", - "status_slug", - ); - expect(statusSlugEnv).toBeDefined(); + test("official-installer-is-used-for-latest", async () => { + const { id, scripts } = await setup({ + skipKiroMock: true, + moduleVariables: { install_kiro_cli: "true" }, + }); + await writeExecutable({ + containerId: id, + filePath: "/tmp/kiro-installer-fixture.sh", + content: ["#!/usr/bin/env bash", fakeKiroInstall("2.26.0-fixture")].join( + "\n", + ), + }); + await writeCurlMock(id, "/tmp/kiro-installer-fixture.sh"); + await runScripts(id, scripts); + const log = await installLog(id); + expect(log).toContain("Installed Kiro CLI"); + expect(log).toContain("2.26.0-fixture"); + const curlArgs = await readFileContainer(id, "/tmp/kiro-curl-args"); + expect(curlArgs).toContain("https://cli.kiro.dev/install"); + expect(curlArgs).toContain("--retry 2"); + expect(curlArgs).toContain("--connect-timeout 10"); + expect(curlArgs).toContain("--max-time 300"); }); - // 7. Version configuration - it("uses specified versions", async () => { - const versionVars = { - ...requiredVars, - kiro_cli_version: "1.14.1", - agentapi_version: "v0.6.0", - }; - - const state = await runTerraformApply(moduleDir, versionVars); - - // Should create the basic resources - const statusSlugEnv = findResourceInstance( - state, - "coder_env", - "status_slug", - ); - expect(statusSlugEnv).toBeDefined(); + test("official-installer-reinstalls-over-existing-binary", async () => { + const { id, scripts } = await setup({ + skipKiroMock: true, + moduleVariables: { install_kiro_cli: "true" }, + }); + // The real installer prompts on /dev/tty when kiro-cli already exists. + await writeExecutable({ + containerId: id, + filePath: "/tmp/kiro-installer-fixture.sh", + content: [ + "#!/usr/bin/env bash", + '[ -e "$HOME/.local/bin/kiro-cli" ] && { echo "would prompt" >&2; exit 1; }', + fakeKiroInstall("2.27.0-fixture"), + ].join("\n"), + }); + await writeCurlMock(id, "/tmp/kiro-installer-fixture.sh"); + await runScripts(id, scripts); + await runScripts(id, scripts); + expect(await installLog(id)).toContain("2.27.0-fixture"); }); - // 8. UI configuration options - it("supports UI customization options", async () => { - const uiCustomVars = { - ...requiredVars, - order: 5, - group: "Custom AI Tools", - icon: "/icon/custom-kiro-cli-icon.svg", - }; - - const state = await runTerraformApply(moduleDir, uiCustomVars); - - // Should create the basic resources - const statusSlugEnv = findResourceInstance( - state, - "coder_env", - "status_slug", + test("pinned-version-uses-release-archive", async () => { + const { id, scripts } = await setup({ + skipKiroMock: true, + moduleVariables: { install_kiro_cli: "true", kiro_cli_version: "2.25.0" }, + }); + await writeArchiveFixture(id, "2.25.0"); + await writeCurlMock(id, "/tmp/kiro-archive.zip"); + await runScripts(id, scripts); + const log = await installLog(id); + expect(log).toContain("Installed Kiro CLI"); + expect(log).toContain("kiro-cli 2.25.0"); + const curlArgs = await readFileContainer(id, "/tmp/kiro-curl-args"); + expect(curlArgs).toMatch( + /https:\/\/desktop-release\.q\.us-east-1\.amazonaws\.com\/2\.25\.0\/kirocli-(x86_64|aarch64)-linux\.zip/, ); - expect(statusSlugEnv).toBeDefined(); + expect(curlArgs).not.toContain("cli.kiro.dev/install"); + const env = await readFileContainer(id, "/tmp/kiro-archive-install-env"); + expect(env.trim()).toBe("KIRO_CLI_SKIP_SETUP=1"); }); - // 9. Pre and post install scripts - it("supports pre and post install scripts", async () => { - const scriptVars = { - ...requiredVars, - pre_install_script: "echo 'Pre-install setup'", - post_install_script: "echo 'Post-install cleanup'", - }; - - const state = await runTerraformApply(moduleDir, scriptVars); - - // Should create the basic resources - const statusSlugEnv = findResourceInstance( - state, - "coder_env", - "status_slug", + test("custom-install-url-is-used-for-latest", async () => { + const { id, scripts } = await setup({ + skipKiroMock: true, + moduleVariables: { + install_kiro_cli: "true", + kiro_install_url: "https://mirror.example.com/kiro/", + }, + }); + await writeArchiveFixture(id, "2.26.0"); + await writeCurlMock(id, "/tmp/kiro-archive.zip"); + await runScripts(id, scripts); + const curlArgs = await readFileContainer(id, "/tmp/kiro-curl-args"); + expect(curlArgs).toMatch( + /https:\/\/mirror\.example\.com\/kiro\/latest\/kirocli-(x86_64|aarch64)-linux\.zip/, ); - expect(statusSlugEnv).toBeDefined(); }); - // 10. Valid agent_config JSON with different agent name - it("handles valid agent_config JSON with custom agent name", async () => { - const customAgentConfig = JSON.stringify({ - name: "production-agent", - description: "Production Kiro CLI agent", - prompt: "You are a production AI assistant.", - mcpServers: {}, - tools: ["fs_read", "fs_write"], - toolAliases: {}, - allowedTools: ["fs_read"], - resources: ["file://README.md"], - hooks: {}, - toolsSettings: {}, - useLegacyMcpJson: true, + test("installer-download-failure-is-terminal", async () => { + const { id, scripts } = await setup({ + skipKiroMock: true, + moduleVariables: { install_kiro_cli: "true" }, }); - - const validAgentConfigVars = { - ...requiredVars, - agent_config: customAgentConfig, - }; - - const state = await runTerraformApply(moduleDir, validAgentConfigVars); - - // Should create the basic resources - const statusSlugEnv = findResourceInstance( - state, - "coder_env", - "status_slug", - ); - expect(statusSlugEnv).toBeDefined(); + await writeExecutable({ + containerId: id, + filePath: "/usr/local/bin/curl", + content: "#!/bin/bash\nexit 22\n", + }); + const resp = await runScript(id, "install", scripts.install); + expect(resp.exitCode).not.toBe(0); + const log = await installLog(id); + expect(log).toContain("could not be downloaded"); }); - // 11. Air-gapped installation support - it("supports air-gapped installation with custom URL", async () => { - const airGappedVars = { - ...requiredVars, - kiro_install_url: "https://artifacts.internal.corp/kiro-cli-releases", - kiro_cli_version: "1.14.1", - }; - - const state = await runTerraformApply(moduleDir, airGappedVars); - - // Should create the basic resources - const statusSlugEnv = findResourceInstance( - state, - "coder_env", - "status_slug", - ); - expect(statusSlugEnv).toBeDefined(); + test("workdir-is-created", async () => { + const { id, scripts } = await setup(); + await runScripts(id, scripts); + const dir = await execContainer(id, ["test", "-d", projectDir]); + expect(dir.exitCode).toBe(0); }); - // 12. Trust all tools configuration - it("handles trust_all_tools configuration", async () => { - const trustVars = { - ...requiredVars, - trust_all_tools: true, + test("agent-config-written-and-set-as-default", async () => { + const agentConfig = { + name: "coder-agent", + description: "Custom agent", + prompt: "You are helpful.", + tools: ["read", "write"], + includeMcpJson: true, }; - - const state = await runTerraformApply(moduleDir, trustVars); - - // Should create the basic resources - const statusSlugEnv = findResourceInstance( - state, - "coder_env", - "status_slug", + const { id, scripts } = await setup({ + moduleVariables: { agent_config: JSON.stringify(agentConfig) }, + }); + await runScripts(id, scripts); + const agent = JSON.parse( + await readFileContainer(id, "/home/coder/.kiro/agents/coder-agent.json"), + ); + expect(agent).toEqual(agentConfig); + expect(await installLog(id)).toContain( + "kiro-cli invoked with: settings chat.defaultAgent coder-agent", ); - expect(statusSlugEnv).toBeDefined(); }); - // 13. AI prompt configuration - it("handles AI prompt configuration", async () => { - const promptVars = { - ...requiredVars, - ai_prompt: "Create a comprehensive test suite for the application", - }; - - const state = await runTerraformApply(moduleDir, promptVars); - - // Should create the basic resources - const statusSlugEnv = findResourceInstance( - state, - "coder_env", - "status_slug", - ); - expect(statusSlugEnv).toBeDefined(); + test("no-agent-config-leaves-settings-untouched", async () => { + const { id, scripts } = await setup(); + await runScripts(id, scripts); + const resp = await execContainer(id, [ + "bash", + "-c", + "test -e /home/coder/.kiro && echo EXISTS || echo ABSENT", + ]); + expect(resp.stdout.trim()).toBe("ABSENT"); }); - // 14. Agent config with minimal structure - it("handles minimal agent config structure", async () => { - const minimalAgentConfig = JSON.stringify({ - name: "minimal-agent", - description: "Minimal agent config", - prompt: "You are a minimal AI assistant.", - mcpServers: {}, - tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"], - toolAliases: {}, - allowedTools: ["fs_read"], - resources: ["file://README.md"], - hooks: {}, - toolsSettings: {}, - useLegacyMcpJson: true, + test("auth-tarball-is-extracted-from-env", async () => { + const { id, scripts } = await setup(); + // tar -I zstd pipes through `zstd -d`; a passthrough keeps the fixture + // independent of zstd being installed in the image. + await writeExecutable({ + containerId: id, + filePath: "/usr/local/bin/zstd", + content: "#!/bin/sh\nexec cat\n", }); - - const minimalVars = { - ...requiredVars, - agent_config: minimalAgentConfig, - }; - - const state = await runTerraformApply(moduleDir, minimalVars); - - // Should create the basic resources - const statusSlugEnv = findResourceInstance( - state, - "coder_env", - "status_slug", + const tarball = await execContainer(id, [ + "bash", + "-c", + "mkdir -p /tmp/auth && echo fake-auth > /tmp/auth/data.sqlite3 && tar -C /tmp/auth -cf - . | base64 -w0", + ]); + expect(tarball.exitCode).toBe(0); + await execContainer(id, [ + "bash", + "-c", + "mkdir -p /home/coder/.local/share/kiro-cli && echo stale > /home/coder/.local/share/kiro-cli/stale", + ]); + await runScripts(id, scripts, { + KIRO_CLI_AUTH_TARBALL: tarball.stdout.trim(), + }); + const db = await readFileContainer( + id, + "/home/coder/.local/share/kiro-cli/data.sqlite3", ); - expect(statusSlugEnv).toBeDefined(); + expect(db.trim()).toBe("fake-auth"); + const stale = await execContainer(id, [ + "test", + "-e", + "/home/coder/.local/share/kiro-cli/stale", + ]); + expect(stale.exitCode).not.toBe(0); }); - // 15. JSON encoding validation for system prompts with newlines - it("handles system prompts with newlines correctly", async () => { - const multilinePromptVars = { - ...requiredVars, - system_prompt: "Multi-line\nsystem prompt\nwith newlines", - }; - - const state = await runTerraformApply(moduleDir, multilinePromptVars); + test("auth-tarball-requires-zstd", async () => { + const { id, scripts } = await setup(); + const resp = await runScript(id, "install", scripts.install, { + KIRO_CLI_AUTH_TARBALL: "dGVzdA==", + }); + expect(resp.exitCode).not.toBe(0); + const log = await installLog(id); + expect(log).toContain("zstd is required"); + }); - // Should create the basic resources without JSON parsing errors - const statusSlugEnv = findResourceInstance( - state, - "coder_env", - "status_slug", + test("secrets-are-env-vars-not-in-script", async () => { + const apiKey = "ksk_test-kiro-api-key-123"; + const authTarball = "dGVzdEF1dGhUYXJiYWxs"; + const { coderEnvVars, scripts } = await setup({ + moduleVariables: { api_key: apiKey, auth_tarball: authTarball }, + }); + expect(coderEnvVars["KIRO_API_KEY"]).toBe(apiKey); + expect(coderEnvVars["KIRO_CLI_AUTH_TARBALL"]).toBe(authTarball); + expect(scripts.install).not.toContain(apiKey); + expect(scripts.install).not.toContain( + Buffer.from(apiKey).toString("base64"), ); - expect(statusSlugEnv).toBeDefined(); - expect(statusSlugEnv.value).toBe("kiro-cli"); + expect(scripts.install).not.toContain(authTarball); }); - // 16. Agent name extraction from custom config - it("extracts agent name from custom configuration correctly", async () => { - const customNameConfig = JSON.stringify({ - name: "enterprise-production-agent", - description: "Enterprise production agent configuration", - prompt: "You are an enterprise production AI assistant.", - mcpServers: {}, - tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"], - toolAliases: {}, - allowedTools: ["fs_read", "fs_write", "execute_bash"], - resources: ["file://README.md", "file://.kiro/steering/**/*.md"], - hooks: {}, - toolsSettings: {}, - useLegacyMcpJson: true, + test("pre-post-install-scripts", async () => { + const { id, scripts } = await setup({ + moduleVariables: { + pre_install_script: "#!/bin/bash\necho 'kiro-pre-install-script'", + post_install_script: "#!/bin/bash\necho 'kiro-post-install-script'", + }, }); - - const customNameVars = { - ...requiredVars, - agent_config: customNameConfig, - }; - - const state = await runTerraformApply(moduleDir, customNameVars); - - // Should create the basic resources - const statusSlugEnv = findResourceInstance( - state, - "coder_env", - "status_slug", - ); - expect(statusSlugEnv).toBeDefined(); - expect(statusSlugEnv.value).toBe("kiro-cli"); + await runScripts(id, scripts); + expect( + await readFileContainer(id, `${moduleLogDir}/pre_install.log`), + ).toContain("kiro-pre-install-script"); + expect( + await readFileContainer(id, `${moduleLogDir}/post_install.log`), + ).toContain("kiro-post-install-script"); }); }); diff --git a/registry/harleylrn/modules/kiro-cli/main.tf b/registry/harleylrn/modules/kiro-cli/main.tf index 5af5e31972..4fa279cddc 100644 --- a/registry/harleylrn/modules/kiro-cli/main.tf +++ b/registry/harleylrn/modules/kiro-cli/main.tf @@ -1,7 +1,5 @@ -# Improved kiro-cli module main.tf - terraform { - required_version = ">= 1.0" + required_version = ">= 1.9" required_providers { coder = { @@ -16,89 +14,33 @@ variable "agent_id" { description = "The ID of a Coder agent." } -data "coder_workspace" "me" {} -data "coder_workspace_owner" "me" {} - -variable "order" { - type = number - description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)." - default = null -} - -variable "group" { - type = string - description = "The name of a group that this app belongs to." - default = null -} - variable "icon" { type = string - description = "The icon to use for the app." + description = "The icon to use for the install scripts." default = "/icon/kiro.svg" } -variable "report_tasks" { - type = bool - description = "Whether to enable task reporting to Coder UI via AgentAPI" - default = true -} - -variable "cli_app" { - type = bool - description = "Whether to create a CLI app for Kiro CLI" - default = false -} - -variable "web_app_display_name" { - type = string - description = "Display name for the web app" - default = "Kiro CLI" -} - -variable "cli_app_display_name" { - type = string - description = "Display name for the CLI app" - default = "Kiro CLI" -} - -variable "install_agentapi" { - type = bool - description = "Whether to install AgentAPI." - default = true -} - -variable "ai_prompt" { +variable "workdir" { type = string - description = "The initial task prompt to send to Kiro CLI." - default = "" + description = "Optional project directory. When set, the module creates it if missing." + default = null } variable "pre_install_script" { type = string - description = "Optional script to run before installing Kiro CLI." + description = "Custom script to run before installing Kiro CLI." default = null } variable "post_install_script" { type = string - description = "Optional script to run after installing Kiro CLI." + description = "Custom script to run after installing Kiro CLI." default = null } -variable "agentapi_version" { - type = string - description = "The version of AgentAPI to install." - default = "v0.10.0" -} - -variable "workdir" { - type = string - description = "The folder to run Kiro CLI in." -} - variable "install_kiro_cli" { type = bool - description = "Whether to install Kiro CLI." + description = "Whether to install Kiro CLI. When false, a working kiro-cli must already be on PATH." default = true } @@ -110,51 +52,20 @@ variable "kiro_cli_version" { variable "kiro_install_url" { type = string - description = "Base URL for Kiro CLI installation downloads." - default = "https://desktop-release.q.us-east-1.amazonaws.com" -} - -variable "trust_all_tools" { - type = bool - description = "Whether to trust all tools in Kiro CLI." - default = false -} - -variable "system_prompt" { - type = string - description = "The system prompt to use for Kiro CLI. This should instruct the agent how to do task reporting." - default = <<-EOT - You are a helpful Coding assistant. Aim to autonomously investigate - and solve issues the user gives you and test your work, whenever possible. - Avoid shortcuts like mocking tests. When you get stuck, you can ask the user - but opt for autonomy. - EOT + description = "Base URL for Kiro CLI installation downloads. Defaults to https://desktop-release.q.us-east-1.amazonaws.com." + default = null } -variable "coder_mcp_instructions" { +variable "auth_tarball" { type = string - description = "Instructions for the Coder MCP server integration. This defines how the agent should report tasks to Coder." - default = <<-EOT - YOU MUST REPORT ALL TASKS TO CODER. - When reporting tasks you MUST follow these EXACT instructions: - - IMMEDIATELY report status after receiving ANY user message - - Be granular If you are investigating with multiple steps report each step to coder. - - Task state MUST be one of the following: - - Use "state": "working" when actively processing WITHOUT needing additional user input - - Use "state": "complete" only when finished with a task - - Use "state": "failure" when you need ANY user input lack sufficient details or encounter blockers. - - Task summaries MUST: - - Include specifics about what you're doing - - Include clear and actionable steps for the user - - Be less than 160 characters in length - EOT + description = "Base64 encoded, zstd compressed tarball of a pre-authenticated ~/.local/share/kiro-cli directory." + default = "" + sensitive = true } -variable "auth_tarball" { +variable "api_key" { type = string - description = "Base64 encoded, zstd compressed tarball of a pre-authenticated ~/.local/share/kiro-cli directory." + description = "Kiro API key, exported as KIRO_API_KEY. See https://kiro.dev/docs/getting-started/authentication/" default = "" sensitive = true } @@ -165,21 +76,6 @@ variable "agent_config" { default = null } -variable "agentapi_chat_based_path" { - type = bool - description = "Whether to use chat-based path for AgentAPI.Required if CODER_WILDCARD_ACCESS_URL is not defined in coder deployment" - default = false -} - -# Expose status slug to the agent environment -resource "coder_env" "status_slug" { - agent_id = var.agent_id - name = "CODER_MCP_APP_STATUS_SLUG" - value = local.app_slug - count = var.report_tasks ? 1 : 0 -} - -# Expose auth tarball as environment variable for install script resource "coder_env" "auth_tarball" { count = var.auth_tarball != "" ? 1 : 0 agent_id = var.agent_id @@ -187,89 +83,40 @@ resource "coder_env" "auth_tarball" { value = var.auth_tarball } -locals { - app_slug = "kiro-cli" - workdir = trimsuffix(var.workdir, "/") - install_script = file("${path.module}/scripts/install.sh") - start_script = file("${path.module}/scripts/start.sh") - module_dir_name = ".kiro" - system_prompt = jsonencode(replace(var.system_prompt, "/[\r\n]/", "")) - coder_mcp_instructions = jsonencode(replace(var.coder_mcp_instructions, "/[\r\n]/", "")) +resource "coder_env" "kiro_api_key" { + count = var.api_key != "" ? 1 : 0 + agent_id = var.agent_id + name = "KIRO_API_KEY" + value = var.api_key +} - # Create default agent config structure - default_agent_config = templatefile("${path.module}/templates/agent-config.json.tpl", { - system_prompt = local.system_prompt +locals { + workdir = var.workdir != null ? trimsuffix(var.workdir, "/") : "" + install_script = templatefile("${path.module}/scripts/install.sh.tftpl", { + ARG_INSTALL = tostring(var.install_kiro_cli) + ARG_VERSION = base64encode(var.kiro_cli_version) + ARG_INSTALL_URL = var.kiro_install_url != null ? base64encode(trimsuffix(var.kiro_install_url, "/")) : "" + ARG_WORKDIR = local.workdir != "" ? base64encode(local.workdir) : "" + ARG_AGENT_CONFIG = var.agent_config != null ? base64encode(var.agent_config) : "" + ARG_AGENT_NAME = base64encode(try(jsondecode(var.agent_config).name, "agent")) }) - - # Choose the JSON string: use var.agent_config if provided, otherwise encode default - agent_config = var.agent_config != null ? var.agent_config : local.default_agent_config - - # Extract agent name from the selected config - agent_name = try(jsondecode(local.agent_config).name, "agent") - - full_prompt = var.ai_prompt != null ? var.ai_prompt : "" - - server_chat_parameters = var.agentapi_chat_based_path ? "--chat-base-path /@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}.${var.agent_id}/apps/${local.app_slug}/chat" : "" + module_dir_name = ".coder-modules/harleylrn/kiro-cli" } +module "coder_utils" { + source = "registry.coder.com/coder/coder-utils/coder" + version = "0.0.1" -module "agentapi" { - source = "registry.coder.com/coder/agentapi/coder" - version = "2.0.0" - - agent_id = var.agent_id - folder = local.workdir - web_app_slug = local.app_slug - web_app_order = var.order - web_app_group = var.group - web_app_icon = var.icon - web_app_display_name = var.web_app_display_name - cli_app = var.cli_app - cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null - cli_app_display_name = var.cli_app ? var.cli_app_display_name : null - module_dir_name = local.module_dir_name - install_agentapi = var.install_agentapi - agentapi_version = var.agentapi_version - pre_install_script = var.pre_install_script - post_install_script = var.post_install_script - - start_script = <<-EOT - #!/usr/bin/env bash - set -o errexit - set -o pipefail - - echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh - chmod +x /tmp/start.sh - ARG_TRUST_ALL_TOOLS='${var.trust_all_tools}' \ - ARG_AI_PROMPT='${base64encode(local.full_prompt)}' \ - ARG_MODULE_DIR_NAME='${local.module_dir_name}' \ - ARG_WORKDIR='${var.workdir}' \ - ARG_SERVER_PARAMETERS="${local.server_chat_parameters}" \ - ARG_REPORT_TASKS='${var.report_tasks}' \ - /tmp/start.sh - EOT - - install_script = <<-EOT - #!/usr/bin/env bash - set -o errexit - set -o pipefail - - echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh - chmod +x /tmp/install.sh - ARG_INSTALL='${var.install_kiro_cli}' \ - ARG_VERSION='${var.kiro_cli_version}' \ - ARG_KIRO_INSTALL_URL='${var.kiro_install_url}' \ - ARG_AUTH_TARBALL='${var.auth_tarball}' \ - ARG_AGENT_CONFIG='${local.agent_config != null ? base64encode(local.agent_config) : ""}' \ - ARG_AGENT_NAME='${local.agent_name}' \ - ARG_MODULE_DIR_NAME='${local.module_dir_name}' \ - ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \ - ARG_CODER_MCP_INSTRUCTIONS='${base64encode(local.coder_mcp_instructions)}' \ - ARG_REPORT_TASKS='${var.report_tasks}' \ - /tmp/install.sh - EOT + agent_id = var.agent_id + module_directory = "$HOME/${local.module_dir_name}" + display_name_prefix = "Kiro CLI" + icon = var.icon + pre_install_script = var.pre_install_script + post_install_script = var.post_install_script + install_script = local.install_script } -output "task_app_id" { - value = module.agentapi.task_app_id +output "scripts" { + description = "Ordered list of coder exp sync names for the coder_script resources this module creates, in run order (pre_install, install, post_install). Scripts that were not configured are absent from the list." + value = module.coder_utils.scripts } diff --git a/registry/harleylrn/modules/kiro-cli/main.tftest.hcl b/registry/harleylrn/modules/kiro-cli/main.tftest.hcl new file mode 100644 index 0000000000..ef2cdeae00 --- /dev/null +++ b/registry/harleylrn/modules/kiro-cli/main.tftest.hcl @@ -0,0 +1,112 @@ +run "defaults_are_correct" { + command = plan + + variables { + agent_id = "test-agent" + } + + assert { + condition = var.install_kiro_cli == true + error_message = "install_kiro_cli should default to true" + } + + assert { + condition = var.kiro_cli_version == "latest" + error_message = "kiro_cli_version should default to latest" + } + + assert { + condition = local.workdir == "" + error_message = "workdir should be empty by default" + } + + assert { + condition = local.module_dir_name == ".coder-modules/harleylrn/kiro-cli" + error_message = "module_dir_name should be '.coder-modules/harleylrn/kiro-cli'" + } + + assert { + condition = length(coder_env.auth_tarball) == 0 && length(coder_env.kiro_api_key) == 0 + error_message = "No auth env vars should be created by default" + } +} + +run "workdir_trailing_slash_is_trimmed" { + command = plan + + variables { + agent_id = "test-agent" + workdir = "/home/coder/project/" + } + + assert { + condition = local.workdir == "/home/coder/project" + error_message = "workdir should have its trailing slash trimmed" + } +} + +run "api_key_creates_env_var" { + command = plan + + variables { + agent_id = "test-agent" + api_key = "ksk_test-kiro-key" + } + + assert { + condition = coder_env.kiro_api_key[0].name == "KIRO_API_KEY" && coder_env.kiro_api_key[0].value == "ksk_test-kiro-key" + error_message = "KIRO_API_KEY env var should be created with the provided key" + } + + assert { + condition = !strcontains(local.install_script, nonsensitive(var.api_key)) && !strcontains(local.install_script, base64encode(nonsensitive(var.api_key))) + error_message = "API key should not be rendered into the install script" + } +} + +run "auth_tarball_creates_env_var" { + command = plan + + variables { + agent_id = "test-agent" + auth_tarball = "dGVzdEF1dGhUYXJiYWxs" + } + + assert { + condition = coder_env.auth_tarball[0].name == "KIRO_CLI_AUTH_TARBALL" && coder_env.auth_tarball[0].value == "dGVzdEF1dGhUYXJiYWxs" + error_message = "KIRO_CLI_AUTH_TARBALL env var should be created with the provided tarball" + } + + assert { + condition = !strcontains(local.install_script, nonsensitive(var.auth_tarball)) + error_message = "Auth tarball should not be rendered into the install script" + } +} + +run "scripts_output_is_ordered" { + command = plan + + variables { + agent_id = "test-agent" + pre_install_script = "echo pre" + post_install_script = "echo post" + } + + assert { + condition = output.scripts == ["harleylrn-kiro-cli-pre_install_script", "harleylrn-kiro-cli-install_script", "harleylrn-kiro-cli-post_install_script"] + error_message = "scripts output should be ordered pre_install, install, post_install" + } +} + +run "scripts_output_install_only" { + command = plan + + variables { + agent_id = "test-agent" + } + + assert { + condition = output.scripts == ["harleylrn-kiro-cli-install_script"] + error_message = "scripts output should only contain install when no pre/post scripts are set" + } +} diff --git a/registry/harleylrn/modules/kiro-cli/scripts/install.sh b/registry/harleylrn/modules/kiro-cli/scripts/install.sh deleted file mode 100644 index 8f18ac0347..0000000000 --- a/registry/harleylrn/modules/kiro-cli/scripts/install.sh +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env bash -# Install script for kiro-cli module - -set -o errexit -set -o pipefail - -command_exists() { - command -v "$1" > /dev/null 2>&1 -} - -# Inputs -ARG_INSTALL=${ARG_INSTALL:-true} -ARG_VERSION=${ARG_VERSION:-latest} -ARG_KIRO_INSTALL_URL=${ARG_KIRO_INSTALL_URL:-https://desktop-release.q.us-east-1.amazonaws.com} -ARG_AUTH_TARBALL=${ARG_AUTH_TARBALL:-} -ARG_AGENT_CONFIG=${ARG_AGENT_CONFIG:-} -ARG_AGENT_NAME=${ARG_AGENT_NAME:-default-agent} -ARG_MODULE_DIR_NAME=${ARG_MODULE_DIR_NAME:-.kiro} -ARG_CODER_MCP_APP_STATUS_SLUG=${ARG_CODER_MCP_APP_STATUS_SLUG:-} -ARG_CODER_MCP_INSTRUCTIONS=${ARG_CODER_MCP_INSTRUCTIONS:-} -ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true} - -mkdir -p "$HOME/$ARG_MODULE_DIR_NAME" - -# Decode base64 inputs -ARG_AGENT_CONFIG_DECODED="" -if [ -n "$ARG_AGENT_CONFIG" ]; then - ARG_AGENT_CONFIG_DECODED=$(echo -n "$ARG_AGENT_CONFIG" | base64 -d) -fi - -ARG_CODER_MCP_INSTRUCTIONS_DECODED="" -if [ -n "$ARG_CODER_MCP_INSTRUCTIONS" ]; then - ARG_CODER_MCP_INSTRUCTIONS_DECODED=$(echo -n "$ARG_CODER_MCP_INSTRUCTIONS" | base64 -d) -fi - -echo "--------------------------------" -echo "install: $ARG_INSTALL" -echo "version: $ARG_VERSION" -echo "kiro_install_url: $ARG_KIRO_INSTALL_URL" -echo "agent_name: $ARG_AGENT_NAME" -echo "coder_mcp_app_status_slug: $ARG_CODER_MCP_APP_STATUS_SLUG" -echo "module_dir_name: $ARG_MODULE_DIR_NAME" -echo "auth_tarball_provided: ${ARG_AUTH_TARBALL}" -echo "report_tasks: ${ARG_REPORT_TASKS}" -echo "--------------------------------" - -# Install Kiro CLI if requested -function install_kiro_cli() { - if [ "$ARG_INSTALL" = "true" ]; then - echo "Installing Kiro CLI..." - PREV_DIR="$PWD" - TMP_DIR="$(mktemp -d)" - cd "$TMP_DIR" - - ARCH="$(uname -m)" - case "$ARCH" in - "x86_64") - KIRO_URL="${ARG_KIRO_INSTALL_URL}/${ARG_VERSION}/kirocli-x86_64-linux.zip" - ;; - "aarch64" | "arm64") - KIRO_URL="${ARG_KIRO_INSTALL_URL}/${ARG_VERSION}/kirocli-aarch64-linux.zip" - ;; - *) - echo "Error: Unsupported architecture: $ARCH. Kiro CLI only supports x86_64 and arm64." - exit 1 - ;; - esac - - echo "Downloading Kiro CLI for $ARCH from $KIRO_URL..." - curl --proto '=https' --tlsv1.2 -sSf "$KIRO_URL" -o "kirocli.zip" - unzip kirocli.zip - ./kirocli/install.sh --no-confirm - cd "$PREV_DIR" - rm -rf "$TMP_DIR" - - # Ensure binaries are discoverable; create stable symlink to kiro-cli - CANDIDATES=( - "$(command -v kiro-cli || true)" - "$HOME/.local/bin/kiro-cli" - ) - FOUND_BIN="" - for c in "${CANDIDATES[@]}"; do - if [ -n "$c" ] && [ -x "$c" ]; then - FOUND_BIN="$c" - break - fi - done - export PATH="$PATH:$HOME/.local/bin" - echo "Installed Kiro CLI at: $(command -v kiro-cli || true) (resolved: $FOUND_BIN)" - fi -} - -# Extract authentication tarball -function extract_auth_tarball() { - if [ -n "$ARG_AUTH_TARBALL" ]; then - echo "Extracting auth tarball..." - - if ! command_exists zstd; then - echo "Error: zstd is required to extract the authentication tarball but is not installed." - echo "Please install zstd using the pre_install_script parameter." - exit 1 - fi - - PREV_DIR="$PWD" - echo "$ARG_AUTH_TARBALL" | base64 -d > /tmp/auth.tar.zst - rm -rf ~/.local/share/kiro-cli - mkdir -p ~/.local/share/kiro-cli - cd ~/.local/share/kiro-cli - tar -I zstd -xf /tmp/auth.tar.zst - rm /tmp/auth.tar.zst - cd "$PREV_DIR" - echo "Extracted auth tarball to ~/.local/share/kiro-cli" - else - echo "Warning: No auth tarball provided. Kiro CLI may require manual authentication." - fi -} - -# Configure MCP integration and create agent -function configure_agent() { - # Create Kiro CLI agent configuration directory - AGENT_CONFIG_DIR="$HOME/.kiro/agents" - mkdir -p "$AGENT_CONFIG_DIR" - ALLOWED_TOOLS="coder_get_workspace\,coder_create_workspace\,coder_list_workspaces\,coder_list_templates\,coder_template_version_parameters\,coder_get_authenticated_user\,coder_create_workspace_build\,coder_create_template_version\,coder_get_workspace_agent_logs\,coder_get_workspace_build_logs\,coder_get_template_version_logs\,coder_update_template_active_version\,coder_upload_tar_file\,coder_create_template\,coder_delete_template\,coder_workspace_bash" - if [ -n "$ARG_AGENT_CONFIG_DECODED" ]; then - echo "Applying custom MCP configuration..." - # Use agent name as filename for the configuration - echo "$ARG_AGENT_CONFIG_DECODED" > "$AGENT_CONFIG_DIR/${ARG_AGENT_NAME}.json" - echo "Custom configuration saved to $AGENT_CONFIG_DIR/${ARG_AGENT_NAME}.json" - fi - if [ "$ARG_REPORT_TASKS" = "true" ]; then - echo "Configuring Kiro CLI to report tasks via Coder MCP..." - kiro-cli mcp add --name coder \ - --command "coder" \ - --agent "$ARG_AGENT_NAME" \ - --args "exp,mcp,server,--allowed-tools,coder_report_task,--instructions,'$ARG_CODER_MCP_INSTRUCTIONS_DECODED'" \ - --env "CODER_MCP_APP_STATUS_SLUG=${ARG_CODER_MCP_APP_STATUS_SLUG}" \ - --env "CODER_MCP_AI_AGENTAPI_URL=http://localhost:3284" \ - --env "CODER_AGENT_URL=${CODER_AGENT_URL}" \ - --env "CODER_AGENT_TOKEN=${CODER_AGENT_TOKEN}" \ - --force || echo "Warning: Failed to add Coder MCP server" - else - kiro-cli mcp add --name coder \ - --command "coder" \ - --agent "$ARG_AGENT_NAME" \ - --args "exp,mcp,server,--allowed-tools,coder_report_task" \ - --env "CODER_AGENT_URL=${CODER_AGENT_URL}" \ - --env "CODER_AGENT_TOKEN=${CODER_AGENT_TOKEN}" \ - --force || echo "Warning: Failed to add Coder MCP server" - fi - echo "Added Coder MCP server into $ARG_AGENT_NAME in Kiro CLI configuration" - kiro-cli settings chat.defaultAgent "$ARG_AGENT_NAME" -} - -# Main execution -install_kiro_cli -extract_auth_tarball -configure_agent - -echo "Kiro CLI installation and configuration complete!" diff --git a/registry/harleylrn/modules/kiro-cli/scripts/install.sh.tftpl b/registry/harleylrn/modules/kiro-cli/scripts/install.sh.tftpl new file mode 100644 index 0000000000..3116cd9383 --- /dev/null +++ b/registry/harleylrn/modules/kiro-cli/scripts/install.sh.tftpl @@ -0,0 +1,238 @@ +#!/usr/bin/env bash + +set -euo pipefail + +command_exists() { + command -v "$1" > /dev/null 2>&1 +} + +ARG_INSTALL='${ARG_INSTALL}' +ARG_VERSION=$(echo -n '${ARG_VERSION}' | base64 -d) +ARG_INSTALL_URL=$(echo -n '${ARG_INSTALL_URL}' | base64 -d) +ARG_WORKDIR=$(echo -n '${ARG_WORKDIR}' | base64 -d) +ARG_AGENT_CONFIG=$(echo -n '${ARG_AGENT_CONFIG}' | base64 -d) +ARG_AGENT_NAME=$(echo -n '${ARG_AGENT_NAME}' | base64 -d) + +# KIRO_HOME overrides ~/.kiro: https://kiro.dev/docs/reference/settings/ +KIRO_DIR="$${KIRO_HOME:-$HOME/.kiro}" +KIRO_DATA_DIR="$HOME/.local/share/kiro-cli" +DEFAULT_ARCHIVE_URL="https://desktop-release.q.us-east-1.amazonaws.com" + +export PATH="$HOME/.local/bin:$PATH" + +echo "--------------------------------" +printf "install_kiro_cli: %s\n" "$${ARG_INSTALL}" +printf "kiro_cli_version: %s\n" "$${ARG_VERSION}" +printf "kiro_install_url: %s\n" "$${ARG_INSTALL_URL:-}" +printf "workdir: %s\n" "$${ARG_WORKDIR}" +echo "--------------------------------" + +function add_path_to_shell_profiles() { + local path_dir="$1" + + for profile in "$HOME/.profile" "$HOME/.bash_profile" "$HOME/.bashrc" "$HOME/.zprofile" "$HOME/.zshrc"; do + if [ -f "$${profile}" ]; then + if ! grep -q "$${path_dir}" "$${profile}" 2> /dev/null; then + echo "export PATH=\"\$PATH:$${path_dir}\"" >> "$${profile}" + echo "Added $${path_dir} to $${profile}" + fi + fi + done + + local fish_config="$HOME/.config/fish/config.fish" + if [ -f "$${fish_config}" ]; then + if ! grep -q "$${path_dir}" "$${fish_config}" 2> /dev/null; then + echo "fish_add_path $${path_dir}" >> "$${fish_config}" + echo "Added $${path_dir} to $${fish_config}" + fi + fi +} + +function ensure_kiro_in_path() { + local KIRO_BIN="" + if command_exists kiro-cli; then + KIRO_BIN=$(command -v kiro-cli) + fi + + if [ -z "$${KIRO_BIN}" ] || [ ! -x "$${KIRO_BIN}" ]; then + echo "Kiro CLI binary (kiro-cli) was not found or is not executable." >&2 + return 1 + fi + + local KIRO_VERSION + if ! KIRO_VERSION=$("$${KIRO_BIN}" --version); then + echo "Kiro CLI binary could not be executed." >&2 + return 1 + fi + printf "Kiro CLI version: %s\n" "$${KIRO_VERSION}" + + # kiro-cli resolves its kiro-cli-chat sibling through the symlink target. + if [ -n "$${CODER_SCRIPT_BIN_DIR:-}" ] && [ ! -e "$${CODER_SCRIPT_BIN_DIR}/kiro-cli" ]; then + ln -s "$${KIRO_BIN}" "$${CODER_SCRIPT_BIN_DIR}/kiro-cli" + echo "Created symlink: $${CODER_SCRIPT_BIN_DIR}/kiro-cli -> $${KIRO_BIN}" + fi + + add_path_to_shell_profiles "$(dirname "$${KIRO_BIN}")" +} + +# Subshell scopes the temp-file cleanup trap to this function. +install_with_official_installer() ( + local installer_file + installer_file=$(mktemp) + trap 'rm -f "$${installer_file}"' EXIT + + if ! curl --fail --silent --show-error --location \ + --retry 2 --retry-delay 1 --retry-all-errors \ + --connect-timeout 10 --max-time 300 \ + --output "$${installer_file}" https://cli.kiro.dev/install; then + echo "Kiro CLI installer could not be downloaded after up to 3 attempts." >&2 + return 1 + fi + + if ! bash -n "$${installer_file}"; then + echo "Kiro CLI installer download was invalid." >&2 + return 1 + fi + + # Installs the latest stable build into ~/.local/bin after verifying its + # checksum. It prompts on /dev/tty if kiro-cli already exists there, so the + # existing binaries are removed first, as the installer does on confirm. + rm -f "$HOME/.local/bin/kiro-cli" "$HOME/.local/bin/kiro-cli-chat" + if ! bash "$${installer_file}" < /dev/null; then + echo "Kiro CLI installation failed." >&2 + return 1 + fi +) + +# Pinned versions and mirrors use the release archive directly, since the +# official installer only installs /latest. +install_from_archive() ( + local arch + case "$(uname -m)" in + x86_64 | amd64) arch="x86_64" ;; + aarch64 | arm64) arch="aarch64" ;; + *) + echo "Unsupported architecture: $(uname -m). Kiro CLI supports x86_64 and aarch64." >&2 + return 1 + ;; + esac + + if ! command_exists unzip; then + echo "ERROR: 'unzip' is required to install Kiro CLI from a release archive." >&2 + return 1 + fi + + local base_url="$${ARG_INSTALL_URL:-$${DEFAULT_ARCHIVE_URL}}" + local archive_url="$${base_url}/$${ARG_VERSION}/kirocli-$${arch}-linux.zip" + local tmp_dir + tmp_dir=$(mktemp -d) + trap 'rm -rf "$${tmp_dir}"' EXIT + + echo "Downloading Kiro CLI from $${archive_url}" + # The archive is ~150 MB, so allow longer than the installer download. + if ! curl --fail --silent --show-error --location \ + --retry 2 --retry-delay 1 --retry-all-errors \ + --connect-timeout 10 --max-time 900 \ + --output "$${tmp_dir}/kirocli.zip" "$${archive_url}"; then + echo "Kiro CLI archive could not be downloaded after up to 3 attempts." >&2 + return 1 + fi + + if ! unzip -q "$${tmp_dir}/kirocli.zip" -d "$${tmp_dir}"; then + echo "Kiro CLI archive download was invalid." >&2 + return 1 + fi + + # KIRO_CLI_SKIP_SETUP skips the interactive shell-integration setup, matching + # the official installer. + if ! KIRO_CLI_SKIP_SETUP=1 sh "$${tmp_dir}/kirocli/install.sh"; then + echo "Kiro CLI installation failed." >&2 + return 1 + fi +) + +install_kiro_cli() { + if [ "$${ARG_INSTALL}" != "true" ]; then + echo "Skipping Kiro CLI installation as per configuration." + if ! ensure_kiro_in_path; then + echo "ERROR: install_kiro_cli is false but a working 'kiro-cli' was not found on PATH." >&2 + exit 1 + fi + echo "Validated existing Kiro CLI" + return + fi + + echo "Installing Kiro CLI" + if [ "$${ARG_VERSION}" = "latest" ] && [ -z "$${ARG_INSTALL_URL}" ]; then + install_with_official_installer + else + install_from_archive + fi + + if ! ensure_kiro_in_path; then + echo "ERROR: Failed to install Kiro CLI" >&2 + exit 1 + fi + echo "Installed Kiro CLI" +} + +setup_workdir() { + if [ -n "$${ARG_WORKDIR}" ] && [ ! -d "$${ARG_WORKDIR}" ]; then + echo "Creating workdir: $${ARG_WORKDIR}" + mkdir -p "$${ARG_WORKDIR}" + fi +} + +# The tarball is read from the KIRO_CLI_AUTH_TARBALL agent env var so the +# credential is never rendered into this script. +extract_auth_tarball() { + local tarball="$${KIRO_CLI_AUTH_TARBALL:-}" + if [ -z "$${tarball}" ]; then + echo "No auth tarball provided; run 'kiro-cli login' or set KIRO_API_KEY to authenticate." + return + fi + + if ! command_exists zstd; then + echo "ERROR: zstd is required to extract the authentication tarball but is not installed." >&2 + echo "Install zstd in the image or with pre_install_script." >&2 + exit 1 + fi + + local archive + archive=$(mktemp) + if ! echo "$${tarball}" | base64 -d > "$${archive}"; then + rm -f "$${archive}" + echo "ERROR: auth_tarball is not valid base64." >&2 + exit 1 + fi + + rm -rf "$${KIRO_DATA_DIR}" + mkdir -p "$${KIRO_DATA_DIR}" + if ! tar -I zstd -xf "$${archive}" -C "$${KIRO_DATA_DIR}"; then + rm -f "$${archive}" + echo "ERROR: Failed to extract the authentication tarball." >&2 + exit 1 + fi + rm -f "$${archive}" + echo "Extracted auth tarball to $${KIRO_DATA_DIR}" +} + +configure_agent() { + if [ -z "$${ARG_AGENT_CONFIG}" ]; then + echo "No custom agent configured; using Kiro's built-in default agent." + return + fi + + local agent_file="$${KIRO_DIR}/agents/$${ARG_AGENT_NAME}.json" + mkdir -p "$(dirname "$${agent_file}")" + printf '%s\n' "$${ARG_AGENT_CONFIG}" > "$${agent_file}" + echo "Wrote agent: $${agent_file}" + kiro-cli settings chat.defaultAgent "$${ARG_AGENT_NAME}" +} + +install_kiro_cli +setup_workdir +extract_auth_tarball +configure_agent + +echo "Kiro CLI module setup completed." diff --git a/registry/harleylrn/modules/kiro-cli/scripts/start.sh b/registry/harleylrn/modules/kiro-cli/scripts/start.sh deleted file mode 100644 index 483e2fa197..0000000000 --- a/registry/harleylrn/modules/kiro-cli/scripts/start.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env bash -# Start script for kiro-cli module - -set -o errexit -set -o pipefail - -command_exists() { - command -v "$1" > /dev/null 2>&1 -} - -# Decode inputs -ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d) -ARG_TRUST_ALL_TOOLS=${ARG_TRUST_ALL_TOOLS:-true} -ARG_MODULE_DIR_NAME=${ARG_MODULE_DIR_NAME:-.kiro} -ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"} -ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true} -ARG_SERVER_PARAMETERS=${ARG_SERVER_PARAMETERS:-""} - -echo "--------------------------------" -echo "ai_prompt: $ARG_AI_PROMPT" -echo "trust_all_tools: $ARG_TRUST_ALL_TOOLS" -echo "module_dir_name: $ARG_MODULE_DIR_NAME" -echo "workdir: $ARG_WORKDIR" -echo "report_tasks: ${ARG_REPORT_TASKS}" -echo "--------------------------------" - -mkdir -p "$HOME/$ARG_MODULE_DIR_NAME" - -# Find Kiro CLI -if command_exists kiro-cli; then - KIRO_CMD=kiro-cli -elif [ -x "$HOME/.local/bin/kiro-cli" ]; then - KIRO_CMD="$HOME/.local/bin/kiro-cli" -else - echo "Error: Kiro CLI not found. Install it or set install_kiro_cli=true." - exit 1 -fi - -mkdir -p "$ARG_WORKDIR" -cd "$ARG_WORKDIR" - -# Set up environment -export LANG=en_US.UTF-8 -export LC_ALL=en_US.UTF-8 - -# Build command arguments -ARGS=(chat) - -if [ "$ARG_TRUST_ALL_TOOLS" = "true" ]; then - ARGS+=(--trust-all-tools) -fi - -# Log and run with agentapi integration -printf "Running: %q %s\n" "$KIRO_CMD" "$(printf '%q ' "${ARGS[@]}")" - -# If we have an AI prompt, we need to handle it specially -if [ -n "$ARG_AI_PROMPT" ]; then - if [ "$ARG_REPORT_TASKS" == "true" ]; then - PROMPT="Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_AI_PROMPT" - else - PROMPT="$ARG_AI_PROMPT" - fi - ARGS+=("$PROMPT") -fi - -# Use agentapi to manage the interactive session with initial prompt -agentapi server ${ARG_SERVER_PARAMETERS} --term-width 67 --term-height 1190 -- "$KIRO_CMD" "${ARGS[@]}" diff --git a/registry/harleylrn/modules/kiro-cli/templates/agent-config.json.tpl b/registry/harleylrn/modules/kiro-cli/templates/agent-config.json.tpl deleted file mode 100644 index 550fbf4209..0000000000 --- a/registry/harleylrn/modules/kiro-cli/templates/agent-config.json.tpl +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "agent", - "description": "This is an default agent config", - "prompt": ${system_prompt}, - "mcpServers": {}, - "tools": [ - "read", - "write", - "shell", - "aws", - "@coder", - "knowledge" - ], - "toolAliases": {}, - "allowedTools": [ - "read", - "@coder" - ], - "resources": [ - "file://KiroQ.md", - "file://README.md", - "file://.kiro/steering/**/*.md" - ], - "hooks": {}, - "toolsSettings": {}, - "useLegacyMcpJson": true -} diff --git a/registry/harleylrn/modules/kiro-cli/testdata/kiro-cli-mock.sh b/registry/harleylrn/modules/kiro-cli/testdata/kiro-cli-mock.sh new file mode 100644 index 0000000000..728708a3fc --- /dev/null +++ b/registry/harleylrn/modules/kiro-cli/testdata/kiro-cli-mock.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +if [[ "$1" == "--version" ]]; then + echo "kiro-cli 2.26.0" + exit 0 +fi + +echo "kiro-cli invoked with: $*" +exit 0