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:
@@ -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: []
|
||||
|
||||
Reference in New Issue
Block a user