1#![allow(clippy::unused_unit)] use crate::error::GgenError;
26use clap_noun_verb::Result as VerbResult;
27use clap_noun_verb_macros::verb;
28use ggen_core::codegen::FileTransaction;
29use serde::Serialize;
30use std::fs;
31use std::path::Path;
32
33pub type Result<T> = std::result::Result<T, GgenError>;
34
35#[derive(Debug, Clone, Serialize)]
41pub struct InitOutput {
42 pub status: String,
44
45 pub project_dir: String,
47
48 pub files_created: Vec<String>,
50
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub files_overwritten: Option<Vec<String>>,
54
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub files_preserved: Option<Vec<String>>,
58
59 pub directories_created: Vec<String>,
61
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub error: Option<String>,
65
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub warning: Option<String>,
69
70 pub next_steps: Vec<String>,
72
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub transaction: Option<TransactionInfo>,
76
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub git_hooks: Option<super::git_hooks::HooksInstallOutput>,
80}
81
82#[derive(Debug, Clone, Serialize)]
84pub struct TransactionInfo {
85 pub total_files: usize,
87
88 pub backups_created: usize,
90
91 pub committed: bool,
93}
94
95const GGEN_TOML: &str = r#"[project]
100name = "my-ggen-project"
101version = "0.1.0"
102description = "A ggen project initialized with default templates"
103authors = ["ggen init"]
104license = "MIT"
105
106# BIG BANG 80/20: Specification Closure First
107# Before using ggen, confirm:
108# 1. Do you have real user data (CSV/JSON)? Not promised—actual files.
109# 2. Can you find one existing standard ontology (schema.org, FOAF, Dublin Core, SKOS)?
110# Should take 5 minutes. If it takes 3 months, you're building custom (wrong path).
111# 3. Can you explain your problem in one sentence? No 100-page documents.
112# 4. Has anyone (not a friend, not a co-founder) committed to this?
113# Email, contract, payment—proof, not enthusiasm.
114# 5. Can you validate with 10 real users in 48 hours?
115#
116# If you answered NO to any of these, stop. Talk to Sean before proceeding.
117
118[ontology]
119# REQUIRED: Path to your RDF ontology file (Turtle format)
120# Approved: schema.org, FOAF, Dublin Core, SKOS, Big Five
121# Replace with your chosen standard ontology below.
122source = "schema/domain.ttl"
123# Use standard ontologies only (BIG BANG 80/20 gate)
124standard_only = true
125
126# Example: Using schema.org for e-commerce domain
127# [[ontology.pack]]
128# name = "schema-org"
129# version = "^3.13.0"
130# namespace = "https://schema.org/"
131
132[generation]
133output_dir = "."
134
135# Define inference rules (required for DMAIC quality gates)
136[inference]
137rules = [
138 { name = "standard-normalization", construct = "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }" }
139]
140
141# Define at least one generation rule (required)
142# This example rule generates Rust structs from the Person ontology
143[[generation.rules]]
144name = "example-rule"
145# SPARQL SELECT query to extract ontology concepts
146query = { inline = """
147PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
148PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
149PREFIX schema: <https://schema.org/>
150
151SELECT ?class ?label ?comment
152WHERE {
153 ?class a rdfs:Class ;
154 rdfs:label ?label .
155 OPTIONAL { ?class rdfs:comment ?comment . }
156}
157LIMIT 10
158""" }
159# Template file to render (relative to templates directory)
160template = { file = "templates/example.txt.tera" }
161# Output file path (relative to output_dir)
162output_file = "ontology-summary.txt"
163# How to handle existing output files
164mode = "Overwrite"
165
166[sync]
167enabled = true
168on_change = "manual"
169validate_after = true
170conflict_mode = "fail"
171
172[rdf]
173formats = ["turtle"]
174default_format = "turtle"
175strict_validation = false
176
177[templates]
178enable_caching = true
179auto_reload = true
180
181[output]
182formatting = "default"
183line_length = 100
184indent = 2
185"#;
186
187const DOMAIN_TTL: &str = r#"# ggen v26_5_19: Schema.org Example Ontology
188# Uses schema.org (standard ontology) instead of custom namespace.
189#
190# In BIG BANG 80/20 mode, replace this with:
191# 1. Your actual user data (CSV → RDF conversion)
192# 2. A chosen standard ontology (schema.org, FOAF, Dublin Core, SKOS)
193# 3. Only custom triples if schema.org + standard combo doesn't fit
194#
195# Why? Because Seth built 3-month custom ontology for what schema.org does in 5 minutes.
196
197@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
198@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
199@prefix schema: <https://schema.org/> .
200
201# Example: Using schema.org Person and properties
202# This is a real, standard vocabulary used by Google, Microsoft, Yahoo, Yandex
203# See: https://schema.org/Person
204
205schema:Person a rdfs:Class ;
206 rdfs:label "Person" ;
207 rdfs:comment "Schema.org Person type - standard vocabulary for person data" .
208
209# Properties from schema.org
210schema:name a rdf:Property ;
211 rdfs:domain schema:Person ;
212 rdfs:label "name" ;
213 rdfs:comment "The name of the person (from schema.org)" .
214
215schema:email a rdf:Property ;
216 rdfs:domain schema:Person ;
217 rdfs:label "email" ;
218 rdfs:comment "The email address (from schema.org)" .
219
220schema:age a rdf:Property ;
221 rdfs:domain schema:Person ;
222 rdfs:label "age" ;
223 rdfs:comment "The age of the person (from schema.org)" .
224
225# NEXT STEPS:
226# 1. Load your actual CSV/JSON user data
227# 2. Validate with 10 real users (not friends)
228# 3. Only extend schema.org if needed (stay standard-first)
229# 4. Run: ggen sync --validate-only
230"#;
231
232const MAKEFILE: &str = r#".PHONY: help setup build clean
233
234help:
235 @echo "ggen project - Available targets:"
236 @echo " make setup - Run startup.sh to initialize project"
237 @echo " make build - Generate code from ontology (ggen sync)"
238 @echo " make clean - Remove generated artifacts"
239 @echo ""
240 @echo "See scripts/startup.sh for custom initialization steps"
241
242setup:
243 @bash scripts/startup.sh
244
245build:
246 ggen sync
247
248clean:
249 rm -rf .ggen/
250"#;
251
252const EXAMPLE_TEMPLATE: &str = r#"# Example Tera Template for ggen
253# This template is called by the generation.rules in ggen.toml
254# It receives data from the SPARQL query and renders output
255
256# Ontology Classes from schema.org
257{% for row in results %}
258## Class: {{ row.label | default(value="Unknown") }}
259
260{% if row.comment %}
261{{ row.comment }}
262{% else %}
263(No description provided)
264{% endif %}
265
266**Identifier**: `{{ row.class }}`
267
268---
269{% endfor %}
270
271*Generated by ggen v26_5_19*
272"#;
273
274const STARTUP_SH: &str = r#"#!/bin/bash
275# Startup script for ggen project initialization
276# Implements BIG BANG 80/20 screening gate before project can proceed
277#
278# Purpose: Prevent Seth-like patterns (custom ontologies, 3-month research, zero validation)
279# by enforcing execution discipline: real data, standard ontologies, quick validation.
280
281set -e
282
283echo "🚀 ggen v26_5_19: BIG BANG 80/20 Screening Gate"
284echo ""
285echo "Before initializing, you must answer 5 questions about execution readiness."
286echo "If you answer NO to any, stop and talk to Sean."
287echo ""
288
289# Screening Question 1: User Data
290echo "❓ Question 1/5: Do you have real user data (CSV/JSON)?"
291echo " (Not promised. Actual files. If building a feature, do you have beta users' data?)"
292echo " Answer (yes/no):"
293read -r q1
294if [[ "$q1" != "yes" ]]; then
295 echo "❌ STOP. You need real data to validate with. Build MVP first, use ggen after."
296 exit 1
297fi
298
299# Screening Question 2: Standard Ontology
300echo ""
301echo "❓ Question 2/5: Can you find ONE existing standard ontology for your domain?"
302echo " (schema.org, FOAF, Dublin Core, SKOS - should take 5 min, not 3 months)"
303echo " Answer (yes/no):"
304read -r q2
305if [[ "$q2" != "yes" ]]; then
306 echo "❌ STOP. You're about to build a custom ontology (Seth's mistake)."
307 echo " 5 min: Find schema.org. 3 months: Build custom. Which path?"
308 exit 1
309fi
310
311# Screening Question 3: Problem Articulation
312echo ""
313echo "❓ Question 3/5: Can you explain your problem in ONE sentence?"
314echo " (No 100-page documents. Just the core job-to-be-done.)"
315echo " Say it out loud, then answer (yes/no):"
316read -r q3
317if [[ "$q3" != "yes" ]]; then
318 echo "❌ STOP. You don't have clarity. Write it down. One sentence. Try again."
319 exit 1
320fi
321
322# Screening Question 4: Market Signal
323echo ""
324echo "❓ Question 4/5: Has anyone (not friends, not co-founders) committed to this?"
325echo " (Email list, signed beta contract, payment - PROOF, not enthusiasm)"
326echo " Answer (yes/no):"
327read -r q4
328if [[ "$q4" != "yes" ]]; then
329 echo "⚠️ WARNING: Zero external validation. You're building in a vacuum."
330 echo " Proceed? (yes/no):"
331 read -r q4_confirm
332 if [[ "$q4_confirm" != "yes" ]]; then
333 exit 1
334 fi
335fi
336
337# Screening Question 5: Validation Speed
338echo ""
339echo "❓ Question 5/5: Can you validate with 10 real users in 48 hours?"
340echo " Answer (yes/no):"
341read -r q5
342if [[ "$q5" != "yes" ]]; then
343 echo "⚠️ WARNING: You don't have a validation plan. How will you know if it works?"
344 echo " Proceed? (yes/no):"
345 read -r q5_confirm
346 if [[ "$q5_confirm" != "yes" ]]; then
347 exit 1
348 fi
349fi
350
351echo ""
352echo "✅ Screening complete. You passed the litmus test."
353echo ""
354
355# Create directories if they don't exist
356mkdir -p schema
357mkdir -p templates
358mkdir -p scripts
359mkdir -p data
360
361echo "📁 Project structure created:"
362echo " schema/ - Your ontology files (use standard bases, not custom)"
363echo " data/ - Your real user data (CSV/JSON)"
364echo " templates/ - Tera templates for code generation"
365echo " scripts/ - Custom scripts"
366
367echo ""
368echo "📋 Next steps (in order):"
369echo " 1. Add your actual user data to data/ (CSV or JSON)"
370echo " 2. Edit schema/domain.ttl with standard ontology (schema.org, FOAF, Dublin Core, SKOS)"
371echo " 3. Create Tera templates in templates/ for your target language"
372echo " 4. Run 'ggen sync --validate-only' to test without writing"
373echo " 5. Run 'ggen sync' to generate code"
374echo " 6. Validate with your 10 real users (not friends)"
375echo ""
376echo "⚡ Speed targets:"
377echo " - Data upload: 1 hour"
378echo " - Ontology selection: 1 hour (use standard, don't build custom)"
379echo " - Template creation: 2-4 hours"
380echo " - First user validation: 24 hours"
381echo ""
382echo "📚 Resources:"
383echo " - schema.org: https://schema.org/ (Google, Microsoft, Yahoo - trusted)"
384echo " - FOAF: http://xmlns.com/foaf/spec/ (Social networks)"
385echo " - Dublin Core: http://dublincore.org/ (Metadata)"
386echo " - SKOS: https://www.w3.org/2004/02/skos/ (Controlled vocabularies)"
387echo " - ggen Docs: https://docs.ggen.io"
388echo " - RDF/Turtle: https://www.w3.org/TR/turtle/"
389echo " - Tera: https://keats.github.io/tera/"
390echo ""
391echo "💡 Remember: Seth's problem was building a custom 100-page ontology instead of"
392echo " using schema.org in 5 minutes. Stay disciplined. Use standards first."
393"#;
394
395#[allow(clippy::unused_unit)]
438#[verb("init", "root")]
439pub fn init(
440 path: Option<String>, force: Option<bool>, skip_hooks: Option<bool>,
441) -> VerbResult<InitOutput> {
442 let project_dir = path.unwrap_or_else(|| ".".to_string());
444 let force = force.unwrap_or(false);
445 let skip_hooks = skip_hooks.unwrap_or(false);
446
447 perform_init(&project_dir, force, skip_hooks).map_err(|e| e.into())
449}
450
451fn perform_init(
468 project_dir: &str, force: bool, skip_hooks: bool,
469) -> std::result::Result<InitOutput, GgenError> {
470 let base_path = Path::new(project_dir);
472
473 let ggen_artifacts = [
475 "ggen.toml",
476 "Makefile",
477 "schema/domain.ttl",
478 "scripts/startup.sh",
479 ];
480
481 let has_ggen_artifacts = ggen_artifacts.iter().any(|artifact| {
483 let path = base_path.join(artifact);
484 path.exists()
485 });
486
487 if has_ggen_artifacts && !force {
488 return Ok(InitOutput {
489 status: "error".to_string(),
490 project_dir: project_dir.to_string(),
491 files_created: vec![],
492 files_overwritten: None,
493 files_preserved: None,
494 directories_created: vec![],
495 error: Some(
496 "ggen project already initialized here. Use --force to reinitialize.".to_string(),
497 ),
498 warning: None,
499 next_steps: vec!["Run 'make build' to regenerate code".to_string()],
500 transaction: None,
501 git_hooks: None,
502 });
503 }
504
505 if let Err(e) = fs::create_dir_all(base_path) {
507 return Ok(InitOutput {
508 status: "error".to_string(),
509 project_dir: project_dir.to_string(),
510 files_created: vec![],
511 files_overwritten: None,
512 files_preserved: None,
513 directories_created: vec![],
514 error: Some(format!("Failed to create project directory: {}", e)),
515 warning: None,
516 next_steps: vec![],
517 transaction: None,
518 git_hooks: None,
519 });
520 }
521
522 let preflight = ggen_core::validation::PreFlightValidator::for_init(base_path);
524 if let Err(e) = preflight.validate(None) {
525 return Ok(InitOutput {
526 status: "error".to_string(),
527 project_dir: project_dir.to_string(),
528 files_created: vec![],
529 files_overwritten: None,
530 files_preserved: None,
531 directories_created: vec![],
532 error: Some(format!("{}", e)),
533 warning: None,
534 next_steps: vec![
535 "Ensure you have at least 100MB of free disk space".to_string(),
536 "Verify write permissions to the target directory".to_string(),
537 ],
538 transaction: None,
539 git_hooks: None,
540 });
541 }
542
543 let temp_test = base_path.join(".ggen_write_test");
545 match fs::write(&temp_test, "") {
546 Ok(_) => {
547 let _ = fs::remove_file(&temp_test); }
549 Err(e) => {
550 return Ok(InitOutput {
551 status: "error".to_string(),
552 project_dir: project_dir.to_string(),
553 files_created: vec![],
554 files_overwritten: None,
555 files_preserved: None,
556 directories_created: vec![],
557 error: Some(format!("No write permission in project directory: {}", e)),
558 warning: None,
559 next_steps: vec![
560 "Check directory permissions or try a different location".to_string()
561 ],
562 transaction: None,
563 git_hooks: None,
564 });
565 }
566 }
567
568 let mut tx = FileTransaction::new().map_err(|e| {
570 GgenError::CommandError(format!("Failed to initialize file transaction: {}", e))
571 })?;
572
573 let mut directories_created = vec![];
574 let mut files_preserved = vec![];
575
576 let dirs = vec!["schema", "templates", "scripts"];
579 for dir in &dirs {
580 let dir_path = base_path.join(dir);
581 let existed = dir_path.exists();
582 fs::create_dir_all(&dir_path).map_err(|e| {
583 GgenError::CommandError(format!("Failed to create directory {}: {}", dir, e))
584 })?;
585 if !existed {
586 directories_created.push(dir.to_string());
587 }
588 }
589
590 let gitignore_path = base_path.join(".gitignore");
592 let gitignore_exists = gitignore_path.exists();
593 if gitignore_exists {
594 files_preserved.push(".gitignore".to_string());
595 }
596
597 let readme_path = base_path.join("README.md");
598 let readme_exists = readme_path.exists();
599 if readme_exists {
600 files_preserved.push("README.md".to_string());
601 }
602
603 let toml_path = base_path.join("ggen.toml");
608 tx.write_file(&toml_path, GGEN_TOML)
609 .map_err(|e| GgenError::CommandError(format!("Failed to write ggen.toml: {}", e)))?;
610
611 let schema_path = base_path.join("schema").join("domain.ttl");
613 tx.write_file(&schema_path, DOMAIN_TTL).map_err(|e| {
614 GgenError::CommandError(format!("Failed to write schema/domain.ttl: {}", e))
615 })?;
616
617 let makefile_path = base_path.join("Makefile");
619 tx.write_file(&makefile_path, MAKEFILE)
620 .map_err(|e| GgenError::CommandError(format!("Failed to write Makefile: {}", e)))?;
621
622 let template_path = base_path.join("templates").join("example.txt.tera");
624 tx.write_file(&template_path, EXAMPLE_TEMPLATE)
625 .map_err(|e| {
626 GgenError::CommandError(format!("Failed to write templates/example.txt.tera: {}", e))
627 })?;
628
629 let startup_sh_path = base_path.join("scripts").join("startup.sh");
631 tx.write_file(&startup_sh_path, STARTUP_SH).map_err(|e| {
632 GgenError::CommandError(format!("Failed to write scripts/startup.sh: {}", e))
633 })?;
634
635 if !gitignore_exists {
637 let gitignore_content = "# ggen outputs\n.ggen/\n";
638 tx.write_file(&gitignore_path, gitignore_content)
639 .map_err(|e| GgenError::CommandError(format!("Failed to write .gitignore: {}", e)))?;
640 }
641
642 let readme_content = r#"# My ggen Project
644
645Generated by `ggen init`.
646
647## Getting Started
648
6491. **Initialize project**: Run `make setup` to prepare your environment
6502. **Define schema**: Edit `schema/domain.ttl` with your domain model
6513. **Create templates**: Add Tera templates in `templates/` for your target languages
6524. **Generate code**: Run `make build` to generate code from your ontology
653
654## MCP/A2A Configuration
655
656This project includes MCP (Model Context Protocol) and A2A (Agent-to-Agent) configuration files:
657
658- `.mcp.json` - MCP server configuration
659- `a2a.toml` - A2A agent configuration
660
661To reinitialize these configs:
662```bash
663ggen mcp init-config --force
664```
665
666To validate configuration:
667```bash
668ggen mcp validate-config
669```
670
671To set up Claude Desktop integration:
672```bash
673ggen mcp setup
674```"#;
675 if !readme_exists {
676 tx.write_file(&readme_path, readme_content)
677 .map_err(|e| GgenError::CommandError(format!("Failed to write README.md: {}", e)))?;
678 }
679
680 #[cfg(unix)]
682 {
683 use std::os::unix::fs::PermissionsExt;
684 fs::set_permissions(&startup_sh_path, std::fs::Permissions::from_mode(0o755)).map_err(
685 |e| {
686 GgenError::CommandError(format!(
687 "Failed to set execute permissions on startup.sh: {}",
688 e
689 ))
690 },
691 )?;
692 }
693
694 let receipt = tx.commit().map_err(|e| {
697 GgenError::CommandError(format!("Failed to commit file transaction: {}", e))
698 })?;
699
700 let git_hooks_result = super::git_hooks::install_git_hooks(base_path, skip_hooks).ok(); let files_created: Vec<String> = receipt
705 .files_created
706 .iter()
707 .filter_map(|p| {
708 p.strip_prefix(base_path)
710 .ok()
711 .map(|rel| rel.display().to_string())
712 })
713 .collect();
714
715 let files_modified: Vec<String> = receipt
716 .files_modified
717 .iter()
718 .filter_map(|p| {
719 p.strip_prefix(base_path)
720 .ok()
721 .map(|rel| rel.display().to_string())
722 })
723 .collect();
724
725 let files_overwritten_opt = if files_modified.is_empty() {
726 None
727 } else {
728 Some(files_modified.clone())
729 };
730
731 let files_preserved_opt = if files_preserved.is_empty() {
732 None
733 } else {
734 Some(files_preserved.clone())
735 };
736
737 let warning = if !files_modified.is_empty() || !files_preserved.is_empty() {
739 let mut msgs = vec![];
740 if !files_modified.is_empty() {
741 msgs.push(format!("Overwrote {} file(s)", files_modified.len()));
742 }
743 if !files_preserved.is_empty() {
744 msgs.push(format!("Preserved {} user file(s)", files_preserved.len()));
745 }
746 Some(msgs.join("; ") + ".")
747 } else {
748 None
749 };
750
751 Ok(InitOutput {
752 status: "success".to_string(),
753 project_dir: project_dir.to_string(),
754 files_created,
755 files_overwritten: files_overwritten_opt,
756 files_preserved: files_preserved_opt,
757 directories_created,
758 error: None,
759 warning,
760 next_steps: vec![
761 "Run 'make setup' to initialize your project".to_string(),
762 "Edit schema/domain.ttl to define your domain model".to_string(),
763 "Create Tera templates in templates/ for your target languages".to_string(),
764 "Run 'make build' to generate code from your ontology".to_string(),
765 ],
766 transaction: Some(TransactionInfo {
767 total_files: receipt.total_files(),
768 backups_created: receipt.backups.len(),
769 committed: true,
770 }),
771 git_hooks: git_hooks_result,
772 })
773}
774
775#[cfg(test)]
776mod tests {
777 use super::*;
778 use std::path::PathBuf;
779 use tempfile::tempdir;
780
781 #[test]
782 fn test_atomic_init_success() {
783 let temp_dir = tempdir().expect("Failed to create temp dir");
784 let project_path = temp_dir.path().to_str().expect("Invalid path");
785
786 let result = perform_init(project_path, false, true).expect("Init should succeed");
787
788 assert_eq!(result.status, "success");
790 assert!(result.error.is_none());
791
792 assert!(result.transaction.is_some());
794 let tx_info = result.transaction.unwrap();
795 assert!(tx_info.total_files > 0, "Should have created files");
796 assert!(tx_info.committed, "Transaction should be committed");
797
798 assert!(
800 !result.files_created.is_empty(),
801 "Should have created files"
802 );
803 assert!(
804 result.directories_created.len() >= 3,
805 "Should have created at least 3 directories"
806 );
807
808 let base = PathBuf::from(project_path);
810 assert!(base.join("ggen.toml").exists(), "ggen.toml should exist");
811 assert!(
812 base.join("schema/domain.ttl").exists(),
813 "domain.ttl should exist"
814 );
815 assert!(base.join("Makefile").exists(), "Makefile should exist");
816 assert!(
817 base.join("scripts/startup.sh").exists(),
818 "startup.sh should exist"
819 );
820 assert!(
821 base.join("templates/example.txt.tera").exists(),
822 "example.txt.tera should exist"
823 );
824 }
825
826 #[test]
827 fn test_init_preserves_existing_files() {
828 let temp_dir = tempdir().expect("Failed to create temp dir");
829 let project_path = temp_dir.path().to_str().expect("Invalid path");
830 let base = PathBuf::from(project_path);
831
832 fs::create_dir_all(&base).expect("Failed to create base dir");
834 let gitignore_content = "# Custom gitignore\n*.log\n";
835 let readme_content = "# Custom README\n\nMy project\n";
836 fs::write(base.join(".gitignore"), gitignore_content).expect("Failed to write .gitignore");
837 fs::write(base.join("README.md"), readme_content).expect("Failed to write README.md");
838
839 let result = perform_init(project_path, false, true).expect("Init should succeed");
840
841 assert!(
843 result.files_preserved.is_some(),
844 "Should have preserved files"
845 );
846 let preserved = result.files_preserved.unwrap();
847 assert!(
848 preserved.contains(&".gitignore".to_string()),
849 "Should preserve .gitignore"
850 );
851 assert!(
852 preserved.contains(&"README.md".to_string()),
853 "Should preserve README.md"
854 );
855
856 let gitignore_after =
858 fs::read_to_string(base.join(".gitignore")).expect("Failed to read .gitignore");
859 let readme_after =
860 fs::read_to_string(base.join("README.md")).expect("Failed to read README.md");
861 assert_eq!(
862 gitignore_after, gitignore_content,
863 ".gitignore should be unchanged"
864 );
865 assert_eq!(
866 readme_after, readme_content,
867 "README.md should be unchanged"
868 );
869 }
870
871 #[test]
872 fn test_init_force_overwrites_files() {
873 let temp_dir = tempdir().expect("Failed to create temp dir");
874 let project_path = temp_dir.path().to_str().expect("Invalid path");
875 let base = PathBuf::from(project_path);
876
877 perform_init(project_path, false, true).expect("First init should succeed");
879
880 let toml_path = base.join("ggen.toml");
882 let original_content = fs::read_to_string(&toml_path).expect("Failed to read ggen.toml");
883 fs::write(&toml_path, "# Modified\n").expect("Failed to modify ggen.toml");
884
885 let result = perform_init(project_path, false, true).expect("Should return result");
887 assert_eq!(result.status, "error");
888 assert!(result.error.is_some());
889 assert!(result.error.unwrap().contains("already initialized"));
890
891 let result = perform_init(project_path, true, true).expect("Force init should succeed");
893 assert_eq!(result.status, "success");
894 assert!(
895 result.files_overwritten.is_some(),
896 "Should have overwritten files"
897 );
898
899 let restored_content = fs::read_to_string(&toml_path).expect("Failed to read ggen.toml");
901 assert_eq!(
902 restored_content, original_content,
903 "Content should be restored"
904 );
905 }
906
907 #[test]
908 fn test_transaction_receipt_tracking() {
909 let temp_dir = tempdir().expect("Failed to create temp dir");
910 let project_path = temp_dir.path().to_str().expect("Invalid path");
911
912 let result = perform_init(project_path, false, true).expect("Init should succeed");
913
914 assert!(result.transaction.is_some());
916 let tx_info = result.transaction.unwrap();
917
918 assert_eq!(
920 tx_info.total_files,
921 result.files_created.len(),
922 "Transaction total_files should match files_created count"
923 );
924
925 assert_eq!(tx_info.backups_created, 0, "No backups on fresh init");
927
928 assert!(tx_info.committed, "Transaction should be committed");
930 }
931
932 #[test]
933 fn test_transaction_creates_backups_on_overwrite() {
934 let temp_dir = tempdir().expect("Failed to create temp dir");
935 let project_path = temp_dir.path().to_str().expect("Invalid path");
936
937 perform_init(project_path, false, true).expect("First init should succeed");
939
940 let result = perform_init(project_path, true, true).expect("Force init should succeed");
942
943 assert!(result.transaction.is_some());
945 let tx_info = result.transaction.unwrap();
946 assert!(
947 tx_info.backups_created > 0,
948 "Should have created backups on overwrite"
949 );
950 }
951
952 #[test]
953 fn test_init_creates_all_required_directories() {
954 let temp_dir = tempdir().expect("Failed to create temp dir");
955 let project_path = temp_dir.path().to_str().expect("Invalid path");
956 let base = PathBuf::from(project_path);
957
958 let result = perform_init(project_path, false, true).expect("Init should succeed");
959
960 assert!(base.join("schema").is_dir(), "schema/ should exist");
962 assert!(base.join("templates").is_dir(), "templates/ should exist");
963 assert!(base.join("scripts").is_dir(), "scripts/ should exist");
964
965 let dirs = &result.directories_created;
967 assert!(
968 dirs.contains(&"schema".to_string()),
969 "Should report schema/ created"
970 );
971 assert!(
972 dirs.contains(&"templates".to_string()),
973 "Should report templates/ created"
974 );
975 assert!(
976 dirs.contains(&"scripts".to_string()),
977 "Should report scripts/ created"
978 );
979 }
980
981 #[test]
982 #[cfg(unix)]
983 fn test_startup_sh_is_executable() {
984 let temp_dir = tempdir().expect("Failed to create temp dir");
985 let project_path = temp_dir.path().to_str().expect("Invalid path");
986 let base = PathBuf::from(project_path);
987
988 perform_init(project_path, false, true).expect("Init should succeed");
989
990 let startup_path = base.join("scripts/startup.sh");
992 let metadata = fs::metadata(&startup_path).expect("Failed to get startup.sh metadata");
993
994 use std::os::unix::fs::PermissionsExt;
995 let mode = metadata.permissions().mode();
996 assert!(mode & 0o111 != 0, "startup.sh should be executable");
997 }
998
999 #[test]
1000 fn test_init_output_structure() {
1001 let temp_dir = tempdir().expect("Failed to create temp dir");
1002 let project_path = temp_dir.path().to_str().expect("Invalid path");
1003
1004 let result = perform_init(project_path, false, true).expect("Init should succeed");
1005
1006 assert_eq!(result.status, "success");
1008 assert_eq!(result.project_dir, project_path);
1009 assert!(!result.files_created.is_empty());
1010 assert!(result.error.is_none());
1011 assert!(!result.next_steps.is_empty());
1012 assert!(result.transaction.is_some());
1013
1014 let json = serde_json::to_string(&result).expect("Should serialize to JSON");
1016 assert!(json.contains("\"status\":\"success\""));
1017 assert!(json.contains("\"transaction\""));
1018 }
1019}