cli: route worker lists through targets
This commit is contained in:
parent
acc7281414
commit
cbbc860366
|
|
@ -21,7 +21,7 @@ pub use backend_auth::{
|
|||
poll_device_login, start_device_login, wait_for_device_login,
|
||||
};
|
||||
pub use backend_runtime::{
|
||||
BackendRuntimeClient, BackendRuntimeClientError, BackendRuntimeListResponse,
|
||||
BackendDiagnostic, BackendRuntimeClient, BackendRuntimeClientError, BackendRuntimeListResponse,
|
||||
BackendRuntimeListTarget, BackendRuntimeSummary, BackendRuntimeTarget,
|
||||
BackendWorkerCapabilitySummary, BackendWorkerImplementationSummary,
|
||||
BackendWorkerRestoreResponse, BackendWorkerRestoreResult, BackendWorkerSummary,
|
||||
|
|
|
|||
|
|
@ -54,11 +54,22 @@ impl BackendTarget {
|
|||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct WorkerListRequest {
|
||||
pub runtime_id: Option<String>,
|
||||
pub include_stopped: bool,
|
||||
}
|
||||
|
||||
impl WorkerListRequest {
|
||||
pub fn new(runtime_id: Option<String>) -> Self {
|
||||
Self { runtime_id }
|
||||
Self {
|
||||
runtime_id,
|
||||
include_stopped: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_stopped(runtime_id: Option<String>) -> Self {
|
||||
Self {
|
||||
runtime_id,
|
||||
include_stopped: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -99,7 +110,9 @@ pub struct Dashboard {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct WorkerList {
|
||||
pub target: BackendRuntimeListTarget,
|
||||
pub local_runtime_command: Option<WorkerRuntimeCommand>,
|
||||
pub backend_target: Option<BackendRuntimeListTarget>,
|
||||
pub include_stopped: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
|
@ -182,11 +195,18 @@ impl Target for LocalTarget {
|
|||
})
|
||||
}
|
||||
|
||||
fn list_workers(&self, _request: WorkerListRequest) -> Result<WorkerList, TargetError> {
|
||||
Err(TargetError::unsupported(
|
||||
"Backend runtime worker listing",
|
||||
self.kind(),
|
||||
))
|
||||
fn list_workers(&self, request: WorkerListRequest) -> Result<WorkerList, TargetError> {
|
||||
if request.runtime_id.is_some() {
|
||||
return Err(TargetError::unsupported(
|
||||
"Explicit runtime id for local worker listing",
|
||||
self.kind(),
|
||||
));
|
||||
}
|
||||
Ok(WorkerList {
|
||||
local_runtime_command: Some(self.runtime_command()?),
|
||||
backend_target: None,
|
||||
include_stopped: request.include_stopped,
|
||||
})
|
||||
}
|
||||
|
||||
fn connect_worker(
|
||||
|
|
@ -226,11 +246,13 @@ impl Target for BackendTarget {
|
|||
|
||||
fn list_workers(&self, request: WorkerListRequest) -> Result<WorkerList, TargetError> {
|
||||
Ok(WorkerList {
|
||||
target: BackendRuntimeListTarget::new(
|
||||
local_runtime_command: None,
|
||||
backend_target: Some(BackendRuntimeListTarget::new(
|
||||
self.base_url.clone(),
|
||||
self.workspace_id.clone(),
|
||||
request.runtime_id,
|
||||
),
|
||||
)),
|
||||
include_stopped: request.include_stopped,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -259,9 +281,28 @@ mod tests {
|
|||
.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"));
|
||||
assert_eq!(
|
||||
workers.backend_target.as_ref().unwrap().base_url,
|
||||
"http://127.0.0.1:8787"
|
||||
);
|
||||
assert_eq!(
|
||||
workers
|
||||
.backend_target
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.workspace_id
|
||||
.as_deref(),
|
||||
Some("workspace-a")
|
||||
);
|
||||
assert_eq!(
|
||||
workers
|
||||
.backend_target
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.runtime_id
|
||||
.as_deref(),
|
||||
Some("runtime-a")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -288,15 +329,14 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn local_target_rejects_backend_worker_operations() {
|
||||
fn local_target_builds_local_worker_list() {
|
||||
let target = LocalTarget::new();
|
||||
let err = target
|
||||
.list_workers(WorkerListRequest::new(None))
|
||||
.unwrap_err();
|
||||
let workers = target
|
||||
.list_workers(WorkerListRequest::with_stopped(None))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"Backend runtime worker listing is not supported by local target"
|
||||
);
|
||||
assert!(workers.local_runtime_command.is_some());
|
||||
assert!(workers.backend_target.is_none());
|
||||
assert!(workers.include_stopped);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ use std::io;
|
|||
use std::time::Duration;
|
||||
|
||||
use client::{
|
||||
BackendRuntimeListTarget, BackendRuntimeTarget, BackendWorkerSummary, list_backend_workers,
|
||||
BackendRuntimeListTarget, BackendRuntimeTarget, BackendWorkerSummary,
|
||||
list_backend_stopped_workers, list_backend_workers, restore_backend_worker,
|
||||
};
|
||||
use crossterm::event::{self, Event as TermEvent, KeyCode, KeyEventKind, KeyModifiers};
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
|
|
@ -18,13 +19,30 @@ use crate::console;
|
|||
const MAX_ROWS: usize = 10;
|
||||
const VIEWPORT_LINES: u16 = MAX_ROWS as u16 + 4;
|
||||
|
||||
pub(crate) async fn run(target: BackendRuntimeListTarget) -> Result<(), Box<dyn Error>> {
|
||||
let response = list_backend_workers(&target).await.map_err(|error| {
|
||||
pub(crate) async fn run(
|
||||
target: BackendRuntimeListTarget,
|
||||
include_stopped: bool,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let mut response = list_backend_workers(&target).await.map_err(|error| {
|
||||
io::Error::other(format!(
|
||||
"failed to list Backend runtime workers from {}: {error}",
|
||||
target.base_url
|
||||
))
|
||||
})?;
|
||||
if include_stopped {
|
||||
match list_backend_stopped_workers(&target).await {
|
||||
Ok(stopped) => {
|
||||
response.items.extend(stopped.items);
|
||||
response.diagnostics.extend(stopped.diagnostics);
|
||||
}
|
||||
Err(error) => response.diagnostics.push(client::BackendDiagnostic {
|
||||
code: "backend_stopped_workers_list_failed".to_string(),
|
||||
severity: Some("error".to_string()),
|
||||
message: error.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
dedup_workers(&mut response.items);
|
||||
if response.items.is_empty() {
|
||||
let diagnostics = response
|
||||
.diagnostics
|
||||
|
|
@ -44,11 +62,36 @@ pub(crate) async fn run(target: BackendRuntimeListTarget) -> Result<(), Box<dyn
|
|||
}
|
||||
|
||||
let selected = pick_worker(target.clone(), response.items)?;
|
||||
let worker = if selected.state == "stopped" {
|
||||
let restore_target = BackendRuntimeTarget::new(
|
||||
target.base_url.clone(),
|
||||
selected.runtime_id.clone(),
|
||||
selected.worker_id.clone(),
|
||||
);
|
||||
restore_backend_worker(&restore_target)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
io::Error::other(format!(
|
||||
"failed to restore Backend worker {}/{}: {error}",
|
||||
selected.runtime_id, selected.worker_id
|
||||
))
|
||||
})?
|
||||
.result
|
||||
.worker
|
||||
.unwrap_or(selected)
|
||||
} else {
|
||||
selected
|
||||
};
|
||||
let attach_target =
|
||||
BackendRuntimeTarget::new(target.base_url, selected.runtime_id, selected.worker_id);
|
||||
BackendRuntimeTarget::new(target.base_url, worker.runtime_id, worker.worker_id);
|
||||
console::run_backend_runtime(attach_target).await
|
||||
}
|
||||
|
||||
fn dedup_workers(workers: &mut Vec<BackendWorkerSummary>) {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
workers.retain(|worker| seen.insert((worker.runtime_id.clone(), worker.worker_id.clone())));
|
||||
}
|
||||
|
||||
fn pick_worker(
|
||||
target: BackendRuntimeListTarget,
|
||||
mut workers: Vec<BackendWorkerSummary>,
|
||||
|
|
|
|||
|
|
@ -331,6 +331,15 @@ pub(crate) async fn run_resume(
|
|||
runtime_command: WorkerRuntimeCommand,
|
||||
workspace_root: PathBuf,
|
||||
all: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
run_worker_picker(runtime_command, workspace_root, all, true).await
|
||||
}
|
||||
|
||||
pub(crate) async fn run_worker_picker(
|
||||
runtime_command: WorkerRuntimeCommand,
|
||||
workspace_root: PathBuf,
|
||||
all: bool,
|
||||
include_stopped: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Pick a Worker in its own inline viewport, dropping the viewport before
|
||||
// attaching/restoring so each phase gets fresh vertical room.
|
||||
|
|
@ -338,7 +347,8 @@ pub(crate) async fn run_resume(
|
|||
picker::PickerOptions::all()
|
||||
} else {
|
||||
picker::PickerOptions::workspace(workspace_root)
|
||||
};
|
||||
}
|
||||
.with_stopped(include_stopped);
|
||||
let (worker_name, socket_override) = match picker::run(picker_options).await? {
|
||||
PickerOutcome::Picked {
|
||||
worker_name,
|
||||
|
|
|
|||
|
|
@ -58,7 +58,11 @@ pub enum LaunchMode {
|
|||
},
|
||||
/// `yoi workers` / `yoi --backend <url>`: list workers through the selected
|
||||
/// connection target, then attach to the selected Worker.
|
||||
Workers { runtime_id: Option<String> },
|
||||
Workers {
|
||||
runtime_id: Option<String>,
|
||||
include_stopped: bool,
|
||||
all: bool,
|
||||
},
|
||||
/// `yoi --backend <url> --runtime-id <id> --worker-id <id>`: open one Worker
|
||||
/// through the selected connection target.
|
||||
OpenWorker {
|
||||
|
|
@ -76,7 +80,7 @@ pub enum LaunchMode {
|
|||
worker_name: Option<String>,
|
||||
},
|
||||
/// `yoi panel`: open the workspace Dashboard from the current workspace.
|
||||
Panel,
|
||||
Panel { include_stopped: bool },
|
||||
}
|
||||
|
||||
pub async fn launch(options: LaunchOptions) -> ExitCode {
|
||||
|
|
@ -128,12 +132,34 @@ pub async fn launch(options: LaunchOptions) -> ExitCode {
|
|||
}
|
||||
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::Workers {
|
||||
runtime_id,
|
||||
include_stopped,
|
||||
all,
|
||||
} => match target.list_workers(if include_stopped {
|
||||
WorkerListRequest::with_stopped(runtime_id)
|
||||
} else {
|
||||
WorkerListRequest::new(runtime_id)
|
||||
}) {
|
||||
Ok(worker_list) => {
|
||||
if let Some(target) = worker_list.backend_target {
|
||||
backend_worker_picker::run(target, worker_list.include_stopped).await
|
||||
} else if let Some(runtime_command) = worker_list.local_runtime_command {
|
||||
console::run_worker_picker(
|
||||
runtime_command,
|
||||
workspace_root.clone(),
|
||||
all,
|
||||
worker_list.include_stopped,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
Err(Box::new(io::Error::other(
|
||||
"worker list target did not include a local or backend source",
|
||||
)) as Box<dyn std::error::Error>)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
|
||||
},
|
||||
LaunchMode::OpenWorker {
|
||||
runtime_id,
|
||||
worker_id,
|
||||
|
|
@ -153,7 +179,7 @@ pub async fn launch(options: LaunchOptions) -> ExitCode {
|
|||
}
|
||||
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
|
||||
},
|
||||
LaunchMode::Panel => match target.dashboard() {
|
||||
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>),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -80,20 +80,28 @@ pub enum PickerOutcome {
|
|||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct PickerOptions {
|
||||
scope: PickerScope,
|
||||
include_stopped: bool,
|
||||
}
|
||||
|
||||
impl PickerOptions {
|
||||
pub(crate) fn workspace(workspace_root: PathBuf) -> Self {
|
||||
Self {
|
||||
scope: PickerScope::Workspace(workspace_root),
|
||||
include_stopped: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn all() -> Self {
|
||||
Self {
|
||||
scope: PickerScope::All,
|
||||
include_stopped: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_stopped(mut self, include_stopped: bool) -> Self {
|
||||
self.include_stopped = include_stopped;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -134,6 +142,11 @@ fn list_for_options(
|
|||
stored_workers: Vec<StoredWorkerInfo>,
|
||||
live_workers: Vec<LiveWorkerInfo>,
|
||||
) -> WorkerList {
|
||||
let stored_workers = if options.include_stopped {
|
||||
stored_workers
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
match &options.scope {
|
||||
PickerScope::Workspace(workspace_root) => WorkerList::from_workspace_sources(
|
||||
WorkerVisibilitySource::ResumePicker,
|
||||
|
|
|
|||
|
|
@ -54,11 +54,12 @@ impl CliCommand {
|
|||
|
||||
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::DefaultTui
|
||||
| CliCommand::Workers
|
||||
| CliCommand::Resume
|
||||
| CliCommand::Panel => CliConnectionRequirement::ConnectionAware,
|
||||
CliCommand::Login => CliConnectionRequirement::BackendOnly,
|
||||
CliCommand::Keys
|
||||
| CliCommand::SetupModel
|
||||
| CliCommand::WorkerRuntime
|
||||
| CliCommand::WorkerCleanup
|
||||
|
|
@ -182,6 +183,33 @@ pub(crate) fn resolve_backend_cli_connection<R: CliConnectionResolver + ?Sized>(
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_connection_aware_cli_connection<R: CliConnectionResolver + ?Sized>(
|
||||
resolver: &R,
|
||||
command: CliCommand,
|
||||
explicit_local: bool,
|
||||
explicit_backend_url: Option<String>,
|
||||
workspace_id: Option<&str>,
|
||||
) -> Result<Box<dyn Target>, ParseError> {
|
||||
if explicit_local && explicit_backend_url.is_some() {
|
||||
return Err(ParseError(
|
||||
"--local and --backend are mutually exclusive".to_string(),
|
||||
));
|
||||
}
|
||||
if explicit_local {
|
||||
return resolver.resolve_connection(command, CliConnectionInput::LocalDefault);
|
||||
}
|
||||
if explicit_backend_url.is_some() {
|
||||
return resolver.resolve_connection(
|
||||
command,
|
||||
CliConnectionInput::BackendTarget {
|
||||
explicit_backend_url,
|
||||
workspace_id,
|
||||
},
|
||||
);
|
||||
}
|
||||
resolver.resolve_connection(command, CliConnectionInput::LocalDefault)
|
||||
}
|
||||
|
||||
pub(crate) fn backend_target_option_error_for_local_command(
|
||||
command: CliCommand,
|
||||
option: &str,
|
||||
|
|
@ -209,22 +237,25 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn cli_command_connection_requirements_are_explicit() {
|
||||
assert_eq!(
|
||||
CliCommand::DefaultTui.connection_requirement(),
|
||||
CliConnectionRequirement::ConnectionAware
|
||||
);
|
||||
assert_eq!(
|
||||
CliCommand::Workers.connection_requirement(),
|
||||
CliConnectionRequirement::BackendOnly
|
||||
);
|
||||
for command in [
|
||||
CliCommand::DefaultTui,
|
||||
CliCommand::Workers,
|
||||
CliCommand::Resume,
|
||||
CliCommand::Panel,
|
||||
] {
|
||||
assert_eq!(
|
||||
command.connection_requirement(),
|
||||
CliConnectionRequirement::ConnectionAware,
|
||||
"{} should be connection-aware",
|
||||
command.display_name()
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
CliCommand::Login.connection_requirement(),
|
||||
CliConnectionRequirement::BackendOnly
|
||||
);
|
||||
|
||||
for command in [
|
||||
CliCommand::Resume,
|
||||
CliCommand::Panel,
|
||||
CliCommand::Keys,
|
||||
CliCommand::SetupModel,
|
||||
CliCommand::WorkerRuntime,
|
||||
|
|
@ -251,7 +282,7 @@ mod tests {
|
|||
let resolver = ClientConfigCliConnectionResolver;
|
||||
let err = resolver
|
||||
.resolve_connection(
|
||||
CliCommand::Resume,
|
||||
CliCommand::Keys,
|
||||
CliConnectionInput::BackendTarget {
|
||||
explicit_backend_url: Some("http://127.0.0.1:8787".to_string()),
|
||||
workspace_id: None,
|
||||
|
|
@ -261,7 +292,7 @@ mod tests {
|
|||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"yoi resume uses a local connection target and cannot accept Backend target options"
|
||||
"yoi keys uses a local connection target and cannot accept Backend target options"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -269,12 +300,12 @@ mod tests {
|
|||
fn cli_connection_resolver_rejects_backend_only_local_default() {
|
||||
let resolver = ClientConfigCliConnectionResolver;
|
||||
let err = resolver
|
||||
.resolve_connection(CliCommand::Workers, CliConnectionInput::LocalDefault)
|
||||
.resolve_connection(CliCommand::Login, CliConnectionInput::LocalDefault)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"yoi workers requires a Backend connection target"
|
||||
"yoi login requires a Backend connection target"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -293,7 +324,8 @@ mod tests {
|
|||
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);
|
||||
let backend_target = workers.backend_target.as_ref().unwrap();
|
||||
assert_eq!(backend_target.base_url, "http://127.0.0.1:8787");
|
||||
assert_eq!(backend_target.workspace_id, None);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ use std::time::Duration;
|
|||
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,
|
||||
resolve_backend_cli_connection, resolve_connection_aware_cli_connection,
|
||||
resolve_local_cli_connection,
|
||||
};
|
||||
use client::{BackendAuthTarget, Target, start_device_login, wait_for_device_login};
|
||||
use memory_lint::{LintCliOptions, LintStatus};
|
||||
|
|
@ -199,6 +200,107 @@ async fn main() -> ExitCode {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
struct TargetSelection {
|
||||
explicit_local: bool,
|
||||
backend_url: Option<String>,
|
||||
workspace_id: Option<String>,
|
||||
}
|
||||
|
||||
impl TargetSelection {
|
||||
fn explicit_backend(&self) -> bool {
|
||||
self.backend_url.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_tui_target<R: CliConnectionResolver + ?Sized>(
|
||||
connection_resolver: &R,
|
||||
command: CliCommand,
|
||||
selection: &TargetSelection,
|
||||
workspace_root: &Path,
|
||||
) -> Result<Box<dyn Target>, ParseError> {
|
||||
let workspace_id = if selection.explicit_backend() && selection.workspace_id.is_none() {
|
||||
resolve_workspace_id_from_root(workspace_root)?
|
||||
} else {
|
||||
selection.workspace_id.clone()
|
||||
};
|
||||
resolve_connection_aware_cli_connection(
|
||||
connection_resolver,
|
||||
command,
|
||||
selection.explicit_local,
|
||||
selection.backend_url.clone(),
|
||||
workspace_id.as_deref(),
|
||||
)
|
||||
}
|
||||
|
||||
fn parse_top_level_target_selection(
|
||||
args: &[String],
|
||||
) -> Result<(TargetSelection, &[String]), ParseError> {
|
||||
let mut selection = TargetSelection::default();
|
||||
let mut i = 0;
|
||||
while i < args.len() {
|
||||
match args[i].as_str() {
|
||||
"--local" => {
|
||||
if selection.backend_url.is_some() {
|
||||
return Err(ParseError(
|
||||
"--local and --backend are mutually exclusive".to_string(),
|
||||
));
|
||||
}
|
||||
selection.explicit_local = true;
|
||||
i += 1;
|
||||
}
|
||||
"--backend" => {
|
||||
if selection.explicit_local {
|
||||
return Err(ParseError(
|
||||
"--local and --backend are mutually exclusive".to_string(),
|
||||
));
|
||||
}
|
||||
let value = args
|
||||
.get(i + 1)
|
||||
.ok_or_else(|| ParseError("--backend requires a URL".to_string()))?;
|
||||
if value.starts_with('-') || value.is_empty() {
|
||||
return Err(ParseError("--backend requires a URL".to_string()));
|
||||
}
|
||||
selection.backend_url = Some(value.clone());
|
||||
i += 2;
|
||||
}
|
||||
"--workspace-id" => {
|
||||
let value = args
|
||||
.get(i + 1)
|
||||
.ok_or_else(|| ParseError("--workspace-id requires a value".to_string()))?;
|
||||
if value.starts_with('-') || value.is_empty() {
|
||||
return Err(ParseError("--workspace-id requires a value".to_string()));
|
||||
}
|
||||
selection.workspace_id = Some(value.clone());
|
||||
i += 2;
|
||||
}
|
||||
arg if arg.starts_with("--backend=") => {
|
||||
if selection.explicit_local {
|
||||
return Err(ParseError(
|
||||
"--local and --backend are mutually exclusive".to_string(),
|
||||
));
|
||||
}
|
||||
let value = arg.trim_start_matches("--backend=");
|
||||
if value.is_empty() {
|
||||
return Err(ParseError("--backend requires a URL".to_string()));
|
||||
}
|
||||
selection.backend_url = Some(value.to_string());
|
||||
i += 1;
|
||||
}
|
||||
arg if arg.starts_with("--workspace-id=") => {
|
||||
let value = arg.trim_start_matches("--workspace-id=");
|
||||
if value.is_empty() {
|
||||
return Err(ParseError("--workspace-id requires a value".to_string()));
|
||||
}
|
||||
selection.workspace_id = Some(value.to_string());
|
||||
i += 1;
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
Ok((selection, &args[i..]))
|
||||
}
|
||||
|
||||
fn parse_args() -> Result<Mode, ParseError> {
|
||||
parse_args_from(std::env::args().skip(1))
|
||||
}
|
||||
|
|
@ -221,22 +323,38 @@ fn parse_args_slice_with_connection_resolver<R: CliConnectionResolver + ?Sized>(
|
|||
args: &[String],
|
||||
connection_resolver: &R,
|
||||
) -> Result<Mode, ParseError> {
|
||||
let (target_selection, args) = parse_top_level_target_selection(args)?;
|
||||
if args.is_empty() {
|
||||
let target = resolve_local_cli_connection(connection_resolver, CliCommand::DefaultTui)?;
|
||||
return Ok(Mode::Tui {
|
||||
target,
|
||||
mode: LaunchMode::Spawn {
|
||||
let workspace_root = current_dir()?;
|
||||
let target = resolve_tui_target(
|
||||
connection_resolver,
|
||||
CliCommand::DefaultTui,
|
||||
&target_selection,
|
||||
&workspace_root,
|
||||
)?;
|
||||
let mode = if target_selection.explicit_backend() {
|
||||
LaunchMode::Workers {
|
||||
runtime_id: None,
|
||||
include_stopped: false,
|
||||
all: false,
|
||||
}
|
||||
} else {
|
||||
LaunchMode::Spawn {
|
||||
worker_name: None,
|
||||
profile: None,
|
||||
},
|
||||
workspace_root: current_dir()?,
|
||||
}
|
||||
};
|
||||
return Ok(Mode::Tui {
|
||||
target,
|
||||
mode,
|
||||
workspace_root,
|
||||
});
|
||||
}
|
||||
|
||||
match args[0].as_str() {
|
||||
"--help" | "-h" => return Ok(Mode::Help),
|
||||
"resume" => return parse_resume_args(&args[1..], connection_resolver),
|
||||
"workers" => return parse_workers_args(&args[1..], connection_resolver),
|
||||
"resume" => return parse_resume_args(&args[1..], &target_selection, connection_resolver),
|
||||
"workers" => return parse_workers_args(&args[1..], &target_selection, connection_resolver),
|
||||
"worker" => {
|
||||
if let Some(cli) = worker_cleanup_cli::parse_worker_management_args(&args[1..])
|
||||
.map_err(|e| ParseError(e.to_string()))?
|
||||
|
|
@ -291,11 +409,19 @@ fn parse_args_slice_with_connection_resolver<R: CliConnectionResolver + ?Sized>(
|
|||
return Ok(Mode::Mcp(mcp_cli));
|
||||
}
|
||||
"panel" => {
|
||||
let target = resolve_local_cli_connection(connection_resolver, CliCommand::Panel)?;
|
||||
let workspace_root = parse_panel_workspace(&args[1..])?;
|
||||
let target = resolve_tui_target(
|
||||
connection_resolver,
|
||||
CliCommand::Panel,
|
||||
&target_selection,
|
||||
&workspace_root,
|
||||
)?;
|
||||
return Ok(Mode::Tui {
|
||||
target,
|
||||
mode: LaunchMode::Panel,
|
||||
workspace_root: parse_panel_workspace(&args[1..])?,
|
||||
mode: LaunchMode::Panel {
|
||||
include_stopped: false,
|
||||
},
|
||||
workspace_root,
|
||||
});
|
||||
}
|
||||
"keys" => {
|
||||
|
|
@ -349,82 +475,73 @@ fn parse_args_slice_with_connection_resolver<R: CliConnectionResolver + ?Sized>(
|
|||
_ => {}
|
||||
}
|
||||
|
||||
parse_console_options(args, connection_resolver)
|
||||
parse_console_options(args, &target_selection, connection_resolver)
|
||||
}
|
||||
|
||||
fn parse_console_options<R: CliConnectionResolver + ?Sized>(
|
||||
args: &[String],
|
||||
target_selection: &TargetSelection,
|
||||
connection_resolver: &R,
|
||||
) -> Result<Mode, ParseError> {
|
||||
let mut workspace_root = current_dir()?;
|
||||
let mut session = None;
|
||||
let mut worker_name = None;
|
||||
let mut session = None;
|
||||
let mut profile = None;
|
||||
let mut socket_override = None;
|
||||
let mut backend_url = None;
|
||||
let mut workspace_id = None;
|
||||
let mut runtime_id = None;
|
||||
let mut worker_id = None;
|
||||
let mut profile = None;
|
||||
|
||||
let mut i = 0;
|
||||
while i < args.len() {
|
||||
let arg = &args[i];
|
||||
match arg.as_str() {
|
||||
"--session" => {
|
||||
let value = args
|
||||
.get(i + 1)
|
||||
.ok_or_else(|| ParseError("--session requires a value".to_string()))?;
|
||||
session = Some(parse_session_id(value)?);
|
||||
i += 2;
|
||||
}
|
||||
"--worker" => {
|
||||
let value = args
|
||||
.get(i + 1)
|
||||
.ok_or_else(|| ParseError("--worker requires a value".to_string()))?;
|
||||
if value.starts_with('-') {
|
||||
return Err(ParseError("--worker requires a value".to_string()));
|
||||
.ok_or_else(|| ParseError("--worker requires a name".to_string()))?;
|
||||
if value.starts_with('-') || value.is_empty() {
|
||||
return Err(ParseError("--worker requires a name".to_string()));
|
||||
}
|
||||
worker_name = Some(value.clone());
|
||||
i += 2;
|
||||
}
|
||||
"--socket" => {
|
||||
let value = args
|
||||
.get(i + 1)
|
||||
.ok_or_else(|| ParseError("--socket requires a value".to_string()))?;
|
||||
if value.starts_with('-') {
|
||||
return Err(ParseError("--socket requires a value".to_string()));
|
||||
}
|
||||
socket_override = Some(PathBuf::from(value));
|
||||
i += 2;
|
||||
}
|
||||
"--workspace" => {
|
||||
let value = args
|
||||
.get(i + 1)
|
||||
.ok_or_else(|| ParseError("--workspace requires a value".to_string()))?;
|
||||
if value.starts_with('-') {
|
||||
return Err(ParseError("--workspace requires a value".to_string()));
|
||||
.ok_or_else(|| ParseError("--workspace requires a path".to_string()))?;
|
||||
if value.starts_with('-') || value.is_empty() {
|
||||
return Err(ParseError("--workspace requires a path".to_string()));
|
||||
}
|
||||
workspace_root = PathBuf::from(value);
|
||||
i += 2;
|
||||
}
|
||||
"--workspace-id" => {
|
||||
"--session" => {
|
||||
let value = args
|
||||
.get(i + 1)
|
||||
.ok_or_else(|| ParseError("--workspace-id requires a value".to_string()))?;
|
||||
.ok_or_else(|| ParseError("--session requires a path".to_string()))?;
|
||||
if value.starts_with('-') || value.is_empty() {
|
||||
return Err(ParseError("--workspace-id requires a value".to_string()));
|
||||
return Err(ParseError("--session requires a path".to_string()));
|
||||
}
|
||||
workspace_id = Some(value.clone());
|
||||
session = Some(PathBuf::from(value));
|
||||
i += 2;
|
||||
}
|
||||
"--backend" => {
|
||||
"--socket" => {
|
||||
let value = args
|
||||
.get(i + 1)
|
||||
.ok_or_else(|| ParseError("--backend requires a URL".to_string()))?;
|
||||
.ok_or_else(|| ParseError("--socket requires a path".to_string()))?;
|
||||
if value.starts_with('-') || value.is_empty() {
|
||||
return Err(ParseError("--backend requires a URL".to_string()));
|
||||
return Err(ParseError("--socket requires a path".to_string()));
|
||||
}
|
||||
backend_url = Some(value.clone());
|
||||
socket_override = Some(PathBuf::from(value));
|
||||
i += 2;
|
||||
}
|
||||
"--profile" => {
|
||||
let value = args
|
||||
.get(i + 1)
|
||||
.ok_or_else(|| ParseError("--profile requires a name".to_string()))?;
|
||||
if value.starts_with('-') || value.is_empty() {
|
||||
return Err(ParseError("--profile requires a name".to_string()));
|
||||
}
|
||||
profile = Some(value.clone());
|
||||
i += 2;
|
||||
}
|
||||
"--runtime-id" | "--runtime" => {
|
||||
|
|
@ -447,62 +564,44 @@ fn parse_console_options<R: CliConnectionResolver + ?Sized>(
|
|||
worker_id = Some(value.clone());
|
||||
i += 2;
|
||||
}
|
||||
"--profile" => {
|
||||
let value = args
|
||||
.get(i + 1)
|
||||
.ok_or_else(|| ParseError("--profile requires a value".to_string()))?;
|
||||
if value.starts_with('-') {
|
||||
return Err(ParseError("--profile requires a value".to_string()));
|
||||
}
|
||||
profile = Some(value.clone());
|
||||
i += 2;
|
||||
}
|
||||
arg if arg.starts_with("--session=") => {
|
||||
let value = arg.trim_start_matches("--session=");
|
||||
if value.is_empty() {
|
||||
return Err(ParseError("--session requires a value".to_string()));
|
||||
}
|
||||
session = Some(parse_session_id(value)?);
|
||||
i += 1;
|
||||
}
|
||||
arg if arg.starts_with("--worker=") => {
|
||||
let value = arg.trim_start_matches("--worker=");
|
||||
if value.is_empty() {
|
||||
return Err(ParseError("--worker requires a value".to_string()));
|
||||
return Err(ParseError("--worker requires a name".to_string()));
|
||||
}
|
||||
worker_name = Some(value.to_string());
|
||||
i += 1;
|
||||
}
|
||||
arg if arg.starts_with("--socket=") => {
|
||||
let value = arg.trim_start_matches("--socket=");
|
||||
if value.is_empty() {
|
||||
return Err(ParseError("--socket requires a value".to_string()));
|
||||
}
|
||||
socket_override = Some(PathBuf::from(value));
|
||||
i += 1;
|
||||
}
|
||||
arg if arg.starts_with("--workspace=") => {
|
||||
let value = arg.trim_start_matches("--workspace=");
|
||||
if value.is_empty() {
|
||||
return Err(ParseError("--workspace requires a value".to_string()));
|
||||
return Err(ParseError("--workspace requires a path".to_string()));
|
||||
}
|
||||
workspace_root = PathBuf::from(value);
|
||||
i += 1;
|
||||
}
|
||||
arg if arg.starts_with("--workspace-id=") => {
|
||||
let value = arg.trim_start_matches("--workspace-id=");
|
||||
arg if arg.starts_with("--session=") => {
|
||||
let value = arg.trim_start_matches("--session=");
|
||||
if value.is_empty() {
|
||||
return Err(ParseError("--workspace-id requires a value".to_string()));
|
||||
return Err(ParseError("--session requires a path".to_string()));
|
||||
}
|
||||
workspace_id = Some(value.to_string());
|
||||
session = Some(PathBuf::from(value));
|
||||
i += 1;
|
||||
}
|
||||
arg if arg.starts_with("--backend=") => {
|
||||
let value = arg.trim_start_matches("--backend=");
|
||||
arg if arg.starts_with("--socket=") => {
|
||||
let value = arg.trim_start_matches("--socket=");
|
||||
if value.is_empty() {
|
||||
return Err(ParseError("--backend requires a URL".to_string()));
|
||||
return Err(ParseError("--socket requires a path".to_string()));
|
||||
}
|
||||
backend_url = Some(value.to_string());
|
||||
socket_override = Some(PathBuf::from(value));
|
||||
i += 1;
|
||||
}
|
||||
arg if arg.starts_with("--profile=") => {
|
||||
let value = arg.trim_start_matches("--profile=");
|
||||
if value.is_empty() {
|
||||
return Err(ParseError("--profile requires a name".to_string()));
|
||||
}
|
||||
profile = Some(value.to_string());
|
||||
i += 1;
|
||||
}
|
||||
arg if arg.starts_with("--runtime-id=") => {
|
||||
|
|
@ -529,144 +628,126 @@ fn parse_console_options<R: CliConnectionResolver + ?Sized>(
|
|||
worker_id = Some(value.to_string());
|
||||
i += 1;
|
||||
}
|
||||
arg if arg.starts_with("--profile=") => {
|
||||
let value = arg.trim_start_matches("--profile=");
|
||||
if value.is_empty() {
|
||||
return Err(ParseError("--profile requires a value".to_string()));
|
||||
}
|
||||
profile = Some(value.to_string());
|
||||
i += 1;
|
||||
}
|
||||
arg if arg.starts_with('-') => {
|
||||
return Err(ParseError(format!("unknown argument: {arg}")));
|
||||
}
|
||||
value => {
|
||||
return Err(ParseError(format!(
|
||||
"unknown command `{value}`; use --worker <NAME> to open a Worker by name"
|
||||
"yoi does not accept positional argument `{value}` before a subcommand"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let backend_target_present = backend_url.is_some()
|
||||
|| workspace_id.is_some()
|
||||
|| runtime_id.is_some()
|
||||
|| worker_id.is_some();
|
||||
if backend_target_present && worker_id.is_some() && runtime_id.is_none() {
|
||||
if worker_id.is_some() && runtime_id.is_none() {
|
||||
return Err(ParseError(
|
||||
"--worker-id requires --runtime-id for Backend Runtime API attach".to_string(),
|
||||
"--worker-id requires --runtime-id for Runtime API attach".to_string(),
|
||||
));
|
||||
}
|
||||
if backend_target_present
|
||||
if (runtime_id.is_some() || worker_id.is_some())
|
||||
&& (session.is_some()
|
||||
|| worker_name.is_some()
|
||||
|| socket_override.is_some()
|
||||
|| profile.is_some())
|
||||
{
|
||||
return Err(ParseError(
|
||||
"Backend Runtime API target cannot be combined with --worker, --socket, --session, or --profile".to_string(),
|
||||
"Runtime API target cannot be combined with --worker, --socket, --session, or --profile".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if profile.is_some() && (session.is_some() || socket_override.is_some()) {
|
||||
return Err(ParseError(
|
||||
"--profile can only be used for fresh spawn".to_string(),
|
||||
));
|
||||
}
|
||||
if session.is_some() && socket_override.is_some() {
|
||||
return Err(ParseError(
|
||||
"--session cannot be combined with --socket".to_string(),
|
||||
));
|
||||
}
|
||||
if socket_override.is_some() && worker_name.is_none() {
|
||||
return Err(ParseError("--socket requires --worker".to_string()));
|
||||
}
|
||||
if socket_override.is_some() && session.is_some() {
|
||||
return Err(ParseError(
|
||||
"--socket can only be used with --worker attach mode".to_string(),
|
||||
));
|
||||
|
||||
let target = resolve_tui_target(
|
||||
connection_resolver,
|
||||
CliCommand::DefaultTui,
|
||||
target_selection,
|
||||
&workspace_root,
|
||||
)?;
|
||||
|
||||
if let (Some(runtime_id), Some(worker_id)) = (runtime_id.clone(), worker_id) {
|
||||
return Ok(Mode::Tui {
|
||||
target,
|
||||
mode: LaunchMode::OpenWorker {
|
||||
runtime_id,
|
||||
worker_id,
|
||||
},
|
||||
workspace_root,
|
||||
});
|
||||
}
|
||||
|
||||
if backend_target_present {
|
||||
let workspace_id = match workspace_id {
|
||||
Some(workspace_id) => Some(workspace_id),
|
||||
None => resolve_workspace_id_from_root(&workspace_root)?,
|
||||
};
|
||||
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 {
|
||||
target,
|
||||
mode: LaunchMode::OpenWorker {
|
||||
runtime_id,
|
||||
worker_id,
|
||||
},
|
||||
workspace_root,
|
||||
});
|
||||
if runtime_id.is_some() {
|
||||
return Ok(Mode::Tui {
|
||||
target,
|
||||
mode: LaunchMode::Workers {
|
||||
runtime_id,
|
||||
include_stopped: false,
|
||||
all: false,
|
||||
},
|
||||
workspace_root,
|
||||
});
|
||||
}
|
||||
|
||||
let mode = if let Some(profile) = profile {
|
||||
LaunchMode::Spawn {
|
||||
worker_name,
|
||||
profile: Some(profile),
|
||||
}
|
||||
return Ok(Mode::Tui {
|
||||
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),
|
||||
},
|
||||
workspace_root,
|
||||
});
|
||||
}
|
||||
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,
|
||||
},
|
||||
workspace_root,
|
||||
});
|
||||
}
|
||||
Ok(Mode::Tui {
|
||||
target,
|
||||
mode: LaunchMode::Spawn {
|
||||
} else if let Some(session) = session {
|
||||
LaunchMode::ResumeWithSession {
|
||||
id: parse_session_id(&session.to_string_lossy())?,
|
||||
worker_name,
|
||||
}
|
||||
} else if let Some(worker_name) = worker_name {
|
||||
LaunchMode::WorkerName {
|
||||
worker_name,
|
||||
socket_override,
|
||||
}
|
||||
} else {
|
||||
LaunchMode::Spawn {
|
||||
worker_name: None,
|
||||
profile: None,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Mode::Tui {
|
||||
target,
|
||||
mode,
|
||||
workspace_root,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_workers_args<R: CliConnectionResolver + ?Sized>(
|
||||
args: &[String],
|
||||
target_selection: &TargetSelection,
|
||||
connection_resolver: &R,
|
||||
) -> Result<Mode, ParseError> {
|
||||
let mut workspace_root = current_dir()?;
|
||||
let mut workspace_id = None;
|
||||
let mut backend_url = None;
|
||||
let mut runtime_id = None;
|
||||
|
||||
let mut include_stopped = false;
|
||||
let mut i = 0;
|
||||
while i < args.len() {
|
||||
let arg = &args[i];
|
||||
match arg.as_str() {
|
||||
"--help" | "-h" => {
|
||||
return Err(ParseError(
|
||||
"usage: yoi workers [--workspace PATH] [--workspace-id ID] [--backend URL] [--runtime-id ID]".to_string(),
|
||||
"usage: yoi [--local|--backend URL] [--workspace-id ID] workers [-r|--stopped] [--workspace PATH] [--runtime-id ID]".to_string(),
|
||||
));
|
||||
}
|
||||
"-r" | "--restoreable" | "--stopped" => {
|
||||
include_stopped = true;
|
||||
i += 1;
|
||||
}
|
||||
"--workspace" => {
|
||||
let value = args
|
||||
.get(i + 1)
|
||||
|
|
@ -677,26 +758,6 @@ fn parse_workers_args<R: CliConnectionResolver + ?Sized>(
|
|||
workspace_root = PathBuf::from(value);
|
||||
i += 2;
|
||||
}
|
||||
"--workspace-id" => {
|
||||
let value = args
|
||||
.get(i + 1)
|
||||
.ok_or_else(|| ParseError("--workspace-id requires a value".to_string()))?;
|
||||
if value.starts_with('-') || value.is_empty() {
|
||||
return Err(ParseError("--workspace-id requires a value".to_string()));
|
||||
}
|
||||
workspace_id = Some(value.clone());
|
||||
i += 2;
|
||||
}
|
||||
"--backend" => {
|
||||
let value = args
|
||||
.get(i + 1)
|
||||
.ok_or_else(|| ParseError("--backend requires a URL".to_string()))?;
|
||||
if value.starts_with('-') || value.is_empty() {
|
||||
return Err(ParseError("--backend requires a URL".to_string()));
|
||||
}
|
||||
backend_url = Some(value.clone());
|
||||
i += 2;
|
||||
}
|
||||
"--runtime-id" | "--runtime" => {
|
||||
let value = args
|
||||
.get(i + 1)
|
||||
|
|
@ -715,22 +776,6 @@ fn parse_workers_args<R: CliConnectionResolver + ?Sized>(
|
|||
workspace_root = PathBuf::from(value);
|
||||
i += 1;
|
||||
}
|
||||
arg if arg.starts_with("--workspace-id=") => {
|
||||
let value = arg.trim_start_matches("--workspace-id=");
|
||||
if value.is_empty() {
|
||||
return Err(ParseError("--workspace-id requires a value".to_string()));
|
||||
}
|
||||
workspace_id = Some(value.to_string());
|
||||
i += 1;
|
||||
}
|
||||
arg if arg.starts_with("--backend=") => {
|
||||
let value = arg.trim_start_matches("--backend=");
|
||||
if value.is_empty() {
|
||||
return Err(ParseError("--backend requires a URL".to_string()));
|
||||
}
|
||||
backend_url = Some(value.to_string());
|
||||
i += 1;
|
||||
}
|
||||
arg if arg.starts_with("--runtime-id=") => {
|
||||
let value = arg.trim_start_matches("--runtime-id=");
|
||||
if value.is_empty() {
|
||||
|
|
@ -757,31 +802,32 @@ fn parse_workers_args<R: CliConnectionResolver + ?Sized>(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
let workspace_id = match workspace_id {
|
||||
Some(workspace_id) => Some(workspace_id),
|
||||
None => resolve_workspace_id_from_root(&workspace_root)?,
|
||||
};
|
||||
let target = resolve_backend_cli_connection(
|
||||
let target = resolve_tui_target(
|
||||
connection_resolver,
|
||||
CliCommand::Workers,
|
||||
backend_url,
|
||||
workspace_id.as_deref(),
|
||||
target_selection,
|
||||
&workspace_root,
|
||||
)?;
|
||||
Ok(Mode::Tui {
|
||||
target,
|
||||
mode: LaunchMode::Workers { runtime_id },
|
||||
mode: LaunchMode::Workers {
|
||||
runtime_id,
|
||||
include_stopped,
|
||||
all: false,
|
||||
},
|
||||
workspace_root,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_resume_args<R: CliConnectionResolver + ?Sized>(
|
||||
args: &[String],
|
||||
target_selection: &TargetSelection,
|
||||
connection_resolver: &R,
|
||||
) -> Result<Mode, ParseError> {
|
||||
let mut workspace_root = current_dir()?;
|
||||
let mut workspace_set = false;
|
||||
let mut all = false;
|
||||
let mut runtime_id = None;
|
||||
|
||||
let mut i = 0;
|
||||
while i < args.len() {
|
||||
|
|
@ -799,6 +845,16 @@ fn parse_resume_args<R: CliConnectionResolver + ?Sized>(
|
|||
all = true;
|
||||
i += 1;
|
||||
}
|
||||
"--runtime-id" | "--runtime" => {
|
||||
let value = args
|
||||
.get(i + 1)
|
||||
.ok_or_else(|| ParseError("--runtime-id requires a value".to_string()))?;
|
||||
if value.starts_with('-') || value.is_empty() {
|
||||
return Err(ParseError("--runtime-id requires a value".to_string()));
|
||||
}
|
||||
runtime_id = Some(value.clone());
|
||||
i += 2;
|
||||
}
|
||||
"--workspace" => {
|
||||
let value = args
|
||||
.get(i + 1)
|
||||
|
|
@ -819,13 +875,23 @@ fn parse_resume_args<R: CliConnectionResolver + ?Sized>(
|
|||
workspace_set = true;
|
||||
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,
|
||||
));
|
||||
arg if arg.starts_with("--runtime-id=") => {
|
||||
let value = arg.trim_start_matches("--runtime-id=");
|
||||
if value.is_empty() {
|
||||
return Err(ParseError("--runtime-id requires a value".to_string()));
|
||||
}
|
||||
runtime_id = Some(value.to_string());
|
||||
i += 1;
|
||||
}
|
||||
arg if arg.starts_with("--runtime=") => {
|
||||
let value = arg.trim_start_matches("--runtime=");
|
||||
if value.is_empty() {
|
||||
return Err(ParseError("--runtime-id requires a value".to_string()));
|
||||
}
|
||||
runtime_id = Some(value.to_string());
|
||||
i += 1;
|
||||
}
|
||||
arg if arg.starts_with('-') => {
|
||||
return Err(ParseError(format!("unknown yoi resume option `{arg}`")));
|
||||
}
|
||||
value => {
|
||||
|
|
@ -842,11 +908,20 @@ fn parse_resume_args<R: CliConnectionResolver + ?Sized>(
|
|||
));
|
||||
}
|
||||
|
||||
let target = resolve_local_cli_connection(connection_resolver, CliCommand::Resume)?;
|
||||
let target = resolve_tui_target(
|
||||
connection_resolver,
|
||||
CliCommand::Resume,
|
||||
target_selection,
|
||||
&workspace_root,
|
||||
)?;
|
||||
|
||||
Ok(Mode::Tui {
|
||||
target,
|
||||
mode: LaunchMode::Resume { all },
|
||||
mode: LaunchMode::Workers {
|
||||
runtime_id,
|
||||
include_stopped: true,
|
||||
all,
|
||||
},
|
||||
workspace_root,
|
||||
})
|
||||
}
|
||||
|
|
@ -1547,7 +1622,7 @@ fn parse_session_id(value: &str) -> Result<SegmentId, ParseError> {
|
|||
|
||||
fn print_help() {
|
||||
println!(
|
||||
"yoi\n\nUsage:\n yoi [OPTIONS]\n yoi resume [--workspace <PATH>] [--all]\n yoi workers [--workspace <PATH>] [--workspace-id <ID>] [--backend <URL>] [--runtime-id <ID>]\n yoi panel [--workspace <PATH>]\n yoi keys\n yoi setup-model\n yoi worker [WORKER_OPTIONS]\n yoi worker delete <NAME> [--force] [--dry-run]\n yoi worker prune --older-than <DURATION> [--force] [--dry-run]\n yoi objective <COMMAND> [OPTIONS]\n yoi session analyze <SESSION_JSONL_PATH> --json\n yoi session prune --unreferenced [--older-than <DURATION>] [--force] [--dry-run]\n yoi ticket <COMMAND> [OPTIONS]\n yoi workspace init [OPTIONS]
|
||||
"yoi\n\nUsage:\n yoi [TARGET_OPTIONS] [OPTIONS]\n yoi [TARGET_OPTIONS] resume [--workspace <PATH>] [--all] [--runtime-id <ID>]\n yoi [TARGET_OPTIONS] workers [-r|--stopped] [--workspace <PATH>] [--runtime-id <ID>]\n yoi [TARGET_OPTIONS] panel [--workspace <PATH>]\n yoi keys\n yoi setup-model\n yoi worker [WORKER_OPTIONS]\n yoi worker delete <NAME> [--force] [--dry-run]\n yoi worker prune --older-than <DURATION> [--force] [--dry-run]\n yoi objective <COMMAND> [OPTIONS]\n yoi session analyze <SESSION_JSONL_PATH> --json\n yoi session prune --unreferenced [--older-than <DURATION>] [--force] [--dry-run]\n yoi ticket <COMMAND> [OPTIONS]\n yoi workspace init [OPTIONS]
|
||||
yoi workspace config <COMMAND> [OPTIONS]
|
||||
yoi workspace identity <COMMAND> [OPTIONS]
|
||||
yoi workspace trust-runtime <COMMAND> [OPTIONS]
|
||||
|
|
@ -1602,7 +1677,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn parser_uses_injected_cli_connection_resolver_for_backend_command() {
|
||||
fn parser_uses_local_target_for_workers_without_backend_option() {
|
||||
let resolver = FixedCliConnectionResolver {
|
||||
backend_url: "http://fake-backend.example",
|
||||
};
|
||||
|
|
@ -1612,13 +1687,14 @@ mod tests {
|
|||
match mode {
|
||||
Mode::Tui {
|
||||
target,
|
||||
mode: LaunchMode::Workers { runtime_id },
|
||||
mode: LaunchMode::Workers { runtime_id, .. },
|
||||
..
|
||||
} => {
|
||||
assert_eq!(runtime_id, None);
|
||||
assert_eq!(target.kind(), TargetKind::Backend);
|
||||
assert_eq!(target.kind(), TargetKind::Local);
|
||||
let workers = target.list_workers(WorkerListRequest::new(None)).unwrap();
|
||||
assert_eq!(workers.target.base_url, "http://fake-backend.example");
|
||||
assert!(workers.local_runtime_command.is_some());
|
||||
assert!(workers.backend_target.is_none());
|
||||
}
|
||||
other => panic!("expected Workers mode, got {other:?}"),
|
||||
}
|
||||
|
|
@ -1632,12 +1708,6 @@ mod tests {
|
|||
|
||||
#[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(),
|
||||
|
|
@ -1711,7 +1781,7 @@ mod tests {
|
|||
.unwrap_err();
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"--worker-id requires --runtime-id for Backend Runtime API attach"
|
||||
"--worker-id requires --runtime-id for Runtime API attach"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1721,15 +1791,26 @@ mod tests {
|
|||
{
|
||||
Mode::Tui {
|
||||
target,
|
||||
mode: LaunchMode::Workers { runtime_id },
|
||||
mode: LaunchMode::Workers { runtime_id, .. },
|
||||
..
|
||||
} => {
|
||||
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!(
|
||||
workers.backend_target.as_ref().unwrap().base_url,
|
||||
"http://127.0.0.1:8787"
|
||||
);
|
||||
assert_eq!(
|
||||
workers
|
||||
.backend_target
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.runtime_id
|
||||
.as_deref(),
|
||||
Some("r")
|
||||
);
|
||||
assert_eq!(runtime_id.as_deref(), Some("r"));
|
||||
}
|
||||
_ => panic!("expected Workers mode"),
|
||||
|
|
@ -1739,25 +1820,36 @@ mod tests {
|
|||
#[test]
|
||||
fn parse_workers_subcommand_uses_backend_runtime_picker() {
|
||||
match parse_args_from([
|
||||
"workers",
|
||||
"--backend",
|
||||
"http://127.0.0.1:8787",
|
||||
"--workspace-id",
|
||||
"workspace-a",
|
||||
"workers",
|
||||
])
|
||||
.unwrap()
|
||||
{
|
||||
Mode::Tui {
|
||||
target,
|
||||
mode: LaunchMode::Workers { runtime_id },
|
||||
mode: LaunchMode::Workers { runtime_id, .. },
|
||||
..
|
||||
} => {
|
||||
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!(
|
||||
workers.backend_target.as_ref().unwrap().base_url,
|
||||
"http://127.0.0.1:8787"
|
||||
);
|
||||
assert_eq!(
|
||||
workers
|
||||
.backend_target
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.workspace_id
|
||||
.as_deref(),
|
||||
Some("workspace-a")
|
||||
);
|
||||
assert_eq!(runtime_id, None);
|
||||
}
|
||||
_ => panic!("expected Workers mode"),
|
||||
|
|
@ -1779,7 +1871,7 @@ mod tests {
|
|||
.unwrap_err();
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"Backend Runtime API target cannot be combined with --worker, --socket, --session, or --profile"
|
||||
"Runtime API target cannot be combined with --worker, --socket, --session, or --profile"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1799,10 +1891,18 @@ mod tests {
|
|||
fn parse_resume_subcommand_defaults_to_workspace_scope() {
|
||||
match parse_args_from(["resume"]).unwrap() {
|
||||
Mode::Tui {
|
||||
mode: LaunchMode::Resume { all },
|
||||
mode:
|
||||
LaunchMode::Workers {
|
||||
include_stopped,
|
||||
all,
|
||||
..
|
||||
},
|
||||
..
|
||||
} => assert!(!all),
|
||||
_ => panic!("expected Resume mode"),
|
||||
} => {
|
||||
assert!(include_stopped);
|
||||
assert!(!all);
|
||||
}
|
||||
_ => panic!("expected Workers mode"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1810,14 +1910,20 @@ mod tests {
|
|||
fn parse_resume_workspace_scope() {
|
||||
match parse_args_from(["resume", "--workspace", "/tmp/resume-workspace"]).unwrap() {
|
||||
Mode::Tui {
|
||||
mode: LaunchMode::Resume { all },
|
||||
mode:
|
||||
LaunchMode::Workers {
|
||||
include_stopped,
|
||||
all,
|
||||
..
|
||||
},
|
||||
workspace_root,
|
||||
..
|
||||
} => {
|
||||
assert!(include_stopped);
|
||||
assert!(!all);
|
||||
assert_eq!(workspace_root, PathBuf::from("/tmp/resume-workspace"));
|
||||
}
|
||||
_ => panic!("expected Resume mode"),
|
||||
_ => panic!("expected Workers mode"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1825,10 +1931,18 @@ mod tests {
|
|||
fn parse_resume_all_scope() {
|
||||
match parse_args_from(["resume", "--all"]).unwrap() {
|
||||
Mode::Tui {
|
||||
mode: LaunchMode::Resume { all },
|
||||
mode:
|
||||
LaunchMode::Workers {
|
||||
include_stopped,
|
||||
all,
|
||||
..
|
||||
},
|
||||
..
|
||||
} => assert!(all),
|
||||
_ => panic!("expected Resume mode"),
|
||||
} => {
|
||||
assert!(include_stopped);
|
||||
assert!(all);
|
||||
}
|
||||
_ => panic!("expected Workers mode"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2228,7 +2342,10 @@ mod tests {
|
|||
fn parse_panel_mode() {
|
||||
match parse_args_from(["panel", "--workspace", "/tmp/other-workspace"]).unwrap() {
|
||||
Mode::Tui {
|
||||
mode: LaunchMode::Panel,
|
||||
mode:
|
||||
LaunchMode::Panel {
|
||||
include_stopped: false,
|
||||
},
|
||||
workspace_root,
|
||||
..
|
||||
} => assert_eq!(workspace_root, PathBuf::from("/tmp/other-workspace")),
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user