use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::{generate, Shell};
use colored::Colorize;
use std::io;
use std::process;
mod api_client;
mod commands;
mod config;
mod telemetry;
mod templates;
mod ui;
#[derive(Parser)]
#[command(name = "hs")]
#[command(about = "Hyperstack CLI - Build, deploy, and manage stream stacks", long_about = None)]
#[command(version)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
#[arg(short, long, global = true, default_value = "hyperstack.toml")]
config: String,
#[arg(long, global = true)]
json: bool,
#[arg(long, global = true)]
verbose: bool,
#[arg(long, global = true, env = "HYPERSTACK_API_URL")]
api_url: Option<String>,
#[arg(long, value_name = "SHELL")]
completions: Option<Shell>,
}
#[derive(Subcommand)]
enum Commands {
Create {
name: Option<String>,
#[arg(short, long)]
template: Option<String>,
#[arg(long)]
offline: bool,
#[arg(long)]
force_refresh: bool,
#[arg(long)]
skip_install: bool,
},
Init,
Up {
stack_name: Option<String>,
#[arg(short, long)]
branch: Option<String>,
#[arg(long, conflicts_with = "branch")]
preview: bool,
#[arg(long)]
dry_run: bool,
},
Status,
Explore {
name: Option<String>,
entity: Option<String>,
},
Push {
stack_name: Option<String>,
},
#[command(subcommand)]
Sdk(SdkCommands),
#[command(subcommand)]
Config(ConfigCommands),
#[command(subcommand)]
Auth(AuthCommands),
#[command(subcommand)]
Stack(StackCommands),
#[command(subcommand, hide = true)]
Build(BuildCommands),
#[command(subcommand)]
Telemetry(TelemetryCommands),
Idl(commands::idl::IdlArgs),
Stream(commands::stream::StreamArgs),
}
#[derive(Subcommand)]
enum SdkCommands {
#[command(subcommand)]
Create(CreateCommands),
List,
}
#[derive(Subcommand)]
enum CreateCommands {
Typescript {
stack_name: String,
#[arg(short, long)]
output: Option<String>,
#[arg(short, long)]
package_name: Option<String>,
#[arg(long)]
url: Option<String>,
},
Rust {
stack_name: String,
#[arg(short, long)]
output: Option<String>,
#[arg(long)]
crate_name: Option<String>,
#[arg(long)]
module: bool,
#[arg(long)]
url: Option<String>,
},
}
#[derive(Subcommand)]
enum ConfigCommands {
Validate,
}
#[derive(Subcommand)]
enum AuthCommands {
Login {
#[arg(short, long)]
key: Option<String>,
},
Logout,
LogoutAll,
Status,
Whoami,
#[command(subcommand)]
Keys(KeysCommands),
}
#[derive(Subcommand)]
enum KeysCommands {
List,
CreatePublishable {
#[arg(short, long)]
name: Option<String>,
#[arg(short, long, required = true, num_args = 1..)]
origin: Vec<String>,
#[arg(short, long)]
expiry_days: Option<i64>,
},
}
#[derive(Subcommand)]
enum StackCommands {
List,
Push {
stack_name: Option<String>,
},
Show {
stack_name: String,
#[arg(short, long)]
version: Option<i32>,
},
Versions {
stack_name: String,
#[arg(short, long, default_value = "20")]
limit: i64,
},
Delete {
stack_name: String,
#[arg(short, long)]
force: bool,
},
Rollback {
stack_name: String,
#[arg(long)]
to: Option<i32>,
#[arg(long)]
build: Option<i32>,
#[arg(long, default_value = "production")]
branch: String,
#[arg(long)]
rebuild: bool,
#[arg(long)]
no_wait: bool,
},
Stop {
stack_name: String,
#[arg(long)]
branch: Option<String>,
#[arg(short, long)]
force: bool,
},
}
#[derive(Subcommand)]
enum TelemetryCommands {
Status,
Enable,
Disable,
}
#[derive(Subcommand)]
enum BuildCommands {
Create {
stack_name: String,
#[arg(short, long)]
version: Option<i32>,
#[arg(long)]
ast_file: Option<String>,
#[arg(long)]
no_wait: bool,
},
List {
#[arg(short, long, default_value = "20")]
limit: i64,
#[arg(short, long)]
status: Option<String>,
},
Status {
build_id: i32,
#[arg(short, long)]
watch: bool,
#[arg(long)]
json: bool,
},
}
fn main() {
let cli = Cli::parse();
if let Some(ref api_url) = cli.api_url {
std::env::set_var("HYPERSTACK_API_URL", api_url);
}
if let Some(shell) = cli.completions {
let mut cmd = Cli::command();
generate(shell, &mut cmd, "hs", &mut io::stdout());
return;
}
telemetry::show_consent_banner_if_needed();
let cmd_name = cli.command.as_ref().map(command_name).unwrap_or("help");
let start = std::time::Instant::now();
let result = run(cli);
telemetry::record_command(
cmd_name,
result.is_ok(),
result
.as_ref()
.err()
.and_then(telemetry::extract_error_code)
.as_deref(),
start.elapsed(),
None,
);
telemetry::flush();
if let Err(e) = result {
eprintln!("{} {}", "Error:".red().bold(), e);
process::exit(1);
}
}
fn command_name(cmd: &Commands) -> &'static str {
match cmd {
Commands::Create { .. } => "create",
Commands::Init => "init",
Commands::Up { .. } => "up",
Commands::Status => "status",
Commands::Explore { .. } => "explore",
Commands::Push { .. } => "push",
Commands::Sdk(_) => "sdk",
Commands::Config(_) => "config",
Commands::Auth(_) => "auth",
Commands::Stack(_) => "stack",
Commands::Build(_) => "build",
Commands::Telemetry(_) => "telemetry",
Commands::Idl(_) => "idl",
Commands::Stream(_) => "stream",
}
}
fn run(cli: Cli) -> anyhow::Result<()> {
let Some(command) = cli.command else {
Cli::command().print_help()?;
return Ok(());
};
match command {
Commands::Create {
name,
template,
offline,
force_refresh,
skip_install,
} => commands::create::create(name, template, offline, force_refresh, skip_install),
Commands::Init => commands::config::init(&cli.config),
Commands::Up {
stack_name,
branch,
preview,
dry_run,
} => commands::up::up(&cli.config, stack_name.as_deref(), branch, preview, dry_run),
Commands::Status => commands::status::status(cli.json),
Commands::Explore { name, entity } => match name {
Some(name) => commands::explore::show(&name, entity.as_deref(), cli.json),
None => commands::explore::list(cli.json),
},
Commands::Push { stack_name } => commands::stack::push(&cli.config, stack_name.as_deref()),
Commands::Sdk(sdk_cmd) => match sdk_cmd {
SdkCommands::Create(create_cmd) => match create_cmd {
CreateCommands::Typescript {
stack_name,
output,
package_name,
url,
} => commands::sdk::create_typescript(
&cli.config,
&stack_name,
output,
package_name,
url,
),
CreateCommands::Rust {
stack_name,
output,
crate_name,
module,
url,
} => commands::sdk::create_rust(
&cli.config,
&stack_name,
output,
crate_name,
module,
url,
),
},
SdkCommands::List => commands::sdk::list(&cli.config),
},
Commands::Config(config_cmd) => match config_cmd {
ConfigCommands::Validate => commands::config::validate(&cli.config),
},
Commands::Auth(auth_cmd) => match auth_cmd {
AuthCommands::Login { key } => commands::auth::login(key),
AuthCommands::Logout => commands::auth::logout(),
AuthCommands::LogoutAll => commands::auth::logout_all(),
AuthCommands::Status => commands::auth::status(),
AuthCommands::Whoami => commands::auth::whoami(),
AuthCommands::Keys(keys_cmd) => match keys_cmd {
KeysCommands::List => commands::auth::list_keys(),
KeysCommands::CreatePublishable {
name,
origin,
expiry_days,
} => commands::auth::create_publishable_key(name, origin, expiry_days),
},
},
Commands::Stack(stack_cmd) => match stack_cmd {
StackCommands::List => commands::stack::list(cli.json),
StackCommands::Push { stack_name } => {
commands::stack::push(&cli.config, stack_name.as_deref())
}
StackCommands::Show {
stack_name,
version,
} => commands::stack::show(&stack_name, version, cli.json),
StackCommands::Versions { stack_name, limit } => {
commands::stack::versions(&stack_name, limit, cli.json)
}
StackCommands::Delete { stack_name, force } => {
commands::stack::delete(&stack_name, force)
}
StackCommands::Rollback {
stack_name,
to,
build,
branch,
rebuild,
no_wait,
} => commands::stack::rollback(&stack_name, to, build, &branch, rebuild, !no_wait),
StackCommands::Stop {
stack_name,
branch,
force,
} => commands::stack::stop(&stack_name, branch.as_deref(), force),
},
Commands::Build(build_cmd) => match build_cmd {
BuildCommands::Create {
stack_name,
version,
ast_file,
no_wait,
} => commands::build::create(
&cli.config,
&stack_name,
version,
ast_file.as_deref(),
!no_wait,
),
BuildCommands::List { limit, status } => {
commands::build::list(limit, status.as_deref(), cli.json)
}
BuildCommands::Status {
build_id,
watch,
json,
} => commands::build::status(build_id, watch, json || cli.json),
},
Commands::Idl(args) => commands::idl::run(args),
Commands::Stream(args) => commands::stream::run(args, &cli.config),
Commands::Telemetry(telemetry_cmd) => match telemetry_cmd {
TelemetryCommands::Status => commands::telemetry::status(),
TelemetryCommands::Enable => commands::telemetry::enable(),
TelemetryCommands::Disable => commands::telemetry::disable(),
},
}
}