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:
@@ -1,5 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
## [1.0.4] - 2026-05-13
|
||||
### Fixed
|
||||
- Global `dart pub global` installs: locate `templates/` from the package root containing `pubspec.yaml` (not under `lib/`).
|
||||
|
||||
## [1.0.3] - 2026-05-13
|
||||
### Fixed
|
||||
- Template directory resolution for `dart pub global` snapshot installs; fail fast when the bundle is missing instead of emitting placeholders.
|
||||
|
||||
### Added
|
||||
- Generated repo-root `AGENTS.md` and `lefthook.yaml` (from `templates/root/`).
|
||||
|
||||
## [1.0.2] - 2026-05-13
|
||||
### Changed
|
||||
- Interactive wizard collects the remaining `project-brief.yaml` fields (design, API docs, features, conditional E2E tool and locales) and pins `cursor_templates_version` from a single `kCursorTemplatesVersion` constant.
|
||||
|
||||
## [1.0.1] - 2026-05-13
|
||||
### Fixed
|
||||
- Resolve bundled templates for local, Git, and hosted/global installs so generated files contain real content instead of `Template not found` placeholders.
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.0.1
|
||||
1.0.4
|
||||
|
||||
@@ -309,8 +309,8 @@
|
||||
}
|
||||
],
|
||||
"generator": "pub",
|
||||
"generatorVersion": "3.8.1",
|
||||
"generatorVersion": "3.11.3",
|
||||
"flutterRoot": "file:///Users/maheshlalwani.devgmail.com/Documents/flutter",
|
||||
"flutterVersion": "3.32.5",
|
||||
"flutterVersion": "3.41.5",
|
||||
"pubCache": "file:///Users/maheshlalwani.devgmail.com/.pub-cache"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"packages": [
|
||||
{
|
||||
"name": "cursor_gen",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.4",
|
||||
"dependencies": [
|
||||
"ansi_styles",
|
||||
"args",
|
||||
@@ -25,6 +25,37 @@
|
||||
"version": "3.0.0",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"name": "test",
|
||||
"version": "1.31.0",
|
||||
"dependencies": [
|
||||
"analyzer",
|
||||
"async",
|
||||
"boolean_selector",
|
||||
"collection",
|
||||
"coverage",
|
||||
"http_multi_server",
|
||||
"io",
|
||||
"matcher",
|
||||
"node_preamble",
|
||||
"package_config",
|
||||
"path",
|
||||
"pool",
|
||||
"shelf",
|
||||
"shelf_packages_handler",
|
||||
"shelf_static",
|
||||
"shelf_web_socket",
|
||||
"source_span",
|
||||
"stack_trace",
|
||||
"stream_channel",
|
||||
"test_api",
|
||||
"test_core",
|
||||
"typed_data",
|
||||
"web_socket_channel",
|
||||
"webkit_inspection_protocol",
|
||||
"yaml"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ansi_styles",
|
||||
"version": "0.3.2+1",
|
||||
@@ -71,93 +102,6 @@
|
||||
"version": "2.7.0",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"name": "typed_data",
|
||||
"version": "1.4.0",
|
||||
"dependencies": [
|
||||
"collection"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "web",
|
||||
"version": "1.1.1",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"name": "meta",
|
||||
"version": "1.18.2",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"name": "http_parser",
|
||||
"version": "4.1.2",
|
||||
"dependencies": [
|
||||
"collection",
|
||||
"source_span",
|
||||
"string_scanner",
|
||||
"typed_data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "async",
|
||||
"version": "2.13.1",
|
||||
"dependencies": [
|
||||
"collection",
|
||||
"meta"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "string_scanner",
|
||||
"version": "1.4.1",
|
||||
"dependencies": [
|
||||
"source_span"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "source_span",
|
||||
"version": "1.10.2",
|
||||
"dependencies": [
|
||||
"collection",
|
||||
"path",
|
||||
"term_glyph"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "term_glyph",
|
||||
"version": "1.2.2",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"name": "test",
|
||||
"version": "1.31.0",
|
||||
"dependencies": [
|
||||
"analyzer",
|
||||
"async",
|
||||
"boolean_selector",
|
||||
"collection",
|
||||
"coverage",
|
||||
"http_multi_server",
|
||||
"io",
|
||||
"matcher",
|
||||
"node_preamble",
|
||||
"package_config",
|
||||
"path",
|
||||
"pool",
|
||||
"shelf",
|
||||
"shelf_packages_handler",
|
||||
"shelf_static",
|
||||
"shelf_web_socket",
|
||||
"source_span",
|
||||
"stack_trace",
|
||||
"stream_channel",
|
||||
"test_api",
|
||||
"test_core",
|
||||
"typed_data",
|
||||
"web_socket_channel",
|
||||
"webkit_inspection_protocol",
|
||||
"yaml"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "webkit_inspection_protocol",
|
||||
"version": "1.2.1",
|
||||
@@ -176,6 +120,13 @@
|
||||
"web_socket"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "typed_data",
|
||||
"version": "1.4.0",
|
||||
"dependencies": [
|
||||
"collection"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "test_core",
|
||||
"version": "0.6.17",
|
||||
@@ -232,6 +183,15 @@
|
||||
"path"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "source_span",
|
||||
"version": "1.10.2",
|
||||
"dependencies": [
|
||||
"collection",
|
||||
"path",
|
||||
"term_glyph"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "shelf_web_socket",
|
||||
"version": "3.0.0",
|
||||
@@ -293,6 +253,17 @@
|
||||
"version": "2.0.2",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"name": "matcher",
|
||||
"version": "0.12.19",
|
||||
"dependencies": [
|
||||
"async",
|
||||
"meta",
|
||||
"stack_trace",
|
||||
"term_glyph",
|
||||
"test_api"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "io",
|
||||
"version": "1.0.5",
|
||||
@@ -334,6 +305,59 @@
|
||||
"string_scanner"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "async",
|
||||
"version": "2.13.1",
|
||||
"dependencies": [
|
||||
"collection",
|
||||
"meta"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "analyzer",
|
||||
"version": "8.1.1",
|
||||
"dependencies": [
|
||||
"_fe_analyzer_shared",
|
||||
"collection",
|
||||
"convert",
|
||||
"crypto",
|
||||
"glob",
|
||||
"meta",
|
||||
"package_config",
|
||||
"path",
|
||||
"pub_semver",
|
||||
"source_span",
|
||||
"watcher",
|
||||
"yaml"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "web",
|
||||
"version": "1.1.1",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"name": "meta",
|
||||
"version": "1.18.2",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"name": "http_parser",
|
||||
"version": "4.1.2",
|
||||
"dependencies": [
|
||||
"collection",
|
||||
"source_span",
|
||||
"string_scanner",
|
||||
"typed_data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "string_scanner",
|
||||
"version": "1.4.1",
|
||||
"dependencies": [
|
||||
"source_span"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "logging",
|
||||
"version": "1.3.0",
|
||||
@@ -386,6 +410,11 @@
|
||||
"path"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "term_glyph",
|
||||
"version": "1.2.2",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"name": "mime",
|
||||
"version": "2.0.0",
|
||||
@@ -406,43 +435,6 @@
|
||||
"yaml"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "file",
|
||||
"version": "7.0.1",
|
||||
"dependencies": [
|
||||
"meta",
|
||||
"path"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "matcher",
|
||||
"version": "0.12.19",
|
||||
"dependencies": [
|
||||
"async",
|
||||
"meta",
|
||||
"stack_trace",
|
||||
"term_glyph",
|
||||
"test_api"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "analyzer",
|
||||
"version": "8.1.1",
|
||||
"dependencies": [
|
||||
"_fe_analyzer_shared",
|
||||
"collection",
|
||||
"convert",
|
||||
"crypto",
|
||||
"glob",
|
||||
"meta",
|
||||
"package_config",
|
||||
"path",
|
||||
"pub_semver",
|
||||
"source_span",
|
||||
"watcher",
|
||||
"yaml"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "watcher",
|
||||
"version": "1.2.1",
|
||||
@@ -464,6 +456,14 @@
|
||||
"dependencies": [
|
||||
"meta"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "file",
|
||||
"version": "7.0.1",
|
||||
"dependencies": [
|
||||
"meta",
|
||||
"path"
|
||||
]
|
||||
}
|
||||
],
|
||||
"configVersion": 1
|
||||
|
||||
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
@@ -1,5 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
## 1.0.4
|
||||
|
||||
- Fix package-root detection for template resolution: walk up from the resolved `package:cursor_gen/...` file until `pubspec.yaml` is found (global installs were incorrectly using `lib/templates`).
|
||||
|
||||
## 1.0.3
|
||||
|
||||
- Resolve template bundle by sentinel file (`rules/universal/flutter-core.mdc.tmpl`); prefer package `templates/` first for global installs; exit with a clear error instead of writing placeholder-only output.
|
||||
- Generate repo-root `AGENTS.md` and `lefthook.yaml` from `templates/root/`.
|
||||
- Golden tests now `await` async golden comparison so repo-root goldens are written reliably.
|
||||
|
||||
## 1.0.2
|
||||
|
||||
- Wizard aligns with full brief schema; shared `kCursorTemplatesVersion` for lock file and generated brief.
|
||||
|
||||
## 1.0.1
|
||||
|
||||
- Fix default template resolution so generated projects use bundled templates instead of writing `Template not found` placeholders.
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"cursor_templates_version": {
|
||||
"type": "string",
|
||||
"description": "Pillar 1: Pin to template version for reproducibility",
|
||||
"examples": ["1.0.1"]
|
||||
"examples": ["1.0.4"]
|
||||
},
|
||||
"project": {
|
||||
"type": "object",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: cursor_gen
|
||||
description: A CLI tool that generates project-specific Cursor AI configurations for Flutter projects.
|
||||
version: 1.0.1
|
||||
version: 1.0.4
|
||||
homepage: https://github.com/company/flutter-cursor-templates
|
||||
|
||||
environment:
|
||||
|
||||
@@ -70,7 +70,7 @@ class Renderer {
|
||||
'ARCH_IMPORT_RULES': _archImportRules(brief.architecture),
|
||||
'TEST_PATTERN': _testPattern(brief.stateManagement),
|
||||
'LOCALES_LIST': brief.locales.join(', '),
|
||||
'TEMPLATE_VERSION': '1.0.1',
|
||||
'TEMPLATE_VERSION': '1.0.4',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -93,6 +93,9 @@ class Renderer {
|
||||
}
|
||||
|
||||
static String _templatePath(String templateSrc, String key) {
|
||||
if (key.startsWith('root/')) {
|
||||
return p.join(templateSrc, '$key.tmpl');
|
||||
}
|
||||
if (key.startsWith('skills/')) {
|
||||
final skillName = p.basename(key);
|
||||
return p.join(templateSrc, 'skills', skillName, 'SKILL.md.tmpl');
|
||||
@@ -108,6 +111,9 @@ class Renderer {
|
||||
}
|
||||
|
||||
static String _outputPath(String key) {
|
||||
if (key.startsWith('root/')) {
|
||||
return '__root__/${key.substring(5)}';
|
||||
}
|
||||
if (key.startsWith('skills/')) {
|
||||
final name = p.basename(key);
|
||||
return 'skills/$name/SKILL.md';
|
||||
|
||||
@@ -94,6 +94,8 @@ class Resolver {
|
||||
files.add('agents/migration-agent');
|
||||
}
|
||||
|
||||
files.addAll(['root/AGENTS.md', 'root/lefthook.yaml']);
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
@@ -127,6 +129,7 @@ class Resolver {
|
||||
if (key == 'skills/build') return 'Always included — universal TDD-first feature implementation command';
|
||||
if (key.contains('api-client')) return 'api_docs.format is set';
|
||||
if (key.contains('realtime')) return 'features.special contains realtime';
|
||||
if (key.startsWith('root/')) return 'Repo-level companion files';
|
||||
return 'Included';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,17 +8,19 @@ import 'models.dart';
|
||||
import 'logger.dart';
|
||||
|
||||
const _lockFileName = '.cursor-gen-lock.json';
|
||||
const _currentVersion = '1.0.1';
|
||||
|
||||
/// Current flutter-cursor-templates bundle version (lock file, wizard, --check-updates).
|
||||
const kCursorTemplatesVersion = '1.0.4';
|
||||
|
||||
class VersionManager {
|
||||
/// Check if the project's locked version differs from the current template version
|
||||
static Future<VersionStatus> check({required ProjectBrief brief}) async {
|
||||
final locked = brief.cursorTemplatesVersion ?? 'unset';
|
||||
final hasUpdate = locked != _currentVersion && locked != 'unset';
|
||||
final hasUpdate = locked != kCursorTemplatesVersion && locked != 'unset';
|
||||
return VersionStatus(
|
||||
hasUpdate: hasUpdate,
|
||||
currentVersion: locked,
|
||||
latestVersion: _currentVersion,
|
||||
latestVersion: kCursorTemplatesVersion,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,7 +32,7 @@ class VersionManager {
|
||||
}) async {
|
||||
final briefHash = await _fileHash(briefPath);
|
||||
final lock = {
|
||||
'templateVersion': _currentVersion,
|
||||
'templateVersion': kCursorTemplatesVersion,
|
||||
'generatedAt': DateTime.now().toIso8601String(),
|
||||
'briefHash': briefHash,
|
||||
'projectName': brief.projectName,
|
||||
@@ -72,15 +74,15 @@ class VersionManager {
|
||||
final lockedVersion = lock['templateVersion'] as String? ?? 'unknown';
|
||||
Logger.info('Template version check:');
|
||||
Logger.info(' Locked: $lockedVersion');
|
||||
Logger.info(' Latest: $_currentVersion');
|
||||
Logger.info(' Latest: $kCursorTemplatesVersion');
|
||||
|
||||
if (lockedVersion == _currentVersion) {
|
||||
if (lockedVersion == kCursorTemplatesVersion) {
|
||||
Logger.success(' ✔ You are on the latest template version.');
|
||||
} else {
|
||||
Logger.warn(' ⚠ Update available!');
|
||||
Logger.info('\nTo update:');
|
||||
Logger.info(
|
||||
' 1. Update cursor_templates_version in project-brief.yaml to "$_currentVersion"');
|
||||
' 1. Update cursor_templates_version in project-brief.yaml to "$kCursorTemplatesVersion"');
|
||||
Logger.info(' 2. Run: cursor_gen --diff (preview changes)');
|
||||
Logger.info(' 3. Run: cursor_gen --refresh (apply updates)');
|
||||
Logger.info(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import 'dart:io';
|
||||
import 'logger.dart';
|
||||
import 'version_manager.dart';
|
||||
|
||||
class Wizard {
|
||||
static Future<void> run({required String outputPath}) async {
|
||||
@@ -80,6 +81,22 @@ class Wizard {
|
||||
|
||||
Logger.info('');
|
||||
|
||||
// Features
|
||||
final modulesRaw = _ask(
|
||||
'Feature modules (comma-separated, optional)', hint: '');
|
||||
answers['feature_modules'] = _splitComma(modulesRaw);
|
||||
answers['special_features'] = _askMultiChoice(
|
||||
'Special capabilities (optional)',
|
||||
[
|
||||
'realtime',
|
||||
'push_notifications',
|
||||
'deep_linking',
|
||||
'offline_first',
|
||||
],
|
||||
defaults: []);
|
||||
|
||||
Logger.info('');
|
||||
|
||||
// Environments
|
||||
final flavorsInput =
|
||||
_ask('Build flavors (comma-separated)', hint: 'dev,staging,prod');
|
||||
@@ -92,10 +109,60 @@ class Wizard {
|
||||
answers['testing_depth'] = _askChoice(
|
||||
'Testing depth', ['unit_widget', 'integration', 'e2e', 'full'],
|
||||
defaultIdx: 0);
|
||||
final testingDepth = answers['testing_depth'] as String;
|
||||
if (testingDepth == 'e2e' || testingDepth == 'full') {
|
||||
answers['e2e_tool'] = _askChoice(
|
||||
'E2E tool', ['patrol', 'maestro'], defaultIdx: 0);
|
||||
} else {
|
||||
answers['e2e_tool'] = 'patrol';
|
||||
}
|
||||
|
||||
Logger.info('');
|
||||
|
||||
// Design (validator-aligned sources)
|
||||
answers['design_source'] = _askChoice('Design source', [
|
||||
'figma_mcp',
|
||||
'figma_manual',
|
||||
'native_ref',
|
||||
'html_ref',
|
||||
'none',
|
||||
], defaultIdx: 4);
|
||||
final designSource = answers['design_source'] as String;
|
||||
if (designSource == 'figma_mcp' || designSource == 'figma_manual') {
|
||||
answers['figma_url'] =
|
||||
_ask('Figma file URL', hint: 'https://www.figma.com/design/...');
|
||||
} else {
|
||||
answers['figma_url'] = '';
|
||||
}
|
||||
|
||||
Logger.info('');
|
||||
|
||||
// API docs
|
||||
answers['api_docs_format'] = _askChoice(
|
||||
'API docs format', ['openapi', 'postman', 'markdown', 'none'],
|
||||
defaultIdx: 3);
|
||||
final apiFmt = answers['api_docs_format'] as String;
|
||||
if (apiFmt != 'none') {
|
||||
answers['api_docs_path'] =
|
||||
_ask('Path to API spec (repo-relative)', hint: 'docs/api.yaml');
|
||||
} else {
|
||||
answers['api_docs_path'] = '';
|
||||
}
|
||||
|
||||
Logger.info('');
|
||||
|
||||
// Localization
|
||||
final l10n = _askBool('Enable localization / i18n?', defaultYes: false);
|
||||
answers['i18n'] = l10n;
|
||||
if (l10n) {
|
||||
final locRaw =
|
||||
_ask('Locale codes (comma-separated)', hint: 'en');
|
||||
var locales = _splitComma(locRaw);
|
||||
if (locales.isEmpty) locales = ['en'];
|
||||
answers['locales'] = locales;
|
||||
} else {
|
||||
answers['locales'] = <String>['en'];
|
||||
}
|
||||
|
||||
// Telemetry opt-in (Pillar 6)
|
||||
final telemetry = _askBool(
|
||||
@@ -183,13 +250,18 @@ class Wizard {
|
||||
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);
|
||||
final featureMods = List<String>.from(a['feature_modules'] as List);
|
||||
final specialFeats = List<String>.from(a['special_features'] as List);
|
||||
final locales = List<String>.from(a['locales'] as List);
|
||||
final figmaUrl = a['figma_url'] as String;
|
||||
final apiPath = a['api_docs_path'] as String;
|
||||
return '''# project-brief.yaml — cursor_gen configuration
|
||||
# Generated by cursor_gen --wizard
|
||||
# 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.1"
|
||||
cursor_templates_version: "$kCursorTemplatesVersion"
|
||||
|
||||
project:
|
||||
name: "${a['name']}"
|
||||
@@ -216,15 +288,15 @@ environments:
|
||||
|
||||
testing:
|
||||
depth: "${a['testing_depth']}"
|
||||
e2e_tool: "patrol"
|
||||
e2e_tool: "${a['e2e_tool']}"
|
||||
|
||||
design:
|
||||
source: "none"
|
||||
figma_url: ""
|
||||
source: "${a['design_source']}"
|
||||
figma_url: "${_yamlEsc(figmaUrl)}"
|
||||
|
||||
api_docs:
|
||||
format: "none"
|
||||
path: ""
|
||||
format: "${a['api_docs_format']}"
|
||||
path: "${_yamlEsc(apiPath)}"
|
||||
|
||||
references:
|
||||
repos: [${_yamlQStringList(refRepos)}]
|
||||
@@ -236,12 +308,12 @@ app_context:
|
||||
role_names: [${_yamlQStringList(roles)}]
|
||||
|
||||
features:
|
||||
modules: []
|
||||
special: []
|
||||
modules: [${_yamlQStringList(featureMods)}]
|
||||
special: [${_yamlQStringList(specialFeats)}]
|
||||
|
||||
localization:
|
||||
enabled: ${a['i18n']}
|
||||
locales: ["en"]
|
||||
locales: [${_yamlQStringList(locales)}]
|
||||
|
||||
# Pillar 6: Opt-in local telemetry (logs rule trigger frequency locally)
|
||||
telemetry_opt_in: ${a['telemetry']}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# {{PROJECT_NAME}}
|
||||
|
||||
Repo-level notes for AI assistants. Authoritative stack and conventions are in `project-brief.yaml` and `.cursor/` (regenerate with `cursor_gen` from the project root).
|
||||
|
||||
- **Package:** `{{PACKAGE_ID}}`
|
||||
@@ -0,0 +1,5 @@
|
||||
# {{PROJECT_NAME}} — generated by cursor_gen; adjust commands to your repo
|
||||
pre-commit:
|
||||
commands:
|
||||
flutter-analyze:
|
||||
run: flutter analyze
|
||||
@@ -141,6 +141,9 @@ void main() {
|
||||
expect(files, containsNot('rules/state-management/getx'));
|
||||
expect(files, containsNot('rules/architecture/feature_first'));
|
||||
expect(files, containsNot('rules/routing/getx_nav'));
|
||||
|
||||
expect(files, contains('root/AGENTS.md'));
|
||||
expect(files, contains('root/lefthook.yaml'));
|
||||
});
|
||||
|
||||
test('Riverpod + Feature-First + Web resolves web platform template', () {
|
||||
@@ -440,7 +443,7 @@ void main() {
|
||||
templateFiles: Resolver.resolve(_riverpodFFBrief),
|
||||
templateSrc: templateDir,
|
||||
);
|
||||
_compareGoldens('test/golden/riverpod-ff-supabase', rendered);
|
||||
await _compareGoldens('test/golden/riverpod-ff-supabase', rendered);
|
||||
});
|
||||
|
||||
test('GetX + MVC renders match golden files', () async {
|
||||
@@ -454,7 +457,7 @@ void main() {
|
||||
templateFiles: Resolver.resolve(_getxMvcBrief),
|
||||
templateSrc: templateDir,
|
||||
);
|
||||
_compareGoldens('test/golden/getx-mvc-rest', rendered);
|
||||
await _compareGoldens('test/golden/getx-mvc-rest', rendered);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# TestApp
|
||||
|
||||
Repo-level notes for AI assistants. Authoritative stack and conventions are in `project-brief.yaml` and `.cursor/` (regenerate with `cursor_gen` from the project root).
|
||||
|
||||
- **Package:** `com.test.testapp`
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
# TestApp — generated by cursor_gen; adjust commands to your repo
|
||||
pre-commit:
|
||||
commands:
|
||||
flutter-analyze:
|
||||
run: flutter analyze
|
||||
+1
-1
@@ -571,4 +571,4 @@ dart run build_runner build --delete-conflicting-outputs
|
||||
|
||||
Commit generated files (`.g.dart`, `.freezed.dart`, `injection.config.dart`) — do not gitignore them.
|
||||
|
||||
**Template version:** 1.0.1
|
||||
**Template version:** 1.0.4
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# LegacyApp
|
||||
|
||||
Repo-level notes for AI assistants. Authoritative stack and conventions are in `project-brief.yaml` and `.cursor/` (regenerate with `cursor_gen` from the project root).
|
||||
|
||||
- **Package:** `com.test.legacy`
|
||||
@@ -0,0 +1,5 @@
|
||||
# LegacyApp — generated by cursor_gen; adjust commands to your repo
|
||||
pre-commit:
|
||||
commands:
|
||||
flutter-analyze:
|
||||
run: flutter analyze
|
||||
@@ -0,0 +1,572 @@
|
||||
# Build — LegacyApp
|
||||
|
||||
Implements any feature end-to-end: deep research → TDD → integration tests → external setup checklist → verified PR.
|
||||
Stack: **GetX** / **MVC** / **REST API** / ios, android.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/build <free-text description of what to implement>
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
- `/build implement notification module end-to-end`
|
||||
- `/build add biometric auth`
|
||||
- `/build integrate Stripe payments`
|
||||
- `/build add deep linking for shared product pages`
|
||||
- `/build implement analytics event tracking`
|
||||
|
||||
---
|
||||
|
||||
## FEATURE_REGISTRY
|
||||
|
||||
The AI reads this registry to classify the request and load the right research, test scenarios, and setup steps.
|
||||
|
||||
```yaml
|
||||
feature_registry:
|
||||
|
||||
notifications:
|
||||
keywords: [notification, push, fcm, apns, alert, badge, silent push,
|
||||
remote notification, local notification, firebase messaging]
|
||||
pub_packages: [firebase_messaging, flutter_local_notifications, firebase_core]
|
||||
rules_to_load: [security-standards.mdc, platform-ios.mdc, platform-android.mdc]
|
||||
unit_test_scenarios:
|
||||
- parse notification payload to domain model
|
||||
- FCM token generation and storage
|
||||
- token refresh triggers re-registration
|
||||
- notification permission denied returns graceful fallback
|
||||
- notification service initialisation is idempotent
|
||||
widget_test_scenarios:
|
||||
- notification banner renders correct title and body
|
||||
- tap on notification navigates to correct route
|
||||
- badge count updates on new message
|
||||
integration_test_scenarios:
|
||||
- foreground notification receipt and display
|
||||
- background notification receipt (app backgrounded, not killed)
|
||||
- killed-state notification receipt (cold launch from notification tap)
|
||||
- tap-to-open in foreground state routes correctly
|
||||
- tap-to-open in background state routes correctly
|
||||
- tap-to-open in killed state routes correctly
|
||||
- notification payload parsing end-to-end
|
||||
- deep link routing from notification data field
|
||||
- multiple simultaneous notifications (ordering and dedup)
|
||||
external_setup:
|
||||
firebase: [Enable Cloud Messaging, download google-services.json, download GoogleService-Info.plist]
|
||||
ios: [Push Notifications capability, Background Modes remote notifications, APNs .p8 key upload to Firebase]
|
||||
android: [POST_NOTIFICATIONS permission, RECEIVE_BOOT_COMPLETED permission, google-services plugin]
|
||||
files_to_touch:
|
||||
new: [lib/features/notifications/, integration_test/notifications/]
|
||||
modified: [lib/main.dart, android/app/src/main/AndroidManifest.xml, ios/Runner/Info.plist, ios/Runner/AppDelegate.swift, pubspec.yaml]
|
||||
|
||||
auth:
|
||||
keywords: [auth, authentication, login, sign in, sign out, logout,
|
||||
biometric, face id, touch id, fingerprint, oauth, jwt,
|
||||
session, token refresh, social login, google sign-in]
|
||||
pub_packages: [firebase_auth, supabase_flutter, local_auth, flutter_secure_storage, google_sign_in]
|
||||
rules_to_load: [security-standards.mdc, platform-ios.mdc, platform-android.mdc]
|
||||
unit_test_scenarios:
|
||||
- login success stores token securely
|
||||
- login failure (wrong password) returns typed error
|
||||
- login failure (network) returns typed error
|
||||
- token refresh succeeds and updates stored token
|
||||
- token refresh failure triggers logout
|
||||
- biometric auth success grants access
|
||||
- biometric auth failure falls back to password
|
||||
- biometric not enrolled returns correct error state
|
||||
- logout clears all stored credentials
|
||||
- session persistence: token loaded on cold start
|
||||
widget_test_scenarios:
|
||||
- login form renders correctly
|
||||
- validation errors shown inline
|
||||
- loading state disables submit button
|
||||
- biometric prompt shown when available
|
||||
integration_test_scenarios:
|
||||
- complete login flow (email and password)
|
||||
- login then logout then login again
|
||||
- invalid credentials shows error and stays on login
|
||||
- biometric login flow on enrolled device
|
||||
- biometric fallback to password
|
||||
- token refresh in background while user navigates
|
||||
- cold start with stored valid session skips login
|
||||
- cold start with expired session redirects to login
|
||||
- social auth OAuth redirect and return
|
||||
- deep link into protected route redirects to login then back
|
||||
external_setup:
|
||||
firebase: [Enable Email/Password provider, Enable Google provider, download updated config files]
|
||||
ios: [NSFaceIDUsageDescription in Info.plist, LocalAuthentication.framework in Xcode]
|
||||
android: [USE_BIOMETRIC permission, USE_FINGERPRINT permission, minSdkVersion 23 for biometric]
|
||||
files_to_touch:
|
||||
new: [lib/features/auth/, integration_test/auth/]
|
||||
modified: [lib/main.dart, lib/core/di/injection.dart, pubspec.yaml]
|
||||
|
||||
payments:
|
||||
keywords: [payment, stripe, in-app purchase, iap, checkout, subscription,
|
||||
billing, apple pay, google pay, card, transaction, refund, revenue cat]
|
||||
pub_packages: [flutter_stripe, purchases_flutter, in_app_purchase]
|
||||
rules_to_load: [security-standards.mdc, platform-ios.mdc, platform-android.mdc]
|
||||
unit_test_scenarios:
|
||||
- checkout request builds correct PaymentIntent params
|
||||
- payment success updates order state
|
||||
- payment failure (card declined) returns typed error
|
||||
- webhook event parsed to domain model
|
||||
- refund request constructs correct API call
|
||||
- subscription status checked on app resume
|
||||
widget_test_scenarios:
|
||||
- checkout form renders with correct amount
|
||||
- payment loading state shown during processing
|
||||
- success confirmation screen renders
|
||||
- error state with retry option
|
||||
integration_test_scenarios:
|
||||
- end-to-end checkout with test card (Stripe test mode)
|
||||
- checkout with 3D Secure challenge
|
||||
- card declined shows error then retry succeeds
|
||||
- Apple Pay / Google Pay sheet appears (device capability check)
|
||||
- subscription purchase and entitlement unlock
|
||||
- subscription restore flow
|
||||
- refund flow from order history
|
||||
external_setup:
|
||||
stripe: [publishable and secret keys in flavor .env files, webhook endpoint in Stripe Dashboard, Apple Pay domain registration]
|
||||
ios: [In-App Purchase capability, Apple Pay capability and merchant identifier, com.apple.developer.in-app-payments entitlement]
|
||||
android: [Google Pay Console setup, BILLING permission for in_app_purchase]
|
||||
files_to_touch:
|
||||
new: [lib/features/payments/, integration_test/payments/]
|
||||
modified: [ios/Runner/Runner.entitlements, android/app/src/main/AndroidManifest.xml, pubspec.yaml]
|
||||
|
||||
deep_links:
|
||||
keywords: [deep link, deep linking, universal link, app link, deferred deep link,
|
||||
dynamic link, branch, uri scheme, custom scheme, url scheme]
|
||||
pub_packages: [go_router, app_links, uni_links]
|
||||
rules_to_load: [platform-ios.mdc, platform-android.mdc]
|
||||
unit_test_scenarios:
|
||||
- URI parsed to correct route and parameters
|
||||
- unknown URI falls back to home
|
||||
- authenticated-only route redirects to login when unauthenticated
|
||||
- deep link preserves query parameters
|
||||
widget_test_scenarios:
|
||||
- navigation guard redirects unauthenticated deep link
|
||||
integration_test_scenarios:
|
||||
- cold start from Universal Link routes to correct screen
|
||||
- cold start from custom URI scheme routes correctly
|
||||
- backgrounded app receives deep link and navigates
|
||||
- foreground app receives deep link and navigates
|
||||
- deep link to authenticated route redirects to login then original destination
|
||||
- deep link with path parameters loads correct content
|
||||
- invalid or malformed deep link shows 404 screen
|
||||
external_setup:
|
||||
ios: [Associated Domains capability applinks:yourdomain.com, host apple-app-site-association file at /.well-known/]
|
||||
android: [intent-filter with android:autoVerify=true in AndroidManifest.xml, host assetlinks.json at /.well-known/]
|
||||
files_to_touch:
|
||||
new: [lib/core/routing/deep_link_handler.dart, integration_test/deep_links/]
|
||||
modified: [lib/core/routing/router.dart, android/app/src/main/AndroidManifest.xml, ios/Runner/Runner.entitlements, ios/Runner/Info.plist]
|
||||
|
||||
analytics:
|
||||
keywords: [analytics, tracking, event, mixpanel, amplitude, firebase analytics,
|
||||
segment, posthog, screen view, funnel, cohort]
|
||||
pub_packages: [firebase_analytics, mixpanel_flutter, amplitude_flutter, posthog_flutter]
|
||||
rules_to_load: [security-standards.mdc]
|
||||
unit_test_scenarios:
|
||||
- analytics service logs correct event name and params
|
||||
- PII fields stripped before event sent
|
||||
- analytics disabled in dev flavor
|
||||
- screen name logged on navigation
|
||||
widget_test_scenarios:
|
||||
- RouteObserver triggers screen_view event
|
||||
integration_test_scenarios:
|
||||
- user action triggers expected event (verify via debug view)
|
||||
- screen transitions log screen_view with correct names
|
||||
- opt-out disables all tracking
|
||||
external_setup:
|
||||
firebase: [Enable Analytics in Firebase Console, enable DebugView for local testing, configure conversion events]
|
||||
files_to_touch:
|
||||
new: [lib/core/analytics/analytics_service.dart, lib/core/analytics/analytics_events.dart, integration_test/analytics/]
|
||||
modified: [lib/main.dart, pubspec.yaml]
|
||||
|
||||
storage:
|
||||
keywords: [storage, file upload, download, cloud storage, firebase storage,
|
||||
supabase storage, s3, image upload, document, file picker,
|
||||
offline, cache, hive, isar, objectbox, sqflite, drift]
|
||||
pub_packages: [firebase_storage, supabase_flutter, hive_flutter, isar, drift, file_picker, image_picker, path_provider]
|
||||
rules_to_load: [security-standards.mdc, platform-ios.mdc, platform-android.mdc]
|
||||
unit_test_scenarios:
|
||||
- upload returns public URL on success
|
||||
- upload failure returns typed error
|
||||
- local cache read before remote fetch (offline-first)
|
||||
- cache invalidation on TTL expiry
|
||||
- large file upload uses resumable upload
|
||||
widget_test_scenarios:
|
||||
- file picker button triggers picker
|
||||
- upload progress indicator shown
|
||||
- image preview renders after selection
|
||||
integration_test_scenarios:
|
||||
- end-to-end file upload and retrieval
|
||||
- offline mode: local cache serves data
|
||||
- background upload completes when connectivity restored
|
||||
- file size limit enforced
|
||||
external_setup:
|
||||
firebase: [Enable Firebase Storage, configure Storage security rules, set CORS policy for web if applicable]
|
||||
ios: [NSPhotoLibraryUsageDescription in Info.plist, NSCameraUsageDescription in Info.plist]
|
||||
android: [READ_EXTERNAL_STORAGE or READ_MEDIA_IMAGES permission]
|
||||
files_to_touch:
|
||||
new: [lib/features/storage/, integration_test/storage/]
|
||||
modified: [android/app/src/main/AndroidManifest.xml, ios/Runner/Info.plist, pubspec.yaml]
|
||||
|
||||
camera_media:
|
||||
keywords: [camera, photo, video, image picker, qr code, barcode scanner,
|
||||
ar, augmented reality, gallery, media, record, capture]
|
||||
pub_packages: [camera, image_picker, mobile_scanner, qr_flutter, image_cropper, video_player]
|
||||
rules_to_load: [security-standards.mdc, platform-ios.mdc, platform-android.mdc]
|
||||
unit_test_scenarios:
|
||||
- QR/barcode parsed to correct domain model
|
||||
- image compressed before upload
|
||||
- camera permission denied returns typed error
|
||||
widget_test_scenarios:
|
||||
- camera preview widget renders
|
||||
- capture button triggers photo take
|
||||
integration_test_scenarios:
|
||||
- camera opens and captures photo on real device
|
||||
- gallery picker selects image and returns
|
||||
- QR scan decodes valid code correctly
|
||||
- permission denied shows correct error UI
|
||||
external_setup:
|
||||
ios: [NSCameraUsageDescription, NSPhotoLibraryUsageDescription, NSMicrophoneUsageDescription for video]
|
||||
android: [CAMERA permission, READ_MEDIA_IMAGES for gallery access]
|
||||
files_to_touch:
|
||||
new: [lib/features/camera/, integration_test/camera/]
|
||||
modified: [android/app/src/main/AndroidManifest.xml, ios/Runner/Info.plist, pubspec.yaml]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Context Loading
|
||||
|
||||
**When the user types `/build <request>`, do the following before writing a single line of code:**
|
||||
|
||||
1. Read `project-brief.yaml` from the repo root. Extract and hold in context:
|
||||
- `stack.state_management` → **getx**
|
||||
- `stack.architecture` → **mvc**
|
||||
- `stack.routing` → **getx_nav**
|
||||
- `stack.backend` → **rest**
|
||||
- `stack.platforms` → **ios, android**
|
||||
- `stack.codegen` → **none**
|
||||
- `testing.depth` → **unit_widget**
|
||||
- `testing.e2e_tool` → **patrol**
|
||||
- `features.modules` (existing features) → **auth, dashboard**
|
||||
- `features.special` → ****
|
||||
- `environments.flavors` → **dev, prod**
|
||||
- `environments.cicd` → **codemagic**
|
||||
|
||||
2. Parse the user's free-text request. Match against `FEATURE_REGISTRY` keywords using longest-match. Multiple types are allowed if the request spans features.
|
||||
|
||||
3. Load `.cursor/rules/` files listed under `rules_to_load` for the matched feature type.
|
||||
|
||||
4. If the user's message contains a URL or GitHub repo reference, fetch and index it for pattern reference.
|
||||
|
||||
5. Check `features.modules` — if the feature already exists in the list, ask: "This feature is already listed in project-brief.yaml. Building additional capability on top of it? (y/n)"
|
||||
|
||||
6. Print the context summary table before proceeding:
|
||||
|
||||
```
|
||||
| Field | Value |
|
||||
|------------------|------------------------------|
|
||||
| Feature type | [detected type] |
|
||||
| State management | GetX |
|
||||
| Architecture | MVC |
|
||||
| Backend | REST API |
|
||||
| Platforms | ios, android |
|
||||
| E2E tool | patrol |
|
||||
| Rules loaded | [list from rules_to_load] |
|
||||
| Existing modules | auth, dashboard |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Deep Research
|
||||
|
||||
**Before touching any file, print the full research output.**
|
||||
|
||||
1. From the FEATURE_REGISTRY, enumerate all `pub_packages` for the detected type. For each, determine the correct version compatible with current Flutter stable (`flutter --version`). Print the full `pubspec.yaml` additions.
|
||||
|
||||
2. Build the complete file manifest, adapting to `MVC`:
|
||||
|
||||
**For MVC (mvc):**
|
||||
- `clean`: expand each feature layer explicitly → `domain/entities/`, `domain/repositories/`, `domain/usecases/`, `data/models/`, `data/datasources/`, `data/repositories/`, `presentation/`
|
||||
- `feature_first`: `[feature]/[feature]_screen.dart`, `[feature]_provider.dart` or `[feature]_bloc.dart`, `[feature]_repository.dart`, `[feature]_model.dart`, `widgets/`
|
||||
- `mvc`: `[feature]/model/`, `[feature]/view/`, `[feature]/controller/`
|
||||
|
||||
**State management file naming (GetX / getx):**
|
||||
- `bloc`: `[feature]_bloc.dart`, `[feature]_event.dart`, `[feature]_state.dart`
|
||||
- `riverpod`: `[feature]_provider.dart`, `[feature]_notifier.dart`
|
||||
- `getx`: `[feature]_controller.dart`, `[feature]_binding.dart`
|
||||
|
||||
3. List all modified files from `files_to_touch.modified` — show exactly what change goes in each file.
|
||||
|
||||
4. List all external service configuration required per platform (from `external_setup`). Only show platforms present in `ios, android`.
|
||||
|
||||
5. Print research results in this format:
|
||||
|
||||
```
|
||||
## Research Results
|
||||
|
||||
### Packages to add to pubspec.yaml
|
||||
- [package_name]: ^[version]
|
||||
|
||||
### Files to create (MVC / GetX)
|
||||
lib/features/[name]/... (full list)
|
||||
|
||||
### Files to modify
|
||||
[file_path] — [what changes]
|
||||
|
||||
### External services requiring configuration
|
||||
- [Service] ([platform]) — [what to do]
|
||||
```
|
||||
|
||||
Do not touch any file until the user has seen this output.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: TDD Implementation
|
||||
|
||||
> **Invoke skill: `superpowers:test-driven-development`**
|
||||
|
||||
Follow Red → Green → Refactor strictly. Show actual terminal output at each step.
|
||||
|
||||
### Step 3a — Red: Write all failing tests first
|
||||
|
||||
1. Mirror the feature directory under `test/features/[feature]/`.
|
||||
2. Write unit tests for **every scenario** in `FEATURE_REGISTRY.unit_test_scenarios` for the detected type.
|
||||
3. Write widget tests for **every scenario** in `FEATURE_REGISTRY.widget_test_scenarios`.
|
||||
4. Use `mocktail` for all dependencies.
|
||||
5. Test naming convention: `'given [precondition], when [action], then [expected outcome]'`
|
||||
6. Use the state management test pattern for **GetX**:
|
||||
```
|
||||
Get.put(MyController()); final ctrl = Get.find<MyController>(); expect(ctrl.value, expected);
|
||||
```
|
||||
7. Run and confirm red:
|
||||
```
|
||||
flutter test test/features/[feature]/ --no-pub
|
||||
```
|
||||
**Paste the actual output here before proceeding.**
|
||||
|
||||
### Step 3b — Green: Implement
|
||||
|
||||
1. Create domain entities and repository interfaces.
|
||||
2. Create data-layer implementations wiring to `REST API`.
|
||||
3. Create presentation layer using **GetX** patterns.
|
||||
4. Register in DI container — follow `- View (Widget) MUST NOT contain business logic
|
||||
- Controller MUST NOT import Flutter widgets directly
|
||||
- Model MUST be plain Dart, no framework dependencies`.
|
||||
5. Wire the route in `GetX Navigation` router.
|
||||
6. If `none` is not `none`: run `dart run build_runner build --delete-conflicting-outputs` after adding models.
|
||||
7. Run tests and confirm green:
|
||||
```
|
||||
flutter test test/features/[feature]/ --no-pub
|
||||
```
|
||||
**Paste the actual output here before proceeding.**
|
||||
|
||||
### Step 3c — Refactor
|
||||
|
||||
1. Review for duplication, naming, and layer boundary violations per `.cursor/rules/flutter-core.mdc`.
|
||||
2. Run and confirm clean:
|
||||
```
|
||||
flutter analyze
|
||||
```
|
||||
**Paste the actual output here before proceeding.**
|
||||
3. Run tests once more to confirm still green.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Integration Test Generation
|
||||
|
||||
**Generate `integration_test/[feature_type]/` before asking the user to run anything.**
|
||||
|
||||
### Integration test file structure
|
||||
|
||||
Each test file follows this template:
|
||||
|
||||
```dart
|
||||
// integration_test/[feature]/[scenario]_test.dart
|
||||
// Generated by /build — LegacyApp
|
||||
// Feature: [feature_type] | Scenario: [scenario_name]
|
||||
// Run with: flutter test integration_test/[feature]/[scenario]_test.dart -d <device_id>
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:com.test.legacy/main.dart' as app;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('[FeatureType] — [Scenario Name]', () {
|
||||
setUp(() async {
|
||||
// Seed state / configure mocks / reset storage
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
// Cleanup
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'given [precondition], when [action], then [expected outcome]',
|
||||
(tester) async {
|
||||
app.main();
|
||||
await tester.pumpAndSettle();
|
||||
// test body
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**For `patrol` (patrol):** swap `flutter_test` imports for `package:patrol` and use Patrol's `$` NativeAutomator selector syntax.
|
||||
|
||||
### Files to generate
|
||||
|
||||
Create one file per logical scenario cluster from `FEATURE_REGISTRY.integration_test_scenarios`.
|
||||
Also generate `integration_test/[feature]/README.md` with the full test matrix and run commands.
|
||||
|
||||
### PAUSE GATE — user must run on device
|
||||
|
||||
After generating all files, print this table and **stop**. Wait for the user to paste device output before Phase 6.
|
||||
|
||||
```
|
||||
## ACTION REQUIRED — Run Integration Tests on Real Device
|
||||
|
||||
| File | Covers | Requires Hardware |
|
||||
|------|--------|------------------|
|
||||
| integration_test/[feature]/[scenario]_test.dart | [what it covers] | Yes/No |
|
||||
...
|
||||
|
||||
Run on iOS:
|
||||
flutter test integration_test/[feature]/ -d <your_ios_device_id>
|
||||
|
||||
Run on Android:
|
||||
flutter test integration_test/[feature]/ -d <your_android_device_id>
|
||||
|
||||
Paste the output here to continue.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: External Setup Checklist
|
||||
|
||||
**Print before asking the user to verify anything. Group by service/platform. Only show sections for platforms in `ios, android`.**
|
||||
|
||||
### Format for console/service steps (numbered list):
|
||||
|
||||
```markdown
|
||||
### Firebase (backend: REST API)
|
||||
- [ ] 1. Open Firebase Console → [exact menu path]
|
||||
- [ ] 2. [Specific action]
|
||||
- [ ] 3. Download updated config file → place at [exact path]
|
||||
```
|
||||
|
||||
### Format for native platform steps (table):
|
||||
|
||||
```markdown
|
||||
### iOS Setup
|
||||
| Step | Where | What | How to Verify |
|
||||
|------|-------|------|---------------|
|
||||
| [Step name] | [Xcode location / file path] | [Exact change] | [Verification method] |
|
||||
```
|
||||
|
||||
```markdown
|
||||
### Android Setup
|
||||
- [ ] 1. Open `android/app/src/main/AndroidManifest.xml`
|
||||
- Add: `<uses-permission android:name="[permission]"/>`
|
||||
- [ ] 2. [Next step with exact value]
|
||||
```
|
||||
|
||||
### Flavor scoping note (flavors: dev, prod)
|
||||
|
||||
Config files and API keys must be scoped per flavor. Never place production keys in `dev` flavor files. Follow the pattern established in `.cursor/rules/` for flavor-based configuration.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Verification Gate
|
||||
|
||||
> **Invoke skill: `superpowers:verification-before-completion`**
|
||||
|
||||
**Do not claim completion without pasting real output for every item below.**
|
||||
|
||||
```
|
||||
VERIFICATION REQUIRED — paste real output for each:
|
||||
|
||||
[ ] flutter test test/features/[feature]/ --no-pub
|
||||
Required: "All N tests passed."
|
||||
|
||||
[ ] flutter analyze
|
||||
Required: "No issues found!"
|
||||
|
||||
[ ] Integration test device output (from Phase 4 pause gate)
|
||||
Required: actual device test output
|
||||
|
||||
[ ] lefthook run pre-commit
|
||||
Required: all hooks passed
|
||||
```
|
||||
|
||||
If any check fails: invoke `superpowers:systematic-debugging` to diagnose before retrying.
|
||||
Do not proceed to Phase 7 until all four checks are green.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: PR Preparation
|
||||
|
||||
> **Invoke skill: `superpowers:finishing-a-development-branch`**
|
||||
|
||||
1. **Commit message** (conventional commits format):
|
||||
```
|
||||
feat([feature_type]): implement [feature] end-to-end
|
||||
```
|
||||
|
||||
2. **PR description template:**
|
||||
```markdown
|
||||
## What
|
||||
[One sentence: what feature was implemented]
|
||||
|
||||
## Test coverage
|
||||
- Unit/widget tests: N passing
|
||||
- Integration test matrix:
|
||||
| Scenario | iOS | Android |
|
||||
|----------|-----|---------|
|
||||
| [scenario] | ✅ | ✅ |
|
||||
|
||||
## External setup completed
|
||||
- [ ] [Service 1 setup]
|
||||
- [ ] [Service 2 setup]
|
||||
|
||||
## Notes
|
||||
[Any follow-up TODOs or known limitations]
|
||||
```
|
||||
|
||||
3. **CI/CD advice for Codemagic (codemagic):**
|
||||
Ensure workflow secrets are scoped per flavor (dev, prod). Never expose production keys in dev environment secrets. Verify the CI workflow runs `flutter test` and `flutter analyze` before deploy steps.
|
||||
|
||||
---
|
||||
|
||||
## Rules applied every phase
|
||||
|
||||
Always active — read before writing any code:
|
||||
- `.cursor/rules/flutter-core.mdc`
|
||||
- `.cursor/rules/security-standards.mdc`
|
||||
- `.cursor/rules/project-context.mdc`
|
||||
- Feature-type-specific rules from `FEATURE_REGISTRY.rules_to_load`
|
||||
|
||||
Architecture import rules for **MVC**:
|
||||
- View (Widget) MUST NOT contain business logic
|
||||
- Controller MUST NOT import Flutter widgets directly
|
||||
- Model MUST be plain Dart, no framework dependencies
|
||||
|
||||
---
|
||||
|
||||
## Code generation notes
|
||||
|
||||
**Codegen tools configured: none**
|
||||
|
||||
After adding any new model or injectable class, run:
|
||||
```
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
```
|
||||
|
||||
Commit generated files (`.g.dart`, `.freezed.dart`, `injection.config.dart`) — do not gitignore them.
|
||||
|
||||
**Template version:** 1.0.4
|
||||
@@ -0,0 +1,5 @@
|
||||
# TaskFlow
|
||||
|
||||
Repo-level notes for AI assistants. Authoritative stack and conventions are in `project-brief.yaml` and `.cursor/` (regenerate with `cursor_gen` from the project root).
|
||||
|
||||
- **Package:** `com.test.taskflow`
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
# TaskFlow — generated by cursor_gen; adjust commands to your repo
|
||||
pre-commit:
|
||||
commands:
|
||||
flutter-analyze:
|
||||
run: flutter analyze
|
||||
+1
-1
@@ -569,4 +569,4 @@ dart run build_runner build --delete-conflicting-outputs
|
||||
|
||||
Commit generated files (`.g.dart`, `.freezed.dart`, `injection.config.dart`) — do not gitignore them.
|
||||
|
||||
**Template version:** 1.0.1
|
||||
**Template version:** 1.0.4
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# {{PROJECT_NAME}}
|
||||
|
||||
Repo-level notes for AI assistants. Authoritative stack and conventions are in `project-brief.yaml` and `.cursor/` (regenerate with `cursor_gen` from the project root).
|
||||
|
||||
- **Package:** `{{PACKAGE_ID}}`
|
||||
@@ -0,0 +1,5 @@
|
||||
# {{PROJECT_NAME}} — generated by cursor_gen; adjust commands to your repo
|
||||
pre-commit:
|
||||
commands:
|
||||
flutter-analyze:
|
||||
run: flutter analyze
|
||||
Reference in New Issue
Block a user