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