local util = require('opencode.util')
local safe_call = util.safe_call
local Promise = require('opencode.promise')
local config = require('opencode.config')
--- @class OpencodeServer
--- @field job any The vim.system job handle
--- @field url string|nil The server URL once ready
--- @field handle any Compatibility property for job.stop interface
--- @field spawn_promise Promise
--- @field shutdown_promise Promise
local OpencodeServer = {}
OpencodeServer.__index = OpencodeServer
local vim_leave_setup = false
local function ensure_vim_leave_autocmd()
if vim_leave_setup then
return
end
vim_leave_setup = true
vim.api.nvim_create_autocmd('VimLeavePre', {
group = vim.api.nvim_create_augroup('OpencodeVimLeavePre', { clear = true }),
callback = function()
local state = require('opencode.state')
if state.opencode_server then
pcall(function()
state.opencode_server:shutdown():wait(2000)
end)
end
end,
})
end
--- Create a new ServerJob instance
--- @return OpencodeServer
function OpencodeServer.new()
ensure_vim_leave_autocmd()
return setmetatable({
job = nil,
url = nil,
handle = nil,
spawn_promise = Promise.new(),
shutdown_promise = Promise.new(),
}, OpencodeServer)
end
function OpencodeServer:is_running()
return self.job and self.job.pid ~= nil
end
--- Clean up this server job
--- @return Promise
function OpencodeServer:shutdown()
if self.shutdown_promise:is_resolved() then
return self.shutdown_promise
end
if self.job and self.job.pid then
local job = self.job
self.job = nil
self.url = nil
self.handle = nil
pcall(function()
job:kill(15) -- SIGTERM
end)
vim.defer_fn(function()
if job and job.pid then
pcall(function()
job:kill(9) -- SIGKILL
end)
end
end, 500)
else
self.shutdown_promise:resolve(true)
end
return self.shutdown_promise
end
--- @class OpencodeServerSpawnOpts
--- @field cwd? string
--- @field on_ready fun(job: any, url: string)
--- @field on_error fun(err: any)
--- @field on_exit fun(exit_opts: vim.SystemCompleted )
--- Spawn the opencode server for this ServerJob instance.
--- @param opts? OpencodeServerSpawnOpts
--- @return Promise
function OpencodeServer:spawn(opts)
opts = opts or {}
self.job = vim.system({
config.opencode_executable,
'serve',
}, {
cwd = opts.cwd,
stdout = function(err, data)
if err then
safe_call(opts.on_error, err)
return
end
if data then
local url = data:match('opencode server listening on ([^%s]+)')
if url then
self.url = url
self.spawn_promise:resolve(self)
safe_call(opts.on_ready, self.job, url)
end
end
end,
stderr = function(err, data)
if err then
self.spawn_promise:reject(err)
safe_call(opts.on_error, err)
return
end
if data then
-- Filter out INFO/WARN/DEBUG log lines (not actual errors)
local log_level = data:match('^%s*(%u+)%s')
if log_level and (log_level == 'INFO' or log_level == 'WARN' or log_level == 'DEBUG') then
-- Ignore log lines, don't reject
return
end
-- Only reject on actual errors
self.spawn_promise:reject(data)
safe_call(opts.on_error, data)
end
end,
}, function(exit_opts)
-- Clear fields if not already cleared by shutdown()
self.job = nil
self.url = nil
self.handle = nil
safe_call(opts.on_exit, exit_opts)
self.shutdown_promise:resolve(true)
end)
self.handle = self.job and self.job.pid
return self.spawn_promise
end
function OpencodeServer:get_shutdown_promise()
return self.shutdown_promise
end
function OpencodeServer:get_spawn_promise()
return self.spawn_promise
end
return OpencodeServer