diff --git a/lua/opencode/cli/client.lua b/lua/opencode/cli/client.lua index c4b1913..486c29f 100644 --- a/lua/opencode/cli/client.lua +++ b/lua/opencode/cli/client.lua @@ -48,6 +48,23 @@ local function generate_uuid() ) end +---Resolve the full URL for a given port and path, ensuring a valid hostname. +---@param port number +---@param path string +---@return string +local function resolve_server_url(port, path) + local config = require("opencode.config") + local hostname = config.opts.hostname + -- fallback to localhost as config is basically not validated + if not hostname or hostname == "" then + hostname = "localhost" + end + if not vim.startswith(path, "/") then + path = "/" .. path + end + return string.format("http://%s:%d%s", hostname, port, path) +end + ---@param url string ---@param method string ---@param body table|nil @@ -70,6 +87,13 @@ local function curl(url, method, body, callback) "-N", -- No buffering, for streaming SSEs } + -- Add basic auth if credentials are available + local auth = require("opencode.config").opts.auth + if auth and auth.username and auth.password and auth.password ~= "" then + table.insert(command, "-u") + table.insert(command, auth.username .. ":" .. auth.password) + end + if body then table.insert(command, "-d") table.insert(command, vim.fn.json_encode(body)) @@ -151,7 +175,8 @@ end ---@param callback fun(response: table)|nil ---@return number job_id function M.call(port, path, method, body, callback) - return curl("http://localhost:" .. port .. path, method, body, callback) + local url = resolve_server_url(port, path) + return curl(url, method, body, callback) end ---@param text string @@ -260,15 +285,26 @@ end function M.get_path(port) -- Query each port synchronously for working directory -- TODO: Migrate to align with async paradigm used elsewhere - local curl_result = vim - .system({ - "curl", - "-s", - "--connect-timeout", - "1", - "http://localhost:" .. port .. "/path", - }) - :wait() + local curl_cmd = { + "curl", + "-s", + "--connect-timeout", + "1", + } + + local config = require("opencode.config") + + -- Add basic auth if credentials are available + local auth = config.opts.auth + if auth and auth.username and auth.password and auth.password ~= "" then + table.insert(curl_cmd, "-u") + table.insert(curl_cmd, auth.username .. ":" .. auth.password) + end + + local url = resolve_server_url(port, "/path") + table.insert(curl_cmd, url) + + local curl_result = vim.system(curl_cmd):wait() require("opencode.util").check_system_call(curl_result, "curl") local path_ok, path_data = pcall(vim.fn.json_decode, curl_result.stdout) diff --git a/lua/opencode/config.lua b/lua/opencode/config.lua index ca4ed22..8b5f703 100644 --- a/lua/opencode/config.lua +++ b/lua/opencode/config.lua @@ -18,6 +18,14 @@ vim.g.opencode_opts = vim.g.opencode_opts ---If set, `opencode.nvim` will append `--port ` to `provider.cmd`. ---@field port? number --- +---The hostname `opencode` server is running on. +---Defaults to `127.0.0.1`. +---@field hostname? string +--- +---Authentication configuration for the `opencode` server. +---If not set, will check for `OPENCODE_SERVER_PASSWORD` and `OPENCODE_SERVER_USERNAME` environment variables. +---@field auth? opencode.auth.Opts +--- ---Contexts to inject into prompts, keyed by their placeholder. ---@field contexts? table --- @@ -38,6 +46,10 @@ vim.g.opencode_opts = vim.g.opencode_opts ---Provide an integrated `opencode` when one is not found. ---@field provider? opencode.Provider|opencode.provider.Opts +---@class opencode.auth.Opts +---@field username? string The username for HTTP basic auth. Defaults to `opencode` if password is set. +---@field password? string The password for HTTP basic auth. + ---@class opencode.Prompt : opencode.api.prompt.Opts ---@field prompt string The prompt to send to `opencode`. ---@field ask? boolean Call `ask(prompt)` instead of `prompt(prompt)`. Useful for prompts that expect additional user input. @@ -45,6 +57,11 @@ vim.g.opencode_opts = vim.g.opencode_opts ---@type opencode.Opts local defaults = { port = nil, + hostname = "127.0.0.1", + auth = vim.env.OPENCODE_SERVER_PASSWORD and { + username = vim.env.OPENCODE_SERVER_USERNAME or "opencode", + password = vim.env.OPENCODE_SERVER_PASSWORD, + } or nil, -- stylua: ignore contexts = { ["@this"] = function(context) return context:this() end, @@ -170,6 +187,14 @@ local defaults = { ---@type opencode.Opts M.opts = vim.tbl_deep_extend("force", vim.deepcopy(defaults), vim.g.opencode_opts or {}) +if M.opts.auth then + if not M.opts.auth.password or M.opts.auth.password == "" then + M.opts.auth = nil + elseif not M.opts.auth.username then + M.opts.auth.username = "opencode" + end +end + -- Allow removing default `contexts` and `prompts` by setting them to `false` in your user config. -- TODO: Add to type definition, and apply to `opts.select.commands`. local user_opts = vim.g.opencode_opts or {}