### ----------------------------------------------------
### -- fez
### -- Licenses: Artistic-2.0
### -- Authors: 
### -- File: lib/Fez/CLI.rakumod
### ----------------------------------------------------
### -- Chunk 1 of 2
unit module Fez::CLI;

my Bool $UNATTENDED = False;

use Fez::Logr;
use Fez::Util::Pass;
use Fez::Util::Json;
use Fez::Util::Config;
use Fez::Util::Uri;
use Fez::Util::META6;
use Fez::Util::FS;
use Fez::Util::Glob;
use Fez::Web;
use Fez::Bundle;
use Fez::API;

set-loglevel(DEBUG) if (@*ARGS.grep({$_ ~~ m/^'-''v'+$/}).first||' ').chars - 1 != 0;
@*ARGS = @*ARGS.grep({$_ !~~ m/^'-''v'+$/});
$UNATTENDED = @*ARGS.grep(* ~~ '--unattended').elems > 0;
@*ARGS = @*ARGS.grep(* ne '--unattended');
log(DEBUG, 'Running in unattended mode') if $UNATTENDED;

sub pass-wrapper(Str:D $prompt --> Str) {
  if $UNATTENDED {
    log(ERROR, 'Requested prompt with --unattended: %s', $prompt);
    log(FATAL, 'Unable to prompt while --unattended is in use');
  }
  getpass($prompt);
}

sub prompt-wrapper(Str:D $prompt --> Str) {
  if $UNATTENDED {
    log(FATAL, 'Unable to prompt while --unattended is in use');
  }
  prompt($prompt);
}

multi MAIN('v') is export is pure {
  MAIN('version');
}
multi MAIN('version') is export is pure {
  log(MSG, 'fez version: %s', $?DISTRIBUTION.meta<version>);
}

multi MAIN('e', 'l') is export {
  MAIN('ecosystem', 'list');
}

# TODO: these should do key management on their own and not require re-login
multi MAIN('ecosystem', 'list') is export {
  my $known = config-value('ecosystems')//{zef => from-j(%?RESOURCES<config.json>>.IO.slurp)<host>};
  my $longest-key = max(0, | $known.keys.map(*.chars));
  for $known.keys -> $eco {
    log(MSG, "{$eco}{' ' x ($longest-key - $eco.chars)}: {$known{$eco}}");
  }
}

multi MAIN('e', 'r', Str $name) is export {
  MAIN('ecosystem', 'remove', $name);
}

multi MAIN('ecosystem', 'remove', Str $name) is export {
  my $knowns = config-value('ecosystems')//{zef => from-j(%?RESOURCES<config.json>>.IO.slurp)<host>};

  log(FATAL, "zef cannot be removed") if $name eq 'zef';
  log(FATAL, "$name is not known by this client") if $knowns{$name}:!exists;
  $knowns{$name}:delete;

  my %new-config = user-config;
  %new-config<ecosystems> = $knowns;
  write-to-user-config(%new-config);

  log(MSG, "Successfully removed {$name}");
}

multi MAIN('ecosystem', 'add', Str $url, Bool :f($force) = False) is export {
  my $knowns = config-value('ecosystems')//{zef => from-j(%?RESOURCES<config.json>>.IO.slurp)<host>};
  my $is-known-by = Nil;
  for $knowns.keys -> $k {
    $is-known-by = $k if $knowns{$k} eq $url;
  }

  my $response = get('/auth-prefix');
  if ! $response<success> {
    log(FATAL, 'Failed to retrieve auth information from '~$url);
  }
  if $knowns{$response<message>}:exists && !$force {
    log(FATAL, "Not willing to overwrite existing ecosystem prefix ({$response<message>}), use -f to force");
  }
  if $is-known-by ~~ Str &&  $is-known-by eq $response<message> {
    log($force ?? WARN !! FATAL, "$url is known by $is-known-by, this can have serious consequences");
  }

  $knowns{$response<message>} = $url;
  my %new-config = user-config;
  %new-config<ecosystems> = $knowns;
  write-to-user-config(%new-config);

  log(MSG, "Successfully added {$response<message>}");
}


