Skip to content
Merged
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
55 changes: 54 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -785,7 +785,60 @@ Use cases:
- **Generated documentation** — pipe into a docs generator or templating step to keep reference docs in sync without manual maintenance.
- **CI validation** — diff `cli-schema.json` across commits to catch unintentional breaking changes to the CLI surface.

The shape is defined by [`ArghCliSchemaDocument`](src/Nullean.Argh.Core/Schema/ArghCliSchemaDocument.cs). Output is indented camelCase JSON. Reserved meta-commands (`__complete`, `__completion`, `__schema`) appear under `reservedMetaCommands`.
The shape is defined by [`ArghCliSchemaDocument`](src/Nullean.Argh.Core/Schema/ArghCliSchemaDocument.cs) and conforms to the [cli-schema v1 specification](https://github.com/cli-schema/cli-schema). Output is indented camelCase JSON. Reserved meta-commands (`__complete`, `__completion`, `__schema`) appear under `reservedMetaCommands`.

### Schema enrichment attributes

Add `using Nullean.Argh.Documentation;` to access the attributes below. They have no effect on parsing or validation — they only enrich the `__schema` output for agent tooling and documentation consumers.

**Side-effect profile** (`intent` object):

```csharp
[CommandIntent(Intent.Destructive | Intent.RequiresConfirmation)]
[MutationScope(MutationScope.Global)] // File | Directory | Global
[RequiresAuth]
public static Task Delete([ConfirmationSkip] bool yes = false, ...) { }
```

`Intent` is a flags enum — combine with `|`. `MutationScope.Global` means the command reaches beyond the local filesystem (cloud resources, databases, registries, etc.). `[RequiresAuth]` signals that an authenticated session is required. `[ConfirmationSkip]` on a flag parameter sets its schema `role` to `"confirmationSkip"` so agent consumers know to pass it automatically on destructive commands. `[DryRun]` sets `role` to `"dryRun"`.

**Output formats** (`output` object):

```csharp
// Enum parameter — formats and formatFlag inferred automatically
public static void Report([CommandOutput] OutputFormat? format = null) { }

// Explicit format list on a string parameter
public static void Export([CommandOutput("json", "table")] string? fmt = null) { }
```

Place `[CommandOutput]` on the parameter (or `[AsParameters]` DTO property, or GlobalOptions property) that selects the output format. The flag name and format list are derived from the parameter — no extra arguments needed for enum types.

**Deprecated commands and parameters** (`deprecated` object):

```csharp
[Obsolete("Use new-cmd instead.")]
public static void OldCmd(...) { }
```

`[Obsolete]` on a handler method or an `[AsParameters]` DTO property emits a `deprecated` object in the schema. The message, if provided, appears as `deprecated.message`.

**Environment variables and config files** (`environment` object):

```csharp
builder.DocumentEnvironmentVariables(
variables:
[
new CliEnvVar("GITHUB_TOKEN", Description: "GitHub API token", Required: true),
new CliEnvVar("XDG_CONFIG_HOME", Description: "Config directory override"),
],
configFiles:
[
new CliConfigFile("~/.config/myapp/config.json", Description: "Main config"),
]);
```

Arguments must be `new CliEnvVar(...)` / `new CliConfigFile(...)` object creation expressions with string/bool literals so the source generator can extract them statically.

**Native AOT in CI:** The GitHub Actions workflow runs an **`aot-validate`** job that publishes [`examples/ArghAotSmoketest`](examples/ArghAotSmoketest) with Native AOT on Linux, macOS, and Windows and invokes `__schema` on the native binary. That sample uses `Microsoft.Extensions.Hosting` and `AddArgh` so **`Map` / `MapNamespace` DI registration** is included in the AOT publish. The repo uses the SDK unified artifacts layout (output under **`.artifacts/`**, gitignored).

Expand Down
140 changes: 140 additions & 0 deletions schema/argh-cli-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,36 @@
"type": "boolean",
"default": false
},
"deprecated": {
"type": [
"object",
"null"
],
"properties": {
"message": {
"type": [
"string",
"null"
],
"default": null
},
"since": {
"type": [
"string",
"null"
],
"default": null
},
"removedIn": {
"type": [
"string",
"null"
],
"default": null
}
},
"default": null
},
"validations": {
"type": [
"array",
Expand Down Expand Up @@ -328,6 +358,36 @@
},
"default": null
},
"deprecated": {
"type": [
"object",
"null"
],
"properties": {
"message": {
"type": [
"string",
"null"
],
"default": null
},
"since": {
"type": [
"string",
"null"
],
"default": null
},
"removedIn": {
"type": [
"string",
"null"
],
"default": null
}
},
"default": null
},
"intent": {
"type": [
"object",
Expand Down Expand Up @@ -553,6 +613,86 @@
]
},
"default": null
},
"environment": {
"type": [
"object",
"null"
],
"properties": {
"variables": {
"type": [
"array",
"null"
],
"items": {
"type": [
"object",
"null"
],
"properties": {
"name": {
"type": "string"
},
"description": {
"type": [
"string",
"null"
],
"default": null
},
"required": {
"type": "boolean",
"default": false
},
"defaultValue": {
"type": [
"string",
"null"
],
"default": null
}
},
"required": [
"name"
]
},
"default": null
},
"configFiles": {
"type": [
"array",
"null"
],
"items": {
"type": [
"object",
"null"
],
"properties": {
"path": {
"type": "string"
},
"description": {
"type": [
"string",
"null"
],
"default": null
},
"required": {
"type": "boolean",
"default": false
}
},
"required": [
"path"
]
},
"default": null
}
},
"default": null
}
},
"required": [
Expand Down
8 changes: 8 additions & 0 deletions src/Nullean.Argh.Core/ArghApp.IArghBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using Nullean.Argh.Builder;
using Nullean.Argh.Documentation;
using Nullean.Argh.Middleware;

namespace Nullean.Argh;
Expand Down Expand Up @@ -34,6 +35,13 @@ IArghRootBuilder IArghRootBuilder.UseCliDescription(string description)
return this;
}

