fix: align spawn user manifest env overlay
This commit is contained in:
parent
9435f44d53
commit
405339fb04
|
|
@ -3,7 +3,8 @@
|
||||||
//! Pod manifests are assembled from up to three on-disk layers (see
|
//! Pod manifests are assembled from up to three on-disk layers (see
|
||||||
//! `pod::PodFactory` for the full cascade story):
|
//! `pod::PodFactory` for the full cascade story):
|
||||||
//!
|
//!
|
||||||
//! 1. **User manifest** — see [`crate::paths::user_manifest_path`]
|
//! 1. **User manifest** — Pod CLI uses
|
||||||
|
//! [`crate::paths::user_manifest_path_with_env_override`]
|
||||||
//! 2. **Project manifest** at the closest `.insomnia/manifest.toml`
|
//! 2. **Project manifest** at the closest `.insomnia/manifest.toml`
|
||||||
//! found by walking up from a starting directory (typically `cwd`)
|
//! found by walking up from a starting directory (typically `cwd`)
|
||||||
//! 3. **Programmatic overlay** supplied at the call site
|
//! 3. **Programmatic overlay** supplied at the call site
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ pub use config::{
|
||||||
pub use model::{
|
pub use model::{
|
||||||
AuthRef, ModelCapability, ModelManifest, ReasoningControl, ReasoningEffort, SchemeKind,
|
AuthRef, ModelCapability, ModelManifest, ReasoningControl, ReasoningEffort, SchemeKind,
|
||||||
};
|
};
|
||||||
pub use paths::user_manifest_path;
|
pub use paths::{
|
||||||
|
user_manifest_path, user_manifest_path_from_env, user_manifest_path_with_env_override,
|
||||||
|
};
|
||||||
pub use protocol::{Permission, ScopeRule};
|
pub use protocol::{Permission, ScopeRule};
|
||||||
pub use scope::{Scope, ScopeError, SharedScope};
|
pub use scope::{Scope, ScopeError, SharedScope};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,16 @@
|
||||||
//! 解決された各 base が存在するか / ディレクトリかは保証しない —
|
//! 解決された各 base が存在するか / ディレクトリかは保証しない —
|
||||||
//! 呼び出し側がファイル操作の前に作成 / 検査する。
|
//! 呼び出し側がファイル操作の前に作成 / 検査する。
|
||||||
|
|
||||||
|
use std::ffi::OsString;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Environment variable that points at an explicit user manifest.
|
||||||
|
///
|
||||||
|
/// Pod CLI treats a non-empty value as an explicit manifest path. Empty values
|
||||||
|
/// are treated the same as an unset variable, so callers fall back to the
|
||||||
|
/// auto-discovered user manifest path.
|
||||||
|
pub const USER_MANIFEST_ENV: &str = "INSOMNIA_USER_MANIFEST";
|
||||||
|
|
||||||
/// 設定ディレクトリ。`manifest.toml`, `providers.toml`, `models.toml`,
|
/// 設定ディレクトリ。`manifest.toml`, `providers.toml`, `models.toml`,
|
||||||
/// `prompts/` などが置かれる。
|
/// `prompts/` などが置かれる。
|
||||||
pub fn config_dir() -> Option<PathBuf> {
|
pub fn config_dir() -> Option<PathBuf> {
|
||||||
|
|
@ -69,11 +77,38 @@ pub fn runtime_dir() -> Option<PathBuf> {
|
||||||
|
|
||||||
// ---- well-known file getters ------------------------------------------------
|
// ---- well-known file getters ------------------------------------------------
|
||||||
|
|
||||||
/// `<config_dir>/manifest.toml` — user manifest。
|
/// `<config_dir>/manifest.toml` — user manifest の既定位置。
|
||||||
|
///
|
||||||
|
/// This deliberately ignores [`USER_MANIFEST_ENV`]. Use
|
||||||
|
/// [`user_manifest_path_with_env_override`] when mirroring the Pod CLI cascade
|
||||||
|
/// resolution rules.
|
||||||
pub fn user_manifest_path() -> Option<PathBuf> {
|
pub fn user_manifest_path() -> Option<PathBuf> {
|
||||||
Some(config_dir()?.join("manifest.toml"))
|
Some(config_dir()?.join("manifest.toml"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve an explicit user manifest override from an env value.
|
||||||
|
///
|
||||||
|
/// Non-empty values are paths. `None` and empty strings are both treated as no
|
||||||
|
/// override, matching the Pod CLI's `INSOMNIA_USER_MANIFEST` handling.
|
||||||
|
pub fn user_manifest_path_from_env(value: Option<OsString>) -> Option<PathBuf> {
|
||||||
|
value.and_then(|value| {
|
||||||
|
if value.as_os_str().is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(PathBuf::from(value))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// User manifest path using the same env override rule as the Pod CLI cascade.
|
||||||
|
///
|
||||||
|
/// A non-empty [`USER_MANIFEST_ENV`] value wins. If the variable is unset or
|
||||||
|
/// empty, this falls back to [`user_manifest_path`]. The returned path is not
|
||||||
|
/// guaranteed to exist.
|
||||||
|
pub fn user_manifest_path_with_env_override() -> Option<PathBuf> {
|
||||||
|
user_manifest_path_from_env(std::env::var_os(USER_MANIFEST_ENV)).or_else(user_manifest_path)
|
||||||
|
}
|
||||||
|
|
||||||
/// `<config_dir>/prompts/` — user prompts ライブラリ。
|
/// `<config_dir>/prompts/` — user prompts ライブラリ。
|
||||||
pub fn user_prompts_dir() -> Option<PathBuf> {
|
pub fn user_prompts_dir() -> Option<PathBuf> {
|
||||||
Some(config_dir()?.join("prompts"))
|
Some(config_dir()?.join("prompts"))
|
||||||
|
|
@ -156,6 +191,7 @@ mod tests {
|
||||||
"INSOMNIA_CONFIG_DIR",
|
"INSOMNIA_CONFIG_DIR",
|
||||||
"INSOMNIA_DATA_DIR",
|
"INSOMNIA_DATA_DIR",
|
||||||
"INSOMNIA_RUNTIME_DIR",
|
"INSOMNIA_RUNTIME_DIR",
|
||||||
|
"INSOMNIA_USER_MANIFEST",
|
||||||
"INSOMNIA_HOME",
|
"INSOMNIA_HOME",
|
||||||
"XDG_CONFIG_HOME",
|
"XDG_CONFIG_HOME",
|
||||||
"XDG_RUNTIME_DIR",
|
"XDG_RUNTIME_DIR",
|
||||||
|
|
@ -281,6 +317,37 @@ mod tests {
|
||||||
assert!(runtime_dir().is_none());
|
assert!(runtime_dir().is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn user_manifest_env_override_wins_when_non_empty() {
|
||||||
|
let _g = EnvGuard::new(&[
|
||||||
|
("HOME", Some("/h")),
|
||||||
|
("INSOMNIA_USER_MANIFEST", Some("/tmp/user.toml")),
|
||||||
|
]);
|
||||||
|
assert_eq!(
|
||||||
|
user_manifest_path_with_env_override().unwrap(),
|
||||||
|
PathBuf::from("/tmp/user.toml")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_user_manifest_env_falls_back_to_default_path() {
|
||||||
|
let _g = EnvGuard::new(&[("HOME", Some("/h")), ("INSOMNIA_USER_MANIFEST", Some(""))]);
|
||||||
|
assert_eq!(
|
||||||
|
user_manifest_path_with_env_override().unwrap(),
|
||||||
|
PathBuf::from("/h/.config/insomnia/manifest.toml")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn user_manifest_path_from_env_treats_empty_as_unset() {
|
||||||
|
assert_eq!(user_manifest_path_from_env(None), None);
|
||||||
|
assert_eq!(user_manifest_path_from_env(Some(OsString::from(""))), None);
|
||||||
|
assert_eq!(
|
||||||
|
user_manifest_path_from_env(Some(OsString::from("/tmp/u.toml"))).unwrap(),
|
||||||
|
PathBuf::from("/tmp/u.toml")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn well_known_files_compose_off_base_dirs() {
|
fn well_known_files_compose_off_base_dirs() {
|
||||||
let _g = EnvGuard::new(&[("INSOMNIA_HOME", Some("/sand"))]);
|
let _g = EnvGuard::new(&[("INSOMNIA_HOME", Some("/sand"))]);
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@ use manifest::{PodManifest, PodManifestConfig, paths};
|
||||||
use pod::{Pod, PodController, PodFactory, PromptLoader};
|
use pod::{Pod, PodController, PodFactory, PromptLoader};
|
||||||
use session_store::{FsStore, PodMetadataStore, SegmentId, Store};
|
use session_store::{FsStore, PodMetadataStore, SegmentId, Store};
|
||||||
|
|
||||||
const USER_MANIFEST_ENV: &str = "INSOMNIA_USER_MANIFEST";
|
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
#[command(
|
#[command(
|
||||||
name = "pod",
|
name = "pod",
|
||||||
|
|
@ -68,19 +66,20 @@ struct Cli {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_manifest(cli: &Cli) -> Result<(PodManifest, PromptLoader), String> {
|
fn resolve_manifest(cli: &Cli) -> Result<(PodManifest, PromptLoader), String> {
|
||||||
resolve_manifest_with_user_manifest_env(cli, std::env::var_os(USER_MANIFEST_ENV))
|
resolve_manifest_with_user_manifest_env(cli, std::env::var_os(paths::USER_MANIFEST_ENV))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_manifest_with_user_manifest_env(
|
fn resolve_manifest_with_user_manifest_env(
|
||||||
cli: &Cli,
|
cli: &Cli,
|
||||||
user_manifest_env: Option<OsString>,
|
user_manifest_env: Option<OsString>,
|
||||||
) -> Result<(PodManifest, PromptLoader), String> {
|
) -> Result<(PodManifest, PromptLoader), String> {
|
||||||
let user_manifest = user_manifest_path_from_env(user_manifest_env);
|
let user_manifest = paths::user_manifest_path_from_env(user_manifest_env);
|
||||||
|
|
||||||
if let Some(path) = &cli.manifest {
|
if let Some(path) = &cli.manifest {
|
||||||
if user_manifest.is_some() {
|
if user_manifest.is_some() {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"--manifest cannot be used when {USER_MANIFEST_ENV} is set"
|
"--manifest cannot be used when {} is set",
|
||||||
|
paths::USER_MANIFEST_ENV
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
return load_single_manifest(path, cli.pod.as_deref());
|
return load_single_manifest(path, cli.pod.as_deref());
|
||||||
|
|
@ -92,16 +91,6 @@ fn resolve_manifest_with_user_manifest_env(
|
||||||
.map_err(|e| format!("failed to resolve manifest cascade: {e}"))
|
.map_err(|e| format!("failed to resolve manifest cascade: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn user_manifest_path_from_env(value: Option<OsString>) -> Option<PathBuf> {
|
|
||||||
value.and_then(|value| {
|
|
||||||
if value.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(PathBuf::from(value))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_single_manifest(
|
fn load_single_manifest(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
pod_name_override: Option<&str>,
|
pod_name_override: Option<&str>,
|
||||||
|
|
@ -408,7 +397,7 @@ permission = "write"
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
|
||||||
assert!(err.contains("--manifest cannot be used"));
|
assert!(err.contains("--manifest cannot be used"));
|
||||||
assert!(err.contains(USER_MANIFEST_ENV));
|
assert!(err.contains(paths::USER_MANIFEST_ENV));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
//! The viewport's last frame stays in the terminal's scrollback so the
|
//! The viewport's last frame stays in the terminal's scrollback so the
|
||||||
//! user has a record of what was spawned (or why a spawn failed).
|
//! user has a record of what was spawned (or why a spawn failed).
|
||||||
|
|
||||||
|
use std::ffi::OsString;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
@ -20,6 +21,7 @@ use client::{SpawnConfig, spawn_pod};
|
||||||
use crossterm::event::{self, Event as TermEvent, KeyCode, KeyEventKind, KeyModifiers};
|
use crossterm::event::{self, Event as TermEvent, KeyCode, KeyEventKind, KeyModifiers};
|
||||||
use manifest::{
|
use manifest::{
|
||||||
PodManifestConfig, ScopeConfig, find_project_manifest_from, load_layer, user_manifest_path,
|
PodManifestConfig, ScopeConfig, find_project_manifest_from, load_layer, user_manifest_path,
|
||||||
|
user_manifest_path_from_env,
|
||||||
};
|
};
|
||||||
use ratatui::Terminal;
|
use ratatui::Terminal;
|
||||||
use ratatui::backend::CrosstermBackend;
|
use ratatui::backend::CrosstermBackend;
|
||||||
|
|
@ -210,9 +212,15 @@ fn load_spawn_defaults() -> Result<SpawnDefaults, SpawnError> {
|
||||||
// Run the same merge pod itself uses, then read what's missing off the
|
// Run the same merge pod itself uses, then read what's missing off the
|
||||||
// result. We only look at `scope.allow` here — `pod.name` is an
|
// result. We only look at `scope.allow` here — `pod.name` is an
|
||||||
// instance-level identifier and is supplied by the dialog or `--pod`.
|
// instance-level identifier and is supplied by the dialog or `--pod`.
|
||||||
let user_layer = user_manifest_path()
|
// TUI must pre-read the same user manifest path that the pod CLI will use,
|
||||||
.filter(|p| p.is_file())
|
// including a non-empty INSOMNIA_USER_MANIFEST override; empty values fall
|
||||||
.and_then(|p| load_layer(&p).ok());
|
// back to the auto-discovered path.
|
||||||
|
let user_layer = user_manifest_path_for_spawn(
|
||||||
|
std::env::var_os(manifest::paths::USER_MANIFEST_ENV),
|
||||||
|
user_manifest_path(),
|
||||||
|
)
|
||||||
|
.filter(|p| p.is_file())
|
||||||
|
.and_then(|p| load_layer(&p).ok());
|
||||||
let project_layer = find_project_manifest_from(&cwd).and_then(|p| load_layer(&p).ok());
|
let project_layer = find_project_manifest_from(&cwd).and_then(|p| load_layer(&p).ok());
|
||||||
|
|
||||||
let mut cascade = PodManifestConfig::builtin_defaults();
|
let mut cascade = PodManifestConfig::builtin_defaults();
|
||||||
|
|
@ -252,6 +260,13 @@ fn load_spawn_defaults() -> Result<SpawnDefaults, SpawnError> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn user_manifest_path_for_spawn(
|
||||||
|
env_value: Option<OsString>,
|
||||||
|
default_user_manifest: Option<PathBuf>,
|
||||||
|
) -> Option<PathBuf> {
|
||||||
|
user_manifest_path_from_env(env_value).or(default_user_manifest)
|
||||||
|
}
|
||||||
|
|
||||||
fn form_for_pod_name(pod_name: String, defaults: SpawnDefaults) -> Form {
|
fn form_for_pod_name(pod_name: String, defaults: SpawnDefaults) -> Form {
|
||||||
Form {
|
Form {
|
||||||
cwd: defaults.cwd,
|
cwd: defaults.cwd,
|
||||||
|
|
@ -712,6 +727,28 @@ permission = "write"
|
||||||
assert!(empty_cascade.scope.allow.is_empty());
|
assert!(empty_cascade.scope.allow.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn user_manifest_path_for_spawn_prefers_non_empty_env_override() {
|
||||||
|
assert_eq!(
|
||||||
|
user_manifest_path_for_spawn(
|
||||||
|
Some(OsString::from("/tmp/override.toml")),
|
||||||
|
Some(PathBuf::from("/default/manifest.toml")),
|
||||||
|
),
|
||||||
|
Some(PathBuf::from("/tmp/override.toml")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn user_manifest_path_for_spawn_treats_empty_env_as_unset() {
|
||||||
|
assert_eq!(
|
||||||
|
user_manifest_path_for_spawn(
|
||||||
|
Some(OsString::from("")),
|
||||||
|
Some(PathBuf::from("/default/manifest.toml")),
|
||||||
|
),
|
||||||
|
Some(PathBuf::from("/default/manifest.toml")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn name_input_handles_insert_backspace_and_cursor() {
|
fn name_input_handles_insert_backspace_and_cursor() {
|
||||||
let mut f = form("", false);
|
let mut f = form("", false);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user