Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
53dc628
Add create, delete & list command for OpenAPI collections
tobias-tengler Dec 4, 2025
68e4f3c
Add upload command
tobias-tengler Dec 5, 2025
7ba481c
Allow to reference OpenApiDocumentParser from net8.0
tobias-tengler Dec 8, 2025
5745bc6
Properly handle globbing
tobias-tengler Dec 8, 2025
021bd33
Minor fixes
tobias-tengler Dec 9, 2025
e3a3e20
Minor fixes
tobias-tengler Dec 9, 2025
a42893d
Ensure deterministic archives
tobias-tengler Dec 9, 2025
525bd98
Add openapi publish command
tobias-tengler Dec 10, 2025
570efdf
Add validation command
tobias-tengler Dec 13, 2025
a7565ce
Merge remote-tracking branch 'origin/main' into tte/nitro-cli-openapi
tobias-tengler Dec 17, 2025
af71f32
Properly serialize settings
tobias-tengler Dec 17, 2025
d61a5c9
Merge remote-tracking branch 'origin/main' into tte/nitro-cli-openapi
tobias-tengler Dec 18, 2025
b96419d
Merge remote-tracking branch 'origin/main' into tte/nitro-cli-openapi
tobias-tengler Dec 19, 2025
9fb48ba
Merge remote-tracking branch 'origin/main' into tte/nitro-cli-openapi
tobias-tengler Jan 3, 2026
6927089
Fix build issues
tobias-tengler Jan 3, 2026
917f631
Merge remote-tracking branch 'origin/main' into tte/nitro-cli-openapi
tobias-tengler Jan 9, 2026
28edbad
Merge remote-tracking branch 'origin/main' into tte/nitro-cli-openapi
tobias-tengler Jan 22, 2026
13d0ca4
Output validation errors
tobias-tengler Jan 22, 2026
21507b9
Merge remote-tracking branch 'origin/main' into tte/nitro-cli-openapi
tobias-tengler Jan 23, 2026
ecd9122
Merge remote-tracking branch 'origin/main' into tte/nitro-cli-openapi
tobias-tengler Jan 24, 2026
b6839d8
Cleanup merge
tobias-tengler Jan 24, 2026
2031352
Spread validation fragment on publish error
tobias-tengler Jan 25, 2026
03ea523
Generate persisted operations
tobias-tengler Jan 26, 2026
d9c2afb
Merge remote-tracking branch 'origin/main' into tte/nitro-cli-openapi
tobias-tengler Jan 28, 2026
c2f1493
Make name optional
tobias-tengler Jan 28, 2026
2154894
Merge remote-tracking branch 'origin/main' into tte/nitro-cli-openapi
tobias-tengler Jan 30, 2026
498fe0d
Fix open TODOs
tobias-tengler Jan 30, 2026
7c18558
Merge remote-tracking branch 'origin/main' into tte/nitro-cli-openapi
tobias-tengler Jan 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Abstractions" Version="1.1.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="Microsoft.Extensions.FileSystemGlobbing" Version="10.0.0" />
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="Microsoft.OpenApi.Readers" Version="1.6.14" />
Expand Down Expand Up @@ -170,4 +171,4 @@
<PackageVersion Include="Roslynator.CodeAnalysis.Analyzers" Version="4.15.0" />
<PackageVersion Include="Roslynator.Formatting.Analyzers" Version="4.15.0" />
</ItemGroup>
</Project>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ private static async Task<int> RenderInteractiveAsync(
IApiClient client,
CancellationToken ct)
{
const string apiMessage = "For which client do you want to list the clients?";
const string apiMessage = "For which API do you want to list the clients?";
var apiId = await context.GetOrSelectApiId(apiMessage);

var container = PaginationContainer
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using ChilliCream.Nitro.CommandLine.Client;

namespace ChilliCream.Nitro.CommandLine.Commands.OpenApi.Components;

internal sealed class OpenApiCollectionDetailPrompt
{
private readonly IOpenApiCollectionDetailPrompt_OpenApiCollection _data;

private OpenApiCollectionDetailPrompt(IOpenApiCollectionDetailPrompt_OpenApiCollection data)
{
_data = data;
}

public OpenApiCollectionDetailPromptResult ToObject(string[] formats)
{
return new OpenApiCollectionDetailPromptResult
{
Id = _data.Id,
Name = _data.Name
};
}

public static OpenApiCollectionDetailPrompt From(IOpenApiCollectionDetailPrompt_OpenApiCollection data) => new(data);

public class OpenApiCollectionDetailPromptResult
{
public required string Id { get; init; }

public required string Name { get; init; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
fragment OpenApiCollectionDetailPrompt_OpenApiCollection on OpenApiCollection {
id
name
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using ChilliCream.Nitro.CommandLine.Client;
using ChilliCream.Nitro.CommandLine.Helpers;

namespace ChilliCream.Nitro.CommandLine.Commands.OpenApi.Components;

public sealed class SelectOpenApiCollectionPrompt(IApiClient client, string apiId)
{
private string _title = "Select an OpenAPI collection from the list below.";

public SelectOpenApiCollectionPrompt Title(string title)
{
_title = title;
return this;
}

public async Task<ISelectOpenApiCollectionPrompt_OpenApiCollection?> RenderAsync(
IAnsiConsole console,
CancellationToken cancellationToken)
{
var paginationContainer = PaginationContainer.Create(
(after, first, ct)
=> client.SelectOpenApiCollectionPromptQuery.ExecuteAsync(apiId, after, first, ct),
p => p.ApiById?.OpenApiCollections?.PageInfo,
p => p.ApiById?.OpenApiCollections?.Edges);

var selectedEdge = await PagedSelectionPrompt
.New(paginationContainer)
.Title(_title)
.UseConverter(x => x.Node.Name)
.RenderAsync(console, cancellationToken);

return selectedEdge?.Node;
}

public static SelectOpenApiCollectionPrompt New(IApiClient client, string apiId)
=> new(client, apiId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
query SelectOpenApiCollectionPromptQuery($apiId: ID!, $after: String, $first: Int) {
apiById(id: $apiId) {
openApiCollections(after: $after, first: $first) {
edges {
...SelectOpenApiCollectionPrompt_OpenApiCollectionEdge
}
pageInfo {
...PageInfo
}
}
}
}

fragment SelectOpenApiCollectionPrompt_OpenApiCollectionEdge on ApiOpenApiCollectionsEdge {
cursor
node {
...SelectOpenApiCollectionPrompt_OpenApiCollection
}
}

fragment SelectOpenApiCollectionPrompt_OpenApiCollection on OpenApiCollection {
id
name
...OpenApiCollectionDetailPrompt_OpenApiCollection
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System.CommandLine.Invocation;
using ChilliCream.Nitro.CommandLine.Client;
using ChilliCream.Nitro.CommandLine.Commands.Apis.Inputs;
using ChilliCream.Nitro.CommandLine.Commands.OpenApi.Components;
using ChilliCream.Nitro.CommandLine.Commands.OpenApi.Options;
using ChilliCream.Nitro.CommandLine.Configuration;
using ChilliCream.Nitro.CommandLine.Helpers;
using ChilliCream.Nitro.CommandLine.Options;
using ChilliCream.Nitro.CommandLine.Results;
using static ChilliCream.Nitro.CommandLine.ThrowHelper;

namespace ChilliCream.Nitro.CommandLine.Commands.OpenApi;

internal sealed class CreateOpenApiCollectionCommand : Command
{
public CreateOpenApiCollectionCommand() : base("create")
{
Description = "Creates a new OpenAPI collection";

AddOption(Opt<OptionalApiIdOption>.Instance);
AddOption(Opt<OpenApiCollectionNameOption>.Instance);

this.SetHandler(
ExecuteAsync,
Bind.FromServiceProvider<InvocationContext>(),
Bind.FromServiceProvider<IAnsiConsole>(),
Bind.FromServiceProvider<IApiClient>(),
Bind.FromServiceProvider<CancellationToken>());
}

private static async Task<int> ExecuteAsync(
InvocationContext context,
IAnsiConsole console,
IApiClient client,
CancellationToken cancellationToken)
{
console.WriteLine();
console.WriteLine("Creating an OpenAPI collection");
console.WriteLine();

const string apiMessage = "For which api do you want to create an OpenAPI collection?";
var apiId = await context.GetOrSelectApiId(apiMessage);

var name = await context
.OptionOrAskAsync("Name", Opt<OpenApiCollectionNameOption>.Instance, cancellationToken);

var input = new CreateOpenApiCollectionInput { Name = name, ApiId = apiId };
var result =
await client.CreateOpenApiCollectionCommandMutation.ExecuteAsync(input, cancellationToken);

console.EnsureNoErrors(result);
var data = console.EnsureData(result);
console.PrintErrorsAndExit(data.CreateOpenApiCollection.Errors);

var createdOpenApiCollection = data.CreateOpenApiCollection.OpenApiCollection;
if (createdOpenApiCollection is null)
{
throw Exit("Could not create OpenAPI collection.");
}

console.OkLine($"OpenAPI collection {createdOpenApiCollection.Name.AsHighlight()} created.");

if (createdOpenApiCollection is IOpenApiCollectionDetailPrompt_OpenApiCollection detail)
{
context.SetResult(OpenApiCollectionDetailPrompt.From(detail).ToObject([]));
}

return ExitCodes.Success;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
mutation CreateOpenApiCollectionCommandMutation($input: CreateOpenApiCollectionInput!) {
createOpenApiCollection(input: $input) {
openApiCollection {
...CreateOpenApiCollectionCommandMutation_OpenApiCollection
}
errors {
...Error
...ApiNotFoundError
...UnauthorizedOperation
}
}
}

fragment CreateOpenApiCollectionCommandMutation_OpenApiCollection on OpenApiCollection {
name
id
...OpenApiCollectionDetailPrompt_OpenApiCollection
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using System.CommandLine.Invocation;
using ChilliCream.Nitro.CommandLine.Arguments;
using ChilliCream.Nitro.CommandLine.Client;
using ChilliCream.Nitro.CommandLine.Commands.Apis.Components;
using ChilliCream.Nitro.CommandLine.Commands.OpenApi.Components;
using ChilliCream.Nitro.CommandLine.Configuration;
using ChilliCream.Nitro.CommandLine.Helpers;
using ChilliCream.Nitro.CommandLine.Options;
using ChilliCream.Nitro.CommandLine.Results;
using ChilliCream.Nitro.CommandLine.Services.Sessions;
using static ChilliCream.Nitro.CommandLine.ThrowHelper;

namespace ChilliCream.Nitro.CommandLine.Commands.OpenApi;

internal sealed class DeleteOpenApiCollectionCommand : Command
{
public DeleteOpenApiCollectionCommand() : base("delete")
{
Description = "Deletes an OpenAPI collection";

AddOption(Opt<ForceOption>.Instance);
AddArgument(Opt<OptionalIdArgument>.Instance);

this.SetHandler(
ExecuteAsync,
Bind.FromServiceProvider<InvocationContext>(),
Bind.FromServiceProvider<IAnsiConsole>(),
Bind.FromServiceProvider<IApiClient>(),
Opt<OptionalIdArgument>.Instance,
Bind.FromServiceProvider<CancellationToken>());
}

private static async Task<int> ExecuteAsync(
InvocationContext context,
IAnsiConsole console,
IApiClient client,
string? openApiCollectionId,
CancellationToken cancellationToken)
{
console.WriteLine();
console.WriteLine("Deleting an OpenAPI collection");
console.WriteLine();

const string apiMessage = "For which api do you want to delete an OpenAPI collection?";
const string openApiCollectionMessage = "Which OpenAPI collection do you want to delete?";

if (openApiCollectionId is null)
{
if (!console.IsHumanReadable())
{
throw Exit("The OpenAPI collection id is required in non-interactive mode.");
}

var workspaceId = context.RequireWorkspaceId();

var selectedApi = await SelectApiPrompt
.New(client, workspaceId)
.Title(apiMessage)
.RenderAsync(console, cancellationToken) ?? throw NoApiSelected();

var apiId = selectedApi.Id;

var selectedOpenApiCollection = await SelectOpenApiCollectionPrompt
.New(client, apiId)
.Title(openApiCollectionMessage)
.RenderAsync(console, cancellationToken) ?? throw NoOpenApiCollectionSelected();

console.WriteLine("Selected OpenAPI collection: " + selectedOpenApiCollection.Name);

openApiCollectionId = selectedOpenApiCollection.Id;
console.OkQuestion(openApiCollectionMessage, openApiCollectionId);
}
else
{
console.OkQuestion(openApiCollectionMessage, openApiCollectionId);
}

var shouldDelete = await context.ConfirmWhenNotForced(
$"Do you want to delete the OpenAPI collection with the id {openApiCollectionId}?"
.EscapeMarkup(),
cancellationToken);

if (!shouldDelete)
{
console.OkLine("Aborted.");
return ExitCodes.Success;
}

var input = new DeleteOpenApiCollectionByIdInput { OpenApiCollectionId = openApiCollectionId };
var result =
await client.DeleteOpenApiCollectionByIdCommandMutation.ExecuteAsync(input, cancellationToken);

console.EnsureNoErrors(result);
var data = console.EnsureData(result);
console.PrintErrorsAndExit(data.DeleteOpenApiCollectionById.Errors);

var deletedOpenApiCollection = data.DeleteOpenApiCollectionById.OpenApiCollection;
if (deletedOpenApiCollection is null)
{
throw Exit("Could not delete the OpenAPI collection.");
}

console.OkLine($"OpenAPI collection {deletedOpenApiCollection.Name.AsHighlight()} was deleted.");

if (deletedOpenApiCollection is IOpenApiCollectionDetailPrompt_OpenApiCollection detail)
{
context.SetResult(OpenApiCollectionDetailPrompt.From(detail).ToObject([]));
}

return ExitCodes.Success;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
mutation DeleteOpenApiCollectionByIdCommandMutation(
$input: DeleteOpenApiCollectionByIdInput!
) {
deleteOpenApiCollectionById(input: $input) {
openApiCollection {
...DeleteOpenApiCollectionByIdCommandMutation_OpenApiCollection
}
errors {
...Error
...OpenApiCollectionNotFoundError
...UnauthorizedOperation
}
}
}

fragment DeleteOpenApiCollectionByIdCommandMutation_OpenApiCollection on OpenApiCollection {
name
id
...OpenApiCollectionDetailPrompt_OpenApiCollection
}
Loading
Loading