Skip to main content

zoi_cli/cmd/
init.rs

1//! Implementation of the `init` command for creating a new `zoi.lua`.
2
3use std::fs;
4use std::io::IsTerminal;
5use std::path::Path;
6
7use anyhow::{Result, anyhow};
8use colored::Colorize;
9use dialoguer::{Input, Select};
10
11/// Derives a Lua-safe project name from a directory name.
12///
13/// Project names end up in a Lua string literal, so anything that could break
14/// out of one has to be neutralised rather than written verbatim.
15fn sanitize_project_name(raw: &str) -> String {
16    let cleaned: String = raw
17        .chars()
18        .map(|c| {
19            if c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.') {
20                c
21            } else {
22                '-'
23            }
24        })
25        .collect();
26
27    // A name must not start with a digit, because `project({ name = "1.0" })`
28    // is fine but the value is often reused in shell and Lua contexts where a
29    // leading dash or dot reads as a flag or a relative path.
30    let trimmed = cleaned.trim_matches(|c| c == '-' || c == '.').to_string();
31    if trimmed.is_empty() {
32        "my-project".to_string()
33    } else {
34        trimmed
35    }
36}
37
38/// Returns the name of the current directory, if it can be determined.
39fn current_dir_name() -> Option<String> {
40    std::env::current_dir()
41        .ok()
42        .and_then(|d| d.file_name().map(|n| n.to_string_lossy().to_string()))
43        .filter(|n| !n.is_empty())
44}
45
46/// Builds the contents of a new `zoi.lua`.
47///
48/// The block is deliberately minimal: `packages({})` is present so that
49/// `zoi install --save` has a block to append to without having to create one.
50fn template(project_name: &str) -> String {
51    format!(
52        "project({{\n    name = \"{project_name}\"\n}})\n\npackages({{\n    \
53         -- Record project dependencies here, or add them with:\n    --   zoi \
54         install --save <package>\n}})\n"
55    )
56}
57
58/// Runs the `init` command, writing a new `zoi.lua` in the current directory.
59///
60/// Refuses to overwrite an existing `zoi.lua`. Every decision is confirmed
61/// first when a terminal is attached, since this writes a file the user may not
62/// have wanted in that particular directory.
63///
64/// # Errors
65///
66/// Returns an error if a `zoi.lua` already exists, if the project name cannot
67/// be determined or is declined at the prompt, or if the file cannot be
68/// written.
69pub fn run(yes: bool, force: bool) -> Result<()> {
70    let config_path = Path::new("zoi.lua");
71
72    if config_path.exists() && !force {
73        return Err(anyhow!(
74            "A 'zoi.lua' already exists in this directory. Run 'zoi init \
75             --force' to overwrite it."
76        ));
77    }
78
79    // Prefer an explicit name, fall back to the directory, and only prompt when
80    // neither is usable.
81    let default_name = current_dir_name().map_or_else(
82        || "my-project".to_string(),
83        |n| sanitize_project_name(&n)
84    );
85
86    let project_name = if yes {
87        default_name
88    } else if std::io::stdin().is_terminal() {
89        let input = Input::<String>::new()
90            .with_prompt("Project name")
91            .default(default_name)
92            .allow_empty(false)
93            .interact_text()?;
94        sanitize_project_name(input.trim())
95    } else {
96        // Non-interactive without --yes: take the directory name rather than
97        // blocking on a prompt nobody can answer.
98        default_name
99    };
100
101    let contents = template(&project_name);
102
103    if config_path.exists() {
104        println!(
105            "{} Overwriting existing 'zoi.lua'...",
106            "Warning:".yellow().bold()
107        );
108    }
109
110    // Show what is about to be written before writing it. A file the user has
111    // never seen is hard to review after the fact.
112    if !yes && std::io::stdout().is_terminal() {
113        println!("\n{}", contents.dimmed());
114        let choice = Select::new()
115            .with_prompt("Write this zoi.lua?")
116            .items(["Yes", "No, cancel"])
117            .default(0)
118            .interact()?;
119        if choice == 1 {
120            println!("{}", "Aborted.".yellow());
121            return Ok(());
122        }
123    }
124
125    fs::write(config_path, &contents)?;
126
127    println!(
128        "{} Created {} for project '{}'",
129        "Success:".green().bold(),
130        "zoi.lua".cyan(),
131        project_name
132    );
133    println!(
134        "\nAdd a dependency with:\n    {}",
135        "zoi install --save <package>".dimmed()
136    );
137
138    Ok(())
139}