Skip to content
Closed
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
429 changes: 429 additions & 0 deletions cli-agent-improvements.md

Large diffs are not rendered by default.

146 changes: 141 additions & 5 deletions src/PackageCliTool.Tests/CommandLineOptionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,25 @@ public void Parse_WithoutTemplatePackageFlag_LeavesTemplatePackageNameNull()

[Theory]
[InlineData("-t", "Umbraco.Templates|17.0.3")]
public void Parse_WithTemplateVersionFlag_SetsTemplateVersion(string flag, string version)
[InlineData("--template-package", "Umbraco.Templates|17.0.3")]
public void Parse_WithTemplatePipeSyntax_SetsTemplateNameAndVersion(string flag, string value)
{
// Arrange
var args = new[] { flag, version };
var args = new[] { flag, value };

// Act
var options = CommandLineOptions.Parse(args);

// Assert
options.TemplatePackageName.Should().Be("Umbraco.Templates");
options.TemplateVersion.Should().Be("17.0.3");
}

[Fact]
public void Parse_WithTemplateVersionFlag_SetsTemplateVersion()
{
// Arrange
var args = new[] { "--template-version", "17.0.3" };

// Act
var options = CommandLineOptions.Parse(args);
Expand Down Expand Up @@ -325,11 +340,10 @@ public void Parse_WithVerboseFlag_SetsVerbose()
[Fact]
public void Parse_WithMultipleFlags_ParsesAllCorrectly()
{
// Arrange
// Arrange - Use pipe syntax for template+version (unified -t and --template-package)
var args = new[]
{
"--template-package", "Umbraco.Templates",
"-t", "14.3.0",
"-t", "Umbraco.Templates|14.3.0",
"-p", "uSync|17.0.0,Umbraco.Forms|14.2.0",
"-n", "MyProject",
"-s", "MySolution",
Expand Down Expand Up @@ -576,6 +590,128 @@ public void Parse_WithVariousStarterKitPipeSeparatedVersions_ShouldParseCorrectl
options.IncludeStarterKit.Should().BeTrue();
}

[Fact]
public void Parse_WithOutputJsonFlag_SetsOutputFormat()
{
// Arrange
var args = new[] { "--output", "json" };

// Act
var options = CommandLineOptions.Parse(args);

// Assert
options.OutputFormat.Should().Be(OutputFormat.Json);
}

[Fact]
public void Parse_WithOutputPlainFlag_SetsOutputFormat()
{
// Arrange
var args = new[] { "--output", "plain" };

// Act
var options = CommandLineOptions.Parse(args);

// Assert
options.OutputFormat.Should().Be(OutputFormat.Plain);
}

[Fact]
public void Parse_WithScriptOnlyFlag_SetsScriptOnly()
{
// Arrange
var args = new[] { "--script-only" };

// Act
var options = CommandLineOptions.Parse(args);

// Assert
options.ScriptOnly.Should().BeTrue();
}

[Theory]
[InlineData("--no-interaction")]
[InlineData("--non-interactive")]
public void Parse_WithNonInteractiveFlag_SetsNonInteractive(string flag)
{
// Arrange
var args = new[] { flag };

// Act
var options = CommandLineOptions.Parse(args);

// Assert
options.NonInteractive.Should().BeTrue();
}

[Fact]
public void Parse_WithDryRunFlag_SetsDryRun()
{
// Arrange
var args = new[] { "--dry-run" };

// Act
var options = CommandLineOptions.Parse(args);

// Assert
options.DryRun.Should().BeTrue();
}

[Fact]
public void Parse_WithHelpJsonFlag_SetsShowHelpJson()
{
// Arrange
var args = new[] { "--help-json" };

// Act
var options = CommandLineOptions.Parse(args);

// Assert
options.ShowHelpJson.Should().BeTrue();
}

[Fact]
public void Parse_WithListOptionsCommand_SetsListOptionsCommand()
{
// Arrange
var args = new[] { "list-options", "database-types" };

// Act
var options = CommandLineOptions.Parse(args);

// Assert
options.ListOptionsCommand.Should().Be("database-types");
options.IsListOptionsCommand().Should().BeTrue();
}

[Fact]
public void Parse_WithListOptionsNoCategory_SetsEmptyString()
{
// Arrange
var args = new[] { "list-options" };

// Act
var options = CommandLineOptions.Parse(args);

// Assert
options.ListOptionsCommand.Should().Be("");
options.IsListOptionsCommand().Should().BeTrue();
}

[Fact]
public void Parse_WithTemplateVersionExplicitFlag_SetsTemplateVersion()
{
// Arrange
var args = new[] { "-t", "Umbraco.Templates", "--template-version", "17.0.3" };

// Act
var options = CommandLineOptions.Parse(args);

// Assert
options.TemplatePackageName.Should().Be("Umbraco.Templates");
options.TemplateVersion.Should().Be("17.0.3");
}

[Fact]
public void Parse_WithCommunityTemplateFlag_SetsCommunityTemplate()
{
Expand Down
27 changes: 20 additions & 7 deletions src/PackageCliTool.Tests/TemplatePackageFlagTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ public void TemplatePackageFlag_WithUmBootstrapTemplate_ShouldParseCorrectly()
[Fact]
public void TemplatePackageFlag_WithVersionFlag_BothShouldBeParsed()
{
// Arrange
// Arrange - Use --template-version for explicit version setting
var args = new[]
{
"--template-package", "Umbraco.Templates",
"-t", "14.3.0"
"--template-version", "14.3.0"
};

// Act
Expand Down Expand Up @@ -96,8 +96,8 @@ public void TemplatePackageFlag_WithoutVersion_VersionShouldBeNull()
[Fact]
public void TemplateVersionFlag_WithoutTemplatePackage_ShouldStillBeParsed()
{
// Arrange
var args = new[] { "-t", "14.3.0" };
// Arrange - Use --template-version for explicit version setting
var args = new[] { "--template-version", "14.3.0" };

// Act
var options = CommandLineOptions.Parse(args);
Expand All @@ -107,14 +107,27 @@ public void TemplateVersionFlag_WithoutTemplatePackage_ShouldStillBeParsed()
options.TemplateVersion.Should().Be("14.3.0");
}

[Fact]
public void ShortTemplateFlag_WithBareValue_ShouldSetAsPackageName()
{
// Arrange - After unification, -t with bare value treats it as package name
var args = new[] { "-t", "Umbraco.Templates" };

// Act
var options = CommandLineOptions.Parse(args);

// Assert
options.TemplatePackageName.Should().Be("Umbraco.Templates");
options.TemplateVersion.Should().BeNull();
}

[Fact]
public void TemplatePackageFlag_InComplexCommand_ShouldParseWithOtherFlags()
{
// Arrange
// Arrange - Use pipe syntax for template+version, or --template-version
var args = new[]
{
"--template-package", "Umbraco.Community.Templates.Clean",
"-t", "14.3.0",
"-t", "Umbraco.Community.Templates.Clean|14.3.0",
"-p", "uSync|17.0.0,Umbraco.Forms",
"-n", "MyCleanProject",
"-s", "MyCleanSolution",
Expand Down
19 changes: 19 additions & 0 deletions src/PackageCliTool/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ All notable changes to Package Script Writer CLI will be documented in this file
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- **Machine-Readable Output (`--output json|plain`)** - New output format flag for AI agents and automation. JSON mode returns structured responses with script, configuration, and metadata. Plain mode outputs raw text with no ANSI codes or decoration
- **Script-Only Output (`--script-only`)** - Outputs only the raw generated script with no panels, spinners, or status messages. Ideal for piping to files or other tools
- **Non-Interactive Mode (`--no-interaction`)** - Suppresses all interactive prompts. In CLI mode, the tool will output the script and exit without asking "What would you like to do?"
- **Dry Run (`--dry-run`)** - Validates all inputs and displays the resolved configuration without generating a script or calling any APIs
- **Structured Help (`--help-json`)** - Outputs all commands, options, types, defaults, and valid values as structured JSON. Enables AI agents to programmatically discover CLI capabilities
- **List Options Command (`psw list-options`)** - New subcommand to list valid values for CLI options. Supports categories: `database-types`, `starter-kits`, `defaults`. Works with `--output json` for machine-readable output
- **Template Version Flag (`--template-version`)** - Explicit flag for setting template version as an alternative to the pipe syntax in `--template-package`
- **Distinct Exit Codes** - Different exit codes for different error categories: 0 (success), 1 (general), 2 (validation), 3 (network), 4 (script execution), 5 (file system)
- **Structured Version Output** - `psw --version --output json` returns version, runtime, and platform as JSON. `--output plain` returns just the version number
- **JSON Error Responses** - When using `--output json`, errors are returned as structured JSON with error message, error code, and suggestion fields

### Changed
- **Unified `-t` and `--template-package` Flags** - Both `-t` and `--template-package` now behave identically: bare values are treated as package names, and `Name|Version` pipe syntax sets both. Previously `-t` treated bare values as version only
- **Improved Help Text** - All options now show their type (string, enum, flag), default values, and dependency notes directly in help output. New "AI Agent / Automation" examples section added
- **Stderr/Stdout Separation** - In `--output plain` and `--script-only` modes, status messages go to stderr while the script goes to stdout, enabling clean piping

## [1.1.2] - 2026-01-16

### Fixed
Expand Down
100 changes: 74 additions & 26 deletions src/PackageCliTool/Models/CommandLineOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,32 @@ public class CommandLineOptions
/// <summary>Gets or sets whether to show Umbraco versions table</summary>
public bool ShowVersionsTable { get; set; }

/// <summary>Gets or sets the output format (default, plain, json)</summary>
public OutputFormat OutputFormat { get; set; } = OutputFormat.Default;

/// <summary>Gets or sets whether to output only the raw script text</summary>
public bool ScriptOnly { get; set; }

/// <summary>Gets or sets whether to suppress all interactive prompts</summary>
public bool NonInteractive { get; set; }

/// <summary>Gets or sets whether to validate inputs without generating a script</summary>
public bool DryRun { get; set; }

/// <summary>Gets or sets whether to show help as structured JSON</summary>
public bool ShowHelpJson { get; set; }

/// <summary>Gets or sets the list-options subcommand category (e.g., database-types, starter-kits, defaults)</summary>
public string? ListOptionsCommand { get; set; }

/// <summary>
/// Checks if this is a list-options command
/// </summary>
public bool IsListOptionsCommand()
{
return ListOptionsCommand != null;
}

/// <summary>
/// Checks if this is a template command
/// </summary>
Expand Down Expand Up @@ -217,32 +243,6 @@ public static CommandLineOptions Parse(string[] args)
break;

case "-t":
var tArg = GetNextArgument(args, ref i);
if (!string.IsNullOrWhiteSpace(tArg))
{
// -t flag: if contains pipe, split into name|version. Otherwise, treat as version only
if (tArg.Contains('|'))
{
var parts = tArg.Split('|', 2, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 2)
{
options.TemplatePackageName = parts[0].Trim();
options.TemplateVersion = parts[1].Trim();
}
else
{
// Invalid format, just set the whole thing as template name
options.TemplatePackageName = tArg;
}
}
else
{
// No pipe, treat as template version only
options.TemplateVersion = tArg;
}
}
break;

case "--template-package":
var templateArg = GetNextArgument(args, ref i);
if (!string.IsNullOrWhiteSpace(templateArg))
Expand Down Expand Up @@ -381,6 +381,40 @@ public static CommandLineOptions Parse(string[] args)
options.VerboseMode = true;
break;

case "--template-version":
options.TemplateVersion = GetNextArgument(args, ref i);
break;

case "--output":
var outputArg = GetNextArgument(args, ref i);
if (!string.IsNullOrWhiteSpace(outputArg))
{
options.OutputFormat = outputArg.ToLower() switch
{
"json" => OutputFormat.Json,
"plain" => OutputFormat.Plain,
_ => OutputFormat.Default
};
}
break;

case "--script-only":
options.ScriptOnly = true;
break;

case "--no-interaction":
case "--non-interactive":
options.NonInteractive = true;
break;

case "--dry-run":
options.DryRun = true;
break;

case "--help-json":
options.ShowHelpJson = true;
break;

// Template commands
case "template":
// Next argument should be the subcommand (save, load, list, etc.)
Expand Down Expand Up @@ -467,6 +501,20 @@ public static CommandLineOptions Parse(string[] args)
options.ShowVersionsTable = true;
break;

case "list-options":
// Get optional category argument
if (i + 1 < args.Length && !args[i + 1].StartsWith("-"))
{
i++;
options.ListOptionsCommand = args[i].ToLower();
}
else
{
// No category - list all
options.ListOptionsCommand = "";
}
break;

default:
// Check if this is a template or history subcommand without explicit prefix
if (!arg.StartsWith("-") && i == 0)
Expand Down
Loading