App-Ordo

 view release on metacpan or  search on metacpan

README.md  view on Meta::CPAN

# App-Ordo

**Official Command Line Interface for Ordo — Hierarchical Job Scheduler**

A terminal client for managing jobs, clusters, calendars, and servers in Ordo.

## Purpose

Ordo is a modern job scheduler designed for complex, hierarchical workloads (DAGs of jobs and clusters, Quartz-style calendars, real-time monitoring, multi-server execution).

The `ordo` CLI gives you full control from the terminal — with both one-shot commands and a rich interactive shell.

## Features

- Interactive shell with command history
- Hierarchical navigation (`ls`, `cd`)
- Full CRUD for jobs, clusters, calendars, and worker servers
- Real-time status, logs, and zombie detection (`sync`)
- Beautiful colored tables and output
- Built-in pager for long results
- Scriptable for automation

## Installation

### From CPAN (recommended)

```

lib/App/Ordo/Command/Cal/Attach.pm  view on Meta::CPAN

package App::Ordo::Command::Cal::Attach;
use Moo;
use Term::ANSIColor qw(colored);
use feature qw(say);
extends 'App::Ordo::Command::Base';

sub name    { "cal attach" }
sub summary { "Attach a calendar to a cluster" }
sub usage   { "<calendar> <cluster-path>" }

sub option_spec { {} }

sub execute {
    my ($self, $opt, $cal_name, $cluster_path) = @_;

    unless ($cal_name && $cluster_path) {
        say colored(["bold red"], "Usage: cal attach <calendar> <cluster-path>");
        return;
    }

    my $res = $self->api->call('update_cluster', {
        name   => $cluster_path,
        cal    => $cal_name,        # server accepts 'cal' => name
    });

    if ($res->{success}) {
        say colored(["bold green"], "Calendar '$cal_name' attached to cluster '$cluster_path'");

lib/App/Ordo/Command/Cal/Create.pm  view on Meta::CPAN

package App::Ordo::Command::Cal::Create;
use Moo;
use feature qw(say);
extends 'App::Ordo::Command::Base';

use Term::ANSIColor qw(colored);

sub name    { "cal create" }
sub summary { "Create a new calendar" }
sub usage   { "<name> [--desc \"description\"]" }

sub option_spec {
    return {
        'description=s' => 'Description for the calendar',
        # No positional options in spec — name is taken from @args
    };
}

sub execute {
    my ($self, $opt, $name) = @_;

    unless ($name) {
        say colored(["bold red"], "Error: calendar name is required");
        $self->show_help;
        return;
    }

    my $res = $self->api->call('create_cal', {
        name        => $name,
        tz          => $self->api->tz,
        description => $opt->{description},
    });

    if ($res->{success}) {
        say colored(["bold green"], "Calendar '$name' created successfully");
    } else {
        say colored(["bold red"], "Failed to create calendar: " . ($res->{message} || 'unknown error'));
    }
}

1;

lib/App/Ordo/Command/Cal/Cron/Add.pm  view on Meta::CPAN

package App::Ordo::Command::Cal::Cron::Add;
use Moo;
use feature qw(say);
extends 'App::Ordo::Command::Base';
use Term::ANSIColor qw(colored);

sub name    { "cal cron add" }
sub summary { "Add a cron expression to a calendar" }
sub usage   { "<calendar> \"<cron expression\" [--description \"text\"]" }

sub option_spec {
    return {
        'description=s' => 'Description for this cron',
    };
}

sub execute {
    my ($self, $opt, $cal_name, $cron_expr) = @_;

    unless ($cal_name && $cron_expr) {
        say colored(["bold red"], "Usage: cal cron add <calendar> \"<cron>\" [--description \"...\"]");
        return;
    }

    my $res = $self->api->call('create_cron', {
        cal         => $cal_name,
        name        => $cron_expr,
        description => $opt->{description},
    });

    if ($res->{success}) {
        say colored(["bold green"], "Cron '$cron_expr' added to calendar '$cal_name'");
    } else {
        say colored(["bold red"], "Failed: " . ($res->{message} || 'calendar not found'));
    }
}

1;

lib/App/Ordo/Command/Cal/Cron/Delete.pm  view on Meta::CPAN

package App::Ordo::Command::Cal::Cron::Delete;
use Moo;
use feature qw(say);
extends 'App::Ordo::Command::Base';

use Term::ANSIColor qw(colored);

sub name    { "cal cron delete" }
sub summary { "Delete a cron expression from a calendar" }
sub usage   { "<cron-id>" }
sub aliases { ['cal cron rm'] }

sub option_spec {
   return {};
}

sub execute {
   my ( $self, $opt, $cron_id ) = @_;

lib/App/Ordo/Command/Cal/Cron/Delete.pm  view on Meta::CPAN


      #        cal => $cal_name,
      id => $cron_id,
   };
   $payload->{force} = 1 if $opt->{force};

   my $res = $self->api->call( 'delete_cron', $payload );

   if ( $res->{success} ) {
      my $name    = $res->{cal};
      my $message = "Cron ID $cron_id removed from calendar";
      $message .= " $res->{cal}" if $res->{cal};
      say colored( ["bold green"], $message );
   }
   else {
      say colored( ["bold red"], "Failed to delete cron: " . ( $res->{message} || 'unknown error' ) );
   }
}

1;

lib/App/Ordo/Command/Cal/Delete.pm  view on Meta::CPAN

package App::Ordo::Command::Cal::Delete;
use Moo;
use feature qw(say);
extends 'App::Ordo::Command::Base';

use Term::ANSIColor qw(colored);

sub name    { "cal delete" }
sub summary { "Delete a calendar and all its cron expressions" }
sub usage   { "<calendar-name>" }
sub aliases { ['cal rm'] }

sub option_spec {
    return {
        'force' => 'Force delete even if attached to clusters',
    };
}

sub execute {
    my ($self, $opt, $cal_name) = @_;

    unless ($cal_name) {
        say colored(["bold red"], "Usage: cal delete <calendar-name> [--force]");
        say "Use --force if the calendar is attached to clusters";
        return;
    }

    my $payload = { name => $cal_name };
    $payload->{force} = 1 if $opt->{force};

    my $res = $self->api->call('delete_cal', $payload);

    if ($res->{success}) {
        say colored(["bold green"], "Calendar '$cal_name' deleted");
        say "All associated cron expressions removed (cascade delete)";
    } else {
        say colored(["bold red"], "Failed to delete calendar: " . ($res->{message} || 'unknown error'));
    }
}

1;

lib/App/Ordo/Command/Cal/Detach.pm  view on Meta::CPAN

package App::Ordo::Command::Cal::Detach;
use Moo;
use Term::ANSIColor qw(colored);
use feature qw(say);
extends 'App::Ordo::Command::Base';

sub name    { "cal detach" }
sub summary { "Detach a calendar from a cluster" }
sub usage   { "<calendar> <cluster-path>" }

sub option_spec { {} }

sub execute {
    my ($self, $opt, $cal_name, $cluster_path) = @_;

    unless ($cal_name && $cluster_path) {
        say colored(["bold red"], "Usage: cal detach <calendar> <cluster-path>");
        return;
    }

    my $res = $self->api->call('update_cluster', {
        name   => $cluster_path,
        cal_id => '',               # empty string = detach
    });

    if ($res->{success}) {
        say colored(["bold green"], "Calendar '$cal_name' detached from cluster '$cluster_path'");

lib/App/Ordo/Command/Cal/List.pm  view on Meta::CPAN

use feature qw(say);
use utf8;
use open ':std', ':utf8'; # Set STDOUT and STDERR to UTF-8
extends 'App::Ordo::Command::Base';
use POSIX qw(strftime);

use Term::ANSIColor qw(colored);
use Text::Table::Tiny 1.02 qw(generate_table);

sub name    { "cal list" }
sub summary { "List all calendars and their cron expressions" }
sub usage   { "[calendar-name]" }
sub aliases { ['cal ls'] }

sub option_spec { {} }

sub execute {
    my ($self, $opt, $filter_name) = @_;

    my $res = $self->api->call('find_cal', {});
    unless ($res->{success} && scalar @{ $res->{cals} }) {
       say colored(["bold yellow"], "No calendars found");
       return;
    }

    my @cals = @{$res->{cals} || []};

    # Optional: filter by name
    if ($filter_name) {
        @cals = grep { ($_->{name} || '') eq $filter_name } @cals;
        unless (@cals) {
            say colored(["bold yellow"], "No calendar found with name: $filter_name");
            return;
        }
    }

    say colored(["bold cyan"], "Calendars (" . scalar(@cals) . " found):\n");

    for my $cal (@cals) {
        my $name = $cal->{name} || colored(["yellow"], "(unnamed id:$cal->{id})");
        my $desc = $cal->{description} ? " - $cal->{description}" : "";
        my $attached = $cal->{cluster_ids} && @{$cal->{cluster_ids}}

lib/App/Ordo/Command/Cal/Show.pm  view on Meta::CPAN

package App::Ordo::Command::Cal::Show;
use Moo;
extends 'App::Ordo::Command::Base';
use feature qw(say);
use POSIX qw(strftime);

use Term::ANSIColor qw(colored);
use Text::Table::Tiny 1.02 qw(generate_table);

sub name    { "cal show" }
sub summary { "Show detailed information about a calendar" }
sub usage   { "<calendar-name>" }

sub option_spec { {} }

sub execute {
    my ($self, $opt, $cal_name) = @_;

    unless ($cal_name) {
        say colored(["bold red"], "Usage: cal show <calendar-name>");
        return;
    }

    my $res = $self->api->call('find_cal', { name => $cal_name });

    unless ($res->{success}) {
        say colored(["bold red"], "Calendar not found: $cal_name");
        say "  " . ($res->{message} || '');
        return;
    }

lib/App/Ordo/Command/Cluster/Create.pm  view on Meta::CPAN

sub name    { "cluster create" }
sub summary { "Create a new cluster (folder/DAG)" }
sub usage   { "<path> [options]" }

sub option_spec {
    return {
        #'desc|description|d=s'  => 'Cluster description',
        'desc'                  => 'Cluster description',
        'needs=s@'              => 'Clusters this depends on (AND logic)',
        'needs_any=s@'          => 'Clusters this depends on (OR logic)',
        'cal=s'                 => 'Attach calendar by name',
        'loops=i'               => 'Number of loops',
        'clonable=i'            => 'Allow cloning (0/1)',
        'on_fail=s'             => 'Cluster to run on failure',
    };
}

sub execute {
    my ($self, $opt, $path) = @_;

    unless ($path) {

lib/App/Ordo/Command/Cluster/Update.pm  view on Meta::CPAN

sub name    { "cluster update" }
sub summary { "Update an existing cluster" }
sub usage   { "<path> [options]" }

sub option_spec {
    return {
        'description=s'         => 'Change description',
        'needs=s@'              => 'Replace AND dependencies',
        'needs_any=s@'          => 'Replace OR dependencies',
        'remove_needs'          => 'Remove all dependencies (boolean)',
        'cal=s'                 => 'Attach/detach calendar (name or empty)',
        'loops=i'               => 'Change loop count',
        'clonable=i'            => 'Change clonable flag',
        'on_fail=s'             => 'Change fail alarm cluster',
    };
}

sub execute {
    my ($self, $opt, $path) = @_;

    unless ($path) {



( run in 1.121 second using v1.01-cache-2.11-cpan-f52f0507bed )