Skip to content

Add 'winapp new' command for creating WinUI 3 projects and items#454

Open
nmetulev wants to merge 2 commits into
mainfrom
reland-winapp-new
Open

Add 'winapp new' command for creating WinUI 3 projects and items#454
nmetulev wants to merge 2 commits into
mainfrom
reland-winapp-new

Conversation

@nmetulev
Copy link
Copy Markdown
Member

@nmetulev nmetulev commented Apr 17, 2026

Adds the \winapp new\ command for creating new WinUI 3 projects and adding items to existing projects from templates.

What it does:

  • Creates WinUI 3 projects from templates (e.g., \winapp new winui --name MyApp)
  • Adds items (pages, windows, controls) to existing projects when run inside a .csproj directory
  • Uses the latest \Microsoft.WindowsAppSDK.WinUI.CSharp.Templates\ (automatically installed/updated)
  • Interactive template selection when no template name is provided
  • Supports passing additional \dotnet new\ arguments after --\

Options:

  • --name\ / -n\ — Name for the created project or item
  • --output\ / -o\ — Output directory for the created project
  • --project\ — Target .csproj file (for item templates, auto-detected if omitted)

nmetulev and others added 2 commits April 16, 2026 17:13
Reverts the revert of PR #411, re-adding the 'winapp new' command for
creating WinUI 3 projects and items from templates. Regenerated all
auto-generated docs and npm wrappers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 17, 2026 00:32
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Re-lands the winapp new command in the native CLI, along with regenerated CLI schema/docs and the npm TypeScript wrappers, to scaffold WinUI 3 projects and items from the Microsoft.WindowsAppSDK.WinUI.CSharp.Templates package.

Changes:

  • Added winapp new command implementation with interactive + non-interactive flows and project/item context detection.
  • Introduced a template abstraction layer (ITemplateProvider/ITemplateService) and a .NET provider that discovers templates from an installed .nupkg.
  • Regenerated CLI schema, docs, skills content, and npm command wrappers to reflect the new command.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/winapp-npm/src/winapp-commands.ts Adds newCommand() wrapper and NewOptions type to the npm API surface.
src/winapp-CLI/WinApp.Cli/Services/TemplateService.cs Adds composite template service to aggregate providers and route template creation.
src/winapp-CLI/WinApp.Cli/Services/ITemplateService.cs Defines the composite template service contract.
src/winapp-CLI/WinApp.Cli/Services/ITemplateProvider.cs Defines per-language template provider contract.
src/winapp-CLI/WinApp.Cli/Services/DotNetTemplateProvider.cs Implements template discovery/creation via dotnet new + .nupkg inspection.
src/winapp-CLI/WinApp.Cli/Models/TemplateInfo.cs Adds template/parameter models and template.json source-gen deserialization types.
src/winapp-CLI/WinApp.Cli/Helpers/HostBuilderExtensions.cs Registers template services/providers and wires NewCommand handler in DI.
src/winapp-CLI/WinApp.Cli/Commands/WinAppRootCommand.cs Adds new into root subcommands and help categorization.
src/winapp-CLI/WinApp.Cli/Commands/NewCommand.cs Implements winapp new behavior, prompting, and project setup steps.
src/winapp-CLI/WinApp.Cli.Tests/NewCommandTests.cs Adds tests for schema presence, deserialization, template discovery, routing, and naming.
docs/usage.md Documents winapp new CLI usage and examples.
docs/npm-usage.md Documents npm wrapper newCommand() and NewOptions.
docs/guides/dotnet.md Updates .NET guide to mention winapp new as a recommended WinUI path.
docs/fragments/skills/winapp-cli/setup.md Updates setup skill fragment to include winapp new scenarios and examples.
docs/cli-schema.json Updates generated CLI schema with the new top-level command.
README.md Adds new to the Setup Commands list and clarifies init is for existing projects.
.github/plugin/skills/winapp-cli/ui-automation/SKILL.md Regenerated content (text normalization in --value description).
.github/plugin/skills/winapp-cli/setup/SKILL.md Regenerated skill content to include winapp new guidance.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +107 to +113
var workingDir = outputDir ?? new DirectoryInfo(Directory.GetCurrentDirectory());
if (!workingDir.Exists)
{
workingDir = workingDir.Parent ?? new DirectoryInfo(Directory.GetCurrentDirectory());
}

