diff --git a/packages/opencode/src/format/formatter.ts b/packages/opencode/src/format/formatter.ts index d34144e7a..bcde6e7aa 100644 --- a/packages/opencode/src/format/formatter.ts +++ b/packages/opencode/src/format/formatter.ts @@ -266,3 +266,12 @@ export const ocamlformat: Info = { return items.length > 0 }, } + +export const terraform: Info = { + name: "terraform", + command: ["terraform", "fmt", "$FILE"], + extensions: [".tf", ".tfvars"], + async enabled() { + return Bun.which("terraform") !== null + }, +} diff --git a/packages/opencode/src/lsp/language.ts b/packages/opencode/src/lsp/language.ts index 7980f05e8..918661ed5 100644 --- a/packages/opencode/src/lsp/language.ts +++ b/packages/opencode/src/lsp/language.ts @@ -103,4 +103,7 @@ export const LANGUAGE_EXTENSIONS: Record = { ".zig": "zig", ".zon": "zig", ".astro": "astro", + ".tf": "terraform", + ".tfvars": "terraform-vars", + ".hcl": "hcl", } as const diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index 7b250c581..c2c70229d 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -1223,4 +1223,85 @@ export namespace LSPServer { } }, } + + export const TerraformLS: Info = { + id: "terraform", + extensions: [".tf", ".tfvars"], + root: NearestRoot([".terraform.lock.hcl", "terraform.tfstate", "*.tf"]), + async spawn(root) { + let bin = Bun.which("terraform-ls", { + PATH: process.env["PATH"] + ":" + Global.Path.bin, + }) + + if (!bin) { + if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return + log.info("downloading terraform-ls from GitHub releases") + + const releaseResponse = await fetch("https://api.github.com/repos/hashicorp/terraform-ls/releases/latest") + if (!releaseResponse.ok) { + log.error("Failed to fetch terraform-ls release info") + return + } + + const release = (await releaseResponse.json()) as { tag_name?: string; assets?: { name?: string; browser_download_url?: string }[] } + const version = release.tag_name?.replace("v", "") + if (!version) { + log.error("terraform-ls release did not include a version tag") + return + } + + const platform = process.platform + const arch = process.arch + + const tfArch = arch === "arm64" ? "arm64" : "amd64" + const tfPlatform = platform === "win32" ? "windows" : platform + + const assetName = `terraform-ls_${version}_${tfPlatform}_${tfArch}.zip` + + const assets = release.assets ?? [] + const asset = assets.find((a) => a.name === assetName) + if (!asset?.browser_download_url) { + log.error(`Could not find asset ${assetName} in terraform-ls release`) + return + } + + const downloadResponse = await fetch(asset.browser_download_url) + if (!downloadResponse.ok) { + log.error("Failed to download terraform-ls") + return + } + + const tempPath = path.join(Global.Path.bin, assetName) + await Bun.file(tempPath).write(downloadResponse) + + await $`unzip -o -q ${tempPath}`.cwd(Global.Path.bin).nothrow() + await fs.rm(tempPath, { force: true }) + + bin = path.join(Global.Path.bin, "terraform-ls" + (platform === "win32" ? ".exe" : "")) + + if (!(await Bun.file(bin).exists())) { + log.error("Failed to extract terraform-ls binary") + return + } + + if (platform !== "win32") { + await $`chmod +x ${bin}`.nothrow() + } + + log.info(`installed terraform-ls`, { bin }) + } + + return { + process: spawn(bin, ["serve"], { + cwd: root, + }), + initialization: { + experimentalFeatures: { + prefillRequiredFields: true, + validateOnSave: true, + }, + }, + } + }, + } }