chore: bump cursor_templates_version to 1.0.4 and update related files

- Updated cursor_templates_version in project-brief.yaml to 1.0.4 for reproducibility.
- Enhanced CHANGELOG with fixes and features for version 1.0.4, including improved template resolution for global installs.
- Updated pubspec.yaml and VERSION file to reflect the new version.
- Added new templates for AGENTS.md and lefthook.yaml to the generator.
- Adjusted tests to include new root-level files in the output.

This release addresses template resolution issues and introduces new repo-level configuration files.
This commit is contained in:
2026-05-14 12:25:13 +05:30
parent 3ee83389bd
commit 44b5d814d4
31 changed files with 956 additions and 172 deletions
@@ -130,8 +130,7 @@ Future<void> main(List<String> arguments) async {
final templateFiles = Resolver.resolve(brief);
Logger.info('Resolved ${templateFiles.length} template files');
final templateDir =
templateSrc.isEmpty ? await _defaultTemplateDir() : templateSrc;
final templateDir = await _resolveTemplateDir(templateSrc);
final rendered = await Renderer.render(
brief: brief,
templateFiles: templateFiles,
@@ -147,7 +146,12 @@ Future<void> main(List<String> arguments) async {
outputDir: outputDir,
templateFiles: templateFiles);
Logger.success('\n✔ Done! ${rendered.length} files written to $outputDir/');
final underCursor =
rendered.keys.where((k) => !k.startsWith('__root__/')).length;
final atRoot = rendered.length - underCursor;
Logger.success(
'\n✔ Done! $underCursor file(s) under $outputDir/'
'${atRoot > 0 ? ", $atRoot at project root" : ""}.');
if (overrideSnapshot.customFiles.isNotEmpty) {
Logger.success(
'${overrideSnapshot.customFiles.length} custom override(s) preserved untouched');
@@ -162,12 +166,11 @@ Future<void> _runDiff(
final rendered = await Renderer.render(
brief: brief,
templateFiles: templateFiles,
templateSrc:
templateSrc.isEmpty ? await _defaultTemplateDir() : templateSrc,
templateSrc: await _resolveTemplateDir(templateSrc),
);
Logger.info('Diff preview (no files written):\n');
for (final entry in rendered.entries) {
final existing = File('$outputDir/${entry.key}');
final existing = File(_resolvedOutputPath(outputDir, entry.key));
if (!existing.existsSync()) {
Logger.success(' + ${entry.key}');
} else {
@@ -187,7 +190,7 @@ Future<void> _writeOutput(
bool isRefresh,
) async {
for (final entry in rendered.entries) {
final file = File('$outputDir/${entry.key}');
final file = File(_resolvedOutputPath(outputDir, entry.key));
await file.parent.create(recursive: true);
if (isRefresh && file.existsSync()) {
final merged = OverrideManager.mergeCustomSections(
@@ -210,27 +213,71 @@ Future<void> _writeMetadataJson(String outputDir, ProjectBrief brief) async {
await file.writeAsString(encoder.convert(brief.toMetadataMap()));
}
Future<String> _defaultTemplateDir() async {
const _templateSentinel = 'rules/universal/flutter-core.mdc.tmpl';
Future<String> _resolveTemplateDir(String override) async {
if (override.isNotEmpty) {
final dir = p.normalize(override);
_assertTemplateBundle(dir);
return dir;
}
final tried = <String>[];
for (final dir in await _templateDirCandidates()) {
tried.add(dir);
if (File(p.join(dir, _templateSentinel)).existsSync()) return dir;
}
Logger.error('cursor_gen: No template bundle found (missing $_templateSentinel). Tried:');
for (final t in tried) Logger.error('$t');
Logger.error(
'Use: cursor_gen --templates /path/to/cursor_gen/templates (folder containing rules/, hooks/, …)');
exit(1);
}
void _assertTemplateBundle(String dir) {
if (!File(p.join(dir, _templateSentinel)).existsSync()) {
Logger.error('Invalid template directory: $dir ($_templateSentinel not found)');
exit(1);
}
}
Future<List<String>> _templateDirCandidates() 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 out = <String>[];
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')));
final packageRoot = _packageRootDir(packageUri.toFilePath());
if (packageRoot.isNotEmpty) {
out.add(p.normalize(p.join(packageRoot, 'templates')));
}
}
// Published layout: <package>/templates next to bin/
out.add(p.normalize(p.join(binDir, '..', 'templates')));
// Monorepo: flutter-cursor-templates/templates
out.add(p.normalize(p.join(binDir, '..', '..', 'templates')));
return candidates.firstWhere(
(path) => Directory(path).existsSync(),
orElse: () => candidates.last,
);
return out;
}
/// From e.g. `.../cursor_gen-1.0.3/lib/src/renderer.dart`, walk up to the directory that contains `pubspec.yaml`.
String _packageRootDir(String resolvedLibFile) {
var dir = p.dirname(resolvedLibFile);
while (true) {
if (File(p.join(dir, 'pubspec.yaml')).existsSync()) return dir;
final parent = p.dirname(dir);
if (parent == dir) return '';
dir = parent;
}
}
String _resolvedOutputPath(String outputDir, String relativeKey) {
if (relativeKey.startsWith('__root__/')) {
final name = relativeKey.substring('__root__/'.length);
return p.normalize(p.join(p.dirname(p.absolute(outputDir)), name));
}
return p.normalize(p.join(p.absolute(outputDir), relativeKey));
}
void _printBanner() {