From 5a25bc8746ee759017396e1c71d307b0bba6826f Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Wed, 30 Sep 2026 13:00:55 +0000 Subject: [PATCH] refactor(registry/harleylrn/modules/kiro-cli)!: remove agentapi, tasks and start logic - Replace the agentapi module with coder-utils; drop Tasks/web-app inputs, the start script, and the task-reporting coder MCP server. - Install via the official installer for latest; pinned versions and mirrors use the release archive. - Make workdir optional; add mcp (merged into ~/.kiro/settings/mcp.json) and api_key (KIRO_API_KEY). - Keep agent_config and auth_tarball; the tarball is read from its env var instead of being rendered into the script. - Add scripts output, rewrite README and tests, bump to 2.0.0. --- registry/harleylrn/modules/kiro-cli/README.md | 397 +++----- .../modules/kiro-cli/kiro-cli.tftest.hcl | 372 ------- .../harleylrn/modules/kiro-cli/main.test.ts | 935 +++++++++--------- registry/harleylrn/modules/kiro-cli/main.tf | 262 ++--- .../modules/kiro-cli/main.tftest.hcl | 164 +++ .../modules/kiro-cli/scripts/install.sh | 159 --- .../modules/kiro-cli/scripts/install.sh.tftpl | 313 ++++++ .../modules/kiro-cli/scripts/start.sh | 67 -- .../kiro-cli/templates/agent-config.json.tpl | 27 - .../kiro-cli/testdata/kiro-cli-mock.sh | 9 + 10 files changed, 1132 insertions(+), 1573 deletions(-) delete mode 100644 registry/harleylrn/modules/kiro-cli/kiro-cli.tftest.hcl create mode 100644 registry/harleylrn/modules/kiro-cli/main.tftest.hcl delete mode 100644 registry/harleylrn/modules/kiro-cli/scripts/install.sh create mode 100644 registry/harleylrn/modules/kiro-cli/scripts/install.sh.tftpl delete mode 100644 registry/harleylrn/modules/kiro-cli/scripts/start.sh delete mode 100644 registry/harleylrn/modules/kiro-cli/templates/agent-config.json.tpl create mode 100644 registry/harleylrn/modules/kiro-cli/testdata/kiro-cli-mock.sh diff --git a/registry/harleylrn/modules/kiro-cli/README.md b/registry/harleylrn/modules/kiro-cli/README.md index bcba4815d..ef53ebf83 100644 --- a/registry/harleylrn/modules/kiro-cli/README.md +++ b/registry/harleylrn/modules/kiro-cli/README.md @@ -1,350 +1,179 @@ --- 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" { +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) +> [!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`. `workdir` is now optional. `trust_all_tools` is removed in favor of the `--trust-all-tools` flag in your launcher. `system_prompt` and the default agent template are removed: without `agent_config`, Kiro's built-in default agent is used. `kiro_install_url` now defaults to the official download host, and `auth_tarball` is no longer rendered into the install script. Keep using v1.x.x if you depend on Coder Tasks. -## Prerequisites +## Examples -- **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. - -**Step 3: Generate tarball** - -```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 -``` +module "kiro_cli" { + source = "registry.coder.com/harleylrn/kiro-cli/coder" + version = "2.0.0" + agent_id = coder_agent.main.id + workdir = local.kiro_workdir + auth_tarball = var.kiro_cli_auth_tarball +} -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 +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 + set -e + cd "${local.kiro_workdir}" + exec kiro-cli chat --trust-all-tools + EOT } ``` -### Configuration Details: +When `workdir` is set, the module creates it if missing. Pass `--model`, `--agent`, `--trust-all-tools`, `--trust-tools`, or any other [`kiro-cli chat` flag](https://kiro.dev/docs/reference/cli-commands/) in the launcher command. -- **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 +> [!CAUTION] +> `--trust-all-tools` lets Kiro CLI run any tool, including shell commands, without asking for confirmation. Use it only in trusted environments, or prefer `--trust-tools` with a specific list. -You can override this configuration by providing your own JSON via the `agent_config` variable. +> [!NOTE] +> The `coder_app` command re-executes on every pane reconnect. This works for interactive `kiro-cli chat`, but one-shot commands like `kiro-cli chat --no-interactive` will re-run each time. For one-shot prompts, use a `coder_script` (runs once at startup) and a `coder_app` that attaches to the existing session (for example, with tmux). -### 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 +### MCP servers and a custom agent ```tf -module "kiro-cli" { +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 + + mcp = jsonencode({ + mcpServers = { + playwright = { + command = "npx" + args = ["-y", "@playwright/mcp@latest", "--headless", "--isolated", "--no-sandbox"] + } + } + }) + + agent_config = jsonencode({ + name = "coder-agent" + description = "Coding agent for this workspace" + prompt = "You are a helpful coding assistant." + tools = ["read", "write", "shell"] + allowedTools = ["read"] + includeMcpJson = true + }) } ``` -This example will: +`mcp` is merged into the user-level `~/.kiro/settings/mcp.json`, so the servers apply to every Kiro CLI session in the workspace. Servers already in that file win on duplicate names, so edits made inside the workspace are never overwritten. -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 +`agent_config` is written to `~/.kiro/agents/.json` and set as `chat.defaultAgent` in `~/.kiro/settings/cli.json`; other settings are preserved. Custom agents only load servers from `mcp.json` when `includeMcpJson` is `true`. See the [agent configuration reference](https://kiro.dev/docs/custom-agents/configuration-reference/). -> [!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 +> [!NOTE] +> The Kiro CLI installer ships self-contained binaries and does not install Node.js. MCP servers whose `command` is `npx` or `uvx` need that runtime available in the workspace image, or installed with `pre_install_script`. -### With Custom AI Prompt +### 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 - ai_prompt = "Help me set up a Python FastAPI project with proper testing structure" - trust_all_tools = true +module "kiro_cli" { + 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" } ``` -> [!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 the defaults, the module runs the [official installer](https://cli.kiro.dev/install), which installs the latest stable release and verifies its checksum. When `kiro_cli_version` or `kiro_install_url` is set, the module downloads `//kirocli--linux.zip` (default host `https://prod.download.cli.kiro.dev/stable`) and runs the archive's `install.sh`. For a mirror, keep that layout and host both `kirocli-x86_64-linux.zip` and `kirocli-aarch64-linux.zip`. The archive path requires `unzip`. -### With Custom Pre/Post Install Scripts +The install is skipped when `kiro-cli` is already on `PATH` (and matches `kiro_cli_version` when pinned). If `install_kiro_cli = false`, a working `kiro-cli` must already be on `PATH`, or workspace startup fails. -```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 +### Serialize a downstream `coder_script` after the install pipeline - pre_install_script = <<-EOT - #!/usr/bin/env bash - echo "Setting up custom environment..." - # Install additional dependencies - sudo apt-get update && sudo apt-get install -y zstd - 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 +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 - kiro_cli_version = "1.14.0" # Specific version - install_kiro_cli = true +module "kiro_cli" { + source = "registry.coder.com/harleylrn/kiro-cli/coder" + version = "2.0.0" + agent_id = coder_agent.main.id } -``` -### Custom Agent Configuration - -```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 +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 - 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"] - } + kiro-cli --version EOT } ``` -### With Custom AgentAPI Configuration +## Authentication -```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" -} -``` +Kiro CLI uses an active browser login first, then the `KIRO_API_KEY` environment variable. See [Kiro authentication](https://kiro.dev/docs/getting-started/authentication/). -### Air-Gapped Installation +- **Auth tarball**: log in on another machine and pass the resulting data directory as `auth_tarball`. It is exported as `KIRO_CLI_AUTH_TARBALL` and extracted to `~/.local/share/kiro-cli` at startup, replacing that directory. The workspace needs `zstd`. +- **API key**: set `api_key` to export `KIRO_API_KEY`. Kiro documents API keys for non-interactive (headless) use and requires a Pro or higher subscription. +- **Device flow**: without either, run `kiro-cli login` in the workspace terminal. -For environments without direct internet access, you can host Kiro CLI installation files internally and configure the module to use your internal repository: - -```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 - - # Point to internal artifact repository - kiro_install_url = "https://artifacts.internal.corp/kiro-cli-releases" +Neither secret is rendered into the install script. To generate the tarball: - # Use specific version available in your repository - kiro_cli_version = "latest" -} +```bash +#!/usr/bin/env bash +kiro-cli login +cd ~/.local/share/kiro-cli +tar -c . | zstd | base64 -w 0 ``` -**Prerequisites for Air-Gapped Setup:** - -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 +> [!IMPORTANT] +> The tarball contains authentication credentials. Store it as a sensitive variable, give each user their own, and regenerate it after logging out or re-authenticating. ## Troubleshooting -### Common Issues - -**Authentication issues:** - -- Regenerate the auth tarball on your local machine -- Ensure the tarball is properly base64 encoded -- Check that the original authentication is still valid +Check the log files in `~/.coder-modules/harleylrn/kiro-cli/logs/` for detailed information, and run `kiro-cli doctor` in the workspace. -**MCP integration not working:** - -- 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 0f183a69e..000000000 --- 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 e0f9b28d5..f181430d2 100644 --- a/registry/harleylrn/modules/kiro-cli/main.test.ts +++ b/registry/harleylrn/modules/kiro-cli/main.test.ts @@ -1,531 +1,532 @@ -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, + }); + 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 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", - }; + 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 }; +}; - const state = await runTerraformApply(moduleDir, autonomousUsageVars); +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}`]); +}; - // 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"); +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}`); + } + } +}; - // 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 +// 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"), }); +}; - // 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 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); +}; - const state = await runTerraformApply(moduleDir, extendedConfigVars); +const moduleLogDir = "/home/coder/.coder-modules/harleylrn/kiro-cli/logs"; +const installLog = (id: string) => + readFileContainer(id, `${moduleLogDir}/install.log`); +const mcpConfigPath = "/home/coder/.kiro/settings/mcp.json"; +const cliSettingsPath = "/home/coder/.kiro/settings/cli.json"; - // 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"); +setDefaultTimeout(60 * 1000); - // 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 +describe("kiro-cli", async () => { + beforeAll(async () => { + await runTerraformInit(import.meta.dir); }); - // 1. Basic functionality test (replaces testRequiredVariables) - it("works with required variables", async () => { - const state = await runTerraformApply(moduleDir, requiredVars); - - // 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"); + 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"); }); - // 2. Environment variables are created correctly - it("creates required environment variables", async () => { - const state = await runTerraformApply(moduleDir, fullConfigVars); - - // 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"); + 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"); }); - // 3. Empty auth tarball handling - it("handles empty auth tarball correctly", async () => { - const noAuthVars = { - ...requiredVars, - auth_tarball: "", - }; - - const state = await runTerraformApply(moduleDir, noAuthVars); - - // 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(); + 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"); }); - // 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", + 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:\/\/prod\.download\.cli\.kiro\.dev\/stable\/2\.25\.0\/kirocli-(x86_64|aarch64)-linux\.zip/, ); - expect(statusSlugEnv).toBeDefined(); - expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG"); - expect(statusSlugEnv.value).toBe("kiro-cli"); + 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"); }); - // 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", + 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(); - expect(statusSlugEnv.value).toBe("kiro-cli"); }); - // 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("matching-installed-version-is-not-reinstalled", async () => { + const { id, scripts } = await setup({ + moduleVariables: { install_kiro_cli: "true", kiro_cli_version: "2.26.0" }, + }); + await writeExecutable({ + containerId: id, + filePath: "/usr/local/bin/curl", + content: "#!/bin/bash\necho called > /tmp/kiro-curl-called\nexit 22\n", + }); + await runScripts(id, scripts); + const log = await installLog(id); + expect(log).toContain("Kiro CLI already installed (2.26.0)"); + const called = await execContainer(id, [ + "test", + "-e", + "/tmp/kiro-curl-called", + ]); + expect(called.exitCode).not.toBe(0); }); - // 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("installer-download-failure-is-terminal", async () => { + const { id, scripts } = await setup({ + skipKiroMock: true, + moduleVariables: { install_kiro_cli: "true" }, + }); + 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"); }); - // 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", - ); - 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); }); - // 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", - ); - expect(statusSlugEnv).toBeDefined(); + test("writes-mcp-servers-without-coder-server", async () => { + const { id, scripts } = await setup({ + moduleVariables: { + mcp: JSON.stringify({ + mcpServers: { + playwright: { command: "npx", args: ["-y", "@playwright/mcp"] }, + }, + }), + }, + }); + await runScripts(id, scripts); + const mcp = JSON.parse(await readFileContainer(id, mcpConfigPath)); + expect(mcp.mcpServers.playwright.command).toBe("npx"); + expect(mcp.mcpServers.coder).toBeUndefined(); }); - // 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("merges-mcp-config-existing-servers-win", async () => { + const { id, scripts } = await setup({ + moduleVariables: { + mcp: JSON.stringify({ + mcpServers: { + shared: { command: "module-command" }, + extra: { command: "extra-command" }, + }, + }), + }, }); - - 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(); + const seed = JSON.stringify({ + mcpServers: { + shared: { command: "existing-command" }, + seeded: { command: "seeded-command" }, + }, + }); + await execContainer(id, [ + "bash", + "-c", + `mkdir -p /home/coder/.kiro/settings && echo '${seed}' > ${mcpConfigPath}`, + ]); + await runScripts(id, scripts); + const mcp = JSON.parse(await readFileContainer(id, mcpConfigPath)); + expect(mcp.mcpServers.shared.command).toBe("existing-command"); + expect(mcp.mcpServers.seeded.command).toBe("seeded-command"); + expect(mcp.mcpServers.extra.command).toBe("extra-command"); }); - // 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("invalid-existing-mcp-config-is-backed-up", async () => { + const { id, scripts } = await setup({ + moduleVariables: { + mcp: JSON.stringify({ mcpServers: { extra: { command: "x" } } }), + }, + }); + await execContainer(id, [ + "bash", + "-c", + `mkdir -p /home/coder/.kiro/settings && echo '{ // jsonc' > ${mcpConfigPath}`, + ]); + await runScripts(id, scripts); + const mcp = JSON.parse(await readFileContainer(id, mcpConfigPath)); + expect(mcp.mcpServers.extra.command).toBe("x"); + const backup = await readFileContainer(id, `${mcpConfigPath}.bak`); + expect(backup).toContain("// jsonc"); }); - // 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) }, + }); + const seed = JSON.stringify({ + "chat.defaultModel": "user-picked-model", + "chat.defaultAgent": "old-agent", + }); + await execContainer(id, [ + "bash", + "-c", + `mkdir -p /home/coder/.kiro/settings && echo '${seed}' > ${cliSettingsPath}`, + ]); + await runScripts(id, scripts); + const agent = JSON.parse( + await readFileContainer(id, "/home/coder/.kiro/agents/coder-agent.json"), ); - expect(statusSlugEnv).toBeDefined(); + expect(agent).toEqual(agentConfig); + const settings = JSON.parse(await readFileContainer(id, cliSettingsPath)); + expect(settings["chat.defaultAgent"]).toBe("coder-agent"); + expect(settings["chat.defaultModel"]).toBe("user-picked-model"); }); - // 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 5af5e3197..f476bb969 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,170 +14,89 @@ 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 } variable "kiro_cli_version" { type = string - description = "The version of Kiro CLI to install." + description = "Kiro CLI version to install, for example '2.26.0'. 'latest' with the default kiro_install_url uses the official installer (https://cli.kiro.dev/install); anything else downloads //kirocli--linux.zip. See https://kiro.dev/docs/getting-started/installation/" default = "latest" -} - -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 + validation { + condition = can(regex("^(latest|[0-9]+\\.[0-9]+\\.[0-9]+)$", var.kiro_cli_version)) + error_message = "kiro_cli_version must be 'latest' or a semantic version such as '2.26.0'." + } } -variable "system_prompt" { +variable "kiro_install_url" { 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 hosting Kiro CLI release archives as //kirocli--linux.zip, for mirrors or air-gapped installs. Defaults to the official stable channel." + 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. Exported as KIRO_CLI_AUTH_TARBALL and extracted at install time; requires zstd in the workspace." + 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. Used when no browser login is active; the Kiro docs scope API keys to non-interactive (headless) use. See https://kiro.dev/docs/getting-started/authentication/" default = "" sensitive = true } variable "agent_config" { type = string - description = "Optional Agent configuration JSON for Kiro CLI." + description = "Optional custom agent configuration JSON. Written to ~/.kiro/agents/.json and set as chat.defaultAgent. See https://kiro.dev/docs/custom-agents/configuration-reference/" 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 + validation { + condition = var.agent_config == null || can(regex("^[A-Za-z0-9][A-Za-z0-9._-]*$", jsondecode(var.agent_config).name)) + error_message = "agent_config must be a JSON object whose name is a plain file name (letters, digits, '.', '_', '-')." + } } -# 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 +variable "mcp" { + type = string + description = "MCP servers as JSON in Kiro's mcp.json format ({\"mcpServers\": {...}}). Merged into the user-level ~/.kiro/settings/mcp.json; servers already on disk win on duplicate names. See https://kiro.dev/docs/mcp/configuration/" + default = "" + + validation { + condition = var.mcp == "" || can(jsondecode(var.mcp).mcpServers) + error_message = "mcp must be a JSON object with an mcpServers key." + } } -# 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 +104,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 = 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_MCP_CONFIG = var.mcp != "" ? base64encode(var.mcp) : "" }) - - # 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 000000000..0636b0077 --- /dev/null +++ b/registry/harleylrn/modules/kiro-cli/main.tftest.hcl @@ -0,0 +1,164 @@ +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 "invalid_version_fails" { + command = plan + + variables { + agent_id = "test-agent" + kiro_cli_version = "2.26.0'; rm -rf /" + } + + expect_failures = [ + var.kiro_cli_version, + ] +} + +run "invalid_mcp_fails" { + command = plan + + variables { + agent_id = "test-agent" + mcp = "{\"servers\": {}}" + } + + expect_failures = [ + var.mcp, + ] +} + +run "agent_config_requires_plain_name" { + command = plan + + variables { + agent_id = "test-agent" + agent_config = "{\"name\": \"../escape\"}" + } + + expect_failures = [ + var.agent_config, + ] +} + +run "agent_config_requires_name" { + command = plan + + variables { + agent_id = "test-agent" + agent_config = "{\"description\": \"no name\"}" + } + + expect_failures = [ + var.agent_config, + ] +} + +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 8f18ac034..000000000 --- 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 000000000..3f2ee0c7f --- /dev/null +++ b/registry/harleylrn/modules/kiro-cli/scripts/install.sh.tftpl @@ -0,0 +1,313 @@ +#!/usr/bin/env bash + +set -euo pipefail + +BOLD='\033[0;1m' + +command_exists() { + command -v "$1" > /dev/null 2>&1 +} + +ARG_INSTALL='${ARG_INSTALL}' +ARG_VERSION='${ARG_VERSION}' +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_MCP_CONFIG=$(echo -n '${ARG_MCP_CONFIG}' | base64 -d) + +# KIRO_HOME overrides ~/.kiro for global agents and settings: +# https://kiro.dev/docs/reference/settings/ +KIRO_DIR="$${KIRO_HOME:-$HOME/.kiro}" +KIRO_DATA_DIR="$HOME/.local/share/kiro-cli" +DEFAULT_ARCHIVE_URL="https://prod.download.cli.kiro.dev/stable" + +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 "--------------------------------" + +require_jq() { + if ! command_exists jq; then + echo "ERROR: 'jq' is required to $1 but was not found." >&2 + exit 1 + fi +} + +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 +} + +installed_kiro_version() { + kiro-cli --version 2> /dev/null | awk '{print $NF}' +} + +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 it + # only runs when kiro-cli is absent. + 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 + printf "%s Validated existing Kiro CLI\n" "$${BOLD}" + return + fi + + if command_exists kiro-cli; then + local current + current=$(installed_kiro_version) + if [ "$${ARG_VERSION}" = "latest" ] || [ "$${current}" = "$${ARG_VERSION}" ]; then + echo "Kiro CLI already installed ($${current})" + ensure_kiro_in_path + return + fi + echo "Kiro CLI $${current} is installed; installing $${ARG_VERSION}" + fi + + printf "%s Installing Kiro CLI\n" "$${BOLD}" + 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 + printf "%s Installed Kiro CLI\n" "$${BOLD}" +} + +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}" +} + +# Reads a JSON file into stdout, or '{}' when missing. An invalid file is +# backed up to .bak so the module never crashes on hand-edited JSONC. +read_json_or_empty() { + local file="$1" + if [ ! -f "$${file}" ]; then + echo '{}' + elif jq empty "$${file}" > /dev/null 2>&1; then + cat "$${file}" + else + cp "$${file}" "$${file}.bak" + echo "Warning: $${file} is not valid JSON; backed it up to $${file}.bak" >&2 + echo '{}' + fi +} + +# The module owns only .json and the chat.defaultAgent key; other +# agents and settings are left untouched. +configure_agent() { + if [ -z "$${ARG_AGENT_CONFIG}" ]; then + echo "No custom agent configured; using Kiro's built-in default agent." + return + fi + + require_jq "configure the custom agent" + + local agent_name agents_dir settings_file existing + agent_name=$(echo "$${ARG_AGENT_CONFIG}" | jq -r '.name') + agents_dir="$${KIRO_DIR}/agents" + settings_file="$${KIRO_DIR}/settings/cli.json" + + mkdir -p "$${agents_dir}" "$(dirname "$${settings_file}")" + echo "$${ARG_AGENT_CONFIG}" | jq '.' > "$${agents_dir}/$${agent_name}.json" + echo "Wrote agent: $${agents_dir}/$${agent_name}.json" + + existing=$(read_json_or_empty "$${settings_file}") + echo "$${existing}" | jq --arg name "$${agent_name}" '.["chat.defaultAgent"] = $name' > "$${settings_file}" + echo "Set chat.defaultAgent to $${agent_name} in $${settings_file}" +} + +# Servers already on disk win on duplicate names, matching 'kiro-cli mcp add' +# without --force, so interactive edits are never overwritten. +setup_mcp_config() { + local mcp_config_file="$${KIRO_DIR}/settings/mcp.json" + + if [ -z "$${ARG_MCP_CONFIG}" ]; then + echo "No custom MCP servers configured; leaving $${mcp_config_file} unchanged." + return + fi + + require_jq "configure MCP servers" + + mkdir -p "$(dirname "$${mcp_config_file}")" + echo "Merging MCP server configuration into $${mcp_config_file}" + + local existing custom + existing=$(read_json_or_empty "$${mcp_config_file}") + custom=$(echo "$${ARG_MCP_CONFIG}" | jq '.mcpServers // {}') + # Right operand wins in jq '+', so existing on-disk servers win on duplicates. + echo "$${existing}" | jq --argjson custom "$${custom}" \ + '.mcpServers = ($custom + (.mcpServers // {}))' > "$${mcp_config_file}" + echo "MCP configuration completed: $${mcp_config_file}" +} + +install_kiro_cli +setup_workdir +extract_auth_tarball +configure_agent +setup_mcp_config + +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 483e2fa19..000000000 --- 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 550fbf420..000000000 --- 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 000000000..728708a3f --- /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