use std::fs;
use std::path::Path;
use anyhow::{Context, Result};
use super::discover::{
canonicalize_existing_path, discover_package_contents, import_claude_plugin_metadata,
import_codex_plugin_metadata, load_claude_marketplace_wrapper, load_claude_plugin_version,
load_codex_marketplace_wrapper, load_manifest_str, quote, should_try_plugin_wrapper_fallback,
};
use super::{DependencyKind, LoadedManifest, MANIFEST_FILE, Manifest, PackageRole};
use crate::paths::display_path;
pub fn load_root_from_dir(root: &Path) -> Result<LoadedManifest> {
load_from_dir(root, PackageRole::Root)
}
pub fn load_root_from_dir_allow_missing(root: &Path) -> Result<LoadedManifest> {
if root.exists() {
return load_root_from_dir(root);
}
let root = if root.is_absolute() {
root.to_path_buf()
} else {
std::env::current_dir()
.context("failed to determine the current directory")?
.join(root)
};
let loaded = LoadedManifest {
root,
manifest_path: None,
manifest: Manifest::default(),
discovered: super::PackageContents::default(),
warnings: Vec::new(),
claude_plugin: None,
extra_package_files: Vec::new(),
allows_empty_dependency_wrapper: false,
allows_unpinned_git_dependencies: false,
manifest_contents_override: None,
};
loaded.validate(PackageRole::Root)?;
Ok(loaded)
}
pub fn load_dependency_from_dir(root: &Path) -> Result<LoadedManifest> {
load_from_dir(root, PackageRole::Dependency)
}
pub fn load_from_dir(root: &Path, role: PackageRole) -> Result<LoadedManifest> {
let root = canonicalize_existing_path(root)
.with_context(|| format!("failed to access project root {}", root.display()))?;
let manifest_path = root.join(MANIFEST_FILE);
let (manifest, warnings, manifest_path) = if manifest_path.exists() {
let contents = fs::read_to_string(&manifest_path)
.with_context(|| format!("failed to read manifest {}", manifest_path.display()))?;
let (manifest, warnings) = load_manifest_str(&manifest_path, &contents)?;
(manifest, warnings, Some(manifest_path))
} else {
(Manifest::default(), Vec::new(), None)
};
let discovered = discover_package_contents(&root, &manifest, None)?;
let mut loaded = LoadedManifest {
root: root.clone(),
manifest_path,
manifest,
discovered,
warnings,
claude_plugin: None,
extra_package_files: Vec::new(),
allows_empty_dependency_wrapper: false,
allows_unpinned_git_dependencies: false,
manifest_contents_override: None,
};
if should_try_plugin_wrapper_fallback(&loaded) {
if let Some(marketplace_loaded) = load_claude_marketplace_wrapper(&loaded)? {
loaded = marketplace_loaded;
} else if let Some(marketplace_loaded) = load_codex_marketplace_wrapper(&loaded)? {
loaded = marketplace_loaded;
}
}
import_claude_plugin_metadata(&mut loaded)?;
import_codex_plugin_metadata(&mut loaded)?;
loaded.discovered = discover_package_contents(
&loaded.root,
&loaded.manifest,
loaded.claude_plugin.as_ref(),
)?;
if loaded.manifest.version.is_none() {
loaded.manifest.version = load_claude_plugin_version(&loaded.root, &mut loaded.warnings)?;
}
loaded.validate(role)?;
loaded.warnings.extend(
loaded
.workspace_member_statuses()?
.into_iter()
.filter_map(|member| member.warning),
);
loaded.warnings.sort();
loaded.warnings.dedup();
Ok(loaded)
}
pub fn serialize_manifest(manifest: &Manifest) -> Result<String> {
let mut output = String::new();
if let Some(api_version) = &manifest.api_version {
output.push_str(&format!("api_version = {}\n", quote(api_version)));
}
if let Some(name) = &manifest.name {
output.push_str(&format!("name = {}\n", quote(name)));
}
if let Some(version) = &manifest.version {
output.push_str(&format!("version = {}\n", quote(&version.to_string())));
}
if !manifest.content_roots.is_empty() {
let encoded = manifest
.content_roots
.iter()
.map(|path| quote(&display_path(path)))
.collect::<Vec<_>>()
.join(", ");
output.push_str(&format!("content_roots = [{encoded}]\n"));
}
if manifest.publish_root {
output.push_str("publish_root = true\n");
}
if !manifest.managed_exports.is_empty() {
if !output.is_empty() && !output.ends_with('\n') {
output.push('\n');
}
for managed_export in &manifest.managed_exports {
output.push_str("[[managed_exports]]\n");
output.push_str(&format!(
"source = {}\n",
quote(&display_path(&managed_export.source))
));
output.push_str(&format!(
"target = {}\n",
quote(&display_path(&managed_export.target))
));
if !super::ManagedPlacement::is_package(&managed_export.placement) {
output.push_str(&format!(
"placement = {}\n",
quote(match managed_export.placement {
super::ManagedPlacement::Package => "package",
super::ManagedPlacement::Project => "project",
})
));
}
output.push('\n');
}
}
if !manifest.capabilities.is_empty() {
if !output.is_empty() {
output.push('\n');
}
for capability in &manifest.capabilities {
output.push_str("[[capabilities]]\n");
output.push_str(&format!("id = {}\n", quote(&capability.id)));
output.push_str(&format!(
"sensitivity = {}\n",
quote(&capability.sensitivity)
));
if let Some(justification) = &capability.justification {
output.push_str(&format!("justification = {}\n", quote(justification)));
}
output.push('\n');
}
}
if !manifest.mcp_servers.is_empty() {
if !output.is_empty() && !output.ends_with('\n') {
output.push('\n');
}
for (id, server) in &manifest.mcp_servers {
output.push_str(&format!("[mcp_servers.{id}]\n"));
if let Some(transport_type) = &server.transport_type {
output.push_str(&format!("type = {}\n", quote(transport_type)));
}
if let Some(command) = &server.command {
output.push_str(&format!("command = {}\n", quote(command)));
}
if let Some(url) = &server.url {
output.push_str(&format!("url = {}\n", quote(url)));
}
if !server.args.is_empty() {
let encoded = server
.args
.iter()
.map(|arg| quote(arg))
.collect::<Vec<_>>()
.join(", ");
output.push_str(&format!("args = [{encoded}]\n"));
}
if let Some(cwd) = &server.cwd {
output.push_str(&format!("cwd = {}\n", quote(&display_path(cwd))));
}
if !server.enabled {
output.push_str("enabled = false\n");
}
if !server.env.is_empty() {
output.push_str("[mcp_servers.");
output.push_str(id);
output.push_str(".env]\n");
for (key, value) in &server.env {
output.push_str(&format!("{key} = {}\n", quote(value)));
}
}
if !server.headers.is_empty() {
output.push_str("[mcp_servers.");
output.push_str(id);
output.push_str(".headers]\n");
for (key, value) in &server.headers {
output.push_str(&format!("{key} = {}\n", quote(value)));
}
}
output.push('\n');
}
}
if let Some(adapters) = &manifest.adapters {
if !output.is_empty() && !output.ends_with('\n') {
output.push('\n');
}
output.push_str("[adapters]\n");
let mut enabled = adapters.enabled.clone();
enabled.sort();
let encoded = enabled
.into_iter()
.map(|adapter| quote(adapter.as_str()))
.collect::<Vec<_>>()
.join(", ");
output.push_str(&format!("enabled = [{encoded}]\n"));
}
if let Some(launch_hooks) = &manifest.launch_hooks {
if !output.is_empty() && !output.ends_with('\n') {
output.push('\n');
}
output.push_str("[launch_hooks]\n");
output.push_str(&format!(
"sync_on_startup = {}\n",
launch_hooks.sync_on_startup
));
}
if let Some(workspace) = &manifest.workspace {
if !output.is_empty() && !output.ends_with('\n') {
output.push('\n');
}
output.push_str("[workspace]\n");
let encoded = workspace
.members
.iter()
.map(|path| quote(&display_path(path)))
.collect::<Vec<_>>()
.join(", ");
output.push_str(&format!("members = [{encoded}]\n"));
for (id, member) in &workspace.package {
output.push('\n');
output.push_str(&format!("[workspace.package.{id}]\n"));
output.push_str(&format!("path = {}\n", quote(&display_path(&member.path))));
if let Some(name) = &member.name {
output.push_str(&format!("name = {}\n", quote(name)));
}
if let Some(codex) = &member.codex {
output.push('\n');
output.push_str(&format!("[workspace.package.{id}.codex]\n"));
output.push_str(&format!("category = {}\n", quote(&codex.category)));
output.push_str(&format!("installation = {}\n", quote(&codex.installation)));
output.push_str(&format!(
"authentication = {}\n",
quote(&codex.authentication)
));
}
}
}
append_dependency_section(&mut output, manifest, DependencyKind::Dependency);
append_dependency_section(&mut output, manifest, DependencyKind::DevDependency);
Ok(output)
}
fn append_dependency_section(output: &mut String, manifest: &Manifest, kind: DependencyKind) {
let dependencies = manifest.dependency_section(kind);
if dependencies.is_empty() {
return;
}
if !output.is_empty() && !output.ends_with('\n') {
output.push('\n');
}
output.push_str(&format!("[{}]\n", kind.manifest_section()));
for (alias, dependency) in dependencies {
if dependency.managed.is_some() {
continue;
}
output.push_str(&format!(
"{alias} = {{ {} }}\n",
dependency.inline_fields().join(", ")
));
}
for (alias, dependency) in dependencies {
let Some(managed) = &dependency.managed else {
continue;
};
if !output.is_empty() && !output.ends_with('\n') {
output.push('\n');
}
output.push_str(&format!("[{}.{alias}]\n", kind.manifest_section()));
for field in dependency.key_value_fields() {
output.push_str(&field);
output.push('\n');
}
for mapping in managed {
output.push('\n');
output.push_str(&format!(
"[[{}.{alias}.managed]]\n",
kind.manifest_section()
));
output.push_str(&format!(
"source = {}\n",
quote(&display_path(&mapping.source))
));
output.push_str(&format!(
"target = {}\n",
quote(&display_path(&mapping.target))
));
}
}
}