From 037f5fde28dce0b9de4f43a3c427bcc8628b8e90 Mon Sep 17 00:00:00 2001 From: Chris Hendrickson Date: Thu, 23 Apr 2026 22:10:12 -0400 Subject: [PATCH] Four polish fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. get shows milestones/labels: GetCommand._format() now shows Milestones:/Labels: lines when non-empty, between Created: and Links: 2. unarchive command: kanban unarchive --id [--column ] restores a ticket from archive/ back to a column (default: first configured column); registered as 'kanban_unarchive_ticket' MCP tool (15 tools total) 3. Test isolation: add dart_test.yaml (concurrency: 1) — Directory.current is a process-global OS chdir(); concurrent test files in the same process would race. Now dart test packages/core packages/kanban passes cleanly. 4. update empty multi-option fix: --milestone '' / --label '' with empty strings now filters them out (treats as 'clear to empty') rather than writing spurious empty-string YAML list entries Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dart_test.yaml | 4 ++ packages/core/lib/src/config.dart | 7 ++- .../kanban/lib/src/commands/get_command.dart | 6 ++ .../lib/src/commands/unarchive_command.dart | 60 +++++++++++++++++++ .../lib/src/commands/update_command.dart | 7 ++- packages/kanban/lib/src/dew_kanban_base.dart | 2 + packages/kanban/test/dew_kanban_test.dart | 5 +- 7 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 dart_test.yaml create mode 100644 packages/kanban/lib/src/commands/unarchive_command.dart diff --git a/dart_test.yaml b/dart_test.yaml new file mode 100644 index 0000000..4ab73eb --- /dev/null +++ b/dart_test.yaml @@ -0,0 +1,4 @@ +# Tests that mutate Directory.current (a process-global OS call) must not run +# concurrently across test files in the same process — the last chdir() wins +# and can leave other suites pointing at a deleted temp directory. +concurrency: 1 diff --git a/packages/core/lib/src/config.dart b/packages/core/lib/src/config.dart index eff6a47..63b2c07 100644 --- a/packages/core/lib/src/config.dart +++ b/packages/core/lib/src/config.dart @@ -23,9 +23,10 @@ class ProjectContext { const ProjectContext({required this.root, required this.config}); - /// Walks up from [Directory.current] until a `.project/dew.yaml` is found. - static Future find() async { - var dir = Directory.current; + /// Walks up from [from] (defaults to [Directory.current]) until a + /// `.project/dew.yaml` is found. + static Future find({Directory? from}) async { + var dir = from ?? Directory.current; while (true) { final configFile = File(p.join(dir.path, '.project', 'dew.yaml')); if (await configFile.exists()) { diff --git a/packages/kanban/lib/src/commands/get_command.dart b/packages/kanban/lib/src/commands/get_command.dart index a80b0ab..23f88e1 100644 --- a/packages/kanban/lib/src/commands/get_command.dart +++ b/packages/kanban/lib/src/commands/get_command.dart @@ -41,6 +41,12 @@ class GetCommand extends DewCommand with DewToolCommand { final buf = StringBuffer(); buf.writeln('[${t.id}] (${t.type}) [${t.column}] ${t.title}'); buf.writeln('Created: ${t.created.toLocal().toString().split('.').first}'); + if (t.milestones.isNotEmpty) { + buf.writeln('Milestones: ${t.milestones.join(', ')}'); + } + if (t.labels.isNotEmpty) { + buf.writeln('Labels: ${t.labels.join(', ')}'); + } if (t.links.isNotEmpty) { buf.writeln(); buf.writeln('Links:'); diff --git a/packages/kanban/lib/src/commands/unarchive_command.dart b/packages/kanban/lib/src/commands/unarchive_command.dart new file mode 100644 index 0000000..0199bca --- /dev/null +++ b/packages/kanban/lib/src/commands/unarchive_command.dart @@ -0,0 +1,60 @@ +import 'dart:io'; + +import 'package:dew_core/dew_core.dart'; +import '../kanban_config.dart'; +import 'package:path/path.dart' as p; + +import '../ticket_store.dart'; + +class UnarchiveCommand extends DewCommand with DewToolCommand { + UnarchiveCommand() { + argParser + ..addOption('id', abbr: 'i', mandatory: true, help: 'Ticket ID to unarchive.') + ..addOption( + 'column', + abbr: 'c', + help: 'Column to restore to. Defaults to the first configured column.', + ); + } + + @override + final String name = 'unarchive'; + + @override + final String description = 'Restore an archived ticket to a column.'; + + @override + final String toolName = 'kanban_unarchive_ticket'; + + @override + Future callAsTool(Map args) async { + final id = (args['id'] as String).toUpperCase(); + + final context = await ProjectContext.find(); + final config = context.config.kanban; + final kanbanDir = p.join(context.root, '.project', 'kanban'); + + final store = TicketStore(kanbanDir: kanbanDir, prefix: config.prefix); + final ticket = await store.findById(id); + if (ticket == null) throw ArgumentError('Ticket $id not found.'); + if (ticket.column != 'archive') return '$id is not archived.'; + + final columnArg = args['column'] as String?; + final targetColumn = columnArg ?? config.columns.first.id; + if (!config.columns.any((c) => c.id == targetColumn)) { + throw ArgumentError( + 'Unknown column "$targetColumn". ' + 'Valid: ${config.columns.map((c) => c.id).join(', ')}', + ); + } + + final targetDir = Directory(p.join(kanbanDir, targetColumn)); + await targetDir.create(recursive: true); + + final srcFile = File(p.join(kanbanDir, 'archive', '$id.md')); + final dstFile = File(p.join(targetDir.path, '$id.md')); + await srcFile.rename(dstFile.path); + + return 'Restored $id to "$targetColumn".'; + } +} diff --git a/packages/kanban/lib/src/commands/update_command.dart b/packages/kanban/lib/src/commands/update_command.dart index 7d03d61..b3ea3e4 100644 --- a/packages/kanban/lib/src/commands/update_command.dart +++ b/packages/kanban/lib/src/commands/update_command.dart @@ -40,11 +40,12 @@ class UpdateCommand extends DewCommand with DewToolCommand { final body = args['body'] as String?; final rawMilestones = args['milestone'] as List?; final milestones = rawMilestones != null && rawMilestones.isNotEmpty - ? rawMilestones.cast() + ? rawMilestones.cast().where((s) => s.isNotEmpty).toList() : null; final rawLabels = args['label'] as List?; - final labels = - rawLabels != null && rawLabels.isNotEmpty ? rawLabels.cast() : null; + final labels = rawLabels != null && rawLabels.isNotEmpty + ? rawLabels.cast().where((s) => s.isNotEmpty).toList() + : null; if (title == null && typeId == null && diff --git a/packages/kanban/lib/src/dew_kanban_base.dart b/packages/kanban/lib/src/dew_kanban_base.dart index bdc1067..ccc48ff 100644 --- a/packages/kanban/lib/src/dew_kanban_base.dart +++ b/packages/kanban/lib/src/dew_kanban_base.dart @@ -12,6 +12,7 @@ import 'commands/list_command.dart'; import 'commands/move_command.dart'; import 'commands/search_command.dart'; import 'commands/stats_command.dart'; +import 'commands/unarchive_command.dart'; import 'commands/unlink_command.dart'; import 'commands/update_command.dart'; @@ -25,6 +26,7 @@ class KanbanCommand extends DewCommand { addSubcommand(UpdateCommand()); addSubcommand(DeleteCommand()); addSubcommand(ArchiveCommand()); + addSubcommand(UnarchiveCommand()); addSubcommand(MoveCommand()); addSubcommand(SearchCommand()); addSubcommand(AddCommentCommand()); diff --git a/packages/kanban/test/dew_kanban_test.dart b/packages/kanban/test/dew_kanban_test.dart index 71c7292..df234d8 100644 --- a/packages/kanban/test/dew_kanban_test.dart +++ b/packages/kanban/test/dew_kanban_test.dart @@ -18,7 +18,7 @@ void main() { expect( cmd.subcommands.keys, containsAll([ - 'create', 'list', 'board', 'get', 'update', 'delete', 'archive', + 'create', 'list', 'board', 'get', 'update', 'delete', 'archive', 'unarchive', 'move', 'search', 'comment', 'config', 'stats', 'link', 'unlink', ]), ); @@ -36,7 +36,7 @@ void main() { final registry = CommandRegistry(); registerCommands(registry); final tools = registry.mcpTools; - expect(tools, hasLength(14)); + expect(tools, hasLength(15)); final names = tools.map((t) => t.name).toSet(); expect(names, { 'kanban_create_ticket', @@ -46,6 +46,7 @@ void main() { 'kanban_update_ticket', 'kanban_delete_ticket', 'kanban_archive_ticket', + 'kanban_unarchive_ticket', 'kanban_move_ticket', 'kanban_search_tickets', 'kanban_add_comment',