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,66 +7,74 @@ class BriefLoader {
|
||||
final file = File(path);
|
||||
if (!file.existsSync()) {
|
||||
throw FileSystemException('project-brief.yaml not found at $path. '
|
||||
'Run: dart run cursor_gen --wizard to create one.');
|
||||
'Run: cursor_gen --wizard to create one.');
|
||||
}
|
||||
final content = await file.readAsString();
|
||||
final yaml = loadYaml(content) as YamlMap;
|
||||
|
||||
final project = yaml['project'] as YamlMap;
|
||||
final stack = yaml['stack'] as YamlMap;
|
||||
final envs = yaml['environments'] as YamlMap? ?? YamlMap();
|
||||
final stack = yaml['stack'] as YamlMap;
|
||||
final envs = yaml['environments'] as YamlMap? ?? YamlMap();
|
||||
final testing = yaml['testing'] as YamlMap? ?? YamlMap();
|
||||
final design = yaml['design'] as YamlMap? ?? YamlMap();
|
||||
final design = yaml['design'] as YamlMap? ?? YamlMap();
|
||||
final apiDocs = yaml['api_docs'] as YamlMap? ?? YamlMap();
|
||||
final refs = yaml['references'] as YamlMap? ?? YamlMap();
|
||||
final refs = yaml['references'] as YamlMap? ?? YamlMap();
|
||||
final appCtx = yaml['app_context'] as YamlMap? ?? YamlMap();
|
||||
final features = yaml['features'] as YamlMap? ?? YamlMap();
|
||||
final l10n = yaml['localization'] as YamlMap? ?? YamlMap();
|
||||
final l10n = yaml['localization'] as YamlMap? ?? YamlMap();
|
||||
|
||||
// Parse backends (can be "firebase+rest" shorthand or list)
|
||||
final backendRaw = stack['backend']?.toString() ?? 'rest';
|
||||
final backends = backendRaw.contains('+')
|
||||
? backendRaw.split('+')
|
||||
: [backendRaw];
|
||||
final backends =
|
||||
backendRaw.contains('+') ? backendRaw.split('+') : [backendRaw];
|
||||
|
||||
return ProjectBrief(
|
||||
projectName: project['name']?.toString() ?? 'MyApp',
|
||||
packageId: project['package']?.toString() ?? 'com.example.myapp',
|
||||
description: project['description']?.toString() ?? '',
|
||||
scale: project['scale']?.toString() ?? 'medium',
|
||||
projectName: project['name']?.toString() ?? 'MyApp',
|
||||
packageId: project['package']?.toString() ?? 'com.example.myapp',
|
||||
description: project['description']?.toString() ?? '',
|
||||
scale: project['scale']?.toString() ?? 'medium',
|
||||
|
||||
stateManagement: stack['state_management']?.toString() ?? 'riverpod',
|
||||
routing: stack['routing']?.toString() ?? 'gorouter',
|
||||
architecture: stack['architecture']?.toString() ?? 'feature_first',
|
||||
backends: backends.map((b) => b.trim()).toList(),
|
||||
auth: stack['auth']?.toString() ?? 'none',
|
||||
routing: stack['routing']?.toString() ?? 'gorouter',
|
||||
architecture: stack['architecture']?.toString() ?? 'feature_first',
|
||||
backends: backends.map((b) => b.trim()).toList(),
|
||||
auth: stack['auth']?.toString() ?? 'none',
|
||||
|
||||
// Pillar 4: platform + codegen
|
||||
platforms: _toStringList(stack['platforms']) ?? ['ios', 'android'],
|
||||
codegenTools: _toStringList(stack['codegen']) ?? [],
|
||||
|
||||
flavors: _toStringList(envs['flavors']) ?? ['dev', 'prod'],
|
||||
cicd: envs['cicd']?.toString() ?? 'github_actions',
|
||||
cicd: envs['cicd']?.toString() ?? 'github_actions',
|
||||
|
||||
testingDepth: testing['depth']?.toString() ?? 'unit_widget',
|
||||
e2eTool: testing['e2e_tool']?.toString() ?? 'patrol',
|
||||
e2eTool: testing['e2e_tool']?.toString() ?? 'patrol',
|
||||
|
||||
designSource: design['source']?.toString() ?? 'none',
|
||||
figmaUrl: design['figma_url']?.toString() ?? '',
|
||||
figmaUrl: design['figma_url']?.toString() ?? '',
|
||||
|
||||
apiDocsFormat: apiDocs['format']?.toString() ?? 'none',
|
||||
apiDocsPath: apiDocs['path']?.toString() ?? '',
|
||||
apiDocsPath: apiDocs['path']?.toString() ?? '',
|
||||
|
||||
referenceRepos: _toStringList(refs['repos']) ?? [],
|
||||
localPaths: _toStringList(refs['local_paths']) ?? [],
|
||||
localPaths: _toStringList(refs['local_paths']) ?? [],
|
||||
|
||||
featureModules: _toStringList(features['modules']) ?? [],
|
||||
featureModules: _toStringList(features['modules']) ?? [],
|
||||
specialFeatures: _toStringList(features['special']) ?? [],
|
||||
|
||||
i18nEnabled: l10n['enabled'] as bool? ?? false,
|
||||
locales: _toStringList(l10n['locales']) ?? ['en'],
|
||||
locales: _toStringList(l10n['locales']) ?? ['en'],
|
||||
|
||||
cursorTemplatesVersion: yaml['cursor_templates_version']?.toString(),
|
||||
telemetryOptIn: yaml['telemetry_opt_in'] as bool? ?? false,
|
||||
|
||||
themeVariants: () {
|
||||
final t = _toStringList(appCtx['theme_variants']);
|
||||
if (t == null || t.isEmpty) return ['light', 'dark'];
|
||||
return t;
|
||||
}(),
|
||||
rolesEnabled: appCtx['roles_enabled'] as bool? ?? false,
|
||||
roleNames: _toStringList(appCtx['role_names']) ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,13 @@ class ProjectBrief {
|
||||
// Telemetry opt-in (Pillar 6)
|
||||
final bool telemetryOptIn;
|
||||
|
||||
/// Theme variants to support (subset of light, dark, high_contrast).
|
||||
final List<String> themeVariants;
|
||||
|
||||
/// App-level RBAC: when true, [roleNames] should list concrete roles.
|
||||
final bool rolesEnabled;
|
||||
final List<String> roleNames;
|
||||
|
||||
const ProjectBrief({
|
||||
required this.projectName,
|
||||
required this.packageId,
|
||||
@@ -80,7 +87,34 @@ class ProjectBrief {
|
||||
required this.locales,
|
||||
this.cursorTemplatesVersion,
|
||||
this.telemetryOptIn = false,
|
||||
this.themeVariants = const ['light', 'dark'],
|
||||
this.rolesEnabled = false,
|
||||
this.roleNames = const [],
|
||||
});
|
||||
|
||||
/// Local snapshot for tooling (written as `cursor-gen-metadata.json` under the output dir).
|
||||
Map<String, dynamic> toMetadataMap({DateTime? generatedAt}) {
|
||||
final at = (generatedAt ?? DateTime.now()).toUtc();
|
||||
return {
|
||||
'schema_version': 1,
|
||||
'generated_at': at.toIso8601String(),
|
||||
'project': {
|
||||
'name': projectName,
|
||||
'package': packageId,
|
||||
'description': description,
|
||||
'scale': scale,
|
||||
},
|
||||
'references': {
|
||||
'repos': List<String>.from(referenceRepos),
|
||||
'local_paths': List<String>.from(localPaths),
|
||||
},
|
||||
'app_context': {
|
||||
'theme_variants': List<String>.from(themeVariants),
|
||||
'roles_enabled': rolesEnabled,
|
||||
'role_names': List<String>.from(roleNames),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ValidationResult {
|
||||
|
||||
@@ -11,7 +11,7 @@ class Renderer {
|
||||
required String templateSrc,
|
||||
}) async {
|
||||
final context = _buildContext(brief);
|
||||
final output = <String, String>{};
|
||||
final output = <String, String>{};
|
||||
|
||||
for (final key in templateFiles) {
|
||||
final tmplPath = _templatePath(templateSrc, key);
|
||||
@@ -23,7 +23,8 @@ class Renderer {
|
||||
}
|
||||
var content = await file.readAsString();
|
||||
content = _substituteAll(content, context);
|
||||
_checkUnreplacedPlaceholders(content, key); // Pillar 3: validate no broken {{VAR}}
|
||||
_checkUnreplacedPlaceholders(
|
||||
content, key); // Pillar 3: validate no broken {{VAR}}
|
||||
output[_outputPath(key)] = content;
|
||||
}
|
||||
return output;
|
||||
@@ -32,38 +33,44 @@ class Renderer {
|
||||
/// Build the full substitution context from a brief
|
||||
static Map<String, String> _buildContext(ProjectBrief brief) {
|
||||
return {
|
||||
'PROJECT_NAME': brief.projectName,
|
||||
'PACKAGE_ID': brief.packageId,
|
||||
'DESCRIPTION': brief.description,
|
||||
'SCALE': brief.scale,
|
||||
'STATE_MANAGEMENT': _displayName(brief.stateManagement),
|
||||
'STATE_MGMT_RAW': brief.stateManagement,
|
||||
'ARCHITECTURE': _displayName(brief.architecture),
|
||||
'ARCH_RAW': brief.architecture,
|
||||
'ROUTING': _displayName(brief.routing),
|
||||
'ROUTING_RAW': brief.routing,
|
||||
'BACKEND': brief.backends.map(_displayName).join(' + '),
|
||||
'BACKENDS_LIST': brief.backends.join(', '),
|
||||
'AUTH': _displayName(brief.auth),
|
||||
'AUTH_RAW': brief.auth,
|
||||
'PLATFORMS_LIST': brief.platforms.join(', '),
|
||||
'CODEGEN_LIST': brief.codegenTools.isEmpty ? 'none' : brief.codegenTools.join(', '),
|
||||
'FLAVORS_LIST': brief.flavors.join(', '),
|
||||
'CICD_TOOL': _displayName(brief.cicd),
|
||||
'CICD_RAW': brief.cicd,
|
||||
'TESTING_DEPTH': brief.testingDepth,
|
||||
'E2E_TOOL': brief.e2eTool,
|
||||
'FEATURES_LIST': brief.featureModules.join(', '),
|
||||
'SPECIAL_FEATURES': brief.specialFeatures.join(', '),
|
||||
'DESIGN_SOURCE': brief.designSource,
|
||||
'FIGMA_URL': brief.figmaUrl,
|
||||
'API_DOCS_FORMAT': brief.apiDocsFormat,
|
||||
'API_DOCS_PATH': brief.apiDocsPath,
|
||||
'REFERENCE_REPOS': brief.referenceRepos.join('\n- '),
|
||||
'ARCH_IMPORT_RULES': _archImportRules(brief.architecture),
|
||||
'TEST_PATTERN': _testPattern(brief.stateManagement),
|
||||
'LOCALES_LIST': brief.locales.join(', '),
|
||||
'TEMPLATE_VERSION': '1.0.0',
|
||||
'PROJECT_NAME': brief.projectName,
|
||||
'PACKAGE_ID': brief.packageId,
|
||||
'DESCRIPTION': brief.description,
|
||||
'SCALE': brief.scale,
|
||||
'STATE_MANAGEMENT': _displayName(brief.stateManagement),
|
||||
'STATE_MGMT_RAW': brief.stateManagement,
|
||||
'ARCHITECTURE': _displayName(brief.architecture),
|
||||
'ARCH_RAW': brief.architecture,
|
||||
'ROUTING': _displayName(brief.routing),
|
||||
'ROUTING_RAW': brief.routing,
|
||||
'BACKEND': brief.backends.map(_displayName).join(' + '),
|
||||
'BACKENDS_LIST': brief.backends.join(', '),
|
||||
'AUTH': _displayName(brief.auth),
|
||||
'AUTH_RAW': brief.auth,
|
||||
'PLATFORMS_LIST': brief.platforms.join(', '),
|
||||
'CODEGEN_LIST':
|
||||
brief.codegenTools.isEmpty ? 'none' : brief.codegenTools.join(', '),
|
||||
'FLAVORS_LIST': brief.flavors.join(', '),
|
||||
'CICD_TOOL': _displayName(brief.cicd),
|
||||
'CICD_RAW': brief.cicd,
|
||||
'TESTING_DEPTH': brief.testingDepth,
|
||||
'E2E_TOOL': brief.e2eTool,
|
||||
'FEATURES_LIST': brief.featureModules.join(', '),
|
||||
'SPECIAL_FEATURES': brief.specialFeatures.join(', '),
|
||||
'DESIGN_SOURCE': brief.designSource,
|
||||
'FIGMA_URL': brief.figmaUrl,
|
||||
'API_DOCS_FORMAT': brief.apiDocsFormat,
|
||||
'API_DOCS_PATH': brief.apiDocsPath,
|
||||
'GIT_REFS_BLOCK': _gitRefsBlock(brief),
|
||||
'LOCAL_PATHS_BLOCK': _localPathsBlock(brief),
|
||||
'THEME_SUMMARY': _themeSummary(brief),
|
||||
'ROLES_SUMMARY': _rolesSummary(brief),
|
||||
'HIGH_CONTRAST_NOTE': _highContrastNote(brief),
|
||||
'HIGH_CONTRAST_UX_LINE': _highContrastUxLine(brief),
|
||||
'ARCH_IMPORT_RULES': _archImportRules(brief.architecture),
|
||||
'TEST_PATTERN': _testPattern(brief.stateManagement),
|
||||
'LOCALES_LIST': brief.locales.join(', '),
|
||||
'TEMPLATE_VERSION': '1.0.1',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -80,21 +87,17 @@ class Renderer {
|
||||
if (matches.isNotEmpty) {
|
||||
final vars = matches.map((m) => m.group(0)).toSet();
|
||||
// Print warning but don't fail — let golden tests catch it
|
||||
stderr.writeln('⚠ Unreplaced placeholder(s) in $key: ${vars.join(", ")}');
|
||||
stderr
|
||||
.writeln('⚠ Unreplaced placeholder(s) in $key: ${vars.join(", ")}');
|
||||
}
|
||||
}
|
||||
|
||||
static String _templatePath(String templateSrc, String key) {
|
||||
// skills use SKILL.md.tmpl, everything else uses .mdc.tmpl
|
||||
final ext = key.startsWith('skills/') ? 'SKILL.md.tmpl' : '.mdc.tmpl';
|
||||
final dir = key.startsWith('skills/') ? p.dirname(key) : '';
|
||||
|
||||
if (key.startsWith('skills/')) {
|
||||
final skillName = p.basename(key);
|
||||
return p.join(templateSrc, 'skills', skillName, 'SKILL.md.tmpl');
|
||||
}
|
||||
if (key.startsWith('hooks/')) {
|
||||
final hookFile = p.basename(key).replaceAll('-', '.');
|
||||
// special: hooks-json → hooks.json.tmpl
|
||||
if (key.endsWith('hooks-json')) {
|
||||
return p.join(templateSrc, 'hooks', 'hooks.json.tmpl');
|
||||
@@ -122,33 +125,83 @@ class Renderer {
|
||||
|
||||
static String _displayName(String raw) {
|
||||
const names = {
|
||||
'bloc': 'BLoC / Cubit',
|
||||
'riverpod': 'Riverpod',
|
||||
'getx': 'GetX',
|
||||
'bloc': 'BLoC / Cubit',
|
||||
'riverpod': 'Riverpod',
|
||||
'getx': 'GetX',
|
||||
'hooks_riverpod': 'Hooks + Riverpod',
|
||||
'clean': 'Clean Architecture',
|
||||
'feature_first': 'Feature-First',
|
||||
'mvvm': 'MVVM',
|
||||
'mvc': 'MVC',
|
||||
'layered': 'Layered',
|
||||
'gorouter': 'GoRouter',
|
||||
'getx_nav': 'GetX Navigation',
|
||||
'auto_route': 'Auto Route',
|
||||
'firebase': 'Firebase',
|
||||
'supabase': 'Supabase',
|
||||
'rest': 'REST API',
|
||||
'firebase_auth': 'Firebase Auth',
|
||||
'supabase_auth': 'Supabase Auth',
|
||||
'jwt_rest': 'JWT / REST Auth',
|
||||
'oauth2': 'OAuth 2.0',
|
||||
'none': 'None',
|
||||
'codemagic': 'Codemagic',
|
||||
'clean': 'Clean Architecture',
|
||||
'feature_first': 'Feature-First',
|
||||
'mvvm': 'MVVM',
|
||||
'mvc': 'MVC',
|
||||
'layered': 'Layered',
|
||||
'gorouter': 'GoRouter',
|
||||
'getx_nav': 'GetX Navigation',
|
||||
'auto_route': 'Auto Route',
|
||||
'firebase': 'Firebase',
|
||||
'supabase': 'Supabase',
|
||||
'rest': 'REST API',
|
||||
'firebase_auth': 'Firebase Auth',
|
||||
'supabase_auth': 'Supabase Auth',
|
||||
'jwt_rest': 'JWT / REST Auth',
|
||||
'oauth2': 'OAuth 2.0',
|
||||
'none': 'None',
|
||||
'codemagic': 'Codemagic',
|
||||
'github_actions': 'GitHub Actions',
|
||||
'fastlane': 'Fastlane',
|
||||
'fastlane': 'Fastlane',
|
||||
};
|
||||
return names[raw] ?? raw;
|
||||
}
|
||||
|
||||
static String _gitRefsBlock(ProjectBrief brief) {
|
||||
if (brief.referenceRepos.isEmpty) {
|
||||
return '_No Git repository URLs listed._ Add entries under `references.repos` in project-brief.yaml when other repos are part of the product context.';
|
||||
}
|
||||
return brief.referenceRepos.map((u) => '- $u').join('\n');
|
||||
}
|
||||
|
||||
static String _localPathsBlock(ProjectBrief brief) {
|
||||
if (brief.localPaths.isEmpty) {
|
||||
return '_No local paths listed._ Add monorepo packages or sibling folders under `references.local_paths` in project-brief.yaml when relevant.';
|
||||
}
|
||||
return brief.localPaths.map((path) => '- `$path`').join('\n');
|
||||
}
|
||||
|
||||
static String _themeSummary(ProjectBrief brief) {
|
||||
if (brief.themeVariants.isEmpty) {
|
||||
return '*(none specified — defaults not applied in brief; check YAML)*';
|
||||
}
|
||||
return brief.themeVariants.map(_themeLabel).join(', ');
|
||||
}
|
||||
|
||||
static String _themeLabel(String token) {
|
||||
switch (token) {
|
||||
case 'high_contrast':
|
||||
return 'high contrast';
|
||||
default:
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
static String _rolesSummary(ProjectBrief brief) {
|
||||
if (!brief.rolesEnabled) {
|
||||
return 'Not enabled (`app_context.roles_enabled: false`).';
|
||||
}
|
||||
if (brief.roleNames.isEmpty) {
|
||||
return 'Enabled but **no role names** — add `app_context.role_names` in project-brief.yaml.';
|
||||
}
|
||||
return brief.roleNames.join(', ');
|
||||
}
|
||||
|
||||
static String _highContrastNote(ProjectBrief brief) {
|
||||
if (!brief.themeVariants.contains('high_contrast')) return '';
|
||||
return '\n- **High contrast:** validate contrast, borders, and focus in the high-contrast theme alongside light/dark (WCAG).\n';
|
||||
}
|
||||
|
||||
static String _highContrastUxLine(ProjectBrief brief) {
|
||||
if (!brief.themeVariants.contains('high_contrast')) return '';
|
||||
return '\n- **High contrast theme:** validate loading, empty, and error states; never rely on color alone for meaning (use icons/text/semantics).';
|
||||
}
|
||||
|
||||
static String _archImportRules(String arch) {
|
||||
switch (arch) {
|
||||
case 'clean':
|
||||
|
||||
@@ -52,6 +52,16 @@ class Resolver {
|
||||
files.add('rules/codegen/codegen-$tool');
|
||||
}
|
||||
|
||||
// ── Hooks (Pillar 4) — tied to codegen, not state_management ─────
|
||||
if (brief.codegenTools.isNotEmpty) {
|
||||
files.addAll([
|
||||
'hooks/hooks-json',
|
||||
'hooks/flutter-analyze',
|
||||
'hooks/grind-tests',
|
||||
'hooks/arch-guard',
|
||||
]);
|
||||
}
|
||||
|
||||
// ── Localization ──────────────────────────────────────────────────
|
||||
if (brief.i18nEnabled) {
|
||||
files.add('rules/i18n/localization');
|
||||
@@ -83,14 +93,6 @@ class Resolver {
|
||||
files.add('agents/migration-agent');
|
||||
}
|
||||
|
||||
// ── Hooks ─────────────────────────────────────────────────────────
|
||||
files.addAll([
|
||||
'hooks/hooks-json',
|
||||
'hooks/flutter-analyze',
|
||||
'hooks/grind-tests',
|
||||
'hooks/arch-guard',
|
||||
]);
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
@@ -114,6 +116,9 @@ class Resolver {
|
||||
if (key.contains('testing-e2e')) return 'testing.depth includes e2e';
|
||||
if (key.contains('testing')) return 'Matches state_management testing patterns';
|
||||
if (key.contains('platform')) return 'Matches stack.platforms';
|
||||
if (key.startsWith('hooks/')) {
|
||||
return 'stack.codegen non-empty — Cursor hooks for analyze, boundaries, and tests';
|
||||
}
|
||||
if (key.contains('codegen')) return 'Matches stack.codegen';
|
||||
if (key.contains('i18n')) return 'localization.enabled: true';
|
||||
if (key.contains('migration')) return 'state_management is GetX — migration guidance included';
|
||||
|
||||
@@ -5,8 +5,6 @@ import 'dart:io';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'logger.dart';
|
||||
|
||||
const _telemetryFile = '.cursor/telemetry.json';
|
||||
|
||||
class Telemetry {
|
||||
/// Record a generation event (local-only, opt-in)
|
||||
static Future<void> record({
|
||||
@@ -27,9 +25,9 @@ class Telemetry {
|
||||
|
||||
final generations = data['generations'] as List;
|
||||
generations.add({
|
||||
'timestamp': DateTime.now().toIso8601String(),
|
||||
'timestamp': DateTime.now().toIso8601String(),
|
||||
'templateCount': templateFiles.length,
|
||||
'templates': templateFiles,
|
||||
'templates': templateFiles,
|
||||
});
|
||||
|
||||
// Keep only last 100 events
|
||||
@@ -48,7 +46,8 @@ class Telemetry {
|
||||
static Future<void> printReport({required String outputDir}) async {
|
||||
final file = File(p.join(outputDir, 'telemetry.json'));
|
||||
if (!file.existsSync()) {
|
||||
Logger.warn('No telemetry data found. Ensure telemetry_opt_in: true in project-brief.yaml');
|
||||
Logger.warn(
|
||||
'No telemetry data found. Ensure telemetry_opt_in: true in project-brief.yaml');
|
||||
return;
|
||||
}
|
||||
final data = jsonDecode(await file.readAsString()) as Map<String, dynamic>;
|
||||
@@ -65,22 +64,29 @@ class Telemetry {
|
||||
final ruleCount = templates.where((t) => t.startsWith('rules/')).length;
|
||||
final agentCount = templates.where((t) => t.startsWith('agents/')).length;
|
||||
final skillCount = templates.where((t) => t.startsWith('skills/')).length;
|
||||
Logger.dim(' Rules: $ruleCount | Agents: $agentCount | Skills: $skillCount');
|
||||
Logger.dim(
|
||||
' Rules: $ruleCount | Agents: $agentCount | Skills: $skillCount');
|
||||
}
|
||||
|
||||
Logger.info('\n💡 Quarterly template quality review checklist:');
|
||||
Logger.dim(' □ Review AI code review comments — which rules are most violated?');
|
||||
Logger.dim(' □ Ask teams: which agents are actually consulted vs. ignored?');
|
||||
Logger.dim(' □ Check if generated rules reduced hallucinations vs. last quarter');
|
||||
Logger.dim(' □ Identify brief combinations that produce the most AI errors');
|
||||
Logger.dim(' □ Update templates based on feedback, bump version in CHANGELOG.md');
|
||||
Logger.dim(
|
||||
' □ Review AI code review comments — which rules are most violated?');
|
||||
Logger.dim(
|
||||
' □ Ask teams: which agents are actually consulted vs. ignored?');
|
||||
Logger.dim(
|
||||
' □ Check if generated rules reduced hallucinations vs. last quarter');
|
||||
Logger.dim(
|
||||
' □ Identify brief combinations that produce the most AI errors');
|
||||
Logger.dim(
|
||||
' □ Update templates based on feedback, bump version in CHANGELOG.md');
|
||||
}
|
||||
|
||||
static Map<String, dynamic> _emptyData(String projectName) => {
|
||||
'projectName': projectName,
|
||||
'totalGenerations': 0,
|
||||
'lastGeneratedAt': '',
|
||||
'generations': [],
|
||||
'note': 'Local-only telemetry. Never sent anywhere. opt-in via telemetry_opt_in: true.',
|
||||
};
|
||||
'projectName': projectName,
|
||||
'totalGenerations': 0,
|
||||
'lastGeneratedAt': '',
|
||||
'generations': [],
|
||||
'note':
|
||||
'Local-only telemetry. Never sent anywhere. opt-in via telemetry_opt_in: true.',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,24 +5,59 @@ import 'package:yaml/yaml.dart';
|
||||
import 'models.dart';
|
||||
|
||||
class Validator {
|
||||
static const _validStateManagement = {'bloc', 'riverpod', 'getx', 'hooks_riverpod'};
|
||||
static const _validArchitectures = {'clean', 'feature_first', 'mvvm', 'mvc', 'layered'};
|
||||
static const _validRouting = {'gorouter', 'getx_nav', 'auto_route'};
|
||||
static const _validBackends = {'firebase', 'supabase', 'rest'};
|
||||
static const _validAuth = {'firebase_auth', 'supabase_auth', 'jwt_rest', 'oauth2', 'none'};
|
||||
static const _validPlatforms = {'ios', 'android', 'web', 'desktop'};
|
||||
static const _validCodegen = {'freezed', 'json_serializable', 'injectable', 'retrofit'};
|
||||
static const _validScale = {'small', 'medium', 'large'};
|
||||
static const _validTestingDepth = {'unit_widget', 'integration', 'e2e', 'full'};
|
||||
static const _validCicd = {'codemagic', 'github_actions', 'fastlane', 'none'};
|
||||
static const _validE2eTools = {'patrol', 'maestro'};
|
||||
static const _validDesignSource = {'figma_mcp', 'figma_manual', 'native_ref', 'html_ref', 'none'};
|
||||
static const _validApiFormats = {'openapi', 'postman', 'markdown', 'none'};
|
||||
static const _validStateManagement = {
|
||||
'bloc',
|
||||
'riverpod',
|
||||
'getx',
|
||||
'hooks_riverpod'
|
||||
};
|
||||
static const _validArchitectures = {
|
||||
'clean',
|
||||
'feature_first',
|
||||
'mvvm',
|
||||
'mvc',
|
||||
'layered'
|
||||
};
|
||||
static const _validRouting = {'gorouter', 'getx_nav', 'auto_route'};
|
||||
static const _validBackends = {'firebase', 'supabase', 'rest'};
|
||||
static const _validAuth = {
|
||||
'firebase_auth',
|
||||
'supabase_auth',
|
||||
'jwt_rest',
|
||||
'oauth2',
|
||||
'none'
|
||||
};
|
||||
static const _validPlatforms = {'ios', 'android', 'web', 'desktop'};
|
||||
static const _validCodegen = {
|
||||
'freezed',
|
||||
'json_serializable',
|
||||
'injectable',
|
||||
'retrofit'
|
||||
};
|
||||
static const _validScale = {'small', 'medium', 'large'};
|
||||
static const _validTestingDepth = {
|
||||
'unit_widget',
|
||||
'integration',
|
||||
'e2e',
|
||||
'full'
|
||||
};
|
||||
static const _validCicd = {'codemagic', 'github_actions', 'fastlane', 'none'};
|
||||
static const _validE2eTools = {'patrol', 'maestro'};
|
||||
static const _validDesignSource = {
|
||||
'figma_mcp',
|
||||
'figma_manual',
|
||||
'native_ref',
|
||||
'html_ref',
|
||||
'none'
|
||||
};
|
||||
static const _validApiFormats = {'openapi', 'postman', 'markdown', 'none'};
|
||||
static const _validThemeVariants = {'light', 'dark', 'high_contrast'};
|
||||
|
||||
static Future<ValidationResult> validateFile(String path) async {
|
||||
final file = File(path);
|
||||
if (!file.existsSync()) {
|
||||
return ValidationResult(isValid: false, errors: ['File not found: $path']);
|
||||
return ValidationResult(
|
||||
isValid: false, errors: ['File not found: $path']);
|
||||
}
|
||||
final content = await file.readAsString();
|
||||
final yaml = loadYaml(content) as YamlMap;
|
||||
@@ -34,34 +69,42 @@ class Validator {
|
||||
}
|
||||
|
||||
static ValidationResult _validateYaml(YamlMap yaml) {
|
||||
final errors = <String>[];
|
||||
final errors = <String>[];
|
||||
final warnings = <String>[];
|
||||
|
||||
final project = yaml['project'] as YamlMap?;
|
||||
final stack = yaml['stack'] as YamlMap?;
|
||||
final stack = yaml['stack'] as YamlMap?;
|
||||
final envs = yaml['environments'] as YamlMap?;
|
||||
final testing = yaml['testing'] as YamlMap?;
|
||||
final design = yaml['design'] as YamlMap?;
|
||||
final apiDocs = yaml['api_docs'] as YamlMap?;
|
||||
final appCtx = yaml['app_context'] as YamlMap?;
|
||||
|
||||
if (project == null) { errors.add('Missing required section: project'); }
|
||||
else {
|
||||
if (project['name'] == null) errors.add('project.name is required');
|
||||
if (project == null) {
|
||||
errors.add('Missing required section: project');
|
||||
} else {
|
||||
if (project['name'] == null) errors.add('project.name is required');
|
||||
if (project['package'] == null) errors.add('project.package is required');
|
||||
if (project['scale'] != null && !_validScale.contains(project['scale'])) {
|
||||
errors.add('project.scale must be one of: ${_validScale.join(", ")}');
|
||||
}
|
||||
}
|
||||
|
||||
if (stack == null) { errors.add('Missing required section: stack'); }
|
||||
else {
|
||||
if (stack == null) {
|
||||
errors.add('Missing required section: stack');
|
||||
} else {
|
||||
_validateField(stack, 'state_management', _validStateManagement, errors);
|
||||
_validateField(stack, 'architecture', _validArchitectures, errors);
|
||||
_validateField(stack, 'routing', _validRouting, errors);
|
||||
_validateField(stack, 'auth', _validAuth, warnings); // warning only
|
||||
_validateField(stack, 'architecture', _validArchitectures, errors);
|
||||
_validateField(stack, 'routing', _validRouting, errors);
|
||||
_validateField(stack, 'auth', _validAuth, warnings); // warning only
|
||||
|
||||
// Validate platforms list
|
||||
final platforms = stack['platforms'];
|
||||
if (platforms != null && platforms is YamlList) {
|
||||
for (final p in platforms) {
|
||||
if (!_validPlatforms.contains(p.toString())) {
|
||||
errors.add('stack.platforms contains unknown value: $p. Valid: ${_validPlatforms.join(", ")}');
|
||||
errors.add(
|
||||
'stack.platforms contains unknown value: $p. Valid: ${_validPlatforms.join(", ")}');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,7 +114,8 @@ class Validator {
|
||||
if (codegen != null && codegen is YamlList) {
|
||||
for (final c in codegen) {
|
||||
if (!_validCodegen.contains(c.toString())) {
|
||||
warnings.add('stack.codegen contains unknown value: $c. Known: ${_validCodegen.join(", ")}');
|
||||
warnings.add(
|
||||
'stack.codegen contains unknown value: $c. Known: ${_validCodegen.join(", ")}');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,50 +123,120 @@ class Validator {
|
||||
// Cross-validation: GetX nav requires GetX SM
|
||||
if (stack['routing']?.toString() == 'getx_nav' &&
|
||||
stack['state_management']?.toString() != 'getx') {
|
||||
warnings.add('stack.routing is getx_nav but state_management is not getx. This is unusual.');
|
||||
warnings.add(
|
||||
'stack.routing is getx_nav but state_management is not getx. This is unusual.');
|
||||
}
|
||||
|
||||
// Cross-validation: web platform warnings
|
||||
final platformsList = (stack['platforms'] as YamlList?)?.map((e) => e.toString()).toList() ?? [];
|
||||
final platformsList = (stack['platforms'] as YamlList?)
|
||||
?.map((e) => e.toString())
|
||||
.toList() ??
|
||||
[];
|
||||
if (platformsList.contains('web')) {
|
||||
warnings.add('Web platform detected: ensure you avoid dart:io imports (use dart:html or flutter_web_plugins)');
|
||||
warnings.add(
|
||||
'Web platform detected: ensure you avoid dart:io imports (use dart:html or flutter_web_plugins)');
|
||||
}
|
||||
}
|
||||
|
||||
if (envs != null) {
|
||||
_validateField(envs, 'cicd', _validCicd, warnings,
|
||||
prefix: 'environments');
|
||||
}
|
||||
if (testing != null) {
|
||||
_validateField(testing, 'depth', _validTestingDepth, errors,
|
||||
prefix: 'testing');
|
||||
_validateField(testing, 'e2e_tool', _validE2eTools, warnings,
|
||||
prefix: 'testing');
|
||||
}
|
||||
if (design != null) {
|
||||
_validateField(design, 'source', _validDesignSource, warnings,
|
||||
prefix: 'design');
|
||||
}
|
||||
if (apiDocs != null) {
|
||||
_validateField(apiDocs, 'format', _validApiFormats, warnings,
|
||||
prefix: 'api_docs');
|
||||
}
|
||||
if (appCtx != null) {
|
||||
final themes = appCtx['theme_variants'];
|
||||
for (final t in _yamlStrings(themes)) {
|
||||
if (!_validThemeVariants.contains(t)) {
|
||||
warnings.add(
|
||||
'app_context.theme_variants contains unknown value: $t (expected: ${_validThemeVariants.join(", ")})');
|
||||
}
|
||||
}
|
||||
if (appCtx['roles_enabled'] == true) {
|
||||
final names = appCtx['role_names'];
|
||||
final list = names is YamlList
|
||||
? names
|
||||
: names is List
|
||||
? names
|
||||
: const [];
|
||||
if (list.isEmpty) {
|
||||
warnings.add(
|
||||
'app_context.roles_enabled is true but role_names is empty');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ValidationResult(
|
||||
isValid: errors.isEmpty,
|
||||
errors: errors,
|
||||
isValid: errors.isEmpty,
|
||||
errors: errors,
|
||||
warnings: warnings,
|
||||
);
|
||||
}
|
||||
|
||||
static ValidationResult _validateBrief(ProjectBrief brief) {
|
||||
final errors = <String>[];
|
||||
final errors = <String>[];
|
||||
final warnings = <String>[];
|
||||
|
||||
if (brief.projectName.isEmpty) errors.add('project.name is required');
|
||||
if (brief.packageId.isEmpty) errors.add('project.package is required');
|
||||
if (brief.packageId.isEmpty) errors.add('project.package is required');
|
||||
if (!_validStateManagement.contains(brief.stateManagement)) {
|
||||
errors.add('stack.state_management "${brief.stateManagement}" is not valid. Use: ${_validStateManagement.join(", ")}');
|
||||
errors.add(
|
||||
'stack.state_management "${brief.stateManagement}" is not valid. Use: ${_validStateManagement.join(", ")}');
|
||||
}
|
||||
if (!_validArchitectures.contains(brief.architecture)) {
|
||||
errors.add('stack.architecture "${brief.architecture}" is not valid. Use: ${_validArchitectures.join(", ")}');
|
||||
errors.add(
|
||||
'stack.architecture "${brief.architecture}" is not valid. Use: ${_validArchitectures.join(", ")}');
|
||||
}
|
||||
for (final b in brief.backends) {
|
||||
if (!_validBackends.contains(b)) {
|
||||
errors.add('stack.backend contains "$b". Valid values: ${_validBackends.join(", ")}');
|
||||
errors.add(
|
||||
'stack.backend contains "$b". Valid values: ${_validBackends.join(", ")}');
|
||||
}
|
||||
}
|
||||
for (final t in brief.themeVariants) {
|
||||
if (!_validThemeVariants.contains(t)) {
|
||||
warnings.add(
|
||||
'app_context.theme_variants contains unknown value: $t (expected: ${_validThemeVariants.join(", ")})');
|
||||
}
|
||||
}
|
||||
if (brief.rolesEnabled && brief.roleNames.isEmpty) {
|
||||
warnings.add('roles_enabled is true but role_names is empty');
|
||||
}
|
||||
|
||||
return ValidationResult(isValid: errors.isEmpty, errors: errors, warnings: warnings);
|
||||
return ValidationResult(
|
||||
isValid: errors.isEmpty, errors: errors, warnings: warnings);
|
||||
}
|
||||
|
||||
static Iterable<String> _yamlStrings(dynamic value) sync* {
|
||||
if (value == null) return;
|
||||
if (value is YamlList) {
|
||||
for (final e in value) yield e.toString();
|
||||
} else if (value is List) {
|
||||
for (final e in value) yield e.toString();
|
||||
} else {
|
||||
yield value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
static void _validateField(
|
||||
YamlMap map, String field, Set<String> valid, List<String> output,
|
||||
) {
|
||||
YamlMap map, String field, Set<String> valid, List<String> output,
|
||||
{String prefix = 'stack'}) {
|
||||
final val = map[field]?.toString();
|
||||
if (val != null && !valid.contains(val)) {
|
||||
output.add('stack.$field "$val" is not valid. Use: ${valid.join(", ")}');
|
||||
output
|
||||
.add('$prefix.$field "$val" is not valid. Use: ${valid.join(", ")}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import 'models.dart';
|
||||
import 'logger.dart';
|
||||
|
||||
const _lockFileName = '.cursor-gen-lock.json';
|
||||
const _currentVersion = '1.0.0';
|
||||
const _currentVersion = '1.0.1';
|
||||
|
||||
class VersionManager {
|
||||
/// Check if the project's locked version differs from the current template version
|
||||
@@ -16,9 +16,9 @@ class VersionManager {
|
||||
final locked = brief.cursorTemplatesVersion ?? 'unset';
|
||||
final hasUpdate = locked != _currentVersion && locked != 'unset';
|
||||
return VersionStatus(
|
||||
hasUpdate: hasUpdate,
|
||||
hasUpdate: hasUpdate,
|
||||
currentVersion: locked,
|
||||
latestVersion: _currentVersion,
|
||||
latestVersion: _currentVersion,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,20 +30,20 @@ class VersionManager {
|
||||
}) async {
|
||||
final briefHash = await _fileHash(briefPath);
|
||||
final lock = {
|
||||
'templateVersion': _currentVersion,
|
||||
'generatedAt': DateTime.now().toIso8601String(),
|
||||
'briefHash': briefHash,
|
||||
'projectName': brief.projectName,
|
||||
'templateVersion': _currentVersion,
|
||||
'generatedAt': DateTime.now().toIso8601String(),
|
||||
'briefHash': briefHash,
|
||||
'projectName': brief.projectName,
|
||||
'stack': {
|
||||
'stateManagement': brief.stateManagement,
|
||||
'architecture': brief.architecture,
|
||||
'routing': brief.routing,
|
||||
'backends': brief.backends,
|
||||
'platforms': brief.platforms,
|
||||
'codegenTools': brief.codegenTools,
|
||||
'architecture': brief.architecture,
|
||||
'routing': brief.routing,
|
||||
'backends': brief.backends,
|
||||
'platforms': brief.platforms,
|
||||
'codegenTools': brief.codegenTools,
|
||||
},
|
||||
'note': 'Auto-generated by cursor_gen. Do not edit manually. '
|
||||
'Run: dart run cursor_gen --check-updates to see available updates.',
|
||||
'Run: cursor_gen --check-updates to see available updates.',
|
||||
};
|
||||
final file = File(p.join(outputDir, _lockFileName));
|
||||
await file.parent.create(recursive: true);
|
||||
@@ -66,7 +66,7 @@ class VersionManager {
|
||||
}
|
||||
final lock = await readLock('.cursor');
|
||||
if (lock == null) {
|
||||
Logger.warn('No lock file found. Run: dart run cursor_gen first.');
|
||||
Logger.warn('No lock file found. Run: cursor_gen first.');
|
||||
return;
|
||||
}
|
||||
final lockedVersion = lock['templateVersion'] as String? ?? 'unknown';
|
||||
@@ -79,10 +79,12 @@ class VersionManager {
|
||||
} else {
|
||||
Logger.warn(' ⚠ Update available!');
|
||||
Logger.info('\nTo update:');
|
||||
Logger.info(' 1. Update cursor_templates_version in project-brief.yaml to "$_currentVersion"');
|
||||
Logger.info(' 2. Run: dart run cursor_gen --diff (preview changes)');
|
||||
Logger.info(' 3. Run: dart run cursor_gen --refresh (apply updates)');
|
||||
Logger.info('\nChangelog: see CHANGELOG.md in flutter-cursor-templates repo');
|
||||
Logger.info(
|
||||
' 1. Update cursor_templates_version in project-brief.yaml to "$_currentVersion"');
|
||||
Logger.info(' 2. Run: cursor_gen --diff (preview changes)');
|
||||
Logger.info(' 3. Run: cursor_gen --refresh (apply updates)');
|
||||
Logger.info(
|
||||
'\nChangelog: see CHANGELOG.md in flutter-cursor-templates repo');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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