Initial commit of the Flutter Cursor Generator project, including the core generator tool, project brief schema, example project setup, and CI configuration. Added README documentation outlining repository structure, quick start guide, and detailed descriptions of features and architecture pillars.
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env dart
|
||||
// cursor_gen — Flutter Cursor AI config generator
|
||||
// Usage: dart run cursor_gen [options]
|
||||
|
||||
import 'dart:io';
|
||||
import 'package:args/args.dart';
|
||||
import '../src/brief_loader.dart';
|
||||
import '../src/resolver.dart';
|
||||
import '../src/renderer.dart';
|
||||
import '../src/version_manager.dart';
|
||||
import '../src/override_manager.dart';
|
||||
import '../src/validator.dart';
|
||||
import '../src/wizard.dart';
|
||||
import '../src/telemetry.dart';
|
||||
import '../src/logger.dart';
|
||||
|
||||
Future<void> main(List<String> arguments) async {
|
||||
final parser = ArgParser()
|
||||
..addFlag('help', abbr: 'h', negatable: false, help: 'Show usage')
|
||||
..addFlag('validate', abbr: 'v', negatable: false, help: 'Validate project-brief.yaml without writing files')
|
||||
..addFlag('refresh', abbr: 'r', negatable: false, help: 'Re-generate .cursor/ preserving custom/ and CURSOR:CUSTOM markers')
|
||||
..addFlag('check-updates', negatable: false, help: 'Check if newer template version is available')
|
||||
..addFlag('diff', negatable: false, help: 'Preview what would change before refreshing')
|
||||
..addFlag('wizard', abbr: 'w', negatable: false, help: 'Interactive wizard to create project-brief.yaml')
|
||||
..addFlag('telemetry', negatable: false, help: 'Show telemetry / rule-trigger report')
|
||||
..addOption('brief', abbr: 'b', defaultsTo: 'project-brief.yaml', help: 'Path to project-brief.yaml')
|
||||
..addOption('output', abbr: 'o', defaultsTo: '.cursor', help: 'Output directory')
|
||||
..addOption('templates', defaultsTo: '', help: 'Override template library path');
|
||||
|
||||
final ArgResults args;
|
||||
try {
|
||||
args = parser.parse(arguments);
|
||||
} catch (e) {
|
||||
Logger.error('$e');
|
||||
print(parser.usage);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (args['help'] as bool) {
|
||||
_printBanner();
|
||||
print(parser.usage);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if (args['wizard'] as bool) {
|
||||
await Wizard.run(outputPath: args['brief'] as String);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
final briefPath = args['brief'] as String;
|
||||
final outputDir = args['output'] as String;
|
||||
final templateSrc = args['templates'] as String;
|
||||
|
||||
if (args['validate'] as bool) {
|
||||
final result = await Validator.validateFile(briefPath);
|
||||
if (result.isValid) {
|
||||
Logger.success('✔ project-brief.yaml is valid');
|
||||
} else {
|
||||
Logger.error('✘ Validation failed:');
|
||||
for (final e in result.errors) Logger.error(' • $e');
|
||||
exit(1);
|
||||
}
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if (args['check-updates'] as bool) {
|
||||
await VersionManager.checkUpdates(briefPath: briefPath);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if (args['telemetry'] as bool) {
|
||||
await Telemetry.printReport(outputDir: outputDir);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
Logger.info('Loading project brief from $briefPath...');
|
||||
final brief = await BriefLoader.load(briefPath);
|
||||
final validation = await Validator.validate(brief);
|
||||
if (!validation.isValid) {
|
||||
Logger.error('✘ Brief validation failed:');
|
||||
for (final e in validation.errors) Logger.error(' • $e');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
final versionStatus = await VersionManager.check(brief: brief);
|
||||
if (versionStatus.hasUpdate) {
|
||||
Logger.warn('\n⚠ Template update available: ${versionStatus.currentVersion} → ${versionStatus.latestVersion}');
|
||||
Logger.warn(' Run: dart run cursor_gen --check-updates for full diff\n');
|
||||
}
|
||||
|
||||
if (args['diff'] as bool) {
|
||||
await _runDiff(brief, outputDir, templateSrc);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
_printBanner();
|
||||
Logger.info('Generating .cursor/ for ${brief.projectName}');
|
||||
Logger.dim(' Stack: ${brief.stateManagement} + ${brief.architecture} + ${brief.backends.join("+")}');
|
||||
Logger.dim(' Platform: ${brief.platforms.join(", ")}');
|
||||
Logger.dim(' Codegen: ${brief.codegenTools.isEmpty ? "none" : brief.codegenTools.join(", ")}\n');
|
||||
|
||||
final isRefresh = args['refresh'] as bool;
|
||||
final overrideSnapshot = await OverrideManager.snapshot(outputDir);
|
||||
|
||||
final templateFiles = Resolver.resolve(brief);
|
||||
Logger.info('Resolved ${templateFiles.length} template files');
|
||||
|
||||
final templateDir = templateSrc.isEmpty ? _defaultTemplateDir() : templateSrc;
|
||||
final rendered = await Renderer.render(
|
||||
brief: brief, templateFiles: templateFiles, templateSrc: templateDir,
|
||||
);
|
||||
|
||||
await _writeOutput(outputDir, rendered, overrideSnapshot, isRefresh);
|
||||
await VersionManager.writeLock(outputDir: outputDir, briefPath: briefPath, brief: brief);
|
||||
await Telemetry.record(projectName: brief.projectName, outputDir: outputDir, templateFiles: templateFiles);
|
||||
|
||||
Logger.success('\n✔ Done! ${rendered.length} files written to $outputDir/');
|
||||
if (overrideSnapshot.customFiles.isNotEmpty) {
|
||||
Logger.success(' ↳ ${overrideSnapshot.customFiles.length} custom override(s) preserved untouched');
|
||||
}
|
||||
Logger.dim('\n Next: commit .cursor/ to git so all teammates get the same config.');
|
||||
}
|
||||
|
||||
Future<void> _runDiff(dynamic brief, String outputDir, String templateSrc) async {
|
||||
final templateFiles = Resolver.resolve(brief);
|
||||
final rendered = await Renderer.render(
|
||||
brief: brief, templateFiles: templateFiles,
|
||||
templateSrc: templateSrc.isEmpty ? _defaultTemplateDir() : templateSrc,
|
||||
);
|
||||
Logger.info('Diff preview (no files written):\n');
|
||||
for (final entry in rendered.entries) {
|
||||
final existing = File('$outputDir/${entry.key}');
|
||||
if (!existing.existsSync()) {
|
||||
Logger.success(' + ${entry.key}');
|
||||
} else {
|
||||
final current = await existing.readAsString();
|
||||
if (current != entry.value) Logger.warn(' ~ ${entry.key}');
|
||||
else Logger.dim(' = ${entry.key}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _writeOutput(
|
||||
String outputDir, Map<String, String> rendered,
|
||||
OverrideSnapshot snapshot, bool isRefresh,
|
||||
) async {
|
||||
for (final entry in rendered.entries) {
|
||||
final file = File('$outputDir/${entry.key}');
|
||||
await file.parent.create(recursive: true);
|
||||
if (isRefresh && file.existsSync()) {
|
||||
final merged = OverrideManager.mergeCustomSections(
|
||||
newContent: entry.value, existingContent: await file.readAsString(),
|
||||
);
|
||||
await file.writeAsString(merged);
|
||||
} else {
|
||||
await file.writeAsString(entry.value);
|
||||
}
|
||||
}
|
||||
await OverrideManager.restoreCustomFolder(snapshot, outputDir);
|
||||
}
|
||||
|
||||
String _defaultTemplateDir() {
|
||||
final scriptDir = Platform.script.toFilePath();
|
||||
return scriptDir.replaceAll(RegExp(r'bin[/\\]cursor_gen\.dart$'), 'templates');
|
||||
}
|
||||
|
||||
void _printBanner() {
|
||||
print('\n╔══════════════════════════════════╗');
|
||||
print('║ cursor_gen — Flutter AI Config ║');
|
||||
print('╚══════════════════════════════════╝\n');
|
||||
}
|
||||
Reference in New Issue
Block a user