multi MAIN('e', 'a', Str $url, :f($force) = False) is export {
  MAIN('ecosystem', 'add', $url, :f($force));
}

multi MAIN('ecosystem', 'add', Str $url, Bool :f($force) = False) is export {
  my $knowns = config-value('ecosystems')//{zef => from-j(%?RESOURCES<config.json>>.IO.slurp)<host>};
  my $is-known-by = Nil;
  for $knowns.keys -> $k {
    $is-known-by = $k if $knowns{$k} eq $url;
  }

  my $response = get('/auth-prefix');
  if ! $response<success> {
    log(FATAL, 'Failed to retrieve auth information from '~$url);
  }
  if $knowns{$response<message>}:exists && !$force {
    log(FATAL, "Not willing to overwrite existing ecosystem prefix ({$response<message>}), use -f to force");
  }
  if $is-known-by ~~ Str &&  $is-known-by eq $response<message> {
    log($force ?? WARN !! FATAL, "$url is known by $is-known-by, this can have serious consequences");
  }

  $knowns{$response<message>} = $url;
  my %new-config = user-config;
  %new-config<ecosystems> = $knowns;
  write-to-user-config(%new-config);

  log(MSG, "Successfully added {$response<message>}");
}

multi MAIN('e', 's', Str $name) is export {
  MAIN('ecosystem', 'switch', $name);
}

multi MAIN('ecosystem', 'switch', Str $name) is export {
  my $knowns = config-value('ecosystems')//{zef => from-j(%?RESOURCES<config.json>>.IO.slurp)<host>};
  if $knowns{$name}:!exists {
    log(FATAL, "Ecosystem $name is unknown");
  }

  my %new-config = user-config;
  %new-config<host> = $knowns{$name};
  %new-config<key> = '';
  write-to-user-config(%new-config);

  log(MSG, "Now using {$name} for distribution, you may need to login or register before continuing");
}

multi MAIN('o', 'c', Str $org-name, Str $org-email) is export {
  MAIN('org', 'create', $org-name, $org-email);
}
multi MAIN('org', 'create', Str $org-name, Str $org-email) is export {
  my $response = org-create(config-value('key'), $org-name, $org-email);
  if $response.success {
    log(MSG, 'You\'re the proud new admin of %s', $org-name);
    $response = org-list(config-value('key'));
    if ! $response.success {
      log(FATAL, 'Failed to retrieve user orgs');
    }
    if $response.success {
      write-to-user-config({ groups => $response.groups });
    } else {
      log(FATAL, 'Failed to update config');
    }
  } else {
    log(FATAL, $response.message);
  }
}

multi MAIN('o', 'l', Str $org-name) is export {
  MAIN('org', 'leave', $org-name);
}
multi MAIN('org', 'leave', Str $org-name) is export {
  my $response = org-leave(config-value('key'), $org-name);
  if $response.success {
    log(MSG, 'You\'re no longer in %s', $org-name);
    $response = org-list(config-value('key'));
    if ! $response.success {
      log(FATAL, 'Failed to retrieve user orgs');
    }
    if $response.success {
      write-to-user-config({ groups => $response.groups });
    } else {
      log(FATAL, 'Failed to update config');
    }
  } else {
    log(FATAL, $response.message);
  }
}

multi MAIN('o', 'a', Str $org-name) is export {
  MAIN('org', 'accept', $org-name);
}
multi MAIN('org', 'accept', Str $org-name) is export {
  my $response = org-join(config-value('key'), $org-name);
  if $response.success {
    log(MSG, 'You\'re now a very nice member of %s', $org-name);
    $response = org-list(config-value('key'));
    if ! $response.success {
      log(FATAL, 'Failed to retrieve user orgs');
    }
    if $response.success {
      write-to-user-config({ groups => $response.groups });
    } else {
      log(FATAL, 'Failed to update config');
    }
  } else {
    log(FATAL, $response.message);
  }
}

