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