dew init with module init hooks (DEW-0010)
- DewInitHook interface and DewInitOptions added to packages/core/src/init.dart - InitCommand: creates .project/dew.yaml from default template, calls hooks --path/-p option (default '.'), --[no-]gitkeep flag (default true) - CommandRegistry gains initHooks list and registerInitHook() - KanbanInitHook: creates column dirs, archive/, attachments/ under .project/kanban/; adds .gitkeep to freshly-created dirs when enabled - KanbanInitHook registered in kanban.registerCommands() - CLI wires InitCommand(registry.initHooks) as a top-level command Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
198d65b7e2
commit
31f9ba4726
9 changed files with 176 additions and 1 deletions
|
|
@ -11,6 +11,7 @@ Future<void> main(List<String> args) async {
|
||||||
|
|
||||||
final runner = CommandRunner<void>('dew', 'A project management tool.');
|
final runner = CommandRunner<void>('dew', 'A project management tool.');
|
||||||
|
|
||||||
|
runner.addCommand(InitCommand(commandRegistry.initHooks));
|
||||||
for (final command in commandRegistry.commands) {
|
for (final command in commandRegistry.commands) {
|
||||||
runner.addCommand(command);
|
runner.addCommand(command);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,4 @@ library;
|
||||||
|
|
||||||
export 'src/config.dart';
|
export 'src/config.dart';
|
||||||
export 'src/dew_core_base.dart';
|
export 'src/dew_core_base.dart';
|
||||||
|
export 'src/init.dart';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import 'package:args/args.dart';
|
import 'package:args/args.dart';
|
||||||
import 'package:args/command_runner.dart';
|
import 'package:args/command_runner.dart';
|
||||||
|
|
||||||
|
import 'init.dart';
|
||||||
|
|
||||||
typedef McpToolHandler = Future<String> Function(Map<String, dynamic> args);
|
typedef McpToolHandler = Future<String> Function(Map<String, dynamic> args);
|
||||||
|
|
||||||
/// A single tool exposed to an MCP client.
|
/// A single tool exposed to an MCP client.
|
||||||
|
|
@ -136,13 +138,20 @@ mixin DewToolCommand on DewCommand {
|
||||||
/// [CommandRunner].
|
/// [CommandRunner].
|
||||||
class CommandRegistry {
|
class CommandRegistry {
|
||||||
final List<DewCommand> _commands = [];
|
final List<DewCommand> _commands = [];
|
||||||
|
final List<DewInitHook> _initHooks = [];
|
||||||
|
|
||||||
/// Adds [command] to the registry.
|
/// Adds [command] to the registry.
|
||||||
void register(DewCommand command) => _commands.add(command);
|
void register(DewCommand command) => _commands.add(command);
|
||||||
|
|
||||||
|
/// Registers an [DewInitHook] to be called during `dew init`.
|
||||||
|
void registerInitHook(DewInitHook hook) => _initHooks.add(hook);
|
||||||
|
|
||||||
/// An unmodifiable view of all registered commands.
|
/// An unmodifiable view of all registered commands.
|
||||||
List<DewCommand> get commands => List.unmodifiable(_commands);
|
List<DewCommand> get commands => List.unmodifiable(_commands);
|
||||||
|
|
||||||
|
/// An unmodifiable view of all registered init hooks.
|
||||||
|
List<DewInitHook> get initHooks => List.unmodifiable(_initHooks);
|
||||||
|
|
||||||
/// Collects all [McpTool]s from commands that mix in [DewToolCommand],
|
/// Collects all [McpTool]s from commands that mix in [DewToolCommand],
|
||||||
/// recursively including subcommands.
|
/// recursively including subcommands.
|
||||||
List<McpTool> get mcpTools {
|
List<McpTool> get mcpTools {
|
||||||
|
|
|
||||||
118
packages/core/lib/src/init.dart
Normal file
118
packages/core/lib/src/init.dart
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:args/command_runner.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:yaml/yaml.dart';
|
||||||
|
|
||||||
|
import 'config.dart';
|
||||||
|
|
||||||
|
/// Options passed to every [DewInitHook] during `dew init`.
|
||||||
|
class DewInitOptions {
|
||||||
|
/// Whether to create `.gitkeep` files in newly-created empty directories.
|
||||||
|
final bool gitkeep;
|
||||||
|
|
||||||
|
const DewInitOptions({this.gitkeep = true});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates the initial `.project/` scaffold for `dew init`.
|
||||||
|
///
|
||||||
|
/// Each module (kanban, mcp, etc.) registers a hook so it can create its own
|
||||||
|
/// subdirectories and config alongside the core scaffold.
|
||||||
|
abstract interface class DewInitHook {
|
||||||
|
/// Called after `dew.yaml` is written. [projectRoot] is the resolved absolute
|
||||||
|
/// path of the project being initialised; [config] is the loaded config;
|
||||||
|
/// [options] carries flags like [DewInitOptions.gitkeep].
|
||||||
|
Future<void> onInit(
|
||||||
|
String projectRoot,
|
||||||
|
DewConfig config,
|
||||||
|
DewInitOptions options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const _defaultDewYaml = '''
|
||||||
|
dew:
|
||||||
|
mcp:
|
||||||
|
host: "localhost"
|
||||||
|
port: 8080
|
||||||
|
|
||||||
|
kanban:
|
||||||
|
prefix: "PROJ"
|
||||||
|
ticket_types:
|
||||||
|
- id: "epic"
|
||||||
|
name: "Epic"
|
||||||
|
- id: "story"
|
||||||
|
name: "Story"
|
||||||
|
- id: "task"
|
||||||
|
name: "Task"
|
||||||
|
- id: "bug"
|
||||||
|
name: "Bug"
|
||||||
|
- id: "spike"
|
||||||
|
name: "Spike"
|
||||||
|
columns:
|
||||||
|
- id: "backlog"
|
||||||
|
name: "Backlog"
|
||||||
|
color: "blue"
|
||||||
|
- id: "doing"
|
||||||
|
name: "Doing"
|
||||||
|
color: "yellow"
|
||||||
|
- id: "done"
|
||||||
|
name: "Done"
|
||||||
|
color: "green"
|
||||||
|
''';
|
||||||
|
|
||||||
|
class InitCommand extends Command<void> {
|
||||||
|
final List<DewInitHook> _hooks;
|
||||||
|
|
||||||
|
InitCommand(this._hooks) {
|
||||||
|
argParser
|
||||||
|
..addOption(
|
||||||
|
'path',
|
||||||
|
abbr: 'p',
|
||||||
|
help: 'Path to the project root to initialise.',
|
||||||
|
defaultsTo: '.',
|
||||||
|
)
|
||||||
|
..addFlag(
|
||||||
|
'gitkeep',
|
||||||
|
help: 'Add .gitkeep files to newly-created empty directories.',
|
||||||
|
defaultsTo: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String name = 'init';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String description =
|
||||||
|
'Initialise a Dew project at the given path, creating '
|
||||||
|
'.project/dew.yaml and scaffolding module directories.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> run() async {
|
||||||
|
final rawPath = argResults!['path'] as String;
|
||||||
|
final gitkeep = argResults!['gitkeep'] as bool;
|
||||||
|
final projectRoot = p.canonicalize(rawPath);
|
||||||
|
final options = DewInitOptions(gitkeep: gitkeep);
|
||||||
|
|
||||||
|
final projectDir = Directory(p.join(projectRoot, '.project'));
|
||||||
|
final configFile = File(p.join(projectDir.path, 'dew.yaml'));
|
||||||
|
|
||||||
|
await projectDir.create(recursive: true);
|
||||||
|
|
||||||
|
if (await configFile.exists()) {
|
||||||
|
print(' found .project/dew.yaml (already exists, skipping)');
|
||||||
|
} else {
|
||||||
|
await configFile.writeAsString(_defaultDewYaml.trimLeft());
|
||||||
|
print(' created .project/dew.yaml');
|
||||||
|
}
|
||||||
|
|
||||||
|
final config = DewConfig.fromYaml(
|
||||||
|
loadYaml(await configFile.readAsString()) as YamlMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (final hook in _hooks) {
|
||||||
|
await hook.onInit(projectRoot, config, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
print('\nProject initialised at $projectRoot');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,13 +2,16 @@ library;
|
||||||
|
|
||||||
export 'src/dew_kanban_base.dart';
|
export 'src/dew_kanban_base.dart';
|
||||||
export 'src/kanban_config.dart';
|
export 'src/kanban_config.dart';
|
||||||
|
export 'src/kanban_init_hook.dart';
|
||||||
export 'src/ticket.dart';
|
export 'src/ticket.dart';
|
||||||
export 'src/ticket_store.dart';
|
export 'src/ticket_store.dart';
|
||||||
|
|
||||||
import 'package:dew_core/dew_core.dart';
|
import 'package:dew_core/dew_core.dart';
|
||||||
import 'package:dew_kanban/src/dew_kanban_base.dart';
|
import 'package:dew_kanban/src/dew_kanban_base.dart';
|
||||||
|
import 'package:dew_kanban/src/kanban_init_hook.dart';
|
||||||
|
|
||||||
/// Registers all Kanban commands into [registry].
|
/// Registers all Kanban commands and init hooks into [registry].
|
||||||
void registerCommands(CommandRegistry registry) {
|
void registerCommands(CommandRegistry registry) {
|
||||||
registry.register(KanbanCommand());
|
registry.register(KanbanCommand());
|
||||||
|
registry.registerInitHook(KanbanInitHook());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
43
packages/kanban/lib/src/kanban_init_hook.dart
Normal file
43
packages/kanban/lib/src/kanban_init_hook.dart
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:dew_core/dew_core.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
import 'kanban_config.dart';
|
||||||
|
|
||||||
|
class KanbanInitHook implements DewInitHook {
|
||||||
|
@override
|
||||||
|
Future<void> onInit(
|
||||||
|
String projectRoot,
|
||||||
|
DewConfig config,
|
||||||
|
DewInitOptions options,
|
||||||
|
) async {
|
||||||
|
final kanbanConfig = config.kanban;
|
||||||
|
final kanbanRoot = p.join(projectRoot, '.project', 'kanban');
|
||||||
|
|
||||||
|
// Column directories.
|
||||||
|
for (final column in kanbanConfig.columns) {
|
||||||
|
await _createDir(p.join(kanbanRoot, column.id), options.gitkeep);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Archive and attachments directories.
|
||||||
|
await _createDir(p.join(kanbanRoot, 'archive'), options.gitkeep);
|
||||||
|
await _createDir(p.join(kanbanRoot, 'attachments'), options.gitkeep);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _createDir(String path, bool gitkeep) async {
|
||||||
|
final dir = Directory(path);
|
||||||
|
final existed = await dir.exists();
|
||||||
|
await dir.create(recursive: true);
|
||||||
|
final rel = '.project/kanban/${p.basename(path)}';
|
||||||
|
if (existed) {
|
||||||
|
print(' found $rel/');
|
||||||
|
} else {
|
||||||
|
print(' created $rel/');
|
||||||
|
if (gitkeep) {
|
||||||
|
await File(p.join(path, '.gitkeep')).writeAsString('');
|
||||||
|
print(' created $rel/.gitkeep');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue