feat: add manifest profile discovery

This commit is contained in:
2026-05-30 02:18:42 +09:00
parent 06c778a725
commit ee7147b355
8 changed files with 822 additions and 63 deletions
+11 -11
View File
@@ -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
View File
@@ -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);