1use crate::ui;
7use console::style;
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10
11#[derive(Debug, clap::Args)]
16pub struct PackArgs {
17 #[command(subcommand)]
18 pub command: PackCommand,
19}
20
21#[derive(Debug, clap::Subcommand)]
22pub enum PackCommand {
23 Emit(EmitArgs),
25 Validate(ValidateArgs),
27}
28
29#[derive(Debug, clap::Args)]
30pub struct ValidateArgs {
31 #[arg(long, default_value = ".")]
33 pub dir: PathBuf,
34}
35
36#[derive(Debug, clap::Args)]
37pub struct EmitArgs {
38 #[arg(value_enum)]
40 pub format: EmitFormat,
41
42 #[arg(long, default_value = ".")]
44 pub dir: PathBuf,
45
46 #[arg(long)]
48 pub base_url: Option<String>,
49
50 #[arg(long, short)]
52 pub output: Option<PathBuf>,
53}
54
55#[derive(Debug, Clone, clap::ValueEnum)]
56pub enum EmitFormat {
57 AgentsMd,
59 LlmsTxt,
61 A2a,
63 Skills,
65 A2uiSchema,
67 All,
69}
70
71pub fn run(args: &PackArgs) -> anyhow::Result<()> {
76 match &args.command {
77 PackCommand::Emit(emit_args) => run_emit(emit_args),
78 PackCommand::Validate(validate_args) => run_validate(validate_args),
79 }
80}
81
82fn run_validate(args: &ValidateArgs) -> anyhow::Result<()> {
83 let dir = if args.dir == Path::new(".") {
84 std::env::current_dir()?
85 } else {
86 args.dir.clone()
87 };
88
89 let manifest_path = dir.join("cstate.toml");
90 if !manifest_path.exists() {
91 anyhow::bail!(
92 "No cstate.toml found in {}. Is this a CELLSTATE pack directory?",
93 dir.display()
94 );
95 }
96
97 let manifest = std::fs::read_to_string(&manifest_path)?;
98 let markdowns = discover_markdowns(&dir)?;
99 let contracts = discover_contracts(&dir)?;
100
101 let input = cellstate_pipeline::pack::PackInput {
102 root: dir.clone(),
103 manifest,
104 markdowns,
105 contracts,
106 };
107
108 match cellstate_pipeline::pack::compose_pack(input) {
109 Ok(_) => {
110 ui::ok(&format!("Pack at {} is valid", dir.display()));
111 Ok(())
112 }
113 Err(e) => {
114 eprintln!("{} {}", style("error:").red().bold(), e);
115 std::process::exit(1);
116 }
117 }
118}
119
120fn run_emit(args: &EmitArgs) -> anyhow::Result<()> {
121 let dir = if args.dir == Path::new(".") {
122 std::env::current_dir()?
123 } else {
124 args.dir.clone()
125 };
126
127 let manifest_path = dir.join("cstate.toml");
129 if !manifest_path.exists() {
130 anyhow::bail!(
131 "No cstate.toml found in {}. Is this a CELLSTATE pack directory?",
132 dir.display()
133 );
134 }
135
136 ui::info(&format!("Compiling pack from {}", dir.display()));
137
138 let manifest = std::fs::read_to_string(&manifest_path)?;
139 let markdowns = discover_markdowns(&dir)?;
140 let contracts = discover_contracts(&dir)?;
141
142 let input = cellstate_pipeline::pack::PackInput {
143 root: dir.clone(),
144 manifest,
145 markdowns,
146 contracts,
147 };
148
149 let output = cellstate_pipeline::pack::compose_pack(input)
150 .map_err(|e| anyhow::anyhow!("Pack compilation failed: {}", e))?;
151
152 let base_url = args.base_url.as_deref();
153
154 match args.format {
155 EmitFormat::AgentsMd => {
156 let md = cellstate_pipeline::pack::agents_md::generate_agents_md(&output.compiled);
157 write_output(args, &md, "agents.md")?;
158 }
159 EmitFormat::LlmsTxt => {
160 let txt =
161 cellstate_pipeline::pack::llms_txt::generate_llms_txt(&output.compiled, base_url);
162 write_output(args, &txt, "llms.txt")?;
163 }
164 EmitFormat::A2a => {
165 let card =
166 cellstate_pipeline::pack::a2a::generate_agent_card(&output.compiled, base_url);
167 let json = serde_json::to_string_pretty(&card)?;
168 write_output(args, &json, "agent.json")?;
169 }
170 EmitFormat::Skills => {
171 let pack = cellstate_pipeline::pack::skills::generate_skill_pack(&output.compiled);
172 let json = serde_json::to_string_pretty(&pack)?;
173 write_output(args, &json, "skills.json")?;
174 }
175 EmitFormat::A2uiSchema => {
176 let schema =
177 cellstate_pipeline::pack::a2ui_compat::generate_a2ui_schema(&output.compiled);
178 let json = serde_json::to_string_pretty(&schema)?;
179 write_output(args, &json, "a2ui-schema.json")?;
180 }
181 EmitFormat::All => {
182 emit_all(&output.compiled, args, base_url)?;
183 }
184 }
185
186 Ok(())
187}
188
189fn emit_all(
190 compiled: &cellstate_pipeline::compiler::CompiledConfig,
191 args: &EmitArgs,
192 base_url: Option<&str>,
193) -> anyhow::Result<()> {
194 let outputs: Vec<(&str, String)> = vec![
195 (
196 "agents.md",
197 cellstate_pipeline::pack::agents_md::generate_agents_md(compiled),
198 ),
199 (
200 "llms.txt",
201 cellstate_pipeline::pack::llms_txt::generate_llms_txt(compiled, base_url),
202 ),
203 (
204 "agent.json",
205 serde_json::to_string_pretty(&cellstate_pipeline::pack::a2a::generate_agent_card(
206 compiled, base_url,
207 ))?,
208 ),
209 (
210 "skills.json",
211 serde_json::to_string_pretty(&cellstate_pipeline::pack::skills::generate_skill_pack(
212 compiled,
213 ))?,
214 ),
215 (
216 "a2ui-schema.json",
217 serde_json::to_string_pretty(
218 &cellstate_pipeline::pack::a2ui_compat::generate_a2ui_schema(compiled),
219 )?,
220 ),
221 ];
222
223 if let Some(ref out_dir) = args.output {
224 std::fs::create_dir_all(out_dir)?;
226 for (filename, content) in &outputs {
227 let path = out_dir.join(filename);
228 std::fs::write(&path, content)?;
229 ui::ok(&format!("Wrote {}", path.display()));
230 }
231 } else {
232 for (filename, content) in &outputs {
234 println!("{} {}", style("───").dim(), style(filename).cyan().bold());
235 println!("{}", content);
236 println!();
237 }
238 }
239
240 ui::ok("All convergence outputs emitted");
241 Ok(())
242}
243
244fn write_output(args: &EmitArgs, content: &str, default_name: &str) -> anyhow::Result<()> {
245 if let Some(ref path) = args.output {
246 std::fs::write(path, content)?;
247 ui::ok(&format!("Wrote {}", path.display()));
248 } else {
249 print!("{}", content);
250 }
251 let _ = default_name; Ok(())
253}
254
255fn discover_markdowns(
261 dir: &PathBuf,
262) -> anyhow::Result<Vec<cellstate_pipeline::pack::PackMarkdownFile>> {
263 let mut files = Vec::new();
264
265 for entry in walkdir(dir)? {
266 let path = entry;
267 if path.extension().map(|e| e == "md").unwrap_or(false) {
268 let rel = path.strip_prefix(dir).unwrap_or(&path);
270 if rel
271 .components()
272 .any(|c| c.as_os_str().to_string_lossy().starts_with('.'))
273 {
274 continue;
275 }
276 let content = std::fs::read_to_string(&path)?;
277 files.push(cellstate_pipeline::pack::PackMarkdownFile {
278 path: path.clone(),
279 content,
280 });
281 }
282 }
283
284 Ok(files)
285}
286
287fn discover_contracts(dir: &PathBuf) -> anyhow::Result<HashMap<String, String>> {
289 let mut contracts = HashMap::new();
290 let contracts_dir = dir.join("contracts");
291 if contracts_dir.exists() {
292 for entry in walkdir(&contracts_dir)? {
293 if entry.extension().map(|e| e == "json").unwrap_or(false) {
294 let rel = entry
295 .strip_prefix(dir)
296 .unwrap_or(&entry)
297 .to_string_lossy()
298 .to_string();
299 let content = std::fs::read_to_string(&entry)?;
300 contracts.insert(rel, content);
301 }
302 }
303 }
304 Ok(contracts)
305}
306
307fn walkdir(dir: &PathBuf) -> anyhow::Result<Vec<PathBuf>> {
309 let mut results = Vec::new();
310 if !dir.is_dir() {
311 return Ok(results);
312 }
313 for entry in std::fs::read_dir(dir)? {
314 let entry = entry?;
315 let path = entry.path();
316 if path.is_dir() {
317 results.extend(walkdir(&path)?);
318 } else {
319 results.push(path);
320 }
321 }
322 Ok(results)
323}