mod api;
mod changelog;
mod commands;
mod config;
mod exit;
mod output;
mod render;
mod update;
use anyhow::{Context, Result};
use clap::{Args, CommandFactory, Parser, Subcommand};
use owo_colors::OwoColorize;
use api::client::ApiClient;
use config::Config;
use output::Format;
use render::theme;
#[derive(Parser)]
#[command(
name = "fundaia",
version,
about = "Proyectos, servicios, variables, despliegues, logs y métricas",
long_about = None,
propagate_version = true
)]
struct Cli {
#[arg(long, short, global = true, env = "FUNDAIA_PROFILE")]
profile: Option<String>,
#[arg(long, global = true, env = "FUNDAIA_URL")]
url: Option<String>,
#[arg(long, global = true, env = "FUNDAIA_TOKEN", hide_env_values = true)]
token: Option<String>,
#[arg(long, global = true)]
no_color: bool,
#[arg(long, global = true)]
json: bool,
#[arg(
long = "env",
global = true,
env = "FUNDAIA_ENV",
value_name = "ENTORNO"
)]
environment: Option<String>,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Login {
#[arg(long, default_value = "https://infrastructure.fundaia.com")]
url: String,
#[arg(long, default_value = "default")]
as_profile: String,
#[arg(long)]
no_browser: bool,
},
Logout {
#[arg(long)]
profile: Option<String>,
},
Upgrade {
#[arg(long)]
check: bool,
},
Changelog {
#[arg(value_name = "VERSION")]
release: Option<String>,
#[arg(long)]
all: bool,
#[arg(long)]
since: Option<String>,
#[arg(short = 'n', long)]
count: Option<usize>,
#[arg(long)]
history: bool,
},
Whoami,
Completions { shell: clap_complete::Shell },
Schema,
Guide,
Skill {
#[command(subcommand)]
command: Option<SkillCommand>,
},
Status,
Projects,
Project {
#[command(subcommand)]
command: ProjectCommand,
},
Envs { project: String },
Env {
#[command(subcommand)]
command: EnvCommand,
},
Webhooks { project: String },
Webhook {
#[command(subcommand)]
command: WebhookCommand,
},
Services { project: String },
Service { project: String, service: String },
Canvas {
#[command(subcommand)]
command: CanvasCommand,
},
New(NewArgs),
Agent {
project: String,
service: String,
#[arg(long)]
model: Option<String>,
#[arg(long)]
effort: Option<String>,
#[arg(long = "setup-token")]
setup_token: Option<String>,
},
Rename {
project: String,
service: String,
name: String,
},
Templates,
Up {
project: String,
service: String,
#[arg(long, default_value = ".")]
path: String,
#[arg(long = "no-deploy")]
no_deploy: bool,
#[arg(long)]
detach: bool,
},
Uploads { project: String, service: String },
Deploy {
project: String,
service: String,
#[arg(long)]
restart: bool,
#[arg(long)]
detach: bool,
},
Redeploy {
project: String,
service: String,
number: u32,
},
Rollback {
project: String,
service: String,
number: u32,
},
Cancel {
project: String,
service: String,
#[arg(long)]
deployment: Option<u32>,
},
Stop { project: String, service: String },
History {
project: String,
service: String,
#[arg(long, default_value_t = 15)]
limit: usize,
#[arg(long)]
commit: Option<String>,
},
#[command(alias = "show")]
Commit {
project: String,
service: String,
commit: Option<String>,
#[arg(long)]
file: Option<String>,
#[arg(long)]
stat: bool,
},
Activity {
project: String,
#[arg(long, default_value_t = 30)]
limit: usize,
},
#[command(alias = "logins")]
SignIns {
#[arg(long, short = 'n', default_value_t = 50)]
limit: usize,
},
Sessions {
#[command(subcommand)]
command: Option<SessionCommand>,
},
Watch {
project: Option<String>,
service: Option<String>,
},
Logs(LogsArgs),
#[command(alias = "inbox")]
Attention,
Health { project: String },
Diagnose { project: String, service: String },
Grep(GrepArgs),
#[command(alias = "dependencies")]
Deps { project: String, service: String },
Export {
project: String,
#[arg(long = "output", short = 'o', value_name = "FILE")]
file: Option<String>,
},
Import {
file: String,
#[arg(long)]
name: Option<String>,
#[arg(long)]
workspace: Option<String>,
#[arg(long)]
deploy: bool,
},
Compare {
project: String,
service: String,
from: Option<u32>,
to: Option<u32>,
},
Probe {
project: String,
service: String,
#[arg(default_value = "/")]
path: String,
},
Wake { project: String, service: String },
Security {
#[command(subcommand)]
command: SecurityCommand,
},
Issue {
#[arg(value_parser = ["incident", "deployment", "security-scan"])]
kind: String,
id: String,
},
Workflows {
#[command(subcommand)]
command: WorkflowCommand,
},
Incidents {
project: String,
service: String,
#[arg(long)]
decided: bool,
},
Approvals {
project: String,
service: Option<String>,
},
Approve {
id: String,
},
Reject {
id: String,
},
#[command(args_conflicts_with_subcommands = true, subcommand_negates_reqs = true)]
Fix {
#[arg(required = true)]
id: Option<String>,
#[command(subcommand)]
command: Option<FixCommand>,
},
Incident {
id: String,
#[arg(value_parser = ["resolved", "ignored", "open"])]
status: String,
},
Autofix(AutofixArgs),
Metrics {
project: String,
service: String,
#[arg(long, default_value_t = 60)]
minutes: u32,
},
#[command(alias = "usage")]
Consumption {
project: String,
service: String,
#[arg(long, default_value = "7d")]
range: String,
},
#[command(name = "agent-activity", alias = "prompts")]
AgentActivity {
project: String,
service: String,
#[arg(long, default_value = "24h")]
range: String,
#[arg(long)]
outcome: Option<String>,
},
#[command(alias = "vars")]
Variables {
#[command(subcommand)]
command: VariableCommand,
},
Shared {
#[command(subcommand)]
command: SharedCommand,
},
#[command(alias = "sql")]
Data {
#[command(subcommand)]
command: DataCommand,
},
Access {
#[command(subcommand)]
command: AccessCommand,
},
#[command(alias = "backup")]
Backups {
#[command(subcommand)]
command: BackupCommand,
},
Domains {
#[command(subcommand)]
command: DomainCommand,
},
Connect {
project: String,
service: String,
provider: String,
},
Disconnect {
project: String,
service: String,
provider: String,
},
Connectable { project: String, service: String },
Volumes {
#[command(subcommand)]
command: VolumeCommand,
},
Recipe {
#[command(subcommand)]
command: RecipeCommand,
},
Config {
#[command(subcommand)]
command: ConfigCommand,
},
#[command(alias = "offers")]
Capabilities { project: String, service: String },
#[command(alias = "workspaces")]
Workspace {
#[command(subcommand)]
command: WorkspaceCommand,
},
Capacity,
Settings(SettingsArgs),
Expose {
project: String,
service: String,
#[arg(long)]
off: bool,
},
Previews {
project: String,
#[arg(value_parser = ["on", "off"])]
switch: Option<String>,
},
Tasks {
#[command(subcommand)]
command: TasksCommand,
},
Files {
#[command(subcommand)]
command: FilesCommand,
},
Pull { project: String, service: String },
Exec {
project: String,
service: String,
#[arg(trailing_var_arg = true, allow_hyphen_values = true, required = true)]
command: Vec<String>,
},
Maintenance {
project: String,
service: String,
#[arg(long)]
off: bool,
#[arg(long)]
message: Option<String>,
},
Protect {
project: String,
service: String,
#[arg(long)]
user: String,
#[arg(long, env = "FUNDAIA_SERVICE_PASSWORD", hide_env_values = true)]
password: Option<String>,
},
Unprotect { project: String, service: String },
Encrypt {
project: String,
service: String,
#[arg(long)]
off: bool,
},
Delete {
project: String,
service: String,
#[arg(long)]
confirm: Option<String>,
},
}
#[derive(Subcommand)]
enum SkillCommand {
Show,
Install {
#[arg(long)]
project: bool,
},
Path {
#[arg(long)]
project: bool,
},
}
#[derive(Subcommand)]
enum WorkspaceCommand {
List,
#[command(alias = "who")]
Members { workspace: Option<String> },
#[command(alias = "saldo")]
Credits { workspace: Option<String> },
Invite {
email: String,
#[arg(long)]
workspace: Option<String>,
#[arg(long)]
admin: bool,
},
Revoke {
invitation_id: String,
#[arg(long)]
workspace: Option<String>,
},
Remove {
user_id: String,
#[arg(long)]
workspace: Option<String>,
},
#[command(name = "2fa")]
SecondFactor {
#[arg(value_parser = ["on", "off"])]
switch: String,
#[arg(long)]
workspace: Option<String>,
},
}
#[derive(Subcommand)]
enum ProjectCommand {
New {
name: String,
#[arg(long)]
description: Option<String>,
#[arg(long)]
workspace: Option<String>,
},
Rename {
project: String,
name: Option<String>,
#[arg(long)]
description: Option<String>,
},
Delete {
project: String,
#[arg(long)]
confirm: Option<String>,
},
Move { project: String, workspace: String },
Star { project: String },
Unstar { project: String },
}
#[derive(Subcommand)]
enum WebhookCommand {
Add {
project: String,
url: String,
#[arg(long)]
name: Option<String>,
#[arg(long)]
format: Option<String>,
#[arg(long, value_delimiter = ',')]
events: Vec<String>,
},
Set {
project: String,
webhook: String,
#[arg(long)]
name: Option<String>,
#[arg(long, value_delimiter = ',')]
events: Option<Vec<String>>,
#[arg(long, conflicts_with = "resume")]
pause: bool,
#[arg(long)]
resume: bool,
},
Rm { project: String, webhook: String },
Test { project: String, webhook: String },
}
#[derive(Subcommand)]
enum EnvCommand {
New {
project: String,
name: String,
#[arg(long, conflicts_with = "empty")]
from: Option<String>,
#[arg(long)]
empty: bool,
#[arg(long = "no-deploy")]
no_deploy: bool,
},
Rename {
project: String,
environment: String,
name: String,
},
Rm {
project: String,
environment: String,
#[arg(long)]
confirm: Option<String>,
},
Copy {
project: String,
service: String,
#[arg(long)]
from: Option<String>,
#[arg(long)]
to: String,
#[arg(long = "no-deploy")]
no_deploy: bool,
},
}
#[derive(Subcommand)]
enum CanvasCommand {
Show {
project: String,
#[arg(long)]
map: bool,
},
Move {
project: String,
service: String,
#[arg(long, allow_negative_numbers = true)]
x: i64,
#[arg(long, allow_negative_numbers = true)]
y: i64,
},
Arrange {
project: String,
#[arg(long, default_value = "layered", value_parser = ["layered", "grid"])]
layout: String,
},
}
#[derive(Subcommand)]
enum FixCommand {
Deployment {
project: String,
service: String,
number: Option<u32>,
},
}
#[derive(Subcommand)]
enum SecurityCommand {
Scan {
project: String,
service: String,
#[arg(long)]
fix: bool,
#[arg(long)]
wait: bool,
},
Show { scan_id: String },
List { project: String, service: String },
}
#[derive(Subcommand)]
enum WorkflowCommand {
List {
project: String,
#[arg(long)]
refresh: bool,
},
Runs {
project: String,
#[arg(long)]
path: Option<String>,
#[arg(long)]
commit: Option<String>,
#[arg(long, default_value_t = 30)]
limit: u32,
},
Run {
project: String,
path: String,
#[arg(long)]
branch: Option<String>,
#[arg(long = "input")]
inputs: Vec<String>,
},
Show { run_id: String },
Logs { job_id: String },
Cancel { run_id: String },
Rerun { run_id: String },
Stats {
project: String,
#[arg(long, default_value_t = 100)]
limit: u32,
},
Wait {
run_id: String,
#[arg(long, value_name = "SECS", default_value_t = 600)]
timeout: u64,
},
Artifact {
job_id: String,
#[arg(long)]
into: Option<String>,
},
}
#[derive(Subcommand)]
enum VolumeCommand {
List {
project: String,
service: String,
},
Add {
project: String,
service: String,
mount_path: String,
},
Resize {
volume_id: String,
#[arg(long)]
gb: u64,
},
Remove {
volume_id: String,
},
}
#[derive(Subcommand)]
enum RecipeCommand {
Show {
project: String,
service: String,
#[arg(long)]
plain: bool,
},
Edit {
project: String,
service: String,
#[arg(long, default_value = "-")]
file: String,
},
}
#[derive(Subcommand)]
enum ConfigCommand {
Show {
project: String,
service: String,
#[arg(long)]
plain: bool,
},
Edit {
project: String,
service: String,
#[arg(long, default_value = "-")]
file: String,
},
}
#[derive(Subcommand)]
enum DataCommand {
Tables { project: String, service: String },
Query {
project: String,
service: String,
sql: String,
#[arg(long, short = 'n')]
limit: Option<u64>,
},
Run {
project: String,
service: String,
sql: String,
#[arg(long)]
preview: bool,
#[arg(long)]
confirm: Option<String>,
},
}
#[derive(Subcommand)]
enum VariableCommand {
List {
project: String,
service: String,
#[arg(long)]
reveal: bool,
},
Set {
project: String,
service: String,
key: String,
value: String,
#[arg(long)]
secret: bool,
#[arg(long)]
sealed: bool,
},
Seal {
project: String,
service: String,
key: String,
#[arg(long)]
confirm: Option<String>,
},
Unset {
project: String,
service: String,
key: String,
},
Audit { project: String },
Import {
project: String,
service: String,
file: String,
#[arg(long)]
secret: bool,
},
}
#[derive(Args)]
struct SettingsArgs {
project: String,
service: String,
#[arg(long)]
repo: Option<String>,
#[arg(long)]
branch: Option<String>,
#[arg(long)]
memory: Option<u32>,
#[arg(long)]
replicas: Option<u32>,
#[arg(long)]
root: Option<String>,
#[arg(long = "start-command")]
start_command: Option<String>,
#[arg(long = "deploy-tags")]
deploy_tags: Option<String>,
#[arg(long = "pre-deploy")]
pre_deploy: Option<String>,
#[arg(long = "health-path")]
health_path: Option<String>,
#[arg(long = "health-probe")]
health_probe: Option<String>,
#[arg(long = "health-timeout")]
health_timeout_seconds: Option<u32>,
#[arg(long = "health-interval")]
health_interval_seconds: Option<u32>,
#[arg(long = "health-grace")]
health_grace_seconds: Option<u32>,
#[arg(long = "health-status")]
health_expected_status: Option<u32>,
#[arg(long = "restart")]
restart_policy: Option<String>,
#[arg(long = "restart-retries")]
restart_max_retries: Option<u32>,
#[arg(long)]
sleep: Option<bool>,
#[arg(long = "sleep-after")]
sleep_after_minutes: Option<u32>,
#[arg(long = "auto-deploy")]
auto_deploy: Option<bool>,
#[arg(long = "auto-update")]
image_auto_update: Option<bool>,
#[arg(long = "cache")]
cache_enabled: Option<bool>,
#[arg(long = "cache-ttl")]
cache_ttl_seconds: Option<u32>,
}
#[derive(Args)]
struct NewArgs {
project: String,
name: String,
#[arg(long)]
repo: Option<String>,
#[arg(long)]
image: Option<String>,
#[arg(long)]
template: Option<String>,
#[arg(long)]
upload: bool,
#[arg(long, default_value = "main")]
branch: String,
#[arg(long = "connect-to", value_delimiter = ',')]
connect_to: Vec<String>,
#[arg(long = "no-deploy")]
no_deploy: bool,
#[arg(long = "setup-token")]
setup_token: Option<String>,
#[arg(long)]
model: Option<String>,
#[arg(long)]
effort: Option<String>,
}
#[derive(Args)]
struct LogsArgs {
project: String,
service: String,
#[arg(long, short)]
follow: bool,
#[arg(long, short = 'n', default_value_t = 200)]
tail: usize,
#[arg(long)]
stream: Option<String>,
#[arg(long)]
step: Option<String>,
#[arg(long)]
deployment: Option<u32>,
#[arg(long, short = 't')]
timestamps: bool,
#[arg(long)]
trace: Option<String>,
}
#[derive(Args)]
struct GrepArgs {
project: String,
text: String,
#[arg(long)]
service: Option<String>,
#[arg(long, value_parser = ["debug", "info", "warn", "error"])]
level: Option<String>,
#[arg(long, value_parser = ["build", "deploy", "runtime", "system"])]
stream: Option<String>,
#[arg(long)]
hours: Option<u32>,
#[arg(long, short = 'n')]
limit: Option<u32>,
}
#[derive(Args)]
struct AutofixArgs {
project: String,
service: String,
#[arg(long, value_parser = ["off", "diagnose", "pullRequest"])]
mode: Option<String>,
#[arg(long)]
agent: Option<String>,
#[arg(long)]
model: Option<String>,
#[arg(long)]
min_occurrences: Option<u32>,
#[arg(long)]
daily_limit: Option<u32>,
}
#[derive(Subcommand)]
enum TasksCommand {
List { project: String, service: String },
Add {
project: String,
service: String,
name: String,
schedule: String,
#[arg(last = true, required = true)]
command: Vec<String>,
#[arg(long)]
paused: bool,
},
Edit {
project: String,
service: String,
task: String,
#[arg(long)]
name: Option<String>,
#[arg(long)]
schedule: Option<String>,
#[arg(last = true)]
command: Vec<String>,
#[arg(long, conflicts_with = "resume")]
pause: bool,
#[arg(long)]
resume: bool,
},
Rm {
project: String,
service: String,
task: String,
},
Run {
project: String,
service: String,
task: String,
#[arg(long)]
no_wait: bool,
},
Runs {
project: String,
service: String,
task: String,
},
}
#[derive(Subcommand)]
enum FilesCommand {
Ls {
project: String,
service: String,
path: Option<String>,
},
Cat {
project: String,
service: String,
path: String,
},
Get {
project: String,
service: String,
path: String,
#[arg(long, short)]
output: Option<String>,
},
}
#[derive(Subcommand)]
enum SharedCommand {
List { project: String },
Set {
project: String,
key: String,
value: String,
#[arg(long)]
secret: bool,
#[arg(long = "env-only")]
env_only: bool,
},
Unset {
project: String,
key: String,
#[arg(long = "env-only")]
env_only: bool,
},
}
#[derive(Subcommand)]
enum AccessCommand {
Status { project: String, service: String },
Request {
project: String,
service: String,
#[arg(long)]
email: Option<String>,
},
}
#[derive(Subcommand)]
enum BackupCommand {
List { project: String, service: String },
New { project: String, service: String },
Schedule {
project: String,
service: String,
#[arg(value_delimiter = ',')]
schedules: Vec<String>,
},
Restore {
backup_id: String,
#[arg(long)]
confirm: Option<String>,
},
Keep { backup_id: String },
Unkeep { backup_id: String },
Remove { backup_id: String },
}
#[derive(Subcommand)]
enum SessionCommand {
Revoke { session_id: String },
}
#[derive(Subcommand)]
enum DomainCommand {
List {
project: String,
service: String,
},
Add {
project: String,
service: String,
host: Option<String>,
},
Verify {
domain_id: String,
},
Remove {
domain_id: String,
},
}
fn main() {
let cli = Cli::parse();
let format = output::chosen(cli.json);
if cli.no_color || format.is_json() {
owo_colors::set_override(false);
theme::silence();
}
if !matches!(cli.command, Command::Completions { .. }) {
changelog::history::record(env!("CARGO_PKG_VERSION"));
}
let runtime;
let outcome = if asks_the_registry(&cli.command) {
runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("the async runtime starts");
runtime.block_on(async {
let update = update::UpdateCheck::start(env!("CARGO_PKG_VERSION"), format.is_json());
let outcome = run(cli, format).await;
update.finish().await;
outcome
})
} else {
run_offline(&cli.command, format)
};
if let Err(error) = outcome {
let status = exit::classify(&error);
if format.is_json() {
output::emit_error(&error, exit::hint(status));
std::process::exit(status.code());
}
eprintln!();
eprintln!(" {} {error}", theme::CROSS.style(theme::danger()));
for cause in error.chain().skip(1) {
eprintln!(" {}", cause.style(theme::muted()));
}
if let Some(hint) = exit::hint(status) {
eprintln!(" {}", hint.style(theme::muted()));
}
eprintln!();
std::process::exit(status.code());
}
}
fn asks_the_registry(command: &Command) -> bool {
!matches!(
command,
Command::Completions { .. } | Command::Schema | Command::Guide | Command::Skill { .. }
)
}
fn built() -> clap::Command {
let mut command = Cli::command();
command.build();
command
}
fn run_offline(command: &Command, format: Format) -> Result<()> {
match command {
Command::Completions { shell } => {
clap_complete::generate(*shell, &mut built(), "fundaia", &mut std::io::stdout());
Ok(())
}
Command::Schema => commands::schema::run(&built(), format),
Command::Guide => commands::guide::run(format),
Command::Skill { command } => match command {
None | Some(SkillCommand::Show) => commands::skill::show(&built(), format),
Some(SkillCommand::Install { project }) => {
commands::skill::install(&built(), *project, format)
}
Some(SkillCommand::Path { project }) => commands::skill::print_path(*project, format),
},
_ => unreachable!("only the commands that reach nothing are answered offline"),
}
}
async fn run(cli: Cli, format: Format) -> Result<()> {
match &cli.command {
Command::Login {
url,
as_profile,
no_browser,
} => return commands::login::run(url, as_profile, !no_browser).await,
Command::Logout { profile } => return commands::login::logout(profile.as_deref()),
Command::Upgrade { check } => {
return update::upgrade(env!("CARGO_PKG_VERSION"), *check).await
}
Command::Changelog {
release,
all,
since,
count,
history,
} => {
return if *history {
commands::changelog::show_history(format)
} else {
commands::changelog::show(
&commands::changelog::Query {
all: *all,
version: release.clone(),
since: since.clone(),
count: *count,
},
format,
)
}
}
Command::Completions { .. } | Command::Schema | Command::Guide | Command::Skill { .. } => {
return run_offline(&cli.command, format)
}
_ => {}
}
let client = connect(&cli)?.with_environment(cli.environment.as_deref());
match cli.command {
Command::Login { .. }
| Command::Logout { .. }
| Command::Upgrade { .. }
| Command::Changelog { .. }
| Command::Completions { .. }
| Command::Schema
| Command::Guide
| Command::Skill { .. } => {
unreachable!("handled above")
}
Command::Whoami => commands::login::whoami(&client, format).await,
Command::Status => commands::read::status(&client, format).await,
Command::Projects => commands::read::projects(&client, format).await,
Command::Project { command } => match command {
ProjectCommand::New {
name,
description,
workspace,
} => {
commands::write::create_project(
&client,
&name,
description.as_deref(),
workspace.as_deref(),
)
.await
}
ProjectCommand::Rename {
project,
name,
description,
} => {
commands::manage::rename_project(
&client,
&project,
name.as_deref(),
description.as_deref(),
)
.await
}
ProjectCommand::Delete { project, confirm } => {
commands::manage::delete_project(&client, &project, confirm.as_deref()).await
}
ProjectCommand::Move { project, workspace } => {
commands::manage::move_project(&client, &project, &workspace).await
}
ProjectCommand::Star { project } => {
commands::manage::favorite_project(&client, &project, true, format).await
}
ProjectCommand::Unstar { project } => {
commands::manage::favorite_project(&client, &project, false, format).await
}
},
Command::Envs { project } => commands::environments::list(&client, &project, format).await,
Command::Webhooks { project } => commands::webhooks::list(&client, &project, format).await,
Command::Webhook { command } => match command {
WebhookCommand::Add {
project,
url,
name,
format: webhook_format,
events,
} => {
commands::webhooks::add(
&client,
commands::webhooks::AddRequest {
project: &project,
url: &url,
name: name.as_deref(),
format: webhook_format.as_deref(),
events: &events,
},
format,
)
.await
}
WebhookCommand::Set {
project,
webhook,
name,
events,
pause,
resume,
} => {
commands::webhooks::set(
&client,
commands::webhooks::SetRequest {
project: &project,
webhook: &webhook,
name: name.as_deref(),
events: events.as_deref(),
enabled: if pause {
Some(false)
} else if resume {
Some(true)
} else {
None
},
},
format,
)
.await
}
WebhookCommand::Rm { project, webhook } => {
commands::webhooks::remove(&client, &project, &webhook, format).await
}
WebhookCommand::Test { project, webhook } => {
commands::webhooks::test(&client, &project, &webhook, format).await
}
},
Command::Env { command } => match command {
EnvCommand::New {
project,
name,
from,
empty,
no_deploy,
} => {
commands::environments::create(
&client,
commands::environments::EnvironmentRequest {
project: &project,
name: &name,
from: from.as_deref(),
empty,
no_deploy,
},
format,
)
.await
}
EnvCommand::Rename {
project,
environment,
name,
} => {
commands::environments::rename(&client, &project, &environment, &name, format).await
}
EnvCommand::Rm {
project,
environment,
confirm,
} => {
commands::environments::remove(
&client,
&project,
&environment,
confirm.as_deref(),
format,
)
.await
}
EnvCommand::Copy {
project,
service,
from,
to,
no_deploy,
} => {
commands::environments::copy(
&client,
commands::environments::CopyRequest {
project: &project,
service: &service,
from: from.as_deref(),
to: &to,
no_deploy,
},
format,
)
.await
}
},
Command::Services { project } => commands::read::services(&client, &project, format).await,
Command::Service { project, service } => {
commands::read::service(&client, &project, &service, format).await
}
Command::Canvas { command } => match command {
CanvasCommand::Show { project, map } => {
commands::read::canvas(&client, &project, map, format).await
}
CanvasCommand::Move {
project,
service,
x,
y,
} => commands::write::move_on_canvas(&client, &project, &service, x, y, format).await,
CanvasCommand::Arrange { project, layout } => {
commands::write::arrange_canvas(&client, &project, &layout, format).await
}
},
Command::Templates => commands::write::templates(&client, format).await,
Command::New(NewArgs {
project,
name,
repo,
image,
template,
upload,
branch,
connect_to,
no_deploy,
setup_token,
model,
effort,
}) => {
commands::write::create_service(
&client,
commands::write::ServiceRequest {
project: &project,
name: &name,
repo: repo.as_deref(),
image: image.as_deref(),
template: template.as_deref(),
upload,
branch: &branch,
connect_to: &connect_to,
no_deploy,
setup_token: setup_token.as_deref(),
model: model.as_deref(),
effort: effort.as_deref(),
},
format,
)
.await
}
Command::Agent {
project,
service,
model,
effort,
setup_token,
} => {
commands::write::agent(
&client,
&project,
&service,
api::models::AgentSetupPatch {
model,
effort,
setup_token,
},
format,
)
.await
}
Command::Rename {
project,
service,
name,
} => commands::manage::rename_service(&client, &project, &service, &name).await,
Command::Up {
project,
service,
path,
no_deploy,
detach,
} => {
commands::upload::run(
&client,
&project,
&service,
std::path::Path::new(&path),
!no_deploy,
!detach,
format,
)
.await
}
Command::Uploads { project, service } => {
commands::upload::list(&client, &project, &service, format).await
}
Command::Deploy {
project,
service,
restart,
detach,
} => commands::deploy::run(&client, &project, &service, restart, !detach, format).await,
Command::Redeploy {
project,
service,
number,
} => commands::deploy::redeploy(&client, &project, &service, number, format).await,
Command::Rollback {
project,
service,
number,
} => commands::deploy::rollback(&client, &project, &service, number, format).await,
Command::Cancel {
project,
service,
deployment,
} => commands::deploy::cancel(&client, &project, &service, deployment, format).await,
Command::Stop { project, service } => {
commands::write::stop(&client, &project, &service).await
}
Command::History {
project,
service,
limit,
commit,
} => {
commands::read::history(
&client,
&project,
&service,
limit,
commit.as_deref(),
format,
)
.await
}
Command::Commit {
project,
service,
commit,
file,
stat,
} => commands::read::commit(&client, &project, &service, commit, file, stat, format).await,
Command::Watch { project, service } => {
commands::watch::run(&client, project.as_deref(), service.as_deref(), format).await
}
Command::Logs(LogsArgs {
project,
service,
follow,
tail,
stream,
step,
deployment,
timestamps,
trace,
}) => {
commands::logs::run(
&client,
&project,
&service,
commands::logs::LogQuery {
follow,
tail,
stream_filter: stream.as_deref(),
stage: step.as_deref(),
number: deployment,
timestamps,
trace: trace.as_deref(),
},
format,
)
.await
}
Command::Incidents {
project,
service,
decided,
} => commands::read::incidents(&client, &project, &service, decided, format).await,
Command::Workflows { command } => match command {
WorkflowCommand::List { project, refresh } => {
commands::workflows::list(&client, &project, refresh, format).await
}
WorkflowCommand::Runs {
project,
path,
commit,
limit,
} => {
commands::workflows::runs(
&client,
&project,
path.as_deref(),
commit.as_deref(),
limit,
format,
)
.await
}
WorkflowCommand::Run {
project,
path,
branch,
inputs,
} => {
commands::workflows::run(
&client,
&project,
&path,
branch.as_deref(),
&inputs,
format,
)
.await
}
WorkflowCommand::Show { run_id } => {
commands::workflows::show(&client, &run_id, format).await
}
WorkflowCommand::Logs { job_id } => {
commands::workflows::logs(&client, &job_id, format).await
}
WorkflowCommand::Cancel { run_id } => {
commands::workflows::cancel(&client, &run_id, format).await
}
WorkflowCommand::Rerun { run_id } => {
commands::workflows::rerun(&client, &run_id, format).await
}
WorkflowCommand::Stats { project, limit } => {
commands::workflows::stats(&client, &project, limit, format).await
}
WorkflowCommand::Wait { run_id, timeout } => {
commands::workflows::wait(&client, &run_id, timeout, format).await
}
WorkflowCommand::Artifact { job_id, into } => {
commands::workflows::artifact(&client, &job_id, into.as_deref(), format).await
}
},
Command::Approvals { project, service } => {
commands::read::approvals(&client, &project, service.as_deref(), format).await
}
Command::Approve { id } => commands::write::approve_deployment(&client, &id, format).await,
Command::Reject { id } => commands::write::reject_deployment(&client, &id, format).await,
Command::Autofix(AutofixArgs {
project,
service,
mode,
agent,
model,
min_occurrences,
daily_limit,
}) => {
commands::read::autofix(
&client,
&project,
&service,
api::models::AutofixPatch {
mode,
agent_service_environment_id: agent,
model,
minimum_occurrences: min_occurrences,
daily_limit,
},
format,
)
.await
}
Command::Fix { id, command } => match (command, id) {
(
Some(FixCommand::Deployment {
project,
service,
number,
}),
_,
) => {
commands::repair::fix_deployment(&client, &project, &service, number, format).await
}
(None, Some(id)) => commands::write::fix_incident(&client, &id, format).await,
(None, None) => unreachable!("clap requires the id when there is no subcommand"),
},
Command::Attention => commands::insights::attention(&client, format).await,
Command::Health { project } => commands::insights::health(&client, &project, format).await,
Command::Diagnose { project, service } => {
commands::insights::diagnose(&client, &project, &service, format).await
}
Command::Grep(GrepArgs {
project,
text,
service,
level,
stream,
hours,
limit,
}) => {
commands::insights::grep(
&client,
&project,
commands::insights::GrepQuery {
text: &text,
service: service.as_deref(),
level: level.as_deref(),
stream: stream.as_deref(),
hours,
limit,
},
format,
)
.await
}
Command::Deps { project, service } => {
commands::insights::dependencies(&client, &project, &service, format).await
}
Command::Import {
file,
name,
workspace,
deploy,
} => {
commands::insights::import(
&client,
&file,
name.as_deref(),
workspace.as_deref(),
deploy,
format,
)
.await
}
Command::Export { project, file } => {
commands::insights::export(&client, &project, file.as_deref(), format).await
}
Command::Compare {
project,
service,
from,
to,
} => commands::insights::compare(&client, &project, &service, from, to, format).await,
Command::Probe {
project,
service,
path,
} => commands::insights::probe(&client, &project, &service, &path, format).await,
Command::Wake { project, service } => {
commands::insights::wake(&client, &project, &service, format).await
}
Command::Security { command } => match command {
SecurityCommand::Scan {
project,
service,
fix,
wait,
} => commands::repair::scan(&client, &project, &service, fix, wait, format).await,
SecurityCommand::Show { scan_id } => {
commands::repair::show(&client, &scan_id, format).await
}
SecurityCommand::List { project, service } => {
commands::repair::list(&client, &project, &service, format).await
}
},
Command::Issue { kind, id } => commands::repair::issue(&client, &kind, &id, format).await,
Command::Incident { id, status } => {
commands::write::decide_incident(&client, &id, &status, format).await
}
Command::SignIns { limit } => commands::read::sign_ins(&client, limit, format).await,
Command::Sessions { command } => match command {
None => commands::read::auth_sessions(&client, format).await,
Some(SessionCommand::Revoke { session_id }) => {
commands::login::revoke_session(&client, &session_id).await
}
},
Command::Activity { project, limit } => {
commands::read::activity(&client, &project, limit, format).await
}
Command::Metrics {
project,
service,
minutes,
} => commands::read::metrics(&client, &project, &service, minutes, format).await,
Command::Consumption {
project,
service,
range,
} => commands::read::consumption(&client, &project, &service, &range, format).await,
Command::AgentActivity {
project,
service,
range,
outcome,
} => {
commands::read::agent_activity(
&client,
&project,
&service,
&range,
outcome.as_deref(),
format,
)
.await
}
Command::Data { command } => match command {
DataCommand::Tables { project, service } => {
commands::data::tables(&client, &project, &service, format).await
}
DataCommand::Query {
project,
service,
sql,
limit,
} => commands::data::query(&client, &project, &service, &sql, limit, format).await,
DataCommand::Run {
project,
service,
sql,
preview,
confirm,
} => {
commands::data::run(
&client,
&project,
&service,
&sql,
preview,
confirm.as_deref(),
format,
)
.await
}
},
Command::Shared { command } => match command {
SharedCommand::List { project } => {
commands::shared::list(&client, &project, format).await
}
SharedCommand::Set {
project,
key,
value,
secret,
env_only,
} => {
commands::shared::set(
&client,
commands::shared::SetRequest {
project: &project,
key: &key,
value: &value,
secret,
environment_only: env_only,
},
format,
)
.await
}
SharedCommand::Unset {
project,
key,
env_only,
} => commands::shared::unset(&client, &project, &key, env_only, format).await,
},
Command::Variables { command } => match command {
VariableCommand::List {
project,
service,
reveal,
} => commands::read::variables(&client, &project, &service, reveal, format).await,
VariableCommand::Set {
project,
service,
key,
value,
secret,
sealed,
} => {
commands::write::set_variable(
&client, &project, &service, &key, &value, secret, sealed,
)
.await
}
VariableCommand::Seal {
project,
service,
key,
confirm,
} => {
commands::write::seal_variable(
&client,
&project,
&service,
&key,
confirm.as_deref(),
)
.await
}
VariableCommand::Unset {
project,
service,
key,
} => commands::write::unset_variable(&client, &project, &service, &key).await,
VariableCommand::Audit { project } => {
commands::insights::audit_variables(&client, &project, format).await
}
VariableCommand::Import {
project,
service,
file,
secret,
} => {
commands::write::import_variables(&client, &project, &service, &file, secret).await
}
},
Command::Access { command } => match command {
AccessCommand::Status { project, service } => {
commands::read::repository_access(&client, &project, &service, format).await
}
AccessCommand::Request {
project,
service,
email,
} => {
commands::write::request_repository_access(
&client,
&project,
&service,
email.as_deref(),
format,
)
.await
}
},
Command::Backups { command } => match command {
BackupCommand::List { project, service } => {
commands::read::backups(&client, &project, &service, format).await
}
BackupCommand::New { project, service } => {
commands::write::take_backup(&client, &project, &service).await
}
BackupCommand::Schedule {
project,
service,
schedules,
} => commands::write::schedule_backups(&client, &project, &service, &schedules).await,
BackupCommand::Restore { backup_id, confirm } => {
commands::write::restore_backup(&client, &backup_id, confirm.as_deref()).await
}
BackupCommand::Keep { backup_id } => {
commands::write::lock_backup(&client, &backup_id, true).await
}
BackupCommand::Unkeep { backup_id } => {
commands::write::lock_backup(&client, &backup_id, false).await
}
BackupCommand::Remove { backup_id } => {
commands::write::delete_backup(&client, &backup_id).await
}
},
Command::Domains { command } => match command {
DomainCommand::List { project, service } => {
commands::read::domains(&client, &project, &service, format).await
}
DomainCommand::Add {
project,
service,
host,
} => commands::write::add_domain(&client, &project, &service, host.as_deref()).await,
DomainCommand::Verify { domain_id } => {
commands::read::verify_domain(&client, &domain_id, format).await
}
DomainCommand::Remove { domain_id } => {
commands::write::remove_domain(&client, &domain_id).await
}
},
Command::Connect {
project,
service,
provider,
} => commands::write::connect(&client, &project, &service, &provider).await,
Command::Disconnect {
project,
service,
provider,
} => commands::manage::disconnect(&client, &project, &service, &provider).await,
Command::Connectable { project, service } => {
commands::write::candidates(&client, &project, &service, format).await
}
Command::Volumes { command } => match command {
VolumeCommand::List { project, service } => {
commands::read::volumes(&client, &project, &service, format).await
}
VolumeCommand::Add {
project,
service,
mount_path,
} => commands::write::attach_volume(&client, &project, &service, &mount_path).await,
VolumeCommand::Resize { volume_id, gb } => {
commands::write::resize_volume(&client, &volume_id, gb).await
}
VolumeCommand::Remove { volume_id } => {
commands::manage::detach_volume(&client, &volume_id).await
}
},
Command::Recipe { command } => match command {
RecipeCommand::Show {
project,
service,
plain,
} => commands::read::recipe(&client, &project, &service, plain, format).await,
RecipeCommand::Edit {
project,
service,
file,
} => commands::write::edit_recipe(&client, &project, &service, &file).await,
},
Command::Config { command } => match command {
ConfigCommand::Show {
project,
service,
plain,
} => commands::read::service_config(&client, &project, &service, plain, format).await,
ConfigCommand::Edit {
project,
service,
file,
} => commands::write::edit_service_config(&client, &project, &service, &file).await,
},
Command::Capabilities { project, service } => {
commands::read::capabilities(&client, &project, &service, format).await
}
Command::Capacity => commands::manage::capacity(&client, format).await,
Command::Settings(SettingsArgs {
project,
service,
repo,
branch,
memory,
replicas,
root,
start_command,
deploy_tags,
pre_deploy,
health_path,
health_probe,
health_timeout_seconds,
health_interval_seconds,
health_grace_seconds,
health_expected_status,
restart_policy,
restart_max_retries,
sleep,
sleep_after_minutes,
auto_deploy,
image_auto_update,
cache_enabled,
cache_ttl_seconds,
}) => {
commands::manage::set_settings(
&client,
&project,
&service,
api::models::SettingsPatch {
repo_full_name: repo,
branch,
memory_mb: memory,
replicas,
root_directory: root,
start_command,
pre_deploy_command: pre_deploy,
deploy_tag_pattern: deploy_tags,
health_path,
health_probe,
health_timeout_seconds,
health_interval_seconds,
health_grace_seconds,
health_expected_status,
restart_policy,
restart_max_retries,
sleep_enabled: sleep,
sleep_after_minutes,
auto_deploy,
image_auto_update,
cache_enabled,
cache_ttl_seconds,
},
)
.await
}
Command::Expose {
project,
service,
off,
} => commands::manage::expose(&client, &project, &service, !off).await,
Command::Previews { project, switch } => {
commands::previews::run(
&client,
&project,
switch.as_deref().map(|word| word == "on"),
format,
)
.await
}
Command::Pull { project, service } => {
commands::console::pull(&client, &project, &service, format).await
}
Command::Tasks { command } => match command {
TasksCommand::List { project, service } => {
commands::tasks::list(&client, &project, &service, format).await
}
TasksCommand::Add {
project,
service,
name,
schedule,
command,
paused,
} => {
commands::tasks::add(
&client,
commands::tasks::AddRequest {
project: &project,
service: &service,
name: &name,
schedule: &schedule,
command: &command,
paused,
},
format,
)
.await
}
TasksCommand::Edit {
project,
service,
task,
name,
schedule,
command,
pause,
resume,
} => {
commands::tasks::edit(
&client,
commands::tasks::EditRequest {
project: &project,
service: &service,
task: &task,
name: name.as_deref(),
schedule: schedule.as_deref(),
command: &command,
enabled: if pause {
Some(false)
} else if resume {
Some(true)
} else {
None
},
},
format,
)
.await
}
TasksCommand::Rm {
project,
service,
task,
} => commands::tasks::remove(&client, &project, &service, &task, format).await,
TasksCommand::Run {
project,
service,
task,
no_wait,
} => commands::tasks::run(&client, &project, &service, &task, !no_wait, format).await,
TasksCommand::Runs {
project,
service,
task,
} => commands::tasks::runs(&client, &project, &service, &task, format).await,
},
Command::Files { command } => match command {
FilesCommand::Ls {
project,
service,
path,
} => commands::files::list(&client, &project, &service, path.as_deref(), format).await,
FilesCommand::Cat {
project,
service,
path,
} => commands::files::cat(&client, &project, &service, &path, format).await,
FilesCommand::Get {
project,
service,
path,
output,
} => {
commands::files::get(
&client,
&project,
&service,
&path,
output.as_deref(),
format,
)
.await
}
},
Command::Exec {
project,
service,
command,
} => commands::console::exec(&client, &project, &service, &command, format).await,
Command::Maintenance {
project,
service,
off,
message,
} => {
commands::edge::maintenance(
&client,
&project,
&service,
!off,
message.as_deref(),
format,
)
.await
}
Command::Protect {
project,
service,
user,
password,
} => {
commands::edge::protect(
&client,
&project,
&service,
&user,
password.as_deref(),
format,
)
.await
}
Command::Unprotect { project, service } => {
commands::edge::unprotect(&client, &project, &service, format).await
}
Command::Encrypt {
project,
service,
off,
} => commands::manage::encryption(&client, &project, &service, !off).await,
Command::Delete {
project,
service,
confirm,
} => commands::manage::delete(&client, &project, &service, confirm.as_deref()).await,
Command::Workspace { command } => match command {
WorkspaceCommand::List => commands::workspace::list(&client, format).await,
WorkspaceCommand::SecondFactor { switch, workspace } => {
commands::workspace::require_second_factor(
&client,
workspace.as_deref(),
switch == "on",
format,
)
.await
}
WorkspaceCommand::Members { workspace } => {
commands::workspace::members(&client, workspace.as_deref(), format).await
}
WorkspaceCommand::Credits { workspace } => {
commands::workspace::credits(&client, workspace.as_deref(), format).await
}
WorkspaceCommand::Invite {
email,
workspace,
admin,
} => commands::workspace::invite(&client, workspace.as_deref(), &email, admin).await,
WorkspaceCommand::Revoke {
invitation_id,
workspace,
} => commands::workspace::revoke(&client, workspace.as_deref(), &invitation_id).await,
WorkspaceCommand::Remove { user_id, workspace } => {
commands::workspace::remove(&client, workspace.as_deref(), &user_id).await
}
},
}
}
fn connect(cli: &Cli) -> Result<ApiClient> {
if let (Some(url), Some(token)) = (&cli.url, &cli.token) {
return ApiClient::new(url, Some(token.clone()));
}
let config = Config::load().context("could not read the configuration")?;
let (name, profile) = config.profile(cli.profile.as_deref())?;
let url = cli.url.as_deref().unwrap_or(&profile.url);
if cli.token.is_none() && !config::same_host(url, &profile.url) {
return Err(exit::Refused::invalid(format!(
"--url points at {} but the profile \"{name}\" was saved for {}: pass --token (or FUNDAIA_TOKEN) as well, or log in to that instance as its own profile",
config::host_of(url),
config::host_of(&profile.url)
))
.into());
}
ApiClient::new(
url,
Some(cli.token.clone().unwrap_or_else(|| profile.token.clone())),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_should_ask_the_registry_before_a_command_that_talks_to_an_instance() {
assert!(asks_the_registry(&Command::Status));
}
#[test]
fn it_should_not_ask_the_registry_to_write_a_completion_script() {
assert!(!asks_the_registry(&Command::Completions {
shell: clap_complete::Shell::Bash,
}));
}
#[test]
fn it_should_not_ask_the_registry_to_describe_itself() {
assert!(!asks_the_registry(&Command::Schema));
}
#[test]
fn it_should_read_fix_with_one_id_as_an_incident() {
let cli = Cli::try_parse_from(["fundaia", "fix", "inc_1"]).unwrap();
assert!(matches!(
cli.command,
Command::Fix { id: Some(id), command: None } if id == "inc_1"
));
}
#[test]
fn it_should_read_fix_deployment_as_a_deployment() {
let cli =
Cli::try_parse_from(["fundaia", "fix", "deployment", "tienda", "api", "42"]).unwrap();
assert!(matches!(
cli.command,
Command::Fix {
command: Some(FixCommand::Deployment {
number: Some(42),
..
}),
..
}
));
}
#[test]
fn it_should_refuse_fix_with_nothing_to_fix() {
assert!(Cli::try_parse_from(["fundaia", "fix"]).is_err());
}
#[test]
fn it_should_still_read_logs_with_a_project_and_a_service() {
assert!(Cli::try_parse_from(["fundaia", "logs", "tienda", "api", "-n", "50"]).is_ok());
}
#[test]
fn it_should_read_the_environment_to_work_in_before_any_command() {
let cli =
Cli::try_parse_from(["fundaia", "--env", "staging", "services", "tienda"]).unwrap();
assert_eq!(cli.environment.as_deref(), Some("staging"));
}
#[test]
fn it_should_read_the_environment_to_work_in_after_the_command_too() {
let cli = Cli::try_parse_from(["fundaia", "deploy", "tienda", "api", "--env", "staging"])
.unwrap();
assert_eq!(cli.environment.as_deref(), Some("staging"));
}
#[test]
fn it_should_refuse_an_environment_both_copied_and_empty() {
assert!(Cli::try_parse_from([
"fundaia", "env", "new", "tienda", "staging", "--from", "qa", "--empty"
])
.is_err());
}
#[test]
fn it_should_read_where_a_service_is_copied_to() {
let cli =
Cli::try_parse_from(["fundaia", "env", "copy", "tienda", "api", "--to", "staging"])
.unwrap();
assert!(matches!(
cli.command,
Command::Env { command: EnvCommand::Copy { to, .. } } if to == "staging"
));
}
#[test]
fn it_should_not_ask_the_registry_to_hand_over_the_guide() {
assert!(!asks_the_registry(&Command::Guide));
}
}