Four polish fixes
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 <id> [--column <col>] 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>
This commit is contained in:
parent
ade243e2c7
commit
037f5fde28
7 changed files with 83 additions and 8 deletions
4
dart_test.yaml
Normal file
4
dart_test.yaml
Normal file
|
|
@ -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
|
||||||
|
|
@ -23,9 +23,10 @@ class ProjectContext {
|
||||||
|
|
||||||
const ProjectContext({required this.root, required this.config});
|
const ProjectContext({required this.root, required this.config});
|
||||||
|
|
||||||
/// Walks up from [Directory.current] until a `.project/dew.yaml` is found.
|
/// Walks up from [from] (defaults to [Directory.current]) until a
|
||||||
static Future<ProjectContext> find() async {
|
/// `.project/dew.yaml` is found.
|
||||||
var dir = Directory.current;
|
static Future<ProjectContext> find({Directory? from}) async {
|
||||||
|
var dir = from ?? Directory.current;
|
||||||
while (true) {
|
while (true) {
|
||||||
final configFile = File(p.join(dir.path, '.project', 'dew.yaml'));
|
final configFile = File(p.join(dir.path, '.project', 'dew.yaml'));
|
||||||
if (await configFile.exists()) {
|
if (await configFile.exists()) {
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,12 @@ class GetCommand extends DewCommand with DewToolCommand {
|
||||||
final buf = StringBuffer();
|
final buf = StringBuffer();
|
||||||
buf.writeln('[${t.id}] (${t.type}) [${t.column}] ${t.title}');
|
buf.writeln('[${t.id}] (${t.type}) [${t.column}] ${t.title}');
|
||||||
buf.writeln('Created: ${t.created.toLocal().toString().split('.').first}');
|
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) {
|
if (t.links.isNotEmpty) {
|
||||||
buf.writeln();
|
buf.writeln();
|
||||||
buf.writeln('Links:');
|
buf.writeln('Links:');
|
||||||
|
|
|
||||||
60
packages/kanban/lib/src/commands/unarchive_command.dart
Normal file
60
packages/kanban/lib/src/commands/unarchive_command.dart
Normal file
|
|
@ -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<String> callAsTool(Map<String, dynamic> 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".';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -40,11 +40,12 @@ class UpdateCommand extends DewCommand with DewToolCommand {
|
||||||
final body = args['body'] as String?;
|
final body = args['body'] as String?;
|
||||||
final rawMilestones = args['milestone'] as List?;
|
final rawMilestones = args['milestone'] as List?;
|
||||||
final milestones = rawMilestones != null && rawMilestones.isNotEmpty
|
final milestones = rawMilestones != null && rawMilestones.isNotEmpty
|
||||||
? rawMilestones.cast<String>()
|
? rawMilestones.cast<String>().where((s) => s.isNotEmpty).toList()
|
||||||
: null;
|
: null;
|
||||||
final rawLabels = args['label'] as List?;
|
final rawLabels = args['label'] as List?;
|
||||||
final labels =
|
final labels = rawLabels != null && rawLabels.isNotEmpty
|
||||||
rawLabels != null && rawLabels.isNotEmpty ? rawLabels.cast<String>() : null;
|
? rawLabels.cast<String>().where((s) => s.isNotEmpty).toList()
|
||||||
|
: null;
|
||||||
|
|
||||||
if (title == null &&
|
if (title == null &&
|
||||||
typeId == null &&
|
typeId == null &&
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import 'commands/list_command.dart';
|
||||||
import 'commands/move_command.dart';
|
import 'commands/move_command.dart';
|
||||||
import 'commands/search_command.dart';
|
import 'commands/search_command.dart';
|
||||||
import 'commands/stats_command.dart';
|
import 'commands/stats_command.dart';
|
||||||
|
import 'commands/unarchive_command.dart';
|
||||||
import 'commands/unlink_command.dart';
|
import 'commands/unlink_command.dart';
|
||||||
import 'commands/update_command.dart';
|
import 'commands/update_command.dart';
|
||||||
|
|
||||||
|
|
@ -25,6 +26,7 @@ class KanbanCommand extends DewCommand {
|
||||||
addSubcommand(UpdateCommand());
|
addSubcommand(UpdateCommand());
|
||||||
addSubcommand(DeleteCommand());
|
addSubcommand(DeleteCommand());
|
||||||
addSubcommand(ArchiveCommand());
|
addSubcommand(ArchiveCommand());
|
||||||
|
addSubcommand(UnarchiveCommand());
|
||||||
addSubcommand(MoveCommand());
|
addSubcommand(MoveCommand());
|
||||||
addSubcommand(SearchCommand());
|
addSubcommand(SearchCommand());
|
||||||
addSubcommand(AddCommentCommand());
|
addSubcommand(AddCommentCommand());
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ void main() {
|
||||||
expect(
|
expect(
|
||||||
cmd.subcommands.keys,
|
cmd.subcommands.keys,
|
||||||
containsAll([
|
containsAll([
|
||||||
'create', 'list', 'board', 'get', 'update', 'delete', 'archive',
|
'create', 'list', 'board', 'get', 'update', 'delete', 'archive', 'unarchive',
|
||||||
'move', 'search', 'comment', 'config', 'stats', 'link', 'unlink',
|
'move', 'search', 'comment', 'config', 'stats', 'link', 'unlink',
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
@ -36,7 +36,7 @@ void main() {
|
||||||
final registry = CommandRegistry();
|
final registry = CommandRegistry();
|
||||||
registerCommands(registry);
|
registerCommands(registry);
|
||||||
final tools = registry.mcpTools;
|
final tools = registry.mcpTools;
|
||||||
expect(tools, hasLength(14));
|
expect(tools, hasLength(15));
|
||||||
final names = tools.map((t) => t.name).toSet();
|
final names = tools.map((t) => t.name).toSet();
|
||||||
expect(names, {
|
expect(names, {
|
||||||
'kanban_create_ticket',
|
'kanban_create_ticket',
|
||||||
|
|
@ -46,6 +46,7 @@ void main() {
|
||||||
'kanban_update_ticket',
|
'kanban_update_ticket',
|
||||||
'kanban_delete_ticket',
|
'kanban_delete_ticket',
|
||||||
'kanban_archive_ticket',
|
'kanban_archive_ticket',
|
||||||
|
'kanban_unarchive_ticket',
|
||||||
'kanban_move_ticket',
|
'kanban_move_ticket',
|
||||||
'kanban_search_tickets',
|
'kanban_search_tickets',
|
||||||
'kanban_add_comment',
|
'kanban_add_comment',
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue