| use std::fs; |
| use std::io::Write; |
| use std::path::Path; |
|
|
| use firm_core::{Entity, EntityId, EntityType, FieldId, FieldValue}; |
| use firm_lang::defaults; |
| use firm_lang::generate::{generate_dsl, generate_schema_dsl}; |
| use inquire::{Confirm, Text}; |
|
|
| use super::add::sanitize_entity_id; |
| use crate::errors::CliError; |
| use crate::ui; |
|
|
| |
| pub fn init_workspace(workspace_path: &Path) -> Result<(), CliError> { |
| ui::header(&format!( |
| "Initializing Firm workspace at {}", |
| workspace_path.display() |
| )); |
|
|
| |
| let include_schemas = Confirm::new("Include default schemas?") |
| .with_default(true) |
| .prompt() |
| .map_err(|_| CliError::InputError)?; |
|
|
| if include_schemas { |
| create_default_schemas(workspace_path)?; |
| } |
|
|
| |
| create_or_update_gitignore(workspace_path)?; |
|
|
| |
| let create_entities = Confirm::new("Add default entities (you and your organization)?") |
| .with_default(true) |
| .prompt() |
| .map_err(|_| CliError::InputError)?; |
|
|
| if create_entities { |
| create_default_entities(workspace_path)?; |
| } |
|
|
| |
| let add_ai_context = Confirm::new("Add AI context (AGENTS.md)?") |
| .with_default(true) |
| .prompt() |
| .map_err(|_| CliError::InputError)?; |
|
|
| if add_ai_context { |
| create_ai_context(workspace_path)?; |
| } |
|
|
| ui::success("Workspace initialized!"); |
|
|
| Ok(()) |
| } |
|
|
| |
| fn create_ai_context(workspace_path: &Path) -> Result<(), CliError> { |
| let agents_md_path = workspace_path.join("AGENTS.md"); |
|
|
| |
| if agents_md_path.exists() { |
| let overwrite = Confirm::new("AGENTS.md already exists. Overwrite?") |
| .with_default(false) |
| .prompt() |
| .map_err(|_| CliError::InputError)?; |
|
|
| if !overwrite { |
| ui::info("Skipped AI context creation"); |
| return Ok(()); |
| } |
| } |
|
|
| |
| let agents_md_content = include_str!("../../AGENTS.md.template"); |
|
|
| fs::write(&agents_md_path, agents_md_content).map_err(|_| CliError::FileError)?; |
| ui::success("Created AGENTS.md"); |
|
|
| Ok(()) |
| } |
|
|
| |
| fn create_default_entities(workspace_path: &Path) -> Result<(), CliError> { |
| let main_file_path = workspace_path.join("main.firm"); |
|
|
| |
| if main_file_path.exists() { |
| let overwrite = Confirm::new("main.firm already exists. Overwrite?") |
| .with_default(false) |
| .prompt() |
| .map_err(|_| CliError::InputError)?; |
|
|
| if !overwrite { |
| ui::info("Skipped entity creation"); |
| return Ok(()); |
| } |
| } |
|
|
| ui::info("Let's set up your core entities"); |
|
|
| |
| let person_name = Text::new("Your name:") |
| .prompt() |
| .map_err(|_| CliError::InputError)?; |
|
|
| |
| let org_name = Text::new("Your organization name:") |
| .prompt() |
| .map_err(|_| CliError::InputError)?; |
|
|
| |
| let person_id = sanitize_entity_id(person_name.clone()); |
|
|
| let person_entity = Entity::new( |
| EntityId(format!("person.{}", person_id)), |
| EntityType::new("person"), |
| ) |
| .with_field(FieldId::new("name"), FieldValue::String(person_name)); |
|
|
| |
| let org_id = sanitize_entity_id(org_name.clone()); |
|
|
| let org_entity = Entity::new( |
| EntityId(format!("organization.{}", org_id)), |
| EntityType::new("organization"), |
| ) |
| .with_field(FieldId::new("name"), FieldValue::String(org_name)); |
|
|
| |
| let entities = vec![person_entity, org_entity]; |
| let dsl_content = generate_dsl(&entities); |
|
|
| |
| fs::write(&main_file_path, dsl_content).map_err(|_| CliError::FileError)?; |
|
|
| ui::success("Created main.firm with your person and organization"); |
|
|
| Ok(()) |
| } |
|
|
| |
| fn create_default_schemas(workspace_path: &Path) -> Result<(), CliError> { |
| let schemas_dir = workspace_path.join("schemas"); |
| let schemas = defaults::all_default_schemas(); |
|
|
| |
| let existing_files: Vec<String> = schemas |
| .iter() |
| .map(|schema| format!("{}.firm", schema.entity_type)) |
| .filter(|filename| schemas_dir.join(filename).exists()) |
| .collect(); |
|
|
| |
| if !existing_files.is_empty() { |
| ui::warning(&format!( |
| "{} schema file(s) already exist:", |
| existing_files.len() |
| )); |
| for filename in &existing_files { |
| ui::info(&format!(" - schemas/{}", filename)); |
| } |
|
|
| let overwrite = Confirm::new("Overwrite existing schema files?") |
| .with_default(false) |
| .prompt() |
| .map_err(|_| CliError::InputError)?; |
|
|
| if !overwrite { |
| ui::info("Skipped schema creation"); |
| return Ok(()); |
| } |
| } |
|
|
| |
| fs::create_dir_all(&schemas_dir).map_err(|_| CliError::FileError)?; |
|
|
| let spinner = ui::spinner(&format!("Creating {} default schemas", schemas.len())); |
|
|
| for schema in &schemas { |
| let schema_name = schema.entity_type.to_string(); |
| let file_path = schemas_dir.join(format!("{}.firm", schema_name)); |
| let dsl_content = generate_schema_dsl(schema); |
|
|
| let mut file = fs::File::create(&file_path).map_err(|_| CliError::FileError)?; |
| file.write_all(dsl_content.as_bytes()) |
| .map_err(|_| CliError::FileError)?; |
| } |
|
|
| spinner.finish_with_message(format!( |
| "Created {} schema files in schemas/", |
| schemas.len() |
| )); |
|
|
| Ok(()) |
| } |
|
|
| |
| fn create_or_update_gitignore(workspace_path: &Path) -> Result<(), CliError> { |
| let gitignore_path = workspace_path.join(".gitignore"); |
| let gitignore_entries = "**/*.firm.graph\n"; |
|
|
| if gitignore_path.exists() { |
| |
| let update = Confirm::new("Update existing .gitignore with Firm entries?") |
| .with_default(true) |
| .prompt() |
| .map_err(|_| CliError::InputError)?; |
|
|
| if !update { |
| ui::info("Skipped .gitignore update"); |
| return Ok(()); |
| } |
|
|
| |
| let existing_content = |
| fs::read_to_string(&gitignore_path).map_err(|_| CliError::FileError)?; |
|
|
| |
| if existing_content.contains(".DS_Store") && existing_content.contains("*.firm.graph") { |
| ui::info(".gitignore already contains Firm entries"); |
| return Ok(()); |
| } |
|
|
| |
| let mut file = fs::OpenOptions::new() |
| .append(true) |
| .open(&gitignore_path) |
| .map_err(|_| CliError::FileError)?; |
|
|
| |
| let prefix = if existing_content.ends_with('\n') { |
| "" |
| } else { |
| "\n" |
| }; |
| file.write_all(format!("{}{}", prefix, gitignore_entries).as_bytes()) |
| .map_err(|_| CliError::FileError)?; |
|
|
| ui::success("Updated .gitignore with Firm entries"); |
| } else { |
| |
| let create = Confirm::new("Create .gitignore file?") |
| .with_default(true) |
| .prompt() |
| .map_err(|_| CliError::InputError)?; |
|
|
| if !create { |
| ui::info("Skipped .gitignore creation"); |
| return Ok(()); |
| } |
|
|
| |
| fs::write(&gitignore_path, gitignore_entries).map_err(|_| CliError::FileError)?; |
|
|
| ui::success("Created .gitignore"); |
| } |
|
|
| Ok(()) |
| } |
|
|