Skip to content

Commit a4ad73e

Browse files
committed
refactor(cli): share runtime health/status core across host and container
1 parent 0bb87a7 commit a4ad73e

10 files changed

Lines changed: 736 additions & 327 deletions

File tree

docs/update-flow.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,15 @@ These are not implemented but are viable options for future work:
170170
3. **Docker socket from container**
171171
- Webapp triggers a helper that calls Docker directly.
172172
- Not recommended due to high privileges in the container.
173+
174+
## Runtime Parity Refactor Checklist
175+
176+
This project now has a shared runtime command core in
177+
`packages/cli-rust/src/commands/runtime_shared/` to prevent host/container drift.
178+
179+
- [x] Move status health mapping + probes into shared runtime core.
180+
- [x] Route host/container status implementations through shared status model.
181+
- [x] Reuse shared broker readiness semantics in startup readiness checks.
182+
- [ ] Migrate logs command builder/normalization into shared runtime core.
183+
- [ ] Migrate user command domain flow into shared runtime core.
184+
- [ ] Migrate `update opencode` shared checks/restart semantics into shared runtime core.

packages/cli-rust/src/commands/container/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
//! Container-mode command implementations and helpers.
2+
//!
3+
//! Runtime parity note:
4+
//! - Dual-runtime commands should keep domain semantics in `commands/runtime_shared`.
5+
//! - Container modules should stay as thin runtime adapters and delegate shared logic.
6+
//! - Remaining parity migrations:
7+
//! - `logs`
8+
//! - `user`
9+
//! - `update opencode`
210
311
pub mod logs;
412
pub mod status;
Lines changed: 39 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,18 @@
11
//! Container-mode status command implementation.
22
3-
use crate::commands::container::{exec_command, exec_command_with_status, systemd_available};
3+
use crate::commands::container::{exec_command_with_status, systemd_available};
4+
use crate::commands::runtime_shared::backend::{ContainerBackend, default_container_port};
5+
use crate::commands::runtime_shared::collect_status_view;
6+
use crate::commands::runtime_shared::status_model::{
7+
OpencodeHealthStatus, format_broker_health_label,
8+
};
49
use crate::output::state_style;
510
use anyhow::Result;
611
use console::style;
7-
use opencode_cloud_core::docker::{HealthError, OPENCODE_WEB_PORT, check_health, get_cli_version};
8-
use std::fs;
9-
use std::path::Path;
12+
use opencode_cloud_core::docker::get_cli_version;
1013

1114
const STATUS_LABEL_WIDTH: usize = 15;
1215

13-
enum OpencodeHealthStatus {
14-
Healthy,
15-
Starting,
16-
Unhealthy,
17-
Failed,
18-
}
19-
20-
#[derive(Clone, Copy)]
21-
enum BrokerHealthStatus {
22-
Healthy,
23-
Degraded,
24-
Unhealthy,
25-
}
26-
2716
pub async fn cmd_status_container(
2817
_args: &crate::commands::StatusArgs,
2918
quiet: bool,
@@ -45,31 +34,17 @@ pub async fn cmd_status_container(
4534
}
4635
}
4736

48-
let broker_running = if systemd {
49-
systemd_service_active("opencode-broker.service").await?
50-
} else {
51-
process_running("pgrep", &["-x", "opencode-broker"]).await?
52-
};
53-
54-
let broker_socket = Path::new("/run/opencode/auth.sock").exists();
55-
let broker_health = map_broker_health_status(broker_running, broker_socket);
56-
57-
let opencode_version = read_opencode_version().await;
58-
let opencode_commit = read_opencode_commit();
59-
let opencode_cloud_version = read_opencode_cloud_version();
60-
61-
let health = if opencode_running {
62-
get_opencode_health_status(true).await
63-
} else {
64-
None
65-
};
37+
let backend = ContainerBackend::new(systemd);
38+
let host_port = default_container_port();
39+
let status_view =
40+
collect_status_view(&backend, opencode_running, "127.0.0.1", host_port).await?;
6641

