Skip to content

Commit c93bcbe

Browse files
authored
Add aiscript new command to create new project (#21)
* Add `aiscript new` command to create new project * Fix unit tests * Cargo fmt and clippy
1 parent 60c8986 commit c93bcbe

File tree

5 files changed

+258
-0
lines changed

5 files changed

+258
-0
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

aiscript/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ dotenv = "0.15.0"
2424
rustyline = "15.0"
2525
dirs = "6.0"
2626
serde.workspace = true
27+
whoami = "1.4.1"
28+
29+
[dev-dependencies]
30+
tempfile = "3.8.1"
2731

2832
[features]
2933
ai_test = ["aiscript-vm/ai_test"]

aiscript/src/main.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ use clap::{Parser, Subcommand};
77
use repr::Repl;
88
use tokio::task;
99

10+
mod project;
1011
mod repr;
1112

13+
use project::ProjectGenerator;
14+
1215
#[derive(Parser)]
1316
#[command(version, about, long_about = None)]
1417
struct AIScriptCli {
@@ -34,6 +37,12 @@ enum Commands {
3437
#[arg(short, long, default_value_t = false)]
3538
reload: bool,
3639
},
40+
/// Create a new AIScript project with a standard directory structure.
41+
New {
42+
/// The name of the new project
43+
#[arg(value_name = "PROJECT_NAME")]
44+
name: String,
45+
},
3746
}
3847

3948
#[tokio::main]
@@ -47,6 +56,13 @@ async fn main() {
4756
println!("Server listening on port http://localhost:{}", port);
4857
aiscript_runtime::run(file, port, reload).await;
4958
}
59+
Some(Commands::New { name }) => {
60+
let generator = ProjectGenerator::new(&name);
61+
if let Err(e) = generator.generate() {
62+
eprintln!("{}", e);
63+
process::exit(1);
64+
}
65+
}
5066
None => {
5167
if let Some(path) = cli.file {
5268
let pg_connection = aiscript_runtime::get_pg_connection().await;

aiscript/src/project.rs

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
use std::fs;
2+
use std::io::Write;
3+
use std::path::PathBuf;
4+
5+
pub struct ProjectGenerator {
6+
project_name: String,
7+
project_path: PathBuf,
8+
}
9+
10+
impl ProjectGenerator {
11+
pub fn new(project_name: &str) -> Self {
12+
let project_path = PathBuf::from(project_name);
13+
Self {
14+
project_name: project_name.to_string(),
15+
project_path,
16+
}
17+
}
18+
19+
pub fn generate(&self) -> Result<(), String> {
20+
// Check if directory already exists
21+
if self.project_path.exists() {
22+
return Err(format!(
23+
"Error: Directory '{}' already exists",
24+
self.project_name
25+
));
26+
}
27+
28+
// Create project directory
29+
fs::create_dir_all(&self.project_path).map_err(|e| {
30+
format!(
31+
"Failed to create project directory '{}': {}",
32+
self.project_name, e
33+
)
34+
})?;
35+
36+
// Create standard directories
37+
self.create_directories()?;
38+
39+
// Create project.toml
40+
self.create_project_toml()?;
41+
42+
// Create basic example file
43+
self.create_example_file()?;
44+
45+
println!(
46+
"Successfully created new AIScript project: {}",
47+
self.project_name
48+
);
49+
println!("Project structure:");
50+
println!("{}", self.display_project_structure());
51+
println!();
52+
println!("Run `aiscript serve` to start the server.");
53+
54+
Ok(())
55+
}
56+
57+
fn create_directories(&self) -> Result<(), String> {
58+
let dirs = vec!["lib", "routes"];
59+
60+
for dir in dirs {
61+
let dir_path = self.project_path.join(dir);
62+
fs::create_dir_all(&dir_path).map_err(|e| {
63+
format!("Failed to create directory '{}': {}", dir_path.display(), e)
64+
})?;
65+
}
66+
67+
Ok(())
68+
}
69+
70+
fn create_project_toml(&self) -> Result<(), String> {
71+
let toml_path = self.project_path.join("project.toml");
72+
let username = whoami::username();
73+
74+
let toml_content = format!(
75+
r#"[project]
76+
name = "{}"
77+
description = "An AIScript project"
78+
version = "0.1.0"
79+
authors = ["{}"]
80+
81+
[network]
82+
host = "0.0.0.0"
83+
port = 8000
84+
85+
[apidoc]
86+
enabled = true
87+
type = "redoc"
88+
path = "/docs"
89+
"#,
90+
self.project_name, username
91+
);
92+
93+
let mut file = fs::File::create(&toml_path)
94+
.map_err(|e| format!("Failed to create project.toml: {}", e))?;
95+
96+
file.write_all(toml_content.as_bytes())
97+
.map_err(|e| format!("Failed to write to project.toml: {}", e))?;
98+
99+
Ok(())
100+
}
101+
102+
fn create_example_file(&self) -> Result<(), String> {
103+
let routes_dir = self.project_path.join("routes");
104+
let example_path = routes_dir.join("index.ai");
105+
106+
let example_content = r#"// Example AIScript route handler
107+
get /hello {
108+
query {
109+
name: str
110+
}
111+
112+
return { message: f"Hello, {query.name}!" };
113+
}
114+
"#;
115+
116+
let mut file = fs::File::create(&example_path)
117+
.map_err(|e| format!("Failed to create example file: {}", e))?;
118+
119+
file.write_all(example_content.as_bytes())
120+
.map_err(|e| format!("Failed to write to example file: {}", e))?;
121+
122+
Ok(())
123+
}
124+
125+
fn display_project_structure(&self) -> String {
126+
let mut result = format!("{}\n", self.project_name);
127+
result.push_str("├── lib/\n");
128+
result.push_str("├── routes/\n");
129+
result.push_str("│ └── index.ai\n");
130+
result.push_str("└── project.toml\n");
131+
132+
result
133+
}
134+
}
135+
136+
#[cfg(test)]
137+
mod tests {
138+
use super::*;
139+
use std::fs;
140+
use tempfile::tempdir;
141+
142+
#[test]
143+
fn test_project_generator() {
144+
// Use tempdir to ensure test files are cleaned up
145+
let temp_dir = tempdir().unwrap();
146+
let temp_path = temp_dir.path();
147+
148+
// Create a test project in the temp directory
149+
let project_name = "test_project";
150+
151+
// Create an absolute path for the project
152+
let project_path = temp_path.join(project_name);
153+
154+
// Create a generator with the project name
155+
let generator = ProjectGenerator::new(project_name);
156+
157+
// Override the project path for testing
158+
let generator = ProjectGenerator {
159+
project_name: project_name.to_string(),
160+
project_path: project_path.clone(),
161+
};
162+
163+
let result = generator.generate();
164+
165+
assert!(result.is_ok(), "Project generation failed: {:?}", result);
166+
167+
// Verify project structure
168+
assert!(project_path.exists(), "Project directory not created");
169+
assert!(
170+
project_path.join("lib").exists(),
171+
"lib directory not created"
172+
);
173+
assert!(
174+
project_path.join("routes").exists(),
175+
"routes directory not created"
176+
);
177+
assert!(
178+
project_path.join("project.toml").exists(),
179+
"project.toml not created"
180+
);
181+
assert!(
182+
project_path.join("routes/index.ai").exists(),
183+
"Example file not created"
184+
);
185+
186+
// Verify project.toml content
187+
let toml_content = fs::read_to_string(project_path.join("project.toml")).unwrap();
188+
assert!(toml_content.contains(&format!("name = \"{}\"", project_name)));
189+
assert!(toml_content.contains("version = \"0.1.0\""));
190+
191+
// Verify example file content
192+
let example_content = fs::read_to_string(project_path.join("routes/index.ai")).unwrap();
193+
assert!(example_content.contains("get /hello"));
194+
}
195+
196+
#[test]
197+
fn test_project_already_exists() {
198+
// Use tempdir to ensure test files are cleaned up
199+
let temp_dir = tempdir().unwrap();
200+
let temp_path = temp_dir.path();
201+
202+
// Create a directory that will conflict
203+
let project_name = "existing_project";
204+
let project_path = temp_path.join(project_name);
205+
fs::create_dir_all(&project_path).unwrap();
206+
207+
// Create a generator with absolute path
208+
let generator = ProjectGenerator {
209+
project_name: project_name.to_string(),
210+
project_path,
211+
};
212+
213+
let result = generator.generate();
214+
215+
assert!(
216+
result.is_err(),
217+
"Project generation should fail for existing directory"
218+
);
219+
if let Err(err) = result {
220+
assert!(
221+
err.contains("already exists"),
222+
"Wrong error message: {}",
223+
err
224+
);
225+
}
226+
}
227+
}

examples/routes/ai.ai

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,12 @@ get /guess {
88

99
let message = "You got it!" if query.value == 42 else "Try again";
1010
return { message };
11+
}
12+
13+
get /hello {
14+
query {
15+
name: str
16+
}
17+
18+
return { message: f"Hello, {query.name}!" };
1119
}

0 commit comments

Comments
 (0)