feat: add explicit resume command
This commit is contained in:
parent
a63b40f460
commit
861c351a96
|
|
@ -275,10 +275,17 @@ async fn connect_live_pod(
|
||||||
|
|
||||||
pub(crate) async fn run_resume(
|
pub(crate) async fn run_resume(
|
||||||
runtime_command: PodRuntimeCommand,
|
runtime_command: PodRuntimeCommand,
|
||||||
|
workspace_root: PathBuf,
|
||||||
|
all: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// Pick a Pod in its own inline viewport, dropping the viewport before
|
// Pick a Pod in its own inline viewport, dropping the viewport before
|
||||||
// attaching/restoring so each phase gets fresh vertical room.
|
// attaching/restoring so each phase gets fresh vertical room.
|
||||||
let (pod_name, socket_override) = match picker::run().await? {
|
let picker_options = if all {
|
||||||
|
picker::PickerOptions::all()
|
||||||
|
} else {
|
||||||
|
picker::PickerOptions::workspace(workspace_root)
|
||||||
|
};
|
||||||
|
let (pod_name, socket_override) = match picker::run(picker_options).await? {
|
||||||
PickerOutcome::Picked {
|
PickerOutcome::Picked {
|
||||||
pod_name,
|
pod_name,
|
||||||
socket_override,
|
socket_override,
|
||||||
|
|
|
||||||
|
|
@ -48,16 +48,17 @@ pub enum LaunchMode {
|
||||||
pod_name: Option<String>,
|
pod_name: Option<String>,
|
||||||
profile: Option<String>,
|
profile: Option<String>,
|
||||||
},
|
},
|
||||||
/// `yoi <name>` / `yoi --pod <name>`: attach to a live Pod by name if
|
/// `yoi --pod <name>`: attach to a live Pod by name if possible;
|
||||||
/// possible; otherwise launch the Pod runtime command with `--pod <name>` so it
|
/// otherwise launch the Pod runtime command with `--pod <name>` so it
|
||||||
/// resumes from name-keyed state or creates a fresh same-name Pod.
|
/// resumes from name-keyed state or creates a fresh same-name Pod.
|
||||||
PodName {
|
PodName {
|
||||||
pod_name: String,
|
pod_name: String,
|
||||||
socket_override: Option<PathBuf>,
|
socket_override: Option<PathBuf>,
|
||||||
},
|
},
|
||||||
/// `yoi -r` / `yoi --resume`: open the Pod picker, then attach to the
|
/// `yoi resume`: open the Pod picker, then attach to the selected live Pod
|
||||||
/// selected live Pod or restore the selected stopped Pod by name.
|
/// or restore the selected stopped Pod by name. Without `--all`, the picker
|
||||||
Resume,
|
/// is scoped to the current runtime workspace.
|
||||||
|
Resume { all: bool },
|
||||||
/// `yoi --session <UUID>`: skip the picker, go straight to the
|
/// `yoi --session <UUID>`: skip the picker, go straight to the
|
||||||
/// resume name dialog with `id` baked in.
|
/// resume name dialog with `id` baked in.
|
||||||
ResumeWithSession {
|
ResumeWithSession {
|
||||||
|
|
@ -101,7 +102,9 @@ pub async fn launch(options: LaunchOptions) -> ExitCode {
|
||||||
pod_name,
|
pod_name,
|
||||||
socket_override,
|
socket_override,
|
||||||
} => console::run_pod_name(pod_name, socket_override, runtime_command).await,
|
} => console::run_pod_name(pod_name, socket_override, runtime_command).await,
|
||||||
LaunchMode::Resume => console::run_resume(runtime_command).await,
|
LaunchMode::Resume { all } => {
|
||||||
|
console::run_resume(runtime_command, workspace_root.clone(), all).await
|
||||||
|
}
|
||||||
LaunchMode::ResumeWithSession { id, pod_name } => {
|
LaunchMode::ResumeWithSession { id, pod_name } => {
|
||||||
console::run_spawn(Some(id), pod_name, None, runtime_command).await
|
console::run_spawn(Some(id), pod_name, None, runtime_command).await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ use ratatui::{Frame, TerminalOptions, Viewport};
|
||||||
use session_store::FsStore;
|
use session_store::FsStore;
|
||||||
|
|
||||||
use crate::pod_list::{
|
use crate::pod_list::{
|
||||||
PodList, PodListEntry, PodVisibilitySource, StoredMetadataState,
|
LivePodInfo, PodList, PodListEntry, PodVisibilitySource, StoredMetadataState, StoredPodInfo,
|
||||||
live_socket_for_pod as pod_list_live_socket_for_pod, read_reachable_live_pod_infos,
|
live_socket_for_pod as pod_list_live_socket_for_pod, read_reachable_live_pod_infos,
|
||||||
read_stored_pod_infos,
|
read_stored_pod_infos,
|
||||||
};
|
};
|
||||||
|
|
@ -73,6 +73,31 @@ pub enum PickerOutcome {
|
||||||
Cancelled,
|
Cancelled,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct PickerOptions {
|
||||||
|
scope: PickerScope,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PickerOptions {
|
||||||
|
pub(crate) fn workspace(workspace_root: PathBuf) -> Self {
|
||||||
|
Self {
|
||||||
|
scope: PickerScope::Workspace(workspace_root),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn all() -> Self {
|
||||||
|
Self {
|
||||||
|
scope: PickerScope::All,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum PickerScope {
|
||||||
|
Workspace(PathBuf),
|
||||||
|
All,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
enum PodRowState {
|
enum PodRowState {
|
||||||
Live,
|
Live,
|
||||||
|
|
@ -100,7 +125,31 @@ impl PodRowState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run() -> Result<PickerOutcome, PickerError> {
|
fn list_for_options(
|
||||||
|
options: &PickerOptions,
|
||||||
|
stored_pods: Vec<StoredPodInfo>,
|
||||||
|
live_pods: Vec<LivePodInfo>,
|
||||||
|
) -> PodList {
|
||||||
|
match &options.scope {
|
||||||
|
PickerScope::Workspace(workspace_root) => PodList::from_workspace_sources(
|
||||||
|
PodVisibilitySource::ResumePicker,
|
||||||
|
stored_pods,
|
||||||
|
live_pods,
|
||||||
|
None,
|
||||||
|
MAX_ROWS,
|
||||||
|
workspace_root,
|
||||||
|
),
|
||||||
|
PickerScope::All => PodList::from_sources(
|
||||||
|
PodVisibilitySource::ResumePicker,
|
||||||
|
stored_pods,
|
||||||
|
live_pods,
|
||||||
|
None,
|
||||||
|
MAX_ROWS,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(options: PickerOptions) -> Result<PickerOutcome, PickerError> {
|
||||||
let store_dir = default_store_dir()?;
|
let store_dir = default_store_dir()?;
|
||||||
let store = FsStore::new(&store_dir)?;
|
let store = FsStore::new(&store_dir)?;
|
||||||
let pod_store = FsPodStore::new(default_pod_store_dir()?).map_err(io::Error::other)?;
|
let pod_store = FsPodStore::new(default_pod_store_dir()?).map_err(io::Error::other)?;
|
||||||
|
|
@ -108,13 +157,7 @@ pub async fn run() -> Result<PickerOutcome, PickerError> {
|
||||||
let live_pods = read_reachable_live_pod_infos(&store)
|
let live_pods = read_reachable_live_pod_infos(&store)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let mut list = PodList::from_sources(
|
let mut list = list_for_options(&options, stored_pods, live_pods);
|
||||||
PodVisibilitySource::ResumePicker,
|
|
||||||
stored_pods,
|
|
||||||
live_pods,
|
|
||||||
None,
|
|
||||||
MAX_ROWS,
|
|
||||||
);
|
|
||||||
if list.entries.is_empty() {
|
if list.entries.is_empty() {
|
||||||
return Err(PickerError::NoPods);
|
return Err(PickerError::NoPods);
|
||||||
}
|
}
|
||||||
|
|
@ -361,6 +404,58 @@ mod tests {
|
||||||
assert_eq!(picker_title(), "resume pod pick a pod");
|
assert_eq!(picker_title(), "resume pod pick a pod");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn picker_workspace_options_filter_by_workspace_metadata() {
|
||||||
|
let list = list_for_options(
|
||||||
|
&PickerOptions::workspace(PathBuf::from("/workspace/current")),
|
||||||
|
vec![
|
||||||
|
stored_pod("current", Some("/workspace/current"), 3),
|
||||||
|
stored_pod("other", Some("/workspace/other"), 2),
|
||||||
|
stored_pod("legacy", None, 1),
|
||||||
|
],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
|
||||||
|
let names: Vec<_> = list
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.map(|entry| entry.name.as_str())
|
||||||
|
.collect();
|
||||||
|
assert_eq!(names, vec!["current"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn picker_all_options_include_host_wide_and_legacy_pods() {
|
||||||
|
let list = list_for_options(
|
||||||
|
&PickerOptions::all(),
|
||||||
|
vec![
|
||||||
|
stored_pod("current", Some("/workspace/current"), 3),
|
||||||
|
stored_pod("other", Some("/workspace/other"), 2),
|
||||||
|
stored_pod("legacy", None, 1),
|
||||||
|
],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
|
||||||
|
let names: Vec<_> = list
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.map(|entry| entry.name.as_str())
|
||||||
|
.collect();
|
||||||
|
assert_eq!(names, vec!["current", "other", "legacy"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stored_pod(name: &str, workspace_root: Option<&str>, updated_at: u64) -> StoredPodInfo {
|
||||||
|
StoredPodInfo {
|
||||||
|
pod_name: name.to_string(),
|
||||||
|
metadata_state: StoredMetadataState::Present,
|
||||||
|
active_session_id: None,
|
||||||
|
active_segment_id: None,
|
||||||
|
updated_at,
|
||||||
|
workspace_root: workspace_root.map(PathBuf::from),
|
||||||
|
preview: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn picker_row_shows_live_pending_preview_and_runtime_segment_id() {
|
fn picker_row_shows_live_pending_preview_and_runtime_segment_id() {
|
||||||
let segment_id = session_store::new_segment_id();
|
let segment_id = session_store::new_segment_id();
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ use tui::{LaunchMode, LaunchOptions};
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum Mode {
|
enum Mode {
|
||||||
Help,
|
Help,
|
||||||
|
ResumeHelp,
|
||||||
MemoryLintHelp,
|
MemoryLintHelp,
|
||||||
MemoryLint(LintCliOptions),
|
MemoryLint(LintCliOptions),
|
||||||
Mcp(mcp_cli::McpCliCommand),
|
Mcp(mcp_cli::McpCliCommand),
|
||||||
|
|
@ -60,6 +61,10 @@ async fn main() -> ExitCode {
|
||||||
print_help();
|
print_help();
|
||||||
ExitCode::SUCCESS
|
ExitCode::SUCCESS
|
||||||
}
|
}
|
||||||
|
Mode::ResumeHelp => {
|
||||||
|
print_resume_help();
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
}
|
||||||
Mode::MemoryLintHelp => {
|
Mode::MemoryLintHelp => {
|
||||||
print_memory_lint_help();
|
print_memory_lint_help();
|
||||||
ExitCode::SUCCESS
|
ExitCode::SUCCESS
|
||||||
|
|
@ -168,13 +173,13 @@ fn parse_args_slice(args: &[String]) -> Result<Mode, ParseError> {
|
||||||
pod_name: None,
|
pod_name: None,
|
||||||
profile: None,
|
profile: None,
|
||||||
},
|
},
|
||||||
workspace_root: std::env::current_dir()
|
workspace_root: current_dir()?,
|
||||||
.map_err(|e| ParseError(format!("failed to resolve current directory: {e}")))?,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
match args[0].as_str() {
|
match args[0].as_str() {
|
||||||
"--help" | "-h" => return Ok(Mode::Help),
|
"--help" | "-h" => return Ok(Mode::Help),
|
||||||
|
"resume" => return parse_resume_args(&args[1..]),
|
||||||
"pod" => return Ok(Mode::PodRuntime(args[1..].to_vec())),
|
"pod" => return Ok(Mode::PodRuntime(args[1..].to_vec())),
|
||||||
"objective" => {
|
"objective" => {
|
||||||
let objective_cli = objective_cli::parse_objective_args(&args[1..])
|
let objective_cli = objective_cli::parse_objective_args(&args[1..])
|
||||||
|
|
@ -229,35 +234,30 @@ fn parse_args_slice(args: &[String]) -> Result<Mode, ParseError> {
|
||||||
return Ok(Mode::MemoryLint(options));
|
return Ok(Mode::MemoryLint(options));
|
||||||
}
|
}
|
||||||
"memory" => {
|
"memory" => {
|
||||||
return Ok(Mode::Tui {
|
return Err(ParseError(
|
||||||
mode: LaunchMode::PodName {
|
"yoi memory requires the `lint` subcommand".to_string(),
|
||||||
pod_name: "memory".to_string(),
|
));
|
||||||
socket_override: None,
|
}
|
||||||
},
|
other if !other.starts_with('-') => {
|
||||||
workspace_root: std::env::current_dir()
|
return Err(ParseError(format!("unknown command `{other}`")));
|
||||||
.map_err(|e| ParseError(format!("failed to resolve current directory: {e}")))?,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut workspace_root = std::env::current_dir()
|
parse_console_options(args)
|
||||||
.map_err(|e| ParseError(format!("failed to resolve current directory: {e}")))?;
|
}
|
||||||
let mut resume = false;
|
|
||||||
|
fn parse_console_options(args: &[String]) -> Result<Mode, ParseError> {
|
||||||
|
let mut workspace_root = current_dir()?;
|
||||||
let mut session = None;
|
let mut session = None;
|
||||||
let mut pod_name = None;
|
let mut pod_name = None;
|
||||||
let mut socket_override = None;
|
let mut socket_override = None;
|
||||||
let mut profile = None;
|
let mut profile = None;
|
||||||
let mut positional = None;
|
|
||||||
|
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while i < args.len() {
|
while i < args.len() {
|
||||||
let arg = &args[i];
|
let arg = &args[i];
|
||||||
match arg.as_str() {
|
match arg.as_str() {
|
||||||
"--resume" | "-r" => {
|
|
||||||
resume = true;
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
"--session" => {
|
"--session" => {
|
||||||
let value = args
|
let value = args
|
||||||
.get(i + 1)
|
.get(i + 1)
|
||||||
|
|
@ -349,51 +349,21 @@ fn parse_args_slice(args: &[String]) -> Result<Mode, ParseError> {
|
||||||
return Err(ParseError(format!("unknown argument: {arg}")));
|
return Err(ParseError(format!("unknown argument: {arg}")));
|
||||||
}
|
}
|
||||||
value => {
|
value => {
|
||||||
if positional.replace(value.to_string()).is_some() {
|
return Err(ParseError(format!(
|
||||||
return Err(ParseError(
|
"unknown command `{value}`; use --pod <NAME> to open a Pod by name"
|
||||||
"only one positional Pod name is supported".to_string(),
|
)));
|
||||||
));
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if pod_name.is_some() && positional.is_some() {
|
if profile.is_some() && (session.is_some() || socket_override.is_some()) {
|
||||||
return Err(ParseError(
|
|
||||||
"--pod and a positional Pod name are mutually exclusive".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if profile.is_some()
|
|
||||||
&& (resume || session.is_some() || positional.is_some() || socket_override.is_some())
|
|
||||||
{
|
|
||||||
return Err(ParseError(
|
return Err(ParseError(
|
||||||
"--profile can only be used for fresh spawn".to_string(),
|
"--profile can only be used for fresh spawn".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if pod_name.is_some() && resume {
|
if socket_override.is_some() && pod_name.is_none() {
|
||||||
return Err(ParseError(
|
return Err(ParseError("--socket requires --pod".to_string()));
|
||||||
"--pod and --resume are mutually exclusive".to_string(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
if positional.is_some() && resume {
|
|
||||||
return Err(ParseError(
|
|
||||||
"--resume cannot be used with a positional Pod name".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if socket_override.is_some() && pod_name.is_none() && positional.is_none() {
|
|
||||||
return Err(ParseError(
|
|
||||||
"--socket requires --pod or a positional Pod name".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if resume && session.is_some() {
|
|
||||||
return Err(ParseError(
|
|
||||||
"--resume and --session are mutually exclusive".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let pod_name = pod_name.or(positional);
|
|
||||||
if socket_override.is_some() && session.is_some() {
|
if socket_override.is_some() && session.is_some() {
|
||||||
return Err(ParseError(
|
return Err(ParseError(
|
||||||
"--socket can only be used with --pod attach mode".to_string(),
|
"--socket can only be used with --pod attach mode".to_string(),
|
||||||
|
|
@ -424,12 +394,6 @@ fn parse_args_slice(args: &[String]) -> Result<Mode, ParseError> {
|
||||||
workspace_root,
|
workspace_root,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if resume {
|
|
||||||
return Ok(Mode::Tui {
|
|
||||||
mode: LaunchMode::Resume,
|
|
||||||
workspace_root,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(Mode::Tui {
|
Ok(Mode::Tui {
|
||||||
mode: LaunchMode::Spawn {
|
mode: LaunchMode::Spawn {
|
||||||
pod_name: None,
|
pod_name: None,
|
||||||
|
|
@ -439,6 +403,75 @@ fn parse_args_slice(args: &[String]) -> Result<Mode, ParseError> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_resume_args(args: &[String]) -> Result<Mode, ParseError> {
|
||||||
|
let mut workspace_root = current_dir()?;
|
||||||
|
let mut workspace_set = false;
|
||||||
|
let mut all = false;
|
||||||
|
|
||||||
|
let mut i = 0;
|
||||||
|
while i < args.len() {
|
||||||
|
let arg = &args[i];
|
||||||
|
match arg.as_str() {
|
||||||
|
"--help" | "-h" => {
|
||||||
|
if args.len() == 1 {
|
||||||
|
return Ok(Mode::ResumeHelp);
|
||||||
|
}
|
||||||
|
return Err(ParseError(
|
||||||
|
"yoi resume --help does not accept other arguments".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--all" => {
|
||||||
|
all = true;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
"--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()));
|
||||||
|
}
|
||||||
|
workspace_root = PathBuf::from(value);
|
||||||
|
workspace_set = true;
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
workspace_root = PathBuf::from(value);
|
||||||
|
workspace_set = true;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
arg if arg.starts_with('-') => {
|
||||||
|
return Err(ParseError(format!("unknown yoi resume option `{arg}`")));
|
||||||
|
}
|
||||||
|
value => {
|
||||||
|
return Err(ParseError(format!(
|
||||||
|
"yoi resume does not accept positional argument `{value}`"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if all && workspace_set {
|
||||||
|
return Err(ParseError(
|
||||||
|
"yoi resume --all and --workspace are mutually exclusive".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Mode::Tui {
|
||||||
|
mode: LaunchMode::Resume { all },
|
||||||
|
workspace_root,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_dir() -> Result<PathBuf, ParseError> {
|
||||||
|
std::env::current_dir()
|
||||||
|
.map_err(|e| ParseError(format!("failed to resolve current directory: {e}")))
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_plugin_args(args: &[String]) -> Result<plugin_cli::PluginCliCommand, ParseError> {
|
fn parse_plugin_args(args: &[String]) -> Result<plugin_cli::PluginCliCommand, ParseError> {
|
||||||
let Some((subcommand, rest)) = args.split_first() else {
|
let Some((subcommand, rest)) = args.split_first() else {
|
||||||
return Err(ParseError(
|
return Err(ParseError(
|
||||||
|
|
@ -777,7 +810,13 @@ fn parse_session_id(value: &str) -> Result<SegmentId, ParseError> {
|
||||||
|
|
||||||
fn print_help() {
|
fn print_help() {
|
||||||
println!(
|
println!(
|
||||||
"yoi\n\nUsage:\n yoi [OPTIONS] [POD_NAME]\n yoi panel [--workspace <PATH>]\n yoi keys\n yoi setup-model\n yoi pod [POD_OPTIONS]\n yoi objective <COMMAND> [OPTIONS]\n yoi session analyze <SESSION_JSONL_PATH> --json\n yoi ticket <COMMAND> [OPTIONS]\n yoi plugin new rust-component-tool <PATH> [--json]\n yoi plugin check <PATH_OR_PACKAGE> [--json]\n yoi plugin pack <PATH> [--output <FILE>] [--json]\n yoi plugin list [--workspace <PATH>] [--profile <REF>] [--json]\n yoi plugin show <REF> [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp list [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp show <SERVER> [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp tools|resources|prompts [SERVER] [--workspace <PATH>] [--profile <REF>] [--json]\n yoi memory lint [OPTIONS]\n\nSurfaces:\n Console Single-Pod chat/client surface (default, --pod, --resume)\n Dashboard Workspace cockpit/action surface (yoi panel)\n TUI Terminal UI implementation umbrella for Console and Dashboard\n\nOptions:\n -r, --resume Open the Pod Console picker and resume/attach a Pod\n --workspace <PATH> Runtime workspace root (defaults to cwd)\n --pod <NAME> Open the Pod Console by name (attach/restore/create)\n --socket <PATH> Attach a Pod Console to a specific socket with --pod\n --session <UUID> Resume a specific session segment in the Pod Console\n --profile <REF> Select a reusable Profile recipe\n -h, --help Print help\n"
|
"yoi\n\nUsage:\n yoi [OPTIONS]\n yoi resume [--workspace <PATH>] [--all]\n yoi panel [--workspace <PATH>]\n yoi keys\n yoi setup-model\n yoi pod [POD_OPTIONS]\n yoi objective <COMMAND> [OPTIONS]\n yoi session analyze <SESSION_JSONL_PATH> --json\n yoi ticket <COMMAND> [OPTIONS]\n yoi plugin new rust-component-tool <PATH> [--json]\n yoi plugin check <PATH_OR_PACKAGE> [--json]\n yoi plugin pack <PATH> [--output <FILE>] [--json]\n yoi plugin list [--workspace <PATH>] [--profile <REF>] [--json]\n yoi plugin show <REF> [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp list [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp show <SERVER> [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp tools|resources|prompts [SERVER] [--workspace <PATH>] [--profile <REF>] [--json]\n yoi memory lint [OPTIONS]\n\nSurfaces:\n Console Single-Pod chat/client surface (default, --pod, yoi resume)\n Dashboard Workspace cockpit/action surface (yoi panel)\n TUI Terminal UI implementation umbrella for Console and Dashboard\n\nOptions:\n --workspace <PATH> Runtime workspace root for default Console/--pod (defaults to cwd)\n --pod <NAME> Open the Pod Console by name (attach/restore/create)\n --socket <PATH> Attach a Pod Console to a specific socket with --pod\n --session <UUID> Resume a specific session segment in the Pod Console\n --profile <REF> Select a reusable Profile recipe\n -h, --help Print help\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_resume_help() {
|
||||||
|
println!(
|
||||||
|
"yoi resume\n\nUsage:\n yoi resume [--workspace <PATH>] [--all]\n\nOptions:\n --workspace <PATH> Open the Pod Console picker scoped to this workspace (defaults to cwd)\n --all Open the Pod Console picker across this host/data dir\n -h, --help Print help\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -810,38 +849,50 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_positional_name_uses_pod_name_mode() {
|
fn parse_bare_word_is_unknown_command() {
|
||||||
match parse_args_from(["agent"]).unwrap() {
|
let err = parse_args_from(["agent"]).unwrap_err();
|
||||||
Mode::Tui {
|
assert_eq!(err.to_string(), "unknown command `agent`");
|
||||||
mode:
|
|
||||||
LaunchMode::PodName {
|
|
||||||
pod_name,
|
|
||||||
socket_override,
|
|
||||||
},
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
assert_eq!(pod_name, "agent");
|
|
||||||
assert_eq!(socket_override, None);
|
|
||||||
}
|
}
|
||||||
_ => panic!("expected PodName mode"),
|
|
||||||
|
#[test]
|
||||||
|
fn parse_memory_without_lint_is_usage_error() {
|
||||||
|
let err = parse_args_from(["memory"]).unwrap_err();
|
||||||
|
assert_eq!(err.to_string(), "yoi memory requires the `lint` subcommand");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_resume_subcommand_defaults_to_workspace_scope() {
|
||||||
|
match parse_args_from(["resume"]).unwrap() {
|
||||||
|
Mode::Tui {
|
||||||
|
mode: LaunchMode::Resume { all },
|
||||||
|
..
|
||||||
|
} => assert!(!all),
|
||||||
|
_ => panic!("expected Resume mode"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_memory_alone_remains_positional_pod_name() {
|
fn parse_resume_workspace_scope() {
|
||||||
match parse_args_from(["memory"]).unwrap() {
|
match parse_args_from(["resume", "--workspace", "/tmp/resume-workspace"]).unwrap() {
|
||||||
Mode::Tui {
|
Mode::Tui {
|
||||||
mode:
|
mode: LaunchMode::Resume { all },
|
||||||
LaunchMode::PodName {
|
workspace_root,
|
||||||
pod_name,
|
|
||||||
socket_override,
|
|
||||||
},
|
|
||||||
..
|
|
||||||
} => {
|
} => {
|
||||||
assert_eq!(pod_name, "memory");
|
assert!(!all);
|
||||||
assert_eq!(socket_override, None);
|
assert_eq!(workspace_root, PathBuf::from("/tmp/resume-workspace"));
|
||||||
}
|
}
|
||||||
_ => panic!("expected PodName mode"),
|
_ => panic!("expected Resume mode"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_resume_all_scope() {
|
||||||
|
match parse_args_from(["resume", "--all"]).unwrap() {
|
||||||
|
Mode::Tui {
|
||||||
|
mode: LaunchMode::Resume { all },
|
||||||
|
..
|
||||||
|
} => assert!(all),
|
||||||
|
_ => panic!("expected Resume mode"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1038,14 +1089,9 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn memory_lint_with_other_second_word_remains_positional_pod_name() {
|
fn memory_lint_with_other_second_word_is_usage_error() {
|
||||||
match parse_args_from(["memory", "other"]).unwrap() {
|
let err = parse_args_from(["memory", "other"]).unwrap_err();
|
||||||
Mode::Tui {
|
assert_eq!(err.to_string(), "yoi memory requires the `lint` subcommand");
|
||||||
mode: LaunchMode::PodName { pod_name, .. },
|
|
||||||
..
|
|
||||||
} => assert_eq!(pod_name, "memory"),
|
|
||||||
_ => panic!("expected PodName mode"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -1075,19 +1121,13 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_rejects_resume_and_pod_name_selection() {
|
fn parse_rejects_legacy_resume_flags() {
|
||||||
let cases = [
|
let cases = [
|
||||||
(
|
(vec!["-r".to_string()], "unknown argument: -r"),
|
||||||
vec!["-r".to_string(), "--pod".to_string(), "agent".to_string()],
|
(vec!["--resume".to_string()], "unknown argument: --resume"),
|
||||||
"--pod and --resume are mutually exclusive",
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
vec!["--pod".to_string(), "agent".to_string(), "-r".to_string()],
|
vec!["--pod".to_string(), "agent".to_string(), "-r".to_string()],
|
||||||
"--pod and --resume are mutually exclusive",
|
"unknown argument: -r",
|
||||||
),
|
|
||||||
(
|
|
||||||
vec!["-r".to_string(), "agent".to_string()],
|
|
||||||
"--resume cannot be used with a positional Pod name",
|
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -1097,6 +1137,15 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_resume_rejects_workspace_with_all() {
|
||||||
|
let err = parse_args_from(["resume", "--workspace", "/tmp/ws", "--all"]).unwrap_err();
|
||||||
|
assert_eq!(
|
||||||
|
err.to_string(),
|
||||||
|
"yoi resume --all and --workspace are mutually exclusive"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_profile_spawn_mode() {
|
fn parse_profile_spawn_mode() {
|
||||||
match parse_args_from([
|
match parse_args_from([
|
||||||
|
|
@ -1125,14 +1174,6 @@ mod tests {
|
||||||
fn parse_profile_rejects_resume_attach_modes() {
|
fn parse_profile_rejects_resume_attach_modes() {
|
||||||
let segment_id = session_store::new_segment_id().to_string();
|
let segment_id = session_store::new_segment_id().to_string();
|
||||||
let cases = [
|
let cases = [
|
||||||
(
|
|
||||||
vec![
|
|
||||||
"--profile".to_string(),
|
|
||||||
"p.lua".to_string(),
|
|
||||||
"--resume".to_string(),
|
|
||||||
],
|
|
||||||
"--profile can only be used for fresh spawn",
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
vec![
|
vec![
|
||||||
"--profile".to_string(),
|
"--profile".to_string(),
|
||||||
|
|
@ -1151,14 +1192,6 @@ mod tests {
|
||||||
],
|
],
|
||||||
"--profile can only be used for fresh spawn",
|
"--profile can only be used for fresh spawn",
|
||||||
),
|
),
|
||||||
(
|
|
||||||
vec![
|
|
||||||
"--profile".to_string(),
|
|
||||||
"p.lua".to_string(),
|
|
||||||
"agent".to_string(),
|
|
||||||
],
|
|
||||||
"--profile can only be used for fresh spawn",
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for (args, message) in cases {
|
for (args, message) in cases {
|
||||||
|
|
@ -1179,15 +1212,9 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_dashboard_word_remains_a_pod_console_name_not_an_alias() {
|
fn parse_dashboard_word_is_not_an_alias_or_pod_name() {
|
||||||
let config = parse_args_from(["dashboard"]).unwrap();
|
let err = parse_args_from(["dashboard"]).unwrap_err();
|
||||||
match config {
|
assert_eq!(err.to_string(), "unknown command `dashboard`");
|
||||||
Mode::Tui {
|
|
||||||
mode: LaunchMode::PodName { pod_name, .. },
|
|
||||||
..
|
|
||||||
} => assert_eq!(pod_name, "dashboard"),
|
|
||||||
other => panic!("expected PodName TUI mode, got {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -1204,6 +1231,14 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_resume_help() {
|
||||||
|
match parse_args_from(["resume", "--help"]).unwrap() {
|
||||||
|
Mode::ResumeHelp => {}
|
||||||
|
_ => panic!("expected ResumeHelp mode"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_memory_lint_help() {
|
fn parse_memory_lint_help() {
|
||||||
match parse_args_from(["memory", "lint", "--help"]).unwrap() {
|
match parse_args_from(["memory", "lint", "--help"]).unwrap() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user