6742
let state_label = if opencode_running {
68-
match health {
43+
match status_view.opencode_health {
6944
Some(OpencodeHealthStatus::Healthy) | None => "running".to_string(),
7045
Some(OpencodeHealthStatus::Starting) => "starting".to_string(),
71-
Some(OpencodeHealthStatus::Unhealthy) => "unhealthy".to_string(),
72-
Some(OpencodeHealthStatus::Failed) => "unknown".to_string(),
46+
Some(OpencodeHealthStatus::Unhealthy(_)) => "unhealthy".to_string(),
47+
Some(OpencodeHealthStatus::CheckFailed) => "unknown".to_string(),
7348
}
7449
} else {
7550
"stopped".to_string()
@@ -80,28 +55,33 @@ pub async fn cmd_status_container(
8055
"{}",
8156
format_kv(
8257
"URL:",
83-
style(format!("http://127.0.0.1:{OPENCODE_WEB_PORT}")).cyan()
58+
style(format!("http://127.0.0.1:{host_port}")).cyan()
8459
)
8560
);
8661

87-
let opencode_display = match (opencode_version.as_deref(), opencode_commit.as_deref()) {
88-
(Some(version), Some(commit)) => format!("{version} ({commit})"),
89-
(Some(version), None) => version.to_string(),
90-
(None, Some(commit)) => format!("unknown ({commit})"),
91-
(None, None) => "unknown".to_string(),
92-
};
62+
let opencode_display =
63+
format_opencode_display(&status_view.opencode_version, &status_view.opencode_commit);
9364
println!("{}", format_kv("Opencode:", opencode_display));
9465

9566
println!(
9667
"{}",
97-
format_kv("Broker:", format_broker_health_status(broker_health))
68+
format_kv(
69+
"Broker:",
70+
format_broker_health_label(status_view.broker_health)
71+
)
9872
);
9973

100-
let runtime = if systemd { "systemd" } else { "tini" };
74+
let runtime = if status_view
75+
.capabilities
76+
.systemd_available
77+
.unwrap_or(systemd)
78+
{
79+
"systemd"
80+
} else {
81+
"tini"
82+
};
10183
println!("{}", format_kv("Runtime:", runtime));
102-
103-
let image_version = opencode_cloud_version.unwrap_or_else(|| "unknown".to_string());
104-
println!("{}", format_kv("Image:", image_version));
84+
println!("{}", format_kv("Image:", &status_view.image_version));
10585

10686
let cli_version = get_cli_version();
10787
println!("{}", format_kv("CLI:", format!("v{cli_version}")));
@@ -110,78 +90,24 @@ pub async fn cmd_status_container(
11090
}
11191

11292
async fn systemd_service_active(service: &str) -> Result<bool> {
113-
let (output, _status) = exec_command_with_status("systemctl", &["is-active", service]).await?;
114-
Ok(output.lines().next().map(|line| line.trim()) == Some("active"))
93+
let (_output, status) = exec_command_with_status("systemctl", &["is-active", service]).await?;
94+
Ok(status == 0)
11595
}
11696

11797
async fn process_running(cmd: &str, args: &[&str]) -> Result<bool> {
11898
let (_output, status) = exec_command_with_status(cmd, args).await?;
11999
Ok(status == 0)
120100
}
121101

122-
async fn read_opencode_version() -> Option<String> {
123-
let output = exec_command("/opt/opencode/bin/opencode", &["--version"])
124-
.await
125-
.ok()?;
126-
let version = output.lines().next()?.trim();
127-
if version.is_empty() {
128-
None
129-
} else {
130-
Some(version.to_string())
131-
}
132-
}
133-
134-
fn read_opencode_commit() -> Option<String> {
135-
let contents = fs::read_to_string("/opt/opencode/COMMIT").ok()?;
136-
let commit = contents.lines().next()?.trim();
137-
if commit.is_empty() {
138-
None
139-
} else {
140-
Some(commit.chars().take(7).collect())
141-
}
142-
}
143-
144-
fn read_opencode_cloud_version() -> Option<String> {
145-
let contents = fs::read_to_string("/etc/opencode-cloud-version").ok()?;
146-
let version = contents.lines().next()?.trim();
147-
if version.is_empty() {
148-
None
149-
} else {
150-
Some(version.to_string())
151-
}
152-
}
153-
154-
async fn get_opencode_health_status(include_probe: bool) -> Option<OpencodeHealthStatus> {
155-
if !include_probe {
156-
return None;
157-
}
158-
159-
match check_health("127.0.0.1", OPENCODE_WEB_PORT).await {
160-
Ok(_) => Some(OpencodeHealthStatus::Healthy),
161-
Err(HealthError::ConnectionRefused) | Err(HealthError::Timeout) => {
162-
Some(OpencodeHealthStatus::Starting)
163-
}
164-
Err(HealthError::Unhealthy(_code)) => Some(OpencodeHealthStatus::Unhealthy),
165-
Err(_) => Some(OpencodeHealthStatus::Failed),
102+
fn format_opencode_display(version: &str, commit: &str) -> String {
103+
match (version, commit) {
104+
("unknown", "unknown") => "unknown".to_string(),
105+
("unknown", commit) => format!("unknown ({commit})"),
106+
(version, "unknown") => version.to_string(),
107+
(version, commit) => format!("{version} ({commit})"),
166108
}
167109
}
168110

169111
fn format_kv(label: &str, value: impl std::fmt::Display) -> String {
170112
format!("{label:<STATUS_LABEL_WIDTH$} {value}")
171113
}
172-
173-
fn map_broker_health_status(process_ok: bool, socket_ok: bool) -> BrokerHealthStatus {
174-
match (process_ok, socket_ok) {
175-
(true, true) => BrokerHealthStatus::Healthy,
176-
(true, false) | (false, true) => BrokerHealthStatus::Degraded,
177-
(false, false) => BrokerHealthStatus::Unhealthy,
178-
}
179-
}
180-
181-
fn format_broker_health_status(status: BrokerHealthStatus) -> String {
182-
match status {
183-
BrokerHealthStatus::Healthy => style("Healthy").green().to_string(),
184-
BrokerHealthStatus::Degraded => style("Degraded").yellow().to_string(),
185-
BrokerHealthStatus::Unhealthy => style("Unhealthy").red().to_string(),
186-
}
187-
}

packages/cli-rust/src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ mod logs;
1313
mod mount;
1414
mod reset;
1515
mod restart;
16+
pub(crate) mod runtime_shared;
1617
mod service;
1718
mod setup;
1819
mod start;

0 commit comments

Comments
 (0)