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.
This commit is contained in:
2026-05-13 12:08:52 +05:30
parent b05cdb7fbe
commit 54c66efe9b
157 changed files with 8233 additions and 570 deletions
@@ -7,57 +7,100 @@ 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');
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);
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);
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]);
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: []);
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');
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);
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);
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);
final telemetry = _askBool(
'\nOpt in to local telemetry (rule trigger logging, stored locally)?',
defaultYes: false);
answers['telemetry'] = telemetry;
Logger.info('');
@@ -65,7 +108,7 @@ class Wizard {
final file = File(outputPath);
await file.writeAsString(yaml);
Logger.success('✔ project-brief.yaml written to $outputPath');
Logger.info('\nNext: dart run cursor_gen --validate → dart run cursor_gen');
Logger.info('\nNext: cursor_gen --validate → cursor_gen');
}
static String _ask(String label, {String hint = ''}) {
@@ -75,7 +118,8 @@ class Wizard {
return input.isEmpty ? hint : input;
}
static String _askChoice(String label, List<String> options, {int defaultIdx = 0}) {
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 ? '' : '';
@@ -85,11 +129,13 @@ class Wizard {
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];
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 []}) {
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) ? '' : '';
@@ -99,7 +145,8 @@ class Wizard {
stdout.write(' Choices [$defaultStr]: ');
final input = stdin.readLineSync()?.trim() ?? '';
if (input.isEmpty) return defaults.map((d) => options[d]).toList();
return input.split(',')
return input
.split(',')
.map((s) => int.tryParse(s.trim()))
.where((i) => i != null && i >= 1 && i <= options.length)
.map((i) => options[i! - 1])
@@ -114,17 +161,35 @@ class Wizard {
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 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: dart run cursor_gen to generate .cursor/
# Run: dart run cursor_gen --refresh to update after changes
# 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.0"
cursor_templates_version: "1.0.1"
project:
name: "${a['name']}"
@@ -162,8 +227,13 @@ api_docs:
path: ""
references:
repos: []
local_paths: []
repos: [${_yamlQStringList(refRepos)}]
local_paths: [${_yamlQStringList(refLocals)}]
app_context:
theme_variants: [${_yamlQStringList(themes)}]
roles_enabled: ${a['roles_enabled']}
role_names: [${_yamlQStringList(roles)}]
features:
modules: []