diff --git a/src/ModularPipelines.Buildah/Options/BuildahManifestOptions.Generated.cs b/src/ModularPipelines.Buildah/Options/BuildahManifestOptions.Generated.cs
index 38d089b2a1b..6211340776f 100644
--- a/src/ModularPipelines.Buildah/Options/BuildahManifestOptions.Generated.cs
+++ b/src/ModularPipelines.Buildah/Options/BuildahManifestOptions.Generated.cs
@@ -26,4 +26,10 @@ public record BuildahManifestOptions : BuildahOptions
[CliFlag("--help", ShortForm = "-h")]
public bool? Help { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Buildah/Options/BuildahSourceOptions.Generated.cs b/src/ModularPipelines.Buildah/Options/BuildahSourceOptions.Generated.cs
index f433446fd6f..9904cfeb9b7 100644
--- a/src/ModularPipelines.Buildah/Options/BuildahSourceOptions.Generated.cs
+++ b/src/ModularPipelines.Buildah/Options/BuildahSourceOptions.Generated.cs
@@ -26,4 +26,10 @@ public record BuildahSourceOptions : BuildahOptions
[CliFlag("--help", ShortForm = "-h")]
public bool? Help { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Docker/Options/DockerComposeBuildOptions.Generated.cs b/src/ModularPipelines.Docker/Options/DockerComposeBuildOptions.Generated.cs
index 4d2aade70d5..28770b259fe 100644
--- a/src/ModularPipelines.Docker/Options/DockerComposeBuildOptions.Generated.cs
+++ b/src/ModularPipelines.Docker/Options/DockerComposeBuildOptions.Generated.cs
@@ -64,6 +64,12 @@ public record DockerComposeBuildOptions : DockerOptions
[CliFlag("--print")]
public bool? Print { get; set; }
+ ///
+ /// Add a provenance attestation
+ ///
+ [CliOption("--provenance", Format = OptionFormat.EqualsSeparated)]
+ public string? Provenance { get; set; }
+
///
/// Always attempt to pull a newer version of the image
///
@@ -77,11 +83,17 @@ public record DockerComposeBuildOptions : DockerOptions
public bool? Push { get; set; }
///
- /// Don't print anything to STDOUT
+ /// Suppress the build output
///
[CliFlag("--quiet", ShortForm = "-q")]
public bool? Quiet { get; set; }
+ ///
+ /// Add a SBOM attestation
+ ///
+ [CliOption("--sbom", Format = OptionFormat.EqualsSeparated)]
+ public string? Sbom { get; set; }
+
///
/// Set SSH authentications used when building service images. (use 'default' for using your default SSH Agent)
///
diff --git a/src/ModularPipelines.Docker/Options/DockerComposeConfigOptions.Generated.cs b/src/ModularPipelines.Docker/Options/DockerComposeConfigOptions.Generated.cs
index 92dff38b2f0..93609f88323 100644
--- a/src/ModularPipelines.Docker/Options/DockerComposeConfigOptions.Generated.cs
+++ b/src/ModularPipelines.Docker/Options/DockerComposeConfigOptions.Generated.cs
@@ -56,6 +56,12 @@ public record DockerComposeConfigOptions : DockerOptions
[CliFlag("--lock-image-digests")]
public bool? LockImageDigests { get; set; }
+ ///
+ /// Print the model names, one per line.
+ ///
+ [CliFlag("--models")]
+ public bool? Models { get; set; }
+
///
/// Print the network names, one per line.
///
diff --git a/src/ModularPipelines.Docker/Options/DockerComposeEventsOptions.Generated.cs b/src/ModularPipelines.Docker/Options/DockerComposeEventsOptions.Generated.cs
index c632088f67f..636283a7566 100644
--- a/src/ModularPipelines.Docker/Options/DockerComposeEventsOptions.Generated.cs
+++ b/src/ModularPipelines.Docker/Options/DockerComposeEventsOptions.Generated.cs
@@ -32,6 +32,18 @@ public record DockerComposeEventsOptions : DockerOptions
[CliFlag("--json")]
public bool? Json { get; set; }
+ ///
+ /// Show all events created since timestamp
+ ///
+ [CliOption("--since", Format = OptionFormat.EqualsSeparated)]
+ public string? Since { get; set; }
+
+ ///
+ /// Stream events until this timestamp
+ ///
+ [CliOption("--until", Format = OptionFormat.EqualsSeparated)]
+ public string? Until { get; set; }
+
///
/// The SERVICE operand.
///
diff --git a/src/ModularPipelines.Docker/Options/DockerComposeExecOptions.Generated.cs b/src/ModularPipelines.Docker/Options/DockerComposeExecOptions.Generated.cs
index 78f65bfb153..3a3c07721bd 100644
--- a/src/ModularPipelines.Docker/Options/DockerComposeExecOptions.Generated.cs
+++ b/src/ModularPipelines.Docker/Options/DockerComposeExecOptions.Generated.cs
@@ -47,6 +47,12 @@ public record DockerComposeExecOptions(
[CliOption("--index", Format = OptionFormat.EqualsSeparated)]
public int? Index { get; set; }
+ ///
+ /// Disable pseudo-TTY allocation. By default 'docker compose exec' allocates a TTY. (default true)
+ ///
+ [CliOption("--no-TTY", ShortForm = "-T", Format = OptionFormat.EqualsSeparated)]
+ public bool? NoTty { get; set; }
+
///
/// Give extended privileges to the process
///
diff --git a/src/ModularPipelines.Docker/Options/DockerComposePublishOptions.Generated.cs b/src/ModularPipelines.Docker/Options/DockerComposePublishOptions.Generated.cs
index 8fe684bd950..85274911354 100644
--- a/src/ModularPipelines.Docker/Options/DockerComposePublishOptions.Generated.cs
+++ b/src/ModularPipelines.Docker/Options/DockerComposePublishOptions.Generated.cs
@@ -22,6 +22,12 @@ public record DockerComposePublishOptions(
[property: CliArgument(0, Phase = CommandLinePhase.Passthrough, Required = true)] string RepositoryTag
) : DockerOptions
{
+ ///
+ /// Published compose application (includes referenced images)
+ ///
+ [CliFlag("--app")]
+ public bool? App { get; set; }
+
///
/// Execute command in dry run mode
///
diff --git a/src/ModularPipelines.Docker/Options/DockerComposeStartOptions.Generated.cs b/src/ModularPipelines.Docker/Options/DockerComposeStartOptions.Generated.cs
index e5be419e028..6dc13cf176e 100644
--- a/src/ModularPipelines.Docker/Options/DockerComposeStartOptions.Generated.cs
+++ b/src/ModularPipelines.Docker/Options/DockerComposeStartOptions.Generated.cs
@@ -26,6 +26,18 @@ public record DockerComposeStartOptions : DockerOptions
[CliFlag("--dry-run")]
public bool? DryRun { get; set; }
+ ///
+ /// Wait for services to be running|healthy. Implies detached mode.
+ ///
+ [CliFlag("--wait")]
+ public bool? Wait { get; set; }
+
+ ///
+ /// Maximum duration in seconds to wait for the project to be running|healthy
+ ///
+ [CliOption("--wait-timeout", Format = OptionFormat.EqualsSeparated)]
+ public int? WaitTimeout { get; set; }
+
///
/// The SERVICE operand.
///
diff --git a/src/ModularPipelines.Docker/Options/DockerComposeUpOptions.Generated.cs b/src/ModularPipelines.Docker/Options/DockerComposeUpOptions.Generated.cs
index da6481c60d0..bafaedf8179 100644
--- a/src/ModularPipelines.Docker/Options/DockerComposeUpOptions.Generated.cs
+++ b/src/ModularPipelines.Docker/Options/DockerComposeUpOptions.Generated.cs
@@ -134,6 +134,12 @@ public record DockerComposeUpOptions : DockerOptions
[CliOption("--pull", Format = OptionFormat.EqualsSeparated)]
public string? Pull { get; set; }
+ ///
+ /// Suppress the build output
+ ///
+ [CliFlag("--quiet-build")]
+ public bool? QuietBuild { get; set; }
+
///
/// Pull without printing progress information
///
diff --git a/src/ModularPipelines.Flux/Generated/Flux.CommandCoverage.json b/src/ModularPipelines.Flux/Generated/Flux.CommandCoverage.json
index a89a01db140..3af1d828b67 100644
--- a/src/ModularPipelines.Flux/Generated/Flux.CommandCoverage.json
+++ b/src/ModularPipelines.Flux/Generated/Flux.CommandCoverage.json
@@ -1,7 +1,7 @@
{
"formatVersion": 1,
"toolName": "flux",
- "toolVersion": "flux version 2.9.4",
+ "toolVersion": "flux version 2.9.3",
"commandCount": 173,
"commandTreeSha256": "cce64ef09e13559016b9deee8b350b34916ce2e46c6e248c25ab24ae62a04c00",
"commands": [
diff --git a/src/ModularPipelines.Flux/Options/FluxBootstrapOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxBootstrapOptions.Generated.cs
index 49b05b8131d..0f276e3f3d0 100644
--- a/src/ModularPipelines.Flux/Options/FluxBootstrapOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxBootstrapOptions.Generated.cs
@@ -368,4 +368,10 @@ public record FluxBootstrapOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxBuildOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxBuildOptions.Generated.cs
index 7d35a3da902..d84274dbe60 100644
--- a/src/ModularPipelines.Flux/Options/FluxBuildOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxBuildOptions.Generated.cs
@@ -165,4 +165,10 @@ public record FluxBuildOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxCreateImageOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxCreateImageOptions.Generated.cs
index f7244886299..5e5055b3039 100644
--- a/src/ModularPipelines.Flux/Options/FluxCreateImageOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxCreateImageOptions.Generated.cs
@@ -183,4 +183,10 @@ public record FluxCreateImageOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxCreateOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxCreateOptions.Generated.cs
index c6dd4d1873d..2c11bb5b413 100644
--- a/src/ModularPipelines.Flux/Options/FluxCreateOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxCreateOptions.Generated.cs
@@ -183,4 +183,10 @@ public record FluxCreateOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxCreateSecretOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxCreateSecretOptions.Generated.cs
index 74c3ced94cc..1e2a16c1e57 100644
--- a/src/ModularPipelines.Flux/Options/FluxCreateSecretOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxCreateSecretOptions.Generated.cs
@@ -183,4 +183,10 @@ public record FluxCreateSecretOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxCreateSourceOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxCreateSourceOptions.Generated.cs
index 4fc82b2d76b..c6d35a75ea2 100644
--- a/src/ModularPipelines.Flux/Options/FluxCreateSourceOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxCreateSourceOptions.Generated.cs
@@ -189,4 +189,10 @@ public record FluxCreateSourceOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxDebugOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxDebugOptions.Generated.cs
index c7b8b26cc8d..118b8abfa19 100644
--- a/src/ModularPipelines.Flux/Options/FluxDebugOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxDebugOptions.Generated.cs
@@ -165,4 +165,10 @@ public record FluxDebugOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxDeleteImageOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxDeleteImageOptions.Generated.cs
index e46a536518a..f27159598ec 100644
--- a/src/ModularPipelines.Flux/Options/FluxDeleteImageOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxDeleteImageOptions.Generated.cs
@@ -171,4 +171,10 @@ public record FluxDeleteImageOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxDeleteOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxDeleteOptions.Generated.cs
index 6c8a732a9db..98ec8fc365a 100644
--- a/src/ModularPipelines.Flux/Options/FluxDeleteOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxDeleteOptions.Generated.cs
@@ -171,4 +171,10 @@ public record FluxDeleteOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxDeleteSourceOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxDeleteSourceOptions.Generated.cs
index eae44beb014..b800fda1fb4 100644
--- a/src/ModularPipelines.Flux/Options/FluxDeleteSourceOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxDeleteSourceOptions.Generated.cs
@@ -171,4 +171,10 @@ public record FluxDeleteSourceOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxDiffOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxDiffOptions.Generated.cs
index ca97ebe35e2..68ca2dd1c0b 100644
--- a/src/ModularPipelines.Flux/Options/FluxDiffOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxDiffOptions.Generated.cs
@@ -165,4 +165,10 @@ public record FluxDiffOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxExportArtifactOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxExportArtifactOptions.Generated.cs
index 083b93322c1..43c6720a1b2 100644
--- a/src/ModularPipelines.Flux/Options/FluxExportArtifactOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxExportArtifactOptions.Generated.cs
@@ -171,4 +171,10 @@ public record FluxExportArtifactOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxExportImageOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxExportImageOptions.Generated.cs
index b772f04f5ac..748c05cb58e 100644
--- a/src/ModularPipelines.Flux/Options/FluxExportImageOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxExportImageOptions.Generated.cs
@@ -171,4 +171,10 @@ public record FluxExportImageOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxExportOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxExportOptions.Generated.cs
index 60c21870932..76e03f9cfd5 100644
--- a/src/ModularPipelines.Flux/Options/FluxExportOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxExportOptions.Generated.cs
@@ -171,4 +171,10 @@ public record FluxExportOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxExportSourceOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxExportSourceOptions.Generated.cs
index 9579817b7c0..d2cfa585df6 100644
--- a/src/ModularPipelines.Flux/Options/FluxExportSourceOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxExportSourceOptions.Generated.cs
@@ -177,4 +177,10 @@ public record FluxExportSourceOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxGetArtifactsOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxGetArtifactsOptions.Generated.cs
index ab4cab7923f..6c8091219fe 100644
--- a/src/ModularPipelines.Flux/Options/FluxGetArtifactsOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxGetArtifactsOptions.Generated.cs
@@ -195,4 +195,10 @@ public record FluxGetArtifactsOptions : FluxOptions
[CliFlag("--watch", ShortForm = "-w")]
public bool? Watch { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxGetImagesOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxGetImagesOptions.Generated.cs
index ab8c4233d1b..dc8dbbb26b2 100644
--- a/src/ModularPipelines.Flux/Options/FluxGetImagesOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxGetImagesOptions.Generated.cs
@@ -195,4 +195,10 @@ public record FluxGetImagesOptions : FluxOptions
[CliFlag("--watch", ShortForm = "-w")]
public bool? Watch { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxGetOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxGetOptions.Generated.cs
index f749ddef1d0..ba7df69416c 100644
--- a/src/ModularPipelines.Flux/Options/FluxGetOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxGetOptions.Generated.cs
@@ -195,4 +195,10 @@ public record FluxGetOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxGetSourcesOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxGetSourcesOptions.Generated.cs
index 8ff46362cac..471c593c0f1 100644
--- a/src/ModularPipelines.Flux/Options/FluxGetSourcesOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxGetSourcesOptions.Generated.cs
@@ -195,4 +195,10 @@ public record FluxGetSourcesOptions : FluxOptions
[CliFlag("--watch", ShortForm = "-w")]
public bool? Watch { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxListOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxListOptions.Generated.cs
index 5cf7e15052b..b69c06fc6e3 100644
--- a/src/ModularPipelines.Flux/Options/FluxListOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxListOptions.Generated.cs
@@ -165,4 +165,10 @@ public record FluxListOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxPluginOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxPluginOptions.Generated.cs
index bb53c85a3e5..eacfb1fd1c2 100644
--- a/src/ModularPipelines.Flux/Options/FluxPluginOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxPluginOptions.Generated.cs
@@ -165,4 +165,10 @@ public record FluxPluginOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxPullOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxPullOptions.Generated.cs
index 01fa4888c18..49cf7ffbcbd 100644
--- a/src/ModularPipelines.Flux/Options/FluxPullOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxPullOptions.Generated.cs
@@ -165,4 +165,10 @@ public record FluxPullOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxPushOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxPushOptions.Generated.cs
index 90bc0a8a594..8667d658752 100644
--- a/src/ModularPipelines.Flux/Options/FluxPushOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxPushOptions.Generated.cs
@@ -165,4 +165,10 @@ public record FluxPushOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxReconcileImageOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxReconcileImageOptions.Generated.cs
index 9d0cec81069..ffb6ef2f883 100644
--- a/src/ModularPipelines.Flux/Options/FluxReconcileImageOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxReconcileImageOptions.Generated.cs
@@ -165,4 +165,10 @@ public record FluxReconcileImageOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxReconcileOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxReconcileOptions.Generated.cs
index e9e281cf255..741d462711f 100644
--- a/src/ModularPipelines.Flux/Options/FluxReconcileOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxReconcileOptions.Generated.cs
@@ -165,4 +165,10 @@ public record FluxReconcileOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxReconcileSourceOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxReconcileSourceOptions.Generated.cs
index 9f98df24a3e..c28052de772 100644
--- a/src/ModularPipelines.Flux/Options/FluxReconcileSourceOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxReconcileSourceOptions.Generated.cs
@@ -165,4 +165,10 @@ public record FluxReconcileSourceOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxResumeImageOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxResumeImageOptions.Generated.cs
index 9f27b436ed9..70cbc2a06f0 100644
--- a/src/ModularPipelines.Flux/Options/FluxResumeImageOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxResumeImageOptions.Generated.cs
@@ -177,4 +177,10 @@ public record FluxResumeImageOptions : FluxOptions
[CliFlag("--wait")]
public bool? Wait { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxResumeOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxResumeOptions.Generated.cs
index 5ba419ea60d..ab3a8066faa 100644
--- a/src/ModularPipelines.Flux/Options/FluxResumeOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxResumeOptions.Generated.cs
@@ -177,4 +177,10 @@ public record FluxResumeOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxResumeSourceOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxResumeSourceOptions.Generated.cs
index ba72e15b78e..d7c7b91d2b9 100644
--- a/src/ModularPipelines.Flux/Options/FluxResumeSourceOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxResumeSourceOptions.Generated.cs
@@ -177,4 +177,10 @@ public record FluxResumeSourceOptions : FluxOptions
[CliFlag("--wait")]
public bool? Wait { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxSuspendImageOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxSuspendImageOptions.Generated.cs
index 0f5cc0de93b..6990d81b5a5 100644
--- a/src/ModularPipelines.Flux/Options/FluxSuspendImageOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxSuspendImageOptions.Generated.cs
@@ -171,4 +171,10 @@ public record FluxSuspendImageOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxSuspendOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxSuspendOptions.Generated.cs
index b9bf75963c9..80bc09d9539 100644
--- a/src/ModularPipelines.Flux/Options/FluxSuspendOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxSuspendOptions.Generated.cs
@@ -171,4 +171,10 @@ public record FluxSuspendOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxSuspendSourceOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxSuspendSourceOptions.Generated.cs
index ff13cf5cd26..8edf4122cf0 100644
--- a/src/ModularPipelines.Flux/Options/FluxSuspendSourceOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxSuspendSourceOptions.Generated.cs
@@ -171,4 +171,10 @@ public record FluxSuspendSourceOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxTagOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxTagOptions.Generated.cs
index c6c448eb761..332fffb8164 100644
--- a/src/ModularPipelines.Flux/Options/FluxTagOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxTagOptions.Generated.cs
@@ -165,4 +165,10 @@ public record FluxTagOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxTreeArtifactOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxTreeArtifactOptions.Generated.cs
index 9385b791ea5..cfe8126ea8a 100644
--- a/src/ModularPipelines.Flux/Options/FluxTreeArtifactOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxTreeArtifactOptions.Generated.cs
@@ -165,4 +165,10 @@ public record FluxTreeArtifactOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxTreeOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxTreeOptions.Generated.cs
index 5056b9ce8c9..62f5ab9eb9d 100644
--- a/src/ModularPipelines.Flux/Options/FluxTreeOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxTreeOptions.Generated.cs
@@ -165,4 +165,10 @@ public record FluxTreeOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Flux/Options/FluxTriggerOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxTriggerOptions.Generated.cs
index da562a3621c..e3a39132a8e 100644
--- a/src/ModularPipelines.Flux/Options/FluxTriggerOptions.Generated.cs
+++ b/src/ModularPipelines.Flux/Options/FluxTriggerOptions.Generated.cs
@@ -165,4 +165,10 @@ public record FluxTriggerOptions : FluxOptions
[CliFlag("--verbose")]
public bool? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Grype/Options/GrypeDbOptions.Generated.cs b/src/ModularPipelines.Grype/Options/GrypeDbOptions.Generated.cs
index 263f4ac7667..aa715a4faef 100644
--- a/src/ModularPipelines.Grype/Options/GrypeDbOptions.Generated.cs
+++ b/src/ModularPipelines.Grype/Options/GrypeDbOptions.Generated.cs
@@ -50,4 +50,10 @@ public record GrypeDbOptions : GrypeOptions
[CliOption("--verbose", ShortForm = "-v", Format = OptionFormat.EqualsSeparated)]
public int? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Grype/Options/GrypeDbSearchOptions.Generated.cs b/src/ModularPipelines.Grype/Options/GrypeDbSearchOptions.Generated.cs
index 74e0d628627..e0a0a430762 100644
--- a/src/ModularPipelines.Grype/Options/GrypeDbSearchOptions.Generated.cs
+++ b/src/ModularPipelines.Grype/Options/GrypeDbSearchOptions.Generated.cs
@@ -116,4 +116,10 @@ public record GrypeDbSearchOptions : GrypeOptions
[CliOption("--verbose", ShortForm = "-v", Format = OptionFormat.EqualsSeparated)]
public int? Verbose { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Kubernetes/Extensions/KustomizeExtensions.Generated.cs b/src/ModularPipelines.Kubernetes/Extensions/KustomizeExtensions.Generated.cs
index d16fb481081..953fa1e9824 100644
--- a/src/ModularPipelines.Kubernetes/Extensions/KustomizeExtensions.Generated.cs
+++ b/src/ModularPipelines.Kubernetes/Extensions/KustomizeExtensions.Generated.cs
@@ -36,7 +36,7 @@ public static IServiceCollection RegisterKustomizeContext(this IServiceCollectio
}
///
- /// Gets the kustomize service from the pipeline context for compatibility.
+ /// Gets the kustomize service from the pipeline context.
///
/// The pipeline context.
/// The service for executing kustomize commands.
diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeCfgCatOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeCfgCatOptions.Generated.cs
index e1c34d5b5b7..41d90800cf0 100644
--- a/src/ModularPipelines.Kubernetes/Options/KustomizeCfgCatOptions.Generated.cs
+++ b/src/ModularPipelines.Kubernetes/Options/KustomizeCfgCatOptions.Generated.cs
@@ -22,6 +22,13 @@ public record KustomizeCfgCatOptions(
[property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string Dir
) : KustomizeOptions
{
+ ///
+ /// Creates compatibility options without the newly required directory operand.
+ ///
+ public KustomizeCfgCatOptions() : this(string.Empty)
+ {
+ }
+
///
/// annotate resources with their file origins.
///
diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeCfgCountOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeCfgCountOptions.Generated.cs
index 1e45b60b304..0fa7899360d 100644
--- a/src/ModularPipelines.Kubernetes/Options/KustomizeCfgCountOptions.Generated.cs
+++ b/src/ModularPipelines.Kubernetes/Options/KustomizeCfgCountOptions.Generated.cs
@@ -44,9 +44,6 @@ public record KustomizeCfgCountOptions : KustomizeOptions
[CliFlag("--stack-trace")]
public bool? StackTrace { get; set; }
- ///
- /// The DIR operand.
- ///
[CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
public string? Dir { get; set; }
diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeCfgTreeOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeCfgTreeOptions.Generated.cs
index a7abe41e911..e76b51595ba 100644
--- a/src/ModularPipelines.Kubernetes/Options/KustomizeCfgTreeOptions.Generated.cs
+++ b/src/ModularPipelines.Kubernetes/Options/KustomizeCfgTreeOptions.Generated.cs
@@ -110,9 +110,6 @@ public record KustomizeCfgTreeOptions : KustomizeOptions
[CliFlag("--stack-trace")]
public bool? StackTrace { get; set; }
- ///
- /// The DIR operand.
- ///
[CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
public string? Dir { get; set; }
diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeFnRunOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeFnRunOptions.Generated.cs
index 543a39f2902..800a6418555 100644
--- a/src/ModularPipelines.Kubernetes/Options/KustomizeFnRunOptions.Generated.cs
+++ b/src/ModularPipelines.Kubernetes/Options/KustomizeFnRunOptions.Generated.cs
@@ -110,9 +110,6 @@ public record KustomizeFnRunOptions : KustomizeOptions
[CliFlag("--stack-trace")]
public bool? StackTrace { get; set; }
- ///
- /// The DIR operand.
- ///
[CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
public string? Dir { get; set; }
diff --git a/src/ModularPipelines.Pulumi/Generated/Pulumi.CommandCoverage.json b/src/ModularPipelines.Pulumi/Generated/Pulumi.CommandCoverage.json
index ff837da322a..f0561157791 100644
--- a/src/ModularPipelines.Pulumi/Generated/Pulumi.CommandCoverage.json
+++ b/src/ModularPipelines.Pulumi/Generated/Pulumi.CommandCoverage.json
@@ -1,7 +1,7 @@
{
"formatVersion": 1,
"toolName": "pulumi",
- "toolVersion": "v3.256.0",
+ "toolVersion": "v3.255.0",
"commandCount": 226,
"commandTreeSha256": "c23fbb83a37eb69c9cf574d6e85ffad67be77a27cab19c1f4a05c22b31237a9e",
"commands": [
diff --git a/src/ModularPipelines.Pulumi/Options/PulumiNewOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiNewOptions.Generated.cs
index 2f363f2dbd2..317a25ab769 100644
--- a/src/ModularPipelines.Pulumi/Options/PulumiNewOptions.Generated.cs
+++ b/src/ModularPipelines.Pulumi/Options/PulumiNewOptions.Generated.cs
@@ -20,6 +20,12 @@ namespace ModularPipelines.Pulumi.Options;
[CliSubCommand("new")]
public record PulumiNewOptions : PulumiOptions
{
+ ///
+ /// Prompt to use for Pulumi AI
+ ///
+ [CliOption("--ai", Format = OptionFormat.EqualsSeparated)]
+ public string? Ai { get; set; }
+
///
/// Config to save
///
@@ -62,6 +68,12 @@ public record PulumiNewOptions : PulumiOptions
[CliFlag("--help", ShortForm = "-h")]
public bool? Help { get; set; }
+ ///
+ /// Language to use for Pulumi AI (must be one of TypeScript, JavaScript, Python, Go, C#, Java, or YAML)
+ ///
+ [CliOption("--language", Format = OptionFormat.EqualsSeparated)]
+ public string? Language { get; set; }
+
///
/// List locally installed templates and exit
///
@@ -98,6 +110,12 @@ public record PulumiNewOptions : PulumiOptions
[CliOption("--stack", ShortForm = "-s", Format = OptionFormat.EqualsSeparated)]
public string? Stack { get; set; }
+ ///
+ /// Run in template mode, which will skip prompting for AI or Template functionality
+ ///
+ [CliFlag("--template-mode", ShortForm = "-t")]
+ public bool? TemplateMode { get; set; }
+
///
/// Skip prompts and proceed with default values
///
diff --git a/src/ModularPipelines.Pulumi/Options/PulumiProjectNewOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiProjectNewOptions.Generated.cs
index d27450855b1..f38511af557 100644
--- a/src/ModularPipelines.Pulumi/Options/PulumiProjectNewOptions.Generated.cs
+++ b/src/ModularPipelines.Pulumi/Options/PulumiProjectNewOptions.Generated.cs
@@ -20,6 +20,12 @@ namespace ModularPipelines.Pulumi.Options;
[CliSubCommand("project", "new")]
public record PulumiProjectNewOptions : PulumiOptions
{
+ ///
+ /// Prompt to use for Pulumi AI
+ ///
+ [CliOption("--ai", Format = OptionFormat.EqualsSeparated)]
+ public string? Ai { get; set; }
+
///
/// Config to save
///
@@ -62,6 +68,12 @@ public record PulumiProjectNewOptions : PulumiOptions
[CliFlag("--help", ShortForm = "-h")]
public bool? Help { get; set; }
+ ///
+ /// Language to use for Pulumi AI (must be one of TypeScript, JavaScript, Python, Go, C#, Java, or YAML)
+ ///
+ [CliOption("--language", Format = OptionFormat.EqualsSeparated)]
+ public string? Language { get; set; }
+
///
/// List locally installed templates and exit
///
@@ -98,6 +110,12 @@ public record PulumiProjectNewOptions : PulumiOptions
[CliOption("--stack", ShortForm = "-s", Format = OptionFormat.EqualsSeparated)]
public string? Stack { get; set; }
+ ///
+ /// Run in template mode, which will skip prompting for AI or Template functionality
+ ///
+ [CliFlag("--template-mode", ShortForm = "-t")]
+ public bool? TemplateMode { get; set; }
+
///
/// Skip prompts and proceed with default values
///
diff --git a/src/ModularPipelines.Trivy/Options/TrivyModuleOptions.Generated.cs b/src/ModularPipelines.Trivy/Options/TrivyModuleOptions.Generated.cs
index f96996dbcbd..9d40aec64e5 100644
--- a/src/ModularPipelines.Trivy/Options/TrivyModuleOptions.Generated.cs
+++ b/src/ModularPipelines.Trivy/Options/TrivyModuleOptions.Generated.cs
@@ -92,4 +92,10 @@ public record TrivyModuleOptions : TrivyOptions
[CliFlag("--version", ShortForm = "-v")]
public bool? Version { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Trivy/Options/TrivyPluginOptions.Generated.cs b/src/ModularPipelines.Trivy/Options/TrivyPluginOptions.Generated.cs
index d7f2d35cb5a..eb35167b424 100644
--- a/src/ModularPipelines.Trivy/Options/TrivyPluginOptions.Generated.cs
+++ b/src/ModularPipelines.Trivy/Options/TrivyPluginOptions.Generated.cs
@@ -80,4 +80,10 @@ public record TrivyPluginOptions : TrivyOptions
[CliFlag("--version", ShortForm = "-v")]
public bool? Version { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Trivy/Options/TrivyRegistryOptions.Generated.cs b/src/ModularPipelines.Trivy/Options/TrivyRegistryOptions.Generated.cs
index 952f7f1457b..c4a65cadfee 100644
--- a/src/ModularPipelines.Trivy/Options/TrivyRegistryOptions.Generated.cs
+++ b/src/ModularPipelines.Trivy/Options/TrivyRegistryOptions.Generated.cs
@@ -80,4 +80,10 @@ public record TrivyRegistryOptions : TrivyOptions
[CliFlag("--version", ShortForm = "-v")]
public bool? Version { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Trivy/Options/TrivyVexOptions.Generated.cs b/src/ModularPipelines.Trivy/Options/TrivyVexOptions.Generated.cs
index 878e31680e0..2017d492db6 100644
--- a/src/ModularPipelines.Trivy/Options/TrivyVexOptions.Generated.cs
+++ b/src/ModularPipelines.Trivy/Options/TrivyVexOptions.Generated.cs
@@ -80,4 +80,10 @@ public record TrivyVexOptions : TrivyOptions
[CliFlag("--version", ShortForm = "-v")]
public bool? Version { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/src/ModularPipelines.Trivy/Options/TrivyVexRepoOptions.Generated.cs b/src/ModularPipelines.Trivy/Options/TrivyVexRepoOptions.Generated.cs
index ef64c50237f..991cacf23aa 100644
--- a/src/ModularPipelines.Trivy/Options/TrivyVexRepoOptions.Generated.cs
+++ b/src/ModularPipelines.Trivy/Options/TrivyVexRepoOptions.Generated.cs
@@ -80,4 +80,10 @@ public record TrivyVexRepoOptions : TrivyOptions
[CliFlag("--version", ShortForm = "-v")]
public bool? Version { get; set; }
+ ///
+ /// The command operand.
+ ///
+ [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)]
+ public string? Command { get; set; }
+
}
diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/CodeGeneratorOrchestratorTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/CodeGeneratorOrchestratorTests.cs
index 5ffd1ad1a64..937d9b04a4c 100644
--- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/CodeGeneratorOrchestratorTests.cs
+++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/CodeGeneratorOrchestratorTests.cs
@@ -145,6 +145,58 @@ public async Task Cli_Metadata_Is_Preserved_For_Generators()
}
}
+ [Test]
+ public async Task Existing_Generated_Options_Are_Used_As_Api_Baseline()
+ {
+ var outputRoot = Path.Combine(Path.GetTempPath(), "mp-orchestrator-tests", Guid.NewGuid().ToString("N"));
+ var scraper = new FakeCliScraper { Commands = [FakeCommand()] };
+ await Orchestrator(scraper, new FakeGenerator()).GenerateAsync("fake", outputRoot);
+ var optionsDirectory = Path.Combine(outputRoot, ToolOutputDirectory, "Options");
+ Directory.CreateDirectory(optionsDirectory);
+ await File.WriteAllTextAsync(
+ Path.Combine(optionsDirectory, "FakeRunOptions.Generated.cs"),
+ """
+ //
+ // Generated compatibility baseline.
+ //
+
+ using ModularPipelines.Attributes;
+
+ public record FakeRunOptions
+ {
+ [CliFlag("--removed")]
+ public bool? Removed { get; set; }
+ }
+ """);
+ CliToolDefinition? generatedTool = null;
+ var generator = new FakeGenerator
+ {
+ OnGenerate = tool =>
+ {
+ generatedTool = tool;
+ return [];
+ },
+ };
+
+ try
+ {
+ var result = await Orchestrator(scraper, generator).GenerateAsync("fake", outputRoot);
+
+ await Assert.That(result.HasErrors).IsFalse();
+ var compatibilityProperty = generatedTool!.Commands.Single().CompatibilityProperties.Single();
+ using (Assert.Multiple())
+ {
+ await Assert.That(compatibilityProperty.PropertyName).IsEqualTo("Removed");
+ await Assert.That(compatibilityProperty.CSharpType).IsEqualTo("bool?");
+ await Assert.That(compatibilityProperty.ForwardToPropertyName).IsNull();
+ }
+ }
+ finally
+ {
+ Directory.Delete(outputRoot, recursive: true);
+ }
+ }
+
[Test]
public async Task Catalog_Metadata_Is_Applied_Before_Every_Generator()
{
diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/EnumDefinitionStabilizerTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/EnumDefinitionStabilizerTests.cs
index 6b4a1f75707..fda406ea9ed 100644
--- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/EnumDefinitionStabilizerTests.cs
+++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/EnumDefinitionStabilizerTests.cs
@@ -131,6 +131,47 @@ await Assert.That(stabilized.AllEnums.Single().Values.Single().MemberName)
}
}
+ [Test]
+ public async Task Stabilize_Retains_Removed_Members_And_Restores_Renamed_Members()
+ {
+ var outputRoot = Path.Combine(Path.GetTempPath(), "mp-enum-tests", Guid.NewGuid().ToString("N"));
+ var enumDirectory = Path.Combine(outputRoot, "src", "Fake", "Enums");
+ Directory.CreateDirectory(enumDirectory);
+ File.WriteAllText(
+ Path.Combine(enumDirectory, "FakeVisibility.Generated.cs"),
+ "public enum FakeVisibility { "
+ + "[EnumValue(\"private\")] Private = 4, "
+ + "[EnumValue(\"public\")] Public = 9 }");
+
+ try
+ {
+ var tool = Tool(
+ new CliEnumValue { MemberName = "PrivateAccess", CliValue = "private" },
+ Value("enterprise"));
+
+ var stabilized = EnumDefinitionStabilizer.Stabilize(tool, outputRoot);
+ var values = stabilized.AllEnums.Single().Values;
+
+ using (Assert.Multiple())
+ {
+ await Assert.That(values.Select(value => value.MemberName))
+ .IsEquivalentTo(["Private", "Public", "Enterprise"]);
+ await Assert.That(values.Single(value => value.MemberName == "Private").NumericValue)
+ .IsEqualTo(4);
+ await Assert.That(values.Single(value => value.MemberName == "Public").CliValue)
+ .IsEqualTo("public");
+ await Assert.That(values.Single(value => value.MemberName == "Public").NumericValue)
+ .IsEqualTo(9);
+ await Assert.That(values.Single(value => value.MemberName == "Enterprise").NumericValue)
+ .IsEqualTo(10);
+ }
+ }
+ finally
+ {
+ Directory.Delete(outputRoot, recursive: true);
+ }
+ }
+
[Test]
public async Task Stabilize_Rejects_Suspicious_Prose_Values()
{
diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs
index 2208e477dbd..a343699c46c 100644
--- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs
+++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs
@@ -100,6 +100,35 @@ private static CliToolDefinition Tool(params CliCommandDefinition[] commands) =>
Commands = commands,
};
+ private static GeneratedApiProperty BaselineProperty(
+ string propertyName,
+ string cSharpType,
+ string? switchName = null,
+ int? argumentPosition = null,
+ bool isRequired = false,
+ bool isCompatibility = false,
+ string? forwardToPropertyName = null,
+ bool useInitAccessor = false) =>
+ new(
+ propertyName,
+ cSharpType,
+ switchName,
+ argumentPosition,
+ isRequired,
+ isCompatibility,
+ forwardToPropertyName,
+ null,
+ useInitAccessor);
+
+ private static CliOptionDefinition RequiredOption(string switchName, string propertyName) =>
+ new()
+ {
+ SwitchName = switchName,
+ PropertyName = propertyName,
+ CSharpType = "string",
+ IsRequired = true,
+ };
+
[Test]
public async Task Command_Facade_Generation_Can_Be_Disabled_Independently_Of_Options()
{
@@ -789,6 +818,1195 @@ public async Task OptionsClassGenerator_Renames_Global_Compatibility_Targets()
}
}
+ [Test]
+ public async Task ApiCompatibilityPreserver_Retains_Removed_Scraped_Options()
+ {
+ var command = Command("ToolBuildOptions", "ToolOptions", ["build"]);
+ var preserved = GeneratedApiCompatibilityPreserver.Preserve(
+ command,
+ [BaselineProperty("RemovedFlag", "bool?", switchName: "--removed-flag")]);
+
+ var generated = (await new OptionsClassGenerator().GenerateAsync(Tool(preserved))).Single().Content;
+
+ using (Assert.Multiple())
+ {
+ await Assert.That(generated).Contains("public bool? RemovedFlag { get; set; }");
+ await Assert.That(generated).Contains("RemovedFlag is no longer supported");
+ await Assert.That(generated).DoesNotContain("CliFlag(\"--removed-flag\")");
+ }
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Restores_Required_Positional_Names()
+ {
+ var command = Command("ToolAddOptions", "ToolOptions", ["add"]) with
+ {
+ PositionalArguments =
+ [
+ new CliPositionalArgument
+ {
+ PropertyName = "Dep",
+ CSharpType = "IEnumerable",
+ IsRequired = true,
+ PositionIndex = 0,
+ },
+ ],
+ };
+ var preserved = GeneratedApiCompatibilityPreserver.Preserve(
+ command,
+ [BaselineProperty(
+ "DepVersion",
+ "IEnumerable",
+ argumentPosition: 0,
+ isRequired: true)]);
+
+ var generated = (await new OptionsClassGenerator().GenerateAsync(Tool(preserved))).Single().Content;
+
+ using (Assert.Multiple())
+ {
+ await Assert.That(generated).Contains("IEnumerable DepVersion");
+ await Assert.That(generated).Contains("public IEnumerable Dep");
+ await Assert.That(generated).Contains("get => DepVersion;");
+ await Assert.That(generated).Contains("init => DepVersion = value;");
+ }
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Rejects_Scalar_To_Collection_Changes()
+ {
+ var command = Command("ToolCopyOptions", "ToolOptions", ["copy"]) with
+ {
+ Options =
+ [
+ new CliOptionDefinition
+ {
+ SwitchName = "--command-options",
+ PropertyName = "CommandOptions",
+ CSharpType = "IEnumerable?",
+ },
+ ],
+ };
+
+ var exception = Assert.Throws(() =>
+ GeneratedApiCompatibilityPreserver.Preserve(
+ command,
+ [BaselineProperty("CommandOptions", "string?", switchName: "--command-options")]));
+
+ await Assert.That(exception.Message)
+ .Contains("ToolCopyOptions.CommandOptions changed type from string? to IEnumerable?");
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Rejects_Renamed_Scalar_To_Collection_Changes()
+ {
+ var command = Command("ToolCopyOptions", "ToolOptions", ["copy"]) with
+ {
+ Options =
+ [
+ new CliOptionDefinition
+ {
+ SwitchName = "--command-options",
+ PropertyName = "CommandOptionValues",
+ CSharpType = "IEnumerable?",
+ },
+ ],
+ };
+
+ var exception = Assert.Throws(() =>
+ GeneratedApiCompatibilityPreserver.Preserve(
+ command,
+ [BaselineProperty("CommandOptions", "string?", switchName: "--command-options")]));
+
+ await Assert.That(exception.Message)
+ .Contains("changed type from string? to IEnumerable? while being renamed");
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Rejects_Reassigned_Property_Names()
+ {
+ var command = Command("ToolCopyOptions", "ToolOptions", ["copy"]) with
+ {
+ Options =
+ [
+ new CliOptionDefinition
+ {
+ SwitchName = "--different",
+ PropertyName = "CommandOptions",
+ CSharpType = "string?",
+ },
+ ],
+ };
+
+ var exception = Assert.Throws(() =>
+ GeneratedApiCompatibilityPreserver.Preserve(
+ command,
+ [BaselineProperty("CommandOptions", "string?", switchName: "--command-options")]));
+
+ await Assert.That(exception.Message)
+ .Contains("ToolCopyOptions.CommandOptions changed CLI switch or argument position");
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Rejects_Reused_Compatibility_Property_With_Different_Type()
+ {
+ var command = Command("ToolCopyOptions", "ToolOptions", ["copy"]) with
+ {
+ Options =
+ [
+ new CliOptionDefinition
+ {
+ SwitchName = "--restored",
+ PropertyName = "RemovedFlag",
+ CSharpType = "string?",
+ },
+ ],
+ };
+
+ var exception = Assert.Throws(() =>
+ GeneratedApiCompatibilityPreserver.Preserve(
+ command,
+ [BaselineProperty("RemovedFlag", "bool?", isCompatibility: true)]));
+
+ await Assert.That(exception.Message)
+ .Contains("ToolCopyOptions.RemovedFlag changed type from bool? to string?");
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Rejects_Reused_Compatibility_Property_With_Cli_Identity()
+ {
+ var command = Command("ToolCopyOptions", "ToolOptions", ["copy"]) with
+ {
+ Options =
+ [
+ new CliOptionDefinition
+ {
+ SwitchName = "--restored",
+ PropertyName = "RemovedFlag",
+ CSharpType = "bool?",
+ },
+ ],
+ };
+
+ var exception = Assert.Throws(() =>
+ GeneratedApiCompatibilityPreserver.Preserve(
+ command,
+ [BaselineProperty("RemovedFlag", "bool?", isCompatibility: true)]));
+
+ await Assert.That(exception.Message)
+ .Contains("ToolCopyOptions.RemovedFlag changed CLI switch or argument position");
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Rejects_Conflicting_Supplied_Compatibility_Property_Type()
+ {
+ var command = Command("ToolCopyOptions", "ToolOptions", ["copy"]) with
+ {
+ CompatibilityProperties =
+ [
+ new CliCompatibilityProperty
+ {
+ PropertyName = "RemovedFlag",
+ CSharpType = "string?",
+ ObsoleteMessage = "Still retained.",
+ },
+ ],
+ };
+
+ var exception = Assert.Throws(() =>
+ GeneratedApiCompatibilityPreserver.Preserve(
+ command,
+ [BaselineProperty("RemovedFlag", "bool?", isCompatibility: true)]));
+
+ await Assert.That(exception.Message)
+ .Contains("ToolCopyOptions.RemovedFlag compatibility property changed type from bool? to string?");
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Rejects_Conflicting_Supplied_Compatibility_Forwarding()
+ {
+ var command = Command("ToolCopyOptions", "ToolOptions", ["copy"]) with
+ {
+ CompatibilityProperties =
+ [
+ new CliCompatibilityProperty
+ {
+ PropertyName = "OldName",
+ CSharpType = "string?",
+ ForwardToPropertyName = "DifferentName",
+ ObsoleteMessage = "Use DifferentName.",
+ },
+ ],
+ };
+
+ var exception = Assert.Throws(() =>
+ GeneratedApiCompatibilityPreserver.Preserve(
+ command,
+ [
+ BaselineProperty(
+ "OldName",
+ "string?",
+ isCompatibility: true,
+ forwardToPropertyName: "CurrentName"),
+ ]));
+
+ await Assert.That(exception.Message)
+ .Contains("ToolCopyOptions.OldName compatibility property changed forwarding target from CurrentName to DifferentName");
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Uses_The_Emitted_Optional_Value_Type()
+ {
+ var command = Command("ToolRunOptions", "ToolOptions", ["run"]) with
+ {
+ Options =
+ [
+ new CliOptionDefinition
+ {
+ SwitchName = "--progress",
+ PropertyName = "Progress",
+ CSharpType = "string?",
+ ValueArity = CliOptionValueArity.Optional,
+ },
+ ],
+ };
+
+ var exception = Assert.Throws(() =>
+ GeneratedApiCompatibilityPreserver.Preserve(
+ command,
+ [BaselineProperty("Progress", "string?", switchName: "--progress")]));
+
+ await Assert.That(exception.Message)
+ .Contains("ToolRunOptions.Progress changed type from string? to CliOptionValue?");
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Rejects_Optional_Member_Becoming_Required()
+ {
+ var command = Command("ToolNewOptions", "ToolOptions", ["new"]) with
+ {
+ PositionalArguments =
+ [
+ new CliPositionalArgument
+ {
+ PropertyName = "Name",
+ CSharpType = "string",
+ IsRequired = true,
+ PositionIndex = 0,
+ },
+ ],
+ };
+
+ var exception = Assert.Throws(() =>
+ GeneratedApiCompatibilityPreserver.Preserve(
+ command,
+ [BaselineProperty("Name", "string", argumentPosition: 0)]));
+
+ await Assert.That(exception.Message)
+ .Contains("Name changed from optional to required and would remove its public setter");
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Restores_Required_Constructor_Order()
+ {
+ var command = Command("ToolMoveOptions", "ToolOptions", ["move"]) with
+ {
+ Options =
+ [
+ new CliOptionDefinition
+ {
+ SwitchName = "--second",
+ PropertyName = "Second",
+ CSharpType = "string",
+ IsRequired = true,
+ },
+ new CliOptionDefinition
+ {
+ SwitchName = "--first",
+ PropertyName = "First",
+ CSharpType = "string",
+ IsRequired = true,
+ },
+ ],
+ };
+
+ var preserved = GeneratedApiCompatibilityPreserver.Preserve(
+ command,
+ [
+ BaselineProperty("First", "string", switchName: "--first", isRequired: true),
+ BaselineProperty("Second", "string", switchName: "--second", isRequired: true),
+ ]);
+ var generated = (await new OptionsClassGenerator().GenerateAsync(Tool(preserved))).Single().Content;
+
+ await Assert.That(generated.IndexOf("string First", StringComparison.Ordinal))
+ .IsLessThan(generated.IndexOf("string Second", StringComparison.Ordinal));
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Rejects_Old_Deconstruct_Arity_For_New_Required_Members()
+ {
+ var command = Command("ToolMoveOptions", "ToolOptions", ["move"]) with
+ {
+ Options =
+ [
+ new CliOptionDefinition
+ {
+ SwitchName = "--source",
+ PropertyName = "Source",
+ CSharpType = "string",
+ IsRequired = true,
+ },
+ new CliOptionDefinition
+ {
+ SwitchName = "--destination",
+ PropertyName = "Destination",
+ CSharpType = "string",
+ IsRequired = true,
+ },
+ ],
+ };
+
+ var exception = Assert.Throws(() =>
+ GeneratedApiCompatibilityPreserver.Preserve(
+ command,
+ [BaselineProperty("Source", "string", switchName: "--source", isRequired: true)]));
+
+ await Assert.That(exception.Message).Contains("newly required member(s) Destination have no baseline value");
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Restores_Required_Member_Demoted_To_Optional()
+ {
+ var command = Command("ToolDiffOptions", "ToolOptions", ["diff"]) with
+ {
+ PositionalArguments =
+ [
+ new CliPositionalArgument
+ {
+ PropertyName = "Target",
+ CSharpType = "string?",
+ PositionIndex = 0,
+ },
+ ],
+ };
+
+ var preserved = GeneratedApiCompatibilityPreserver.Preserve(
+ command,
+ [BaselineProperty("Target", "string", argumentPosition: 0, isRequired: true)]);
+ var generated = (await new OptionsClassGenerator().GenerateAsync(Tool(preserved))).Single().Content;
+
+ await Assert.That(generated).Contains("string Target");
+ await Assert.That(generated).DoesNotContain("public string? Target");
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Rejects_Removed_Positional_Operands()
+ {
+ var exception = Assert.Throws(() =>
+ GeneratedApiCompatibilityPreserver.Preserve(
+ Command("ToolInstallOptions", "ToolOptions", ["install"]),
+ [BaselineProperty("Name", "string?", argumentPosition: 1)]));
+
+ await Assert.That(exception.Message).Contains("Name positional argument was removed");
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Rejects_Removed_Required_Members()
+ {
+ var exception = Assert.Throws(() =>
+ GeneratedApiCompatibilityPreserver.Preserve(
+ Command("ToolAddOptions", "ToolOptions", ["add"]),
+ [BaselineProperty(
+ "Package",
+ "string",
+ argumentPosition: 0,
+ isRequired: true)]));
+
+ await Assert.That(exception.Message)
+ .Contains("ToolAddOptions.Package positional argument was removed");
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Rejects_Constructor_Preservation_When_Required_Member_Is_Added()
+ {
+ var command = Command("ToolAddOptions", "ToolOptions", ["add"]) with
+ {
+ PositionalArguments =
+ [
+ new CliPositionalArgument
+ {
+ PropertyName = "Package",
+ CSharpType = "string",
+ IsRequired = true,
+ PositionIndex = 0,
+ },
+ ],
+ };
+
+ var exception = Assert.Throws(() =>
+ GeneratedApiCompatibilityPreserver.Preserve(command, []));
+
+ await Assert.That(exception.Message).Contains("newly required member(s) Package have no baseline value");
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Retains_Previously_Generated_Secondary_Constructors()
+ {
+ var root = Path.Combine(Path.GetTempPath(), $"options-api-{Guid.NewGuid():N}");
+ var optionsDirectory = Path.Combine(root, "src", "ModularPipelines.Tool", "Options");
+ Directory.CreateDirectory(optionsDirectory);
+ try
+ {
+ await File.WriteAllTextAsync(
+ Path.Combine(optionsDirectory, "ToolAddOptions.Generated.cs"),
+ "public record ToolAddOptions([property: CliArgument(0)] string Package) "
+ + "{ public ToolAddOptions() : this(default!) { } }");
+ var command = Command("ToolAddOptions", "ToolOptions", ["add"]) with
+ {
+ PositionalArguments =
+ [
+ new CliPositionalArgument
+ {
+ PropertyName = "Package",
+ CSharpType = "string",
+ IsRequired = true,
+ PositionIndex = 0,
+ },
+ ],
+ };
+
+ var preserved = GeneratedApiCompatibilityPreserver.Preserve(Tool(command), root);
+ var generated = (await new OptionsClassGenerator().GenerateAsync(preserved)).Single().Content;
+
+ await Assert.That(generated).Contains("public ToolAddOptions()");
+ await Assert.That(generated).Contains(": this(default!)");
+ }
+ finally
+ {
+ Directory.Delete(root, recursive: true);
+ }
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Does_Not_Retain_Nullability_Only_Constructor_Duplicates()
+ {
+ var root = Path.Combine(Path.GetTempPath(), $"options-api-{Guid.NewGuid():N}");
+ var optionsDirectory = Path.Combine(root, "src", "ModularPipelines.Tool", "Options");
+ Directory.CreateDirectory(optionsDirectory);
+ try
+ {
+ await File.WriteAllTextAsync(
+ Path.Combine(optionsDirectory, "ToolAddOptions.Generated.cs"),
+ "public record ToolAddOptions([property: CliArgument(0)] string Package) "
+ + "{ public ToolAddOptions(string? LegacyPackage) : this(LegacyPackage!) { } }");
+ var command = Command("ToolAddOptions", "ToolOptions", ["add"]) with
+ {
+ PositionalArguments =
+ [
+ new CliPositionalArgument
+ {
+ PropertyName = "Package",
+ CSharpType = "string",
+ IsRequired = true,
+ PositionIndex = 0,
+ },
+ ],
+ };
+
+ var preserved = GeneratedApiCompatibilityPreserver.Preserve(Tool(command), root);
+ var generated = (await new OptionsClassGenerator().GenerateAsync(preserved)).Single().Content;
+
+ await Assert.That(generated).DoesNotContain("public ToolAddOptions(string? LegacyPackage)");
+ }
+ finally
+ {
+ Directory.Delete(root, recursive: true);
+ }
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Rejects_Deconstruct_Preservation_For_New_Required_Members()
+ {
+ var root = Path.Combine(Path.GetTempPath(), $"options-api-{Guid.NewGuid():N}");
+ var optionsDirectory = Path.Combine(root, "src", "ModularPipelines.Tool", "Options");
+ Directory.CreateDirectory(optionsDirectory);
+ try
+ {
+ await File.WriteAllTextAsync(
+ Path.Combine(optionsDirectory, "ToolMoveOptions.Generated.cs"),
+ "public record ToolMoveOptions([property: CliOption(\"--source\")] string Source, "
+ + "[property: CliOption(\"--destination\")] string Destination) "
+ + "{ public ToolMoveOptions(string Source) : this(Source, default!) { } "
+ + "public void Deconstruct(out string Source) { Source = this.Source; } }");
+ var command = Command("ToolMoveOptions", "ToolOptions", ["move"]) with
+ {
+ Options =
+ [
+ RequiredOption("--source", "Source"),
+ RequiredOption("--destination", "Destination"),
+ RequiredOption("--force", "Force"),
+ ],
+ };
+
+ var exception = Assert.Throws(() =>
+ GeneratedApiCompatibilityPreserver.Preserve(Tool(command), root));
+
+ await Assert.That(exception.Message).Contains("newly required member(s) Force have no baseline value");
+ }
+ finally
+ {
+ Directory.Delete(root, recursive: true);
+ }
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Retains_Command_Group_Execute_Facades()
+ {
+ var root = Path.Combine(Path.GetTempPath(), $"service-api-{Guid.NewGuid():N}");
+ var packageDirectory = Path.Combine(root, "src", "ModularPipelines.Tool");
+ Directory.CreateDirectory(Path.Combine(packageDirectory, "Options"));
+ Directory.CreateDirectory(Path.Combine(packageDirectory, "Services"));
+ try
+ {
+ await File.WriteAllTextAsync(
+ Path.Combine(packageDirectory, "Options", "ToolEditAddOptions.Generated.cs"),
+ "public record ToolEditAddOptions;");
+ await File.WriteAllTextAsync(
+ Path.Combine(packageDirectory, "Services", "ToolEditAdd.Generated.cs"),
+ "namespace ModularPipelines.Tool.Services; "
+ + "public class ToolEditAdd { public Task ExecuteAsync(ToolEditAddOptions? options = null) => Task.CompletedTask; }");
+ var parent = Command(
+ "ToolEditAddOptions",
+ "ToolOptions",
+ ["edit", "add"],
+ subDomainGroup: "edit");
+ var child = Command(
+ "ToolEditAddSecretOptions",
+ "ToolOptions",
+ ["edit", "add", "secret"],
+ subDomainGroup: "edit");
+
+ var preserved = GeneratedApiCompatibilityPreserver.Preserve(Tool(parent, child), root);
+ var generated = (await new SubDomainClassGenerator().GenerateAsync(preserved))
+ .Single(file => file.RelativePath.EndsWith(
+ "ToolEditAdd.Generated.cs",
+ StringComparison.Ordinal))
+ .Content;
+
+ await Assert.That(generated).Contains("Task ExecuteAsync(");
+ }
+ finally
+ {
+ Directory.Delete(root, recursive: true);
+ }
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Rejects_Removed_Command_Facades()
+ {
+ var root = Path.Combine(Path.GetTempPath(), $"service-api-{Guid.NewGuid():N}");
+ var packageDirectory = Path.Combine(root, "src", "ModularPipelines.Tool");
+ Directory.CreateDirectory(Path.Combine(packageDirectory, "Options"));
+ Directory.CreateDirectory(Path.Combine(packageDirectory, "Services"));
+ try
+ {
+ await File.WriteAllTextAsync(
+ Path.Combine(packageDirectory, "Options", "ToolRemovedOptions.Generated.cs"),
+ "public record ToolRemovedOptions;");
+ await File.WriteAllTextAsync(
+ Path.Combine(packageDirectory, "Services", "Tool.Generated.cs"),
+ "namespace ModularPipelines.Tool.Services; "
+ + "public class Tool { public Task RemovedAsync(ToolRemovedOptions? options = null) => Task.CompletedTask; }");
+
+ var exception = Assert.Throws(() =>
+ GeneratedApiCompatibilityPreserver.Preserve(
+ Tool(Command("ToolCurrentOptions", "ToolOptions", ["current"])),
+ root));
+
+ await Assert.That(exception.Message)
+ .Contains("ToolRemovedOptions command disappeared from generated facade");
+ }
+ finally
+ {
+ Directory.Delete(root, recursive: true);
+ }
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Rejects_Optional_Facade_When_Required_Member_Is_Added()
+ {
+ var root = Path.Combine(Path.GetTempPath(), $"service-api-{Guid.NewGuid():N}");
+ var packageDirectory = Path.Combine(root, "src", "ModularPipelines.Tool");
+ Directory.CreateDirectory(Path.Combine(packageDirectory, "Options"));
+ Directory.CreateDirectory(Path.Combine(packageDirectory, "Services"));
+ try
+ {
+ await File.WriteAllTextAsync(
+ Path.Combine(packageDirectory, "Options", "ToolAddOptions.Generated.cs"),
+ "public record ToolAddOptions;");
+ await File.WriteAllTextAsync(
+ Path.Combine(packageDirectory, "Services", "Tool.Generated.cs"),
+ "namespace ModularPipelines.Tool.Services; "
+ + "public class Tool { public Task AddAsync(ToolAddOptions? options = null) => Task.CompletedTask; }");
+ var command = Command("ToolAddOptions", "ToolOptions", ["add"]) with
+ {
+ PositionalArguments =
+ [
+ new CliPositionalArgument
+ {
+ PropertyName = "Package",
+ CSharpType = "string",
+ IsRequired = true,
+ PositionIndex = 0,
+ },
+ ],
+ };
+
+ var exception = Assert.Throws(() =>
+ GeneratedApiCompatibilityPreserver.Preserve(Tool(command), root));
+
+ await Assert.That(exception.Message).Contains("newly required member(s) Package have no baseline value");
+ }
+ finally
+ {
+ Directory.Delete(root, recursive: true);
+ }
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Retains_Leaf_Facade_When_It_Gains_Children()
+ {
+ var root = Path.Combine(Path.GetTempPath(), $"service-api-{Guid.NewGuid():N}");
+ var packageDirectory = Path.Combine(root, "src", "ModularPipelines.Tool");
+ Directory.CreateDirectory(Path.Combine(packageDirectory, "Options"));
+ Directory.CreateDirectory(Path.Combine(packageDirectory, "Services"));
+ try
+ {
+ await File.WriteAllTextAsync(
+ Path.Combine(packageDirectory, "Options", "ToolGroupChildOptions.Generated.cs"),
+ "public record ToolGroupChildOptions;");
+ await File.WriteAllTextAsync(
+ Path.Combine(packageDirectory, "Services", "ToolGroup.Generated.cs"),
+ "namespace ModularPipelines.Tool.Services; "
+ + "public class ToolGroup { public Task ChildAsync(ToolGroupChildOptions? options = null) => Task.CompletedTask; }");
+ var child = Command(
+ "ToolGroupChildOptions",
+ "ToolOptions",
+ ["group", "child"],
+ subDomainGroup: "group");
+ var grandchild = Command(
+ "ToolGroupChildSubOptions",
+ "ToolOptions",
+ ["group", "child", "sub"],
+ subDomainGroup: "group");
+
+ var preserved = GeneratedApiCompatibilityPreserver.Preserve(
+ Tool(child, grandchild),
+ root);
+ var generated = await new SubDomainClassGenerator().GenerateAsync(preserved);
+ var groupFacade = generated.Single(file => Path.GetFileName(file.RelativePath)
+ .Equals("ToolGroup.Generated.cs", StringComparison.Ordinal));
+ var childFacade = generated.Single(file => Path.GetFileName(file.RelativePath)
+ .Equals("ToolGroupChild.Generated.cs", StringComparison.Ordinal));
+
+ using (Assert.Multiple())
+ {
+ await Assert.That(groupFacade.Content)
+ .Contains("Task ChildAsync(");
+ await Assert.That(childFacade.Content)
+ .Contains("Task ExecuteAsync(");
+ }
+ }
+ finally
+ {
+ Directory.Delete(root, recursive: true);
+ }
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Ignores_Other_Tool_Facades_In_Shared_Package()
+ {
+ var root = Path.Combine(Path.GetTempPath(), $"service-api-{Guid.NewGuid():N}");
+ var packageDirectory = Path.Combine(root, "src", "ModularPipelines.Tool");
+ Directory.CreateDirectory(Path.Combine(packageDirectory, "Options"));
+ Directory.CreateDirectory(Path.Combine(packageDirectory, "Services"));
+ try
+ {
+ await File.WriteAllTextAsync(
+ Path.Combine(packageDirectory, "Options", "ToolCurrentOptions.Generated.cs"),
+ "public record ToolCurrentOptions;");
+ await File.WriteAllTextAsync(
+ Path.Combine(packageDirectory, "Services", "Tool.Generated.cs"),
+ "namespace ModularPipelines.Tool.Services; "
+ + "public class Tool { public Task CurrentAsync(ToolCurrentOptions? options = null) => Task.CompletedTask; }");
+ await File.WriteAllTextAsync(
+ Path.Combine(packageDirectory, "Services", "Other.Generated.cs"),
+ "namespace ModularPipelines.Tool.Services; "
+ + "public class Other { public Task RemovedAsync(OtherRemovedOptions? options = null) => Task.CompletedTask; }");
+
+ var preserved = GeneratedApiCompatibilityPreserver.Preserve(
+ Tool(Command("ToolCurrentOptions", "ToolOptions", ["current"])),
+ root);
+
+ await Assert.That(preserved.Commands).HasSingleItem();
+ }
+ finally
+ {
+ Directory.Delete(root, recursive: true);
+ }
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Accepts_Current_Command_Group_Alias_Facades()
+ {
+ var root = Path.Combine(Path.GetTempPath(), $"service-api-{Guid.NewGuid():N}");
+ var packageDirectory = Path.Combine(root, "src", "ModularPipelines.Tool");
+ Directory.CreateDirectory(Path.Combine(packageDirectory, "Options"));
+ Directory.CreateDirectory(Path.Combine(packageDirectory, "Services"));
+ try
+ {
+ await File.WriteAllTextAsync(
+ Path.Combine(packageDirectory, "Options", "ToolBuilderBakeOptions.Generated.cs"),
+ "public record ToolBuilderBakeOptions;");
+ await File.WriteAllTextAsync(
+ Path.Combine(packageDirectory, "Services", "ToolBuilder.Generated.cs"),
+ "namespace ModularPipelines.Tool.Services; "
+ + "public class ToolBuilder { public Task BakeAsync(ToolBuilderBakeOptions? options = null) => Task.CompletedTask; }");
+ var tool = Tool(Command(
+ "ToolBuildxBakeOptions",
+ "ToolOptions",
+ ["buildx", "bake"],
+ subDomainGroup: "Buildx")) with
+ {
+ CommandGroupAliases =
+ [
+ new CliCommandGroupAlias
+ {
+ Alias = "builder",
+ CanonicalCommand = "buildx",
+ ObsoleteMessage = "Use buildx instead.",
+ },
+ ],
+ };
+
+ var preserved = GeneratedApiCompatibilityPreserver.Preserve(tool, root);
+
+ await Assert.That(preserved.Commands).HasSingleItem();
+ }
+ finally
+ {
+ Directory.Delete(root, recursive: true);
+ }
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Rejects_Alias_Constructors_For_New_Required_Members()
+ {
+ var root = Path.Combine(Path.GetTempPath(), $"options-api-{Guid.NewGuid():N}");
+ var optionsDirectory = Path.Combine(root, "src", "ModularPipelines.Tool", "Options");
+ Directory.CreateDirectory(optionsDirectory);
+ try
+ {
+ await File.WriteAllTextAsync(
+ Path.Combine(optionsDirectory, "ToolBuildxBakeOptions.Generated.cs"),
+ "public record ToolBuildxBakeOptions([property: CliOption(\"--source\")] string Source);");
+ await File.WriteAllTextAsync(
+ Path.Combine(optionsDirectory, "ToolBuilderBakeOptions.Generated.cs"),
+ "public record ToolBuilderBakeOptions : ToolBuildxBakeOptions "
+ + "{ public ToolBuilderBakeOptions(string Source) : base(Source) { } }");
+ var command = Command(
+ "ToolBuildxBakeOptions",
+ "ToolOptions",
+ ["buildx", "bake"],
+ subDomainGroup: "Buildx") with
+ {
+ Options =
+ [
+ RequiredOption("--source", "Source"),
+ RequiredOption("--destination", "Destination"),
+ ],
+ };
+ var tool = Tool(command) with
+ {
+ CommandGroupAliases =
+ [
+ new CliCommandGroupAlias
+ {
+ Alias = "builder",
+ CanonicalCommand = "buildx",
+ ObsoleteMessage = "Use buildx instead.",
+ },
+ ],
+ };
+
+ var exception = Assert.Throws(() =>
+ GeneratedApiCompatibilityPreserver.Preserve(tool, root));
+
+ await Assert.That(exception.Message).Contains("newly required member(s) Destination have no baseline value");
+ }
+ finally
+ {
+ Directory.Delete(root, recursive: true);
+ }
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Rejects_Parameterless_Alias_Constructor_For_New_Required_Members()
+ {
+ var root = Path.Combine(Path.GetTempPath(), $"options-api-{Guid.NewGuid():N}");
+ var optionsDirectory = Path.Combine(root, "src", "ModularPipelines.Tool", "Options");
+ Directory.CreateDirectory(optionsDirectory);
+ try
+ {
+ await File.WriteAllTextAsync(
+ Path.Combine(optionsDirectory, "ToolBuildxBakeOptions.Generated.cs"),
+ "public record ToolBuildxBakeOptions;");
+ await File.WriteAllTextAsync(
+ Path.Combine(optionsDirectory, "ToolBuilderBakeOptions.Generated.cs"),
+ "public record ToolBuilderBakeOptions : ToolBuildxBakeOptions;");
+ var command = Command(
+ "ToolBuildxBakeOptions",
+ "ToolOptions",
+ ["buildx", "bake"],
+ subDomainGroup: "Buildx") with
+ {
+ Options = [RequiredOption("--source", "Source")],
+ };
+ var tool = Tool(command) with
+ {
+ CommandGroupAliases =
+ [
+ new CliCommandGroupAlias
+ {
+ Alias = "builder",
+ CanonicalCommand = "buildx",
+ ObsoleteMessage = "Use buildx instead.",
+ },
+ ],
+ };
+
+ var exception = Assert.Throws(() =>
+ GeneratedApiCompatibilityPreserver.Preserve(tool, root));
+
+ await Assert.That(exception.Message).Contains("newly required member(s) Source have no baseline value");
+ }
+ finally
+ {
+ Directory.Delete(root, recursive: true);
+ }
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Retains_Command_Group_Alias_Enum_Properties()
+ {
+ var root = Path.Combine(Path.GetTempPath(), $"options-api-{Guid.NewGuid():N}");
+ var packageDirectory = Path.Combine(root, "src", "ModularPipelines.Tool");
+ var optionsDirectory = Path.Combine(packageDirectory, "Options");
+ var enumsDirectory = Path.Combine(packageDirectory, "Enums");
+ Directory.CreateDirectory(optionsDirectory);
+ Directory.CreateDirectory(enumsDirectory);
+ try
+ {
+ await File.WriteAllTextAsync(
+ Path.Combine(optionsDirectory, "ToolBuildxBakeOptions.Generated.cs"),
+ "public record ToolBuildxBakeOptions { "
+ + "[CliOption(\"--progress\")] public ToolBuildxBakeProgress? Progress { get; set; } }");
+ await File.WriteAllTextAsync(
+ Path.Combine(optionsDirectory, "ToolBuilderBakeOptions.Generated.cs"),
+ "public record ToolBuilderBakeOptions : ToolBuildxBakeOptions { "
+ + "[CliOption(\"--progress\")] public new ToolBuilderBakeProgress? Progress { get; set; } }");
+ await File.WriteAllTextAsync(
+ Path.Combine(enumsDirectory, "ToolBuildxBakeProgress.Generated.cs"),
+ "public enum ToolBuildxBakeProgress { [EnumValue(\"plain\")] Plain = 4 }");
+ await File.WriteAllTextAsync(
+ Path.Combine(enumsDirectory, "ToolBuilderBakeProgress.Generated.cs"),
+ "public enum ToolBuilderBakeProgress { [EnumValue(\"plain\")] Plain = 4 }");
+ var enumDefinition = new CliEnumDefinition
+ {
+ EnumName = "ToolBuildxBakeProgress",
+ Values =
+ [
+ new CliEnumValue { MemberName = "Plain", CliValue = "plain" },
+ new CliEnumValue { MemberName = "Tty", CliValue = "tty" },
+ ],
+ };
+ var command = Command(
+ "ToolBuildxBakeOptions",
+ "ToolOptions",
+ ["buildx", "bake"],
+ subDomainGroup: "Buildx",
+ options:
+ [
+ new CliOptionDefinition
+ {
+ SwitchName = "--progress",
+ PropertyName = "Progress",
+ CSharpType = "ToolBuildxBakeProgress?",
+ EnumDefinition = enumDefinition,
+ },
+ ]);
+ var tool = Tool(command) with
+ {
+ CommandGroupAliases =
+ [
+ new CliCommandGroupAlias
+ {
+ Alias = "builder",
+ CanonicalCommand = "buildx",
+ ObsoleteMessage = "Use buildx instead.",
+ },
+ ],
+ };
+
+ var preserved = GeneratedApiCompatibilityPreserver.Preserve(tool, root);
+ var generatedOptions = await new OptionsClassGenerator().GenerateAsync(preserved);
+ var generatedAlias = generatedOptions.Single(file => Path.GetFileName(file.RelativePath)
+ .Equals("ToolBuilderBakeOptions.Generated.cs", StringComparison.Ordinal)).Content;
+ var generatedCanonical = generatedOptions.Single(file => Path.GetFileName(file.RelativePath)
+ .Equals("ToolBuildxBakeOptions.Generated.cs", StringComparison.Ordinal)).Content;
+ var generatedEnums = await new EnumGenerator().GenerateAsync(preserved);
+
+ using (Assert.Multiple())
+ {
+ await Assert.That(generatedCanonical)
+ .Contains("public ToolBuildxBakeProgress? Progress { get; set; }");
+ await Assert.That(generatedAlias)
+ .Contains("public new ToolBuilderBakeProgress? Progress");
+ await Assert.That(generatedAlias)
+ .Contains("(ToolBuildxBakeProgress)(int)value.Value");
+ await Assert.That(generatedEnums.Select(file => Path.GetFileName(file.RelativePath)))
+ .Contains("ToolBuildxBakeProgress.Generated.cs");
+ await Assert.That(generatedEnums.Select(file => Path.GetFileName(file.RelativePath)))
+ .Contains("ToolBuilderBakeProgress.Generated.cs");
+ await Assert.That(generatedEnums.Single(file => Path.GetFileName(file.RelativePath)
+ .Equals("ToolBuilderBakeProgress.Generated.cs", StringComparison.Ordinal)).Content)
+ .Contains("Tty");
+ }
+ }
+ finally
+ {
+ Directory.Delete(root, recursive: true);
+ }
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Scopes_Enum_Baselines_To_The_Current_Tool()
+ {
+ var root = Path.Combine(Path.GetTempPath(), $"options-api-{Guid.NewGuid():N}");
+ var packageDirectory = Path.Combine(root, "src", "ModularPipelines.Tool");
+ var optionsDirectory = Path.Combine(packageDirectory, "Options");
+ var enumsDirectory = Path.Combine(packageDirectory, "Enums");
+ Directory.CreateDirectory(optionsDirectory);
+ Directory.CreateDirectory(enumsDirectory);
+ try
+ {
+ await File.WriteAllTextAsync(
+ Path.Combine(optionsDirectory, "ToolRunOptions.Generated.cs"),
+ "public record ToolRunOptions;");
+ await File.WriteAllTextAsync(
+ Path.Combine(enumsDirectory, "ToolLegacy.Generated.cs"),
+ "public enum ToolLegacy { Value }");
+ await File.WriteAllTextAsync(
+ Path.Combine(enumsDirectory, "OtherLegacy.Generated.cs"),
+ "public enum OtherLegacy { Value }");
+
+ var preserved = GeneratedApiCompatibilityPreserver.Preserve(
+ Tool(Command("ToolRunOptions", "ToolOptions", ["run"])),
+ root);
+
+ await Assert.That(preserved.CompatibilityEnums.Select(static definition => definition.EnumName))
+ .IsEquivalentTo(["ToolLegacy"]);
+ }
+ finally
+ {
+ Directory.Delete(root, recursive: true);
+ }
+ }
+
+ [Test]
+ public async Task OptionsClassGenerator_Rejects_Alias_Enum_Nullability_Changes()
+ {
+ const string aliasClassName = "ToolBuilderBakeOptions";
+ var command = Command(
+ "ToolBuildxBakeOptions",
+ "ToolOptions",
+ ["buildx", "bake"],
+ subDomainGroup: "Buildx") with
+ {
+ AliasCompatibilityProperties = new Dictionary>
+ {
+ [aliasClassName] =
+ [
+ new CliAliasCompatibilityProperty
+ {
+ PropertyName = "Progress",
+ AliasCSharpType = "ToolBuilderBakeProgress?",
+ CanonicalCSharpType = "ToolBuildxBakeProgress",
+ ObsoleteMessage = "Use the canonical property instead.",
+ },
+ ],
+ },
+ };
+ var tool = Tool(command) with
+ {
+ CommandGroupAliases =
+ [
+ new CliCommandGroupAlias
+ {
+ Alias = "builder",
+ CanonicalCommand = "buildx",
+ ObsoleteMessage = "Use buildx instead.",
+ },
+ ],
+ };
+
+ var exception = Assert.Throws(() =>
+ new OptionsClassGenerator().GenerateAsync(tool));
+
+ await Assert.That(exception.Message)
+ .Contains("alias property Progress because its nullability changed");
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Rekeys_Documentation_Examples_After_Required_Rename()
+ {
+ var command = Command("ToolAddOptions", "ToolOptions", ["add"]) with
+ {
+ PositionalArguments =
+ [
+ new CliPositionalArgument
+ {
+ PropertyName = "CurrentName",
+ CSharpType = "string",
+ IsRequired = true,
+ PositionIndex = 0,
+ },
+ ],
+ DocumentationExampleValues = new Dictionary(StringComparer.Ordinal)
+ {
+ ["CurrentName"] = "\"example\"",
+ },
+ };
+
+ var preserved = GeneratedApiCompatibilityPreserver.Preserve(
+ command,
+ [BaselineProperty("StableName", "string", argumentPosition: 0, isRequired: true)]);
+
+ await Assert.That(preserved.DocumentationExampleValues.Keys).IsEquivalentTo(["StableName"]);
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Rejects_Required_Rename_Collisions()
+ {
+ var command = Command("ToolAddOptions", "ToolOptions", ["add"]) with
+ {
+ PositionalArguments =
+ [
+ new CliPositionalArgument
+ {
+ PropertyName = "CurrentName",
+ CSharpType = "string",
+ IsRequired = true,
+ PositionIndex = 0,
+ },
+ ],
+ Options =
+ [
+ new CliOptionDefinition
+ {
+ SwitchName = "--stable-name",
+ PropertyName = "StableName",
+ CSharpType = "string?",
+ },
+ ],
+ };
+
+ var exception = Assert.Throws(() =>
+ GeneratedApiCompatibilityPreserver.Preserve(
+ command,
+ [BaselineProperty("StableName", "string", argumentPosition: 0, isRequired: true)]));
+
+ await Assert.That(exception.Message).Contains("would duplicate a member name");
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Retains_Removed_Global_Options()
+ {
+ var preserved = GeneratedApiCompatibilityPreserver.PreserveGlobalOptions(
+ Tool(Command("ToolRunOptions", "ToolOptions", ["run"])),
+ [BaselineProperty("LegacyFlag", "bool?", switchName: "--legacy-flag")]);
+
+ var generated = (await new GlobalOptionsBaseGenerator().GenerateAsync(preserved)).Single().Content;
+
+ using (Assert.Multiple())
+ {
+ await Assert.That(generated).Contains("public virtual bool? LegacyFlag { get; set; }");
+ await Assert.That(generated).Contains("LegacyFlag is no longer supported");
+ await Assert.That(generated).DoesNotContain("CliFlag(\"--legacy-flag\")");
+ }
+ }
+
+ [Test]
+ public async Task ApiCompatibilityPreserver_Validates_Supplemental_Global_Options()
+ {
+ var tool = Tool(Command("ToolRunOptions", "ToolOptions", ["run"])) with
+ {
+ SupplementalGlobalOptions =
+ [
+ new CliOptionDefinition
+ {
+ SwitchName = "--progress",
+ PropertyName = "Progress",
+ CSharpType = "string?",
+ },
+ ],
+ };
+
+ var exception = Assert.Throws(() =>
+ GeneratedApiCompatibilityPreserver.PreserveGlobalOptions(
+ tool,
+ [BaselineProperty("Progress", "CliOptionValue?", switchName: "--progress")]));
+
+ await Assert.That(exception.Message)
+ .Contains("ToolOptions.Progress changed type from CliOptionValue? to string?");
+ }
+
+ [Test]
+ public async Task Global_Compatibility_Targets_Follow_Inherited_Property_Renames()
+ {
+ var tool = Tool(Command("ToolRunOptions", "ToolOptions", ["run"])) with
+ {
+ GlobalOptions =
+ [
+ new CliOptionDefinition
+ {
+ SwitchName = "--arguments",
+ PropertyName = "Arguments",
+ CSharpType = "IEnumerable?",
+ },
+ ],
+ GlobalCompatibilityProperties =
+ [
+ new CliCompatibilityProperty
+ {
+ PropertyName = "LegacyArguments",
+ CSharpType = "IEnumerable?",
+ ForwardToPropertyName = "Arguments",
+ ObsoleteMessage = "Use Arguments instead.",
+ },
+ ],
+ };
+
+ var resolved = InheritedPropertyCollisionResolver.Resolve(tool);
+ var generated = (await new GlobalOptionsBaseGenerator().GenerateAsync(resolved)).Single().Content;
+
+ using (Assert.Multiple())
+ {
+ await Assert.That(generated).Contains("public virtual IEnumerable? CliArguments");
+ await Assert.That(generated).Contains("get => CliArguments;");
+ await Assert.That(generated).Contains("set => CliArguments = value;");
+ }
+ }
+
[Test]
public async Task OptionsClassGenerator_Marks_Secret_Positional_Arguments()
{
diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/CosignCliScraperTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/CosignCliScraperTests.cs
index 03b964c17a3..b5a3b986449 100644
--- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/CosignCliScraperTests.cs
+++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/CosignCliScraperTests.cs
@@ -145,6 +145,6 @@ public IReadOnlyList ApplyFix(
IReadOnlyList positionalArguments) =>
ApplyPositionalArgumentFixes(commandParts, positionalArguments);
- public bool IsSecret(string propertyName, bool isFlag) => IsSecretOption(propertyName, isFlag);
+ public bool IsSecret(string propertyName, bool isFlag) => IsSecretOption(propertyName, isFlag, string.Empty);
}
}
diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/DockerCliCompatibilityTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/DockerCliCompatibilityTests.cs
index 35882aaa027..421cb8d9331 100644
--- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/DockerCliCompatibilityTests.cs
+++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/DockerCliCompatibilityTests.cs
@@ -1,5 +1,8 @@
+using Microsoft.Extensions.Logging.Abstractions;
using ModularPipelines.OptionsGenerator.Models;
using ModularPipelines.OptionsGenerator.Scrapers;
+using ModularPipelines.OptionsGenerator.Scrapers.Cli;
+using ModularPipelines.OptionsGenerator.TypeDetection;
namespace ModularPipelines.OptionsGenerator.Tests.Scrapers;
@@ -25,6 +28,62 @@ public async Task SupportedAliases_ExcludeAlias_WhenCanonicalCommandWasNotScrape
await Assert.That(aliases).IsEmpty();
}
+ [Test]
+ public async Task ComposeExec_Preserves_Canonical_NoTty_Switch_Casing()
+ {
+ const string helpText = """
+ Execute a command in a running container
+
+ Usage: docker compose exec [OPTIONS] SERVICE COMMAND [ARGS...]
+
+ Options:
+ -T, --no-tty Disable pseudo-TTY allocation
+ """;
+ var command = await new TestDockerCliScraper().Parse(
+ ["docker", "compose", "exec"],
+ helpText);
+
+ var option = command!.Options.Single();
+ await Assert.That(option.SwitchName).IsEqualTo("--no-TTY");
+ await Assert.That(option.ShortForm).IsEqualTo("-T");
+ }
+
+ [Test]
+ public async Task Switch_Normalization_Rejects_Distinct_Options_With_One_Canonical_Name()
+ {
+ const string helpText = """
+ Usage: fake run [OPTIONS]
+
+ Options:
+ --current string Current value
+ --legacy string Legacy value
+ """;
+
+ await Assert.That(() => new CollidingSwitchScraper().Parse(
+ ["fake", "run"],
+ helpText))
+ .Throws()
+ .And.HasMessageContaining("maps both '--current' and '--legacy'");
+ }
+
+ [Test]
+ public async Task Switch_Normalization_Treats_Source_Switch_Casing_As_Distinct()
+ {
+ const string helpText = """
+ Usage: fake run [OPTIONS]
+
+ Options:
+ --current string Current value
+ --CURRENT string Upper-case value
+ """;
+
+ await Assert.That(() => new CollidingSwitchScraper().Parse(
+ ["fake", "run"],
+ helpText))
+ .Throws()
+ .And.HasMessageContaining("maps both '--current' and '--CURRENT'");
+ }
+
private static CliCommandDefinition CreateCommand(string commandGroup)
{
return new CliCommandDefinition
@@ -37,4 +96,50 @@ private static CliCommandDefinition CreateCommand(string commandGroup)
Options = [],
};
}
+
+ private sealed class TestDockerCliScraper : DockerCliScraper
+ {
+ public TestDockerCliScraper()
+ : base(
+ new ProcessCliCommandExecutor(NullLogger.Instance),
+ new HelpTextCache(NullLogger.Instance),
+ NullLogger.Instance)
+ {
+ }
+
+ public Task Parse(string[] commandPath, string helpText)
+ {
+ var usage = ParseUsageSynopsis(commandPath, helpText);
+ return ParseCommandAsync(commandPath, helpText, usage, CancellationToken.None);
+ }
+ }
+
+ private sealed class CollidingSwitchScraper : CobraCliScraper
+ {
+ public CollidingSwitchScraper()
+ : base(
+ new ProcessCliCommandExecutor(NullLogger.Instance),
+ new HelpTextCache(NullLogger.Instance),
+ NullLogger.Instance)
+ {
+ }
+
+ public override string ToolName => "fake";
+
+ public override string NamespacePrefix => "Fake";
+
+ public override string TargetNamespace => "ModularPipelines.Fake";
+
+ public override string OutputDirectory => "src/ModularPipelines.Fake";
+
+ protected override string NormalizeOptionSwitchName(
+ string[] commandParts,
+ string switchName) => "--canonical";
+
+ public Task Parse(string[] commandPath, string helpText)
+ {
+ var usage = ParseUsageSynopsis(commandPath, helpText);
+ return ParseCommandAsync(commandPath, helpText, usage, CancellationToken.None);
+ }
+ }
}
diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/TrivyCliScraperTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/TrivyCliScraperTests.cs
index 9221cc6eae4..0e2699c28cf 100644
--- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/TrivyCliScraperTests.cs
+++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/TrivyCliScraperTests.cs
@@ -80,6 +80,18 @@ await Assert.That(scraper.NormalizeDescription(description))
}
}
+ [Test]
+ public async Task Home_Directory_Path_Is_Normalized_Across_Platforms()
+ {
+ var scraper = new TestTrivyCliScraper();
+
+ var description = scraper.NormalizeDescription(
+ @"module directory (default ""C:\Users\runneradmin\.trivy\modules"")");
+
+ await Assert.That(description)
+ .IsEqualTo("module directory (default \"/.trivy/modules\")");
+ }
+
[Test]
public async Task Image_Help_Parses_Target_Types_And_Secrets()
{
@@ -117,6 +129,25 @@ await Assert.That(command.Options.Single(x => x.SwitchName == "--timeout").CShar
await Assert.That(command.Options.Single(x => x.SwitchName == "--password").IsSecret).IsTrue();
}
+ [Test]
+ public async Task Secret_Config_Path_Is_Not_Classified_As_Secret()
+ {
+ const string helpText = """
+ Scan a container image
+
+ Usage:
+ trivy image [flags] IMAGE_NAME
+
+ Secret Flags
+ --secret-config string specify a path to config file for secret scanning
+ """;
+
+ var command = await new TestTrivyCliScraper().Parse(["trivy", "image"], helpText);
+ var secretConfig = command!.Options.Single(x => x.SwitchName == "--secret-config");
+
+ await Assert.That(secretConfig.IsSecret).IsFalse();
+ }
+
[Test]
public async Task Plugin_Upgrade_Accepts_Multiple_Optional_Names()
{
@@ -160,7 +191,7 @@ trivy plugin run [flags] NAME | URL | FILE_PATH
await Assert.That(positionals[1].PropertyName).IsEqualTo("PluginArguments");
await Assert.That(positionals[1].CSharpType).IsEqualTo("IEnumerable?");
await Assert.That(positionals[1].IsRequired).IsFalse();
- await Assert.That(positionals[1].PositionIndex).IsEqualTo(1);
+ await Assert.That(positionals[1].PositionIndex).IsEqualTo(0);
await Assert.That(positionals[1].Phase).IsEqualTo(CommandLinePhase.Passthrough);
}
@@ -185,6 +216,27 @@ trivy vex repo download [REPO_NAMES] [flags]
await Assert.That(positional.IsRequired).IsFalse();
}
+ [Test]
+ public async Task Vex_Command_Preserves_Optional_Command_Operand()
+ {
+ const string helpText = """
+ [EXPERIMENTAL] VEX utilities
+
+ Usage:
+ trivy vex [command]
+
+ Available Commands:
+ repo Manage VEX repositories
+ """;
+
+ var command = await new TestTrivyCliScraper().Parse(["trivy", "vex"], helpText);
+ var positional = command!.PositionalArguments.Single();
+
+ await Assert.That(positional.PropertyName).IsEqualTo("Command");
+ await Assert.That(positional.CSharpType).IsEqualTo("string?");
+ await Assert.That(positional.IsRequired).IsFalse();
+ }
+
private sealed class TestTrivyCliScraper : TrivyCliScraper
{
public TestTrivyCliScraper()
diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/CodeGeneratorOrchestrator.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/CodeGeneratorOrchestrator.cs
index a360b9c56ba..70cf4a1cb9a 100644
--- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/CodeGeneratorOrchestrator.cs
+++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/CodeGeneratorOrchestrator.cs
@@ -834,13 +834,17 @@ private async Task GenerateForToolAsync(
{
var globalOptions = tool.GetGlobalOptions();
var normalizedCommands = GeneratorUtils.NormalizeCommandClassNames(tool.Commands);
- var toolDefinition = InheritedPropertyCollisionResolver.Resolve(
- ExecutablePrerequisiteCatalog.PrepareForGeneration(tool with
+ var collisionResolvedTool = InheritedPropertyCollisionResolver.Resolve(
+ tool with
{
Commands = normalizedCommands,
GlobalOptions = globalOptions,
SupplementalGlobalOptions = [],
- }));
+ });
+ var compatibleTool = GeneratedApiCompatibilityPreserver.Preserve(
+ collisionResolvedTool,
+ outputDirectory);
+ var toolDefinition = ExecutablePrerequisiteCatalog.PrepareForGeneration(compatibleTool);
if (enforceOutputContainment)
{
diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/EnumDefinitionStabilizer.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/EnumDefinitionStabilizer.cs
index 9fff6663789..987b195c4c6 100644
--- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/EnumDefinitionStabilizer.cs
+++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/EnumDefinitionStabilizer.cs
@@ -4,7 +4,7 @@
namespace ModularPipelines.OptionsGenerator.Generators;
///
-/// Preserves the public numeric contract of generated enums across regeneration.
+/// Preserves generated enum members, CLI values, and numeric values across regeneration.
///
internal static partial class EnumDefinitionStabilizer
{
@@ -43,6 +43,9 @@ public static CliToolDefinition Stabilize(
{
Commands = commands,
GlobalOptions = StabilizeOptions(tool.GlobalOptions, stabilizedEnums),
+ CompatibilityEnums = tool.CompatibilityEnums
+ .Select(definition => stabilizedEnums[definition.EnumName])
+ .ToList(),
};
}
@@ -61,14 +64,30 @@ private static CliEnumDefinition Stabilize(
{
ValidateValues(definition);
+ var existingValues = ReadExistingValues(existingFile, fallbackExistingFile);
+ var stabilizedValues = PreserveExistingValues(definition, existingValues);
+ AppendNewValues(definition, existingValues, stabilizedValues);
+
+ ValidateUniqueMemberNames(definition.EnumName, stabilizedValues);
+ return definition with { Values = stabilizedValues };
+ }
+
+ private static IReadOnlyList ReadExistingValues(
+ string existingFile,
+ string? fallbackExistingFile)
+ {
var baselineFile = File.Exists(existingFile)
? existingFile
: fallbackExistingFile;
- var existingValues = baselineFile is not null && File.Exists(baselineFile)
+ return baselineFile is not null && File.Exists(baselineFile)
? ParseExistingValues(File.ReadAllText(baselineFile))
: [];
+ }
- var existingByCliValue = existingValues.ToDictionary(value => value.CliValue, StringComparer.Ordinal);
+ private static List PreserveExistingValues(
+ CliEnumDefinition definition,
+ IReadOnlyList existingValues)
+ {
var incomingByCliValue = definition.Values.ToDictionary(value => value.CliValue, StringComparer.Ordinal);
var stabilizedValues = new List(definition.Values.Count);
@@ -81,14 +100,43 @@ private static CliEnumDefinition Stabilize(
MemberName = existingValue.MemberName,
NumericValue = existingValue.NumericValue,
});
+ continue;
}
+
+ var reusedMember = definition.Values.FirstOrDefault(value => value.MemberName.Equals(
+ existingValue.MemberName,
+ StringComparison.Ordinal));
+ if (reusedMember is not null)
+ {
+ throw new InvalidOperationException(
+ $"Enum '{definition.EnumName}' member '{existingValue.MemberName}' changed CLI value from "
+ + $"'{existingValue.CliValue}' to '{reusedMember.CliValue}'.");
+ }
+
+ stabilizedValues.Add(new CliEnumValue
+ {
+ MemberName = existingValue.MemberName,
+ CliValue = existingValue.CliValue,
+ NumericValue = existingValue.NumericValue,
+ });
}
+ return stabilizedValues;
+ }
+
+ private static void AppendNewValues(
+ CliEnumDefinition definition,
+ IReadOnlyList existingValues,
+ List stabilizedValues)
+ {
+ var existingCliValues = existingValues
+ .Select(value => value.CliValue)
+ .ToHashSet(StringComparer.Ordinal);
var usedNumericValues = existingValues
.Select(value => value.NumericValue)
.ToHashSet();
var newValues = definition.Values
- .Where(value => !existingByCliValue.ContainsKey(value.CliValue))
+ .Where(value => !existingCliValues.Contains(value.CliValue))
.ToList();
var nextNumericValue = newValues.Count > 0 && usedNumericValues.Count > 0
? checked(usedNumericValues.Max() + 1)
@@ -109,9 +157,6 @@ private static CliEnumDefinition Stabilize(
nextNumericValue = checked(nextNumericValue + 1);
}
}
-
- ValidateUniqueMemberNames(definition.EnumName, stabilizedValues);
- return definition with { Values = stabilizedValues };
}
private static void ValidateValues(CliEnumDefinition definition)
@@ -125,6 +170,15 @@ private static void ValidateValues(CliEnumDefinition definition)
$"Enum '{definition.EnumName}' contains duplicate CLI value '{duplicateCliValue.Key}'.");
}
+ var duplicateMember = definition.Values
+ .GroupBy(value => value.MemberName, StringComparer.Ordinal)
+ .FirstOrDefault(group => group.Count() > 1);
+ if (duplicateMember is not null)
+ {
+ throw new InvalidOperationException(
+ $"Enum '{definition.EnumName}' contains duplicate member '{duplicateMember.Key}'.");
+ }
+
var suspiciousValue = definition.Values
.FirstOrDefault(value => SuspiciousProseValues.Contains(value.CliValue));
if (suspiciousValue is not null)
diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs
new file mode 100644
index 00000000000..c000e29a21a
--- /dev/null
+++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs
@@ -0,0 +1,1313 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using ModularPipelines.Attributes;
+using ModularPipelines.OptionsGenerator.Models;
+
+namespace ModularPipelines.OptionsGenerator.Generators;
+
+internal static class GeneratedApiCompatibilityPreserver
+{
+ private enum RequiredMemberRestoreResult
+ {
+ NotFound,
+ Restored,
+ Rejected,
+ }
+
+ public static CliToolDefinition Preserve(CliToolDefinition tool, string outputDirectory)
+ {
+ var optionsDirectory = Path.Combine(
+ outputDirectory,
+ tool.OutputDirectory,
+ "Options");
+ if (!Directory.Exists(optionsDirectory))
+ {
+ return tool;
+ }
+
+ var enumBaseline = ReadEnumBaseline(Path.Combine(
+ outputDirectory,
+ tool.OutputDirectory,
+ "Enums"))
+ .Where(pair => pair.Key.StartsWith(tool.NamespacePrefix, StringComparison.Ordinal))
+ .ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal);
+ MergeCurrentAliasEnumValues(tool, enumBaseline);
+ tool = tool with
+ {
+ CompatibilityEnums = tool.CompatibilityEnums
+ .Concat(enumBaseline.Values)
+ .DistinctBy(static definition => definition.EnumName)
+ .ToArray(),
+ };
+ var baseline = ReadBaseline(optionsDirectory);
+ var compatibleTool = baseline.TryGetValue($"{tool.NamespacePrefix}Options", out var globalBaseline)
+ ? PreserveGlobalOptions(tool, globalBaseline.Properties)
+ : tool;
+ var facadeMethods = ReadFacadeMethods(
+ Path.Combine(outputDirectory, tool.OutputDirectory, "Services"),
+ $"{tool.TargetNamespace}.Services",
+ tool.NamespacePrefix);
+ var executeFacadeOptionTypes = facadeMethods
+ .Where(static method => method.MethodName.Equals("ExecuteAsync", StringComparison.Ordinal))
+ .Select(static method => method.OptionsType)
+ .ToHashSet(StringComparer.Ordinal);
+ var namedFacadeOptionTypes = facadeMethods
+ .Where(static method => !method.MethodName.Equals("ExecuteAsync", StringComparison.Ordinal))
+ .Select(static method => method.OptionsType)
+ .ToHashSet(StringComparer.Ordinal);
+ var optionalFacadeOptionTypes = facadeMethods
+ .Where(static method => method.IsOptionsOptional)
+ .Select(static method => method.OptionsType)
+ .ToHashSet(StringComparer.Ordinal);
+ var preservedTool = compatibleTool with
+ {
+ Commands = compatibleTool.Commands
+ .Select(command => baseline.TryGetValue(command.ClassName, out var commandBaseline)
+ ? Preserve(command, commandBaseline.Properties, commandBaseline.Constructors)
+ : command)
+ .Select(command => PreserveAliasCompatibility(
+ compatibleTool,
+ command,
+ baseline,
+ enumBaseline))
+ .Select(command => executeFacadeOptionTypes.Contains(command.ClassName)
+ ? command with { PreserveExecuteFacade = true }
+ : command)
+ .Select(command => namedFacadeOptionTypes.Contains(command.ClassName)
+ ? command with { PreserveNamedFacade = true }
+ : command)
+ .Select(command => optionalFacadeOptionTypes.Contains(command.ClassName)
+ ? command with { PreserveOptionalOptionsParameter = true }
+ : command)
+ .ToArray(),
+ };
+ RejectRemovedFacadeMethods(preservedTool, facadeMethods);
+ return preservedTool;
+ }
+
+ private static CliCommandDefinition PreserveAliasCompatibility(
+ CliToolDefinition tool,
+ CliCommandDefinition command,
+ IReadOnlyDictionary baseline,
+ IReadOnlyDictionary enumBaseline)
+ {
+ if (command.CommandParts.Length == 0)
+ {
+ return command;
+ }
+
+ var constructorsByAlias = command.AliasCompatibilityConstructors
+ .ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal);
+ var propertiesByAlias = command.AliasCompatibilityProperties
+ .ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal);
+ foreach (var alias in tool.CommandGroupAliases.Where(alias =>
+ command.CommandParts[0].Equals(
+ alias.CanonicalCommand,
+ StringComparison.OrdinalIgnoreCase)))
+ {
+ var aliasClassName = GeneratorUtils.GetAliasedClassName(tool, alias, command.ClassName);
+ if (!baseline.TryGetValue(aliasClassName, out var aliasBaseline))
+ {
+ continue;
+ }
+
+ PreserveAliasConstructors(
+ tool,
+ command,
+ alias,
+ aliasClassName,
+ aliasBaseline,
+ constructorsByAlias);
+ PreserveAliasProperties(
+ tool,
+ command,
+ alias,
+ aliasClassName,
+ aliasBaseline,
+ baseline,
+ enumBaseline,
+ propertiesByAlias);
+ }
+
+ return command with
+ {
+ AliasCompatibilityConstructors = constructorsByAlias,
+ AliasCompatibilityProperties = propertiesByAlias,
+ };
+ }
+
+ private static void PreserveAliasConstructors(
+ CliToolDefinition tool,
+ CliCommandDefinition command,
+ CliCommandGroupAlias alias,
+ string aliasClassName,
+ GeneratedApiBaseline aliasBaseline,
+ Dictionary> constructorsByAlias)
+ {
+ var compatibilityConstructors = constructorsByAlias
+ .GetValueOrDefault(aliasClassName, [])
+ .ToList();
+ var currentRequired = GeneratorUtils.GetRequiredConstructorParameters(command)
+ .Select(parameter => new GeneratedApiProperty(
+ parameter.PropertyName,
+ GeneratorUtils.GetAliasedRequiredConstructorParameterType(parameter, tool, alias),
+ null,
+ null,
+ true,
+ false,
+ null,
+ null))
+ .ToArray();
+ PreserveCompatibilityConstructors(
+ aliasBaseline.Properties,
+ aliasBaseline.Constructors,
+ currentRequired,
+ compatibilityConstructors);
+ SetCompatibilityEntries(constructorsByAlias, aliasClassName, compatibilityConstructors);
+ }
+
+ private static void PreserveAliasProperties(
+ CliToolDefinition tool,
+ CliCommandDefinition command,
+ CliCommandGroupAlias alias,
+ string aliasClassName,
+ GeneratedApiBaseline aliasBaseline,
+ IReadOnlyDictionary baseline,
+ IReadOnlyDictionary enumBaseline,
+ Dictionary> propertiesByAlias)
+ {
+ var currentAliasProperties = command.Options
+ .Where(static option => option.EnumDefinition is not null
+ && option.ValueArity != CliOptionValueArity.Optional)
+ .Select(option => (
+ option.PropertyName,
+ option.CSharpType.Replace(
+ option.EnumDefinition!.EnumName,
+ GeneratorUtils.GetAliasedClassName(
+ tool,
+ alias,
+ option.EnumDefinition.EnumName),
+ StringComparison.Ordinal)))
+ .ToHashSet();
+ var compatibilityProperties = propertiesByAlias
+ .GetValueOrDefault(aliasClassName, [])
+ .ToList();
+ var canonicalProperties = baseline.GetValueOrDefault(command.ClassName)?.Properties ?? [];
+
+ foreach (var baselineProperty in aliasBaseline.Properties)
+ {
+ var compatibilityProperty = CreateAliasCompatibilityProperty(
+ baselineProperty,
+ canonicalProperties,
+ enumBaseline,
+ currentAliasProperties,
+ compatibilityProperties);
+ if (compatibilityProperty is not null)
+ {
+ compatibilityProperties.Add(compatibilityProperty);
+ }
+ }
+
+ SetCompatibilityEntries(propertiesByAlias, aliasClassName, compatibilityProperties);
+ }
+
+ private static CliAliasCompatibilityProperty? CreateAliasCompatibilityProperty(
+ GeneratedApiProperty baselineProperty,
+ IReadOnlyList canonicalProperties,
+ IReadOnlyDictionary enumBaseline,
+ IReadOnlySet<(string PropertyName, string CSharpType)> currentAliasProperties,
+ IReadOnlyCollection compatibilityProperties)
+ {
+ var aliasEnumName = GeneratorUtils.GetEnumTypeName(baselineProperty.CSharpType);
+ if (!enumBaseline.ContainsKey(aliasEnumName)
+ || currentAliasProperties.Contains((baselineProperty.PropertyName, baselineProperty.CSharpType))
+ || compatibilityProperties.Any(existing => existing.PropertyName.Equals(
+ baselineProperty.PropertyName,
+ StringComparison.Ordinal)))
+ {
+ return null;
+ }
+
+ var canonicalProperty = canonicalProperties.FirstOrDefault(property =>
+ property.PropertyName.Equals(baselineProperty.PropertyName, StringComparison.Ordinal));
+ if (canonicalProperty is null
+ || !enumBaseline.ContainsKey(GeneratorUtils.GetEnumTypeName(canonicalProperty.CSharpType)))
+ {
+ return null;
+ }
+
+ return new CliAliasCompatibilityProperty
+ {
+ PropertyName = baselineProperty.PropertyName,
+ AliasCSharpType = baselineProperty.CSharpType,
+ CanonicalCSharpType = canonicalProperty.CSharpType,
+ ObsoleteMessage = baselineProperty.ObsoleteMessage
+ ?? $"{baselineProperty.PropertyName} is retained for compatibility.",
+ };
+ }
+
+ private static void SetCompatibilityEntries(
+ Dictionary> entriesByAlias,
+ string aliasClassName,
+ IReadOnlyList entries)
+ {
+ if (entries.Count == 0)
+ {
+ entriesByAlias.Remove(aliasClassName);
+ return;
+ }
+
+ entriesByAlias[aliasClassName] = entries;
+ }
+
+ internal static CliToolDefinition PreserveGlobalOptions(
+ CliToolDefinition tool,
+ IReadOnlyList baselineProperties)
+ {
+ var globalClassName = $"{tool.NamespacePrefix}Options";
+ var preserved = Preserve(
+ new CliCommandDefinition
+ {
+ FullCommand = tool.ToolName,
+ CommandParts = [],
+ ClassName = globalClassName,
+ ParentClassName = "CommandLineToolOptions",
+ ToolNamespacePrefix = tool.NamespacePrefix,
+ Options = tool.GetGlobalOptions(),
+ CompatibilityProperties = tool.GlobalCompatibilityProperties,
+ },
+ baselineProperties);
+
+ return tool with
+ {
+ GlobalOptions = preserved.Options,
+ SupplementalGlobalOptions = [],
+ GlobalCompatibilityProperties = preserved.CompatibilityProperties,
+ };
+ }
+
+ internal static CliCommandDefinition Preserve(
+ CliCommandDefinition command,
+ IReadOnlyList baselineProperties) =>
+ Preserve(command, baselineProperties, []);
+
+ private static CliCommandDefinition Preserve(
+ CliCommandDefinition command,
+ IReadOnlyList baselineProperties,
+ IReadOnlyList baselineConstructors)
+ {
+ var compatibilityProperties = command.CompatibilityProperties.ToList();
+ var compatibilityConstructors = command.CompatibilityConstructors.ToList();
+ var positionalArguments = command.PositionalArguments.ToArray();
+ var options = command.Options.ToArray();
+ var violations = new List();
+ var renamedProperties = new Dictionary(StringComparer.Ordinal);
+
+ RestoreRequiredMemberNames(
+ baselineProperties,
+ positionalArguments,
+ options,
+ compatibilityProperties,
+ renamedProperties,
+ violations);
+ RestoreRequiredMemberOrder(baselineProperties, positionalArguments, options);
+
+ var currentProperties = GetCurrentProperties(positionalArguments, options);
+ foreach (var baseline in baselineProperties)
+ {
+ PreserveBaselineProperty(
+ command,
+ baseline,
+ currentProperties,
+ compatibilityProperties,
+ violations);
+ }
+
+ if (violations.Count > 0)
+ {
+ throw new InvalidOperationException(
+ $"Generated API compatibility validation failed for {command.FullCommand}:"
+ + Environment.NewLine
+ + string.Join(Environment.NewLine, violations.Select(violation => $"- {violation}")));
+ }
+
+ PreserveCompatibilityConstructors(
+ baselineProperties,
+ baselineConstructors,
+ positionalArguments,
+ options,
+ compatibilityConstructors);
+
+ return command with
+ {
+ Options = options,
+ PositionalArguments = positionalArguments,
+ CompatibilityProperties = compatibilityProperties,
+ CompatibilityConstructors = compatibilityConstructors,
+ DocumentationExampleValues = RenameDocumentationExampleValues(
+ command.DocumentationExampleValues,
+ renamedProperties),
+ };
+ }
+
+ private static void PreserveBaselineProperty(
+ CliCommandDefinition command,
+ GeneratedApiProperty baseline,
+ IReadOnlyList currentProperties,
+ ICollection compatibilityProperties,
+ List violations)
+ {
+ var sameName = currentProperties.FirstOrDefault(property =>
+ property.PropertyName.Equals(baseline.PropertyName, StringComparison.Ordinal));
+ if (sameName is not null)
+ {
+ ValidateMatchingProperty(command, baseline, sameName, violations);
+ return;
+ }
+
+ if (baseline.IsCompatibility)
+ {
+ PreserveCompatibilityProperty(
+ command,
+ baseline,
+ compatibilityProperties,
+ violations);
+ return;
+ }
+
+ var replacement = currentProperties.FirstOrDefault(property =>
+ HasSameCliIdentity(property, baseline));
+ if (TryRecordRemovedPropertyViolation(command, baseline, replacement, violations))
+ {
+ return;
+ }
+
+ AddCompatibilityProperty(
+ compatibilityProperties,
+ new CliCompatibilityProperty
+ {
+ PropertyName = baseline.PropertyName,
+ CSharpType = baseline.CSharpType,
+ ForwardToPropertyName = replacement?.PropertyName,
+ ObsoleteMessage = replacement is null
+ ? $"{baseline.PropertyName} is no longer supported by the installed CLI and has no effect."
+ : $"Use {replacement.PropertyName} instead.",
+ });
+ }
+
+ private static bool TryRecordRemovedPropertyViolation(
+ CliCommandDefinition command,
+ GeneratedApiProperty baseline,
+ GeneratedApiProperty? replacement,
+ ICollection violations)
+ {
+ if (replacement is not null
+ && !replacement.CSharpType.Equals(baseline.CSharpType, StringComparison.Ordinal))
+ {
+ violations.Add(
+ $"{command.ClassName}.{baseline.PropertyName} changed type from "
+ + $"{baseline.CSharpType} to {replacement.CSharpType} "
+ + $"while being renamed to {replacement.PropertyName}");
+ return true;
+ }
+
+ if (baseline.ArgumentPosition is not null && replacement is null)
+ {
+ violations.Add(
+ $"{command.ClassName}.{baseline.PropertyName} positional argument was removed");
+ return true;
+ }
+
+ if (baseline.IsRequired && replacement is null)
+ {
+ violations.Add(
+ $"{command.ClassName}.{baseline.PropertyName} was removed from the required constructor");
+ return true;
+ }
+
+ return false;
+ }
+
+ private static void PreserveCompatibilityProperty(
+ CliCommandDefinition command,
+ GeneratedApiProperty baseline,
+ ICollection compatibilityProperties,
+ ICollection violations)
+ {
+ var supplied = compatibilityProperties.FirstOrDefault(property =>
+ property.PropertyName.Equals(baseline.PropertyName, StringComparison.Ordinal));
+ if (supplied is null)
+ {
+ AddCompatibilityProperty(
+ compatibilityProperties,
+ new CliCompatibilityProperty
+ {
+ PropertyName = baseline.PropertyName,
+ CSharpType = baseline.CSharpType,
+ ForwardToPropertyName = baseline.ForwardToPropertyName,
+ UseInitAccessor = baseline.UseInitAccessor,
+ ObsoleteMessage = baseline.ObsoleteMessage
+ ?? $"{baseline.PropertyName} is retained for compatibility.",
+ });
+ return;
+ }
+
+ if (!supplied.CSharpType.Equals(baseline.CSharpType, StringComparison.Ordinal))
+ {
+ violations.Add(
+ $"{command.ClassName}.{baseline.PropertyName} compatibility property changed type from "
+ + $"{baseline.CSharpType} to {supplied.CSharpType}");
+ }
+ else if (!string.Equals(
+ supplied.ForwardToPropertyName,
+ baseline.ForwardToPropertyName,
+ StringComparison.Ordinal))
+ {
+ violations.Add(
+ $"{command.ClassName}.{baseline.PropertyName} compatibility property changed forwarding target from "
+ + $"{baseline.ForwardToPropertyName ?? ""} to {supplied.ForwardToPropertyName ?? ""}");
+ }
+ else if (supplied.UseInitAccessor != baseline.UseInitAccessor)
+ {
+ violations.Add(
+ $"{command.ClassName}.{baseline.PropertyName} compatibility property changed accessor from "
+ + $"{(baseline.UseInitAccessor ? "init" : "set")} to {(supplied.UseInitAccessor ? "init" : "set")}");
+ }
+ }
+
+ private static void ValidateMatchingProperty(
+ CliCommandDefinition command,
+ GeneratedApiProperty baseline,
+ GeneratedApiProperty current,
+ List violations)
+ {
+ if (!current.CSharpType.Equals(baseline.CSharpType, StringComparison.Ordinal))
+ {
+ violations.Add(
+ $"{command.ClassName}.{baseline.PropertyName} changed type from "
+ + $"{baseline.CSharpType} to {current.CSharpType}");
+ }
+ else if (baseline.IsRequired && !current.IsRequired)
+ {
+ violations.Add(
+ $"{command.ClassName}.{baseline.PropertyName} changed from required to optional");
+ }
+ else if (!baseline.IsRequired && current.IsRequired)
+ {
+ violations.Add(
+ $"{command.ClassName}.{baseline.PropertyName} changed from optional to required "
+ + "and would remove its public setter");
+ }
+ else if (!HasSameCliIdentity(current, baseline))
+ {
+ violations.Add(
+ $"{command.ClassName}.{baseline.PropertyName} changed CLI switch or argument position");
+ }
+ }
+
+ private static void RestoreRequiredMemberNames(
+ IReadOnlyList baselineProperties,
+ CliPositionalArgument[] positionalArguments,
+ CliOptionDefinition[] options,
+ ICollection compatibilityProperties,
+ IDictionary renamedProperties,
+ ICollection violations)
+ {
+ foreach (var baseline in baselineProperties.Where(property =>
+ property.IsRequired && !property.IsCompatibility))
+ {
+ var propertyNames = positionalArguments.Select(argument => argument.PropertyName)
+ .Concat(options.Select(option => option.PropertyName));
+ var positionalResult = TryRestoreRequiredMember(
+ baseline,
+ positionalArguments,
+ propertyNames,
+ ToGeneratedProperty,
+ static argument => argument.CSharpType,
+ argument => argument with
+ {
+ PropertyName = baseline.PropertyName,
+ CSharpType = baseline.CSharpType,
+ IsRequired = true,
+ },
+ violations,
+ out var currentName);
+ if (positionalResult != RequiredMemberRestoreResult.NotFound)
+ {
+ if (positionalResult == RequiredMemberRestoreResult.Restored)
+ {
+ RecordRequiredMemberRename(
+ compatibilityProperties,
+ renamedProperties,
+ currentName,
+ baseline);
+ }
+
+ continue;
+ }
+
+ var optionResult = TryRestoreRequiredMember(
+ baseline,
+ options,
+ propertyNames,
+ ToGeneratedProperty,
+ static option => option.PropertyType,
+ option => option with
+ {
+ PropertyName = baseline.PropertyName,
+ CSharpType = baseline.CSharpType,
+ IsRequired = true,
+ },
+ violations,
+ out currentName);
+ if (optionResult == RequiredMemberRestoreResult.Restored)
+ {
+ RecordRequiredMemberRename(
+ compatibilityProperties,
+ renamedProperties,
+ currentName,
+ baseline);
+ }
+ }
+ }
+
+ private static RequiredMemberRestoreResult TryRestoreRequiredMember(
+ GeneratedApiProperty baseline,
+ T[] members,
+ IEnumerable propertyNames,
+ Func toGeneratedProperty,
+ Func getEmittedType,
+ Func restore,
+ ICollection violations,
+ out string currentName)
+ {
+ var index = Array.FindIndex(members, current =>
+ HasSameCliIdentity(toGeneratedProperty(current), baseline)
+ && getEmittedType(current).TrimEnd('?').Equals(
+ baseline.CSharpType.TrimEnd('?'),
+ StringComparison.Ordinal));
+ if (index < 0)
+ {
+ currentName = string.Empty;
+ return RequiredMemberRestoreResult.NotFound;
+ }
+
+ currentName = toGeneratedProperty(members[index]).PropertyName;
+ if (!CanRestoreName(
+ baseline.PropertyName,
+ currentName,
+ propertyNames,
+ violations))
+ {
+ return RequiredMemberRestoreResult.Rejected;
+ }
+
+ members[index] = restore(members[index]);
+ return RequiredMemberRestoreResult.Restored;
+ }
+
+ private static bool CanRestoreName(
+ string baselineName,
+ string currentName,
+ IEnumerable propertyNames,
+ ICollection violations)
+ {
+ if (baselineName.Equals(currentName, StringComparison.Ordinal))
+ {
+ return true;
+ }
+
+ if (!propertyNames.Any(name => name.Equals(baselineName, StringComparison.Ordinal)))
+ {
+ return true;
+ }
+
+ violations.Add($"restoring required member {currentName} to {baselineName} would duplicate a member name");
+ return false;
+ }
+
+ private static void RecordRequiredMemberRename(
+ ICollection compatibilityProperties,
+ IDictionary renamedProperties,
+ string currentName,
+ GeneratedApiProperty baseline)
+ {
+ if (currentName.Equals(baseline.PropertyName, StringComparison.Ordinal))
+ {
+ return;
+ }
+
+ renamedProperties[currentName] = baseline.PropertyName;
+ AddRenamedCurrentProperty(
+ compatibilityProperties,
+ currentName,
+ baseline.CSharpType,
+ baseline.PropertyName);
+ }
+
+ private static void AddRenamedCurrentProperty(
+ ICollection compatibilityProperties,
+ string propertyName,
+ string cSharpType,
+ string forwardToPropertyName) =>
+ AddCompatibilityProperty(
+ compatibilityProperties,
+ new CliCompatibilityProperty
+ {
+ PropertyName = propertyName,
+ CSharpType = cSharpType,
+ ForwardToPropertyName = forwardToPropertyName,
+ UseInitAccessor = true,
+ ObsoleteMessage = $"Use {forwardToPropertyName} instead.",
+ });
+
+ private static void RestoreRequiredMemberOrder(
+ IReadOnlyList baselineProperties,
+ CliPositionalArgument[] positionalArguments,
+ CliOptionDefinition[] options)
+ {
+ var baselineOrder = baselineProperties
+ .Where(static property => property.IsRequired && !property.IsCompatibility)
+ .Select((property, index) => (property.PropertyName, index))
+ .ToDictionary(pair => pair.PropertyName, pair => pair.index, StringComparer.Ordinal);
+ RestoreRequiredMemberOrder(
+ positionalArguments,
+ static argument => argument.IsRequired,
+ static argument => argument.PropertyName,
+ baselineOrder);
+ RestoreRequiredMemberOrder(
+ options,
+ static option => option.IsRequired,
+ static option => option.PropertyName,
+ baselineOrder);
+ }
+
+ private static void RestoreRequiredMemberOrder(
+ T[] members,
+ Func isRequired,
+ Func getPropertyName,
+ IReadOnlyDictionary baselineOrder)
+ {
+ var orderedRequired = members
+ .Where(isRequired)
+ .OrderBy(member => baselineOrder.GetValueOrDefault(getPropertyName(member), int.MaxValue))
+ .ToArray();
+ var requiredIndex = 0;
+ for (var index = 0; index < members.Length; index++)
+ {
+ if (isRequired(members[index]))
+ {
+ members[index] = orderedRequired[requiredIndex++];
+ }
+ }
+ }
+
+ private static void AddCompatibilityProperty(
+ ICollection compatibilityProperties,
+ CliCompatibilityProperty property)
+ {
+ if (compatibilityProperties.Any(existing =>
+ existing.PropertyName.Equals(property.PropertyName, StringComparison.Ordinal)))
+ {
+ return;
+ }
+
+ compatibilityProperties.Add(property);
+ }
+
+ private static void PreserveCompatibilityConstructors(
+ IReadOnlyList baselineProperties,
+ IReadOnlyList baselineConstructors,
+ IReadOnlyList positionalArguments,
+ IReadOnlyList options,
+ List compatibilityConstructors) =>
+ PreserveCompatibilityConstructors(
+ baselineProperties,
+ baselineConstructors,
+ GetCurrentProperties(positionalArguments, options)
+ .Where(static property => property.IsRequired)
+ .ToArray(),
+ compatibilityConstructors);
+
+ private static void PreserveCompatibilityConstructors(
+ IReadOnlyList baselineProperties,
+ IReadOnlyList baselineConstructors,
+ IReadOnlyList currentRequired,
+ List compatibilityConstructors)
+ {
+ if (currentRequired.Count == 0)
+ {
+ compatibilityConstructors.Clear();
+ return;
+ }
+
+ var baselineRequired = baselineProperties
+ .Where(static property => property.IsRequired && !property.IsCompatibility)
+ .ToArray();
+ var addedRequired = currentRequired
+ .Where(current => !baselineRequired.Any(baseline =>
+ baseline.PropertyName.Equals(current.PropertyName, StringComparison.Ordinal)
+ && baseline.CSharpType.Equals(current.CSharpType, StringComparison.Ordinal)))
+ .Select(static property => property.PropertyName)
+ .ToArray();
+ if (currentRequired.Count > baselineRequired.Length && addedRequired.Length > 0)
+ {
+ throw new InvalidOperationException(
+ "Cannot retain generated constructors because newly required member(s) "
+ + $"{string.Join(", ", addedRequired)} have no baseline value.");
+ }
+
+ foreach (var constructor in baselineConstructors)
+ {
+ AddCompatibilityConstructor(compatibilityConstructors, constructor, currentRequired);
+ }
+
+ if (HasSameConstructorContract(baselineRequired, currentRequired))
+ {
+ return;
+ }
+
+ var baselineParameters = baselineRequired
+ .Select(property => new CliCompatibilityConstructorParameter(
+ property.PropertyName,
+ property.CSharpType))
+ .ToArray();
+ var primaryArguments = currentRequired
+ .Select(current => baselineRequired.Any(baseline =>
+ baseline.PropertyName.Equals(current.PropertyName, StringComparison.Ordinal)
+ && baseline.CSharpType.Equals(current.CSharpType, StringComparison.Ordinal))
+ ? current.PropertyName
+ : "default!")
+ .ToArray();
+ AddCompatibilityConstructor(
+ compatibilityConstructors,
+ new CliCompatibilityConstructor
+ {
+ Parameters = baselineParameters,
+ PrimaryConstructorArguments = primaryArguments,
+ PreserveDeconstruct = baselineParameters.Length > 0,
+ },
+ currentRequired);
+ }
+
+ private static void AddCompatibilityConstructor(
+ ICollection constructors,
+ CliCompatibilityConstructor constructor,
+ IReadOnlyList currentRequired)
+ {
+ if (HasSameConstructorSignature(constructor.Parameters, currentRequired))
+ {
+ return;
+ }
+
+ var existing = constructors.FirstOrDefault(candidate => HasSameConstructorSignature(
+ candidate.Parameters,
+ constructor.Parameters));
+ if (existing is not null)
+ {
+ if (constructor.PreserveDeconstruct && !existing.PreserveDeconstruct)
+ {
+ constructors.Remove(existing);
+ constructors.Add(existing with { PreserveDeconstruct = true });
+ }
+
+ return;
+ }
+
+ constructors.Add(constructor);
+ }
+
+ private static bool HasSameConstructorContract(
+ IReadOnlyList left,
+ IReadOnlyList right)
+ where TLeft : notnull
+ where TRight : notnull
+ {
+ if (left.Count != right.Count)
+ {
+ return false;
+ }
+
+ return left.Select(GetConstructorParameterContract)
+ .SequenceEqual(right.Select(GetConstructorParameterContract));
+ }
+
+ private static bool HasSameConstructorSignature(
+ IReadOnlyList left,
+ IReadOnlyList right)
+ where TLeft : notnull
+ where TRight : notnull
+ {
+ if (left.Count != right.Count)
+ {
+ return false;
+ }
+
+ return left.Select(GetConstructorParameterSignatureType)
+ .SequenceEqual(right.Select(GetConstructorParameterSignatureType), StringComparer.Ordinal);
+ }
+
+ private static string GetConstructorParameterSignatureType(T parameter) =>
+ GetConstructorParameterType(parameter).TrimEnd('?');
+
+ private static string GetConstructorParameterType(T parameter) => parameter switch
+ {
+ GeneratedApiProperty property => property.CSharpType,
+ CliCompatibilityConstructorParameter compatibilityParameter => compatibilityParameter.CSharpType,
+ _ => throw new ArgumentOutOfRangeException(nameof(parameter)),
+ };
+
+ private static (string PropertyName, string CSharpType) GetConstructorParameterContract(T parameter) =>
+ parameter switch
+ {
+ GeneratedApiProperty property => (property.PropertyName, property.CSharpType),
+ CliCompatibilityConstructorParameter compatibilityParameter =>
+ (compatibilityParameter.PropertyName, compatibilityParameter.CSharpType),
+ _ => throw new ArgumentOutOfRangeException(nameof(parameter)),
+ };
+
+ private static IReadOnlyDictionary RenameDocumentationExampleValues(
+ IReadOnlyDictionary values,
+ Dictionary renamedProperties)
+ {
+ if (renamedProperties.Count == 0)
+ {
+ return values;
+ }
+
+ return values.ToDictionary(
+ pair => renamedProperties.GetValueOrDefault(pair.Key, pair.Key),
+ pair => pair.Value,
+ StringComparer.Ordinal);
+ }
+
+ private static GeneratedApiProperty[] GetCurrentProperties(
+ IEnumerable positionalArguments,
+ IEnumerable options) =>
+ options.Select(ToGeneratedProperty)
+ .Concat(positionalArguments.Select(ToGeneratedProperty))
+ .ToArray();
+
+ private static GeneratedApiProperty ToGeneratedProperty(CliPositionalArgument argument) =>
+ new(
+ argument.PropertyName,
+ argument.IsRequired ? argument.CSharpType.TrimEnd('?') : argument.CSharpType,
+ null,
+ argument.PositionIndex,
+ argument.IsRequired,
+ false,
+ null,
+ null);
+
+ private static GeneratedApiProperty ToGeneratedProperty(CliOptionDefinition option) =>
+ new(
+ option.PropertyName,
+ option.IsRequired ? option.PropertyType.TrimEnd('?') : option.PropertyType,
+ option.SwitchName,
+ null,
+ option.IsRequired,
+ false,
+ null,
+ null);
+
+ private static bool HasSameCliIdentity(
+ GeneratedApiProperty left,
+ GeneratedApiProperty right)
+ {
+ if (left.ArgumentPosition is not null || right.ArgumentPosition is not null)
+ {
+ return left.ArgumentPosition == right.ArgumentPosition;
+ }
+
+ if (left.SwitchName is not null || right.SwitchName is not null)
+ {
+ return left.SwitchName?.Equals(right.SwitchName, StringComparison.Ordinal) == true;
+ }
+
+ return true;
+ }
+
+ private static Dictionary ReadBaseline(
+ string optionsDirectory)
+ {
+ var baseline = new Dictionary(StringComparer.Ordinal);
+ foreach (var path in Directory.EnumerateFiles(
+ optionsDirectory,
+ "*.Generated.cs",
+ SearchOption.TopDirectoryOnly))
+ {
+ var root = CSharpSyntaxTree.ParseText(File.ReadAllText(path)).GetRoot();
+ foreach (var declaration in root.DescendantNodes().OfType())
+ {
+ baseline[declaration.Identifier.ValueText] = new GeneratedApiBaseline(
+ ReadProperties(declaration),
+ ReadCompatibilityConstructors(declaration));
+ }
+ }
+
+ return baseline;
+ }
+
+ private static Dictionary ReadEnumBaseline(string enumsDirectory)
+ {
+ var baseline = new Dictionary(StringComparer.Ordinal);
+ if (!Directory.Exists(enumsDirectory))
+ {
+ return baseline;
+ }
+
+ foreach (var path in Directory.EnumerateFiles(
+ enumsDirectory,
+ "*.Generated.cs",
+ SearchOption.TopDirectoryOnly))
+ {
+ var root = CSharpSyntaxTree.ParseText(File.ReadAllText(path)).GetRoot();
+ foreach (var declaration in root.DescendantNodes().OfType())
+ {
+ var nextNumericValue = 0;
+ var values = new List(declaration.Members.Count);
+ foreach (var member in declaration.Members)
+ {
+ var attributes = member.AttributeLists.SelectMany(static list => list.Attributes);
+ var cliValueAttribute = FindAttribute(attributes, "EnumValue")
+ ?? FindAttribute(attributes, "Description");
+ var numericValue = GetEnumNumericValue(member.EqualsValue?.Value) ?? nextNumericValue;
+ values.Add(new CliEnumValue
+ {
+ MemberName = member.Identifier.ValueText,
+ CliValue = GetStringArgument(cliValueAttribute) ?? member.Identifier.ValueText,
+ NumericValue = numericValue,
+ });
+ nextNumericValue = checked(numericValue + 1);
+ }
+
+ baseline[declaration.Identifier.ValueText] = new CliEnumDefinition
+ {
+ EnumName = declaration.Identifier.ValueText,
+ Values = values,
+ };
+ }
+ }
+
+ return baseline;
+ }
+
+ private static void MergeCurrentAliasEnumValues(
+ CliToolDefinition tool,
+ IDictionary enumBaseline)
+ {
+ var currentAliasEnums = tool.CommandGroupAliases
+ .SelectMany(alias => tool.Commands
+ .Where(command => command.CommandParts.Length > 0
+ && command.CommandParts[0].Equals(
+ alias.CanonicalCommand,
+ StringComparison.OrdinalIgnoreCase))
+ .SelectMany(command => command.Options
+ .Where(static option => option.EnumDefinition is not null)
+ .Select(option => option.EnumDefinition! with
+ {
+ EnumName = GeneratorUtils.GetAliasedClassName(
+ tool,
+ alias,
+ option.EnumDefinition!.EnumName),
+ })))
+ .GroupBy(static definition => definition.EnumName, StringComparer.Ordinal)
+ .ToDictionary(
+ static group => group.Key,
+ static group => group.SelectMany(static definition => definition.Values)
+ .DistinctBy(static value => value.CliValue, StringComparer.Ordinal)
+ .ToArray(),
+ StringComparer.Ordinal);
+
+ foreach (var pair in enumBaseline.ToArray())
+ {
+ if (!currentAliasEnums.TryGetValue(pair.Key, out var currentValues))
+ {
+ continue;
+ }
+
+ enumBaseline[pair.Key] = pair.Value with
+ {
+ Values = pair.Value.Values
+ .Concat(currentValues)
+ .DistinctBy(static value => value.CliValue, StringComparer.Ordinal)
+ .ToArray(),
+ };
+ }
+ }
+
+ private static int? GetEnumNumericValue(ExpressionSyntax? expression) => expression switch
+ {
+ LiteralExpressionSyntax literal when literal.Token.Value is int value => value,
+ PrefixUnaryExpressionSyntax prefix when prefix.IsKind(SyntaxKind.UnaryMinusExpression)
+ && prefix.Operand is LiteralExpressionSyntax literal
+ && literal.Token.Value is int value => -value,
+ _ => null,
+ };
+
+ private static CliCompatibilityConstructor[] ReadCompatibilityConstructors(
+ RecordDeclarationSyntax declaration) =>
+ declaration.Members
+ .OfType()
+ .Where(constructor => constructor.Modifiers.Any(SyntaxKind.PublicKeyword))
+ .Where(constructor => constructor.Initializer?.IsKind(
+ SyntaxKind.ThisConstructorInitializer) == true)
+ .Select(constructor => ReadCompatibilityConstructor(declaration, constructor))
+ .ToArray();
+
+ private static CliCompatibilityConstructor ReadCompatibilityConstructor(
+ RecordDeclarationSyntax declaration,
+ ConstructorDeclarationSyntax constructor)
+ {
+ var parameters = constructor.ParameterList.Parameters
+ .Select(parameter => new CliCompatibilityConstructorParameter(
+ parameter.Identifier.ValueText,
+ parameter.Type?.ToString() ?? string.Empty))
+ .ToArray();
+ return new CliCompatibilityConstructor
+ {
+ Parameters = parameters,
+ PrimaryConstructorArguments = constructor.Initializer!.ArgumentList.Arguments
+ .Select(argument => argument.Expression.ToString())
+ .ToArray(),
+ PreserveDeconstruct = HasMatchingDeconstruct(declaration, parameters),
+ };
+ }
+
+ private static bool HasMatchingDeconstruct(
+ RecordDeclarationSyntax declaration,
+ IReadOnlyList parameters) =>
+ declaration.Members
+ .OfType()
+ .Where(method => method.Identifier.ValueText.Equals("Deconstruct", StringComparison.Ordinal))
+ .Where(method => method.Modifiers.Any(SyntaxKind.PublicKeyword))
+ .Any(method => method.ParameterList.Parameters.Count == parameters.Count
+ && method.ParameterList.Parameters
+ .Zip(parameters)
+ .All(pair => pair.First.Modifiers.Any(SyntaxKind.OutKeyword)
+ && pair.First.Identifier.ValueText.Equals(
+ pair.Second.PropertyName,
+ StringComparison.Ordinal)
+ && (pair.First.Type?.ToString() ?? string.Empty).Equals(
+ pair.Second.CSharpType,
+ StringComparison.Ordinal)));
+
+ private static void RejectRemovedFacadeMethods(
+ CliToolDefinition tool,
+ IReadOnlyList baselineFacadeMethods)
+ {
+ var currentFacadeMethods = GenerateFacadeMethods(tool).ToHashSet();
+ var removedMethods = baselineFacadeMethods
+ .Where(method => !currentFacadeMethods.Contains(method))
+ .Distinct()
+ .OrderBy(static method => method.DeclaringType, StringComparer.Ordinal)
+ .ThenBy(static method => method.MethodName, StringComparer.Ordinal)
+ .ThenBy(static method => method.OptionsType, StringComparer.Ordinal)
+ .ToArray();
+ if (removedMethods.Length == 0)
+ {
+ return;
+ }
+
+ throw new InvalidOperationException(
+ $"Generated API compatibility validation failed for {tool.ToolName}:"
+ + Environment.NewLine
+ + string.Join(
+ Environment.NewLine,
+ removedMethods.Select(method =>
+ $"- {method.DeclaringType}.{method.MethodName}({method.OptionsType}): "
+ + $"{method.OptionsType} command disappeared from generated facade")));
+ }
+
+ private static IReadOnlyList GenerateFacadeMethods(CliToolDefinition tool)
+ {
+ var generatedFiles = new List();
+ generatedFiles.AddRange(
+ new ServiceInterfaceGenerator().GenerateAsync(tool).GetAwaiter().GetResult());
+ generatedFiles.AddRange(
+ new ServiceImplementationGenerator().GenerateAsync(tool).GetAwaiter().GetResult());
+ generatedFiles.AddRange(
+ new SubDomainClassGenerator().GenerateAsync(tool).GetAwaiter().GetResult());
+ return ReadFacadeMethods(
+ generatedFiles.Select(static file => file.Content),
+ $"{tool.TargetNamespace}.Services");
+ }
+
+ private static IReadOnlyList ReadFacadeMethods(
+ string servicesDirectory,
+ string targetNamespace,
+ string namespacePrefix)
+ {
+ if (!Directory.Exists(servicesDirectory))
+ {
+ return [];
+ }
+
+ var sources = Directory.EnumerateFiles(
+ servicesDirectory,
+ $"{namespacePrefix}*.Generated.cs",
+ SearchOption.TopDirectoryOnly)
+ .Select(File.ReadAllText);
+ return ReadFacadeMethods(sources, targetNamespace);
+ }
+
+ private static IReadOnlyList ReadFacadeMethods(
+ IEnumerable sources,
+ string targetNamespace)
+ {
+ var methods = new List();
+ foreach (var source in sources)
+ {
+ var root = CSharpSyntaxTree.ParseText(source).GetRoot();
+ foreach (var method in root.DescendantNodes().OfType()
+ .Where(method => method.Ancestors()
+ .OfType()
+ .FirstOrDefault()?.Name.ToString().Equals(
+ targetNamespace,
+ StringComparison.Ordinal) == true)
+ .Where(method => method.Modifiers.Any(SyntaxKind.PublicKeyword)))
+ {
+ var optionsParameter = method.ParameterList.Parameters.FirstOrDefault();
+ var optionsType = optionsParameter?.Type?.ToString();
+ var declaringType = method.Ancestors()
+ .OfType()
+ .FirstOrDefault()?.Identifier.ValueText;
+ if (!string.IsNullOrWhiteSpace(declaringType)
+ && !string.IsNullOrWhiteSpace(optionsType)
+ && optionsType.TrimEnd('?').EndsWith("Options", StringComparison.Ordinal))
+ {
+ methods.Add(new GeneratedFacadeMethod(
+ declaringType,
+ method.Identifier.ValueText,
+ optionsType.TrimEnd('?'),
+ optionsParameter?.Default is not null));
+ }
+ }
+ }
+
+ return methods;
+ }
+
+ private static List ReadProperties(
+ RecordDeclarationSyntax declaration)
+ {
+ var properties = new List();
+ if (declaration.ParameterList is not null)
+ {
+ properties.AddRange(declaration.ParameterList.Parameters.Select(parameter =>
+ ReadProperty(
+ parameter.Identifier.ValueText,
+ parameter.Type?.ToString() ?? string.Empty,
+ parameter.AttributeLists,
+ isRequired: true,
+ accessorList: null)));
+ }
+ else
+ {
+ var baseConstructor = declaration.Members
+ .OfType()
+ .FirstOrDefault(constructor =>
+ constructor.Modifiers.Any(SyntaxKind.PublicKeyword)
+ && constructor.Initializer?.IsKind(SyntaxKind.BaseConstructorInitializer) == true);
+ if (baseConstructor is not null)
+ {
+ properties.AddRange(baseConstructor.ParameterList.Parameters.Select(parameter =>
+ new GeneratedApiProperty(
+ parameter.Identifier.ValueText,
+ parameter.Type?.ToString() ?? string.Empty,
+ null,
+ null,
+ true,
+ false,
+ null,
+ null)));
+ }
+ }
+
+ properties.AddRange(declaration.Members
+ .OfType()
+ .Where(property => property.Modifiers.Any(SyntaxKind.PublicKeyword))
+ .Select(property => ReadProperty(
+ property.Identifier.ValueText,
+ property.Type.ToString(),
+ property.AttributeLists,
+ isRequired: false,
+ property.AccessorList)));
+ return properties;
+ }
+
+ private static GeneratedApiProperty ReadProperty(
+ string propertyName,
+ string cSharpType,
+ SyntaxList attributeLists,
+ bool isRequired,
+ AccessorListSyntax? accessorList)
+ {
+ var attributes = attributeLists.SelectMany(list => list.Attributes).ToArray();
+ var cliArgument = FindAttribute(attributes, "CliArgument");
+ var cliOption = FindAttribute(attributes, "CliOption")
+ ?? FindAttribute(attributes, "CliFlag");
+ var obsolete = FindAttribute(attributes, "Obsolete");
+
+ return new GeneratedApiProperty(
+ propertyName,
+ cSharpType,
+ GetStringArgument(cliOption),
+ GetIntegerArgument(cliArgument),
+ isRequired,
+ obsolete is not null,
+ GetForwardTarget(accessorList),
+ GetStringArgument(obsolete),
+ accessorList?.Accessors.Any(static accessor =>
+ accessor.IsKind(SyntaxKind.InitAccessorDeclaration)) == true);
+ }
+
+ private static AttributeSyntax? FindAttribute(
+ IEnumerable attributes,
+ string name) =>
+ attributes.FirstOrDefault(attribute =>
+ attribute.Name.ToString().Equals(name, StringComparison.Ordinal)
+ || attribute.Name.ToString().Equals($"{name}Attribute", StringComparison.Ordinal));
+
+ private static string? GetStringArgument(AttributeSyntax? attribute) =>
+ attribute?.ArgumentList?.Arguments.FirstOrDefault()?.Expression is LiteralExpressionSyntax literal
+ && literal.IsKind(SyntaxKind.StringLiteralExpression)
+ ? literal.Token.ValueText
+ : null;
+
+ private static int? GetIntegerArgument(AttributeSyntax? attribute) =>
+ attribute?.ArgumentList?.Arguments.FirstOrDefault()?.Expression is LiteralExpressionSyntax literal
+ && literal.IsKind(SyntaxKind.NumericLiteralExpression)
+ && literal.Token.Value is int value
+ ? value
+ : null;
+
+ private static string? GetForwardTarget(AccessorListSyntax? accessorList) =>
+ accessorList?.Accessors
+ .FirstOrDefault(accessor => accessor.IsKind(SyntaxKind.GetAccessorDeclaration))?
+ .ExpressionBody?.Expression is IdentifierNameSyntax identifier
+ ? identifier.Identifier.ValueText
+ : null;
+}
+
+internal sealed record GeneratedApiProperty(
+ string PropertyName,
+ string CSharpType,
+ string? SwitchName,
+ int? ArgumentPosition,
+ bool IsRequired,
+ bool IsCompatibility,
+ string? ForwardToPropertyName,
+ string? ObsoleteMessage,
+ bool UseInitAccessor = false);
+
+internal sealed record GeneratedApiBaseline(
+ IReadOnlyList Properties,
+ IReadOnlyList Constructors);
+
+internal sealed record GeneratedFacadeMethod(
+ string DeclaringType,
+ string MethodName,
+ string OptionsType,
+ bool IsOptionsOptional);
diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs
index 4dc47a4c78c..bbce36767c3 100644
--- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs
+++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs
@@ -14,6 +14,15 @@ namespace ModularPipelines.OptionsGenerator.Generators;
///
public static partial class GeneratorUtils
{
+ internal static string GetEnumTypeName(string cSharpType)
+ {
+ var type = cSharpType.TrimEnd('?');
+ const string enumerablePrefix = "IEnumerable<";
+ return type.StartsWith(enumerablePrefix, StringComparison.Ordinal)
+ ? type[enumerablePrefix.Length..^1]
+ : type;
+ }
+
private static readonly string[] KnownRunnerHomeDirectories =
[
"/home/runner",
@@ -21,6 +30,27 @@ public static partial class GeneratorUtils
@"C:\Users\runneradmin",
];
+ internal static void GenerateCompatibilityProperty(
+ StringBuilder sb,
+ CliCompatibilityProperty property,
+ string modifiers = "")
+ {
+ sb.AppendLine($" [Obsolete({FormatStringLiteral(property.ObsoleteMessage)})]");
+
+ if (property.ForwardToPropertyName is null)
+ {
+ sb.AppendLine($" public {modifiers}{property.CSharpType} {property.PropertyName} {{ get; set; }}");
+ return;
+ }
+
+ sb.AppendLine($" public {modifiers}{property.CSharpType} {property.PropertyName}");
+ sb.AppendLine(" {");
+ sb.AppendLine($" get => {property.ForwardToPropertyName};");
+ var setter = property.UseInitAccessor ? "init" : "set";
+ sb.AppendLine($" {setter} => {property.ForwardToPropertyName} = value;");
+ sb.AppendLine(" }");
+ }
+
internal readonly record struct RequiredConstructorParameter(
string PropertyName,
string CSharpType,
@@ -609,11 +639,21 @@ internal static IReadOnlyList GetRequiredConstruct
///
public static string BuildOptionsParameter(CliCommandDefinition command)
{
- return HasRequiredParameters(command)
+ return RequiresOptionsParameter(command)
? $"{command.ClassName} options"
: $"{command.ClassName}? options = null";
}
+ ///
+ /// Whether callers must supply the generated options object.
+ ///
+ public static bool RequiresOptionsParameter(CliCommandDefinition command)
+ {
+ ArgumentNullException.ThrowIfNull(command);
+
+ return HasRequiredParameters(command) && !command.PreserveOptionalOptionsParameter;
+ }
+
///
/// Generates a command method signature for a service interface.
///
@@ -677,7 +717,7 @@ public static void GenerateServiceMethod(
ArgumentNullException.ThrowIfNull(command);
methodName = EnsureAsyncSuffix(methodName);
- var hasRequiredParams = HasRequiredParameters(command);
+ var requiresOptions = RequiresOptionsParameter(command);
// XML documentation
if (includeXmlDoc && !string.IsNullOrEmpty(command.Description))
@@ -699,7 +739,7 @@ public static void GenerateServiceMethod(
sb.AppendLine($"{indent} CancellationToken cancellationToken = default)");
sb.AppendLine($"{indent}{{");
- if (hasRequiredParams)
+ if (requiresOptions)
{
sb.AppendLine($"{indent} return await _command.ExecuteCommandLineToolAsync(options, executionOptions, cancellationToken);");
}
@@ -968,7 +1008,8 @@ public static IReadOnlyList GetNonCollidingRootCommands(Cl
var rootCommands = tool.Commands
.Where(c => c.SubDomainGroup is null)
- .Where(c => !subDomainNames.Contains(GetCommandGroupIdentifier(c)))
+ .Where(c => c.PreserveNamedFacade
+ || !subDomainNames.Contains(GetCommandGroupIdentifier(c)))
.ToList();
// Distinct commands can normalize to the same method name (e.g. "build-server"
@@ -1119,6 +1160,22 @@ internal static string GetAliasedClassName(
+ canonicalClassName[canonicalPrefix.Length..];
}
+ internal static string GetAliasedRequiredConstructorParameterType(
+ RequiredConstructorParameter parameter,
+ CliToolDefinition tool,
+ CliCommandGroupAlias alias)
+ {
+ var type = parameter.CSharpType.TrimEnd('?');
+ var canonicalEnumName = parameter.Option?.EnumDefinition?.EnumName;
+ if (canonicalEnumName is null)
+ {
+ return type;
+ }
+
+ var aliasEnumName = GetAliasedClassName(tool, alias, canonicalEnumName);
+ return type.Replace(canonicalEnumName, aliasEnumName, StringComparison.Ordinal);
+ }
+
internal static string GetCommandGroupIdentifier(CliCommandDefinition command) =>
command.CommandGroupIdentifierOverride
?? GenerateMethodNameFromCommandParts(command.CommandParts);
diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GlobalOptionsBaseGenerator.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GlobalOptionsBaseGenerator.cs
index 06c035e9c8f..829c06c3b35 100644
--- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GlobalOptionsBaseGenerator.cs
+++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GlobalOptionsBaseGenerator.cs
@@ -53,7 +53,11 @@ private static string GenerateBaseOptionsClass(CliToolDefinition tool)
sb.AppendLine("using System.ComponentModel.DataAnnotations;");
}
- if (globalOptions.Any(o => o.EnumDefinition is not null))
+ if (globalOptions.Any(o => o.EnumDefinition is not null)
+ || tool.GlobalCompatibilityProperties.Any(property => tool.AllEnums.Any(definition =>
+ definition.EnumName.Equals(
+ GeneratorUtils.GetEnumTypeName(property.CSharpType),
+ StringComparison.Ordinal))))
{
sb.AppendLine($"using {tool.TargetNamespace}.Enums;");
}
@@ -86,6 +90,19 @@ private static string GenerateBaseOptionsClass(CliToolDefinition tool)
sb.AppendLine();
}
+ foreach (var compatibilityProperty in tool.GlobalCompatibilityProperties)
+ {
+ var newModifier = InheritedPropertyCollisionResolver.IsInheritedPropertyName(
+ compatibilityProperty.PropertyName)
+ ? "new "
+ : string.Empty;
+ GeneratorUtils.GenerateCompatibilityProperty(
+ sb,
+ compatibilityProperty,
+ $"{newModifier}virtual ");
+ sb.AppendLine();
+ }
+
sb.AppendLine("}");
return sb.ToString();
diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/InheritedPropertyCollisionResolver.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/InheritedPropertyCollisionResolver.cs
index c9593ce222a..7e9e91de68f 100644
--- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/InheritedPropertyCollisionResolver.cs
+++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/InheritedPropertyCollisionResolver.cs
@@ -21,6 +21,7 @@ public static CliToolDefinition Resolve(CliToolDefinition tool)
var globalNames = tool.GlobalOptions
.Concat(tool.SupplementalGlobalOptions)
.Select(option => option.PropertyName)
+ .Concat(tool.GlobalCompatibilityProperties.Select(property => property.PropertyName))
.ToHashSet(StringComparer.Ordinal);
var globalRenamedProperties = new Dictionary(StringComparer.Ordinal);
var globalOptions = ResolveOptions(
@@ -36,6 +37,7 @@ public static CliToolDefinition Resolve(CliToolDefinition tool)
var resolvedGlobalNames = globalOptions
.Concat(supplementalGlobalOptions)
.Select(option => option.PropertyName)
+ .Concat(tool.GlobalCompatibilityProperties.Select(property => property.PropertyName))
.ToHashSet(StringComparer.Ordinal);
return tool with
@@ -48,6 +50,12 @@ public static CliToolDefinition Resolve(CliToolDefinition tool)
.ToArray(),
GlobalOptions = globalOptions,
SupplementalGlobalOptions = supplementalGlobalOptions,
+ GlobalCompatibilityProperties = tool.GlobalCompatibilityProperties
+ .Select(property => property.ForwardToPropertyName is { } target
+ && globalRenamedProperties.TryGetValue(target, out var renamedTarget)
+ ? property with { ForwardToPropertyName = renamedTarget }
+ : property)
+ .ToArray(),
};
}
diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs
index 9a7891e54bf..9aa19e53213 100644
--- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs
+++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs
@@ -52,10 +52,18 @@ private static GeneratedFile GenerateCompatibilityOptionsAlias(
tool,
alias,
command.ClassName);
+ var compatibilityConstructors = command.AliasCompatibilityConstructors
+ .GetValueOrDefault(aliasClassName, []);
+ var compatibilityProperties = command.AliasCompatibilityProperties
+ .GetValueOrDefault(aliasClassName, []);
+ var compatibilityPropertyNames = compatibilityProperties
+ .Select(static property => property.PropertyName)
+ .ToHashSet(StringComparer.Ordinal);
var requiredParameters = GeneratorUtils.GetRequiredConstructorParameters(command);
var enumOptions = command.Options
.Where(option => option.EnumDefinition is not null
- && option.ValueArity != CliOptionValueArity.Optional)
+ && option.ValueArity != CliOptionValueArity.Optional
+ && !compatibilityPropertyNames.Contains(option.PropertyName))
.ToArray();
var sb = new StringBuilder();
GeneratorUtils.GenerateFileHeaderWithNullable(sb, command.DocumentationUrl);
@@ -67,9 +75,13 @@ private static GeneratedFile GenerateCompatibilityOptionsAlias(
sb.AppendLine("using ModularPipelines.Models;");
}
- if (enumOptions.Length > 0)
+ if (enumOptions.Length > 0 || compatibilityProperties.Count > 0)
{
- sb.AppendLine("using ModularPipelines.Attributes;");
+ if (enumOptions.Length > 0)
+ {
+ sb.AppendLine("using ModularPipelines.Attributes;");
+ }
+
sb.AppendLine($"using {tool.TargetNamespace}.Enums;");
}
@@ -78,7 +90,10 @@ private static GeneratedFile GenerateCompatibilityOptionsAlias(
sb.AppendLine();
sb.AppendLine(GeneratorUtils.GeneratedCodeAttribute);
sb.AppendLine("[ExcludeFromCodeCoverage]");
- if (enumOptions.Length == 0 && requiredParameters.Count == 0)
+ if (enumOptions.Length == 0
+ && requiredParameters.Count == 0
+ && compatibilityConstructors.Count == 0
+ && compatibilityProperties.Count == 0)
{
sb.AppendLine($"public record {aliasClassName} : {command.ClassName};");
}
@@ -92,11 +107,20 @@ private static GeneratedFile GenerateCompatibilityOptionsAlias(
requiredParameters,
tool,
alias);
+ GenerateCompatibilityConstructors(
+ sb,
+ aliasClassName,
+ compatibilityConstructors);
foreach (var option in enumOptions)
{
GenerateCompatibilityEnumProperty(sb, option, tool, alias);
}
+ foreach (var property in compatibilityProperties)
+ {
+ GenerateAliasCompatibilityProperty(sb, property);
+ }
+
sb.AppendLine("}");
}
@@ -123,7 +147,7 @@ private static void GenerateCompatibilityConstructor(
}
var parameterDeclarations = requiredParameters.Select(parameter =>
- $" {GetCompatibilityParameterType(parameter, tool, alias)} {parameter.PropertyName}");
+ $" {GeneratorUtils.GetAliasedRequiredConstructorParameterType(parameter, tool, alias)} {parameter.PropertyName}");
var baseArguments = requiredParameters.Select(parameter =>
GetCompatibilityBaseArgument(parameter, parameter.PropertyName));
sb.AppendLine($" public {aliasClassName}(");
@@ -135,22 +159,6 @@ private static void GenerateCompatibilityConstructor(
sb.AppendLine();
}
- private static string GetCompatibilityParameterType(
- GeneratorUtils.RequiredConstructorParameter parameter,
- CliToolDefinition tool,
- CliCommandGroupAlias alias)
- {
- var type = parameter.CSharpType.TrimEnd('?');
- var canonicalEnumName = parameter.Option?.EnumDefinition?.EnumName;
- if (canonicalEnumName is null)
- {
- return type;
- }
-
- var aliasEnumName = GeneratorUtils.GetAliasedClassName(tool, alias, canonicalEnumName);
- return type.Replace(canonicalEnumName, aliasEnumName, StringComparison.Ordinal);
- }
-
private static string GetCompatibilityBaseArgument(
GeneratorUtils.RequiredConstructorParameter parameter,
string parameterName)
@@ -237,6 +245,60 @@ private static void GenerateCompatibilityEnumCollectionAccessors(
+ $"static value => ({canonicalEnumName})(int)value);");
}
+ private static void GenerateAliasCompatibilityProperty(
+ StringBuilder sb,
+ CliAliasCompatibilityProperty property)
+ {
+ var aliasEnumName = GeneratorUtils.GetEnumTypeName(property.AliasCSharpType);
+ var canonicalEnumName = GeneratorUtils.GetEnumTypeName(property.CanonicalCSharpType);
+ var aliasIsEnumerable = property.AliasCSharpType.StartsWith("IEnumerable<", StringComparison.Ordinal);
+ var canonicalIsEnumerable = property.CanonicalCSharpType.StartsWith("IEnumerable<", StringComparison.Ordinal);
+ if (aliasIsEnumerable != canonicalIsEnumerable)
+ {
+ throw new InvalidOperationException(
+ $"Cannot retain alias property {property.PropertyName} because its collection shape changed.");
+ }
+
+ var aliasIsNullable = property.AliasCSharpType.EndsWith('?');
+ var canonicalIsNullable = property.CanonicalCSharpType.EndsWith('?');
+ if (aliasIsNullable != canonicalIsNullable)
+ {
+ throw new InvalidOperationException(
+ $"Cannot retain alias property {property.PropertyName} because its nullability changed.");
+ }
+
+ sb.AppendLine($" [Obsolete({GeneratorUtils.FormatStringLiteral(property.ObsoleteMessage)})]");
+ sb.AppendLine($" public new {property.AliasCSharpType} {property.PropertyName}");
+ sb.AppendLine(" {");
+ if (aliasIsEnumerable)
+ {
+ var baseNullableOperator = canonicalIsNullable ? "?" : string.Empty;
+ var aliasNullableOperator = aliasIsNullable ? "?" : string.Empty;
+ sb.AppendLine(
+ $" get => base.{property.PropertyName}{baseNullableOperator}.Select("
+ + $"static value => ({aliasEnumName})(int)value);");
+ sb.AppendLine(
+ $" set => base.{property.PropertyName} = value{aliasNullableOperator}.Select("
+ + $"static value => ({canonicalEnumName})(int)value);");
+ }
+ else if (aliasIsNullable)
+ {
+ sb.AppendLine($" get => base.{property.PropertyName} is null");
+ sb.AppendLine(" ? null");
+ sb.AppendLine($" : ({aliasEnumName})(int)base.{property.PropertyName}.Value;");
+ sb.AppendLine($" set => base.{property.PropertyName} = value is null");
+ sb.AppendLine(" ? null");
+ sb.AppendLine($" : ({canonicalEnumName})(int)value.Value;");
+ }
+ else
+ {
+ sb.AppendLine($" get => ({aliasEnumName})(int)base.{property.PropertyName};");
+ sb.AppendLine($" set => base.{property.PropertyName} = ({canonicalEnumName})(int)value;");
+ }
+
+ sb.AppendLine(" }");
+ }
+
private static string GenerateOptionsClass(CliCommandDefinition command, CliToolDefinition tool)
{
var sb = new StringBuilder();
@@ -262,12 +324,48 @@ private static string GenerateOptionsClass(CliCommandDefinition command, CliTool
var existingPropertyNames = GenerateClassDeclaration(sb, command, positionalArguments);
sb.AppendLine("{");
+ GenerateCompatibilityConstructors(
+ sb,
+ command.ClassName,
+ command.CompatibilityConstructors);
GenerateProperties(sb, command, positionalArguments, existingPropertyNames);
sb.AppendLine("}");
return sb.ToString();
}
+ private static void GenerateCompatibilityConstructors(
+ StringBuilder sb,
+ string className,
+ IReadOnlyList constructors)
+ {
+ foreach (var constructor in constructors)
+ {
+ var parameters = constructor.Parameters
+ .Select(parameter => $"{parameter.CSharpType} {parameter.PropertyName}");
+ sb.AppendLine($" public {className}({string.Join(", ", parameters)})");
+ sb.AppendLine($" : this({string.Join(", ", constructor.PrimaryConstructorArguments)})");
+ sb.AppendLine(" {");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+
+ if (constructor.PreserveDeconstruct)
+ {
+ var deconstructParameters = constructor.Parameters
+ .Select(parameter => $"out {parameter.CSharpType} {parameter.PropertyName}");
+ sb.AppendLine($" public void Deconstruct({string.Join(", ", deconstructParameters)})");
+ sb.AppendLine(" {");
+ foreach (var parameter in constructor.Parameters)
+ {
+ sb.AppendLine($" {parameter.PropertyName} = this.{parameter.PropertyName};");
+ }
+
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ }
+ }
+ }
+
private static void GenerateUsings(StringBuilder sb, CliCommandDefinition command, CliToolDefinition tool)
{
sb.AppendLine("using System.CodeDom.Compiler;");
@@ -288,7 +386,11 @@ private static void GenerateUsings(StringBuilder sb, CliCommandDefinition comman
}
// Include enums namespace if any options use enum types
- if (command.Options.Any(o => o.EnumDefinition is not null))
+ if (command.Options.Any(o => o.EnumDefinition is not null)
+ || command.CompatibilityProperties.Any(property => tool.AllEnums.Any(definition =>
+ definition.EnumName.Equals(
+ GeneratorUtils.GetEnumTypeName(property.CSharpType),
+ StringComparison.Ordinal))))
{
sb.AppendLine($"using {tool.TargetNamespace}.Enums;");
}
@@ -350,7 +452,10 @@ private static void GenerateProperties(
continue;
}
- GenerateCompatibilityProperty(sb, compatibilityProperty);
+ GeneratorUtils.GenerateCompatibilityProperty(
+ sb,
+ compatibilityProperty,
+ GetNewModifier(compatibilityProperty.PropertyName));
sb.AppendLine();
}
}
@@ -445,23 +550,6 @@ private static void GeneratePositionalArgument(StringBuilder sb, CliPositionalAr
sb.AppendLine($" public {positional.CSharpType} {positional.PropertyName} {{ get; set; }}");
}
- private static void GenerateCompatibilityProperty(StringBuilder sb, CliCompatibilityProperty property)
- {
- sb.AppendLine($" [Obsolete({GeneratorUtils.FormatStringLiteral(property.ObsoleteMessage)})]");
-
- if (property.ForwardToPropertyName is null)
- {
- sb.AppendLine($" public {GetNewModifier(property.PropertyName)}{property.CSharpType} {property.PropertyName} {{ get; set; }}");
- return;
- }
-
- sb.AppendLine($" public {GetNewModifier(property.PropertyName)}{property.CSharpType} {property.PropertyName}");
- sb.AppendLine(" {");
- sb.AppendLine($" get => {property.ForwardToPropertyName};");
- sb.AppendLine($" set => {property.ForwardToPropertyName} = value;");
- sb.AppendLine(" }");
- }
-
private static string GetNewModifier(string propertyName) =>
InheritedPropertyCollisionResolver.IsInheritedPropertyName(propertyName) ? "new " : "";
diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/SubDomainClassGenerator.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/SubDomainClassGenerator.cs
index 22ed87fe3a6..c3035f097b3 100644
--- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/SubDomainClassGenerator.cs
+++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/SubDomainClassGenerator.cs
@@ -81,6 +81,10 @@ private static void GenerateFilesFromTree(
IReadOnlyList commandGroupAliases,
CliCommandDefinition? parentCommand = null)
{
+ parentCommand ??= tool.Commands.FirstOrDefault(command =>
+ command.PreserveExecuteFacade
+ && command.ClassName.Equals($"{node.ClassName}Options", StringComparison.Ordinal));
+
// Build map of commands that collide with child property names
// These will become ExecuteAsync() methods on the child classes instead
var collidingCommands = new Dictionary(StringComparer.OrdinalIgnoreCase);
@@ -96,7 +100,9 @@ private static void GenerateFilesFromTree(
}
}
- var excludedCommands = collidingCommands.Values.ToHashSet();
+ var excludedCommands = collidingCommands.Values
+ .Where(static command => !command.PreserveNamedFacade)
+ .ToHashSet();
// Generate the command represented by this node as ExecuteAsync(). Root nodes receive
// their top-level command; nested nodes receive a command that collided with the
@@ -409,7 +415,7 @@ private static string BuildCompatibilityOptionsParameter(
tool,
alias,
command.ClassName);
- return GeneratorUtils.HasRequiredParameters(command)
+ return GeneratorUtils.RequiresOptionsParameter(command)
? $"{aliasOptionsClassName} options"
: $"{aliasOptionsClassName}? options = null";
}
diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs
index f40daf3406c..549d236980f 100644
--- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs
+++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs
@@ -92,6 +92,38 @@ public record CliCommandDefinition
///
public IReadOnlyList CompatibilityProperties { get; init; } = [];
+ ///
+ /// Previously generated secondary constructors retained for source compatibility.
+ ///
+ public IReadOnlyList CompatibilityConstructors { get; init; } = [];
+
+ ///
+ /// Secondary constructors retained on command-group alias option records, keyed by alias class name.
+ ///
+ public IReadOnlyDictionary> AliasCompatibilityConstructors
+ { get; init; } = new Dictionary>(StringComparer.Ordinal);
+
+ ///
+ /// Enum-valued properties retained on command-group alias records, keyed by alias class name.
+ ///
+ public IReadOnlyDictionary> AliasCompatibilityProperties
+ { get; init; } = new Dictionary>(StringComparer.Ordinal);
+
+ ///
+ /// Whether an existing command-group ExecuteAsync facade must remain generated.
+ ///
+ public bool PreserveExecuteFacade { get; init; }
+
+ ///
+ /// Whether an existing named command facade must remain when the command gains children.
+ ///
+ public bool PreserveNamedFacade { get; init; }
+
+ ///
+ /// Whether an existing facade allowed its options argument to be omitted.
+ ///
+ public bool PreserveOptionalOptionsParameter { get; init; }
+
///
/// Public methods retained as obsolete forwarding aliases for compatibility.
///
@@ -180,6 +212,63 @@ public record CliCompatibilityProperty
///
public string? ForwardToPropertyName { get; init; }
+ ///
+ /// Whether the forwarding accessor must use init because its target is init-only.
+ ///
+ public bool UseInitAccessor { get; init; }
+
+ ///
+ /// Obsolete diagnostic shown to consumers.
+ ///
+ public required string ObsoleteMessage { get; init; }
+}
+
+///
+/// Describes a secondary generated constructor retained for source compatibility.
+///
+public record CliCompatibilityConstructor
+{
+ ///
+ /// Parameters exposed by the retained constructor.
+ ///
+ public required IReadOnlyList Parameters { get; init; }
+
+ ///
+ /// Arguments forwarded to the current primary constructor.
+ ///
+ public required IReadOnlyList PrimaryConstructorArguments { get; init; }
+
+ ///
+ /// Whether to retain the deconstruction shape synthesized by the former primary constructor.
+ ///
+ public bool PreserveDeconstruct { get; init; }
+}
+
+///
+/// Describes a parameter on a retained generated constructor.
+///
+public readonly record struct CliCompatibilityConstructorParameter(string PropertyName, string CSharpType);
+
+///
+/// Describes an enum-valued property retained on a command-group alias record.
+///
+public record CliAliasCompatibilityProperty
+{
+ ///
+ /// CLR property name shared with the canonical options record.
+ ///
+ public required string PropertyName { get; init; }
+
+ ///
+ /// Previously generated alias enum type exposed to consumers.
+ ///
+ public required string AliasCSharpType { get; init; }
+
+ ///
+ /// Canonical enum type used by the base options record.
+ ///
+ public required string CanonicalCSharpType { get; init; }
+
///
/// Obsolete diagnostic shown to consumers.
///
diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliToolDefinition.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliToolDefinition.cs
index e8a8b99d5d7..81562609364 100644
--- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliToolDefinition.cs
+++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliToolDefinition.cs
@@ -75,6 +75,11 @@ public record CliToolDefinition
///
public IReadOnlyList SupplementalGlobalOptions { get; init; } = [];
+ ///
+ /// Public global properties retained for source and binary compatibility but excluded from CLI rendering.
+ ///
+ public IReadOnlyList GlobalCompatibilityProperties { get; init; } = [];
+
///
/// Returns the validated, deterministic union of scraped and supplemental global options.
///
@@ -124,9 +129,15 @@ public IReadOnlyList GetGlobalOptions() =>
.Concat(GetGlobalOptions()
.Where(option => option.EnumDefinition is not null)
.Select(option => option.EnumDefinition!))
+ .Concat(CompatibilityEnums)
.DistinctBy(e => e.EnumName)
.ToList();
+ ///
+ /// Generated enum types retained after the installed CLI stops advertising them.
+ ///
+ public IReadOnlyList CompatibilityEnums { get; init; } = [];
+
///
/// Any scraping errors encountered.
///
diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CobraCliScraper.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CobraCliScraper.cs
index 2c7e915db75..65e5301f1bf 100644
--- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CobraCliScraper.cs
+++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CobraCliScraper.cs
@@ -264,7 +264,7 @@ protected override IEnumerable ExtractSubcommands(string helpText)
private List ParseOptions(string helpText, string[] commandParts)
{
var options = new List();
- var seenOptions = new HashSet(StringComparer.OrdinalIgnoreCase);
+ var seenOptions = new Dictionary(StringComparer.OrdinalIgnoreCase);
var className = GenerateClassName([ToolName, .. commandParts]);
// Find Flags, Options, and Global Flags sections
@@ -312,13 +312,25 @@ private List ParseOptions(string helpText, string[] command
continue;
}
+ var scrapedLongForm = longForm;
+ longForm = NormalizeOptionSwitchName(commandParts, longForm);
+
// Skip duplicates
- if (seenOptions.Contains(longForm))
+ if (seenOptions.TryGetValue(longForm, out var existingScrapedLongForm))
{
+ if (!existingScrapedLongForm.Equals(
+ scrapedLongForm,
+ StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException(
+ $"Option switch normalization for '{string.Join(' ', commandParts)}' "
+ + $"maps both '{existingScrapedLongForm}' and '{scrapedLongForm}' to '{longForm}'.");
+ }
+
continue;
}
- seenOptions.Add(longForm);
+ seenOptions.Add(longForm, scrapedLongForm);
var propertyName = NormalizePropertyName(longForm);
if (propertyName is null)
@@ -374,7 +386,7 @@ private List ParseOptions(string helpText, string[] command
? CliOptionValueArity.Optional
: CliOptionValueArity.Required,
EnumDefinition = enumDef,
- IsSecret = !isBoolean && IsSecretOption(propertyName, isFlag)
+ IsSecret = !isBoolean && IsSecretOption(propertyName, isFlag, description)
});
}
}
@@ -428,14 +440,24 @@ private static string NormalizeTypeHint(string typeHint, bool hasDefaultValue)
///
/// Determines whether a tool option value must be masked in command logs.
///
- protected virtual bool IsSecretOption(string propertyName, bool isFlag) =>
- GeneratorUtils.IsSecretOption(propertyName, isFlag);
+ protected virtual bool IsSecretOption(
+ string propertyName,
+ bool isFlag,
+ string description) =>
+ GeneratorUtils.IsSecretOption(propertyName, isFlag, description);
///
/// Determines whether an option's documented values form a closed set.
///
protected virtual bool ShouldGenerateEnum(string[] commandParts, string switchName) => true;
+ ///
+ /// Applies tool-specific corrections to an option's canonical switch spelling.
+ ///
+ protected virtual string NormalizeOptionSwitchName(
+ string[] commandParts,
+ string switchName) => switchName;
+
///
/// Applies tool-specific corrections when CLI help omits or misreports an option type.
///
diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CosignCliScraper.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CosignCliScraper.cs
index 5f0be1f967b..f3aaaf8116c 100644
--- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CosignCliScraper.cs
+++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CosignCliScraper.cs
@@ -85,8 +85,11 @@ protected override IReadOnlyList ApplyPositionalArgumentF
};
}
- protected override bool IsSecretOption(string propertyName, bool isFlag) =>
- base.IsSecretOption(propertyName, isFlag) ||
+ protected override bool IsSecretOption(
+ string propertyName,
+ bool isFlag,
+ string description) =>
+ base.IsSecretOption(propertyName, isFlag, description) ||
(!isFlag &&
(propertyName.Equals("NewKey", StringComparison.OrdinalIgnoreCase) ||
propertyName.Equals("OldKey", StringComparison.OrdinalIgnoreCase) ||
diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/DockerCliScraper.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/DockerCliScraper.cs
index c3a74703338..d1eeb4284e1 100644
--- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/DockerCliScraper.cs
+++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/DockerCliScraper.cs
@@ -31,4 +31,13 @@ public DockerCliScraper(ICliCommandExecutor executor, IHelpTextCache helpCache,
string[] commandPath,
string helpText) =>
DockerCliCompatibility.DetectCommandGroupAlias(commandPath, helpText);
+
+ ///
+ protected override string NormalizeOptionSwitchName(
+ string[] commandParts,
+ string switchName) =>
+ commandParts is ["compose", "exec"]
+ && switchName.Equals("--no-tty", StringComparison.OrdinalIgnoreCase)
+ ? "--no-TTY"
+ : switchName;
}
diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/TrivyCliScraper.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/TrivyCliScraper.cs
index 97916f0c083..a0527964399 100644
--- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/TrivyCliScraper.cs
+++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/TrivyCliScraper.cs
@@ -122,7 +122,7 @@ private static IReadOnlyList CreatePluginRunArguments(
{
CSharpType = "IEnumerable?",
Phase = CommandLinePhase.Passthrough,
- PositionIndex = 1,
+ PositionIndex = 0,
},
];
}
@@ -148,15 +148,33 @@ private static IReadOnlyList CreateMissingPositionalArgum
[RequiredArgument("Source")],
["registry", "login"] or ["registry", "logout"] =>
[RequiredArgument("Server")],
+ ["module"] or ["plugin"] or ["registry"] or ["vex"] or ["vex", "repo"] =>
+ [OptionalArgument("Command") with { Description = "The command operand." }],
_ => positionalArguments,
};
protected override string NormalizeOptionDescription(string description)
{
- var normalizedDescription = UserHomeDirectoryPattern().Replace(description, "");
+ var normalizedDescription = NormalizeHomePathSeparators(
+ UserHomeDirectoryPattern().Replace(description, ""));
return TrivyCacheDirectoryPattern().Replace(normalizedDescription, "/trivy");
}
+ private static string NormalizeHomePathSeparators(string description)
+ {
+ const string homePlaceholder = "";
+ var homeEnd = description.IndexOf(homePlaceholder, StringComparison.Ordinal);
+ if (homeEnd < 0)
+ {
+ return description;
+ }
+
+ homeEnd += homePlaceholder.Length;
+ return string.Concat(
+ description.AsSpan(0, homeEnd),
+ PathSeparatorPattern().Replace(description[homeEnd..], "/"));
+ }
+
private static CliPositionalArgument RequiredArgument(string propertyName) => new()
{
PropertyName = propertyName,
@@ -177,4 +195,7 @@ private static CliPositionalArgument OptionalArgument(string propertyName) =>
[GeneratedRegex(@"(?i)(?:[\\/]+AppData[\\/]+Local|[\\/]+\.cache|[\\/]+Library[\\/]+Caches)[\\/]+trivy")]
private static partial Regex TrivyCacheDirectoryPattern();
+
+ [GeneratedRegex(@"[\\/]+")]
+ private static partial Regex PathSeparatorPattern();
}