cascade_cli/cli/mod.rs
1pub mod commands;
2pub mod output;
3
4use crate::errors::Result;
5use clap::{Parser, Subcommand};
6use clap_complete::Shell;
7use commands::entry::EntryAction;
8use commands::stack::StackAction;
9use commands::{MergeStrategyArg, RebaseStrategyArg};
10
11#[derive(Debug, Subcommand)]
12pub enum SyncAction {
13 /// Continue after resolving conflicts
14 Continue,
15 /// Abort in-progress sync
16 Abort,
17}
18
19#[derive(Debug, Subcommand)]
20pub enum RebaseAction {
21 /// Continue after resolving conflicts
22 Continue,
23 /// Abort in-progress rebase
24 Abort,
25}
26
27#[derive(Parser)]
28#[command(name = "ca")]
29#[command(about = "Cascade CLI - Stacked Diffs for Bitbucket")]
30#[command(version)]
31pub struct Cli {
32 #[command(subcommand)]
33 pub command: Commands,
34
35 /// Enable verbose logging
36 #[arg(long, short, global = true)]
37 pub verbose: bool,
38
39 /// Disable colored output
40 #[arg(long, global = true)]
41 pub no_color: bool,
42}
43
44/// Commands available in the CLI
45#[derive(Debug, Subcommand)]
46pub enum Commands {
47 /// Initialize repository for Cascade
48 Init {
49 /// Bitbucket Server URL
50 #[arg(long)]
51 bitbucket_url: Option<String>,
52
53 /// Force initialization even if already initialized
54 #[arg(long)]
55 force: bool,
56 },
57
58 /// Configuration management
59 Config {
60 #[command(subcommand)]
61 action: ConfigAction,
62 },
63
64 /// Stack management
65 Stacks {
66 #[command(subcommand)]
67 action: StackAction,
68 },
69
70 /// Entry management and editing
71 Entry {
72 #[command(subcommand)]
73 action: EntryAction,
74 },
75
76 /// Show repository overview and all stacks
77 Repo,
78
79 /// Show version information
80 Version,
81
82 /// Check repository health and configuration
83 Doctor,
84
85 /// Diagnose git2 TLS/SSH support issues
86 Diagnose,
87
88 /// Generate shell completions
89 Completions {
90 #[command(subcommand)]
91 action: CompletionsAction,
92 },
93
94 /// Interactive setup wizard
95 Setup {
96 /// Force reconfiguration if already initialized
97 #[arg(long)]
98 force: bool,
99 },
100
101 /// Launch interactive TUI for stack management
102 Tui,
103
104 /// Git hooks management
105 Hooks {
106 #[command(subcommand)]
107 action: HooksAction,
108 },
109
110 /// Visualize stacks and dependencies
111 Viz {
112 #[command(subcommand)]
113 action: VizAction,
114 },
115
116 /// Clean up orphaned temporary branches
117 Cleanup {
118 /// Actually delete branches (default is dry-run)
119 #[arg(long)]
120 execute: bool,
121
122 /// Force deletion even if branches have unmerged commits
123 #[arg(long)]
124 force: bool,
125 },
126
127 // Stack command shortcuts for commonly used operations
128 /// Show current stack details
129 Stack {
130 /// Show detailed pull request information
131 #[arg(short, long)]
132 verbose: bool,
133 /// Show mergability status for all PRs
134 #[arg(short, long)]
135 mergeable: bool,
136 },
137
138 /// Push current commit to the top of the stack (shortcut for 'stack push')
139 Push {
140 /// Branch name for this commit
141 #[arg(long, short)]
142 branch: Option<String>,
143 /// Commit message (if creating a new commit)
144 #[arg(long, short)]
145 message: Option<String>,
146 /// Use specific commit hash instead of HEAD
147 #[arg(long)]
148 commit: Option<String>,
149 /// Push commits since this reference (e.g., HEAD~3)
150 #[arg(long)]
151 since: Option<String>,
152 /// Push multiple specific commits (comma-separated)
153 #[arg(long)]
154 commits: Option<String>,
155 /// Squash last N commits into one before pushing
156 #[arg(long)]
157 squash: Option<usize>,
158 /// Squash all commits since this reference (e.g., HEAD~5)
159 #[arg(long)]
160 squash_since: Option<String>,
161 /// Auto-create feature branch when pushing from base branch
162 #[arg(long)]
163 auto_branch: bool,
164 /// Allow pushing commits from base branch (not recommended)
165 #[arg(long)]
166 allow_base_branch: bool,
167 /// Show what would be pushed without actually pushing
168 #[arg(long)]
169 dry_run: bool,
170 /// Skip confirmation prompts
171 #[arg(long, short = 'y')]
172 yes: bool,
173 },
174
175 /// Pop the top commit from the stack (shortcut for 'stack pop')
176 Pop {
177 /// Keep the branch (don't delete it)
178 #[arg(long)]
179 keep_branch: bool,
180 },
181
182 /// Drop (remove) stack entries by position (shortcut for 'stacks drop')
183 Drop {
184 /// Entry position or range (e.g., "3", "1-5", "1,3,5")
185 entry: String,
186 /// Keep the branch (don't delete it)
187 #[arg(long)]
188 keep_branch: bool,
189 /// Skip all confirmation prompts
190 #[arg(long, short)]
191 force: bool,
192 /// Skip confirmation prompts
193 #[arg(long, short = 'y')]
194 yes: bool,
195 },
196
197 /// Land (merge) approved stack entries (shortcut for 'stack land')
198 Land {
199 /// Stack entry number to land (1-based index, optional)
200 entry: Option<usize>,
201 /// Force land even with blocking issues (dangerous)
202 #[arg(short, long)]
203 force: bool,
204 /// Dry run - show what would be landed without doing it
205 #[arg(short, long)]
206 dry_run: bool,
207 /// Use server-side validation (safer, checks approvals/builds)
208 #[arg(long)]
209 auto: bool,
210 /// Wait for builds to complete before merging
211 #[arg(long)]
212 wait_for_builds: bool,
213 /// Merge strategy to use
214 #[arg(long, value_enum, default_value = "squash")]
215 strategy: Option<MergeStrategyArg>,
216 /// Maximum time to wait for builds (seconds)
217 #[arg(long, default_value = "1800")]
218 build_timeout: u64,
219 },
220
221 /// Auto-land all ready PRs (shortcut for 'stack autoland')
222 Autoland {
223 /// Force land even with blocking issues (dangerous)
224 #[arg(short, long)]
225 force: bool,
226 /// Dry run - show what would be landed without doing it
227 #[arg(short, long)]
228 dry_run: bool,
229 /// Wait for builds to complete before merging
230 #[arg(long)]
231 wait_for_builds: bool,
232 /// Merge strategy to use
233 #[arg(long, value_enum, default_value = "squash")]
234 strategy: Option<MergeStrategyArg>,
235 /// Maximum time to wait for builds (seconds)
236 #[arg(long, default_value = "1800")]
237 build_timeout: u64,
238 },
239
240 /// Sync operations (shortcut for 'stack sync')
241 Sync {
242 #[command(subcommand)]
243 action: Option<SyncAction>,
244
245 /// Force sync even if there are conflicts
246 #[arg(long, global = true)]
247 force: bool,
248 /// Also cleanup merged branches after sync
249 #[arg(long, global = true)]
250 cleanup: bool,
251 /// Interactive mode for conflict resolution
252 #[arg(long, short, global = true)]
253 interactive: bool,
254 },
255
256 /// Rebase operations (shortcut for 'stack rebase')
257 Rebase {
258 #[command(subcommand)]
259 action: Option<RebaseAction>,
260
261 /// Interactive rebase
262 #[arg(long, short, global = true)]
263 interactive: bool,
264 /// Target base branch (defaults to stack's base branch)
265 #[arg(long, global = true)]
266 onto: Option<String>,
267 /// Rebase strategy to use
268 #[arg(long, value_enum, global = true)]
269 strategy: Option<RebaseStrategyArg>,
270 },
271
272 /// Switch to a different stack (shortcut for 'stacks switch')
273 Switch {
274 /// Name of the stack to switch to
275 #[arg(value_hint = clap::ValueHint::Other)]
276 name: String,
277 },
278
279 /// Analyze conflicts in the repository
280 Conflicts {
281 /// Show detailed information about each conflict
282 #[arg(long)]
283 detailed: bool,
284
285 /// Only show conflicts that can be auto-resolved
286 #[arg(long)]
287 auto_only: bool,
288
289 /// Only show conflicts that require manual resolution
290 #[arg(long)]
291 manual_only: bool,
292
293 /// Analyze specific files (if not provided, analyzes all conflicted files)
294 #[arg(value_name = "FILE")]
295 files: Vec<String>,
296 },
297
298 /// Deactivate the current stack - turn off stack mode (shortcut for 'stacks deactivate')
299 Deactivate {
300 /// Force deactivation without confirmation
301 #[arg(long)]
302 force: bool,
303 },
304
305 /// Submit a stack entry for review (shortcut for 'stacks submit')
306 Submit {
307 /// Stack entry number (1-based, defaults to all unsubmitted)
308 entry: Option<usize>,
309 /// Pull request title
310 #[arg(long, short)]
311 title: Option<String>,
312 /// Pull request description
313 #[arg(long, short)]
314 description: Option<String>,
315 /// Submit range of entries (e.g., "1-3" or "2,4,6")
316 #[arg(long)]
317 range: Option<String>,
318 /// Create draft pull requests (default: true, use --no-draft to create ready PRs)
319 #[arg(long, default_value_t = true)]
320 draft: bool,
321 /// Open the PR(s) in your default browser after submission (default: true, use --no-open to disable)
322 #[arg(long, default_value_t = true)]
323 open: bool,
324 },
325
326 /// Validate stack integrity and handle branch modifications (shortcut for 'stacks validate')
327 Validate {
328 /// Name of the stack (defaults to active stack)
329 name: Option<String>,
330 /// Auto-fix mode: incorporate, split, or reset
331 #[arg(long)]
332 fix: Option<String>,
333 },
334
335 /// Internal command for shell completion (hidden)
336 #[command(hide = true)]
337 CompletionHelper {
338 #[command(subcommand)]
339 action: CompletionHelperAction,
340 },
341}
342
343/// Git hooks actions
344#[derive(Debug, Subcommand)]
345pub enum HooksAction {
346 /// Install Cascade Git hooks
347 Install {
348 /// Install all hooks including post-commit (default: essential hooks only)
349 #[arg(long)]
350 all: bool,
351
352 /// Skip prerequisite checks (repository type, configuration validation)
353 #[arg(long)]
354 skip_checks: bool,
355
356 /// Allow installation on main/master branches (not recommended)
357 #[arg(long)]
358 allow_main_branch: bool,
359
360 /// Skip confirmation prompt
361 #[arg(long, short)]
362 yes: bool,
363
364 /// Force installation even if checks fail (not recommended)
365 #[arg(long)]
366 force: bool,
367 },
368
369 /// Uninstall all Cascade Git hooks
370 Uninstall,
371
372 /// Show Git hooks status
373 Status,
374
375 /// Install a specific hook
376 Add {
377 /// Hook name (post-commit, pre-push, commit-msg, prepare-commit-msg)
378 hook: String,
379
380 /// Skip prerequisite checks
381 #[arg(long)]
382 skip_checks: bool,
383
384 /// Force installation even if checks fail
385 #[arg(long)]
386 force: bool,
387 },
388
389 /// Remove a specific hook
390 Remove {
391 /// Hook name (post-commit, pre-push, commit-msg, prepare-commit-msg)
392 hook: String,
393 },
394}
395
396/// Visualization actions
397#[derive(Debug, Subcommand)]
398pub enum VizAction {
399 /// Show stack diagram
400 Stack {
401 /// Stack name (defaults to active stack)
402 name: Option<String>,
403 /// Output format (ascii, mermaid, dot, plantuml)
404 #[arg(long, short)]
405 format: Option<String>,
406 /// Output file path
407 #[arg(long, short)]
408 output: Option<String>,
409 /// Compact mode (less details)
410 #[arg(long)]
411 compact: bool,
412 /// Disable colors
413 #[arg(long)]
414 no_colors: bool,
415 },
416
417 /// Show dependency graph of all stacks
418 Deps {
419 /// Output format (ascii, mermaid, dot, plantuml)
420 #[arg(long, short)]
421 format: Option<String>,
422 /// Output file path
423 #[arg(long, short)]
424 output: Option<String>,
425 /// Compact mode (less details)
426 #[arg(long)]
427 compact: bool,
428 /// Disable colors
429 #[arg(long)]
430 no_colors: bool,
431 },
432}
433
434/// Shell completion actions
435#[derive(Debug, Subcommand)]
436pub enum CompletionsAction {
437 /// Generate completions for a shell
438 Generate {
439 /// Shell to generate completions for
440 #[arg(value_enum)]
441 shell: Shell,
442 },
443
444 /// Install completions for available shells
445 Install {
446 /// Specific shell to install for
447 #[arg(long, value_enum)]
448 shell: Option<Shell>,
449 },
450
451 /// Show completion installation status
452 Status,
453}
454
455/// Hidden completion helper actions
456#[derive(Debug, Subcommand)]
457pub enum CompletionHelperAction {
458 /// List available stack names
459 StackNames,
460}
461
462#[derive(Debug, Subcommand)]
463pub enum ConfigAction {
464 /// Set a configuration value
465 Set {
466 /// Configuration key (e.g., bitbucket.url)
467 key: String,
468 /// Configuration value
469 value: String,
470 },
471
472 /// Get a configuration value
473 Get {
474 /// Configuration key
475 key: String,
476 },
477
478 /// List all configuration values
479 List,
480
481 /// Remove a configuration value
482 Unset {
483 /// Configuration key
484 key: String,
485 },
486}
487
488impl Cli {
489 pub async fn run(self) -> Result<()> {
490 // Set up logging based on verbosity
491 self.setup_logging();
492
493 // Initialize git2 to use system certificates by default
494 // This ensures we work out-of-the-box in corporate environments
495 // just like git CLI and other modern dev tools (Graphite, Sapling, Phabricator)
496 self.init_git2_ssl()?;
497
498 match self.command {
499 Commands::Init {
500 bitbucket_url,
501 force,
502 } => commands::init::run(bitbucket_url, force).await,
503 Commands::Config { action } => commands::config::run(action).await,
504 Commands::Stacks { action } => commands::stack::run(action).await,
505 Commands::Entry { action } => commands::entry::run(action).await,
506 Commands::Repo => commands::status::run().await,
507 Commands::Version => commands::version::run().await,
508 Commands::Doctor => commands::doctor::run().await,
509 Commands::Diagnose => commands::diagnose::run().await,
510
511 Commands::Completions { action } => match action {
512 CompletionsAction::Generate { shell } => {
513 commands::completions::generate_completions(shell)
514 }
515 CompletionsAction::Install { shell } => {
516 commands::completions::install_completions(shell)
517 }
518 CompletionsAction::Status => commands::completions::show_completions_status(),
519 },
520
521 Commands::Setup { force } => commands::setup::run(force).await,
522
523 Commands::Tui => commands::tui::run().await,
524
525 Commands::Cleanup { execute, force } => commands::cleanup::run(execute, force).await,
526
527 Commands::Hooks { action } => match action {
528 HooksAction::Install {
529 all,
530 skip_checks,
531 allow_main_branch,
532 yes,
533 force,
534 } => {
535 if all {
536 // Install all hooks including post-commit
537 commands::hooks::install_with_options(
538 skip_checks,
539 allow_main_branch,
540 yes,
541 force,
542 )
543 .await
544 } else {
545 // Install essential hooks by default (excludes post-commit)
546 // Users can install post-commit separately with 'ca hooks add post-commit'
547 commands::hooks::install_essential().await
548 }
549 }
550 HooksAction::Uninstall => commands::hooks::uninstall().await,
551 HooksAction::Status => commands::hooks::status().await,
552 HooksAction::Add {
553 hook,
554 skip_checks,
555 force,
556 } => commands::hooks::install_hook_with_options(&hook, skip_checks, force).await,
557 HooksAction::Remove { hook } => commands::hooks::uninstall_hook(&hook).await,
558 },
559
560 Commands::Viz { action } => match action {
561 VizAction::Stack {
562 name,
563 format,
564 output,
565 compact,
566 no_colors,
567 } => {
568 commands::viz::show_stack(
569 name.clone(),
570 format.clone(),
571 output.clone(),
572 compact,
573 no_colors,
574 )
575 .await
576 }
577 VizAction::Deps {
578 format,
579 output,
580 compact,
581 no_colors,
582 } => {
583 commands::viz::show_dependencies(
584 format.clone(),
585 output.clone(),
586 compact,
587 no_colors,
588 )
589 .await
590 }
591 },
592
593 Commands::Stack { verbose, mergeable } => {
594 commands::stack::show(verbose, mergeable).await
595 }
596
597 Commands::Push {
598 branch,
599 message,
600 commit,
601 since,
602 commits,
603 squash,
604 squash_since,
605 auto_branch,
606 allow_base_branch,
607 dry_run,
608 yes,
609 } => {
610 commands::stack::push(
611 branch,
612 message,
613 commit,
614 since,
615 commits,
616 squash,
617 squash_since,
618 auto_branch,
619 allow_base_branch,
620 dry_run,
621 yes,
622 )
623 .await
624 }
625
626 Commands::Pop { keep_branch } => commands::stack::pop(keep_branch).await,
627
628 Commands::Drop {
629 entry,
630 keep_branch,
631 force,
632 yes,
633 } => commands::stack::drop(entry, keep_branch, force, yes).await,
634
635 Commands::Land {
636 entry,
637 force,
638 dry_run,
639 auto,
640 wait_for_builds,
641 strategy,
642 build_timeout,
643 } => {
644 commands::stack::land(
645 entry,
646 force,
647 dry_run,
648 auto,
649 wait_for_builds,
650 strategy,
651 build_timeout,
652 )
653 .await
654 }
655
656 Commands::Autoland {
657 force,
658 dry_run,
659 wait_for_builds,
660 strategy,
661 build_timeout,
662 } => {
663 commands::stack::autoland(force, dry_run, wait_for_builds, strategy, build_timeout)
664 .await
665 }
666
667 Commands::Sync {
668 action,
669 force,
670 cleanup,
671 interactive,
672 } => match action {
673 Some(SyncAction::Continue) => commands::stack::continue_sync().await,
674 Some(SyncAction::Abort) => commands::stack::abort_sync().await,
675 None => commands::stack::sync(force, cleanup, interactive).await,
676 },
677
678 Commands::Rebase {
679 action,
680 interactive,
681 onto,
682 strategy,
683 } => match action {
684 Some(RebaseAction::Continue) => commands::stack::continue_rebase().await,
685 Some(RebaseAction::Abort) => commands::stack::abort_rebase().await,
686 None => commands::stack::rebase(interactive, onto, strategy).await,
687 },
688
689 Commands::Switch { name } => commands::stack::switch(name).await,
690
691 Commands::Conflicts {
692 detailed,
693 auto_only,
694 manual_only,
695 files,
696 } => {
697 commands::conflicts::run(commands::conflicts::ConflictsArgs {
698 detailed,
699 auto_only,
700 manual_only,
701 files,
702 })
703 .await
704 }
705
706 Commands::Deactivate { force } => commands::stack::deactivate(force).await,
707
708 Commands::Submit {
709 entry,
710 title,
711 description,
712 range,
713 draft,
714 open,
715 } => {
716 // Delegate to the stacks submit functionality
717 let submit_action = StackAction::Submit {
718 entry,
719 title,
720 description,
721 range,
722 draft,
723 open,
724 };
725 commands::stack::run(submit_action).await
726 }
727
728 Commands::Validate { name, fix } => {
729 // Delegate to the stacks validate functionality
730 let validate_action = StackAction::Validate { name, fix };
731 commands::stack::run(validate_action).await
732 }
733
734 Commands::CompletionHelper { action } => handle_completion_helper(action).await,
735 }
736 }
737
738 /// Initialize git2 to use system certificates by default
739 /// This makes Cascade work like git CLI in corporate environments
740 fn init_git2_ssl(&self) -> Result<()> {
741 // Only import SSL functions on platforms that use them
742 #[cfg(any(target_os = "macos", target_os = "linux"))]
743 use git2::opts::{set_ssl_cert_dir, set_ssl_cert_file};
744
745 // Configure git2 to use system certificate store
746 // This matches behavior of git CLI and tools like Graphite/Sapling
747 tracing::debug!("Initializing git2 SSL configuration with system certificates");
748
749 // Try to use system certificate locations
750 // On macOS: /etc/ssl/cert.pem, /usr/local/etc/ssl/cert.pem
751 // On Linux: /etc/ssl/certs/ca-certificates.crt, /etc/ssl/certs/ca-bundle.crt
752 // On Windows: Uses Windows certificate store automatically
753
754 #[cfg(target_os = "macos")]
755 {
756 // macOS certificate locations (certificate files)
757 let cert_files = [
758 "/etc/ssl/cert.pem",
759 "/usr/local/etc/ssl/cert.pem",
760 "/opt/homebrew/etc/ca-certificates/cert.pem",
761 ];
762
763 for cert_path in &cert_files {
764 if std::path::Path::new(cert_path).exists() {
765 tracing::debug!("Using macOS system certificates from: {}", cert_path);
766 if let Err(e) = unsafe { set_ssl_cert_file(cert_path) } {
767 tracing::trace!(
768 "SSL cert file {} not supported by TLS backend: {}",
769 cert_path,
770 e
771 );
772 } else {
773 return Ok(());
774 }
775 }
776 }
777
778 // Fallback to certificate directories
779 let cert_dirs = ["/etc/ssl/certs", "/usr/local/etc/ssl/certs"];
780
781 for cert_dir in &cert_dirs {
782 if std::path::Path::new(cert_dir).exists() {
783 tracing::debug!("Using macOS system certificate directory: {}", cert_dir);
784 if let Err(e) = unsafe { set_ssl_cert_dir(cert_dir) } {
785 tracing::trace!(
786 "SSL cert directory {} not supported by TLS backend: {}",
787 cert_dir,
788 e
789 );
790 } else {
791 return Ok(());
792 }
793 }
794 }
795 }
796
797 #[cfg(target_os = "linux")]
798 {
799 // Linux certificate files
800 let cert_files = [
801 "/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu
802 "/etc/ssl/certs/ca-bundle.crt", // RHEL/CentOS
803 "/etc/pki/tls/certs/ca-bundle.crt", // Fedora/RHEL
804 "/etc/ssl/ca-bundle.pem", // OpenSUSE
805 ];
806
807 for cert_path in &cert_files {
808 if std::path::Path::new(cert_path).exists() {
809 tracing::debug!("Using Linux system certificates from: {}", cert_path);
810 if let Err(e) = unsafe { set_ssl_cert_file(cert_path) } {
811 tracing::trace!(
812 "SSL cert file {} not supported by TLS backend: {}",
813 cert_path,
814 e
815 );
816 } else {
817 return Ok(());
818 }
819 }
820 }
821
822 // Fallback to certificate directories
823 let cert_dirs = ["/etc/ssl/certs", "/etc/pki/tls/certs"];
824
825 for cert_dir in &cert_dirs {
826 if std::path::Path::new(cert_dir).exists() {
827 tracing::debug!("Using Linux system certificate directory: {}", cert_dir);
828 if let Err(e) = unsafe { set_ssl_cert_dir(cert_dir) } {
829 tracing::trace!(
830 "SSL cert directory {} not supported by TLS backend: {}",
831 cert_dir,
832 e
833 );
834 } else {
835 return Ok(());
836 }
837 }
838 }
839 }
840
841 #[cfg(target_os = "windows")]
842 {
843 // Windows uses system certificate store automatically via git2's default configuration
844 tracing::debug!("Using Windows system certificate store (automatic)");
845 }
846
847 tracing::debug!("System SSL certificate configuration complete");
848 tracing::debug!(
849 "Note: SSL warnings from libgit2 are normal - git CLI fallback will be used if needed"
850 );
851 Ok(())
852 }
853
854 fn setup_logging(&self) {
855 let level = if self.verbose {
856 tracing::Level::DEBUG
857 } else {
858 tracing::Level::INFO
859 };
860
861 let subscriber = tracing_subscriber::fmt()
862 .with_max_level(level)
863 .with_target(false)
864 .without_time();
865
866 if self.no_color {
867 subscriber.with_ansi(false).init();
868 } else {
869 subscriber.init();
870 }
871 }
872}
873
874/// Handle completion helper commands
875async fn handle_completion_helper(action: CompletionHelperAction) -> Result<()> {
876 match action {
877 CompletionHelperAction::StackNames => {
878 use crate::git::find_repository_root;
879 use crate::stack::StackManager;
880 use std::env;
881
882 // Try to get stack names, but silently fail if not in a repository
883 if let Ok(current_dir) = env::current_dir() {
884 if let Ok(repo_root) = find_repository_root(¤t_dir) {
885 if let Ok(manager) = StackManager::new(&repo_root) {
886 for (_, name, _, _, _) in manager.list_stacks() {
887 println!("{name}");
888 }
889 }
890 }
891 }
892 Ok(())
893 }
894 }
895}