Jesús Pérez 9cef9b8d57 refactor: consolidate configuration directories
Merge _configs/ into config/ for single configuration directory.
Update all path references.

Changes:
- Move _configs/* to config/
- Update .gitignore for new patterns
- No code references to _configs/ found

Impact: -1 root directory (layout_conventions.md compliance)
2025-12-26 18:36:23 +00:00

613 lines
19 KiB
Rust

use anyhow::Result;
use clap::{Parser, Subcommand};
use tools_shared::find_config_path_warn_conflicts;
mod api_client;
mod handlers;
mod remote_ops;
mod ui_config;
use handlers::{
BatchHandler, ChecklistHandler, ConfigHandler, CreateHandler, ExportFormat, ExportHandler,
InitHandler, MigrateDbHandler, MigrationHandler, PhaseHandler, ProjectHandler, SbomHandler,
SearchHandler, SecurityHandler, StatusHandler, ToolHandler,
};
#[derive(Parser)]
#[command(name = "workspace")]
#[command(about = "Syntaxis")]
#[command(long_about = r#"Syntaxis - Orchestrate software syntaxis
CONFIGURATION SEARCH:
Looks for config in order (uses first found):
1. .project/lifecycle.toml - If using Tools
2. .vapora/lifecycle.toml - If VAPORA project
The tool automatically uses the first existing location.
Example: If .project/ doesn't exist but .vapora/ does,
it will use .vapora/lifecycle.toml automatically.
DIRECTORIES:
.project/ - Tools installation (create if using Tools)
.vapora/ - VAPORA platform configuration
.coder/ - Documentation tracking & agent interaction
.claude/ - Claude Code integration (skills, commands)
PHASES:
creation → devel → update → review ↔ status → publish → archive
"#)]
struct Cli {
#[arg(short, long, global = true)]
verbose: bool,
#[arg(
short = 'p',
long,
global = true,
help = "Project name (auto-detected if omitted)"
)]
project: Option<String>,
#[arg(
long,
global = true,
value_name = "PATH",
help = "Configuration file path (overrides auto-discovery)"
)]
config: Option<String>,
#[arg(long, global = true, help = "Use remote API instead of local database")]
remote: bool,
#[arg(
long,
global = true,
value_name = "URL",
help = "API server base URL (default: http://localhost:3000)"
)]
api_url: Option<String>,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Initialize a new project
Init {
/// Project path
path: Option<String>,
/// Project type
#[arg(short, long)]
r#type: Option<String>,
},
/// Create project in database from TOML configuration
Create,
/// Migrate existing project to lifecycle management
Migrate {
/// Project path to migrate
path: String,
/// Skip backup creation
#[arg(long)]
no_backup: bool,
},
/// Show current phase
Phase {
#[command(subcommand)]
subcommand: Option<PhaseCommands>,
},
/// Manage tools
Tool {
#[command(subcommand)]
subcommand: Option<ToolCommands>,
},
/// Manage checklists
Checklist {
#[command(subcommand)]
subcommand: Option<ChecklistCommands>,
},
/// Security management and assessment
Security {
#[command(subcommand)]
subcommand: Option<SecurityCommands>,
},
/// Run compliance audit with vulnerability scanning
Audit {
/// Scan for vulnerabilities using cargo-audit
#[arg(long)]
audit_vulns: bool,
/// Filter vulnerabilities against SBOM components (only show vulns affecting SBOM)
#[arg(long)]
sbom_filter: bool,
/// Show detailed vulnerability analysis with remediation
#[arg(long)]
detailed: bool,
},
/// Show project status
Status,
/// Show configuration
Config {
#[command(subcommand)]
subcommand: Option<ConfigCommands>,
},
/// Batch operations on multiple projects
Batch {
/// Batch operation file (TOML/JSON)
file: String,
},
/// Search projects with advanced filters
Search {
/// Search query
query: String,
/// Filter by phase
#[arg(long)]
phase: Option<String>,
/// Filter by type
#[arg(long)]
r#type: Option<String>,
/// Sort by field
#[arg(long, default_value = "name")]
sort: String,
/// Sort order (asc/desc)
#[arg(long, default_value = "asc")]
order: String,
},
/// Export project data
Export {
/// Export format (json, csv, toml, yaml, markdown) - uses config default if not specified
#[arg(long)]
format: Option<String>,
/// Output file path - uses config default if not specified
#[arg(long)]
output: Option<String>,
/// Project to export (optional, all if not specified)
#[arg(long)]
project: Option<String>,
},
/// Import project data
Import {
/// Import file path
file: String,
/// Import format (auto-detected if not specified)
#[arg(long)]
format: Option<String>,
/// Overwrite existing projects
#[arg(long)]
overwrite: bool,
/// Dry run (preview without importing)
#[arg(long)]
dry_run: bool,
},
/// Plugin management
Plugin {
#[command(subcommand)]
subcommand: Option<PluginCommands>,
},
/// Generate Software Bill of Materials (SBOM) and dependency tree
Sbom {
#[command(subcommand)]
subcommand: Option<SbomCommands>,
},
/// Manage projects
Project {
#[command(subcommand)]
subcommand: Option<ProjectCommands>,
},
/// Migrate databases from local to global or run SQL migrations
#[command(name = "migrate-db")]
MigrateDb {
/// Local database path to migrate (to global)
#[arg(value_name = "PATH")]
path: Option<String>,
/// Scan for local databases instead of migrating
#[arg(long)]
scan: bool,
/// Show migration status
#[arg(long)]
status: bool,
/// Path to SQL migration file to execute
#[arg(long, value_name = "FILE")]
sql_file: Option<String>,
/// Find and run all migration files in data/migration directory
#[arg(long, value_name = "ROOT")]
run_all: Option<String>,
},
}
#[derive(Subcommand)]
enum PhaseCommands {
/// Show current phase
Current,
/// Transition to new phase
Transition {
/// Target phase name
phase: String,
/// Check checklist before transitioning
#[arg(long)]
check_checklist: bool,
},
}
#[derive(Subcommand)]
enum ToolCommands {
/// List all tools
List,
/// Enable a tool
Enable { tool: String },
/// Disable a tool
Disable { tool: String },
/// Show tool status
Status,
/// List available Rust tool templates
#[command(name = "list-rust")]
ListRust,
/// Add a Rust tool template to the project
#[command(name = "add-rust-template")]
AddRustTemplate { tool: String },
}
#[derive(Subcommand)]
enum ChecklistCommands {
/// Show checklist
Show { phase: Option<String> },
/// Mark item complete
Complete { item_id: String },
/// Add item to checklist
Add { description: String },
/// Show available task types
Types,
}
#[derive(Subcommand)]
enum ConfigCommands {
/// Show configuration
Show,
/// Validate configuration
Validate,
}
#[derive(Subcommand)]
enum PluginCommands {
/// List installed plugins
List,
/// Install a plugin
Install { path: String },
/// Uninstall a plugin
Uninstall { name: String },
/// Enable a plugin
Enable { name: String },
/// Disable a plugin
Disable { name: String },
/// Show plugin info
Info { name: String },
}
#[derive(Subcommand)]
enum SecurityCommands {
/// Set security profile for the project
Profile {
/// Profile type (webapi, clitool, dataprocessing, iot, ml, opensourcelib, enterprise, desktop, mobile, microservice)
profile_type: String,
},
/// Assess security posture
Assess {
/// Show detailed recommendations
#[arg(long)]
detailed: bool,
},
}
#[derive(Subcommand)]
enum SbomCommands {
/// Generate Software Bill of Materials in configured format(s)
Generate,
/// Generate and display dependency tree (cargo tree)
Tree,
}
#[derive(Subcommand)]
enum ProjectCommands {
/// List all projects in database
List,
/// Show details of a project
Show {
/// Project name
name: String,
},
/// Detect current project from .project/lifecycle.toml
Detect,
/// Interactively select a project
Select,
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
// Set config path as environment variable for handlers (before loading config)
if let Some(config_path) = &cli.config {
std::env::set_var("SYNTAXIS_CONFIG_PATH", config_path);
}
// Load UI configuration after setting environment variable
let _ui_config = if let Ok(Some(config_path)) =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
std::env::var("SYNTAXIS_CONFIG_PATH").ok()
})) {
ui_config::UiConfig::load_from_file(&config_path).ok()
} else if let Some(config_path) = find_config_path_warn_conflicts("config.toml", None) {
ui_config::UiConfig::load_from_file(config_path).ok()
} else {
None
};
// Setup logging
if cli.verbose {
tracing_subscriber::fmt::init();
}
// Note: Logo display is handled by the NuShell wrapper script based on
// show_logo_on_empty config. The Rust binary runs after wrapper has already
// determined whether to show the logo. This just loads config for consistency.
// Initialize remote API client if --remote flag is set
if cli.remote {
let client = remote_ops::init_api_client(cli.api_url.clone())?;
remote_ops::check_api_health(&client).await?;
}
match cli.command {
Commands::Init { path, r#type } => {
InitHandler::init(path.as_deref(), r#type.as_deref()).await?;
}
Commands::Create => {
CreateHandler::create_project().await?;
}
Commands::Migrate { path, no_backup } => {
MigrationHandler::migrate(&path, !no_backup).await?;
}
Commands::Phase { subcommand } => match subcommand {
Some(PhaseCommands::Current) => PhaseHandler::show_current()?,
Some(PhaseCommands::Transition {
phase,
check_checklist,
}) => {
PhaseHandler::transition_to(&phase, check_checklist)?;
}
None => {
println!("Phase command requires a subcommand: current or transition");
PhaseHandler::show_current()?;
}
},
Commands::Tool { subcommand } => match subcommand {
Some(ToolCommands::List) => ToolHandler::list_tools()?,
Some(ToolCommands::Enable { tool }) => {
ToolHandler::enable_tool(&tool)?;
}
Some(ToolCommands::Disable { tool }) => {
ToolHandler::disable_tool(&tool)?;
}
Some(ToolCommands::Status) => ToolHandler::show_status()?,
Some(ToolCommands::ListRust) => ToolHandler::list_rust_tools()?,
Some(ToolCommands::AddRustTemplate { tool }) => {
ToolHandler::add_rust_tool(&tool)?;
}
None => {
println!("Tool command requires a subcommand");
ToolHandler::list_tools()?;
}
},
Commands::Checklist { subcommand } => match subcommand {
Some(ChecklistCommands::Show { phase }) => {
ChecklistHandler::show_checklist(phase.as_deref()).await?;
}
Some(ChecklistCommands::Complete { item_id }) => {
ChecklistHandler::mark_complete(&item_id).await?;
}
Some(ChecklistCommands::Add { description }) => {
ChecklistHandler::add_item(&description).await?;
}
Some(ChecklistCommands::Types) => {
ChecklistHandler::show_types_help()?;
}
None => {
ChecklistHandler::show_checklist(None).await?;
}
},
Commands::Security { subcommand } => match subcommand {
Some(SecurityCommands::Profile { profile_type }) => {
SecurityHandler::set_security_profile(&profile_type)?;
}
Some(SecurityCommands::Assess { detailed }) => {
SecurityHandler::assess_security(detailed)?;
}
None => println!("Security command requires a subcommand"),
},
Commands::Audit {
audit_vulns,
sbom_filter,
detailed,
} => {
SecurityHandler::run_audit(audit_vulns, sbom_filter, detailed).await?;
}
Commands::Status => StatusHandler::show_status()?,
Commands::Config { subcommand } => match subcommand {
Some(ConfigCommands::Show) => ConfigHandler::show_config()?,
Some(ConfigCommands::Validate) => ConfigHandler::validate_config()?,
None => {
println!("Config command requires a subcommand");
ConfigHandler::show_config()?;
}
},
Commands::Batch { file } => {
let batch = BatchHandler::load_from_file(&file)?;
let results = BatchHandler::execute(&batch)?;
println!(
"Batch operation completed: {} successful, {} failed",
results.iter().filter(|r| r.success).count(),
results.iter().filter(|r| !r.success).count()
);
}
Commands::Search {
query,
phase,
r#type,
sort,
order,
} => {
let mut filter = SearchHandler::parse_query(&query);
if let Some(p) = phase {
filter.phase = Some(p);
}
if let Some(t) = r#type {
filter.project_type = Some(t);
}
filter.sort = sort;
filter.order = order;
println!(
"Search filters: phase={:?}, type={:?}, query={:?}",
filter.phase, filter.project_type, filter.query
);
}
Commands::Export {
format,
output,
project: _project,
} => {
// Parse format if provided
let export_format = format
.as_ref()
.map(|f| ExportFormat::from_str(f))
.transpose()?;
// Find config file
let config_path = find_config_path_warn_conflicts(
"config.toml",
cli.config.as_ref().map(|s| std::path::Path::new(s)),
)
.ok_or_else(|| {
anyhow::anyhow!(
"Configuration not found. Please create:\n \
- .syntaxis/config.toml (local), or\n \
- .vapora/config.toml (local - takes precedence), or\n \
- ~/.syntaxis/config.toml (global), or\n \
- ~/.vapora/config.toml (global - takes precedence)\n\n \
Or provide explicit path with --config PATH\n \
Or run: syntaxis init"
)
})?;
ExportHandler::execute_export(&config_path, export_format, output.as_deref(), None)
.await?;
}
Commands::Import {
file,
format,
overwrite,
dry_run,
} => {
let import_format = if let Some(fmt) = format {
handlers::ExportFormat::from_str(&fmt)?
} else if file.ends_with(".json") {
handlers::ExportFormat::Json
} else if file.ends_with(".csv") {
handlers::ExportFormat::Csv
} else if file.ends_with(".toml") {
handlers::ExportFormat::Toml
} else {
handlers::ExportFormat::Json
};
println!("Importing from {}", file);
println!(
"Format: {:?}, Overwrite: {}, Dry run: {}",
import_format, overwrite, dry_run
);
println!(
"Import configuration: source_file={}, format={:?}, overwrite={}, dry_run={}",
file, import_format, overwrite, dry_run
);
}
Commands::Plugin { subcommand } => match subcommand {
Some(PluginCommands::List) => {
println!("Plugin management not yet fully implemented");
}
Some(PluginCommands::Install { path }) => {
println!("Installing plugin from: {}", path);
}
Some(PluginCommands::Uninstall { name }) => {
println!("Uninstalling plugin: {}", name);
}
Some(PluginCommands::Enable { name }) => {
println!("Enabling plugin: {}", name);
}
Some(PluginCommands::Disable { name }) => {
println!("Disabling plugin: {}", name);
}
Some(PluginCommands::Info { name }) => {
println!("Plugin info: {}", name);
}
None => {
println!("Plugin command requires a subcommand");
}
},
Commands::Sbom { subcommand } => match subcommand {
Some(SbomCommands::Generate) => {
SbomHandler::generate_sbom()?;
}
Some(SbomCommands::Tree) => {
SbomHandler::generate_tree()?;
}
None => {
println!("SBOM command requires a subcommand: generate or tree");
SbomHandler::generate_sbom()?;
}
},
Commands::Project { subcommand } => match subcommand {
Some(ProjectCommands::List) => {
ProjectHandler::list_projects().await?;
}
Some(ProjectCommands::Show { name }) => {
ProjectHandler::show_project(&name).await?;
}
Some(ProjectCommands::Detect) => {
ProjectHandler::detect_current()?;
}
Some(ProjectCommands::Select) => {
let selected = ProjectHandler::select_from_list().await?;
println!("\n✅ Selected project: {}", selected);
}
None => {
println!("Project command requires a subcommand: list, show, detect, or select");
ProjectHandler::list_projects().await?;
}
},
Commands::MigrateDb {
path,
scan,
status,
sql_file,
run_all,
} => {
if status {
MigrateDbHandler::show_status().await?;
} else if scan {
let root = path.as_deref().unwrap_or(".");
MigrateDbHandler::find_local_databases(root).await?;
} else if let Some(db_path) = path {
MigrateDbHandler::migrate_to_global(db_path.as_str()).await?;
} else if let Some(sql_path) = sql_file {
MigrateDbHandler::run_migration_file(&sql_path).await?;
} else if let Some(root_path) = run_all {
MigrateDbHandler::run_all_migrations(&root_path).await?;
} else {
println!("migrate-db requires one of: <PATH>, --scan, --status, --sql-file <FILE>, or --run-all <ROOT>");
MigrateDbHandler::show_status().await?;
}
}
}
Ok(())
}