merge: insomnia cli ownership
This commit is contained in:
commit
6b49be085d
16
Cargo.lock
generated
16
Cargo.lock
generated
|
|
@ -332,7 +332,6 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
|||
name = "client"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"insomnia",
|
||||
"manifest",
|
||||
"protocol",
|
||||
"serde_json",
|
||||
|
|
@ -1483,6 +1482,17 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "insomnia"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"client",
|
||||
"memory",
|
||||
"pod",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"session-store",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instability"
|
||||
|
|
@ -2334,11 +2344,11 @@ dependencies = [
|
|||
"async-trait",
|
||||
"chrono",
|
||||
"clap",
|
||||
"client",
|
||||
"dotenv",
|
||||
"fs4",
|
||||
"futures",
|
||||
"include_dir",
|
||||
"insomnia",
|
||||
"libc",
|
||||
"llm-worker",
|
||||
"manifest",
|
||||
|
|
@ -3917,8 +3927,6 @@ dependencies = [
|
|||
"crossterm 0.28.1",
|
||||
"llm-worker",
|
||||
"manifest",
|
||||
"memory",
|
||||
"pod",
|
||||
"pod-registry",
|
||||
"pod-store",
|
||||
"protocol",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ provider = { path = "crates/provider" }
|
|||
session-metrics = { path = "crates/session-metrics" }
|
||||
session-store = { path = "crates/session-store" }
|
||||
tools = { path = "crates/tools" }
|
||||
tui = { path = "crates/tui" }
|
||||
|
||||
# External
|
||||
# Note: `reqwest` and `chrono` are not aggregated here because some crates
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ license.workspace = true
|
|||
[dependencies]
|
||||
protocol = { workspace = true }
|
||||
manifest = { workspace = true }
|
||||
insomnia = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt", "macros", "net", "io-util", "sync", "time", "process", "fs"] }
|
||||
uuid = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@
|
|||
//! TUI / GUI / E2E ハーネスはこの crate に依存して protocol を喋る。
|
||||
|
||||
mod pod_client;
|
||||
pub mod runtime_command;
|
||||
pub mod spawn;
|
||||
|
||||
pub use runtime_command::PodRuntimeCommand;
|
||||
|
||||
pub use pod_client::PodClient;
|
||||
pub use spawn::{SpawnConfig, SpawnError, SpawnReady, spawn_pod};
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use std::path::{Path, PathBuf};
|
|||
use std::process::Stdio;
|
||||
use std::time::Duration;
|
||||
|
||||
use insomnia::PodRuntimeCommand;
|
||||
use crate::PodRuntimeCommand;
|
||||
use tokio::process::Command;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
|
@ -24,6 +24,7 @@ const READY_TIMEOUT: Duration = Duration::from_secs(20);
|
|||
|
||||
/// `spawn_pod` の入力。
|
||||
pub struct SpawnConfig {
|
||||
pub runtime_command: PodRuntimeCommand,
|
||||
/// `pod.name` として使う識別子。runtime ディレクトリ
|
||||
/// (`manifest::paths::pod_runtime_dir`) の解決と、ready 行に乗る
|
||||
/// 名前との突き合わせに使う。
|
||||
|
|
@ -100,17 +101,15 @@ pub async fn spawn_pod<F>(config: SpawnConfig, mut progress: F) -> Result<SpawnR
|
|||
where
|
||||
F: FnMut(&str),
|
||||
{
|
||||
let runtime_command = PodRuntimeCommand::resolve().map_err(SpawnError::Io)?;
|
||||
|
||||
let pod_runtime_dir = manifest::paths::pod_runtime_dir(&config.pod_name)
|
||||
.ok_or(SpawnError::RuntimeDirUnavailable)?;
|
||||
std::fs::create_dir_all(&pod_runtime_dir).map_err(SpawnError::Io)?;
|
||||
let stderr_path = pod_runtime_dir.join("stderr.log");
|
||||
let stderr_file = std::fs::File::create(&stderr_path).map_err(SpawnError::Io)?;
|
||||
|
||||
let mut command = Command::new(runtime_command.program());
|
||||
let mut command = Command::new(config.runtime_command.program());
|
||||
command
|
||||
.args(runtime_command.prefix_args())
|
||||
.args(config.runtime_command.prefix_args())
|
||||
.current_dir(&config.cwd)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
|
|
|
|||
|
|
@ -5,3 +5,14 @@ edition.workspace = true
|
|||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
client = { workspace = true }
|
||||
memory = { workspace = true }
|
||||
pod = { workspace = true }
|
||||
session-store = { workspace = true }
|
||||
tui = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
|
|
|||
594
crates/insomnia/src/main.rs
Normal file
594
crates/insomnia/src/main.rs
Normal file
|
|
@ -0,0 +1,594 @@
|
|||
mod memory_lint;
|
||||
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
use std::process::ExitCode;
|
||||
|
||||
use client::PodRuntimeCommand;
|
||||
use memory_lint::{LintCliOptions, LintStatus};
|
||||
use session_store::SegmentId;
|
||||
use tui::{LaunchMode, LaunchOptions};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Mode {
|
||||
Help,
|
||||
MemoryLintHelp,
|
||||
MemoryLint(LintCliOptions),
|
||||
PodRuntime(Vec<String>),
|
||||
Tui(LaunchMode),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ParseError(String);
|
||||
|
||||
impl fmt::Display for ParseError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ParseError {}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> ExitCode {
|
||||
let mode = match parse_args() {
|
||||
Ok(mode) => mode,
|
||||
Err(e) => {
|
||||
eprintln!("insomnia: {e}");
|
||||
eprintln!("try `insomnia --help` for usage.");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
match mode {
|
||||
Mode::Help => {
|
||||
print_help();
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Mode::MemoryLintHelp => {
|
||||
print_memory_lint_help();
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Mode::MemoryLint(options) => match memory_lint::run(&options) {
|
||||
Ok(LintStatus::Clean) => ExitCode::SUCCESS,
|
||||
Ok(LintStatus::Failed) => ExitCode::FAILURE,
|
||||
Err(e) => {
|
||||
eprintln!("insomnia memory lint: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
},
|
||||
Mode::PodRuntime(args) => pod::entrypoint::run_cli_from("insomnia pod", args).await,
|
||||
Mode::Tui(mode) => {
|
||||
let runtime_command = match PodRuntimeCommand::resolve() {
|
||||
Ok(command) => command,
|
||||
Err(e) => {
|
||||
eprintln!("insomnia: failed to resolve Pod runtime command: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
tui::launch(LaunchOptions {
|
||||
mode,
|
||||
runtime_command,
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_args() -> Result<Mode, ParseError> {
|
||||
parse_args_from(std::env::args().skip(1))
|
||||
}
|
||||
|
||||
fn parse_args_from<I, S>(args: I) -> Result<Mode, ParseError>
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: Into<String>,
|
||||
{
|
||||
let args = args.into_iter().map(Into::into).collect::<Vec<_>>();
|
||||
parse_args_slice(&args)
|
||||
}
|
||||
|
||||
fn parse_args_slice(args: &[String]) -> Result<Mode, ParseError> {
|
||||
if args.is_empty() {
|
||||
return Ok(Mode::Tui(LaunchMode::Spawn { profile: None }));
|
||||
}
|
||||
|
||||
match args[0].as_str() {
|
||||
"--help" | "-h" => return Ok(Mode::Help),
|
||||
"pod" => return Ok(Mode::PodRuntime(args[1..].to_vec())),
|
||||
"memory" if args.get(1).map(String::as_str) == Some("lint") => {
|
||||
let lint_args = &args[2..];
|
||||
if lint_args.iter().any(|arg| arg == "--help" || arg == "-h") {
|
||||
return Ok(Mode::MemoryLintHelp);
|
||||
}
|
||||
let options =
|
||||
memory_lint::parse_lint_args(lint_args).map_err(|e| ParseError(e.to_string()))?;
|
||||
return Ok(Mode::MemoryLint(options));
|
||||
}
|
||||
"memory" => {
|
||||
return Ok(Mode::Tui(LaunchMode::PodName {
|
||||
pod_name: "memory".to_string(),
|
||||
socket_override: None,
|
||||
}));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut resume = false;
|
||||
let mut session = None;
|
||||
let mut pod_name = None;
|
||||
let mut socket_override = None;
|
||||
let mut profile = None;
|
||||
let mut multi = false;
|
||||
let mut positional = None;
|
||||
|
||||
let mut i = 0;
|
||||
while i < args.len() {
|
||||
let arg = &args[i];
|
||||
match arg.as_str() {
|
||||
"--resume" | "-r" => {
|
||||
resume = true;
|
||||
i += 1;
|
||||
}
|
||||
"--multi" => {
|
||||
multi = true;
|
||||
i += 1;
|
||||
}
|
||||
"--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;
|
||||
}
|
||||
"--pod" => {
|
||||
let value = args
|
||||
.get(i + 1)
|
||||
.ok_or_else(|| ParseError("--pod requires a value".to_string()))?;
|
||||
if value.starts_with('-') {
|
||||
return Err(ParseError("--pod requires a value".to_string()));
|
||||
}
|
||||
pod_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;
|
||||
}
|
||||
"--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("--pod=") => {
|
||||
let value = arg.trim_start_matches("--pod=");
|
||||
if value.is_empty() {
|
||||
return Err(ParseError("--pod requires a value".to_string()));
|
||||
}
|
||||
pod_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("--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 => {
|
||||
if positional.replace(value.to_string()).is_some() {
|
||||
return Err(ParseError(
|
||||
"only one positional Pod name is supported".to_string(),
|
||||
));
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if pod_name.is_some() && positional.is_some() {
|
||||
return Err(ParseError(
|
||||
"--pod and a positional Pod name are mutually exclusive".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if profile.is_some()
|
||||
&& (resume
|
||||
|| session.is_some()
|
||||
|| pod_name.is_some()
|
||||
|| positional.is_some()
|
||||
|| socket_override.is_some()
|
||||
|| multi)
|
||||
{
|
||||
return Err(ParseError(
|
||||
"--profile can only be used for fresh spawn".to_string(),
|
||||
));
|
||||
}
|
||||
if multi && resume {
|
||||
return Err(ParseError(
|
||||
"--multi and --resume are mutually exclusive".to_string(),
|
||||
));
|
||||
}
|
||||
if multi && session.is_some() {
|
||||
return Err(ParseError(
|
||||
"--multi and --session are mutually exclusive".to_string(),
|
||||
));
|
||||
}
|
||||
if multi && pod_name.is_some() {
|
||||
return Err(ParseError(
|
||||
"--multi and --pod are mutually exclusive".to_string(),
|
||||
));
|
||||
}
|
||||
if multi && positional.is_some() {
|
||||
return Err(ParseError(
|
||||
"--multi cannot be used with a positional Pod name".to_string(),
|
||||
));
|
||||
}
|
||||
if multi && socket_override.is_some() {
|
||||
return Err(ParseError(
|
||||
"--multi and --socket are mutually exclusive".to_string(),
|
||||
));
|
||||
}
|
||||
if pod_name.is_some() && session.is_some() {
|
||||
return Err(ParseError(
|
||||
"--pod and --session are mutually exclusive".to_string(),
|
||||
));
|
||||
}
|
||||
if pod_name.is_some() && resume {
|
||||
return Err(ParseError(
|
||||
"--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(),
|
||||
));
|
||||
}
|
||||
|
||||
if multi {
|
||||
return Ok(Mode::Tui(LaunchMode::Multi));
|
||||
}
|
||||
let pod_name = pod_name.or(positional);
|
||||
if let Some(pod_name) = pod_name {
|
||||
return Ok(Mode::Tui(LaunchMode::PodName {
|
||||
pod_name,
|
||||
socket_override,
|
||||
}));
|
||||
}
|
||||
if resume {
|
||||
return Ok(Mode::Tui(LaunchMode::Resume));
|
||||
}
|
||||
if let Some(id) = session {
|
||||
return Ok(Mode::Tui(LaunchMode::ResumeWithSession(id)));
|
||||
}
|
||||
|
||||
Ok(Mode::Tui(LaunchMode::Spawn { profile }))
|
||||
}
|
||||
|
||||
fn parse_session_id(value: &str) -> Result<SegmentId, ParseError> {
|
||||
value
|
||||
.parse()
|
||||
.map_err(|_| ParseError(format!("invalid --session UUID: {value}")))
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
println!(
|
||||
"insomnia\n\nUsage:\n insomnia [OPTIONS] [POD_NAME]\n insomnia pod [POD_OPTIONS]\n insomnia memory lint [OPTIONS]\n\nOptions:\n -r, --resume Open the Pod picker and resume/attach a Pod\n --multi Open the multi-Pod dashboard\n --pod <NAME> Attach/restore/create a Pod by name\n --socket <PATH> Attach to a specific Pod socket with --pod\n --session <UUID> Resume a specific session segment\n --profile <REF> Start a fresh Pod from a profile\n -h, --help Print help\n"
|
||||
);
|
||||
}
|
||||
|
||||
fn print_memory_lint_help() {
|
||||
println!(
|
||||
"insomnia memory lint\n\nUsage:\n insomnia memory lint [OPTIONS]\n\nOptions:\n --workspace <PATH> Workspace root to lint (defaults to cwd)\n --json Emit a JSON report\n --warnings-as-errors Return failure when warnings are present\n -h, --help Print help\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_pod_name_mode() {
|
||||
match parse_args_from(["--pod", "agent", "--socket", "/tmp/agent.sock"]).unwrap() {
|
||||
Mode::Tui(LaunchMode::PodName {
|
||||
pod_name,
|
||||
socket_override,
|
||||
}) => {
|
||||
assert_eq!(pod_name, "agent");
|
||||
assert_eq!(socket_override, Some(PathBuf::from("/tmp/agent.sock")));
|
||||
}
|
||||
_ => panic!("expected PodName mode"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_positional_name_uses_pod_name_mode() {
|
||||
match parse_args_from(["agent"]).unwrap() {
|
||||
Mode::Tui(LaunchMode::PodName {
|
||||
pod_name,
|
||||
socket_override,
|
||||
}) => {
|
||||
assert_eq!(pod_name, "agent");
|
||||
assert_eq!(socket_override, None);
|
||||
}
|
||||
_ => panic!("expected PodName mode"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_memory_alone_remains_positional_pod_name() {
|
||||
match parse_args_from(["memory"]).unwrap() {
|
||||
Mode::Tui(LaunchMode::PodName {
|
||||
pod_name,
|
||||
socket_override,
|
||||
}) => {
|
||||
assert_eq!(pod_name, "memory");
|
||||
assert_eq!(socket_override, None);
|
||||
}
|
||||
_ => panic!("expected PodName mode"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_pod_subcommand_uses_runtime_mode() {
|
||||
match parse_args_from(["pod", "--pod", "agent", "--profile", "default"]).unwrap() {
|
||||
Mode::PodRuntime(args) => assert_eq!(args, ["--pod", "agent", "--profile", "default"]),
|
||||
_ => panic!("expected PodRuntime mode"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_literal_pod_name_still_available_with_flag() {
|
||||
match parse_args_from(["--pod", "pod"]).unwrap() {
|
||||
Mode::Tui(LaunchMode::PodName {
|
||||
pod_name,
|
||||
socket_override,
|
||||
}) => {
|
||||
assert_eq!(pod_name, "pod");
|
||||
assert_eq!(socket_override, None);
|
||||
}
|
||||
_ => panic!("expected PodName mode"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_memory_lint_mode() {
|
||||
match parse_args_from([
|
||||
"memory",
|
||||
"lint",
|
||||
"--workspace",
|
||||
"/tmp/ws",
|
||||
"--json",
|
||||
"--warnings-as-errors",
|
||||
])
|
||||
.unwrap()
|
||||
{
|
||||
Mode::MemoryLint(options) => {
|
||||
assert_eq!(options.workspace, Some(PathBuf::from("/tmp/ws")));
|
||||
assert!(options.json);
|
||||
assert!(options.warnings_as_errors);
|
||||
}
|
||||
_ => panic!("expected MemoryLint mode"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_memory_lint_rejects_usage_errors() {
|
||||
let err = parse_args_from(["memory", "lint", "--workspace"]).unwrap_err();
|
||||
assert_eq!(err.to_string(), "--workspace requires a value");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_memory_lint_workspace_equals() {
|
||||
match parse_args_from(["memory", "lint", "--workspace=/tmp/ws"]).unwrap() {
|
||||
Mode::MemoryLint(options) => {
|
||||
assert_eq!(options.workspace, Some(PathBuf::from("/tmp/ws")));
|
||||
assert!(!options.json);
|
||||
assert!(!options.warnings_as_errors);
|
||||
}
|
||||
_ => panic!("expected MemoryLint mode"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memory_lint_with_other_second_word_remains_positional_pod_name() {
|
||||
match parse_args_from(["memory", "other"]).unwrap() {
|
||||
Mode::Tui(LaunchMode::PodName { pod_name, .. }) => assert_eq!(pod_name, "memory"),
|
||||
_ => panic!("expected PodName mode"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rejects_pod_and_session() {
|
||||
let segment_id = session_store::new_segment_id().to_string();
|
||||
let err = parse_args_from(["--pod", "agent", "--session", &segment_id]).unwrap_err();
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"--pod and --session are mutually exclusive"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rejects_resume_and_pod_name_selection() {
|
||||
let cases = [
|
||||
(
|
||||
vec!["-r".to_string(), "--pod".to_string(), "agent".to_string()],
|
||||
"--pod and --resume are mutually exclusive",
|
||||
),
|
||||
(
|
||||
vec!["--pod".to_string(), "agent".to_string(), "-r".to_string()],
|
||||
"--pod and --resume are mutually exclusive",
|
||||
),
|
||||
(
|
||||
vec!["-r".to_string(), "agent".to_string()],
|
||||
"--resume cannot be used with a positional Pod name",
|
||||
),
|
||||
];
|
||||
|
||||
for (args, message) in cases {
|
||||
let err = parse_args_from(args).unwrap_err();
|
||||
assert_eq!(err.to_string(), message);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_profile_spawn_mode() {
|
||||
match parse_args_from(["--profile", "/profiles/coder.lua"]).unwrap() {
|
||||
Mode::Tui(LaunchMode::Spawn { profile }) => {
|
||||
assert_eq!(profile, Some("/profiles/coder.lua".to_string()));
|
||||
}
|
||||
_ => panic!("expected Spawn mode"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_profile_rejects_resume_attach_modes() {
|
||||
let segment_id = session_store::new_segment_id().to_string();
|
||||
let cases = [
|
||||
(
|
||||
vec![
|
||||
"--profile".to_string(),
|
||||
"p.lua".to_string(),
|
||||
"--resume".to_string(),
|
||||
],
|
||||
"--profile can only be used for fresh spawn",
|
||||
),
|
||||
(
|
||||
vec![
|
||||
"--profile".to_string(),
|
||||
"p.lua".to_string(),
|
||||
"--session".to_string(),
|
||||
segment_id,
|
||||
],
|
||||
"--profile can only be used for fresh spawn",
|
||||
),
|
||||
(
|
||||
vec![
|
||||
"--profile".to_string(),
|
||||
"p.lua".to_string(),
|
||||
"--socket".to_string(),
|
||||
"/tmp/insomnia/sock".to_string(),
|
||||
],
|
||||
"--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 {
|
||||
let err = parse_args_from(args).unwrap_err();
|
||||
assert_eq!(err.to_string(), message);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_multi_mode() {
|
||||
match parse_args_from(["--multi"]).unwrap() {
|
||||
Mode::Tui(LaunchMode::Multi) => {}
|
||||
_ => panic!("expected Multi mode"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_top_level_help() {
|
||||
match parse_args_from(["--help"]).unwrap() {
|
||||
Mode::Help => {}
|
||||
_ => panic!("expected Help mode"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_memory_lint_help() {
|
||||
match parse_args_from(["memory", "lint", "--help"]).unwrap() {
|
||||
Mode::MemoryLintHelp => {}
|
||||
_ => panic!("expected MemoryLintHelp mode"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_multi_conflicts_are_clear() {
|
||||
let segment_id = session_store::new_segment_id().to_string();
|
||||
let cases = [
|
||||
(
|
||||
vec!["--multi".to_string(), "--resume".to_string()],
|
||||
"--multi and --resume are mutually exclusive",
|
||||
),
|
||||
(
|
||||
vec!["--multi".to_string(), "--session".to_string(), segment_id],
|
||||
"--multi and --session are mutually exclusive",
|
||||
),
|
||||
(
|
||||
vec![
|
||||
"--multi".to_string(),
|
||||
"--pod".to_string(),
|
||||
"agent".to_string(),
|
||||
],
|
||||
"--multi and --pod are mutually exclusive",
|
||||
),
|
||||
(
|
||||
vec!["--multi".to_string(), "agent".to_string()],
|
||||
"--multi cannot be used with a positional Pod name",
|
||||
),
|
||||
(
|
||||
vec![
|
||||
"--multi".to_string(),
|
||||
"--socket".to_string(),
|
||||
"/tmp/a.sock".to_string(),
|
||||
],
|
||||
"--multi and --socket are mutually exclusive",
|
||||
),
|
||||
];
|
||||
|
||||
for (args, message) in cases {
|
||||
let err = parse_args_from(args).unwrap_err();
|
||||
assert_eq!(err.to_string(), message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ pod-store = { workspace = true }
|
|||
manifest = { workspace = true }
|
||||
protocol = { workspace = true }
|
||||
provider = { workspace = true }
|
||||
insomnia = { workspace = true }
|
||||
client = { workspace = true }
|
||||
pod-registry = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use std::sync::Arc;
|
|||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use insomnia::PodRuntimeCommand;
|
||||
use client::PodRuntimeCommand;
|
||||
use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
|
||||
use manifest::{Permission, ScopeRule};
|
||||
use pod_store::{PodActiveSegmentRef, PodMetadata, PodMetadataStore};
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use std::sync::Arc;
|
|||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use insomnia::PodRuntimeCommand;
|
||||
use client::PodRuntimeCommand;
|
||||
use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
|
||||
use manifest::{
|
||||
CompactionConfigPartial, FileUploadLimitsPartial, Permission, PermissionConfigPartial,
|
||||
|
|
@ -1300,7 +1300,7 @@ return profile {
|
|||
assert!(invalid.contains("Use `default`, `inherit`"));
|
||||
assert!(invalid.contains("`project:coder`"));
|
||||
|
||||
let no_default = build_spawn_config_json_for_profile(
|
||||
let default_config = build_spawn_config_json_for_profile(
|
||||
&parent,
|
||||
&available,
|
||||
&project,
|
||||
|
|
@ -1309,10 +1309,8 @@ return profile {
|
|||
&scope,
|
||||
SpawnProfileSelector::Default,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(no_default.contains("no default profile"), "{no_default}");
|
||||
assert!(no_default.contains("Use `default`, `inherit`"));
|
||||
assert!(no_default.contains("`project:coder`"));
|
||||
.unwrap();
|
||||
assert!(default_config.contains("\"name\":\"child\""));
|
||||
|
||||
let user_config = tmp.path().join("user-profiles.toml");
|
||||
std::fs::write(&user_config, "[profile]\ncoder = \"user-coder.lua\"\n").unwrap();
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
|
||||
use insomnia::PodRuntimeCommand;
|
||||
use client::PodRuntimeCommand;
|
||||
use llm_worker::tool::{ToolError, ToolOutput};
|
||||
use manifest::{
|
||||
AuthRef, ModelManifest, Permission, PodManifest, PodManifestConfig, PodMetaConfig, SchemeKind,
|
||||
|
|
|
|||
|
|
@ -4,10 +4,6 @@ version = "0.1.0"
|
|||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "insomnia"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
client = { workspace = true }
|
||||
protocol = { workspace = true }
|
||||
|
|
@ -19,10 +15,8 @@ unicode-width = "0.2.2"
|
|||
uuid = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
manifest = { workspace = true }
|
||||
memory = { workspace = true }
|
||||
session-store = { workspace = true }
|
||||
pod-store = { workspace = true }
|
||||
pod = { workspace = true }
|
||||
pod-registry = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
pulldown-cmark = { version = "0.13.3", default-features = false }
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ mod cache;
|
|||
mod command;
|
||||
mod input;
|
||||
mod markdown;
|
||||
mod memory_lint;
|
||||
mod multi_pod;
|
||||
mod picker;
|
||||
mod pod_list;
|
||||
|
|
@ -39,7 +38,7 @@ use ratatui::backend::CrosstermBackend;
|
|||
use session_store::SegmentId;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use client::PodClient;
|
||||
use client::{PodClient, PodRuntimeCommand};
|
||||
|
||||
use crate::app::{ActionbarNoticeLevel, ActionbarNoticeSource, App};
|
||||
use crate::picker::PickerOutcome;
|
||||
|
|
@ -59,8 +58,14 @@ fn resolve_socket(pod_name: &str, override_path: Option<PathBuf>) -> PathBuf {
|
|||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Mode {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LaunchOptions {
|
||||
pub mode: LaunchMode,
|
||||
pub runtime_command: PodRuntimeCommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum LaunchMode {
|
||||
Spawn {
|
||||
profile: Option<String>,
|
||||
},
|
||||
|
|
@ -81,218 +86,13 @@ enum Mode {
|
|||
/// separate from `-r`/`--resume`, which keeps its single-Pod picker
|
||||
/// meaning.
|
||||
Multi,
|
||||
/// `insomnia memory lint`: headless lint for workspace memory and knowledge files.
|
||||
MemoryLint(memory_lint::LintCliOptions),
|
||||
/// `insomnia pod ...`: run the Pod runtime parser/entrypoint without TUI side effects.
|
||||
PodRuntime(Vec<String>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ParseError {
|
||||
Conflict(&'static str),
|
||||
InvalidSession(String),
|
||||
MemoryLint(memory_lint::UsageError),
|
||||
MissingValue(&'static str),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ParseError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Conflict(message) => write!(f, "{message}"),
|
||||
Self::InvalidSession(s) => write!(f, "invalid --session UUID: {s}"),
|
||||
Self::MemoryLint(err) => write!(f, "{err}"),
|
||||
Self::MissingValue(flag) => write!(f, "{flag} requires a value"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_args() -> Result<Mode, ParseError> {
|
||||
parse_args_from(std::env::args().skip(1))
|
||||
}
|
||||
|
||||
fn parse_args_from<I, S>(args: I) -> Result<Mode, ParseError>
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: Into<String>,
|
||||
{
|
||||
let args: Vec<String> = args.into_iter().map(Into::into).collect();
|
||||
if args.first().map(String::as_str) == Some("memory")
|
||||
&& args.get(1).map(String::as_str) == Some("lint")
|
||||
{
|
||||
let options = memory_lint::parse_lint_args(&args[2..]).map_err(ParseError::MemoryLint)?;
|
||||
return Ok(Mode::MemoryLint(options));
|
||||
}
|
||||
if args.first().map(String::as_str) == Some("pod") {
|
||||
return Ok(Mode::PodRuntime(args[1..].to_vec()));
|
||||
}
|
||||
|
||||
let mut resume = false;
|
||||
let mut multi = false;
|
||||
let mut session: Option<SegmentId> = None;
|
||||
let mut pod: Option<String> = None;
|
||||
let mut profile: Option<String> = None;
|
||||
let mut socket_override: Option<PathBuf> = None;
|
||||
let mut socket_seen = false;
|
||||
let mut positional: Option<String> = None;
|
||||
|
||||
let mut i = 0;
|
||||
while i < args.len() {
|
||||
match args[i].as_str() {
|
||||
"-r" | "--resume" => {
|
||||
resume = true;
|
||||
i += 1;
|
||||
}
|
||||
"--multi" => {
|
||||
multi = true;
|
||||
i += 1;
|
||||
}
|
||||
"--session" => {
|
||||
let raw = args
|
||||
.get(i + 1)
|
||||
.ok_or(ParseError::MissingValue("--session"))?;
|
||||
session = Some(
|
||||
raw.parse::<SegmentId>()
|
||||
.map_err(|_| ParseError::InvalidSession(raw.clone()))?,
|
||||
);
|
||||
i += 2;
|
||||
}
|
||||
"--pod" => {
|
||||
let raw = args.get(i + 1).ok_or(ParseError::MissingValue("--pod"))?;
|
||||
pod = Some(raw.clone());
|
||||
i += 2;
|
||||
}
|
||||
"--profile" => {
|
||||
let raw = args
|
||||
.get(i + 1)
|
||||
.ok_or(ParseError::MissingValue("--profile"))?;
|
||||
profile = Some(raw.clone());
|
||||
i += 2;
|
||||
}
|
||||
"--socket" => {
|
||||
socket_seen = true;
|
||||
let raw = args
|
||||
.get(i + 1)
|
||||
.ok_or(ParseError::MissingValue("--socket"))?;
|
||||
socket_override = Some(PathBuf::from(raw));
|
||||
i += 2;
|
||||
}
|
||||
other if positional.is_none() && !other.starts_with('-') => {
|
||||
positional = Some(other.to_string());
|
||||
i += 1;
|
||||
}
|
||||
_ => {
|
||||
// Unknown flag or extra positional — keep older
|
||||
// behaviour of ignoring unknowns rather than aborting.
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if multi {
|
||||
if resume {
|
||||
return Err(ParseError::Conflict(
|
||||
"--multi and --resume are mutually exclusive",
|
||||
));
|
||||
}
|
||||
if session.is_some() {
|
||||
return Err(ParseError::Conflict(
|
||||
"--multi and --session are mutually exclusive",
|
||||
));
|
||||
}
|
||||
if pod.is_some() {
|
||||
return Err(ParseError::Conflict(
|
||||
"--multi and --pod are mutually exclusive",
|
||||
));
|
||||
}
|
||||
if positional.is_some() {
|
||||
return Err(ParseError::Conflict(
|
||||
"--multi cannot be used with a positional Pod name",
|
||||
));
|
||||
}
|
||||
if socket_seen {
|
||||
return Err(ParseError::Conflict(
|
||||
"--multi and --socket are mutually exclusive",
|
||||
));
|
||||
}
|
||||
if profile.is_some() {
|
||||
return Err(ParseError::Conflict(
|
||||
"--multi and --profile are mutually exclusive",
|
||||
));
|
||||
}
|
||||
return Ok(Mode::Multi);
|
||||
}
|
||||
|
||||
if resume && session.is_some() {
|
||||
return Err(ParseError::Conflict(
|
||||
"--resume and --session are mutually exclusive",
|
||||
));
|
||||
}
|
||||
if pod.is_some() && session.is_some() {
|
||||
return Err(ParseError::Conflict(
|
||||
"--pod and --session are mutually exclusive",
|
||||
));
|
||||
}
|
||||
if pod.is_some() && resume {
|
||||
return Err(ParseError::Conflict(
|
||||
"--pod and --resume are mutually exclusive",
|
||||
));
|
||||
}
|
||||
if profile.is_some()
|
||||
&& (resume || session.is_some() || pod.is_some() || positional.is_some() || socket_seen)
|
||||
{
|
||||
return Err(ParseError::Conflict(
|
||||
"--profile can only be used for fresh spawn",
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(pod_name) = pod {
|
||||
return Ok(Mode::PodName {
|
||||
pod_name,
|
||||
socket_override,
|
||||
});
|
||||
}
|
||||
if let Some(id) = session {
|
||||
return Ok(Mode::ResumeWithSession(id));
|
||||
}
|
||||
if resume {
|
||||
return Ok(Mode::Resume);
|
||||
}
|
||||
if let Some(pod_name) = positional {
|
||||
return Ok(Mode::PodName {
|
||||
pod_name,
|
||||
socket_override,
|
||||
});
|
||||
}
|
||||
Ok(Mode::Spawn { profile })
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> ExitCode {
|
||||
let mode = match parse_args() {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
eprintln!("insomnia: {e}");
|
||||
return match e {
|
||||
ParseError::MemoryLint(_) => ExitCode::from(2),
|
||||
_ => ExitCode::FAILURE,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
if let Mode::MemoryLint(ref options) = mode {
|
||||
return match memory_lint::run(options) {
|
||||
Ok(memory_lint::LintStatus::Clean) => ExitCode::SUCCESS,
|
||||
Ok(memory_lint::LintStatus::Failed) => ExitCode::FAILURE,
|
||||
Err(err) => {
|
||||
eprintln!("insomnia: {err}");
|
||||
ExitCode::from(2)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if let Mode::PodRuntime(args) = mode {
|
||||
return pod::entrypoint::run_cli_from("insomnia pod", args).await;
|
||||
}
|
||||
pub async fn launch(options: LaunchOptions) -> ExitCode {
|
||||
let LaunchOptions {
|
||||
mode,
|
||||
runtime_command,
|
||||
} = options;
|
||||
|
||||
if let Err(e) = enable_raw_mode() {
|
||||
eprintln!("insomnia: failed to enter raw mode: {e}");
|
||||
|
|
@ -305,16 +105,14 @@ async fn main() -> ExitCode {
|
|||
}
|
||||
|
||||
let result = match mode {
|
||||
Mode::Spawn { profile } => run_spawn(None, profile).await,
|
||||
Mode::PodName {
|
||||
LaunchMode::Spawn { profile } => run_spawn(None, profile, runtime_command).await,
|
||||
LaunchMode::PodName {
|
||||
pod_name,
|
||||
socket_override,
|
||||
} => run_pod_name(pod_name, socket_override).await,
|
||||
Mode::Resume => run_resume().await,
|
||||
Mode::ResumeWithSession(id) => run_spawn(Some(id), None).await,
|
||||
Mode::Multi => run_multi().await,
|
||||
Mode::MemoryLint(_) => unreachable!("memory lint returns before terminal setup"),
|
||||
Mode::PodRuntime(_) => unreachable!("pod runtime returns before terminal setup"),
|
||||
} => run_pod_name(pod_name, socket_override, runtime_command).await,
|
||||
LaunchMode::Resume => run_resume(runtime_command).await,
|
||||
LaunchMode::ResumeWithSession(id) => run_spawn(Some(id), None, runtime_command).await,
|
||||
LaunchMode::Multi => run_multi(runtime_command).await,
|
||||
};
|
||||
|
||||
// Always restore the terminal first so any pending eprintln below
|
||||
|
|
@ -349,6 +147,7 @@ async fn main() -> ExitCode {
|
|||
async fn run_pod_name(
|
||||
pod_name: String,
|
||||
socket_override: Option<PathBuf>,
|
||||
runtime_command: PodRuntimeCommand,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if let Some(client) = try_connect_live_pod(&pod_name, socket_override.clone()).await {
|
||||
let mut terminal = enter_fullscreen()?;
|
||||
|
|
@ -356,7 +155,7 @@ async fn run_pod_name(
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
let ready = match spawn::run_pod_name(pod_name).await? {
|
||||
let ready = match spawn::run_pod_name(pod_name, runtime_command).await? {
|
||||
SpawnOutcome::Ready(r) => r,
|
||||
SpawnOutcome::Cancelled => return Ok(()),
|
||||
};
|
||||
|
|
@ -380,6 +179,7 @@ async fn run_connected_pod(
|
|||
async fn run_pod_name_nested(
|
||||
terminal: &mut FullscreenTerminal,
|
||||
request: multi_pod::OpenPodRequest,
|
||||
runtime_command: PodRuntimeCommand,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let multi_pod::OpenPodRequest {
|
||||
pod_name,
|
||||
|
|
@ -390,16 +190,17 @@ async fn run_pod_name_nested(
|
|||
return run_connected_pod(terminal, pod_name, client).await;
|
||||
}
|
||||
|
||||
let ready = spawn_pod_name_from_fullscreen(terminal, &pod_name).await?;
|
||||
let ready = spawn_pod_name_from_fullscreen(terminal, &pod_name, runtime_command).await?;
|
||||
run_ready_pod(terminal, ready).await
|
||||
}
|
||||
|
||||
async fn spawn_pod_name_from_fullscreen(
|
||||
terminal: &mut FullscreenTerminal,
|
||||
pod_name: &str,
|
||||
runtime_command: PodRuntimeCommand,
|
||||
) -> Result<SpawnReady, Box<dyn std::error::Error>> {
|
||||
leave_fullscreen(terminal)?;
|
||||
let outcome = spawn::run_pod_name(pod_name.to_string()).await;
|
||||
let outcome = spawn::run_pod_name(pod_name.to_string(), runtime_command).await;
|
||||
enter_fullscreen_existing(terminal)?;
|
||||
terminal.clear()?;
|
||||
|
||||
|
|
@ -463,7 +264,7 @@ async fn connect_live_pod(
|
|||
.map(|client| (registry_socket, client))
|
||||
}
|
||||
|
||||
async fn run_resume() -> Result<(), Box<dyn std::error::Error>> {
|
||||
async fn run_resume(runtime_command: PodRuntimeCommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Pick a Pod in its own inline viewport, dropping the viewport before
|
||||
// attaching/restoring so each phase gets fresh vertical room.
|
||||
let (pod_name, socket_override) = match picker::run().await? {
|
||||
|
|
@ -473,10 +274,10 @@ async fn run_resume() -> Result<(), Box<dyn std::error::Error>> {
|
|||
} => (pod_name, socket_override),
|
||||
PickerOutcome::Cancelled => return Ok(()),
|
||||
};
|
||||
run_pod_name(pod_name, socket_override).await
|
||||
run_pod_name(pod_name, socket_override, runtime_command).await
|
||||
}
|
||||
|
||||
async fn run_multi() -> Result<(), Box<dyn std::error::Error>> {
|
||||
async fn run_multi(runtime_command: PodRuntimeCommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut app = multi_pod::load_app().await?;
|
||||
let mut terminal = enter_fullscreen()?;
|
||||
|
||||
|
|
@ -488,7 +289,7 @@ async fn run_multi() -> Result<(), Box<dyn std::error::Error>> {
|
|||
}
|
||||
multi_pod::MultiPodOutcome::Open(request) => {
|
||||
let pod_name = request.pod_name.clone();
|
||||
match run_pod_name_nested(&mut terminal, request).await {
|
||||
match run_pod_name_nested(&mut terminal, request, runtime_command.clone()).await {
|
||||
Ok(()) => app.finish_open(&pod_name, Ok(())),
|
||||
Err(error) if is_recoverable_multi_open_error(error.as_ref()) => {
|
||||
app.finish_open(&pod_name, Err(error.as_ref()));
|
||||
|
|
@ -511,8 +312,9 @@ fn is_recoverable_multi_open_error(error: &(dyn std::error::Error + 'static)) ->
|
|||
async fn run_spawn(
|
||||
resume_from: Option<SegmentId>,
|
||||
profile: Option<String>,
|
||||
runtime_command: PodRuntimeCommand,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let ready = match spawn::run(resume_from, profile).await? {
|
||||
let ready = match spawn::run(resume_from, profile, runtime_command).await? {
|
||||
SpawnOutcome::Ready(r) => r,
|
||||
SpawnOutcome::Cancelled => return Ok(()),
|
||||
};
|
||||
|
|
@ -1178,231 +980,6 @@ mod tests {
|
|||
use super::*;
|
||||
use protocol::{Event, RewindTarget, RewindTargetId, Segment};
|
||||
|
||||
#[test]
|
||||
fn parse_pod_name_mode() {
|
||||
match parse_args_from(["--pod", "agent", "--socket", "/tmp/agent.sock"]).unwrap() {
|
||||
Mode::PodName {
|
||||
pod_name,
|
||||
socket_override,
|
||||
} => {
|
||||
assert_eq!(pod_name, "agent");
|
||||
assert_eq!(socket_override, Some(PathBuf::from("/tmp/agent.sock")));
|
||||
}
|
||||
_ => panic!("expected PodName mode"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_positional_name_uses_pod_name_mode() {
|
||||
match parse_args_from(["agent"]).unwrap() {
|
||||
Mode::PodName {
|
||||
pod_name,
|
||||
socket_override,
|
||||
} => {
|
||||
assert_eq!(pod_name, "agent");
|
||||
assert_eq!(socket_override, None);
|
||||
}
|
||||
_ => panic!("expected PodName mode"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_memory_alone_remains_positional_pod_name() {
|
||||
match parse_args_from(["memory"]).unwrap() {
|
||||
Mode::PodName {
|
||||
pod_name,
|
||||
socket_override,
|
||||
} => {
|
||||
assert_eq!(pod_name, "memory");
|
||||
assert_eq!(socket_override, None);
|
||||
}
|
||||
_ => panic!("expected PodName mode"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_pod_subcommand_uses_runtime_mode() {
|
||||
match parse_args_from(["pod", "--pod", "agent", "--profile", "default"]).unwrap() {
|
||||
Mode::PodRuntime(args) => assert_eq!(args, ["--pod", "agent", "--profile", "default"]),
|
||||
_ => panic!("expected PodRuntime mode"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_literal_pod_name_still_available_with_flag() {
|
||||
match parse_args_from(["--pod", "pod"]).unwrap() {
|
||||
Mode::PodName {
|
||||
pod_name,
|
||||
socket_override,
|
||||
} => {
|
||||
assert_eq!(pod_name, "pod");
|
||||
assert_eq!(socket_override, None);
|
||||
}
|
||||
_ => panic!("expected PodName mode"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_memory_lint_mode() {
|
||||
match parse_args_from([
|
||||
"memory",
|
||||
"lint",
|
||||
"--workspace",
|
||||
"/tmp/ws",
|
||||
"--json",
|
||||
"--warnings-as-errors",
|
||||
])
|
||||
.unwrap()
|
||||
{
|
||||
Mode::MemoryLint(options) => {
|
||||
assert_eq!(options.workspace, Some(PathBuf::from("/tmp/ws")));
|
||||
assert!(options.json);
|
||||
assert!(options.warnings_as_errors);
|
||||
}
|
||||
_ => panic!("expected MemoryLint mode"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_memory_lint_rejects_usage_errors() {
|
||||
let err = parse_args_from(["memory", "lint", "--workspace"]).unwrap_err();
|
||||
assert_eq!(err.to_string(), "--workspace requires a value");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_memory_lint_workspace_equals() {
|
||||
match parse_args_from(["memory", "lint", "--workspace=/tmp/ws"]).unwrap() {
|
||||
Mode::MemoryLint(options) => {
|
||||
assert_eq!(options.workspace, Some(PathBuf::from("/tmp/ws")));
|
||||
assert!(!options.json);
|
||||
assert!(!options.warnings_as_errors);
|
||||
}
|
||||
_ => panic!("expected MemoryLint mode"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memory_lint_with_other_second_word_remains_positional_pod_name() {
|
||||
match parse_args_from(["memory", "other"]).unwrap() {
|
||||
Mode::PodName { pod_name, .. } => assert_eq!(pod_name, "memory"),
|
||||
_ => panic!("expected PodName mode"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rejects_pod_and_session() {
|
||||
let segment_id = session_store::new_segment_id().to_string();
|
||||
let err = parse_args_from(["--pod", "agent", "--session", &segment_id]).unwrap_err();
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"--pod and --session are mutually exclusive"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_profile_spawn_mode() {
|
||||
match parse_args_from(["--profile", "/profiles/coder.lua"]).unwrap() {
|
||||
Mode::Spawn { profile } => {
|
||||
assert_eq!(profile, Some("/profiles/coder.lua".to_string()));
|
||||
}
|
||||
_ => panic!("expected Spawn mode"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_profile_rejects_resume_attach_modes() {
|
||||
let segment_id = session_store::new_segment_id().to_string();
|
||||
let cases = [
|
||||
(
|
||||
vec![
|
||||
"--profile".to_string(),
|
||||
"p.lua".to_string(),
|
||||
"--resume".to_string(),
|
||||
],
|
||||
"--profile can only be used for fresh spawn",
|
||||
),
|
||||
(
|
||||
vec![
|
||||
"--profile".to_string(),
|
||||
"p.lua".to_string(),
|
||||
"--session".to_string(),
|
||||
segment_id,
|
||||
],
|
||||
"--profile can only be used for fresh spawn",
|
||||
),
|
||||
(
|
||||
vec![
|
||||
"--profile".to_string(),
|
||||
"p.lua".to_string(),
|
||||
"--socket".to_string(),
|
||||
"/tmp/insomnia/sock".to_string(),
|
||||
],
|
||||
"--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 {
|
||||
let err = parse_args_from(args).unwrap_err();
|
||||
assert_eq!(err.to_string(), message);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_multi_mode() {
|
||||
match parse_args_from(["--multi"]).unwrap() {
|
||||
Mode::Multi => {}
|
||||
_ => panic!("expected Multi mode"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_multi_conflicts_are_clear() {
|
||||
let segment_id = session_store::new_segment_id().to_string();
|
||||
let cases = [
|
||||
(
|
||||
vec!["--multi".to_string(), "--resume".to_string()],
|
||||
"--multi and --resume are mutually exclusive",
|
||||
),
|
||||
(
|
||||
vec!["--multi".to_string(), "--session".to_string(), segment_id],
|
||||
"--multi and --session are mutually exclusive",
|
||||
),
|
||||
(
|
||||
vec![
|
||||
"--multi".to_string(),
|
||||
"--pod".to_string(),
|
||||
"agent".to_string(),
|
||||
],
|
||||
"--multi and --pod are mutually exclusive",
|
||||
),
|
||||
(
|
||||
vec!["--multi".to_string(), "agent".to_string()],
|
||||
"--multi cannot be used with a positional Pod name",
|
||||
),
|
||||
(
|
||||
vec![
|
||||
"--multi".to_string(),
|
||||
"--socket".to_string(),
|
||||
"/tmp/a.sock".to_string(),
|
||||
],
|
||||
"--multi and --socket are mutually exclusive",
|
||||
),
|
||||
];
|
||||
|
||||
for (args, message) in cases {
|
||||
let err = parse_args_from(args).unwrap_err();
|
||||
assert_eq!(err.to_string(), message);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn terminal_event_is_selected_before_ready_pod_event() {
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
|
|
@ -15,7 +15,7 @@ use std::io;
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use client::{SpawnConfig, spawn_pod};
|
||||
use client::{PodRuntimeCommand, SpawnConfig, spawn_pod};
|
||||
use crossterm::event::{self, Event as TermEvent, KeyCode, KeyEventKind, KeyModifiers};
|
||||
use manifest::ProfileDiscovery;
|
||||
use ratatui::Terminal;
|
||||
|
|
@ -76,6 +76,7 @@ type InlineTerminal = Terminal<CrosstermBackend<io::Stdout>>;
|
|||
pub async fn run(
|
||||
resume_from: Option<SegmentId>,
|
||||
profile: Option<String>,
|
||||
runtime_command: PodRuntimeCommand,
|
||||
) -> Result<SpawnOutcome, SpawnError> {
|
||||
let defaults = load_spawn_defaults()?;
|
||||
let mut profile_choices = if resume_from.is_some() {
|
||||
|
|
@ -143,7 +144,7 @@ pub async fn run(
|
|||
form.message = Some(("starting pod...".to_string(), MessageKind::Progress));
|
||||
terminal.draw(|f| draw_form(f, &form))?;
|
||||
|
||||
match wait_for_ready(&mut terminal, &mut form).await {
|
||||
match wait_for_ready(&mut terminal, &mut form, &runtime_command).await {
|
||||
Ok(ready) => {
|
||||
form.message = Some((
|
||||
format!("ready: {} attaching...", ready.pod_name),
|
||||
|
|
@ -165,13 +166,16 @@ pub async fn run(
|
|||
/// Launch a Pod runtime command with `--pod <name>` without opening the name dialog. The child Pod
|
||||
/// resolves persisted Pod metadata if present, or creates a fresh same-name Pod
|
||||
/// from the default profile.
|
||||
pub async fn run_pod_name(pod_name: String) -> Result<SpawnOutcome, SpawnError> {
|
||||
pub async fn run_pod_name(
|
||||
pod_name: String,
|
||||
runtime_command: PodRuntimeCommand,
|
||||
) -> Result<SpawnOutcome, SpawnError> {
|
||||
let defaults = load_spawn_defaults()?;
|
||||
let mut form = form_for_pod_name(pod_name, defaults);
|
||||
let mut terminal = make_inline_terminal()?;
|
||||
terminal.draw(|f| draw_form(f, &form))?;
|
||||
|
||||
match wait_for_ready(&mut terminal, &mut form).await {
|
||||
match wait_for_ready(&mut terminal, &mut form, &runtime_command).await {
|
||||
Ok(ready) => {
|
||||
form.message = Some((
|
||||
format!("ready: {} attaching...", ready.pod_name),
|
||||
|
|
@ -360,8 +364,10 @@ fn sanitise_default_name(s: &str) -> String {
|
|||
async fn wait_for_ready(
|
||||
terminal: &mut InlineTerminal,
|
||||
form: &mut Form,
|
||||
runtime_command: &PodRuntimeCommand,
|
||||
) -> Result<SpawnReady, SpawnError> {
|
||||
let config = SpawnConfig {
|
||||
runtime_command: runtime_command.clone(),
|
||||
pod_name: form.name.clone(),
|
||||
profile: form.selected_profile_selector(),
|
||||
cwd: form.cwd.clone(),
|
||||
|
|
@ -687,7 +693,10 @@ description = "Project coder"
|
|||
|
||||
let (choices, default_index) = profile_choices_for_cwd(&project);
|
||||
assert_eq!(choices[0].selector.as_deref(), Some("builtin:default"));
|
||||
assert_eq!(choices[0].label, "builtin:default");
|
||||
assert_eq!(
|
||||
choices[0].label,
|
||||
"builtin:default — Bundled default Insomnia coding profile"
|
||||
);
|
||||
assert_eq!(default_index, 1);
|
||||
assert_eq!(choices[1].selector.as_deref(), Some("project:coder"));
|
||||
assert_eq!(choices[1].label, "project:coder (default) — Project coder");
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ rustPlatform.buildRustPackage rec {
|
|||
filter = sourceFilter;
|
||||
};
|
||||
|
||||
cargoHash = "sha256-Nu+QAXwRhqqSwgc5/9XLwQEpjEnF54tWoEknM17wYq8=";
|
||||
cargoHash = "sha256-tYql+E6s7WeZ2vQSfjH0BzmXmNQaqm54VQy8mUnpzLg=";
|
||||
|
||||
depsExtraArgs = {
|
||||
# nixpkgs 25.11's fetchCargoVendor still uses crates.io's API
|
||||
|
|
@ -83,7 +83,7 @@ rustPlatform.buildRustPackage rec {
|
|||
|
||||
cargoBuildFlags = [
|
||||
"-p"
|
||||
"tui"
|
||||
"insomnia"
|
||||
];
|
||||
|
||||
# The package check is a credential-free install smoke check below. Running the
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user