cli: introduce connection target abstraction
This commit is contained in:
parent
450e0cddbd
commit
98c1599d1a
|
|
@ -12,6 +12,7 @@ pub mod backend_auth;
|
|||
pub mod backend_runtime;
|
||||
pub mod runtime_command;
|
||||
pub mod spawn;
|
||||
pub mod target;
|
||||
pub mod ticket_role;
|
||||
mod worker_client;
|
||||
|
||||
|
|
@ -26,6 +27,11 @@ pub use backend_runtime::{
|
|||
BackendWorkerWorkspaceSummary, BackendWorkingDirectorySummary, list_backend_workers,
|
||||
};
|
||||
pub use runtime_command::WorkerRuntimeCommand;
|
||||
pub use target::{
|
||||
BackendTarget, Dashboard, LocalTarget, Target, TargetError, TargetKind, WorkerByName,
|
||||
WorkerConnection, WorkerConnectionSelector, WorkerList, WorkerListRequest, WorkerResume,
|
||||
WorkerSpawn,
|
||||
};
|
||||
|
||||
pub use spawn::{
|
||||
SpawnConfig, SpawnError, SpawnReady, WorkerProcessLaunchConfig, WorkerProcessLaunchOptions,
|
||||
|
|
|
|||
302
crates/client/src/target.rs
Normal file
302
crates/client/src/target.rs
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
use std::fmt;
|
||||
|
||||
use crate::{BackendRuntimeListTarget, BackendRuntimeTarget, WorkerRuntimeCommand};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TargetKind {
|
||||
Local,
|
||||
Backend,
|
||||
}
|
||||
|
||||
impl fmt::Display for TargetKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Local => f.write_str("local"),
|
||||
Self::Backend => f.write_str("Backend"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct LocalTarget;
|
||||
|
||||
impl LocalTarget {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn runtime_command(&self) -> Result<WorkerRuntimeCommand, TargetError> {
|
||||
WorkerRuntimeCommand::resolve().map_err(TargetError::local_runtime_command)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LocalTarget {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct BackendTarget {
|
||||
pub base_url: String,
|
||||
pub workspace_id: Option<String>,
|
||||
}
|
||||
|
||||
impl BackendTarget {
|
||||
pub fn new(base_url: impl Into<String>, workspace_id: Option<impl Into<String>>) -> Self {
|
||||
Self {
|
||||
base_url: base_url.into(),
|
||||
workspace_id: workspace_id.map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct WorkerListRequest {
|
||||
pub runtime_id: Option<String>,
|
||||
}
|
||||
|
||||
impl WorkerListRequest {
|
||||
pub fn new(runtime_id: Option<String>) -> Self {
|
||||
Self { runtime_id }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct WorkerConnectionSelector {
|
||||
pub runtime_id: String,
|
||||
pub worker_id: String,
|
||||
}
|
||||
|
||||
impl WorkerConnectionSelector {
|
||||
pub fn new(runtime_id: impl Into<String>, worker_id: impl Into<String>) -> Self {
|
||||
Self {
|
||||
runtime_id: runtime_id.into(),
|
||||
worker_id: worker_id.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct WorkerSpawn {
|
||||
pub runtime_command: WorkerRuntimeCommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct WorkerByName {
|
||||
pub runtime_command: WorkerRuntimeCommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct WorkerResume {
|
||||
pub runtime_command: WorkerRuntimeCommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Dashboard {
|
||||
pub runtime_command: WorkerRuntimeCommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct WorkerList {
|
||||
pub target: BackendRuntimeListTarget,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct WorkerConnection {
|
||||
pub target: BackendRuntimeTarget,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TargetError {
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl TargetError {
|
||||
fn unsupported(operation: &'static str, target: TargetKind) -> Self {
|
||||
Self {
|
||||
message: format!("{operation} is not supported by {target} target"),
|
||||
}
|
||||
}
|
||||
|
||||
fn local_runtime_command(error: std::io::Error) -> Self {
|
||||
Self {
|
||||
message: format!("failed to resolve local Worker runtime command: {error}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TargetError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.message)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for TargetError {}
|
||||
|
||||
pub trait Target: fmt::Debug + Send + Sync {
|
||||
fn kind(&self) -> TargetKind;
|
||||
|
||||
fn spawn_worker(&self) -> Result<WorkerSpawn, TargetError>;
|
||||
|
||||
fn worker_by_name(&self) -> Result<WorkerByName, TargetError>;
|
||||
|
||||
fn resume_worker(&self) -> Result<WorkerResume, TargetError>;
|
||||
|
||||
fn dashboard(&self) -> Result<Dashboard, TargetError>;
|
||||
|
||||
fn list_workers(&self, request: WorkerListRequest) -> Result<WorkerList, TargetError>;
|
||||
|
||||
fn connect_worker(
|
||||
&self,
|
||||
selector: WorkerConnectionSelector,
|
||||
) -> Result<WorkerConnection, TargetError>;
|
||||
}
|
||||
|
||||
impl Target for LocalTarget {
|
||||
fn kind(&self) -> TargetKind {
|
||||
TargetKind::Local
|
||||
}
|
||||
|
||||
fn spawn_worker(&self) -> Result<WorkerSpawn, TargetError> {
|
||||
Ok(WorkerSpawn {
|
||||
runtime_command: self.runtime_command()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn worker_by_name(&self) -> Result<WorkerByName, TargetError> {
|
||||
Ok(WorkerByName {
|
||||
runtime_command: self.runtime_command()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn resume_worker(&self) -> Result<WorkerResume, TargetError> {
|
||||
Ok(WorkerResume {
|
||||
runtime_command: self.runtime_command()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn dashboard(&self) -> Result<Dashboard, TargetError> {
|
||||
Ok(Dashboard {
|
||||
runtime_command: self.runtime_command()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn list_workers(&self, _request: WorkerListRequest) -> Result<WorkerList, TargetError> {
|
||||
Err(TargetError::unsupported(
|
||||
"Backend runtime worker listing",
|
||||
self.kind(),
|
||||
))
|
||||
}
|
||||
|
||||
fn connect_worker(
|
||||
&self,
|
||||
_selector: WorkerConnectionSelector,
|
||||
) -> Result<WorkerConnection, TargetError> {
|
||||
Err(TargetError::unsupported(
|
||||
"Backend runtime worker connection",
|
||||
self.kind(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Target for BackendTarget {
|
||||
fn kind(&self) -> TargetKind {
|
||||
TargetKind::Backend
|
||||
}
|
||||
|
||||
fn spawn_worker(&self) -> Result<WorkerSpawn, TargetError> {
|
||||
Err(TargetError::unsupported("Worker spawn", self.kind()))
|
||||
}
|
||||
|
||||
fn worker_by_name(&self) -> Result<WorkerByName, TargetError> {
|
||||
Err(TargetError::unsupported(
|
||||
"Worker name attachment",
|
||||
self.kind(),
|
||||
))
|
||||
}
|
||||
|
||||
fn resume_worker(&self) -> Result<WorkerResume, TargetError> {
|
||||
Err(TargetError::unsupported("Worker resume", self.kind()))
|
||||
}
|
||||
|
||||
fn dashboard(&self) -> Result<Dashboard, TargetError> {
|
||||
Err(TargetError::unsupported("Dashboard", self.kind()))
|
||||
}
|
||||
|
||||
fn list_workers(&self, request: WorkerListRequest) -> Result<WorkerList, TargetError> {
|
||||
Ok(WorkerList {
|
||||
target: BackendRuntimeListTarget::new(
|
||||
self.base_url.clone(),
|
||||
self.workspace_id.clone(),
|
||||
request.runtime_id,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
fn connect_worker(
|
||||
&self,
|
||||
selector: WorkerConnectionSelector,
|
||||
) -> Result<WorkerConnection, TargetError> {
|
||||
Ok(WorkerConnection {
|
||||
target: BackendRuntimeTarget::new(
|
||||
self.base_url.clone(),
|
||||
selector.runtime_id,
|
||||
selector.worker_id,
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn backend_target_builds_worker_list() {
|
||||
let target = BackendTarget::new("http://127.0.0.1:8787", Some("workspace-a"));
|
||||
let workers = target
|
||||
.list_workers(WorkerListRequest::new(Some("runtime-a".to_string())))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(workers.target.base_url, "http://127.0.0.1:8787");
|
||||
assert_eq!(workers.target.workspace_id.as_deref(), Some("workspace-a"));
|
||||
assert_eq!(workers.target.runtime_id.as_deref(), Some("runtime-a"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backend_target_builds_worker_connection() {
|
||||
let target = BackendTarget::new("http://127.0.0.1:8787", Some("workspace-a"));
|
||||
let connection = target
|
||||
.connect_worker(WorkerConnectionSelector::new("runtime-a", "worker-b"))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(connection.target.base_url, "http://127.0.0.1:8787");
|
||||
assert_eq!(connection.target.runtime_id, "runtime-a");
|
||||
assert_eq!(connection.target.worker_id, "worker-b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backend_target_rejects_local_worker_operations() {
|
||||
let target = BackendTarget::new("http://127.0.0.1:8787", None::<String>);
|
||||
let err = target.spawn_worker().unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"Worker spawn is not supported by Backend target"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_target_rejects_backend_worker_operations() {
|
||||
let target = LocalTarget::new();
|
||||
let err = target
|
||||
.list_workers(WorkerListRequest::new(None))
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"Backend runtime worker listing is not supported by local target"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -317,6 +317,9 @@ mod tests {
|
|||
worker_id: worker_id.to_string(),
|
||||
host_id: "host".to_string(),
|
||||
label: "label".to_string(),
|
||||
display_name: "label".to_string(),
|
||||
singleton_key: None,
|
||||
tags: Vec::new(),
|
||||
role: None,
|
||||
profile: profile.map(str::to_string),
|
||||
workspace: BackendWorkerWorkspaceSummary {
|
||||
|
|
|
|||
|
|
@ -34,12 +34,12 @@ use crossterm::execute;
|
|||
use crossterm::terminal::{LeaveAlternateScreen, disable_raw_mode, enable_raw_mode};
|
||||
use session_store::SegmentId;
|
||||
|
||||
use client::{BackendRuntimeListTarget, BackendRuntimeTarget, WorkerRuntimeCommand};
|
||||
use client::{Target, WorkerConnectionSelector, WorkerListRequest};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug)]
|
||||
pub struct LaunchOptions {
|
||||
pub target: Box<dyn Target>,
|
||||
pub mode: LaunchMode,
|
||||
pub runtime_command: WorkerRuntimeCommand,
|
||||
pub workspace_root: PathBuf,
|
||||
}
|
||||
|
||||
|
|
@ -56,12 +56,15 @@ pub enum LaunchMode {
|
|||
worker_name: String,
|
||||
socket_override: Option<PathBuf>,
|
||||
},
|
||||
/// `yoi workers` / `yoi --backend <url>`: list Backend-authoritative runtime workers,
|
||||
/// then attach to the selected Worker through the Backend Runtime API.
|
||||
BackendRuntimePicker { target: BackendRuntimeListTarget },
|
||||
/// `yoi --backend <url> --runtime-id <id> --worker-id <id>`: connect through the
|
||||
/// Workspace Backend Runtime API and observe the Backend-proxied event stream.
|
||||
BackendRuntime { target: BackendRuntimeTarget },
|
||||
/// `yoi workers` / `yoi --backend <url>`: list workers through the selected
|
||||
/// connection target, then attach to the selected Worker.
|
||||
Workers { runtime_id: Option<String> },
|
||||
/// `yoi --backend <url> --runtime-id <id> --worker-id <id>`: open one Worker
|
||||
/// through the selected connection target.
|
||||
OpenWorker {
|
||||
runtime_id: String,
|
||||
worker_id: String,
|
||||
},
|
||||
/// `yoi resume`: open the Worker picker, then attach to the selected live Worker
|
||||
/// or restore the selected stopped Worker by name. Without `--all`, the picker
|
||||
/// is scoped to the current runtime workspace.
|
||||
|
|
@ -78,8 +81,8 @@ pub enum LaunchMode {
|
|||
|
||||
pub async fn launch(options: LaunchOptions) -> ExitCode {
|
||||
let LaunchOptions {
|
||||
target,
|
||||
mode,
|
||||
runtime_command,
|
||||
workspace_root,
|
||||
} = options;
|
||||
|
||||
|
|
@ -105,20 +108,55 @@ pub async fn launch(options: LaunchOptions) -> ExitCode {
|
|||
LaunchMode::Spawn {
|
||||
worker_name,
|
||||
profile,
|
||||
} => console::run_spawn(None, worker_name, profile, runtime_command).await,
|
||||
} => match target.spawn_worker() {
|
||||
Ok(spawn) => {
|
||||
console::run_spawn(None, worker_name, profile, spawn.runtime_command).await
|
||||
}
|
||||
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
|
||||
},
|
||||
LaunchMode::WorkerName {
|
||||
worker_name,
|
||||
socket_override,
|
||||
} => console::run_worker_name(worker_name, socket_override, runtime_command).await,
|
||||
LaunchMode::BackendRuntimePicker { target } => backend_worker_picker::run(target).await,
|
||||
LaunchMode::BackendRuntime { target } => console::run_backend_runtime(target).await,
|
||||
LaunchMode::Resume { all } => {
|
||||
console::run_resume(runtime_command, workspace_root.clone(), all).await
|
||||
} => match target.worker_by_name() {
|
||||
Ok(worker_by_name) => {
|
||||
console::run_worker_name(
|
||||
worker_name,
|
||||
socket_override,
|
||||
worker_by_name.runtime_command,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
|
||||
},
|
||||
LaunchMode::Workers { runtime_id } => {
|
||||
match target.list_workers(WorkerListRequest::new(runtime_id)) {
|
||||
Ok(worker_list) => backend_worker_picker::run(worker_list.target).await,
|
||||
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
|
||||
}
|
||||
}
|
||||
LaunchMode::ResumeWithSession { id, worker_name } => {
|
||||
console::run_spawn(Some(id), worker_name, None, runtime_command).await
|
||||
}
|
||||
LaunchMode::Panel => dashboard::launch(runtime_command).await,
|
||||
LaunchMode::OpenWorker {
|
||||
runtime_id,
|
||||
worker_id,
|
||||
} => match target.connect_worker(WorkerConnectionSelector::new(runtime_id, worker_id)) {
|
||||
Ok(connection) => console::run_backend_runtime(connection.target).await,
|
||||
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
|
||||
},
|
||||
LaunchMode::Resume { all } => match target.resume_worker() {
|
||||
Ok(resume) => {
|
||||
console::run_resume(resume.runtime_command, workspace_root.clone(), all).await
|
||||
}
|
||||
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
|
||||
},
|
||||
LaunchMode::ResumeWithSession { id, worker_name } => match target.spawn_worker() {
|
||||
Ok(spawn) => {
|
||||
console::run_spawn(Some(id), worker_name, None, spawn.runtime_command).await
|
||||
}
|
||||
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
|
||||
},
|
||||
LaunchMode::Panel => match target.dashboard() {
|
||||
Ok(dashboard) => dashboard::launch(dashboard.runtime_command).await,
|
||||
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
|
||||
},
|
||||
};
|
||||
|
||||
// Always restore the terminal first so any pending eprintln below
|
||||
|
|
|
|||
299
crates/yoi/src/cli_connection.rs
Normal file
299
crates/yoi/src/cli_connection.rs
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
use client::{BackendTarget, LocalTarget, Target, TargetKind};
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::{ParseError, resolve_backend_url};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum CliConnectionRequirement {
|
||||
LocalOnly,
|
||||
BackendOnly,
|
||||
ConnectionAware,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum CliCommand {
|
||||
DefaultTui,
|
||||
Workers,
|
||||
Resume,
|
||||
Panel,
|
||||
Keys,
|
||||
SetupModel,
|
||||
WorkerRuntime,
|
||||
WorkerCleanup,
|
||||
Ticket,
|
||||
Objective,
|
||||
Plugin,
|
||||
Mcp,
|
||||
MemoryLint,
|
||||
Session,
|
||||
WorkspaceServer,
|
||||
Login,
|
||||
}
|
||||
|
||||
impl CliCommand {
|
||||
pub(crate) fn display_name(self) -> &'static str {
|
||||
match self {
|
||||
CliCommand::DefaultTui => "yoi",
|
||||
CliCommand::Workers => "yoi workers",
|
||||
CliCommand::Resume => "yoi resume",
|
||||
CliCommand::Panel => "yoi panel",
|
||||
CliCommand::Keys => "yoi keys",
|
||||
CliCommand::SetupModel => "yoi setup-model",
|
||||
CliCommand::WorkerRuntime => "yoi worker",
|
||||
CliCommand::WorkerCleanup => "yoi worker management",
|
||||
CliCommand::Ticket => "yoi ticket",
|
||||
CliCommand::Objective => "yoi objective",
|
||||
CliCommand::Plugin => "yoi plugin",
|
||||
CliCommand::Mcp => "yoi mcp",
|
||||
CliCommand::MemoryLint => "yoi memory lint",
|
||||
CliCommand::Session => "yoi session",
|
||||
CliCommand::WorkspaceServer => "yoi workspace/server",
|
||||
CliCommand::Login => "yoi login",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn connection_requirement(self) -> CliConnectionRequirement {
|
||||
match self {
|
||||
CliCommand::DefaultTui => CliConnectionRequirement::ConnectionAware,
|
||||
CliCommand::Workers | CliCommand::Login => CliConnectionRequirement::BackendOnly,
|
||||
CliCommand::Resume
|
||||
| CliCommand::Panel
|
||||
| CliCommand::Keys
|
||||
| CliCommand::SetupModel
|
||||
| CliCommand::WorkerRuntime
|
||||
| CliCommand::WorkerCleanup
|
||||
| CliCommand::Ticket
|
||||
| CliCommand::Objective
|
||||
| CliCommand::Plugin
|
||||
| CliCommand::Mcp
|
||||
| CliCommand::MemoryLint
|
||||
| CliCommand::Session
|
||||
| CliCommand::WorkspaceServer => CliConnectionRequirement::LocalOnly,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) enum ClientDefaultConnection {
|
||||
Local,
|
||||
Backend,
|
||||
}
|
||||
|
||||
impl Default for ClientDefaultConnection {
|
||||
fn default() -> Self {
|
||||
Self::Local
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum CliConnectionInput<'a> {
|
||||
LocalDefault,
|
||||
BackendTarget {
|
||||
explicit_backend_url: Option<String>,
|
||||
workspace_id: Option<&'a str>,
|
||||
},
|
||||
}
|
||||
|
||||
pub(crate) trait CliConnectionResolver {
|
||||
fn resolve_connection(
|
||||
&self,
|
||||
command: CliCommand,
|
||||
input: CliConnectionInput<'_>,
|
||||
) -> Result<Box<dyn Target>, ParseError>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub(crate) struct ClientConfigCliConnectionResolver;
|
||||
|
||||
impl CliConnectionResolver for ClientConfigCliConnectionResolver {
|
||||
fn resolve_connection(
|
||||
&self,
|
||||
command: CliCommand,
|
||||
input: CliConnectionInput<'_>,
|
||||
) -> Result<Box<dyn Target>, ParseError> {
|
||||
match (command.connection_requirement(), input) {
|
||||
(CliConnectionRequirement::LocalOnly, CliConnectionInput::LocalDefault) => {
|
||||
Ok(Box::new(LocalTarget::new()))
|
||||
}
|
||||
(CliConnectionRequirement::LocalOnly, CliConnectionInput::BackendTarget { .. }) => {
|
||||
Err(ParseError(format!(
|
||||
"{} uses a local connection target and cannot accept Backend target options",
|
||||
command.display_name()
|
||||
)))
|
||||
}
|
||||
(CliConnectionRequirement::BackendOnly, CliConnectionInput::LocalDefault) => {
|
||||
Err(ParseError(format!(
|
||||
"{} requires a Backend connection target",
|
||||
command.display_name()
|
||||
)))
|
||||
}
|
||||
(
|
||||
CliConnectionRequirement::BackendOnly | CliConnectionRequirement::ConnectionAware,
|
||||
CliConnectionInput::BackendTarget {
|
||||
explicit_backend_url,
|
||||
workspace_id,
|
||||
},
|
||||
) => Ok(Box::new(BackendTarget::new(
|
||||
resolve_backend_url(explicit_backend_url, workspace_id)?,
|
||||
workspace_id.map(str::to_string),
|
||||
))),
|
||||
(CliConnectionRequirement::ConnectionAware, CliConnectionInput::LocalDefault) => {
|
||||
Ok(Box::new(LocalTarget::new()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_local_cli_connection<R: CliConnectionResolver + ?Sized>(
|
||||
resolver: &R,
|
||||
command: CliCommand,
|
||||
) -> Result<Box<dyn Target>, ParseError> {
|
||||
let target = resolver.resolve_connection(command, CliConnectionInput::LocalDefault)?;
|
||||
match target.kind() {
|
||||
TargetKind::Local => Ok(target),
|
||||
TargetKind::Backend => Err(ParseError(format!(
|
||||
"{} resolved a Backend target where a local target was required",
|
||||
command.display_name()
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_backend_cli_connection<R: CliConnectionResolver + ?Sized>(
|
||||
resolver: &R,
|
||||
command: CliCommand,
|
||||
explicit_backend_url: Option<String>,
|
||||
workspace_id: Option<&str>,
|
||||
) -> Result<Box<dyn Target>, ParseError> {
|
||||
let target = resolver.resolve_connection(
|
||||
command,
|
||||
CliConnectionInput::BackendTarget {
|
||||
explicit_backend_url,
|
||||
workspace_id,
|
||||
},
|
||||
)?;
|
||||
match target.kind() {
|
||||
TargetKind::Backend => Ok(target),
|
||||
TargetKind::Local => Err(ParseError(format!(
|
||||
"{} resolved a local target where a Backend target was required",
|
||||
command.display_name()
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn backend_target_option_error_for_local_command(
|
||||
command: CliCommand,
|
||||
option: &str,
|
||||
) -> ParseError {
|
||||
ParseError(format!(
|
||||
"{} uses a local connection target and cannot accept Backend target option `{option}`",
|
||||
command.display_name()
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn is_backend_target_option(arg: &str) -> bool {
|
||||
matches!(
|
||||
arg,
|
||||
"--backend" | "--workspace-id" | "--runtime-id" | "--runtime" | "--worker-id"
|
||||
) || arg.starts_with("--backend=")
|
||||
|| arg.starts_with("--workspace-id=")
|
||||
|| arg.starts_with("--runtime-id=")
|
||||
|| arg.starts_with("--runtime=")
|
||||
|| arg.starts_with("--worker-id=")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cli_command_connection_requirements_are_explicit() {
|
||||
assert_eq!(
|
||||
CliCommand::DefaultTui.connection_requirement(),
|
||||
CliConnectionRequirement::ConnectionAware
|
||||
);
|
||||
assert_eq!(
|
||||
CliCommand::Workers.connection_requirement(),
|
||||
CliConnectionRequirement::BackendOnly
|
||||
);
|
||||
assert_eq!(
|
||||
CliCommand::Login.connection_requirement(),
|
||||
CliConnectionRequirement::BackendOnly
|
||||
);
|
||||
|
||||
for command in [
|
||||
CliCommand::Resume,
|
||||
CliCommand::Panel,
|
||||
CliCommand::Keys,
|
||||
CliCommand::SetupModel,
|
||||
CliCommand::WorkerRuntime,
|
||||
CliCommand::WorkerCleanup,
|
||||
CliCommand::Ticket,
|
||||
CliCommand::Objective,
|
||||
CliCommand::Plugin,
|
||||
CliCommand::Mcp,
|
||||
CliCommand::MemoryLint,
|
||||
CliCommand::Session,
|
||||
CliCommand::WorkspaceServer,
|
||||
] {
|
||||
assert_eq!(
|
||||
command.connection_requirement(),
|
||||
CliConnectionRequirement::LocalOnly,
|
||||
"{} should be local-only",
|
||||
command.display_name()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_connection_resolver_rejects_local_only_backend_target() {
|
||||
let resolver = ClientConfigCliConnectionResolver;
|
||||
let err = resolver
|
||||
.resolve_connection(
|
||||
CliCommand::Resume,
|
||||
CliConnectionInput::BackendTarget {
|
||||
explicit_backend_url: Some("http://127.0.0.1:8787".to_string()),
|
||||
workspace_id: None,
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"yoi resume uses a local connection target and cannot accept Backend target options"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_connection_resolver_rejects_backend_only_local_default() {
|
||||
let resolver = ClientConfigCliConnectionResolver;
|
||||
let err = resolver
|
||||
.resolve_connection(CliCommand::Workers, CliConnectionInput::LocalDefault)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"yoi workers requires a Backend connection target"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_connection_resolver_selects_backend_for_connection_aware_target() {
|
||||
let resolver = ClientConfigCliConnectionResolver;
|
||||
let target = resolve_backend_cli_connection(
|
||||
&resolver,
|
||||
CliCommand::DefaultTui,
|
||||
Some("http://127.0.0.1:8787".to_string()),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(target.kind(), TargetKind::Backend);
|
||||
let workers = target
|
||||
.list_workers(client::WorkerListRequest::new(None))
|
||||
.unwrap();
|
||||
assert_eq!(workers.target.base_url, "http://127.0.0.1:8787");
|
||||
assert_eq!(workers.target.workspace_id, None);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
mod cli_connection;
|
||||
mod mcp_cli;
|
||||
mod memory_lint;
|
||||
mod objective_cli;
|
||||
|
|
@ -14,10 +15,12 @@ use std::path::{Path, PathBuf};
|
|||
use std::process::{Command, ExitCode};
|
||||
use std::time::Duration;
|
||||
|
||||
use client::{
|
||||
BackendAuthTarget, BackendRuntimeListTarget, BackendRuntimeTarget, WorkerRuntimeCommand,
|
||||
start_device_login, wait_for_device_login,
|
||||
use cli_connection::{
|
||||
CliCommand, CliConnectionResolver, ClientConfigCliConnectionResolver, ClientDefaultConnection,
|
||||
backend_target_option_error_for_local_command, is_backend_target_option,
|
||||
resolve_backend_cli_connection, resolve_local_cli_connection,
|
||||
};
|
||||
use client::{BackendAuthTarget, Target, start_device_login, wait_for_device_login};
|
||||
use memory_lint::{LintCliOptions, LintStatus};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use session_store::SegmentId;
|
||||
|
|
@ -48,6 +51,7 @@ enum Mode {
|
|||
Keys,
|
||||
SetupModel,
|
||||
Tui {
|
||||
target: Box<dyn Target>,
|
||||
mode: LaunchMode,
|
||||
workspace_root: PathBuf,
|
||||
},
|
||||
|
|
@ -181,19 +185,13 @@ async fn main() -> ExitCode {
|
|||
Mode::Keys => tui::keys::launch().await,
|
||||
Mode::SetupModel => tui::setup_model::launch().await,
|
||||
Mode::Tui {
|
||||
target,
|
||||
mode,
|
||||
workspace_root,
|
||||
} => {
|
||||
let runtime_command = match WorkerRuntimeCommand::resolve() {
|
||||
Ok(command) => command,
|
||||
Err(e) => {
|
||||
eprintln!("yoi: failed to resolve Worker runtime command: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
tui::launch(LaunchOptions {
|
||||
target,
|
||||
mode,
|
||||
runtime_command,
|
||||
workspace_root,
|
||||
})
|
||||
.await
|
||||
|
|
@ -215,8 +213,18 @@ where
|
|||
}
|
||||
|
||||
fn parse_args_slice(args: &[String]) -> Result<Mode, ParseError> {
|
||||
let resolver = ClientConfigCliConnectionResolver;
|
||||
parse_args_slice_with_connection_resolver(args, &resolver)
|
||||
}
|
||||
|
||||
fn parse_args_slice_with_connection_resolver<R: CliConnectionResolver + ?Sized>(
|
||||
args: &[String],
|
||||
connection_resolver: &R,
|
||||
) -> Result<Mode, ParseError> {
|
||||
if args.is_empty() {
|
||||
let target = resolve_local_cli_connection(connection_resolver, CliCommand::DefaultTui)?;
|
||||
return Ok(Mode::Tui {
|
||||
target,
|
||||
mode: LaunchMode::Spawn {
|
||||
worker_name: None,
|
||||
profile: None,
|
||||
|
|
@ -227,69 +235,101 @@ fn parse_args_slice(args: &[String]) -> Result<Mode, ParseError> {
|
|||
|
||||
match args[0].as_str() {
|
||||
"--help" | "-h" => return Ok(Mode::Help),
|
||||
"resume" => return parse_resume_args(&args[1..]),
|
||||
"workers" => return parse_workers_args(&args[1..]),
|
||||
"resume" => return parse_resume_args(&args[1..], connection_resolver),
|
||||
"workers" => return parse_workers_args(&args[1..], connection_resolver),
|
||||
"worker" => {
|
||||
if let Some(cli) = worker_cleanup_cli::parse_worker_management_args(&args[1..])
|
||||
.map_err(|e| ParseError(e.to_string()))?
|
||||
{
|
||||
let _target =
|
||||
resolve_local_cli_connection(connection_resolver, CliCommand::WorkerCleanup)?;
|
||||
return Ok(Mode::WorkerCleanup(cli));
|
||||
}
|
||||
let _target =
|
||||
resolve_local_cli_connection(connection_resolver, CliCommand::WorkerRuntime)?;
|
||||
return Ok(Mode::WorkerRuntime(args[1..].to_vec()));
|
||||
}
|
||||
"objective" => {
|
||||
let _target = resolve_local_cli_connection(connection_resolver, CliCommand::Objective)?;
|
||||
let objective_cli = objective_cli::parse_objective_args(&args[1..])
|
||||
.map_err(|e| ParseError(e.to_string()))?;
|
||||
return Ok(Mode::Objective(objective_cli));
|
||||
}
|
||||
"session" => {
|
||||
let _target = resolve_local_cli_connection(connection_resolver, CliCommand::Session)?;
|
||||
let session_cli = session_cli::parse_session_args(&args[1..])
|
||||
.map_err(|e| ParseError(e.to_string()))?;
|
||||
return Ok(Mode::Session(session_cli));
|
||||
}
|
||||
"ticket" => {
|
||||
let _target = resolve_local_cli_connection(connection_resolver, CliCommand::Ticket)?;
|
||||
let ticket_cli =
|
||||
ticket_cli::parse_ticket_args(&args[1..]).map_err(|e| ParseError(e.to_string()))?;
|
||||
return Ok(Mode::Ticket(ticket_cli));
|
||||
}
|
||||
"plugin" => {
|
||||
let _target = resolve_local_cli_connection(connection_resolver, CliCommand::Plugin)?;
|
||||
let plugin_cli = parse_plugin_args(&args[1..])?;
|
||||
return Ok(Mode::Plugin(plugin_cli));
|
||||
}
|
||||
"workspace" => {
|
||||
let _target =
|
||||
resolve_local_cli_connection(connection_resolver, CliCommand::WorkspaceServer)?;
|
||||
return parse_workspace_args(&args[1..]);
|
||||
}
|
||||
"server" => {
|
||||
let _target =
|
||||
resolve_local_cli_connection(connection_resolver, CliCommand::WorkspaceServer)?;
|
||||
return parse_workspace_args(&args[1..]);
|
||||
}
|
||||
"login" => {
|
||||
return parse_login_args(&args[1..]);
|
||||
return parse_login_args(&args[1..], connection_resolver);
|
||||
}
|
||||
"mcp" => {
|
||||
let _target = resolve_local_cli_connection(connection_resolver, CliCommand::Mcp)?;
|
||||
let mcp_cli = parse_mcp_args(&args[1..])?;
|
||||
return Ok(Mode::Mcp(mcp_cli));
|
||||
}
|
||||
"panel" => {
|
||||
let target = resolve_local_cli_connection(connection_resolver, CliCommand::Panel)?;
|
||||
return Ok(Mode::Tui {
|
||||
target,
|
||||
mode: LaunchMode::Panel,
|
||||
workspace_root: parse_panel_workspace(&args[1..])?,
|
||||
});
|
||||
}
|
||||
"keys" => {
|
||||
if args.len() != 1 {
|
||||
if let Some(arg) = args[1..].iter().find(|arg| is_backend_target_option(arg)) {
|
||||
return Err(backend_target_option_error_for_local_command(
|
||||
CliCommand::Keys,
|
||||
arg,
|
||||
));
|
||||
}
|
||||
return Err(ParseError("yoi keys does not accept arguments".into()));
|
||||
}
|
||||
let _target = resolve_local_cli_connection(connection_resolver, CliCommand::Keys)?;
|
||||
return Ok(Mode::Keys);
|
||||
}
|
||||
"setup-model" => {
|
||||
if args.len() != 1 {
|
||||
if let Some(arg) = args[1..].iter().find(|arg| is_backend_target_option(arg)) {
|
||||
return Err(backend_target_option_error_for_local_command(
|
||||
CliCommand::SetupModel,
|
||||
arg,
|
||||
));
|
||||
}
|
||||
return Err(ParseError(
|
||||
"yoi setup-model does not accept arguments".into(),
|
||||
));
|
||||
}
|
||||
let _target =
|
||||
resolve_local_cli_connection(connection_resolver, CliCommand::SetupModel)?;
|
||||
return Ok(Mode::SetupModel);
|
||||
}
|
||||
"memory" if args.get(1).map(String::as_str) == Some("lint") => {
|
||||
let _target =
|
||||
resolve_local_cli_connection(connection_resolver, CliCommand::MemoryLint)?;
|
||||
let lint_args = &args[2..];
|
||||
if lint_args.iter().any(|arg| arg == "--help" || arg == "-h") {
|
||||
return Ok(Mode::MemoryLintHelp);
|
||||
|
|
@ -309,10 +349,13 @@ fn parse_args_slice(args: &[String]) -> Result<Mode, ParseError> {
|
|||
_ => {}
|
||||
}
|
||||
|
||||
parse_console_options(args)
|
||||
parse_console_options(args, connection_resolver)
|
||||
}
|
||||
|
||||
fn parse_console_options(args: &[String]) -> Result<Mode, ParseError> {
|
||||
fn parse_console_options<R: CliConnectionResolver + ?Sized>(
|
||||
args: &[String],
|
||||
connection_resolver: &R,
|
||||
) -> Result<Mode, ParseError> {
|
||||
let mut workspace_root = current_dir()?;
|
||||
let mut session = None;
|
||||
let mut worker_name = None;
|
||||
|
|
@ -544,25 +587,34 @@ fn parse_console_options(args: &[String]) -> Result<Mode, ParseError> {
|
|||
Some(workspace_id) => Some(workspace_id),
|
||||
None => resolve_workspace_id_from_root(&workspace_root)?,
|
||||
};
|
||||
let backend_url = resolve_backend_url(backend_url, workspace_id.as_deref())?;
|
||||
let target = resolve_backend_cli_connection(
|
||||
connection_resolver,
|
||||
CliCommand::DefaultTui,
|
||||
backend_url,
|
||||
workspace_id.as_deref(),
|
||||
)?;
|
||||
if let (Some(runtime_id), Some(worker_id)) = (runtime_id.clone(), worker_id) {
|
||||
return Ok(Mode::Tui {
|
||||
mode: LaunchMode::BackendRuntime {
|
||||
target: BackendRuntimeTarget::new(backend_url, runtime_id, worker_id),
|
||||
target,
|
||||
mode: LaunchMode::OpenWorker {
|
||||
runtime_id,
|
||||
worker_id,
|
||||
},
|
||||
workspace_root,
|
||||
});
|
||||
}
|
||||
return Ok(Mode::Tui {
|
||||
mode: LaunchMode::BackendRuntimePicker {
|
||||
target: BackendRuntimeListTarget::new(backend_url, workspace_id, runtime_id),
|
||||
},
|
||||
target,
|
||||
mode: LaunchMode::Workers { runtime_id },
|
||||
workspace_root,
|
||||
});
|
||||
}
|
||||
|
||||
let target = resolve_local_cli_connection(connection_resolver, CliCommand::DefaultTui)?;
|
||||
|
||||
if let Some(profile) = profile {
|
||||
return Ok(Mode::Tui {
|
||||
target,
|
||||
mode: LaunchMode::Spawn {
|
||||
worker_name,
|
||||
profile: Some(profile),
|
||||
|
|
@ -572,12 +624,14 @@ fn parse_console_options(args: &[String]) -> Result<Mode, ParseError> {
|
|||
}
|
||||
if let Some(id) = session {
|
||||
return Ok(Mode::Tui {
|
||||
target,
|
||||
mode: LaunchMode::ResumeWithSession { id, worker_name },
|
||||
workspace_root,
|
||||
});
|
||||
}
|
||||
if let Some(worker_name) = worker_name {
|
||||
return Ok(Mode::Tui {
|
||||
target,
|
||||
mode: LaunchMode::WorkerName {
|
||||
worker_name,
|
||||
socket_override,
|
||||
|
|
@ -586,6 +640,7 @@ fn parse_console_options(args: &[String]) -> Result<Mode, ParseError> {
|
|||
});
|
||||
}
|
||||
Ok(Mode::Tui {
|
||||
target,
|
||||
mode: LaunchMode::Spawn {
|
||||
worker_name: None,
|
||||
profile: None,
|
||||
|
|
@ -594,7 +649,10 @@ fn parse_console_options(args: &[String]) -> Result<Mode, ParseError> {
|
|||
})
|
||||
}
|
||||
|
||||
fn parse_workers_args(args: &[String]) -> Result<Mode, ParseError> {
|
||||
fn parse_workers_args<R: CliConnectionResolver + ?Sized>(
|
||||
args: &[String],
|
||||
connection_resolver: &R,
|
||||
) -> Result<Mode, ParseError> {
|
||||
let mut workspace_root = current_dir()?;
|
||||
let mut workspace_id = None;
|
||||
let mut backend_url = None;
|
||||
|
|
@ -704,16 +762,23 @@ fn parse_workers_args(args: &[String]) -> Result<Mode, ParseError> {
|
|||
Some(workspace_id) => Some(workspace_id),
|
||||
None => resolve_workspace_id_from_root(&workspace_root)?,
|
||||
};
|
||||
let backend_url = resolve_backend_url(backend_url, workspace_id.as_deref())?;
|
||||
let target = resolve_backend_cli_connection(
|
||||
connection_resolver,
|
||||
CliCommand::Workers,
|
||||
backend_url,
|
||||
workspace_id.as_deref(),
|
||||
)?;
|
||||
Ok(Mode::Tui {
|
||||
mode: LaunchMode::BackendRuntimePicker {
|
||||
target: BackendRuntimeListTarget::new(backend_url, workspace_id, runtime_id),
|
||||
},
|
||||
target,
|
||||
mode: LaunchMode::Workers { runtime_id },
|
||||
workspace_root,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_resume_args(args: &[String]) -> Result<Mode, ParseError> {
|
||||
fn parse_resume_args<R: CliConnectionResolver + ?Sized>(
|
||||
args: &[String],
|
||||
connection_resolver: &R,
|
||||
) -> Result<Mode, ParseError> {
|
||||
let mut workspace_root = current_dir()?;
|
||||
let mut workspace_set = false;
|
||||
let mut all = false;
|
||||
|
|
@ -755,6 +820,12 @@ fn parse_resume_args(args: &[String]) -> Result<Mode, ParseError> {
|
|||
i += 1;
|
||||
}
|
||||
arg if arg.starts_with('-') => {
|
||||
if is_backend_target_option(arg) {
|
||||
return Err(backend_target_option_error_for_local_command(
|
||||
CliCommand::Resume,
|
||||
arg,
|
||||
));
|
||||
}
|
||||
return Err(ParseError(format!("unknown yoi resume option `{arg}`")));
|
||||
}
|
||||
value => {
|
||||
|
|
@ -771,7 +842,10 @@ fn parse_resume_args(args: &[String]) -> Result<Mode, ParseError> {
|
|||
));
|
||||
}
|
||||
|
||||
let target = resolve_local_cli_connection(connection_resolver, CliCommand::Resume)?;
|
||||
|
||||
Ok(Mode::Tui {
|
||||
target,
|
||||
mode: LaunchMode::Resume { all },
|
||||
workspace_root,
|
||||
})
|
||||
|
|
@ -791,6 +865,11 @@ struct WorkspaceIdentityFile {
|
|||
#[derive(Debug, Default, Deserialize)]
|
||||
struct ClientConfigFile {
|
||||
default_backend: Option<String>,
|
||||
// Parsed as part of the target model; applying configured connection defaults is left to
|
||||
// the later user-facing connection-mode work so current `yoi` default launch stays local.
|
||||
#[allow(dead_code)]
|
||||
#[serde(default)]
|
||||
default_connection: ClientDefaultConnection,
|
||||
#[serde(default)]
|
||||
backends: BTreeMap<String, ClientBackendConfig>,
|
||||
#[serde(default)]
|
||||
|
|
@ -895,7 +974,10 @@ fn client_config_missing_message(workspace_id: Option<&str>) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
fn parse_login_args(args: &[String]) -> Result<Mode, ParseError> {
|
||||
fn parse_login_args<R: CliConnectionResolver + ?Sized>(
|
||||
args: &[String],
|
||||
connection_resolver: &R,
|
||||
) -> Result<Mode, ParseError> {
|
||||
if args.iter().any(|arg| arg == "--help" || arg == "-h") {
|
||||
return Err(ParseError(
|
||||
"yoi login usage: yoi login [--backend <URL>] [--no-wait]".to_string(),
|
||||
|
|
@ -935,6 +1017,12 @@ fn parse_login_args(args: &[String]) -> Result<Mode, ParseError> {
|
|||
}
|
||||
}
|
||||
}
|
||||
let _target = resolve_backend_cli_connection(
|
||||
connection_resolver,
|
||||
CliCommand::Login,
|
||||
backend_url.clone(),
|
||||
None,
|
||||
)?;
|
||||
let backend_url = resolve_backend_url(backend_url, None)?;
|
||||
Ok(Mode::Login {
|
||||
backend_url,
|
||||
|
|
@ -1423,6 +1511,13 @@ fn mcp_usage() -> &'static str {
|
|||
}
|
||||
|
||||
fn parse_panel_workspace(args: &[String]) -> Result<PathBuf, ParseError> {
|
||||
if let Some(arg) = args.iter().find(|arg| is_backend_target_option(arg)) {
|
||||
return Err(backend_target_option_error_for_local_command(
|
||||
CliCommand::Panel,
|
||||
arg,
|
||||
));
|
||||
}
|
||||
|
||||
match args {
|
||||
[] => std::env::current_dir()
|
||||
.map_err(|e| ParseError(format!("failed to resolve current directory: {e}"))),
|
||||
|
|
@ -1484,6 +1579,77 @@ fn print_memory_lint_help() {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::cli_connection::CliConnectionInput;
|
||||
use client::{BackendTarget, LocalTarget, TargetKind, WorkerListRequest};
|
||||
|
||||
struct FixedCliConnectionResolver {
|
||||
backend_url: &'static str,
|
||||
}
|
||||
|
||||
impl CliConnectionResolver for FixedCliConnectionResolver {
|
||||
fn resolve_connection(
|
||||
&self,
|
||||
_command: CliCommand,
|
||||
input: CliConnectionInput<'_>,
|
||||
) -> Result<Box<dyn Target>, ParseError> {
|
||||
match input {
|
||||
CliConnectionInput::LocalDefault => Ok(Box::new(LocalTarget::new())),
|
||||
CliConnectionInput::BackendTarget { workspace_id, .. } => Ok(Box::new(
|
||||
BackendTarget::new(self.backend_url, workspace_id.map(str::to_string)),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parser_uses_injected_cli_connection_resolver_for_backend_command() {
|
||||
let resolver = FixedCliConnectionResolver {
|
||||
backend_url: "http://fake-backend.example",
|
||||
};
|
||||
let args = vec!["workers".to_string()];
|
||||
let mode = parse_args_slice_with_connection_resolver(&args, &resolver).unwrap();
|
||||
|
||||
match mode {
|
||||
Mode::Tui {
|
||||
target,
|
||||
mode: LaunchMode::Workers { runtime_id },
|
||||
..
|
||||
} => {
|
||||
assert_eq!(runtime_id, None);
|
||||
assert_eq!(target.kind(), TargetKind::Backend);
|
||||
let workers = target.list_workers(WorkerListRequest::new(None)).unwrap();
|
||||
assert_eq!(workers.target.base_url, "http://fake-backend.example");
|
||||
}
|
||||
other => panic!("expected Workers mode, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_config_default_connection_defaults_to_local() {
|
||||
let config = ClientConfigFile::default();
|
||||
assert_eq!(config.default_connection, ClientDefaultConnection::Local);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_local_only_commands_reject_backend_target_options() {
|
||||
let err = parse_args_from(["resume", "--backend", "http://127.0.0.1:8787"]).unwrap_err();
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"yoi resume uses a local connection target and cannot accept Backend target option `--backend`"
|
||||
);
|
||||
|
||||
let err = parse_args_from(["panel", "--runtime-id", "runtime-a"]).unwrap_err();
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"yoi panel uses a local connection target and cannot accept Backend target option `--runtime-id`"
|
||||
);
|
||||
|
||||
let err = parse_args_from(["keys", "--workspace-id=workspace-a"]).unwrap_err();
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"yoi keys uses a local connection target and cannot accept Backend target option `--workspace-id=workspace-a`"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_worker_name_mode() {
|
||||
|
|
@ -1516,14 +1682,26 @@ mod tests {
|
|||
.unwrap()
|
||||
{
|
||||
Mode::Tui {
|
||||
mode: LaunchMode::BackendRuntime { target },
|
||||
target,
|
||||
mode:
|
||||
LaunchMode::OpenWorker {
|
||||
runtime_id,
|
||||
worker_id,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
assert_eq!(target.base_url, "http://127.0.0.1:8787");
|
||||
assert_eq!(target.runtime_id, "runtime-a");
|
||||
assert_eq!(target.worker_id, "worker-b");
|
||||
assert_eq!(target.kind(), TargetKind::Backend);
|
||||
let connection = target
|
||||
.connect_worker(client::WorkerConnectionSelector::new(
|
||||
runtime_id.clone(),
|
||||
worker_id.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(connection.target.base_url, "http://127.0.0.1:8787");
|
||||
assert_eq!(runtime_id, "runtime-a");
|
||||
assert_eq!(worker_id, "worker-b");
|
||||
}
|
||||
_ => panic!("expected BackendRuntime mode"),
|
||||
_ => panic!("expected OpenWorker mode"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1542,13 +1720,19 @@ mod tests {
|
|||
match parse_args_from(["--backend", "http://127.0.0.1:8787", "--runtime-id", "r"]).unwrap()
|
||||
{
|
||||
Mode::Tui {
|
||||
mode: LaunchMode::BackendRuntimePicker { target },
|
||||
target,
|
||||
mode: LaunchMode::Workers { runtime_id },
|
||||
..
|
||||
} => {
|
||||
assert_eq!(target.base_url, "http://127.0.0.1:8787");
|
||||
assert_eq!(target.runtime_id.as_deref(), Some("r"));
|
||||
assert_eq!(target.kind(), TargetKind::Backend);
|
||||
let workers = target
|
||||
.list_workers(WorkerListRequest::new(runtime_id.clone()))
|
||||
.unwrap();
|
||||
assert_eq!(workers.target.base_url, "http://127.0.0.1:8787");
|
||||
assert_eq!(workers.target.runtime_id.as_deref(), Some("r"));
|
||||
assert_eq!(runtime_id.as_deref(), Some("r"));
|
||||
}
|
||||
_ => panic!("expected BackendRuntimePicker mode"),
|
||||
_ => panic!("expected Workers mode"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1564,14 +1748,19 @@ mod tests {
|
|||
.unwrap()
|
||||
{
|
||||
Mode::Tui {
|
||||
mode: LaunchMode::BackendRuntimePicker { target },
|
||||
target,
|
||||
mode: LaunchMode::Workers { runtime_id },
|
||||
..
|
||||
} => {
|
||||
assert_eq!(target.base_url, "http://127.0.0.1:8787");
|
||||
assert_eq!(target.workspace_id.as_deref(), Some("workspace-a"));
|
||||
assert_eq!(target.runtime_id, None);
|
||||
assert_eq!(target.kind(), TargetKind::Backend);
|
||||
let workers = target
|
||||
.list_workers(WorkerListRequest::new(runtime_id.clone()))
|
||||
.unwrap();
|
||||
assert_eq!(workers.target.base_url, "http://127.0.0.1:8787");
|
||||
assert_eq!(workers.target.workspace_id.as_deref(), Some("workspace-a"));
|
||||
assert_eq!(runtime_id, None);
|
||||
}
|
||||
_ => panic!("expected BackendRuntimePicker mode"),
|
||||
_ => panic!("expected Workers mode"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1623,6 +1812,7 @@ mod tests {
|
|||
Mode::Tui {
|
||||
mode: LaunchMode::Resume { all },
|
||||
workspace_root,
|
||||
..
|
||||
} => {
|
||||
assert!(!all);
|
||||
assert_eq!(workspace_root, PathBuf::from("/tmp/resume-workspace"));
|
||||
|
|
@ -1994,6 +2184,7 @@ mod tests {
|
|||
profile,
|
||||
},
|
||||
workspace_root,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(worker_name, Some("agent".to_string()));
|
||||
assert_eq!(profile, Some("project:companion".to_string()));
|
||||
|
|
@ -2039,6 +2230,7 @@ mod tests {
|
|||
Mode::Tui {
|
||||
mode: LaunchMode::Panel,
|
||||
workspace_root,
|
||||
..
|
||||
} => assert_eq!(workspace_root, PathBuf::from("/tmp/other-workspace")),
|
||||
_ => panic!("expected Panel mode"),
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user