diff --git a/registry/coder-labs/modules/sourcegraph-amp/README.md b/registry/coder-labs/modules/sourcegraph-amp/README.md index 5703faec8..4b716f51a 100644 --- a/registry/coder-labs/modules/sourcegraph-amp/README.md +++ b/registry/coder-labs/modules/sourcegraph-amp/README.md @@ -1,95 +1,140 @@ --- display_name: Amp icon: ../../../../.icons/sourcegraph-amp.svg -description: Sourcegraph's AI coding agent with deep codebase understanding and intelligent code search capabilities +description: Install and configure the Amp CLI in your workspace. verified: true -tags: [agent, sourcegraph, amp, ai, tasks] +tags: [agent, sourcegraph, amp, ai] --- -# Sourcegraph Amp CLI +# Amp -Run [Amp CLI](https://ampcode.com/) in your workspace to access Sourcegraph's AI-powered code search and analysis tools, with AgentAPI integration for seamless Coder Tasks support. +Install and configure the [Amp CLI](https://ampcode.com/docs/cli) in your workspace. ```tf -module "amp-cli" { - source = "registry.coder.com/coder-labs/sourcegraph-amp/coder" - version = "3.0.1" - agent_id = coder_agent.example.id - amp_api_key = var.amp_api_key - install_amp = true - agentapi_version = "latest" +module "amp" { + source = "registry.coder.com/coder-labs/sourcegraph-amp/coder" + version = "4.0.0" + agent_id = coder_agent.main.id + amp_api_key = var.amp_api_key } ``` -## Prerequisites +> [!WARNING] +> If upgrading from v3.x.x of this module: v4 is a major refactor that drops support for Coder Tasks and AgentAPI. The module now only installs and configures Amp; launch it with your own `coder_app`. `ai_prompt`, `mode`, `report_tasks`, `install_via_npm`, and the web/CLI app inputs are removed; pass `--mode` in your launcher instead. `base_amp_config` no longer writes default keys, and the Coder task-reporting MCP server is no longer added. Keep using v3.x.x if you depend on Coder Tasks. -- **Default (official installer)**: No prerequisites - the official installer includes its own runtime (Bun) -- **npm installation (`install_via_npm = true`)**: Requires Node.js and npm to be installed before Amp installation - - Required for Alpine Linux or other musl-based systems - - Ensure Node.js and npm are available in your workspace image or via earlier provisioning steps +## Examples -## Usage Example +### Standalone mode with a launcher app ```tf -data "coder_parameter" "ai_prompt" { - name = "AI Prompt" - description = "Write an initial prompt for Amp to work on." - type = "string" - default = "" - mutable = true +locals { + amp_workdir = "/home/coder/project" } -variable "amp_api_key" { - type = string - description = "Sourcegraph Amp API key. Get one at https://ampcode.com/settings" - sensitive = true +module "amp" { + source = "registry.coder.com/coder-labs/sourcegraph-amp/coder" + version = "4.0.0" + agent_id = coder_agent.main.id + workdir = local.amp_workdir + amp_api_key = var.amp_api_key } -module "amp-cli" { - count = data.coder_workspace.me.start_count - source = "registry.coder.com/coder-labs/sourcegraph-amp/coder" - amp_version = "3.0.0" - agent_id = coder_agent.example.id - amp_api_key = var.amp_api_key # recommended for tasks usage - workdir = "/home/coder/project" - instruction_prompt = <<-EOT - # Instructions - - Start every response with `amp > ` -EOT - ai_prompt = data.coder_parameter.ai_prompt.value +resource "coder_app" "amp" { + agent_id = coder_agent.main.id + slug = "amp" + display_name = "Amp" + icon = "/icon/sourcegraph-amp.svg" + open_in = "slim-window" + command = <<-EOT + #!/usr/bin/env bash + set -e + cd "${local.amp_workdir}" + exec amp --mode medium + EOT +} +``` + +See the [Amp CLI](https://ampcode.com/docs/cli) for launcher flags such as `--mode`. + +> [!NOTE] +> The `coder_app` command re-executes on every pane reconnect. This works for interactive `amp`, but one-shot commands like `amp -x` 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). + +### Settings, MCP servers, and guidance + +```tf +module "amp" { + source = "registry.coder.com/coder-labs/sourcegraph-amp/coder" + version = "4.0.0" + agent_id = coder_agent.main.id + workdir = "/home/coder/project" + amp_api_key = var.amp_api_key + base_amp_config = jsonencode({ - "amp.anthropic.thinking.enabled" = true - "amp.todos.enabled" = true - "amp.tools.stopTimeout" = 600 - "amp.git.commit.ampThread.enabled" = true - "amp.git.commit.coauthor.enabled" = true - "amp.terminal.commands.nodeSpawn.loadProfile" = "daily" + "amp.git.commit.coauthor.enabled" = true "amp.permissions" = [ - { "tool" : "mcp__coder__*", "action" : "allow" }, - { "tool" : "Bash", "action" : "allow", "context" : "thread" }, - { "tool" : "Bash", "matches" : { "cmd" : ["rm -rf /*", "rm -rf ~/*"] }, "action" : "reject", "context" : "subagent" }, - { "tool" : "edit_file", "action" : "allow" }, - { "tool" : "write_file", "action" : "allow" }, - { "tool" : "read_file", "action" : "allow" }, - { "tool" : "Grep", "action" : "allow" } + { tool = "Bash", action = "ask", matches = { cmd = ["git push*"] } } ] }) + + mcp = jsonencode({ + playwright = { + command = "npx" + args = ["-y", "@playwright/mcp@latest", "--headless", "--isolated", "--no-sandbox"] + } + }) + + instruction_prompt = <<-EOT + # Instructions + - Run the test suite before committing. + EOT } ``` +`base_amp_config` uses the [Amp settings](https://ampcode.com/docs/cli/settings) format and overwrites `~/.config/amp/settings.json` on every start. `mcp` uses the [`amp.mcpServers`](https://ampcode.com/docs/customize/mcp) format and is applied after it. MCP servers that already exist in `settings.json` are kept. `instruction_prompt` is written to `~/.config/AGENTS.md` ([AGENTS.md](https://ampcode.com/docs/customize/agents-md)). + +### Serialize a downstream `coder_script` after the install pipeline + +The module exposes the `scripts` output: an ordered list of `coder exp sync` names for the scripts this module creates (pre_install, install, post_install). Scripts that were not configured are absent. + +```tf +module "amp" { + source = "registry.coder.com/coder-labs/sourcegraph-amp/coder" + version = "4.0.0" + agent_id = coder_agent.main.id +} + +resource "coder_script" "post_amp" { + agent_id = coder_agent.main.id + display_name = "Run after Amp install" + run_on_start = true + script = <<-EOT + #!/usr/bin/env bash + set -euo pipefail + trap 'coder exp sync complete post-amp' EXIT + coder exp sync want post-amp ${join(" ", module.amp.scripts)} + coder exp sync start post-amp + + amp --version + EOT +} +``` + +## Authentication + +Set `amp_api_key` (exported as `AMP_API_KEY`) or run `amp login` in the workspace. See [Amp CLI](https://ampcode.com/docs/cli). + ## Troubleshooting -- If `amp` is not found, ensure `install_amp = true` and your API key is valid -- Logs are written under `/home/coder/.amp-module/` (`install.log`, `agentapi-start.log`) for debugging -- If AgentAPI fails to start, verify that your container has network access and executable permissions for the scripts +Check the log files in `~/.coder-modules/coder-labs/sourcegraph-amp/logs/` for detailed information. -> [!IMPORTANT] -> To use tasks with Amp CLI, create a `coder_parameter` named `"AI Prompt"` and pass its value to the amp-cli module's `ai_prompt` variable. The `folder` variable is required for the module to function correctly. -> For using **Coder Tasks** with Amp CLI, make sure to set `amp_api_key`. -> This ensures task reporting and status updates work seamlessly. +```bash +cat ~/.coder-modules/coder-labs/sourcegraph-amp/logs/install.log +cat ~/.coder-modules/coder-labs/sourcegraph-amp/logs/pre_install.log +cat ~/.coder-modules/coder-labs/sourcegraph-amp/logs/post_install.log +``` ## References -- [Amp CLI Documentation](https://ampcode.com/manual) -- [AgentAPI Documentation](https://github.com/coder/agentapi) -- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents) +- [Amp CLI documentation](https://ampcode.com/docs/cli) +- [Amp configuration](https://ampcode.com/docs/cli/settings) +- [Amp MCP](https://ampcode.com/docs/customize/mcp) diff --git a/registry/coder-labs/modules/sourcegraph-amp/main.test.ts b/registry/coder-labs/modules/sourcegraph-amp/main.test.ts index 105f73868..24ee1e601 100644 --- a/registry/coder-labs/modules/sourcegraph-amp/main.test.ts +++ b/registry/coder-labs/modules/sourcegraph-amp/main.test.ts @@ -6,14 +6,67 @@ import { beforeAll, expect, } from "bun:test"; -import { execContainer, readFileContainer, runTerraformInit } from "~test"; import { - loadTestFile, + execContainer, + readFileContainer, + removeContainer, + runContainer, + runTerraformApply, + runTerraformInit, + TerraformState, +} from "~test"; +import { + extractCoderEnvVars, writeExecutable, - setup as setupUtil, - execModuleScript, - expectAgentAPIStarted, } from "../../../coder/modules/agentapi/test-util"; +import path from "path"; + +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 = `Amp: ${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; +}; let cleanupFunctions: (() => Promise)[] = []; const registerCleanup = (cleanup: () => Promise) => { @@ -32,241 +85,323 @@ afterEach(async () => { }); interface SetupProps { - skipAgentAPIMock?: boolean; skipAmpMock?: 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: { - workdir: "/home/coder", - install_amp: props?.skipAmpMock ? "true" : "false", - install_agentapi: props?.skipAgentAPIMock ? "true" : "false", - ...props?.moduleVariables, - }, - registerCleanup, - projectDir, - skipAgentAPIMock: props?.skipAgentAPIMock, - agentapiMockScript: props?.agentapiMockScript, +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_amp: "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); }); - // Place the AMP mock CLI binary inside the container + await writeExecutable({ + containerId: id, + filePath: "/usr/bin/coder", + content: "#!/bin/bash\nexit 0\n", + }); if (!props?.skipAmpMock) { await writeExecutable({ containerId: id, filePath: "/usr/bin/amp", - content: await loadTestFile(`${import.meta.dir}`, "amp-mock.sh"), + content: await Bun.file( + path.join(moduleDir, "testdata", "amp-mock.sh"), + ).text(), }); } + return { id, coderEnvVars, scripts }; +}; + +const runScript = async (id: string, name: string, script: string) => { + const target = `/tmp/coder-utils-${name}.sh`; + await writeExecutable({ containerId: id, filePath: target, content: script }); + return execContainer(id, ["bash", "-c", target]); +}; + +const runScripts = async (id: string, scripts: ModuleScripts) => { + 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); + 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}`); + } + } +}; + +const moduleLogDir = + "/home/coder/.coder-modules/coder-labs/sourcegraph-amp/logs"; +const installLog = (id: string) => + readFileContainer(id, `${moduleLogDir}/install.log`); +const settingsPath = "/home/coder/.config/amp/settings.json"; + +const seedSettings = (id: string, content: string) => + execContainer(id, [ + "bash", + "-c", + `mkdir -p /home/coder/.config/amp && cat > ${settingsPath} <<'EOF'\n${content}\nEOF`, + ]); + +// Installer fixture that drops a fake amp binary where the real installer does. +const installerFixture = [ + "#!/usr/bin/env bash", + 'printf "%s" "${AMP_VERSION:-}" > /tmp/amp-installer-version', + 'mkdir -p "$HOME/.amp/bin" "$HOME/.local/bin"', + `cat > "$HOME/.amp/bin/amp" <<'EOF'`, + "#!/bin/sh", + 'if [ "$1" = "--version" ]; then echo "0.0.1790769659-g954f35 (released 2026-09-30T12:00:59.000Z, 1m ago)"; fi', + "EOF", + 'chmod +x "$HOME/.amp/bin/amp"', + 'ln -sf "$HOME/.amp/bin/amp" "$HOME/.local/bin/amp"', +].join("\n"); - return { id }; +const mockCurl = async (id: string, exitCode = 0) => { + await writeExecutable({ + containerId: id, + filePath: "/tmp/amp-installer-fixture.sh", + content: installerFixture, + }); + await writeExecutable({ + containerId: id, + filePath: "/usr/local/bin/curl", + content: + exitCode === 0 + ? [ + "#!/bin/bash", + "printf '%s\\n' \"$*\" > /tmp/amp-curl-args", + 'while [ $# -gt 0 ]; do if [ "$1" = "--output" ]; then out="$2"; fi; shift; done', + 'cp /tmp/amp-installer-fixture.sh "$out"', + ].join("\n") + : `#!/bin/bash\nexit ${exitCode}\n`, + }); }; setDefaultTimeout(60 * 1000); -describe("amp", async () => { +describe("sourcegraph-amp", async () => { beforeAll(async () => { await runTerraformInit(import.meta.dir); }); - // test("happy-path", async () => { - // const { id } = await setup(); - // await execModuleScript(id); - // await expectAgentAPIStarted(id); - // }); - // - // test("api-key", async () => { - // const apiKey = "test-api-key-123"; - // const { id } = await setup({ - // moduleVariables: { - // amp_api_key: apiKey, - // }, - // }); - // await execModuleScript(id); - // const resp = await readFileContainer( - // id, - // "/home/coder/.amp-module/agentapi-start.log", - // ); - // expect(resp).toContain("amp_api_key provided !"); - // }); - // - test("install-latest-version", async () => { - const { id } = await setup({ - skipAmpMock: true, - skipAgentAPIMock: true, - moduleVariables: { - amp_version: "", - }, - }); - await execModuleScript(id); - await expectAgentAPIStarted(id); + 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 Amp CLI installation"); + expect(log).toContain("Validated existing Amp CLI"); + expect(log).toContain("0.0.1700000000-gmock00"); + expect(log).toContain("Amp module setup completed."); + expect(log).not.toContain("agentapi"); + // No settings are written when nothing is configured. + const settings = await execContainer(id, ["test", "-e", settingsPath]); + expect(settings.exitCode).not.toBe(0); + }); + + test("preinstalled-binary-required-when-install-disabled", async () => { + const { id, scripts } = await setup({ skipAmpMock: 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"); }); - test("install-specific-version", async () => { - const { id } = await setup({ + test("official-installer-is-used", async () => { + const { id, scripts } = await setup({ skipAmpMock: true, moduleVariables: { - install_via_npm: "true", - amp_version: "0.0.1755964909-g31e083", + install_amp: "true", + amp_version: "0.0.1790769659-g954f35", }, }); - await execModuleScript(id); - const resp = await readFileContainer( - id, - "/home/coder/.amp-module/agentapi-start.log", - ); - expect(resp).toContain("0.0.1755964909-g31e08"); + await mockCurl(id); + await runScripts(id, scripts); + const log = await installLog(id); + expect(log).toContain("Installed Amp CLI"); + expect(log).toContain("0.0.1790769659-g954f35"); + const curlArgs = await readFileContainer(id, "/tmp/amp-curl-args"); + expect(curlArgs).toContain("https://ampcode.com/install.sh"); + expect(curlArgs).toContain("--retry 2"); + expect(curlArgs).toContain("--connect-timeout 10"); + expect(curlArgs).toContain("--max-time 300"); + const version = await readFileContainer(id, "/tmp/amp-installer-version"); + expect(version).toBe("0.0.1790769659-g954f35"); }); - test("install-via-npm", async () => { - const { id } = await setup({ + test("installer-download-failure-is-terminal", async () => { + const { id, scripts } = await setup({ skipAmpMock: true, - moduleVariables: { - install_via_npm: "true", - }, + moduleVariables: { install_amp: "true" }, }); - await execModuleScript(id); - - const installLog = await readFileContainer( - id, - "/home/coder/.amp-module/install.log", - ); - expect(installLog).toContain("Installing Amp via npm"); + await mockCurl(id, 22); + 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"); + }); - const startLog = await readFileContainer( - id, - "/home/coder/.amp-module/agentapi-start.log", - ); - expect(startLog).toContain("AMP version:"); + test("workdir-created", async () => { + const { id, scripts } = await setup(); + await runScripts(id, scripts); + const dir = await execContainer(id, ["test", "-d", projectDir]); + expect(dir.exitCode).toBe(0); }); - test("custom-workdir", async () => { - const workdir = "/tmp/amp-test"; - const { id } = await setup({ - moduleVariables: { - workdir, - }, + test("instruction-prompt", async () => { + const prompt = "Start every response with `amp > `"; + const { id, scripts } = await setup({ + moduleVariables: { instruction_prompt: prompt }, }); - await execModuleScript(id); - const resp = await readFileContainer( - id, - "/home/coder/.amp-module/agentapi-start.log", - ); - expect(resp).toContain(workdir); + await runScripts(id, scripts); + const agents = await readFileContainer(id, "/home/coder/.config/AGENTS.md"); + expect(agents.trim()).toBe(prompt); }); - test("pre-post-install-scripts", async () => { - const { id } = await setup({ + test("base-config-overwrites-then-mcp-is-applied", async () => { + const { id, scripts } = await setup({ moduleVariables: { - pre_install_script: "#!/bin/bash\necho 'pre-install-script'", - post_install_script: "#!/bin/bash\necho 'post-install-script'", + base_amp_config: JSON.stringify({ + "amp.showCosts": false, + "amp.mcpServers": { base: { command: "base-command" } }, + }), + mcp: JSON.stringify({ + base: { command: "module-command" }, + extra: { command: "extra-command" }, + }), }, }); - await execModuleScript(id); - const preLog = await readFileContainer( - id, - "/home/coder/.amp-module/pre_install.log", - ); - expect(preLog).toContain("pre-install-script"); - const postLog = await readFileContainer( + await seedSettings( id, - "/home/coder/.amp-module/post_install.log", + JSON.stringify({ + "amp.notifications.enabled": false, + "amp.mcpServers": { seeded: { command: "seeded-command" } }, + }), ); - expect(postLog).toContain("post-install-script"); + await runScripts(id, scripts); + const settings = JSON.parse(await readFileContainer(id, settingsPath)); + expect(settings["amp.showCosts"]).toBe(false); + expect(settings["amp.notifications.enabled"]).toBeUndefined(); + const servers = settings["amp.mcpServers"]; + expect(servers.seeded).toBeUndefined(); + expect(servers.base.command).toBe("base-command"); + expect(servers.extra.command).toBe("extra-command"); }); - test("instruction-prompt", async () => { - const prompt = "this is a instruction prompt for AMP"; - const { id } = await setup({ + test("writes-mcp-servers-without-coder-server", async () => { + const { id, scripts } = await setup({ moduleVariables: { - instruction_prompt: prompt, + mcp: JSON.stringify({ + playwright: { command: "npx", args: ["-y", "@playwright/mcp"] }, + }), }, }); - await execModuleScript(id); - const resp = await readFileContainer(id, "/home/coder/.config/AGENTS.md"); - expect(resp).toContain(prompt); + await runScripts(id, scripts); + const settings = JSON.parse(await readFileContainer(id, settingsPath)); + expect(settings["amp.mcpServers"].playwright.command).toBe("npx"); + expect(settings["amp.mcpServers"].coder).toBeUndefined(); + const mode = await execContainer(id, ["stat", "-c", "%a", settingsPath]); + expect(mode.stdout.trim()).toBe("600"); }); - test("ai-prompt", async () => { - const prompt = "this is a task prompt for AMP"; - const { id } = await setup({ + test("merges-mcp-config-existing-servers-win", async () => { + const { id, scripts } = await setup({ moduleVariables: { - ai_prompt: prompt, + mcp: JSON.stringify({ + shared: { command: "module-command" }, + extra: { command: "extra-command" }, + }), }, }); - await execModuleScript(id); - const resp = await readFileContainer( + await seedSettings( id, - "/home/coder/.amp-module/agentapi-start.log", + JSON.stringify({ + "amp.showCosts": false, + "amp.mcpServers": { + shared: { command: "existing-command" }, + seeded: { command: "seeded-command" }, + }, + }), + ); + await runScripts(id, scripts); + const settings = JSON.parse(await readFileContainer(id, settingsPath)); + const servers = settings["amp.mcpServers"]; + expect(servers.shared.command).toBe("existing-command"); + expect(servers.seeded.command).toBe("seeded-command"); + expect(servers.extra.command).toBe("extra-command"); + expect(settings["amp.showCosts"]).toBe(false); + expect(await installLog(id)).toContain( + "Warning: MCP server 'shared' already exists", ); - expect(resp).toContain(`amp task prompt provided : ${prompt}`); }); - test("custom-base-config", async () => { - const customConfig = JSON.stringify({ - "amp.anthropic.thinking.enabled": false, - "amp.todos.enabled": false, - "amp.tools.stopTimeout": 900, - "amp.git.commit.ampThread.enabled": true, - }); - const customMcp = JSON.stringify({ - "test-server": { - command: "/usr/bin/test-mcp", - args: ["--test-arg"], - type: "stdio", - }, - }); - const { id } = await setup({ + test("jsonc-settings-are-left-untouched", async () => { + const { id, scripts } = await setup({ moduleVariables: { - base_amp_config: customConfig, - mcp: customMcp, + mcp: JSON.stringify({ extra: { command: "x" } }), }, }); - await execModuleScript(id, { - CODER_AGENT_TOKEN: "test-token", - CODER_AGENT_URL: "http://test-url:3000", - }); - const settingsContent = await readFileContainer( - id, - "/home/coder/.config/amp/settings.json", - ); - const settings = JSON.parse(settingsContent); - - expect(settings["amp.anthropic.thinking.enabled"]).toBe(false); - expect(settings["amp.todos.enabled"]).toBe(false); - expect(settings["amp.tools.stopTimeout"]).toBe(900); - expect(settings["amp.git.commit.ampThread.enabled"]).toBe(true); - expect(settings["amp.mcpServers"]).toBeDefined(); - expect(settings["amp.mcpServers"].coder).toBeDefined(); - expect(settings["amp.mcpServers"]["test-server"]).toBeDefined(); - expect(settings["amp.mcpServers"]["test-server"].command).toBe( - "/usr/bin/test-mcp", - ); - expect(settings["amp.mcpServers"]["test-server"].args).toEqual([ - "--test-arg", - ]); + const jsonc = '{\n // user comment\n "amp.showCosts": false,\n}'; + await seedSettings(id, jsonc); + await runScripts(id, scripts); + const settings = await readFileContainer(id, settingsPath); + expect(settings.trim()).toBe(jsonc); + const log = await installLog(id); + expect(log).toContain("is not a strict JSON object"); }); - test("default-base-config", async () => { - const { id } = await setup(); - await execModuleScript(id, { - CODER_AGENT_TOKEN: "test-token", - CODER_AGENT_URL: "http://test-url:3000", + test("api-key-env-var-not-in-script", async () => { + const apiKey = "sgamp_test-api-key-123"; + const { coderEnvVars, scripts } = await setup({ + moduleVariables: { amp_api_key: apiKey }, }); - const settingsContent = await readFileContainer( - id, - "/home/coder/.config/amp/settings.json", + expect(coderEnvVars["AMP_API_KEY"]).toBe(apiKey); + expect(scripts.install).not.toContain(apiKey); + expect(scripts.install).not.toContain( + Buffer.from(apiKey).toString("base64"), ); - const settings = JSON.parse(settingsContent); + }); - expect(settings["amp.anthropic.thinking.enabled"]).toBe(true); - expect(settings["amp.todos.enabled"]).toBe(true); - expect(settings["amp.mcpServers"]).toBeDefined(); - expect(settings["amp.mcpServers"].coder).toBeDefined(); - expect(settings["amp.mcpServers"].coder.command).toBe("coder"); + test("pre-post-install-scripts", async () => { + const { id, scripts } = await setup({ + moduleVariables: { + pre_install_script: "#!/bin/bash\necho 'amp-pre-install-script'", + post_install_script: "#!/bin/bash\necho 'amp-post-install-script'", + }, + }); + await runScripts(id, scripts); + expect( + await readFileContainer(id, `${moduleLogDir}/pre_install.log`), + ).toContain("amp-pre-install-script"); + expect( + await readFileContainer(id, `${moduleLogDir}/post_install.log`), + ).toContain("amp-post-install-script"); }); }); diff --git a/registry/coder-labs/modules/sourcegraph-amp/main.tf b/registry/coder-labs/modules/sourcegraph-amp/main.tf index 907b4316b..cd6eda86d 100644 --- a/registry/coder-labs/modules/sourcegraph-amp/main.tf +++ b/registry/coder-labs/modules/sourcegraph-amp/main.tf @@ -1,17 +1,12 @@ terraform { - required_version = ">= 1.0" + required_version = ">= 1.9" required_providers { coder = { source = "coder/coder" version = ">= 2.12" } - external = { - source = "hashicorp/external" - version = "2.3.5" - } } - } variable "agent_id" { @@ -19,255 +14,101 @@ 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/sourcegraph-amp.svg" } variable "workdir" { type = string - description = "The folder to run AMP CLI in." -} - -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.11.1" -} - -variable "cli_app" { - type = bool - description = "Whether to create a CLI app for Claude Code" - default = false -} - -variable "web_app_display_name" { - type = string - description = "Display name for the web app" - default = "Amp" -} - -variable "cli_app_display_name" { - type = string - description = "Display name for the CLI app" - default = "Amp CLI" + description = "Optional project directory. When set, the module pre-creates it if missing. Amp has no folder trust prompt, so nothing else is written for it." + default = null } variable "pre_install_script" { type = string - description = "Custom script to run before installing amp cli" + description = "Custom script to run before installing Amp." default = null } variable "post_install_script" { type = string - description = "Custom script to run after installing amp cli." + description = "Custom script to run after installing Amp." default = null } -variable "report_tasks" { - type = bool - description = "Whether to enable task reporting to Coder UI" - default = true -} - variable "install_amp" { type = bool - description = "Whether to install amp cli." + description = "Whether to install Amp with the official installer. When false, a working amp binary must already be on PATH." default = true } -variable "install_via_npm" { - type = bool - description = "Install Amp via npm instead of the official installer." - default = false -} - -variable "amp_api_key" { - type = string - description = "amp cli API Key" - default = "" -} - variable "amp_version" { type = string - description = "The version of amp cli to install." + description = "The version of Amp CLI to install. Empty installs the latest release." default = "" } -variable "ai_prompt" { +variable "amp_api_key" { type = string - description = "Task prompt for the Amp CLI" + description = "Amp access token, exported as AMP_API_KEY. See https://ampcode.com/docs/cli/execute-mode#non-interactive-environments" + sensitive = true default = "" } variable "instruction_prompt" { type = string - description = "Instruction prompt for the Amp CLI. https://ampcode.com/manual#AGENTS.md" + description = "Instruction prompt written to ~/.config/AGENTS.md. See https://ampcode.com/docs/customize/agents-md" default = "" } -resource "coder_env" "amp_api_key" { - agent_id = var.agent_id - name = "AMP_API_KEY" - value = var.amp_api_key -} - variable "base_amp_config" { type = string - description = <<-EOT - Base AMP configuration in JSON format. Can be overridden to customize AMP settings. - - If empty, defaults enable thinking and todos for autonomous operation. Additional options include: - - "amp.permissions": [] (tool permissions) - - "amp.tools.stopTimeout": 600 (extend timeout for long operations) - - "amp.terminal.commands.nodeSpawn.loadProfile": "daily" (environment loading) - - "amp.tools.disable": ["builtin:open"] (disable tools for containers) - - "amp.git.commit.ampThread.enabled": true (link commits to threads) - - "amp.git.commit.coauthor.enabled": true (add Amp as co-author) - - Reference: https://ampcode.com/manual - EOT + description = "Amp settings as JSON. When set, it overwrites ~/.config/amp/settings.json on every start, before mcp is applied. See https://ampcode.com/docs/cli/settings" default = "" } variable "mcp" { type = string - description = "Additional MCP servers configuration in JSON format to append to amp.mcpServers." - default = null -} - -variable "mode" { - type = string - description = "Set the agent mode (free, rush, smart) — controls the model, system prompt, and tool selection. Default: smart" - default = "smart" - validation { - condition = contains(["", "free", "rush", "smart"], var.mode) - error_message = "Invalid mode. Select one from (free, rush, smart)" - } + description = "Additional MCP servers as a JSON object keyed by server name, in the amp.mcpServers format. See https://ampcode.com/docs/customize/mcp" + default = "" } -data "external" "env" { - program = ["sh", "-c", "echo '{\"CODER_AGENT_TOKEN\":\"'$CODER_AGENT_TOKEN'\",\"CODER_AGENT_URL\":\"'$CODER_AGENT_URL'\"}'"] +resource "coder_env" "amp_api_key" { + count = var.amp_api_key != "" ? 1 : 0 + agent_id = var.agent_id + name = "AMP_API_KEY" + value = var.amp_api_key } locals { - app_slug = "amp" - - default_base_config = jsonencode({ - "amp.anthropic.thinking.enabled" = true - "amp.todos.enabled" = true - "amp.terminal.animation" = false - }) - - user_config = jsondecode(var.base_amp_config != "" ? var.base_amp_config : local.default_base_config) - base_amp_settings = { for k, v in local.user_config : k => v if k != "amp.mcpServers" } - - coder_mcp = { - "coder" = { - "command" = "coder" - "args" = ["exp", "mcp", "server"] - "env" = { - "CODER_MCP_APP_STATUS_SLUG" = var.report_tasks == true ? local.app_slug : "" - "CODER_MCP_AI_AGENTAPI_URL" = var.report_tasks == true ? "http://localhost:3284" : "" - "CODER_AGENT_TOKEN" = data.external.env.result.CODER_AGENT_TOKEN - "CODER_AGENT_URL" = data.external.env.result.CODER_AGENT_URL - } - "type" = "stdio" - } - } - - additional_mcp = var.mcp != null ? jsondecode(var.mcp) : {} - - merged_mcp_servers = merge( - lookup(local.user_config, "amp.mcpServers", {}), - local.coder_mcp, - local.additional_mcp - ) - - final_config = merge(local.base_amp_settings, { - "amp.mcpServers" = local.merged_mcp_servers + workdir = var.workdir != null ? trimsuffix(var.workdir, "/") : "" + install_script = templatefile("${path.module}/scripts/install.sh.tftpl", { + ARG_INSTALL = tostring(var.install_amp) + ARG_AMP_VERSION = base64encode(var.amp_version) + ARG_WORKDIR = local.workdir != "" ? base64encode(local.workdir) : "" + ARG_INSTRUCTION_PROMPT = var.instruction_prompt != "" ? base64encode(var.instruction_prompt) : "" + ARG_BASE_CONFIG = var.base_amp_config != "" ? base64encode(var.base_amp_config) : "" + ARG_MCP_CONFIG = var.mcp != "" ? base64encode(var.mcp) : "" }) - - install_script = file("${path.module}/scripts/install.sh") - start_script = file("${path.module}/scripts/start.sh") - module_dir_name = ".amp-module" - workdir = trimsuffix(var.workdir, "/") + module_dir_name = ".coder-modules/coder-labs/sourcegraph-amp" } -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_AMP_API_KEY='${var.amp_api_key}' \ - ARG_AMP_START_DIRECTORY='${var.workdir}' \ - ARG_AMP_TASK_PROMPT='${base64encode(var.ai_prompt)}' \ - ARG_REPORT_TASKS='${var.report_tasks}' \ - ARG_MODE='${var.mode}' \ - /tmp/start.sh - EOT - - install_script = <<-EOT - #!/usr/bin/env bash - set -o errexit - set -o pipefail +module "coder_utils" { + source = "registry.coder.com/coder/coder-utils/coder" + version = "0.0.1" - echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh - chmod +x /tmp/install.sh - ARG_INSTALL_AMP='${var.install_amp}' \ - ARG_INSTALL_VIA_NPM='${var.install_via_npm}' \ - ARG_AMP_CONFIG="${base64encode(jsonencode(local.final_config))}" \ - ARG_AMP_VERSION='${var.amp_version}' \ - ARG_AMP_INSTRUCTION_PROMPT='${base64encode(var.instruction_prompt)}' \ - /tmp/install.sh - EOT + agent_id = var.agent_id + module_directory = "$HOME/${local.module_dir_name}" + display_name_prefix = "Amp" + 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/coder-labs/modules/sourcegraph-amp/main.tftest.hcl b/registry/coder-labs/modules/sourcegraph-amp/main.tftest.hcl new file mode 100644 index 000000000..b97744855 --- /dev/null +++ b/registry/coder-labs/modules/sourcegraph-amp/main.tftest.hcl @@ -0,0 +1,107 @@ +run "defaults_are_correct" { + command = plan + + variables { + agent_id = "test-agent" + } + + assert { + condition = var.install_amp == true + error_message = "install_amp should default to true" + } + + assert { + condition = var.amp_version == "" + error_message = "amp_version should default to empty (latest)" + } + + assert { + condition = local.workdir == "" + error_message = "workdir should be empty by default" + } + + assert { + condition = local.module_dir_name == ".coder-modules/coder-labs/sourcegraph-amp" + error_message = "module_dir_name should be '.coder-modules/coder-labs/sourcegraph-amp'" + } + + assert { + condition = length(coder_env.amp_api_key) == 0 + error_message = "AMP_API_KEY should not be created when amp_api_key is empty" + } +} + +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" + amp_api_key = "sgamp_test-key" + } + + assert { + condition = coder_env.amp_api_key[0].name == "AMP_API_KEY" && coder_env.amp_api_key[0].value == "sgamp_test-key" + error_message = "AMP_API_KEY env var should be created with the provided key" + } + + assert { + condition = !strcontains(local.install_script, nonsensitive(var.amp_api_key)) && !strcontains(local.install_script, base64encode(nonsensitive(var.amp_api_key))) + error_message = "API key should not be rendered into the install script" + } +} + +run "amp_version_is_passed_through" { + command = plan + + variables { + agent_id = "test-agent" + amp_version = "0.0.1790769659-g954f35" + } + + assert { + condition = strcontains(local.install_script, base64encode("0.0.1790769659-g954f35")) + error_message = "amp_version should be rendered into the install script" + } +} + +run "scripts_output_is_ordered" { + command = plan + + variables { + agent_id = "test-agent" + pre_install_script = "echo pre" + post_install_script = "echo post" + } + + assert { + condition = output.scripts == ["coder-labs-sourcegraph-amp-pre_install_script", "coder-labs-sourcegraph-amp-install_script", "coder-labs-sourcegraph-amp-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 == ["coder-labs-sourcegraph-amp-install_script"] + error_message = "scripts output should only contain install when no pre/post scripts are set" + } +} diff --git a/registry/coder-labs/modules/sourcegraph-amp/scripts/install.sh b/registry/coder-labs/modules/sourcegraph-amp/scripts/install.sh deleted file mode 100644 index fd42ed7ca..000000000 --- a/registry/coder-labs/modules/sourcegraph-amp/scripts/install.sh +++ /dev/null @@ -1,142 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# ANSI colors -BOLD='\033[1m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' - -ARG_INSTALL_AMP=${ARG_INSTALL_AMP:-true} -ARG_INSTALL_VIA_NPM=${ARG_INSTALL_VIA_NPM:-false} -ARG_AMP_VERSION=${ARG_AMP_VERSION:-} -ARG_AMP_INSTRUCTION_PROMPT=$(echo -n "${ARG_AMP_INSTRUCTION_PROMPT:-}" | base64 -d) -ARG_AMP_CONFIG=$(echo -n "${ARG_AMP_CONFIG:-}" | base64 -d) - -echo "--------------------------------" -printf "Install flag: %s\n" "$ARG_INSTALL_AMP" -printf "Install via npm: %s\n" "$ARG_INSTALL_VIA_NPM" -printf "Amp Version: %s\n" "$ARG_AMP_VERSION" -printf "AMP Config: %s\n" "$ARG_AMP_CONFIG" -printf "Instruction Prompt: %s\n" "$ARG_AMP_INSTRUCTION_PROMPT" -echo "--------------------------------" - -command_exists() { - command -v "$1" > /dev/null 2>&1 -} - -install_amp_npm() { - printf "%s${YELLOW}Installing Amp via npm${NC}\n" "${BOLD}" - - # Load nvm if available - # shellcheck source=/dev/null - if [ -f "$HOME/.nvm/nvm.sh" ]; then - source "$HOME/.nvm/nvm.sh" - fi - - if ! command_exists node || ! command_exists npm; then - printf "${YELLOW}Warning: Node.js/npm not found. Skipping Amp installation.${NC}\n" - printf "To install Amp via npm, please install Node.js and npm first.\n" - return 1 - fi - - printf "Node.js version: %s\n" "$(node --version)" - printf "npm version: %s\n" "$(npm --version)" - - NPM_GLOBAL_PREFIX="${HOME}/.npm-global" - if [ ! -d "$NPM_GLOBAL_PREFIX" ]; then - mkdir -p "$NPM_GLOBAL_PREFIX" - fi - - npm config set prefix "$NPM_GLOBAL_PREFIX" - export PATH="$NPM_GLOBAL_PREFIX/bin:$PATH" - - if [ -n "$ARG_AMP_VERSION" ]; then - npm install -g "@sourcegraph/amp@$ARG_AMP_VERSION" - else - npm install -g "@sourcegraph/amp" - fi - - if ! grep -q 'export PATH="$HOME/.npm-global/bin:$PATH"' "$HOME/.bashrc"; then - echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> "$HOME/.bashrc" - fi -} - -install_amp_official() { - printf "%s Installing Amp using official installer\n" "${BOLD}" - - if [ -n "$ARG_AMP_VERSION" ]; then - export AMP_VERSION="$ARG_AMP_VERSION" - printf "Installing Amp version: %s\n" "$AMP_VERSION" - fi - - if curl -fsSL https://ampcode.com/install.sh | bash; then - export PATH="$HOME/.local/bin:$HOME/.amp/bin:$PATH" - - if ! grep -q 'export PATH="$HOME/.local/bin:$PATH"' "$HOME/.bashrc"; then - echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$HOME/.bashrc" - fi - else - printf "${YELLOW}Warning: Official installer failed. Installation skipped.${NC}\n" - return 1 - fi -} - -function install_amp() { - if [ "${ARG_INSTALL_AMP}" = "true" ]; then - if [ "${ARG_INSTALL_VIA_NPM}" = "true" ]; then - install_amp_npm || { - printf "${YELLOW}Amp installation via npm failed.${NC}\n" - return 0 - } - else - install_amp_official || { - printf "${YELLOW}Amp installation via official installer failed.${NC}\n" - return 0 - } - fi - - if command_exists amp; then - printf "%s${GREEN}Successfully installed Sourcegraph Amp CLI. Version: %s${NC}\n" "${BOLD}" "$(amp --version)" - fi - else - printf "Skipping Sourcegraph Amp CLI installation (install_amp=false)\n" - fi -} - -function setup_instruction_prompt() { - if [ -n "${ARG_AMP_INSTRUCTION_PROMPT:-}" ]; then - echo "Setting AMP instruction prompt..." - mkdir -p "$HOME/.config" - echo "$ARG_AMP_INSTRUCTION_PROMPT" > "$HOME/.config/AGENTS.md" - echo "Instruction prompt saved to $HOME/.config/AGENTS.md" - else - echo "No instruction prompt provided for Sourcegraph AMP." - fi -} - -function configure_amp_settings() { - echo "Configuring AMP settings..." - SETTINGS_PATH="$HOME/.config/amp/settings.json" - mkdir -p "$(dirname "$SETTINGS_PATH")" - - if [ -z "${ARG_AMP_CONFIG:-}" ]; then - echo "No AMP config provided, skipping configuration" - return - fi - - echo "Writing AMP configuration to $SETTINGS_PATH" - UPDATED_CONFIG=$(echo "$ARG_AMP_CONFIG" | jq --arg token "$CODER_AGENT_TOKEN" --arg url "$CODER_AGENT_URL" \ - ".[\"amp.mcpServers\"].coder.env += { - \"CODER_AGENT_TOKEN\": \"$CODER_AGENT_TOKEN\", - \"CODER_AGENT_URL\": \"$CODER_AGENT_URL\" - }") - printf "UPDATED_CONFIG: %s\n" "$UPDATED_CONFIG" - printf '%s\n' "$UPDATED_CONFIG" > "$SETTINGS_PATH" - - echo "AMP configuration complete" -} - -install_amp -setup_instruction_prompt -configure_amp_settings diff --git a/registry/coder-labs/modules/sourcegraph-amp/scripts/install.sh.tftpl b/registry/coder-labs/modules/sourcegraph-amp/scripts/install.sh.tftpl new file mode 100644 index 000000000..3a97b74e2 --- /dev/null +++ b/registry/coder-labs/modules/sourcegraph-amp/scripts/install.sh.tftpl @@ -0,0 +1,208 @@ +#!/usr/bin/env bash + +set -euo pipefail + +command_exists() { + command -v "$1" > /dev/null 2>&1 +} + +ARG_INSTALL='${ARG_INSTALL}' +ARG_AMP_VERSION=$(echo -n '${ARG_AMP_VERSION}' | base64 -d) +ARG_WORKDIR=$(echo -n '${ARG_WORKDIR}' | base64 -d) +ARG_INSTRUCTION_PROMPT=$(echo -n '${ARG_INSTRUCTION_PROMPT}' | base64 -d) +ARG_BASE_CONFIG=$(echo -n '${ARG_BASE_CONFIG}' | base64 -d) +ARG_MCP_CONFIG=$(echo -n '${ARG_MCP_CONFIG}' | base64 -d) + +export PATH="$HOME/.local/bin:$HOME/.amp/bin:$PATH" + +SETTINGS_FILE="$${AMP_SETTINGS_FILE:-$HOME/.config/amp/settings.json}" + +echo "--------------------------------" +printf "install_amp: %s\n" "$${ARG_INSTALL}" +printf "amp_version: %s\n" "$${ARG_AMP_VERSION:-latest}" +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_amp_version() { + amp --version 2> /dev/null | awk '{print $1}' +} + +function ensure_amp_in_path() { + local AMP_BIN="" + if command_exists amp; then + AMP_BIN=$(command -v amp) + fi + + if [ -z "$${AMP_BIN}" ] || [ ! -x "$${AMP_BIN}" ]; then + echo "Amp CLI binary (amp) was not found or is not executable." >&2 + return 1 + fi + + local version + if ! version=$(installed_amp_version) || [ -z "$${version}" ]; then + echo "Amp CLI binary could not be executed." >&2 + return 1 + fi + printf "Amp CLI version: %s\n" "$${version}" + + if [ -n "$${CODER_SCRIPT_BIN_DIR:-}" ] && [ ! -e "$${CODER_SCRIPT_BIN_DIR}/amp" ]; then + ln -s "$${AMP_BIN}" "$${CODER_SCRIPT_BIN_DIR}/amp" + echo "Created symlink: $${CODER_SCRIPT_BIN_DIR}/amp -> $${AMP_BIN}" + fi + + add_path_to_shell_profiles "$(dirname "$${AMP_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://ampcode.com/install.sh; then + echo "Amp CLI installer could not be downloaded after up to 3 attempts." >&2 + return 1 + fi + + if ! bash -n "$${installer_file}"; then + echo "Amp CLI installer download was invalid." >&2 + return 1 + fi + + # Installs to ~/.amp/bin and links ~/.local/bin/amp; empty AMP_VERSION means latest. + if ! AMP_VERSION="$${ARG_AMP_VERSION}" bash "$${installer_file}"; then + echo "Amp CLI installation failed." >&2 + return 1 + fi +) + +install_amp() { + if [ "$${ARG_INSTALL}" != "true" ]; then + echo "Skipping Amp CLI installation as per configuration." + if ! ensure_amp_in_path; then + echo "ERROR: install_amp is false but a working 'amp' was not found on PATH." >&2 + exit 1 + fi + echo "Validated existing Amp CLI" + return + fi + + echo "Installing Amp CLI" + install_with_official_installer + + if ! ensure_amp_in_path; then + echo "ERROR: Failed to install Amp CLI" >&2 + exit 1 + fi + echo "Installed Amp CLI" +} + +setup_workdir() { + if [ -n "$${ARG_WORKDIR}" ] && [ ! -d "$${ARG_WORKDIR}" ]; then + echo "Creating workdir: $${ARG_WORKDIR}" + mkdir -p "$${ARG_WORKDIR}" + fi +} + +write_instruction_prompt() { + if [ -z "$${ARG_INSTRUCTION_PROMPT}" ]; then + return + fi + + local agents_file="$HOME/.config/AGENTS.md" + mkdir -p "$(dirname "$${agents_file}")" + printf '%s\n' "$${ARG_INSTRUCTION_PROMPT}" > "$${agents_file}" + echo "Wrote instruction prompt to $${agents_file}" +} + +# Prints the current user settings as JSON, or {} when the file is absent. +# Returns non-zero when the file exists but is not a strict JSON object (Amp +# also accepts JSONC), so callers can leave it untouched. +read_user_settings() { + if [ ! -f "$${SETTINGS_FILE}" ]; then + echo '{}' + return + fi + if ! jq -e 'type == "object"' "$${SETTINGS_FILE}" > /dev/null 2>&1; then + return 1 + fi + cat "$${SETTINGS_FILE}" +} + +write_user_settings() { + local content="$1" + mkdir -p "$(dirname "$${SETTINGS_FILE}")" + ( + umask 077 + printf '%s\n' "$${content}" > "$${SETTINGS_FILE}" + ) +} + +write_base_config() { + if [ -z "$${ARG_BASE_CONFIG}" ]; then + return + fi + write_user_settings "$${ARG_BASE_CONFIG}" + echo "Amp settings written: $${SETTINGS_FILE}" +} + +# MCP servers already in the file win on duplicate names, matching `amp mcp add`. +setup_mcp() { + if [ -z "$${ARG_MCP_CONFIG}" ]; then + return + fi + + require_jq "configure Amp MCP servers" + + local existing name + if ! existing=$(read_user_settings); then + echo "Warning: $${SETTINGS_FILE} is not a strict JSON object (comments or trailing commas); leaving it unchanged." >&2 + return + fi + + while IFS= read -r name; do + echo "Warning: MCP server '$${name}' already exists in $${SETTINGS_FILE}, keeping the existing one." >&2 + done < <(echo "$${existing}" | jq -r --argjson mcp "$${ARG_MCP_CONFIG}" '(.["amp.mcpServers"] // {}) as $current | $mcp | keys[] | select(. as $k | $current | has($k))') + + write_user_settings "$(echo "$${existing}" | jq --argjson mcp "$${ARG_MCP_CONFIG}" '.["amp.mcpServers"] = ($mcp + (.["amp.mcpServers"] // {}))')" + echo "Amp MCP configuration written: $${SETTINGS_FILE}" +} + +install_amp +setup_workdir +write_instruction_prompt +write_base_config +setup_mcp + +echo "Amp module setup completed." diff --git a/registry/coder-labs/modules/sourcegraph-amp/scripts/start.sh b/registry/coder-labs/modules/sourcegraph-amp/scripts/start.sh deleted file mode 100644 index a36561b84..000000000 --- a/registry/coder-labs/modules/sourcegraph-amp/scripts/start.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env bash - -# Load user environment -if [ -f "$HOME/.bashrc" ]; then - source "$HOME/.bashrc" -fi - -if [ -f "$HOME/.nvm/nvm.sh" ]; then - source "$HOME/.nvm/nvm.sh" -fi - -set -euo pipefail - -export PATH="$HOME/.local/bin:$HOME/.amp/bin:$HOME/.npm-global/bin:$PATH" - -function ensure_command() { - command -v "$1" &> /dev/null || { - echo "Error: '$1' not found." >&2 - exit 1 - } -} - -ARG_AMP_START_DIRECTORY=${ARG_AMP_START_DIRECTORY:-"$HOME"} -ARG_AMP_API_KEY=${ARG_AMP_API_KEY:-} -ARG_AMP_TASK_PROMPT=$(echo -n "${ARG_AMP_TASK_PROMPT:-}" | base64 -d) -ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true} - -echo "--------------------------------" -printf "Workspace: %s\n" "$ARG_AMP_START_DIRECTORY" -printf "Task Prompt: %s\n" "$ARG_AMP_TASK_PROMPT" -printf "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS" -printf "ARG_MODE: %s\n" "$ARG_MODE" -echo "--------------------------------" - -ensure_command amp -echo "AMP version: $(amp --version)" - -dir="$ARG_AMP_START_DIRECTORY" -if [[ -d "$dir" ]]; then - echo "Using existing directory: $dir" -else - echo "Creating directory: $dir" - mkdir -p "$dir" -fi -cd "$dir" - -if [ -n "$ARG_AMP_API_KEY" ]; then - printf "amp_api_key provided !\n" - export AMP_API_KEY=$ARG_AMP_API_KEY -else - printf "amp_api_key not provided\n" -fi - -ARGS=() - -if [ -n "$ARG_MODE" ]; then - printf "Running agent in: %s mode" "$ARG_MODE" - ARGS+=(--mode "$ARG_MODE") -fi - -if [ -n "$ARG_AMP_TASK_PROMPT" ]; then - if [ "$ARG_REPORT_TASKS" == "true" ]; then - printf "amp task prompt provided : %s" "$ARG_AMP_TASK_PROMPT\n" - PROMPT="Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_AMP_TASK_PROMPT" - else - PROMPT="$ARG_AMP_TASK_PROMPT" - fi - # Pipe the prompt into amp, which will be run inside agentapi - agentapi server --type amp --term-width=67 --term-height=1190 -- bash -c "echo \"$PROMPT\" | amp" "${ARGS[@]}" -else - printf "No task prompt given.\n" - agentapi server --type amp --term-width=67 --term-height=1190 -- amp "${ARGS[@]}" -fi diff --git a/registry/coder-labs/modules/sourcegraph-amp/testdata/amp-mock.sh b/registry/coder-labs/modules/sourcegraph-amp/testdata/amp-mock.sh index 259db57ad..2285fbb6f 100644 --- a/registry/coder-labs/modules/sourcegraph-amp/testdata/amp-mock.sh +++ b/registry/coder-labs/modules/sourcegraph-amp/testdata/amp-mock.sh @@ -1,14 +1,9 @@ -#!/bin/bash +#!/usr/bin/env bash -# Mock behavior of the AMP CLI if [[ "$1" == "--version" ]]; then - echo "AMP CLI mock version v1.0.0" + echo "0.0.1700000000-gmock00 (released 2026-01-01T00:00:00.000Z, 1d ago)" exit 0 fi -# Simulate AMP running in a loop for AgentAPI to connect -set -e -while true; do - echo "$(date) - AMP mock is running..." - sleep 15 -done +echo "amp invoked with: $*" +exit 0