Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/Clet/Hosting/CletExitCodes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Text.Json;
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);
}

public static int MapPackageExit (int exitCode, string stdout)
{
if (exitCode == ExitCodes.UsageError && HasInputTooLargeCode (stdout))
{
return ExitCodes.ValidationError;
Comment on lines +20 to +22

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep oversized unknown aliases as usage errors

When an invocation uses an unknown alias with an oversized --initial and --json, TryHandleOversizedInitial deliberately falls through because registry.TryResolve fails, but the package parser still emits an input-too-large JSON error before command resolution. This broad remap then turns that usage failure into exit 65, contradicting the spec/commit note that unknown aliases must remain usage errors (exit 2), e.g. clet nope --json --initial <65K> now exits as validation instead of unknown-command usage.

Useful? React with 👍 / 👎.

}

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;
}
}
}
97 changes: 81 additions & 16 deletions src/Clet/Hosting/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,90 @@ internal static class Program
public static async Task<int> Main (string[] args)
{
CletLogging.Initialize ();
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);

return await host.RunAsync (args);
if (TryHandleOversizedInitial (args, host.Registry, parserOptions, 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,
CliHostOptions options,
out int exitCode)
{
exitCode = ExitCodes.UsageError;
ArgParser parser = new (options.GlobalOptions, int.MaxValue);
ArgParser.ParseResult rootParse = parser.Parse (args);

if (!rootParse.Success
|| rootParse.RootFlag is not null
|| rootParse.Alias is null
|| !registry.TryResolve (rootParse.Alias, out _))
{
return false;
}

CommandRunOptions? runOptions = rootParse.Options;

if (runOptions is null
|| runOptions.Initial is null
|| runOptions.Initial.Length <= options.MaxInitialChars)
{
return false;
}

CommandResult result = new (
CommandStatus.Error,
null,
"input-too-large",
$"--initial exceeds the maximum length of {options.MaxInitialChars} characters.");

ResultWriter.Write (result, runOptions.JsonOutput, Console.Out, Console.Error, runOptions.OutputPath);
exitCode = CletExitCodes.FromResult (result);

return true;
}

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));
}
}
2 changes: 1 addition & 1 deletion tests/Clet.SmokeTests/CletSmokeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
7 changes: 2 additions & 5 deletions tests/Clet.UnitTests/ExitCodesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
Loading