App-Ordo

 view release on metacpan or  search on metacpan

lib/App/Ordo.pm  view on Meta::CPAN

sub ensure_session {
    my ($self) = @_;

    my $config = $self->api->config;
    my $token  = $config->{token} // '';

    my $res = $self->api->call('login_user', { token => $token });

    if ($res->{success}) {
        $CURRENT_PATH = $res->{path} // '/';
        say colored(["bold green"], "Connected to " . $self->api->config->{api} . " as $res->{email}");
        return $res;
    }

    say colored(["bold yellow"], "No valid token found");
    return $self->prompt_for_token;
}

sub prompt_for_token {
    my $self = shift;
    while (1) {
        print colored(["bold white"], "Enter API token: ");
        chomp(my $token = <STDIN>);
        $token =~ s/^\s+|\s+$//g;

        my $res = $self->api->call('login_user', { token => $token });

        if ($res->{success} && $res->{level} >= 1) {
            my $config = $self->api->config;
            $config->{token} = $token;
            open my $fh, '>', $self->api->config_file or die "Cannot save config: $!";
            print $fh encode_json($config);
            close $fh;
            $CURRENT_PATH = $res->{path} // '/';
            say colored(["bold green"], "Logged in successfully");
            return $res;
        }
        say colored(["bold red"], $res->{message} || "Invalid token");
    }
}

# ------------------------------------------------------------------
# Interactive Shell
# ------------------------------------------------------------------
sub run_interactive {
    my $self = shift;

    $self->ensure_session;

    my $term = Term::ReadLine::Perl5->new('ordo');

    say colored(["bold white"], "\nWelcome to Ordo - the hierarchical job scheduler");
    say "Type 'help' for commands, Ctrl-D to exit.\n";

    while (defined(my $line = $term->readline("ordo:$CURRENT_PATH> "))) {
        $line =~ s/^\s+|\s+$//g;
        next unless $line;
        my @args = extract_command($line);
        App::Ordo::Runner->new(api => $self->api)->run(@args);
    }

    say colored(["bold yellow"], "\nGoodbye!");
}

# ------------------------------------------------------------------
# Command line parsing
# ------------------------------------------------------------------
sub extract_command {
    my ($line) = @_;
    return () unless defined $line;
    my @args;
    my $current = '';

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

    }

    my %opt;
    my $ok = GetOptionsFromArray(
        \@args,
        \%opt,
        keys %{$self->option_spec},
    );

    unless ($ok) {
        say colored(["bold red"], "Invalid options — use --help");
        return;
    }

    $self->execute(\%opt, @args);
}

sub execute {
    my ($self, $opt, @args) = @_;
    die "execute() not implemented in " . ref($self);
}

sub show_help {
    my ($self) = @_;
    say colored(["bold cyan"], "ordo @{[lc $self->name]} " . $self->usage);
    say $self->summary;

    my $spec = $self->option_spec;
    return unless $spec && keys %$spec;

    say "\nOptions:";
    for my $key (sort keys %$spec) {
        my $desc = $spec->{$key} || '';
        (my $clean = $key) =~ s/[=:!+].*//;
        $clean =~ s/\|/ | /g;

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

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'");
    } else {
        say colored(["bold red"], "Failed: " . ($res->{message} || 'not found'));
    }
}

1;

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

    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

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

sub aliases { ['cal cron rm'] }

sub option_spec {
   return {};
}

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

   unless ( $cron_id && $cron_id =~ /^\d+$/ ) {
      say colored( ["bold red"], "Usage: cal cron delete <cron-id>" );
      say "Example: cal cron delete 13";
      return;
   }

   my $payload = {

      #        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

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

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'");
    } else {
        say colored(["bold red"], "Failed: " . ($res->{message} || 'not found'));
    }
}