multi MAIN('o', 'p') is export {
  MAIN('org', 'pending');
}
multi MAIN('org', 'pending') is export {
  my $response = org-pending(config-value('key'));
  if $response.success {
    log(MSG, 'No pending invites found') unless $response.groups;
    log(MSG, 'R Org') if $response.groups;
    for $response.groups.sort({ $^a<role> eq $^b<role> ?? $^a<group> cmp $^b<group> !! $^a<role> cmp $^b<role> }) -> %g {
      log(MSG, '%s %s', %g<role>.substr(0,1), %g<group>);
    }
  } else {
    log(FATAL, $response.message);
  }
}

multi MAIN('o', 'm', Str $org-name) is export {
  MAIN('org', 'members', $org-name);
}
multi MAIN('org', 'members', Str $org-name) is export {
  my $response = org-members(config-value('key'), $org-name);
  if $response.success {
    log(MSG, 'No members') unless $response.members; # Weird edge case
    log(MSG, 'R Org Name') if $response.members;
    for $response.members.sort({ $^a<role> eq $^b<role> ?? $^a<username> cmp $^b<username> !! $^a<role> cmp $^b<role> }) -> %m {
      log(MSG, '%s %s', %m<role>.substr(0,1), %m<username>);
    }
  } else {
    log(FATAL, $response.message);
  }
}

multi MAIN('o', 'i', Str $org-name, Str $role, Str $user) is export {
  MAIN('org', 'invite', $org-name, $role, $user);
}
multi MAIN('org', 'invite', Str $org-name, Str $role, Str $user) is export {
  my $response = org-invite(config-value('key'), $org-name, $role, $user);
  if $response.success {
    log(MSG, 'Invitation sent');
  } else {
    log(FATAL, $response.message);
  }
}

multi MAIN('o', 'mod', Str $org-name, Str $role, Str $user) is export {
  MAIN('org', 'modify', $org-name, $role, $user);
}
multi MAIN('org', 'modify', Str $org-name, Str $role, Str $user) is export {
  my $response = org-mod(config-value('key'), $org-name, $role, $user);
  if $response.success {
    log(MSG, 'User\'s role was modified');
  } else {
    log(FATAL, $response.message);
  }
}

