diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index 8cd41237df8..6debbdfaa79 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -52,9 +52,9 @@
-
-
-
+
+
+
diff --git a/src/Nitro/CommandLine/src/CommandLine/Commands/Apis/CreateApiCommand.cs b/src/Nitro/CommandLine/src/CommandLine/Commands/Apis/CreateApiCommand.cs
index 3db6d225766..221c9581961 100644
--- a/src/Nitro/CommandLine/src/CommandLine/Commands/Apis/CreateApiCommand.cs
+++ b/src/Nitro/CommandLine/src/CommandLine/Commands/Apis/CreateApiCommand.cs
@@ -54,6 +54,11 @@ private static async Task ExecuteAsync(
Opt.Instance,
ct);
+ if (!pathResult.StartsWith('/'))
+ {
+ throw new ExitException($"The path '{pathResult.EscapeMarkup()}' is invalid. It must start with '/'.");
+ }
+
var path = pathResult.Split("/", TrimEntries | RemoveEmptyEntries);
var kind = GetApiKind(parseResult);
diff --git a/src/Nitro/CommandLine/src/CommandLine/Helpers/SelectableTable.cs b/src/Nitro/CommandLine/src/CommandLine/Helpers/SelectableTable.cs
index 09779cbbc88..14481f479ea 100644
--- a/src/Nitro/CommandLine/src/CommandLine/Helpers/SelectableTable.cs
+++ b/src/Nitro/CommandLine/src/CommandLine/Helpers/SelectableTable.cs
@@ -174,9 +174,7 @@ private Table CreateTable()
ctx.UpdateTarget(new Rows(
title,
- CreateTable()
- .Centered()
- .AddRows(SelectedIndex, Items.Select(CreateRow)),
+ Align.Center(CreateTable().AddRows(SelectedIndex, Items.Select(CreateRow))),
Align.Center(new Rows(columns))));
ctx.Refresh();
diff --git a/src/Nitro/CommandLine/src/CommandLine/Services/Console/ActivityTree.cs b/src/Nitro/CommandLine/src/CommandLine/Services/Console/ActivityTree.cs
index c56eb04a2a2..2e250a06eb2 100644
--- a/src/Nitro/CommandLine/src/CommandLine/Services/Console/ActivityTree.cs
+++ b/src/Nitro/CommandLine/src/CommandLine/Services/Console/ActivityTree.cs
@@ -17,6 +17,8 @@ internal sealed class ActivityTree : Renderable
private readonly List _rootEntries = [];
private readonly Spinner _spinner = Spinner.Known.Default;
+ public bool EmitLivePadding { get; set; } = true;
+
public ActivityEntry AddRoot(string text)
{
lock (_lock)
@@ -92,13 +94,15 @@ protected override IEnumerable Render(RenderOptions options, int maxWid
RenderEntry(segments, root, parentPrefix: "", NodePosition.Root, options, maxWidth);
}
- // Workaround for a Spectre.Console bug: LiveRenderable.PositionCursor emits
- // `CSI 0 A` when the rendered shape is one line tall, and most terminals treat
- // that as `CSI 1 A` (move cursor up one row) per ECMA-48 default-parameter
- // handling. The result is that a single-line tree drifts up one row on every
- // refresh and overwrites previously-printed output. Forcing the shape to be at
- // least two lines tall keeps Spectre on the `CursorUp(n>=1)` path.
- segments.Add(Segment.LineBreak);
+ // Workaround for a Spectre.Console bug (issue #2076): LiveRenderable.PositionCursor
+ // emits `CSI 0 A` when the rendered shape is one line tall, and most terminals treat
+ // that as `CSI 1 A` (move cursor up one row) per ECMA-48 default-parameter handling.
+ // Forcing the shape to be at least two lines tall keeps Spectre on the
+ // `CursorUp(n>=1)` path. Only needed while the tree is rendered through Live.
+ if (EmitLivePadding)
+ {
+ segments.Add(Segment.LineBreak);
+ }
return segments;
}
diff --git a/src/Nitro/CommandLine/src/CommandLine/Services/Console/LiveActivitySink.cs b/src/Nitro/CommandLine/src/CommandLine/Services/Console/LiveActivitySink.cs
index 02b8712e85e..48e6dfc6af8 100644
--- a/src/Nitro/CommandLine/src/CommandLine/Services/Console/LiveActivitySink.cs
+++ b/src/Nitro/CommandLine/src/CommandLine/Services/Console/LiveActivitySink.cs
@@ -61,7 +61,7 @@ private async Task RunAsync()
{
await _console
.Live(_tree)
- .AutoClear(false)
+ .AutoClear(true)
.Overflow(VerticalOverflow.Visible)
.StartAsync(async ctx =>
{
@@ -73,10 +73,11 @@ await _console
ctx.Refresh();
});
- // ActivityTree pads its output with a trailing blank line to work around a
- // Spectre.Console bug (see ActivityTree.Render). After Live completes, Spectre
- // has left the cursor one row below that padding. Step back onto the padded
- // row so subsequent output overwrites it instead of leaving a visible gap.
- _console.Cursor.Move(CursorDirection.Up, 1);
+ // Live cleared its rendered region. Re-emit the final tree state as a plain
+ // renderable so it lands in scrollback as normal output. This avoids relying
+ // on cursor math to merge live output with subsequent stdout/stderr writes,
+ // which was the source of rendering bleed on Windows terminals.
+ _tree.EmitLivePadding = false;
+ _console.Write(_tree);
}
}
diff --git a/src/Nitro/CommandLine/src/CommandLine/Services/Console/NitroConsole.cs b/src/Nitro/CommandLine/src/CommandLine/Services/Console/NitroConsole.cs
index 1df8d052dbb..056505a8626 100644
--- a/src/Nitro/CommandLine/src/CommandLine/Services/Console/NitroConsole.cs
+++ b/src/Nitro/CommandLine/src/CommandLine/Services/Console/NitroConsole.cs
@@ -64,6 +64,20 @@ public void Write(IRenderable renderable)
+ "Check the documentation of the command to see all options");
}
+ public void WriteAnsi(Action action)
+ {
+ if (IsHumanReadable)
+ {
+ _hasWrittenOutput = true;
+ outConsole.WriteAnsi(action);
+ return;
+ }
+
+ throw new ExitException(
+ "Console runs in non interactive mode, yet a user interaction was attempted. "
+ + "Check the documentation of the command to see all options");
+ }
+
public Profile Profile => outConsole.Profile;
public IAnsiConsoleCursor Cursor => outConsole.Cursor;
diff --git a/src/Nitro/CommandLine/test/CommandLine.Tests/Commands/Apis/CreateApiCommandTests.cs b/src/Nitro/CommandLine/test/CommandLine.Tests/Commands/Apis/CreateApiCommandTests.cs
index e09724f8988..5eb4c0834f7 100644
--- a/src/Nitro/CommandLine/test/CommandLine.Tests/Commands/Apis/CreateApiCommandTests.cs
+++ b/src/Nitro/CommandLine/test/CommandLine.Tests/Commands/Apis/CreateApiCommandTests.cs
@@ -476,6 +476,28 @@ Creating API 'my-api'
""");
}
+ [Theory]
+ [InlineData("products")]
+ [InlineData("C:/Program Files/Git/test")]
+ public async Task Create_Should_ReturnError_When_PathDoesNotStartWithSlash(string path)
+ {
+ // arrange
+ SetupSessionWithWorkspace();
+
+ // act
+ var result = await ExecuteCommandAsync(
+ "api",
+ "create",
+ "--name",
+ ApiName,
+ "--path",
+ path);
+
+ // assert
+ result.AssertError(
+ $"The path '{path}' is invalid. It must start with '/'.");
+ }
+
[Fact]
public async Task Create_Should_ReturnSuccess_When_KindNotProvided()
{