feat: add manifest profile discovery
This commit is contained in:
+11
-11
@@ -61,7 +61,7 @@ fn resolve_socket(pod_name: &str, override_path: Option<PathBuf>) -> PathBuf {
|
||||
#[derive(Debug)]
|
||||
enum Mode {
|
||||
Spawn {
|
||||
profile_path: Option<PathBuf>,
|
||||
profile: Option<String>,
|
||||
},
|
||||
/// `insomnia <name>` / `insomnia --pod <name>`: attach to a live Pod by name if
|
||||
/// possible; otherwise launch `insomnia-pod --pod <name>` so the pod process
|
||||
@@ -113,7 +113,7 @@ where
|
||||
let mut multi = false;
|
||||
let mut session: Option<SegmentId> = None;
|
||||
let mut pod: Option<String> = None;
|
||||
let mut profile_path: Option<PathBuf> = 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;
|
||||
@@ -148,7 +148,7 @@ where
|
||||
let raw = args
|
||||
.get(i + 1)
|
||||
.ok_or(ParseError::MissingValue("--profile"))?;
|
||||
profile_path = Some(PathBuf::from(raw));
|
||||
profile = Some(raw.clone());
|
||||
i += 2;
|
||||
}
|
||||
"--socket" => {
|
||||
@@ -197,7 +197,7 @@ where
|
||||
"--multi and --socket are mutually exclusive",
|
||||
));
|
||||
}
|
||||
if profile_path.is_some() {
|
||||
if profile.is_some() {
|
||||
return Err(ParseError::Conflict(
|
||||
"--multi and --profile are mutually exclusive",
|
||||
));
|
||||
@@ -220,7 +220,7 @@ where
|
||||
"--pod and --resume are mutually exclusive",
|
||||
));
|
||||
}
|
||||
if profile_path.is_some()
|
||||
if profile.is_some()
|
||||
&& (resume || session.is_some() || pod.is_some() || positional.is_some() || socket_seen)
|
||||
{
|
||||
return Err(ParseError::Conflict(
|
||||
@@ -246,7 +246,7 @@ where
|
||||
socket_override,
|
||||
});
|
||||
}
|
||||
Ok(Mode::Spawn { profile_path })
|
||||
Ok(Mode::Spawn { profile })
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
@@ -270,7 +270,7 @@ async fn main() -> ExitCode {
|
||||
}
|
||||
|
||||
let result = match mode {
|
||||
Mode::Spawn { profile_path } => run_spawn(None, profile_path).await,
|
||||
Mode::Spawn { profile } => run_spawn(None, profile).await,
|
||||
Mode::PodName {
|
||||
pod_name,
|
||||
socket_override,
|
||||
@@ -473,9 +473,9 @@ fn is_recoverable_multi_open_error(error: &(dyn std::error::Error + 'static)) ->
|
||||
|
||||
async fn run_spawn(
|
||||
resume_from: Option<SegmentId>,
|
||||
profile_path: Option<PathBuf>,
|
||||
profile: Option<String>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let ready = match spawn::run(resume_from, profile_path).await? {
|
||||
let ready = match spawn::run(resume_from, profile).await? {
|
||||
SpawnOutcome::Ready(r) => r,
|
||||
SpawnOutcome::Cancelled => return Ok(()),
|
||||
};
|
||||
@@ -1182,8 +1182,8 @@ mod tests {
|
||||
#[test]
|
||||
fn parse_profile_spawn_mode() {
|
||||
match parse_args_from(["--profile", "/profiles/coder.nix"]).unwrap() {
|
||||
Mode::Spawn { profile_path } => {
|
||||
assert_eq!(profile_path, Some(PathBuf::from("/profiles/coder.nix")));
|
||||
Mode::Spawn { profile } => {
|
||||
assert_eq!(profile, Some("/profiles/coder.nix".to_string()));
|
||||
}
|
||||
_ => panic!("expected Spawn mode"),
|
||||
}
|
||||
|
||||
+68
-16
@@ -20,8 +20,8 @@ use std::time::Duration;
|
||||
use client::{SpawnConfig, spawn_pod};
|
||||
use crossterm::event::{self, Event as TermEvent, KeyCode, KeyEventKind, KeyModifiers};
|
||||
use manifest::{
|
||||
PodManifestConfig, ScopeConfig, find_project_manifest_from, load_layer, user_manifest_path,
|
||||
user_manifest_path_from_env,
|
||||
PodManifestConfig, ProfileDiscovery, ScopeConfig, find_project_manifest_from, load_layer,
|
||||
user_manifest_path, user_manifest_path_from_env,
|
||||
};
|
||||
use ratatui::Terminal;
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
@@ -93,11 +93,18 @@ type InlineTerminal = Terminal<CrosstermBackend<io::Stdout>>;
|
||||
/// passes `--session <id>` to the spawned `insomnia-pod` child.
|
||||
pub async fn run(
|
||||
resume_from: Option<SegmentId>,
|
||||
profile_path: Option<PathBuf>,
|
||||
profile: Option<String>,
|
||||
) -> Result<SpawnOutcome, SpawnError> {
|
||||
let defaults = load_spawn_defaults()?;
|
||||
let scope_origin = match profile_path.as_ref() {
|
||||
Some(path) => ScopeOrigin::FromProfile(path.clone()),
|
||||
let selected_profile = profile
|
||||
.map(|selector| ProfileSelection {
|
||||
label: selector.clone(),
|
||||
selector,
|
||||
is_default: false,
|
||||
})
|
||||
.or(defaults.default_profile);
|
||||
let scope_origin = match selected_profile.as_ref() {
|
||||
Some(profile) => ScopeOrigin::FromProfile(profile.label.clone()),
|
||||
None => defaults.scope_origin,
|
||||
};
|
||||
|
||||
@@ -112,7 +119,7 @@ pub async fn run(
|
||||
resume_from,
|
||||
resume_by_pod_name: false,
|
||||
resume_scope: None,
|
||||
profile_path,
|
||||
profile: selected_profile,
|
||||
};
|
||||
|
||||
let mut terminal = make_inline_terminal()?;
|
||||
@@ -212,6 +219,14 @@ struct SpawnDefaults {
|
||||
cascade_has_scope: bool,
|
||||
scope_origin: ScopeOrigin,
|
||||
default_name: String,
|
||||
default_profile: Option<ProfileSelection>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct ProfileSelection {
|
||||
selector: String,
|
||||
label: String,
|
||||
is_default: bool,
|
||||
}
|
||||
|
||||
fn load_spawn_defaults() -> Result<SpawnDefaults, SpawnError> {
|
||||
@@ -260,11 +275,24 @@ fn load_spawn_defaults() -> Result<SpawnDefaults, SpawnError> {
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| "pod".to_string());
|
||||
|
||||
let default_profile = default_profile_selection(&cwd);
|
||||
|
||||
Ok(SpawnDefaults {
|
||||
cwd,
|
||||
cascade_has_scope,
|
||||
scope_origin,
|
||||
default_name,
|
||||
default_profile,
|
||||
})
|
||||
}
|
||||
|
||||
fn default_profile_selection(cwd: &std::path::Path) -> Option<ProfileSelection> {
|
||||
let registry = ProfileDiscovery::for_cwd(cwd).discover().ok()?;
|
||||
let entry = registry.default_entry().ok()?;
|
||||
Some(ProfileSelection {
|
||||
selector: entry.qualified_name(),
|
||||
label: format!("{} (default)", entry.name),
|
||||
is_default: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -287,7 +315,7 @@ fn form_for_pod_name(pod_name: String, defaults: SpawnDefaults) -> Form {
|
||||
resume_from: None,
|
||||
resume_by_pod_name: true,
|
||||
resume_scope: None,
|
||||
profile_path: None,
|
||||
profile: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,7 +389,10 @@ async fn wait_for_ready(
|
||||
|
||||
let config = SpawnConfig {
|
||||
pod_name: form.name.clone(),
|
||||
profile_path: form.profile_path.clone(),
|
||||
profile: form
|
||||
.profile
|
||||
.as_ref()
|
||||
.map(|profile| profile.selector.clone()),
|
||||
overlay_toml: overlay_toml.to_string(),
|
||||
cwd,
|
||||
resume_from: form.resume_from,
|
||||
@@ -438,7 +469,7 @@ enum ScopeOrigin {
|
||||
FromUser,
|
||||
FromProject,
|
||||
CwdDefault,
|
||||
FromProfile(PathBuf),
|
||||
FromProfile(String),
|
||||
}
|
||||
|
||||
struct Form {
|
||||
@@ -476,7 +507,7 @@ struct Form {
|
||||
/// Optional Nix profile passed to `insomnia-pod --profile` for fresh spawns.
|
||||
/// This is not used for resume/attach flows because those must restore Pod
|
||||
/// state rather than re-evaluate a profile source.
|
||||
profile_path: Option<PathBuf>,
|
||||
profile: Option<ProfileSelection>,
|
||||
}
|
||||
|
||||
impl Form {
|
||||
@@ -608,13 +639,10 @@ fn context_line(form: &Form) -> Line<'_> {
|
||||
),
|
||||
Span::styled(" (write, default)", Style::default().fg(Color::DarkGray)),
|
||||
]),
|
||||
ScopeOrigin::FromProfile(ref path) => Line::from(vec![
|
||||
ScopeOrigin::FromProfile(ref label) => Line::from(vec![
|
||||
Span::raw(" "),
|
||||
Span::styled("profile: ", Style::default().fg(Color::DarkGray)),
|
||||
Span::styled(
|
||||
path.display().to_string(),
|
||||
Style::default().fg(Color::Green),
|
||||
),
|
||||
Span::styled(label.as_str(), Style::default().fg(Color::Green)),
|
||||
Span::styled(" (resolved by pod)", Style::default().fg(Color::DarkGray)),
|
||||
]),
|
||||
}
|
||||
@@ -663,7 +691,7 @@ mod tests {
|
||||
resume_from: None,
|
||||
resume_by_pod_name: false,
|
||||
resume_scope: None,
|
||||
profile_path: None,
|
||||
profile: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -674,6 +702,7 @@ mod tests {
|
||||
cascade_has_scope: true,
|
||||
scope_origin: ScopeOrigin::FromProject,
|
||||
default_name: "ignored".to_string(),
|
||||
default_profile: None,
|
||||
};
|
||||
let f = form_for_pod_name("agent".to_string(), defaults);
|
||||
|
||||
@@ -774,6 +803,29 @@ permission = "write"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_profile_selection_uses_project_registry_default() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let project = temp.path().join("project");
|
||||
let insomnia = project.join(".insomnia");
|
||||
std::fs::create_dir_all(&insomnia).unwrap();
|
||||
std::fs::write(
|
||||
insomnia.join("manifest.toml"),
|
||||
r#"
|
||||
[profiles]
|
||||
default = "coder"
|
||||
[profiles.profile]
|
||||
coder = "profiles/coder.nix"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let selected = default_profile_selection(&project).unwrap();
|
||||
assert_eq!(selected.selector, "project:coder");
|
||||
assert_eq!(selected.label, "coder (default)");
|
||||
assert!(selected.is_default);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn name_input_handles_insert_backspace_and_cursor() {
|
||||
let mut f = form("", false);
|
||||
|
||||
Reference in New Issue
Block a user