multi MAIN('reset') is export {
  MAIN('reset-password');
}
multi MAIN('reset-password') is export {
  my $un = prompt-wrapper('>>= Username: ') while ($un//'').chars < 3;
  my $response = init-reset-password($un);
  if ! $response.success {
    log(FATAL, ['There was an error communicating with the service, please',
                'try again in a few minutes.'].join("\n"));
  }
  log(MSG, 'A reset key was successfully requested, please check your email');
  my $key  = prompt-wrapper('>>= What is the key in your email (ctrl+c to cancel)? ') while ($key//'') eq '';
  my $pass = pass-wrapper('>>= New Password: ') while ($pass//'').chars < 8;
  $response = reset-password($un, $key, $pass);
  if ! $response.success {
    log(FATAL, 'Password reset failed: %s', $response<message>);
  }
  write-to-user-config({
    key => $response.key,
    un  => $un,
  });
  log(MSG, 'Password reset successful, you now have a new key and can upload dists');
}

multi MAIN('reg') is export {
  MAIN('register');
}
multi MAIN('register') is export {
  my ($em, $un, $pw);
  $em = prompt-wrapper('>>= Email: ');
  $em = prompt-wrapper('>>= Email: ') while ($em//'').chars < 6
                                         && Nil ~~ log(ERROR, 'Please enter a valid email');
  $un = prompt-wrapper('>>= Username: ');
  $un = prompt-wrapper('>>= Username: ') while ($un//'').chars < 3
                                            && Nil ~~ log(ERROR, 'Username must be longer than 3 chars');
  $pw = pass-wrapper('>>= Password: ');
  $pw = pass-wrapper('>>= Password: ')  while ($pw//'').chars < 8
                                           && Nil ~~ log(ERROR, 'Password must be longer than 8 chars');

  my $response = register($em, $un, $pw);

  if ! $response.success {
    log(FATAL, 'Registration failed: %s', $response.message);
  }
  log(MSG, 'Registration successful, requesting auth key');
  my $*USERNAME = $un;
  my $*PASSWORD = $pw;
  MAIN('login');
  MAIN('meta');
}

multi MAIN('l') is export {
  MAIN('login');
}
multi MAIN('login') is export {
  my $un = $*USERNAME // '';
  my $pw = $*PASSWORD // '';
  $un = prompt-wrapper('>>= Username: ');
  $un = prompt-wrapper('>>= Username: ') while ($un//'').chars < 3
                                            && Nil ~~ log(ERROR, 'Username must be longer than 3 chars');
  $pw = pass-wrapper('>>= Password: ');
  $pw = pass-wrapper('>>= Password: ')  while ($pw//'').chars < 8
                                           && log(ERROR, 'Password must be longer than 8 chars');

  my $response = login($un, $pw);
  if ! $response.success {
    log(FATAL, 'Failed to login: %s', $response.message);
  }

  my $ukey = $response.key;
  $response = org-list($ukey);
  if ! $response.success {
    log(FATAL, 'Failed to retrieve user groups');
  }

  write-to-user-config({
    key    => $ukey,
    un     => $un,
    groups => $response.groups,
  });
  log(MSG, 'Login successful, you can now upload dists');
}

multi MAIN('rev') is export {
  MAIN('review');
}
multi MAIN('review') is export {
  my $has-error = False;
  my %findings  = MAIN('ref', :d, :q);
  my @manifest  = ls(%findings<meta-dir>, -> $t { %findings<ignorer>.rmatch($t.relative) })
      .sort({ $^a.lc cmp $^b.lc });
  my $repo-cfg  = %findings<meta-dir>.add('.fez').f
               ?? (try {
                 CATCH { default { log(FATAL, 'error reading .fez: %s', $_); } };
                 from-j(%findings<meta-dir>.add('.fez').slurp)
               })
               !! {};

  log(MSG, "Bundle manifest:\n  %s", @manifest.join("\n  "));

  my %metas = (
    #'Depends'       => 'depends',
    #'Provides'      => 'provides',
    #'Build depends' => 'build-depends',
    #'Test depends'  => 'test-depends',
    'Resources'     => 'resources',
  );
  my ($ok, $key, $left, $right, $l-diff, $r-diff, $ignores);
  for %metas.keys.sort -> $k {
    $key = %metas{$k};
    $left  = %findings<meta>{$key} ~~ Hash
          ?? %findings<meta>{$key}.keys.Set
          !! %findings<meta>{$key}.Set;
    $right = %findings<new-meta>{$key} ~~ Hash
          ?? %findings<new-meta>{$key}.keys.Set
          !! %findings<new-meta>{$key}.Set;
    $ignores = Set.new('NativeCall', 'Test', 'nqp', |$repo-cfg{"ignore-{$key}"}//Set.new);
    $l-diff  = ($left (-) $right) (-) $ignores;
    $r-diff  = ($right (-) $left) (-) $ignores;

    $ok = $l-diff.elems == 0 && $r-diff.elems == 0;

    $has-error ||= !$ok;

    log(MSG, "%s%s ok", $k, $ok??''!!' not');
    if !$ok {
      my $str = $r-diff
        .keys
        .map({sprintf '%s', $_})
        .join("\n    ");

      log(MSG,"  not in meta:\n    %s",  $str) if $r-diff.elems;

      $str = $l-diff
        .keys
        .map({sprintf '%s', $_})
        .join("\n    ");

      log(MSG, "  in meta but unexpected:\n    %s", $str) if $l-diff.elems;
    }
  }
  log(WARN, '`.rakumod` should be used for module extensions, not `.pm6`')
    if %findings<modfiles>.grep({ $_.ends-with('.pm6') });

  if %findings<meta><production>:exists {
    log(WARN,
        ['"production" in META is deprecated. Please create a .fez file with the json object',
         '  {"production":%s}',
         'to continue using this functionality after v53',
        ].join("\n"),
        %findings<meta><production> ?? 'true' !! 'false');
  }
  if ($repo-cfg<production>:exists && !$repo-cfg<production>.so) || (%findings<meta><production>:exists && !%findings<meta><production>.so) {
    log(ERROR, '"production" in project config is set to false');
    $has-error = True;
  }

  my $ver = %findings<meta><ver>
         ?? 'ver'
         !! %findings<meta><vers>
         ?? 'vers'
         !! 'version';
  if $ver ne 'version' {
    log(ERROR, '"version" is the correct version specifier to use (found:%s)', $ver);
    $has-error = True;
  }
  unless %findings<meta><name>//False {
    log(ERROR, 'name should be a value');
    $has-error = True;
  }
  if (%findings<meta>{$ver}//'') eq '' {
    log(ERROR, 'ver should not be nil');
    $has-error = True;
  }
  unless %findings<meta><auth>//False {
    log(ERROR, 'auth should not be nil');
    $has-error = True;
  }
  if %findings<meta>{$ver}.trim eq '*' {
    log(ERROR, 'ver cannot be "*"');
    $has-error = True;
  }
  my @group-auths = |(config-value('groups')//[]).map({"{current-prefix}{$_<group>}"});
  @group-auths.push("{current-prefix}{config-value('un')}") if config-value('un');
  unless %findings<meta><auth> ~~ any(@group-auths) {
    log(
      ERROR,
      "auth does not match logged in user or user's groups\n  expected any of: %s\n  got: %s",
      @group-auths.join(', '),
      %findings<meta><auth>,
    );
    $has-error = True;
  }

  exit 1 if $has-error && !($*DIST//False);

  $has-error;
}

multi MAIN('m', Str :n(:$name) is copy, Str :w(:$website) is copy, Str :e(:$email) is copy) is export {
  MAIN('meta', :$name, :$website, :$email);
}
multi MAIN('meta', Str :n(:$name) is copy, Str :w(:$website) is copy, Str :e(:$email) is copy) is export {
  MAIN('login') unless config-value('key');
  if ! (config-value('key')//0) {
    log(ERROR, 'You must login to change your info');
    exit 255;
  }

  my $ukey = "{current-prefix}{config-value('un')}";
  my $response = get('/meta.json');
  if $response{$ukey} {
    log(MSG, 'Name:    %s', $response{$ukey}<name>//'<none provided>');
    log(MSG, 'Email:   %s', $response{$ukey}<email>//'<none provided>');
    log(MSG, 'Website: %s', $response{$ukey}<website>//'<none provided>');
    my $should-update = prompt-wrapper('>>= Would you like to update [y/N]? ').trim;
    if $should-update.uc !~~ 'Y'|'YE'|'YES' {
      exit 0;
    }
  } else {
    log(MSG, 'No existing meta for current user');
  }

  my %data;
  if ($name//'') eq '' && ($website//'') eq '' && ($email//'') eq '' {
    %data<name>    = prompt-wrapper('>>= What would you like your display name to show? ').trim;
    %data<website> = prompt-wrapper('>>= What\'s your website? ').trim;
    %data<email>   = prompt-wrapper('>>= Public email address? ').trim;
  } else {
    %data<name> = $name if ($name//'') ne '';
    %data<website> = $website if ($website//'') ne '';
    %data<email> = $email if ($email//'') ne '';
  }
  for %data.keys {
    %data{$_}:delete if %data{$_} eq '';
  }
  $response = Fez::Types::api-response.new(:!success);
  while ! ($response.success//False) {
    $response = update-meta(config-value('key'), %data<name> // '', %data<website> // '', %data<email> // '');

    last if $response.success;
    if ($response.message//'') eq 'expired' {
      log(ERROR, 'Key is expired, please login:');
      MAIN('login');
      reload-config;
      next;
    }
    my $error = $response.message // 'no reason';
    log(FATAL, "There was an error, please try again in a few minutes\nMessage from server: %s", $error);
  }
  log(MSG, 'Your meta info has been updated');
}

multi MAIN('o', 'l') is export {
  MAIN('org', 'list');
}
multi MAIN('org', 'list') is export {
  MAIN('login') unless config-value('key');
  if ! (config-value('key')//0) {
    log(FATAL, 'You must login to upload');
  }

  my $response = org-list(config-value('key'));
  if $response.success {
    log(MSG, 'Not a member of any orgs, yet') unless $response.groups;
    log(MSG, 'R Org Name') if $response.groups;
    for $response.groups -> $g {
      log(MSG, '%s %s', $g<role>.substr(0,1), $g<group>);
    }
  } else {
    log(FATAL, $response.message);
  }
}

multi MAIN('o', 'meta', Str $org-name, Str :n(:$name) is copy, Str :w(:$website) is copy, Str :e(:$email) is copy) is export {
  MAIN('o', 'meta', $org-name, :$name, :$website, :$email);
}
multi MAIN('org', 'meta', Str $org-name, Str :n(:$name) is copy, Str :w(:$website) is copy, Str :e(:$email) is copy) is export {
  MAIN('login') unless config-value('key');
  if ! (config-value('key')//0) {
    log(FATAL, 'You must login to change your org\'s info');
  }
  my %data;
  if ($name//'') eq '' && ($website//'') eq '' && ($email//'') eq '' {
    %data<name>    = prompt-wrapper('>>= What would you like your display name to show? ').trim;
    %data<website> = prompt-wrapper('>>= What\'s your website? ').trim;
    %data<email>   = prompt-wrapper('>>= Public email address? ').trim;
  } else {
    %data<name> = $name if ($name//'') ne '';
    %data<website> = $website if ($website//'') ne '';
    %data<email> = $email if ($email//'') ne '';
  }
  for %data.keys {
    %data{$_}:delete if %data{$_} eq '';
  }
  unless +%data.keys {
    log(MSG, 'Nothing to update');
  }
  %data<org> = $org-name;
  my $response = Fez::Types::api-response.new(:!success);
  while ! ($response.success//False) {
    $response = try update-org-meta(config-value('key'), $org-name, %data<name>, %data<website>, %data<email>);

    last if $response.success;
    if ($response.message//'') eq 'expired' {
      log(ERROR, '=<< Key is expired, please login:');
      MAIN('login');
      reload-config;
      next;
    }
    my $error = $response.message // 'no reason';
    log(FATAL, 'There was an error: %s', $error);
  }
  log(MSG, '%s\'s meta info has been updated', $org-name);
}

multi MAIN('up', Str :i(:$file) = '', Bool :d(:$dry-run) = False, Bool :s(:$save-autobundle) = False, Bool :f(:$force) = False) is export {
  MAIN('upload', :i($file), :s($save-autobundle), :f($force));
}
multi MAIN('upload', Str :i(:$file) = '', Bool :d(:$dry-run) = False,  Bool :s(:$save-autobundle) = False, Bool :f(:$force) = False) is export {
  MAIN('login') unless config-value('key');
  if ! (config-value('key')//0) {
    log(FATAL, 'You must login to upload');
  }
  my $fn = $file;
  try {
    CATCH { default { log(FATAL, 'Bundling error: %s', .message); } }
    if !$file {
      our $*DIST = True;
      my $has-error = MAIN('review');
      if $has-error && !$force {
        my $resp = prompt-wrapper('>>= Upload anyway (y/N)? ') while ($resp//' ').lc !~~ any('y'|'ye'|'yes'|'n'|'no'|'');
        if $resp.lc ~~ any('n'|'no'|'') {
          log(FATAL, 'Ok, exiting');
        }
      }
      $fn = bundle('.'.IO.absolute);
    } elsif ! $file.IO.f {
      log(FATAL, 'Cannot find %s', $file);
    }
  };

  if $dry-run {
    log(INFO, 'Exiting now because --dry-run (or -d) was supplied');
    exit 0;
  }

  my $response = Fez::Types::api-response.new(:!success);
  while ! ($response.success//False) {
    $response = direct-upload(config-value('key'), $fn.IO);

    last if $response.success;
    if ($response.message//'') eq 'expired' {
      log(ERROR, 'Key is expired, please login:');
      MAIN('login');
      reload-config;
      next;
    }
    my $error = $response.message // 'no reason';
    log(FATAL, "Something went wrong: %s.\nDo you need to run 'fez login' again?", $error);
  }

  if '' eq $file && !$save-autobundle {
    try {
      CATCH { default {
        log(ERROR, 'Failed to remove temporary file %s: %s', $fn.relative, $_);
      } }
      $fn.unlink;
    }
    if $fn.parent.dir.elems == 0 {
      try {
        CATCH { default {
          log(ERROR, 'Failed to remove directory %s: %s', $fn.parent.relative, $_);
        } }
        $fn.parent.rmdir;
      }
    }
  }
  log(MSG, 'Hey! You did it! Your dist will be indexed shortly.');
}


multi MAIN('ls', Str $name?, Str() :$url = '/index.json') is export {
  MAIN('list', $name, :$url);
}
multi MAIN('list', Str $name?, Str() :$url = '/index.json') is export {
  MAIN('login') unless config-value('key');
  my $show-login = False;
  my $response = org-list(config-value('key'));
  if ! $response.success {
    log(ERROR, 'Failed to retrieve user orgs, the following list may be incomplete');
    $show-login = True;
  }
  if $response.success {
    write-to-user-config({ groups => $response.groups });
  } else {
    log(ERROR, 'Failed to update config');
  }
  my @auths = ["{current-prefix}{config-value('un')}", |($response.groups//()).map({"{current-prefix}{$_<group>}"})];
  my @dists = (get($url)||[]).grep({$_<auth> (elem) @auths})
                             .grep({!$name.defined || $_<name>.lc.index($name.lc) !~~ Nil})
                             .sort({$^a<name>.lc cmp $^b<name>.lc ~~ Same
                                      ?? Version.new($^a<ver>//$^a<vers>//$^a<version>) cmp
                                         Version.new($^b<ver>//$^b<vers>//$^b<version>)
                                      !! $^a<name>.lc cmp $^b<name>.lc})
                             .map({$_<dist>});
  if +@dists {
    log(MSG, $_) for @dists;
  } else {
    log(MSG, 'No results');
  }
  log(WARN, 'A login may be required to see updated results') if $show-login;
}

multi MAIN('rm', Str $dist, Str() :$url = '/index.json') is export {
  MAIN('remove', $dist, :$url);
}
multi MAIN('remove', Str $dist, Str() :$url = '/index.json') is export {
  my $response = org-list(config-value('key'));
  if ! $response.success {
    log(ERROR, 'Failed to retrieve user orgs, the following list may be incomplete');
  }
  if $response.success {
    write-to-user-config({ groups => $response<groups> });
  } else {
    log(ERROR, 'Failed to update config');
  }
  my @auths = ["{current-prefix}{config-value('un')}", |@($response.groups//[]).map({"{current-prefix}{$_<group>}"})];
  my $d = (get($url)||[]).grep({$_<auth> (elem) @auths})
                            .grep({$dist eq $_<dist>})
                            .first;
  if !$d || !$d<path> {
    log(FATAL, 'Couldn\'t find %s', $dist);
  }
  $response = remove(config-value('key'), $d<dist>);
  if $response.success {
    log(MSG, 'Request received, if it is past the 24 hour window for removing modules then the module will remain in the ecosystem and indexed');
  } else {
    log(FATAL, 'Error processing request');
  }
}

multi MAIN('p', Bool :a(:$all) = False) is export {
  MAIN('plugin', :a($all));
}
multi MAIN('plugin', Bool :a(:$all) = False) is export {
  my @base = qw<bundlers requestors>;
  my $user-config = user-config;
  my @keys = $all ?? $user-config.keys.sort !! @base.grep({ $user-config{$_}.defined && +($user-config{$_}) });
  log(MSG, 'User config: %s', user-config-path) if +@keys;
  for @keys -> $k {
    log(MSG, '   %s:', $k);
    log(MSG, '     %s', $user-config{$k});
  }
  my $env-config = env-config;
  @keys = $all ?? $env-config.keys.sort !! @base.grep({ $env-config{$_}.defined && +($env-config{$_}) });
  log(MSG, 'Environment config: %s', env-config-path) if +@keys;
  for @keys -> $k {
    log(MSG, '   %s:', $k);
    log(MSG, '     %s', $env-config{$k}.join("\n     "));
  }
}

multi MAIN('p', Str $key where * !~~ 'key'|'un', Str $action where * ~~ 'remove'|'append'|'prepend', Str $value) is export {
  MAIN('plugin', $key, $action, $value);
}
multi MAIN('plugin', Str $key where * !~~ 'key'|'un', Str $action where * ~~ 'remove'|'append'|'prepend', Str $value) is export {
  if $action ~~ 'append'|'prepend' {
    my $cfg = user-config{$key}//[];
    write-to-user-config($key => $cfg.^can($action).first.($cfg, $value));
    log(MSG, 'Added %s.\'%s\'', $key, $value);
  } else {
    my $cfg = user-config{$key}//[];
    $cfg = $cfg.grep(* ne $value).unique;
    write-to-user-config($key => $cfg);
    log(MSG, 'Removed %s.\'%s\'', $key, $value);
  }
}

constant template-repl-vals = {
  '$user-config-path'   => user-config-path,
  '$system-config-path' => env-config-path,
  '$effective-config'   => to-j(%(|user-config, |env-config)),
};
sub template-repl(Str:D $in is copy, %replacements = template-repl-vals --> Str) {
  for %replacements.pairs -> $p {
    $in = $in.subst($p.key, $p.value, :g);
  }

  $in;
}

constant \HELP-HEADER = 'Fez - Raku dist manager';
multi MAIN('h') is export { MAIN(:help); }
multi MAIN('help') is export { MAIN(:help); }
multi MAIN(Bool :h(:$help)?) is export {
  say HELP-HEADER ~ "\n";
  say template-repl(%?RESOURCES<usage/_>.slurp);
}

multi USAGE is export {
  my (@usage, $rx);
  my @keys  = |@*ARGS.grep({!$_.starts-with('-')});
  my @as;
  my @max-usage;
  if @keys.elems >= 1 {
    my @usages = $?DISTRIBUTION.meta<resources>.grep(* ~~ /^'usage/'/).map(*.substr(6)).grep(*.defined);
    @max-usage = @usages.grep({$_ eq @keys[0..min(+$_.indices('_'), +@keys-1)].join('_')});
    if +@max-usage && +@keys > 1 {
      @max-usage.=sort({ $^a.indices('_') cmp $^b.indices('_') });
      @max-usage.=tail;
    }
    @max-usage .=map({"usage/{$_}"});
  }
  if @max-usage == 0 {
    for