Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
109 commits
Select commit Hold shift + click to select a range
53b1a7f
Add dapper to central package management
michaelstaib Aug 18, 2026
22d9b29
Initial Implementation
michaelstaib Aug 18, 2026
b4383d2
Define backend-agnostic ITaskStore contract (bd-oyf.1)
PascalSenn Aug 18, 2026
38ad736
Add e2e VHS pipeline scaffolding for the Nitro CLI (perles-net-4gb.1)
PascalSenn Aug 18, 2026
cc16c19
Add TUI runtime event loop, live render, resize, and teardown
PascalSenn Aug 18, 2026
33ba961
Add CliWrap smoke-test tier for Nitro CommandLine
PascalSenn Aug 18, 2026
ff8f6b5
Add TUI keymap dispatcher and TuiMessage intents
PascalSenn Aug 18, 2026
4fb2d8c
Add TUI render primitives: task badge, viewport, column pane, theme t…
PascalSenn Aug 18, 2026
d18ea3b
Fix TaskBadge markup: escape type-code brackets, handle empty style t…
PascalSenn Aug 18, 2026
35d6404
Fix e2e run.sh --update mode to propagate recording failures in exit …
PascalSenn Aug 18, 2026
d686d69
Ensure TUI background tasks are torn down when the event handler throws
PascalSenn Aug 18, 2026
44c913b
Extract TaskGlyphs from TaskBadge for search/tree/detail reuse
PascalSenn Aug 18, 2026
dcc8519
Add deterministic fixture task workspace for e2e tapes
PascalSenn Aug 18, 2026
d2b0adb
Implement ITaskStore read side in TaskStore (bd-oyf.2)
PascalSenn Aug 18, 2026
654d9c7
feat(nitro-tui): add TUI shell mode container, toaster, and quit conf…
PascalSenn Aug 18, 2026
3e0e6bd
Add TaskQuery parser for TUI search mode (perles-net-4j7.2)
PascalSenn Aug 18, 2026
91fc419
Add board model: views, column filters, loader over ITaskStore
PascalSenn Aug 18, 2026
1d62b92
Add direct TaskStore tests for the new read surface (bd-oyf.2)
PascalSenn Aug 18, 2026
846782f
Fix Dapper record materialization for COUNT(*) in TaskStore reads
PascalSenn Aug 18, 2026
d740f87
feat(nitro-tui): add scoped form widget engine
PascalSenn Aug 18, 2026
e27ba8f
Implement ITaskStore write side in TaskStore (bd-oyf.3)
PascalSenn Aug 18, 2026
939011f
Add e2e tapes + goldens for non-interactive nitro task flows
PascalSenn Aug 18, 2026
da8ee1e
Migrate task read commands onto ITaskStore's new surface
PascalSenn Aug 18, 2026
023a71c
Migrate task write commands onto ITaskStore's new surface
PascalSenn Aug 18, 2026
1b09a7c
Add nitro-cli-e2e CI workflow with sticky before/after GIF PR comment
PascalSenn Aug 18, 2026
2eb156c
Add kanban board mode: column rendering, navigation, refresh
PascalSenn Aug 18, 2026
6d93fc8
Remove legacy connection-exposing ITaskStore members
PascalSenn Aug 18, 2026
6f7ea1d
Add search mode: query input, live results, focus switching (perles-n…
PascalSenn Aug 18, 2026
499c9ba
Remove em dashes from nitro-cli-e2e workflow text
PascalSenn Aug 18, 2026
d1b498d
test(nitro-tui): cover board viewport more-above/more-below indicators
PascalSenn Aug 18, 2026
2076d43
fix(nitro-tasks): exclude tombstones from empty-query search results
PascalSenn Aug 18, 2026
a2b43e0
Add AOT self-contained publish smoke job to nitro-cli-e2e.yml
PascalSenn Aug 18, 2026
21cf044
Mark aot-publish-smoke informational until Dapper AOT trim errors are…
PascalSenn Aug 18, 2026
a570def
feat(nitro-tasks): add task board entry command
PascalSenn Aug 18, 2026
e999bf6
Add board layout modes: maximize, stacked fallback, resize handling
PascalSenn Aug 18, 2026
c5294c0
Add task detail view: scrollable body, sidebar, dependency navigation
PascalSenn Aug 18, 2026
998f8af
Fix TaskDetailView scroll saturation via Viewport.ScrollBy
PascalSenn Aug 18, 2026
184aedc
Scope IL3053/IL2104 suppression to Dapper in Nitro CLI AOT publish
PascalSenn Aug 18, 2026
f6dabb2
Add ConfirmDialog and TaskLifecycleActions for task close/reopen/dele…
PascalSenn Aug 18, 2026
e11eee3
Add dependency tree explorer view for the tasks TUI
PascalSenn Aug 18, 2026
539bc05
Unhide the nitro task command family per user ruling
PascalSenn Aug 18, 2026
6417eff
Prepare (blocked) board TUI VHS tapes for perles-net-4gb.7
PascalSenn Aug 18, 2026
1012983
Add TaskEditorForm for task title/status/priority/type/labels/descrip…
PascalSenn Aug 18, 2026
37e34bb
Make TaskStore NativeAOT-compatible
PascalSenn Aug 18, 2026
de05472
fix(nitro-tui): paint the initial frame before any dirty event
PascalSenn Aug 18, 2026
507758a
fix(nitro-tui): keep selected row's font color, background-only highl…
PascalSenn Aug 18, 2026
0976d9a
Wire search, detail, tree, and editing modes into the TUI shell
PascalSenn Aug 18, 2026
d6f36c4
Revert quit-confirm modality and recursion guard added by umu.3 wiring
PascalSenn Aug 18, 2026
8dd6d1b
Add status and priority quick pickers to the tasks TUI
PascalSenn Aug 18, 2026
fbf2393
fix(tui): pin ColumnPane panel height so board columns fill full term…
PascalSenn Aug 18, 2026
7ff088d
Add create task and create epic flows to the tasks TUI
PascalSenn Aug 18, 2026
1198efd
Record board TUI e2e goldens now that the pty paint hang is fixed (pe…
PascalSenn Aug 18, 2026
1fbe284
Add tests for create task/epic key bindings and shell wiring (4j7.9)
PascalSenn Aug 18, 2026
4d8e1b5
Prepare (blocked) search/detail TUI VHS tapes for perles-net-4gb.8
PascalSenn Aug 18, 2026
8551007
Fix dead '/' search chord on real pty (Oem2 vs Divide)
PascalSenn Aug 18, 2026
235fdf7
nitro task: add --output json to query commands (bd-5dd)
PascalSenn Aug 18, 2026
30ee010
Record search/detail TUI e2e goldens for perles-net-4gb.8
PascalSenn Aug 19, 2026
38f5145
feat(nitro-task): add task sync command for JSONL export/import
PascalSenn Aug 19, 2026
3ea73f7
Degrade dependency tree breadcrumb header at narrow widths (bd-595)
PascalSenn Aug 19, 2026
bceca65
feat(nitro-task): accept priority ranges in list/ready --priority (bd…
PascalSenn Aug 19, 2026
d5d9cb3
nitro task search: match comment text (bd-g34)
PascalSenn Aug 19, 2026
c4291b6
fix(bd-595): budget dependency tree header against panel's rendered w…
PascalSenn Aug 19, 2026
10cc638
nitro task: add --output json to mutation commands (perles-net-teq)
PascalSenn Aug 19, 2026
7cc37f1
feat(nitro-task): accept multiple task IDs on update (bd-izm)
PascalSenn Aug 19, 2026
fbace0f
feat(nitro-task): add --add-label/--remove-label, --parent, --claim t…
PascalSenn Aug 19, 2026
95a01cd
fix(nitro-cli): guard task update --parent against duplicate edges an…
PascalSenn Aug 19, 2026
fc573cf
nitro task q: quick capture that prints only the new task ID
PascalSenn Aug 19, 2026
9f98edf
feat(nitro-task): add task doctor and lint workspace diagnostics (per…
PascalSenn Aug 19, 2026
0762cca
test(e2e): re-record help-flow and detail-flow goldens for final sweep
PascalSenn Aug 19, 2026
7797707
fix(nitro-task): unify task label list JSON shape across arities (per…
PascalSenn Aug 19, 2026
87d50a2
feat(nitro-tui): expose BoardMode.SelectedTaskId from focused column
PascalSenn Aug 19, 2026
64eec0b
task sync: include config table in tasks.jsonl so the id prefix survi…
PascalSenn Aug 19, 2026
ab9a821
fix(tui): touched validation gate, confirm-discard on dirty Esc, trim…
PascalSenn Aug 19, 2026
74d0bb1
fix(nitro-tui): selection highlight background Aqua -> Grey35 (bd-d4l)
PascalSenn Aug 19, 2026
18c7d3b
feat(nitro-tui): Enter on the board opens the task detail view (perle…
PascalSenn Aug 19, 2026
5f55a05
fix(nitro-tui): task form field min heights, horizontal radios, width…
PascalSenn Aug 19, 2026
62124e9
fix(nitro-tui): drop detail.section.header token committed from anoth…
PascalSenn Aug 19, 2026
171debc
fix(nitro-tui): detail view redesign, fill frame, sections, sidebar r…
PascalSenn Aug 19, 2026
e7a7021
fix: only keep selected detail row visible on selection change (perle…
PascalSenn Aug 19, 2026
975198b
test: make dependency-row scroll-offset test discriminate against the…
PascalSenn Aug 19, 2026
c2ceb39
feat(nitro-tasks-tui): add always-visible per-view keybinding footer
PascalSenn Aug 19, 2026
b331e79
fix(nitro-tasks): add Enter hint to search input footer (perles-net-t…
PascalSenn Aug 19, 2026
4701cbe
feat(tui): per-column border and header accent colors on the board
PascalSenn Aug 19, 2026
5d9ce53
fix(nitro-tui): stacked board columns share height equally (perles-ne…
PascalSenn Aug 19, 2026
a5ed9b8
test(e2e): re-record board, board-maximize, search, detail goldens fo…
PascalSenn Aug 19, 2026
818752a
fix(nitro-tasks): wrap-aware sidebar line count in stacked task detai…
PascalSenn Aug 19, 2026
596b9e6
fix(nitro-tui): make the quit confirmation dialog fully modal
PascalSenn Aug 19, 2026
a76bad7
fix(nitro-tasks): avoid comment id collisions on cross-clone sync import
PascalSenn Aug 19, 2026
dd5d8b3
fix(tui): keep editor/create form open with values when store rejects…
PascalSenn Aug 19, 2026
eca1aec
fix(tui): let the create form clear its parent to create a root task
PascalSenn Aug 19, 2026
837d2eb
fix(nitro-task): restore child ID counters on sync import (perles-net…
PascalSenn Aug 19, 2026
59fb58e
fix(nitro-cli): reject dependency and parent cycles before commit (pe…
PascalSenn Aug 19, 2026
77cb77f
fixup: restore other-session comment block in TaskStore.cs accidental…
PascalSenn Aug 19, 2026
5bcca59
fix(cli): reject whitespace-only task titles in create/update/q
PascalSenn Aug 19, 2026
b9acdbb
fix: include manual status=blocked tasks in the Blocked board lane
PascalSenn Aug 19, 2026
7114e55
feat(nitro-tasks): render Description, Design, Acceptance criteria, a…
PascalSenn Aug 19, 2026
00de8e8
feat(nitro-tui): watch the task db and publish DataChangedEvent (perl…
PascalSenn Aug 19, 2026
2ddf4dc
fix(nitro-tasks): blank separator row between stacked board columns
PascalSenn Aug 19, 2026
6493632
fix(nitro-tui): clamp the edit/create modal to the frame and scroll f…
PascalSenn Aug 19, 2026
a6c007e
feat(nitro-tui): Ctrl+Enter (and Ctrl+S fallback) saves the task form…
PascalSenn Aug 19, 2026
3840ffe
test(nitro-tasks): re-record board/board-maximize e2e goldens for sta…
PascalSenn Aug 19, 2026
6f50ddb
refactor(nitro-tasks): rename CLI command family to nitro agent tasks
PascalSenn Aug 19, 2026
16c71ff
test(nitro-tasks): re-record e2e goldens for nitro agent tasks rename
PascalSenn Aug 19, 2026
91267c9
refactor: hoist (await ...).Member chains into named locals in Comman…
PascalSenn Aug 20, 2026
d43a119
refactor(nitro-task): convert TaskStore SQL to raw string literals (b…
PascalSenn Aug 20, 2026
12156a0
Format remaining column-list label INSERTs as raw string literals
PascalSenn Aug 20, 2026
dbca30d
chore(nitro-tasks): remove stale task references
PascalSenn Aug 20, 2026
356cfbe
Merge branch 'main' into mst/nitro-tasks
PascalSenn Aug 20, 2026
f2d499a
fix(ci): use the renamed agent tasks command path in the AOT publish …
PascalSenn Aug 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
197 changes: 197 additions & 0 deletions .github/workflows/nitro-cli-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
name: nitro-cli-e2e

on:
push:
paths:
- 'src/Nitro/CommandLine/**'
- 'src/Nitro/Common/**'
- '.github/workflows/nitro-cli-e2e.yml'
- 'global.json'
- 'src/Directory.Packages.props'
pull_request:
paths:
- 'src/Nitro/CommandLine/**'
- 'src/Nitro/Common/**'
- '.github/workflows/nitro-cli-e2e.yml'
- 'global.json'
- 'src/Directory.Packages.props'
workflow_dispatch:

permissions:
contents: read
pull-requests: write

concurrency:
group: nitro-cli-e2e-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
aot-publish-smoke:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

- name: Install .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
with:
dotnet-version: 11.0.100-preview.6.26359.118

- name: Publish AOT self-contained binary (linux-x64)
run: |
dotnet publish src/Nitro/CommandLine/src/CommandLine \
-c Release \
-r linux-x64 \
-f net11.0 \
-o publish \
--self-contained \
-p:PublishAot=true \
-p:TargetFrameworks=net11.0 \
-p:RuntimeIdentifiers=linux-x64

- name: Smoke test the published binary
run: |
set -euo pipefail

nitro_bin="$(pwd)/publish/nitro"
"$nitro_bin" --version

work_dir="$(mktemp -d)"
cd "$work_dir"
export NITRO_TASK_ACTOR=ci

"$nitro_bin" agent tasks init
"$nitro_bin" agent tasks list

create_output="$("$nitro_bin" agent tasks create "AOT smoke test task")"
echo "$create_output"
task_id=$(printf '%s' "$create_output" | sed -n "s/.*'\([^']*\)'.*/\1/p")
if [ -z "$task_id" ]; then
echo "::error::Could not parse a task ID out of the create output above." >&2
exit 1
fi

"$nitro_bin" agent tasks show "$task_id"

record-and-verify:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

- name: Install .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
with:
dotnet-version: 11.0.100-preview.6.26359.118

- name: Record + verify all flows
id: record
continue-on-error: true
run: ./src/Nitro/CommandLine/test/e2e/run.sh

- name: Upload snapshot artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: nitro-e2e-snapshots
path: |
src/Nitro/CommandLine/test/e2e/out/*.gif
src/Nitro/CommandLine/test/e2e/out/report/**
if-no-files-found: warn

- name: Comment changed snapshots on PR
if: always() && github.event_name == 'pull_request'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
RUN_ID: ${{ github.run_id }}
with:
script: |
const { owner, repo } = context.repo;
const pr = context.payload.pull_request;
const prNum = pr.number;
const baseRef = pr.base.ref;
const MARKER = '<!-- nitro-e2e-snapshots -->';
const bust = `?run=${process.env.RUN_ID}`;

const rawBase = `https://raw.githubusercontent.com/${pr.base.repo.full_name}/${pr.base.sha}`;
const headFull = pr.head.repo && pr.head.repo.full_name;
const rawHead = headFull
? `https://raw.githubusercontent.com/${headFull}/${pr.head.sha}`
: null;

const files = await github.paginate(github.rest.pulls.listFiles,
{ owner, repo, pull_number: prNum, per_page: 100 });
const re = /^src\/Nitro\/CommandLine\/test\/e2e\/([^/]+)-flow\.gif$/;
const changed = [];
for (const f of files) {
const m = f.filename.match(re);
if (m) changed.push(
{ flow: m[1], status: f.status, path: f.filename, prev: f.previous_filename });
}
changed.sort((a, b) => a.flow.localeCompare(b.flow));

async function upsertComment(body) {
const { data: comments } = await github.rest.issues.listComments(
{ owner, repo, issue_number: prNum, per_page: 100 });
const existing = comments.find(c => c.body && c.body.includes(MARKER));
if (existing) {
await github.rest.issues.updateComment(
{ owner, repo, comment_id: existing.id, body });
} else {
await github.rest.issues.createComment(
{ owner, repo, issue_number: prNum, body });
}
}

if (changed.length === 0) {
try {
await upsertComment(
`${MARKER}\n✅ **Nitro CLI E2E snapshots**: no flow recordings changed vs \`${baseRef}\`.`);
} catch (e) {
core.warning(`comment failed (expected on forks): ${e}`);
}
return;
}

const img = url => `<img src="${url}${bust}" width="480" alt="recording">`;
const ICON = { added: '🆕', removed: '🗑️' };

const blocks = [];
for (const { flow, status, path, prev } of changed) {
const before = `${rawBase}/${prev || path}`;
const after = rawHead ? `${rawHead}/${path}` : null;
let inner;
if (status === 'added') {
inner = after ? `\n\n${img(after)}\n` : '\n\n_after unavailable_\n';
} else if (status === 'removed') {
inner = `\n\n${img(before)}\n`;
} else {
const afterCell = after ? img(after) : '<em>unavailable</em>';
inner =
`\n\n<table><tr><th>Before: <code>${baseRef}</code></th><th>After: this PR</th></tr>`
+ `<tr><td>${img(before)}</td><td>${afterCell}</td></tr></table>\n`;
}
blocks.push(
`<details><summary>${ICON[status] || '🔁'} ${flow}: ${status}</summary>${inner}</details>`);
}

const body = [
MARKER,
'### 🎬 Nitro CLI E2E snapshots changed',
'',
`These flow recordings differ from \`${baseRef}\` (added, removed, or modified). `
+ 'Expand each for the before/after.',
'',
...blocks,
'',
`<sub>Run ${process.env.RUN_ID} · full set in the <b>nitro-e2e-snapshots</b> artifact.</sub>`,
].join('\n');

try { await upsertComment(body); }
catch (e) { core.warning(`comment failed (expected on forks): ${e}`); }

- name: Fail if any flow drifted from its golden
if: always() && steps.record.outcome == 'failure'
run: |
echo "::error::E2E recordings drifted from their committed golden, review the nitro-e2e-snapshots artifact, then run ./src/Nitro/CommandLine/test/e2e/run.sh --update <flow> if intended." >&2
exit 1
1 change: 1 addition & 0 deletions src/All.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@
</Folder>
<Folder Name="/Nitro/CommandLine/test/">
<Project Path="Nitro/CommandLine/test/CommandLine.Tests/Nitro.CommandLine.Tests.csproj" />
<Project Path="Nitro/CommandLine/test/CommandLine.Smoke.Tests/Nitro.CommandLine.Smoke.Tests.csproj" />
</Folder>
<Folder Name="/Nitro/Common/" />
<Folder Name="/Nitro/Common/src/">
Expand Down
3 changes: 3 additions & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
<PackageVersion Include="BenchmarkDotNet.Diagnostics.dotMemory" Version="0.15.8" />
<PackageVersion Include="ChilliCream.ModelContextProtocol.AspNetCore" Version="1.4.1" />
<PackageVersion Include="ChilliCream.Nitro.App" Version="$(NitroVersion)" />
<PackageVersion Include="CliWrap" Version="3.8.2" />
<PackageVersion Include="Confluent.Kafka" Version="2.14.2" />
<PackageVersion Include="Dapper" Version="2.1.79" />
<PackageVersion Include="Dapper.AOT" Version="1.0.52" />
Comment thread
PascalSenn marked this conversation as resolved.
Comment thread
PascalSenn marked this conversation as resolved.
<PackageVersion Include="DiffPlex" Version="1.9.0" />
<PackageVersion Include="Duende.IdentityModel" Version="7.1.0" />
<PackageVersion Include="Duende.IdentityModel.OidcClient" Version="6.0.1" />
Expand Down
1 change: 1 addition & 0 deletions src/Nitro/CommandLine/Nitro.CommandLine.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
</Folder>
<Folder Name="/test/">
<Project Path="test/CommandLine.Tests/Nitro.CommandLine.Tests.csproj" />
<Project Path="test/CommandLine.Smoke.Tests/Nitro.CommandLine.Smoke.Tests.csproj" />
</Folder>
</Solution>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using ChilliCream.Nitro.CommandLine.Commands.Tasks;

namespace ChilliCream.Nitro.CommandLine.Commands.Agent;

internal sealed class AgentCommand : Command
{
public AgentCommand() : base("agent")
{
Description = "Commands for coding agents.";

Subcommands.Add(new TasksCommand());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -605,8 +605,8 @@ private static async Task<int> ComposeAsync(

if (removeSourceSchemas.Count > 0)
{
var existing = (await archive.GetSourceSchemaNamesAsync(cancellationToken))
.ToHashSet(StringComparer.Ordinal);
var sourceSchemaNames = await archive.GetSourceSchemaNamesAsync(cancellationToken);
var existing = sourceSchemaNames.ToHashSet(StringComparer.Ordinal);

var missingSourceSchema =
removeSourceSchemas.FirstOrDefault(name => !existing.Contains(name));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using ChilliCream.Nitro.CommandLine.Commands.Tasks.Options;
using ChilliCream.Nitro.CommandLine.Helpers;
using ChilliCream.Nitro.CommandLine.Results;
using ChilliCream.Nitro.CommandLine.Services;
using ChilliCream.Nitro.CommandLine.Services.Tasks;

namespace ChilliCream.Nitro.CommandLine.Commands.Tasks;

internal sealed class AddTaskCommentCommand : Command
{
public AddTaskCommentCommand() : base("add")
{
Description = "Add a comment to a task.";

Arguments.Add(Opt<TaskIdArgument>.Instance);
Arguments.Add(Opt<CommentTextArgument>.Instance);
Options.Add(Opt<TaskActorOption>.Instance);
Options.Add(Opt<OptionalOutputFormatOption>.Instance);

this.AddExamples("agent tasks comment add \"acme-1a2\" \"Looks good to me.\"");

this.SetActionWithExceptionHandling(ExecuteAsync);
}

private static async Task<int> ExecuteAsync(
ICommandServices services,
ParseResult parseResult,
CancellationToken cancellationToken)
{
var console = services.GetRequiredService<INitroConsole>();
var store = services.GetRequiredService<ITaskStore>();
var environmentVariableProvider = services.GetRequiredService<IEnvironmentVariableProvider>();
var resultHolder = services.GetRequiredService<IResultHolder>();

var id = parseResult.GetRequiredValue(Opt<TaskIdArgument>.Instance);
var text = parseResult.GetRequiredValue(Opt<CommentTextArgument>.Instance);
var actor = TaskActor.Resolve(
parseResult.GetValue(Opt<TaskActorOption>.Instance), environmentVariableProvider);

var comment = await store.AddCommentAsync(id, text, actor, cancellationToken);

if (!console.IsHumanReadable)
{
resultHolder.SetResult(new ObjectResult(comment));
return ExitCodes.Success;
}

console.OkLine($"Added comment to '{comment.TaskId.EscapeMarkup()}'.");

return ExitCodes.Success;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using ChilliCream.Nitro.CommandLine.Commands.Tasks.Options;
using ChilliCream.Nitro.CommandLine.Helpers;
using ChilliCream.Nitro.CommandLine.Results;
using ChilliCream.Nitro.CommandLine.Services;
using ChilliCream.Nitro.CommandLine.Services.Tasks;

namespace ChilliCream.Nitro.CommandLine.Commands.Tasks;

internal sealed class AddTaskDependencyCommand : Command
{
public AddTaskDependencyCommand() : base("add")
{
Description = "Add a dependency between two tasks.";

Arguments.Add(Opt<TaskIdArgument>.Instance);
Arguments.Add(Opt<DependsOnIdArgument>.Instance);

Options.Add(Opt<TaskDependencyTypeOption>.Instance);
Options.Add(Opt<TaskActorOption>.Instance);
Options.Add(Opt<OptionalOutputFormatOption>.Instance);

this.AddExamples(
"agent tasks dep add \"acme-1a2\" \"acme-9z8\"",
"agent tasks dep add \"acme-1a2\" \"acme-9z8\" --type waits-for");

this.SetActionWithExceptionHandling(ExecuteAsync);
}

private static async Task<int> ExecuteAsync(
ICommandServices services,
ParseResult parseResult,
CancellationToken cancellationToken)
{
var console = services.GetRequiredService<INitroConsole>();
var store = services.GetRequiredService<ITaskStore>();
var environmentVariableProvider = services.GetRequiredService<IEnvironmentVariableProvider>();
var resultHolder = services.GetRequiredService<IResultHolder>();

var id = parseResult.GetRequiredValue(Opt<TaskIdArgument>.Instance);
var dependsOnId = parseResult.GetRequiredValue(Opt<DependsOnIdArgument>.Instance);
var typeValue = parseResult.GetValue(Opt<TaskDependencyTypeOption>.Instance);
var type = typeValue is null
? TaskDependencyTypes.Blocks
: TaskDependencyTypes.Normalize(typeValue);
var actor = TaskActor.Resolve(
parseResult.GetValue(Opt<TaskActorOption>.Instance), environmentVariableProvider);

var result = await store.AddDependencyAsync(id, dependsOnId, type, actor, cancellationToken);

if (!console.IsHumanReadable)
{
resultHolder.SetResult(new ObjectResult(new TaskDependencyAddedResult
{
Id = id,
DependsOnId = dependsOnId,
Type = type,
Cycle = result.Cycle
}));

return ExitCodes.Success;
}

console.OkLine(
$"Added {type.EscapeMarkup()} dependency: "
+ $"'{id.EscapeMarkup()}' -> '{dependsOnId.EscapeMarkup()}'.");

return ExitCodes.Success;
}

public sealed record TaskDependencyAddedResult
{
public required string Id { get; init; }
public required string DependsOnId { get; init; }
public required string Type { get; init; }
public IReadOnlyList<string>? Cycle { get; init; }
}
}
Loading
Loading