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