返回 DeepSeek-TUI-2026
init.rs
根目录 / crates / tui / src / commands / init.rs
1 //! /init command - Generate AGENTS.md for project
2
3 use std::fmt::Write;
4 use std::path::Path;
5
6 use crate::tui::app::App;
7
8 use super::CommandResult;
9
10 /// Generate an AGENTS.md file for the current project
11 pub fn init(app: &mut App) -> CommandResult {
12 let workspace = &app.workspace;
13
14 // Check if AGENTS.md already exists
15 let agents_path = workspace.join("AGENTS.md");
16 if agents_path.exists() {
17 return CommandResult::error("AGENTS.md already exists. Delete it first to reinitialize.");
18 }
19
20 // Detect project type and generate appropriate content
21 let content = generate_project_doc(workspace);
22
23 // Write the file
24 match std::fs::write(&agents_path, &content) {
25 Ok(()) => CommandResult::message(format!(
26 "Created AGENTS.md at {}\n\nEdit this file to customize agent behavior for your project.",
27 agents_path.display()
28 )),
29 Err(e) => CommandResult::error(format!("Failed to create AGENTS.md: {e}")),
30 }
31 }
32
33 /// Generate project documentation based on detected project type
34 fn generate_project_doc(workspace: &Path) -> String {
35 let mut doc = String::new();
36
37 // Header
38 doc.push_str("# Project Instructions\n\n");
39 doc.push_str("This file provides context for AI assistants working on this project.\n\n");
40
41 // Detect project type
42 let project_info = detect_project_type(workspace);
43 doc.push_str(&project_info);
44
45 // Add standard sections
46 doc.push_str("\n## Guidelines\n\n");
47 doc.push_str("- Follow existing code style and patterns\n");
48 doc.push_str("- Write tests for new functionality\n");
49 doc.push_str("- Keep changes focused and atomic\n");
50 doc.push_str("- Document public APIs\n");
51
52 doc.push_str("\n## Important Notes\n\n");
53 doc.push_str("<!-- Add project-specific notes here -->\n");
54
55 doc
56 }
57
58 /// Detect project type and return relevant information
59 fn detect_project_type(workspace: &Path) -> String {
60 let mut info = String::new();
61
62 // Check for Rust project
63 if workspace.join("Cargo.toml").exists() {
64 info.push_str("## Project Type: Rust\n\n");
65 info.push_str("### Commands\n");
66 info.push_str("- Build: `cargo build`\n");
67 info.push_str("- Test: `cargo test`\n");
68 info.push_str("- Run: `cargo run`\n");
69 info.push_str("- Check: `cargo check`\n");
70 info.push_str("- Format: `cargo fmt`\n");
71 info.push_str("- Lint: `cargo clippy`\n\n");
72
73 // Try to extract project name from Cargo.toml
74 if let Some(name) = std::fs::read_to_string(workspace.join("Cargo.toml"))
75 .ok()
76 .and_then(|content| extract_cargo_name(&content))
77 {
78 let _ = write!(info, "### Project: {name}\n\n");
79 }
80 }
81 // Check for Node.js project
82 else if workspace.join("package.json").exists() {
83 info.push_str("## Project Type: Node.js\n\n");
84 info.push_str("### Commands\n");
85 info.push_str("- Install: `npm install`\n");
86 info.push_str("- Test: `npm test`\n");
87 info.push_str("- Build: `npm run build`\n");
88 info.push_str("- Start: `npm start`\n\n");
89
90 // Check for common frameworks
91 if workspace.join("next.config.js").exists() || workspace.join("next.config.ts").exists() {
92 info.push_str("### Framework: Next.js\n\n");
93 } else if workspace.join("vite.config.js").exists()
94 || workspace.join("vite.config.ts").exists()
95 {
96 info.push_str("### Framework: Vite\n\n");
97 }
98 }
99 // Check for Python project
100 else if workspace.join("pyproject.toml").exists() || workspace.join("setup.py").exists() {
101 info.push_str("## Project Type: Python\n\n");
102 info.push_str("### Commands\n");
103 if workspace.join("pyproject.toml").exists() {
104 info.push_str("- Install: `pip install -e .`\n");
105 }
106 info.push_str("- Test: `pytest`\n");
107 info.push_str("- Format: `black .`\n");
108 info.push_str("- Lint: `ruff check .`\n\n");
109 }
110 // Check for Go project
111 else if workspace.join("go.mod").exists() {
112 info.push_str("## Project Type: Go\n\n");
113 info.push_str("### Commands\n");
114 info.push_str("- Build: `go build`\n");
115 info.push_str("- Test: `go test ./...`\n");
116 info.push_str("- Run: `go run .`\n");
117 info.push_str("- Format: `go fmt ./...`\n\n");
118 }
119 // Unknown project type
120 else {
121 info.push_str("## Project Type: Unknown\n\n");
122 info.push_str("<!-- Add build/test commands here -->\n\n");
123 }
124
125 // Check for README
126 if workspace.join("README.md").exists() {
127 info.push_str("### Documentation\n");
128 info.push_str("See README.md for project overview.\n\n");
129 }
130
131 // Check for .gitignore
132 if workspace.join(".gitignore").exists() {
133 info.push_str("### Version Control\n");
134 info.push_str("This project uses Git. See .gitignore for excluded files.\n\n");
135 }
136
137 info
138 }
139
140 /// Extract project name from Cargo.toml
141 fn extract_cargo_name(content: &str) -> Option<String> {
142 for line in content.lines() {
143 let line = line.trim();
144 if line.starts_with("name") && line.contains('=') {
145 let parts: Vec<&str> = line.splitn(2, '=').collect();
146 if parts.len() == 2 {
147 let name = parts[1].trim().trim_matches('"').trim_matches('\'');
148 return Some(name.to_string());
149 }
150 }
151 }
152 None
153 }
154
155 #[cfg(test)]
156 mod tests {
157 use super::*;
158 use crate::config::Config;
159 use crate::tui::app::{App, TuiOptions};
160 use tempfile::TempDir;
161
162 fn create_test_app_with_tmpdir(tmpdir: &TempDir) -> App {
163 let options = TuiOptions {
164 model: "deepseek-v4-pro".to_string(),
165 workspace: tmpdir.path().to_path_buf(),
166 config_path: None,
167 config_profile: None,
168 allow_shell: false,
169 use_alt_screen: true,
170 use_mouse_capture: false,
171 use_bracketed_paste: true,
172 max_subagents: 1,
173 skills_dir: tmpdir.path().join("skills"),
174 memory_path: tmpdir.path().join("memory.md"),
175 notes_path: tmpdir.path().join("notes.txt"),
176 mcp_config_path: tmpdir.path().join("mcp.json"),
177 use_memory: false,
178 start_in_agent_mode: false,
179 skip_onboarding: true,
180 yolo: false,
181 resume_session_id: None,
182 initial_input: None,
183 };
184 App::new(options, &Config::default())
185 }
186
187 #[test]
188 fn test_init_creates_agents_md() {
189 let tmpdir = TempDir::new().unwrap();
190 let mut app = create_test_app_with_tmpdir(&tmpdir);
191 let result = init(&mut app);
192 assert!(result.message.is_some());
193 let msg = result.message.unwrap();
194 assert!(msg.contains("Created AGENTS.md"));
195 let agents_path = tmpdir.path().join("AGENTS.md");
196 assert!(agents_path.exists());
197 }
198
199 #[test]
200 fn test_init_fails_if_exists() {
201 let tmpdir = TempDir::new().unwrap();
202 let mut app = create_test_app_with_tmpdir(&tmpdir);
203 // Create file first
204 std::fs::write(tmpdir.path().join("AGENTS.md"), "existing").unwrap();
205 let result = init(&mut app);
206 assert!(result.message.is_some());
207 assert!(result.message.unwrap().contains("already exists"));
208 }
209
210 #[test]
211 fn test_detect_project_type_rust() {
212 let tmpdir = TempDir::new().unwrap();
213 std::fs::write(
214 tmpdir.path().join("Cargo.toml"),
215 "[package]\nname = \"test\"",
216 )
217 .unwrap();
218 let info = detect_project_type(tmpdir.path());
219 assert!(info.contains("Project Type: Rust"));
220 assert!(info.contains("cargo build"));
221 assert!(info.contains("cargo test"));
222 }
223
224 #[test]
225 fn test_detect_project_type_node() {
226 let tmpdir = TempDir::new().unwrap();
227 std::fs::write(tmpdir.path().join("package.json"), "{}").unwrap();
228 let info = detect_project_type(tmpdir.path());
229 assert!(info.contains("Project Type: Node.js"));
230 assert!(info.contains("npm install"));
231 }
232
233 #[test]
234 fn test_detect_project_type_python() {
235 let tmpdir = TempDir::new().unwrap();
236 std::fs::write(tmpdir.path().join("pyproject.toml"), "[project]").unwrap();
237 let info = detect_project_type(tmpdir.path());
238 assert!(info.contains("Project Type: Python"));
239 }
240
241 #[test]
242 fn test_detect_project_type_go() {
243 let tmpdir = TempDir::new().unwrap();
244 std::fs::write(tmpdir.path().join("go.mod"), "module test").unwrap();
245 let info = detect_project_type(tmpdir.path());
246 assert!(info.contains("Project Type: Go"));
247 }
248
249 #[test]
250 fn test_detect_project_type_unknown() {
251 let tmpdir = TempDir::new().unwrap();
252 let info = detect_project_type(tmpdir.path());
253 assert!(info.contains("Project Type: Unknown"));
254 }
255
256 #[test]
257 fn test_extract_cargo_name() {
258 let cargo = r#"
259 [package]
260 name = "my-project"
261 version = "1.0.0"
262 "#;
263 assert_eq!(extract_cargo_name(cargo), Some("my-project".to_string()));
264 }
265
266 #[test]
267 fn test_extract_cargo_name_single_quotes() {
268 let cargo = r#"name = 'single-quoted'"#;
269 assert_eq!(extract_cargo_name(cargo), Some("single-quoted".to_string()));
270 }
271
272 #[test]
273 fn test_extract_cargo_name_not_found() {
274 let cargo = "[package]\nversion = \"1.0.0\"";
275 assert_eq!(extract_cargo_name(cargo), None);
276 }
277 }
278
278 lines RUST