Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d43f830
fix: return exit code 1 on parse errors
akhil9tiet Oct 7, 2026
9bdba15
Delete src/winapp-CLI/WinApp.Cli/Program.cs
akhil9tiet Oct 7, 2026
f76e481
fix: return exit code 1 on parse errors
akhil9tiet Oct 7, 2026
5c28e9e
Merge branch 'main' into fix/1015-parse-error-exit-code
zateutsch Oct 7, 2026
d3b7f1c
fix: make parse-error callback async (compilation fix)
akhil9tiet Oct 8, 2026
994ad2d
Delete src/winapp-CLI/WinApp.Cli/Program.cs
akhil9tiet Oct 8, 2026
21a4170
Rename winapp-Program-fix.cs to Program.cs
akhil9tiet Oct 8, 2026
772a4bb
test: regression for parse-error exit code 1
akhil9tiet Oct 8, 2026
30ef112
Delete src/winapp-CLI/WinApp.Cli.Tests/ProgramTests.cs
akhil9tiet Oct 8, 2026
e88cba2
Rename winapp-ProgramTests.cs to ProgramTests.cs
akhil9tiet Oct 8, 2026
8cb5472
Upload winapp-Program-rebased.cs (intermediate step)
akhil9tiet Oct 8, 2026
383952b
Delete src/winapp-CLI/WinApp.Cli/Program.cs
akhil9tiet Oct 8, 2026
b884eeb
fix: rebase exit-code fix onto current main (resolve conflict)
akhil9tiet Oct 8, 2026
915f370
Upload winapp-Program-proper.cs (intermediate step)
akhil9tiet Oct 8, 2026
6856c28
Delete src/winapp-CLI/WinApp.Cli/Program.cs
akhil9tiet Oct 8, 2026
d6fb795
fix: rebase exit-code fix onto current main with help-pointer flow
akhil9tiet Oct 8, 2026
63d5a4f
Merge branch 'main' into fix/1015-parse-error-exit-code
akhil9tiet Oct 8, 2026
e376c97
Merge branch 'main' into fix/1015-parse-error-exit-code
zateutsch Oct 9, 2026
2bb0e69
Merge branch 'main' into fix/1015-parse-error-exit-code
nmetulev Oct 9, 2026
79018fd
fix: add missing await in async lambda
akhil9tiet Oct 9, 2026
69043b7
fix: add missing await in async lambda
akhil9tiet Oct 9, 2026
49b3e1e
fix: add missing await in async lambda
akhil9tiet Oct 9, 2026
612074b
Merge branch 'main' into fix/1015-parse-error-exit-code
nmetulev Oct 9, 2026
c32e12c
fix: await InvokeWithHelpPointerAsync to fix CS4016
akhil9tiet Oct 9, 2026
378e707
Merge branch 'main' into fix/1015-parse-error-exit-code
nmetulev Oct 9, 2026
1415191
Merge branch 'main' into fix/1015-parse-error-exit-code
zateutsch Oct 10, 2026
2c1fc5e
Merge branch 'main' into fix/1015-parse-error-exit-code
zateutsch Oct 12, 2026
2874236
Merge branch 'main' into fix/1015-parse-error-exit-code
akhil9tiet Oct 13, 2026
9b66209
fix: keep help/schema requests at exit 0 when parse has errors
akhil9tiet Oct 13, 2026
afab5f2
Merge branch 'main' into fix/1015-parse-error-exit-code
nmetulev Oct 14, 2026
75662e0
Fix HelpOption namespace for System.CommandLine v2.0.5
akhil9tiet Oct 14, 2026
5211179
Merge remote-tracking branch 'upstream/main' into fix/1015-parse-erro…
akhil9tiet Oct 24, 2026
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
14 changes: 14 additions & 0 deletions src/winapp-CLI/WinApp.Cli.Tests/ProgramTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,20 @@ public async Task Main_UnknownCommand_FallsThroughToInvocation_WithoutTypoSugges
Assert.IsFalse(stderr.Contains("Did you mean", StringComparison.Ordinal),
$"A non-dash unknown command must not trigger the single-dash typo hint. Got stderr: {stderr}");
}

[TestMethod]
[DataRow(new[] { "cert", "bogus" })]
[DataRow(new[] { "init", "a", "b", "c" })]
[DataRow(new[] { "--verbose", "bogus" })]
public async Task Main_ParseError_ExitsOne(string[] args)
{
// Issue #1015: parse errors that print help must exit 1, not 0, so scripts
// and agents don't treat a rejected command line as success.
var (_, _, exitCode) = await ProgramMainTestHarness.InvokeProgramAsync(args);

Assert.AreEqual(1, exitCode,
$"Parse error for '{string.Join(" ", args)}' must exit with code 1.");
}
}

[TestClass]
Expand Down
67 changes: 63 additions & 4 deletions src/winapp-CLI/WinApp.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -340,13 +340,17 @@ private static async Task<int> RunCoreAsync(string[] args, bool framedStdout, Ac
}
}