/// <inheritdoc />
IArghRootBuilder IArghRootBuilder.DocumentEnvironmentVariables(CliEnvVar[]? variables, CliConfigFile[]? configFiles)
{
_ = DocumentEnvironmentVariables(variables, configFiles);
return this;
}

/// <inheritdoc />
IArghBuilder IArghBuilder.MapRoot(Delegate handler)
{
Expand Down
4 changes: 4 additions & 0 deletions src/Nullean.Argh.Core/ArghApp.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using Nullean.Argh.Builder;
using Nullean.Argh.Documentation;
using Nullean.Argh.Help;
using Nullean.Argh.Runtime;

Expand Down Expand Up @@ -33,6 +34,9 @@ public ArghApp Map(string name, Delegate handler)
/// <summary>Sets a one-line description shown in root <c>--help</c> output. Analyzed by the source generator; no-op at runtime.</summary>
public ArghApp UseCliDescription(string description) => this;

/// <summary>Documents environment variables and config files the program reads. Analyzed by the source generator; no-op at runtime.</summary>
public ArghApp DocumentEnvironmentVariables(CliEnvVar[]? variables = null, CliConfigFile[]? configFiles = null) => this;

/// <summary>
/// Registers a default handler when no subcommand or namespace segment applies at the current scope
/// (app root or inside a <see cref="MapNamespace"/> block). Analyzed by the source generator.
Expand Down
10 changes: 10 additions & 0 deletions src/Nullean.Argh.Core/Builder/ArghBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using Nullean.Argh;
using Nullean.Argh.Documentation;
using Nullean.Argh.Middleware;
using Nullean.Argh.Runtime;

Expand Down Expand Up @@ -50,6 +51,15 @@ public IArghRootBuilder UseCliDescription(string description)
return this;
}

