use std::path::{Path, PathBuf};
use anyhow::{bail, Context as _};
use clap::Args;
use agent_block_core::embedded::{self, Entry};
use agent_block_core::host::PROJECT_DIR;
const PACKS: &[&str] = &["policy", "supervisor"];
#[derive(Debug, Args)]
pub struct VendorArgs {
#[arg(value_name = "NAME", required_unless_present = "list")]
pub names: Vec<String>,
#[arg(long, value_name = "DIR")]
pub path: Option<PathBuf>,
#[arg(long)]
pub force: bool,
#[arg(long, conflicts_with_all = ["names", "force"])]
pub list: bool,
}
#[derive(Debug)]
struct Vendored {
rel: PathBuf,
name: String,
source: &'static str,
}
#[derive(Debug)]
struct Plan {
files: Vec<Vendored>,
warning: Option<String>,
}
pub fn run(args: VendorArgs, project: &Path) -> anyhow::Result<()> {
let dir = args.path.clone().unwrap_or_else(|| project.to_path_buf());
if args.list {
print!("{}", listing(&dir));
return Ok(());
}
let mut files: Vec<Vendored> = Vec::new();
for name in &args.names {
let plan = resolve(name)?;
if let Some(warning) = plan.warning {
eprintln!("{warning}");
}
files.extend(plan.files);
}
let root = dir.join(PROJECT_DIR);
if !args.force {
for file in &files {
let target = root.join(&file.rel);
if target.exists() {
bail!(
"'{}' is already there. That copy is this project's own and may well have \
been edited; `--force` overwrites it.",
target.display()
);
}
}
}
for file in &files {
let target = root.join(&file.rel);
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("creating '{}'", parent.display()))?;
}
let body = render(&file.name, file.source);
std::fs::write(&target, body).with_context(|| format!("writing '{}'", target.display()))?;
println!("{}", target.display());
}
Ok(())
}
fn resolve(name: &str) -> anyhow::Result<Plan> {
if embedded::is_sealed(name) {
bail!(
"`{name}` is sealed: a project cannot shadow it (the kernel is the contract every \
block is written against); read it with require(\"embedded.{name}\")"
);
}
if let Some((root, _)) = name.split_once('.') {
bail!(
"`{name}` is part of `{root}`, and a module vendors whole: run `agent-block vendor \
{root}`, which writes `{root}` and every sub-module it has. Half a module on disk \
and half in memory is two versions of it under one name."
);
}
let Some(entry) = embedded::find(name) else {
bail!(
"`{name}` is not an embedded module. There is: {}. \
`agent-block vendor --list` says what each one is.",
roots().join(", ")
);
};
let mut files = vec![Vendored {
rel: module_path(name),
name: name.to_string(),
source: entry.source,
}];
for sub in subs_of(name) {
let tail = sub.name.trim_start_matches(name).trim_start_matches('.');
files.push(Vendored {
rel: PathBuf::from("lib")
.join(name)
.join(format!("{}.lua", tail.replace('.', "/"))),
name: sub.name.to_string(),
source: sub.source,
});
}
let warning = PACKS.contains(&name).then(|| {
format!(
"warning: `{name}` is a pack — a value you hand to `knl.device` or consult in your \
own loop, not a registry the host reads — so a whole copy of it is rarely the \
change you meant; for a partial one, delegate through require(\"embedded.{name}\"). \
Writing it anyway."
)
});
Ok(Plan { files, warning })
}
fn module_path(name: &str) -> PathBuf {
PathBuf::from("lib").join(name).join("init.lua")
}
fn subs_of(name: &str) -> Vec<&'static Entry> {
let prefix = format!("{name}.");
embedded::entries()
.iter()
.filter(|e| e.name.starts_with(&prefix))
.collect()
}
fn roots() -> Vec<&'static str> {
embedded::entries()
.iter()
.map(|e| e.name)
.filter(|name| !name.contains('.'))
.collect()
}
fn render(name: &str, source: &str) -> String {
format!("{}\n{source}", header(name))
}
fn header(name: &str) -> String {
format!(
"-- vendored from agent-block {version} (embedded {name})\n\
-- This copy is what require(\"{name}\") resolves to in this project;\n\
-- the original stays reachable as require(\"embedded.{name}\"). Edit freely; nothing keeps it in step with upstream.\n",
version = env!("CARGO_PKG_VERSION"),
)
}
const KIND: &str = "lib";
struct Row {
label: String,
tag: &'static str,
vendored: bool,
}
fn listing(dir: &Path) -> String {
let rows = rows(dir);
let width = rows.iter().map(|r| r.label.len()).max().unwrap_or(0);
let mut out = String::new();
for row in rows {
let mut line = format!("{:<width$} {KIND:<5}", row.label, width = width);
for extra in [row.tag, if row.vendored { "vendored" } else { "" }] {
if !extra.is_empty() {
line.push_str(" ");
line.push_str(extra);
}
}
out.push_str(line.trim_end());
out.push('\n');
}
out
}
fn rows(dir: &Path) -> Vec<Row> {
let root_dir = dir.join(PROJECT_DIR);
embedded::entries()
.iter()
.filter(|e| !e.name.contains('.'))
.map(|e| {
let subs = subs_of(e.name);
let label = if subs.is_empty() {
e.name.to_string()
} else {
let tails: Vec<&str> = subs
.iter()
.map(|s| s.name.trim_start_matches(e.name).trim_start_matches('.'))
.collect();
format!("{} (+{})", e.name, tails.join(", "))
};
let tag = if embedded::is_sealed(e.name) {
"sealed"
} else if PACKS.contains(&e.name) {
"pack"
} else {
""
};
Row {
label,
tag,
vendored: root_dir.join(module_path(e.name)).exists(),
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn rels(plan: &Plan) -> Vec<String> {
plan.files
.iter()
.map(|f| f.rel.display().to_string())
.collect()
}
#[test]
fn an_embedded_consumer_vendors_as_a_module() {
let plan = resolve("agent").expect("agent is embedded");
assert_eq!(rels(&plan), ["lib/agent/init.lua"]);
assert!(plan.warning.is_none());
assert!(plan.files[0].source.contains("function"));
let plan = resolve("coding").expect("coding is embedded");
assert_eq!(rels(&plan), ["lib/coding/init.lua"]);
}
#[test]
fn a_module_vendors_with_every_sub_module_it_has() {
let plan = resolve("llm_proto").expect("llm_proto is embedded");
assert_eq!(
rels(&plan),
[
"lib/llm_proto/init.lua",
"lib/llm_proto/openai.lua",
"lib/llm_proto/anthropic.lua",
]
);
assert_eq!(plan.files[1].name, "llm_proto.openai");
let plan = resolve("session").expect("session is embedded");
assert_eq!(rels(&plan), ["lib/session/init.lua"]);
}
#[test]
fn a_sub_module_on_its_own_is_refused_naming_the_root() {
let err = resolve("llm_proto.openai").expect_err("a sub-module is not a unit");
let msg = err.to_string();
assert!(msg.contains("vendor llm_proto"), "{msg}");
assert!(msg.contains("vendors whole"), "{msg}");
}
#[test]
fn a_sealed_name_is_refused_with_the_way_to_read_it() {
let err = resolve("knl").expect_err("knl is sealed");
let msg = err.to_string();
assert!(msg.contains("`knl` is sealed"), "{msg}");
assert!(msg.contains("require(\"embedded.knl\")"), "{msg}");
assert!(resolve("knl_types").is_err());
let err = resolve("lshape.t").expect_err("lshape.t is sealed");
assert!(err.to_string().contains("sealed"), "{err}");
}
#[test]
fn an_unknown_name_is_refused_listing_what_exists() {
let err = resolve("nope").expect_err("nope is not embedded");
let msg = err.to_string();
assert!(msg.contains("`nope` is not an embedded module"), "{msg}");
assert!(msg.contains("agent"), "{msg}");
assert!(msg.contains("session"), "{msg}");
assert!(!msg.contains("lshape.t"), "{msg}");
}
#[test]
fn a_pack_warns_and_is_written_anyway() {
let plan = resolve("policy").expect("policy is embedded");
assert_eq!(rels(&plan), ["lib/policy/init.lua"]);
let warning = plan.warning.expect("a pack warns");
assert!(warning.contains("knl.device"), "{warning}");
assert!(warning.contains("embedded.policy"), "{warning}");
}
#[test]
fn the_header_names_the_version_and_the_original() {
let body = render("session", "return {}\n");
let mut lines = body.lines();
assert_eq!(
lines.next(),
Some(
format!(
"-- vendored from agent-block {} (embedded session)",
env!("CARGO_PKG_VERSION")
)
.as_str()
)
);
assert!(body.contains("require(\"session\")"), "{body}");
assert!(body.contains("require(\"embedded.session\")"), "{body}");
assert!(!body.contains("-b session"), "{body}");
assert!(body.ends_with("\nreturn {}\n"), "{body}");
}
#[test]
fn the_listing_folds_sub_modules_and_marks_what_is_already_vendored() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::create_dir_all(dir.path().join(".agent-block/lib/session")).expect("mkdir");
std::fs::write(
dir.path().join(".agent-block/lib/session/init.lua"),
"return {}",
)
.expect("write");
let listing = listing(dir.path());
let line = |name: &str| {
listing
.lines()
.find(|l| l.starts_with(name))
.unwrap_or_else(|| panic!("no line for {name} in:\n{listing}"))
.to_string()
};
assert!(line("agent").contains("lib"), "{listing}");
assert!(!listing.contains("block"), "{listing}");
assert!(line("session").ends_with("vendored"), "{listing}");
assert!(!line("coding").contains("vendored"), "{listing}");
assert!(
line("lshape").starts_with("lshape (+t, check, reflect, luacats)"),
"{listing}"
);
assert!(line("lshape").contains("sealed"), "{listing}");
assert!(line("policy").contains("pack"), "{listing}");
assert!(
!listing.lines().any(|l| l.starts_with("lshape.t")),
"{listing}"
);
}
}