Skip to main content

ralph/commands/tutorial/
mod.rs

1//! Interactive tutorial command implementation.
2//!
3//! Responsibilities:
4//! - Orchestrate tutorial phases in sequence.
5//! - Provide options struct for tutorial configuration.
6//! - Re-export types for CLI handler.
7//!
8//! Not handled here:
9//! - CLI argument parsing (see cli/tutorial.rs).
10//! - Phase implementations (see phases.rs).
11
12mod phases;
13mod prompter;
14mod sandbox;
15
16pub use prompter::{
17    DialoguerTutorialPrompter, ScriptedResponse, ScriptedTutorialPrompter, TutorialPrompter,
18};
19pub use sandbox::TutorialSandbox;
20
21use anyhow::Result;
22
23/// Tutorial configuration options.
24pub struct TutorialOptions {
25    /// Run in interactive mode (require TTY).
26    pub interactive: bool,
27    /// Keep sandbox after completion.
28    pub keep_sandbox: bool,
29}
30
31/// Run the interactive tutorial.
32pub fn run_tutorial(opts: TutorialOptions) -> Result<()> {
33    if opts.interactive {
34        run_tutorial_interactive(opts.keep_sandbox)
35    } else {
36        run_tutorial_non_interactive(opts.keep_sandbox)
37    }
38}
39
40/// Run tutorial in interactive mode with Dialoguer.
41fn run_tutorial_interactive(keep_sandbox: bool) -> Result<()> {
42    let prompter = DialoguerTutorialPrompter;
43
44    phases::phase_welcome(&prompter)?;
45    let sandbox = phases::phase_setup(&prompter)?;
46    phases::phase_init(&prompter, &sandbox)?;
47    let task_id = phases::phase_create_task(&prompter, &sandbox)?;
48    phases::phase_dry_run(&prompter, &sandbox, &task_id)?;
49    phases::phase_review(&prompter, &sandbox)?;
50    phases::phase_cleanup(&prompter, sandbox, keep_sandbox)?;
51
52    Ok(())
53}
54
55/// Run tutorial with a custom prompter (for testing).
56pub fn run_tutorial_with_prompter(
57    prompter: &dyn TutorialPrompter,
58    keep_sandbox: bool,
59) -> Result<()> {
60    phases::phase_welcome(prompter)?;
61    let sandbox = phases::phase_setup(prompter)?;
62    phases::phase_init(prompter, &sandbox)?;
63    let task_id = phases::phase_create_task(prompter, &sandbox)?;
64    phases::phase_dry_run(prompter, &sandbox, &task_id)?;
65    phases::phase_review(prompter, &sandbox)?;
66    phases::phase_cleanup(prompter, sandbox, keep_sandbox)?;
67
68    Ok(())
69}
70
71/// Run tutorial non-interactively (minimal output, no prompts).
72fn run_tutorial_non_interactive(keep_sandbox: bool) -> Result<()> {
73    log::info!("Running tutorial in non-interactive mode");
74
75    let sandbox = sandbox::TutorialSandbox::create()?;
76    log::info!("Created sandbox at: {}", sandbox.path.display());
77
78    // Run init
79    let original_dir = std::env::current_dir()?;
80    std::env::set_current_dir(&sandbox.path)?;
81
82    let resolved = crate::config::resolve_from_cwd()?;
83    crate::commands::init::run_init(
84        &resolved,
85        crate::commands::init::InitOptions {
86            force: false,
87            force_lock: false,
88            interactive: false,
89            update_readme: false,
90        },
91    )?;
92
93    // Add task
94    let task_id = "RQ-0001";
95    let task = crate::contracts::Task {
96        id: task_id.to_string(),
97        title: "Tutorial task".to_string(),
98        description: Some("A sample tutorial task".to_string()),
99        status: crate::contracts::TaskStatus::Todo,
100        priority: crate::contracts::TaskPriority::Medium,
101        tags: vec!["tutorial".to_string()],
102        scope: vec!["src/lib.rs".to_string()],
103        plan: vec!["Add a farewell function".to_string()],
104        request: Some("Tutorial task".to_string()),
105        created_at: Some(crate::timeutil::now_utc_rfc3339_or_fallback()),
106        updated_at: Some(crate::timeutil::now_utc_rfc3339_or_fallback()),
107        ..Default::default()
108    };
109    let queue = crate::contracts::QueueFile {
110        version: 1,
111        tasks: vec![task],
112    };
113    crate::queue::save_queue(&sandbox.path.join(".ralph/queue.jsonc"), &queue)?;
114
115    std::env::set_current_dir(&original_dir)?;
116
117    log::info!("Tutorial completed successfully");
118
119    if keep_sandbox {
120        let path = sandbox.preserve();
121        log::info!("Sandbox preserved at: {}", path.display());
122    }
123
124    Ok(())
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn tutorial_options_creation() {
133        let opts = TutorialOptions {
134            interactive: true,
135            keep_sandbox: false,
136        };
137        assert!(opts.interactive);
138        assert!(!opts.keep_sandbox);
139    }
140}