Skip to content

winapp init crashes with unhandled InvalidOperationException in non-interactive shells (no TTY) #556

@Jaylyn-Barbee

Description

@Jaylyn-Barbee

Summary

winapp init throws an unhandled System.InvalidOperationException with a Spectre.Console stack trace when stdin is not a TTY (CI, npm scripts, build pipelines, agents, Start-Process -RedirectStandardInput, anything that pipes input). The user is dropped to a raw .NET exception dump instead of either:

  1. Auto-detecting non-interactive mode and applying defaults, or
  2. Exiting cleanly with an actionable error like error: this command requires --use-defaults in non-interactive shells.

Two prompts are affected (both blocking):

  • "No known projects type were found. Initialize with winapp.yaml here? [y/n] (n):" — when no project is detected
  • "appxmanifest.xml already exists. Overwrite? [y/n] (y):" — when a manifest is found

There are likely more ConfirmationPrompt / TextPrompt call sites with the same problem; init is just the most commonly hit.

Environment

  • winapp CLI version: 0.3.2-prerelease.23
  • OS: Windows 11
  • Install path: C:\Users\<user>\AppData\Local\Microsoft\WindowsApps\winapp.exe (MSIX wrapper)
  • Shell: PowerShell 7, stdin redirected via Start-Process -RedirectStandardOutput/Error

Repro steps

Scenario A — empty directory (no project detected)

$dir = "$env:TEMP\winapp-init-repro-a"
New-Item -ItemType Directory -Force $dir | Out-Null
Set-Location $dir

$out = "$env:TEMP\init-a-out.txt"; $err = "$env:TEMP\init-a-err.txt"
$p = Start-Process winapp -ArgumentList "init" -NoNewWindow -Wait -PassThru `
       -RedirectStandardOutput $out -RedirectStandardError $err
"Exit: $($p.ExitCode)"
Get-Content $err

Scenario B — detected project with existing manifest

$dir = "$env:TEMP\winapp-init-repro-b"
New-Item -ItemType Directory -Force $dir | Out-Null
"" | Out-File "$dir\CMakeLists.txt"
"<?xml version='1.0'?><Package/>" | Out-File "$dir\appxmanifest.xml"
Set-Location $dir

$out = "$env:TEMP\init-b-out.txt"; $err = "$env:TEMP\init-b-err.txt"
$p = Start-Process winapp -ArgumentList "init" -NoNewWindow -Wait -PassThru `
       -RedirectStandardOutput $out -RedirectStandardError $err
"Exit: $($p.ExitCode)"
Get-Content $err

Actual results

Both scenarios exit 1 with this on stderr:

Unhandled exception: System.InvalidOperationException: Failed to read input in non-interactive mode.
   at Spectre.Console.DefaultInput.<ReadKeyAsync>d__4.MoveNext() + 0x168
--- End of stack trace from previous location ---
   at Spectre.Console.AnsiConsoleExtensions.<ReadLine>d__11.MoveNext() + 0x6c
--- End of stack trace from previous location ---
   at Spectre.Console.TextPrompt`1.<>c__DisplayClass63_0.<<ShowAsync>b__0>d.MoveNext() + 0x70
--- End of stack trace from previous location ---
   at Spectre.Console.Internal.DefaultExclusivityMode.<RunAsync>d__3`1.MoveNext() + 0x168
--- End of stack trace from previous location ---
   at Spectre.Console.TextPrompt`1.<ShowAsync>d__63.MoveNext() + 0x64
--- End of stack trace from previous location ---
   at Spectre.Console.ConfirmationPrompt.<ShowAsync>d__39.MoveNext() + 0x68
--- End of stack trace from previous location ---
   at WinApp.Cli.Commands.InitCommand.Handler.<HandleNoProjectsFoundAsync>d__9.MoveNext() + 0x5c
   ...
   at WinApp.Cli.Commands.InitCommand.Handler.<InvokeAsync>d__6.MoveNext() + 0x160
   at System.CommandLine.Invocation.InvocationPipeline.<InvokeAsync>d__0.MoveNext() + 0xd0

Expected results

One of:

  1. Auto-detect non-interactive mode via Console.IsInputRedirected and behave as if --use-defaults had been passed (preferred — matches how dotnet, gh, and npm handle this).
  2. Fail fast with a clean message:
    error: 'winapp init' is interactive and stdin is not a TTY.
    Re-run with '--use-defaults' (and an explicit directory) to apply defaults non-interactively.
    
    No stack trace leak.

Workaround (confirmed working)

winapp init . --use-defaults
# Exit: 0, no prompts, manifest + yaml created

Suggested fix

src/winapp-CLI/WinApp.Cli/Commands/InitCommand.cs and any other site that calls ConfirmationPrompt.ShowAsync / TextPrompt.ShowAsync:

private static bool IsInteractive() =>
    !Console.IsInputRedirected && !Console.IsOutputRedirected;

// At each prompt site:
if (!useDefaults && !IsInteractive())
{
    // Option A: silently apply the default
    useDefaults = true;
    // Option B: fail fast
    logger.LogError("'winapp init' requires --use-defaults when stdin is redirected.");
    return 1;
}

Recommend Option A (auto-promote to --use-defaults) for prompts that already have a sensible default in the prompt itself — that's the existing semantic and matches user intent when they piped/redirected. Reserve Option B for prompts where there's no safe default.

A broader fix would be wrapping the top-level invocation pipeline in a try/catch for InvalidOperationException from Spectre so the user never sees the raw .NET stack trace regardless of which prompt fires.

Impact

  • CI/build pipelines, npm postinstall hooks, MCP/agent tooling, dotnet tool wrappers — any non-TTY caller that runs winapp init without --use-defaults hits this.
  • Stack trace is confusing for non-.NET users and makes the failure look like a CLI bug rather than a missing flag.
  • This was hit during a manual bug-bash session against samples/cpp-app/test-bundle/ while following the documented dotnet bundle scenario adapted to C++.

Related

May be worth auditing all ConfirmationPrompt/TextPrompt/SelectionPrompt call sites in WinApp.Cli for the same crash mode.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions