feat: add nix manifest profile foundation

This commit is contained in:
2026-05-30 01:41:06 +09:00
parent 1d64c50cf2
commit 5de31a9be2
16 changed files with 850 additions and 23 deletions
+87 -6
View File
@@ -60,7 +60,9 @@ fn resolve_socket(pod_name: &str, override_path: Option<PathBuf>) -> PathBuf {
#[derive(Debug)]
enum Mode {
Spawn,
Spawn {
profile_path: Option<PathBuf>,
},
/// `insomnia <name>` / `insomnia --pod <name>`: attach to a live Pod by name if
/// possible; otherwise launch `insomnia-pod --pod <name>` so the pod process
/// resumes from name-keyed state or creates a fresh same-name Pod.
@@ -111,6 +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 socket_override: Option<PathBuf> = None;
let mut socket_seen = false;
let mut positional: Option<String> = None;
@@ -141,6 +144,13 @@ where
pod = Some(raw.clone());
i += 2;
}
"--profile" => {
let raw = args
.get(i + 1)
.ok_or(ParseError::MissingValue("--profile"))?;
profile_path = Some(PathBuf::from(raw));
i += 2;
}
"--socket" => {
socket_seen = true;
let raw = args
@@ -187,6 +197,11 @@ where
"--multi and --socket are mutually exclusive",
));
}
if profile_path.is_some() {
return Err(ParseError::Conflict(
"--multi and --profile are mutually exclusive",
));
}
return Ok(Mode::Multi);
}
@@ -205,6 +220,13 @@ where
"--pod and --resume are mutually exclusive",
));
}
if profile_path.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 {
@@ -224,7 +246,7 @@ where
socket_override,
});
}
Ok(Mode::Spawn)
Ok(Mode::Spawn { profile_path })
}
#[tokio::main]
@@ -248,13 +270,13 @@ async fn main() -> ExitCode {
}
let result = match mode {
Mode::Spawn => run_spawn(None).await,
Mode::Spawn { profile_path } => run_spawn(None, profile_path).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)).await,
Mode::ResumeWithSession(id) => run_spawn(Some(id), None).await,
Mode::Multi => run_multi().await,
};
@@ -449,8 +471,11 @@ fn is_recoverable_multi_open_error(error: &(dyn std::error::Error + 'static)) ->
error.is::<spawn::SpawnError>() || error.is::<NestedOpenCancelled>()
}
async fn run_spawn(resume_from: Option<SegmentId>) -> Result<(), Box<dyn std::error::Error>> {
let ready = match spawn::run(resume_from).await? {
async fn run_spawn(
resume_from: Option<SegmentId>,
profile_path: Option<PathBuf>,
) -> Result<(), Box<dyn std::error::Error>> {
let ready = match spawn::run(resume_from, profile_path).await? {
SpawnOutcome::Ready(r) => r,
SpawnOutcome::Cancelled => return Ok(()),
};
@@ -1154,6 +1179,62 @@ 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")));
}
_ => 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.nix".to_string(),
"--resume".to_string(),
],
"--profile can only be used for fresh spawn",
),
(
vec![
"--profile".to_string(),
"p.nix".to_string(),
"--session".to_string(),
segment_id,
],
"--profile can only be used for fresh spawn",
),
(
vec![
"--profile".to_string(),
"p.nix".to_string(),
"--socket".to_string(),
"/tmp/insomnia/sock".to_string(),
],
"--profile can only be used for fresh spawn",
),
(
vec![
"--profile".to_string(),
"p.nix".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() {
+27 -2
View File
@@ -91,13 +91,20 @@ type InlineTerminal = Terminal<CrosstermBackend<io::Stdout>>;
/// Source session for a resume run. `None` = fresh spawn (current
/// behaviour); `Some(id)` swaps the dialog into "Resume Pod" mode and
/// passes `--session <id>` to the spawned `insomnia-pod` child.
pub async fn run(resume_from: Option<SegmentId>) -> Result<SpawnOutcome, SpawnError> {
pub async fn run(
resume_from: Option<SegmentId>,
profile_path: Option<PathBuf>,
) -> Result<SpawnOutcome, SpawnError> {
let defaults = load_spawn_defaults()?;
let scope_origin = match profile_path.as_ref() {
Some(path) => ScopeOrigin::FromProfile(path.clone()),
None => defaults.scope_origin,
};
let mut form = Form {
cwd: defaults.cwd.clone(),
cascade_has_scope: defaults.cascade_has_scope,
scope_origin: defaults.scope_origin,
scope_origin,
name_cursor: defaults.default_name.chars().count(),
name: defaults.default_name,
message: None,
@@ -105,6 +112,7 @@ pub async fn run(resume_from: Option<SegmentId>) -> Result<SpawnOutcome, SpawnEr
resume_from,
resume_by_pod_name: false,
resume_scope: None,
profile_path,
};
let mut terminal = make_inline_terminal()?;
@@ -279,6 +287,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,
}
}
@@ -352,6 +361,7 @@ async fn wait_for_ready(
let config = SpawnConfig {
pod_name: form.name.clone(),
profile_path: form.profile_path.clone(),
overlay_toml: overlay_toml.to_string(),
cwd,
resume_from: form.resume_from,
@@ -428,6 +438,7 @@ enum ScopeOrigin {
FromUser,
FromProject,
CwdDefault,
FromProfile(PathBuf),
}
struct Form {
@@ -462,6 +473,10 @@ struct Form {
/// resume runs, and serialized into the overlay instead of cwd-default
/// scope so resume does not silently broaden access.
resume_scope: Option<ScopeConfig>,
/// 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>,
}
impl Form {
@@ -593,6 +608,15 @@ fn context_line(form: &Form) -> Line<'_> {
),
Span::styled(" (write, default)", Style::default().fg(Color::DarkGray)),
]),
ScopeOrigin::FromProfile(ref path) => 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(" (resolved by pod)", Style::default().fg(Color::DarkGray)),
]),
}
}
@@ -639,6 +663,7 @@ mod tests {
resume_from: None,
resume_by_pod_name: false,
resume_scope: None,
profile_path: None,
}
}