Skip to content

Commit b7159c3

Browse files
committed
Add aiscript new command to create new project
1 parent 60c8986 commit b7159c3

File tree

5 files changed

+249
-0
lines changed

5 files changed

+249
-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
@@ -8,6 +8,9 @@ use repr::Repl;
88
use tokio::task;
99

1010
mod repr;
11+
mod project;
12+
13+
use project::ProjectGenerator;
1114

1215
#[derive(Parser)]
1316
#[command(version, about, long_about = None)]
@@ -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: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
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+
let project_path = temp_path.join(project_name);
151+
152+
// Run in the temp directory
153+
std::env::set_current_dir(temp_path).unwrap();
154+
155+
let generator = ProjectGenerator::new(project_name);
156+
let result = generator.generate();
157+
158+
assert!(result.is_ok(), "Project generation failed: {:?}", result);
159+
160+
// Verify project structure
161+
assert!(project_path.exists(), "Project directory not created");
162+
assert!(
163+
project_path.join("lib").exists(),
164+
"lib directory not created"
165+
);
166+
assert!(
167+
project_path.join("routes").exists(),
168+
"routes directory not created"
169+
);
170+
assert!(
171+
project_path.join("project.toml").exists(),
172+
"project.toml not created"
173+
);
174+
assert!(
175+
project_path.join("routes/index.ai").exists(),
176+
"Example file not created"
177+
);
178+
179+
// Verify project.toml content
180+
let toml_content = fs::read_to_string(project_path.join("project.toml")).unwrap();
181+
assert!(toml_content.contains(&format!("name = \"{}\"", project_name)));
182+
assert!(toml_content.contains("version = \"0.1.0\""));
183+
184+
// Verify example file content
185+
let example_content = fs::read_to_string(project_path.join("routes/index.ai")).unwrap();
186+
assert!(example_content.contains("fn index(req, res)"));
187+
}
188+
189+
#[test]
190+
fn test_project_already_exists() {
191+
// Use tempdir to ensure test files are cleaned up
192+
let temp_dir = tempdir().unwrap();
193+
let temp_path = temp_dir.path();
194+
195+
// Create a directory that will conflict
196+
let project_name = "existing_project";
197+
let project_path = temp_path.join(project_name);
198+
fs::create_dir_all(&project_path).unwrap();
199+
200+
// Run in the temp directory
201+
std::env::set_current_dir(temp_path).unwrap();
202+
203+
let generator = ProjectGenerator::new(project_name);
204+
let result = generator.generate();
205+
206+
assert!(
207+
result.is_err(),
208+
"Project generation should fail for existing directory"
209+
);
210+
if let Err(err) = result {
211+
assert!(
212+
err.contains("already exists"),
213+
"Wrong error message: {}",
214+
err
215+
);
216+
}
217+
}
218+
}

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)