// wizard.dart — Interactive CLI wizard (brief discovery UX gap fix) import 'dart:io'; import 'logger.dart'; class Wizard { static Future run({required String outputPath}) async { Logger.info('\nšŸ§™ cursor_gen Interactive Wizard'); Logger.info('─' * 42); Logger.info( 'Answer the questions below to generate your project-brief.yaml.\n'); final answers = {}; // Project basics answers['name'] = _ask('Project name', hint: 'ShopEasy'); answers['package'] = _ask('Package ID', hint: 'com.company.shopeasy'); answers['description'] = _ask('Short description', hint: 'E-commerce app with real-time inventory'); answers['scale'] = _askChoice('Project scale', ['small', 'medium', 'large'], defaultIdx: 1); Logger.info(''); // Stack answers['state_management'] = _askChoice( 'State management', ['bloc', 'riverpod', 'getx', 'hooks_riverpod'], defaultIdx: 1); answers['architecture'] = _askChoice( 'Architecture', ['clean', 'feature_first', 'mvvm', 'mvc', 'layered'], defaultIdx: 0); answers['routing'] = _askChoice( 'Routing', ['gorouter', 'getx_nav', 'auto_route'], defaultIdx: 0); answers['backend'] = _askChoice('Backend(s)', ['firebase', 'supabase', 'rest', 'firebase+rest', 'supabase+rest'], defaultIdx: 2); answers['auth'] = _askChoice('Auth method', ['firebase_auth', 'supabase_auth', 'jwt_rest', 'oauth2', 'none'], defaultIdx: 4); Logger.info(''); // Platforms (Pillar 4) answers['platforms'] = _askMultiChoice( 'Target platforms', ['ios', 'android', 'web', 'desktop'], defaults: [0, 1]); // Codegen (Pillar 4) answers['codegen'] = _askMultiChoice('Code generation tools (optional)', [ 'freezed', 'json_serializable', 'injectable', 'retrofit' ], defaults: []); Logger.info(''); // References & app UX final reposRaw = _ask('Git reference repo URLs (comma-separated, optional)', hint: ''); answers['reference_repos'] = _splitComma(reposRaw); final localRaw = _ask('Local reference paths (comma-separated, optional)', hint: ''); answers['local_paths'] = _splitComma(localRaw); answers['theme_variants'] = _askMultiChoice( 'Theme variants to support', ['light', 'dark', 'high_contrast'], defaults: [0, 1]); final rolesOn = _askBool('Does the app support user roles?', defaultYes: false); answers['roles_enabled'] = rolesOn; if (rolesOn) { final raw = _ask('Role names (comma-separated)', hint: 'admin,member,guest'); answers['role_names'] = _splitComma(raw); } else { answers['role_names'] = []; } Logger.info(''); // Environments final flavorsInput = _ask('Build flavors (comma-separated)', hint: 'dev,staging,prod'); answers['flavors'] = flavorsInput.split(',').map((e) => e.trim()).toList(); answers['cicd'] = _askChoice( 'CI/CD', ['github_actions', 'codemagic', 'fastlane', 'none'], defaultIdx: 0); // Testing answers['testing_depth'] = _askChoice( 'Testing depth', ['unit_widget', 'integration', 'e2e', 'full'], defaultIdx: 0); // Localization final l10n = _askBool('Enable localization / i18n?', defaultYes: false); answers['i18n'] = l10n; // Telemetry opt-in (Pillar 6) final telemetry = _askBool( '\nOpt in to local telemetry (rule trigger logging, stored locally)?', defaultYes: false); answers['telemetry'] = telemetry; Logger.info(''); final yaml = _buildYaml(answers); final file = File(outputPath); await file.writeAsString(yaml); Logger.success('āœ” project-brief.yaml written to $outputPath'); Logger.info('\nNext: cursor_gen --validate → cursor_gen'); } static String _ask(String label, {String hint = ''}) { final display = hint.isNotEmpty ? '$label [$hint]: ' : '$label: '; stdout.write(' $display'); final input = stdin.readLineSync()?.trim() ?? ''; return input.isEmpty ? hint : input; } static String _askChoice(String label, List options, {int defaultIdx = 0}) { Logger.info(' $label:'); for (var i = 0; i < options.length; i++) { final marker = i == defaultIdx ? ' ā—€' : ''; Logger.dim(' ${i + 1}) ${options[i]}$marker'); } stdout.write(' Choice [${defaultIdx + 1}]: '); final input = stdin.readLineSync()?.trim() ?? ''; if (input.isEmpty) return options[defaultIdx]; final idx = int.tryParse(input); if (idx != null && idx >= 1 && idx <= options.length) return options[idx - 1]; return options[defaultIdx]; } static List _askMultiChoice(String label, List options, {List defaults = const []}) { Logger.info(' $label (comma-separated numbers, or leave blank):'); for (var i = 0; i < options.length; i++) { final marker = defaults.contains(i) ? ' ā—€' : ''; Logger.dim(' ${i + 1}) ${options[i]}$marker'); } final defaultStr = defaults.map((d) => '${d + 1}').join(','); stdout.write(' Choices [$defaultStr]: '); final input = stdin.readLineSync()?.trim() ?? ''; if (input.isEmpty) return defaults.map((d) => options[d]).toList(); return input .split(',') .map((s) => int.tryParse(s.trim())) .where((i) => i != null && i >= 1 && i <= options.length) .map((i) => options[i! - 1]) .toList(); } static bool _askBool(String label, {bool defaultYes = false}) { final hint = defaultYes ? 'Y/n' : 'y/N'; stdout.write(' $label ($hint): '); final input = stdin.readLineSync()?.trim().toLowerCase() ?? ''; if (input.isEmpty) return defaultYes; return input == 'y' || input == 'yes'; } static List _splitComma(String s) => s .split(',') .map((e) => e.trim()) .where((e) => e.isNotEmpty) .toList(); static String _yamlQStringList(List xs) { if (xs.isEmpty) return ''; return xs.map((e) => '"${_yamlEsc(e)}"').join(', '); } static String _yamlEsc(String s) => s.replaceAll(r'\', r'\\').replaceAll('"', r'\"'); static String _buildYaml(Map a) { final platforms = (a['platforms'] as List).map((p) => '"$p"').join(', '); final codegen = (a['codegen'] as List).map((c) => '"$c"').join(', '); final flavors = (a['flavors'] as List).map((f) => '"$f"').join(', '); final refRepos = List.from(a['reference_repos'] as List); final refLocals = List.from(a['local_paths'] as List); final themes = List.from(a['theme_variants'] as List); final roles = List.from(a['role_names'] as List); return '''# project-brief.yaml — cursor_gen configuration # Generated by cursor_gen --wizard # Run: cursor_gen to generate .cursor/ # Run: cursor_gen --refresh to update after changes # Pillar 1: Pin to template version for reproducibility cursor_templates_version: "1.0.1" project: name: "${a['name']}" package: "${a['package']}" description: "${a['description']}" scale: "${a['scale']}" stack: state_management: "${a['state_management']}" routing: "${a['routing']}" architecture: "${a['architecture']}" backend: "${a['backend']}" auth: "${a['auth']}" # Pillar 4: Platform targets platforms: [$platforms] # Pillar 4: Code generation tools codegen: [$codegen] environments: flavors: [$flavors] cicd: "${a['cicd']}" testing: depth: "${a['testing_depth']}" e2e_tool: "patrol" design: source: "none" figma_url: "" api_docs: format: "none" path: "" references: repos: [${_yamlQStringList(refRepos)}] local_paths: [${_yamlQStringList(refLocals)}] app_context: theme_variants: [${_yamlQStringList(themes)}] roles_enabled: ${a['roles_enabled']} role_names: [${_yamlQStringList(roles)}] features: modules: [] special: [] localization: enabled: ${a['i18n']} locales: ["en"] # Pillar 6: Opt-in local telemetry (logs rule trigger frequency locally) telemetry_opt_in: ${a['telemetry']} '''; } }