Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 46 additions & 10 deletions lua/opencode/cli/client.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
25 changes: 25 additions & 0 deletions lua/opencode/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ vim.g.opencode_opts = vim.g.opencode_opts
---If set, `opencode.nvim` will append `--port <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<string, fun(context: opencode.Context): string|nil>
---
Expand All @@ -38,13 +46,22 @@ 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.

---@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,
Expand Down Expand Up @@ -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 {}
Expand Down