diff --git a/example-project/project-brief.yaml b/example-project/project-brief.yaml index d0c4801..ab0dc9c 100644 --- a/example-project/project-brief.yaml +++ b/example-project/project-brief.yaml @@ -13,7 +13,7 @@ # ----------------------------------------------------------------------------- # Template pin (Pillar 1) — bump when you intentionally upgrade template output # ----------------------------------------------------------------------------- -cursor_templates_version: "1.0.1" +cursor_templates_version: "1.0.4" # ----------------------------------------------------------------------------- # project — identity and rough size (affects rule tone / scaffolding hints) diff --git a/flutter-cursor-templates/CHANGELOG.md b/flutter-cursor-templates/CHANGELOG.md index 17dae7f..a35febc 100644 --- a/flutter-cursor-templates/CHANGELOG.md +++ b/flutter-cursor-templates/CHANGELOG.md @@ -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. diff --git a/flutter-cursor-templates/VERSION b/flutter-cursor-templates/VERSION index 7dea76e..ee90284 100644 --- a/flutter-cursor-templates/VERSION +++ b/flutter-cursor-templates/VERSION @@ -1 +1 @@ -1.0.1 +1.0.4 diff --git a/flutter-cursor-templates/generator/.dart_tool/package_config.json b/flutter-cursor-templates/generator/.dart_tool/package_config.json index 523db57..8a12454 100644 --- a/flutter-cursor-templates/generator/.dart_tool/package_config.json +++ b/flutter-cursor-templates/generator/.dart_tool/package_config.json @@ -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" } diff --git a/flutter-cursor-templates/generator/.dart_tool/package_graph.json b/flutter-cursor-templates/generator/.dart_tool/package_graph.json index 835dcd3..1e44e28 100644 --- a/flutter-cursor-templates/generator/.dart_tool/package_graph.json +++ b/flutter-cursor-templates/generator/.dart_tool/package_graph.json @@ -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 diff --git a/flutter-cursor-templates/generator/.dart_tool/pub/bin/cursor_gen/cursor_gen.dart-3.8.1.snapshot b/flutter-cursor-templates/generator/.dart_tool/pub/bin/cursor_gen/cursor_gen.dart-3.8.1.snapshot new file mode 100644 index 0000000..a73c58f Binary files /dev/null and b/flutter-cursor-templates/generator/.dart_tool/pub/bin/cursor_gen/cursor_gen.dart-3.8.1.snapshot differ diff --git a/flutter-cursor-templates/generator/.dart_tool/pub/bin/test/test.dart-3.11.3.snapshot b/flutter-cursor-templates/generator/.dart_tool/pub/bin/test/test.dart-3.11.3.snapshot new file mode 100644 index 0000000..84d751a Binary files /dev/null and b/flutter-cursor-templates/generator/.dart_tool/pub/bin/test/test.dart-3.11.3.snapshot differ diff --git a/flutter-cursor-templates/generator/.dart_tool/pub/bin/test/test.dart-3.8.1.snapshot b/flutter-cursor-templates/generator/.dart_tool/pub/bin/test/test.dart-3.8.1.snapshot index 3f6029d..f361c7e 100644 Binary files a/flutter-cursor-templates/generator/.dart_tool/pub/bin/test/test.dart-3.8.1.snapshot and b/flutter-cursor-templates/generator/.dart_tool/pub/bin/test/test.dart-3.8.1.snapshot differ diff --git a/flutter-cursor-templates/generator/.dart_tool/test/incremental_kernel.Ly9AZGFydD0zLjM= b/flutter-cursor-templates/generator/.dart_tool/test/incremental_kernel.Ly9AZGFydD0zLjM= index aaa9015..0421383 100644 Binary files a/flutter-cursor-templates/generator/.dart_tool/test/incremental_kernel.Ly9AZGFydD0zLjM= and b/flutter-cursor-templates/generator/.dart_tool/test/incremental_kernel.Ly9AZGFydD0zLjM= differ diff --git a/flutter-cursor-templates/generator/CHANGELOG.md b/flutter-cursor-templates/generator/CHANGELOG.md index e108ba1..ca98c86 100644 --- a/flutter-cursor-templates/generator/CHANGELOG.md +++ b/flutter-cursor-templates/generator/CHANGELOG.md @@ -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. diff --git a/flutter-cursor-templates/generator/bin/cursor_gen.dart b/flutter-cursor-templates/generator/bin/cursor_gen.dart index b62199c..4dd9a1d 100644 --- a/flutter-cursor-templates/generator/bin/cursor_gen.dart +++ b/flutter-cursor-templates/generator/bin/cursor_gen.dart @@ -130,8 +130,7 @@ Future main(List 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 main(List 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 _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 _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 _writeMetadataJson(String outputDir, ProjectBrief brief) async { await file.writeAsString(encoder.convert(brief.toMetadataMap())); } -Future _defaultTemplateDir() async { +const _templateSentinel = 'rules/universal/flutter-core.mdc.tmpl'; + +Future _resolveTemplateDir(String override) async { + if (override.isNotEmpty) { + final dir = p.normalize(override); + _assertTemplateBundle(dir); + return dir; + } + final tried = []; + 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> _templateDirCandidates() async { final binDir = p.dirname(Platform.script.toFilePath()); - final candidates = [ - // 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 = []; 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: /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() { diff --git a/flutter-cursor-templates/generator/brief-schema.json b/flutter-cursor-templates/generator/brief-schema.json index 48b1402..0a04c26 100644 --- a/flutter-cursor-templates/generator/brief-schema.json +++ b/flutter-cursor-templates/generator/brief-schema.json @@ -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", diff --git a/flutter-cursor-templates/generator/pubspec.yaml b/flutter-cursor-templates/generator/pubspec.yaml index 5c55a36..3dafc59 100644 --- a/flutter-cursor-templates/generator/pubspec.yaml +++ b/flutter-cursor-templates/generator/pubspec.yaml @@ -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: diff --git a/flutter-cursor-templates/generator/src/renderer.dart b/flutter-cursor-templates/generator/src/renderer.dart index dd675d8..179208a 100644 --- a/flutter-cursor-templates/generator/src/renderer.dart +++ b/flutter-cursor-templates/generator/src/renderer.dart @@ -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'; diff --git a/flutter-cursor-templates/generator/src/resolver.dart b/flutter-cursor-templates/generator/src/resolver.dart index f38a14d..5684974 100644 --- a/flutter-cursor-templates/generator/src/resolver.dart +++ b/flutter-cursor-templates/generator/src/resolver.dart @@ -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'; } } diff --git a/flutter-cursor-templates/generator/src/version_manager.dart b/flutter-cursor-templates/generator/src/version_manager.dart index 13b68cc..8b75338 100644 --- a/flutter-cursor-templates/generator/src/version_manager.dart +++ b/flutter-cursor-templates/generator/src/version_manager.dart @@ -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 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( diff --git a/flutter-cursor-templates/generator/src/wizard.dart b/flutter-cursor-templates/generator/src/wizard.dart index cf2ecc8..e8c62cd 100644 --- a/flutter-cursor-templates/generator/src/wizard.dart +++ b/flutter-cursor-templates/generator/src/wizard.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'logger.dart'; +import 'version_manager.dart'; class Wizard { static Future 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'] = ['en']; + } // Telemetry opt-in (Pillar 6) final telemetry = _askBool( @@ -183,13 +250,18 @@ class Wizard { final refLocals = List.from(a['local_paths'] as List); final themes = List.from(a['theme_variants'] as List); final roles = List.from(a['role_names'] as List); + final featureMods = List.from(a['feature_modules'] as List); + final specialFeats = List.from(a['special_features'] as List); + final locales = List.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']} diff --git a/flutter-cursor-templates/generator/templates/root/AGENTS.md.tmpl b/flutter-cursor-templates/generator/templates/root/AGENTS.md.tmpl new file mode 100644 index 0000000..fde21a4 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/root/AGENTS.md.tmpl @@ -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}}` diff --git a/flutter-cursor-templates/generator/templates/root/lefthook.yaml.tmpl b/flutter-cursor-templates/generator/templates/root/lefthook.yaml.tmpl new file mode 100644 index 0000000..2709629 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/root/lefthook.yaml.tmpl @@ -0,0 +1,5 @@ +# {{PROJECT_NAME}} — generated by cursor_gen; adjust commands to your repo +pre-commit: + commands: + flutter-analyze: + run: flutter analyze diff --git a/flutter-cursor-templates/generator/test/generator_test.dart b/flutter-cursor-templates/generator/test/generator_test.dart index b8a6efe..c08d3c0 100644 --- a/flutter-cursor-templates/generator/test/generator_test.dart +++ b/flutter-cursor-templates/generator/test/generator_test.dart @@ -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); }); }); } diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/__root__/AGENTS.md b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/__root__/AGENTS.md new file mode 100644 index 0000000..5894c87 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/__root__/AGENTS.md @@ -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` diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/__root__/lefthook.yaml b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/__root__/lefthook.yaml new file mode 100644 index 0000000..d781613 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/__root__/lefthook.yaml @@ -0,0 +1,5 @@ +# TestApp — generated by cursor_gen; adjust commands to your repo +pre-commit: + commands: + flutter-analyze: + run: flutter analyze diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/skills/build/SKILL.md b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/skills/build/SKILL.md index 5c1a902..b02e7db 100644 --- a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/skills/build/SKILL.md +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/skills/build/SKILL.md @@ -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 diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/__root__/AGENTS.md b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/__root__/AGENTS.md new file mode 100644 index 0000000..4eeae33 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/__root__/AGENTS.md @@ -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` diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/__root__/lefthook.yaml b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/__root__/lefthook.yaml new file mode 100644 index 0000000..892cb74 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/__root__/lefthook.yaml @@ -0,0 +1,5 @@ +# LegacyApp — generated by cursor_gen; adjust commands to your repo +pre-commit: + commands: + flutter-analyze: + run: flutter analyze diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/skills/build/SKILL.md b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/skills/build/SKILL.md new file mode 100644 index 0000000..eab697f --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/skills/build/SKILL.md @@ -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 +``` + +**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 `, 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(); 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 + +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 + +Run on Android: + flutter test integration_test/[feature]/ -d + +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: `` +- [ ] 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 diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/__root__/AGENTS.md b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/__root__/AGENTS.md new file mode 100644 index 0000000..ac7ca1a --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/__root__/AGENTS.md @@ -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` diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/__root__/lefthook.yaml b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/__root__/lefthook.yaml new file mode 100644 index 0000000..73d2045 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/__root__/lefthook.yaml @@ -0,0 +1,5 @@ +# TaskFlow — generated by cursor_gen; adjust commands to your repo +pre-commit: + commands: + flutter-analyze: + run: flutter analyze diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/skills/build/SKILL.md b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/skills/build/SKILL.md index 6ddcd8b..3f030e2 100644 --- a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/skills/build/SKILL.md +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/skills/build/SKILL.md @@ -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 diff --git a/flutter-cursor-templates/templates/root/AGENTS.md.tmpl b/flutter-cursor-templates/templates/root/AGENTS.md.tmpl new file mode 100644 index 0000000..fde21a4 --- /dev/null +++ b/flutter-cursor-templates/templates/root/AGENTS.md.tmpl @@ -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}}` diff --git a/flutter-cursor-templates/templates/root/lefthook.yaml.tmpl b/flutter-cursor-templates/templates/root/lefthook.yaml.tmpl new file mode 100644 index 0000000..2709629 --- /dev/null +++ b/flutter-cursor-templates/templates/root/lefthook.yaml.tmpl @@ -0,0 +1,5 @@ +# {{PROJECT_NAME}} — generated by cursor_gen; adjust commands to your repo +pre-commit: + commands: + flutter-analyze: + run: flutter analyze