1;

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

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}}
            ? " (attached to " . scalar(@{$cal->{cluster_ids}}) . " clusters)"
            : "";

        say colored(["bold white"], "  $name$desc$attached");

        my $crons = $cal->{crons} || [];
        if (@$crons) {
            my $rows = [ [qw(ID CRON NEXT_START DESCRIPTION)] ];
            for my $cron (@$crons) {
                my $next = $cron->{next_start}
                    ? strftime("%Y-%m-%d %H:%M", localtime($cron->{next_start}))
                    : '-';
                my $desc = $cron->{description} || $cron->{english} || '';
                push @$rows, [

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

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;
    }

    my ($cal) = grep { ($_->{name} || '') eq $cal_name } @{$res->{cals} || []};
    unless ($cal) {
        say colored(["bold red"], "Calendar '$cal_name' not found");
        return;
    }

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

    say colored(["bold cyan"], "Calendar: $name$desc");
    say colored(["bold cyan"], "TimeZone: $tz");
    say colored(["bold cyan"], "Attached: $attached\n");

    my $crons = $cal->{crons} || [];
    if (@$crons) {
        my $rows = [ [qw(ID CRON NEXT_START DESCRIPTION)] ];
        for my $cron (@$crons) {
            my $next = $cron->{next_start}
                ? strftime("%Y-%m-%d %H:%M", localtime($cron->{next_start}))
                : '-';
            my $desc = $cron->{description} || $cron->{english} || '';
            push @$rows, [ $cron->{id}, $cron->{name}, $next, $desc ];

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

sub option_spec { {} }

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

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

    if ($res->{success}) {
        $App::Ordo::CURRENT_PATH = $res->{path};
    } else {
        say colored(["bold red"], "Cannot cd to '$target': " . ($res->{message} || 'not found'));
    }
}

1;

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

sub name    { "cluster complete" }
sub summary { "Force a cluster and all jobs to complete state" }
sub usage   { "<path>" }

sub option_spec { {} }

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

    unless ($path) {
        say colored(["bold red"], "Usage: cluster complete <path>");
        return;
    }

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

    if ($res->{success}) {
        say colored(["bold green"], "Cluster '$path' marked complete - dependent clusters may now run");
    } else {
        say colored(["bold red"], "Failed: " . ($res->{message} || 'unknown error'));
    }
}

1;

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

        '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) {
        say colored(["bold red"], "Usage: cluster create <path> [options]");
        return;
    }

    if ($opt->{needs}) {
        my @expanded;
        for my $item (@{$opt->{needs}}) {
            push @expanded, split /,/, $item;
        }
        $opt->{needs} = \@expanded;
    }

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

        needs_any   => $opt->{needs_any} || [],
        cal         => $opt->{cal},
        loops       => $opt->{loops},
        clonable    => $opt->{clonable},
        on_fail     => $opt->{on_fail},
    };

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

    if ($res->{success}) {
        say colored(["bold green"], "Cluster '$path' created");
    } else {
        say colored(["bold red"], "Failed: " . ($res->{message} || 'unknown error'));
    }
}

1;

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

sub option_spec {
    return {
        'force|f' => 'Force delete even if jobs are running',
    };
}

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

    unless ($path) {
        say colored(["bold red"], "Usage: cluster delete <path> [--force]");
        return;
    }

    #my $full_path = $path =~ m|^/| ? $path : "$CURRENT_PATH/$path";
    #$full_path =~ s|//+|/|g;

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

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

    if ($res->{success}) {
        say colored(["bold green"], "Cluster and all jobs deleted");
    } else {
        say colored(["bold red"], "Failed: " . ($res->{message} || 'unknown error'));
        say "Hint: use --force if jobs are running" if $res->{message} && $res->{message} =~ /running|active/i;
    }
}

1;

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


sub name    { "cluster hold" }
sub summary { "Prevent cluster from running" }
sub usage   { "<path/name>" }

sub option_spec { {} }

sub execute {
    my ($self, $opt, $name) = @_;
    unless ($name) {
        say colored(["bold red"], "Usage: cluster hold <path/name>");
        return;
    }

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

    $res->{success}
        ? say colored(["bold yellow"], "Cluster '$name' held")
        : say colored(["bold red"], "Failed: " . ($res->{message} || 'error'));
}

1;

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

sub name    { "cluster ice" }
sub summary { "Skip cluster without blocking dependents" }
sub usage   { "<path/name>" }

