use clap::{Parser, Subcommand};
use onlyne_client::{
ClientInit, ops::init::InitArgs, ops::local_cli, ops::local_cli::LocalCli, runtime::daemon,
runtime::intent::IntentMachine,
};
use onlyne_proto::QueryRolesArgs;
use std::path::{Path, PathBuf};
#[derive(Parser)]
#[command(name = "onlyne-client", version)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
#[command(
after_help = "config: --workspace names the role workspace; its `.onlyne/config.toml` \
carries `backend` (herdr|orca|zellij|exec|headless|acp|fake|auto; empty \
probes the host, ONLYNE_BACKEND wins) and, for the acp backend, the \
`[acp]` table: `mode`, `model`, `reasoning_effort` (each validated by \
the agent, empty keeps its default) and `permission` (`deny`, the \
default, or `allow`)."
)]
Run {
#[arg(long)]
workspace: PathBuf,
},
Init {
#[arg(long)]
workspace: PathBuf,
#[arg(long)]
role: String,
#[arg(long = "server-root")]
server_root: PathBuf,
#[arg(
long,
default_value = "",
help = "Role control plane text; spec.toml holds it per plan §5 and the client caches it from welcome into prose_cache"
)]
prose: String,
},
Status {
#[arg(long)]
workspace: PathBuf,
},
Roles {
#[arg(long)]
workspace: PathBuf,
},
Sessions {
#[arg(long)]
workspace: PathBuf,
},
Watch {
#[arg(long)]
workspace: PathBuf,
},
History {
#[arg(long)]
workspace: PathBuf,
},
Agent {
#[command(subcommand)]
command: AgentCommand,
},
Doctor,
}
#[derive(Subcommand)]
enum AgentCommand {
Install {
#[arg(long)]
workspace: PathBuf,
#[arg(long)]
package: PathBuf,
#[arg(long)]
id: String,
#[arg(long)]
agent: Option<String>,
},
Uninstall {
#[arg(long)]
workspace: PathBuf,
#[arg(long)]
id: String,
},
}
#[tokio::main]
async fn main() {
init_logging();
let cli = Cli::parse();
let code = match cli.command {
Command::Init {
workspace,
role,
server_root,
prose,
} => match onlyne_client::ops::init::init(InitArgs {
workspace,
role,
server_root,
prose,
})
.await
{
Ok(fragment) => {
print!("{fragment}");
0
}
Err(error) if error.to_string() == "legacy workspace" => 2,
Err(error) => {
eprintln!("onlyne-client: {error}");
1
}
},
Command::Run { workspace } => {
let workspace = onlyne_layout::absolute_path(&workspace);
let path = onlyne_layout::RoleWorkspace::resolve(&workspace);
local_cli::heal_workspace_config(&workspace);
match load_workspace_config(&path.config_path()) {
Ok(config) => match onlyne_client::run(
ClientInit::new(
workspace,
config.role,
format!("{}:{}", config.server.host, config.server.port),
path.resolve_key_path(&config.key_path),
config.cert_pin,
)
.with_orca_worktree(config.orca.worktree)
.with_stall_report_secs(config.stall_report_secs)
.with_reconnect_grace_secs(config.reconnect_grace_secs)
.with_backend(config.backend)
.with_acp(config.acp),
)
.await
{
Ok(()) => 0,
Err(error)
if error
.downcast_ref::<onlyne_session::NoSupportedHost>()
.is_some() =>
{
eprintln!("{error}");
5
}
Err(error) => {
eprintln!("onlyne-client: {error:#}");
1
}
},
Err(error) => {
eprintln!("onlyne-client: {error}");
1
}
}
}
Command::Status { workspace } => match daemon::status(&workspace).await {
Ok(Some(report)) => {
println!("{}", report.line());
if !report.connected {
eprintln!("{}", daemon::NOT_CONNECTED);
}
report.exit_code()
}
Ok(None) => {
eprintln!("{}", daemon::NOT_RUNNING);
2
}
Err(error) => {
eprintln!("onlyne-client: {error:#}");
1
}
},
Command::Roles { workspace } => local_roles(&workspace),
Command::Sessions { .. } | Command::Watch { .. } | Command::History { .. } => {
eprintln!(
"onlyne: no local query socket here; these three answer on the admin surface as \
onlyne sessions, onlyne watch, and onlyne history"
);
1
}
Command::Agent { command } => match command {
AgentCommand::Install {
workspace,
package,
id,
agent,
} => plugin_verb(
local_cli::install_verb(&workspace, &package, &id, agent.as_deref()).await,
),
AgentCommand::Uninstall { workspace, id } => {
plugin_verb(local_cli::uninstall_verb(&workspace, &id).await)
}
},
Command::Doctor => {
let report = onlyne_client::host::doctor_report(&onlyne_session::process_env());
println!("{report}");
0
}
};
std::process::exit(code);
}
fn load_workspace_config(
path: &Path,
) -> Result<onlyne_config::ClientConfig, onlyne_config::SpecError> {
let mut config = onlyne_config::ClientConfig::load(path)?;
config.resolve_secrets(&onlyne_config::Env::current())?;
Ok(config)
}
fn init_logging() {
use tracing_subscriber::EnvFilter;
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
let _ = tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.try_init();
}
fn plugin_verb(result: anyhow::Result<local_cli::PluginAction>) -> i32 {
match result {
Ok(action) => {
for line in &action.lines {
println!("{line}");
}
if let Some(hint) = &action.hint {
eprintln!("{hint}");
}
0
}
Err(error) => {
eprintln!("{error}");
local_cli::plugin_exit_code(&error)
}
}
}
fn local_roles(workspace: &Path) -> i32 {
let layout = onlyne_layout::RoleWorkspace::resolve(workspace);
if !layout.client_db_path().exists() {
println!("{}", serde_json::json!({"ok": true, "data": {"roles": []}}));
return 0;
}
let store = match onlyne_store::ClientStore::open(layout.client_db_path()) {
Ok(store) => store,
Err(error) => {
eprintln!("onlyne-client: {error}");
return 1;
}
};
let cli = LocalCli::new(IntentMachine::new(store, 0, Vec::new()));
match cli.query_roles_local(&QueryRolesArgs { role: None }) {
Ok(body) => {
println!("{}", serde_json::json!({"ok": body.ok, "data": body.data}));
0
}
Err(error) => {
eprintln!("onlyne-client: {error}");
1
}
}
}
#[cfg(test)]
mod tests {
use super::load_workspace_config;
#[test]
fn missing_env_secret_names_the_variable_in_the_error() {
let var = format!("ONLYNE_TEST_MISSING_PIN_{}", std::process::id());
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
std::fs::write(
&path,
format!(
"role = \"planner\"\ncert_pin = \"${var}\"\nkey_path = \"keys/role.key\"\n\n[server]\nhost = \"127.0.0.1\"\nport = 7811\n"
),
)
.unwrap();
let error = load_workspace_config(&path).unwrap_err();
assert_eq!(
error.to_string(),
format!("missing secret ${var} for cert_pin; set the environment variable")
);
}
}