diff --git a/lib/src/cli/cli_runner.dart b/lib/src/cli/cli_runner.dart index d4ba4129..0a6f8107 100644 --- a/lib/src/cli/cli_runner.dart +++ b/lib/src/cli/cli_runner.dart @@ -18,6 +18,7 @@ import '../commands/manifest_command.dart'; import '../commands/apply_command.dart'; import '../commands/module_command.dart'; import '../commands/xray_command.dart'; +import '../commands/setup_command.dart'; import '../core/plugin_system/cli_aware_plugin.dart'; import '../core/plugin_system/plugin_registry.dart'; import '../core/error/suggestion_engine.dart'; @@ -85,6 +86,7 @@ class CliRunner { _runner.addCommand(ModuleCommand()); _runner.addCommand(XrayCommand()); _runner.addCommand(UpdateCommand()); + _runner.addCommand(SetupCommand()); } /// Run CLI with arguments. @@ -216,16 +218,21 @@ zfa - Zuraffa Code Generator v$version USAGE: zfa [options] +BOOTSTRAP: + setup Create a new Flutter/Dart app with zuraffa deps wired in + init Wire zuraffa dependencies + scaffold a test entity + CORE COMMANDS: make Canonical architecture/code generation command feature Wrapper over `make --preset=feature` - initialize Initialize a test entity + initialize Alias of init — wire deps + scaffold a test entity entity Create and manage Zorphy entities config Manage ZFA configuration doctor Check your environment and v5 migration readiness schema Output JSON schema validate Validate JSON configuration migrate Migrate v5 artifacts to v6 (state, gql, di) + build Run build_runner to generate code from annotations update Check for updates and update the installed CLI MODULAR COMMANDS: @@ -294,11 +301,18 @@ class _InitializeCommand extends Command { String get name => 'initialize'; @override - String get description => 'Initialize a test entity'; + List get aliases => ['init']; + + @override + String get description => + 'Wire zuraffa dependencies into pubspec.yaml + scaffold a test entity'; + + @override + ArgParser get argParser => ArgParser.allowAnything(); @override Future run() async { - await init.InitializeCommand().execute(argResults!.rest.toList()); + await init.InitializeCommand().execute(argResults!.arguments); } } diff --git a/lib/src/commands/initialize_command.dart b/lib/src/commands/initialize_command.dart index 46629cde..66060b66 100644 --- a/lib/src/commands/initialize_command.dart +++ b/lib/src/commands/initialize_command.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:args/args.dart'; import 'package:path/path.dart' as path; import '../config/zfa_config.dart'; +import '../core/dependencies/dependency_wirer.dart'; import '../utils/file_utils.dart'; import '../utils/string_utils.dart'; @@ -34,6 +35,18 @@ class InitializeCommand { help: 'Preview what would be generated without writing files', negatable: false, ) + ..addFlag( + 'deps-only', + negatable: false, + help: + 'Only wire zuraffa dependencies into pubspec.yaml; skip entity scaffolding.', + ) + ..addFlag( + 'no-deps', + negatable: false, + help: + 'Skip dependency wiring; only scaffold the test entity (legacy behavior).', + ) ..addFlag( 'verbose', abbr: 'v', @@ -53,7 +66,78 @@ class InitializeCommand { final force = results['force'] as bool; final dryRun = results['dry-run'] as bool; final verbose = results['verbose'] as bool; + final depsOnly = results['deps-only'] as bool; + final noDeps = results['no-deps'] as bool; + + if (depsOnly && noDeps) { + print('āŒ --deps-only and --no-deps are mutually exclusive.'); + exit(1); + } + + // --- Dependency wiring (issue #275) ----------------------------------- + // `zfa init` / `zfa initialize` now wires the standard zuraffa dependency + // set (build_runner, zuraffa[_flutter], zorphy_annotation, analyzer + // override) into pubspec.yaml before scaffolding the test entity. This + // makes the "only zfa commands" contract viable on a fresh project. + if (!noDeps) { + final pubspecFile = File('pubspec.yaml'); + if (!pubspecFile.existsSync()) { + print('āŒ No pubspec.yaml found in current directory.'); + print( + ' Run `zfa setup ` to create a new app, or cd to a project root.', + ); + exit(1); + } + + final pubspecContent = pubspecFile.readAsStringSync(); + final isFlutter = DependencyWirer.isFlutterProject(pubspecContent); + + print('šŸ”§ Wiring zuraffa dependencies' + '${isFlutter ? ' (Flutter project)' : ' (Dart project)'}...\n'); + final wireResult = await DependencyWirer.wire( + isFlutter: isFlutter, + dryRun: dryRun, + projectRoot: '.', + ); + + if (!wireResult.isSuccess) { + print( + '\nāš ļø Some dependencies could not be wired automatically: ' + '${wireResult.failed.join(', ')}', + ); + print(' Add them manually and re-run `zfa init`.'); + } + // Ensure build.yaml + domain directory structure exist. + print(''); + await DependencyWirer.ensureProjectStructure(dryRun: dryRun); + + // Ensure .zfa.json exists. + final config = ZfaConfig.load(); + if (config == null) { + print(''); + if (dryRun) { + print('šŸ” Would create: .zfa.json (default configuration)'); + } else { + await ZfaConfig.init(); + } + } + print(''); + } + + if (depsOnly) { + if (dryRun) { + print('šŸ” Dry-run: would skip entity scaffolding (--deps-only).'); + } else { + print('āœ… Dependencies wired. Skipping entity scaffolding (--deps-only).'); + } + print('\nšŸ“ Next steps:'); + print(' • Create an entity: zfa entity create -n Product --field id:String'); + print(' • Generate feature: zfa make Product --preset=crud --with=vpc,state,di,test'); + return; + } + + // --- Entity scaffolding (existing behavior) --------------------------- final entitySnake = StringUtils.camelToSnake(entityName); // Create entity directory path @@ -94,7 +178,7 @@ class InitializeCommand { void _printHelp(ArgParser parser) { print(''' -Initialize a test entity to quickly try out Zuraffa +Initialize a project for Zuraffa: wire dependencies + scaffold a test entity USAGE: zfa initialize [options] @@ -104,18 +188,24 @@ OPTIONS: ${parser.usage} EXAMPLES: - zfa initialize # Generate Product entity - zfa initialize --entity=User # Generate User entity - zfa init -e Order # Generate Order entity + zfa initialize # Wire deps + generate Product entity + zfa init # Same as above (alias) + zfa initialize --entity=User # Wire deps + generate User entity + zfa init --deps-only # Wire deps only, skip entity + zfa init --no-deps -e Order # Skip deps, only scaffold entity zfa initialize --dry-run # Preview without writing files DESCRIPTION: - Creates a sample entity with common fields (id, name, description, price, etc.) - under lib/src/domain/entities to help you quickly test Zuraffa's code generation - capabilities. + Wires the standard zuraffa dependency set (build_runner, zuraffa/zuraffa_flutter, + zorphy_annotation, analyzer override) into pubspec.yaml, creates build.yaml with + zorphy builder registration, ensures .zfa.json exists, then creates a sample + entity with common fields under lib/src/domain/entities. + + For a brand-new app, prefer `zfa setup ` which runs flutter/dart create + AND wires dependencies in one step. - After running this command, use 'zfa make' to create the full Clean Architecture - structure around your entity. + Use --deps-only to wire dependencies without scaffolding an entity. + Use --no-deps to scaffold only the entity (legacy behavior). '''); } diff --git a/lib/src/commands/setup_command.dart b/lib/src/commands/setup_command.dart new file mode 100644 index 00000000..9456b529 --- /dev/null +++ b/lib/src/commands/setup_command.dart @@ -0,0 +1,273 @@ +import 'dart:io'; + +import 'package:args/command_runner.dart'; + +import '../config/zfa_config.dart'; +import '../core/dependencies/dependency_wirer.dart'; + +/// `zfa setup ` — Bootstrap a new Flutter/Dart app with the standard +/// zuraffa dependency set wired in. +/// +/// This is the app-creation flow that `zfa init`/`zfa initialize` deliberately +/// does not provide: it runs `flutter create` (or `dart create`), wires the +/// zuraffa dependency set into the new project's pubspec.yaml, creates +/// `build.yaml` with zorphy builder registration, writes a default `.zfa.json`, +/// and scaffolds the domain directory structure. +/// +/// Usage: +/// `zfa setup [--flutter] [--dart] [--platforms=ios,macos] [--org=com.example] [--dry-run] [--force]` +class SetupCommand extends Command { + @override + final String name = 'setup'; + + @override + final String description = + 'Bootstrap a new Flutter/Dart app with zuraffa dependencies wired in'; + + @override + String get invocation => 'zfa setup [options]'; + + SetupCommand() { + argParser.addFlag( + 'flutter', + negatable: false, + help: 'Create a Flutter app (default). Passes --platforms through to flutter create.', + ); + argParser.addFlag( + 'dart', + negatable: false, + help: 'Create a pure Dart package (dart create -t package).', + ); + argParser.addOption( + 'platforms', + valueHelp: 'ios,macos', + help: 'Comma-separated platforms for `flutter create` (ignored with --dart).', + ); + argParser.addOption( + 'org', + valueHelp: 'com.example', + help: 'Organization name for `flutter create` (e.g. com.example).', + ); + argParser.addFlag( + 'dry-run', + negatable: false, + help: 'Preview what would be created/wired without writing files.', + ); + argParser.addFlag( + 'force', + abbr: 'f', + negatable: false, + help: 'Overwrite the target directory if it already exists.', + ); + argParser.addFlag( + 'verbose', + abbr: 'v', + negatable: false, + help: 'Enable verbose output.', + ); + } + + @override + Future run() async { + final rest = argResults!.rest; + if (rest.isEmpty) { + usageException('App name is required: zfa setup '); + } + final appName = rest.first; + + final wantDart = argResults!['dart'] as bool; + final wantFlutter = argResults!['flutter'] as bool; + if (wantDart && wantFlutter) { + usageException('Pass either --flutter or --dart, not both.'); + } + final isFlutter = !wantDart; // default: Flutter + + final platforms = argResults!['platforms'] as String?; + final org = argResults!['org'] as String?; + final dryRun = argResults!['dry-run'] as bool; + final force = argResults!['force'] as bool; + final verbose = argResults!['verbose'] as bool; + + if (_isInvalidAppName(appName)) { + usageException( + 'Invalid app name: "$appName". Use snake_case (lowercase letters, digits, underscores).', + ); + } + + print('\nBootstrap: $appName (${isFlutter ? "Flutter" : "Dart"})'); + print('=' * 40); + + // 1. Create the app (flutter create / dart create). + final created = await _createApp( + appName: appName, + isFlutter: isFlutter, + platforms: platforms, + org: org, + dryRun: dryRun, + force: force, + verbose: verbose, + ); + if (!created) return; + + // 2. Wire the standard zuraffa dependency set (dart pub add + overrides). + print('\n[2/5] Wiring zuraffa dependencies...'); + if (dryRun) { + final missing = DependencyWirer.findMissing( + _dryRunPubspec(appName, isFlutter), + isFlutter: isFlutter, + ); + print(' Would add ${missing.length} dependencies:'); + for (final spec in missing) { + print(' • $spec'); + } + } else { + await DependencyWirer.wire( + isFlutter: isFlutter, + dryRun: false, + projectRoot: appName, + ); + } + + // 3. Create build.yaml + domain directory structure. + print('\n[3/5] Creating build.yaml + domain structure...'); + if (dryRun) { + await DependencyWirer.ensureProjectStructure( + projectRoot: appName, + dryRun: true, + ); + } else { + await DependencyWirer.ensureProjectStructure( + projectRoot: appName, + dryRun: false, + ); + print(' Created: build.yaml + domain directories'); + } + + // 4. Create default .zfa.json in the new project. + print('\n[4/5] Creating .zfa.json...'); + if (dryRun) { + print(' Would create: $appName/.zfa.json'); + } else { + await ZfaConfig.init(projectRoot: appName); + } + + // 5. Summary. + print('\n[5/5] Setup complete!'); + + // Next steps. + print('\n── Next steps ──'); + print(' cd $appName'); + print(' zfa entity create -n Product --field id:String --field name:String'); + print(' zfa make Product --preset=crud --with=vpc,state,di,test'); + print(' zfa build'); + print(''); + if (isFlutter) { + print(' Run the app: flutter run'); + } else { + print(' Run tests: dart test'); + } + } + + /// Runs `flutter create` or `dart create` and returns whether the app was + /// (or would be) created successfully. + Future _createApp({ + required String appName, + required bool isFlutter, + required String? platforms, + required String? org, + required bool dryRun, + required bool force, + required bool verbose, + }) async { + final targetDir = Directory(appName); + if (targetDir.existsSync()) { + if (!force) { + print('āŒ Directory already exists: $appName'); + print(' Use --force to overwrite, or pick a different name.'); + return false; + } + if (!dryRun) { + await targetDir.delete(recursive: true); + print(' Removed existing $appName (--force)'); + } else { + print(' Would remove existing $appName (--force)'); + } + } + + if (isFlutter) { + final args = ['create', '--empty', appName]; + if (platforms != null && platforms.isNotEmpty) { + for (final plat in platforms.split(',')) { + final trimmed = plat.trim(); + if (trimmed.isNotEmpty) { + args.addAll(['--platforms', trimmed]); + } + } + } + if (org != null && org.isNotEmpty) { + args.addAll(['--org', org]); + } + if (dryRun) { + print('\n[1/5] Would run: flutter ${args.join(" ")}'); + return true; + } + print('\n[1/5] Creating Flutter app: $appName' + '${platforms != null ? ' (platforms: $platforms)' : ''}' + '${org != null ? ' (org: $org)' : ''}'); + if (verbose) print(' Running: flutter ${args.join(" ")}'); + final result = await Process.run('flutter', args); + if (result.exitCode != 0) { + final err = result.stderr.toString().trim(); + final out = result.stdout.toString().trim(); + print('āŒ flutter create failed (exit ${result.exitCode}).'); + if (err.isNotEmpty) print(' $err'); + if (out.isNotEmpty) print(' $out'); + print(' Make sure Flutter is installed: https://docs.flutter.dev/get-started/install'); + return false; + } + print(' Created Flutter app: $appName'); + return true; + } + + // Pure Dart package. + final args = ['create', '-t', 'package', appName]; + if (dryRun) { + print('\n[1/5] Would run: dart ${args.join(" ")}'); + return true; + } + print('\n[1/5] Creating Dart package: $appName'); + if (verbose) print(' Running: dart ${args.join(" ")}'); + final result = await Process.run('dart', args); + if (result.exitCode != 0) { + final err = result.stderr.toString().trim(); + final out = result.stdout.toString().trim(); + print('āŒ dart create failed (exit ${result.exitCode}).'); + if (err.isNotEmpty) print(' $err'); + if (out.isNotEmpty) print(' $out'); + return false; + } + print(' Created Dart package: $appName'); + return true; + } + + /// Minimal pubspec for dry-run preview (so findMissing has something to parse). + String _dryRunPubspec(String name, bool isFlutter) { + final flutterDep = isFlutter ? ''' +dependencies: + flutter: + sdk: flutter +''' : ''' +dependencies: +'''; + return ''' +name: $name +environment: + sdk: ^3.11.0 +$flutterDep +'''; + } + + bool _isInvalidAppName(String name) { + return !RegExp(r'^[a-z][a-z0-9_]*$').hasMatch(name); + } +} diff --git a/lib/src/core/dependencies/dependency_wirer.dart b/lib/src/core/dependencies/dependency_wirer.dart new file mode 100644 index 00000000..b37e57d4 --- /dev/null +++ b/lib/src/core/dependencies/dependency_wirer.dart @@ -0,0 +1,439 @@ +import 'dart:io'; + +import 'package:yaml/yaml.dart'; + +/// Kind of dependency entry in pubspec.yaml. +enum DependencyKind { regular, dev, override } + +/// Describes a zuraffa dependency to be wired into a project's pubspec.yaml. +class DependencySpec { + final String name; + final DependencyKind kind; + + /// Git source (when the dependency is fetched from git rather than pub.dev). + final String? gitUrl; + final String? gitPath; + final String? gitRef; + + /// Concrete version for [DependencyKind.override] entries + /// (e.g. `14.1.0` for the analyzer override). + final String? version; + + const DependencySpec({ + required this.name, + required this.kind, + this.gitUrl, + this.gitPath, + this.gitRef, + this.version, + }); + + bool get isGit => gitUrl != null; + bool get isOverride => kind == DependencyKind.override; + + @override + String toString() { + switch (kind) { + case DependencyKind.regular: + return isGit ? '$name (git)' : name; + case DependencyKind.dev: + return 'dev:$name'; + case DependencyKind.override: + return 'override:$name=${version ?? "?"}'; + } + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is DependencySpec && + name == other.name && + kind == other.kind && + gitUrl == other.gitUrl && + gitPath == other.gitPath && + gitRef == other.gitRef && + version == other.version; + + @override + int get hashCode => + Object.hash(name, kind, gitUrl, gitPath, gitRef, version); +} + +/// Result of wiring dependencies into a project. +class WireResult { + /// Names of dependencies that were successfully added. + final List added; + + /// Names of dependencies that were already present (skipped). + final List skipped; + + /// Names of dependencies that could not be added. + final List failed; + + /// Whether this was a dry run (no writes). + final bool dryRun; + + const WireResult({ + this.added = const [], + this.skipped = const [], + this.failed = const [], + this.dryRun = false, + }); + + bool get isSuccess => failed.isEmpty; + bool get didNothing => added.isEmpty && skipped.isNotEmpty; +} + +/// Wires the standard zuraffa dependency set into a project's pubspec.yaml. +/// +/// Used by both `zfa setup` (new app bootstrap) and `zfa init` (existing +/// project dependency wiring) so the two commands stay in sync. +class DependencyWirer { + /// Git source for the zuraffa monorepo (contains both `zuraffa` at the root + /// and `zuraffa_flutter` as a sub-package). + static const zuraffaGitUrl = 'https://github.com/arrrrny/zuraffa'; + + /// Git source for the zorphy monorepo (contains `zorphy` and + /// `zorphy_annotation` sub-packages). + static const zorphyGitUrl = 'https://github.com/arrrrny/zorphy'; + + /// Default git ref — tracks the development branch which is where active + /// v6 work lands before merging to master. + static const defaultGitRef = 'development'; + + /// The analyzer version zuraffa pins (see root pubspec.yaml). Overriding + /// analyzer in downstream apps prevents version-conflict failures when + /// `dart pub get` resolves the transitive graph. + static const analyzerOverrideVersion = '14.1.0'; + + /// Returns the standard zuraffa dependency set for the given project type. + /// + /// [isFlutter] selects `zuraffa_flutter` + `flutter_lints` (Flutter apps) + /// versus `zuraffa` (pure Dart packages). All other entries are shared. + static List standardSet({required bool isFlutter}) { + return [ + DependencySpec( + name: isFlutter ? 'zuraffa_flutter' : 'zuraffa', + kind: DependencyKind.regular, + gitUrl: zuraffaGitUrl, + gitPath: isFlutter ? 'zuraffa_flutter' : null, + gitRef: defaultGitRef, + ), + DependencySpec( + name: 'zorphy_annotation', + kind: DependencyKind.regular, + gitUrl: zorphyGitUrl, + gitPath: 'zorphy_annotation', + gitRef: defaultGitRef, + ), + const DependencySpec(name: 'build_runner', kind: DependencyKind.dev), + const DependencySpec(name: 'mocktail', kind: DependencyKind.dev), + if (isFlutter) + const DependencySpec(name: 'flutter_lints', kind: DependencyKind.dev), + const DependencySpec( + name: 'analyzer', + kind: DependencyKind.override, + version: analyzerOverrideVersion, + ), + ]; + } + + /// Finds which dependencies from [standardSet] are missing from the given + /// pubspec.yaml content. + /// + /// Pure function — no I/O. Callers can unit-test this directly. + static List findMissing( + String pubspecContent, { + required bool isFlutter, + }) { + final specs = standardSet(isFlutter: isFlutter); + final YamlMap pubspec; + try { + pubspec = loadYaml(pubspecContent) as YamlMap; + } catch (_) { + // Unparseable pubspec → treat everything as missing so wire() reports it. + return specs; + } + + final deps = (pubspec['dependencies'] as YamlMap?) ?? YamlMap(); + final devDeps = (pubspec['dev_dependencies'] as YamlMap?) ?? YamlMap(); + final overrides = + (pubspec['dependency_overrides'] as YamlMap?) ?? YamlMap(); + + return specs.where((spec) { + switch (spec.kind) { + case DependencyKind.regular: + return !deps.containsKey(spec.name); + case DependencyKind.dev: + return !devDeps.containsKey(spec.name); + case DependencyKind.override: + return !overrides.containsKey(spec.name); + } + }).toList(); + } + + /// Detects whether the given pubspec.yaml content declares a Flutter + /// dependency (`flutter: sdk: flutter`). + static bool isFlutterProject(String pubspecContent) { + try { + final pubspec = loadYaml(pubspecContent) as YamlMap; + final deps = (pubspec['dependencies'] as YamlMap?) ?? YamlMap(); + return deps.containsKey('flutter'); + } catch (_) { + return false; + } + } + + /// Adds a `dependency_overrides` entry to pubspec.yaml content. + /// + /// - If the `dependency_overrides:` section does not exist, it is appended. + /// - If it exists but [key] is absent, the entry is inserted under it. + /// - If [key] already exists, the content is returned unchanged (idempotent). + /// + /// Pure function — does not perform I/O. + static String addOverrideToPubspec( + String content, + String key, + String value, + ) { + final lines = content.split('\n'); + final overrideRegex = RegExp(r'^dependency_overrides:\s*$'); + final overrideIdx = lines.indexWhere((l) => overrideRegex.hasMatch(l)); + + if (overrideIdx == -1) { + // Append a new dependency_overrides section. + var result = content; + if (!result.endsWith('\n')) { + result = '$result\n'; + } + // Ensure a blank line separates the new section from the previous one. + if (!result.endsWith('\n\n')) { + result = '$result\n'; + } + return '$result dependency_overrides:\n $key: $value\n'.replaceFirst( + ' dependency_overrides', + 'dependency_overrides', + ); + } + + // Walk the existing section looking for the key. + final keyPrefix = ' $key:'; + for (var i = overrideIdx + 1; i < lines.length; i++) { + final line = lines[i]; + // A non-indented, non-empty line means we hit the next top-level key. + if (line.isNotEmpty && !line.startsWith(' ') && !line.startsWith('\t')) { + break; + } + if (line.startsWith(keyPrefix)) { + return content; // already present + } + } + + // Insert immediately after the `dependency_overrides:` header. + lines.insert(overrideIdx + 1, ' $key: $value'); + return lines.join('\n'); + } + + /// Wires missing zuraffa dependencies into the pubspec.yaml at [projectRoot]. + /// + /// Uses `dart pub add` for regular/dev dependencies (preserves pubspec + /// formatting and runs `pub get` atomically) and direct YAML editing for + /// `dependency_overrides` entries (which `dart pub add` does not support). + /// + /// When [dryRun] is true, reports what would be added without writing. + static Future wire({ + required bool isFlutter, + bool dryRun = false, + String? projectRoot, + }) async { + final root = projectRoot ?? Directory.current.path; + final pubspecFile = File('$root/pubspec.yaml'); + + if (!pubspecFile.existsSync()) { + print('āŒ No pubspec.yaml found in $root'); + print( + ' Run `zfa setup ` to create a new app, or cd to a project root.', + ); + return const WireResult(failed: ['pubspec.yaml not found']); + } + + final content = pubspecFile.readAsStringSync(); + final missing = findMissing(content, isFlutter: isFlutter); + + if (missing.isEmpty) { + print('āœ… All zuraffa dependencies are already present.'); + return const WireResult(); + } + + print('šŸ”§ Wiring ${missing.length} missing dependenc${missing.length == 1 ? 'y' : 'ies'}:'); + for (final spec in missing) { + print(' • $spec'); + } + + if (dryRun) { + print('\nšŸ” Dry-run: no changes written. Re-run without --dry-run to apply.'); + return WireResult( + added: missing.map((s) => s.name).toList(), + dryRun: true, + ); + } + + final added = []; + final failed = []; + + // Split: pub-addable (regular + dev) vs override (direct edit). + final pubAddSpecs = missing + .where((s) => s.kind != DependencyKind.override) + .toList(); + final overrideSpecs = missing + .where((s) => s.kind == DependencyKind.override) + .toList(); + + // --- regular / dev deps via `dart pub add` --- + for (final spec in pubAddSpecs) { + final args = _buildPubAddArgs(spec); + try { + final result = await Process.run( + 'dart', + ['pub', 'add', ...args], + workingDirectory: root, + ); + if (result.exitCode == 0) { + added.add(spec.name); + print(' āœ… Added $spec'); + } else { + final err = result.stderr.toString().trim(); + final out = result.stdout.toString().trim(); + print(' āš ļø Failed to add $spec: ${err.isNotEmpty ? err : out}'); + failed.add(spec.name); + } + } catch (e) { + print(' āš ļø Failed to add $spec: $e'); + failed.add(spec.name); + } + } + + // --- dependency_overrides via direct pubspec edit --- + if (overrideSpecs.isNotEmpty) { + var newContent = pubspecFile.readAsStringSync(); + for (final spec in overrideSpecs) { + newContent = addOverrideToPubspec( + newContent, + spec.name, + spec.version ?? '', + ); + added.add(spec.name); + print(' āœ… Added override:${spec.name}=${spec.version}'); + } + await pubspecFile.writeAsString(newContent); + // Re-resolve so the override takes effect. + try { + final getResult = await Process.run( + 'dart', + ['pub', 'get'], + workingDirectory: root, + ); + if (getResult.exitCode != 0) { + final err = getResult.stderr.toString().trim(); + if (err.isNotEmpty) { + print(' āš ļø dart pub get reported issues after override edit:'); + print(' ${err.split('\n').take(3).join('\n ')}'); + } + } + } catch (e) { + // Non-fatal: overrides are written; user can resolve later. + print(' āš ļø Could not run dart pub get after override edit: $e'); + } + } + + return WireResult(added: added, failed: failed); + } + + /// Builds the argument list for `dart pub add` from a [DependencySpec]. + static List _buildPubAddArgs(DependencySpec spec) { + final args = []; + if (spec.kind == DependencyKind.dev) { + args.add('dev:${spec.name}'); + } else { + args.add(spec.name); + } + if (spec.isGit) { + args.add('--git-url=${spec.gitUrl}'); + if (spec.gitPath != null) { + args.add('--git-path=${spec.gitPath}'); + } + if (spec.gitRef != null) { + args.add('--git-ref=${spec.gitRef}'); + } + } + return args; + } + + /// The `build.yaml` content that registers the zorphy + json_serializable + /// builders for the project. Used by `zfa setup` and `zfa init` to ensure + /// `zfa build` (build_runner) picks up `@Zorphy` annotations. + static const buildYamlContent = ''' +targets: + \$default: + builders: + zorphy:zorphy: + enabled: true + generate_for: + - lib/src/** + - test/** + json_serializable: + enabled: true + generate_for: + - lib/src/** + - test/** + options: + explicit_to_json: false + include_if_null: false + generic_argument_factories: true + source_gen:combining_builder: + enabled: true +'''; + + /// Standard domain/data directory structure created by `zfa setup` and + /// `zfa init` so the generated code has a home. + static const standardDirs = [ + 'lib/src/domain/entities', + 'lib/src/domain/repositories', + 'lib/src/domain/usecases', + 'lib/src/data/datasources', + 'lib/src/data/repositories', + ]; + + /// Ensures `build.yaml` and the standard domain/data directories exist in + /// [projectRoot]. Skips entries that already exist. When [dryRun] is true, + /// reports what would be created without writing. + static Future ensureProjectStructure({ + String? projectRoot, + bool dryRun = false, + }) async { + final root = projectRoot ?? Directory.current.path; + + // build.yaml + final buildYaml = File('$root/build.yaml'); + if (!buildYaml.existsSync()) { + if (dryRun) { + print(' Would create: $root/build.yaml'); + } else { + await buildYaml.writeAsString(buildYamlContent); + print(' Created: build.yaml'); + } + } + + // Domain/data directories + for (final dir in standardDirs) { + final full = '$root/$dir'; + if (!Directory(full).existsSync()) { + if (dryRun) { + print(' Would create: $full'); + } else { + await Directory(full).create(recursive: true); + } + } + } + } +} diff --git a/test/commands/setup_command_test.dart b/test/commands/setup_command_test.dart new file mode 100644 index 00000000..aa76a06b --- /dev/null +++ b/test/commands/setup_command_test.dart @@ -0,0 +1,231 @@ +import 'package:test/test.dart'; +import 'package:args/args.dart'; +import 'package:args/command_runner.dart'; +import 'package:zuraffa/src/commands/setup_command.dart'; +import 'package:zuraffa/src/core/dependencies/dependency_wirer.dart'; + +void main() { + group('SetupCommand', () { + test('has correct name', () { + final cmd = SetupCommand(); + expect(cmd.name, 'setup'); + }); + + test('has correct description', () { + final cmd = SetupCommand(); + expect(cmd.description, contains('Bootstrap')); + expect(cmd.description, contains('Flutter')); + expect(cmd.description, contains('Dart')); + }); + + test('exposes --flutter flag', () { + final cmd = SetupCommand(); + expect(cmd.argParser.options, contains('flutter')); + }); + + test('exposes --dart flag', () { + final cmd = SetupCommand(); + expect(cmd.argParser.options, contains('dart')); + }); + + test('exposes --platforms option', () { + final cmd = SetupCommand(); + expect(cmd.argParser.options, contains('platforms')); + }); + + test('exposes --dry-run flag', () { + final cmd = SetupCommand(); + expect(cmd.argParser.options, contains('dry-run')); + }); + + test('exposes --force flag', () { + final cmd = SetupCommand(); + expect(cmd.argParser.options, contains('force')); + }); + + test('exposes --org option', () { + final cmd = SetupCommand(); + expect(cmd.argParser.options, contains('org')); + }); + + test('takes a positional name argument', () { + final cmd = SetupCommand(); + // CommandRunner passes positional args via argResults.rest. + // Verify the command doesn't declare the name as an option (it's positional). + expect(cmd.argParser.options, isNot(contains('name'))); + }); + }); + + group('InitializeCommand flags', () { + test('accepts --deps-only flag', () { + final parser = _buildInitializeParser(); + final result = parser.parse(['--deps-only']); + expect(result['deps-only'], isTrue); + }); + + test('accepts --no-deps flag', () { + final parser = _buildInitializeParser(); + final result = parser.parse(['--no-deps']); + expect(result['no-deps'], isTrue); + }); + + test('--deps-only defaults to false', () { + final parser = _buildInitializeParser(); + final result = parser.parse([]); + expect(result['deps-only'], isFalse); + }); + + test('--no-deps defaults to false', () { + final parser = _buildInitializeParser(); + final result = parser.parse([]); + expect(result['no-deps'], isFalse); + }); + + test('still accepts legacy --entity option', () { + final parser = _buildInitializeParser(); + final result = parser.parse(['--entity=User']); + expect(result['entity'], 'User'); + }); + + test('still accepts --force flag', () { + final parser = _buildInitializeParser(); + final result = parser.parse(['--force']); + expect(result['force'], isTrue); + }); + }); + + group('CLI registration', () { + test('SetupCommand can be added to a CommandRunner', () { + final runner = CommandRunner('zfa', 'test') + ..addCommand(SetupCommand()); + final setupCmd = runner.commands['setup']; + expect(setupCmd, isNotNull); + expect(setupCmd!.name, 'setup'); + }); + + test('SetupCommand accepts both --flutter and --dart in parser', () { + final cmd = SetupCommand(); + // Both flags can be parsed independently (the mutual-exclusion check + // is in run(), not in the parser). Verify both are recognized. + final result = cmd.argParser.parse(['--flutter', '--dart']); + expect(result['flutter'], isTrue); + expect(result['dart'], isTrue); + }); + }); + + group('DependencyWirer', () { + test('findMissing detects all missing deps in empty pubspec', () { + const emptyPubspec = 'name: test\nenvironment:\n sdk: ^3.11.0\n'; + final missing = DependencyWirer.findMissing(emptyPubspec, isFlutter: true); + final names = missing.map((s) => s.name).toList(); + expect(names, contains('zuraffa_flutter')); + expect(names, contains('zorphy_annotation')); + expect(names, contains('build_runner')); + expect(names, contains('mocktail')); + expect(names, contains('flutter_lints')); + expect(names, contains('analyzer')); + }); + + test('findMissing detects missing deps for dart project', () { + const emptyPubspec = 'name: test\nenvironment:\n sdk: ^3.11.0\n'; + final missing = DependencyWirer.findMissing(emptyPubspec, isFlutter: false); + final names = missing.map((s) => s.name).toList(); + expect(names, contains('zuraffa')); + expect(names, isNot(contains('zuraffa_flutter'))); + expect(names, isNot(contains('flutter_lints'))); + }); + + test('findMissing returns empty when all deps present', () { + const fullPubspec = ''' +name: test +environment: + sdk: ^3.11.0 +dependencies: + zuraffa_flutter: any + zorphy_annotation: any +dev_dependencies: + build_runner: ^2.15.2 + mocktail: ^1.0.4 + flutter_lints: ^6.0.0 +dependency_overrides: + analyzer: 14.1.0 +'''; + final missing = DependencyWirer.findMissing(fullPubspec, isFlutter: true); + expect(missing, isEmpty); + }); + + test('isFlutterProject detects flutter sdk dependency', () { + const flutterPubspec = ''' +name: test +environment: + sdk: ^3.11.0 +dependencies: + flutter: + sdk: flutter +'''; + expect(DependencyWirer.isFlutterProject(flutterPubspec), isTrue); + }); + + test('isFlutterProject returns false for pure dart', () { + const dartPubspec = 'name: test\nenvironment:\n sdk: ^3.11.0\n'; + expect(DependencyWirer.isFlutterProject(dartPubspec), isFalse); + }); + + test('addOverrideToPubspec adds new section when missing', () { + const pubspec = 'name: test\nenvironment:\n sdk: ^3.11.0\n'; + final result = DependencyWirer.addOverrideToPubspec(pubspec, 'analyzer', '14.1.0'); + expect(result, contains('dependency_overrides:')); + expect(result, contains('analyzer: 14.1.0')); + }); + + test('addOverrideToPubspec is idempotent', () { + const pubspec = ''' +name: test +dependency_overrides: + analyzer: 14.1.0 +'''; + final result = DependencyWirer.addOverrideToPubspec(pubspec, 'analyzer', '14.1.0'); + expect(result, equals(pubspec)); + }); + + test('addOverrideToPubspec appends to existing section', () { + const pubspec = ''' +name: test +dependency_overrides: + meta: ^1.19.0 +'''; + final result = DependencyWirer.addOverrideToPubspec(pubspec, 'analyzer', '14.1.0'); + expect(result, contains('analyzer: 14.1.0')); + expect(result, contains('meta: ^1.19.0')); + }); + + test('standardSet returns different deps for flutter vs dart', () { + final flutterSpecs = DependencyWirer.standardSet(isFlutter: true); + final dartSpecs = DependencyWirer.standardSet(isFlutter: false); + + final flutterNames = flutterSpecs.map((s) => s.name).toSet(); + final dartNames = dartSpecs.map((s) => s.name).toSet(); + + expect(flutterNames, contains('zuraffa_flutter')); + expect(flutterNames, isNot(contains('zuraffa'))); + expect(flutterNames, contains('flutter_lints')); + + expect(dartNames, contains('zuraffa')); + expect(dartNames, isNot(contains('zuraffa_flutter'))); + expect(dartNames, isNot(contains('flutter_lints'))); + }); + }); +} + +/// Builds the same ArgParser that InitializeCommand.execute() uses. +ArgParser _buildInitializeParser() { + return ArgParser() + ..addOption('entity', abbr: 'e', defaultsTo: 'Product') + ..addOption('output', abbr: 'o', defaultsTo: 'lib/src/domain/entities') + ..addFlag('force', abbr: 'f', negatable: false) + ..addFlag('dry-run', negatable: false) + ..addFlag('deps-only', negatable: false) + ..addFlag('no-deps', negatable: false) + ..addFlag('verbose', abbr: 'v', negatable: false) + ..addFlag('help', abbr: 'h', negatable: false); +} \ No newline at end of file diff --git a/test/core/dependencies/dependency_wirer_test.dart b/test/core/dependencies/dependency_wirer_test.dart new file mode 100644 index 00000000..dad3e1ed --- /dev/null +++ b/test/core/dependencies/dependency_wirer_test.dart @@ -0,0 +1,522 @@ +import 'package:test/test.dart'; +import 'package:zuraffa/src/core/dependencies/dependency_wirer.dart'; + +void main() { + group('DependencyWirer', () { + group('standardSet', () { + test('flutter project includes zuraffa_flutter + flutter_lints', () { + final specs = DependencyWirer.standardSet(isFlutter: true); + final names = specs.map((s) => s.name).toList(); + + expect(names, contains('zuraffa_flutter')); + expect(names, isNot(contains('zuraffa'))); + expect(names, contains('zorphy_annotation')); + expect(names, contains('build_runner')); + expect(names, contains('mocktail')); + expect(names, contains('flutter_lints')); + expect(names, contains('analyzer')); + }); + + test('dart project includes zuraffa (not zuraffa_flutter) and no flutter_lints', () { + final specs = DependencyWirer.standardSet(isFlutter: false); + final names = specs.map((s) => s.name).toList(); + + expect(names, contains('zuraffa')); + expect(names, isNot(contains('zuraffa_flutter'))); + expect(names, contains('zorphy_annotation')); + expect(names, contains('build_runner')); + expect(names, contains('mocktail')); + expect(names, isNot(contains('flutter_lints'))); + expect(names, contains('analyzer')); + }); + + test('zuraffa_flutter is a git dependency with path zuraffa_flutter', () { + final specs = DependencyWirer.standardSet(isFlutter: true); + final zuraffaFlutter = specs.firstWhere((s) => s.name == 'zuraffa_flutter'); + + expect(zuraffaFlutter.kind, DependencyKind.regular); + expect(zuraffaFlutter.isGit, isTrue); + expect(zuraffaFlutter.gitUrl, DependencyWirer.zuraffaGitUrl); + expect(zuraffaFlutter.gitPath, 'zuraffa_flutter'); + expect(zuraffaFlutter.gitRef, DependencyWirer.defaultGitRef); + }); + + test('zuraffa (dart) is a git dependency with no git-path (repo root)', () { + final specs = DependencyWirer.standardSet(isFlutter: false); + final zuraffa = specs.firstWhere((s) => s.name == 'zuraffa'); + + expect(zuraffa.isGit, isTrue); + expect(zuraffa.gitUrl, DependencyWirer.zuraffaGitUrl); + expect(zuraffa.gitPath, isNull); + }); + + test('zorphy_annotation is a git dependency from the zorphy repo', () { + final specs = DependencyWirer.standardSet(isFlutter: true); + final zorphyAnn = specs.firstWhere((s) => s.name == 'zorphy_annotation'); + + expect(zorphyAnn.isGit, isTrue); + expect(zorphyAnn.gitUrl, DependencyWirer.zorphyGitUrl); + expect(zorphyAnn.gitPath, 'zorphy_annotation'); + }); + + test('build_runner and mocktail are dev dependencies', () { + final specs = DependencyWirer.standardSet(isFlutter: true); + final buildRunner = specs.firstWhere((s) => s.name == 'build_runner'); + final mocktail = specs.firstWhere((s) => s.name == 'mocktail'); + + expect(buildRunner.kind, DependencyKind.dev); + expect(buildRunner.isGit, isFalse); + expect(mocktail.kind, DependencyKind.dev); + expect(mocktail.isGit, isFalse); + }); + + test('analyzer is an override with the pinned version', () { + final specs = DependencyWirer.standardSet(isFlutter: true); + final analyzer = specs.firstWhere((s) => s.name == 'analyzer'); + + expect(analyzer.kind, DependencyKind.override); + expect(analyzer.version, DependencyWirer.analyzerOverrideVersion); + expect(analyzer.isOverride, isTrue); + }); + }); + + group('findMissing', () { + test('returns all specs for an empty pubspec', () { + final pubspec = ''' +name: my_app +description: A new app +environment: + sdk: ^3.11.0 +'''; + final missing = DependencyWirer.findMissing( + pubspec, + isFlutter: true, + ); + + final names = missing.map((s) => s.name).toSet(); + expect(names, containsAll([ + 'zuraffa_flutter', + 'zorphy_annotation', + 'build_runner', + 'mocktail', + 'flutter_lints', + 'analyzer', + ])); + }); + + test('returns empty when all deps are present', () { + final pubspec = ''' +name: my_app +environment: + sdk: ^3.11.0 + +dependencies: + flutter: + sdk: flutter + zuraffa_flutter: + git: + url: https://github.com/arrrrny/zuraffa + path: zuraffa_flutter + ref: development + zorphy_annotation: + git: + url: https://github.com/arrrrny/zorphy + path: zorphy_annotation + ref: development + +dev_dependencies: + build_runner: ^2.15.2 + mocktail: ^1.0.4 + flutter_lints: ^6.0.0 + +dependency_overrides: + analyzer: 14.1.0 +'''; + final missing = DependencyWirer.findMissing( + pubspec, + isFlutter: true, + ); + + expect(missing, isEmpty); + }); + + test('detects missing zuraffa_flutter only', () { + final pubspec = ''' +name: my_app +environment: + sdk: ^3.11.0 + +dependencies: + flutter: + sdk: flutter + zorphy_annotation: + git: + url: https://github.com/arrrrny/zorphy + path: zorphy_annotation + +dev_dependencies: + build_runner: ^2.15.2 + mocktail: ^1.0.4 + flutter_lints: ^6.0.0 + +dependency_overrides: + analyzer: 14.1.0 +'''; + final missing = DependencyWirer.findMissing( + pubspec, + isFlutter: true, + ); + + expect(missing.length, 1); + expect(missing.first.name, 'zuraffa_flutter'); + }); + + test('detects missing build_runner only', () { + final pubspec = ''' +name: my_app +environment: + sdk: ^3.11.0 + +dependencies: + flutter: + sdk: flutter + zuraffa_flutter: + git: + url: https://github.com/arrrrny/zuraffa + path: zuraffa_flutter + zorphy_annotation: + git: + url: https://github.com/arrrrny/zorphy + path: zorphy_annotation + +dev_dependencies: + mocktail: ^1.0.4 + flutter_lints: ^6.0.0 + +dependency_overrides: + analyzer: 14.1.0 +'''; + final missing = DependencyWirer.findMissing( + pubspec, + isFlutter: true, + ); + + expect(missing.length, 1); + expect(missing.first.name, 'build_runner'); + expect(missing.first.kind, DependencyKind.dev); + }); + + test('detects missing analyzer override only', () { + final pubspec = ''' +name: my_app +environment: + sdk: ^3.11.0 + +dependencies: + flutter: + sdk: flutter + zuraffa_flutter: + git: + url: https://github.com/arrrrny/zuraffa + path: zuraffa_flutter + zorphy_annotation: + git: + url: https://github.com/arrrrny/zorphy + path: zorphy_annotation + +dev_dependencies: + build_runner: ^2.15.2 + mocktail: ^1.0.4 + flutter_lints: ^6.0.0 +'''; + final missing = DependencyWirer.findMissing( + pubspec, + isFlutter: true, + ); + + expect(missing.length, 1); + expect(missing.first.name, 'analyzer'); + expect(missing.first.kind, DependencyKind.override); + }); + + test('dart project: zuraffa present means not missing', () { + final pubspec = ''' +name: my_pkg +environment: + sdk: ^3.11.0 + +dependencies: + zuraffa: + git: + url: https://github.com/arrrrny/zuraffa + zorphy_annotation: + git: + url: https://github.com/arrrrny/zorphy + path: zorphy_annotation + +dev_dependencies: + build_runner: ^2.15.2 + mocktail: ^1.0.4 + +dependency_overrides: + analyzer: 14.1.0 +'''; + final missing = DependencyWirer.findMissing( + pubspec, + isFlutter: false, + ); + + expect(missing, isEmpty); + }); + + test('returns all specs for unparseable pubspec', () { + final pubspec = 'this is not ::: valid yaml {{{'; + final missing = DependencyWirer.findMissing( + pubspec, + isFlutter: true, + ); + + expect(missing.length, greaterThan(0)); + }); + }); + + group('isFlutterProject', () { + test('returns true when flutter SDK dep is present', () { + final pubspec = ''' +name: my_app +environment: + sdk: ^3.11.0 +dependencies: + flutter: + sdk: flutter +'''; + expect(DependencyWirer.isFlutterProject(pubspec), isTrue); + }); + + test('returns false for a pure Dart package', () { + final pubspec = ''' +name: my_pkg +environment: + sdk: ^3.11.0 +dependencies: + http: ^1.6.0 +'''; + expect(DependencyWirer.isFlutterProject(pubspec), isFalse); + }); + + test('returns false for unparseable pubspec', () { + expect(DependencyWirer.isFlutterProject('garbage'), isFalse); + }); + }); + + group('addOverrideToPubspec', () { + test('appends new dependency_overrides section when absent', () { + final pubspec = ''' +name: my_app +environment: + sdk: ^3.11.0 +dependencies: + http: ^1.6.0 +'''; + final result = DependencyWirer.addOverrideToPubspec( + pubspec, + 'analyzer', + '14.1.0', + ); + + expect(result, contains('dependency_overrides:')); + expect(result, contains(' analyzer: 14.1.0')); + // Original content preserved + expect(result, contains('name: my_app')); + expect(result, contains('dependencies:')); + }); + + test('inserts key into existing dependency_overrides section', () { + final pubspec = ''' +name: my_app +environment: + sdk: ^3.11.0 + +dependency_overrides: + meta: ^1.19.0 + +dependencies: + http: ^1.6.0 +'''; + final result = DependencyWirer.addOverrideToPubspec( + pubspec, + 'analyzer', + '14.1.0', + ); + + expect(result, contains(' analyzer: 14.1.0')); + expect(result, contains(' meta: ^1.19.0')); + // The analyzer entry should be in the overrides section (before dependencies) + final analyzerIdx = result.indexOf(' analyzer: 14.1.0'); + final depsIdx = result.indexOf('\ndependencies:'); + expect(analyzerIdx, lessThan(depsIdx)); + }); + + test('is idempotent when key already exists', () { + final pubspec = ''' +name: my_app +environment: + sdk: ^3.11.0 + +dependency_overrides: + analyzer: 13.0.0 + +dependencies: + http: ^1.6.0 +'''; + final result = DependencyWirer.addOverrideToPubspec( + pubspec, + 'analyzer', + '14.1.0', + ); + + // Should be unchanged — existing value preserved + expect(result, equals(pubspec)); + }); + + test('handles pubspec with no trailing newline', () { + final pubspec = 'name: my_app\nenvironment:\n sdk: ^3.11.0'; + final result = DependencyWirer.addOverrideToPubspec( + pubspec, + 'analyzer', + '14.1.0', + ); + + expect(result, contains('dependency_overrides:')); + expect(result, contains(' analyzer: 14.1.0')); + }); + + test('does not modify entries in other sections', () { + final pubspec = ''' +name: my_app + +dependencies: + analyzer: ^5.0.0 + +dev_dependencies: + build_runner: ^2.0.0 +'''; + final result = DependencyWirer.addOverrideToPubspec( + pubspec, + 'analyzer', + '14.1.0', + ); + + // The original analyzer in dependencies should be untouched + expect(result, contains(' analyzer: ^5.0.0')); + // And the new override entry added + expect(result, contains('dependency_overrides:')); + expect(result, contains(' analyzer: 14.1.0')); + }); + }); + + group('DependencySpec', () { + test('toString renders dev deps with dev: prefix', () { + const spec = DependencySpec(name: 'build_runner', kind: DependencyKind.dev); + expect(spec.toString(), 'dev:build_runner'); + }); + + test('toString renders overrides with version', () { + const spec = DependencySpec( + name: 'analyzer', + kind: DependencyKind.override, + version: '14.1.0', + ); + expect(spec.toString(), 'override:analyzer=14.1.0'); + }); + + test('toString renders git deps with (git) marker', () { + const spec = DependencySpec( + name: 'zuraffa', + kind: DependencyKind.regular, + gitUrl: 'https://github.com/arrrrny/zuraffa', + ); + expect(spec.toString(), 'zuraffa (git)'); + }); + + test('equality compares all fields', () { + const a = DependencySpec( + name: 'analyzer', + kind: DependencyKind.override, + version: '14.1.0', + ); + const b = DependencySpec( + name: 'analyzer', + kind: DependencyKind.override, + version: '14.1.0', + ); + const c = DependencySpec( + name: 'analyzer', + kind: DependencyKind.override, + version: '15.0.0', + ); + expect(a, equals(b)); + expect(a, isNot(equals(c))); + }); + }); + + group('WireResult', () { + test('isSuccess when no failures', () { + const result = WireResult(added: ['build_runner'], failed: []); + expect(result.isSuccess, isTrue); + }); + + test('is not success when there are failures', () { + const result = WireResult(added: [], failed: ['zuraffa_flutter']); + expect(result.isSuccess, isFalse); + }); + + test('dryRun flag is preserved', () { + const result = WireResult(added: ['a'], dryRun: true); + expect(result.dryRun, isTrue); + }); + }); + + group('buildYamlContent', () { + test('contains zorphy builder registration', () { + expect(DependencyWirer.buildYamlContent, contains('zorphy:zorphy')); + expect(DependencyWirer.buildYamlContent, contains('enabled: true')); + }); + + test('contains json_serializable builder', () { + expect(DependencyWirer.buildYamlContent, contains('json_serializable')); + }); + + test('contains source_gen combining_builder', () { + expect(DependencyWirer.buildYamlContent, contains('source_gen:combining_builder')); + }); + + test('targets lib/src/** and test/**', () { + expect(DependencyWirer.buildYamlContent, contains('lib/src/**')); + expect(DependencyWirer.buildYamlContent, contains('test/**')); + }); + }); + + group('standardDirs', () { + test('includes domain/entities', () { + expect(DependencyWirer.standardDirs, contains('lib/src/domain/entities')); + }); + + test('includes domain/repositories', () { + expect(DependencyWirer.standardDirs, contains('lib/src/domain/repositories')); + }); + + test('includes domain/usecases', () { + expect(DependencyWirer.standardDirs, contains('lib/src/domain/usecases')); + }); + + test('includes data/datasources', () { + expect(DependencyWirer.standardDirs, contains('lib/src/data/datasources')); + }); + + test('includes data/repositories', () { + expect(DependencyWirer.standardDirs, contains('lib/src/data/repositories')); + }); + }); + }); +}