Skip to main content

relayburn_cli/harnesses/
opencode.rs

1//! OpenCode `HarnessAdapter` — Rust port of `packages/cli/src/harnesses/opencode.ts`.
2//!
3//! OpenCode shares the pending-stamp + watch-loop shape with codex, so the
4//! adapter is constructed via [`super::pending_stamp::adapter_static`]
5//! instead of re-implementing the trait. The only opencode-specific bits are:
6//!
7//! * `name = "opencode"` — the dispatch key and log-line label.
8//! * `session_root` — `$HOME/.local/share/opencode/storage/session`,
9//!   resolved lazily so tests that override `$HOME` see the override.
10//!   Mirrors the TS sibling's `path.join(homedir(), '.local', 'share',
11//!   'opencode', 'storage', 'session')` exactly.
12//! * `ingest_sessions` — opens a fresh ledger handle and runs
13//!   [`relayburn_sdk::ingest_opencode_sessions`] (the opencode-only ingest
14//!   pass). The TS sibling calls `ingestOpencodeSessions()` directly here;
15//!   the Rust SDK function takes `&mut Ledger`, so the closure opens a
16//!   handle each call. That mirrors the TS lock-then-write-then-close
17//!   shape, and the per-tick open is cheap (SQLite WAL, no DDL after first
18//!   open).
19//!
20//! The factory's [`super::pending_stamp::adapter_static`] does the
21//! `Box::leak` so the registry can store the result as
22//! `&'static dyn HarnessAdapter`. See the factory module for the leak
23//! rationale (codex/opencode are the only two callers; runtime cost is
24//! a few dozen bytes per process).
25
26use std::path::PathBuf;
27use std::sync::Arc;
28
29use relayburn_sdk::{ingest_opencode_sessions, Ledger, LedgerOpenOptions, RawIngestOptions};
30
31use super::pending_stamp::{self, IngestSessionsFn, PendingStampAdapter};
32use super::HarnessAdapter;
33
34/// Resolve the opencode session-store root. Mirrors the TS sibling
35/// (`path.join(homedir(), '.local', 'share', 'opencode', 'storage',
36/// 'session')`) and the SDK's internal `opencode_sessions_dir` default.
37/// Resolved on every call so tests that flip `$HOME` between runs see
38/// the override.
39fn opencode_sessions_dir() -> PathBuf {
40    let home = std::env::var_os("HOME")
41        .map(PathBuf::from)
42        .unwrap_or_else(|| PathBuf::from("."));
43    home.join(".local")
44        .join("share")
45        .join("opencode")
46        .join("storage")
47        .join("session")
48}
49
50/// Build the [`PendingStampAdapter`] config for opencode. Exposed as a
51/// constructor function (rather than a `static`) because the closure
52/// captures and the `Arc<dyn Fn>`s inside don't fit a const initializer.
53/// The registry calls this once and feeds the result to
54/// [`pending_stamp::adapter_static`].
55pub fn config() -> PendingStampAdapter {
56    let session_root: Arc<dyn Fn() -> PathBuf + Send + Sync> = Arc::new(opencode_sessions_dir);
57    let ingest_sessions: IngestSessionsFn = Arc::new(|ledger_home| {
58        Box::pin(async move {
59            // Open a fresh ledger handle per tick. The TS sibling's
60            // `ingestOpencodeSessions` does the same via `withLock('ledger', …)`;
61            // SQLite WAL keeps the per-call open cheap. Use the same typed
62            // ledger home the pending-stamp writer used so explicit
63            // `--ledger-path` runs keep manifest writes and resolution scoped
64            // to one home.
65            let ledger_opts = match ledger_home.as_deref() {
66                Some(home) => LedgerOpenOptions::with_home(home),
67                None => LedgerOpenOptions::default(),
68            };
69            let mut handle = Ledger::open(ledger_opts)?;
70            let opts = RawIngestOptions {
71                ledger_home,
72                ..RawIngestOptions::default()
73            };
74            ingest_opencode_sessions(handle.raw_mut(), &opts).await
75        })
76    });
77    PendingStampAdapter::new("opencode", session_root, ingest_sessions)
78}
79
80/// Convenience: hand out a `&'static dyn HarnessAdapter` for the opencode
81/// adapter. The registry calls this once at lazy-init time. See
82/// [`pending_stamp::adapter_static`] for the leak semantics — opencode is
83/// one of two callers and the leaked footprint is bytes, not megabytes.
84pub fn adapter() -> &'static dyn HarnessAdapter {
85    pending_stamp::adapter_static(config())
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use crate::harnesses::test_env::with_test_home;
92
93    /// `config()` returns a `PendingStampAdapter` named `opencode` with
94    /// the standard 1s tick interval. Sanity check that the constructor
95    /// wires the name through the factory contract and that the
96    /// `session_root` closure resolves to the TS-mirrored path.
97    #[test]
98    fn config_has_opencode_name() {
99        let cfg = config();
100        assert_eq!(cfg.name, "opencode");
101        // session_root closure resolves to
102        // `$HOME/.local/share/opencode/storage/session`. Use a controlled
103        // $HOME so the assertion doesn't depend on the developer's actual
104        // home dir; restored after via `with_test_home`.
105        with_test_home("/tmp/burn-opencode-test-home", || {
106            let resolved = (cfg.session_root)();
107            assert_eq!(
108                resolved,
109                PathBuf::from(
110                    "/tmp/burn-opencode-test-home/.local/share/opencode/storage/session"
111                )
112            );
113        });
114    }
115
116    /// `adapter()` round-trips through the trait surface — name, session
117    /// root, and the `&'static` lifetime the registry requires. Mirrors
118    /// the registry's `pending_stamp_adapter_static_fits_runtime_registry`
119    /// check, but pinned to the opencode configuration specifically.
120    #[test]
121    fn adapter_round_trip() {
122        let a: &'static dyn HarnessAdapter = adapter();
123        assert_eq!(a.name(), "opencode");
124        with_test_home("/tmp/burn-opencode-test-home", || {
125            assert_eq!(
126                a.session_root(),
127                PathBuf::from(
128                    "/tmp/burn-opencode-test-home/.local/share/opencode/storage/session"
129                )
130            );
131        });
132    }
133}