diff --git a/Source/LibationCli/Options/ListAccountsOptions.cs b/Source/LibationCli/Options/ListAccountsOptions.cs new file mode 100644 index 00000000..742cd7b8 --- /dev/null +++ b/Source/LibationCli/Options/ListAccountsOptions.cs @@ -0,0 +1,56 @@ +using AudibleUtilities; +using CommandLine; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace LibationCli; + +[Verb("list-accounts", HelpText = "List configured Audible accounts, locale, scan flag, and whether stored credentials are valid.")] +internal class ListAccountsOptions : OptionsBase +{ + [Option('b', "bare", HelpText = "Print tab-separated values without table borders (account id, name, locale, scan library, authenticated).")] + public bool Bare { get; set; } + + protected override Task ProcessAsync() + { + using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); + var accounts = persister.AccountsSettings.GetAll().ToArray(); + + if (accounts.Length == 0) + { + Console.WriteLine("No accounts configured."); + return Task.CompletedTask; + } + + var rows = accounts + .Select(a => new AccountListRow( + a.AccountId, + a.AccountName ?? "", + a.Locale?.Name ?? "", + a.LibraryScan ? "yes" : "no", + a.IdentityTokens?.IsValid == true ? "yes" : "no")) + .ToArray(); + + if (Bare) + { + foreach (var r in rows) + Console.WriteLine($"{r.AccountId}\t{r.AccountName}\t{r.Locale}\t{r.LibraryScan}\t{r.Authenticated}"); + } + else + { + Console.Out.DrawTable( + rows, + new TextTableOptions(), + new ColumnDef("Account ID", r => r.AccountId), + new ColumnDef("Name", r => r.AccountName), + new ColumnDef("Locale", r => r.Locale), + new ColumnDef("Scan library", r => r.LibraryScan), + new ColumnDef("Authenticated", r => r.Authenticated)); + } + + return Task.CompletedTask; + } + + private sealed record AccountListRow(string AccountId, string AccountName, string Locale, string LibraryScan, string Authenticated); +} diff --git a/Source/LibationCli/Program.cs b/Source/LibationCli/Program.cs index 03ff3776..444d530c 100644 --- a/Source/LibationCli/Program.cs +++ b/Source/LibationCli/Program.cs @@ -7,6 +7,20 @@ using System.Threading.Tasks; namespace LibationCli; +file static class GlobalCliHelp +{ + internal static bool IsGlobalHelpToken(string token) + { + if (string.IsNullOrWhiteSpace(token)) + return false; + return token.Equals("--help", StringComparison.OrdinalIgnoreCase) + || token.Equals("-h", StringComparison.OrdinalIgnoreCase) + || token.Equals("/?", StringComparison.OrdinalIgnoreCase) + || token.Equals("/h", StringComparison.OrdinalIgnoreCase) + || token.Equals("/help", StringComparison.OrdinalIgnoreCase); + } +} + public enum ExitCode { ProcessCompletedSuccessfully = 0, @@ -42,6 +56,11 @@ class Program var setBreakPointHere = args; #endif + if (TryPrintGlobalHelpOnly(args)) + return; + + args = NormalizeVerbShortHelpAliases(args); + var result = new Parser(ConfigureParser).ParseArguments(args, VerbTypes); if (result.Value is HelpVerb helper) @@ -67,9 +86,25 @@ class Program private static void HandleErrors(ParserResult result) { var errorsList = result.Errors.ToList(); - if (errorsList.Any(e => e.Tag.In(ErrorType.HelpRequestedError, ErrorType.VersionRequestedError, ErrorType.HelpVerbRequestedError))) + + if (errorsList.Any(e => e.Tag == ErrorType.HelpRequestedError)) { - Environment.ExitCode = (int)ExitCode.NonRunNonError; + Environment.ExitCode = (int)ExitCode.ProcessCompletedSuccessfully; + WriteVerbOptionsHelp(result); + return; + } + + if (errorsList.OfType().FirstOrDefault() is { } helpVerbErr) + { + Environment.ExitCode = (int)ExitCode.ProcessCompletedSuccessfully; + WriteHelpForVerbRequestedError(helpVerbErr); + return; + } + + if (errorsList.Any(e => e.Tag == ErrorType.VersionRequestedError)) + { + Environment.ExitCode = (int)ExitCode.ProcessCompletedSuccessfully; + Console.Error.WriteLine(HelpVerb.CreateHelpText().Heading); return; } @@ -100,10 +135,77 @@ class Program Console.Error.WriteLine(helpText); } + /// + /// Multi-verb parsing treats the first token as a verb name, so bare --help / -h must be handled here. + /// + private static bool TryPrintGlobalHelpOnly(string[] args) + { + if (args is not { Length: 1 } || !GlobalCliHelp.IsGlobalHelpToken(args[0])) + return false; + + WriteGlobalVerbListHelp(); + Environment.ExitCode = (int)ExitCode.ProcessCompletedSuccessfully; + return true; + } + + /// + /// CommandLineParser's implicit help is --help only; map the first -h after the verb (case-insensitive, so -H too) to --help. + /// + private static string[] NormalizeVerbShortHelpAliases(string[] args) + { + if (args.Length < 2) + return args; + + var copy = (string[])args.Clone(); + for (var i = 1; i < copy.Length; i++) + { + if (copy[i].Equals("-h", StringComparison.OrdinalIgnoreCase)) + { + copy[i] = "--help"; + break; + } + } + + return copy; + } + + private static void WriteGlobalVerbListHelp(string? preOptionsLine = null) + { + var helpText = HelpVerb.CreateHelpText(); + if (preOptionsLine is not null) + helpText.AddPreOptionsLine(preOptionsLine); + helpText.AddVerbs(VerbTypes); + Console.Error.WriteLine(helpText); + } + + private static void WriteVerbOptionsHelp(ParserResult result) + { + var helpText = HelpVerb.CreateHelpText(); + helpText.AddDashesToOption = true; + helpText.AutoHelp = true; + helpText.AddOptions(result); + Console.Error.WriteLine(helpText); + } + + private static void WriteHelpForVerbRequestedError(HelpVerbRequestedError helpVerbErr) + { + if (!helpVerbErr.Matched || helpVerbErr.Type is null || string.IsNullOrWhiteSpace(helpVerbErr.Verb)) + { + WriteGlobalVerbListHelp(); + return; + } + + var subResult = new Parser(ConfigureParser).ParseArguments(new[] { helpVerbErr.Verb }, VerbTypes); + if (subResult.TypeInfo.Current != typeof(NullInstance)) + WriteVerbOptionsHelp(subResult); + else + WriteGlobalVerbListHelp(); + } + private static void ConfigureParser(ParserSettings settings) { settings.AllowMultiInstance = true; settings.AutoVersion = false; - settings.AutoHelp = false; + settings.AutoHelp = true; } } diff --git a/docs/advanced/command-line-interface.md b/docs/advanced/command-line-interface.md index 6c58c6b1..91fcc423 100644 --- a/docs/advanced/command-line-interface.md +++ b/docs/advanced/command-line-interface.md @@ -2,7 +2,7 @@ Libationcli.exe allows limited access to Libation's functionalities as a CLI. -Warnings about relying solely on on the CLI: +Warnings about relying solely on the CLI: - CLI will not perform any upgrades. - It will show that there is an upgrade, but that will likely scroll by too fast to notice. @@ -23,16 +23,28 @@ Redirecting also avoids progress-bar control characters in log files. ## Help +LibationCli uses a **verb-first** layout: the first argument is always a command name (for example `scan`, `liberate`). + +**Global help** — list every verb and its short description (use this when you are not sure which verb to run): + ```console libationcli --help +libationcli -h ``` -## Verb-Specific Help +On Windows, `/?`, `/h`, and `/help` are also accepted when they are the only argument. + +**Verb-specific help** — options for a single command: ```console libationcli scan --help +libationcli scan -h ``` +The `help` verb is equivalent for many cases: `libationcli help scan`. + +Help-only invocations exit with status code **0** (success), so scripts can treat them as non-errors. + ## Libation files location All verbs use the same Libation data directory as the GUI (where `AccountsSettings.json` and `Settings.json` live). To point the CLI elsewhere: @@ -79,6 +91,19 @@ If the account row already has valid saved tokens, the CLI reports that no brows Use `libationcli login-external --help` for the exact options on your build. +## List configured accounts (`list-accounts`) + +Prints each row from `AccountsSettings.json`: Audible login id, optional nickname, marketplace (`us`, `uk`, …), whether **Scan library** is enabled for that account, and whether stored identity tokens are currently **valid** (the same check `login-external` uses before starting a browser flow). Use this on headless setups to see which accounts still need `login-external` or `import-account`. + +```console +libationcli list-accounts +libationcli list-accounts --bare +``` + +`--bare` (`-b`) prints tab-separated values with no table: account id, name, locale, scan library (`yes` / `no`), authenticated (`yes` / `no`), for scripts and `cut` / `awk`. + +If no accounts exist yet, the CLI prints `No accounts configured.` and exits successfully. + ## Scan All Libraries ```console diff --git a/docs/installation/docker.md b/docs/installation/docker.md index ff1a23bc..9f72161d 100644 --- a/docs/installation/docker.md +++ b/docs/installation/docker.md @@ -23,6 +23,8 @@ If you run Libation on a server or in Docker and do not want to copy `AccountsSe - `login-external` — Browser-based sign-in: the CLI prints an Audible login URL; you open it in a normal browser, sign in, then paste the final URL from the address bar back into the terminal. Example: `LibationCli login-external --account you@example.com --locale us` If standard input is not a TTY (for example in some automation), pass the final URL with `--response-url "https://..."` instead of pasting interactively. +- `list-accounts` — List configured accounts and whether each has valid stored credentials (and scan-on/off). Example: + `LibationCli list-accounts` or `LibationCli list-accounts --bare` for tab-separated output. For full syntax, overrides, and the `--libationFiles` option (or the `LIBATION_FILES_DIR` environment variable) when your Libation data directory is not the default, see [Command Line Interface](/docs/advanced/command-line-interface).