#!/usr/bin/env perl
use v5.36;
use FindBin qw($Bin);
use lib "$Bin/../lib";

use Getopt::Long qw(GetOptions);
use Pod::Usage   qw(pod2usage);
use Remote::Perl;

# -- Option parsing ------------------------------------------------------------

my $rsh        = 'ssh';
my $pipe_cmd   = 0;
my $window     = 65_536;
my $eval_code  = undef;
my $stdin_file = undef;
my $stdin_str      = undef;
my $serve_modules  = 0;
my @inc_local;
my $help           = 0;
my $version        = 0;

GetOptions(
    'rsh=s'               => \$rsh,
    'pipe-cmd'            => \$pipe_cmd,
    'window-size=i'       => \$window,
    'e=s'                 => \$eval_code,
    'stdin-file=s'        => \$stdin_file,
    'stdin-str=s'         => \$stdin_str,
    'serve-modules!'      => \$serve_modules,
    'inc-local=s@'        => \@inc_local,
    'help|h'              => \$help,
    'version|V'           => \$version,
) or pod2usage(2);

pod2usage(-verbose => 1, -exitstatus => 0) if $help;

if ($version) {
    print "remperl $Remote::Perl::VERSION\n";
    exit 0;
}

# -- Build the remote command --------------------------------------------------

my @cmd;
if ($pipe_cmd) {
    my $spec = shift @ARGV
        // die "remperl: --pipe-cmd requires a command argument\n";
    @cmd = ('sh', '-c', $spec);
}
else {
    my $host = shift @ARGV
        // die "remperl: missing host argument\n";
    @cmd = ($rsh, $host, 'perl');
}

# -- Source: file or -e --------------------------------------------------------

my $script;
if (!defined $eval_code) {
    $script = shift @ARGV
        // die "remperl: missing script argument (or use -e CODE)\n";
}

# -- stdin ---------------------------------------------------------------------

die "remperl: --stdin-file and --stdin-str are mutually exclusive\n"
    if defined $stdin_file && defined $stdin_str;

my $stdin;
if (defined $stdin_str) {
    $stdin = $stdin_str;
}
elsif (defined $stdin_file) {
    open(my $fh, '<', $stdin_file)
        or die "remperl: cannot open '$stdin_file': $!\n";
    binmode($fh);
    $stdin = $fh;
}
else {
    $stdin = \*STDIN;
}

# -- Connect and run -----------------------------------------------------------

STDOUT->autoflush(1);

my @inc = (@inc_local, @INC);
my $r = Remote::Perl->new(
    cmd      => \@cmd,
    window   => $window,
    serve    => $serve_modules,
    inc      => \@inc,
);

# Forward signals through the protocol so the remote executor receives them
# regardless of transport (SSH, docker, etc.).
for my $sig (qw(INT TERM QUIT HUP)) {
    $SIG{$sig} = sub { eval { $r->send_signal($sig) } };
}

my @script_args = @ARGV;

my ($rc, $msg) = defined($eval_code)
    ? $r->run_code($eval_code,
        on_stdout => sub { print STDOUT $_[0] },
        on_stderr => sub { print STDERR $_[0] },
        stdin     => $stdin,
        args      => \@script_args,
      )
    : $r->run_file($script,
        on_stdout => sub { print STDOUT $_[0] },
        on_stderr => sub { print STDERR $_[0] },
        stdin     => $stdin,
        args      => \@script_args,
      );

print STDERR "remperl: $msg\n" if $rc && defined $msg && length $msg;

$r->disconnect;
exit($rc // 1);

__END__

=head1 NAME

remperl - run Perl scripts on remote machines over any pipe transport

=head1 SYNOPSIS

  remperl [options] HOST script.pl [script-args...]
  remperl [options] HOST -e CODE [script-args...]
  remperl --pipe-cmd [options] COMMAND script.pl [script-args...]

C<HOST> accepts: C<hostname>, C<user@hostname>, C<ssh://[user@]host[:port]>.

=head1 DESCRIPTION

C<remperl> connects to a remote Perl interpreter through an arbitrary pipe
command, bootstraps a self-contained protocol client on the remote end, and
executes Perl code there.  C<STDOUT> and C<STDERR> from the remote script are
relayed in real time; local C<STDIN> is forwarded on demand.

When C<--serve-modules> is enabled, any module not found in the remote's own
C<@INC> is fetched transparently from the local machine.  The remote machine
needs no pre-installed dependencies beyond a bare Perl interpreter.

For the library interface, see L<Remote::Perl>.

=head1 OPTIONS

=over 4

=item B<--rsh>=I<EXECUTABLE>

SSH-like command to use (default: C<ssh>).  Invoked as
C<EXECUTABLE HOST perl>.

=item B<--pipe-cmd>

Treat the first positional argument as a complete pipe command instead of a
hostname.  The command is interpreted by C<sh -c>, so quoting and environment
variable assignments work as expected.

=item B<-e> I<CODE>

Evaluate CODE on the remote side instead of running a script file.

=item B<--stdin-file>=I<FILE>

Read remote STDIN from FILE instead of local STDIN.

=item B<--stdin-str>=I<STRING>

Use STRING verbatim as remote STDIN.  Mutually exclusive with
C<--stdin-file>.

=item B<--window-size>=I<N>

Initial flow-control credit per stream in bytes (default: 65536).

=item B<--serve-modules>

Enable module serving: missing modules are fetched from the local machine's
C<@INC> on demand.  Disabled by default.  Use C<--no-serve-modules> to
explicitly disable.

=item B<--inc-local>=I<PATH>

Prepend PATH to the local C<@INC> used when serving modules.  May be
specified multiple times.

=item B<--version>

Print the version and exit.

=item B<--help>

Print this message and exit.

=back

=head1 EXAMPLES

Run a script on a remote host:

  remperl hostx myscript.pl

Pass C<user@hostname> directly to ssh:

  remperl alice@hostx myscript.pl

Evaluate inline code:

  remperl hostx -e 'use Some::Module; print Some::Module->greet'

Pass arguments to the remote script:

  remperl hostx myscript.pl arg1 arg2 "arg three"

Pipe stdin to the remote script:

  echo "hello" | remperl hostx myscript.pl

Use a custom ssh-like executable:

  remperl --rsh=my-ssh hostx myscript.pl

Use an arbitrary pipe command (Docker, kubectl, etc.):

  remperl --pipe-cmd 'docker exec -i mycontainer perl' myscript.pl
  remperl --pipe-cmd 'kubectl exec -i mypod -- perl' myscript.pl
  remperl --pipe-cmd 'nice ssh hostx perl' myscript.pl

=head1 SECURITY

Authentication and access control are the transport's responsibility (SSH
keys, container permissions, etc.).

Module serving (C<--serve-modules>) is disabled by default.  When enabled,
the remote side can request any module by filename; the local side searches
C<@INC> and returns the source.  Path traversal sequences (C<..>) in module
filenames are rejected.  Only enable module serving with endpoints you trust
or when the exposure of your local C<@INC> contents is acceptable.

=head1 REQUIREMENTS

Perl 5.36 or later on both local and remote machines.  No non-core modules
required on either side.

=head1 BUGS

C<__DATA__> and C<__END__> sections in remote scripts are not currently
supported.  The code before the marker executes correctly, but C<< <DATA> >>
reads will not work.

=head1 SEE ALSO

L<Remote::Perl> -- the library interface.

=head1 AUTHOR

Pied Crow <crow@cpan.org>

=head1 LICENSE

This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

=cut
