Compare commits
52 Commits
44ff1411a3
...
6e5ed683d6
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e5ed683d6 | |||
| faac237f0d | |||
| 50ccf0f21b | |||
| 9a00c91632 | |||
| 52d8807282 | |||
| 32d20eea01 | |||
| 17e799a766 | |||
| 0031953ed3 | |||
| ca9e1840cf | |||
| a24d026b0e | |||
| 8ec398522e | |||
| 4d897186c2 | |||
| c4cc93512f | |||
| 0a538380a9 | |||
| 9afe8a3243 | |||
| 72d6ac8719 | |||
| bc31bfac58 | |||
| 80a41d3264 | |||
| 7141734f07 | |||
| 6b49be085d | |||
| b12e3320a9 | |||
| 37281b64f2 | |||
| 22d974a722 | |||
| 2deb93c7ce | |||
| 4c7088d757 | |||
| f0efafbba9 | |||
| 5783503f74 | |||
| 530c14e4b8 | |||
| f7293411cd | |||
| 18ae9934e1 | |||
| 365ec8b7fa | |||
| 607923dfbd | |||
| f5c4943337 | |||
| 3a18842ac7 | |||
| 469410cbcf | |||
| 813ff7e199 | |||
| 7851fc0aac | |||
| 974dde3b88 | |||
| e232f5468a | |||
| 6fa67097aa | |||
| 506719796e | |||
| 20f975c9b7 | |||
| 1f29d1a39d | |||
| e605a5886f | |||
| 400cca9252 | |||
| 458a87e81a | |||
| e8d8f139d0 | |||
| e8e50bfa6b | |||
| b61504e821 | |||
| fc3a0718a1 | |||
| c618fa694c | |||
| e64a559595 |
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};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::fmt;
|
|||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub const POD_COMMAND_OVERRIDE_ENV: &str = "INSOMNIA_POD_COMMAND";
|
||||
const POD_RUNTIME_COMMAND_ENV: &str = "INSOMNIA_POD_RUNTIME_COMMAND";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PodRuntimeCommand {
|
||||
|
|
@ -19,10 +19,6 @@ impl PodRuntimeCommand {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn executable_only(program: impl Into<PathBuf>) -> Self {
|
||||
Self::new(program, Vec::new())
|
||||
}
|
||||
|
||||
pub fn for_current_exe() -> io::Result<Self> {
|
||||
Ok(Self::for_executable(std::env::current_exe()?))
|
||||
}
|
||||
|
|
@ -33,23 +29,30 @@ impl PodRuntimeCommand {
|
|||
|
||||
/// Resolve the Pod runtime command used for subprocess launches.
|
||||
///
|
||||
/// `INSOMNIA_POD_COMMAND` is intentionally executable-only: its value is
|
||||
/// used as the program path without shell parsing and without the unified
|
||||
/// `pod` prefix arg. That keeps development/test overrides safe while the
|
||||
/// default path is always `current_exe() + ["pod"]`.
|
||||
/// The default launch path is always the current `insomnia` executable plus
|
||||
/// the unified `pod` prefix argument. During development, a non-empty
|
||||
/// `INSOMNIA_POD_RUNTIME_COMMAND` value replaces only the executable path;
|
||||
/// the `pod` prefix is still added here and the env value is not parsed as a
|
||||
/// shell command.
|
||||
pub fn resolve() -> io::Result<Self> {
|
||||
if let Some(command) = Self::from_override_env() {
|
||||
return Ok(command);
|
||||
}
|
||||
Self::for_current_exe()
|
||||
Self::resolve_from_env_value(
|
||||
std::env::var_os(POD_RUNTIME_COMMAND_ENV),
|
||||
std::env::current_exe,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn from_override_env() -> Option<Self> {
|
||||
let raw = std::env::var_os(POD_COMMAND_OVERRIDE_ENV)?;
|
||||
if raw.is_empty() {
|
||||
return None;
|
||||
fn resolve_from_env_value<F>(
|
||||
override_program: Option<OsString>,
|
||||
current_exe: F,
|
||||
) -> io::Result<Self>
|
||||
where
|
||||
F: FnOnce() -> io::Result<PathBuf>,
|
||||
{
|
||||
if let Some(program) = override_program.filter(|program| !program.as_os_str().is_empty()) {
|
||||
return Ok(Self::for_executable(program));
|
||||
}
|
||||
Some(Self::executable_only(raw))
|
||||
|
||||
Ok(Self::for_executable(current_exe()?))
|
||||
}
|
||||
|
||||
pub fn program(&self) -> &Path {
|
||||
|
|
@ -84,28 +87,6 @@ impl fmt::Display for PodRuntimeCommand {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::Mutex;
|
||||
|
||||
static ENV_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
struct EnvRestore(Option<OsString>);
|
||||
|
||||
impl EnvRestore {
|
||||
fn capture() -> Self {
|
||||
Self(std::env::var_os(POD_COMMAND_OVERRIDE_ENV))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EnvRestore {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
match &self.0 {
|
||||
Some(value) => std::env::set_var(POD_COMMAND_OVERRIDE_ENV, value),
|
||||
None => std::env::remove_var(POD_COMMAND_OVERRIDE_ENV),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insomnia_binary_defaults_to_pod_prefix() {
|
||||
|
|
@ -141,16 +122,47 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn env_override_is_executable_only_and_not_shell_parsed() {
|
||||
let _guard = ENV_LOCK.lock().unwrap();
|
||||
let _restore = EnvRestore::capture();
|
||||
unsafe {
|
||||
std::env::set_var(POD_COMMAND_OVERRIDE_ENV, "/tmp/mock pod --flag");
|
||||
}
|
||||
fn resolve_uses_current_exe_when_override_is_unset() {
|
||||
let command = PodRuntimeCommand::resolve_from_env_value(None, || {
|
||||
Ok(PathBuf::from("/opt/insomnia/bin/insomnia"))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let command = PodRuntimeCommand::resolve().unwrap();
|
||||
assert_eq!(
|
||||
command,
|
||||
PodRuntimeCommand::for_executable("/opt/insomnia/bin/insomnia")
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(command.program(), Path::new("/tmp/mock pod --flag"));
|
||||
assert!(command.prefix_args().is_empty());
|
||||
#[test]
|
||||
fn resolve_uses_current_exe_when_override_is_empty() {
|
||||
let command = PodRuntimeCommand::resolve_from_env_value(Some(OsString::new()), || {
|
||||
Ok(PathBuf::from("/opt/insomnia/bin/insomnia"))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
command,
|
||||
PodRuntimeCommand::for_executable("/opt/insomnia/bin/insomnia")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_override_replaces_only_program_and_keeps_pod_prefix() {
|
||||
let command = PodRuntimeCommand::resolve_from_env_value(
|
||||
Some(OsString::from("/tmp/rebuilt insomnia")),
|
||||
|| panic!("override must not inspect current_exe"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(command.program(), Path::new("/tmp/rebuilt insomnia"));
|
||||
assert_eq!(command.prefix_args(), [OsString::from("pod")]);
|
||||
assert_eq!(
|
||||
command.argv_with(["--pod", "agent"]),
|
||||
vec!["pod", "--pod", "agent"]
|
||||
.into_iter()
|
||||
.map(OsString::from)
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 行に乗る
|
||||
/// 名前との突き合わせに使う。
|
||||
|
|
@ -52,7 +53,10 @@ pub enum SpawnError {
|
|||
Io(io::Error),
|
||||
/// runtime ディレクトリが解決できなかった (環境変数未設定等)。
|
||||
RuntimeDirUnavailable,
|
||||
PodLaunchFailed(io::Error),
|
||||
PodLaunchFailed {
|
||||
command: PodRuntimeCommand,
|
||||
source: io::Error,
|
||||
},
|
||||
PodExitedEarly {
|
||||
stderr_tail: String,
|
||||
},
|
||||
|
|
@ -67,7 +71,10 @@ impl std::fmt::Display for SpawnError {
|
|||
f,
|
||||
"could not resolve runtime directory (set INSOMNIA_HOME, INSOMNIA_RUNTIME_DIR, XDG_RUNTIME_DIR, or HOME)"
|
||||
),
|
||||
Self::PodLaunchFailed(e) => write!(f, "failed to launch pod: {e}"),
|
||||
Self::PodLaunchFailed { command, source } => write!(
|
||||
f,
|
||||
"failed to launch pod runtime command `{command}`: {source}"
|
||||
),
|
||||
Self::PodExitedEarly { stderr_tail } => {
|
||||
if stderr_tail.is_empty() {
|
||||
write!(f, "pod exited before becoming ready")
|
||||
|
|
@ -84,7 +91,14 @@ impl std::fmt::Display for SpawnError {
|
|||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for SpawnError {}
|
||||
impl std::error::Error for SpawnError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::Io(error) | Self::PodLaunchFailed { source: error, .. } => Some(error),
|
||||
Self::RuntimeDirUnavailable | Self::PodExitedEarly { .. } | Self::Timeout => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for SpawnError {
|
||||
fn from(e: io::Error) -> Self {
|
||||
|
|
@ -100,17 +114,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())
|
||||
|
|
@ -133,7 +145,12 @@ where
|
|||
.arg("--session-pod-name")
|
||||
.arg(&config.pod_name);
|
||||
}
|
||||
let mut child = command.spawn().map_err(SpawnError::PodLaunchFailed)?;
|
||||
let mut child = command
|
||||
.spawn()
|
||||
.map_err(|source| SpawnError::PodLaunchFailed {
|
||||
command: config.runtime_command.clone(),
|
||||
source,
|
||||
})?;
|
||||
|
||||
// Default `kill_on_drop = false` plus `process_group(0)` makes this
|
||||
// a detached Pod once startup succeeds: dropping the handle does not
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,49 +25,36 @@
|
|||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Environment variable that points at installed project resources.
|
||||
pub const RESOURCE_DIR_ENV: &str = "INSOMNIA_RESOURCE_DIR";
|
||||
|
||||
/// 設定ディレクトリ。`profiles.toml`, `providers.toml`, `models.toml`,
|
||||
/// `prompts/` などが置かれる。
|
||||
pub fn config_dir() -> Option<PathBuf> {
|
||||
if let Some(p) = env_path("INSOMNIA_CONFIG_DIR") {
|
||||
return Some(p);
|
||||
}
|
||||
if let Some(p) = env_path("INSOMNIA_HOME") {
|
||||
return Some(p.join("config"));
|
||||
}
|
||||
if let Some(p) = env_path("XDG_CONFIG_HOME") {
|
||||
return Some(p.join("insomnia"));
|
||||
}
|
||||
Some(env_path("HOME")?.join(".config").join("insomnia"))
|
||||
resolve_config_dir_from_parts(
|
||||
env_path("INSOMNIA_CONFIG_DIR"),
|
||||
env_path("INSOMNIA_HOME"),
|
||||
env_path("XDG_CONFIG_HOME"),
|
||||
env_path("HOME"),
|
||||
)
|
||||
}
|
||||
|
||||
/// データディレクトリ。`sessions/` などプログラムが書く永続データの
|
||||
/// 置き場。
|
||||
pub fn data_dir() -> Option<PathBuf> {
|
||||
if let Some(p) = env_path("INSOMNIA_DATA_DIR") {
|
||||
return Some(p);
|
||||
}
|
||||
if let Some(p) = env_path("INSOMNIA_HOME") {
|
||||
return Some(p);
|
||||
}
|
||||
Some(env_path("HOME")?.join(".insomnia"))
|
||||
resolve_data_dir_from_parts(
|
||||
env_path("INSOMNIA_DATA_DIR"),
|
||||
env_path("INSOMNIA_HOME"),
|
||||
env_path("HOME"),
|
||||
)
|
||||
}
|
||||
|
||||
/// ランタイムディレクトリ。socket, `pods.json`, Pod ごとの `pid` /
|
||||
/// `status.json` 等が置かれる。再起動で消えて構わない。
|
||||
pub fn runtime_dir() -> Option<PathBuf> {
|
||||
if let Some(p) = env_path("INSOMNIA_RUNTIME_DIR") {
|
||||
return Some(p);
|
||||
}
|
||||
if let Some(p) = env_path("INSOMNIA_HOME") {
|
||||
return Some(p.join("run"));
|
||||
}
|
||||
if let Some(p) = env_path("XDG_RUNTIME_DIR") {
|
||||
return Some(p.join("insomnia"));
|
||||
}
|
||||
Some(env_path("HOME")?.join(".insomnia").join("run"))
|
||||
resolve_runtime_dir_from_parts(
|
||||
env_path("INSOMNIA_RUNTIME_DIR"),
|
||||
env_path("INSOMNIA_HOME"),
|
||||
env_path("XDG_RUNTIME_DIR"),
|
||||
env_path("HOME"),
|
||||
)
|
||||
}
|
||||
|
||||
// ---- well-known file getters ------------------------------------------------
|
||||
|
|
@ -77,64 +64,38 @@ pub fn runtime_dir() -> Option<PathBuf> {
|
|||
/// This is application/profile selection configuration, not a Pod manifest
|
||||
/// layer.
|
||||
pub fn user_profiles_path() -> Option<PathBuf> {
|
||||
Some(config_dir()?.join("profiles.toml"))
|
||||
user_profiles_path_from_config_dir(config_dir())
|
||||
}
|
||||
|
||||
/// `<config_dir>/prompts/` — user prompts ライブラリ。
|
||||
pub fn user_prompts_dir() -> Option<PathBuf> {
|
||||
Some(config_dir()?.join("prompts"))
|
||||
}
|
||||
|
||||
/// Root resource directory used for bundled prompts, profiles, catalogs, and docs.
|
||||
pub fn resource_dir() -> Option<PathBuf> {
|
||||
if let Some(p) = env_path(RESOURCE_DIR_ENV) {
|
||||
return Some(p);
|
||||
}
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(prefix) = exe.parent().and_then(|bin| bin.parent()) {
|
||||
let installed = prefix.join("share").join("insomnia").join("resources");
|
||||
if installed.exists() {
|
||||
return Some(installed);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../..")
|
||||
.join("resources"),
|
||||
)
|
||||
}
|
||||
|
||||
/// Bundled Lua profile registry directory. Missing directories are treated as
|
||||
/// an empty builtin registry by discovery.
|
||||
pub fn builtin_profiles_dir() -> Option<PathBuf> {
|
||||
Some(resource_dir()?.join("profiles"))
|
||||
user_prompts_dir_from_config_dir(config_dir())
|
||||
}
|
||||
|
||||
/// `<config_dir>/prompts.toml` — user prompt pack。
|
||||
pub fn user_pack_file() -> Option<PathBuf> {
|
||||
Some(config_dir()?.join("prompts.toml"))
|
||||
user_pack_file_from_config_dir(config_dir())
|
||||
}
|
||||
|
||||
/// `<config_dir>/<file_name>` — providers.toml / models.toml 等の
|
||||
/// user override ファイル。
|
||||
pub fn user_catalog_override(file_name: &str) -> Option<PathBuf> {
|
||||
Some(config_dir()?.join(file_name))
|
||||
user_catalog_override_from_config_dir(config_dir(), file_name)
|
||||
}
|
||||
|
||||
/// `<data_dir>/sessions/` — session store のデフォルト位置。
|
||||
pub fn sessions_dir() -> Option<PathBuf> {
|
||||
Some(data_dir()?.join("sessions"))
|
||||
sessions_dir_from_data_dir(data_dir())
|
||||
}
|
||||
|
||||
/// `<runtime_dir>/pods.json` — machine-wide Pod allocation registry。
|
||||
pub fn pod_registry_path() -> Option<PathBuf> {
|
||||
Some(runtime_dir()?.join("pods.json"))
|
||||
pod_registry_path_from_runtime_dir(runtime_dir())
|
||||
}
|
||||
|
||||
/// `<runtime_dir>/<pod_name>/` — Pod ごとのランタイムディレクトリ。
|
||||
pub fn pod_runtime_dir(pod_name: &str) -> Option<PathBuf> {
|
||||
Some(runtime_dir()?.join(pod_name))
|
||||
pod_runtime_dir_from_runtime_dir(runtime_dir(), pod_name)
|
||||
}
|
||||
|
||||
/// `<runtime_dir>/<pod_name>/sock` — Pod の Unix socket パス。
|
||||
|
|
@ -144,206 +105,306 @@ pub fn pod_runtime_dir(pod_name: &str) -> Option<PathBuf> {
|
|||
/// attach フロー等) からの**予測**はこの関数で行う。両者は同じパス
|
||||
/// を返すことが期待される。
|
||||
pub fn pod_socket_path(pod_name: &str) -> Option<PathBuf> {
|
||||
Some(pod_runtime_dir(pod_name)?.join("sock"))
|
||||
pod_socket_path_from_runtime_dir(runtime_dir(), pod_name)
|
||||
}
|
||||
|
||||
// ---- internals --------------------------------------------------------------
|
||||
|
||||
fn resolve_config_dir_from_parts(
|
||||
insomnia_config_dir: Option<PathBuf>,
|
||||
insomnia_home: Option<PathBuf>,
|
||||
xdg_config_home: Option<PathBuf>,
|
||||
home: Option<PathBuf>,
|
||||
) -> Option<PathBuf> {
|
||||
if let Some(p) = insomnia_config_dir {
|
||||
return Some(p);
|
||||
}
|
||||
if let Some(p) = insomnia_home {
|
||||
return Some(p.join("config"));
|
||||
}
|
||||
if let Some(p) = xdg_config_home {
|
||||
return Some(p.join("insomnia"));
|
||||
}
|
||||
Some(home?.join(".config").join("insomnia"))
|
||||
}
|
||||
|
||||
fn resolve_data_dir_from_parts(
|
||||
insomnia_data_dir: Option<PathBuf>,
|
||||
insomnia_home: Option<PathBuf>,
|
||||
home: Option<PathBuf>,
|
||||
) -> Option<PathBuf> {
|
||||
if let Some(p) = insomnia_data_dir {
|
||||
return Some(p);
|
||||
}
|
||||
if let Some(p) = insomnia_home {
|
||||
return Some(p);
|
||||
}
|
||||
Some(home?.join(".insomnia"))
|
||||
}
|
||||
|
||||
fn resolve_runtime_dir_from_parts(
|
||||
insomnia_runtime_dir: Option<PathBuf>,
|
||||
insomnia_home: Option<PathBuf>,
|
||||
xdg_runtime_dir: Option<PathBuf>,
|
||||
home: Option<PathBuf>,
|
||||
) -> Option<PathBuf> {
|
||||
if let Some(p) = insomnia_runtime_dir {
|
||||
return Some(p);
|
||||
}
|
||||
if let Some(p) = insomnia_home {
|
||||
return Some(p.join("run"));
|
||||
}
|
||||
if let Some(p) = xdg_runtime_dir {
|
||||
return Some(p.join("insomnia"));
|
||||
}
|
||||
Some(home?.join(".insomnia").join("run"))
|
||||
}
|
||||
|
||||
fn user_profiles_path_from_config_dir(config_dir: Option<PathBuf>) -> Option<PathBuf> {
|
||||
Some(config_dir?.join("profiles.toml"))
|
||||
}
|
||||
|
||||
fn user_prompts_dir_from_config_dir(config_dir: Option<PathBuf>) -> Option<PathBuf> {
|
||||
Some(config_dir?.join("prompts"))
|
||||
}
|
||||
|
||||
fn user_pack_file_from_config_dir(config_dir: Option<PathBuf>) -> Option<PathBuf> {
|
||||
Some(config_dir?.join("prompts.toml"))
|
||||
}
|
||||
|
||||
fn user_catalog_override_from_config_dir(
|
||||
config_dir: Option<PathBuf>,
|
||||
file_name: &str,
|
||||
) -> Option<PathBuf> {
|
||||
Some(config_dir?.join(file_name))
|
||||
}
|
||||
|
||||
fn sessions_dir_from_data_dir(data_dir: Option<PathBuf>) -> Option<PathBuf> {
|
||||
Some(data_dir?.join("sessions"))
|
||||
}
|
||||
|
||||
fn pod_registry_path_from_runtime_dir(runtime_dir: Option<PathBuf>) -> Option<PathBuf> {
|
||||
Some(runtime_dir?.join("pods.json"))
|
||||
}
|
||||
|
||||
fn pod_runtime_dir_from_runtime_dir(
|
||||
runtime_dir: Option<PathBuf>,
|
||||
pod_name: &str,
|
||||
) -> Option<PathBuf> {
|
||||
Some(runtime_dir?.join(pod_name))
|
||||
}
|
||||
|
||||
fn pod_socket_path_from_runtime_dir(
|
||||
runtime_dir: Option<PathBuf>,
|
||||
pod_name: &str,
|
||||
) -> Option<PathBuf> {
|
||||
Some(pod_runtime_dir_from_runtime_dir(runtime_dir, pod_name)?.join("sock"))
|
||||
}
|
||||
|
||||
/// 空文字列の env は未設定として扱う。`std::env::var` は `Ok("")` と
|
||||
/// `Err(NotPresent)` を区別するが、パス解決においては両者を未設定と
|
||||
/// 同等に扱うのが直感的。
|
||||
fn env_path(name: &str) -> Option<PathBuf> {
|
||||
std::env::var(name)
|
||||
.ok()
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(PathBuf::from)
|
||||
let value = std::env::var(name).ok()?;
|
||||
path_from_env_value(Some(value.as_str()))
|
||||
}
|
||||
|
||||
fn path_from_env_value(value: Option<&str>) -> Option<PathBuf> {
|
||||
value.filter(|s| !s.is_empty()).map(PathBuf::from)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::{Mutex, MutexGuard, OnceLock};
|
||||
|
||||
/// プロセス全体で env を弄るテスト同士が並行に走らないように保護
|
||||
/// する。Cargo の test harness はファイル単位で別プロセスにせず
|
||||
/// マルチスレッドで実行するため、env を読む全テストはこの lock を
|
||||
/// 取ってから操作する。
|
||||
fn env_lock() -> MutexGuard<'static, ()> {
|
||||
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
LOCK.get_or_init(|| Mutex::new(()))
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// テスト中だけ env を上書きし、drop 時に元の値に戻す RAII guard。
|
||||
struct EnvGuard {
|
||||
vars: Vec<(&'static str, Option<String>)>,
|
||||
_lock: MutexGuard<'static, ()>,
|
||||
}
|
||||
|
||||
impl EnvGuard {
|
||||
fn new(overrides: &[(&'static str, Option<&str>)]) -> Self {
|
||||
let lock = env_lock();
|
||||
let names = [
|
||||
"INSOMNIA_CONFIG_DIR",
|
||||
"INSOMNIA_DATA_DIR",
|
||||
"INSOMNIA_RUNTIME_DIR",
|
||||
"INSOMNIA_RESOURCE_DIR",
|
||||
"INSOMNIA_HOME",
|
||||
"XDG_CONFIG_HOME",
|
||||
"XDG_RUNTIME_DIR",
|
||||
"HOME",
|
||||
];
|
||||
let saved: Vec<_> = names.iter().map(|n| (*n, std::env::var(n).ok())).collect();
|
||||
// SAFETY: env_lock() 取得済みなので env への並行アクセスは
|
||||
// この test バイナリ内では発生しない。
|
||||
unsafe {
|
||||
for (n, _) in &saved {
|
||||
std::env::remove_var(n);
|
||||
}
|
||||
for (n, v) in overrides {
|
||||
if let Some(v) = v {
|
||||
std::env::set_var(n, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
Self {
|
||||
vars: saved,
|
||||
_lock: lock,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EnvGuard {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: lock を握ったまま元に戻す。
|
||||
unsafe {
|
||||
for (n, v) in &self.vars {
|
||||
match v {
|
||||
Some(v) => std::env::set_var(n, v),
|
||||
None => std::env::remove_var(n),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_dir_falls_back_to_home_dot_config() {
|
||||
let _g = EnvGuard::new(&[("HOME", Some("/h"))]);
|
||||
assert_eq!(config_dir().unwrap(), PathBuf::from("/h/.config/insomnia"));
|
||||
assert_eq!(
|
||||
resolve_config_dir_from_parts(None, None, None, Some(PathBuf::from("/h"))).unwrap(),
|
||||
PathBuf::from("/h/.config/insomnia")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_dir_uses_xdg_when_set() {
|
||||
let _g = EnvGuard::new(&[("HOME", Some("/h")), ("XDG_CONFIG_HOME", Some("/x"))]);
|
||||
assert_eq!(config_dir().unwrap(), PathBuf::from("/x/insomnia"));
|
||||
assert_eq!(
|
||||
resolve_config_dir_from_parts(
|
||||
None,
|
||||
None,
|
||||
Some(PathBuf::from("/x")),
|
||||
Some(PathBuf::from("/h")),
|
||||
)
|
||||
.unwrap(),
|
||||
PathBuf::from("/x/insomnia")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_dir_insomnia_home_outranks_xdg() {
|
||||
let _g = EnvGuard::new(&[
|
||||
("HOME", Some("/h")),
|
||||
("XDG_CONFIG_HOME", Some("/x")),
|
||||
("INSOMNIA_HOME", Some("/sand")),
|
||||
]);
|
||||
assert_eq!(config_dir().unwrap(), PathBuf::from("/sand/config"));
|
||||
assert_eq!(
|
||||
resolve_config_dir_from_parts(
|
||||
None,
|
||||
Some(PathBuf::from("/sand")),
|
||||
Some(PathBuf::from("/x")),
|
||||
Some(PathBuf::from("/h")),
|
||||
)
|
||||
.unwrap(),
|
||||
PathBuf::from("/sand/config")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_dir_explicit_wins_over_insomnia_home() {
|
||||
let _g = EnvGuard::new(&[
|
||||
("HOME", Some("/h")),
|
||||
("INSOMNIA_HOME", Some("/sand")),
|
||||
("INSOMNIA_CONFIG_DIR", Some("/explicit-cfg")),
|
||||
]);
|
||||
assert_eq!(config_dir().unwrap(), PathBuf::from("/explicit-cfg"));
|
||||
assert_eq!(
|
||||
resolve_config_dir_from_parts(
|
||||
Some(PathBuf::from("/explicit-cfg")),
|
||||
Some(PathBuf::from("/sand")),
|
||||
None,
|
||||
Some(PathBuf::from("/h")),
|
||||
)
|
||||
.unwrap(),
|
||||
PathBuf::from("/explicit-cfg")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_dir_default_is_dot_insomnia() {
|
||||
let _g = EnvGuard::new(&[("HOME", Some("/h"))]);
|
||||
assert_eq!(data_dir().unwrap(), PathBuf::from("/h/.insomnia"));
|
||||
assert_eq!(
|
||||
resolve_data_dir_from_parts(None, None, Some(PathBuf::from("/h"))).unwrap(),
|
||||
PathBuf::from("/h/.insomnia")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_dir_insomnia_home_is_data_dir_itself() {
|
||||
let _g = EnvGuard::new(&[("HOME", Some("/h")), ("INSOMNIA_HOME", Some("/sand"))]);
|
||||
assert_eq!(data_dir().unwrap(), PathBuf::from("/sand"));
|
||||
assert_eq!(
|
||||
resolve_data_dir_from_parts(
|
||||
None,
|
||||
Some(PathBuf::from("/sand")),
|
||||
Some(PathBuf::from("/h"))
|
||||
)
|
||||
.unwrap(),
|
||||
PathBuf::from("/sand")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_dir_explicit_wins_over_insomnia_home() {
|
||||
assert_eq!(
|
||||
resolve_data_dir_from_parts(
|
||||
Some(PathBuf::from("/explicit-data")),
|
||||
Some(PathBuf::from("/sand")),
|
||||
Some(PathBuf::from("/h")),
|
||||
)
|
||||
.unwrap(),
|
||||
PathBuf::from("/explicit-data")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_dir_prefers_xdg_runtime_dir() {
|
||||
let _g = EnvGuard::new(&[
|
||||
("HOME", Some("/h")),
|
||||
("XDG_RUNTIME_DIR", Some("/xdg-runtime")),
|
||||
]);
|
||||
assert_eq!(
|
||||
runtime_dir().unwrap(),
|
||||
resolve_runtime_dir_from_parts(
|
||||
None,
|
||||
None,
|
||||
Some(PathBuf::from("/xdg-runtime")),
|
||||
Some(PathBuf::from("/h")),
|
||||
)
|
||||
.unwrap(),
|
||||
PathBuf::from("/xdg-runtime/insomnia")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_dir_falls_back_to_dot_insomnia_run() {
|
||||
let _g = EnvGuard::new(&[("HOME", Some("/h"))]);
|
||||
assert_eq!(runtime_dir().unwrap(), PathBuf::from("/h/.insomnia/run"));
|
||||
assert_eq!(
|
||||
resolve_runtime_dir_from_parts(None, None, None, Some(PathBuf::from("/h"))).unwrap(),
|
||||
PathBuf::from("/h/.insomnia/run")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_dir_insomnia_home_is_run_subdir() {
|
||||
let _g = EnvGuard::new(&[
|
||||
("HOME", Some("/h")),
|
||||
("XDG_RUNTIME_DIR", Some("/run/user/1000")),
|
||||
("INSOMNIA_HOME", Some("/sand")),
|
||||
]);
|
||||
assert_eq!(runtime_dir().unwrap(), PathBuf::from("/sand/run"));
|
||||
assert_eq!(
|
||||
resolve_runtime_dir_from_parts(
|
||||
None,
|
||||
Some(PathBuf::from("/sand")),
|
||||
Some(PathBuf::from("/run/user/1000")),
|
||||
Some(PathBuf::from("/h")),
|
||||
)
|
||||
.unwrap(),
|
||||
PathBuf::from("/sand/run")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_env_treated_as_unset() {
|
||||
let _g = EnvGuard::new(&[("HOME", Some("/h")), ("XDG_CONFIG_HOME", Some(""))]);
|
||||
assert_eq!(config_dir().unwrap(), PathBuf::from("/h/.config/insomnia"));
|
||||
fn runtime_dir_explicit_wins_over_insomnia_home() {
|
||||
assert_eq!(
|
||||
resolve_runtime_dir_from_parts(
|
||||
Some(PathBuf::from("/explicit-run")),
|
||||
Some(PathBuf::from("/sand")),
|
||||
Some(PathBuf::from("/run/user/1000")),
|
||||
Some(PathBuf::from("/h")),
|
||||
)
|
||||
.unwrap(),
|
||||
PathBuf::from("/explicit-run")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_env_value_treated_as_unset_before_path_resolution() {
|
||||
let xdg_config_home = path_from_env_value(Some(""));
|
||||
|
||||
assert_eq!(
|
||||
resolve_config_dir_from_parts(None, None, xdg_config_home, Some(PathBuf::from("/h")))
|
||||
.unwrap(),
|
||||
PathBuf::from("/h/.config/insomnia")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_none_when_nothing_set() {
|
||||
let _g = EnvGuard::new(&[]);
|
||||
assert!(config_dir().is_none());
|
||||
assert!(data_dir().is_none());
|
||||
assert!(runtime_dir().is_none());
|
||||
assert!(resolve_config_dir_from_parts(None, None, None, None).is_none());
|
||||
assert!(resolve_data_dir_from_parts(None, None, None).is_none());
|
||||
assert!(resolve_runtime_dir_from_parts(None, None, None, None).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn well_known_files_compose_off_base_dirs() {
|
||||
let _g = EnvGuard::new(&[("INSOMNIA_HOME", Some("/sand"))]);
|
||||
let config_dir = Some(PathBuf::from("/sand/config"));
|
||||
let data_dir = Some(PathBuf::from("/sand"));
|
||||
let runtime_dir = Some(PathBuf::from("/sand/run"));
|
||||
|
||||
assert_eq!(
|
||||
user_profiles_path().unwrap(),
|
||||
user_profiles_path_from_config_dir(config_dir.clone()).unwrap(),
|
||||
PathBuf::from("/sand/config/profiles.toml")
|
||||
);
|
||||
assert_eq!(
|
||||
user_prompts_dir().unwrap(),
|
||||
user_prompts_dir_from_config_dir(config_dir.clone()).unwrap(),
|
||||
PathBuf::from("/sand/config/prompts")
|
||||
);
|
||||
assert_eq!(
|
||||
user_pack_file().unwrap(),
|
||||
user_pack_file_from_config_dir(config_dir.clone()).unwrap(),
|
||||
PathBuf::from("/sand/config/prompts.toml")
|
||||
);
|
||||
assert_eq!(
|
||||
user_catalog_override("providers.toml").unwrap(),
|
||||
user_catalog_override_from_config_dir(config_dir, "providers.toml").unwrap(),
|
||||
PathBuf::from("/sand/config/providers.toml")
|
||||
);
|
||||
assert_eq!(sessions_dir().unwrap(), PathBuf::from("/sand/sessions"));
|
||||
assert_eq!(
|
||||
pod_registry_path().unwrap(),
|
||||
sessions_dir_from_data_dir(data_dir).unwrap(),
|
||||
PathBuf::from("/sand/sessions")
|
||||
);
|
||||
assert_eq!(
|
||||
pod_registry_path_from_runtime_dir(runtime_dir.clone()).unwrap(),
|
||||
PathBuf::from("/sand/run/pods.json")
|
||||
);
|
||||
assert_eq!(
|
||||
pod_runtime_dir("foo").unwrap(),
|
||||
pod_runtime_dir_from_runtime_dir(runtime_dir.clone(), "foo").unwrap(),
|
||||
PathBuf::from("/sand/run/foo")
|
||||
);
|
||||
assert_eq!(
|
||||
pod_socket_path("foo").unwrap(),
|
||||
pod_socket_path_from_runtime_dir(runtime_dir, "foo").unwrap(),
|
||||
PathBuf::from("/sand/run/foo/sock")
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ use crate::{
|
|||
|
||||
const PROFILE_FORMAT_V1: &str = "insomnia.lua-profile.v1";
|
||||
const BUILTIN_DEFAULT_PROFILE_NAME: &str = "default";
|
||||
const BUILTIN_DEFAULT_PROFILE: &str = include_str!("../../../resources/profiles/default.lua");
|
||||
const BUILTIN_MODEL_CATALOG: &str = include_str!("../../../resources/models/builtin.toml");
|
||||
const DEFAULT_POD_NAME: &str = "insomnia";
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
|
|
@ -126,7 +128,10 @@ pub enum ProfileSource {
|
|||
Registry {
|
||||
source: ProfileRegistrySource,
|
||||
name: String,
|
||||
path: PathBuf,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
path: Option<PathBuf>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
provenance: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -134,15 +139,62 @@ pub enum ProfileSource {
|
|||
pub struct ProfileRegistryEntry {
|
||||
pub source: ProfileRegistrySource,
|
||||
pub name: String,
|
||||
pub path: PathBuf,
|
||||
pub path: Option<PathBuf>,
|
||||
pub provenance: String,
|
||||
pub description: Option<String>,
|
||||
pub is_default: bool,
|
||||
artifact: ProfileRegistryArtifact,
|
||||
}
|
||||
|
||||
impl ProfileRegistryEntry {
|
||||
pub fn qualified_name(&self) -> String {
|
||||
format!("{}:{}", self.source, self.name)
|
||||
}
|
||||
|
||||
fn path(
|
||||
source: ProfileRegistrySource,
|
||||
name: String,
|
||||
path: PathBuf,
|
||||
description: Option<String>,
|
||||
) -> Self {
|
||||
let provenance = path.display().to_string();
|
||||
Self {
|
||||
source,
|
||||
name,
|
||||
path: Some(path.clone()),
|
||||
provenance,
|
||||
description,
|
||||
is_default: false,
|
||||
artifact: ProfileRegistryArtifact::Path(path),
|
||||
}
|
||||
}
|
||||
|
||||
fn embedded(
|
||||
source: ProfileRegistrySource,
|
||||
name: &'static str,
|
||||
label: &'static str,
|
||||
content: &'static str,
|
||||
description: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
source,
|
||||
name: name.to_string(),
|
||||
path: None,
|
||||
provenance: label.to_string(),
|
||||
description,
|
||||
is_default: false,
|
||||
artifact: ProfileRegistryArtifact::Embedded { label, content },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum ProfileRegistryArtifact {
|
||||
Path(PathBuf),
|
||||
Embedded {
|
||||
label: &'static str,
|
||||
content: &'static str,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
|
|
@ -241,7 +293,6 @@ struct ProfileDefault {
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProfileDiscovery {
|
||||
builtin_dir: Option<PathBuf>,
|
||||
user_config: Option<PathBuf>,
|
||||
project_config: Option<PathBuf>,
|
||||
}
|
||||
|
|
@ -249,27 +300,19 @@ pub struct ProfileDiscovery {
|
|||
impl ProfileDiscovery {
|
||||
pub fn for_cwd(cwd: &Path) -> Self {
|
||||
Self {
|
||||
builtin_dir: paths::builtin_profiles_dir(),
|
||||
user_config: paths::user_profiles_path(),
|
||||
project_config: find_project_profiles_from(cwd),
|
||||
}
|
||||
}
|
||||
pub fn with_sources(
|
||||
builtin_dir: Option<PathBuf>,
|
||||
user_config: Option<PathBuf>,
|
||||
project_config: Option<PathBuf>,
|
||||
) -> Self {
|
||||
pub fn with_sources(user_config: Option<PathBuf>, project_config: Option<PathBuf>) -> Self {
|
||||
Self {
|
||||
builtin_dir,
|
||||
user_config,
|
||||
project_config,
|
||||
}
|
||||
}
|
||||
pub fn discover(&self) -> Result<ProfileRegistry, ProfileError> {
|
||||
let mut registry = ProfileRegistry::default();
|
||||
if let Some(dir) = &self.builtin_dir {
|
||||
discover_profile_dir(&mut registry, ProfileRegistrySource::Builtin, dir)?;
|
||||
}
|
||||
add_builtin_profiles(&mut registry);
|
||||
if let Some(path) = &self.user_config {
|
||||
load_profile_registry_file(&mut registry, ProfileRegistrySource::User, path)?;
|
||||
}
|
||||
|
|
@ -371,15 +414,27 @@ impl ProfileResolver {
|
|||
)),
|
||||
ProfileSelector::Named { .. } | ProfileSelector::Default => {
|
||||
let entry = registry.select(selector)?.clone();
|
||||
self.resolve_path(
|
||||
&entry.path,
|
||||
ProfileSource::Registry {
|
||||
source: entry.source,
|
||||
name: entry.name,
|
||||
path: absolutize(&entry.path)?,
|
||||
},
|
||||
options,
|
||||
)
|
||||
let source = ProfileSource::Registry {
|
||||
source: entry.source,
|
||||
name: entry.name.clone(),
|
||||
path: entry.path.as_deref().map(absolutize).transpose()?,
|
||||
provenance: (entry.path.is_none()).then(|| entry.provenance.clone()),
|
||||
};
|
||||
self.resolve_registry_entry(&entry, source, options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_registry_entry(
|
||||
&self,
|
||||
entry: &ProfileRegistryEntry,
|
||||
source: ProfileSource,
|
||||
options: ProfileResolveOptions,
|
||||
) -> Result<ResolvedProfile, ProfileError> {
|
||||
match &entry.artifact {
|
||||
ProfileRegistryArtifact::Path(path) => self.resolve_path(path, source, options),
|
||||
ProfileRegistryArtifact::Embedded { label, content } => {
|
||||
self.resolve_embedded_profile(label, content, source, options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -431,6 +486,30 @@ impl ProfileResolver {
|
|||
raw_artifact,
|
||||
)
|
||||
}
|
||||
|
||||
fn resolve_embedded_profile(
|
||||
&self,
|
||||
label: &'static str,
|
||||
content: &'static str,
|
||||
source: ProfileSource,
|
||||
options: ProfileResolveOptions,
|
||||
) -> Result<ResolvedProfile, ProfileError> {
|
||||
let workspace_base = absolutize(
|
||||
self.workspace_base
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| Path::new(".")),
|
||||
)?;
|
||||
let lua_value = evaluate_embedded_lua_profile(label, content)?;
|
||||
let raw_artifact = lua_value.clone();
|
||||
resolve_lua_profile_value(
|
||||
source,
|
||||
&workspace_base,
|
||||
&workspace_base,
|
||||
options,
|
||||
lua_value,
|
||||
raw_artifact,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_lua_profile_value(
|
||||
|
|
@ -591,13 +670,12 @@ fn load_profile_registry_file(
|
|||
let base = path.parent().unwrap_or_else(|| Path::new("."));
|
||||
for (name, entry_config) in config.profile {
|
||||
let (entry_path, description) = entry_config.into_parts();
|
||||
registry.push_entry(ProfileRegistryEntry {
|
||||
registry.push_entry(ProfileRegistryEntry::path(
|
||||
source,
|
||||
name,
|
||||
path: join_if_relative(base, &entry_path),
|
||||
join_if_relative(base, &entry_path),
|
||||
description,
|
||||
is_default: false,
|
||||
});
|
||||
));
|
||||
}
|
||||
if let Some(default) = config.default {
|
||||
let (default_source, default_name) = parse_profile_ref(&default);
|
||||
|
|
@ -625,49 +703,14 @@ fn find_project_profiles_from(start: &Path) -> Option<PathBuf> {
|
|||
None
|
||||
}
|
||||
|
||||
fn discover_profile_dir(
|
||||
registry: &mut ProfileRegistry,
|
||||
source: ProfileRegistrySource,
|
||||
dir: &Path,
|
||||
) -> Result<(), ProfileError> {
|
||||
if !dir.is_dir() {
|
||||
return Ok(());
|
||||
}
|
||||
for entry in std::fs::read_dir(dir).map_err(|source| ProfileError::ConfigRead {
|
||||
path: dir.to_path_buf(),
|
||||
source,
|
||||
})? {
|
||||
let entry = entry.map_err(|source| ProfileError::ConfigRead {
|
||||
path: dir.to_path_buf(),
|
||||
source,
|
||||
})?;
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("lua") {
|
||||
if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
|
||||
registry.push_entry(ProfileRegistryEntry {
|
||||
source,
|
||||
name: name.to_string(),
|
||||
path,
|
||||
description: None,
|
||||
is_default: false,
|
||||
});
|
||||
}
|
||||
} else if path.is_dir() {
|
||||
let profile = path.join("profile.lua");
|
||||
if profile.is_file()
|
||||
&& let Some(name) = path.file_name().and_then(|s| s.to_str())
|
||||
{
|
||||
registry.push_entry(ProfileRegistryEntry {
|
||||
source,
|
||||
name: name.to_string(),
|
||||
path: profile,
|
||||
description: None,
|
||||
is_default: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
fn add_builtin_profiles(registry: &mut ProfileRegistry) {
|
||||
registry.push_entry(ProfileRegistryEntry::embedded(
|
||||
ProfileRegistrySource::Builtin,
|
||||
BUILTIN_DEFAULT_PROFILE_NAME,
|
||||
"builtin:default",
|
||||
BUILTIN_DEFAULT_PROFILE,
|
||||
Some("Bundled default Insomnia coding profile".into()),
|
||||
));
|
||||
}
|
||||
|
||||
fn parse_profile_ref(raw: &str) -> (Option<ProfileRegistrySource>, String) {
|
||||
|
|
@ -687,15 +730,38 @@ fn evaluate_lua_profile(
|
|||
path: path.to_path_buf(),
|
||||
source,
|
||||
})?;
|
||||
evaluate_lua_profile_source(
|
||||
&content,
|
||||
path.display().to_string(),
|
||||
LocalModuleRoot::Filesystem(module_root.to_path_buf()),
|
||||
)
|
||||
}
|
||||
|
||||
fn evaluate_embedded_lua_profile(
|
||||
label: &'static str,
|
||||
content: &'static str,
|
||||
) -> Result<serde_json::Value, ProfileError> {
|
||||
evaluate_lua_profile_source(
|
||||
content,
|
||||
label.to_string(),
|
||||
LocalModuleRoot::Disabled { label },
|
||||
)
|
||||
}
|
||||
|
||||
fn evaluate_lua_profile_source(
|
||||
content: &str,
|
||||
chunk_name: String,
|
||||
module_root: LocalModuleRoot,
|
||||
) -> Result<serde_json::Value, ProfileError> {
|
||||
let lua = Lua::new_with(
|
||||
StdLib::TABLE | StdLib::STRING | StdLib::MATH | StdLib::UTF8,
|
||||
LuaOptions::default(),
|
||||
)
|
||||
.map_err(ProfileError::Lua)?;
|
||||
install_lua_api(&lua, module_root.to_path_buf())?;
|
||||
install_lua_api(&lua, module_root)?;
|
||||
let value: LuaValue = lua
|
||||
.load(&content)
|
||||
.set_name(path.display().to_string())
|
||||
.load(content)
|
||||
.set_name(chunk_name)
|
||||
.eval()
|
||||
.map_err(ProfileError::Lua)?;
|
||||
match value {
|
||||
|
|
@ -706,7 +772,7 @@ fn evaluate_lua_profile(
|
|||
}
|
||||
}
|
||||
|
||||
fn install_lua_api(lua: &Lua, module_root: PathBuf) -> Result<(), ProfileError> {
|
||||
fn install_lua_api(lua: &Lua, module_root: LocalModuleRoot) -> Result<(), ProfileError> {
|
||||
let loader = Rc::new(RefCell::new(LocalModuleLoader {
|
||||
root: module_root,
|
||||
cache: HashMap::new(),
|
||||
|
|
@ -743,11 +809,16 @@ fn install_lua_api(lua: &Lua, module_root: PathBuf) -> Result<(), ProfileError>
|
|||
}
|
||||
|
||||
struct LocalModuleLoader {
|
||||
root: PathBuf,
|
||||
root: LocalModuleRoot,
|
||||
cache: HashMap<String, RegistryKey>,
|
||||
loading: HashSet<String>,
|
||||
}
|
||||
|
||||
enum LocalModuleRoot {
|
||||
Filesystem(PathBuf),
|
||||
Disabled { label: &'static str },
|
||||
}
|
||||
|
||||
fn require_module(
|
||||
lua: &Lua,
|
||||
loader: &Rc<RefCell<LocalModuleLoader>>,
|
||||
|
|
@ -773,7 +844,19 @@ fn require_module(
|
|||
)));
|
||||
}
|
||||
}
|
||||
let path = local_module_path(&loader.borrow().root, name).map_err(mlua::Error::RuntimeError)?;
|
||||
let path = {
|
||||
let state = loader.borrow();
|
||||
match &state.root {
|
||||
LocalModuleRoot::Filesystem(root) => {
|
||||
local_module_path(root, name).map_err(mlua::Error::RuntimeError)?
|
||||
}
|
||||
LocalModuleRoot::Disabled { label } => {
|
||||
return Err(mlua::Error::RuntimeError(format!(
|
||||
"local require `{name}` is not available for embedded profile `{label}`"
|
||||
)));
|
||||
}
|
||||
}
|
||||
};
|
||||
let content = std::fs::read_to_string(&path).map_err(|e| {
|
||||
mlua::Error::RuntimeError(format!(
|
||||
"failed to read local module `{name}` ({}): {e}",
|
||||
|
|
@ -1042,9 +1125,7 @@ fn model_context_window(model: Option<&ModelManifest>) -> Option<u64> {
|
|||
}
|
||||
fn builtin_model_context_window(reference: &str) -> Option<u64> {
|
||||
let (provider, model_id) = reference.split_once('/')?;
|
||||
let path = paths::resource_dir()?.join("models").join("builtin.toml");
|
||||
let content = std::fs::read_to_string(path).ok()?;
|
||||
let parsed: toml::Value = toml::from_str(&content).ok()?;
|
||||
let parsed: toml::Value = toml::from_str(BUILTIN_MODEL_CATALOG).ok()?;
|
||||
for entry in parsed.get("model")?.as_array()? {
|
||||
let table = entry.as_table()?;
|
||||
if table.get("provider")?.as_str()? == provider && table.get("id")?.as_str()? == model_id {
|
||||
|
|
@ -1187,58 +1268,8 @@ pub enum ProfileError {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use crate::{ReasoningControl, ReasoningEffort, SchemeKind};
|
||||
use std::sync::{Mutex, MutexGuard, OnceLock};
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn env_lock() -> MutexGuard<'static, ()> {
|
||||
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
LOCK.get_or_init(|| Mutex::new(()))
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
struct EnvGuard {
|
||||
vars: Vec<(&'static str, Option<String>)>,
|
||||
_lock: MutexGuard<'static, ()>,
|
||||
}
|
||||
impl EnvGuard {
|
||||
fn new(overrides: &[(&'static str, Option<&str>)]) -> Self {
|
||||
let lock = env_lock();
|
||||
let names = [
|
||||
"INSOMNIA_CONFIG_DIR",
|
||||
"INSOMNIA_RESOURCE_DIR",
|
||||
"INSOMNIA_HOME",
|
||||
"XDG_CONFIG_HOME",
|
||||
"HOME",
|
||||
];
|
||||
let saved: Vec<_> = names.iter().map(|n| (*n, std::env::var(n).ok())).collect();
|
||||
unsafe {
|
||||
for (n, _) in &saved {
|
||||
std::env::remove_var(n);
|
||||
}
|
||||
for (n, v) in overrides {
|
||||
if let Some(v) = v {
|
||||
std::env::set_var(n, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
Self {
|
||||
vars: saved,
|
||||
_lock: lock,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Drop for EnvGuard {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
for (n, v) in &self.vars {
|
||||
match v {
|
||||
Some(v) => std::env::set_var(n, v),
|
||||
None => std::env::remove_var(n),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn write_profile(dir: &Path, name: &str, body: &str) -> PathBuf {
|
||||
let path = dir.join(name);
|
||||
std::fs::write(&path, body).unwrap();
|
||||
|
|
@ -1266,14 +1297,15 @@ mod tests {
|
|||
}
|
||||
#[test]
|
||||
fn builtin_default_profile_is_registered_as_default() {
|
||||
let registry = ProfileDiscovery::with_sources(paths::builtin_profiles_dir(), None, None)
|
||||
let registry = ProfileDiscovery::with_sources(None, None)
|
||||
.discover()
|
||||
.unwrap();
|
||||
let default = registry.default_entry().unwrap();
|
||||
assert_eq!(default.source, ProfileRegistrySource::Builtin);
|
||||
assert_eq!(default.name, BUILTIN_DEFAULT_PROFILE_NAME);
|
||||
assert!(default.is_default);
|
||||
assert!(default.path.ends_with("resources/profiles/default.lua"));
|
||||
assert_eq!(default.path, None);
|
||||
assert_eq!(default.provenance, "builtin:default");
|
||||
}
|
||||
#[test]
|
||||
fn resolves_plain_lua_profile_with_runtime_pod_name_and_scope_intent() {
|
||||
|
|
@ -1458,6 +1490,15 @@ return profile {
|
|||
resolved.profile.as_ref().unwrap().name.as_deref(),
|
||||
Some("default")
|
||||
);
|
||||
assert_eq!(
|
||||
resolved.source,
|
||||
ProfileSource::Registry {
|
||||
source: ProfileRegistrySource::Builtin,
|
||||
name: "default".into(),
|
||||
path: None,
|
||||
provenance: Some("builtin:default".into()),
|
||||
}
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn unsupported_profile_extension_has_clear_diagnostic() {
|
||||
|
|
@ -1486,14 +1527,19 @@ return profile {
|
|||
)
|
||||
.unwrap();
|
||||
std::fs::write(&project_config, "default = \"project:coder\"\n[profile.coder]\npath = \"profiles/project-coder.lua\"\ndescription = \"Project coder\"\n").unwrap();
|
||||
let registry =
|
||||
ProfileDiscovery::with_sources(None, Some(user_config), Some(project_config))
|
||||
.discover()
|
||||
.unwrap();
|
||||
let registry = ProfileDiscovery::with_sources(Some(user_config), Some(project_config))
|
||||
.discover()
|
||||
.unwrap();
|
||||
let default = registry.default_entry().unwrap();
|
||||
assert_eq!(default.source, ProfileRegistrySource::Project);
|
||||
assert_eq!(default.name, "coder");
|
||||
assert!(default.path.ends_with("profiles/project-coder.lua"));
|
||||
assert!(
|
||||
default
|
||||
.path
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.ends_with("profiles/project-coder.lua")
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn default_marks_direct_profile_entry() {
|
||||
|
|
@ -1506,7 +1552,7 @@ return profile {
|
|||
"default = \"coder\"\n[profile]\ncoder = \"profiles/coder.lua\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
let registry = ProfileDiscovery::with_sources(None, None, Some(project_config))
|
||||
let registry = ProfileDiscovery::with_sources(None, Some(project_config))
|
||||
.discover()
|
||||
.unwrap();
|
||||
let default = registry.default_entry().unwrap();
|
||||
|
|
@ -1525,20 +1571,18 @@ return profile {
|
|||
#[test]
|
||||
fn unqualified_ambiguous_names_fail_closed() {
|
||||
let mut registry = ProfileRegistry::default();
|
||||
registry.push_entry(ProfileRegistryEntry {
|
||||
source: ProfileRegistrySource::User,
|
||||
name: "coder".to_string(),
|
||||
path: PathBuf::from("/user/coder.lua"),
|
||||
description: None,
|
||||
is_default: false,
|
||||
});
|
||||
registry.push_entry(ProfileRegistryEntry {
|
||||
source: ProfileRegistrySource::Project,
|
||||
name: "coder".to_string(),
|
||||
path: PathBuf::from("/project/coder.lua"),
|
||||
description: None,
|
||||
is_default: false,
|
||||
});
|
||||
registry.push_entry(ProfileRegistryEntry::path(
|
||||
ProfileRegistrySource::User,
|
||||
"coder".to_string(),
|
||||
PathBuf::from("/user/coder.lua"),
|
||||
None,
|
||||
));
|
||||
registry.push_entry(ProfileRegistryEntry::path(
|
||||
ProfileRegistrySource::Project,
|
||||
"coder".to_string(),
|
||||
PathBuf::from("/project/coder.lua"),
|
||||
None,
|
||||
));
|
||||
let err = registry
|
||||
.select(&ProfileSelector::named("coder"))
|
||||
.unwrap_err();
|
||||
|
|
@ -1549,6 +1593,9 @@ return profile {
|
|||
"coder",
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(selected.path, PathBuf::from("/project/coder.lua"));
|
||||
assert_eq!(
|
||||
selected.path.as_deref(),
|
||||
Some(Path::new("/project/coder.lua"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
@ -346,7 +346,13 @@ where
|
|||
command.arg("--store").arg(store_dir);
|
||||
}
|
||||
|
||||
let mut child = command.spawn().map_err(PodDiscoveryError::RestoreSpawn)?;
|
||||
let mut child =
|
||||
command
|
||||
.spawn()
|
||||
.map_err(|source| PodDiscoveryError::RestoreLaunchFailed {
|
||||
command: runtime_command.clone(),
|
||||
source,
|
||||
})?;
|
||||
let deadline = tokio::time::Instant::now() + RESTORE_START_TIMEOUT;
|
||||
loop {
|
||||
if probe_socket(socket_path).await.reachable {
|
||||
|
|
@ -545,6 +551,12 @@ pub enum PodDiscoveryError {
|
|||
ScopeLock(#[from] pod_registry::ScopeLockError),
|
||||
#[error("failed to launch restore process: {0}")]
|
||||
RestoreSpawn(io::Error),
|
||||
#[error("failed to launch restore runtime command `{command}`: {source}")]
|
||||
RestoreLaunchFailed {
|
||||
command: PodRuntimeCommand,
|
||||
#[source]
|
||||
source: io::Error,
|
||||
},
|
||||
#[error("restore process exited before socket became reachable: {status}")]
|
||||
RestoreExited { status: std::process::ExitStatus },
|
||||
#[error("restore process did not become reachable before timeout")]
|
||||
|
|
@ -779,6 +791,7 @@ fn discovery_error_to_tool_error(error: PodDiscoveryError) -> ToolError {
|
|||
| PodDiscoveryError::PodStore(_)
|
||||
| PodDiscoveryError::ScopeLock(_)
|
||||
| PodDiscoveryError::RestoreSpawn(_)
|
||||
| PodDiscoveryError::RestoreLaunchFailed { .. }
|
||||
| PodDiscoveryError::RestoreExited { .. }
|
||||
| PodDiscoveryError::RestoreTimeout => ToolError::ExecutionFailed(error.to_string()),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -220,6 +220,9 @@ pub struct SpawnPodTool {
|
|||
/// Directory the spawned Pod should run in when the LLM did not
|
||||
/// override it. Defaults to the spawner's pwd — see module docs.
|
||||
spawner_pwd: PathBuf,
|
||||
/// Optional typed runtime command injected by tests. Production resolves
|
||||
/// the runtime command from `std::env::current_exe()` at launch time.
|
||||
runtime_command: Option<PodRuntimeCommand>,
|
||||
/// Shared registry of spawned children, also used by the
|
||||
/// pod-comm tools (`SendToPod` / `ReadPodOutput` / `StopPod`) and by
|
||||
/// Pod discovery. Writes the list to runtime and durable Pod state on
|
||||
|
|
@ -258,12 +261,14 @@ impl SpawnPodTool {
|
|||
spawner_manifest: PodManifest,
|
||||
available_profiles: AvailableProfiles,
|
||||
spawner_scope: SharedScope,
|
||||
runtime_command: Option<PodRuntimeCommand>,
|
||||
) -> Self {
|
||||
Self {
|
||||
spawner_name,
|
||||
callback_socket,
|
||||
runtime_base,
|
||||
spawner_pwd,
|
||||
runtime_command,
|
||||
registry,
|
||||
parent_socket,
|
||||
spawner_manifest,
|
||||
|
|
@ -409,9 +414,14 @@ impl SpawnPodTool {
|
|||
spawn_config_json: &str,
|
||||
predicted_socket: &Path,
|
||||
) -> Result<(), ToolError> {
|
||||
let runtime_command = PodRuntimeCommand::resolve().map_err(|error| {
|
||||
ToolError::ExecutionFailed(format!("failed to resolve Pod runtime command: {error}"))
|
||||
})?;
|
||||
let runtime_command = match &self.runtime_command {
|
||||
Some(command) => command.clone(),
|
||||
None => PodRuntimeCommand::resolve().map_err(|error| {
|
||||
ToolError::ExecutionFailed(format!(
|
||||
"failed to resolve Pod runtime command: {error}"
|
||||
))
|
||||
})?,
|
||||
};
|
||||
|
||||
// Pre-create the child's runtime dir so we have a stable place to
|
||||
// capture its stderr before it has had a chance to bind anything.
|
||||
|
|
@ -764,6 +774,59 @@ pub fn spawn_pod_tool(
|
|||
spawner_manifest: PodManifest,
|
||||
spawner_scope: SharedScope,
|
||||
prompts: Arc<PromptCatalog>,
|
||||
) -> ToolDefinition {
|
||||
spawn_pod_tool_impl(
|
||||
spawner_name,
|
||||
callback_socket,
|
||||
runtime_base,
|
||||
spawner_pwd,
|
||||
registry,
|
||||
parent_socket,
|
||||
spawner_manifest,
|
||||
spawner_scope,
|
||||
prompts,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn spawn_pod_tool_with_runtime_command(
|
||||
spawner_name: String,
|
||||
callback_socket: PathBuf,
|
||||
runtime_base: PathBuf,
|
||||
spawner_pwd: PathBuf,
|
||||
registry: Arc<SpawnedPodRegistry>,
|
||||
parent_socket: Option<PathBuf>,
|
||||
spawner_manifest: PodManifest,
|
||||
spawner_scope: SharedScope,
|
||||
prompts: Arc<PromptCatalog>,
|
||||
runtime_command: PodRuntimeCommand,
|
||||
) -> ToolDefinition {
|
||||
spawn_pod_tool_impl(
|
||||
spawner_name,
|
||||
callback_socket,
|
||||
runtime_base,
|
||||
spawner_pwd,
|
||||
registry,
|
||||
parent_socket,
|
||||
spawner_manifest,
|
||||
spawner_scope,
|
||||
prompts,
|
||||
Some(runtime_command),
|
||||
)
|
||||
}
|
||||
|
||||
fn spawn_pod_tool_impl(
|
||||
spawner_name: String,
|
||||
callback_socket: PathBuf,
|
||||
runtime_base: PathBuf,
|
||||
spawner_pwd: PathBuf,
|
||||
registry: Arc<SpawnedPodRegistry>,
|
||||
parent_socket: Option<PathBuf>,
|
||||
spawner_manifest: PodManifest,
|
||||
spawner_scope: SharedScope,
|
||||
prompts: Arc<PromptCatalog>,
|
||||
runtime_command: Option<PodRuntimeCommand>,
|
||||
) -> ToolDefinition {
|
||||
Arc::new(move || {
|
||||
let schema = schemars::schema_for!(SpawnPodInput);
|
||||
|
|
@ -794,6 +857,7 @@ pub fn spawn_pod_tool(
|
|||
spawner_manifest.clone(),
|
||||
available_profiles,
|
||||
spawner_scope.clone(),
|
||||
runtime_command.clone(),
|
||||
));
|
||||
(meta, tool)
|
||||
})
|
||||
|
|
@ -868,7 +932,7 @@ mod tests {
|
|||
std::fs::write(®istry_path, registry_toml).unwrap();
|
||||
AvailableProfiles {
|
||||
registry: Some(
|
||||
ProfileDiscovery::with_sources(None, None, Some(registry_path))
|
||||
ProfileDiscovery::with_sources(None, Some(registry_path))
|
||||
.discover()
|
||||
.unwrap(),
|
||||
),
|
||||
|
|
@ -1236,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,
|
||||
|
|
@ -1245,17 +1309,15 @@ 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();
|
||||
let project_config = project.join(".insomnia/profiles.toml");
|
||||
let ambiguous = AvailableProfiles {
|
||||
registry: Some(
|
||||
ProfileDiscovery::with_sources(None, Some(user_config), Some(project_config))
|
||||
ProfileDiscovery::with_sources(Some(user_config), Some(project_config))
|
||||
.discover()
|
||||
.unwrap(),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
//! Integration tests for the `SpawnPod` tool.
|
||||
//!
|
||||
//! These tests exercise the tool's pod-registry delegation, subprocess
|
||||
//! launch, socket handoff, and `spawned_pods.json` write without relying
|
||||
//! on the real Pod runtime executable. `INSOMNIA_POD_COMMAND` is pointed at
|
||||
//! `/bin/true` (which exits immediately) while a test-owned Unix
|
||||
//! listener pre-binds the predicted socket path, so the tool sees the
|
||||
//! "child" as live.
|
||||
//! launch, socket handoff, and `spawned_pods.json` write through an injected
|
||||
//! typed runtime command. The mock command exits immediately while a
|
||||
//! test-owned Unix listener pre-binds the predicted socket path, so the tool
|
||||
//! sees the "child" as live.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
|
||||
use client::PodRuntimeCommand;
|
||||
use llm_worker::tool::{ToolError, ToolOutput};
|
||||
use manifest::{
|
||||
AuthRef, ModelManifest, Permission, PodManifest, PodManifestConfig, PodMetaConfig, SchemeKind,
|
||||
|
|
@ -18,7 +18,7 @@ use manifest::{
|
|||
use pod::runtime::dir::{RuntimeDir, SpawnedPodRecord};
|
||||
use pod::runtime::pod_registry::{self, LockFileGuard};
|
||||
use pod::spawn::registry::SpawnedPodRegistry;
|
||||
use pod::spawn::tool::spawn_pod_tool;
|
||||
use pod::spawn::tool::spawn_pod_tool_with_runtime_command;
|
||||
use protocol::stream::{JsonLineReader, JsonLineWriter};
|
||||
use protocol::{Event, Method};
|
||||
use serde_json::json;
|
||||
|
|
@ -26,8 +26,8 @@ use std::sync::Arc;
|
|||
use tempfile::TempDir;
|
||||
use tokio::net::UnixListener;
|
||||
|
||||
/// Serialises tests that mutate `INSOMNIA_RUNTIME_DIR` /
|
||||
/// `INSOMNIA_POD_COMMAND` across the thread-pooled test harness.
|
||||
/// Serialises tests that mutate `INSOMNIA_RUNTIME_DIR` across the
|
||||
/// thread-pooled test harness.
|
||||
static ENV_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
|
||||
|
||||
struct EnvGuard {
|
||||
|
|
@ -141,11 +141,8 @@ fn accept_one_method(listener: UnixListener) -> tokio::task::JoinHandle<Option<M
|
|||
})
|
||||
}
|
||||
|
||||
fn point_runtime_command_at_true() {
|
||||
let path = which_true();
|
||||
unsafe {
|
||||
std::env::set_var("INSOMNIA_POD_COMMAND", &path);
|
||||
}
|
||||
fn mock_runtime_command() -> PodRuntimeCommand {
|
||||
PodRuntimeCommand::new(which_true(), Vec::new())
|
||||
}
|
||||
|
||||
/// `/bin/true` only exists on FHS-compliant systems. Resolve it via PATH
|
||||
|
|
@ -213,7 +210,6 @@ fn shared_scope_for(allow_root: &Path) -> SharedScope {
|
|||
fn clear_env() {
|
||||
unsafe {
|
||||
std::env::remove_var("INSOMNIA_RUNTIME_DIR");
|
||||
std::env::remove_var("INSOMNIA_POD_COMMAND");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -224,14 +220,13 @@ async fn spawn_pod_delegates_scope_and_sends_run() {
|
|||
let allow_root = TempDir::new().unwrap();
|
||||
let (_tmp, runtime_base, spawner_socket, spawner_rd) =
|
||||
setup_spawner("root", allow_root.path()).await;
|
||||
point_runtime_command_at_true();
|
||||
|
||||
let (_predicted_socket, listener) = bind_mock_pod_socket(&runtime_base, "child").await;
|
||||
let received = accept_one_method(listener);
|
||||
|
||||
let registry = SpawnedPodRegistry::new(spawner_rd.clone());
|
||||
let spawner_scope = shared_scope_for(allow_root.path());
|
||||
let def = spawn_pod_tool(
|
||||
let def = spawn_pod_tool_with_runtime_command(
|
||||
"root".into(),
|
||||
spawner_socket.clone(),
|
||||
runtime_base.clone(),
|
||||
|
|
@ -241,6 +236,7 @@ async fn spawn_pod_delegates_scope_and_sends_run() {
|
|||
dummy_manifest(allow_root.path()),
|
||||
spawner_scope.clone(),
|
||||
builtin_prompts(),
|
||||
mock_runtime_command(),
|
||||
);
|
||||
let (_meta, tool) = def();
|
||||
|
||||
|
|
@ -317,11 +313,10 @@ async fn spawn_pod_rejects_scope_outside_spawner() {
|
|||
let outside = TempDir::new().unwrap();
|
||||
let (_tmp, runtime_base, spawner_socket, spawner_rd) =
|
||||
setup_spawner("root", allow_root.path()).await;
|
||||
point_runtime_command_at_true();
|
||||
|
||||
let registry = SpawnedPodRegistry::new(spawner_rd);
|
||||
let spawner_scope = shared_scope_for(allow_root.path());
|
||||
let def = spawn_pod_tool(
|
||||
let def = spawn_pod_tool_with_runtime_command(
|
||||
"root".into(),
|
||||
spawner_socket,
|
||||
runtime_base,
|
||||
|
|
@ -331,6 +326,7 @@ async fn spawn_pod_rejects_scope_outside_spawner() {
|
|||
dummy_manifest(allow_root.path()),
|
||||
spawner_scope.clone(),
|
||||
builtin_prompts(),
|
||||
mock_runtime_command(),
|
||||
);
|
||||
let (_meta, tool) = def();
|
||||
|
||||
|
|
@ -379,7 +375,6 @@ async fn spawn_pod_rolls_back_reservation_when_socket_never_appears() {
|
|||
let allow_root = TempDir::new().unwrap();
|
||||
let (_tmp, runtime_base, spawner_socket, spawner_rd) =
|
||||
setup_spawner("root", allow_root.path()).await;
|
||||
point_runtime_command_at_true();
|
||||
|
||||
// Deliberately do NOT bind a socket at the predicted path. The
|
||||
// tool's wait_for_socket should time out, triggering rollback.
|
||||
|
|
@ -394,7 +389,7 @@ async fn spawn_pod_rolls_back_reservation_when_socket_never_appears() {
|
|||
|
||||
let registry = SpawnedPodRegistry::new(spawner_rd);
|
||||
let spawner_scope = shared_scope_for(allow_root.path());
|
||||
let def = spawn_pod_tool(
|
||||
let def = spawn_pod_tool_with_runtime_command(
|
||||
"root".into(),
|
||||
spawner_socket,
|
||||
runtime_base,
|
||||
|
|
@ -404,6 +399,7 @@ async fn spawn_pod_rolls_back_reservation_when_socket_never_appears() {
|
|||
dummy_manifest(allow_root.path()),
|
||||
spawner_scope.clone(),
|
||||
builtin_prompts(),
|
||||
mock_runtime_command(),
|
||||
);
|
||||
let (_meta, tool) = def();
|
||||
|
||||
|
|
|
|||
|
|
@ -234,6 +234,17 @@ async fn brave_search(
|
|||
)));
|
||||
}
|
||||
|
||||
brave_search_with_api_key(client, cfg, &api_key, query, limit, offset).await
|
||||
}
|
||||
|
||||
async fn brave_search_with_api_key(
|
||||
client: &Client,
|
||||
cfg: &WebSearchConfig,
|
||||
api_key: &str,
|
||||
query: &str,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let endpoint = cfg.base_url.as_deref().unwrap_or(BRAVE_SEARCH_ENDPOINT);
|
||||
let mut url = Url::parse(endpoint).map_err(|err| {
|
||||
ToolError::InvalidArgument(format!("invalid Brave search endpoint: {err}"))
|
||||
|
|
@ -1694,6 +1705,17 @@ mod tests {
|
|||
}))
|
||||
}
|
||||
|
||||
fn brave_search_config(base_url: String) -> WebSearchConfig {
|
||||
WebSearchConfig {
|
||||
enabled: Some(true),
|
||||
provider: Some(WebSearchProvider::Brave),
|
||||
api_key_env: None,
|
||||
timeout_secs: Some(2),
|
||||
base_url: Some(base_url),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_brave_query_limits() {
|
||||
validate_brave_query("hello world").unwrap();
|
||||
|
|
@ -2019,30 +2041,16 @@ mod tests {
|
|||
async fn searches_brave_with_bounded_output() {
|
||||
let response = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n{\"web\":{\"results\":[{\"title\":\"Example\",\"url\":\"https://example.com\",\"description\":\"Snippet\",\"extra_snippets\":[\"Extra\"],\"language\":\"en\"}]}}";
|
||||
let (addr, captured) = serve_once_capture(response).await;
|
||||
let env_name = format!("INSOMNIA_TEST_BRAVE_KEY_{}", std::process::id());
|
||||
unsafe { std::env::set_var(&env_name, "test-key") };
|
||||
let tools = WebTools::new(Some(WebConfig {
|
||||
enabled: Some(true),
|
||||
allow_private_addresses: Some(true),
|
||||
search: Some(WebSearchConfig {
|
||||
enabled: Some(true),
|
||||
provider: Some(WebSearchProvider::Brave),
|
||||
api_key_env: Some(env_name.clone()),
|
||||
timeout_secs: Some(2),
|
||||
base_url: Some(format!("http://{addr}/search")),
|
||||
..Default::default()
|
||||
}),
|
||||
search: None,
|
||||
fetch: None,
|
||||
}));
|
||||
let result = tools
|
||||
.run_search(WebSearchInput {
|
||||
query: "insomnia".into(),
|
||||
limit: Some(1),
|
||||
offset: Some(0),
|
||||
})
|
||||
let cfg = brave_search_config(format!("http://{addr}/search"));
|
||||
let result = brave_search_with_api_key(&tools.client, &cfg, "test-key", "insomnia", 1, 0)
|
||||
.await
|
||||
.unwrap();
|
||||
unsafe { std::env::remove_var(&env_name) };
|
||||
let value: Value = serde_json::from_str(result.content.as_deref().unwrap()).unwrap();
|
||||
let request = captured.lock().await.clone().unwrap();
|
||||
assert!(request.starts_with("GET /search?q=insomnia&count=1&offset=0 "));
|
||||
|
|
@ -2065,29 +2073,16 @@ mod tests {
|
|||
);
|
||||
let response: &'static str = Box::leak(response.into_boxed_str());
|
||||
let addr = serve_once(response).await;
|
||||
let env_name = format!("INSOMNIA_TEST_BRAVE_OVERSIZED_KEY_{}", std::process::id());
|
||||
unsafe { std::env::set_var(&env_name, "test-key") };
|
||||
let tools = WebTools::new(Some(WebConfig {
|
||||
enabled: Some(true),
|
||||
allow_private_addresses: Some(true),
|
||||
search: Some(WebSearchConfig {
|
||||
enabled: Some(true),
|
||||
provider: Some(WebSearchProvider::Brave),
|
||||
api_key_env: Some(env_name.clone()),
|
||||
base_url: Some(format!("http://{addr}/search")),
|
||||
..Default::default()
|
||||
}),
|
||||
search: None,
|
||||
fetch: None,
|
||||
}));
|
||||
let err = tools
|
||||
.run_search(WebSearchInput {
|
||||
query: "insomnia".into(),
|
||||
limit: Some(1),
|
||||
offset: Some(0),
|
||||
})
|
||||
let cfg = brave_search_config(format!("http://{addr}/search"));
|
||||
let err = brave_search_with_api_key(&tools.client, &cfg, "test-key", "insomnia", 1, 0)
|
||||
.await
|
||||
.unwrap_err();
|
||||
unsafe { std::env::remove_var(&env_name) };
|
||||
assert!(err.to_string().contains("Content-Length"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ use crate::command::{
|
|||
use crate::input::InputBuffer;
|
||||
use crate::scroll::Scroll;
|
||||
use crate::task::TaskStore;
|
||||
use crate::ui::Mode;
|
||||
use crate::view_mode::Mode;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CommandCompletionApply {
|
||||
|
|
|
|||
117
crates/tui/src/lib.rs
Normal file
117
crates/tui/src/lib.rs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
mod app;
|
||||
mod block;
|
||||
mod cache;
|
||||
mod command;
|
||||
mod input;
|
||||
mod markdown;
|
||||
mod multi_pod;
|
||||
mod picker;
|
||||
mod pod_list;
|
||||
mod scroll;
|
||||
mod single_pod;
|
||||
mod spawn;
|
||||
mod task;
|
||||
mod tool;
|
||||
mod ui;
|
||||
mod view_mode;
|
||||
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::process::ExitCode;
|
||||
|
||||
use crossterm::event::{DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste};
|
||||
use crossterm::execute;
|
||||
use crossterm::terminal::{LeaveAlternateScreen, disable_raw_mode, enable_raw_mode};
|
||||
use session_store::SegmentId;
|
||||
|
||||
use client::PodRuntimeCommand;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LaunchOptions {
|
||||
pub mode: LaunchMode,
|
||||
pub runtime_command: PodRuntimeCommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum LaunchMode {
|
||||
Spawn {
|
||||
profile: Option<String>,
|
||||
},
|
||||
/// `insomnia <name>` / `insomnia --pod <name>`: attach to a live Pod by name if
|
||||
/// possible; otherwise launch the Pod runtime command with `--pod <name>` so it
|
||||
/// resumes from name-keyed state or creates a fresh same-name Pod.
|
||||
PodName {
|
||||
pod_name: String,
|
||||
socket_override: Option<PathBuf>,
|
||||
},
|
||||
/// `insomnia -r` / `insomnia --resume`: open the Pod picker, then attach to the
|
||||
/// selected live Pod or restore the selected stopped Pod by name.
|
||||
Resume,
|
||||
/// `insomnia --session <UUID>`: skip the picker, go straight to the
|
||||
/// resume name dialog with `id` baked in.
|
||||
ResumeWithSession(SegmentId),
|
||||
/// `insomnia --multi`: open the multi-Pod dashboard. This is intentionally
|
||||
/// separate from `-r`/`--resume`, which keeps its single-Pod picker
|
||||
/// meaning.
|
||||
Multi,
|
||||
}
|
||||
|
||||
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}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
if let Err(e) = execute!(io::stdout(), EnableBracketedPaste) {
|
||||
let _ = disable_raw_mode();
|
||||
eprintln!("insomnia: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
let result = match mode {
|
||||
LaunchMode::Spawn { profile } => {
|
||||
single_pod::run_spawn(None, profile, runtime_command).await
|
||||
}
|
||||
LaunchMode::PodName {
|
||||
pod_name,
|
||||
socket_override,
|
||||
} => single_pod::run_pod_name(pod_name, socket_override, runtime_command).await,
|
||||
LaunchMode::Resume => single_pod::run_resume(runtime_command).await,
|
||||
LaunchMode::ResumeWithSession(id) => {
|
||||
single_pod::run_spawn(Some(id), None, runtime_command).await
|
||||
}
|
||||
LaunchMode::Multi => single_pod::run_multi(runtime_command).await,
|
||||
};
|
||||
|
||||
// Always restore the terminal first so any pending eprintln below
|
||||
// shows up cleanly in scrollback rather than inside an active
|
||||
// alternate-screen buffer.
|
||||
let mut stdout = io::stdout();
|
||||
let _ = execute!(
|
||||
stdout,
|
||||
DisableMouseCapture,
|
||||
LeaveAlternateScreen,
|
||||
DisableBracketedPaste
|
||||
);
|
||||
let _ = disable_raw_mode();
|
||||
let _ = execute!(stdout, crossterm::cursor::Show);
|
||||
|
||||
match result {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
// SpawnError has already been painted into the inline
|
||||
// viewport's final frame, so it's already visible in the
|
||||
// user's scrollback — printing it again would be a noisy
|
||||
// duplicate. Other errors (pod-name failures, terminal setup
|
||||
// hiccups, etc.) need surfacing here.
|
||||
if e.downcast_ref::<spawn::SpawnError>().is_none() {
|
||||
eprintln!("insomnia: {e}");
|
||||
}
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,6 @@
|
|||
mod app;
|
||||
mod block;
|
||||
mod cache;
|
||||
mod command;
|
||||
mod input;
|
||||
mod markdown;
|
||||
mod memory_lint;
|
||||
mod multi_pod;
|
||||
mod picker;
|
||||
mod pod_list;
|
||||
mod scroll;
|
||||
mod spawn;
|
||||
mod task;
|
||||
mod tool;
|
||||
mod ui;
|
||||
|
||||
use std::future::Future;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::process::ExitCode;
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
|
|
@ -26,24 +9,23 @@ use std::thread;
|
|||
use std::time::Duration;
|
||||
|
||||
use crossterm::event::{
|
||||
self, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
|
||||
Event as TermEvent, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind,
|
||||
self, DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyEvent,
|
||||
KeyModifiers, MouseEvent, MouseEventKind,
|
||||
};
|
||||
use crossterm::execute;
|
||||
use crossterm::terminal::{
|
||||
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
|
||||
};
|
||||
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
|
||||
use protocol::{Method, PodStatus};
|
||||
use ratatui::Terminal;
|
||||
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;
|
||||
use crate::spawn::{SpawnOutcome, SpawnReady};
|
||||
use crate::{multi_pod, picker, spawn, ui};
|
||||
|
||||
type FullscreenTerminal = Terminal<CrosstermBackend<io::Stdout>>;
|
||||
|
||||
|
|
@ -59,296 +41,10 @@ fn resolve_socket(pod_name: &str, override_path: Option<PathBuf>) -> PathBuf {
|
|||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Mode {
|
||||
Spawn {
|
||||
profile: Option<String>,
|
||||
},
|
||||
/// `insomnia <name>` / `insomnia --pod <name>`: attach to a live Pod by name if
|
||||
/// possible; otherwise launch the Pod runtime command with `--pod <name>` so it
|
||||
/// resumes from name-keyed state or creates a fresh same-name Pod.
|
||||
PodName {
|
||||
pod_name: String,
|
||||
socket_override: Option<PathBuf>,
|
||||
},
|
||||
/// `insomnia -r` / `insomnia --resume`: open the Pod picker, then attach to the
|
||||
/// selected live Pod or restore the selected stopped Pod by name.
|
||||
Resume,
|
||||
/// `insomnia --session <UUID>`: skip the picker, go straight to the
|
||||
/// resume name dialog with `id` baked in.
|
||||
ResumeWithSession(SegmentId),
|
||||
/// `insomnia --multi`: open the multi-Pod dashboard. This is intentionally
|
||||
/// 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;
|
||||
}
|
||||
|
||||
if let Err(e) = enable_raw_mode() {
|
||||
eprintln!("insomnia: failed to enter raw mode: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
if let Err(e) = execute!(io::stdout(), EnableBracketedPaste) {
|
||||
let _ = disable_raw_mode();
|
||||
eprintln!("insomnia: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
let result = match mode {
|
||||
Mode::Spawn { profile } => run_spawn(None, profile).await,
|
||||
Mode::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"),
|
||||
};
|
||||
|
||||
// Always restore the terminal first so any pending eprintln below
|
||||
// shows up cleanly in scrollback rather than inside an active
|
||||
// alternate-screen buffer.
|
||||
let mut stdout = io::stdout();
|
||||
let _ = execute!(
|
||||
stdout,
|
||||
DisableMouseCapture,
|
||||
LeaveAlternateScreen,
|
||||
DisableBracketedPaste
|
||||
);
|
||||
let _ = disable_raw_mode();
|
||||
let _ = execute!(stdout, crossterm::cursor::Show);
|
||||
|
||||
match result {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
// SpawnError has already been painted into the inline
|
||||
// viewport's final frame, so it's already visible in the
|
||||
// user's scrollback — printing it again would be a noisy
|
||||
// duplicate. Other errors (pod-name failures, terminal setup
|
||||
// hiccups, etc.) need surfacing here.
|
||||
if e.downcast_ref::<spawn::SpawnError>().is_none() {
|
||||
eprintln!("insomnia: {e}");
|
||||
}
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_pod_name(
|
||||
pub(crate) 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 +52,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 +76,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 +87,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 +161,9 @@ async fn connect_live_pod(
|
|||
.map(|client| (registry_socket, client))
|
||||
}
|
||||
|
||||
async fn run_resume() -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub(crate) 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 +173,12 @@ 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>> {
|
||||
pub(crate) 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 +190,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()));
|
||||
|
|
@ -508,11 +210,12 @@ fn is_recoverable_multi_open_error(error: &(dyn std::error::Error + 'static)) ->
|
|||
error.is::<spawn::SpawnError>() || error.is::<NestedOpenCancelled>()
|
||||
}
|
||||
|
||||
async fn run_spawn(
|
||||
pub(crate) 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 +881,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");
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
|||
|
||||
use crate::block::{Block, ToolCallBlock, ToolCallState};
|
||||
use crate::cache::FileCache;
|
||||
use crate::ui::Mode;
|
||||
use crate::view_mode::Mode;
|
||||
|
||||
/// Maximum body lines in normal mode for tool output previews.
|
||||
const NORMAL_MAX_BODY: usize = 5;
|
||||
|
|
|
|||
|
|
@ -31,36 +31,7 @@ use crate::app::{ActionbarNoticeLevel, App, CompletionState, alert_source_label,
|
|||
use crate::block::{Block, CompactEvent, ThinkingBlock, ThinkingState};
|
||||
use crate::command::CommandCandidate;
|
||||
use crate::task::{TaskCounts, TaskEntry, TaskStatus, TaskStore};
|
||||
|
||||
/// Display density for the history view.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Mode {
|
||||
/// Every block fully expanded.
|
||||
Detail,
|
||||
/// Completed blocks compressed to roughly 5–6 lines; in-progress
|
||||
/// tool blocks stay in detail.
|
||||
Normal,
|
||||
/// Each block rendered as a single line.
|
||||
Overview,
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
pub fn cycle(self) -> Self {
|
||||
match self {
|
||||
Mode::Detail => Mode::Normal,
|
||||
Mode::Normal => Mode::Overview,
|
||||
Mode::Overview => Mode::Detail,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Mode::Detail => "detail",
|
||||
Mode::Normal => "normal",
|
||||
Mode::Overview => "overview",
|
||||
}
|
||||
}
|
||||
}
|
||||
use crate::view_mode::Mode;
|
||||
|
||||
pub fn draw(frame: &mut Frame, app: &mut App) {
|
||||
let area = frame.area();
|
||||
|
|
|
|||
29
crates/tui/src/view_mode.rs
Normal file
29
crates/tui/src/view_mode.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/// Display density for the history view.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Mode {
|
||||
/// Every block fully expanded.
|
||||
Detail,
|
||||
/// Completed blocks compressed to roughly 5–6 lines; in-progress
|
||||
/// tool blocks stay in detail.
|
||||
Normal,
|
||||
/// Each block rendered as a single line.
|
||||
Overview,
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
pub fn cycle(self) -> Self {
|
||||
match self {
|
||||
Mode::Detail => Mode::Normal,
|
||||
Mode::Normal => Mode::Overview,
|
||||
Mode::Overview => Mode::Detail,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Mode::Detail => "detail",
|
||||
Mode::Normal => "normal",
|
||||
Mode::Overview => "overview",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
INSOMNIA では、プロセス境界で本当に必要な場合を除き、環境変数の利用を避ける。新しい ambient な入力を増やすより、明示的な profile / manifest / config file / typed secret reference / CLI argument を優先する。
|
||||
|
||||
それでも、path discovery、runtime directory、package resource lookup、外部 provider の credential 慣習との移行互換のために、一部の環境変数はまだサポートしている。この文書に載せた環境変数は公開 surface として扱う。ただし、fallback 変数は独立した設定項目ではなく、対応する main key の解決順の一部として扱う。開発・テスト都合だけの環境変数は原則として追加せず、既存のものも削除する。
|
||||
それでも、path discovery、runtime directory、外部 provider の credential 慣習との移行互換のために、一部の環境変数はまだサポートしている。この文書に載せた通常 runtime 用の環境変数は公開 surface として扱う。ただし、fallback 変数は独立した設定項目ではなく、対応する main key の解決順の一部として扱う。開発・テスト都合だけの環境変数は、通常ユーザー向け configuration として扱わない明確な escape hatch に限る。
|
||||
|
||||
## 原則
|
||||
|
||||
|
|
@ -23,7 +23,6 @@ Path 系の環境変数は論理的な key ごとに立項する。`XDG_*` や `
|
|||
| `config_dir` | `INSOMNIA_CONFIG_DIR` | `$INSOMNIA_HOME/config` → `$XDG_CONFIG_HOME/insomnia` → `$HOME/.config/insomnia` | 人が書く設定・override の置き場。`profiles.toml`、prompt override、model/provider override など。 |
|
||||
| `data_dir` | `INSOMNIA_DATA_DIR` | `$INSOMNIA_HOME` → `$HOME/.insomnia` | プログラムが書く永続データの置き場。session log、Pod metadata など、再起動後も restore / replay の根拠になるもの。通常ユーザー向けの primary knob ではなく、migration、test、isolated data store 用の advanced override。 |
|
||||
| `runtime_dir` | `INSOMNIA_RUNTIME_DIR` | `$INSOMNIA_HOME/run` → `$XDG_RUNTIME_DIR/insomnia` → `$HOME/.insomnia/run` | socket、pid/status file、live registry mirror など、再起動で捨ててよい runtime state の置き場。 |
|
||||
| `resource_dir` | `INSOMNIA_RESOURCE_DIR` | installed executable から見た `share/insomnia/resources` → build tree の `resources/` | bundled prompts、builtin profiles、provider/model catalog など、package が所有する immutable-ish な builtin asset の置き場。通常 user configuration ではなく、packaging / development / debug 用の override。 |
|
||||
|
||||
空の path 環境変数は、`manifest::paths` では原則として unset 相当に扱う。
|
||||
|
||||
|
|
@ -35,13 +34,9 @@ Path 系の環境変数は論理的な key ごとに立項する。`XDG_*` や `
|
|||
|
||||
このため、socket や pid file を `data_dir` に置かない。永続データと揮発 runtime state は分ける。
|
||||
|
||||
### `resource_dir` と `config_dir` の違い
|
||||
### Builtin assets と `config_dir`
|
||||
|
||||
`resource_dir` は package-owned builtin asset の場所である。installed package では `share/insomnia/resources` に置かれ、binary version と対応する。ユーザーが普段編集する場所ではない。
|
||||
|
||||
`config_dir` は user/project-owned override の場所である。`profiles.toml` や prompt/model/provider override はここに置き、package update で上書きされない。
|
||||
|
||||
つまり、builtin fallback は `resource_dir`、user override は `config_dir` で扱う。`INSOMNIA_RESOURCE_DIR` を user configuration の代わりに使わない。
|
||||
Builtin profiles and catalogs are embedded in the binary at build time. User/project-owned overrides remain under `config_dir` and project `.insomnia/` files such as `profiles.toml`; package runtime resource lookup is not a supported configuration surface.
|
||||
|
||||
## Credential と外部 auth
|
||||
|
||||
|
|
@ -61,9 +56,17 @@ Provider credential は、現在は manifest / profile / catalog の設定から
|
|||
|
||||
Credential env var は interoperability のために現時点では残っているが、長期的に望ましい secret mechanism ではない。現時点では適切なら `auth.file` を優先し、今後は typed secret reference へ寄せる。credential UX のために implicit `.env` loading を追加しないこと。project secret を漏らしやすく、profile ごとの credential model とも相性が悪い。
|
||||
|
||||
## Build / example / test 変数
|
||||
## Development-only escape hatches
|
||||
|
||||
これらは通常の application configuration ではない。test-only の user-facing env var は supported surface として立てず、既存の `INSOMNIA_TEST_*` も削除する。test が public env behavior を検証する必要がある場合だけ、shared guard / test-support crate で process environment mutation を閉じ込める。
|
||||
これらは dogfooding / self-rebuild / fixture などの開発運用だけの逃げ道であり、通常ユーザー向けの configuration surface ではない。profile、manifest、CLI option の代替として案内しない。
|
||||
|
||||
| 変数 | Context | 備考 |
|
||||
| --- | --- | --- |
|
||||
| `INSOMNIA_POD_RUNTIME_COMMAND` | 開発中に起動中の `insomnia` binary が rebuild され、`std::env::current_exe()` が `target/debug/insomnia (deleted)` のような stale path を返す場合の Pod runtime executable override。 | Unset または empty の場合は既定どおり current executable に `pod` prefix argument を付けて起動する。Non-empty の場合は値を executable path としてそのまま使い、`pod` prefix argument は常に自動追加する。shell parsing や argument splitting は行わないため、値に flags や `pod` を含めない。 |
|
||||
|
||||
## Build / example variables
|
||||
|
||||
これらは通常の application configuration ではない。
|
||||
|
||||
| 変数 | Context | 備考 |
|
||||
| --- | --- | --- |
|
||||
|
|
@ -73,23 +76,14 @@ Credential env var は interoperability のために現時点では残ってい
|
|||
| `RUST_LOG` | example / dev diagnostics。 | example CLI が tracing setup 経由で読む場合がある。 |
|
||||
| `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY` などの provider example vars | `llm-worker` や Pod example / fixture recorder。 | example code が `dotenv::dotenv().ok()` を呼ぶことがある。通常の `insomnia` runtime startup には適用されない。 |
|
||||
|
||||
## deprecated または意図的に存在しない surface
|
||||
|
||||
- `INSOMNIA_USER_MANIFEST` は通常の profile-based Pod/TUI startup の一部ではない。one-file manifest の debug / compatibility path には `insomnia pod --manifest <PATH>` を使う。
|
||||
- ambient `.insomnia/manifest.toml` discovery は通常の fresh startup の一部ではない。
|
||||
- `INSOMNIA_POD_COMMAND` は single-binary 化に伴って削除する。Pod runtime は `insomnia pod ...` の typed command として起動する。
|
||||
- `INSOMNIA_TEST_*` のような test-only 環境変数は supported surface にしない。既存利用も削除する。
|
||||
- `insomnia-pod` は installed command ではない。Pod runtime は `insomnia pod ...` から起動する。
|
||||
- 通常 runtime は `.env` ファイルを load しない。
|
||||
|
||||
## 整理方針
|
||||
|
||||
環境変数に関わるコードを触る場合は、以下を優先する。
|
||||
|
||||
1. test-only env var を削除し、public env behavior を検証する test だけを shared guard / test-support crate に集約する。
|
||||
1. fallback / precedence の test は、process environment を読ませず、直接入力を渡せる小さな pure helper で検証する。
|
||||
2. path resolution は `manifest::paths` に集約し、path precedence rule を別の場所で重複実装しない。
|
||||
3. credential source は resolved config 上で明示し、process-env convention を増やすより typed secret reference へ寄せる。encrypted secret store 導入時に credential env var 依存を削除する。
|
||||
4. `INSOMNIA_POD_COMMAND` は削除し、Pod runtime 起動は `current_exe() + ["pod"]` の typed command に一本化する。
|
||||
3. test が process environment を変更するのは、process env から読む thin wrapper 自体を検証する場合や、subprocess isolation に必要な場合に限る。
|
||||
4. credential source は resolved config 上で明示し、process-env convention を増やすより typed secret reference へ寄せる。encrypted secret store 導入時に credential env var 依存を削除する。
|
||||
5. fallback env は独立した設定項目として増やさず、対応する main key の解決順として文書化する。
|
||||
6. 空の env value は、変数 category に応じて unset / invalid のどちらとして扱うかを一貫させ、新しい supported variable を追加する場合は挙動を文書化する。
|
||||
7. 外部 process integration が env inheritance / filtering を必要とする場合は、ambient な inherited process state に頼らず、明示的な policy boundary として設計する。
|
||||
|
|
|
|||
76
docs/nix.md
76
docs/nix.md
|
|
@ -1,76 +0,0 @@
|
|||
# Nix package
|
||||
|
||||
INSOMNIA provides a flake package for installing the user-facing `insomnia` command without relying on a source checkout at runtime. The Pod runtime still runs as a separate process through `insomnia pod ...`; the installed package does not expose a separate `insomnia-pod` command.
|
||||
|
||||
## Build
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
nix build .#
|
||||
```
|
||||
|
||||
The default package is implemented by `package.nix` and builds the Cargo package `tui` as the installed binary `insomnia`. The `pod` crate remains a library dependency that provides the Pod runtime entrypoint used by `insomnia pod ...`. The derivation uses the checked-in `Cargo.lock`, so Cargo dependencies are fetched by the normal Nix Rust packaging path instead of by network access during the build.
|
||||
|
||||
The package output contains:
|
||||
|
||||
- `bin/insomnia` — terminal UI and `insomnia pod ...` runtime entrypoint.
|
||||
- `share/insomnia/resources/` — bundled runtime resources, including `resources/prompts/`.
|
||||
- `share/doc/insomnia/nix.md` — this document.
|
||||
- `share/doc/insomnia/environment.md` — environment-variable policy and supported variables.
|
||||
|
||||
## Run
|
||||
|
||||
After `nix build`:
|
||||
|
||||
```sh
|
||||
./result/bin/insomnia pod --help
|
||||
./result/bin/insomnia
|
||||
```
|
||||
|
||||
With flakes:
|
||||
|
||||
```sh
|
||||
nix run .#insomnia
|
||||
nix run .#insomnia -- pod --help
|
||||
```
|
||||
|
||||
`nix run .#` defaults to the TUI.
|
||||
|
||||
## Configuration discovery
|
||||
|
||||
The Nix package does not put user configuration, sessions, sockets, or other mutable state in the Nix store. The installed binary keeps the same path semantics as non-Nix builds:
|
||||
|
||||
| Purpose | Override | `INSOMNIA_HOME` fallback | XDG / default fallback |
|
||||
| --- | --- | --- | --- |
|
||||
| User config (`profiles.toml`, prompt overrides, model/provider overrides) | `INSOMNIA_CONFIG_DIR` | `$INSOMNIA_HOME/config` | `$XDG_CONFIG_HOME/insomnia`, then `$HOME/.config/insomnia` |
|
||||
| Persistent data (`sessions/`, Pod metadata) | `INSOMNIA_DATA_DIR` | `$INSOMNIA_HOME` | `$HOME/.insomnia` |
|
||||
| Runtime state (sockets, lock files, live registry) | `INSOMNIA_RUNTIME_DIR` | `$INSOMNIA_HOME/run` | `$XDG_RUNTIME_DIR/insomnia`, then `$HOME/.insomnia/run` |
|
||||
|
||||
Normal fresh startup is profile-based. The package ships a builtin default profile, user/project `profiles.toml` files may select or define profiles, and `insomnia pod --manifest <PATH>` remains a one-file compatibility/debug input. `INSOMNIA_USER_MANIFEST` and ambient `.insomnia/manifest.toml` discovery are not part of normal Pod/TUI startup. See [`environment.md`](environment.md) for the environment-variable policy; new configuration should prefer profiles/manifests/config files over additional environment variables.
|
||||
|
||||
## Validation
|
||||
|
||||
The package derivation has a credential-free install check that verifies:
|
||||
|
||||
- `insomnia pod --help` starts successfully.
|
||||
- `insomnia` is installed and reaches argument parsing.
|
||||
- `bin/insomnia-pod` is not installed.
|
||||
- bundled prompt resources and this Nix usage document are present in the output.
|
||||
|
||||
For full validation before handing changes to review, run:
|
||||
|
||||
```sh
|
||||
nix build .#
|
||||
nix flake check
|
||||
cargo fmt --check
|
||||
cargo check -p tui -p pod -p client
|
||||
```
|
||||
|
||||
These checks do not require provider credentials.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- The package currently installs only the `insomnia` command; development-only wrappers from `devshell.nix` are not part of the installable package.
|
||||
- The TUI does not currently expose a conventional `--help` / `--version` CLI path, so the package smoke check uses an argument-parse failure path for the TUI rather than launching an interactive session.
|
||||
- Bundled resources are installed under `share/insomnia/resources/` for packaging completeness and inspection. Built-in prompt/resource loading remains governed by the existing application code and user/project override rules.
|
||||
|
|
@ -119,12 +119,12 @@ Use `--manifest` only when you need the complete low-level Manifest escape hatch
|
|||
|
||||
## Builtin defaults
|
||||
|
||||
Base defaults that are independent of profile choice live in Rust constants under `crates/manifest/src/defaults.rs` and in `PodManifestConfig::builtin_defaults()`. The bundled default role profile lives at `resources/profiles/default.lua` and is discovered as `builtin:default`.
|
||||
Base defaults that are independent of profile choice live in Rust constants under `crates/manifest/src/defaults.rs` and in `PodManifestConfig::builtin_defaults()`. The default role profile is embedded from `resources/profiles/default.lua` at build time and is discovered as `builtin:default`.
|
||||
|
||||
デフォルト値を変更するときは、次のどちらを変更するのかを明確にする。
|
||||
|
||||
- all manifests/profiles の baseline default: Rust defaults
|
||||
- ordinary dogfooding/default role: `resources/profiles/default.lua`
|
||||
- ordinary dogfooding/default role: embedded `builtin:default` profile sourced from `resources/profiles/default.lua`
|
||||
|
||||
## Path resolution
|
||||
|
||||
|
|
|
|||
10
package.nix
10
package.nix
|
|
@ -40,7 +40,7 @@ rustPlatform.buildRustPackage rec {
|
|||
filter = sourceFilter;
|
||||
};
|
||||
|
||||
cargoHash = "sha256-fisV77ZqAPsI0eLZIqw06HTj1CfmnL3NBHhjruZPZUE=";
|
||||
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
|
||||
|
|
@ -92,10 +92,7 @@ rustPlatform.buildRustPackage rec {
|
|||
doCheck = false;
|
||||
|
||||
postInstall = ''
|
||||
install -Dm644 docs/nix.md "$out/share/doc/insomnia/nix.md"
|
||||
install -Dm644 docs/environment.md "$out/share/doc/insomnia/environment.md"
|
||||
mkdir -p "$out/share/insomnia"
|
||||
cp -R resources "$out/share/insomnia/resources"
|
||||
'';
|
||||
|
||||
doInstallCheck = true;
|
||||
|
|
@ -105,14 +102,13 @@ rustPlatform.buildRustPackage rec {
|
|||
"$out/bin/insomnia" pod --help >/dev/null
|
||||
test -x "$out/bin/insomnia"
|
||||
test ! -e "$out/bin/insomnia-pod"
|
||||
test ! -e "$out/share/insomnia/resources"
|
||||
if "$out/bin/insomnia" --session not-a-uuid 2>insomnia.err; then
|
||||
echo "insomnia unexpectedly accepted an invalid --session value" >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -q "invalid --session UUID" insomnia.err
|
||||
|
||||
test -d "$out/share/insomnia/resources/prompts"
|
||||
test -f "$out/share/doc/insomnia/nix.md"
|
||||
test -f "$out/share/doc/insomnia/environment.md"
|
||||
|
||||
runHook postInstallCheck
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
id: 20260531-005557-single-binary-insomnia-cli
|
||||
slug: single-binary-insomnia-cli
|
||||
title: CLI: migrate toward a single insomnia binary
|
||||
status: open
|
||||
status: closed
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [cli, architecture, nix]
|
||||
created_at: 2026-05-31T00:55:57Z
|
||||
updated_at: 2026-05-31T04:32:30Z
|
||||
updated_at: 2026-05-31T12:15:50Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
Completed the umbrella migration from the previous two-command installed layout toward a single primary `insomnia` executable.
|
||||
|
||||
Completed phases:
|
||||
- `insomnia-pod-subcommand-runtime`: moved Pod runtime startup behind `pod::entrypoint` and added `insomnia pod ...` dispatch.
|
||||
- `spawn-through-insomnia-pod-subcommand`: changed internal spawn/restore defaults to typed runtime command resolution using current executable plus the `pod` prefix argument.
|
||||
- `remove-insomnia-pod-binary`: removed the long-term `insomnia-pod` binary/package/devshell/flake output.
|
||||
- Follow-up cleanup removed `INSOMNIA_POD_COMMAND` and `INSOMNIA_RESOURCE_DIR`, keeping the runtime command/config surface narrower.
|
||||
|
||||
Outcome:
|
||||
- The installed package exposes `bin/insomnia` only.
|
||||
- `insomnia pod ...` is the Pod runtime entrypoint; Pods remain separate processes.
|
||||
- Internal spawn/restore uses typed command construction rather than shell string parsing.
|
||||
- `insomnia-pod` is not kept as a compatibility alias.
|
||||
- Headless `insomnia memory lint` behavior remains part of the `insomnia` CLI surface.
|
||||
|
||||
Validation/evidence across completed phases included:
|
||||
- `insomnia pod --help`
|
||||
- focused parser/spawn/restore tests
|
||||
- `cargo fmt --check`
|
||||
- `cargo check -p tui -p pod -p client`
|
||||
- `nix build .#insomnia`
|
||||
- checks that `bin/insomnia-pod` is absent
|
||||
- `./tickets.sh doctor`
|
||||
- `git diff --check`
|
||||
|
||||
Follow-up intentionally remains separate:
|
||||
- `insomnia-crate-cli-owner` now tracks the architectural cleanup where the `insomnia` crate owns the product CLI/binary entrypoint and `tui` becomes a library implementation crate. That is not required for this umbrella's original single-installed-binary migration to be complete.
|
||||
|
|
@ -36,4 +36,39 @@ Revised decision from user discussion:
|
|||
Initial implementation should start by extracting the Pod runtime into a library entrypoint and adding `insomnia pod ...`; subsequent steps can migrate spawn defaults and remove `insomnia-pod` from packaging.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-05-31T12:15:50Z status: closed -->
|
||||
|
||||
## Closed
|
||||
|
||||
Completed the umbrella migration from the previous two-command installed layout toward a single primary `insomnia` executable.
|
||||
|
||||
Completed phases:
|
||||
- `insomnia-pod-subcommand-runtime`: moved Pod runtime startup behind `pod::entrypoint` and added `insomnia pod ...` dispatch.
|
||||
- `spawn-through-insomnia-pod-subcommand`: changed internal spawn/restore defaults to typed runtime command resolution using current executable plus the `pod` prefix argument.
|
||||
- `remove-insomnia-pod-binary`: removed the long-term `insomnia-pod` binary/package/devshell/flake output.
|
||||
- Follow-up cleanup removed `INSOMNIA_POD_COMMAND` and `INSOMNIA_RESOURCE_DIR`, keeping the runtime command/config surface narrower.
|
||||
|
||||
Outcome:
|
||||
- The installed package exposes `bin/insomnia` only.
|
||||
- `insomnia pod ...` is the Pod runtime entrypoint; Pods remain separate processes.
|
||||
- Internal spawn/restore uses typed command construction rather than shell string parsing.
|
||||
- `insomnia-pod` is not kept as a compatibility alias.
|
||||
- Headless `insomnia memory lint` behavior remains part of the `insomnia` CLI surface.
|
||||
|
||||
Validation/evidence across completed phases included:
|
||||
- `insomnia pod --help`
|
||||
- focused parser/spawn/restore tests
|
||||
- `cargo fmt --check`
|
||||
- `cargo check -p tui -p pod -p client`
|
||||
- `nix build .#insomnia`
|
||||
- checks that `bin/insomnia-pod` is absent
|
||||
- `./tickets.sh doctor`
|
||||
- `git diff --check`
|
||||
|
||||
Follow-up intentionally remains separate:
|
||||
- `insomnia-crate-cli-owner` now tracks the architectural cleanup where the `insomnia` crate owns the product CLI/binary entrypoint and `tui` becomes a library implementation crate. That is not required for this umbrella's original single-installed-binary migration to be complete.
|
||||
|
||||
|
||||
---
|
||||
|
|
@ -2,12 +2,12 @@
|
|||
id: 20260531-074258-tui-extract-cli-parsing
|
||||
slug: tui-extract-cli-parsing
|
||||
title: TUI: extract CLI parsing from main.rs
|
||||
status: open
|
||||
status: closed
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [tui, cleanup]
|
||||
created_at: 2026-05-31T07:42:58Z
|
||||
updated_at: 2026-05-31T07:42:58Z
|
||||
updated_at: 2026-05-31T13:38:30Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
This ticket is completed/superseded by `insomnia-crate-cli-owner`.
|
||||
|
||||
The original goal was to remove CLI parsing from `crates/tui/src/main.rs` so the TUI crate would stop owning top-level product CLI parsing. The later `insomnia-crate-cli-owner` implementation went further:
|
||||
|
||||
- the product binary moved from package `tui` to package `insomnia`;
|
||||
- top-level CLI parsing and dispatch now live in `crates/insomnia/src/main.rs`;
|
||||
- `tui` is now a library implementation crate with no product `main.rs`;
|
||||
- `insomnia pod ...` and `insomnia memory lint ...` are routed by the `insomnia` crate;
|
||||
- normal TUI launch is delegated to `tui::launch(...)`.
|
||||
|
||||
Because the TUI crate no longer owns the product entrypoint or CLI parser, there is no remaining `tui/src/main.rs` CLI parsing extraction to implement under this ticket. Further CLI parser cleanup, if desired, belongs to the `insomnia` crate, not this TUI cleanup ticket.
|
||||
|
||||
Validation evidence from `insomnia-crate-cli-owner` closure:
|
||||
- `cargo test -p insomnia`
|
||||
- CLI smoke for `--help`, `pod --help`, `memory lint --help`, invalid `--session`, and resume/Pod selection conflicts
|
||||
- `cargo check -p client -p pod -p tui -p insomnia`
|
||||
- `nix build .#insomnia`
|
||||
- `./tickets.sh doctor`
|
||||
- `git diff --check`
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<!-- event: create author: tickets.sh at: 2026-05-31T07:42:58Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by tickets.sh create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-05-31T13:38:30Z status: closed -->
|
||||
|
||||
## Closed
|
||||
|
||||
This ticket is completed/superseded by `insomnia-crate-cli-owner`.
|
||||
|
||||
The original goal was to remove CLI parsing from `crates/tui/src/main.rs` so the TUI crate would stop owning top-level product CLI parsing. The later `insomnia-crate-cli-owner` implementation went further:
|
||||
|
||||
- the product binary moved from package `tui` to package `insomnia`;
|
||||
- top-level CLI parsing and dispatch now live in `crates/insomnia/src/main.rs`;
|
||||
- `tui` is now a library implementation crate with no product `main.rs`;
|
||||
- `insomnia pod ...` and `insomnia memory lint ...` are routed by the `insomnia` crate;
|
||||
- normal TUI launch is delegated to `tui::launch(...)`.
|
||||
|
||||
Because the TUI crate no longer owns the product entrypoint or CLI parser, there is no remaining `tui/src/main.rs` CLI parsing extraction to implement under this ticket. Further CLI parser cleanup, if desired, belongs to the `insomnia` crate, not this TUI cleanup ticket.
|
||||
|
||||
Validation evidence from `insomnia-crate-cli-owner` closure:
|
||||
- `cargo test -p insomnia`
|
||||
- CLI smoke for `--help`, `pod --help`, `memory lint --help`, invalid `--session`, and resume/Pod selection conflicts
|
||||
- `cargo check -p client -p pod -p tui -p insomnia`
|
||||
- `nix build .#insomnia`
|
||||
- `./tickets.sh doctor`
|
||||
- `git diff --check`
|
||||
|
||||
|
||||
---
|
||||
|
|
@ -2,12 +2,12 @@
|
|||
id: 20260531-074258-tui-extract-single-pod-runtime
|
||||
slug: tui-extract-single-pod-runtime
|
||||
title: TUI: extract single-Pod runtime loop from main.rs
|
||||
status: open
|
||||
status: closed
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [tui, cleanup]
|
||||
created_at: 2026-05-31T07:42:58Z
|
||||
updated_at: 2026-05-31T07:42:58Z
|
||||
updated_at: 2026-05-31T13:57:02Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
Extracted the current single-Pod TUI runtime/event-loop implementation out of `crates/tui/src/lib.rs`.
|
||||
|
||||
Implementation:
|
||||
- Added `crates/tui/src/single_pod.rs`.
|
||||
- Moved single-Pod attach/spawn/resume orchestration, fullscreen terminal helpers, event loop/drain logic, key/mouse handling, compact/rewind/queue/interrupt handling, and related tests into `single_pod.rs`.
|
||||
- Left `lib.rs` as the TUI library façade/high-level dispatcher with module declarations, `LaunchOptions`, `LaunchMode`, and `launch(...)`.
|
||||
- Preserved public `tui::launch`, `LaunchOptions`, and `LaunchMode` behavior for the `insomnia` crate.
|
||||
- Did not split `multi_pod.rs`, move render helpers, redesign keybindings, rewrite App state, or change Pod protocol/profile/manifest semantics.
|
||||
|
||||
Review:
|
||||
- External reviewer `tui-runtime-reviewer-20260531` approved implementation commit `4d89718`.
|
||||
- Reviewer noted one non-blocking follow-up: the thin `run_multi` bridge now lives in `single_pod.rs`; acceptable for this extraction, but could move to a small launch/runtime coordination module if dashboard orchestration grows.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo fmt --check`
|
||||
- `cargo test -p tui`
|
||||
- `cargo check -p tui -p insomnia` (passed with existing dead-code warnings)
|
||||
- `./tickets.sh doctor`
|
||||
- `git diff --check`
|
||||
- `wc -l crates/tui/src/lib.rs crates/tui/src/single_pod.rs` showed `lib.rs` at 117 lines and `single_pod.rs` at 1765 lines.
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
<!-- event: create author: tickets.sh at: 2026-05-31T07:42:58Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by tickets.sh create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: hare at: 2026-05-31T13:46:30Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
Preflight classification: implementation-ready with updated current-code scope.
|
||||
|
||||
The original ticket was written before `insomnia-crate-cli-owner`, when `crates/tui/src/main.rs` owned both product CLI and single-Pod runtime loop. That product CLI entrypoint has since moved to the `insomnia` crate and `tui` is now library-only. The remaining cleanup target is therefore the same runtime/event-loop code now living in `crates/tui/src/lib.rs`, not an obsolete `main.rs` file.
|
||||
|
||||
Updated intent:
|
||||
- Move the single-Pod TUI runtime/event-loop implementation out of `crates/tui/src/lib.rs` into a focused TUI module, likely `single_pod.rs`.
|
||||
- Keep `lib.rs` as the library façade: module declarations, launch API/types, and high-level dispatch.
|
||||
|
||||
Requirements:
|
||||
- Extract single-Pod runtime functions from `lib.rs` into a focused module while preserving behavior.
|
||||
- Include related helpers only where mechanical and clear: event loop, key/mouse handling, stream/event drain helpers, and per-run connection orchestration.
|
||||
- Keep terminal setup/restore behavior, input queueing, interruption, task reminders, manual rewind/compact handling, and Pod socket behavior unchanged.
|
||||
- Do not move render helpers, split `multi_pod.rs`, redesign keybindings, or perform unrelated TUI module cleanup.
|
||||
- Keep public `tui::launch`, `LaunchOptions`, and `LaunchMode` behavior stable for the `insomnia` crate.
|
||||
|
||||
Current code map:
|
||||
- `crates/tui/src/lib.rs`: library façade plus large single-Pod runtime loop and helpers after CLI ownership migration.
|
||||
- `crates/tui/src/multi_pod.rs`: dashboard runtime; out of scope except imports needed by `lib.rs`.
|
||||
- `crates/tui/src/app.rs`, `input.rs`, `ui.rs`, `tool.rs`, `view_mode.rs`: single-Pod runtime dependencies; should be imported by the extracted module as needed.
|
||||
|
||||
Critical risks:
|
||||
- Over-extracting unrelated launch/multi-Pod code and increasing churn.
|
||||
- Accidentally changing key/mouse/event handling semantics while moving functions.
|
||||
- Creating circular module visibility or broad `pub` exposure. Prefer `pub(crate)` only where `lib.rs` must call into `single_pod`.
|
||||
|
||||
Intent packet for coder:
|
||||
|
||||
Intent:
|
||||
- Extract the single-Pod runtime loop from current `tui/src/lib.rs` into a focused module and leave `lib.rs` as a thin façade/high-level dispatcher.
|
||||
|
||||
Invariants:
|
||||
- Behavior-preserving move.
|
||||
- `tui` remains library-only; `insomnia` remains CLI owner.
|
||||
- No Pod protocol/profile/manifest changes.
|
||||
- No broad TUI layout refactor beyond this extraction.
|
||||
|
||||
Validation:
|
||||
- `cargo fmt --check`
|
||||
- focused `cargo test -p tui` tests for queueing/interrupt/compact/rewind/stream handling if available
|
||||
- full `cargo test -p tui`
|
||||
- `cargo check -p tui -p insomnia`
|
||||
- `./tickets.sh doctor`
|
||||
- `git diff --check`
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: hare at: 2026-05-31T13:56:32Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
External reviewer: `tui-runtime-reviewer-20260531`
|
||||
Reviewed implementation commit: `4d89718` (`tui: extract single pod runtime`)
|
||||
Verdict: approve
|
||||
|
||||
Summary:
|
||||
- This is a behavior-preserving mechanical extraction.
|
||||
- `crates/tui/src/lib.rs` is now a façade/high-level dispatcher.
|
||||
- Single-Pod runtime/event-loop code now lives in `crates/tui/src/single_pod.rs`.
|
||||
- Public `tui::launch`, `LaunchOptions`, and `LaunchMode` signatures remain stable.
|
||||
|
||||
Requirements mapping:
|
||||
- `lib.rs` keeps module declarations, public launch API/types, and high-level mode dispatch.
|
||||
- `single_pod.rs` owns Pod-name/spawn/resume attach paths, fullscreen terminal helpers, main loop/drain logic, key/mouse handling, and the focused tests that cover those behaviors.
|
||||
- Terminal setup/restore behavior is preserved: raw mode/bracketed paste global setup remains in `lib.rs`, while fullscreen/alternate-screen helpers moved with single-Pod runtime code.
|
||||
- Input queueing, interruption, task reminders, compact/rewind handling, and Pod socket behavior are moved with tests and unchanged by inspection.
|
||||
- No render helper move, `multi_pod.rs` split, App rewrite, keybinding redesign, Pod protocol/profile/manifest change, crate rename, or CLI ownership regression was introduced.
|
||||
|
||||
Blockers: none.
|
||||
|
||||
Non-blocking follow-up:
|
||||
- `single_pod.rs` now contains a thin `run_multi` dashboard bridge. This is acceptable here because it bridges dashboard open requests into private single-Pod attach helpers, but if dashboard orchestration grows a future cleanup could move that bridge into a small launch/runtime coordination module.
|
||||
|
||||
Validation adequacy:
|
||||
- Coder validation covered fmt, full TUI tests, TUI/insomnia check, doctor, and diff check.
|
||||
- Reviewer reran `cargo fmt --check`, `cargo test -p tui`, `cargo check -p tui -p insomnia`, `./tickets.sh doctor`, and `git diff --check HEAD^ HEAD`; all passed with existing dead-code warnings.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-05-31T13:57:02Z status: closed -->
|
||||
|
||||
## Closed
|
||||
|
||||
Extracted the current single-Pod TUI runtime/event-loop implementation out of `crates/tui/src/lib.rs`.
|
||||
|
||||
Implementation:
|
||||
- Added `crates/tui/src/single_pod.rs`.
|
||||
- Moved single-Pod attach/spawn/resume orchestration, fullscreen terminal helpers, event loop/drain logic, key/mouse handling, compact/rewind/queue/interrupt handling, and related tests into `single_pod.rs`.
|
||||
- Left `lib.rs` as the TUI library façade/high-level dispatcher with module declarations, `LaunchOptions`, `LaunchMode`, and `launch(...)`.
|
||||
- Preserved public `tui::launch`, `LaunchOptions`, and `LaunchMode` behavior for the `insomnia` crate.
|
||||
- Did not split `multi_pod.rs`, move render helpers, redesign keybindings, rewrite App state, or change Pod protocol/profile/manifest semantics.
|
||||
|
||||
Review:
|
||||
- External reviewer `tui-runtime-reviewer-20260531` approved implementation commit `4d89718`.
|
||||
- Reviewer noted one non-blocking follow-up: the thin `run_multi` bridge now lives in `single_pod.rs`; acceptable for this extraction, but could move to a small launch/runtime coordination module if dashboard orchestration grows.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo fmt --check`
|
||||
- `cargo test -p tui`
|
||||
- `cargo check -p tui -p insomnia` (passed with existing dead-code warnings)
|
||||
- `./tickets.sh doctor`
|
||||
- `git diff --check`
|
||||
- `wc -l crates/tui/src/lib.rs crates/tui/src/single_pod.rs` showed `lib.rs` at 117 lines and `single_pod.rs` at 1765 lines.
|
||||
|
||||
|
||||
---
|
||||
|
|
@ -2,12 +2,12 @@
|
|||
id: 20260531-074258-tui-move-view-mode-state
|
||||
slug: tui-move-view-mode-state
|
||||
title: TUI: move view mode state out of ui module
|
||||
status: open
|
||||
status: closed
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [tui, cleanup]
|
||||
created_at: 2026-05-31T07:42:58Z
|
||||
updated_at: 2026-05-31T07:42:58Z
|
||||
updated_at: 2026-05-31T13:45:39Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
Moved single-Pod view/history mode state out of the render module.
|
||||
|
||||
Implementation:
|
||||
- Added `crates/tui/src/view_mode.rs`.
|
||||
- Moved `Mode` from `ui.rs` to `view_mode.rs` without changing variants or methods.
|
||||
- Updated `app.rs`, `ui.rs`, and `tool.rs` imports to use `crate::view_mode::Mode`.
|
||||
- Removed the `app.rs -> ui.rs` dependency caused solely by `Mode`.
|
||||
- Kept render/key/command behavior unchanged.
|
||||
|
||||
Review:
|
||||
- External reviewer `tui-view-mode-reviewer-20260531` approved implementation commit `bc31bfa`.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo fmt --check`
|
||||
- `cargo test -p tui mode`
|
||||
- `cargo test -p tui`
|
||||
- `cargo check -p tui` (passed with existing dead-code warnings)
|
||||
- `./tickets.sh doctor`
|
||||
- `git diff --check`
|
||||
- `rg "crate::ui::Mode|ui::Mode" crates/tui/src || true` produced no active source hits.
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
<!-- event: create author: tickets.sh at: 2026-05-31T07:42:58Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by tickets.sh create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: hare at: 2026-05-31T13:45:04Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
External reviewer: `tui-view-mode-reviewer-20260531`
|
||||
Reviewed implementation commit: `bc31bfa` (`tui: move view mode state`)
|
||||
Verdict: approve
|
||||
|
||||
Summary:
|
||||
- Moved the single-Pod history display mode type from `ui.rs` to `view_mode.rs`.
|
||||
- Kept the type name `Mode` and preserved variants/methods (`Detail`, `Normal`, `Overview`, `cycle()`, `label()`).
|
||||
- Updated `app.rs`, `ui.rs`, and `tool.rs` to import `crate::view_mode::Mode`.
|
||||
- Removed the state-model dependency from `app.rs` to the render module for this type.
|
||||
|
||||
Requirements mapping:
|
||||
- `Mode` now lives in a focused state-oriented module.
|
||||
- `app.rs -> ui.rs` dependency caused by `Mode` is removed.
|
||||
- Rendering/key/command behavior is unchanged by inspection because the enum and methods are a straight move.
|
||||
- No broad `App` privatization, render-tree move, app split, crate/package rename, or unrelated runtime extraction was introduced.
|
||||
- No active `crate::ui::Mode` / `ui::Mode` references remain under `crates/tui/src`.
|
||||
|
||||
Blockers: none.
|
||||
Non-blocking follow-ups: none.
|
||||
|
||||
Validation adequacy:
|
||||
- Coder validation covered fmt, focused/full TUI tests, TUI check, doctor, diff-check, and `ui::Mode` grep.
|
||||
- Reviewer additionally performed read-only diff/reference checks and found no issues.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-05-31T13:45:39Z status: closed -->
|
||||
|
||||
## Closed
|
||||
|
||||
Moved single-Pod view/history mode state out of the render module.
|
||||
|
||||
Implementation:
|
||||
- Added `crates/tui/src/view_mode.rs`.
|
||||
- Moved `Mode` from `ui.rs` to `view_mode.rs` without changing variants or methods.
|
||||
- Updated `app.rs`, `ui.rs`, and `tool.rs` imports to use `crate::view_mode::Mode`.
|
||||
- Removed the `app.rs -> ui.rs` dependency caused solely by `Mode`.
|
||||
- Kept render/key/command behavior unchanged.
|
||||
|
||||
Review:
|
||||
- External reviewer `tui-view-mode-reviewer-20260531` approved implementation commit `bc31bfa`.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo fmt --check`
|
||||
- `cargo test -p tui mode`
|
||||
- `cargo test -p tui`
|
||||
- `cargo check -p tui` (passed with existing dead-code warnings)
|
||||
- `./tickets.sh doctor`
|
||||
- `git diff --check`
|
||||
- `rg "crate::ui::Mode|ui::Mode" crates/tui/src || true` produced no active source hits.
|
||||
|
||||
|
||||
---
|
||||
|
|
@ -2,12 +2,12 @@
|
|||
id: 20260531-085959-eliminate-test-only-env-vars
|
||||
slug: eliminate-test-only-env-vars
|
||||
title: Tests: eliminate test-only environment variables
|
||||
status: open
|
||||
status: closed
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [test, env, cleanup]
|
||||
created_at: 2026-05-31T08:59:59Z
|
||||
updated_at: 2026-05-31T08:59:59Z
|
||||
updated_at: 2026-05-31T10:04:28Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
Removed test-only environment-variable usage from active code.
|
||||
|
||||
Implementation:
|
||||
- Removed `INSOMNIA_TEST_*` Brave WebSearch test key generation/dependency.
|
||||
- Split Brave search request execution so tests can inject an API key directly into a private helper.
|
||||
- Preserved production behavior: WebSearch still reads configured `web.search.api_key_env` and fails closed for missing/empty values.
|
||||
- Updated `docs/environment.md` so test-only env vars are not listed as supported surface.
|
||||
|
||||
Review:
|
||||
- External reviewer `eliminate-test-env-vars-reviewer-20260531` approved implementation commit `e64a5595956c970b090cdce851cc962e92723a97`.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo fmt --check`
|
||||
- `cargo test -p tools`
|
||||
- `cargo check -p tools` (passed with unrelated existing `llm-worker` dead_code warning)
|
||||
- `./tickets.sh doctor`
|
||||
- `git diff --check`
|
||||
- `git grep -n "INSOMNIA_TEST" -- ':!work-items' || true` produced no active references.
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
<!-- event: create author: tickets.sh at: 2026-05-31T08:59:59Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by tickets.sh create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: hare at: 2026-05-31T10:03:55Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
External reviewer: `eliminate-test-env-vars-reviewer-20260531`
|
||||
Reviewed implementation commit: `e64a5595956c970b090cdce851cc962e92723a97` (`test: remove test-only env vars`)
|
||||
Verdict: approve
|
||||
|
||||
Summary:
|
||||
- WebSearch tests no longer generate or depend on `INSOMNIA_TEST_*` API-key env names.
|
||||
- The request/search implementation was split so tests can inject an API key directly into a private helper while production still reads the configured `web.search.api_key_env` and fails closed for missing/empty values.
|
||||
- `docs/environment.md` no longer presents test-only env vars as a supported surface.
|
||||
|
||||
Requirements mapping:
|
||||
- No active non-work-item `INSOMNIA_TEST` references remain.
|
||||
- No replacement test-only env var was introduced.
|
||||
- Credential env vars and `INSOMNIA_POD_COMMAND` were not removed by this ticket.
|
||||
- WebSearch production behavior and network safety boundaries are preserved.
|
||||
|
||||
Blockers: none.
|
||||
|
||||
Non-blocking follow-up:
|
||||
- A future public-path fail-closed test could guard missing/empty `api_key_env`, but this is not required for this ticket.
|
||||
|
||||
Validation adequacy:
|
||||
- Coder validation covered fmt, tools tests/check, ticket doctor, diff check, and residual `INSOMNIA_TEST` grep.
|
||||
- Reviewer performed read-only diff/source/docs/grep review and did not rerun tests.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-05-31T10:04:28Z status: closed -->
|
||||
|
||||
## Closed
|
||||
|
||||
Removed test-only environment-variable usage from active code.
|
||||
|
||||
Implementation:
|
||||
- Removed `INSOMNIA_TEST_*` Brave WebSearch test key generation/dependency.
|
||||
- Split Brave search request execution so tests can inject an API key directly into a private helper.
|
||||
- Preserved production behavior: WebSearch still reads configured `web.search.api_key_env` and fails closed for missing/empty values.
|
||||
- Updated `docs/environment.md` so test-only env vars are not listed as supported surface.
|
||||
|
||||
Review:
|
||||
- External reviewer `eliminate-test-env-vars-reviewer-20260531` approved implementation commit `e64a5595956c970b090cdce851cc962e92723a97`.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo fmt --check`
|
||||
- `cargo test -p tools`
|
||||
- `cargo check -p tools` (passed with unrelated existing `llm-worker` dead_code warning)
|
||||
- `./tickets.sh doctor`
|
||||
- `git diff --check`
|
||||
- `git grep -n "INSOMNIA_TEST" -- ':!work-items' || true` produced no active references.
|
||||
|
||||
|
||||
---
|
||||
|
|
@ -2,12 +2,12 @@
|
|||
id: 20260531-085959-remove-insomnia-pod-command-env
|
||||
slug: remove-insomnia-pod-command-env
|
||||
title: CLI: remove INSOMNIA_POD_COMMAND override
|
||||
status: open
|
||||
status: closed
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [cli, pod, env]
|
||||
created_at: 2026-05-31T08:59:59Z
|
||||
updated_at: 2026-05-31T08:59:59Z
|
||||
updated_at: 2026-05-31T10:12:03Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
Removed `INSOMNIA_POD_COMMAND` as an active runtime/config/test override.
|
||||
|
||||
Implementation:
|
||||
- `PodRuntimeCommand::resolve()` now always uses the typed default: current executable plus the `pod` prefix argument.
|
||||
- Removed override/env parsing and related tests from the `insomnia` helper crate.
|
||||
- Updated SpawnPod tests to use typed `PodRuntimeCommand` injection instead of process-wide env mutation.
|
||||
- Preserved production spawn/restore behavior, detached process handling, and ready/socket delivery behavior.
|
||||
- Updated docs so no active supported surface documents the removed env override.
|
||||
|
||||
Review:
|
||||
- External reviewer `remove-insomnia-pod-command-reviewer-20260531` approved implementation commit `c618fa6`.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo fmt --check`
|
||||
- `cargo test -p insomnia`
|
||||
- `cargo test -p client -p insomnia`
|
||||
- `cargo test -p pod --lib discovery::tests`
|
||||
- `cargo test -p pod --test spawn_pod_test`
|
||||
- `cargo check -p tui -p pod -p client -p insomnia` (passed with unrelated existing warnings)
|
||||
- `./tickets.sh doctor`
|
||||
- `git diff --check`
|
||||
- `git grep -n "INSOMNIA_POD_COMMAND" -- ':!work-items' || true` produced no active references.
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
<!-- event: create author: tickets.sh at: 2026-05-31T08:59:59Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by tickets.sh create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: hare at: 2026-05-31T10:09:52Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
External reviewer: `remove-insomnia-pod-command-reviewer-20260531`
|
||||
Reviewed implementation commit: `c618fa6` (`cli: remove pod command env override`)
|
||||
Verdict: approve
|
||||
|
||||
Summary:
|
||||
- `INSOMNIA_POD_COMMAND` is removed as an active runtime/config/test override.
|
||||
- Pod runtime launch remains typed through `PodRuntimeCommand`, defaulting to current executable plus the `pod` prefix argument.
|
||||
- SpawnPod tests now use typed injection rather than process-wide env mutation.
|
||||
- Production spawn/restore paths still use `PodRuntimeCommand::resolve()`, preserve detached process handling, and retain the ready/socket delivery behavior.
|
||||
|
||||
Requirements mapping:
|
||||
- No active non-work-item `INSOMNIA_POD_COMMAND` references remain in the implementation branch.
|
||||
- No replacement env var was introduced.
|
||||
- No `insomnia-pod` binary/alias, Pod protocol/flag/profile semantic changes, or `tui` rename were introduced.
|
||||
|
||||
Blockers: none.
|
||||
|
||||
Non-blocking follow-up:
|
||||
- Merge conflict in `docs/environment.md` is expected after `eliminate-test-only-env-vars`; integration should keep the newer generic test-only wording while preserving removal of explicit `INSOMNIA_POD_COMMAND` references.
|
||||
|
||||
Validation adequacy:
|
||||
- Coder validation covered fmt, helper/client/pod tests, cargo check, doctor, diff check, and active-reference grep.
|
||||
- Reviewer additionally reran targeted tests/checks in the implementation worktree and confirmed no active refs for `INSOMNIA_POD_COMMAND`, `POD_COMMAND_OVERRIDE_ENV`, `from_override_env`, or `executable_only` outside work items.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-05-31T10:12:03Z status: closed -->
|
||||
|
||||
## Closed
|
||||
|
||||
Removed `INSOMNIA_POD_COMMAND` as an active runtime/config/test override.
|
||||
|
||||
Implementation:
|
||||
- `PodRuntimeCommand::resolve()` now always uses the typed default: current executable plus the `pod` prefix argument.
|
||||
- Removed override/env parsing and related tests from the `insomnia` helper crate.
|
||||
- Updated SpawnPod tests to use typed `PodRuntimeCommand` injection instead of process-wide env mutation.
|
||||
- Preserved production spawn/restore behavior, detached process handling, and ready/socket delivery behavior.
|
||||
- Updated docs so no active supported surface documents the removed env override.
|
||||
|
||||
Review:
|
||||
- External reviewer `remove-insomnia-pod-command-reviewer-20260531` approved implementation commit `c618fa6`.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo fmt --check`
|
||||
- `cargo test -p insomnia`
|
||||
- `cargo test -p client -p insomnia`
|
||||
- `cargo test -p pod --lib discovery::tests`
|
||||
- `cargo test -p pod --test spawn_pod_test`
|
||||
- `cargo check -p tui -p pod -p client -p insomnia` (passed with unrelated existing warnings)
|
||||
- `./tickets.sh doctor`
|
||||
- `git diff --check`
|
||||
- `git grep -n "INSOMNIA_POD_COMMAND" -- ':!work-items' || true` produced no active references.
|
||||
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
id: 20260531-104614-pure-path-fallback-tests
|
||||
slug: pure-path-fallback-tests
|
||||
title: Tests: make path fallback tests independent from process env
|
||||
status: closed
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [test, env, manifest, cleanup]
|
||||
created_at: 2026-05-31T10:46:14Z
|
||||
updated_at: 2026-05-31T10:54:49Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
The environment-variable cleanup removed test-only env surfaces such as `INSOMNIA_TEST_*` and the Pod runtime command env override. The next issue is that several tests still mutate process-global environment even when they only need to verify deterministic fallback/precedence logic.
|
||||
|
||||
The user clarified the intended direction:
|
||||
|
||||
- even env vars used by the real application do not need to be read from process env in most tests;
|
||||
- tests should verify fallback/precedence behavior with direct inputs;
|
||||
- do not introduce a `PathEnv`/`EnvSnapshot` abstraction or a shared test-support crate just to preserve process-env mutation;
|
||||
- write the fallback helpers directly and narrowly where needed.
|
||||
|
||||
This ticket starts with path resolution, where the behavior is centralized and the env mutation is avoidable.
|
||||
|
||||
## Requirements
|
||||
|
||||
- In `manifest::paths`, separate fallback/precedence logic from process env reads enough that fallback tests can call pure helpers with direct `Option<PathBuf>`-style inputs.
|
||||
- Prefer small per-key helpers over a general `PathEnv` struct/trait.
|
||||
- Example shape: `resolve_config_dir_from_parts(...)`, `resolve_data_dir_from_parts(...)`, `resolve_runtime_dir_from_parts(...)`, or equivalent private helpers.
|
||||
- Keep helper visibility private or `pub(crate)` only if needed by nearby tests.
|
||||
- Keep runtime behavior unchanged:
|
||||
- public path functions still read the same supported env vars in the same order;
|
||||
- empty env values keep the current unset-equivalent behavior;
|
||||
- resource lookup fallback behavior remains compatible with packaging/dev use.
|
||||
- Convert path fallback tests that currently use `std::env::set_var` / `remove_var` to direct helper tests where possible.
|
||||
- Do not add a new helper crate or broad env abstraction.
|
||||
- Do not remove credential env behavior in this ticket; that belongs to `manifest-profile-encrypted-secrets`.
|
||||
- Do not attempt to eliminate subprocess integration env setup if a spawned process still needs runtime isolation; report any remaining env mutation rather than forcing risky API changes.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Removing all env reads from production code.
|
||||
- Removing provider credential env support.
|
||||
- Redesigning runtime directory authority or Pod process startup.
|
||||
- Introducing test-support / EnvSnapshot / trait-based environment abstraction.
|
||||
- Changing documented path semantics.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `manifest::paths` fallback/precedence tests no longer mutate process-global environment just to test fallback order.
|
||||
- The fallback order documented in `docs/environment.md` still matches the code.
|
||||
- Any remaining `set_var` / `remove_var` in touched tests is either eliminated or explicitly justified in the implementation report.
|
||||
- No new environment variable surface is introduced.
|
||||
- `cargo fmt --check`, `cargo test -p manifest paths`, relevant affected tests, `cargo check -p manifest`, `./tickets.sh doctor`, and `git diff --check` pass.
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
Made `manifest::paths` fallback tests independent from process-global environment mutation.
|
||||
|
||||
Implementation:
|
||||
- Added narrow private per-key fallback helpers for config/data/runtime path resolution.
|
||||
- Public path functions still read the same env vars and pass them in the same precedence order.
|
||||
- Empty path env values still resolve as unset.
|
||||
- Fallback-order tests now pass direct `Option<PathBuf>` inputs instead of using `std::env::set_var` / `remove_var`.
|
||||
- No `PathEnv`, `EnvSnapshot`, env trait, test-support crate, or new env var surface was introduced.
|
||||
- Credential env behavior and Pod runtime/process behavior were not changed.
|
||||
|
||||
Review:
|
||||
- External reviewer `pure-path-fallback-reviewer-20260531` approved implementation commit `e232f54`.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo fmt --check`
|
||||
- `cargo test -p manifest paths`
|
||||
- `cargo test -p manifest`
|
||||
- `cargo check -p manifest` (passed with unrelated existing `llm-worker` dead_code warning)
|
||||
- `./tickets.sh doctor`
|
||||
- `git diff --check`
|
||||
- `git grep -n -E 'set_var\(|remove_var\(' -- crates/manifest/src/paths.rs || true` produced no matches.
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
<!-- event: create author: tickets.sh at: 2026-05-31T10:46:14Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by tickets.sh create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: hare at: 2026-05-31T10:46:51Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
Preflight classification: implementation-ready.
|
||||
|
||||
Intent:
|
||||
- Remove unnecessary process-global environment mutation from path fallback tests by making the fallback/precedence logic directly testable with explicit inputs.
|
||||
|
||||
Requirements:
|
||||
- Start with `manifest::paths`, where path resolution is centralized and currently owns env fallback tests.
|
||||
- Use small per-key pure helpers with direct optional path inputs. Do not introduce a general `PathEnv`, `EnvSnapshot`, env trait, or test-support crate.
|
||||
- Keep public runtime behavior unchanged: the production wrappers still read the same env vars, ignore empty path env values as before, and resolve the same fallback order.
|
||||
- Keep docs/environment.md behavior aligned with code.
|
||||
|
||||
Current code map:
|
||||
- `crates/manifest/src/paths.rs`: core config/data/runtime/resource path resolution and env fallback tests using local `EnvGuard`.
|
||||
- `docs/environment.md`: documented fallback order for home/config_dir/data_dir/runtime_dir/resource_dir.
|
||||
- Other env mutations in `pod`/`pod-registry` are mostly subprocess/runtime-isolation integration setup and should not be force-fixed in this ticket.
|
||||
- Provider credential env mutation belongs to `manifest-profile-encrypted-secrets`, not this ticket.
|
||||
|
||||
Critical risks:
|
||||
- Accidentally changing path precedence while making tests pure.
|
||||
- Creating an over-broad abstraction that keeps env mutation around indirectly.
|
||||
- Treating spawned-process runtime isolation env setup as equivalent to pure fallback tests and breaking integration tests.
|
||||
|
||||
Intent packet for implementation:
|
||||
|
||||
Intent:
|
||||
- Make `manifest::paths` fallback tests pure and independent from process env mutation.
|
||||
|
||||
Requirements:
|
||||
- Extract narrow pure helper functions for fallback precedence.
|
||||
- Convert fallback-order tests to call those helpers directly.
|
||||
- Keep production path functions and documented behavior unchanged.
|
||||
- Do not introduce a new crate, `PathEnv` struct, env trait, or test-only env var.
|
||||
|
||||
Invariants:
|
||||
- Path authority remains in `manifest::paths`.
|
||||
- No new environment-variable surface.
|
||||
- No provider credential/env cleanup in this ticket.
|
||||
- No Pod runtime/process model changes.
|
||||
|
||||
Non-goals:
|
||||
- Eliminating all `set_var`/`remove_var` in the repository.
|
||||
- Reworking resource packaging lookup beyond preserving behavior.
|
||||
|
||||
Escalate if:
|
||||
- Removing env mutation requires changing public APIs or path semantics.
|
||||
- Resource lookup behavior is ambiguous enough to need a product decision.
|
||||
- Pod runtime integration tests appear to need broader runtime-dir API changes.
|
||||
|
||||
Validation:
|
||||
- `cargo fmt --check`
|
||||
- `cargo test -p manifest paths`
|
||||
- relevant additional manifest tests if names differ
|
||||
- `cargo check -p manifest`
|
||||
- `./tickets.sh doctor`
|
||||
- `git diff --check`
|
||||
- report remaining `set_var`/`remove_var` in touched areas and why they remain, if any.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: hare at: 2026-05-31T10:54:18Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
External reviewer: `pure-path-fallback-reviewer-20260531`
|
||||
Reviewed implementation commit: `e232f54` (`test: make path fallback tests pure`)
|
||||
Verdict: approve
|
||||
|
||||
Summary:
|
||||
- `manifest::paths` fallback/precedence logic was split into private pure helpers.
|
||||
- Tests now pass direct `Option<PathBuf>` inputs to verify fallback order rather than mutating process environment.
|
||||
- No `PathEnv`/`EnvSnapshot`/trait/test-support crate was introduced.
|
||||
- The change is limited to `crates/manifest/src/paths.rs`.
|
||||
|
||||
Requirements mapping:
|
||||
- `config_dir`, `data_dir`, and `runtime_dir` public wrappers still read the same env keys and pass them in the existing precedence order.
|
||||
- Empty env values still become `None` through the retained `env_path`/path conversion behavior.
|
||||
- `docs/environment.md` fallback order matches the code.
|
||||
- `git grep -n -E 'set_var\(|remove_var\(' -- crates/manifest/src/paths.rs` has no matches.
|
||||
- No credential env behavior, Pod runtime/process behavior, or new env var surface was changed.
|
||||
|
||||
Blockers: none.
|
||||
Non-blocking follow-ups: none.
|
||||
|
||||
Validation adequacy:
|
||||
- Coder validation covered fmt, focused/full manifest tests, manifest check, doctor, diff check, and set_var/remove_var grep.
|
||||
- Reviewer performed read-only diff/source/docs consistency review and grep/diff-check spot checks; no additional Cargo tests were rerun by reviewer.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-05-31T10:54:49Z status: closed -->
|
||||
|
||||
## Closed
|
||||
|
||||
Made `manifest::paths` fallback tests independent from process-global environment mutation.
|
||||
|
||||
Implementation:
|
||||
- Added narrow private per-key fallback helpers for config/data/runtime path resolution.
|
||||
- Public path functions still read the same env vars and pass them in the same precedence order.
|
||||
- Empty path env values still resolve as unset.
|
||||
- Fallback-order tests now pass direct `Option<PathBuf>` inputs instead of using `std::env::set_var` / `remove_var`.
|
||||
- No `PathEnv`, `EnvSnapshot`, env trait, test-support crate, or new env var surface was introduced.
|
||||
- Credential env behavior and Pod runtime/process behavior were not changed.
|
||||
|
||||
Review:
|
||||
- External reviewer `pure-path-fallback-reviewer-20260531` approved implementation commit `e232f54`.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo fmt --check`
|
||||
- `cargo test -p manifest paths`
|
||||
- `cargo test -p manifest`
|
||||
- `cargo check -p manifest` (passed with unrelated existing `llm-worker` dead_code warning)
|
||||
- `./tickets.sh doctor`
|
||||
- `git diff --check`
|
||||
- `git grep -n -E 'set_var\(|remove_var\(' -- crates/manifest/src/paths.rs || true` produced no matches.
|
||||
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
id: 20260531-110818-remove-resource-dir
|
||||
slug: remove-resource-dir
|
||||
title: Manifest: remove filesystem resource_dir dependency
|
||||
status: closed
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [manifest, profile, nix, env, cleanup]
|
||||
created_at: 2026-05-31T11:08:18Z
|
||||
updated_at: 2026-05-31T11:58:28Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
`manifest::paths::resource_dir()` and `INSOMNIA_RESOURCE_DIR` currently keep a filesystem resource lookup boundary for bundled assets. That boundary appears increasingly historical:
|
||||
|
||||
- prompts are already embedded through `include_dir!` in the Pod prompt loader;
|
||||
- provider/model builtin catalogs are already embedded in the provider catalog path;
|
||||
- the remaining important uses are builtin Lua profile discovery and a manifest-profile model-context lookup that still reads `resources/models/builtin.toml` through `resource_dir()`.
|
||||
|
||||
The desired direction is to remove the filesystem `resource_dir` dependency rather than preserve `INSOMNIA_RESOURCE_DIR` as a public/user-facing configuration surface.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Investigate and then remove the need for `manifest::paths::resource_dir()` if feasible.
|
||||
- Remove `INSOMNIA_RESOURCE_DIR` as an active supported environment variable once no production code needs it.
|
||||
- Preserve current builtin behavior:
|
||||
- builtin/default profiles remain available through normal profile selection;
|
||||
- builtin provider/model metadata remains available;
|
||||
- prompts and profile assets still resolve correctly in dev and installed/Nix builds.
|
||||
- Prefer embedding builtin assets in Rust over runtime filesystem discovery where this does not create a worse design.
|
||||
- If builtin Lua profiles need synthetic source labels or special handling for diagnostics/local requires, design that explicitly rather than keeping `resource_dir` by inertia.
|
||||
- Update Nix packaging only after confirming resources no longer need to be installed at `share/insomnia/resources` for runtime behavior.
|
||||
- Update `docs/environment.md` to remove `resource_dir` / `INSOMNIA_RESOURCE_DIR` from supported path keys if implementation removes it.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Changing user/project `config_dir` override semantics.
|
||||
- Removing user profile files or project profile support.
|
||||
- Changing profile selector semantics except where needed to replace builtin profile filesystem discovery.
|
||||
- Reworking credential env handling.
|
||||
- Reintroducing ambient manifest discovery.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- No production code depends on `manifest::paths::resource_dir()` for bundled asset lookup, or any remaining dependency is explicitly justified and documented in the ticket thread.
|
||||
- `INSOMNIA_RESOURCE_DIR` is removed from active code/docs if `resource_dir()` is removed.
|
||||
- Builtin default profile/profile registry tests pass without filesystem resource discovery.
|
||||
- Builtin model/provider lookup behavior remains available and tested.
|
||||
- Nix build/install checks still pass; installed package does not need runtime filesystem resources unless explicitly justified.
|
||||
- `cargo fmt --check`, focused manifest/provider/pod/tui tests as relevant, `cargo check` for affected crates, `./tickets.sh doctor`, and `git diff --check` pass.
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
Removed runtime filesystem resource discovery for bundled assets and removed `INSOMNIA_RESOURCE_DIR` as an active configuration surface.
|
||||
|
||||
Implementation:
|
||||
- Removed `RESOURCE_DIR_ENV`, `resource_dir()`, and `builtin_profiles_dir()` from `manifest::paths`.
|
||||
- Embedded builtin Lua profile source (`default.lua`) and exposed builtin profile metadata without fake filesystem paths.
|
||||
- Preserved user/project registry profile and explicit path profile filesystem semantics.
|
||||
- Replaced manifest-side builtin model context lookup with an embedded `resources/models/builtin.toml` source, avoiding a `manifest -> provider` dependency.
|
||||
- Removed installed runtime `share/insomnia/resources` packaging and checks from Nix package output.
|
||||
- Updated environment/Nix/pod-factory docs so runtime resource directory and `INSOMNIA_RESOURCE_DIR` are no longer supported/documented surfaces.
|
||||
|
||||
Review:
|
||||
- External reviewer `remove-resource-dir-reviewer-20260531` approved implementation commit `365ec8b7fad008ab36bdc4de3adadb3696739a07`.
|
||||
- Reviewer found no blockers. A suggested non-blocking future regression test for embedded profile local `require` diagnostics was recorded in the review thread.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo fmt --check`
|
||||
- `cargo test -p manifest profile`
|
||||
- `cargo test -p provider catalog`
|
||||
- `cargo test -p pod prompt`
|
||||
- `cargo check -p manifest -p provider -p pod -p tui`
|
||||
- `./tickets.sh doctor`
|
||||
- `git diff --check`
|
||||
- `rg -n 'INSOMNIA_RESOURCE_DIR|RESOURCE_DIR_ENV|resource_dir\(|builtin_profiles_dir\(' crates docs package.nix || true` produced no active hits.
|
||||
- `nix build .#insomnia`
|
||||
- `test -x result/bin/insomnia`
|
||||
- `test ! -e result/bin/insomnia-pod`
|
||||
- `test ! -e result/share/insomnia/resources`
|
||||
- `result/bin/insomnia pod --help`
|
||||
|
||||
Note:
|
||||
- Initial post-merge validation hit `No space left on device` while linking. `cargo clean` freed build artifacts, after which Rust and Nix validation passed.
|
||||
179
work-items/closed/20260531-110818-remove-resource-dir/thread.md
Normal file
179
work-items/closed/20260531-110818-remove-resource-dir/thread.md
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
<!-- event: create author: tickets.sh at: 2026-05-31T11:08:18Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by tickets.sh create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: hare at: 2026-05-31T11:28:41Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
Read-only investigator: `resource-dir-removal-investigator-20260531`
|
||||
Classification: implementation-ready
|
||||
|
||||
Conclusion:
|
||||
- `manifest::paths::resource_dir()` can be removed.
|
||||
- `INSOMNIA_RESOURCE_DIR` can be removed as an active supported environment variable after replacing the two remaining runtime filesystem resource dependencies.
|
||||
- Builtin assets should be embedded rather than discovered through installed `share/insomnia/resources`.
|
||||
|
||||
Current code map:
|
||||
- `crates/manifest/src/paths.rs`
|
||||
- `RESOURCE_DIR_ENV = "INSOMNIA_RESOURCE_DIR"`
|
||||
- `resource_dir()` checks env, installed `$prefix/share/insomnia/resources`, then source-tree `resources/`.
|
||||
- `builtin_profiles_dir()` returns `resource_dir()/profiles`.
|
||||
- `crates/manifest/src/profile.rs`
|
||||
- builtin profile discovery still scans `paths::builtin_profiles_dir()`.
|
||||
- `builtin_model_context_window()` still reads `resource_dir()/models/builtin.toml`.
|
||||
- `crates/provider/src/catalog.rs`
|
||||
- provider/model builtin catalogs are already embedded with `include_str!`.
|
||||
- `crates/pod/src/prompt/loader.rs` and `catalog.rs`
|
||||
- builtin prompts are already embedded with `include_dir!` / `include_str!`.
|
||||
- `package.nix` still installs `resources` to `$out/share/insomnia/resources` and checks prompt dir existence.
|
||||
- `docs/environment.md` and `docs/nix.md` still document runtime resource directory/package resources.
|
||||
|
||||
Embedded vs filesystem map:
|
||||
- Already runtime-embedded:
|
||||
- `resources/prompts/**`
|
||||
- `resources/providers/builtin.toml`
|
||||
- `resources/models/builtin.toml` in provider crate.
|
||||
- Still runtime-filesystem-dependent:
|
||||
- `resources/profiles/default.lua` through builtin profile directory scan.
|
||||
- manifest-side model context lookup through `resource_dir()/models/builtin.toml`.
|
||||
- Should remain filesystem-based:
|
||||
- user/project `profiles.toml` and profile paths;
|
||||
- config prompt/model/provider overrides;
|
||||
- explicit `--manifest <path>` and explicit filesystem profile path selectors.
|
||||
|
||||
Recommended implementation:
|
||||
1. Remove `RESOURCE_DIR_ENV`, `resource_dir()`, and `builtin_profiles_dir()` from `manifest::paths`.
|
||||
2. Embed builtin Lua profiles explicitly in `manifest::profile`, starting with `resources/profiles/default.lua`.
|
||||
3. Preserve profile selector semantics for `default`, `builtin:default`, user/project source-qualified selectors, explicit paths, and ambiguity failures.
|
||||
4. Use synthetic builtin provenance/diagnostics rather than fake filesystem paths.
|
||||
5. Keep host Lua modules unchanged. Do not support embedded local filesystem require by falling back to `resources/profiles`; current default profile does not need local require.
|
||||
6. Replace manifest-side `builtin_model_context_window()` filesystem read with `include_str!("../../../resources/models/builtin.toml")`. Do not make `manifest` depend on `provider` because `provider -> manifest` already exists.
|
||||
7. Remove Nix runtime install/check for `$out/share/insomnia/resources` after runtime code no longer needs it.
|
||||
8. Remove `INSOMNIA_RESOURCE_DIR` / `resource_dir` docs from `docs/environment.md`; update Nix/profile docs where they describe installed runtime resources.
|
||||
|
||||
Critical risks:
|
||||
- Changing serialized profile provenance shape for builtin profiles. This is acceptable if tests/docs are updated, but do not pretend embedded profiles have real paths.
|
||||
- Embedded profile local `require`: current builtin default has none. If future builtin profiles need it, add an explicit embedded module map; do not retain filesystem fallback by inertia.
|
||||
- Manifest/provider catalog dependency cycle: do not call provider builtin APIs from manifest in this ticket.
|
||||
- Keep source-tree `resources/` in the Nix build source closure because compile-time `include_str!` / `include_dir!` still need it; only installed runtime resources should disappear.
|
||||
|
||||
Implementation-ready intent packet:
|
||||
|
||||
Intent:
|
||||
- Remove runtime filesystem resource discovery for bundled assets and delete `INSOMNIA_RESOURCE_DIR` as an active configuration surface.
|
||||
|
||||
Requirements:
|
||||
- Embed builtin Lua profile(s) and manifest-side builtin model context lookup.
|
||||
- Preserve existing profile selection behavior and user/project profile semantics.
|
||||
- Remove runtime `resource_dir` path API and Nix installed resources dependency.
|
||||
- Update docs to reflect embedded builtin assets and remove `INSOMNIA_RESOURCE_DIR`.
|
||||
|
||||
Invariants:
|
||||
- `config_dir` / user profile / project profile semantics remain unchanged.
|
||||
- No ambient manifest discovery is restored.
|
||||
- No `INSOMNIA_RESOURCE_DIR` compatibility path is kept.
|
||||
- `manifest` must not depend on `provider`.
|
||||
- Pod runtime remains separate and prompt embedding behavior remains intact.
|
||||
|
||||
Non-goals:
|
||||
- Credential env cleanup.
|
||||
- Reworking provider catalog ownership into a shared crate.
|
||||
- Changing user/project config override paths.
|
||||
|
||||
Escalate if:
|
||||
- Builtin profile embedding requires local filesystem require support beyond current default profile.
|
||||
- A public API requires real builtin profile paths rather than provenance labels.
|
||||
- Nix packaging unexpectedly needs installed resources for runtime behavior after embedding.
|
||||
|
||||
Validation:
|
||||
- grep for `INSOMNIA_RESOURCE_DIR|RESOURCE_DIR_ENV|resource_dir\(|builtin_profiles_dir\(` in active code/docs/package files.
|
||||
- `cargo fmt --check`
|
||||
- `cargo test -p manifest profile`
|
||||
- focused tests for builtin default profile and compact ratio known-model context
|
||||
- `cargo test -p provider catalog`
|
||||
- `cargo test -p pod prompt`
|
||||
- `cargo check -p manifest -p provider -p pod -p tui`
|
||||
- `nix build .#insomnia` and install-output checks: `bin/insomnia` exists, `bin/insomnia-pod` absent, `share/insomnia/resources` absent if removed, `insomnia pod --help` works.
|
||||
- `./tickets.sh doctor`
|
||||
- `git diff --check`
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: hare at: 2026-05-31T11:54:28Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
External reviewer: `remove-resource-dir-reviewer-20260531`
|
||||
Reviewed implementation commit: `365ec8b7fad008ab36bdc4de3adadb3696739a07` (`manifest: embed builtin resources`)
|
||||
Verdict: approve
|
||||
|
||||
Summary:
|
||||
- Runtime filesystem resource discovery and `INSOMNIA_RESOURCE_DIR` active surface were removed.
|
||||
- Builtin profile is embedded and represented as `builtin:default` without pretending to have a runtime filesystem path under `resources/profiles`.
|
||||
- User/project/path profile semantics remain filesystem-backed.
|
||||
- Builtin model context lookup is embedded on the manifest side without adding a `manifest -> provider` dependency.
|
||||
- Nix package/docs no longer require installed runtime resources.
|
||||
|
||||
Requirements mapping:
|
||||
- `INSOMNIA_RESOURCE_DIR`, `RESOURCE_DIR_ENV`, `resource_dir()`, and `builtin_profiles_dir()` have no active hits in `crates/`, `docs/`, or `package.nix`.
|
||||
- Embedded builtin profiles do not silently support local filesystem `require` by falling back to resources; non-filesystem profile local modules produce the existing disabled diagnostic.
|
||||
- `crates/pod/src/spawn/tool.rs` change is test/API fallout for `ProfileDiscovery::with_sources`, not a runtime/protocol behavior change.
|
||||
- Runtime installed resources are no longer required; source `resources/` remains build-time input for embeds.
|
||||
|
||||
Blockers: none.
|
||||
|
||||
Non-blocking follow-ups:
|
||||
- Add a narrow regression test for an embedded builtin-like profile attempting local `require("x")`, asserting the explicit disabled diagnostic.
|
||||
- Rerun `nix build .#insomnia` in an environment with enough disk space before treating the ticket as fully release-validated; the final rerun is environment-blocked by disk full.
|
||||
|
||||
Validation adequacy:
|
||||
- Rust validation and forbidden-symbol grep are well targeted.
|
||||
- Final Nix validation remains pending due to `No space left on device`, not due to a code review finding.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-05-31T11:58:28Z status: closed -->
|
||||
|
||||
## Closed
|
||||
|
||||
Removed runtime filesystem resource discovery for bundled assets and removed `INSOMNIA_RESOURCE_DIR` as an active configuration surface.
|
||||
|
||||
Implementation:
|
||||
- Removed `RESOURCE_DIR_ENV`, `resource_dir()`, and `builtin_profiles_dir()` from `manifest::paths`.
|
||||
- Embedded builtin Lua profile source (`default.lua`) and exposed builtin profile metadata without fake filesystem paths.
|
||||
- Preserved user/project registry profile and explicit path profile filesystem semantics.
|
||||
- Replaced manifest-side builtin model context lookup with an embedded `resources/models/builtin.toml` source, avoiding a `manifest -> provider` dependency.
|
||||
- Removed installed runtime `share/insomnia/resources` packaging and checks from Nix package output.
|
||||
- Updated environment/Nix/pod-factory docs so runtime resource directory and `INSOMNIA_RESOURCE_DIR` are no longer supported/documented surfaces.
|
||||
|
||||
Review:
|
||||
- External reviewer `remove-resource-dir-reviewer-20260531` approved implementation commit `365ec8b7fad008ab36bdc4de3adadb3696739a07`.
|
||||
- Reviewer found no blockers. A suggested non-blocking future regression test for embedded profile local `require` diagnostics was recorded in the review thread.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo fmt --check`
|
||||
- `cargo test -p manifest profile`
|
||||
- `cargo test -p provider catalog`
|
||||
- `cargo test -p pod prompt`
|
||||
- `cargo check -p manifest -p provider -p pod -p tui`
|
||||
- `./tickets.sh doctor`
|
||||
- `git diff --check`
|
||||
- `rg -n 'INSOMNIA_RESOURCE_DIR|RESOURCE_DIR_ENV|resource_dir\(|builtin_profiles_dir\(' crates docs package.nix || true` produced no active hits.
|
||||
- `nix build .#insomnia`
|
||||
- `test -x result/bin/insomnia`
|
||||
- `test ! -e result/bin/insomnia-pod`
|
||||
- `test ! -e result/share/insomnia/resources`
|
||||
- `result/bin/insomnia pod --help`
|
||||
|
||||
Note:
|
||||
- Initial post-merge validation hit `No space left on device` while linking. `cargo clean` freed build artifacts, after which Rust and Nix validation passed.
|
||||
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
---
|
||||
id: 20260531-111956-insomnia-crate-cli-owner
|
||||
slug: insomnia-crate-cli-owner
|
||||
title: CLI: make insomnia crate own binary entrypoint and CLI dispatch
|
||||
status: closed
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [cli, tui, pod, architecture]
|
||||
created_at: 2026-05-31T11:19:56Z
|
||||
updated_at: 2026-05-31T13:20:02Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
The single-binary migration currently exposes the installed command as `insomnia`, but the binary target and CLI dispatch are still owned by the `tui` package (`cargo run -p tui -- ...`, `crates/tui/src/main.rs`). The helper crate now named `insomnia` was originally introduced for Pod runtime command resolution, which creates a responsibility mismatch.
|
||||
|
||||
The desired architecture is that the `insomnia` crate owns the product CLI and binary entrypoint. The `tui` and `pod` crates should be libraries with no `main.rs`, and CLI/tool subcommand routing should happen at the `insomnia` boundary.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Move product CLI ownership to the `insomnia` crate:
|
||||
- binary target `insomnia` belongs to package/crate `insomnia`;
|
||||
- CLI argument parsing and top-level mode/subcommand dispatch live in `insomnia`;
|
||||
- development entrypoint becomes `cargo run -p insomnia -- ...`.
|
||||
- Make `tui` a library implementation crate rather than binary/CLI owner.
|
||||
- `tui` should expose an API for launching the terminal UI with already-parsed options/configuration.
|
||||
- `tui` should not own `insomnia pod ...` dispatch or headless CLI tool routing.
|
||||
- Keep `pod` as a library runtime crate with no dependency on `insomnia`.
|
||||
- `pod` may expose its runtime entrypoint/parser API for `insomnia pod ...`, but it must not know the product CLI crate.
|
||||
- Move or relocate `PodRuntimeCommand` responsibility out of the current `insomnia` helper role if needed to avoid inverted dependencies.
|
||||
- `pod -> insomnia` is not acceptable.
|
||||
- Prefer passing the runtime command from the top-level CLI/orchestrating layer into TUI/client spawn paths.
|
||||
- If a shared type is still needed, place it in a lower-level crate whose responsibility is process/client mechanics, not in the product CLI crate by accident.
|
||||
- CLI tools/headless commands should be routed by `insomnia`, not by `tui`.
|
||||
- Examples: `memory lint`, `pod ...`, future non-TUI commands.
|
||||
- Preserve observable behavior:
|
||||
- installed command remains `insomnia`;
|
||||
- `insomnia -r`, `insomnia --multi`, `insomnia <podname>`, and `insomnia pod ...` keep their behavior;
|
||||
- no `insomnia-pod` alias is reintroduced;
|
||||
- Pod runtime remains a separate process.
|
||||
- Do not add compatibility layers for `cargo run -p tui -- ...`; updating docs/dev commands is sufficient.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Renaming the user-facing command away from `insomnia`.
|
||||
- Merging Pod runtime into the TUI process.
|
||||
- Reintroducing `INSOMNIA_POD_COMMAND` or test-only env command overrides.
|
||||
- Redesigning Pod protocol, profile semantics, or manifest semantics.
|
||||
- Broad TUI module-layout refactors beyond what is necessary to make `tui` library-callable.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `cargo run -p insomnia -- --help` and `cargo run -p insomnia -- pod --help` work as the main development entrypoints.
|
||||
- Package `tui` no longer owns a binary `main.rs` entrypoint, or any remaining binary target is explicitly temporary and not the product CLI owner.
|
||||
- No active code path requires `pod -> insomnia` or `tui -> insomnia` dependency.
|
||||
- `insomnia` crate owns top-level CLI parsing/dispatch for TUI launch, Pod runtime launch, and headless CLI tools.
|
||||
- Pod spawn/restore still launches runtime subprocesses as `insomnia pod ...` through typed command injection from the top-level owner.
|
||||
- Nix/package outputs still expose only `bin/insomnia`.
|
||||
- Current docs/dev instructions no longer tell developers to use `cargo run -p tui -- ...` as the primary product CLI path.
|
||||
- Focused CLI/parser/spawn tests, `cargo fmt --check`, relevant `cargo check`/`cargo test`, `nix build .#insomnia` if packaging changed, `./tickets.sh doctor`, and `git diff --check` pass.
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
Moved product CLI/binary ownership from `tui` to `insomnia`.
|
||||
|
||||
Implementation:
|
||||
- Moved `PodRuntimeCommand` from the transitional `insomnia` helper role into `client`, so lower crates no longer depend on the product CLI crate.
|
||||
- Made `tui` a library implementation crate and exposed launch APIs for already-parsed modes/options.
|
||||
- Added the `insomnia` package binary entrypoint and moved top-level CLI parsing/dispatch there.
|
||||
- Routed `insomnia pod ...` through `pod::entrypoint` from the `insomnia` crate.
|
||||
- Routed `insomnia memory lint ...` from the `insomnia` crate.
|
||||
- Kept normal TUI launch behavior for resume, multi-Pod dashboard, Pod name, `--pod`, and `--session` modes.
|
||||
- Updated packaging so `package.nix` builds package `insomnia` and still exposes only `bin/insomnia`.
|
||||
- Updated active development/docs references away from `cargo run -p tui -- ...`.
|
||||
|
||||
Review:
|
||||
- External reviewer `insomnia-cli-owner-reviewer-20260531` initially requested changes for a parser regression around `--resume` combined with Pod selection.
|
||||
- Fix commit `37281b6` restored mutual exclusion for `-r --pod`, `--pod -r`, and `-r <podname>`, and improved `--multi --pod` diagnostics.
|
||||
- Reviewer approved after re-review.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo fmt --check`
|
||||
- `cargo check -p client -p pod -p tui -p insomnia`
|
||||
- `cargo test -p client`
|
||||
- `cargo test -p pod`
|
||||
- `cargo test -p tui`
|
||||
- `cargo test -p insomnia`
|
||||
- `cargo run -p insomnia -- --help`
|
||||
- `cargo run -p insomnia -- pod --help`
|
||||
- `cargo run -p insomnia -- memory lint --help`
|
||||
- invalid/conflict CLI smoke tests for `--session not-a-uuid`, `-r --pod`, `--pod -r`, and `-r <podname>`
|
||||
- dependency checks confirming `client`, `pod`, and `tui` do not depend on `insomnia`, and `tui` does not depend on `pod`
|
||||
- `nix build .#insomnia`
|
||||
- `test -x ./result/bin/insomnia`
|
||||
- `test ! -e ./result/bin/insomnia-pod`
|
||||
- `./result/bin/insomnia pod --help`
|
||||
- `./tickets.sh doctor`
|
||||
- `git diff --check`
|
||||
- `rg "cargo run -p tui" docs README.md crates package.nix flake.nix || true` produced no active hits.
|
||||
|
||||
Notes:
|
||||
- Validation emitted pre-existing dead-code warnings in `llm-worker`/`tui` but passed.
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
<!-- event: create author: tickets.sh at: 2026-05-31T11:19:56Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by tickets.sh create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: hare at: 2026-05-31T12:42:22Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
Read-only investigator: `insomnia-cli-owner-investigator-20260531`
|
||||
Classification: implementation-ready
|
||||
|
||||
Conclusion:
|
||||
- Proceed with this ticket as one implementation ticket, but implement in ordered internal phases: move `PodRuntimeCommand` to `client`, make `tui` library-callable, move product CLI/binary ownership to `insomnia`, then update packaging/docs.
|
||||
|
||||
Current code/dependency map:
|
||||
- `crates/tui` currently owns the product binary target:
|
||||
- `crates/tui/Cargo.toml` has `[[bin]] name = "insomnia" path = "src/main.rs"`.
|
||||
- `crates/tui/src/main.rs` owns top-level CLI parsing, `insomnia pod ...` dispatch, `insomnia memory lint`, and TUI launch.
|
||||
- `crates/insomnia` currently has no binary and mostly owns `PodRuntimeCommand` only.
|
||||
- `crates/pod` is already library-only and exposes `pod::entrypoint::run_cli_from`.
|
||||
- `client`, `pod`, and tests depend on `insomnia` only for `PodRuntimeCommand`, creating the wrong dependency direction for the desired architecture.
|
||||
- `package.nix` builds `-p tui` even though the installed command is `insomnia`.
|
||||
|
||||
Recommended architecture:
|
||||
```text
|
||||
insomnia -> tui
|
||||
insomnia -> pod
|
||||
insomnia -> client
|
||||
tui -> client
|
||||
pod -> client
|
||||
client -> no insomnia
|
||||
pod -> no insomnia
|
||||
tui -> no insomnia
|
||||
```
|
||||
|
||||
Implementation plan:
|
||||
1. Move `PodRuntimeCommand` from `insomnia` to `client`.
|
||||
- Prefer `client::runtime_command` with `pub use` from `client`.
|
||||
- Update `client`, `pod`, and tests to use `client::PodRuntimeCommand`.
|
||||
- Remove `insomnia` dependency from lower crates.
|
||||
2. Make `tui` a library implementation crate.
|
||||
- Add `crates/tui/src/lib.rs`.
|
||||
- Move TUI module declarations and TUI launch code behind public library API.
|
||||
- Expose narrow API such as `LaunchOptions`, `LaunchMode`, `launch(...)`.
|
||||
- Keep terminal setup/teardown in `tui` because it owns terminal behavior.
|
||||
- Remove product CLI parsing and `pod`/`memory lint` dispatch from `tui`.
|
||||
3. Make `insomnia` own product CLI/binary.
|
||||
- Add `crates/insomnia/src/main.rs`.
|
||||
- Move top-level CLI parser/dispatch from `tui` to `insomnia`.
|
||||
- Route `pod` to `pod::entrypoint::run_cli_from("insomnia pod", args)`.
|
||||
- Route `memory lint` from `insomnia`, not `tui`.
|
||||
- Route normal TUI modes to `tui::launch(...)` with a typed runtime command.
|
||||
4. Update packaging/docs.
|
||||
- Remove `[[bin]] name = "insomnia"` from `tui`.
|
||||
- Make `package.nix` build `-p insomnia`.
|
||||
- Update active docs/dev commands from `cargo run -p tui -- ...` to `cargo run -p insomnia -- ...`.
|
||||
|
||||
Important constraints:
|
||||
- Do not introduce `pod -> insomnia` or `tui -> insomnia`.
|
||||
- Do not keep compatibility for `cargo run -p tui -- ...`.
|
||||
- Do not reintroduce `insomnia-pod`, `INSOMNIA_POD_COMMAND`, or the separate `INSOMNIA_POD_RUNTIME_COMMAND`; that dev override is tracked by `dev-pod-runtime-command-env`.
|
||||
- Do not change Pod protocol/profile/manifest semantics.
|
||||
- Avoid broad TUI refactoring beyond making the crate library-callable.
|
||||
|
||||
Critical risks:
|
||||
- `tui/src/main.rs` is large and mixes CLI, terminal lifecycle, event loop, key handling, and tests. Keep changes mechanical and avoid unrelated module cleanup.
|
||||
- Parser tests should move to `insomnia`; TUI behavior tests should remain with `tui`.
|
||||
- `memory_lint` is headless product CLI code and should not remain routed through TUI.
|
||||
- `--help` behavior needs explicit handling because the old parser mostly ignored unknown flags.
|
||||
|
||||
Validation:
|
||||
- `cargo fmt --check`
|
||||
- `cargo check -p client -p pod -p tui -p insomnia`
|
||||
- `cargo test -p client`
|
||||
- `cargo test -p pod`
|
||||
- `cargo test -p tui`
|
||||
- `cargo test -p insomnia`
|
||||
- CLI smoke:
|
||||
- `cargo run -p insomnia -- --help`
|
||||
- `cargo run -p insomnia -- pod --help`
|
||||
- `cargo run -p insomnia -- memory lint --help`
|
||||
- `cargo run -p insomnia -- --session not-a-uuid`
|
||||
- Dependency ownership checks:
|
||||
- `cargo tree -p client -e no-dev --depth 1`
|
||||
- `cargo tree -p pod -e no-dev --depth 1`
|
||||
- `cargo tree -p tui -e no-dev --depth 1`
|
||||
- `cargo tree -p insomnia -e no-dev --depth 1`
|
||||
- If packaging changed:
|
||||
- `nix build .#insomnia`
|
||||
- `./result/bin/insomnia pod --help`
|
||||
- `test -x ./result/bin/insomnia`
|
||||
- `test ! -e ./result/bin/insomnia-pod`
|
||||
- `./tickets.sh doctor`
|
||||
- `git diff --check`
|
||||
- `rg "cargo run -p tui" docs README.md crates package.nix flake.nix`
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: hare at: 2026-05-31T13:17:15Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
External reviewer: `insomnia-cli-owner-reviewer-20260531`
|
||||
Reviewed implementation commits:
|
||||
- `22d974a` (`cli: move product entrypoint to insomnia`)
|
||||
- `37281b6` (`cli: reject resume with pod selection`)
|
||||
Verdict: approve
|
||||
|
||||
Summary:
|
||||
- Product binary and top-level CLI dispatch moved from the `tui` package to the `insomnia` package.
|
||||
- `tui` is now a library implementation crate and no longer owns `pod`/`memory lint` top-level dispatch.
|
||||
- `PodRuntimeCommand` moved to `client`, removing the wrong lower-crate dependency on the product CLI crate.
|
||||
- `pod` remains library-only and does not depend on `insomnia`.
|
||||
- Packaging now builds package `insomnia` for the installed `bin/insomnia` output.
|
||||
|
||||
Initial review blocker:
|
||||
- `--resume` combined with `--pod` or positional Pod name selection initially stopped erroring.
|
||||
|
||||
Fix:
|
||||
- Commit `37281b6` restored mutual exclusion:
|
||||
- `insomnia -r --pod agent` errors.
|
||||
- `insomnia --pod agent -r` errors.
|
||||
- `insomnia -r agent` errors.
|
||||
- `insomnia --multi --pod agent` reports `--multi and --pod are mutually exclusive`.
|
||||
|
||||
Requirements mapping:
|
||||
- `insomnia` owns binary target and top-level CLI dispatch.
|
||||
- `tui`, `pod`, and `client` do not depend on `insomnia`.
|
||||
- `tui` does not depend on `pod`.
|
||||
- `insomnia pod ...`, `insomnia memory lint ...`, and normal TUI launch modes are preserved.
|
||||
- No `insomnia-pod`, `INSOMNIA_POD_COMMAND`, or `INSOMNIA_POD_RUNTIME_COMMAND` was introduced.
|
||||
- No Pod protocol/profile/manifest semantic change was found.
|
||||
|
||||
Blockers: none.
|
||||
|
||||
Validation adequacy:
|
||||
- Coder ran Rust tests/checks, CLI smoke, dependency ownership checks, Nix build/install smoke, doctor, diff-check, and active `cargo run -p tui` grep.
|
||||
- Reviewer reran focused parser tests and manually confirmed conflict diagnostics through the built binary after the fix.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-05-31T13:20:02Z status: closed -->
|
||||
|
||||
## Closed
|
||||
|
||||
Moved product CLI/binary ownership from `tui` to `insomnia`.
|
||||
|
||||
Implementation:
|
||||
- Moved `PodRuntimeCommand` from the transitional `insomnia` helper role into `client`, so lower crates no longer depend on the product CLI crate.
|
||||
- Made `tui` a library implementation crate and exposed launch APIs for already-parsed modes/options.
|
||||
- Added the `insomnia` package binary entrypoint and moved top-level CLI parsing/dispatch there.
|
||||
- Routed `insomnia pod ...` through `pod::entrypoint` from the `insomnia` crate.
|
||||
- Routed `insomnia memory lint ...` from the `insomnia` crate.
|
||||
- Kept normal TUI launch behavior for resume, multi-Pod dashboard, Pod name, `--pod`, and `--session` modes.
|
||||
- Updated packaging so `package.nix` builds package `insomnia` and still exposes only `bin/insomnia`.
|
||||
- Updated active development/docs references away from `cargo run -p tui -- ...`.
|
||||
|
||||
Review:
|
||||
- External reviewer `insomnia-cli-owner-reviewer-20260531` initially requested changes for a parser regression around `--resume` combined with Pod selection.
|
||||
- Fix commit `37281b6` restored mutual exclusion for `-r --pod`, `--pod -r`, and `-r <podname>`, and improved `--multi --pod` diagnostics.
|
||||
- Reviewer approved after re-review.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo fmt --check`
|
||||
- `cargo check -p client -p pod -p tui -p insomnia`
|
||||
- `cargo test -p client`
|
||||
- `cargo test -p pod`
|
||||
- `cargo test -p tui`
|
||||
- `cargo test -p insomnia`
|
||||
- `cargo run -p insomnia -- --help`
|
||||
- `cargo run -p insomnia -- pod --help`
|
||||
- `cargo run -p insomnia -- memory lint --help`
|
||||
- invalid/conflict CLI smoke tests for `--session not-a-uuid`, `-r --pod`, `--pod -r`, and `-r <podname>`
|
||||
- dependency checks confirming `client`, `pod`, and `tui` do not depend on `insomnia`, and `tui` does not depend on `pod`
|
||||
- `nix build .#insomnia`
|
||||
- `test -x ./result/bin/insomnia`
|
||||
- `test ! -e ./result/bin/insomnia-pod`
|
||||
- `./result/bin/insomnia pod --help`
|
||||
- `./tickets.sh doctor`
|
||||
- `git diff --check`
|
||||
- `rg "cargo run -p tui" docs README.md crates package.nix flake.nix || true` produced no active hits.
|
||||
|
||||
Notes:
|
||||
- Validation emitted pre-existing dead-code warnings in `llm-worker`/`tui` but passed.
|
||||
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
id: 20260531-124040-dev-pod-runtime-command-env
|
||||
slug: dev-pod-runtime-command-env
|
||||
title: Dev: add Pod runtime executable override env
|
||||
status: closed
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [cli, pod, env, dev]
|
||||
created_at: 2026-05-31T12:40:40Z
|
||||
updated_at: 2026-05-31T20:41:56Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
During dogfooding, long-running TUI/Pod processes can keep executing an old `target/debug/insomnia` inode after `cargo build` or `cargo clean` replaces/removes the binary. On Linux, `std::env::current_exe()` then returns a path like:
|
||||
|
||||
```text
|
||||
/home/.../target/debug/insomnia (deleted)
|
||||
```
|
||||
|
||||
Current Pod spawn/restore paths use `current_exe() + ["pod"]` as the runtime command, so a long-running process can try to spawn `.../insomnia (deleted) pod` and fail with `No such file or directory`.
|
||||
|
||||
This is primarily a development/dogfooding problem. The previous general-purpose `INSOMNIA_POD_COMMAND` override was intentionally removed; do not restore that semantics. Instead, add a narrow development escape hatch that replaces only the executable used in the standard `insomnia pod ...` runtime command.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Add a narrowly scoped development override environment variable, tentatively `INSOMNIA_POD_RUNTIME_COMMAND`.
|
||||
- Semantics:
|
||||
- if unset/empty, default remains `current_exe() + ["pod"]`;
|
||||
- if set, the value is the executable path used instead of `current_exe()`;
|
||||
- the `pod` prefix argument is still automatically added;
|
||||
- the value is not shell-parsed and cannot include arguments.
|
||||
- Keep this as a development escape hatch, not normal user configuration.
|
||||
- Update diagnostics so a failed spawn shows the resolved runtime command clearly enough to debug deleted-path issues.
|
||||
- Document the variable in `docs/environment.md` under a clearly development-only section.
|
||||
- Do not reintroduce `INSOMNIA_POD_COMMAND` or its old executable-without-prefix semantics.
|
||||
- Do not change Pod runtime flags/profile/manifest/protocol semantics.
|
||||
- This ticket may be implemented before or folded into `insomnia-crate-cli-owner`, but must not create `pod -> insomnia` or `tui -> insomnia` architectural coupling beyond the current transitional state.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Making environment variables a general configuration mechanism.
|
||||
- Adding shell-string command parsing.
|
||||
- Supporting wrapper commands with arguments.
|
||||
- Solving all hot-reload/self-rebuild lifecycle issues.
|
||||
- Reintroducing the removed `insomnia-pod` binary/alias.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Setting `INSOMNIA_POD_RUNTIME_COMMAND=/path/to/insomnia` causes spawn/restore paths to execute `/path/to/insomnia pod ...`.
|
||||
- Unset or empty `INSOMNIA_POD_RUNTIME_COMMAND` preserves the default `current_exe() + ["pod"]` behavior.
|
||||
- Tests cover override, empty-as-unset, and no shell parsing/argument splitting semantics.
|
||||
- Active code/docs do not reference the old `INSOMNIA_POD_COMMAND` name except historical work items.
|
||||
- `docs/environment.md` explains this as a development-only escape hatch for dogfooding/rebuild deleted-exe cases.
|
||||
- `cargo fmt --check`, focused tests for the runtime command helper/spawn paths, `cargo check` for affected crates, `./tickets.sh doctor`, and `git diff --check` pass.
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
Added `INSOMNIA_POD_RUNTIME_COMMAND` as a narrow development escape hatch for dogfooding/self-rebuild cases where `current_exe()` can point at a deleted debug binary.
|
||||
|
||||
Implementation:
|
||||
- Added the override in `client::PodRuntimeCommand`.
|
||||
- Unset/empty values preserve the default `current_exe() + ["pod"]` behavior.
|
||||
- Non-empty values replace only the executable path and still automatically receive the `pod` prefix argument.
|
||||
- The value is not shell-parsed and is not argument-split.
|
||||
- Spawn/restore failure diagnostics include the resolved runtime command.
|
||||
- Documented the variable in `docs/environment.md` as development-only, not normal user configuration.
|
||||
- Did not reintroduce `INSOMNIA_POD_COMMAND` or old executable-without-prefix semantics.
|
||||
|
||||
Review:
|
||||
- External reviewer `dev-pod-runtime-env-reviewer-20260531` approved implementation commit `0031953ed352ba7fae9e798b6aeee1e8ea080816`.
|
||||
- Reviewer noted a non-blocking future improvement: display formatting for runtime commands could quote argv pieces with spaces more clearly.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo fmt --check`
|
||||
- `cargo test -p client runtime_command`
|
||||
- `cargo test -p client`
|
||||
- `cargo check -p client -p pod -p tui -p insomnia` (passed with existing dead-code warnings)
|
||||
- `./tickets.sh doctor`
|
||||
- `git diff --check`
|
||||
- `git grep -n "INSOMNIA_POD_COMMAND" -- ':!work-items' || true` produced no active references.
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
<!-- event: create author: tickets.sh at: 2026-05-31T12:40:40Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by tickets.sh create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: hare at: 2026-05-31T20:41:28Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
External reviewer: `dev-pod-runtime-env-reviewer-20260531`
|
||||
Reviewed implementation commit: `0031953ed352ba7fae9e798b6aeee1e8ea080816` (`dev: add pod runtime command override`)
|
||||
Verdict: approve
|
||||
|
||||
Summary:
|
||||
- Added `INSOMNIA_POD_RUNTIME_COMMAND` as a narrow development-only executable override for the standard `insomnia pod ...` runtime command.
|
||||
- Unset/empty values preserve `current_exe() + ["pod"]`.
|
||||
- Non-empty values replace only the executable path; the `pod` prefix argument is still automatically added.
|
||||
- The value is passed as a single `PathBuf`/program and is not shell-parsed or argument-split.
|
||||
- Diagnostics for failed client spawn/restore include the resolved runtime command.
|
||||
|
||||
Requirements mapping:
|
||||
- Override implementation lives in `client::PodRuntimeCommand`, preserving the post-CLI-owner dependency boundary.
|
||||
- Spawn/restore paths continue to use typed command construction with `Command::new(program)` and prefix args.
|
||||
- `docs/environment.md` documents the env var as a development-only escape hatch, not normal configuration.
|
||||
- Old `INSOMNIA_POD_COMMAND` was not reintroduced and has no active non-work-item refs.
|
||||
- No Pod runtime flag/profile/manifest/protocol semantic changes were found.
|
||||
|
||||
Blockers: none.
|
||||
|
||||
Non-blocking follow-up:
|
||||
- `PodRuntimeCommand::Display` joins argv-like pieces with spaces, so diagnostics can be visually ambiguous when an executable path contains spaces. It is sufficient for this ticket, but a future quoted/structured argv display would be clearer.
|
||||
|
||||
Validation adequacy:
|
||||
- Coder validation covered fmt, focused/full client tests, affected crate check, doctor, diff-check, and old env-name grep.
|
||||
- Reviewer performed read-only diff/source/docs/grep review and did not rerun Cargo tests.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-05-31T20:41:56Z status: closed -->
|
||||
|
||||
## Closed
|
||||
|
||||
Added `INSOMNIA_POD_RUNTIME_COMMAND` as a narrow development escape hatch for dogfooding/self-rebuild cases where `current_exe()` can point at a deleted debug binary.
|
||||
|
||||
Implementation:
|
||||
- Added the override in `client::PodRuntimeCommand`.
|
||||
- Unset/empty values preserve the default `current_exe() + ["pod"]` behavior.
|
||||
- Non-empty values replace only the executable path and still automatically receive the `pod` prefix argument.
|
||||
- The value is not shell-parsed and is not argument-split.
|
||||
- Spawn/restore failure diagnostics include the resolved runtime command.
|
||||
- Documented the variable in `docs/environment.md` as development-only, not normal user configuration.
|
||||
- Did not reintroduce `INSOMNIA_POD_COMMAND` or old executable-without-prefix semantics.
|
||||
|
||||
Review:
|
||||
- External reviewer `dev-pod-runtime-env-reviewer-20260531` approved implementation commit `0031953ed352ba7fae9e798b6aeee1e8ea080816`.
|
||||
- Reviewer noted a non-blocking future improvement: display formatting for runtime commands could quote argv pieces with spaces more clearly.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo fmt --check`
|
||||
- `cargo test -p client runtime_command`
|
||||
- `cargo test -p client`
|
||||
- `cargo check -p client -p pod -p tui -p insomnia` (passed with existing dead-code warnings)
|
||||
- `./tickets.sh doctor`
|
||||
- `git diff --check`
|
||||
- `git grep -n "INSOMNIA_POD_COMMAND" -- ':!work-items' || true` produced no active references.
|
||||
|
||||
|
||||
---
|
||||
|
|
@ -1,64 +1,155 @@
|
|||
---
|
||||
id: 20260529-145355-manifest-profile-encrypted-secrets
|
||||
slug: manifest-profile-encrypted-secrets
|
||||
title: Encrypted secret store for manifest profiles
|
||||
title: Manifest/Profile: local key-value secret store
|
||||
status: open
|
||||
kind: feature
|
||||
priority: P2
|
||||
labels: [manifest, profiles, secrets, security]
|
||||
labels: [manifest, profiles, secrets, security, cli, tui]
|
||||
created_at: 2026-05-29T14:53:55Z
|
||||
updated_at: 2026-05-29T14:53:55Z
|
||||
updated_at: 2026-05-31T21:23:46Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
WebSearch/WebFetch made API keys more visible as a UX problem: `WebSearch` currently expects `web.search.api_key_env`, so users must export `BRAVE_SEARCH_API_KEY` before starting the Pod/TUI process. That is inconvenient for long-lived Pods, profile switching, and per-project/provider configuration.
|
||||
Credential configuration still relies on process environment variables in important paths:
|
||||
|
||||
This should not be solved by adding `.env` loading as an implicit side effect. `.env` files are easy to leak into projects, do not solve profile-specific credential selection cleanly, and still expose secrets through process environments. Instead, when manifest profiles are designed/implemented, add a first-class encrypted secret store that manifests/profiles can reference.
|
||||
- provider API keys use `AuthRef::ApiKey { env, file }` and provider-default `INSOMNIA_API_KEY_*` names;
|
||||
- WebSearch currently uses `web.search.api_key_env`;
|
||||
- normal runtime intentionally does not load `.env` files.
|
||||
|
||||
Related work item: `work-items/open/20260527-000022-manifest-profiles/item.md`.
|
||||
This should not be solved by implicit `.env` loading. `.env` files are easy to leak into projects, do not solve profile-specific credential selection cleanly, and still expose secrets through process environments.
|
||||
|
||||
The desired replacement is a local key-value secret store plus explicit references from manifest/profile/tool configuration.
|
||||
|
||||
The security target is intentionally modest. This is not a high-assurance password manager. The goal is to avoid casual plaintext exposure and generic environment-variable scraping, not to defend against a local attacker with the user's account or process memory access.
|
||||
|
||||
## Intent
|
||||
|
||||
Implement a provider-independent local key-value secret store and use it as the normal credential path for provider and WebSearch credentials.
|
||||
|
||||
The logical store model is just:
|
||||
|
||||
```text
|
||||
{
|
||||
"anthropic/default" = "sk-..."
|
||||
"web/brave/default" = "..."
|
||||
}
|
||||
```
|
||||
|
||||
The store must not know that a key is Anthropic, Brave, OpenAI, or any other provider-specific kind. Provider/model/tool configuration chooses which key to reference.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Design a typed secret reference format for manifest/profile fields that need credentials.
|
||||
- Add a new encrypted-store reference form, e.g. `api_key_secret = "brave.search.default"` or a more general `SecretRef` enum.
|
||||
- Existing env references such as `api_key_env = "BRAVE_SEARCH_API_KEY"` may be supported only as a migration/compatibility input during the transition; the target state is to remove credential environment-variable configuration rather than keep it as a normal fallback.
|
||||
- Secret references must be explicit in resolved config; do not silently read arbitrary `.env` files.
|
||||
- Add an encrypted local secret store suitable for API keys/tokens.
|
||||
- Store secrets outside tracked project files by default, under the user data/config directory.
|
||||
- Use authenticated encryption and atomic writes.
|
||||
- Do not log plaintext secrets, include them in session logs, expose them to model context, or return them through normal tool output.
|
||||
- Keep encrypted blobs out of git-managed work-items/memory/session records.
|
||||
- Integrate with manifest profiles.
|
||||
- Profiles should be able to select different secret names for different roles/providers, e.g. Orchestrator/Coder/Researcher or web search provider variants.
|
||||
- Profile resolution should validate that referenced secrets exist or produce a clear startup/tool diagnostic.
|
||||
- A profile switch must not require restarting the shell just to change API keys.
|
||||
- Provide a small CLI/TUI management surface.
|
||||
- Add/update/list/delete secrets without printing plaintext by default.
|
||||
- Support non-interactive set from stdin for scripts.
|
||||
- Show references and metadata, not secret values.
|
||||
- Consider migration helpers from existing env-var based configuration, but keep migration optional.
|
||||
- Update credential consumers.
|
||||
- WebSearch should use encrypted secret refs instead of requiring env vars.
|
||||
- Provider API keys/tokens and future hosted/search credentials should use the same mechanism.
|
||||
- Remove env-var credential configuration from the normal supported path once encrypted secret refs and migration diagnostics exist.
|
||||
- Security and UX constraints.
|
||||
- Fail closed when a referenced secret is missing or cannot be decrypted.
|
||||
- Diagnostics should name the missing reference, not the secret value.
|
||||
- Do not add hidden context injection or history mutation for secret resolution.
|
||||
- Document the threat model and limitations, including OS account access and backup implications.
|
||||
### Store model
|
||||
|
||||
- Add a local secret/key store that maps a validated string id to a secret string value.
|
||||
- Keep the user-visible logical schema provider-independent: `id -> value`.
|
||||
- Do not add provider-specific slots, credential kinds, or required metadata to the store schema.
|
||||
- Technical envelope fields needed for versioning/nonce/ciphertext/checksum are allowed, but they must not become user-facing semantic metadata.
|
||||
- Store data outside the repository, under the user data directory, e.g. `<data_dir>/secrets/store.json` or equivalent.
|
||||
- Use atomic writes.
|
||||
- Validate ids:
|
||||
- reject empty ids;
|
||||
- reject path traversal / absolute-path-like ids;
|
||||
- reject control characters;
|
||||
- bound length;
|
||||
- allow a conservative useful set such as ASCII alnum plus `._/-`.
|
||||
|
||||
### Obfuscation / encryption stance
|
||||
|
||||
- Apply lightweight encryption or obfuscation at rest so the file is not a casual plaintext key dump.
|
||||
- Do not claim strong local security guarantees.
|
||||
- Do not introduce OS keychain dependency or interactive passphrase UX in this ticket.
|
||||
- Do not store plaintext values in logs, work items, session history, diagnostics, or normal command output.
|
||||
- Decryption/decoding failures must fail closed and name only the key id, not the value.
|
||||
|
||||
### `insomnia keys` TUI management
|
||||
|
||||
- Add `insomnia keys` as an interactive TUI key manager.
|
||||
- The product CLI owner (`insomnia` crate) routes the subcommand.
|
||||
- Use the TUI implementation crate for the terminal screen if practical.
|
||||
- Minimum UI features:
|
||||
- list key ids;
|
||||
- add/set a key;
|
||||
- delete a key with confirmation;
|
||||
- quit.
|
||||
- Do not display plaintext values in the list or normal screen output.
|
||||
- During add/set, mask the value input or otherwise avoid echoing plaintext.
|
||||
- Scriptable commands such as `insomnia keys set <id> --stdin` may be added if convenient, but the required user surface is the TUI manager.
|
||||
|
||||
### Config references / consumers
|
||||
|
||||
- Use explicit secret references from configuration.
|
||||
- Existing `AuthRef::SecretRef { ref_ }` in manifest model should resolve through the new store for provider API keys.
|
||||
- WebSearch must gain an explicit secret reference path so Brave search can be configured without `BRAVE_SEARCH_API_KEY` / `api_key_env`.
|
||||
- Prefer a generic auth/secret-ref shape if it stays small.
|
||||
- A focused `api_key_secret = "web/brave/default"` field is acceptable if it avoids a broad schema redesign.
|
||||
- Secret refs are resolved at the consumer/runtime boundary only; resolved config/debug output must not contain plaintext.
|
||||
- The store must not implicitly choose default keys based on provider name. No ambient lookup like "anthropic automatically reads anthropic/default" unless the profile/config explicitly references it.
|
||||
|
||||
### Env credential removal
|
||||
|
||||
- Do not load `.env` files.
|
||||
- Do not add new credential environment variables.
|
||||
- Do not keep migration/backward-compatibility behavior for credential env config in the normal profile path.
|
||||
- Remove credential env configuration from normal provider/WebSearch use as part of this ticket.
|
||||
- Docs and diagnostics should point users to `insomnia keys` + secret refs as the credential path.
|
||||
|
||||
### Codex OAuth relationship
|
||||
|
||||
- Codex OAuth is not part of this key-value secret store in this ticket.
|
||||
- Current Codex OAuth intentionally interoperates with Codex CLI's `auth.json` file and refresh behavior; that file contains a structured token bundle, not a single provider API key string.
|
||||
- Do not store or refresh Codex OAuth token bundles through the key-value store as part of this ticket.
|
||||
- Do not change `CODEX_HOME` / `$HOME/.codex` lookup behavior in this ticket.
|
||||
- A future Insomnia-owned Codex login/token store could be designed separately if needed, but it should be a dedicated OAuth token-store design, not an implicit use of the simple key-value API-key store.
|
||||
|
||||
## Phases within this ticket
|
||||
|
||||
1. Core store
|
||||
- key-value store API;
|
||||
- id validation;
|
||||
- lightweight encrypted/obfuscated file format;
|
||||
- atomic load/save;
|
||||
- focused tests.
|
||||
2. `insomnia keys` TUI manager
|
||||
- list/add/delete;
|
||||
- masked input;
|
||||
- no plaintext display.
|
||||
3. Provider integration
|
||||
- implement provider `AuthRef::SecretRef` resolution through the store;
|
||||
- keep plaintext in memory only;
|
||||
- fail closed on missing/invalid/decode failures.
|
||||
4. WebSearch integration
|
||||
- add a secret-ref credential path;
|
||||
- make Brave search usable without env credentials.
|
||||
5. Docs and env removal
|
||||
- update `docs/environment.md` and manifest/profile docs;
|
||||
- document the modest security target honestly;
|
||||
- point users to `insomnia keys` and secret refs as the credential path;
|
||||
- remove credential env configuration from normal provider/WebSearch docs and code paths.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- A high-assurance password manager.
|
||||
- OS keychain integration.
|
||||
- Passphrase prompt UX.
|
||||
- Provider-specific secret-store schema.
|
||||
- Automatic provider-name-to-secret-id lookup.
|
||||
- Loading `.env` files.
|
||||
- Changing Codex OAuth behavior. Codex OAuth remains an external structured token-source integration in this ticket.
|
||||
- Reworking model/provider catalog ownership.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Manifest/profile schema has a typed credential reference for encrypted secret-store entries; env-var credential inputs are at most transitional migration inputs, not the final supported configuration path.
|
||||
- Encrypted secret-store files are created outside the repository by default and use authenticated encryption with atomic update behavior.
|
||||
- A user can add/list/delete a Brave Search API key in the secret store and configure `WebSearch` to use it without exporting an environment variable.
|
||||
- Resolved configuration and diagnostics never display plaintext secrets.
|
||||
- Missing/decryption-failed secrets produce clear fail-closed errors.
|
||||
- Existing env-var based credential configuration is either removed or produces an explicit migration diagnostic after encrypted secret references are available.
|
||||
- Documentation explains how profiles reference secrets, how to manage them, and why credential env vars are no longer the normal path.
|
||||
- Focused tests cover config parsing/resolution, missing secret diagnostics, no-plaintext serialization/logging paths, and WebSearch secret resolution.
|
||||
- `cargo fmt --check`
|
||||
- Relevant manifest/provider/tools/pod tests pass.
|
||||
- A user can run `insomnia keys` and manage key ids interactively.
|
||||
- The store persists key-value entries under the user data directory without plaintext values in the on-disk file.
|
||||
- Store id validation rejects unsafe ids.
|
||||
- Provider `AuthRef::SecretRef` resolves through the store and does not print/serialize plaintext.
|
||||
- WebSearch can use a configured secret ref without exporting an environment variable.
|
||||
- Missing key, invalid id, unreadable store, and decode/decrypt failure produce clear fail-closed errors naming only the key id.
|
||||
- `docs/environment.md` no longer presents credential env vars as the normal path, removes normal provider/WebSearch credential env configuration, and documents the limited protection goal.
|
||||
- Focused tests cover store round-trip, id validation, decode failure, provider secret-ref resolution, WebSearch secret-ref resolution, and no-plaintext debug/serialization paths where applicable.
|
||||
- `cargo fmt --check`, relevant crate tests/checks, `./tickets.sh doctor`, and `git diff --check` pass.
|
||||
|
|
|
|||
|
|
@ -4,4 +4,203 @@
|
|||
|
||||
Created by tickets.sh create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: hare at: 2026-05-31T20:58:00Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
Preflight/detailing status: requirements-sync-needed before implementation.
|
||||
|
||||
This ticket is the remaining credential/env cleanup boundary. It should not go directly to coding until the secret-store key-management decision is settled. The current code already has some typed-reference groundwork, but no runtime secret store exists.
|
||||
|
||||
Current code map:
|
||||
- `crates/manifest/src/model.rs`
|
||||
- `AuthRef::SecretRef { ref_ }` already exists in the manifest model and serializes as `auth = { kind = "secret_ref", ref = "..." }`.
|
||||
- `AuthRef::ApiKey { env, file }` still documents and models env/file API-key sources.
|
||||
- `SchemeKind::default_env_var()` still provides `INSOMNIA_API_KEY_*` defaults.
|
||||
- `crates/provider/src/lib.rs`
|
||||
- `AuthRef::ApiKey` resolves env first, then `auth.file`.
|
||||
- `AuthRef::SecretRef` currently errors with "secret store references are not implemented yet".
|
||||
- `crates/tools/src/web.rs`
|
||||
- Brave WebSearch currently requires `web.search.api_key_env` and reads process env directly.
|
||||
- `docs/environment.md`
|
||||
- credential env vars are documented as migration compatibility, not the target supported configuration path.
|
||||
|
||||
Proposed target architecture:
|
||||
- Add a small secret-store boundary independent from `manifest`, `provider`, and `tools` cycles.
|
||||
- Candidate crate name: `secret-store` or `secrets`.
|
||||
- Responsibilities: secret id validation, encrypted store read/write, metadata listing, redacted diagnostics, and test-only in-memory/key fixtures.
|
||||
- Non-responsibilities: provider-specific auth semantics, profile selection, tool networking.
|
||||
- Keep manifest/profile config as references only.
|
||||
- Model auth should use the existing `AuthRef::SecretRef { ref_ }` shape rather than adding another model-auth syntax.
|
||||
- WebSearch needs an explicit secret reference field, e.g. `web.search.api_key_secret = "web/brave/default"` or a typed `web.search.auth = { kind = "secret_ref", ref = "..." }`. Prefer the latter if it avoids another provider-specific one-off, but choose based on minimal config churn.
|
||||
- Secret IDs should be logical names, not paths.
|
||||
- Allow a conservative character set such as `[A-Za-z0-9._/-]`.
|
||||
- Reject empty ids, absolute/path-traversal-like ids, control characters, and extremely long ids.
|
||||
- IDs are diagnostics-safe; values are not.
|
||||
- Secret values are resolved only at consumer/runtime boundary.
|
||||
- Resolved manifests/profiles may contain secret refs, not plaintext.
|
||||
- Provider/WebSearch consumers receive plaintext only in memory and must not serialize/log it.
|
||||
|
||||
Key-management decision required before implementation:
|
||||
- Do not store an encryption key next to the encrypted store; that provides obfuscation, not meaningful encryption.
|
||||
- Preferred direction to evaluate: OS keychain/credential manager as the wrapping-key source where available.
|
||||
- Linux Secret Service / macOS Keychain / Windows Credential Manager through a maintained crate such as `keyring`, if acceptable for dependencies/packaging/headless use.
|
||||
- If no OS key provider is available, fail closed with clear diagnostics rather than silently writing plaintext or adjacent keys.
|
||||
- Tests should use an explicit test-only key provider/in-memory store without process-env mutation.
|
||||
- If a passphrase fallback is desired later, make it explicit interactive/CLI UX; do not read passphrases from ambient env.
|
||||
|
||||
Suggested store layout:
|
||||
- Store encrypted blobs outside the repository, under the user data directory, e.g. `<data_dir>/secrets/store.json` or `<data_dir>/secrets/<id>.json`.
|
||||
- Use atomic write + fsync where the project already has or can add an atomic-write helper.
|
||||
- Store metadata needed for listing without plaintext values:
|
||||
- id
|
||||
- created_at / updated_at
|
||||
- optional description/provider/kind
|
||||
- encryption metadata: algorithm, nonce, key provider/version
|
||||
- Do not put encrypted blobs under work-items, memory, session logs, project `.insomnia/`, or generated reports.
|
||||
|
||||
Encryption requirements:
|
||||
- Authenticated encryption only (AEAD), e.g. XChaCha20-Poly1305 or AES-GCM depending on dependency choice.
|
||||
- Unique nonce per encryption.
|
||||
- Include associated data that binds ciphertext to secret id and store metadata version.
|
||||
- Decryption/auth failure is a fail-closed error naming the secret id only.
|
||||
|
||||
CLI/TUI management scope:
|
||||
- Initial scope can be CLI-first under `insomnia secrets ...`; TUI management can be follow-up unless UX is trivial.
|
||||
- Minimum CLI:
|
||||
- `insomnia secrets set <id> --stdin [--description ...]`
|
||||
- `insomnia secrets list`
|
||||
- `insomnia secrets delete <id>`
|
||||
- optionally `insomnia secrets rename <old> <new>` later, not required for MVP.
|
||||
- Do not print plaintext by default. Avoid adding `show` unless protected by an explicit `--reveal` decision in a later ticket.
|
||||
- Non-interactive `set --stdin` is required so scripts can load keys without shell env exports.
|
||||
|
||||
Consumer migration plan:
|
||||
1. Implement secret store + model `AuthRef::SecretRef` resolution in `provider`.
|
||||
2. Add WebSearch secret reference support and tests.
|
||||
3. Add CLI management commands.
|
||||
4. Update docs and examples to use secret refs as the normal path.
|
||||
5. Convert env-var credential paths into migration diagnostics or compatibility-only one-file/debug behavior, then remove from normal profile path.
|
||||
|
||||
Important migration constraints:
|
||||
- Do not load `.env` files.
|
||||
- Do not keep env as a normal fallback once secret refs are available.
|
||||
- If env inputs remain temporarily, diagnostics should say they are migration compatibility and point to `insomnia secrets set ...` / profile secret refs.
|
||||
- Avoid changing Codex OAuth in this ticket unless a clear secret-store integration is needed; `CODEX_HOME` remains external compatibility.
|
||||
|
||||
Acceptance criteria additions:
|
||||
- `AuthRef::SecretRef` can resolve through the encrypted store for provider API keys without exposing plaintext in resolved config/debug output.
|
||||
- WebSearch can use a secret ref without `BRAVE_SEARCH_API_KEY` or `web.search.api_key_env`.
|
||||
- CLI can add/list/delete a secret without printing plaintext.
|
||||
- Missing secret, invalid id, unavailable key provider, and decryption/auth failure produce clear fail-closed errors naming only the reference.
|
||||
- Tests cover:
|
||||
- secret id validation;
|
||||
- encrypted round-trip with test key provider;
|
||||
- wrong-key/auth-failure diagnostics;
|
||||
- provider `AuthRef::SecretRef` resolution;
|
||||
- WebSearch secret ref resolution;
|
||||
- no plaintext in `Debug`/serialization paths checked by focused assertions.
|
||||
|
||||
Recommended next step:
|
||||
- Create a short spike/preflight sub-ticket specifically for key provider/dependency choice (`keyring` vs explicit passphrase vs other OS-backed provider). Once that decision is recorded, the implementation ticket can be split into:
|
||||
1. secret-store crate + CLI management;
|
||||
2. provider `AuthRef::SecretRef` integration;
|
||||
3. WebSearch secret ref integration and env migration docs.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: hare at: 2026-05-31T21:04:45Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Decision: keep the secret store as a provider-independent key-value store.
|
||||
|
||||
User decision:
|
||||
- The secret store path should be completely limited to a simple key-value store.
|
||||
- Do not hard-code provider-specific slots or provider-specific semantics into the store.
|
||||
- Do not require metadata in the store schema.
|
||||
- Conceptual model is:
|
||||
|
||||
```text
|
||||
{
|
||||
"anthropic/default" = "sk-..."
|
||||
}
|
||||
```
|
||||
|
||||
- Provider/model/tool configuration is responsible for choosing which secret key to reference.
|
||||
- The store does not know whether a value is an Anthropic key, Brave key, OpenAI key, token, or anything else.
|
||||
- Any provider-aware UX should be a higher-level helper that writes ordinary key-value entries and/or config references; it must not change the store schema.
|
||||
|
||||
Security stance:
|
||||
- Use light encryption/obfuscation at rest if practical, but do not claim strong security guarantees.
|
||||
- The goal is to avoid casual plaintext exposure in files, logs, work items, and accidental grep/cat output, not to defend against a local attacker with access to the user account.
|
||||
- Avoid complicated key-management requirements such as OS keychain dependency as a prerequisite for this ticket unless a later explicit decision changes the security target.
|
||||
- Documentation and diagnostics should be honest: this is an obfuscated/encrypted local key-value store with limited protection, not a high-assurance secret manager.
|
||||
|
||||
Implications for implementation planning:
|
||||
- Remove the previous requirement for metadata such as provider/kind/description/created_at/updated_at unless the implementation needs internal versioning/encryption fields.
|
||||
- Store format may still need technical envelope fields for version/nonce/ciphertext/checksum, but the user-visible logical schema is only `id -> value`.
|
||||
- Secret id validation remains useful because ids are referenced from manifest/profile/tool config and diagnostics.
|
||||
- Provider/WebSearch integration should resolve `secret_ref` by direct key lookup only.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: hare at: 2026-05-31T21:19:29Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
Preflight update: implementation-ready as a single phased ticket.
|
||||
|
||||
Finalized intent:
|
||||
- Build a provider-independent local key-value secret store and wire it into provider/WebSearch credential resolution.
|
||||
- Add `insomnia keys` as the required interactive TUI management surface.
|
||||
- Keep the security claim modest: avoid casual plaintext exposure and generic env scraping; do not claim strong local security.
|
||||
|
||||
Settled decisions:
|
||||
- Store model is user-visible `id -> value`; no provider-specific slots and no required metadata.
|
||||
- Store may use technical envelope fields for version/nonce/ciphertext/checksum, but those are implementation details.
|
||||
- No OS keychain, passphrase UX, or high-assurance key-management dependency in this ticket.
|
||||
- No automatic provider-name-to-secret-id lookup. Config must explicitly reference a key id.
|
||||
- This is one ticket with phases, not separate tickets.
|
||||
|
||||
Suggested implementation order:
|
||||
1. Add a focused secret store crate/module with id validation, obfuscated/encrypted persistence, atomic writes, and tests.
|
||||
2. Add `insomnia keys` TUI manager for list/add/delete with masked input.
|
||||
3. Implement provider `AuthRef::SecretRef` resolution through the store.
|
||||
4. Add WebSearch secret-ref configuration path.
|
||||
5. Update docs and migration messaging.
|
||||
|
||||
Critical risks for coder/reviewer:
|
||||
- Accidentally turning the store into provider-aware schema.
|
||||
- Displaying plaintext in Debug/errors/TUI list output.
|
||||
- Overstating security guarantees in docs.
|
||||
- Adding ambient defaults that recreate the env-var problem in secret-store form.
|
||||
- Expanding into OS keychain/passphrase design despite the settled modest threat model.
|
||||
|
||||
Validation should include focused tests for id validation, store round-trip, decode failure, provider secret-ref resolution, WebSearch secret-ref resolution, and no plaintext in display/debug paths where applicable.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: hare at: 2026-05-31T21:23:46Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Decision update:
|
||||
- Migration/backward compatibility for credential environment variables is not required.
|
||||
- The implementation should remove credential env configuration from the normal provider/WebSearch path rather than keep env fallback as a compatibility layer.
|
||||
- Docs should point to `insomnia keys` + explicit secret refs as the credential path.
|
||||
|
||||
Codex OAuth relationship:
|
||||
- Codex OAuth should not be connected to this key-value API-key store in this ticket.
|
||||
- The current Codex integration interoperates with Codex CLI's structured `auth.json` token bundle and refresh behavior.
|
||||
- That is a different credential shape than `id -> secret string` API keys.
|
||||
- Keep `CODEX_HOME` / `$HOME/.codex` behavior unchanged for this ticket.
|
||||
- If Insomnia later owns Codex login/token storage, design it as a separate OAuth token-store feature, not as an implicit use of the simple key-value store.
|
||||
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
<!-- event: create author: tickets.sh at: 2026-05-31T07:42:58Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by tickets.sh create.
|
||||
|
||||
---
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<!-- event: create author: tickets.sh at: 2026-05-31T07:42:58Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by tickets.sh create.
|
||||
|
||||
---
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<!-- event: create author: tickets.sh at: 2026-05-31T07:42:58Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by tickets.sh create.
|
||||
|
||||
---
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<!-- event: create author: tickets.sh at: 2026-05-31T08:59:59Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by tickets.sh create.
|
||||
|
||||
---
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<!-- event: create author: tickets.sh at: 2026-05-31T08:59:59Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by tickets.sh create.
|
||||
|
||||
---
|
||||
Loading…
Reference in New Issue
Block a user