1pub mod core;
79pub mod plugins;
80
81use core::{
82 db, docs_cli, error, migration, proof, repomap, scaffold,
83 store::{Store, StoreKind},
84 tui, validate,
85};
86use plugins::{
87 archive, context, cron, feedback, health, knowledge, policy, reflex, teammate, todo, verify,
88 watcher,
89};
90
91use clap::{Parser, Subcommand};
92use std::fs;
93use std::path::{Path, PathBuf};
94
95#[derive(Parser, Debug)]
96#[clap(
97 name = "decapod",
98 version = env!("CARGO_PKG_VERSION"),
99 about = "The Intent-Driven Engineering System"
100)]
101struct Cli {
102 #[clap(subcommand)]
103 command: Command,
104}
105
106#[derive(clap::Args, Debug)]
107struct ValidateCli {
108 #[clap(long, default_value = "repo")]
110 store: String,
111 #[clap(long, default_value = "text")]
113 format: String,
114}
115
116#[derive(clap::Args, Debug)]
119struct InitGroupCli {
120 #[clap(subcommand)]
121 command: Option<InitCommand>,
122 #[clap(short, long)]
124 dir: Option<PathBuf>,
125 #[clap(long)]
127 force: bool,
128 #[clap(long)]
130 dry_run: bool,
131 #[clap(long)]
133 all: bool,
134 #[clap(long)]
136 claude: bool,
137 #[clap(long)]
139 gemini: bool,
140 #[clap(long)]
142 agents: bool,
143}
144
145#[derive(Subcommand, Debug)]
146enum InitCommand {
147 Clean {
149 #[clap(short, long)]
151 dir: Option<PathBuf>,
152 },
153}
154
155#[derive(clap::Args, Debug)]
156struct SetupCli {
157 #[clap(subcommand)]
158 command: SetupCommand,
159}
160
161#[derive(Subcommand, Debug)]
162enum SetupCommand {
163 Hook {
165 #[clap(long, default_value = "true")]
167 commit_msg: bool,
168 #[clap(long)]
170 pre_commit: bool,
171 #[clap(long)]
173 uninstall: bool,
174 },
175}
176
177#[derive(clap::Args, Debug)]
178struct GovernCli {
179 #[clap(subcommand)]
180 command: GovernCommand,
181}
182
183#[derive(Subcommand, Debug)]
184enum GovernCommand {
185 Policy(policy::PolicyCli),
187
188 Health(health::HealthCli),
190
191 Proof(ProofCommandCli),
193
194 Watcher(WatcherCli),
196
197 Feedback(FeedbackCli),
199}
200
201#[derive(clap::Args, Debug)]
202struct DataCli {
203 #[clap(subcommand)]
204 command: DataCommand,
205}
206
207#[derive(Subcommand, Debug)]
208enum DataCommand {
209 Archive(ArchiveCli),
211
212 Knowledge(KnowledgeCli),
214
215 Context(ContextCli),
217
218 Schema(SchemaCli),
220
221 Repo(RepoCli),
223
224 Broker(BrokerCli),
226
227 Teammate(teammate::TeammateCli),
229}
230
231#[derive(clap::Args, Debug)]
232struct AutoCli {
233 #[clap(subcommand)]
234 command: AutoCommand,
235}
236
237#[derive(Subcommand, Debug)]
238enum AutoCommand {
239 Cron(cron::CronCli),
241
242 Reflex(reflex::ReflexCli),
244}
245
246#[derive(clap::Args, Debug)]
247struct QaCli {
248 #[clap(subcommand)]
249 command: QaCommand,
250}
251
252#[derive(Subcommand, Debug)]
253enum QaCommand {
254 Verify(verify::VerifyCli),
256
257 Check {
259 #[clap(long)]
261 crate_description: bool,
262 #[clap(long)]
264 all: bool,
265 },
266
267 Gatling(plugins::gatling::GatlingCli),
269}
270
271#[derive(Subcommand, Debug)]
274enum Command {
275 #[clap(name = "init", visible_alias = "i")]
277 Init(InitGroupCli),
278
279 #[clap(name = "setup")]
281 Setup(SetupCli),
282
283 #[clap(name = "docs", visible_alias = "d")]
285 Docs(docs_cli::DocsCli),
286
287 #[clap(name = "todo", visible_alias = "t")]
289 Todo(todo::TodoCli),
290
291 #[clap(name = "validate", visible_alias = "v")]
293 Validate(ValidateCli),
294
295 #[clap(name = "update")]
297 Update,
298
299 #[clap(name = "version")]
301 Version,
302
303 #[clap(name = "govern", visible_alias = "g")]
305 Govern(GovernCli),
306
307 #[clap(name = "data")]
309 Data(DataCli),
310
311 #[clap(name = "auto", visible_alias = "a")]
313 Auto(AutoCli),
314
315 #[clap(name = "qa", visible_alias = "q")]
317 Qa(QaCli),
318}
319
320#[derive(clap::Args, Debug)]
321struct BrokerCli {
322 #[clap(subcommand)]
323 command: BrokerCommand,
324}
325
326#[derive(Subcommand, Debug)]
327enum BrokerCommand {
328 Audit,
330}
331
332#[derive(clap::Args, Debug)]
333struct KnowledgeCli {
334 #[clap(subcommand)]
335 command: KnowledgeCommand,
336}
337
338#[derive(Subcommand, Debug)]
339enum KnowledgeCommand {
340 Add {
342 #[clap(long)]
343 id: String,
344 #[clap(long)]
345 title: String,
346 #[clap(long)]
347 text: String,
348 #[clap(long)]
349 provenance: String,
350 #[clap(long)]
351 claim_id: Option<String>,
352 },
353 Search {
355 #[clap(long)]
356 query: String,
357 },
358}
359
360#[derive(clap::Args, Debug)]
361struct RepoCli {
362 #[clap(subcommand)]
363 command: RepoCommand,
364}
365
366#[derive(Subcommand, Debug)]
367enum RepoCommand {
368 Map,
370 Graph,
372}
373
374#[derive(clap::Args, Debug)]
375struct WatcherCli {
376 #[clap(subcommand)]
377 command: WatcherCommand,
378}
379
380#[derive(Subcommand, Debug)]
381enum WatcherCommand {
382 Run,
384}
385
386#[derive(clap::Args, Debug)]
387struct ArchiveCli {
388 #[clap(subcommand)]
389 command: ArchiveCommand,
390}
391
392#[derive(Subcommand, Debug)]
393enum ArchiveCommand {
394 List,
396 Verify,
398}
399
400#[derive(clap::Args, Debug)]
401struct FeedbackCli {
402 #[clap(subcommand)]
403 command: FeedbackCommand,
404}
405
406#[derive(Subcommand, Debug)]
407enum FeedbackCommand {
408 Add {
410 #[clap(long)]
411 source: String,
412 #[clap(long)]
413 text: String,
414 #[clap(long)]
415 links: Option<String>,
416 },
417 Propose,
419}
420
421#[derive(clap::Args, Debug)]
422pub struct ProofCommandCli {
423 #[clap(subcommand)]
424 pub command: ProofSubCommand,
425}
426
427#[derive(Subcommand, Debug)]
428pub enum ProofSubCommand {
429 Run,
431 Test {
433 #[clap(long)]
434 name: String,
435 },
436 List,
438}
439
440#[derive(clap::Args, Debug)]
441struct ContextCli {
442 #[clap(subcommand)]
443 command: ContextCommand,
444}
445
446#[derive(Subcommand, Debug)]
447enum ContextCommand {
448 Audit {
450 #[clap(long)]
451 profile: String,
452 #[clap(long)]
453 files: Vec<PathBuf>,
454 },
455 Pack {
457 #[clap(long)]
458 path: PathBuf,
459 #[clap(long)]
460 summary: String,
461 },
462 Restore {
464 #[clap(long)]
465 id: String,
466 #[clap(long, default_value = "main")]
467 profile: String,
468 #[clap(long)]
469 current_files: Vec<PathBuf>,
470 },
471}
472
473#[derive(clap::Args, Debug)]
474struct SchemaCli {
475 #[clap(long, default_value = "json")]
477 format: String,
478 #[clap(long)]
480 subsystem: Option<String>,
481 #[clap(long)]
483 deterministic: bool,
484}
485
486fn find_decapod_project_root(start_dir: &Path) -> Result<PathBuf, error::DecapodError> {
487 let mut current_dir = PathBuf::from(start_dir);
488 loop {
489 if current_dir.join(".decapod").exists() {
490 return Ok(current_dir);
491 }
492 if !current_dir.pop() {
493 return Err(error::DecapodError::NotFound(
494 "'.decapod' directory not found in current or parent directories. Run `decapod init` first.".to_string(),
495 ));
496 }
497 }
498}
499
500fn clean_project(dir: Option<PathBuf>) -> Result<(), error::DecapodError> {
501 let raw_dir = match dir {
502 Some(d) => d,
503 None => std::env::current_dir()?,
504 };
505 let target_dir = std::fs::canonicalize(&raw_dir).map_err(error::DecapodError::IoError)?;
506
507 let decapod_root = target_dir.join(".decapod");
508 if decapod_root.exists() {
509 println!("Removing directory: {}", decapod_root.display());
510 fs::remove_dir_all(&decapod_root).map_err(error::DecapodError::IoError)?;
511 }
512
513 for file in ["AGENTS.md", "CLAUDE.md", "GEMINI.md", "CODEX.md"] {
514 let path = target_dir.join(file);
515 if path.exists() {
516 println!("Removing file: {}", path.display());
517 fs::remove_file(&path).map_err(error::DecapodError::IoError)?;
518 }
519 }
520 println!("Decapod files cleaned from {}", target_dir.display());
521 Ok(())
522}
523
524pub fn run() -> Result<(), error::DecapodError> {
525 let cli = Cli::parse();
526 let current_dir = std::env::current_dir()?;
527 let decapod_root_option = find_decapod_project_root(¤t_dir);
528 let store_root: PathBuf;
529
530 match cli.command {
531 Command::Version => {
532 println!("v{}", migration::DECAPOD_VERSION);
534 return Ok(());
535 }
536 Command::Init(init_group) => {
537 if let Some(subcmd) = init_group.command {
539 match subcmd {
540 InitCommand::Clean { dir } => {
541 clean_project(dir)?;
542 return Ok(());
543 }
544 }
545 }
546
547 use colored::Colorize;
549
550 print!("\x1B[2J\x1B[1;1H");
552
553 let _width = tui::terminal_width();
554
555 println!();
557 println!();
558 println!(
559 "{}",
560 " ▗▄▄▄▄▖ ▗▄▄▄▄▄▄▄▄▄▄▄▄▖ ▗▄▄▄▄▖"
561 .bright_magenta()
562 .bold()
563 );
564 println!(
565 "{}",
566 " ▗▀▀ ▝▀ ▀▘ ▀▀▖"
567 .bright_magenta()
568 .bold()
569 );
570 println!(
571 " {} {} {}",
572 "▗▀".bright_magenta().bold(),
573 "🦀 D E C A P O D 🦀".bright_white().bold().underline(),
574 "▀▖".bright_magenta().bold()
575 );
576 println!(
577 "{}",
578 " ▐ ▌"
579 .bright_cyan()
580 .bold()
581 );
582 println!(
583 " {} {} {}",
584 "▐".bright_cyan().bold(),
585 "C O N T R O L P L A N E".bright_cyan().bold(),
586 "▌".bright_cyan().bold()
587 );
588 println!(
589 "{}",
590 " ▐ ▌"
591 .bright_cyan()
592 .bold()
593 );
594 println!(
595 "{}",
596 " ▝▖ ▗▘"
597 .bright_magenta()
598 .bold()
599 );
600 println!(
601 "{}",
602 " ▝▄▄ ▄▄▘"
603 .bright_magenta()
604 .bold()
605 );
606 println!(
607 "{}",
608 " ▝▀▀▀▀▖ ▝▀▀▀▀▀▀▀▀▀▀▀▀▘ ▗▀▀▀▀▘"
609 .bright_magenta()
610 .bold()
611 );
612 println!();
613 println!();
614
615 let target_dir = match init_group.dir {
616 Some(d) => d,
617 None => current_dir.clone(),
618 };
619 let target_dir =
620 std::fs::canonicalize(&target_dir).map_err(error::DecapodError::IoError)?;
621
622 let setup_decapod_root = target_dir.join(".decapod");
624 if setup_decapod_root.exists() && !init_group.force {
625 tui::render_box(
626 "⚠ SYSTEM ALREADY INITIALIZED",
627 "Use --force to override",
628 tui::BoxStyle::Warning,
629 );
630 println!();
631 println!(" {} Detected existing control plane", "▸".bright_yellow());
632 println!(
633 " {} Use {} flag to override",
634 "▸".bright_yellow(),
635 "--force".bright_cyan().bold()
636 );
637 println!();
638 return Ok(());
639 }
640
641 use sha2::{Digest, Sha256};
643 let mut existing_agent_files = vec![];
644 for file in ["AGENTS.md", "CLAUDE.md", "GEMINI.md", "CODEX.md"] {
645 if target_dir.join(file).exists() {
646 existing_agent_files.push(file);
647 }
648 }
649
650 let mut created_backups = false;
652 if !init_group.dry_run {
653 let mut backed_up = false;
654 for file in &existing_agent_files {
655 let path = target_dir.join(file);
656
657 let template_content = core::assets::get_template(file).unwrap_or_default();
659
660 let mut hasher = Sha256::new();
662 hasher.update(template_content.as_bytes());
663 let template_hash = format!("{:x}", hasher.finalize());
664
665 let existing_content = fs::read_to_string(&path).unwrap_or_default();
667 let mut hasher = Sha256::new();
668 hasher.update(existing_content.as_bytes());
669 let existing_hash = format!("{:x}", hasher.finalize());
670
671 if template_hash != existing_hash {
673 if !backed_up {
674 println!(
675 " {}",
676 "▼▼▼ PRESERVATION PROTOCOL ACTIVATED ▼▼▼"
677 .bright_yellow()
678 .bold()
679 );
680 println!();
681 backed_up = true;
682 created_backups = true;
683 }
684 let backup_path = target_dir.join(format!("{}.bak", file));
685 fs::rename(&path, &backup_path).map_err(error::DecapodError::IoError)?;
686 println!(
687 " {} {} {} {}",
688 "◆".bright_cyan(),
689 file.bright_white().bold(),
690 "⟿".bright_yellow(),
691 format!("{}.bak", file.strip_suffix(".md").unwrap_or(file))
692 .bright_black()
693 );
694 }
695 }
696 if backed_up {
697 println!();
698 }
699 }
700
701 let setup_store_root = setup_decapod_root.join("data");
703 if !init_group.dry_run {
704 std::fs::create_dir_all(&setup_store_root).map_err(error::DecapodError::IoError)?;
705 }
706
707 if !init_group.dry_run {
709 tui::render_box(
711 "⚡ SUBSYSTEM INITIALIZATION",
712 "Database & State Management",
713 tui::BoxStyle::Cyan,
714 );
715 println!();
716
717 let dbs = [
719 ("todo.db", setup_store_root.join("todo.db")),
720 ("knowledge.db", setup_store_root.join("knowledge.db")),
721 ("cron.db", setup_store_root.join("cron.db")),
722 ("reflex.db", setup_store_root.join("reflex.db")),
723 ("health.db", setup_store_root.join("health.db")),
724 ("policy.db", setup_store_root.join("policy.db")),
725 ("archive.db", setup_store_root.join("archive.db")),
726 ("feedback.db", setup_store_root.join("feedback.db")),
727 ("teammate.db", setup_store_root.join("teammate.db")),
728 ];
729
730 for (db_name, db_path) in dbs {
731 if db_path.exists() {
732 println!(
733 " {} {} {}",
734 "✓".bright_green(),
735 db_name.bright_white(),
736 "(preserved - existing data kept)".bright_black()
737 );
738 } else {
739 match db_name {
740 "todo.db" => todo::initialize_todo_db(&setup_store_root)?,
741 "knowledge.db" => db::initialize_knowledge_db(&setup_store_root)?,
742 "cron.db" => cron::initialize_cron_db(&setup_store_root)?,
743 "reflex.db" => reflex::initialize_reflex_db(&setup_store_root)?,
744 "health.db" => health::initialize_health_db(&setup_store_root)?,
745 "policy.db" => policy::initialize_policy_db(&setup_store_root)?,
746 "archive.db" => archive::initialize_archive_db(&setup_store_root)?,
747 "feedback.db" => feedback::initialize_feedback_db(&setup_store_root)?,
748 "teammate.db" => teammate::initialize_teammate_db(&setup_store_root)?,
749 _ => unreachable!(),
750 }
751 println!(" {} {}", "●".bright_green(), db_name.bright_white());
752 }
753 }
754
755 println!();
756
757 let events_path = setup_store_root.join("todo.events.jsonl");
759 if events_path.exists() {
760 println!(
761 " {} {} {}",
762 "✓".bright_green(),
763 "todo.events.jsonl".bright_white(),
764 "(preserved - event history kept)".bright_black()
765 );
766 } else {
767 std::fs::write(&events_path, "").map_err(error::DecapodError::IoError)?;
768 println!(
769 " {} {}",
770 "●".bright_green(),
771 "todo.events.jsonl".bright_white()
772 );
773 }
774
775 let generated_dir = setup_decapod_root.join("generated");
777 if generated_dir.exists() {
778 println!(
779 " {} {} {}",
780 "✓".bright_green(),
781 "generated/".bright_white(),
782 "(preserved - existing files kept)".bright_black()
783 );
784 } else {
785 std::fs::create_dir_all(&generated_dir)
786 .map_err(error::DecapodError::IoError)?;
787 println!(" {} {}", "●".bright_green(), "generated/".bright_white());
788 }
789
790 println!();
791 }
792
793 let agent_files_to_generate =
796 if init_group.claude || init_group.gemini || init_group.agents {
797 let mut files = vec![];
798 if init_group.claude {
799 files.push("CLAUDE.md".to_string());
800 }
801 if init_group.gemini {
802 files.push("GEMINI.md".to_string());
803 }
804 if init_group.agents {
805 files.push("AGENTS.md".to_string());
806 }
807 files
808 } else {
809 existing_agent_files
810 .into_iter()
811 .map(|s| s.to_string())
812 .collect()
813 };
814
815 scaffold::scaffold_project_entrypoints(&scaffold::ScaffoldOptions {
816 target_dir,
817 force: init_group.force,
818 dry_run: init_group.dry_run,
819 agent_files: agent_files_to_generate,
820 created_backups,
821 all: init_group.all,
822 })?;
823
824 if !init_group.dry_run {
826 migration::write_version(&setup_decapod_root)?;
827 }
828 }
829 Command::Setup(setup_cli) => match setup_cli.command {
830 SetupCommand::Hook {
831 commit_msg,
832 pre_commit,
833 uninstall,
834 } => {
835 run_hook_install(commit_msg, pre_commit, uninstall)?;
836 }
837 },
838 _ => {
839 let project_root = decapod_root_option?;
841 let decapod_root_path = project_root.join(".decapod");
842 store_root = decapod_root_path.join("data");
843 std::fs::create_dir_all(&store_root).map_err(error::DecapodError::IoError)?;
844
845 check_version_compatibility(&decapod_root_path)?;
847
848 migration::check_and_migrate(&decapod_root_path)?;
850
851 let project_store = Store {
852 kind: StoreKind::Repo,
853 root: store_root.clone(),
854 };
855
856 match cli.command {
857 Command::Validate(validate_cli) => {
858 let decapod_root = project_root.clone();
859 let store = match validate_cli.store.as_str() {
860 "user" => {
861 let tmp_root = std::env::temp_dir()
863 .join(format!("decapod_validate_user_{}", ulid::Ulid::new()));
864 std::fs::create_dir_all(&tmp_root)
865 .map_err(error::DecapodError::IoError)?;
866 Store {
867 kind: StoreKind::User,
868 root: tmp_root,
869 }
870 }
871 _ => project_store.clone(),
872 };
873 validate::run_validation(&store, &decapod_root, &decapod_root)?;
874 }
875 Command::Update => {
876 run_self_update(&project_root)?;
877 }
878 Command::Version => {
879 show_version_info(&project_root)?;
880 }
881 Command::Docs(docs_cli) => {
882 docs_cli::run_docs_cli(docs_cli)?;
883 }
884 Command::Todo(todo_cli) => {
885 todo::run_todo_cli(&project_store, todo_cli)?;
886 }
887 Command::Govern(govern_cli) => match govern_cli.command {
888 GovernCommand::Policy(policy_cli) => {
889 policy::run_policy_cli(&project_store, policy_cli)?;
890 }
891 GovernCommand::Health(health_cli) => {
892 health::run_health_cli(&project_store, health_cli)?;
893 }
894 GovernCommand::Proof(proof_cli) => {
895 proof::execute_proof_cli(&proof_cli, &store_root)?;
896 }
897 GovernCommand::Watcher(watcher_cli) => match watcher_cli.command {
898 WatcherCommand::Run => {
899 let report = watcher::run_watcher(&project_store)?;
900 println!("{}", serde_json::to_string_pretty(&report).unwrap());
901 }
902 },
903 GovernCommand::Feedback(feedback_cli) => {
904 feedback::initialize_feedback_db(&store_root)?;
905 match feedback_cli.command {
906 FeedbackCommand::Add {
907 source,
908 text,
909 links,
910 } => {
911 let id = feedback::add_feedback(
912 &project_store,
913 &source,
914 &text,
915 links.as_deref(),
916 )?;
917 println!("Feedback recorded: {}", id);
918 }
919 FeedbackCommand::Propose => {
920 let proposal = feedback::propose_prefs(&project_store)?;
921 println!("{}", proposal);
922 }
923 }
924 }
925 },
926 Command::Data(data_cli) => match data_cli.command {
927 DataCommand::Archive(archive_cli) => {
928 archive::initialize_archive_db(&store_root)?;
929 match archive_cli.command {
930 ArchiveCommand::List => {
931 let items = archive::list_archives(&project_store)?;
932 println!("{}", serde_json::to_string_pretty(&items).unwrap());
933 }
934 ArchiveCommand::Verify => {
935 let failures = archive::verify_archives(&project_store)?;
936 if failures.is_empty() {
937 println!("All archives verified successfully.");
938 } else {
939 println!("Archive verification failed:");
940 for f in failures {
941 println!("- {}", f);
942 }
943 }
944 }
945 }
946 }
947 DataCommand::Knowledge(knowledge_cli) => {
948 db::initialize_knowledge_db(&store_root)?;
949 match knowledge_cli.command {
950 KnowledgeCommand::Add {
951 id,
952 title,
953 text,
954 provenance,
955 claim_id,
956 } => {
957 knowledge::add_knowledge(
958 &project_store,
959 &id,
960 &title,
961 &text,
962 &provenance,
963 claim_id.as_deref(),
964 )?;
965 println!("Knowledge entry added: {}", id);
966 }
967 KnowledgeCommand::Search { query } => {
968 let results = knowledge::search_knowledge(&project_store, &query)?;
969 println!("{}", serde_json::to_string_pretty(&results).unwrap());
970 }
971 }
972 }
973 DataCommand::Context(context_cli) => {
974 let manager = context::ContextManager::new(&store_root)?;
975 match context_cli.command {
976 ContextCommand::Audit { profile, files } => {
977 let total = manager.audit_session(&files)?;
978 match manager.get_profile(&profile) {
979 Some(p) => {
980 println!(
981 "Total tokens for profile '{}': {} / {} (budget)",
982 profile, total, p.budget_tokens
983 );
984 if total > p.budget_tokens {
985 println!("⚠ OVER BUDGET");
986 }
987 }
988 None => {
989 println!(
990 "Total tokens: {} (Profile '{}' not found)",
991 total, profile
992 );
993 }
994 }
995 }
996 ContextCommand::Pack { path, summary } => {
997 match manager.pack_and_archive(&project_store, &path, &summary) {
998 Ok(archive_path) => {
999 println!("Session archived to: {}", archive_path.display());
1000 }
1001 Err(error::DecapodError::ContextPackError(msg)) => {
1002 eprintln!("Context pack failed: {}", msg);
1003 }
1004 Err(e) => {
1005 eprintln!("Unexpected error during context pack: {}", e);
1006 }
1007 }
1008 }
1009 ContextCommand::Restore {
1010 id,
1011 profile,
1012 current_files,
1013 } => {
1014 let content =
1015 manager.restore_archive(&id, &profile, ¤t_files)?;
1016 println!(
1017 "--- RESTORED CONTENT (Archive: {}) ---\n{}\n--- END RESTORED ---",
1018 id, content
1019 );
1020 }
1021 }
1022 }
1023 DataCommand::Schema(schema_cli) => {
1024 let mut schemas = std::collections::BTreeMap::new();
1025 schemas.insert("todo", todo::schema());
1026 schemas.insert("cron", cron::schema());
1027 schemas.insert("reflex", reflex::schema());
1028 schemas.insert("health", health::health_schema());
1029 schemas.insert("broker", core::broker::schema());
1030 schemas.insert("context", context::schema());
1031 schemas.insert("policy", policy::schema());
1032 schemas.insert("knowledge", knowledge::schema());
1033 schemas.insert("repomap", repomap::schema());
1034 schemas.insert("watcher", watcher::schema());
1035 schemas.insert("archive", archive::schema());
1036 schemas.insert("feedback", feedback::schema());
1037 schemas.insert("teammate", teammate::schema());
1038 schemas.insert("docs", docs_cli::schema());
1039
1040 let output = if let Some(sub) = schema_cli.subsystem {
1041 schemas
1042 .get(sub.as_str())
1043 .cloned()
1044 .unwrap_or(serde_json::json!({ "error": "subsystem not found" }))
1045 } else {
1046 let mut envelope = serde_json::json!({
1047 "schema_version": "1.0.0",
1048 "subsystems": schemas
1049 });
1050 if !schema_cli.deterministic {
1051 envelope.as_object_mut().unwrap().insert(
1052 "generated_at".to_string(),
1053 serde_json::json!(format!(
1054 "{:?}",
1055 std::time::SystemTime::now()
1056 )),
1057 );
1058 }
1059 envelope
1060 };
1061
1062 if schema_cli.format == "json" {
1063 println!("{}", serde_json::to_string_pretty(&output).unwrap());
1064 } else {
1065 println!(
1066 "Markdown schema format not yet implemented. Defaulting to JSON."
1067 );
1068 println!("{}", serde_json::to_string_pretty(&output).unwrap());
1069 }
1070 }
1071 DataCommand::Repo(repo_cli) => match repo_cli.command {
1072 RepoCommand::Map => {
1073 let map = repomap::generate_map(&project_root);
1074 println!("{}", serde_json::to_string_pretty(&map).unwrap());
1075 }
1076 RepoCommand::Graph => {
1077 let graph = repomap::generate_doc_graph(&project_root);
1078 println!("{}", graph.mermaid);
1079 }
1080 },
1081 DataCommand::Broker(broker_cli) => match broker_cli.command {
1082 BrokerCommand::Audit => {
1083 let audit_log = store_root.join("broker.events.jsonl");
1084 if audit_log.exists() {
1085 let content = std::fs::read_to_string(audit_log)?;
1086 println!("{}", content);
1087 } else {
1088 println!("No audit log found.");
1089 }
1090 }
1091 },
1092 DataCommand::Teammate(teammate_cli) => {
1093 teammate::run_teammate_cli(&project_store, teammate_cli)?;
1094 }
1095 },
1096 Command::Auto(auto_cli) => match auto_cli.command {
1097 AutoCommand::Cron(cron_cli) => {
1098 cron::run_cron_cli(&project_store, cron_cli)?;
1099 }
1100 AutoCommand::Reflex(reflex_cli) => {
1101 reflex::run_reflex_cli(&project_store, reflex_cli);
1102 }
1103 },
1104 Command::Qa(qa_cli) => match qa_cli.command {
1105 QaCommand::Verify(verify_cli) => {
1106 verify::run_verify_cli(&project_store, &project_root, verify_cli)?;
1107 }
1108 QaCommand::Check {
1109 crate_description,
1110 all,
1111 } => {
1112 run_check(crate_description, all)?;
1113 }
1114 QaCommand::Gatling(ref gatling_cli) => {
1115 plugins::gatling::run_gatling_cli(gatling_cli)?;
1116 }
1117 },
1118 _ => unreachable!(),
1119 }
1120 }
1121 }
1122 Ok(())
1123}
1124
1125fn run_hook_install(
1126 commit_msg: bool,
1127 pre_commit: bool,
1128 uninstall: bool,
1129) -> Result<(), error::DecapodError> {
1130 use std::fs;
1131 use std::io::Write;
1132
1133 let git_dir = Path::new(".git");
1134 if !git_dir.exists() {
1135 return Err(error::DecapodError::ValidationError(
1136 ".git directory not found. Are you in the root of the project?".into(),
1137 ));
1138 }
1139
1140 let hooks_dir = git_dir.join("hooks");
1141 fs::create_dir_all(&hooks_dir).map_err(error::DecapodError::IoError)?;
1142
1143 if uninstall {
1144 let commit_msg_path = hooks_dir.join("commit-msg");
1145 let pre_commit_path = hooks_dir.join("pre-commit");
1146
1147 let mut removed = false;
1148 if commit_msg_path.exists() {
1149 fs::remove_file(&commit_msg_path)?;
1150 println!("✓ Removed commit-msg hook");
1151 removed = true;
1152 }
1153 if pre_commit_path.exists() {
1154 fs::remove_file(&pre_commit_path)?;
1155 println!("✓ Removed pre-commit hook");
1156 removed = true;
1157 }
1158 if !removed {
1159 println!("No hooks found to remove");
1160 }
1161 return Ok(());
1162 }
1163
1164 if commit_msg {
1166 let hook_content = r#"#!/bin/sh
1167# Conventional commit validation hook
1168# Installed by Decapod
1169
1170MSG=$(cat "$1")
1171REGEX="^(feat|fix|chore|ci|docs|style|refactor|perf|test)(\(.*\))?!?: .+"
1172
1173if ! echo "$MSG" | grep -qE "$REGEX"; then
1174 echo "Error: Invalid commit message format."
1175 echo " Commit messages must follow the Conventional Commits format."
1176 echo " Example: 'feat: add login functionality'"
1177 echo " Allowed prefixes: feat, fix, chore, ci, docs, style, refactor, perf, test"
1178 exit 1
1179fi
1180"#;
1181
1182 let hook_path = hooks_dir.join("commit-msg");
1183 let mut file = fs::File::create(&hook_path).map_err(error::DecapodError::IoError)?;
1184 file.write_all(hook_content.as_bytes())
1185 .map_err(error::DecapodError::IoError)?;
1186 drop(file);
1187
1188 #[cfg(unix)]
1189 {
1190 use std::os::unix::fs::PermissionsExt;
1191 let mut perms = fs::metadata(&hook_path)
1192 .map_err(error::DecapodError::IoError)?
1193 .permissions();
1194 perms.set_mode(0o755);
1195 fs::set_permissions(&hook_path, perms).map_err(error::DecapodError::IoError)?;
1196 }
1197
1198 println!("✓ Installed commit-msg hook for conventional commits");
1199 }
1200
1201 if pre_commit {
1203 let hook_content = r#"#!/bin/sh
1205# Pre-commit hook - runs cargo fmt and clippy
1206# Installed by Decapod
1207
1208echo "Running pre-commit checks..."
1209
1210# Run cargo fmt
1211if ! cargo fmt --all -- --check 2>/dev/null; then
1212 echo "Formatting check failed. Run 'cargo fmt --all' to fix."
1213 exit 1
1214fi
1215
1216# Run cargo clippy
1217if ! cargo clippy --all-targets --all-features -- -D warnings 2>/dev/null; then
1218 echo "Clippy check failed."
1219 exit 1
1220fi
1221
1222echo "Pre-commit checks passed!"
1223exit 0
1224"#;
1225
1226 let hook_path = hooks_dir.join("pre-commit");
1227 let mut file = fs::File::create(&hook_path).map_err(error::DecapodError::IoError)?;
1228 file.write_all(hook_content.as_bytes())
1229 .map_err(error::DecapodError::IoError)?;
1230 drop(file);
1231
1232 #[cfg(unix)]
1233 {
1234 use std::os::unix::fs::PermissionsExt;
1235 let mut perms = fs::metadata(&hook_path)
1236 .map_err(error::DecapodError::IoError)?
1237 .permissions();
1238 perms.set_mode(0o755);
1239 fs::set_permissions(&hook_path, perms).map_err(error::DecapodError::IoError)?;
1240 }
1241
1242 println!("✓ Installed pre-commit hook (fmt + clippy)");
1243 }
1244
1245 if !commit_msg && !pre_commit {
1246 println!("No hooks specified. Use --commit-msg and/or --pre-commit");
1247 }
1248
1249 Ok(())
1250}
1251
1252fn run_check(crate_description: bool, all: bool) -> Result<(), error::DecapodError> {
1253 if crate_description || all {
1254 let expected = "Decapod is a Rust-built governance runtime for AI agents: repo-native state, enforced workflow, proof gates, safe coordination.";
1255
1256 let output = std::process::Command::new("cargo")
1257 .args(["metadata", "--no-deps", "--format-version", "1"])
1258 .output()
1259 .map_err(|e| error::DecapodError::IoError(std::io::Error::other(e)))?;
1260
1261 let json_str = String::from_utf8_lossy(&output.stdout);
1262
1263 if json_str.contains(expected) {
1264 println!("✓ Crate description matches");
1265 } else {
1266 println!("✗ Crate description mismatch!");
1267 println!(" Expected: {}", expected);
1268 return Err(error::DecapodError::ValidationError(
1269 "Crate description check failed".into(),
1270 ));
1271 }
1272 }
1273
1274 if all && !crate_description {
1275 println!("Note: --all requires --crate-description");
1276 }
1277
1278 Ok(())
1279}
1280
1281fn run_self_update(project_root: &Path) -> Result<(), error::DecapodError> {
1282 use std::process::Command;
1283
1284 println!("Updating decapod binary from current directory...");
1285 println!("Running: cargo install --path . --locked");
1286 println!();
1287
1288 let status = Command::new("cargo")
1289 .args(["install", "--path", ".", "--locked"])
1290 .current_dir(project_root)
1291 .status()
1292 .map_err(error::DecapodError::IoError)?;
1293
1294 if !status.success() {
1295 return Err(error::DecapodError::ValidationError(
1296 "cargo install failed - see output above for details".into(),
1297 ));
1298 }
1299
1300 println!();
1301 println!("✓ Decapod binary updated successfully");
1302 println!(" Run 'decapod --version' to verify the new version");
1303
1304 let decapod_root = project_root.join(".decapod");
1306 if decapod_root.exists() {
1307 migration::write_version(&decapod_root)?;
1308 }
1309
1310 Ok(())
1311}
1312
1313fn show_version_info(project_root: &Path) -> Result<(), error::DecapodError> {
1315 use colored::Colorize;
1316
1317 let binary_version = migration::DECAPOD_VERSION;
1318
1319 println!(
1320 "{} {}",
1321 "Decapod version:".bright_white(),
1322 binary_version.bright_green()
1323 );
1324
1325 let decapod_root = project_root.join(".decapod");
1326
1327 if !decapod_root.exists() {
1329 println!(
1330 "{} No .decapod directory found in {}",
1331 "ℹ".bright_blue(),
1332 project_root.display()
1333 );
1334 println!(" Run 'decapod init' to initialize the project");
1335 return Ok(());
1336 }
1337
1338 let version_file = decapod_root.join("generated/decapod.version");
1339
1340 if version_file.exists() {
1341 let repo_version = std::fs::read_to_string(&version_file)
1342 .map_err(error::DecapodError::IoError)?
1343 .trim()
1344 .to_string();
1345
1346 if repo_version.is_empty() {
1347 println!(
1348 "{} Repo version file exists but is empty",
1349 "⚠".bright_yellow()
1350 );
1351 } else if repo_version == binary_version {
1352 println!("{} Repo version matches binary version", "✓".bright_green());
1353 } else {
1354 match compare_versions(binary_version, &repo_version) {
1356 std::cmp::Ordering::Less => {
1357 println!(
1358 "{} Repo version ({}) is newer than binary version",
1359 "⚠".bright_yellow(),
1360 repo_version.bright_yellow()
1361 );
1362 println!(
1363 " Consider running: {} to update the binary",
1364 "decapod update".bright_cyan()
1365 );
1366 }
1367 std::cmp::Ordering::Greater => {
1368 println!(
1369 "{} Binary version is newer than repo version ({})",
1370 "✓".bright_green(),
1371 repo_version.bright_yellow()
1372 );
1373 println!(" Migration will run on next command to update repo");
1374 }
1375 _ => {} }
1377 }
1378 } else {
1379 println!(
1380 "{} No version file found in .decapod/generated/decapod.version",
1381 "ℹ".bright_blue()
1382 );
1383 println!(" Run 'decapod init' to set up version tracking");
1384 }
1385
1386 Ok(())
1387}
1388
1389fn compare_versions(a: &str, b: &str) -> std::cmp::Ordering {
1391 let parse_version =
1392 |v: &str| -> Vec<u32> { v.split('.').filter_map(|s| s.parse::<u32>().ok()).collect() };
1393
1394 let a_parts = parse_version(a);
1395 let b_parts = parse_version(b);
1396
1397 for (a_part, b_part) in a_parts.iter().zip(b_parts.iter()) {
1398 match a_part.cmp(b_part) {
1399 std::cmp::Ordering::Equal => continue,
1400 other => return other,
1401 }
1402 }
1403
1404 a_parts.len().cmp(&b_parts.len())
1405}
1406
1407fn check_version_compatibility(decapod_root: &Path) -> Result<(), error::DecapodError> {
1409 use colored::Colorize;
1410
1411 let version_file = decapod_root.join("generated/decapod.version");
1412
1413 if !version_file.exists() {
1414 return Ok(()); }
1416
1417 let repo_version = std::fs::read_to_string(&version_file)
1418 .map_err(error::DecapodError::IoError)?
1419 .trim()
1420 .to_string();
1421
1422 if repo_version.is_empty() {
1423 return Ok(()); }
1425
1426 let binary_version = migration::DECAPOD_VERSION;
1427
1428 if compare_versions(binary_version, &repo_version) == std::cmp::Ordering::Less {
1430 eprintln!();
1431 eprintln!(
1432 "{} {} {}",
1433 "⚠ WARNING:".bright_yellow().bold(),
1434 "Binary version".bright_white(),
1435 binary_version.bright_yellow()
1436 );
1437 eprintln!(
1438 " {} {} {}",
1439 "is older than repo version".bright_white(),
1440 repo_version.bright_yellow(),
1441 "- some features may not work correctly".bright_white()
1442 );
1443 eprintln!(
1444 " {} {}",
1445 "Run:".bright_white(),
1446 "decapod update".bright_cyan().bold()
1447 );
1448 eprintln!();
1449 }
1450
1451 Ok(())
1452}