manifest: embed builtin resources
This commit is contained in:
parent
607923dfbd
commit
365ec8b7fa
|
|
@ -25,9 +25,6 @@
|
||||||
|
|
||||||
use std::path::PathBuf;
|
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`,
|
/// 設定ディレクトリ。`profiles.toml`, `providers.toml`, `models.toml`,
|
||||||
/// `prompts/` などが置かれる。
|
/// `prompts/` などが置かれる。
|
||||||
pub fn config_dir() -> Option<PathBuf> {
|
pub fn config_dir() -> Option<PathBuf> {
|
||||||
|
|
@ -75,32 +72,6 @@ pub fn user_prompts_dir() -> Option<PathBuf> {
|
||||||
user_prompts_dir_from_config_dir(config_dir())
|
user_prompts_dir_from_config_dir(config_dir())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `<config_dir>/prompts.toml` — user prompt pack。
|
/// `<config_dir>/prompts.toml` — user prompt pack。
|
||||||
pub fn user_pack_file() -> Option<PathBuf> {
|
pub fn user_pack_file() -> Option<PathBuf> {
|
||||||
user_pack_file_from_config_dir(config_dir())
|
user_pack_file_from_config_dir(config_dir())
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ use crate::{
|
||||||
|
|
||||||
const PROFILE_FORMAT_V1: &str = "insomnia.lua-profile.v1";
|
const PROFILE_FORMAT_V1: &str = "insomnia.lua-profile.v1";
|
||||||
const BUILTIN_DEFAULT_PROFILE_NAME: &str = "default";
|
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";
|
const DEFAULT_POD_NAME: &str = "insomnia";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||||
|
|
@ -126,7 +128,10 @@ pub enum ProfileSource {
|
||||||
Registry {
|
Registry {
|
||||||
source: ProfileRegistrySource,
|
source: ProfileRegistrySource,
|
||||||
name: String,
|
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 struct ProfileRegistryEntry {
|
||||||
pub source: ProfileRegistrySource,
|
pub source: ProfileRegistrySource,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub path: PathBuf,
|
pub path: Option<PathBuf>,
|
||||||
|
pub provenance: String,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub is_default: bool,
|
pub is_default: bool,
|
||||||
|
artifact: ProfileRegistryArtifact,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProfileRegistryEntry {
|
impl ProfileRegistryEntry {
|
||||||
pub fn qualified_name(&self) -> String {
|
pub fn qualified_name(&self) -> String {
|
||||||
format!("{}:{}", self.source, self.name)
|
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)]
|
#[derive(Debug, Clone, Default)]
|
||||||
|
|
@ -241,7 +293,6 @@ struct ProfileDefault {
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ProfileDiscovery {
|
pub struct ProfileDiscovery {
|
||||||
builtin_dir: Option<PathBuf>,
|
|
||||||
user_config: Option<PathBuf>,
|
user_config: Option<PathBuf>,
|
||||||
project_config: Option<PathBuf>,
|
project_config: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
@ -249,27 +300,19 @@ pub struct ProfileDiscovery {
|
||||||
impl ProfileDiscovery {
|
impl ProfileDiscovery {
|
||||||
pub fn for_cwd(cwd: &Path) -> Self {
|
pub fn for_cwd(cwd: &Path) -> Self {
|
||||||
Self {
|
Self {
|
||||||
builtin_dir: paths::builtin_profiles_dir(),
|
|
||||||
user_config: paths::user_profiles_path(),
|
user_config: paths::user_profiles_path(),
|
||||||
project_config: find_project_profiles_from(cwd),
|
project_config: find_project_profiles_from(cwd),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn with_sources(
|
pub fn with_sources(user_config: Option<PathBuf>, project_config: Option<PathBuf>) -> Self {
|
||||||
builtin_dir: Option<PathBuf>,
|
|
||||||
user_config: Option<PathBuf>,
|
|
||||||
project_config: Option<PathBuf>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
builtin_dir,
|
|
||||||
user_config,
|
user_config,
|
||||||
project_config,
|
project_config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn discover(&self) -> Result<ProfileRegistry, ProfileError> {
|
pub fn discover(&self) -> Result<ProfileRegistry, ProfileError> {
|
||||||
let mut registry = ProfileRegistry::default();
|
let mut registry = ProfileRegistry::default();
|
||||||
if let Some(dir) = &self.builtin_dir {
|
add_builtin_profiles(&mut registry);
|
||||||
discover_profile_dir(&mut registry, ProfileRegistrySource::Builtin, dir)?;
|
|
||||||
}
|
|
||||||
if let Some(path) = &self.user_config {
|
if let Some(path) = &self.user_config {
|
||||||
load_profile_registry_file(&mut registry, ProfileRegistrySource::User, path)?;
|
load_profile_registry_file(&mut registry, ProfileRegistrySource::User, path)?;
|
||||||
}
|
}
|
||||||
|
|
@ -371,15 +414,27 @@ impl ProfileResolver {
|
||||||
)),
|
)),
|
||||||
ProfileSelector::Named { .. } | ProfileSelector::Default => {
|
ProfileSelector::Named { .. } | ProfileSelector::Default => {
|
||||||
let entry = registry.select(selector)?.clone();
|
let entry = registry.select(selector)?.clone();
|
||||||
self.resolve_path(
|
let source = ProfileSource::Registry {
|
||||||
&entry.path,
|
source: entry.source,
|
||||||
ProfileSource::Registry {
|
name: entry.name.clone(),
|
||||||
source: entry.source,
|
path: entry.path.as_deref().map(absolutize).transpose()?,
|
||||||
name: entry.name,
|
provenance: (entry.path.is_none()).then(|| entry.provenance.clone()),
|
||||||
path: absolutize(&entry.path)?,
|
};
|
||||||
},
|
self.resolve_registry_entry(&entry, source, options)
|
||||||
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,
|
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(
|
fn resolve_lua_profile_value(
|
||||||
|
|
@ -591,13 +670,12 @@ fn load_profile_registry_file(
|
||||||
let base = path.parent().unwrap_or_else(|| Path::new("."));
|
let base = path.parent().unwrap_or_else(|| Path::new("."));
|
||||||
for (name, entry_config) in config.profile {
|
for (name, entry_config) in config.profile {
|
||||||
let (entry_path, description) = entry_config.into_parts();
|
let (entry_path, description) = entry_config.into_parts();
|
||||||
registry.push_entry(ProfileRegistryEntry {
|
registry.push_entry(ProfileRegistryEntry::path(
|
||||||
source,
|
source,
|
||||||
name,
|
name,
|
||||||
path: join_if_relative(base, &entry_path),
|
join_if_relative(base, &entry_path),
|
||||||
description,
|
description,
|
||||||
is_default: false,
|
));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if let Some(default) = config.default {
|
if let Some(default) = config.default {
|
||||||
let (default_source, default_name) = parse_profile_ref(&default);
|
let (default_source, default_name) = parse_profile_ref(&default);
|
||||||
|
|
@ -625,49 +703,14 @@ fn find_project_profiles_from(start: &Path) -> Option<PathBuf> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn discover_profile_dir(
|
fn add_builtin_profiles(registry: &mut ProfileRegistry) {
|
||||||
registry: &mut ProfileRegistry,
|
registry.push_entry(ProfileRegistryEntry::embedded(
|
||||||
source: ProfileRegistrySource,
|
ProfileRegistrySource::Builtin,
|
||||||
dir: &Path,
|
BUILTIN_DEFAULT_PROFILE_NAME,
|
||||||
) -> Result<(), ProfileError> {
|
"builtin:default",
|
||||||
if !dir.is_dir() {
|
BUILTIN_DEFAULT_PROFILE,
|
||||||
return Ok(());
|
Some("Bundled default Insomnia coding profile".into()),
|
||||||
}
|
));
|
||||||
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 parse_profile_ref(raw: &str) -> (Option<ProfileRegistrySource>, String) {
|
fn parse_profile_ref(raw: &str) -> (Option<ProfileRegistrySource>, String) {
|
||||||
|
|
@ -687,15 +730,38 @@ fn evaluate_lua_profile(
|
||||||
path: path.to_path_buf(),
|
path: path.to_path_buf(),
|
||||||
source,
|
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(
|
let lua = Lua::new_with(
|
||||||
StdLib::TABLE | StdLib::STRING | StdLib::MATH | StdLib::UTF8,
|
StdLib::TABLE | StdLib::STRING | StdLib::MATH | StdLib::UTF8,
|
||||||
LuaOptions::default(),
|
LuaOptions::default(),
|
||||||
)
|
)
|
||||||
.map_err(ProfileError::Lua)?;
|
.map_err(ProfileError::Lua)?;
|
||||||
install_lua_api(&lua, module_root.to_path_buf())?;
|
install_lua_api(&lua, module_root)?;
|
||||||
let value: LuaValue = lua
|
let value: LuaValue = lua
|
||||||
.load(&content)
|
.load(content)
|
||||||
.set_name(path.display().to_string())
|
.set_name(chunk_name)
|
||||||
.eval()
|
.eval()
|
||||||
.map_err(ProfileError::Lua)?;
|
.map_err(ProfileError::Lua)?;
|
||||||
match value {
|
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 {
|
let loader = Rc::new(RefCell::new(LocalModuleLoader {
|
||||||
root: module_root,
|
root: module_root,
|
||||||
cache: HashMap::new(),
|
cache: HashMap::new(),
|
||||||
|
|
@ -743,11 +809,16 @@ fn install_lua_api(lua: &Lua, module_root: PathBuf) -> Result<(), ProfileError>
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LocalModuleLoader {
|
struct LocalModuleLoader {
|
||||||
root: PathBuf,
|
root: LocalModuleRoot,
|
||||||
cache: HashMap<String, RegistryKey>,
|
cache: HashMap<String, RegistryKey>,
|
||||||
loading: HashSet<String>,
|
loading: HashSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum LocalModuleRoot {
|
||||||
|
Filesystem(PathBuf),
|
||||||
|
Disabled { label: &'static str },
|
||||||
|
}
|
||||||
|
|
||||||
fn require_module(
|
fn require_module(
|
||||||
lua: &Lua,
|
lua: &Lua,
|
||||||
loader: &Rc<RefCell<LocalModuleLoader>>,
|
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| {
|
let content = std::fs::read_to_string(&path).map_err(|e| {
|
||||||
mlua::Error::RuntimeError(format!(
|
mlua::Error::RuntimeError(format!(
|
||||||
"failed to read local module `{name}` ({}): {e}",
|
"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> {
|
fn builtin_model_context_window(reference: &str) -> Option<u64> {
|
||||||
let (provider, model_id) = reference.split_once('/')?;
|
let (provider, model_id) = reference.split_once('/')?;
|
||||||
let path = paths::resource_dir()?.join("models").join("builtin.toml");
|
let parsed: toml::Value = toml::from_str(BUILTIN_MODEL_CATALOG).ok()?;
|
||||||
let content = std::fs::read_to_string(path).ok()?;
|
|
||||||
let parsed: toml::Value = toml::from_str(&content).ok()?;
|
|
||||||
for entry in parsed.get("model")?.as_array()? {
|
for entry in parsed.get("model")?.as_array()? {
|
||||||
let table = entry.as_table()?;
|
let table = entry.as_table()?;
|
||||||
if table.get("provider")?.as_str()? == provider && table.get("id")?.as_str()? == model_id {
|
if table.get("provider")?.as_str()? == provider && table.get("id")?.as_str()? == model_id {
|
||||||
|
|
@ -1187,58 +1268,8 @@ pub enum ProfileError {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{ReasoningControl, ReasoningEffort, SchemeKind};
|
use crate::{ReasoningControl, ReasoningEffort, SchemeKind};
|
||||||
use std::sync::{Mutex, MutexGuard, OnceLock};
|
|
||||||
use tempfile::TempDir;
|
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 {
|
fn write_profile(dir: &Path, name: &str, body: &str) -> PathBuf {
|
||||||
let path = dir.join(name);
|
let path = dir.join(name);
|
||||||
std::fs::write(&path, body).unwrap();
|
std::fs::write(&path, body).unwrap();
|
||||||
|
|
@ -1266,14 +1297,15 @@ mod tests {
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn builtin_default_profile_is_registered_as_default() {
|
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()
|
.discover()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let default = registry.default_entry().unwrap();
|
let default = registry.default_entry().unwrap();
|
||||||
assert_eq!(default.source, ProfileRegistrySource::Builtin);
|
assert_eq!(default.source, ProfileRegistrySource::Builtin);
|
||||||
assert_eq!(default.name, BUILTIN_DEFAULT_PROFILE_NAME);
|
assert_eq!(default.name, BUILTIN_DEFAULT_PROFILE_NAME);
|
||||||
assert!(default.is_default);
|
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]
|
#[test]
|
||||||
fn resolves_plain_lua_profile_with_runtime_pod_name_and_scope_intent() {
|
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(),
|
resolved.profile.as_ref().unwrap().name.as_deref(),
|
||||||
Some("default")
|
Some("default")
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolved.source,
|
||||||
|
ProfileSource::Registry {
|
||||||
|
source: ProfileRegistrySource::Builtin,
|
||||||
|
name: "default".into(),
|
||||||
|
path: None,
|
||||||
|
provenance: Some("builtin:default".into()),
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn unsupported_profile_extension_has_clear_diagnostic() {
|
fn unsupported_profile_extension_has_clear_diagnostic() {
|
||||||
|
|
@ -1486,14 +1527,19 @@ return profile {
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
std::fs::write(&project_config, "default = \"project:coder\"\n[profile.coder]\npath = \"profiles/project-coder.lua\"\ndescription = \"Project coder\"\n").unwrap();
|
std::fs::write(&project_config, "default = \"project:coder\"\n[profile.coder]\npath = \"profiles/project-coder.lua\"\ndescription = \"Project coder\"\n").unwrap();
|
||||||
let registry =
|
let registry = ProfileDiscovery::with_sources(Some(user_config), Some(project_config))
|
||||||
ProfileDiscovery::with_sources(None, Some(user_config), Some(project_config))
|
.discover()
|
||||||
.discover()
|
.unwrap();
|
||||||
.unwrap();
|
|
||||||
let default = registry.default_entry().unwrap();
|
let default = registry.default_entry().unwrap();
|
||||||
assert_eq!(default.source, ProfileRegistrySource::Project);
|
assert_eq!(default.source, ProfileRegistrySource::Project);
|
||||||
assert_eq!(default.name, "coder");
|
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]
|
#[test]
|
||||||
fn default_marks_direct_profile_entry() {
|
fn default_marks_direct_profile_entry() {
|
||||||
|
|
@ -1506,7 +1552,7 @@ return profile {
|
||||||
"default = \"coder\"\n[profile]\ncoder = \"profiles/coder.lua\"\n",
|
"default = \"coder\"\n[profile]\ncoder = \"profiles/coder.lua\"\n",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let registry = ProfileDiscovery::with_sources(None, None, Some(project_config))
|
let registry = ProfileDiscovery::with_sources(None, Some(project_config))
|
||||||
.discover()
|
.discover()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let default = registry.default_entry().unwrap();
|
let default = registry.default_entry().unwrap();
|
||||||
|
|
@ -1525,20 +1571,18 @@ return profile {
|
||||||
#[test]
|
#[test]
|
||||||
fn unqualified_ambiguous_names_fail_closed() {
|
fn unqualified_ambiguous_names_fail_closed() {
|
||||||
let mut registry = ProfileRegistry::default();
|
let mut registry = ProfileRegistry::default();
|
||||||
registry.push_entry(ProfileRegistryEntry {
|
registry.push_entry(ProfileRegistryEntry::path(
|
||||||
source: ProfileRegistrySource::User,
|
ProfileRegistrySource::User,
|
||||||
name: "coder".to_string(),
|
"coder".to_string(),
|
||||||
path: PathBuf::from("/user/coder.lua"),
|
PathBuf::from("/user/coder.lua"),
|
||||||
description: None,
|
None,
|
||||||
is_default: false,
|
));
|
||||||
});
|
registry.push_entry(ProfileRegistryEntry::path(
|
||||||
registry.push_entry(ProfileRegistryEntry {
|
ProfileRegistrySource::Project,
|
||||||
source: ProfileRegistrySource::Project,
|
"coder".to_string(),
|
||||||
name: "coder".to_string(),
|
PathBuf::from("/project/coder.lua"),
|
||||||
path: PathBuf::from("/project/coder.lua"),
|
None,
|
||||||
description: None,
|
));
|
||||||
is_default: false,
|
|
||||||
});
|
|
||||||
let err = registry
|
let err = registry
|
||||||
.select(&ProfileSelector::named("coder"))
|
.select(&ProfileSelector::named("coder"))
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
|
@ -1549,6 +1593,9 @@ return profile {
|
||||||
"coder",
|
"coder",
|
||||||
))
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(selected.path, PathBuf::from("/project/coder.lua"));
|
assert_eq!(
|
||||||
|
selected.path.as_deref(),
|
||||||
|
Some(Path::new("/project/coder.lua"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -932,7 +932,7 @@ mod tests {
|
||||||
std::fs::write(®istry_path, registry_toml).unwrap();
|
std::fs::write(®istry_path, registry_toml).unwrap();
|
||||||
AvailableProfiles {
|
AvailableProfiles {
|
||||||
registry: Some(
|
registry: Some(
|
||||||
ProfileDiscovery::with_sources(None, None, Some(registry_path))
|
ProfileDiscovery::with_sources(None, Some(registry_path))
|
||||||
.discover()
|
.discover()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
),
|
),
|
||||||
|
|
@ -1319,7 +1319,7 @@ return profile {
|
||||||
let project_config = project.join(".insomnia/profiles.toml");
|
let project_config = project.join(".insomnia/profiles.toml");
|
||||||
let ambiguous = AvailableProfiles {
|
let ambiguous = AvailableProfiles {
|
||||||
registry: Some(
|
registry: Some(
|
||||||
ProfileDiscovery::with_sources(None, Some(user_config), Some(project_config))
|
ProfileDiscovery::with_sources(Some(user_config), Some(project_config))
|
||||||
.discover()
|
.discover()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
INSOMNIA では、プロセス境界で本当に必要な場合を除き、環境変数の利用を避ける。新しい ambient な入力を増やすより、明示的な profile / manifest / config file / typed secret reference / CLI argument を優先する。
|
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 慣習との移行互換のために、一部の環境変数はまだサポートしている。この文書に載せた環境変数は公開 surface として扱う。ただし、fallback 変数は独立した設定項目ではなく、対応する main key の解決順の一部として扱う。開発・テスト都合だけの環境変数は追加しない。
|
||||||
|
|
||||||
## 原則
|
## 原則
|
||||||
|
|
||||||
|
|
@ -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 など。 |
|
| `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。 |
|
| `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 の置き場。 |
|
| `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 相当に扱う。
|
空の path 環境変数は、`manifest::paths` では原則として unset 相当に扱う。
|
||||||
|
|
||||||
|
|
@ -35,13 +34,9 @@ Path 系の環境変数は論理的な key ごとに立項する。`XDG_*` や `
|
||||||
|
|
||||||
このため、socket や pid file を `data_dir` に置かない。永続データと揮発 runtime state は分ける。
|
このため、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 と対応する。ユーザーが普段編集する場所ではない。
|
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.
|
||||||
|
|
||||||
`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 の代わりに使わない。
|
|
||||||
|
|
||||||
## Credential と外部 auth
|
## Credential と外部 auth
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ The default package is implemented by `package.nix` and builds the Cargo package
|
||||||
The package output contains:
|
The package output contains:
|
||||||
|
|
||||||
- `bin/insomnia` — terminal UI and `insomnia pod ...` runtime entrypoint.
|
- `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/nix.md` — this document.
|
||||||
- `share/doc/insomnia/environment.md` — environment-variable policy and supported variables.
|
- `share/doc/insomnia/environment.md` — environment-variable policy and supported variables.
|
||||||
|
|
||||||
|
|
@ -47,7 +46,7 @@ The Nix package does not put user configuration, sessions, sockets, or other mut
|
||||||
| Persistent data (`sessions/`, Pod metadata) | `INSOMNIA_DATA_DIR` | `$INSOMNIA_HOME` | `$HOME/.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` |
|
| 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.
|
Normal fresh startup is profile-based. The package includes the builtin default profile in the binary, 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
|
## Validation
|
||||||
|
|
||||||
|
|
@ -56,7 +55,8 @@ The package derivation has a credential-free install check that verifies:
|
||||||
- `insomnia pod --help` starts successfully.
|
- `insomnia pod --help` starts successfully.
|
||||||
- `insomnia` is installed and reaches argument parsing.
|
- `insomnia` is installed and reaches argument parsing.
|
||||||
- `bin/insomnia-pod` is not installed.
|
- `bin/insomnia-pod` is not installed.
|
||||||
- bundled prompt resources and this Nix usage document are present in the output.
|
- embedded builtin profiles do not require `share/insomnia/resources` at runtime.
|
||||||
|
- this Nix usage document is present in the output.
|
||||||
|
|
||||||
For full validation before handing changes to review, run:
|
For full validation before handing changes to review, run:
|
||||||
|
|
||||||
|
|
@ -73,4 +73,3 @@ These checks do not require provider credentials.
|
||||||
|
|
||||||
- The package currently installs only the `insomnia` command; development-only wrappers from `devshell.nix` are not part of the installable package.
|
- 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.
|
- 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
|
## 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
|
- 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
|
## Path resolution
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ rustPlatform.buildRustPackage rec {
|
||||||
filter = sourceFilter;
|
filter = sourceFilter;
|
||||||
};
|
};
|
||||||
|
|
||||||
cargoHash = "sha256-fisV77ZqAPsI0eLZIqw06HTj1CfmnL3NBHhjruZPZUE=";
|
cargoHash = "sha256-Nu+QAXwRhqqSwgc5/9XLwQEpjEnF54tWoEknM17wYq8=";
|
||||||
|
|
||||||
depsExtraArgs = {
|
depsExtraArgs = {
|
||||||
# nixpkgs 25.11's fetchCargoVendor still uses crates.io's API
|
# nixpkgs 25.11's fetchCargoVendor still uses crates.io's API
|
||||||
|
|
@ -94,8 +94,6 @@ rustPlatform.buildRustPackage rec {
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
install -Dm644 docs/nix.md "$out/share/doc/insomnia/nix.md"
|
install -Dm644 docs/nix.md "$out/share/doc/insomnia/nix.md"
|
||||||
install -Dm644 docs/environment.md "$out/share/doc/insomnia/environment.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;
|
doInstallCheck = true;
|
||||||
|
|
@ -105,13 +103,13 @@ rustPlatform.buildRustPackage rec {
|
||||||
"$out/bin/insomnia" pod --help >/dev/null
|
"$out/bin/insomnia" pod --help >/dev/null
|
||||||
test -x "$out/bin/insomnia"
|
test -x "$out/bin/insomnia"
|
||||||
test ! -e "$out/bin/insomnia-pod"
|
test ! -e "$out/bin/insomnia-pod"
|
||||||
|
test ! -e "$out/share/insomnia/resources"
|
||||||
if "$out/bin/insomnia" --session not-a-uuid 2>insomnia.err; then
|
if "$out/bin/insomnia" --session not-a-uuid 2>insomnia.err; then
|
||||||
echo "insomnia unexpectedly accepted an invalid --session value" >&2
|
echo "insomnia unexpectedly accepted an invalid --session value" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
grep -q "invalid --session UUID" insomnia.err
|
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/nix.md"
|
||||||
test -f "$out/share/doc/insomnia/environment.md"
|
test -f "$out/share/doc/insomnia/environment.md"
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user