1use super::output::Output;
6use std::fs;
7use std::path::Path;
8
9pub enum InitResult {
11 Success,
13 AlreadyExists,
15 Error(String),
17}
18
19pub struct InitConfig {
21 pub path: std::path::PathBuf,
23 pub force: bool,
25 pub minimal: bool,
27 pub no_examples: bool,
29 pub provider: String,
31 pub host: String,
33 pub port: u16,
35}
36
37pub fn run(config: InitConfig, output: &Output) -> InitResult {
39 output.banner();
40 output.header("Initializing A.R.E.S Project");
41
42 let base_path = &config.path;
43
44 let config_path = base_path.join("ares.toml");
46 if config_path.exists() && !config.force {
47 output.warning("ares.toml already exists!");
48 output.hint("Use --force to overwrite existing files");
49 return InitResult::AlreadyExists;
50 }
51
52 output.subheader("Creating directories");
54
55 let directories = [
56 "data",
57 "config",
58 "config/agents",
59 "config/models",
60 "config/tools",
61 "config/workflows",
62 "config/mcps",
63 ];
64
65 for dir in &directories {
66 let dir_path = base_path.join(dir);
67 if !dir_path.exists() {
68 if let Err(e) = fs::create_dir_all(&dir_path) {
69 output.error(&format!("Failed to create {}: {}", dir, e));
70 return InitResult::Error(e.to_string());
71 }
72 output.created_dir(dir);
73 } else {
74 output.skipped(dir, "already exists");
75 }
76 }
77
78 output.subheader("Creating configuration files");
80
81 let toml_content = generate_ares_toml(&config);
82 if let Err(e) = write_file(&config_path, &toml_content, config.force) {
83 output.error(&format!("Failed to create ares.toml: {}", e));
84 return InitResult::Error(e.to_string());
85 }
86 output.created("config", "ares.toml");
87
88 let env_example_path = base_path.join(".env.example");
90 let env_content = generate_env_example();
91 if let Err(e) = write_file(&env_example_path, &env_content, config.force) {
92 output.error(&format!("Failed to create .env.example: {}", e));
93 return InitResult::Error(e.to_string());
94 }
95 output.created("env", ".env.example");
96
97 if !config.no_examples {
99 output.subheader("Creating example configurations");
100
101 create_model_files(base_path, &config, output);
103
104 create_agent_files(base_path, &config, output);
106
107 create_tool_files(base_path, output);
109
110 create_workflow_files(base_path, output);
112 }
113
114 let gitignore_path = base_path.join(".gitignore");
116 if !gitignore_path.exists() {
117 let gitignore_content = generate_gitignore();
118 if let Err(e) = write_file(&gitignore_path, &gitignore_content, false) {
119 output.warning(&format!("Failed to create .gitignore: {}", e));
120 } else {
121 output.created("file", ".gitignore");
122 }
123 }
124
125 output.complete("A.R.E.S project initialized successfully!");
127
128 output.header("Next Steps");
129 output.newline();
130 output.info("1. Set up environment variables:");
131 output.command("cp .env.example .env");
132 output.command("# Edit .env and set JWT_SECRET (min 32 chars) and API_KEY");
133 output.newline();
134
135 if config.provider == "ollama" {
136 output.info("2. Start Ollama (if not running):");
137 output.command("ollama serve");
138 output.command("ollama pull ministral-3:3b # or your preferred model");
139 output.newline();
140 }
141
142 output.info("3. Start the server:");
143 output.command("ares-server");
144 output.newline();
145
146 output.hint(&format!(
147 "Server will be available at http://{}:{}",
148 config.host, config.port
149 ));
150 output.hint("API docs available at /swagger-ui/ (requires 'swagger-ui' feature)");
151 output.hint("Build with: cargo build --features swagger-ui");
152
153 InitResult::Success
154}
155
156fn write_file(path: &Path, content: &str, force: bool) -> std::io::Result<()> {
157 if path.exists() && !force {
158 return Ok(()); }
160 fs::write(path, content)
161}
162
163fn generate_ares_toml(config: &InitConfig) -> String {
164 let provider_section = if config.provider == "openai" {
165 r#"# OpenAI API (set OPENAI_API_KEY in .env)
166[providers.openai]
167type = "openai"
168api_key_env = "OPENAI_API_KEY"
169api_base = "https://api.openai.com/v1"
170default_model = "gpt-4o-mini"
171"#
172 } else if config.provider == "both" {
173 r#"# Ollama - Local inference (default)
174[providers.ollama-local]
175type = "ollama"
176base_url = "http://localhost:11434"
177default_model = "ministral-3:3b"
178
179# OpenAI API (set OPENAI_API_KEY in .env)
180[providers.openai]
181type = "openai"
182api_key_env = "OPENAI_API_KEY"
183api_base = "https://api.openai.com/v1"
184default_model = "gpt-4o-mini"
185"#
186 } else {
187 r#"# Ollama - Local inference (no API key required)
189[providers.ollama-local]
190type = "ollama"
191base_url = "http://localhost:11434"
192default_model = "ministral-3:3b"
193"#
194 };
195
196 let model_provider = if config.provider == "openai" {
197 "openai"
198 } else {
199 "ollama-local"
200 };
201
202 let model_name = if config.provider == "openai" {
203 "gpt-4o-mini"
204 } else {
205 "ministral-3:3b"
206 };
207
208 format!(
209 r#"# A.R.E.S Configuration
210# =====================
211# Generated by: ares-server init
212#
213# REQUIRED: Set these environment variables before starting:
214# - JWT_SECRET: A secret key for JWT signing (min 32 characters)
215# - API_KEY: API key for service-to-service authentication
216#
217# Hot Reloading: Changes to this file are automatically detected and applied
218# without restarting the server.
219
220# =============================================================================
221# Server Configuration
222# =============================================================================
223[server]
224host = "{host}"
225port = {port}
226log_level = "info"
227
228# =============================================================================
229# Authentication Configuration
230# =============================================================================
231[auth]
232jwt_secret_env = "JWT_SECRET"
233jwt_access_expiry = 900
234jwt_refresh_expiry = 604800
235api_key_env = "API_KEY"
236
237# =============================================================================
238# Database Configuration
239# =============================================================================
240[database]
241url = "./data/ares.db"
242
243# =============================================================================
244# LLM Providers
245# =============================================================================
246{provider_section}
247# =============================================================================
248# Model Configurations
249# =============================================================================
250[models.fast]
251provider = "{model_provider}"
252model = "{model_name}"
253temperature = 0.7
254max_tokens = 256
255
256[models.balanced]
257provider = "{model_provider}"
258model = "{model_name}"
259temperature = 0.7
260max_tokens = 512
261
262[models.powerful]
263provider = "{model_provider}"
264model = "{model_name}"
265temperature = 0.5
266max_tokens = 1024
267
268# =============================================================================
269# Tools Configuration
270# =============================================================================
271[tools.calculator]
272enabled = true
273description = "Performs basic arithmetic operations (+, -, *, /)"
274timeout_secs = 10
275
276[tools.web_search]
277enabled = true
278description = "Search the web using DuckDuckGo (no API key required)"
279timeout_secs = 30
280
281# =============================================================================
282# Agent Configurations
283# =============================================================================
284[agents.router]
285model = "fast"
286tools = []
287max_tool_iterations = 1
288parallel_tools = false
289system_prompt = """
290You are a routing agent that classifies user queries.
291
292Available agents:
293- orchestrator: General purpose agent for complex queries
294
295Respond with ONLY the agent name (one word, lowercase).
296"""
297
298[agents.orchestrator]
299model = "powerful"
300tools = ["calculator", "web_search"]
301max_tool_iterations = 10
302parallel_tools = false
303system_prompt = """
304You are an orchestrator agent for complex queries.
305
306Capabilities:
307- Break down complex requests
308- Perform web searches
309- Execute calculations
310- Provide comprehensive answers
311
312Be helpful, accurate, and thorough.
313"""
314
315# =============================================================================
316# Workflow Configurations
317# =============================================================================
318[workflows.default]
319entry_agent = "router"
320fallback_agent = "orchestrator"
321max_depth = 3
322max_iterations = 5
323
324# =============================================================================
325# RAG Configuration
326# =============================================================================
327[rag]
328embedding_model = "BAAI/bge-small-en-v1.5"
329chunk_size = 1000
330chunk_overlap = 200
331
332# =============================================================================
333# Dynamic Configuration Paths (TOON Files)
334# =============================================================================
335[config]
336agents_dir = "config/agents"
337workflows_dir = "config/workflows"
338models_dir = "config/models"
339tools_dir = "config/tools"
340mcps_dir = "config/mcps"
341hot_reload = true
342watch_interval_ms = 1000
343"#,
344 host = config.host,
345 port = config.port,
346 provider_section = provider_section,
347 model_provider = model_provider,
348 model_name = model_name,
349 )
350}
351
352fn generate_env_example() -> String {
353 r#"# A.R.E.S Environment Variables
354# =============================
355# Copy this file to .env and fill in the values.
356
357# REQUIRED: JWT secret for authentication (minimum 32 characters)
358# Generate with: openssl rand -base64 32
359JWT_SECRET=change-me-in-production-use-at-least-32-characters
360
361# REQUIRED: API key for service-to-service authentication
362API_KEY=your-api-key-here
363
364# Optional: Logging level (trace, debug, info, warn, error)
365RUST_LOG=info,ares=debug
366
367# Optional: OpenAI API key (if using OpenAI provider)
368# OPENAI_API_KEY=sk-...
369
370# Optional: Turso cloud database (if using remote database)
371# TURSO_URL=libsql://your-db.turso.io
372# TURSO_AUTH_TOKEN=your-token
373
374# Optional: Qdrant vector database
375# QDRANT_URL=http://localhost:6334
376# QDRANT_API_KEY=your-key
377"#
378 .to_string()
379}
380
381fn generate_gitignore() -> String {
382 r#"# A.R.E.S Generated Files
383/data/
384*.db
385*.db-journal
386
387# Environment
388.env
389.env.local
390.env.*.local
391
392# Rust
393/target/
394Cargo.lock
395
396# IDE
397.idea/
398.vscode/
399*.swp
400*.swo
401*~
402
403# OS
404.DS_Store
405Thumbs.db
406"#
407 .to_string()
408}
409
410fn create_model_files(base_path: &Path, config: &InitConfig, output: &Output) {
411 let model_provider = if config.provider == "openai" {
412 "openai"
413 } else {
414 "ollama-local"
415 };
416
417 let model_name = if config.provider == "openai" {
418 "gpt-4o-mini"
419 } else {
420 "ministral-3:3b"
421 };
422
423 let models = [
424 (
425 "fast.toon",
426 format!(
427 r#"name: fast
428provider: {provider}
429model: {model}
430temperature: 0.7
431max_tokens: 256
432"#,
433 provider = model_provider,
434 model = model_name
435 ),
436 ),
437 (
438 "balanced.toon",
439 format!(
440 r#"name: balanced
441provider: {provider}
442model: {model}
443temperature: 0.7
444max_tokens: 512
445"#,
446 provider = model_provider,
447 model = model_name
448 ),
449 ),
450 (
451 "powerful.toon",
452 format!(
453 r#"name: powerful
454provider: {provider}
455model: {model}
456temperature: 0.5
457max_tokens: 1024
458"#,
459 provider = model_provider,
460 model = model_name
461 ),
462 ),
463 ];
464
465 for (filename, content) in &models {
466 let path = base_path.join("config/models").join(filename);
467 if let Err(e) = write_file(&path, content, config.force) {
468 output.warning(&format!("Failed to create {}: {}", filename, e));
469 } else {
470 output.created("model", &format!("config/models/{}", filename));
471 }
472 }
473}
474
475fn create_agent_files(base_path: &Path, config: &InitConfig, output: &Output) {
476 let agents = [
477 (
478 "router.toon",
479 r#"name: router
480model: fast
481max_tool_iterations: 1
482parallel_tools: false
483tools[0]:
484system_prompt: "You are a routing agent that classifies user queries and routes them to the appropriate specialized agent.\n\nAvailable agents:\n- orchestrator: Complex queries requiring multiple steps or research\n\nAnalyze the user's query and respond with ONLY the agent name (lowercase, one word)."
485"#.to_string(),
486 ),
487 (
488 "orchestrator.toon",
489 r#"name: orchestrator
490model: powerful
491max_tool_iterations: 10
492parallel_tools: false
493tools[0]: calculator
494tools[1]: web_search
495system_prompt: "You are an orchestrator agent for complex queries.\n\nCapabilities:\n- Break down complex requests\n- Perform web searches\n- Execute calculations\n- Synthesize information\n\nProvide comprehensive, well-structured answers."
496"#.to_string(),
497 ),
498 ];
499
500 for (filename, content) in &agents {
501 let path = base_path.join("config/agents").join(filename);
502 if let Err(e) = write_file(&path, content, config.force) {
503 output.warning(&format!("Failed to create {}: {}", filename, e));
504 } else {
505 output.created("agent", &format!("config/agents/{}", filename));
506 }
507 }
508}
509
510fn create_tool_files(base_path: &Path, output: &Output) {
511 let tools = [
512 (
513 "calculator.toon",
514 r#"name: calculator
515enabled: true
516description: Performs basic arithmetic operations (+, -, *, /)
517timeout_secs: 10
518"#,
519 ),
520 (
521 "web_search.toon",
522 r#"name: web_search
523enabled: true
524description: Search the web using DuckDuckGo (no API key required)
525timeout_secs: 30
526"#,
527 ),
528 ];
529
530 for (filename, content) in &tools {
531 let path = base_path.join("config/tools").join(filename);
532 if let Err(e) = write_file(&path, content, false) {
533 output.warning(&format!("Failed to create {}: {}", filename, e));
534 } else {
535 output.created("tool", &format!("config/tools/{}", filename));
536 }
537 }
538}
539
540fn create_workflow_files(base_path: &Path, output: &Output) {
541 let workflows = [
542 (
543 "default.toon",
544 r#"name: default
545entry_agent: router
546fallback_agent: orchestrator
547max_depth: 3
548max_iterations: 5
549parallel_subagents: false
550"#,
551 ),
552 (
553 "research.toon",
554 r#"name: research
555entry_agent: orchestrator
556max_depth: 3
557max_iterations: 10
558parallel_subagents: true
559"#,
560 ),
561 ];
562
563 for (filename, content) in &workflows {
564 let path = base_path.join("config/workflows").join(filename);
565 if let Err(e) = write_file(&path, content, false) {
566 output.warning(&format!("Failed to create {}: {}", filename, e));
567 } else {
568 output.created("workflow", &format!("config/workflows/{}", filename));
569 }
570 }
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576 use tempfile::TempDir;
577
578 fn create_test_config(temp_dir: &TempDir) -> InitConfig {
579 InitConfig {
580 path: temp_dir.path().to_path_buf(),
581 force: false,
582 minimal: false,
583 no_examples: false,
584 provider: "ollama".to_string(),
585 host: "127.0.0.1".to_string(),
586 port: 3000,
587 }
588 }
589
590 #[test]
591 fn test_init_config_creation() {
592 let config = InitConfig {
593 path: std::path::PathBuf::from("/tmp/test"),
594 force: false,
595 minimal: false,
596 no_examples: false,
597 provider: "ollama".to_string(),
598 host: "127.0.0.1".to_string(),
599 port: 3000,
600 };
601
602 assert_eq!(config.path, std::path::PathBuf::from("/tmp/test"));
603 assert!(!config.force);
604 assert!(!config.minimal);
605 assert_eq!(config.provider, "ollama");
606 assert_eq!(config.port, 3000);
607 }
608
609 #[test]
610 fn test_init_result_variants() {
611 let success = InitResult::Success;
613 let exists = InitResult::AlreadyExists;
614 let error = InitResult::Error("test error".to_string());
615
616 match success {
617 InitResult::Success => (),
618 _ => panic!("Expected Success"),
619 }
620
621 match exists {
622 InitResult::AlreadyExists => (),
623 _ => panic!("Expected AlreadyExists"),
624 }
625
626 match error {
627 InitResult::Error(msg) => assert_eq!(msg, "test error"),
628 _ => panic!("Expected Error"),
629 }
630 }
631
632 #[test]
633 fn test_generate_ares_toml_ollama() {
634 let config = InitConfig {
635 path: std::path::PathBuf::from("/tmp"),
636 force: false,
637 minimal: false,
638 no_examples: false,
639 provider: "ollama".to_string(),
640 host: "127.0.0.1".to_string(),
641 port: 3000,
642 };
643
644 let content = generate_ares_toml(&config);
645
646 assert!(content.contains("[server]"));
647 assert!(content.contains("host = \"127.0.0.1\""));
648 assert!(content.contains("port = 3000"));
649 assert!(content.contains("[providers.ollama-local]"));
650 assert!(content.contains("type = \"ollama\""));
651 }
652
653 #[test]
654 fn test_generate_ares_toml_openai() {
655 let config = InitConfig {
656 path: std::path::PathBuf::from("/tmp"),
657 force: false,
658 minimal: false,
659 no_examples: false,
660 provider: "openai".to_string(),
661 host: "0.0.0.0".to_string(),
662 port: 8080,
663 };
664
665 let content = generate_ares_toml(&config);
666
667 assert!(content.contains("host = \"0.0.0.0\""));
668 assert!(content.contains("port = 8080"));
669 assert!(content.contains("[providers.openai]"));
670 assert!(content.contains("OPENAI_API_KEY"));
671 }
672
673 #[test]
674 fn test_generate_ares_toml_both() {
675 let config = InitConfig {
676 path: std::path::PathBuf::from("/tmp"),
677 force: false,
678 minimal: false,
679 no_examples: false,
680 provider: "both".to_string(),
681 host: "127.0.0.1".to_string(),
682 port: 3000,
683 };
684
685 let content = generate_ares_toml(&config);
686
687 assert!(content.contains("[providers.ollama-local]"));
688 assert!(content.contains("[providers.openai]"));
689 }
690
691 #[test]
692 fn test_generate_env_example() {
693 let content = generate_env_example();
694
695 assert!(content.contains("JWT_SECRET"));
696 assert!(content.contains("API_KEY"));
697 assert!(content.contains("RUST_LOG"));
698 assert!(content.contains("OPENAI_API_KEY"));
699 }
700
701 #[test]
702 fn test_generate_gitignore() {
703 let content = generate_gitignore();
704
705 assert!(content.contains("/data/"));
706 assert!(content.contains(".env"));
707 assert!(content.contains("/target/"));
708 assert!(content.contains(".DS_Store"));
709 }
710
711 #[test]
712 fn test_write_file_creates_new() {
713 let temp_dir = TempDir::new().expect("Failed to create temp dir");
714 let file_path = temp_dir.path().join("test.txt");
715
716 let result = write_file(&file_path, "test content", false);
717 assert!(result.is_ok());
718 assert!(file_path.exists());
719
720 let content = fs::read_to_string(&file_path).expect("Failed to read file");
721 assert_eq!(content, "test content");
722 }
723
724 #[test]
725 fn test_write_file_skips_existing_without_force() {
726 let temp_dir = TempDir::new().expect("Failed to create temp dir");
727 let file_path = temp_dir.path().join("test.txt");
728
729 fs::write(&file_path, "original").expect("Failed to write");
731
732 let result = write_file(&file_path, "new content", false);
734 assert!(result.is_ok());
735
736 let content = fs::read_to_string(&file_path).expect("Failed to read file");
738 assert_eq!(content, "original");
739 }
740
741 #[test]
742 fn test_write_file_overwrites_with_force() {
743 let temp_dir = TempDir::new().expect("Failed to create temp dir");
744 let file_path = temp_dir.path().join("test.txt");
745
746 fs::write(&file_path, "original").expect("Failed to write");
748
749 let result = write_file(&file_path, "new content", true);
751 assert!(result.is_ok());
752
753 let content = fs::read_to_string(&file_path).expect("Failed to read file");
755 assert_eq!(content, "new content");
756 }
757
758 #[test]
759 fn test_run_creates_all_files() {
760 let temp_dir = TempDir::new().expect("Failed to create temp dir");
761 let config = create_test_config(&temp_dir);
762 let output = Output::no_color();
763
764 let result = run(config, &output);
765
766 match result {
767 InitResult::Success => (),
768 _ => panic!("Expected Success"),
769 }
770
771 assert!(temp_dir.path().join("ares.toml").exists());
773 assert!(temp_dir.path().join(".env.example").exists());
774 assert!(temp_dir.path().join(".gitignore").exists());
775 assert!(temp_dir.path().join("data").is_dir());
776 assert!(temp_dir.path().join("config/agents").is_dir());
777 assert!(temp_dir.path().join("config/models").is_dir());
778 assert!(temp_dir.path().join("config/tools").is_dir());
779 assert!(temp_dir.path().join("config/workflows").is_dir());
780 }
781
782 #[test]
783 fn test_run_no_examples_skips_toon_files() {
784 let temp_dir = TempDir::new().expect("Failed to create temp dir");
785 let config = InitConfig {
786 path: temp_dir.path().to_path_buf(),
787 force: false,
788 minimal: false,
789 no_examples: true,
790 provider: "ollama".to_string(),
791 host: "127.0.0.1".to_string(),
792 port: 3000,
793 };
794 let output = Output::no_color();
795
796 let result = run(config, &output);
797
798 match result {
799 InitResult::Success => (),
800 _ => panic!("Expected Success"),
801 }
802
803 assert!(temp_dir.path().join("ares.toml").exists());
805
806 assert!(!temp_dir.path().join("config/models/fast.toon").exists());
808 assert!(!temp_dir.path().join("config/agents/router.toon").exists());
809 }
810
811 #[test]
812 fn test_run_already_exists_without_force() {
813 let temp_dir = TempDir::new().expect("Failed to create temp dir");
814
815 fs::write(temp_dir.path().join("ares.toml"), "existing").expect("Failed to write");
817
818 let config = create_test_config(&temp_dir);
819 let output = Output::no_color();
820
821 let result = run(config, &output);
822
823 match result {
824 InitResult::AlreadyExists => (),
825 _ => panic!("Expected AlreadyExists"),
826 }
827 }
828
829 #[test]
830 fn test_run_force_overwrites() {
831 let temp_dir = TempDir::new().expect("Failed to create temp dir");
832
833 fs::write(temp_dir.path().join("ares.toml"), "existing").expect("Failed to write");
835
836 let config = InitConfig {
837 path: temp_dir.path().to_path_buf(),
838 force: true,
839 minimal: false,
840 no_examples: true,
841 provider: "ollama".to_string(),
842 host: "127.0.0.1".to_string(),
843 port: 3000,
844 };
845 let output = Output::no_color();
846
847 let result = run(config, &output);
848
849 match result {
850 InitResult::Success => (),
851 _ => panic!("Expected Success"),
852 }
853
854 let content =
856 fs::read_to_string(temp_dir.path().join("ares.toml")).expect("Failed to read");
857 assert!(content.contains("[server]"));
858 assert!(!content.contains("existing"));
859 }
860}