Skip to main content

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(&current_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}