package process import ( "fmt" "os" "os/exec" "sync" "codespace/internal/util" ) // Manager manages OpenCode processes per workspace. type Manager interface { Start(workspaceID string, workspaceRoot string) error Stop(workspaceID string) error Restart(workspaceID string, workspaceRoot string) error Status(workspaceID string) Status } // LocalManager implements Manager using local OS processes. type LocalManager struct { command string args []string mu sync.Mutex sessions map[string]*Session } // NewManager creates a LocalManager with the given command. // If command is empty, it defaults to "opencode". func NewManager(command string) *LocalManager { return &LocalManager{ command: normalizeCommand(command), args: []string{}, sessions: make(map[string]*Session), } } // Start launches a process for the given workspace. // Returns CodeConflict if a session is already running. func (m *LocalManager) Start(workspaceID string, workspaceRoot string) error { m.mu.Lock() defer m.mu.Unlock() if sess, ok := m.sessions[workspaceID]; ok && sess.Cmd.Process != nil { if sess.Cmd.ProcessState == nil { return util.New(util.CodeConflict, "process already running") } } cmd := exec.Command(m.command, m.args...) cmd.Dir = workspaceRoot cmd.Env = append(os.Environ(), fmt.Sprintf("HOME=%s", workspaceRoot)) if err := cmd.Start(); err != nil { return util.Wrap(util.CodeInternal, "failed to start process", err) } m.sessions[workspaceID] = &Session{ WorkspaceID: workspaceID, Root: workspaceRoot, Cmd: cmd, } return nil } // Stop kills the process for the given workspace. // Returns CodeNotFound if no session exists. func (m *LocalManager) Stop(workspaceID string) error { m.mu.Lock() defer m.mu.Unlock() sess, ok := m.sessions[workspaceID] if !ok || sess.Cmd.Process == nil { return util.New(util.CodeNotFound, "no running process for workspace") } if err := sess.Cmd.Process.Kill(); err != nil { return util.Wrap(util.CodeInternal, "failed to stop process", err) } delete(m.sessions, workspaceID) return nil } // Restart stops (if running) then starts the process. func (m *LocalManager) Restart(workspaceID string, workspaceRoot string) error { m.mu.Lock() if sess, ok := m.sessions[workspaceID]; ok && sess.Cmd.Process != nil { if sess.Cmd.ProcessState == nil { _ = sess.Cmd.Process.Kill() delete(m.sessions, workspaceID) } } m.mu.Unlock() return m.Start(workspaceID, workspaceRoot) } // Status returns the current process status for the workspace. func (m *LocalManager) Status(workspaceID string) Status { m.mu.Lock() defer m.mu.Unlock() sess, ok := m.sessions[workspaceID] if !ok || sess.Cmd.Process == nil { return Status{WorkspaceID: workspaceID, Running: false} } if sess.Cmd.ProcessState != nil { return Status{WorkspaceID: workspaceID, Running: false} } return Status{ WorkspaceID: workspaceID, Running: true, PID: sess.Cmd.Process.Pid, } }