<<<<<<< HEAD
return await RunWithTelemetryAsync(parsedArgs, isCompleteMode, async () =>
=======
return await RunWithTelemetryAsync(parsedArgs, isCompleteMode || framedStdout, () =>
>>>>>>> upstream/main
{
// --help and --cli-schema describe the command; they never execute it, so they run here
// regardless of --on rather than validating or preparing a target just to print text.
if (IsDescriptiveAction(parsedArgs))
{
return parsedArgs.InvokeAsync();
return await parsedArgs.InvokeAsync();
}

// Target selection is settled before anything else, and settled for every command.
Expand All @@ -355,12 +359,17 @@ private static async Task<int> RunCoreAsync(string[] args, bool framedStdout, Ac
// exists to prevent.
if (ExecutionTargetSelection.Validate(parsedArgs) is { } selectionError)
{
<<<<<<< HEAD
return TargetOutput.RejectSelection(
serviceProvider.GetRequiredService<IAnsiConsole>(), effectiveJson, selectionError);
=======
if (effectiveJson && TryEmitCommandJsonError(parsedArgs, selectionError.Message))
{
return Task.FromResult(TargetOutput.InvalidCommandLineExitCode);
}
return Task.FromResult(TargetOutput.RejectSelection(
serviceProvider.GetRequiredService<IAnsiConsole>(), effectiveJson, selectionError));
>>>>>>> upstream/main
}

// System.CommandLine binds an unrecognised option to a nearby optional positional rather
Expand All @@ -371,7 +380,7 @@ private static async Task<int> RunCoreAsync(string[] args, bool framedStdout, Ac
if (parsedArgs.CommandResult.Command is not RunCommand &&
WindowsCommandLine.FindOptionLikePositionals(parsedArgs) is { Count: > 0 } stray)
{
return Task.FromResult(RejectOptionLikePositionals(parsedArgs, stray, effectiveJson));
return RejectOptionLikePositionals(parsedArgs, stray, effectiveJson);
}

// One pre-dispatch interception, before any local UI service runs. A command the user
Expand All @@ -398,7 +407,7 @@ private static async Task<int> RunCoreAsync(string[] args, bool framedStdout, Ac
if (parsedArgs.Errors.Count == 0 && ExecutionTargetUiRouter.ShouldRoute(parsedArgs))
{
var router = serviceProvider.GetRequiredService<ExecutionTargetUiRouter>();
return router.RouteAsync(
return await router.RouteAsync(
args,
TargetUiRequirements.For(parsedArgs),
effectiveJson,
Expand All @@ -413,9 +422,23 @@ private static async Task<int> RunCoreAsync(string[] args, bool framedStdout, Ac
// error, so point at --help instead. A bare "winapp ui" keeps its help: that is
// how people discover the commands.
parseError.ShowHelp = false;
return InvokeWithHelpPointerAsync(parsedArgs);
return await InvokeWithHelpPointerAsync(parsedArgs);
}

<<<<<<< HEAD
// Parse errors must exit non-zero (issue #1015). InvokeAsync displays
// the error and help text, but return 1 explicitly so scripts and
// agents don't treat a rejected command line as success.
// Exception: a help / CLI-schema request succeeds by displaying text,
// even when the parser also recorded errors for other tokens
// (e.g. `run --help --args guest-agent` must still exit 0).
var invokeExitCode = await parsedArgs.InvokeAsync();
if (parsedArgs.Errors.Count > 0 && invokeExitCode == 0 && !RequestedDescriptiveAction(parsedArgs))
{
return 1;
}
return invokeExitCode;
=======
return InvokeCommandAsync();

async Task<int> InvokeCommandAsync()
Expand All @@ -434,6 +457,7 @@ async Task<int> InvokeCommandAsync()
return serviceProvider.GetRequiredService<ExecutionTargets.GuestAgent.GuestCommentContext>()
.ClassifyWriterResult(result);
}
>>>>>>> upstream/main
});
}

Expand All @@ -442,9 +466,44 @@ internal static bool IsDescriptiveAction(System.CommandLine.ParseResult parsedAr
parsedArgs.Action is { Terminating: true } action &&
action != parsedArgs.CommandResult.Command.Action;

/// <summary>
/// True when the user asked for a describe-the-command action (help / CLI schema) on any
/// command in this parse, even when the parser also recorded errors for other tokens.
/// Unlike <see cref="IsDescriptiveAction"/>, this does not require a clean parse: the
/// request itself is what matters, because a help/schema request succeeds by displaying
/// text. Detected from the matched options rather than the selected action, so it holds
/// whether the parser selected the help action or a parse-error action for the request.
/// </summary>
private static bool RequestedDescriptiveAction(System.CommandLine.ParseResult parsedArgs)
{
for (System.CommandLine.Parsing.CommandResult? command = parsedArgs.CommandResult;
command is not null;
command = command.Parent as System.CommandLine.Parsing.CommandResult)
{
foreach (var option in command.Children.OfType<System.CommandLine.Parsing.OptionResult>())
{
if (option.Implicit)
{
continue;
}
if (option.Option is System.CommandLine.Help.HelpOption ||
option.Option == WinAppRootCommand.CliSchemaOption)
{
return true;
}
}
}
return false;
}

private static async Task<int> InvokeWithHelpPointerAsync(System.CommandLine.ParseResult parsedArgs)
{
var exitCode = await parsedArgs.InvokeAsync();
// Parse errors must exit non-zero (issue #1015).
if (parsedArgs.Errors.Count > 0 && exitCode == 0)
{
return 1;
}
var path = string.Join(" ", parsedArgs.CommandResult.Command.Parents
.OfType<System.CommandLine.Command>()
.Where(c => c is not System.CommandLine.RootCommand)
Expand Down
Loading