Files
cursor_gen/flutter-cursor-templates/generator/src/wizard.dart
T
mansi.kansara 54c66efe9b chore: update README and CLI usage for cursor_gen, version bump to 1.0.1
- Changed CLI usage instructions from `dart run cursor_gen` to `cursor_gen` for global activation.
- Updated project-brief.yaml example and README to reflect new command usage.
- Added app_context section in project-brief.yaml for theme variants and RBAC roles.
- Fixed bundled template resolution for local and global installs to prevent 'Template not found' errors.
- Version bump to 1.0.1 with corresponding updates in CHANGELOG and pubspec.yaml.
2026-05-13 12:08:52 +05:30

251 lines
8.0 KiB
Dart

// wizard.dart — Interactive CLI wizard (brief discovery UX gap fix)
import 'dart:io';
import 'logger.dart';
class Wizard {
static Future<void> 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 = <String, dynamic>{};
// 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'] = <String>[];
}
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<String> 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<String> _askMultiChoice(String label, List<String> options,
{List<int> 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<String> _splitComma(String s) => s
.split(',')
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.toList();
static String _yamlQStringList(List<String> 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<String, dynamic> 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<String>.from(a['reference_repos'] as List);
final refLocals = List<String>.from(a['local_paths'] as List);
final themes = List<String>.from(a['theme_variants'] as List);
final roles = List<String>.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']}
''';
}
}