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:
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user