From 680dd6981bc37d083d94630fd1eefafb73832fe0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 15:18:18 +0000 Subject: [PATCH 1/5] Initial plan From ae1b76527e816e3cfee85af612d06ca9e7386f1a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 15:26:50 +0000 Subject: [PATCH 2/5] Restore validation exit for large inputs --- src/Clet/Hosting/CletCliHost.cs | 219 ++++++++++++++++++++++++ src/Clet/Hosting/CletExitCodes.cs | 16 ++ src/Clet/Hosting/Program.cs | 2 +- tests/Clet.SmokeTests/CletSmokeTests.cs | 2 +- tests/Clet.UnitTests/ExitCodesTests.cs | 7 +- 5 files changed, 239 insertions(+), 7 deletions(-) create mode 100644 src/Clet/Hosting/CletCliHost.cs create mode 100644 src/Clet/Hosting/CletExitCodes.cs diff --git a/src/Clet/Hosting/CletCliHost.cs b/src/Clet/Hosting/CletCliHost.cs new file mode 100644 index 0000000..6272925 --- /dev/null +++ b/src/Clet/Hosting/CletCliHost.cs @@ -0,0 +1,219 @@ +using Terminal.Gui.App; +using Terminal.Gui.Cli; +using Terminal.Gui.Time; + +namespace Clet; + +internal sealed class CletCliHost +{ + private readonly IHelpProvider _helpProvider; + private readonly int _maxInitialChars; + private readonly CliHostOptions _options; + private readonly ArgParser _parser; + + public CletCliHost (Action? configure = null) + { + _options = new (); + configure?.Invoke (_options); + + _helpProvider = _options.HelpProvider ?? new MetadataHelpProvider (); + _maxInitialChars = _options.MaxInitialChars; + Registry = new CommandRegistry (); + RegisterBuiltIns (); + + // Parse without the package's --initial cap so clet can map known-command + // oversized input through its own validation error path instead of usage. + _parser = new (_options.GlobalOptions, int.MaxValue); + } + + public ICommandRegistry Registry { get; } + + public async Task RunAsync ( + string [] args, + CancellationToken cancellationToken = default, + TextWriter? stdout = null, + TextWriter? stderr = null) + { + stdout ??= Console.Out; + stderr ??= Console.Error; + + ArgParser.ParseResult parseResult = _parser.Parse (args); + + if (!parseResult.Success) + { + stderr.WriteLine (parseResult.Error); + + return ExitCodes.UsageError; + } + + if (parseResult.RootFlag is { } rootFlag) + { + WriteRootFlag (rootFlag, stdout); + + return ExitCodes.Ok; + } + + if (parseResult.Alias is null + || !Registry.TryResolve (parseResult.Alias, out ICliCommand? command) + || command is null) + { + stderr.WriteLine ($"Unknown command '{parseResult.Alias}'."); + + return ExitCodes.UsageError; + } + + if (parseResult.Options is not null + && parseResult.Initial is { } initial + && initial.Length > _maxInitialChars) + { + return WriteInputTooLargeResult (parseResult.Options, stdout, stderr); + } + + return await DispatchCommandAsync (args, command, cancellationToken, stdout, stderr); + } + + private async Task DispatchCommandAsync ( + string [] args, + ICliCommand command, + CancellationToken cancellationToken, + TextWriter stdout, + TextWriter stderr) + { + ArgParser.ParseResult parseResult = _parser.Parse (args, command); + + if (!parseResult.Success || parseResult.Options is null) + { + stderr.WriteLine (parseResult.Error); + + return ExitCodes.UsageError; + } + + return await ExecuteCommandAsync (command, parseResult.Options, cancellationToken, stdout, stderr); + } + + private async Task ExecuteCommandAsync ( + ICliCommand command, + CommandRunOptions runOptions, + CancellationToken cancellationToken, + TextWriter stdout, + TextWriter stderr) + { + if (runOptions.Initial is { } initial && initial.Length > _maxInitialChars) + { + return WriteInputTooLargeResult (runOptions, stdout, stderr); + } + + if (runOptions.Initial is not null && !command.TryValidateInitial (runOptions.Initial, runOptions)) + { + stderr.WriteLine ("Invalid --initial value."); + + return ExitCodes.ValidationError; + } + + using CancellationTokenSource? timeoutSource = runOptions.Timeout.HasValue + ? CancellationTokenSource.CreateLinkedTokenSource (cancellationToken) + : null; + + if (timeoutSource is not null) + { + timeoutSource.CancelAfter (runOptions.Timeout.GetValueOrDefault ()); + } + + CancellationToken effectiveToken = timeoutSource?.Token ?? cancellationToken; + + if (command is IViewerCommand viewerCommand && runOptions.Cat) + { + CommandResult? commandResult; + + try + { + commandResult = await viewerCommand.RenderCatAsync (runOptions, stdout, effectiveToken); + } + catch (OperationCanceledException) + { + return ExitCodes.Cancelled; + } + + if (commandResult.HasValue) + { + return CletExitCodes.FromResult (commandResult.Value); + } + } + + CommandResult runResult; + + try + { + runResult = await RunWithTerminalGuiAsync (command, runOptions, effectiveToken); + } + catch (OperationCanceledException) + { + runResult = new (CommandStatus.Cancelled, null, null, null); + } + + if (!ResultWriter.Write (runResult, runOptions.JsonOutput, stdout, stderr, runOptions.OutputPath)) + { + return ExitCodes.UsageError; + } + + return CletExitCodes.FromResult (runResult); + } + + private int WriteInputTooLargeResult (CommandRunOptions runOptions, TextWriter stdout, TextWriter stderr) + { + CommandResult result = new ( + CommandStatus.Error, + null, + "input-too-large", + $"--initial exceeds the maximum length of {_maxInitialChars} characters."); + + ResultWriter.Write (result, runOptions.JsonOutput, stdout, stderr, runOptions.OutputPath); + + return CletExitCodes.FromResult (result); + } + + private async Task RunWithTerminalGuiAsync ( + ICliCommand command, + CommandRunOptions runOptions, + CancellationToken cancellationToken) + { + IApplication app = Application.Create ((ITimeProvider?)null).Init ((string?)null); + + try + { + return await command.RunAsync (app, runOptions.Initial, runOptions, cancellationToken); + } + finally + { + (app as IDisposable)?.Dispose (); + } + } + + private void WriteRootFlag (ArgParser.RootFlag rootFlag, TextWriter stdout) + { + switch (rootFlag) + { + case ArgParser.RootFlag.Help: + MarkdownRenderer.RenderToAnsi ( + _helpProvider.GetRootHelp (Registry) + ?? new MetadataHelpProvider ().GetRootHelp (Registry) + ?? string.Empty, + stdout); + + break; + case ArgParser.RootFlag.Version: + stdout.WriteLine ($"{_options.ApplicationName} {_options.Version ?? "0.0.0"}"); + + break; + case ArgParser.RootFlag.OpenCli: + stdout.WriteLine (OpenCliWriter.Generate (Registry, _options)); + + break; + } + } + + private void RegisterBuiltIns () + { + Registry.Register (new HelpCommand (Registry, _helpProvider)); + } +} diff --git a/src/Clet/Hosting/CletExitCodes.cs b/src/Clet/Hosting/CletExitCodes.cs new file mode 100644 index 0000000..bfef08d --- /dev/null +++ b/src/Clet/Hosting/CletExitCodes.cs @@ -0,0 +1,16 @@ +using Terminal.Gui.Cli; + +namespace Clet; + +internal static class CletExitCodes +{ + public static int FromResult (CommandResult result) + { + if (result.Status == CommandStatus.Error && result.ErrorCode == "input-too-large") + { + return ExitCodes.ValidationError; + } + + return ExitCodes.FromResult (result); + } +} diff --git a/src/Clet/Hosting/Program.cs b/src/Clet/Hosting/Program.cs index 3bb4837..5986252 100644 --- a/src/Clet/Hosting/Program.cs +++ b/src/Clet/Hosting/Program.cs @@ -8,7 +8,7 @@ public static async Task Main (string[] args) { CletLogging.Initialize (); - CliHost host = new (options => + CletCliHost host = new (options => { options.ApplicationName = "clet"; options.Version = VersionInfo.GetCletVersion (); diff --git a/tests/Clet.SmokeTests/CletSmokeTests.cs b/tests/Clet.SmokeTests/CletSmokeTests.cs index c3b689d..b46d6e7 100644 --- a/tests/Clet.SmokeTests/CletSmokeTests.cs +++ b/tests/Clet.SmokeTests/CletSmokeTests.cs @@ -111,7 +111,7 @@ public async Task MdOversizedStdin_ExitsWithError () (int exit, string stdout, _) = await CletProcess.RunAsync ( ["md", "--json"], stdin: oversized); - Assert.Equal (2, exit); + Assert.Equal (65, exit); Assert.Contains ("input-too-large", stdout); Assert.Contains ("\"status\":\"error\"", stdout); } diff --git a/tests/Clet.UnitTests/ExitCodesTests.cs b/tests/Clet.UnitTests/ExitCodesTests.cs index 280e377..99199d0 100644 --- a/tests/Clet.UnitTests/ExitCodesTests.cs +++ b/tests/Clet.UnitTests/ExitCodesTests.cs @@ -22,16 +22,13 @@ public void Constants_MatchSpec () [InlineData ((int)CommandStatus.Cancelled, null, 130)] [InlineData ((int)CommandStatus.NoResult, null, 1)] [InlineData ((int)CommandStatus.Error, "validation", 65)] - // NOTE: spec requires input-too-large → 65, but the package maps all unknown error codes to 2. - // Tracked upstream: Terminal.Gui.Cli needs a custom exit code mapper or treating - // "input-too-large" as a validation error. See PR #185 review discussion. - [InlineData ((int)CommandStatus.Error, "input-too-large", 2)] + [InlineData ((int)CommandStatus.Error, "input-too-large", 65)] [InlineData ((int)CommandStatus.Error, "io", 74)] [InlineData ((int)CommandStatus.Error, "anything-else", 2)] public void FromResult_MapsStatusToExit (int statusInt, string? errorCode, int expected) { CommandResult result = new ((CommandStatus)statusInt, null, errorCode, null); - Assert.Equal (expected, ExitCodes.FromResult (result)); + Assert.Equal (expected, CletExitCodes.FromResult (result)); } } From dcec64ef9642bd413d784a889581fc044f16b0a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 15:28:05 +0000 Subject: [PATCH 3/5] Fix host formatting --- src/Clet/Hosting/CletCliHost.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Clet/Hosting/CletCliHost.cs b/src/Clet/Hosting/CletCliHost.cs index 6272925..a397696 100644 --- a/src/Clet/Hosting/CletCliHost.cs +++ b/src/Clet/Hosting/CletCliHost.cs @@ -29,7 +29,7 @@ public CletCliHost (Action? configure = null) public ICommandRegistry Registry { get; } public async Task RunAsync ( - string [] args, + string[] args, CancellationToken cancellationToken = default, TextWriter? stdout = null, TextWriter? stderr = null) @@ -73,7 +73,7 @@ public async Task RunAsync ( } private async Task DispatchCommandAsync ( - string [] args, + string[] args, ICliCommand command, CancellationToken cancellationToken, TextWriter stdout, From 66003d34b38247f849e6c221fa423ac902e7c8c6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 15:32:37 +0000 Subject: [PATCH 4/5] Avoid duplicating CLI host --- src/Clet/Hosting/CletCliHost.cs | 219 ------------------------------ src/Clet/Hosting/CletExitCodes.cs | 12 ++ src/Clet/Hosting/Program.cs | 88 +++++++++++- 3 files changed, 98 insertions(+), 221 deletions(-) delete mode 100644 src/Clet/Hosting/CletCliHost.cs diff --git a/src/Clet/Hosting/CletCliHost.cs b/src/Clet/Hosting/CletCliHost.cs deleted file mode 100644 index a397696..0000000 --- a/src/Clet/Hosting/CletCliHost.cs +++ /dev/null @@ -1,219 +0,0 @@ -using Terminal.Gui.App; -using Terminal.Gui.Cli; -using Terminal.Gui.Time; - -namespace Clet; - -internal sealed class CletCliHost -{ - private readonly IHelpProvider _helpProvider; - private readonly int _maxInitialChars; - private readonly CliHostOptions _options; - private readonly ArgParser _parser; - - public CletCliHost (Action? configure = null) - { - _options = new (); - configure?.Invoke (_options); - - _helpProvider = _options.HelpProvider ?? new MetadataHelpProvider (); - _maxInitialChars = _options.MaxInitialChars; - Registry = new CommandRegistry (); - RegisterBuiltIns (); - - // Parse without the package's --initial cap so clet can map known-command - // oversized input through its own validation error path instead of usage. - _parser = new (_options.GlobalOptions, int.MaxValue); - } - - public ICommandRegistry Registry { get; } - - public async Task RunAsync ( - string[] args, - CancellationToken cancellationToken = default, - TextWriter? stdout = null, - TextWriter? stderr = null) - { - stdout ??= Console.Out; - stderr ??= Console.Error; - - ArgParser.ParseResult parseResult = _parser.Parse (args); - - if (!parseResult.Success) - { - stderr.WriteLine (parseResult.Error); - - return ExitCodes.UsageError; - } - - if (parseResult.RootFlag is { } rootFlag) - { - WriteRootFlag (rootFlag, stdout); - - return ExitCodes.Ok; - } - - if (parseResult.Alias is null - || !Registry.TryResolve (parseResult.Alias, out ICliCommand? command) - || command is null) - { - stderr.WriteLine ($"Unknown command '{parseResult.Alias}'."); - - return ExitCodes.UsageError; - } - - if (parseResult.Options is not null - && parseResult.Initial is { } initial - && initial.Length > _maxInitialChars) - { - return WriteInputTooLargeResult (parseResult.Options, stdout, stderr); - } - - return await DispatchCommandAsync (args, command, cancellationToken, stdout, stderr); - } - - private async Task DispatchCommandAsync ( - string[] args, - ICliCommand command, - CancellationToken cancellationToken, - TextWriter stdout, - TextWriter stderr) - { - ArgParser.ParseResult parseResult = _parser.Parse (args, command); - - if (!parseResult.Success || parseResult.Options is null) - { - stderr.WriteLine (parseResult.Error); - - return ExitCodes.UsageError; - } - - return await ExecuteCommandAsync (command, parseResult.Options, cancellationToken, stdout, stderr); - } - - private async Task ExecuteCommandAsync ( - ICliCommand command, - CommandRunOptions runOptions, - CancellationToken cancellationToken, - TextWriter stdout, - TextWriter stderr) - { - if (runOptions.Initial is { } initial && initial.Length > _maxInitialChars) - { - return WriteInputTooLargeResult (runOptions, stdout, stderr); - } - - if (runOptions.Initial is not null && !command.TryValidateInitial (runOptions.Initial, runOptions)) - { - stderr.WriteLine ("Invalid --initial value."); - - return ExitCodes.ValidationError; - } - - using CancellationTokenSource? timeoutSource = runOptions.Timeout.HasValue - ? CancellationTokenSource.CreateLinkedTokenSource (cancellationToken) - : null; - - if (timeoutSource is not null) - { - timeoutSource.CancelAfter (runOptions.Timeout.GetValueOrDefault ()); - } - - CancellationToken effectiveToken = timeoutSource?.Token ?? cancellationToken; - - if (command is IViewerCommand viewerCommand && runOptions.Cat) - { - CommandResult? commandResult; - - try - { - commandResult = await viewerCommand.RenderCatAsync (runOptions, stdout, effectiveToken); - } - catch (OperationCanceledException) - { - return ExitCodes.Cancelled; - } - - if (commandResult.HasValue) - { - return CletExitCodes.FromResult (commandResult.Value); - } - } - - CommandResult runResult; - - try - { - runResult = await RunWithTerminalGuiAsync (command, runOptions, effectiveToken); - } - catch (OperationCanceledException) - { - runResult = new (CommandStatus.Cancelled, null, null, null); - } - - if (!ResultWriter.Write (runResult, runOptions.JsonOutput, stdout, stderr, runOptions.OutputPath)) - { - return ExitCodes.UsageError; - } - - return CletExitCodes.FromResult (runResult); - } - - private int WriteInputTooLargeResult (CommandRunOptions runOptions, TextWriter stdout, TextWriter stderr) - { - CommandResult result = new ( - CommandStatus.Error, - null, - "input-too-large", - $"--initial exceeds the maximum length of {_maxInitialChars} characters."); - - ResultWriter.Write (result, runOptions.JsonOutput, stdout, stderr, runOptions.OutputPath); - - return CletExitCodes.FromResult (result); - } - - private async Task RunWithTerminalGuiAsync ( - ICliCommand command, - CommandRunOptions runOptions, - CancellationToken cancellationToken) - { - IApplication app = Application.Create ((ITimeProvider?)null).Init ((string?)null); - - try - { - return await command.RunAsync (app, runOptions.Initial, runOptions, cancellationToken); - } - finally - { - (app as IDisposable)?.Dispose (); - } - } - - private void WriteRootFlag (ArgParser.RootFlag rootFlag, TextWriter stdout) - { - switch (rootFlag) - { - case ArgParser.RootFlag.Help: - MarkdownRenderer.RenderToAnsi ( - _helpProvider.GetRootHelp (Registry) - ?? new MetadataHelpProvider ().GetRootHelp (Registry) - ?? string.Empty, - stdout); - - break; - case ArgParser.RootFlag.Version: - stdout.WriteLine ($"{_options.ApplicationName} {_options.Version ?? "0.0.0"}"); - - break; - case ArgParser.RootFlag.OpenCli: - stdout.WriteLine (OpenCliWriter.Generate (Registry, _options)); - - break; - } - } - - private void RegisterBuiltIns () - { - Registry.Register (new HelpCommand (Registry, _helpProvider)); - } -} diff --git a/src/Clet/Hosting/CletExitCodes.cs b/src/Clet/Hosting/CletExitCodes.cs index bfef08d..f5a3e08 100644 --- a/src/Clet/Hosting/CletExitCodes.cs +++ b/src/Clet/Hosting/CletExitCodes.cs @@ -4,6 +4,8 @@ namespace Clet; internal static class CletExitCodes { + private const string InputTooLargeJsonCode = "\"code\":\"input-too-large\""; + public static int FromResult (CommandResult result) { if (result.Status == CommandStatus.Error && result.ErrorCode == "input-too-large") @@ -13,4 +15,14 @@ public static int FromResult (CommandResult result) return ExitCodes.FromResult (result); } + + public static int MapPackageExit (int exitCode, string stdout) + { + if (exitCode == ExitCodes.UsageError && stdout.Contains (InputTooLargeJsonCode, StringComparison.Ordinal)) + { + return ExitCodes.ValidationError; + } + + return exitCode; + } } diff --git a/src/Clet/Hosting/Program.cs b/src/Clet/Hosting/Program.cs index 5986252..7a5bb67 100644 --- a/src/Clet/Hosting/Program.cs +++ b/src/Clet/Hosting/Program.cs @@ -7,8 +7,9 @@ internal static class Program public static async Task Main (string[] args) { CletLogging.Initialize (); + int maxInitialChars = new CliHostOptions ().MaxInitialChars; - CletCliHost host = new (options => + CliHost host = new (options => { options.ApplicationName = "clet"; options.Version = VersionInfo.GetCletVersion (); @@ -26,6 +27,89 @@ public static async Task Main (string[] args) BuiltInCommands.RegisterAll (host.Registry); - return await host.RunAsync (args); + if (TryHandleOversizedInitial (args, host.Registry, maxInitialChars, out int exitCode)) + { + return exitCode; + } + + using StringWriter stdout = new (); + using StringWriter stderr = new (); + + int packageExitCode = await host.RunAsync (args, stdout: stdout, stderr: stderr); + string stdoutText = stdout.ToString (); + string stderrText = stderr.ToString (); + + Console.Out.Write (stdoutText); + Console.Error.Write (stderrText); + + return CletExitCodes.MapPackageExit (packageExitCode, stdoutText); + } + + private static bool TryHandleOversizedInitial ( + string[] args, + ICommandRegistry registry, + int maxInitialChars, + out int exitCode) + { + exitCode = ExitCodes.UsageError; + string? alias = null; + string? initial = null; + bool jsonOutput = false; + + for (int i = 0; i < args.Length; i++) + { + string arg = args[i]; + + switch (arg) + { + case "--json": + jsonOutput = true; + + continue; + case "--initial" when i + 1 < args.Length: + initial = args[++i]; + + continue; + case "--initial": + return false; + } + + if (alias is null) + { + if (arg.StartsWith ('-')) + { + if (OptionConsumesValue (arg)) + { + i++; + } + + continue; + } + + alias = arg; + } + } + + if (alias is null + || initial is null + || initial.Length <= maxInitialChars + || !registry.TryResolve (alias, out _)) + { + return false; + } + + CommandResult result = new ( + CommandStatus.Error, + null, + "input-too-large", + $"--initial exceeds the maximum length of {maxInitialChars} characters."); + + ResultWriter.Write (result, jsonOutput, Console.Out, Console.Error); + exitCode = CletExitCodes.FromResult (result); + + return true; } + + private static bool OptionConsumesValue (string arg) => + arg is "--prompt" or "--title" or "-p" or "-t" or "--timeout" or "--output" or "-o" or "--rows" or "--allow-file"; } From 1ce9817183482c05c75a4b8e808c45b738dce3c8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 15:36:39 +0000 Subject: [PATCH 5/5] Address validation review feedback --- src/Clet/Hosting/CletExitCodes.cs | 21 ++++++- src/Clet/Hosting/Program.cs | 101 ++++++++++++------------------ 2 files changed, 59 insertions(+), 63 deletions(-) diff --git a/src/Clet/Hosting/CletExitCodes.cs b/src/Clet/Hosting/CletExitCodes.cs index f5a3e08..8819af1 100644 --- a/src/Clet/Hosting/CletExitCodes.cs +++ b/src/Clet/Hosting/CletExitCodes.cs @@ -1,11 +1,10 @@ +using System.Text.Json; using Terminal.Gui.Cli; namespace Clet; internal static class CletExitCodes { - private const string InputTooLargeJsonCode = "\"code\":\"input-too-large\""; - public static int FromResult (CommandResult result) { if (result.Status == CommandStatus.Error && result.ErrorCode == "input-too-large") @@ -18,11 +17,27 @@ public static int FromResult (CommandResult result) public static int MapPackageExit (int exitCode, string stdout) { - if (exitCode == ExitCodes.UsageError && stdout.Contains (InputTooLargeJsonCode, StringComparison.Ordinal)) + if (exitCode == ExitCodes.UsageError && HasInputTooLargeCode (stdout)) { return ExitCodes.ValidationError; } return exitCode; } + + private static bool HasInputTooLargeCode (string stdout) + { + try + { + using JsonDocument document = JsonDocument.Parse (stdout); + + return document.RootElement.TryGetProperty ("code", out JsonElement code) + && code.ValueKind == JsonValueKind.String + && code.GetString () == "input-too-large"; + } + catch (JsonException) + { + return false; + } + } } diff --git a/src/Clet/Hosting/Program.cs b/src/Clet/Hosting/Program.cs index 7a5bb67..ef24443 100644 --- a/src/Clet/Hosting/Program.cs +++ b/src/Clet/Hosting/Program.cs @@ -7,27 +7,13 @@ internal static class Program public static async Task Main (string[] args) { CletLogging.Initialize (); - int maxInitialChars = new CliHostOptions ().MaxInitialChars; + CliHostOptions parserOptions = CreateOptions (); - CliHost host = new (options => - { - options.ApplicationName = "clet"; - options.Version = VersionInfo.GetCletVersion (); - options.HelpProvider = new CletHelpProvider (); - options.ResourceAssembly = typeof (Program).Assembly; - - // clet-specific global options - options.GlobalOptions.Add (new GlobalOptionDescriptor ("allow-file", null, - "Explicitly allow reading a file path (bypasses extension + cwd checks).", false, Repeatable: true)); - options.GlobalOptions.Add (new GlobalOptionDescriptor ("allow-binary", null, - "Permit binary file content (NUL bytes).", true)); - options.GlobalOptions.Add (new GlobalOptionDescriptor ("no-browse", null, - "Disable browser-mode navigation for viewer clets.", true)); - }); + CliHost host = new (ConfigureOptions); BuiltInCommands.RegisterAll (host.Registry); - if (TryHandleOversizedInitial (args, host.Registry, maxInitialChars, out int exitCode)) + if (TryHandleOversizedInitial (args, host.Registry, parserOptions, out int exitCode)) { return exitCode; } @@ -48,52 +34,26 @@ public static async Task Main (string[] args) private static bool TryHandleOversizedInitial ( string[] args, ICommandRegistry registry, - int maxInitialChars, + CliHostOptions options, out int exitCode) { exitCode = ExitCodes.UsageError; - string? alias = null; - string? initial = null; - bool jsonOutput = false; + ArgParser parser = new (options.GlobalOptions, int.MaxValue); + ArgParser.ParseResult rootParse = parser.Parse (args); - for (int i = 0; i < args.Length; i++) + if (!rootParse.Success + || rootParse.RootFlag is not null + || rootParse.Alias is null + || !registry.TryResolve (rootParse.Alias, out _)) { - string arg = args[i]; - - switch (arg) - { - case "--json": - jsonOutput = true; - - continue; - case "--initial" when i + 1 < args.Length: - initial = args[++i]; - - continue; - case "--initial": - return false; - } - - if (alias is null) - { - if (arg.StartsWith ('-')) - { - if (OptionConsumesValue (arg)) - { - i++; - } - - continue; - } - - alias = arg; - } + return false; } - if (alias is null - || initial is null - || initial.Length <= maxInitialChars - || !registry.TryResolve (alias, out _)) + CommandRunOptions? runOptions = rootParse.Options; + + if (runOptions is null + || runOptions.Initial is null + || runOptions.Initial.Length <= options.MaxInitialChars) { return false; } @@ -102,14 +62,35 @@ private static bool TryHandleOversizedInitial ( CommandStatus.Error, null, "input-too-large", - $"--initial exceeds the maximum length of {maxInitialChars} characters."); + $"--initial exceeds the maximum length of {options.MaxInitialChars} characters."); - ResultWriter.Write (result, jsonOutput, Console.Out, Console.Error); + ResultWriter.Write (result, runOptions.JsonOutput, Console.Out, Console.Error, runOptions.OutputPath); exitCode = CletExitCodes.FromResult (result); return true; } - private static bool OptionConsumesValue (string arg) => - arg is "--prompt" or "--title" or "-p" or "-t" or "--timeout" or "--output" or "-o" or "--rows" or "--allow-file"; + private static CliHostOptions CreateOptions () + { + CliHostOptions options = new (); + ConfigureOptions (options); + + return options; + } + + private static void ConfigureOptions (CliHostOptions options) + { + options.ApplicationName = "clet"; + options.Version = VersionInfo.GetCletVersion (); + options.HelpProvider = new CletHelpProvider (); + options.ResourceAssembly = typeof (Program).Assembly; + + // clet-specific global options + options.GlobalOptions.Add (new GlobalOptionDescriptor ("allow-file", null, + "Explicitly allow reading a file path (bypasses extension + cwd checks).", false, Repeatable: true)); + options.GlobalOptions.Add (new GlobalOptionDescriptor ("allow-binary", null, + "Permit binary file content (NUL bytes).", true)); + options.GlobalOptions.Add (new GlobalOptionDescriptor ("no-browse", null, + "Disable browser-mode navigation for viewer clets.", true)); + } }