sub option_spec { {} }

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

    unless ($name) {
        say colored(["bold red"], "Usage: cluster ice <path/name>");
        return;
    }

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

    if ($res->{success}) {
        say colored(["bold cyan"], "Cluster '$name' iced - will be skipped");
        say colored(["bright_black"], "Downstream clusters will run as soon as upstream complete");
    } else {
        say colored(["bold red"], "Failed to ice cluster: " . ($res->{message} || 'error'));
    }
}

1;

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

sub usage   { "<path>" }

sub option_spec {
    return {};
}

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

    unless ($name) {
        say colored(["bold red"], "Usage: cluster kill <path>");
        return;
    }

    my $payload = { name => $name };

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

    if ($res->{success}) {
        say colored(["bold yellow"], "All running jobs in cluster '$name' killed");
    } else {
        say colored(["bold red"], "Failed to kill cluster: " . ($res->{message} || 'unknown error'));
    }
}

1;

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

sub name    { "cluster melt" }
sub summary { "Unfreeze a cluster previously iced" }
sub usage   { "<path/name>" }

sub option_spec { {} }

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

    unless ($name) {
        say colored(["bold red"], "Usage: cluster melt <path/name>");
        return;
    }

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

    if ($res->{success}) {
        say colored(["bold green"], "Cluster '$name' melted — back in rotation");
    } else {
        say colored(["bold red"], "Failed to melt cluster: " . ($res->{message} || 'error'));
    }
}

1;

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


sub name    { "cluster release" }
sub summary { "Release a held cluster" }
sub usage   { "<path/name>" }

sub option_spec { {} }

sub execute {
    my ($self, $opt, $name) = @_;
    unless ($name) {
        say colored(["bold red"], "Usage: release hold <path/name>");
        return;
    }

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

    $res->{success}
        ? say colored(["bold yellow"], "Cluster '$name' released")
        : say colored(["bold red"], "Failed: " . ($res->{message} || 'error'));
}

1;

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

sub name    { "cluster reset" }
sub summary { "Reset cluster and all jobs to ready state" }
sub usage   { "<path>" }

sub option_spec { {} }

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

    unless ($path) {
        say colored(["bold red"], "Usage: cluster reset <path>");
        return;
    }

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

    if ($res->{success}) {
        say colored(["bold cyan"], "Cluster '$path' and all jobs reset to ready");
    } else {
        say colored(["bold red"], "Failed: " . ($res->{message} || 'unknown error'));
    }
}

1;

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

sub name    { "cluster run" }
sub summary { "Run all jobs in a cluster (in dependency order)" }
sub usage   { "<path>" }

sub option_spec { {} }

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

    #unless ($path) {
    #    say colored(["bold red"], "Usage: cluster run <path>");
    #    return;
    #}

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

    if ($res->{success}) {
        say colored(["bold green"], "Cluster started");
    } else {
        say colored(["bold red"], "Failed to start cluster: " . ($res->{message} || 'not found'));
    }
}

1;

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

sub usage   { "[path]" }