/// <inheritdoc />
public IArghRootBuilder DocumentEnvironmentVariables(
CliEnvVar[]? variables = null,
CliConfigFile[]? configFiles = null)
{
_ = _app.DocumentEnvironmentVariables(variables, configFiles);
return this;
}

/// <inheritdoc />
public IArghBuilder MapRoot(Delegate handler)
{
Expand Down
44 changes: 42 additions & 2 deletions src/Nullean.Argh.Core/Schema/ArghCliSchemaDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ public sealed record ArghCliSchemaDocument(
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
bool? RequiresAuth = null,
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
string[]? AuthCommands = null);
string[]? AuthCommands = null,
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
CliEnvironmentSchema? Environment = null);

/// <summary>Nested command namespace (subcommand group).</summary>
public sealed record CliNamespaceSchema(
Expand Down Expand Up @@ -46,6 +48,8 @@ public sealed record CliCommandSchema(
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
string[]? Tags = null,
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
CliDeprecationSchema? Deprecated = null,
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
CliIntentSchema? Intent = null,
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
CliOutputSchema? Output = null,
Expand Down Expand Up @@ -86,7 +90,7 @@ public sealed record CliDefaultHandlerSchema(
bool Hidden = false);

/// <summary>CLI flag or positional parameter description.</summary>
/// <param name="Role"><c>flag</c> or <c>positional</c>.</param>
/// <param name="Role"><c>flag</c>, <c>positional</c>, <c>confirmationSkip</c>, or <c>dryRun</c>.</param>
/// <param name="Type">JSON Schema primitive: <c>string</c>, <c>integer</c>, <c>number</c>, <c>boolean</c>, <c>array</c>, or <c>enum</c>.</param>
public sealed record CliParameterSchema(
string Role,
Expand All @@ -112,6 +116,8 @@ public sealed record CliParameterSchema(
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
bool Variadic = false,
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
CliDeprecationSchema? Deprecated = null,
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
CliConstraintSchema[]? Validations = null);

/// <summary>A single validation constraint on a CLI parameter.</summary>
Expand All @@ -122,3 +128,37 @@ public sealed record CliConstraintSchema(
string? Max = null,
string? Pattern = null,
string[]? Values = null);

/// <summary>Structured deprecation metadata for a command or parameter.</summary>
public sealed record CliDeprecationSchema(
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
string? Message = null,
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
string? Since = null,
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
string? RemovedIn = null);

/// <summary>External context the program depends on (env vars and config files).</summary>
public sealed record CliEnvironmentSchema(
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
CliEnvVarSchema[]? Variables = null,
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
CliConfigFileSchema[]? ConfigFiles = null);

/// <summary>An environment variable the program reads.</summary>
public sealed record CliEnvVarSchema(
string Name,
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
string? Description = null,
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
bool Required = false,
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
string? DefaultValue = null);

/// <summary>A configuration file the program reads.</summary>
public sealed record CliConfigFileSchema(
string Path,
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
string? Description = null,
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
bool Required = false);
6 changes: 6 additions & 0 deletions src/Nullean.Argh.Core/Schema/ArghCliSchemaJsonContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ namespace Nullean.Argh.Schema;
[JsonSerializable(typeof(CliParameterSchema))]
[JsonSerializable(typeof(CliIntentSchema))]
[JsonSerializable(typeof(CliOutputSchema))]
[JsonSerializable(typeof(CliDeprecationSchema))]
[JsonSerializable(typeof(CliEnvironmentSchema))]
[JsonSerializable(typeof(CliEnvVarSchema))]
[JsonSerializable(typeof(CliConfigFileSchema))]
[JsonSerializable(typeof(CliEnvVarSchema[]))]
[JsonSerializable(typeof(CliConfigFileSchema[]))]
[JsonSerializable(typeof(string[]))]
[JsonSerializable(typeof(CliParameterSchema[]))]
[JsonSerializable(typeof(CliCommandSchema[]))]
Expand Down
Loading
Loading