use clap::{Parser, Subcommand};
mod codegen;
mod commands;
mod config;
mod data_io;
mod executor;
mod formatter;
mod repl;
mod script;
use config::Config;
use formatter::OutputFormat;
use repl::Repl;
use script::ScriptExecutor;
#[derive(Parser, Debug)]
#[command(name = "vibesql")]
#[command(version = "0.1.0")]
#[command(about = "VibeSQL - SQL:1999 FULL Compliance Database")]
#[command(long_about = "VibeSQL command-line interface
USAGE MODES:
Interactive REPL: vibesql [--database <FILE>]
Execute Command: vibesql -c \"SELECT * FROM users\"
Execute File: vibesql -f script.sql
Execute from stdin: cat data.sql | vibesql
Generate Types: vibesql codegen --schema schema.sql --output types.ts
INTERACTIVE REPL:
When started without -c, -f, or piped input, VibeSQL enters an interactive
REPL with readline support, command history, and meta-commands like:
\\d [table] - Describe table or list all tables
\\dt - List tables
\\f <format> - Set output format
\\copy - Import/export CSV/JSON
\\help - Show all REPL commands
SUBCOMMANDS:
codegen Generate TypeScript types from database schema
CONFIGURATION:
Settings can be configured in ~/.vibesqlrc (TOML format):
[display]
format = \"table\" # Default output format
[database]
default_path = \"~/data.db\" # Default database file
auto_save = true # Auto-save on exit
[history]
file = \"~/.vibesql_history\" # Command history file
max_entries = 10000 # Max history entries
[query]
timeout_seconds = 0 # Query timeout (0 = no limit)
EXAMPLES:
# Start interactive REPL with in-memory database
vibesql
# Use persistent database file
vibesql --database mydata.db
# Execute single command
vibesql -c \"CREATE TABLE users (id INT, name VARCHAR(100))\"
# Run SQL script file
vibesql -f schema.sql -v
# Import data from CSV
echo \"\\\\copy users FROM 'data.csv'\" | vibesql --database mydata.db
# Export query results as JSON
vibesql -d mydata.db -c \"SELECT * FROM users\" --format json
# Generate TypeScript types from a schema file
vibesql codegen --schema schema.sql --output src/types.ts
# Generate TypeScript types from a running database
vibesql codegen --database mydata.db --output src/types.ts")]
struct Args {
#[arg(short, long, value_name = "FILE", global = true)]
database: Option<String>,
#[arg(short, long, value_name = "FILE")]
file: Option<String>,
#[arg(short, long, value_name = "SQL")]
command: Option<String>,
#[arg(long)]
stdin: bool,
#[arg(short, long)]
verbose: bool,
#[arg(long, value_parser = ["table", "json", "csv", "markdown", "html"], value_name = "FORMAT")]
format: Option<String>,
#[command(subcommand)]
subcommand: Option<Commands>,
}
#[derive(Subcommand, Debug)]
enum Commands {
#[command(
about = "Generate TypeScript types from database schema",
long_about = "Generate TypeScript type definitions from a VibeSQL database schema.
This command creates TypeScript interfaces for all tables in the database,
along with metadata objects for runtime type checking and IDE support.
INPUT SOURCES:
--database <FILE> Generate from an existing database file
--schema <FILE> Generate from a SQL schema file (CREATE TABLE statements)
OUTPUT:
--output <FILE> Write generated types to this file (default: types.ts)
OPTIONS:
--camel-case Convert column names to camelCase
--no-metadata Skip generating the tables metadata object
EXAMPLES:
# From a database file
vibesql codegen --database mydata.db --output src/db/types.ts
# From a SQL schema file
vibesql codegen --schema schema.sql --output src/db/types.ts
# With camelCase property names
vibesql codegen --schema schema.sql --output types.ts --camel-case"
)]
Codegen {
#[arg(short, long, value_name = "FILE")]
schema: Option<String>,
#[arg(short, long, value_name = "FILE", default_value = "types.ts")]
output: String,
#[arg(long)]
camel_case: bool,
#[arg(long)]
no_metadata: bool,
},
}
fn main() -> anyhow::Result<()> {
let args = Args::parse();
if let Some(cmd) = args.subcommand {
return match cmd {
Commands::Codegen { schema, output, camel_case, no_metadata } => {
run_codegen(args.database, schema, output, camel_case, no_metadata)
}
};
}
let config = Config::load().unwrap_or_else(|e| {
eprintln!("Warning: Could not load config file: {}", e);
Config::default()
});
let format =
args.format.as_deref().and_then(parse_format).or_else(|| config.get_output_format());
let database = args.database.or(config.database.default_path.clone());
if let Some(cmd) = args.command {
execute_command(&cmd, database, format)?;
} else if let Some(file_path) = args.file {
execute_file(&file_path, database, args.verbose, format)?;
} else if args.stdin || is_stdin_piped() {
execute_stdin(database, args.verbose, format)?;
} else {
let mut repl = Repl::new(database, format)?;
repl.run()?;
}
Ok(())
}
fn run_codegen(
database: Option<String>,
schema: Option<String>,
output: String,
camel_case: bool,
no_metadata: bool,
) -> anyhow::Result<()> {
let config = codegen::CodegenConfig {
output: output.clone(),
include_metadata: !no_metadata,
camel_case,
};
let typescript = if let Some(schema_path) = schema {
println!("Generating TypeScript types from schema file: {}", schema_path);
codegen::generate_from_schema_file(&schema_path, &config)?
} else if let Some(db_path) = database {
println!("Generating TypeScript types from database: {}", db_path);
let db = vibesql_executor::load_sql_dump(&db_path)
.map_err(|e| anyhow::anyhow!("Failed to load database: {}", e))?;
codegen::generate_from_database(&db, &config)?
} else {
return Err(anyhow::anyhow!(
"Either --database or --schema must be specified.\n\
Use 'vibesql codegen --help' for usage information."
));
};
codegen::write_to_file(&typescript, &output)?;
println!("TypeScript types written to: {}", output);
Ok(())
}
fn parse_format(format_str: &str) -> Option<OutputFormat> {
match format_str {
"table" => Some(OutputFormat::Table),
"json" => Some(OutputFormat::Json),
"csv" => Some(OutputFormat::Csv),
"markdown" => Some(OutputFormat::Markdown),
"html" => Some(OutputFormat::Html),
_ => None,
}
}
fn execute_command(
cmd: &str,
database: Option<String>,
format: Option<OutputFormat>,
) -> anyhow::Result<()> {
let mut executor = ScriptExecutor::new(database, false, format)?;
executor.execute_script(cmd)?;
Ok(())
}
fn execute_file(
path: &str,
database: Option<String>,
verbose: bool,
format: Option<OutputFormat>,
) -> anyhow::Result<()> {
let mut executor = ScriptExecutor::new(database, verbose, format)?;
executor.execute_file(path)?;
Ok(())
}
fn execute_stdin(
database: Option<String>,
verbose: bool,
format: Option<OutputFormat>,
) -> anyhow::Result<()> {
let mut executor = ScriptExecutor::new(database, verbose, format)?;
executor.execute_stdin()?;
Ok(())
}
fn is_stdin_piped() -> bool {
!atty::is(atty::Stream::Stdin)
}