return await dotNetService.RunDotnetCommandAsync(workingDir, args, cancellationToken);
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

When creating item templates with --project, workingDir is still set to the current directory (because outputDir is null). If the user invokes winapp new from outside the project folder but points --project to a specific .csproj, the generated item files may be created in the wrong directory. Consider using projectFile.Directory as the working directory (or explicitly passing an output directory) when projectFile is provided.

Copilot uses AI. Check for mistakes.
Comment on lines +378 to +388
/**
* Create a new WinUI 3 project or add an item to an existing project. Uses the latest Microsoft.WindowsAppSDK.WinUI.CSharp.Templates (automatically installed/updated). When run inside a .csproj directory, shows item templates (pages, windows, controls). Otherwise, shows project templates. Pass additional dotnet new arguments after --.
*/
export async function newCommand(options: NewOptions = {}): Promise<WinappResult> {
const args: string[] = ['new'];
if (options.template) args.push(options.template);
if (options.name) args.push('--name', options.name);
if (options.output) args.push('--output', options.output);
if (options.project) args.push('--project', options.project);
return execCommand(args, options);
}
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

The CLI supports passing additional dotnet new arguments after --, but the npm wrapper currently has no way to supply those passthrough args (unlike tool() which exposes toolArgs). This makes the wrapper API incomplete for winapp new compared to the underlying CLI. Consider adding an option like dotnetNewArgs?: string[] and ensuring common flags (--quiet/--verbose) are placed before -- so they’re not forwarded to dotnet new.

Copilot uses AI. Check for mistakes.
Comment thread docs/npm-usage.md
Comment on lines +277 to +283
### `newCommand()`

Create a new WinUI 3 project or add an item to an existing project. Uses the latest Microsoft.WindowsAppSDK.WinUI.CSharp.Templates (automatically installed/updated). When run inside a .csproj directory, shows item templates (pages, windows, controls). Otherwise, shows project templates. Pass additional dotnet new arguments after --.

```typescript
function newCommand(options?: NewOptions): Promise<WinappResult>
```
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

This npm-usage entry says you can “Pass additional dotnet new arguments after --”, but the generated TypeScript wrapper (newCommand(options)) does not expose any field for passthrough args. Either add passthrough support to the wrapper API or adjust this docstring to avoid advertising unsupported functionality in the npm package.

