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)
613 lines
19 KiB
Rust
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(())
|
|
}
|