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
@@ -1,9 +1,12 @@
#!/usr/bin/env dart
// cursor_gen — Flutter Cursor AI config generator
// Usage: dart run cursor_gen [options]
// Usage: cursor_gen [options]
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'package:args/args.dart';
import 'package:path/path.dart' as p;
import '../src/models.dart';
import '../src/brief_loader.dart';
import '../src/resolver.dart';
@@ -17,16 +20,34 @@ 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');
..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 {
@@ -48,8 +69,8 @@ Future<void> main(List<String> arguments) async {
exit(0);
}
final briefPath = args['brief'] as String;
final outputDir = args['output'] as String;
final briefPath = args['brief'] as String;
final outputDir = args['output'] as String;
final templateSrc = args['templates'] as String;
if (args['validate'] as bool) {
@@ -85,8 +106,9 @@ Future<void> main(List<String> arguments) async {
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');
Logger.warn(
'\n⚠ Template update available: ${versionStatus.currentVersion}${versionStatus.latestVersion}');
Logger.warn(' Run: cursor_gen --check-updates for full diff\n');
}
if (args['diff'] as bool) {
@@ -96,9 +118,11 @@ Future<void> main(List<String> arguments) async {
_printBanner();
Logger.info('Generating .cursor/ for ${brief.projectName}');
Logger.dim(' Stack: ${brief.stateManagement} + ${brief.architecture} + ${brief.backends.join("+")}');
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');
Logger.dim(
' Codegen: ${brief.codegenTools.isEmpty ? "none" : brief.codegenTools.join(", ")}\n');
final isRefresh = args['refresh'] as bool;
final overrideSnapshot = await OverrideManager.snapshot(outputDir);
@@ -106,27 +130,40 @@ Future<void> main(List<String> arguments) async {
final templateFiles = Resolver.resolve(brief);
Logger.info('Resolved ${templateFiles.length} template files');
final templateDir = templateSrc.isEmpty ? _defaultTemplateDir() : templateSrc;
final templateDir =
templateSrc.isEmpty ? await _defaultTemplateDir() : templateSrc;
final rendered = await Renderer.render(
brief: brief, templateFiles: templateFiles, templateSrc: templateDir,
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);
await _writeMetadataJson(outputDir, brief);
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.success(
'${overrideSnapshot.customFiles.length} custom override(s) preserved untouched');
}
Logger.dim('\n Next: commit .cursor/ to git so all teammates get the same config.');
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 {
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,
brief: brief,
templateFiles: templateFiles,
templateSrc:
templateSrc.isEmpty ? await _defaultTemplateDir() : templateSrc,
);
Logger.info('Diff preview (no files written):\n');
for (final entry in rendered.entries) {
@@ -135,22 +172,27 @@ Future<void> _runDiff(dynamic brief, String outputDir, String templateSrc) async
Logger.success(' + ${entry.key}');
} else {
final current = await existing.readAsString();
if (current != entry.value) Logger.warn(' ~ ${entry.key}');
else Logger.dim(' = ${entry.key}');
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,
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(),
newContent: entry.value,
existingContent: await file.readAsString(),
);
await file.writeAsString(merged);
} else {
@@ -160,9 +202,35 @@ Future<void> _writeOutput(
await OverrideManager.restoreCustomFolder(snapshot, outputDir);
}
String _defaultTemplateDir() {
final scriptDir = Platform.script.toFilePath();
return scriptDir.replaceAll(RegExp(r'bin[/\\]cursor_gen\.dart$'), 'templates');
Future<void> _writeMetadataJson(String outputDir, ProjectBrief brief) async {
final path = p.join(outputDir, 'cursor-gen-metadata.json');
final file = File(path);
await file.parent.create(recursive: true);
final encoder = JsonEncoder.withIndent(' ');
await file.writeAsString(encoder.convert(brief.toMetadataMap()));
}
Future<String> _defaultTemplateDir() async {
final binDir = p.dirname(Platform.script.toFilePath());
final candidates = <String>[
// Monorepo layout: flutter-cursor-templates/generator/bin -> ../templates.
p.normalize(p.join(binDir, '..', '..', 'templates')),
// Published package layout: cursor_gen/templates.
p.normalize(p.join(binDir, '..', 'templates')),
];
final packageUri = await Isolate.resolvePackageUri(
Uri.parse('package:cursor_gen/src/renderer.dart'),
);
if (packageUri != null && packageUri.isScheme('file')) {
final packageRoot = p.dirname(p.dirname(packageUri.toFilePath()));
candidates.add(p.normalize(p.join(packageRoot, 'templates')));
}
return candidates.firstWhere(
(path) => Directory(path).existsSync(),
orElse: () => candidates.last,
);
}
void _printBanner() {