From 53f949336445e929ef90ddb318065c469f400ae7 Mon Sep 17 00:00:00 2001 From: Chris Hendrickson Date: Thu, 23 Apr 2026 23:15:38 -0400 Subject: [PATCH] Add ProjectDirs extension pattern and injectable clock - 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> --- packages/core/lib/src/config.dart | 15 +++++++++++++++ .../lib/src/commands/add_comment_command.dart | 3 +-- .../kanban/lib/src/commands/archive_command.dart | 4 ++-- .../kanban/lib/src/commands/board_command.dart | 3 +-- .../kanban/lib/src/commands/create_command.dart | 3 +-- .../kanban/lib/src/commands/delete_command.dart | 3 +-- packages/kanban/lib/src/commands/get_command.dart | 3 +-- .../kanban/lib/src/commands/link_command.dart | 3 +-- .../kanban/lib/src/commands/list_command.dart | 3 +-- .../kanban/lib/src/commands/move_command.dart | 3 +-- .../kanban/lib/src/commands/search_command.dart | 3 +-- .../kanban/lib/src/commands/stats_command.dart | 3 +-- .../lib/src/commands/unarchive_command.dart | 4 ++-- .../kanban/lib/src/commands/unlink_command.dart | 3 +-- .../kanban/lib/src/commands/update_command.dart | 3 +-- packages/kanban/lib/src/kanban_config.dart | 7 +++++++ packages/kanban/lib/src/ticket_store.dart | 6 +++++- 17 files changed, 43 insertions(+), 29 deletions(-) diff --git a/packages/core/lib/src/config.dart b/packages/core/lib/src/config.dart index 0931276..dd48aee 100644 --- a/packages/core/lib/src/config.dart +++ b/packages/core/lib/src/config.dart @@ -14,6 +14,18 @@ class DewConfig { factory DewConfig.fromYaml(YamlMap yaml) => DewConfig(raw: yaml); } +/// Path helper for well-known directories under a project root. +/// +/// Feature packages extend this via extension methods to expose their own +/// directories (e.g. [KanbanDirs.kanban]). +class ProjectDirs { + final String _root; + const ProjectDirs(this._root); + + /// `.project/` directory. + String get project => p.join(_root, '.project'); +} + /// Locates the nearest project root and exposes the parsed [DewConfig]. class ProjectContext { final String root; @@ -22,6 +34,9 @@ class ProjectContext { const ProjectContext({required this.root, required this.config, required this.fs}); + /// Typed path helpers for this project's well-known directories. + ProjectDirs get dirs => ProjectDirs(root); + /// Walks up from [from] (defaults to [fs.currentDirectory]) until a /// `.project/dew.yaml` is found. static Future find({ diff --git a/packages/kanban/lib/src/commands/add_comment_command.dart b/packages/kanban/lib/src/commands/add_comment_command.dart index 2436dea..eee0376 100644 --- a/packages/kanban/lib/src/commands/add_comment_command.dart +++ b/packages/kanban/lib/src/commands/add_comment_command.dart @@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; import '../kanban_config.dart'; -import 'package:path/path.dart' as p; import '../ticket_store.dart'; @@ -31,7 +30,7 @@ class AddCommentCommand extends DewCommand with DewToolCommand { final context = await ProjectContext.find(fs: _fs); final store = TicketStore( - kanbanDir: p.join(context.root, '.project', 'kanban'), + kanbanDir: context.dirs.kanban, prefix: context.config.kanban.prefix, fs: context.fs, ); diff --git a/packages/kanban/lib/src/commands/archive_command.dart b/packages/kanban/lib/src/commands/archive_command.dart index b22fe20..53360f2 100644 --- a/packages/kanban/lib/src/commands/archive_command.dart +++ b/packages/kanban/lib/src/commands/archive_command.dart @@ -1,8 +1,8 @@ import 'package:dew_core/dew_core.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; -import '../kanban_config.dart'; import 'package:path/path.dart' as p; +import '../kanban_config.dart'; import '../ticket_store.dart'; @@ -28,7 +28,7 @@ class ArchiveCommand extends DewCommand with DewToolCommand { final context = await ProjectContext.find(fs: _fs); final config = context.config.kanban; - final kanbanDir = p.join(context.root, '.project', 'kanban'); + final kanbanDir = context.dirs.kanban; final store = TicketStore(kanbanDir: kanbanDir, prefix: config.prefix, fs: context.fs); final ticket = await store.findById(id); diff --git a/packages/kanban/lib/src/commands/board_command.dart b/packages/kanban/lib/src/commands/board_command.dart index 596adc3..54f3a8d 100644 --- a/packages/kanban/lib/src/commands/board_command.dart +++ b/packages/kanban/lib/src/commands/board_command.dart @@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; import '../kanban_config.dart'; -import 'package:path/path.dart' as p; import '../ticket.dart'; import '../ticket_store.dart'; @@ -35,7 +34,7 @@ class BoardCommand extends DewCommand with DewToolCommand { final context = await ProjectContext.find(fs: _fs); final config = context.config.kanban; final store = TicketStore( - kanbanDir: p.join(context.root, '.project', 'kanban'), + kanbanDir: context.dirs.kanban, prefix: config.prefix, fs: context.fs, ); diff --git a/packages/kanban/lib/src/commands/create_command.dart b/packages/kanban/lib/src/commands/create_command.dart index e672752..6b1b7ac 100644 --- a/packages/kanban/lib/src/commands/create_command.dart +++ b/packages/kanban/lib/src/commands/create_command.dart @@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; import '../kanban_config.dart'; -import 'package:path/path.dart' as p; import '../ticket_store.dart'; @@ -67,7 +66,7 @@ class CreateCommand extends DewCommand with DewToolCommand { } final store = TicketStore( - kanbanDir: p.join(context.root, '.project', 'kanban'), + kanbanDir: context.dirs.kanban, prefix: config.prefix, fs: context.fs, ); diff --git a/packages/kanban/lib/src/commands/delete_command.dart b/packages/kanban/lib/src/commands/delete_command.dart index a1a82c3..49e2d98 100644 --- a/packages/kanban/lib/src/commands/delete_command.dart +++ b/packages/kanban/lib/src/commands/delete_command.dart @@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; import '../kanban_config.dart'; -import 'package:path/path.dart' as p; import '../ticket_store.dart'; @@ -32,7 +31,7 @@ class DeleteCommand extends DewCommand with DewToolCommand { final id = (args['id'] as String).toUpperCase(); final context = await ProjectContext.find(fs: _fs); final store = TicketStore( - kanbanDir: p.join(context.root, '.project', 'kanban'), + kanbanDir: context.dirs.kanban, prefix: context.config.kanban.prefix, fs: context.fs, ); diff --git a/packages/kanban/lib/src/commands/get_command.dart b/packages/kanban/lib/src/commands/get_command.dart index 56a20c2..a256aba 100644 --- a/packages/kanban/lib/src/commands/get_command.dart +++ b/packages/kanban/lib/src/commands/get_command.dart @@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; import '../kanban_config.dart'; -import 'package:path/path.dart' as p; import '../ticket.dart'; import '../ticket_store.dart'; @@ -33,7 +32,7 @@ class GetCommand extends DewCommand with DewToolCommand { final id = (args['id'] as String).toUpperCase(); final context = await ProjectContext.find(fs: _fs); final store = TicketStore( - kanbanDir: p.join(context.root, '.project', 'kanban'), + kanbanDir: context.dirs.kanban, prefix: context.config.kanban.prefix, fs: context.fs, ); diff --git a/packages/kanban/lib/src/commands/link_command.dart b/packages/kanban/lib/src/commands/link_command.dart index 4b4e6db..b4bd8cf 100644 --- a/packages/kanban/lib/src/commands/link_command.dart +++ b/packages/kanban/lib/src/commands/link_command.dart @@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; import '../kanban_config.dart'; -import 'package:path/path.dart' as p; import '../ticket.dart'; import '../ticket_store.dart'; @@ -44,7 +43,7 @@ class LinkCommand extends DewCommand with DewToolCommand { final context = await ProjectContext.find(fs: _fs); final store = TicketStore( - kanbanDir: p.join(context.root, '.project', 'kanban'), + kanbanDir: context.dirs.kanban, prefix: context.config.kanban.prefix, fs: context.fs, ); diff --git a/packages/kanban/lib/src/commands/list_command.dart b/packages/kanban/lib/src/commands/list_command.dart index f2a573a..912daa1 100644 --- a/packages/kanban/lib/src/commands/list_command.dart +++ b/packages/kanban/lib/src/commands/list_command.dart @@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; import '../kanban_config.dart'; -import 'package:path/path.dart' as p; import '../ticket.dart'; import '../ticket_store.dart'; @@ -38,7 +37,7 @@ class ListCommand extends DewCommand with DewToolCommand { final context = await ProjectContext.find(fs: _fs); final store = TicketStore( - kanbanDir: p.join(context.root, '.project', 'kanban'), + kanbanDir: context.dirs.kanban, prefix: context.config.kanban.prefix, fs: context.fs, ); diff --git a/packages/kanban/lib/src/commands/move_command.dart b/packages/kanban/lib/src/commands/move_command.dart index ec7b12f..587035a 100644 --- a/packages/kanban/lib/src/commands/move_command.dart +++ b/packages/kanban/lib/src/commands/move_command.dart @@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; import '../kanban_config.dart'; -import 'package:path/path.dart' as p; import '../ticket_store.dart'; @@ -40,7 +39,7 @@ class MoveCommand extends DewCommand with DewToolCommand { } final store = TicketStore( - kanbanDir: p.join(context.root, '.project', 'kanban'), + kanbanDir: context.dirs.kanban, prefix: config.prefix, fs: context.fs, ); diff --git a/packages/kanban/lib/src/commands/search_command.dart b/packages/kanban/lib/src/commands/search_command.dart index 5b3f198..e7d3c50 100644 --- a/packages/kanban/lib/src/commands/search_command.dart +++ b/packages/kanban/lib/src/commands/search_command.dart @@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; import '../kanban_config.dart'; -import 'package:path/path.dart' as p; import '../ticket_store.dart'; @@ -44,7 +43,7 @@ class SearchCommand extends DewCommand with DewToolCommand { final context = await ProjectContext.find(fs: _fs); final store = TicketStore( - kanbanDir: p.join(context.root, '.project', 'kanban'), + kanbanDir: context.dirs.kanban, prefix: context.config.kanban.prefix, fs: context.fs, ); diff --git a/packages/kanban/lib/src/commands/stats_command.dart b/packages/kanban/lib/src/commands/stats_command.dart index 0e45bb9..3cd45d8 100644 --- a/packages/kanban/lib/src/commands/stats_command.dart +++ b/packages/kanban/lib/src/commands/stats_command.dart @@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; import '../kanban_config.dart'; -import 'package:path/path.dart' as p; import '../ticket_store.dart'; @@ -24,7 +23,7 @@ class StatsCommand extends DewCommand with DewToolCommand { final context = await ProjectContext.find(fs: _fs); final config = context.config.kanban; final store = TicketStore( - kanbanDir: p.join(context.root, '.project', 'kanban'), + kanbanDir: context.dirs.kanban, prefix: config.prefix, fs: context.fs, ); diff --git a/packages/kanban/lib/src/commands/unarchive_command.dart b/packages/kanban/lib/src/commands/unarchive_command.dart index fd0eb9b..8bd0af7 100644 --- a/packages/kanban/lib/src/commands/unarchive_command.dart +++ b/packages/kanban/lib/src/commands/unarchive_command.dart @@ -1,8 +1,8 @@ import 'package:dew_core/dew_core.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; -import '../kanban_config.dart'; import 'package:path/path.dart' as p; +import '../kanban_config.dart'; import '../ticket_store.dart'; @@ -34,7 +34,7 @@ class UnarchiveCommand extends DewCommand with DewToolCommand { final context = await ProjectContext.find(fs: _fs); final config = context.config.kanban; - final kanbanDir = p.join(context.root, '.project', 'kanban'); + final kanbanDir = context.dirs.kanban; final store = TicketStore(kanbanDir: kanbanDir, prefix: config.prefix, fs: context.fs); final ticket = await store.findById(id); diff --git a/packages/kanban/lib/src/commands/unlink_command.dart b/packages/kanban/lib/src/commands/unlink_command.dart index 926349c..46aefe5 100644 --- a/packages/kanban/lib/src/commands/unlink_command.dart +++ b/packages/kanban/lib/src/commands/unlink_command.dart @@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; import '../kanban_config.dart'; -import 'package:path/path.dart' as p; import '../ticket_store.dart'; @@ -31,7 +30,7 @@ class UnlinkCommand extends DewCommand with DewToolCommand { final context = await ProjectContext.find(fs: _fs); final store = TicketStore( - kanbanDir: p.join(context.root, '.project', 'kanban'), + kanbanDir: context.dirs.kanban, prefix: context.config.kanban.prefix, fs: context.fs, ); diff --git a/packages/kanban/lib/src/commands/update_command.dart b/packages/kanban/lib/src/commands/update_command.dart index 7caf981..32b6890 100644 --- a/packages/kanban/lib/src/commands/update_command.dart +++ b/packages/kanban/lib/src/commands/update_command.dart @@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; import '../kanban_config.dart'; -import 'package:path/path.dart' as p; import '../ticket_store.dart'; @@ -79,7 +78,7 @@ class UpdateCommand extends DewCommand with DewToolCommand { } final store = TicketStore( - kanbanDir: p.join(context.root, '.project', 'kanban'), + kanbanDir: context.dirs.kanban, prefix: config.prefix, fs: context.fs, ); diff --git a/packages/kanban/lib/src/kanban_config.dart b/packages/kanban/lib/src/kanban_config.dart index 0f6ed00..4250b92 100644 --- a/packages/kanban/lib/src/kanban_config.dart +++ b/packages/kanban/lib/src/kanban_config.dart @@ -1,4 +1,5 @@ import 'package:dew_core/dew_core.dart'; +import 'package:path/path.dart' as p; import 'package:yaml/yaml.dart'; class TicketTypeConfig { @@ -63,3 +64,9 @@ extension KanbanDewConfig on DewConfig { ); } } + +/// Extends [ProjectDirs] with the kanban board directory. +extension KanbanDirs on ProjectDirs { + /// Absolute path to `.project/kanban/`. + String get kanban => p.join(project, 'kanban'); +} diff --git a/packages/kanban/lib/src/ticket_store.dart b/packages/kanban/lib/src/ticket_store.dart index e4f7ef3..54197f4 100644 --- a/packages/kanban/lib/src/ticket_store.dart +++ b/packages/kanban/lib/src/ticket_store.dart @@ -9,10 +9,14 @@ class TicketStore { final String prefix; final FileSystem fs; + /// Provides the current time for ticket creation. Injectable for testing. + final DateTime Function() clock; + const TicketStore({ required this.kanbanDir, required this.prefix, this.fs = const LocalFileSystem(), + this.clock = DateTime.now, }); Future create({ @@ -31,7 +35,7 @@ class TicketStore { title: title, type: type, column: column, - created: DateTime.now().toUtc(), + created: clock().toUtc(), body: body, comments: const [], milestones: milestones,