1use std::path::PathBuf;
4use std::sync::Arc;
5
6use anyhow::{Context, Result};
7use clap::{Parser, Subcommand};
8
9use crate::daemon::{Daemon, DaemonClient, DaemonConfig};
10use crate::git::{find_beads_dir, find_database_path, init_beads_dir};
11use crate::storage::{SqliteStorage, Storage};
12use crate::types::{IssueFilter, Status};
13use crate::compact::Compactor;
14use crate::context::{ContextStore, ContextEntry, Namespace, FileContext, ProjectContext};
15use crate::{Issue, Dependency, generate_id};
16
17#[derive(Parser)]
19#[command(name = "bd", version, about, long_about = None)]
20pub struct Cli {
21 #[command(subcommand)]
22 pub command: Command,
23
24 #[arg(long, global = true)]
26 pub no_daemon: bool,
27
28 #[arg(long, global = true)]
30 pub actor: Option<String>,
31
32 #[arg(long, global = true)]
34 pub json: bool,
35}
36
37#[derive(Subcommand)]
38pub enum Command {
39 Init {
41 #[arg(short, long)]
43 path: Option<PathBuf>,
44 },
45
46 Create {
48 #[arg(short, long)]
50 title: String,
51
52 #[arg(short, long)]
54 description: Option<String>,
55
56 #[arg(short = 'T', long, default_value = "task")]
58 issue_type: String,
59
60 #[arg(short, long, default_value = "2")]
62 priority: i32,
63
64 #[arg(short, long)]
66 assignee: Option<String>,
67
68 #[arg(long)]
70 parent: Option<String>,
71
72 #[arg(short, long)]
74 labels: Option<String>,
75 },
76
77 Show {
79 id: String,
81
82 #[arg(long)]
84 comments: bool,
85
86 #[arg(long)]
88 events: bool,
89 },
90
91 List {
93 #[arg(short, long)]
95 status: Option<String>,
96
97 #[arg(short = 'T', long)]
99 issue_type: Option<String>,
100
101 #[arg(short, long)]
103 assignee: Option<String>,
104
105 #[arg(short, long)]
107 label: Option<String>,
108
109 #[arg(long)]
111 all: bool,
112
113 #[arg(short = 'n', long)]
115 limit: Option<usize>,
116 },
117
118 Ready {
120 #[arg(short = 'n', long)]
122 limit: Option<usize>,
123 },
124
125 Update {
127 id: String,
129
130 #[arg(short, long)]
132 title: Option<String>,
133
134 #[arg(short, long)]
136 description: Option<String>,
137
138 #[arg(short, long)]
140 status: Option<String>,
141
142 #[arg(short, long)]
144 priority: Option<i32>,
145
146 #[arg(short, long)]
148 assignee: Option<String>,
149 },
150
151 Close {
153 id: String,
155
156 #[arg(short, long)]
158 reason: Option<String>,
159 },
160
161 Delete {
163 id: String,
165
166 #[arg(short, long)]
168 reason: Option<String>,
169 },
170
171 Dep {
173 #[command(subcommand)]
174 action: DepAction,
175 },
176
177 Label {
179 #[command(subcommand)]
180 action: LabelAction,
181 },
182
183 Stats,
185
186 Daemon {
188 #[command(subcommand)]
189 action: DaemonAction,
190 },
191
192 Compact {
194 #[arg(short, long, default_value = "1")]
196 level: i32,
197
198 #[arg(short, long)]
200 id: Option<String>,
201 },
202
203 Config {
205 key: Option<String>,
207
208 value: Option<String>,
210
211 #[arg(long)]
213 delete: bool,
214 },
215
216 Context {
218 #[command(subcommand)]
219 action: ContextAction,
220 },
221}
222
223#[derive(Subcommand)]
224pub enum DepAction {
225 Add {
227 issue_id: String,
229
230 depends_on: String,
232
233 #[arg(short = 'T', long, default_value = "blocks")]
235 dep_type: String,
236 },
237
238 Remove {
240 issue_id: String,
242
243 depends_on: String,
245 },
246
247 List {
249 issue_id: String,
251 },
252}
253
254#[derive(Subcommand)]
255pub enum LabelAction {
256 Add {
258 issue_id: String,
260
261 label: String,
263 },
264
265 Remove {
267 issue_id: String,
269
270 label: String,
272 },
273
274 List {
276 issue_id: String,
278 },
279}
280
281#[derive(Subcommand)]
282pub enum DaemonAction {
283 Start {
285 #[arg(long)]
287 foreground: bool,
288
289 #[arg(long)]
291 auto_commit: bool,
292
293 #[arg(long)]
295 auto_push: bool,
296 },
297
298 Stop,
300
301 Status,
303}
304
305#[derive(Subcommand)]
306pub enum ContextAction {
307 Get {
309 key: String,
311 },
312
313 Set {
315 key: String,
317
318 value: String,
320
321 #[arg(long)]
323 ttl: Option<i64>,
324
325 #[arg(long)]
327 file: Option<String>,
328 },
329
330 Delete {
332 key: String,
334 },
335
336 List {
338 #[arg(short = 'N', long)]
340 namespace: Option<String>,
341
342 #[arg(short, long)]
344 prefix: Option<String>,
345
346 #[arg(short = 'n', long)]
348 limit: Option<usize>,
349 },
350
351 Clear {
353 #[arg(short = 'N', long)]
355 namespace: Option<String>,
356
357 #[arg(long)]
359 expired: bool,
360 },
361
362 Stats,
364
365 File {
367 path: String,
369 },
370
371 Project {
373 #[arg(long)]
375 set: Option<String>,
376 },
377
378 Session {
380 session_id: String,
382 },
383
384 Refresh,
386}
387
388pub async fn run(cli: Cli) -> Result<()> {
390 let actor = cli.actor.unwrap_or_else(get_default_actor);
391 let json_output = cli.json;
392
393 match cli.command {
394 Command::Init { path } => {
395 let target = path.unwrap_or_else(|| std::env::current_dir().unwrap());
396 let beads_path = init_beads_dir(&target)?;
397
398 let db_path = beads_path.join("beads.db");
400 SqliteStorage::open(&db_path)?;
401
402 if json_output {
403 println!(r#"{{"success": true, "path": {:?}}}"#, beads_path);
404 } else {
405 println!("Initialized .beads directory at {:?}", beads_path);
406 }
407 }
408
409 Command::Create {
410 title,
411 description,
412 issue_type,
413 priority,
414 assignee,
415 parent,
416 labels,
417 } => {
418 let storage = open_storage()?;
419
420 let id = if let Some(ref parent_id) = parent {
421 let counter = storage.next_child_counter(parent_id)?;
422 crate::idgen::generate_child_id(parent_id, counter)
423 } else {
424 generate_id("bd")
425 };
426
427 let mut issue = Issue::new(&id, &title, &actor);
428 issue.description = description;
429 issue.issue_type = issue_type.parse().unwrap_or_default();
430 issue.priority = priority.clamp(0, 4);
431 issue.assignee = assignee;
432 issue.update_content_hash();
433
434 storage.create_issue(&issue)?;
435
436 if let Some(labels_str) = labels {
438 for label in labels_str.split(',').map(|s| s.trim()) {
439 storage.add_label(&id, label)?;
440 }
441 }
442
443 if let Some(ref parent_id) = parent {
445 let dep = Dependency::parent_child(&id, parent_id)
446 .with_creator(&actor);
447 storage.add_dependency(&dep)?;
448 }
449
450 if json_output {
451 println!("{}", serde_json::to_string(&issue)?);
452 } else {
453 println!("Created issue: {}", id);
454 }
455 }
456
457 Command::Show { id, comments, events } => {
458 let storage = open_storage()?;
459
460 let issue = storage.get_issue(&id)?
461 .context(format!("Issue not found: {}", id))?;
462
463 if json_output {
464 let mut output = serde_json::to_value(&issue)?;
465 if comments {
466 let comments = storage.get_comments(&id)?;
467 output["comments"] = serde_json::to_value(&comments)?;
468 }
469 if events {
470 let events = storage.get_events(&id)?;
471 output["events"] = serde_json::to_value(&events)?;
472 }
473 println!("{}", serde_json::to_string_pretty(&output)?);
474 } else {
475 print_issue(&issue);
476
477 if comments {
478 let comments = storage.get_comments(&id)?;
479 if !comments.is_empty() {
480 println!("\nComments:");
481 for comment in comments {
482 println!(" [{} by {}]: {}", comment.created_at.format("%Y-%m-%d"), comment.author, comment.text);
483 }
484 }
485 }
486 }
487 }
488
489 Command::List {
490 status,
491 issue_type,
492 assignee,
493 label,
494 all,
495 limit,
496 } => {
497 let storage = open_storage()?;
498
499 let mut filter = if all {
500 IssueFilter::new().include_deleted()
501 } else {
502 IssueFilter::active()
503 };
504
505 if let Some(ref s) = status {
506 filter.status = s.parse().ok();
507 }
508 if let Some(ref t) = issue_type {
509 filter.issue_type = t.parse().ok();
510 }
511 filter.assignee = assignee;
512 filter.label = label;
513 filter.limit = limit;
514
515 let issues = storage.search_issues(&filter)?;
516
517 if json_output {
518 println!("{}", serde_json::to_string(&issues)?);
519 } else {
520 if issues.is_empty() {
521 println!("No issues found");
522 } else {
523 for issue in &issues {
524 print_issue_summary(issue);
525 }
526 println!("\n{} issue(s)", issues.len());
527 }
528 }
529 }
530
531 Command::Ready { limit } => {
532 let storage = open_storage()?;
533
534 let mut issues = storage.get_ready_work()?;
535 if let Some(n) = limit {
536 issues.truncate(n);
537 }
538
539 if json_output {
540 println!("{}", serde_json::to_string(&issues)?);
541 } else {
542 if issues.is_empty() {
543 println!("No ready issues");
544 } else {
545 println!("Ready issues:");
546 for issue in &issues {
547 print_issue_summary(issue);
548 }
549 println!("\n{} ready issue(s)", issues.len());
550 }
551 }
552 }
553
554 Command::Update {
555 id,
556 title,
557 description,
558 status,
559 priority,
560 assignee,
561 } => {
562 let storage = open_storage()?;
563
564 let mut issue = storage.get_issue(&id)?
565 .context(format!("Issue not found: {}", id))?;
566
567 if let Some(t) = title {
568 issue.title = t;
569 }
570 if let Some(d) = description {
571 issue.description = Some(d);
572 }
573 if let Some(s) = status {
574 issue.status = s.parse().unwrap_or(issue.status);
575 }
576 if let Some(p) = priority {
577 issue.priority = p.clamp(0, 4);
578 }
579 if let Some(a) = assignee {
580 issue.assignee = Some(a);
581 }
582
583 issue.touch();
584 issue.update_content_hash();
585 storage.update_issue(&issue)?;
586
587 if json_output {
588 println!("{}", serde_json::to_string(&issue)?);
589 } else {
590 println!("Updated issue: {}", id);
591 }
592 }
593
594 Command::Close { id, reason } => {
595 let storage = open_storage()?;
596 storage.close_issue(&id, &actor, reason.as_deref())?;
597
598 if json_output {
599 println!(r#"{{"success": true, "id": "{}"}}"#, id);
600 } else {
601 println!("Closed issue: {}", id);
602 }
603 }
604
605 Command::Delete { id, reason } => {
606 let storage = open_storage()?;
607 storage.delete_issue(&id, &actor, reason.as_deref())?;
608
609 if json_output {
610 println!(r#"{{"success": true, "id": "{}"}}"#, id);
611 } else {
612 println!("Deleted issue: {}", id);
613 }
614 }
615
616 Command::Dep { action } => {
617 let storage = open_storage()?;
618
619 match action {
620 DepAction::Add { issue_id, depends_on, dep_type } => {
621 let dep = Dependency {
622 issue_id: issue_id.clone(),
623 depends_on_id: depends_on.clone(),
624 dep_type: dep_type.parse().unwrap_or_default(),
625 created_at: chrono::Utc::now(),
626 created_by: Some(actor),
627 metadata: None,
628 thread_id: None,
629 };
630 storage.add_dependency(&dep)?;
631
632 if json_output {
633 println!(r#"{{"success": true}}"#);
634 } else {
635 println!("Added dependency: {} -> {}", issue_id, depends_on);
636 }
637 }
638
639 DepAction::Remove { issue_id, depends_on } => {
640 storage.remove_dependency(&issue_id, &depends_on)?;
641
642 if json_output {
643 println!(r#"{{"success": true}}"#);
644 } else {
645 println!("Removed dependency: {} -> {}", issue_id, depends_on);
646 }
647 }
648
649 DepAction::List { issue_id } => {
650 let deps = storage.get_dependencies(&issue_id)?;
651
652 if json_output {
653 println!("{}", serde_json::to_string(&deps)?);
654 } else {
655 if deps.is_empty() {
656 println!("No dependencies");
657 } else {
658 println!("Dependencies for {}:", issue_id);
659 for dep in &deps {
660 println!(" {} ({})", dep.depends_on_id, dep.dep_type);
661 }
662 }
663 }
664 }
665 }
666 }
667
668 Command::Label { action } => {
669 let storage = open_storage()?;
670
671 match action {
672 LabelAction::Add { issue_id, label } => {
673 storage.add_label(&issue_id, &label)?;
674
675 if json_output {
676 println!(r#"{{"success": true}}"#);
677 } else {
678 println!("Added label '{}' to {}", label, issue_id);
679 }
680 }
681
682 LabelAction::Remove { issue_id, label } => {
683 storage.remove_label(&issue_id, &label)?;
684
685 if json_output {
686 println!(r#"{{"success": true}}"#);
687 } else {
688 println!("Removed label '{}' from {}", label, issue_id);
689 }
690 }
691
692 LabelAction::List { issue_id } => {
693 let labels = storage.get_labels(&issue_id)?;
694
695 if json_output {
696 println!("{}", serde_json::to_string(&labels)?);
697 } else {
698 if labels.is_empty() {
699 println!("No labels");
700 } else {
701 println!("Labels: {}", labels.join(", "));
702 }
703 }
704 }
705 }
706 }
707
708 Command::Stats => {
709 let storage = open_storage()?;
710 let stats = storage.get_statistics()?;
711
712 if json_output {
713 println!("{}", serde_json::to_string(&stats)?);
714 } else {
715 println!("Issues:");
716 println!(" Total: {}", stats.total_issues);
717 println!(" Open: {}", stats.open_issues);
718 println!(" In Progress: {}", stats.in_progress_issues);
719 println!(" Blocked: {}", stats.blocked_issues);
720 println!(" Closed: {}", stats.closed_issues);
721 println!(" Ready: {}", stats.ready_issues);
722 println!("\nDependencies: {}", stats.total_dependencies);
723 }
724 }
725
726 Command::Daemon { action } => {
727 let beads_dir = find_beads_dir()
728 .context("Not in a beads repository")?;
729
730 match action {
731 DaemonAction::Start { foreground, auto_commit, auto_push } => {
732 if !foreground {
733 println!("Starting daemon in background...");
735
736 let db_path = beads_dir.join("beads.db");
737 let storage = SqliteStorage::open(&db_path)?;
738 let config = DaemonConfig {
739 auto_commit,
740 auto_push,
741 ..Default::default()
742 };
743
744 let daemon = Daemon::new(storage, beads_dir, config);
745 daemon.start().await?;
746 } else {
747 let db_path = beads_dir.join("beads.db");
749 let storage = SqliteStorage::open(&db_path)?;
750 let config = DaemonConfig {
751 auto_commit,
752 auto_push,
753 ..Default::default()
754 };
755
756 let daemon = Daemon::new(storage, beads_dir, config);
757 daemon.start().await?;
758 }
759 }
760
761 DaemonAction::Stop => {
762 let client = DaemonClient::new(&beads_dir);
763 client.shutdown().await?;
764 println!("Daemon stopped");
765 }
766
767 DaemonAction::Status => {
768 let client = DaemonClient::new(&beads_dir);
769 match client.health().await {
770 Ok(health) => {
771 if json_output {
772 println!("{}", serde_json::to_string(&health)?);
773 } else {
774 println!("Daemon running:");
775 println!(" PID: {}", health.pid);
776 println!(" Uptime: {}s", health.uptime_secs);
777 println!(" Version: {}", health.version);
778 }
779 }
780 Err(_) => {
781 if json_output {
782 println!(r#"{{"running": false}}"#);
783 } else {
784 println!("Daemon not running");
785 }
786 }
787 }
788 }
789 }
790 }
791
792 Command::Compact { level, id } => {
793 let storage = Arc::new(open_storage()?);
794 let compactor = Compactor::new(storage);
795
796 let stats = if let Some(issue_id) = id {
797 compactor.compact_issue(&issue_id, level)?
798 } else {
799 compactor.compact_completed(level)?
800 };
801
802 if json_output {
803 println!("{}", serde_json::to_string(&stats)?);
804 } else {
805 println!("Compaction complete:");
806 println!(" Compacted: {}", stats.compacted);
807 println!(" Skipped: {}", stats.skipped);
808 println!(" Bytes saved: {}", stats.bytes_saved);
809 if !stats.errors.is_empty() {
810 println!(" Errors: {}", stats.errors.len());
811 }
812 }
813 }
814
815 Command::Config { key, value, delete } => {
816 let storage = open_storage()?;
817
818 match (key, value, delete) {
819 (None, None, _) => {
820 let config = storage.get_all_config()?;
822 if json_output {
823 println!("{}", serde_json::to_string(&config)?);
824 } else {
825 for (k, v) in &config {
826 println!("{} = {}", k, v);
827 }
828 }
829 }
830 (Some(k), None, false) => {
831 if let Some(v) = storage.get_config(&k)? {
833 if json_output {
834 println!(r#"{{"{}": "{}"}}"#, k, v);
835 } else {
836 println!("{}", v);
837 }
838 } else if !json_output {
839 println!("Not set");
840 }
841 }
842 (Some(k), Some(v), _) => {
843 storage.set_config(&k, &v)?;
845 if json_output {
846 println!(r#"{{"success": true}}"#);
847 } else {
848 println!("Set {} = {}", k, v);
849 }
850 }
851 (Some(k), None, true) => {
852 storage.delete_config(&k)?;
854 if json_output {
855 println!(r#"{{"success": true}}"#);
856 } else {
857 println!("Deleted {}", k);
858 }
859 }
860 (None, Some(_), _) => {
861 anyhow::bail!("Cannot set a value without a key");
863 }
864 }
865 }
866
867 Command::Context { action } => {
868 let ctx_store = open_context_store()?;
869
870 match action {
871 ContextAction::Get { key } => {
872 if let Some(entry) = ctx_store.get(&key)? {
873 if json_output {
874 println!("{}", serde_json::to_string(&entry)?);
875 } else {
876 println!("Key: {}", entry.key);
877 println!("Value: {}", serde_json::to_string_pretty(&entry.value)?);
878 println!("Created: {}", entry.created_at.format("%Y-%m-%d %H:%M"));
879 println!("Updated: {}", entry.updated_at.format("%Y-%m-%d %H:%M"));
880 if let Some(ref expires) = entry.expires_at {
881 println!("Expires: {}", expires.format("%Y-%m-%d %H:%M"));
882 }
883 }
884 } else if json_output {
885 println!("null");
886 } else {
887 println!("Not found: {}", key);
888 }
889 }
890
891 ContextAction::Set { key, value, ttl, file } => {
892 let json_value: serde_json::Value = serde_json::from_str(&value)
893 .context("Invalid JSON value")?;
894
895 let mut entry = ContextEntry::new(&key, json_value);
896
897 if let Some(ttl_secs) = ttl {
898 entry = entry.with_ttl(ttl_secs);
899 }
900
901 if let Some(file_path) = file {
902 if let Some(mtime) = ctx_store.get_file_mtime(&file_path) {
903 entry = entry.with_file_info(&file_path, mtime);
904 }
905 }
906
907 ctx_store.set(entry)?;
908
909 if json_output {
910 println!(r#"{{"success": true}}"#);
911 } else {
912 println!("Set: {}", key);
913 }
914 }
915
916 ContextAction::Delete { key } => {
917 ctx_store.delete(&key)?;
918
919 if json_output {
920 println!(r#"{{"success": true}}"#);
921 } else {
922 println!("Deleted: {}", key);
923 }
924 }
925
926 ContextAction::List { namespace, prefix, limit } => {
927 let ns = namespace.as_deref().and_then(parse_namespace);
928 let entries = ctx_store.list_simple(ns, prefix.as_deref())?;
929
930 let entries: Vec<_> = if let Some(n) = limit {
931 entries.into_iter().take(n).collect()
932 } else {
933 entries
934 };
935
936 if json_output {
937 println!("{}", serde_json::to_string(&entries)?);
938 } else {
939 if entries.is_empty() {
940 println!("No entries found");
941 } else {
942 for entry in &entries {
943 let expired = if entry.is_expired() { " [EXPIRED]" } else { "" };
944 println!(" {}{}", entry.key, expired);
945 }
946 println!("\n{} entries", entries.len());
947 }
948 }
949 }
950
951 ContextAction::Clear { namespace, expired } => {
952 let count = if expired {
953 ctx_store.cleanup_expired()?
954 } else if let Some(ref ns_str) = namespace {
955 if let Some(ns) = parse_namespace(ns_str) {
956 ctx_store.clear_namespace(ns)?
957 } else {
958 anyhow::bail!("Invalid namespace: {}", ns_str);
959 }
960 } else {
961 ctx_store.clear_all()?
962 };
963
964 if json_output {
965 println!(r#"{{"cleared": {}}}"#, count);
966 } else {
967 println!("Cleared {} entries", count);
968 }
969 }
970
971 ContextAction::Stats => {
972 let stats = ctx_store.stats()?;
973
974 if json_output {
975 println!("{}", serde_json::to_string(&stats)?);
976 } else {
977 println!("Context Store Statistics:");
978 println!(" Total entries: {}", stats.total_entries);
979 println!(" Expired entries: {}", stats.expired_entries);
980 println!("\nBy namespace:");
981 for (ns, count) in &stats.by_namespace {
982 println!(" {}: {}", ns, count);
983 }
984 }
985 }
986
987 ContextAction::File { path } => {
988 if let Some(file_ctx) = ctx_store.get_file_context(&path)? {
989 if json_output {
990 println!("{}", serde_json::to_string(&file_ctx)?);
991 } else {
992 println!("File: {}", file_ctx.path);
993 if let Some(ref summary) = file_ctx.summary {
994 println!("Summary: {}", summary);
995 }
996 if let Some(ref lang) = file_ctx.language {
997 println!("Language: {}", lang);
998 }
999 if !file_ctx.symbols.is_empty() {
1000 println!("Symbols: {}", file_ctx.symbols.len());
1001 for sym in &file_ctx.symbols {
1002 println!(" - {} ({:?})", sym.name, sym.kind);
1003 }
1004 }
1005 }
1006 } else if json_output {
1007 println!("null");
1008 } else {
1009 println!("No context for file: {}", path);
1010 }
1011 }
1012
1013 ContextAction::Project { set } => {
1014 if let Some(value) = set {
1015 let project_ctx: ProjectContext = serde_json::from_str(&value)
1016 .context("Invalid project context JSON")?;
1017 ctx_store.set_project_context(&project_ctx)?;
1018
1019 if json_output {
1020 println!(r#"{{"success": true}}"#);
1021 } else {
1022 println!("Project context updated");
1023 }
1024 } else {
1025 if let Some(project_ctx) = ctx_store.get_project_context()? {
1026 if json_output {
1027 println!("{}", serde_json::to_string(&project_ctx)?);
1028 } else {
1029 if let Some(ref name) = project_ctx.name {
1030 println!("Name: {}", name);
1031 }
1032 if let Some(ref desc) = project_ctx.description {
1033 println!("Description: {}", desc);
1034 }
1035 if !project_ctx.languages.is_empty() {
1036 println!("Languages: {}", project_ctx.languages.join(", "));
1037 }
1038 if !project_ctx.frameworks.is_empty() {
1039 println!("Frameworks: {}", project_ctx.frameworks.join(", "));
1040 }
1041 }
1042 } else if json_output {
1043 println!("null");
1044 } else {
1045 println!("No project context set");
1046 }
1047 }
1048 }
1049
1050 ContextAction::Session { session_id } => {
1051 if let Some(session) = ctx_store.get_session(&session_id)? {
1052 if json_output {
1053 println!("{}", serde_json::to_string(&session)?);
1054 } else {
1055 println!("Session: {}", session.session_id);
1056 if let Some(ref agent) = session.agent_id {
1057 println!("Agent: {}", agent);
1058 }
1059 if let Some(ref task) = session.current_task {
1060 println!("Task: {}", task);
1061 }
1062 if !session.working_files.is_empty() {
1063 println!("Working files:");
1064 for f in &session.working_files {
1065 println!(" - {}", f);
1066 }
1067 }
1068 if !session.decisions.is_empty() {
1069 println!("Decisions: {}", session.decisions.len());
1070 }
1071 }
1072 } else if json_output {
1073 println!("null");
1074 } else {
1075 println!("No session found: {}", session_id);
1076 }
1077 }
1078
1079 ContextAction::Refresh => {
1080 ctx_store.refresh_invalidation()?;
1081
1082 if json_output {
1083 println!(r#"{{"success": true}}"#);
1084 } else {
1085 println!("Git state refreshed");
1086 }
1087 }
1088 }
1089 }
1090 }
1091
1092 Ok(())
1093}
1094
1095fn open_storage() -> Result<SqliteStorage> {
1097 let db_path = find_database_path()
1098 .context("Not in a beads repository. Run 'bd init' first.")?;
1099 SqliteStorage::open(&db_path).context("Failed to open database")
1100}
1101
1102fn open_context_store() -> Result<ContextStore> {
1104 let beads_dir = find_beads_dir()
1105 .context("Not in a beads repository. Run 'bd init' first.")?;
1106 let ctx_path = beads_dir.join("context.db");
1107 ContextStore::open(&ctx_path).context("Failed to open context store")
1108}
1109
1110fn parse_namespace(s: &str) -> Option<Namespace> {
1112 match s.to_lowercase().as_str() {
1113 "file" => Some(Namespace::File),
1114 "symbol" => Some(Namespace::Symbol),
1115 "project" => Some(Namespace::Project),
1116 "session" => Some(Namespace::Session),
1117 "agent" => Some(Namespace::Agent),
1118 "custom" => Some(Namespace::Custom),
1119 _ => None,
1120 }
1121}
1122
1123fn get_default_actor() -> String {
1125 std::env::var("BD_ACTOR")
1126 .or_else(|_| std::env::var("BEADS_ACTOR"))
1127 .or_else(|_| std::env::var("USER"))
1128 .unwrap_or_else(|_| "unknown".to_string())
1129}
1130
1131fn print_issue_summary(issue: &Issue) {
1133 let status_icon = match issue.status {
1134 Status::Open => "○",
1135 Status::InProgress => "◐",
1136 Status::Blocked => "◌",
1137 Status::Closed => "●",
1138 Status::Deferred => "◇",
1139 _ => "○",
1140 };
1141
1142 let priority_str = match issue.priority {
1143 0 => "P0",
1144 1 => "P1",
1145 2 => "P2",
1146 3 => "P3",
1147 _ => "P4",
1148 };
1149
1150 println!(
1151 "{} {} [{}] {} - {}",
1152 status_icon,
1153 issue.id,
1154 priority_str,
1155 issue.issue_type,
1156 issue.title
1157 );
1158}
1159
1160fn print_issue(issue: &Issue) {
1162 println!("ID: {}", issue.id);
1163 println!("Title: {}", issue.title);
1164 println!("Status: {}", issue.status);
1165 println!("Type: {}", issue.issue_type);
1166 println!("Priority: {}", issue.priority);
1167
1168 if let Some(ref assignee) = issue.assignee {
1169 println!("Assignee: {}", assignee);
1170 }
1171
1172 if !issue.labels.is_empty() {
1173 println!("Labels: {}", issue.labels.join(", "));
1174 }
1175
1176 if let Some(ref desc) = issue.description {
1177 println!("\nDescription:");
1178 println!("{}", desc);
1179 }
1180
1181 println!("\nCreated: {} by {}", issue.created_at.format("%Y-%m-%d %H:%M"), issue.created_by);
1182 println!("Updated: {}", issue.updated_at.format("%Y-%m-%d %H:%M"));
1183
1184 if let Some(ref closed_at) = issue.closed_at {
1185 println!("Closed: {}", closed_at.format("%Y-%m-%d %H:%M"));
1186 if let Some(ref reason) = issue.close_reason {
1187 println!("Reason: {}", reason);
1188 }
1189 }
1190}