use std::path::PathBuf;
use clap::{Args, Parser, Subcommand, ValueEnum};
pub use crate::cli_agent::{AuthCommand, AuthLoginArgs, SkillArgs, SkillCommand, SkillTarget};
pub use crate::completion::CompletionShell;
#[derive(Debug, Parser)]
#[command(
name = "koban",
version,
about = "Invoice Ninja from the terminal",
long_about = "koban is an Invoice Ninja CLI for humans and AI agents.",
arg_required_else_help = true,
propagate_version = true,
term_width = 100,
after_help = "\
Examples:
koban statics --output json
koban clients list --page 1 --per-page 20
koban products create --name Consulting --price 100 --dry-run
koban invoices update <invoice_id> --data-file invoice.json --dry-run
koban search run --field query=acme --dry-run
koban update --check
Environment:
INVOICE_NINJA_API_TOKEN Required API token
INVOICE_NINJA_BASE_URL Optional API base URL, defaults to https://invoicing.co"
)]
pub struct Cli {
#[arg(long, value_enum, default_value_t = OutputFormat::Table, global = true)]
pub output: OutputFormat,
#[command(subcommand)]
pub command: Option<Commands>,
}
#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
pub enum OutputFormat {
Table,
Json,
}
#[derive(Debug, Subcommand)]
pub enum Commands {
#[command(after_help = "\
Examples:
koban statics
koban statics --output json")]
Statics,
#[command(subcommand)]
Clients(ResourceCommand),
#[command(subcommand)]
Invoices(Box<InvoiceCommand>),
#[command(subcommand)]
Payments(ResourceCommand),
#[command(subcommand)]
Quotes(ResourceCommand),
#[command(subcommand)]
Credits(ResourceCommand),
#[command(subcommand)]
Vendors(ResourceCommand),
#[command(subcommand)]
Expenses(ResourceCommand),
#[command(subcommand)]
Projects(ResourceCommand),
#[command(subcommand)]
Tasks(ResourceCommand),
#[command(subcommand)]
Locations(ResourceCommand),
#[command(subcommand)]
Products(ResourceCommand),
#[command(name = "recurring-invoices", subcommand)]
RecurringInvoices(ResourceCommand),
#[command(name = "purchase-orders", subcommand)]
PurchaseOrders(ResourceCommand),
#[command(name = "recurring-expenses", subcommand)]
RecurringExpenses(ResourceCommand),
#[command(name = "recurring-quotes", subcommand)]
RecurringQuotes(ResourceCommand),
#[command(name = "bank-transactions", subcommand)]
BankTransactions(ResourceCommand),
#[command(name = "bank-integrations", subcommand)]
BankIntegrations(ResourceCommand),
#[command(name = "bank-transaction-rules", subcommand)]
BankTransactionRules(ResourceCommand),
#[command(name = "group-settings", subcommand)]
GroupSettings(ResourceCommand),
#[command(name = "expense-categories", subcommand)]
ExpenseCategories(ResourceCommand),
#[command(name = "tax-rates", subcommand)]
TaxRates(ResourceCommand),
#[command(name = "payment-terms", subcommand)]
PaymentTerms(ResourceCommand),
#[command(name = "task-schedulers", subcommand)]
TaskSchedulers(ResourceCommand),
#[command(name = "task-statuses", subcommand)]
TaskStatuses(ResourceCommand),
#[command(subcommand)]
Activities(InspectResourceCommand),
#[command(name = "system-logs", subcommand)]
SystemLogs(InspectResourceCommand),
#[command(subcommand)]
Documents(ResourceCommand),
#[command(subcommand)]
Designs(ResourceCommand),
#[command(subcommand)]
Templates(ResourceCommand),
#[command(subcommand)]
Users(ResourceCommand),
#[command(subcommand)]
Companies(ResourceCommand),
#[command(name = "company-gateways", subcommand)]
CompanyGateways(ResourceCommand),
#[command(name = "company-ledger", subcommand)]
CompanyLedger(InspectResourceCommand),
#[command(name = "company-users", subcommand)]
CompanyUsers(ResourceCommand),
#[command(subcommand)]
Tokens(ResourceCommand),
#[command(subcommand)]
Webhooks(ResourceCommand),
#[command(subcommand)]
Subscriptions(ResourceCommand),
#[command(name = "client-gateway-tokens", subcommand)]
ClientGatewayTokens(ResourceCommand),
#[command(subcommand)]
Reports(EndpointCommand),
#[command(subcommand)]
Charts(EndpointCommand),
#[command(subcommand)]
Search(EndpointCommand),
#[command(subcommand)]
Utility(EndpointCommand),
#[command(subcommand)]
Auth(AuthCommand),
#[command(subcommand)]
Skill(SkillCommand),
#[command(after_long_help = "\
Upgrade koban in place when installed from a release tarball. For other install
sources the command prints the right upgrade recipe and exits:
Nix: nix profile upgrade koban (or flake update)
cargo: cargo install --git https://github.com/jamesbrink/koban --tag vX.Y.Z --force koban
Homebrew: brew upgrade koban
The latest tag is resolved by following the /releases/latest redirect, so this
command does not hit api.github.com and avoids anonymous API rate limits. Use
--nightly to install the rolling nightly build produced from main.")]
Update {
#[arg(long)]
check: bool,
#[arg(long)]
force: bool,
#[arg(long, value_name = "TAG")]
tag: Option<String>,
#[arg(long, conflicts_with = "tag")]
nightly: bool,
},
#[command(after_long_help = "\
Setup examples:
zsh:
source <(koban completions zsh)
bash:
source <(koban completions bash)
fish:
koban completions fish | source
nushell:
koban completions nushell | save ~/.config/nushell/completions/koban.nu")]
Completions {
#[arg(value_enum)]
shell: CompletionShell,
},
}
#[derive(Debug, Subcommand)]
pub enum ResourceCommand {
#[command(after_help = "\
Examples:
koban clients list --page 1 --per-page 20
koban invoices list --include client --output json")]
List(ListArgs),
#[command(after_help = "\
Examples:
koban clients show k9avmeG1P0 --output json
koban payments show k9avmeG1P0")]
Show(ShowArgs),
#[command(
alias = "blank",
alias = "new-template",
after_help = "\
Examples:
koban clients template --output json
koban invoices template --include client --output json"
)]
Template(TemplateArgs),
#[command(
name = "edit-template",
alias = "edit-form",
after_help = "\
Examples:
koban clients edit-template k9avmeG1P0 --output json
koban payments edit-template k9avmeG1P0"
)]
EditTemplate(ShowArgs),
Create(ResourceWriteArgs),
Update(UpdateResourceArgs),
Delete(ConfirmableIdArgs),
Bulk(BulkArgs),
Upload(UploadArgs),
Action(ResourceActionArgs),
Download(DownloadArgs),
}
#[derive(Debug, Subcommand)]
pub enum InspectResourceCommand {
List(ListArgs),
Show(ShowArgs),
}
#[derive(Debug, Subcommand)]
pub enum EndpointCommand {
Run(EndpointArgs),
}
#[derive(Debug, Subcommand)]
pub enum InvoiceCommand {
#[command(after_help = "\
Examples:
koban invoices list --page 1 --per-page 20
koban invoices list --filter status_id=gt:1 --sort 'date|desc' --output json")]
List(ListArgs),
#[command(after_help = "\
Examples:
koban invoices show k9avmeG1P0 --output json
koban invoices show k9avmeG1P0 --include client")]
Show(ShowArgs),
#[command(
alias = "blank",
alias = "new-template",
after_help = "\
Examples:
koban invoices template --output json
koban invoices template --include client --output json"
)]
Template(TemplateArgs),
#[command(
name = "edit-template",
alias = "edit-form",
after_help = "\
Examples:
koban invoices edit-template k9avmeG1P0 --output json
koban invoices edit-template k9avmeG1P0 --include client"
)]
EditTemplate(ShowArgs),
#[command(after_help = "\
Examples:
koban invoices create --client-id k9avmeG1P0 --line-item product_key=Consulting,quantity=1,cost=100 --dry-run
koban invoices create --data-file invoice.json --include client
printf '%s' '{\"client_id\":\"k9avmeG1P0\",\"line_items\":[]}' | koban invoices create --stdin --dry-run")]
Create(InvoiceWriteArgs),
#[command(after_help = "\
Examples:
koban invoices update k9avmeG1P0 --data-file invoice.json --dry-run
koban invoices update k9avmeG1P0 --public-notes 'Thanks again' --mark-sent --yes")]
Update(UpdateInvoiceArgs),
#[command(after_help = "\
Examples:
koban invoices delete k9avmeG1P0 --dry-run
koban invoices delete k9avmeG1P0 --yes")]
Delete(ConfirmableIdArgs),
#[command(after_help = "\
Examples:
koban invoices bulk --action archive --id inv_1 --id inv_2 --dry-run
koban invoices bulk --action email --email-type invoice --id inv_1 --yes")]
Bulk(BulkArgs),
#[command(after_help = "\
Examples:
koban invoices upload k9avmeG1P0 --file contract.pdf --dry-run
koban invoices upload k9avmeG1P0 --file contract.pdf --yes")]
Upload(UploadArgs),
#[command(after_help = "\
Examples:
koban invoices action k9avmeG1P0 --action mark_paid --dry-run
koban invoices action k9avmeG1P0 --action email --yes")]
Action(InvoiceActionArgs),
#[command(after_help = "\
Examples:
koban invoices download invitation_key --output-file invoice.pdf
koban invoices download invitation_key --output-file invoice.pdf --force")]
Download(DownloadArgs),
#[command(
name = "delivery-note",
after_help = "\
Examples:
koban invoices delivery-note k9avmeG1P0 --output-file delivery-note.pdf
koban invoices delivery-note k9avmeG1P0 --output-file delivery-note.pdf --force"
)]
DeliveryNote(DownloadArgs),
}
#[derive(Debug, Args)]
pub struct ListArgs {
#[arg(long, default_value_t = 1, value_parser = clap::value_parser!(u32).range(1..))]
pub page: u32,
#[arg(long, default_value_t = 20, value_parser = clap::value_parser!(u32).range(1..=100))]
pub per_page: u32,
#[arg(long, value_name = "name[,name]", value_delimiter = ',', action = clap::ArgAction::Append)]
pub include: Vec<String>,
#[arg(long = "filter", value_name = "key=value", action = clap::ArgAction::Append)]
pub filters: Vec<String>,
#[arg(long, value_name = "field|asc")]
pub sort: Option<String>,
#[arg(long)]
pub all: bool,
#[arg(long, value_parser = clap::value_parser!(u32).range(1..))]
pub limit: Option<u32>,
}
#[derive(Debug, Args)]
pub struct ShowArgs {
pub id: String,
#[arg(long, value_name = "name[,name]", value_delimiter = ',', action = clap::ArgAction::Append)]
pub include: Vec<String>,
}
#[derive(Debug, Args)]
pub struct TemplateArgs {
#[arg(long, value_name = "name[,name]", value_delimiter = ',', action = clap::ArgAction::Append)]
pub include: Vec<String>,
}
#[derive(Debug, Args)]
pub struct ResourceWriteArgs {
#[command(flatten)]
pub payload: ResourcePayloadArgs,
#[command(flatten)]
pub safety: WriteSafetyArgs,
#[arg(long, value_name = "name[,name]", value_delimiter = ',', action = clap::ArgAction::Append)]
pub include: Vec<String>,
}
#[derive(Debug, Args)]
pub struct UpdateResourceArgs {
pub id: String,
#[command(flatten)]
pub payload: ResourcePayloadArgs,
#[command(flatten)]
pub safety: WriteSafetyArgs,
#[arg(long, value_name = "name[,name]", value_delimiter = ',', action = clap::ArgAction::Append)]
pub include: Vec<String>,
}
#[derive(Debug, Args)]
pub struct ResourcePayloadArgs {
#[arg(long, value_name = "JSON", conflicts_with_all = ["data_file", "stdin"])]
pub data: Option<String>,
#[arg(long = "data-file", value_name = "PATH", conflicts_with_all = ["data", "stdin"])]
pub data_file: Option<PathBuf>,
#[arg(long, conflicts_with_all = ["data", "data_file"])]
pub stdin: bool,
#[arg(long = "field", value_name = "key=value", action = clap::ArgAction::Append)]
pub fields: Vec<String>,
#[arg(long)]
pub name: Option<String>,
#[arg(long)]
pub number: Option<String>,
#[arg(long)]
pub client_id: Option<String>,
#[arg(long)]
pub vendor_id: Option<String>,
#[arg(long)]
pub project_id: Option<String>,
#[arg(long)]
pub date: Option<String>,
#[arg(long)]
pub due_date: Option<String>,
#[arg(long)]
pub amount: Option<String>,
#[arg(long)]
pub price: Option<String>,
#[arg(long)]
pub quantity: Option<String>,
#[arg(long)]
pub public_notes: Option<String>,
#[arg(long)]
pub private_notes: Option<String>,
#[arg(long = "line-item", value_name = "key=value,...", action = clap::ArgAction::Append)]
pub line_items: Vec<String>,
}
#[derive(Debug, Args)]
pub struct ResourceActionArgs {
pub id: String,
#[arg(long)]
pub action: String,
#[command(flatten)]
pub payload: ResourcePayloadArgs,
#[command(flatten)]
pub safety: WriteSafetyArgs,
#[arg(long, value_name = "name[,name]", value_delimiter = ',', action = clap::ArgAction::Append)]
pub include: Vec<String>,
}
#[derive(Debug, Args)]
pub struct EndpointArgs {
#[arg(long)]
pub endpoint: Option<String>,
#[arg(long, value_enum)]
pub method: Option<HttpMethod>,
#[command(flatten)]
pub payload: ResourcePayloadArgs,
#[command(flatten)]
pub safety: WriteSafetyArgs,
#[arg(long, value_name = "name[,name]", value_delimiter = ',', action = clap::ArgAction::Append)]
pub include: Vec<String>,
}
#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
pub enum HttpMethod {
Get,
Post,
Put,
Delete,
}
impl HttpMethod {
pub(crate) fn label(self) -> &'static str {
match self {
Self::Get => "GET",
Self::Post => "POST",
Self::Put => "PUT",
Self::Delete => "DELETE",
}
}
}
#[derive(Debug, Args)]
pub struct DownloadArgs {
pub id: String,
#[arg(long = "output-file", short = 'o', value_name = "PATH")]
pub output_file: PathBuf,
#[arg(long)]
pub force: bool,
#[arg(long, value_name = "name[,name]", value_delimiter = ',', action = clap::ArgAction::Append)]
pub include: Vec<String>,
}
#[derive(Debug, Args)]
pub struct InvoiceWriteArgs {
#[command(flatten)]
pub payload: InvoicePayloadArgs,
#[command(flatten)]
pub triggers: InvoiceTriggerArgs,
#[command(flatten)]
pub safety: WriteSafetyArgs,
#[arg(long, value_name = "name[,name]", value_delimiter = ',', action = clap::ArgAction::Append)]
pub include: Vec<String>,
}
#[derive(Debug, Args)]
pub struct UpdateInvoiceArgs {
pub id: String,
#[command(flatten)]
pub payload: InvoicePayloadArgs,
#[command(flatten)]
pub triggers: InvoiceTriggerArgs,
#[command(flatten)]
pub safety: WriteSafetyArgs,
#[arg(long, value_name = "name[,name]", value_delimiter = ',', action = clap::ArgAction::Append)]
pub include: Vec<String>,
}
#[derive(Debug, Args)]
pub struct InvoicePayloadArgs {
#[arg(long, value_name = "JSON", conflicts_with_all = ["data_file", "stdin"])]
pub data: Option<String>,
#[arg(long = "data-file", value_name = "PATH", conflicts_with_all = ["data", "stdin"])]
pub data_file: Option<PathBuf>,
#[arg(long, conflicts_with_all = ["data", "data_file"])]
pub stdin: bool,
#[arg(long)]
pub client_id: Option<String>,
#[arg(long)]
pub date: Option<String>,
#[arg(long)]
pub due_date: Option<String>,
#[arg(long)]
pub number: Option<String>,
#[arg(long)]
pub po_number: Option<String>,
#[arg(long)]
pub public_notes: Option<String>,
#[arg(long)]
pub private_notes: Option<String>,
#[arg(long)]
pub terms: Option<String>,
#[arg(long)]
pub footer: Option<String>,
#[arg(long)]
pub project_id: Option<String>,
#[arg(long = "line-item", value_name = "key=value,...", action = clap::ArgAction::Append)]
pub line_items: Vec<String>,
}
#[derive(Debug, Args)]
pub struct InvoiceTriggerArgs {
#[arg(long)]
pub send_email: bool,
#[arg(long)]
pub mark_sent: bool,
#[arg(long)]
pub paid: bool,
#[arg(long)]
pub amount_paid: Option<String>,
#[arg(long)]
pub cancel: bool,
#[arg(long)]
pub save_default_footer: bool,
#[arg(long)]
pub save_default_terms: bool,
#[arg(long)]
pub retry_e_send: bool,
}
#[derive(Debug, Args)]
pub struct WriteSafetyArgs {
#[arg(long)]
pub dry_run: bool,
#[arg(long)]
pub yes: bool,
}
#[derive(Debug, Args)]
pub struct ConfirmableIdArgs {
pub id: String,
#[command(flatten)]
pub safety: WriteSafetyArgs,
#[arg(long, value_name = "name[,name]", value_delimiter = ',', action = clap::ArgAction::Append)]
pub include: Vec<String>,
}
#[derive(Debug, Args)]
pub struct BulkArgs {
#[arg(long)]
pub action: String,
#[arg(long = "id", value_name = "ID", action = clap::ArgAction::Append, required = true)]
pub ids: Vec<String>,
#[arg(long)]
pub email_type: Option<String>,
#[command(flatten)]
pub safety: WriteSafetyArgs,
#[arg(long, value_name = "name[,name]", value_delimiter = ',', action = clap::ArgAction::Append)]
pub include: Vec<String>,
}
#[derive(Debug, Args)]
pub struct UploadArgs {
pub id: String,
#[arg(long = "file", value_name = "PATH", action = clap::ArgAction::Append, required = true)]
pub files: Vec<PathBuf>,
#[command(flatten)]
pub safety: WriteSafetyArgs,
#[arg(long, value_name = "name[,name]", value_delimiter = ',', action = clap::ArgAction::Append)]
pub include: Vec<String>,
}
#[derive(Debug, Args)]
pub struct InvoiceActionArgs {
pub id: String,
#[arg(long)]
pub action: String,
#[command(flatten)]
pub safety: WriteSafetyArgs,
#[arg(long, value_name = "name[,name]", value_delimiter = ',', action = clap::ArgAction::Append)]
pub include: Vec<String>,
}