Copilot uses AI. Check for mistakes.
{
var langTemplates = templates.Where(t => t.Language == language).ToList();
prompt.AddChoiceGroup(
($"[bold yellow]{Markup.Escape(language)}[/]", (TemplateInfo?)null),
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

SelectionPrompt<T>.AddChoiceGroup(...) in Spectre.Console expects the group title as a string (non-selectable header), but this code is passing a tuple value as the group. This likely won’t compile against Spectre.Console 0.54.0, and even if an overload exists it risks allowing the group header to be selected (returning null and failing). Use the AddChoiceGroup(string title, IEnumerable<T> choices) overload and pass only the template choices as selectable items.

Suggested change
($"[bold yellow]{Markup.Escape(language)}[/]", (TemplateInfo?)null),
$"[bold yellow]{Markup.Escape(language)}[/]",

Copilot uses AI. Check for mistakes.
Comment on lines +84 to +106
// Phase 1: Load templates (simple spinner, no residual output)
IReadOnlyList<TemplateInfo>? allTemplates = null;
try
{
var spinnerTask = Task.Run(async () =>
{
await templateService.EnsureAllProvidersAsync(cancellationToken);
return await templateService.GetAvailableTemplatesAsync(cancellationToken);
}, cancellationToken);

if (isInteractive)
{
var spinnerChars = new[] { "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" };
int i = 0;
while (!spinnerTask.IsCompleted)
{
Console.Write($"\r\x1b[33m{spinnerChars[i++ % spinnerChars.Length]}\x1b[0m Loading templates...");
await Task.WhenAny(spinnerTask, Task.Delay(100, cancellationToken));
}
Console.Write("\r\x1b[2K");
}

allTemplates = await spinnerTask;
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

The template-loading spinner writes directly to Console and will still render even when --quiet is set, despite --quiet being documented as “Suppress progress messages” and enforced elsewhere via log level/status rendering. Consider gating this spinner behind logger.IsEnabled(LogLevel.Information) / the WinAppRootCommand.QuietOption value, or using IStatusService for the load phase so quiet mode behaves consistently.

Suggested change
// Phase 1: Load templates (simple spinner, no residual output)
IReadOnlyList<TemplateInfo>? allTemplates = null;
try
{
var spinnerTask = Task.Run(async () =>
{
await templateService.EnsureAllProvidersAsync(cancellationToken);
return await templateService.GetAvailableTemplatesAsync(cancellationToken);
}, cancellationToken);
if (isInteractive)
{
var spinnerChars = new[] { "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" };
int i = 0;
while (!spinnerTask.IsCompleted)
{
Console.Write($"\r\x1b[33m{spinnerChars[i++ % spinnerChars.Length]}\x1b[0m Loading templates...");
await Task.WhenAny(spinnerTask, Task.Delay(100, cancellationToken));
}
Console.Write("\r\x1b[2K");
}
allTemplates = await spinnerTask;
// Phase 1: Load templates
IReadOnlyList<TemplateInfo>? allTemplates = null;
try
{
if (isInteractive)
{
allTemplates = await ansiConsole.Status()
.StartAsync("Loading templates...", async _ =>
{
await templateService.EnsureAllProvidersAsync(cancellationToken);
return await templateService.GetAvailableTemplatesAsync(cancellationToken);
});
}
else
{
await templateService.EnsureAllProvidersAsync(cancellationToken);
allTemplates = await templateService.GetAvailableTemplatesAsync(cancellationToken);
}

Copilot uses AI. Check for mistakes.
Comment on lines +139 to +152
// Parse version from filename and sort semantically
// Filename format: Microsoft.WindowsAppSDK.WinUI.CSharp.Templates.0.0.3-alpha.nupkg
return matchingFiles
.Select(f =>
{
var fileName = Path.GetFileNameWithoutExtension(f);
var versionStr = fileName[prefix.Length..];
var parsed = Version.TryParse(versionStr.Split('-')[0], out var version);
return (Path: f, Version: parsed ? version! : new Version(0, 0, 0), Raw: versionStr);
})
.OrderByDescending(x => x.Version)
.ThenByDescending(x => x.Raw) // Pre-release tie-breaker
.Select(x => x.Path)
.FirstOrDefault();
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

FindInstalledNupkg() prefers the highest System.Version and then uses ThenByDescending(x.Raw) as a prerelease tie-breaker. For the same numeric version, this will incorrectly select a prerelease (e.g. 1.0.0-beta sorts after 1.0.0) because the raw string is longer/greater. Prefer stable versions over prerelease for the same numeric version, or switch to NuGet’s version comparer (e.g., NuGetVersion) for correct SemVer precedence.

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown
Contributor

Build Metrics Report

Binary Sizes

Artifact Baseline Current Delta
CLI (ARM64) 30.52 MB 30.75 MB 📈 +233.5 KB (+0.75%)
CLI (x64) 30.89 MB 31.11 MB 📈 +226.0 KB (+0.71%)
MSIX (ARM64) 12.89 MB 12.97 MB 📈 +86.2 KB (+0.65%)
MSIX (x64) 13.68 MB 13.79 MB 📈 +117.0 KB (+0.84%)
NPM Package 26.79 MB 26.99 MB 📈 +205.1 KB (+0.75%)
NuGet Package 26.87 MB 27.07 MB 📈 +199.2 KB (+0.72%)

Test Results

737 passed out of 737 tests in 377.8s (+15 tests, +23.8s vs. baseline)

Test Coverage

20.9% line coverage, 34.6% branch coverage · ✅ +0.3% vs. baseline

CLI Startup Time

42ms median (x64, winapp --version) · ✅ no change vs. baseline


Updated 2026-04-17 00:44:38 UTC · commit 3a9f4dd · workflow run

@nmetulev nmetulev changed the title Re-land winapp new command Add 'winapp new' command for creating WinUI 3 projects and items Apr 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants