mod auth;
mod commands;
mod config;
mod error;
mod output;
mod spike;
mod storage;
use clap::{Parser, Subcommand};
use commands::delete::DeleteOptions;
use commands::deploy::DeployOptions;
use commands::export::ExportFormat;
use commands::inject::InjectOptions;
use commands::list::ListOptions;
use commands::login::LoginOptions;
use commands::pull::PullOptions;
use commands::push::PushOptions;
use commands::resolve::ResolveOptions;
use commands::serve::ServeOptions;
use commands::share::ShareOptions;
use commands::shares::SharesOptions;
use commands::unshare::UnshareOptions;
use commands::usage::UsageOptions;
#[derive(Parser)]
#[command(name = "spikes")]
#[command(about = "Feedback collection for static mockups", long_about = None)]
#[command(version)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
#[arg(long, short, default_value = "3847", global = true)]
port: u16,
}
#[derive(Subcommand)]
enum Commands {
Init {
#[arg(long)]
json: bool,
},
List {
#[arg(long)]
json: bool,
#[arg(long)]
page: Option<String>,
#[arg(long)]
reviewer: Option<String>,
#[arg(long)]
rating: Option<String>,
#[arg(long)]
unresolved: bool,
},
Show {
id: String,
#[arg(long)]
json: bool,
},
Export {
#[arg(long, short, default_value = "json")]
format: String,
},
Hotspots {
#[arg(long)]
json: bool,
},
Reviewers {
#[arg(long)]
json: bool,
},
Inject {
directory: String,
#[arg(long)]
remove: bool,
#[arg(long)]
widget_url: Option<String>,
#[arg(long)]
json: bool,
},
Serve {
#[arg(long, short, default_value = "3847")]
port: u16,
#[arg(long, short, default_value = ".")]
dir: String,
#[arg(long, short)]
marked: bool,
#[arg(long)]
cors_allow_origin: Option<String>,
},
Deploy {
#[command(subcommand)]
backend: DeployBackend,
},
Pull {
#[arg(long)]
endpoint: Option<String>,
#[arg(long)]
token: Option<String>,
#[arg(long)]
from: Option<String>,
#[arg(long)]
json: bool,
},
Push {
#[arg(long)]
endpoint: Option<String>,
#[arg(long)]
token: Option<String>,
#[arg(long)]
json: bool,
},
Sync {
#[arg(long)]
json: bool,
},
Remote {
#[command(subcommand)]
action: RemoteAction,
},
Config {
#[arg(long)]
json: bool,
},
Version,
Update,
Login {
#[arg(long)]
token: Option<String>,
#[arg(long)]
json: bool,
},
Logout {
#[arg(long)]
json: bool,
},
Whoami {
#[arg(long)]
json: bool,
},
Share {
directory: String,
#[arg(long)]
name: Option<String>,
#[arg(long)]
password: Option<String>,
#[arg(long, default_value = "https://spikes.sh")]
host: String,
#[arg(long)]
json: bool,
},
Shares {
#[arg(long)]
json: bool,
},
Unshare {
slug: String,
#[arg(long, short)]
force: bool,
#[arg(long)]
json: bool,
},
Delete {
id: String,
#[arg(long, short)]
force: bool,
#[arg(long)]
json: bool,
},
Resolve {
id: String,
#[arg(long)]
unresolve: bool,
#[arg(long)]
json: bool,
},
Billing {
#[arg(long)]
json: bool,
},
Upgrade {
#[arg(long)]
json: bool,
},
Usage {
#[arg(long)]
json: bool,
},
Mcp {
#[command(subcommand)]
action: McpAction,
},
Auth {
#[command(subcommand)]
action: AuthAction,
},
}
#[derive(Subcommand)]
enum DeployBackend {
Cloudflare {
#[arg(long)]
dir: Option<String>,
#[arg(long)]
json: bool,
},
}
#[derive(Subcommand)]
enum McpAction {
Serve {
#[arg(long)]
remote: bool,
#[arg(long, value_enum, default_value = "stdio")]
transport: McpTransport,
#[arg(long, default_value = "3848")]
port: u16,
#[arg(long, default_value = "127.0.0.1")]
bind: String,
},
Install {
#[arg(long)]
json: bool,
},
}
#[derive(Clone, Debug, clap::ValueEnum)]
enum McpTransport {
Stdio,
Http,
}
#[derive(Subcommand)]
enum AuthAction {
CreateKey {
#[arg(long)]
name: Option<String>,
#[arg(long)]
json: bool,
},
ListKeys {
#[arg(long)]
json: bool,
},
RevokeKey {
key_id: String,
#[arg(long)]
json: bool,
},
}
#[derive(Subcommand)]
enum RemoteAction {
Add {
endpoint: String,
#[arg(long)]
token: Option<String>,
#[arg(long)]
hosted: bool,
},
Remove,
Show {
#[arg(long)]
json: bool,
},
}
fn main() {
let cli = Cli::parse();
let result = match cli.command {
None => commands::magic::run(cli.port),
Some(Commands::Init { json }) => commands::init::run(json),
Some(Commands::List {
json,
page,
reviewer,
rating,
unresolved,
}) => commands::list::run(ListOptions {
json,
page,
reviewer,
rating,
unresolved,
}),
Some(Commands::Show { id, json }) => commands::show::run(&id, json),
Some(Commands::Export { format }) => {
let fmt = match format.parse::<ExportFormat>() {
Ok(f) => f,
Err(e) => {
eprintln!("Error: {}", e);
std::process::exit(1);
}
};
commands::export::run(fmt)
}
Some(Commands::Hotspots { json }) => commands::hotspots::run(json),
Some(Commands::Reviewers { json }) => commands::reviewers::run(json),
Some(Commands::Inject {
directory,
remove,
widget_url,
json,
}) => commands::inject::run(InjectOptions {
directory,
remove,
widget_url,
json,
}),
Some(Commands::Serve { port, dir, marked, cors_allow_origin }) => commands::serve::run(ServeOptions {
port,
directory: dir,
marked,
cors_allow_origin,
}),
Some(Commands::Deploy { backend }) => match backend {
DeployBackend::Cloudflare { dir, json } => {
commands::deploy::run(DeployOptions { dir, json })
}
},
Some(Commands::Pull {
endpoint,
token,
from,
json,
}) => commands::pull::run(PullOptions {
endpoint,
token,
from,
json,
}),
Some(Commands::Push {
endpoint,
token,
json,
}) => commands::push::run(PushOptions {
endpoint,
token,
json,
}),
Some(Commands::Sync { json }) => commands::sync::run(json),
Some(Commands::Remote { action }) => match action {
RemoteAction::Add { endpoint, token, hosted } => {
commands::remote::add(&endpoint, token, hosted)
}
RemoteAction::Remove => commands::remote::remove(),
RemoteAction::Show { json } => commands::remote::show(json),
},
Some(Commands::Config { json }) => commands::config_cmd::run(json),
Some(Commands::Version) => {
println!("spikes {}", env!("CARGO_PKG_VERSION"));
Ok(())
}
Some(Commands::Update) => commands::update::run(),
Some(Commands::Login { token, json }) => commands::login::run(LoginOptions { token, json }),
Some(Commands::Logout { json }) => commands::logout::run(json),
Some(Commands::Whoami { json }) => commands::whoami::run(json),
Some(Commands::Share { directory, name, password, host, json }) => {
commands::share::run(ShareOptions { directory, name, password, host, json })
}
Some(Commands::Shares { json }) => commands::shares::run(SharesOptions { json }),
Some(Commands::Unshare { slug, force, json }) => {
commands::unshare::run(UnshareOptions { slug, force, json })
}
Some(Commands::Delete { id, force, json }) => {
commands::delete::run(DeleteOptions { id, force, json })
}
Some(Commands::Resolve { id, unresolve, json }) => {
commands::resolve::run(ResolveOptions { id, unresolve, json })
}
Some(Commands::Billing { json }) => commands::billing::run(json),
Some(Commands::Upgrade { json }) => commands::upgrade::run(json),
Some(Commands::Usage { json }) => commands::usage::run(UsageOptions { json }),
Some(Commands::Mcp { action }) => match action {
McpAction::Serve { remote, transport, port, bind } => {
let transport_mode = match transport {
McpTransport::Stdio => commands::mcp::TransportMode::Stdio,
McpTransport::Http => commands::mcp::TransportMode::Http { port, bind },
};
commands::mcp::run(remote, transport_mode)
}
McpAction::Install { json } => {
commands::mcp::install(json)
}
},
Some(Commands::Auth { action }) => match action {
AuthAction::CreateKey { name, json } => {
commands::auth_keys::create_key(name, json)
}
AuthAction::ListKeys { json } => {
commands::auth_keys::list_keys(json)
}
AuthAction::RevokeKey { key_id, json } => {
commands::auth_keys::revoke_key(&key_id, json)
}
},
};
if let Err(e) = result {
eprintln!("Error: {}", e);
std::process::exit(1);
}
}