From 9fb2ed68f2b0a05576135aaaefc42780d9ea63fd Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Feb 2026 10:33:45 +0000 Subject: [PATCH 1/2] Add CLI agent improvements analysis document Analyse the PSW CLI tool for AI agent usability and document a prioritised list of 16 improvements across 4 priority tiers. Key recommendations include structured output modes (--output json/plain), non-interactive flag, machine-readable help, valid value discovery commands, distinct exit codes, and stderr/stdout separation. https://claude.ai/code/session_01GKrYtcRN1vAEWzdPQakFqV --- cli-agent-improvements.md | 427 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 427 insertions(+) create mode 100644 cli-agent-improvements.md diff --git a/cli-agent-improvements.md b/cli-agent-improvements.md new file mode 100644 index 0000000..7e3a6a3 --- /dev/null +++ b/cli-agent-improvements.md @@ -0,0 +1,427 @@ +# CLI Agent Improvements: Making PSW CLI AI-Friendly + +Analysis of the Package Script Writer CLI tool, focused on how AI agents (Claude, Copilot, ChatGPT, custom agents) can understand and use this tool effectively. This covers documentation, help text, output format, and discoverability. + +--- + +## Current State Assessment + +The CLI is well-built for human interactive use. Spectre.Console provides a polished terminal experience. However, several patterns make it harder for AI agents to use effectively: + +1. **Help text is rendered with Spectre.Console markup** (`[green]`, `[cyan]`, etc.), which is great visually but produces noisy output when an agent reads it. AI agents parse raw text; ANSI escape codes and markup tags are clutter. +2. **No machine-readable output mode.** Every output path uses Spectre.Console formatting. There is no `--output json` or `--output plain` mode for agents to consume structured results. +3. **Exit codes are limited.** The tool exits with `0` (success) or `1` (any error). There is no differentiation between validation errors, network errors, and execution errors at the process level. +4. **The CLI mode workflow still prompts interactively** in some paths (e.g., `HandleInteractiveScriptActionAsync` after script generation when `--auto-run` is not set), which blocks AI agents that cannot respond to stdin prompts. +5. **Flag semantics have subtle ambiguities.** For example, `-t` treats a bare value as a version, but `--template-package` treats a bare value as a package name. This is documented in the help text but requires careful reading. +6. **No command to list valid values.** An agent cannot ask the CLI "what database types are valid?" or "what starter kits are available?" without reading source code. + +--- + +## Prioritised Improvements + +### Priority 1: Critical for AI Agent Usability + +#### 1.1 Add `--output` flag for machine-readable output + +**Problem:** All output uses Spectre.Console markup. An AI agent running `psw --default` gets a script wrapped in a decorative panel with ANSI escape codes. Parsing the actual script content from that output is fragile. + +**Recommendation:** Add `--output ` flag supporting: +- `--output json` - Returns structured JSON with the generated script, configuration used, and metadata +- `--output plain` - Returns the raw script text only, no panels, no colours, no spinners +- Default (no flag) - Current behaviour, unchanged + +**Example JSON output:** +```json +{ + "success": true, + "script": "#!/bin/bash\n# Install Umbraco...", + "configuration": { + "templateName": "Umbraco.Templates", + "templateVersion": "17.0.3", + "projectName": "MyProject", + "packages": ["uSync|17.0.0"], + "databaseType": "SQLite" + }, + "metadata": { + "generatedAt": "2025-01-15T10:30:00Z", + "cliVersion": "1.1.2", + "historyId": 42 + } +} +``` + +**Impact:** This is the single most important improvement. Without it, every AI agent has to parse ANSI-decorated text to extract useful content. + +**Files affected:** +- `Models/CommandLineOptions.cs` - Add `OutputFormat` property and parsing +- `UI/ConsoleDisplay.cs` - Add plain/JSON rendering paths +- `Workflows/CliModeWorkflow.cs` - Conditionally suppress Spectre output +- `Program.cs` - Route output format through workflows + +--- + +#### 1.2 Add `--no-interaction` / `--non-interactive` flag + +**Problem:** Even in CLI mode, some code paths call `InteractivePrompts.PromptForScriptAction()` which blocks waiting for user input. An AI agent cannot respond to Spectre.Console prompts on stdin. When `--auto-run` is not set and the script is generated, the workflow still prompts "What would you like to do with this script?" (`CliModeWorkflow.cs:439`). + +**Recommendation:** Add a `--no-interaction` flag that: +- Suppresses all interactive prompts +- Falls back to sensible defaults (print the script, exit) +- Returns non-zero exit code if a prompt would have been required but cannot proceed + +**Files affected:** +- `Models/CommandLineOptions.cs` - Add `NonInteractive` property +- `Workflows/CliModeWorkflow.cs` - Skip prompts when non-interactive +- `UI/InteractivePrompts.cs` - Guard all prompts + +--- + +#### 1.3 Add `--script-only` flag to output just the script + +**Problem:** An AI agent that wants to capture the generated script has to strip the Spectre.Console panel, figlet banner, status spinners, and colour codes. This is the most common agent use case: "generate a script and give it to me." + +**Recommendation:** Add `--script-only` flag that outputs only the raw script text to stdout with no decoration. This is simpler than `--output plain` and covers the most common case. + +**Implementation:** When set, skip `ConsoleDisplay.DisplayGeneratedScript()` and instead write the raw script string to `Console.Out`. + +**Files affected:** +- `Models/CommandLineOptions.cs` - Add `ScriptOnly` property +- `Workflows/CliModeWorkflow.cs` - Conditional output path +- `Program.cs` - Suppress banner/spinners when script-only + +--- + +### Priority 2: Important for Documentation and Discoverability + +#### 2.1 Add `--help-json` flag for machine-readable help + +**Problem:** The current `--help` output is a Spectre.Console panel with markup tags. An AI agent reading this gets text like `[green] --admin-email[/] Admin email for unattended install`. The agent has to strip markup to understand the options. + +**Recommendation:** Add `--help-json` that outputs a structured JSON schema of all commands, flags, their types, defaults, valid values, and descriptions. + +**Example output:** +```json +{ + "name": "psw", + "description": "Package Script Writer - Generate Umbraco CMS installation scripts", + "version": "1.1.2", + "commands": [ + { + "name": "template", + "description": "Manage script configuration templates", + "subcommands": [ + { + "name": "save", + "description": "Save current configuration as a template", + "arguments": [{"name": "name", "required": true, "description": "Template name"}] + } + ] + } + ], + "options": [ + { + "name": "--packages", + "shortName": "-p", + "type": "string", + "required": false, + "description": "Comma-separated list of packages with optional versions", + "format": "Package1|Version1,Package2|Version2", + "examples": ["uSync|17.0.0,Umbraco.Forms", "uSync,Diplo.GodMode"] + }, + { + "name": "--database-type", + "type": "enum", + "required": false, + "validValues": ["SQLite", "LocalDb", "SQLServer", "SQLAzure", "SQLCE"], + "default": null, + "description": "Database type for unattended install" + } + ] +} +``` + +**Impact:** Allows AI agents to programmatically discover capabilities without parsing decorated text. + +**Files affected:** +- `Models/CommandLineOptions.cs` - Add `ShowHelpJson` property +- `UI/ConsoleDisplay.cs` - Add `DisplayHelpJson()` method +- `Program.cs` - Handle the flag + +--- + +#### 2.2 Add `psw list-options` subcommand for valid value discovery + +**Problem:** An AI agent does not know what database types, starter kits, or template packages are valid without reading source code. The valid database types are hardcoded in `InputValidator.cs:213` as `["SQLite", "LocalDb", "SQLServer", "SQLAzure", "SQLCE"]`. The starter kits are hardcoded in `InteractivePrompts.cs:76-85`. + +**Recommendation:** Add a `list-options` subcommand: +```bash +psw list-options # List all option categories +psw list-options database-types # List valid database types +psw list-options starter-kits # List available starter kits +psw list-options template-packages # List known template packages +``` + +**With `--output json`:** +```json +{ + "databaseTypes": ["SQLite", "LocalDb", "SQLServer", "SQLAzure", "SQLCE"], + "starterKits": ["clean", "Articulate", "Portfolio", "LittleNorth.Igloo", "Umbraco.BlockGrid.Example.Website", "Umbraco.TheStarterKit", "uSkinnedSiteBuilder"], + "defaultValues": { + "projectName": "MyProject", + "solutionName": "MySolution", + "databaseType": "SQLite", + "adminEmail": "admin@example.com" + } +} +``` + +**Impact:** Agents can dynamically discover valid inputs instead of guessing or relying on stale documentation. + +**Files affected:** +- New workflow: `Workflows/ListOptionsWorkflow.cs` +- `Models/CommandLineOptions.cs` - Add parsing for `list-options` +- `Program.cs` - Route to new workflow + +--- + +#### 2.3 Improve `--help` text with explicit type annotations and defaults + +**Problem:** The current help text does not consistently show: +- What type each option expects (string, boolean, enum) +- What the default value is +- Whether the option is required or optional +- What other options it depends on (e.g., `--connection-string` is required when `--database-type` is `SQLServer`) + +**Current:** +``` +--database-type Database type (SQLite, LocalDb, SQLServer, SQLAzure, SQLCE) +``` + +**Recommended:** +``` +--database-type Database type [enum: SQLite, LocalDb, SQLServer, SQLAzure, SQLCE] + Default: none. Requires: --unattended-defaults or related flags. + Note: SQLServer and SQLAzure require --connection-string. +``` + +**Impact:** AI agents and humans both benefit from explicit type/default/dependency documentation directly in help text. + +**Files affected:** +- `UI/ConsoleDisplay.cs` - Expand help text descriptions + +--- + +#### 2.4 Add a `--dry-run` flag + +**Problem:** An AI agent may want to validate a command without actually generating a script or hitting APIs. Currently there is no way to check "would this command succeed?" without running it. + +**Recommendation:** Add `--dry-run` that: +- Validates all inputs +- Reports what would be generated (configuration summary) +- Does not call any APIs or generate scripts +- Returns exit code 0 if inputs are valid, non-zero if not + +**Example:** +```bash +psw --dry-run -p "uSync|17.0.0" -n MyProject --database-type InvalidDB +# Exit code 1 +# Error: Invalid database type: InvalidDB. Valid values: SQLite, LocalDb, SQLServer, SQLAzure, SQLCE +``` + +**Files affected:** +- `Models/CommandLineOptions.cs` - Add `DryRun` property +- `Workflows/CliModeWorkflow.cs` - Short-circuit after validation + +--- + +### Priority 3: Valuable Enhancements + +#### 3.1 Use distinct exit codes for different error categories + +**Problem:** The tool always exits with code `1` for any error (`Program.cs:245`). An AI agent cannot distinguish between "invalid input" (fixable by changing arguments), "network error" (retryable), and "script execution failed" (investigate output). + +**Recommendation:** Use distinct exit codes: +| Code | Meaning | +|------|---------| +| 0 | Success | +| 1 | General/unknown error | +| 2 | Invalid arguments / validation error | +| 3 | Network / API error | +| 4 | Script execution failed | +| 5 | File system / permission error | + +**Files affected:** +- `Program.cs` - Map exception types to exit codes +- `Exceptions/PswException.cs` - Add `ExitCode` property to base exception + +--- + +#### 3.2 Add `--version --output json` for structured version info + +**Problem:** `psw --version` outputs a Figlet banner plus text. An agent checking whether the tool is installed or what version is running has to parse decorative text. + +**Recommendation:** When `--output json` is combined with `--version`: +```json +{ + "name": "PackageScriptWriter.Cli", + "version": "1.1.2", + "runtime": ".NET 10.0", + "platform": "linux-x64" +} +``` + +When `--output plain` is combined with `--version`: +``` +1.1.2 +``` + +**Files affected:** +- `UI/ConsoleDisplay.cs` - Add plain/JSON version output + +--- + +#### 3.3 Add stderr/stdout separation + +**Problem:** Currently, status messages ("Generating script..."), errors, and the actual script output all go to stdout via `AnsiConsole`. An AI agent piping stdout to capture the script also captures spinners and status messages. + +**Recommendation:** Follow Unix conventions: +- **stdout**: The generated script (the useful output) +- **stderr**: Status messages, spinners, errors, warnings + +This enables `psw --default 2>/dev/null` to get just the script. + +**Impact:** Works with standard Unix tooling and piping, which AI agents commonly use. + +**Files affected:** +- `Workflows/CliModeWorkflow.cs` - Route status to stderr +- `UI/ConsoleDisplay.cs` - Use `Console.Error` for non-data output +- `Services/ScriptExecutor.cs` - Route execution status to stderr + +--- + +#### 3.4 Document the CLI for AI agents explicitly + +**Problem:** The existing README is written for human developers. AI agents benefit from a different documentation format: a concise reference card with exact command syntax, all valid values, and copy-paste examples for common workflows. + +**Recommendation:** Add an `AI-USAGE.md` or `AGENT-GUIDE.md` file (or a section in the README) with: + +1. **Quick reference table** of all flags with types, defaults, and valid values +2. **Common AI agent workflows** as exact copy-paste commands: + - "Generate a default Umbraco project script" + - "Generate a script with specific packages" + - "Generate a script with full unattended install configuration" + - "List available community templates" + - "Save and reuse a configuration template" +3. **Input format specifications** - Exact syntax for the pipe-separated package format, version format, etc. +4. **Error code reference** - What each error code means and how to handle it +5. **Exit code reference** - What each exit code means + +**Files affected:** +- New file: `src/PackageCliTool/AI-USAGE.md` or section in README + +--- + +#### 3.5 Resolve the `-t` vs `--template-package` ambiguity + +**Problem:** The `-t` and `--template-package` flags behave differently for bare values (no pipe): +- `-t 17.0.3` sets `TemplateVersion = "17.0.3"` (treats bare value as version) +- `--template-package Umbraco.Templates` sets `TemplatePackageName = "Umbraco.Templates"` (treats bare value as package name) + +This is documented but unintuitive. An AI agent that learns one flag's behaviour may incorrectly assume the other works the same way. + +**Recommendation:** Either: +- Make them consistent (both treat bare values the same way), or +- Deprecate the ambiguous shorthand and introduce explicit `--template-version` flag, or +- At minimum, add a prominent note in help text explaining the difference + +**Files affected:** +- `Models/CommandLineOptions.cs` - Align parsing logic or add `--template-version` +- `UI/ConsoleDisplay.cs` - Update help text + +--- + +### Priority 4: Nice-to-Have + +#### 4.1 Add `--validate` flag for input checking + +**Problem:** An AI agent building up a command incrementally cannot check if partial input is valid. The validation currently happens deep inside the workflow. + +**Recommendation:** `psw --validate --database-type SQLite --admin-email bad` would validate inputs and report all validation errors without generating anything. + +Difference from `--dry-run`: `--validate` only checks input format, `--dry-run` would also check API reachability and resolve package names. + +--- + +#### 4.2 Add shell completion scripts + +**Problem:** AI agents using shell often benefit from tab completion data to understand available commands. Generating shell completions also serves as structured documentation. + +**Recommendation:** Add `psw completion bash|zsh|fish|powershell` that outputs shell completion scripts. This is standard practice for modern CLI tools and provides a machine-parseable description of all commands and options. + +--- + +#### 4.3 Support reading configuration from stdin or file + +**Problem:** Complex configurations require very long command lines. An AI agent generating a script with many packages, custom credentials, Docker options, and template settings produces an unwieldy single command. + +**Recommendation:** Support `psw --config config.json` or `cat config.json | psw --config -` where `config.json` is a JSON file matching the `ScriptModel` schema. + +**Example:** +```json +{ + "templateName": "Umbraco.Templates", + "templateVersion": "17.0.3", + "projectName": "MyBlog", + "packages": "uSync|17.0.0,Diplo.GodMode", + "databaseType": "SQLite", + "useUnattendedInstall": true, + "adminEmail": "admin@example.com", + "adminPassword": "1234567890" +} +``` + +**Impact:** Enables structured input rather than complex flag parsing, which is the natural output format for AI agents. + +--- + +#### 4.4 Add `psw explain` subcommand + +**Problem:** An AI agent may want to understand what a specific generated script does before recommending execution. + +**Recommendation:** `psw explain --default` would output a human/AI-readable explanation of what the default script does, step by step, without generating the actual script. This helps agents provide context to users. + +--- + +## Summary: Implementation Order + +| # | Improvement | Priority | Effort | AI Impact | +|---|-------------|----------|--------|-----------| +| 1.1 | `--output json/plain` flag | Critical | Medium | Very High | +| 1.2 | `--no-interaction` flag | Critical | Low | Very High | +| 1.3 | `--script-only` flag | Critical | Low | High | +| 2.1 | `--help-json` structured help | Important | Medium | High | +| 2.2 | `psw list-options` subcommand | Important | Medium | High | +| 2.3 | Improve help text with types/defaults | Important | Low | Medium | +| 2.4 | `--dry-run` flag | Important | Low | Medium | +| 3.1 | Distinct exit codes | Valuable | Low | Medium | +| 3.2 | Structured version output | Valuable | Low | Low | +| 3.3 | stderr/stdout separation | Valuable | Medium | High | +| 3.4 | AI agent documentation | Valuable | Low | High | +| 3.5 | Resolve `-t` ambiguity | Valuable | Low | Medium | +| 4.1 | `--validate` flag | Nice-to-have | Low | Low | +| 4.2 | Shell completion scripts | Nice-to-have | Medium | Low | +| 4.3 | Config from stdin/file | Nice-to-have | Medium | High | +| 4.4 | `psw explain` subcommand | Nice-to-have | Medium | Medium | + +--- + +## What Already Works Well + +Credit where due -- these aspects of the current CLI are already AI-friendly: + +- **Comprehensive CLI mode**: The tool already supports full non-interactive operation via flags. An agent can construct a complete command without interactive prompts (as long as `--auto-run` is set or the script output is the goal). +- **Good error messages**: The `PswException` hierarchy with `UserMessage`, `Suggestion`, and `ErrorCode` is excellent. Agents can parse these structured error messages. +- **Input validation**: The `InputValidator` class provides clear, specific validation errors with field names and suggestions. This helps agents understand what went wrong. +- **Template system**: YAML-based templates with save/load/export/import provide a workflow that agents can use to persist and share configurations. +- **History system**: The ability to `history list` and `history rerun` means agents can reference previous successful configurations. +- **Verbose logging**: `--verbose` provides detailed diagnostic output that helps agents troubleshoot failures. From 1e8620193fd5a225a6b72692dd111ade5d66a811 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Feb 2026 07:25:55 +0000 Subject: [PATCH 2/2] Implement CLI agent improvements for AI-friendly operation Add machine-readable output modes and automation flags to make the CLI tool usable by AI agents without parsing decorated Spectre.Console text. New features: - --output json|plain flag for structured/plain output - --script-only flag for raw script output without decoration - --no-interaction flag to suppress all interactive prompts - --dry-run flag to validate inputs without generating scripts - --help-json flag for structured JSON help discovery - psw list-options subcommand with database-types, starter-kits, defaults - --template-version flag as explicit alternative to pipe syntax - Distinct exit codes (0=success, 2=validation, 3=network, 4=execution, 5=fs) - JSON error responses when using --output json Breaking change: - Unified -t and --template-package flags: bare values now treated as package names (not versions). Use --template-version or pipe syntax (Package|Version) for version specification. Updated existing tests and added new tests for all new flags. https://claude.ai/code/session_01GKrYtcRN1vAEWzdPQakFqV --- cli-agent-improvements.md | 2 + .../CommandLineOptionsTests.cs | 146 ++++++++++- .../TemplatePackageFlagTests.cs | 27 +- src/PackageCliTool/CHANGELOG.md | 19 ++ .../Models/CommandLineOptions.cs | 100 +++++-- src/PackageCliTool/Models/ExitCodes.cs | 27 ++ src/PackageCliTool/Models/OutputFormat.cs | 16 ++ src/PackageCliTool/Program.cs | 57 +++- src/PackageCliTool/UI/ConsoleDisplay.cs | 85 ++++-- src/PackageCliTool/UI/OutputHelper.cs | 245 ++++++++++++++++++ .../Workflows/CliModeWorkflow.cs | 215 +++++++++++++-- .../Workflows/ListOptionsWorkflow.cs | 208 +++++++++++++++ 12 files changed, 1053 insertions(+), 94 deletions(-) create mode 100644 src/PackageCliTool/Models/ExitCodes.cs create mode 100644 src/PackageCliTool/Models/OutputFormat.cs create mode 100644 src/PackageCliTool/UI/OutputHelper.cs create mode 100644 src/PackageCliTool/Workflows/ListOptionsWorkflow.cs diff --git a/cli-agent-improvements.md b/cli-agent-improvements.md index 7e3a6a3..3f4a082 100644 --- a/cli-agent-improvements.md +++ b/cli-agent-improvements.md @@ -2,6 +2,8 @@ Analysis of the Package Script Writer CLI tool, focused on how AI agents (Claude, Copilot, ChatGPT, custom agents) can understand and use this tool effectively. This covers documentation, help text, output format, and discoverability. +> **Status:** Priority 1 (Critical), Priority 2 (Important), and Priority 3 items 3.1 and 3.5 have been **implemented** in this branch. + --- ## Current State Assessment diff --git a/src/PackageCliTool.Tests/CommandLineOptionsTests.cs b/src/PackageCliTool.Tests/CommandLineOptionsTests.cs index 8ed10dd..1935f8f 100644 --- a/src/PackageCliTool.Tests/CommandLineOptionsTests.cs +++ b/src/PackageCliTool.Tests/CommandLineOptionsTests.cs @@ -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); @@ -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", @@ -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() { diff --git a/src/PackageCliTool.Tests/TemplatePackageFlagTests.cs b/src/PackageCliTool.Tests/TemplatePackageFlagTests.cs index 6187370..b15477c 100644 --- a/src/PackageCliTool.Tests/TemplatePackageFlagTests.cs +++ b/src/PackageCliTool.Tests/TemplatePackageFlagTests.cs @@ -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 @@ -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); @@ -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", diff --git a/src/PackageCliTool/CHANGELOG.md b/src/PackageCliTool/CHANGELOG.md index d6f95ea..93da0e2 100644 --- a/src/PackageCliTool/CHANGELOG.md +++ b/src/PackageCliTool/CHANGELOG.md @@ -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 diff --git a/src/PackageCliTool/Models/CommandLineOptions.cs b/src/PackageCliTool/Models/CommandLineOptions.cs index f9d40fd..bb7e557 100644 --- a/src/PackageCliTool/Models/CommandLineOptions.cs +++ b/src/PackageCliTool/Models/CommandLineOptions.cs @@ -121,6 +121,32 @@ public class CommandLineOptions /// Gets or sets whether to show Umbraco versions table public bool ShowVersionsTable { get; set; } + /// Gets or sets the output format (default, plain, json) + public OutputFormat OutputFormat { get; set; } = OutputFormat.Default; + + /// Gets or sets whether to output only the raw script text + public bool ScriptOnly { get; set; } + + /// Gets or sets whether to suppress all interactive prompts + public bool NonInteractive { get; set; } + + /// Gets or sets whether to validate inputs without generating a script + public bool DryRun { get; set; } + + /// Gets or sets whether to show help as structured JSON + public bool ShowHelpJson { get; set; } + + /// Gets or sets the list-options subcommand category (e.g., database-types, starter-kits, defaults) + public string? ListOptionsCommand { get; set; } + + /// + /// Checks if this is a list-options command + /// + public bool IsListOptionsCommand() + { + return ListOptionsCommand != null; + } + /// /// Checks if this is a template command /// @@ -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)) @@ -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.) @@ -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) diff --git a/src/PackageCliTool/Models/ExitCodes.cs b/src/PackageCliTool/Models/ExitCodes.cs new file mode 100644 index 0000000..7091bfe --- /dev/null +++ b/src/PackageCliTool/Models/ExitCodes.cs @@ -0,0 +1,27 @@ +namespace PackageCliTool.Models; + +/// +/// Defines exit codes for the CLI tool. +/// Distinct codes allow AI agents and scripts to programmatically +/// determine the type of failure without parsing output text. +/// +public static class ExitCodes +{ + /// Operation completed successfully + public const int Success = 0; + + /// General or unknown error + public const int GeneralError = 1; + + /// Invalid arguments or input validation failed + public const int ValidationError = 2; + + /// Network or API communication error + public const int NetworkError = 3; + + /// Generated script execution failed + public const int ScriptExecutionError = 4; + + /// File system or permission error + public const int FileSystemError = 5; +} diff --git a/src/PackageCliTool/Models/OutputFormat.cs b/src/PackageCliTool/Models/OutputFormat.cs new file mode 100644 index 0000000..12c72f0 --- /dev/null +++ b/src/PackageCliTool/Models/OutputFormat.cs @@ -0,0 +1,16 @@ +namespace PackageCliTool.Models; + +/// +/// Specifies the output format for CLI results +/// +public enum OutputFormat +{ + /// Default rich Spectre.Console output with colours and panels + Default, + + /// Plain text output with no ANSI codes or decoration + Plain, + + /// Structured JSON output for machine consumption + Json +} diff --git a/src/PackageCliTool/Program.cs b/src/PackageCliTool/Program.cs index 7620a51..fc85f1c 100644 --- a/src/PackageCliTool/Program.cs +++ b/src/PackageCliTool/Program.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using PackageCliTool.Configuration; +using PackageCliTool.Exceptions; using PackageCliTool.Logging; using PackageCliTool.Models; using PackageCliTool.Services; @@ -152,6 +153,13 @@ static async Task Main(string[] args) var historyService = serviceProvider.GetRequiredService(); var versionCheckService = serviceProvider.GetRequiredService(); + // Handle --help-json flag (before regular help, for AI agents) + if (options.ShowHelpJson) + { + OutputHelper.WriteHelpJson(); + return; + } + // Handle help flag if (options.ShowHelp) { @@ -159,10 +167,23 @@ static async Task Main(string[] args) return; } - // Handle version flag + // Handle version flag with output format support if (options.ShowVersion) { - ConsoleDisplay.DisplayVersion(); + if (options.OutputFormat == OutputFormat.Json) + OutputHelper.WriteVersionJson(); + else if (options.OutputFormat == OutputFormat.Plain) + OutputHelper.WriteVersionPlain(); + else + ConsoleDisplay.DisplayVersion(); + return; + } + + // Handle list-options command + if (options.IsListOptionsCommand()) + { + var listOptionsWorkflow = new ListOptionsWorkflow(logger); + listOptionsWorkflow.Run(options); return; } @@ -170,7 +191,8 @@ static async Task Main(string[] args) if (options.ClearCache) { cacheService.Clear(); - AnsiConsole.MarkupLine("[green]✓ Cache cleared successfully[/]"); + if (!OutputHelper.IsMachineReadable(options.OutputFormat)) + AnsiConsole.MarkupLine("[green]✓ Cache cleared successfully[/]"); // If only clearing cache, exit if (!options.HasAnyOptions() && !options.IsTemplateCommand() && !options.IsHistoryCommand()) @@ -241,8 +263,33 @@ static async Task Main(string[] args) } catch (Exception ex) { - ErrorHandler.Handle(ex, logger, showStackTrace: logger != null); - Environment.ExitCode = 1; + // Determine output format from args (options may not be available if parsing failed) + var useJson = args.Any(a => a.Equals("--output", StringComparison.OrdinalIgnoreCase)) && + args.SkipWhile(a => !a.Equals("--output", StringComparison.OrdinalIgnoreCase)).Skip(1).FirstOrDefault()?.Equals("json", StringComparison.OrdinalIgnoreCase) == true; + + if (useJson) + { + var errorCode = ex is PswException pswEx ? pswEx.ErrorCode : null; + var suggestion = ex is PswException pswEx2 ? pswEx2.Suggestion : null; + OutputHelper.WriteErrorJson(ex.Message, errorCode, suggestion); + } + else + { + ErrorHandler.Handle(ex, logger, showStackTrace: logger != null); + } + + // Use distinct exit codes based on exception type + Environment.ExitCode = ex switch + { + ValidationException => ExitCodes.ValidationError, + ApiException => ExitCodes.NetworkError, + ScriptExecutionException => ExitCodes.ScriptExecutionError, + HttpRequestException => ExitCodes.NetworkError, + TimeoutException => ExitCodes.NetworkError, + UnauthorizedAccessException => ExitCodes.FileSystemError, + IOException => ExitCodes.FileSystemError, + _ => ExitCodes.GeneralError + }; } finally { diff --git a/src/PackageCliTool/UI/ConsoleDisplay.cs b/src/PackageCliTool/UI/ConsoleDisplay.cs index 5388dda..f4c568e 100644 --- a/src/PackageCliTool/UI/ConsoleDisplay.cs +++ b/src/PackageCliTool/UI/ConsoleDisplay.cs @@ -45,39 +45,51 @@ psw [[options]] psw template [[options]] psw history [[options]] psw versions + psw list-options [[category]] [bold yellow]MAIN OPTIONS:[/] - [green] --admin-email[/] Admin email for unattended install - [green] --admin-name[/] Admin user friendly name for unattended install - [green] --admin-password[/] Admin password for unattended install - [green] --auto-run[/] Automatically run the generated script - [green] --clear-cache[/] Clear all cached API responses - [green] --connection-string[/] Connection string (for SQLServer/SQLAzure) - [green] --database-type[/] Database type (SQLite, LocalDb, SQLServer, SQLAzure, SQLCE) [green]-d, --default[/] Generate a default script with minimal configuration + [green]-p, --packages[/] [dim](string)[/] Comma-separated packages with optional versions + Format: ""Package1|Version1,Package2"" + Example: ""uSync|17.0.0,Umbraco.Forms"" + [green]-t, --template-package[/] [dim](string)[/] Template package with optional version + Format: ""PackageName|Version"" or just ""PackageName"" + Example: ""Umbraco.Templates|17.0.3"" + [green] --template-version[/] [dim](string)[/] Template version (alternative to pipe syntax) + [green]-n, --project-name[/] [dim](string, default: MyProject)[/] Project name + [green]-s, --solution[/] [dim](string)[/] Solution name (enables solution file creation) + [green]-k, --starter-kit[/] [dim](string)[/] Starter kit: clean, Articulate, Portfolio, etc. + Format: ""PackageName|Version"" or just ""PackageName"" [green] --dockerfile[/] Include Dockerfile in generated script [green] --docker-compose[/] Include Docker Compose file in generated script [green]-da, --delivery-api[/] Enable Content Delivery API - [green]-h, --help[/] Show this help information - [green] --include-prerelease[/] Include prerelease package versions - [green]-k, --starter-kit[/] Starter kit package name - [green]-n, --project-name[/] Project name (default: MyProject) + [green]-u, --unattended-defaults[/] Use unattended install with defaults (SQLite, admin@example.com) + [green] --database-type[/] [dim](enum: SQLite, LocalDb, SQLServer, SQLAzure, SQLCE)[/] + Database type. Implies unattended install. + [dim]Note: SQLServer/SQLAzure require --connection-string[/] + [green] --connection-string[/] [dim](string)[/] Database connection string + [green] --admin-name[/] [dim](string, default: Administrator)[/] Admin friendly name + [green] --admin-email[/] [dim](string, default: admin@example.com)[/] Admin email + [green] --admin-password[/] [dim](string, default: 1234567890)[/] Admin password (min 10 chars) [green]-o, --oneliner[/] Output script as one-liner - [green]-p, --packages[/] Comma-separated list of packages with optional versions - Format: ""Package1|Version1,Package2|Version2"" - Or just package names: ""uSync,Umbraco.Forms"" (uses latest) - Example: ""uSync|17.0.0,clean|7.0.1"" [green]-r, --remove-comments[/] Remove comments from generated script - [green] --run-dir[/] Directory to run script in - [green]-s, --solution[/] Solution name - [green] --starter-kit-package[/] Starter kit package name - [green]-t, --template-package[/] Comma-separated list of packages with optional version - Format: ""Package|Version"" - Or just template name: ""Umbraco.Templates"" (uses latest) - Example: ""Umbraco.Templates|17.0.3"" - [green]-u, --unattended-defaults[/] Use unattended install defaults + [green] --include-prerelease[/] Include prerelease package versions + [green] --auto-run[/] Automatically run the generated script + [green] --run-dir[/] [dim](string)[/] Directory to run script in + [green] --community-template[/] [dim](string)[/] Load community template by name, or 'list' + +[bold yellow]OUTPUT & AI AGENT OPTIONS:[/] + [green] --output[/] [dim](enum: json, plain)[/] Output format for machine consumption + [green] --script-only[/] Output only the raw script text, no decoration + [green] --no-interaction[/] Suppress all interactive prompts (fail if input needed) + [green] --dry-run[/] Validate inputs and show config without generating script + [green] --help-json[/] Show all commands and options as structured JSON + +[bold yellow]GENERAL OPTIONS:[/] + [green]-h, --help[/] Show this help information [green]-v, --version[/] Show version information [green] --verbose[/] Enable verbose logging mode + [green] --clear-cache[/] Clear all cached API responses [bold yellow]TEMPLATE COMMANDS:[/] [green]psw template save[/] Save current configuration as a template @@ -165,7 +177,32 @@ [cyan]psw history clear[/] [bold yellow]VERSIONS EXAMPLES:[/] Display Umbraco versions table: - [cyan]psw versions[/]") + [cyan]psw versions[/] + +[bold yellow]AI AGENT / AUTOMATION EXAMPLES:[/] + Generate script as JSON (for programmatic use): + [cyan]psw --default --output json[/] + + Generate script with no decoration (pipe-friendly): + [cyan]psw --default --script-only[/] + + Validate config without generating (dry run): + [cyan]psw --dry-run -p ""uSync|17.0.0"" --database-type SQLite[/] + + Get help as JSON (for AI tool discovery): + [cyan]psw --help-json[/] + + List valid option values as JSON: + [cyan]psw list-options --output json[/] + + List valid database types: + [cyan]psw list-options database-types[/] + + Non-interactive mode (no prompts): + [cyan]psw --default --no-interaction --script-only[/] + + Get version as plain text: + [cyan]psw --version --output plain[/]") .Header("[bold blue]Package Script Writer Help[/]") .Border(BoxBorder.Rounded) .BorderColor(Color.Blue) diff --git a/src/PackageCliTool/UI/OutputHelper.cs b/src/PackageCliTool/UI/OutputHelper.cs new file mode 100644 index 0000000..04a01ee --- /dev/null +++ b/src/PackageCliTool/UI/OutputHelper.cs @@ -0,0 +1,245 @@ +using PackageCliTool.Models; +using PackageCliTool.Models.Api; +using Spectre.Console; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace PackageCliTool.UI; + +/// +/// Provides output methods that respect the chosen OutputFormat. +/// When format is Json or Plain, Spectre.Console markup is suppressed. +/// +public static class OutputHelper +{ + private static readonly JsonSerializerOptions JsonOptions = new() + { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + /// + /// Returns true if the format suppresses Spectre.Console rich output + /// + public static bool IsMachineReadable(OutputFormat format) => + format is OutputFormat.Json or OutputFormat.Plain; + + /// + /// Writes a status message to stderr (visible to humans, ignored by piped output). + /// Suppressed entirely in Json mode. + /// + public static void Status(string message, OutputFormat format) + { + if (format == OutputFormat.Json) return; + if (format == OutputFormat.Plain) + { + Console.Error.WriteLine(message); + return; + } + AnsiConsole.MarkupLine(message); + } + + /// + /// Writes a generated script to the appropriate output stream. + /// + public static void WriteScript(string script, OutputFormat format, ScriptModel? configuration = null, string? title = null) + { + switch (format) + { + case OutputFormat.Json: + WriteScriptJson(script, configuration); + break; + case OutputFormat.Plain: + Console.Write(script); + break; + default: + ConsoleDisplay.DisplayGeneratedScript(script, title ?? "Generated Installation Script"); + break; + } + } + + /// + /// Writes the script as a structured JSON response to stdout + /// + private static void WriteScriptJson(string script, ScriptModel? configuration) + { + var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown"; + var response = new + { + success = true, + script, + configuration = configuration != null ? new + { + templateName = configuration.TemplateName, + templateVersion = configuration.TemplateVersion, + projectName = configuration.ProjectName, + solutionName = configuration.SolutionName, + createSolutionFile = configuration.CreateSolutionFile, + packages = configuration.Packages, + includeStarterKit = configuration.IncludeStarterKit, + starterKitPackage = configuration.StarterKitPackage, + includeDockerfile = configuration.IncludeDockerfile, + includeDockerCompose = configuration.IncludeDockerCompose, + enableContentDeliveryApi = configuration.EnableContentDeliveryApi, + useUnattendedInstall = configuration.UseUnattendedInstall, + databaseType = configuration.DatabaseType, + onelinerOutput = configuration.OnelinerOutput, + removeComments = configuration.RemoveComments + } : null, + metadata = new + { + generatedAt = DateTime.UtcNow.ToString("o"), + cliVersion = version + } + }; + Console.WriteLine(JsonSerializer.Serialize(response, JsonOptions)); + } + + /// + /// Writes an error as JSON to stdout (for machine-readable error handling) + /// + public static void WriteErrorJson(string error, string? errorCode = null, string? suggestion = null) + { + var response = new + { + success = false, + error, + errorCode, + suggestion + }; + Console.WriteLine(JsonSerializer.Serialize(response, JsonOptions)); + } + + /// + /// Writes structured JSON output for version information + /// + public static void WriteVersionJson() + { + var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown"; + var response = new + { + name = "PackageScriptWriter.Cli", + version, + runtime = System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription, + platform = System.Runtime.InteropServices.RuntimeInformation.RuntimeIdentifier + }; + Console.WriteLine(JsonSerializer.Serialize(response, JsonOptions)); + } + + /// + /// Writes plain version string (just the number) + /// + public static void WriteVersionPlain() + { + var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown"; + Console.WriteLine(version); + } + + /// + /// Writes the full help information as structured JSON + /// + public static void WriteHelpJson() + { + var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown"; + + var help = new + { + name = "psw", + description = "Package Script Writer - Generate Umbraco CMS installation scripts", + version, + usage = new[] + { + "psw [options]", + "psw template [options]", + "psw history [options]", + "psw versions", + "psw list-options [category]" + }, + options = new object[] + { + new { name = "--help", shortName = "-h", type = "flag", description = "Show help information" }, + new { name = "--help-json", shortName = (string?)null, type = "flag", description = "Show help as structured JSON (for AI agents and tooling)" }, + new { name = "--version", shortName = "-v", type = "flag", description = "Show version information" }, + new { name = "--default", shortName = "-d", type = "flag", description = "Generate a default script with minimal configuration" }, + new { name = "--packages", shortName = "-p", type = "string", required = false, description = "Comma-separated list of packages with optional versions", format = "Package1|Version1,Package2|Version2", examples = new[] { "uSync|17.0.0,Umbraco.Forms", "uSync,Diplo.GodMode" } }, + new { name = "--template-package", shortName = "-t", type = "string", required = false, description = "Template package name with optional version", format = "PackageName|Version", examples = new[] { "Umbraco.Templates|17.0.3", "Umbraco.Templates" } }, + new { name = "--template-version", shortName = (string?)null, type = "string", required = false, description = "Template version (alternative to pipe syntax in --template-package)", format = "major.minor.patch", examples = new[] { "17.0.3", "14.0.0" } }, + new { name = "--project-name", shortName = "-n", type = "string", required = false, description = "Project name", @default = "MyProject" }, + new { name = "--solution", shortName = "-s", type = "string", required = false, description = "Solution name (also enables solution file creation)" }, + new { name = "--starter-kit", shortName = "-k", type = "string", required = false, description = "Starter kit package with optional version", format = "PackageName|Version", validValues = new[] { "clean", "Articulate", "Portfolio", "LittleNorth.Igloo", "Umbraco.BlockGrid.Example.Website", "Umbraco.TheStarterKit", "uSkinnedSiteBuilder" } }, + new { name = "--dockerfile", shortName = (string?)null, type = "flag", description = "Include Dockerfile in generated script" }, + new { name = "--docker-compose", shortName = (string?)null, type = "flag", description = "Include Docker Compose file in generated script" }, + new { name = "--delivery-api", shortName = "-da", type = "flag", description = "Enable Content Delivery API" }, + new { name = "--unattended-defaults", shortName = "-u", type = "flag", description = "Use unattended install with defaults (SQLite, admin@example.com, 1234567890)" }, + new { name = "--database-type", shortName = (string?)null, type = "enum", required = false, description = "Database type for unattended install. Implies --unattended-defaults.", validValues = new[] { "SQLite", "LocalDb", "SQLServer", "SQLAzure", "SQLCE" }, note = "SQLServer and SQLAzure require --connection-string" }, + new { name = "--connection-string", shortName = (string?)null, type = "string", required = false, description = "Database connection string (required for SQLServer/SQLAzure)" }, + new { name = "--admin-name", shortName = (string?)null, type = "string", required = false, description = "Admin user friendly name for unattended install" }, + new { name = "--admin-email", shortName = (string?)null, type = "string", required = false, description = "Admin email for unattended install", format = "email" }, + new { name = "--admin-password", shortName = (string?)null, type = "string", required = false, description = "Admin password for unattended install (min 10 characters)" }, + new { name = "--oneliner", shortName = "-o", type = "flag", description = "Output script as one-liner" }, + new { name = "--remove-comments", shortName = "-r", type = "flag", description = "Remove comments from generated script" }, + new { name = "--include-prerelease", shortName = (string?)null, type = "flag", description = "Include prerelease package versions" }, + new { name = "--auto-run", shortName = (string?)null, type = "flag", description = "Automatically run the generated script" }, + new { name = "--run-dir", shortName = (string?)null, type = "string", required = false, description = "Directory to run script in" }, + new { name = "--community-template", shortName = (string?)null, type = "string", required = false, description = "Load a community template by name, or 'list' to show all" }, + new { name = "--output", shortName = (string?)null, type = "enum", required = false, description = "Output format", validValues = new[] { "json", "plain" }, @default = "default (rich Spectre.Console)" }, + new { name = "--script-only", shortName = (string?)null, type = "flag", description = "Output only the raw script text, no decoration or status messages" }, + new { name = "--no-interaction", shortName = (string?)null, type = "flag", description = "Suppress all interactive prompts. Fails if user input would be required." }, + new { name = "--dry-run", shortName = (string?)null, type = "flag", description = "Validate inputs and show configuration without generating a script" }, + new { name = "--verbose", shortName = (string?)null, type = "flag", description = "Enable verbose logging" }, + new { name = "--clear-cache", shortName = (string?)null, type = "flag", description = "Clear all cached API responses" } + }, + commands = new object[] + { + new + { + name = "template", + description = "Manage script configuration templates", + subcommands = new object[] + { + new { name = "save", arguments = new[] { new { name = "name", required = true, description = "Template name" } }, description = "Save current configuration as a template" }, + new { name = "load", arguments = new[] { new { name = "name", required = true, description = "Template name" } }, description = "Load and execute a template" }, + new { name = "list", arguments = Array.Empty(), description = "List all available templates" }, + new { name = "delete", arguments = new[] { new { name = "name", required = true, description = "Template name" } }, description = "Delete a template" }, + new { name = "export", arguments = new[] { new { name = "name", required = true, description = "Template name" } }, description = "Export template to file" }, + new { name = "import", arguments = new[] { new { name = "file", required = true, description = "Template file path" } }, description = "Import template from file" }, + new { name = "validate", arguments = new[] { new { name = "file", required = true, description = "Template file path" } }, description = "Validate template file" } + } + }, + new + { + name = "history", + description = "Manage script generation history", + subcommands = new object[] + { + new { name = "list", arguments = Array.Empty(), description = "List recent script generation history" }, + new { name = "rerun", arguments = new[] { new { name = "number", required = true, description = "History entry number" } }, description = "Regenerate and re-run a script from history" }, + new { name = "delete", arguments = new[] { new { name = "number", required = true, description = "History entry number" } }, description = "Delete a history entry" }, + new { name = "clear", arguments = Array.Empty(), description = "Clear all history" } + } + }, + new + { + name = "versions", + description = "Display Umbraco versions table with support lifecycle information", + subcommands = Array.Empty() + }, + new + { + name = "list-options", + description = "List valid values for CLI options (for AI agents and tooling)", + subcommands = new object[] + { + new { name = "database-types", arguments = Array.Empty(), description = "List valid database types" }, + new { name = "starter-kits", arguments = Array.Empty(), description = "List available starter kits" }, + new { name = "defaults", arguments = Array.Empty(), description = "Show default values for all options" } + } + } + } + }; + + Console.WriteLine(JsonSerializer.Serialize(help, JsonOptions)); + } +} diff --git a/src/PackageCliTool/Workflows/CliModeWorkflow.cs b/src/PackageCliTool/Workflows/CliModeWorkflow.cs index 82dd74e..ee14be7 100644 --- a/src/PackageCliTool/Workflows/CliModeWorkflow.cs +++ b/src/PackageCliTool/Workflows/CliModeWorkflow.cs @@ -10,6 +10,8 @@ using PackageCliTool.Logging; using PSW.Shared.Services; using PackageCliTool.Extensions; +using System.Text.Json; +using System.Text.Json.Serialization; namespace PackageCliTool.Workflows; @@ -78,11 +80,15 @@ public async Task RunAsync(CommandLineOptions options) private async Task GenerateDefaultScriptAsync(CommandLineOptions options) { _logger?.LogInformation("Generating default script"); + var machineReadable = OutputHelper.IsMachineReadable(options.OutputFormat) || options.ScriptOnly; - AnsiConsole.WriteLine(); - AnsiConsole.MarkupLine("[bold blue]Generating Default Script[/]\n"); - AnsiConsole.MarkupLine("[dim]Using default configuration (latest stable Umbraco with clean starter kit)[/]"); - AnsiConsole.WriteLine(); + if (!machineReadable) + { + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine("[bold blue]Generating Default Script[/]\n"); + AnsiConsole.MarkupLine("[dim]Using default configuration (latest stable Umbraco with clean starter kit)[/]"); + AnsiConsole.WriteLine(); + } // Create default script model matching website defaults var model = new ScriptModel @@ -145,13 +151,28 @@ private async Task GenerateDefaultScriptAsync(CommandLineOptions options) HandleStarterKitPackage(options, model); HandleTemplatePackage(options, model); - var script = await AnsiConsole.Status() - .Spinner(Spinner.Known.Star) - .SpinnerStyle(Style.Parse("green")) - .StartAsync("Generating default installation script...", async ctx => - { - return _scriptGeneratorService.GenerateScript(model.ToViewModel()); - }); + // Dry-run: validate and show config without generating + if (options.DryRun) + { + WriteDryRunResult(options, model); + return; + } + + string script; + if (machineReadable) + { + script = _scriptGeneratorService.GenerateScript(model.ToViewModel()); + } + else + { + script = await AnsiConsole.Status() + .Spinner(Spinner.Known.Star) + .SpinnerStyle(Style.Parse("green")) + .StartAsync("Generating default installation script...", async ctx => + { + return _scriptGeneratorService.GenerateScript(model.ToViewModel()); + }); + } _logger?.LogInformation("Default script generated successfully"); @@ -161,7 +182,7 @@ private async Task GenerateDefaultScriptAsync(CommandLineOptions options) templateName: model.TemplateName, description: $"Default script for {model.ProjectName}"); - ConsoleDisplay.DisplayGeneratedScript(script, "Generated Default Installation Script"); + OutputHelper.WriteScript(script, options.ScriptOnly ? OutputFormat.Plain : options.OutputFormat, model, "Generated Default Installation Script"); // Handle auto-run or interactive run await HandleScriptExecutionAsync(script, options); @@ -173,6 +194,7 @@ private async Task GenerateDefaultScriptAsync(CommandLineOptions options) private async Task GenerateCustomScriptFromOptionsAsync(CommandLineOptions options) { _logger?.LogInformation("Generating custom script from command-line options"); + var machineReadable = OutputHelper.IsMachineReadable(options.OutputFormat) || options.ScriptOnly; var projectName = !string.IsNullOrWhiteSpace(options.ProjectName) ? options.ProjectName @@ -249,16 +271,31 @@ private async Task GenerateCustomScriptFromOptionsAsync(CommandLineOptions optio HandleStarterKitPackage(options, model); HandleTemplatePackage(options, model); + // Dry-run: validate and show config without generating + if (options.DryRun) + { + WriteDryRunResult(options, model); + return; + } + // Generate the script _logger?.LogInformation("Generating installation script via API"); - var script = await AnsiConsole.Status() - .Spinner(Spinner.Known.Star) - .SpinnerStyle(Style.Parse("green")) - .StartAsync("Generating installation script...", async ctx => - { - return _scriptGeneratorService.GenerateScript(model.ToViewModel()); - }); + string script; + if (machineReadable) + { + script = _scriptGeneratorService.GenerateScript(model.ToViewModel()); + } + else + { + script = await AnsiConsole.Status() + .Spinner(Spinner.Known.Star) + .SpinnerStyle(Style.Parse("green")) + .StartAsync("Generating installation script...", async ctx => + { + return _scriptGeneratorService.GenerateScript(model.ToViewModel()); + }); + } _logger?.LogInformation("Script generated successfully"); @@ -268,7 +305,7 @@ private async Task GenerateCustomScriptFromOptionsAsync(CommandLineOptions optio templateName: model.TemplateName, description: $"Custom script for {model.ProjectName ?? "project"}"); - ConsoleDisplay.DisplayGeneratedScript(script); + OutputHelper.WriteScript(script, options.ScriptOnly ? OutputFormat.Plain : options.OutputFormat, model); // Handle auto-run or interactive run await HandleScriptExecutionAsync(script, options); @@ -276,6 +313,8 @@ private async Task GenerateCustomScriptFromOptionsAsync(CommandLineOptions optio private void HandlePackages(CommandLineOptions options, ScriptModel model) { + var machineReadable = OutputHelper.IsMachineReadable(options.OutputFormat) || options.ScriptOnly; + // Handle packages if (!string.IsNullOrWhiteSpace(options.Packages)) { @@ -305,12 +344,14 @@ private void HandlePackages(CommandLineOptions options, ScriptModel model) InputValidator.ValidateVersion(version); processedPackages.Add($"{packageName}|{version}"); - AnsiConsole.MarkupLine($"[green]✓[/] Using {packageName} version {version}"); + if (!machineReadable) + AnsiConsole.MarkupLine($"[green]✓[/] Using {packageName} version {version}"); _logger?.LogDebug("Added package {Package} with version {Version}", packageName, version); } else { - ErrorHandler.Warning($"Invalid package format: {entry}, skipping...", _logger); + if (!machineReadable) + ErrorHandler.Warning($"Invalid package format: {entry}, skipping...", _logger); } } else @@ -320,7 +361,8 @@ private void HandlePackages(CommandLineOptions options, ScriptModel model) InputValidator.ValidatePackageName(packageName); processedPackages.Add(packageName); - AnsiConsole.MarkupLine($"[green]✓[/] Using {packageName} (latest version)"); + if (!machineReadable) + AnsiConsole.MarkupLine($"[green]✓[/] Using {packageName} (latest version)"); _logger?.LogDebug("Added package {Package} with latest version", packageName); } } @@ -336,6 +378,8 @@ private void HandlePackages(CommandLineOptions options, ScriptModel model) private void HandleStarterKitPackage(CommandLineOptions options, ScriptModel model) { + var machineReadable = OutputHelper.IsMachineReadable(options.OutputFormat) || options.ScriptOnly; + // Handle starter kit package if (!string.IsNullOrWhiteSpace(options.StarterKitPackage)) { @@ -355,14 +399,16 @@ private void HandleStarterKitPackage(CommandLineOptions options, ScriptModel mod // Store with --version flag for the model model.StarterKitPackage = $"{options.StarterKitPackage} --version {options.StarterKitVersion}"; - AnsiConsole.MarkupLine($"[green]✓[/] Using starter kit {options.StarterKitPackage} version {options.StarterKitVersion}"); + if (!machineReadable) + AnsiConsole.MarkupLine($"[green]✓[/] Using starter kit {options.StarterKitPackage} version {options.StarterKitVersion}"); _logger?.LogDebug("Using starter kit {Package} with version {Version}", options.StarterKitPackage, options.StarterKitVersion); } else { model.StarterKitPackage = options.StarterKitPackage; - AnsiConsole.MarkupLine($"[green]✓[/] Using starter kit {options.StarterKitPackage} (latest version)"); + if (!machineReadable) + AnsiConsole.MarkupLine($"[green]✓[/] Using starter kit {options.StarterKitPackage} (latest version)"); _logger?.LogDebug("Using starter kit {Package} with latest version", options.StarterKitPackage); } } @@ -370,6 +416,8 @@ private void HandleStarterKitPackage(CommandLineOptions options, ScriptModel mod private void HandleTemplatePackage(CommandLineOptions options, ScriptModel model) { + var machineReadable = OutputHelper.IsMachineReadable(options.OutputFormat) || options.ScriptOnly; + // Handle template package if (!string.IsNullOrWhiteSpace(options.TemplatePackageName)) { @@ -389,12 +437,14 @@ private void HandleTemplatePackage(CommandLineOptions options, ScriptModel model model.TemplateVersion = options.TemplateVersion; - AnsiConsole.MarkupLine($"[green]✓[/] Using {options.TemplatePackageName} version {options.TemplateVersion}"); + if (!machineReadable) + AnsiConsole.MarkupLine($"[green]✓[/] Using {options.TemplatePackageName} version {options.TemplateVersion}"); _logger?.LogDebug("Using template {Template} with version {Version}", options.TemplatePackageName, options.TemplateVersion); } else { - AnsiConsole.MarkupLine($"[green]✓[/] Using {options.TemplatePackageName} (latest version)"); + if (!machineReadable) + AnsiConsole.MarkupLine($"[green]✓[/] Using {options.TemplatePackageName} (latest version)"); _logger?.LogDebug("Using template {Template} with latest version", options.TemplatePackageName); } } @@ -405,6 +455,30 @@ private void HandleTemplatePackage(CommandLineOptions options, ScriptModel model /// private async Task HandleScriptExecutionAsync(string script, CommandLineOptions options) { + // In machine-readable or non-interactive mode, skip post-generation prompts + if (OutputHelper.IsMachineReadable(options.OutputFormat) || options.ScriptOnly || options.NonInteractive) + { + // Only auto-run if explicitly requested + if (options.AutoRun || !string.IsNullOrWhiteSpace(options.RunDirectory)) + { + var targetDir = !string.IsNullOrWhiteSpace(options.RunDirectory) + ? options.RunDirectory + : Directory.GetCurrentDirectory(); + + InputValidator.ValidateDirectoryPath(targetDir); + targetDir = Path.GetFullPath(targetDir); + + if (!Directory.Exists(targetDir)) + { + _logger?.LogInformation("Creating directory: {Directory}", targetDir); + Directory.CreateDirectory(targetDir); + } + + await _scriptExecutor.RunScriptAsync(script, targetDir); + } + return; + } + if (options.AutoRun || !string.IsNullOrWhiteSpace(options.RunDirectory)) { var targetDir = !string.IsNullOrWhiteSpace(options.RunDirectory) @@ -726,4 +800,91 @@ private ScriptModel ConvertTemplateToScriptModel(Template template, CommandLineO return model; } + + /// + /// Outputs a dry-run result showing validated configuration without generating a script + /// + private void WriteDryRunResult(CommandLineOptions options, ScriptModel model) + { + _logger?.LogInformation("Dry-run: validation passed, displaying configuration"); + + if (options.OutputFormat == OutputFormat.Json) + { + var jsonOptions = new JsonSerializerOptions + { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + var result = new + { + success = true, + dryRun = true, + message = "Validation passed. Configuration is valid.", + configuration = new + { + templateName = model.TemplateName, + templateVersion = model.TemplateVersion, + projectName = model.ProjectName, + solutionName = model.SolutionName, + createSolutionFile = model.CreateSolutionFile, + packages = model.Packages, + includeStarterKit = model.IncludeStarterKit, + starterKitPackage = model.StarterKitPackage, + includeDockerfile = model.IncludeDockerfile, + includeDockerCompose = model.IncludeDockerCompose, + enableContentDeliveryApi = model.EnableContentDeliveryApi, + useUnattendedInstall = model.UseUnattendedInstall, + databaseType = model.DatabaseType, + onelinerOutput = model.OnelinerOutput, + removeComments = model.RemoveComments + } + }; + Console.WriteLine(JsonSerializer.Serialize(result, jsonOptions)); + } + else if (options.OutputFormat == OutputFormat.Plain || options.ScriptOnly) + { + Console.WriteLine("Dry-run: validation passed"); + Console.WriteLine($"Template: {model.TemplateName} {model.TemplateVersion}"); + Console.WriteLine($"Project: {model.ProjectName}"); + if (!string.IsNullOrWhiteSpace(model.Packages)) + Console.WriteLine($"Packages: {model.Packages}"); + if (model.UseUnattendedInstall) + Console.WriteLine($"Database: {model.DatabaseType}"); + } + else + { + AnsiConsole.MarkupLine("[green]✓ Dry-run: validation passed[/]"); + AnsiConsole.MarkupLine("[dim]Configuration is valid. Remove --dry-run to generate the script.[/]"); + AnsiConsole.WriteLine(); + + var table = new Table() + .Border(TableBorder.Rounded) + .BorderColor(Color.Green) + .Title("[bold green]Validated Configuration[/]"); + + table.AddColumn("[bold]Setting[/]"); + table.AddColumn("[bold]Value[/]"); + + table.AddRow("Template", $"{model.TemplateName} {model.TemplateVersion}".Trim()); + table.AddRow("Project Name", model.ProjectName ?? "N/A"); + if (model.CreateSolutionFile) + table.AddRow("Solution Name", model.SolutionName ?? "N/A"); + if (!string.IsNullOrWhiteSpace(model.Packages)) + table.AddRow("Packages", model.Packages); + if (model.IncludeStarterKit) + table.AddRow("Starter Kit", model.StarterKitPackage ?? "N/A"); + if (model.IncludeDockerfile) + table.AddRow("Dockerfile", "Yes"); + if (model.IncludeDockerCompose) + table.AddRow("Docker Compose", "Yes"); + if (model.UseUnattendedInstall) + { + table.AddRow("Unattended Install", "Yes"); + table.AddRow("Database Type", model.DatabaseType ?? "N/A"); + } + + AnsiConsole.Write(table); + } + } } diff --git a/src/PackageCliTool/Workflows/ListOptionsWorkflow.cs b/src/PackageCliTool/Workflows/ListOptionsWorkflow.cs new file mode 100644 index 0000000..92d6b5e --- /dev/null +++ b/src/PackageCliTool/Workflows/ListOptionsWorkflow.cs @@ -0,0 +1,208 @@ +using Microsoft.Extensions.Logging; +using PackageCliTool.Models; +using PackageCliTool.UI; +using Spectre.Console; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace PackageCliTool.Workflows; + +/// +/// Handles the list-options command, which outputs valid values for CLI options. +/// Designed primarily for AI agents and tooling to discover valid inputs. +/// +public class ListOptionsWorkflow +{ + private readonly ILogger? _logger; + + private static readonly JsonSerializerOptions JsonOptions = new() + { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + /// Valid database types accepted by --database-type + public static readonly string[] DatabaseTypes = { "SQLite", "LocalDb", "SQLServer", "SQLAzure", "SQLCE" }; + + /// Available starter kits accepted by --starter-kit + public static readonly string[] StarterKits = { "clean", "Articulate", "Portfolio", "LittleNorth.Igloo", "Umbraco.BlockGrid.Example.Website", "Umbraco.TheStarterKit", "uSkinnedSiteBuilder" }; + + public ListOptionsWorkflow(ILogger? logger = null) + { + _logger = logger; + } + + /// + /// Runs the list-options command + /// + public void Run(CommandLineOptions options) + { + _logger?.LogInformation("Listing options: {Category}", options.ListOptionsCommand); + + var category = options.ListOptionsCommand?.ToLower() ?? ""; + + if (options.OutputFormat == OutputFormat.Json) + { + WriteJson(category); + return; + } + + switch (category) + { + case "database-types": + WriteDatabaseTypes(options.OutputFormat); + break; + case "starter-kits": + WriteStarterKits(options.OutputFormat); + break; + case "defaults": + WriteDefaults(options.OutputFormat); + break; + case "": + WriteAll(options.OutputFormat); + break; + default: + if (options.OutputFormat == OutputFormat.Plain) + { + Console.Error.WriteLine($"Unknown category: {category}"); + Console.Error.WriteLine("Valid categories: database-types, starter-kits, defaults"); + } + else + { + AnsiConsole.MarkupLine($"[red]Unknown category:[/] {category}"); + AnsiConsole.MarkupLine("[dim]Valid categories: database-types, starter-kits, defaults[/]"); + } + Environment.ExitCode = ExitCodes.ValidationError; + break; + } + } + + private void WriteJson(string category) + { + object result = category switch + { + "database-types" => new { databaseTypes = DatabaseTypes }, + "starter-kits" => new { starterKits = StarterKits }, + "defaults" => GetDefaultsObject(), + _ => new + { + databaseTypes = DatabaseTypes, + starterKits = StarterKits, + defaults = GetDefaultsObject() + } + }; + + Console.WriteLine(JsonSerializer.Serialize(result, JsonOptions)); + } + + private static object GetDefaultsObject() => new + { + projectName = "MyProject", + solutionName = "MySolution", + templatePackage = "Umbraco.Templates", + databaseType = "SQLite", + adminEmail = "admin@example.com", + adminPassword = "1234567890", + adminName = "Administrator", + starterKit = "clean", + createSolution = true, + includeStarterKit = true, + useUnattendedInstall = true + }; + + private void WriteDatabaseTypes(OutputFormat format) + { + if (format == OutputFormat.Plain) + { + foreach (var type in DatabaseTypes) + Console.WriteLine(type); + return; + } + + AnsiConsole.MarkupLine("[bold yellow]Valid Database Types:[/]"); + foreach (var type in DatabaseTypes) + AnsiConsole.MarkupLine($" [green]{type}[/]"); + AnsiConsole.MarkupLine("\n[dim]Usage: psw --database-type SQLite[/]"); + } + + private void WriteStarterKits(OutputFormat format) + { + if (format == OutputFormat.Plain) + { + foreach (var kit in StarterKits) + Console.WriteLine(kit); + return; + } + + AnsiConsole.MarkupLine("[bold yellow]Available Starter Kits:[/]"); + foreach (var kit in StarterKits) + AnsiConsole.MarkupLine($" [green]{kit}[/]"); + AnsiConsole.MarkupLine("\n[dim]Usage: psw --starter-kit clean[/]"); + } + + private void WriteDefaults(OutputFormat format) + { + if (format == OutputFormat.Plain) + { + Console.WriteLine("projectName=MyProject"); + Console.WriteLine("solutionName=MySolution"); + Console.WriteLine("templatePackage=Umbraco.Templates"); + Console.WriteLine("databaseType=SQLite"); + Console.WriteLine("adminEmail=admin@example.com"); + Console.WriteLine("adminPassword=1234567890"); + Console.WriteLine("adminName=Administrator"); + Console.WriteLine("starterKit=clean"); + Console.WriteLine("createSolution=true"); + Console.WriteLine("includeStarterKit=true"); + Console.WriteLine("useUnattendedInstall=true"); + return; + } + + var table = new Table() + .Border(TableBorder.Rounded) + .BorderColor(Color.Blue) + .Title("[bold blue]Default Values[/]"); + + table.AddColumn("[bold]Option[/]"); + table.AddColumn("[bold]Default[/]"); + + table.AddRow("--project-name", "MyProject"); + table.AddRow("--solution", "MySolution"); + table.AddRow("--template-package", "Umbraco.Templates"); + table.AddRow("--database-type", "SQLite"); + table.AddRow("--admin-email", "admin@example.com"); + table.AddRow("--admin-password", "1234567890"); + table.AddRow("--admin-name", "Administrator"); + table.AddRow("--starter-kit", "clean"); + + AnsiConsole.Write(table); + AnsiConsole.MarkupLine("\n[dim]These are the defaults used with --default flag.[/]"); + } + + private void WriteAll(OutputFormat format) + { + if (format == OutputFormat.Plain) + { + Console.WriteLine("# Database Types"); + WriteDatabaseTypes(format); + Console.WriteLine(); + Console.WriteLine("# Starter Kits"); + WriteStarterKits(format); + Console.WriteLine(); + Console.WriteLine("# Defaults"); + WriteDefaults(format); + return; + } + + AnsiConsole.MarkupLine("[bold blue]Available Option Categories[/]\n"); + WriteDatabaseTypes(format); + AnsiConsole.WriteLine(); + WriteStarterKits(format); + AnsiConsole.WriteLine(); + WriteDefaults(format); + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine("[dim]Tip: Use psw list-options to show a specific category[/]"); + AnsiConsole.MarkupLine("[dim] Use psw list-options --output json for machine-readable output[/]"); + } +}