sub option_spec { {} }

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

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

    unless ($res->{success}) {
        say colored(["bold red"], "Cluster not found: $path");
        say $res->{message} if $res->{message};
        return;
    }

    my $c = $res;
    $path //= $c->{name};

    say colored(["bold cyan"], "Cluster: ") . colored(["bold white"], $c->{name});
    say colored(["cyan"], "Path: ") . $path;
    say colored(["cyan"], "State: ") . colored(["bold " . ($c->{jobstate} eq 'running' ? 'green' : 'yellow')], $c->{jobstate});
    say colored(["cyan"], "Description: ") . ($c->{description} || colored(["bright_black"], "none"));
    say colored(["cyan"], "Calendar: ") . ($c->{cal_name} ? $c->{cal_name} : colored(["bright_black"], "none"));
    say colored(["cyan"], "Created: ") . ($c->{creation_time}
        ? strftime("%a %b %d %H:%M:%S %Y", localtime($c->{creation_time}))
        : "unknown");

    if ($c->{needs} && keys %{$c->{needs}}) {
        my @all = grep { $c->{needs}{$_}{mode} == 0 } keys %{$c->{needs}};
        my @any = grep { $c->{needs}{$_}{mode} == 1 } keys %{$c->{needs}};
        say colored(["cyan"], "Depends on: ") . join(", ", @all) if scalar @all;

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

        my $when = $ago < 3600 ? "$ago seconds ago"
                 : $ago < 86400 ? int($ago/3600) . " hours ago"
                 : int($ago/86400) . " days ago";
        say colored(["cyan"], "Last run: ") . strftime("%a %b %d %H:%M:%S %Y", localtime($c->{ended}))
            . colored(["green"], " ($when)");
    } else {
        say colored(["cyan"], "Last run: ") . colored(["yellow"], "never");
    }

    if ($c->{jobs} && @{$c->{jobs}}) {
        say "\n" . colored(["bold green"], "Jobs in this cluster:");
        my $rows = [ [qw(ID NAME SERVER STATE SCRIPT)] ];
        for my $j (@{$c->{jobs}}) {
            my $state_color = $j->{jobstate} eq 'complete' ? 'green' :
                              $j->{jobstate} eq 'running'  ? 'magenta' :
                              $j->{jobstate} eq 'failed'   ? 'red' : 'yellow';
            push @$rows, [
                $j->{id},
                $j->{name},
                $j->{server_name} || '-',
                colored(["bold $state_color"], $j->{jobstate}),
                $j->{script} || '(no script)',
            ];
        }
        say generate_table(rows => $rows, header_row => 1, style => 'boxrule');
    } else {
        say colored(["yellow"], "\nNo jobs in this cluster");
    }
    say "";
}

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

        '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) {
        say colored(["bold red"], "Usage: cluster update <path> [options]");
        return;
    }

    my $payload = { name => $path };

    if ($opt->{needs}) {
        my @expanded;
        for my $item (@{$opt->{needs}}) {
            push @expanded, split /,/, $item;
        }

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

    $payload->{description} = $opt->{description} if $opt->{description};
    $payload->{needs}       = $opt->{needs}       if $opt->{needs};
    $payload->{needs_any}   = $opt->{needs_any}   if $opt->{needs_any};
    $payload->{remove_needs} = $opt->{remove_needs} if $opt->{remove_needs};
    $payload->{cal}         = $opt->{cal}         if exists $opt->{cal};
    $payload->{loops}       = $opt->{loops}       if defined $opt->{loops};
    $payload->{clonable}    = $opt->{clonable}    if defined $opt->{clonable};
    $payload->{on_fail}     = $opt->{on_fail}     if $opt->{on_fail};

    unless (keys %$payload > 1) {
        say colored(["bold yellow"], "No changes specified");
        return;
    }

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

    if ($res->{success}) {
        say colored(["bold green"], "Cluster '$path' updated");
    } else {
        say colored(["bold red"], "Failed: " . ($res->{message} || 'unknown error'));
    }
}

1;

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


sub name    { "exit" }
sub summary { "Exit the interactive shell" }
sub usage   { "" }
sub aliases { ['quit'] }

sub option_spec { {} }

sub execute {
    my ($self) = @_;
    say colored(["bold yellow"], "Goodbye!");
    exit 0;
}

1;

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

sub _help_topic {
    my ($self, $topic) = @_;
    $topic =~ s/^\s+|\s+$//g;

    my @parts = split /\s+/, $topic;
    my $cmd_class = "App::Ordo::Command::" . join('::', map { ucfirst lc } @parts);

    if (eval "require $cmd_class; 1") {
        $cmd_class->new(api => $self->api)->show_help;
    } else {
        say colored(["bold red"], "No help available for '$topic'");
        say "Try 'help' for a list of commands";
    }
}

