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.');
|
||||
|
||||
runner.addCommand(InitCommand(commandRegistry.initHooks));
|
||||
for (final command in commandRegistry.commands) {
|
||||
runner.addCommand(command);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,3 +2,4 @@ library;
|
|||
|
||||
export 'src/config.dart';
|
||||
export 'src/dew_core_base.dart';
|
||||
export 'src/init.dart';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import 'package:args/args.dart';
|
||||
import 'package:args/command_runner.dart';
|
||||
|
||||
import 'init.dart';
|
||||
|
||||
typedef McpToolHandler = Future<String> Function(Map<String, dynamic> args);
|
||||
|
||||
/// A single tool exposed to an MCP client.
|
||||
|
|
@ -136,13 +138,20 @@ mixin DewToolCommand on DewCommand {
|
|||
/// [CommandRunner].
|
||||
class CommandRegistry {
|
||||
final List<DewCommand> _commands = [];
|
||||
final List<DewInitHook> _initHooks = [];
|
||||
|
||||
/// Adds [command] to the registry.
|
||||
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.
|
||||
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],
|
||||
/// recursively including subcommands.
|
||||
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/kanban_config.dart';
|
||||
export 'src/kanban_init_hook.dart';
|
||||
export 'src/ticket.dart';
|
||||
export 'src/ticket_store.dart';
|
||||
|
||||
import 'package:dew_core/dew_core.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) {
|
||||
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