diff --git a/registry/coder-labs/modules/opencode/README.md b/registry/coder-labs/modules/opencode/README.md new file mode 100644 index 000000000..c06c12715 --- /dev/null +++ b/registry/coder-labs/modules/opencode/README.md @@ -0,0 +1,108 @@ +--- +display_name: OpenCode +icon: ../../../../.icons/opencode.svg +description: Run OpenCode AI coding assistant for AI-powered terminal assistance +verified: false +tags: [agent, opencode, ai, tasks] +--- + +# OpenCode + +Run [OpenCode](https://opencode.ai) AI coding assistant in your workspace for intelligent code generation, analysis, and development assistance. This module integrates with [AgentAPI](https://github.com/coder/agentapi) for seamless task reporting in the Coder UI. + +```tf +module "opencode" { + source = "registry.coder.com/coder-labs/opencode/coder" + version = "0.1.0" + agent_id = coder_agent.example.id + workdir = "/home/coder/project" +} +``` + +## Prerequisites + +- **Authentication credentials** - OpenCode auth.json file is required for non-interactive authentication, you can find this file on your system: `$HOME/.local/share/opencode/auth.json` + +## Examples + +### Basic Usage with Tasks + +```tf +resource "coder_ai_task" "task" { + app_id = module.opencode.task_app_id +} + +module "opencode" { + source = "registry.coder.com/coder-labs/opencode/coder" + version = "0.1.0" + agent_id = coder_agent.example.id + workdir = "/home/coder/project" + + ai_prompt = coder_ai_task.task.prompt + + auth_json = <<-EOT +{ + "google": { + "type": "api", + "key": "gem-xxx-xxxx" + }, + "anthropic": { + "type": "api", + "key": "sk-ant-api03-xxx-xxxxxxx" + } +} +EOT + + config_json = jsonencode({ + "$schema" = "https://opencode.ai/config.json" + mcp = { + filesystem = { + command = ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/home/coder/projects"] + enabled = true + type = "local" + environment = { + SOME_VARIABLE_X = "value" + } + } + playwright = { + command = ["npx", "-y", "@playwright/mcp@latest", "--headless", "--isolated"] + enabled = true + type = "local" + } + } + model = "anthropic/claude-sonnet-4-20250514" + }) + + pre_install_script = <<-EOT + #!/bin/bash + curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - + sudo apt-get install -y nodejs + EOT +} +``` + +### Standalone CLI Mode + +Run OpenCode as a command-line tool without web interface or task reporting: + +```tf +module "opencode" { + source = "registry.coder.com/coder-labs/opencode/coder" + version = "0.1.0" + agent_id = coder_agent.example.id + workdir = "/home/coder" + report_tasks = false + cli_app = true +} +``` + +## Troubleshooting + +If you encounter any issues, check the log files in the `~/.opencode-module` directory within your workspace for detailed information. + +## References + +- [Opencode JSON Config](https://opencode.ai/docs/config/) +- [OpenCode Documentation](https://opencode.ai/docs) +- [AgentAPI Documentation](https://github.com/coder/agentapi) +- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents) diff --git a/registry/coder-labs/modules/opencode/main.test.ts b/registry/coder-labs/modules/opencode/main.test.ts new file mode 100644 index 000000000..dec42a592 --- /dev/null +++ b/registry/coder-labs/modules/opencode/main.test.ts @@ -0,0 +1,362 @@ +import { + test, + afterEach, + describe, + setDefaultTimeout, + beforeAll, + expect, +} from "bun:test"; +import { execContainer, readFileContainer, runTerraformInit } from "~test"; +import { + loadTestFile, + writeExecutable, + setup as setupUtil, + execModuleScript, + expectAgentAPIStarted, +} from "../../../coder/modules/agentapi/test-util"; +import dedent from "dedent"; + +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); + } + } +}); + +interface SetupProps { + skipAgentAPIMock?: boolean; + skipOpencodeMock?: boolean; + moduleVariables?: Record; + agentapiMockScript?: string; +} + +const setup = async (props?: SetupProps): Promise<{ id: string }> => { + const projectDir = "/home/coder/project"; + const { id } = await setupUtil({ + moduleDir: import.meta.dir, + moduleVariables: { + install_opencode: props?.skipOpencodeMock ? "true" : "false", + install_agentapi: props?.skipAgentAPIMock ? "true" : "false", + workdir: projectDir, + ...props?.moduleVariables, + }, + registerCleanup, + projectDir, + skipAgentAPIMock: props?.skipAgentAPIMock, + agentapiMockScript: props?.agentapiMockScript, + }); + if (!props?.skipOpencodeMock) { + await writeExecutable({ + containerId: id, + filePath: "/usr/bin/opencode", + content: await loadTestFile(import.meta.dir, "opencode-mock.sh"), + }); + } + return { id }; +}; + +setDefaultTimeout(60 * 1000); + +describe("opencode", async () => { + beforeAll(async () => { + await runTerraformInit(import.meta.dir); + }); + + test("happy-path", async () => { + const { id } = await setup(); + await execModuleScript(id); + await expectAgentAPIStarted(id); + }); + + test("install-opencode-version", async () => { + const version_to_install = "0.1.0"; + const { id } = await setup({ + skipOpencodeMock: true, + moduleVariables: { + install_opencode: "true", + opencode_version: version_to_install, + pre_install_script: dedent` + #!/usr/bin/env bash + set -euo pipefail + + # Mock the opencode install for testing + mkdir -p /home/coder/.opencode/bin + echo '#!/bin/bash\necho "opencode mock version ${version_to_install}"' > /home/coder/.opencode/bin/opencode + chmod +x /home/coder/.opencode/bin/opencode + `, + }, + }); + await execModuleScript(id); + const resp = await execContainer(id, [ + "bash", + "-c", + `cat /home/coder/.opencode-module/install.log`, + ]); + expect(resp.stdout).toContain(version_to_install); + }); + + test("check-latest-opencode-version-works", async () => { + const { id } = await setup({ + skipOpencodeMock: true, + skipAgentAPIMock: true, + moduleVariables: { + install_opencode: "true", + pre_install_script: dedent` + #!/usr/bin/env bash + set -euo pipefail + + # Mock the opencode install for testing + mkdir -p /home/coder/.opencode/bin + echo '#!/bin/bash\necho "opencode mock latest version"' > /home/coder/.opencode/bin/opencode + chmod +x /home/coder/.opencode/bin/opencode + `, + }, + }); + await execModuleScript(id); + await expectAgentAPIStarted(id); + }); + + test("opencode-auth-json", async () => { + const authJson = JSON.stringify({ + token: "test-auth-token-123", + user: "test-user", + }); + const { id } = await setup({ + moduleVariables: { + auth_json: authJson, + }, + }); + await execModuleScript(id); + + const authFile = await readFileContainer( + id, + "/home/coder/.local/share/opencode/auth.json", + ); + + expect(authFile).toContain("test-auth-token-123"); + expect(authFile).toContain("test-user"); + }); + + test("opencode-config-json", async () => { + const configJson = JSON.stringify({ + $schema: "https://opencode.ai/config.json", + mcp: { + test: { + command: ["test-cmd"], + type: "local", + }, + }, + model: "anthropic/claude-sonnet-4-20250514", + }); + const { id } = await setup({ + moduleVariables: { + config_json: configJson, + }, + }); + await execModuleScript(id); + + const configFile = await readFileContainer( + id, + "/home/coder/.config/opencode/opencode.json", + ); + expect(configFile).toContain("test-cmd"); + expect(configFile).toContain("anthropic/claude-sonnet-4-20250514"); + }); + + test("opencode-ai-prompt", async () => { + const prompt = "This is a task prompt for OpenCode."; + const { id } = await setup({ + moduleVariables: { + ai_prompt: prompt, + }, + }); + await execModuleScript(id); + + const resp = await execContainer(id, [ + "bash", + "-c", + `cat /home/coder/.opencode-module/agentapi-start.log`, + ]); + expect(resp.stdout).toContain(prompt); + }); + + test("opencode-continue-flag", async () => { + const { id } = await setup({ + moduleVariables: { + continue: "true", + ai_prompt: "test prompt", + }, + }); + await execModuleScript(id); + + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.opencode-module/agentapi-start.log", + ]); + expect(startLog.stdout).toContain("--continue"); + }); + + test("opencode-continue-with-session-id", async () => { + const sessionId = "session-123"; + const { id } = await setup({ + moduleVariables: { + continue: "true", + session_id: sessionId, + ai_prompt: "test prompt", + }, + }); + await execModuleScript(id); + + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.opencode-module/agentapi-start.log", + ]); + expect(startLog.stdout).toContain("--continue"); + expect(startLog.stdout).toContain(`--session ${sessionId}`); + }); + + test("opencode-session-id", async () => { + const sessionId = "session-123"; + const { id } = await setup({ + moduleVariables: { + session_id: sessionId, + ai_prompt: "test prompt", + }, + }); + await execModuleScript(id); + + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.opencode-module/agentapi-start.log", + ]); + expect(startLog.stdout).toContain(`--session ${sessionId}`); + }); + + test("opencode-report-tasks-enabled", async () => { + const { id } = await setup({ + moduleVariables: { + report_tasks: "true", + ai_prompt: "test prompt", + }, + }); + await execModuleScript(id); + + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.opencode-module/agentapi-start.log", + ]); + expect(startLog.stdout).toContain( + "report your progress using coder_report_task", + ); + }); + + test("opencode-report-tasks-disabled", async () => { + const { id } = await setup({ + moduleVariables: { + report_tasks: "false", + ai_prompt: "test prompt", + }, + }); + await execModuleScript(id); + + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.opencode-module/agentapi-start.log", + ]); + expect(startLog.stdout).not.toContain( + "report your progress using coder_report_task", + ); + }); + + test("cli-app-creation", async () => { + const { id } = await setup({ + moduleVariables: { + cli_app: "true", + cli_app_display_name: "OpenCode Terminal", + }, + }); + await execModuleScript(id); + // CLI app creation is handled by the agentapi module + // We just verify the setup completed successfully + await expectAgentAPIStarted(id); + }); + + test("pre-post-install-scripts", async () => { + const { id } = await setup({ + moduleVariables: { + pre_install_script: "#!/bin/bash\necho 'opencode-pre-install-script'", + post_install_script: "#!/bin/bash\necho 'opencode-post-install-script'", + }, + }); + await execModuleScript(id); + + const preInstallLog = await readFileContainer( + id, + "/home/coder/.opencode-module/pre_install.log", + ); + expect(preInstallLog).toContain("opencode-pre-install-script"); + + const postInstallLog = await readFileContainer( + id, + "/home/coder/.opencode-module/post_install.log", + ); + expect(postInstallLog).toContain("opencode-post-install-script"); + }); + + test("workdir-variable", async () => { + const workdir = "/home/coder/opencode-test-folder"; + const { id } = await setup({ + skipOpencodeMock: false, + moduleVariables: { + workdir, + }, + }); + await execModuleScript(id); + + const resp = await readFileContainer( + id, + "/home/coder/.opencode-module/agentapi-start.log", + ); + expect(resp).toContain(workdir); + }); + + test("subdomain-enabled", async () => { + const { id } = await setup({ + moduleVariables: { + subdomain: "true", + }, + }); + await execModuleScript(id); + // Subdomain configuration is handled by the agentapi module + // We just verify the setup completed successfully + await expectAgentAPIStarted(id); + }); + + test("custom-display-names", async () => { + const { id } = await setup({ + moduleVariables: { + web_app_display_name: "Custom OpenCode Web", + cli_app_display_name: "Custom OpenCode CLI", + cli_app: "true", + }, + }); + await execModuleScript(id); + // Display names are handled by the agentapi module + // We just verify the setup completed successfully + await expectAgentAPIStarted(id); + }); +}); diff --git a/registry/coder-labs/modules/opencode/main.tf b/registry/coder-labs/modules/opencode/main.tf new file mode 100644 index 000000000..cedf3da9b --- /dev/null +++ b/registry/coder-labs/modules/opencode/main.tf @@ -0,0 +1,203 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.12" + } + } +} + +variable "agent_id" { + type = string + 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." + default = "/icon/opencode.svg" +} + +variable "workdir" { + type = string + description = "The folder to run OpenCode in." +} + +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 OpenCode" + default = false +} + +variable "web_app_display_name" { + type = string + description = "Display name for the web app" + default = "OpenCode" +} + +variable "cli_app_display_name" { + type = string + description = "Display name for the CLI app" + default = "OpenCode CLI" +} + +variable "pre_install_script" { + type = string + description = "Custom script to run before installing OpenCode." + default = null +} + +variable "post_install_script" { + type = string + description = "Custom script to run after installing OpenCode." + default = null +} + +variable "install_agentapi" { + type = bool + description = "Whether to install AgentAPI." + default = true +} + +variable "agentapi_version" { + type = string + description = "The version of AgentAPI to install." + default = "v0.10.0" +} + +variable "ai_prompt" { + type = string + description = "Initial task prompt for OpenCode." + default = "" +} + +variable "subdomain" { + type = bool + description = "Whether to use a subdomain for AgentAPI." + default = false +} + +variable "install_opencode" { + type = bool + description = "Whether to install OpenCode." + default = true +} + +variable "opencode_version" { + type = string + description = "The version of OpenCode to install." + default = "latest" +} + +variable "continue" { + type = bool + description = "continue the last session. Uses the --continue flag" + default = false +} + +variable "session_id" { + type = string + description = "Session id to continue. Passed via --session" + default = "" +} + +variable "auth_json" { + type = string + description = "Your auth.json from $HOME/.local/share/opencode/auth.json, Required for non-interactive authentication" + default = "" +} + +variable "config_json" { + type = string + description = "OpenCode JSON config. https://opencode.ai/docs/config/" + default = "" +} + +locals { + workdir = trimsuffix(var.workdir, "/") + app_slug = "opencode" + install_script = file("${path.module}/scripts/install.sh") + start_script = file("${path.module}/scripts/start.sh") + module_dir_name = ".opencode-module" +} + +module "agentapi" { + source = "registry.coder.com/coder/agentapi/coder" + version = "2.0.0" + + agent_id = var.agent_id + 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 + agentapi_subdomain = var.subdomain + folder = local.workdir + 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 + #!/bin/bash + set -o errexit + set -o pipefail + echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh + chmod +x /tmp/start.sh + + ARG_WORKDIR='${local.workdir}' \ + ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \ + ARG_SESSION_ID='${var.session_id}' \ + ARG_REPORT_TASKS='${var.report_tasks}' \ + ARG_CONTINUE='${var.continue}' \ + /tmp/start.sh + EOT + + install_script = <<-EOT + #!/bin/bash + set -o errexit + set -o pipefail + + echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh + chmod +x /tmp/install.sh + ARG_OPENCODE_VERSION='${var.opencode_version}' \ + ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' \ + ARG_INSTALL_OPENCODE='${var.install_opencode}' \ + ARG_REPORT_TASKS='${var.report_tasks}' \ + ARG_WORKDIR='${local.workdir}' \ + ARG_AUTH_JSON='${var.auth_json != null ? base64encode(replace(var.auth_json, "'", "'\\''")) : ""}' \ + ARG_OPENCODE_CONFIG='${var.config_json != null ? base64encode(replace(var.config_json, "'", "'\\''")) : ""}' \ + /tmp/install.sh + EOT +} + +output "task_app_id" { + value = module.agentapi.task_app_id +} diff --git a/registry/coder-labs/modules/opencode/main.tftest.hcl b/registry/coder-labs/modules/opencode/main.tftest.hcl new file mode 100644 index 000000000..c0b4b4d61 --- /dev/null +++ b/registry/coder-labs/modules/opencode/main.tftest.hcl @@ -0,0 +1,374 @@ +run "defaults_are_correct" { + command = plan + + variables { + agent_id = "test-agent" + workdir = "/home/coder/project" + } + + assert { + condition = var.install_opencode == true + error_message = "OpenCode installation should be enabled by default" + } + + assert { + condition = var.install_agentapi == true + error_message = "AgentAPI installation should be enabled by default" + } + + assert { + condition = var.agentapi_version == "v0.10.0" + error_message = "Default AgentAPI version should be 'v0.10.0'" + } + + assert { + condition = var.opencode_version == "latest" + error_message = "Default OpenCode version should be 'latest'" + } + + assert { + condition = var.report_tasks == true + error_message = "Task reporting should be enabled by default" + } + + assert { + condition = var.cli_app == false + error_message = "CLI app should be disabled by default" + } + + assert { + condition = var.subdomain == false + error_message = "Subdomain should be disabled by default" + } + + assert { + condition = var.web_app_display_name == "OpenCode" + error_message = "Default web app display name should be 'OpenCode'" + } + + assert { + condition = var.cli_app_display_name == "OpenCode CLI" + error_message = "Default CLI app display name should be 'OpenCode CLI'" + } + + assert { + condition = local.app_slug == "opencode" + error_message = "App slug should be 'opencode'" + } + + assert { + condition = local.module_dir_name == ".opencode-module" + error_message = "Module dir name should be '.opencode-module'" + } + + assert { + condition = local.workdir == "/home/coder/project" + error_message = "Workdir should be trimmed of trailing slash" + } + + assert { + condition = var.continue == false + error_message = "Continue flag should be disabled by default" + } +} + +run "workdir_trailing_slash_trimmed" { + command = plan + + variables { + agent_id = "test-agent" + workdir = "/home/coder/project/" + } + + assert { + condition = local.workdir == "/home/coder/project" + error_message = "Workdir should be trimmed of trailing slash" + } +} + +run "opencode_version_configuration" { + command = plan + + variables { + agent_id = "test-agent" + workdir = "/home/coder/project" + opencode_version = "v1.0.0" + } + + assert { + condition = var.opencode_version == "v1.0.0" + error_message = "OpenCode version should be set correctly" + } +} + +run "agentapi_version_configuration" { + command = plan + + variables { + agent_id = "test-agent" + workdir = "/home/coder/project" + agentapi_version = "v0.9.0" + } + + assert { + condition = var.agentapi_version == "v0.9.0" + error_message = "AgentAPI version should be set correctly" + } +} + +run "cli_app_configuration" { + command = plan + + variables { + agent_id = "test-agent" + workdir = "/home/coder/project" + cli_app = true + cli_app_display_name = "Custom OpenCode CLI" + } + + assert { + condition = var.cli_app == true + error_message = "CLI app should be enabled when specified" + } + + assert { + condition = var.cli_app_display_name == "Custom OpenCode CLI" + error_message = "Custom CLI app display name should be set" + } +} + +run "web_app_configuration" { + command = plan + + variables { + agent_id = "test-agent" + workdir = "/home/coder/project" + web_app_display_name = "Custom OpenCode Web" + order = 5 + group = "AI Tools" + icon = "/custom/icon.svg" + } + + assert { + condition = var.web_app_display_name == "Custom OpenCode Web" + error_message = "Custom web app display name should be set" + } + + assert { + condition = var.order == 5 + error_message = "Custom order should be set" + } + + assert { + condition = var.group == "AI Tools" + error_message = "Custom group should be set" + } + + assert { + condition = var.icon == "/custom/icon.svg" + error_message = "Custom icon should be set" + } +} + +run "ai_configuration_variables" { + command = plan + + variables { + agent_id = "test-agent" + workdir = "/home/coder/project" + ai_prompt = "This is a test prompt" + session_id = "session-123" + continue = true + } + + assert { + condition = var.ai_prompt == "This is a test prompt" + error_message = "AI prompt should be set correctly" + } + + assert { + condition = var.session_id == "session-123" + error_message = "Session ID should be set correctly" + } + + assert { + condition = var.continue == true + error_message = "Continue flag should be set correctly" + } +} + +run "auth_json_configuration" { + command = plan + + variables { + agent_id = "test-agent" + workdir = "/home/coder/project" + auth_json = "{\"token\": \"test-token\", \"user\": \"test-user\"}" + } + + assert { + condition = var.auth_json != "" + error_message = "Auth JSON should be set" + } + + assert { + condition = can(jsondecode(var.auth_json)) + error_message = "Auth JSON should be valid JSON" + } +} + +run "config_json_configuration" { + command = plan + + variables { + agent_id = "test-agent" + workdir = "/home/coder/project" + config_json = "{\"$schema\": \"https://opencode.ai/config.json\", \"mcp\": {\"test\": {\"command\": [\"test-cmd\"], \"type\": \"local\"}}, \"model\": \"anthropic/claude-sonnet-4-20250514\"}" + } + + assert { + condition = var.config_json != "" + error_message = "OpenCode JSON configuration should be set" + } + + assert { + condition = can(jsondecode(var.config_json)) + error_message = "OpenCode JSON configuration should be valid JSON" + } +} + +run "task_reporting_configuration" { + command = plan + + variables { + agent_id = "test-agent" + workdir = "/home/coder/project" + report_tasks = false + } + + assert { + condition = var.report_tasks == false + error_message = "Task reporting should be disabled when specified" + } +} + +run "subdomain_configuration" { + command = plan + + variables { + agent_id = "test-agent" + workdir = "/home/coder/project" + subdomain = true + } + + assert { + condition = var.subdomain == true + error_message = "Subdomain should be enabled when specified" + } +} + +run "install_flags_configuration" { + command = plan + + variables { + agent_id = "test-agent" + workdir = "/home/coder/project" + install_opencode = false + install_agentapi = false + } + + assert { + condition = var.install_opencode == false + error_message = "OpenCode installation should be disabled when specified" + } + + assert { + condition = var.install_agentapi == false + error_message = "AgentAPI installation should be disabled when specified" + } +} + +run "custom_scripts_configuration" { + command = plan + + variables { + agent_id = "test-agent" + workdir = "/home/coder/project" + pre_install_script = "#!/bin/bash\necho 'pre-install'" + post_install_script = "#!/bin/bash\necho 'post-install'" + } + + assert { + condition = var.pre_install_script != null + error_message = "Pre-install script should be set" + } + + assert { + condition = var.post_install_script != null + error_message = "Post-install script should be set" + } + + assert { + condition = can(regex("pre-install", var.pre_install_script)) + error_message = "Pre-install script should contain expected content" + } + + assert { + condition = can(regex("post-install", var.post_install_script)) + error_message = "Post-install script should contain expected content" + } +} + +run "empty_variables_handled_correctly" { + command = plan + + variables { + agent_id = "test-agent" + workdir = "/home/coder/project" + ai_prompt = "" + session_id = "" + auth_json = "" + config_json = "" + continue = false + } + + assert { + condition = var.ai_prompt == "" + error_message = "Empty AI prompt should be handled correctly" + } + + assert { + condition = var.session_id == "" + error_message = "Empty session ID should be handled correctly" + } + + assert { + condition = var.auth_json == "" + error_message = "Empty auth JSON should be handled correctly" + } + + assert { + condition = var.config_json == "" + error_message = "Empty config JSON should be handled correctly" + } + + assert { + condition = var.continue == false + error_message = "Continue flag default should be handled correctly" + } +} + +run "continue_flag_configuration" { + command = plan + + variables { + agent_id = "test-agent" + workdir = "/home/coder/project" + continue = true + } + + assert { + condition = var.continue == true + error_message = "Continue flag should be enabled when specified" + } +} \ No newline at end of file diff --git a/registry/coder-labs/modules/opencode/scripts/install.sh b/registry/coder-labs/modules/opencode/scripts/install.sh new file mode 100755 index 000000000..6d5531082 --- /dev/null +++ b/registry/coder-labs/modules/opencode/scripts/install.sh @@ -0,0 +1,131 @@ +#!/bin/bash +set -euo pipefail + +command_exists() { + command -v "$1" > /dev/null 2>&1 +} + +ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"} +ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true} +ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-} +ARG_OPENCODE_VERSION=${ARG_OPENCODE_VERSION:-latest} +ARG_INSTALL_OPENCODE=${ARG_INSTALL_OPENCODE:-true} +ARG_AUTH_JSON=$(echo -n "$ARG_AUTH_JSON" | base64 -d 2> /dev/null || echo "") +ARG_OPENCODE_CONFIG=$(echo -n "$ARG_OPENCODE_CONFIG" | base64 -d 2> /dev/null || echo "") + +# Print all received environment variables +printf "=== INSTALL CONFIG ===\n" +printf "ARG_WORKDIR: %s\n" "$ARG_WORKDIR" +printf "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS" +printf "ARG_MCP_APP_STATUS_SLUG: %s\n" "$ARG_MCP_APP_STATUS_SLUG" +printf "ARG_OPENCODE_VERSION: %s\n" "$ARG_OPENCODE_VERSION" +printf "ARG_INSTALL_OPENCODE: %s\n" "$ARG_INSTALL_OPENCODE" +if [ -n "$ARG_AUTH_JSON" ]; then + printf "ARG_AUTH_JSON: [AUTH DATA RECEIVED]\n" +else + printf "ARG_AUTH_JSON: [NOT PROVIDED]\n" +fi +if [ -n "$ARG_OPENCODE_CONFIG" ]; then + printf "ARG_OPENCODE_CONFIG: [RECEIVED]\n" +else + printf "ARG_OPENCODE_CONFIG: [NOT PROVIDED]\n" +fi +printf "==================================\n" + +install_opencode() { + if [ "$ARG_INSTALL_OPENCODE" = "true" ]; then + if ! command_exists opencode; then + echo "Installing OpenCode (version: ${ARG_OPENCODE_VERSION})..." + if [ "$ARG_OPENCODE_VERSION" = "latest" ]; then + curl -fsSL https://opencode.ai/install | bash + else + VERSION=$ARG_OPENCODE_VERSION curl -fsSL https://opencode.ai/install | bash + fi + export PATH=/home/coder/.opencode/bin:$PATH + printf "Opencode location: %s\n" "$(which opencode)" + if ! command_exists opencode; then + echo "ERROR: Failed to install OpenCode" + exit 1 + fi + echo "OpenCode installed successfully" + else + echo "OpenCode already installed" + fi + else + echo "OpenCode installation skipped (ARG_INSTALL_OPENCODE=false)" + fi +} + +setup_opencode_config() { + local opencode_config_file="$HOME/.config/opencode/opencode.json" + local auth_json_file="$HOME/.local/share/opencode/auth.json" + + mkdir -p "$(dirname "$auth_json_file")" + mkdir -p "$(dirname "$opencode_config_file")" + + setup_opencode_auth "$auth_json_file" + + if [ -n "$ARG_OPENCODE_CONFIG" ]; then + echo "Writing to the config file" + echo "$ARG_OPENCODE_CONFIG" > "$opencode_config_file" + fi + + if [ "$ARG_REPORT_TASKS" = "true" ]; then + setup_coder_mcp_server "$opencode_config_file" + fi + + echo "MCP configuration completed: $opencode_config_file" +} + +setup_opencode_auth() { + local auth_json_file="$1" + + if [ -n "$ARG_AUTH_JSON" ]; then + echo "$ARG_AUTH_JSON" > "$auth_json_file" + printf "added auth json to %s" "$auth_json_file" + else + printf "auth json not provided" + fi +} + +setup_coder_mcp_server() { + local opencode_config_file="$1" + + # Set environment variables based on task reporting setting + echo "Configuring OpenCode task reporting" + export CODER_MCP_APP_STATUS_SLUG="$ARG_MCP_APP_STATUS_SLUG" + export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284" + echo "Coder integration configured for task reporting" + + # Add coder MCP server configuration to the JSON file + echo "Adding Coder MCP server configuration" + + # Create the coder server configuration JSON + coder_config=$( + cat << EOF +{ + "type": "local", + "command": ["coder", "exp", "mcp", "server"], + "enabled": true, + "environment": { + "CODER_MCP_APP_STATUS_SLUG": "${CODER_MCP_APP_STATUS_SLUG:-}", + "CODER_MCP_AI_AGENTAPI_URL": "${CODER_MCP_AI_AGENTAPI_URL:-}", + "CODER_AGENT_URL": "${CODER_AGENT_URL:-}", + "CODER_AGENT_TOKEN": "${CODER_AGENT_TOKEN:-}", + "CODER_MCP_ALLOWED_TOOLS": "coder_report_task" + } +} +EOF + ) + + temp_file=$(mktemp) + jq --argjson coder_config "$coder_config" '.mcp.coder = $coder_config' "$opencode_config_file" > "$temp_file" + mv "$temp_file" "$opencode_config_file" + echo "Coder MCP server configuration added" + +} + +install_opencode +setup_opencode_config + +echo "OpenCode module setup completed." diff --git a/registry/coder-labs/modules/opencode/scripts/start.sh b/registry/coder-labs/modules/opencode/scripts/start.sh new file mode 100755 index 000000000..c3c51beb3 --- /dev/null +++ b/registry/coder-labs/modules/opencode/scripts/start.sh @@ -0,0 +1,71 @@ +#!/bin/bash +set -euo pipefail + +export PATH=/home/coder/.opencode/bin:$PATH + +command_exists() { + command -v "$1" > /dev/null 2>&1 +} + +ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"} +ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d 2> /dev/null || echo "") +ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true} +ARG_SESSION_ID=${ARG_SESSION_ID:-} +ARG_CONTINUE=${ARG_CONTINUE:-false} + +# Print all received environment variables +printf "=== START CONFIG ===\n" +printf "ARG_WORKDIR: %s\n" "$ARG_WORKDIR" +printf "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS" +printf "ARG_CONTINUE: %s\n" "$ARG_CONTINUE" +printf "ARG_SESSION_ID: %s\n" "$ARG_SESSION_ID" +if [ -n "$ARG_AI_PROMPT" ]; then + printf "ARG_AI_PROMPT: [AI PROMPT RECEIVED]\n" +else + printf "ARG_AI_PROMPT: [NOT PROVIDED]\n" +fi +printf "==================================\n" + +OPENCODE_ARGS=() +AGENTAPI_ARGS=() + +validate_opencode_installation() { + if ! command_exists opencode; then + printf "ERROR: OpenCode not installed. Set install_opencode to true\n" + exit 1 + fi +} + +build_opencode_args() { + + if [ -n "$ARG_SESSION_ID" ]; then + OPENCODE_ARGS+=(--session "$ARG_SESSION_ID") + fi + + if [ "$ARG_CONTINUE" = "true" ]; then + OPENCODE_ARGS+=(--continue) + fi + + 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 + AGENTAPI_ARGS+=(-I "$PROMPT") + fi +} + +start_agentapi() { + printf "Starting in directory: %s\n" "$ARG_WORKDIR" + cd "$ARG_WORKDIR" + + build_opencode_args + + printf "Running OpenCode with args: %s\n" "${OPENCODE_ARGS[*]}" + echo agentapi server "${AGENTAPI_ARGS[@]}" --type opencode --term-width 67 --term-height 1190 -- opencode "${OPENCODE_ARGS[@]}" + agentapi server "${AGENTAPI_ARGS[@]}" --type opencode --term-width 67 --term-height 1190 -- opencode "${OPENCODE_ARGS[@]}" +} + +validate_opencode_installation +start_agentapi diff --git a/registry/coder-labs/modules/opencode/testdata/opencode-mock.sh b/registry/coder-labs/modules/opencode/testdata/opencode-mock.sh new file mode 100644 index 000000000..dcde8756c --- /dev/null +++ b/registry/coder-labs/modules/opencode/testdata/opencode-mock.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# Mock OpenCode CLI for testing purposes +# This script simulates the OpenCode command-line interface + +echo "OpenCode Mock CLI - Test Version" +echo "Args received: $*" + +# Simulate opencode behavior based on arguments +case "$1" in + --version | -v) + echo "opencode mock version 0.1.0-test" + ;; + --help | -h) + echo "OpenCode Mock Help" + echo "Usage: opencode [options] [command]" + echo "This is a mock version for testing" + ;; + *) + echo "Running OpenCode mock with arguments: $*" + echo "Mock execution completed successfully" + ;; +esac + +exit 0