sub _help_overview {
    my ($self) = @_;

    say colored(["bold cyan"],   "Usage:");
    say "  ordo                                 # interactive shell";
    say "  ordo <command> [args]                # one-shot mode\n";

    say colored(["bold yellow"], "Commands:\n");

    my %groups = (
        Navigation => [],
        Jobs       => [],
        Clusters   => [],
        Calendars  => [],
        Servers    => [],
        User       => [],
        System     => [],
    );

    $self->_walk_tree($self->commands, \%groups, []);

    for my $group (qw(Navigation Jobs Clusters Calendars Servers User System)) {
        say colored(["bold green"], "  $group:");
        if (@{$groups{$group}}) {
            my $max = 0;
            $max = length($_->[0]) > $max ? length($_->[0]) : $max for @{$groups{$group}};
            printf "    %-*s  %s\n", $max + 4, $_->[0], $_->[1] for @{$groups{$group}};
        } else {
            say "    (no commands)";
        }
        say "";
    }

    say colored(["bold magenta"], "Examples:");
    say "  server add myserver --host 192.168.1.100 --user alice";
    say "  cluster create mycluster";
    say "  job create mycluster/myjob --server myserver --script 'ls -ltr'";

    say "\nType 'help <command>' for detailed help";
}

sub _walk_tree {
    my ($self, $node, $groups, $path) = @_;

lib/App/Ordo/Command/Job/Complete.pm  view on Meta::CPAN

sub name    { "job complete" }
sub summary { "Force a job to complete state (triggers dependents)" }
sub usage   { "<name>" }

sub option_spec { {} }

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

    unless ($name) {
        say colored(["bold red"], "Usage: job complete <name>");
        return;
    }

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

    if ($res->{success}) {
        say colored(["bold green"], "Job '$name' marked complete - dependents may now run");
    } else {
        say colored(["bold red"], "Failed: " . ($res->{message} || 'unknown error'));
    }
}

1;

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

        'clonable=i'         => 'Allow cloning',
        'on_fail=i'          => 'Cluster to run on failure',
        'json=s'             => 'Extra JSON data',
    };
}

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

    unless ($name && $opt->{server} && $opt->{script}) {
        say colored(["bold red"], "Missing required: name, --server, --script");
        $self->show_help;
        return;
    }

    if ($opt->{needs}) {
        my @expanded;
        for my $item (@{$opt->{needs}}) {
            push @expanded, split /,/, $item;
        }
        $opt->{needs} = \@expanded;

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

        loops       => $opt->{loops},
        delay       => $opt->{delay},
        clonable    => $opt->{clonable},
        on_fail     => $opt->{on_fail},
        json        => $opt->{json},
    };

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

    if ($res->{success}) {
        say colored(["bold green"], "Job '$name' created on server '$opt->{server}'");
    } else {
        say colored(["bold red"], "Failed: " . ($res->{message} || 'unknown error'));
    }
}

1;

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

sub option_spec {
    return {
        'force|f' => 'Force delete even if running',
    };
}

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

    unless ($name) {
        say colored(["bold red"], "Usage: job delete <path/name> [--force]");
        return;
    }

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

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

    if ($res->{success}) {
        say colored(["bold green"], "Job '$name' deleted");
    } else {
        my $msg = $res->{message} || 'unknown error';
        say colored(["bold red"], "Failed to delete job: $msg");
        say "Try --force if the job is running" if $msg =~ /running/i;
    }
}

1;

lib/App/Ordo/Command/Job/Hold.pm  view on Meta::CPAN


sub name    { "job hold" }
sub summary { "Pause a job from running" }
sub usage   { "<path/name>" }

sub option_spec { {} }

sub execute {
    my ($self, $opt, $name) = @_;
    unless ($name) {
        say colored(["bold red"], "Usage: job hold <path/name>");
        return;
    }

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

    $res->{success}
        ? say colored(["bold yellow"], "Job '$name' held")
        : say colored(["bold red"], "Failed: " . ($res->{message} || 'error'));
}

1;

lib/App/Ordo/Command/Job/Ice.pm  view on Meta::CPAN

sub name    { "job ice" }
sub summary { "Skip job without blocking dependents" }
sub usage   { "<path/name>" }

sub option_spec { {} }

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

    unless ($name) {
        say colored(["bold red"], "Usage: job ice <path/name>");
        return;
    }

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

    if ($res->{success}) {
        say colored(["bold cyan"], "Job '$name' iced");
        say colored(["bright_black"], "Downstream jobs will run as soon as upstream complete");
    } else {
        say colored(["bold red"], "Failed to ice job: " . ($res->{message} || 'error'));
    }
}

1;



( run in 1.519 second using v1.01-cache-2.11-cpan-9581c071862 )