- core: Add ProjectDirs helper class on ProjectContext (context.dirs) with project getter for .project/ dir; feature packages extend it via extension methods to expose their own dirs - kanban: Add KanbanDirs extension on ProjectDirs exposing .kanban; replace all 14 p.join(context.root, '.project', 'kanban') call sites with context.dirs.kanban; drop now-unused path imports - kanban: Add clock: DateTime Function() field to TicketStore (defaults to DateTime.now); use clock().toUtc() in create() for deterministic timestamps in tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
96 lines
3 KiB
Dart
96 lines
3 KiB
Dart
import 'package:dew_core/dew_core.dart';
|
|
import 'package:file/file.dart';
|
|
import 'package:file/local.dart';
|
|
import '../kanban_config.dart';
|
|
|
|
import '../ticket_store.dart';
|
|
|
|
class UpdateCommand extends DewCommand with DewToolCommand {
|
|
final FileSystem _fs;
|
|
|
|
UpdateCommand({FileSystem fs = const LocalFileSystem()}) : _fs = fs {
|
|
argParser
|
|
..addOption('id', abbr: 'i', mandatory: true, help: 'Ticket ID.')
|
|
..addOption('title', abbr: 't', help: 'New title.')
|
|
..addOption('type', help: 'New ticket type.')
|
|
..addOption('column', abbr: 'c', help: 'New column.')
|
|
..addOption('body', abbr: 'b', help: 'New body (replaces existing body).')
|
|
..addMultiOption(
|
|
'milestone',
|
|
help: 'Replace milestone list (repeatable; omit to leave unchanged).',
|
|
)
|
|
..addMultiOption(
|
|
'label',
|
|
help: 'Replace label list (repeatable; omit to leave unchanged).',
|
|
);
|
|
}
|
|
|
|
@override
|
|
final String name = 'update';
|
|
|
|
@override
|
|
final String description = 'Update one or more fields on an existing kanban ticket.';
|
|
|
|
@override
|
|
final String toolName = 'kanban_update_ticket';
|
|
|
|
@override
|
|
Future<String> callAsTool(Map<String, dynamic> args) async {
|
|
final id = (args['id'] as String).toUpperCase();
|
|
final title = args['title'] as String?;
|
|
final typeId = args['type'] as String?;
|
|
final column = args['column'] as String?;
|
|
final body = args['body'] as String?;
|
|
final rawMilestones = args['milestone'] as List?;
|
|
final milestones = rawMilestones != null && rawMilestones.isNotEmpty
|
|
? rawMilestones.cast<String>().where((s) => s.isNotEmpty).toList()
|
|
: null;
|
|
final rawLabels = args['label'] as List?;
|
|
final labels = rawLabels != null && rawLabels.isNotEmpty
|
|
? rawLabels.cast<String>().where((s) => s.isNotEmpty).toList()
|
|
: null;
|
|
|
|
if (title == null &&
|
|
typeId == null &&
|
|
column == null &&
|
|
body == null &&
|
|
milestones == null &&
|
|
labels == null) {
|
|
throw ArgumentError(
|
|
'At least one of --title, --type, --column, --body, --milestone, --label must be specified.',
|
|
);
|
|
}
|
|
|
|
final context = await ProjectContext.find(fs: _fs);
|
|
final config = context.config.kanban;
|
|
|
|
if (typeId != null && !config.ticketTypes.any((t) => t.id == typeId)) {
|
|
throw ArgumentError(
|
|
'Unknown type "$typeId". '
|
|
'Valid: ${config.ticketTypes.map((t) => t.id).join(', ')}',
|
|
);
|
|
}
|
|
if (column != null && !config.columns.any((c) => c.id == column)) {
|
|
throw ArgumentError(
|
|
'Unknown column "$column". '
|
|
'Valid: ${config.columns.map((c) => c.id).join(', ')}',
|
|
);
|
|
}
|
|
|
|
final store = TicketStore(
|
|
kanbanDir: context.dirs.kanban,
|
|
prefix: config.prefix,
|
|
fs: context.fs,
|
|
);
|
|
final ticket = await store.update(
|
|
id,
|
|
title: title,
|
|
type: typeId,
|
|
column: column,
|
|
body: body,
|
|
milestones: milestones,
|
|
labels: labels,
|
|
);
|
|
return 'Updated ${ticket.id}.';
|
|
}
|
|
}
|