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::session_store_adapter`]
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` — defers to
13//! [`relayburn_sdk::ingest_opencode_sessions`], the opencode-only ingest
14//! pass. The factory opens a fresh ledger handle per call (mirrors the
15//! TS lock-then-write-then-close shape; SQLite WAL keeps the per-tick
16//! open cheap).
17
18use std::future::Future;
19use std::path::PathBuf;
20use std::pin::Pin;
21
22use relayburn_sdk::{ingest_opencode_sessions, IngestReport, RawIngestOptions, RawLedger};
23
24use super::pending_stamp;
25use super::HarnessAdapter;
26use crate::util::home::home_dir;
27
28/// `$HOME/.local/share/opencode/storage/session`. Mirrors the TS sibling
29/// and the SDK's internal `opencode_sessions_dir` default.
30fn opencode_sessions_dir() -> PathBuf {
31 home_dir()
32 .join(".local")
33 .join("share")
34 .join("opencode")
35 .join("storage")
36 .join("session")
37}
38
39/// Box-pin the SDK's `async fn ingest_opencode_sessions` into a fn
40/// pointer the [`pending_stamp::SessionIngestor`] type alias accepts.
41fn opencode_ingest<'a>(
42 ledger: &'a mut RawLedger,
43 opts: &'a RawIngestOptions,
44) -> Pin<Box<dyn Future<Output = anyhow::Result<IngestReport>> + Send + 'a>> {
45 Box::pin(ingest_opencode_sessions(ledger, opts))
46}
47
48/// Hand out a `&'static dyn HarnessAdapter` for opencode. The registry
49/// calls this once at lazy-init time. See
50/// [`pending_stamp::session_store_adapter`] for the leak semantics.
51pub fn adapter() -> &'static dyn HarnessAdapter {
52 pending_stamp::session_store_adapter("opencode", opencode_sessions_dir, opencode_ingest)
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58 use crate::harnesses::test_env::with_test_home;
59
60 /// `adapter()` round-trips through the trait surface — name, session
61 /// root, and the `&'static` lifetime the registry requires. Mirrors
62 /// the registry's `pending_stamp_adapter_static_fits_runtime_registry`
63 /// check, but pinned to the opencode configuration specifically.
64 #[test]
65 fn adapter_round_trip() {
66 let a: &'static dyn HarnessAdapter = adapter();
67 assert_eq!(a.name(), "opencode");
68 with_test_home("/tmp/burn-opencode-test-home", || {
69 assert_eq!(
70 a.session_root(),
71 PathBuf::from("/tmp/burn-opencode-test-home/.local/share/opencode/storage/session")
72 );
73 });
74 }
75}