diff --git a/README.md b/README.md index 3f80840..947630c 100644 --- a/README.md +++ b/README.md @@ -54,21 +54,37 @@ flutter-cursor-gen/ ## Quick Start -### 1. Add to your Flutter project +### 1. Install the CLI globally -```yaml -# pubspec.yaml (dev dependency) -dev_dependencies: - cursor_gen: - git: - url: https://github.com/company/flutter-cursor-templates - path: generator +From pub.dev: + +```bash +dart pub global activate cursor_gen +``` + +From this repository: + +```bash +cd flutter-cursor-templates/generator +dart pub global activate --source path . +``` + +From Git: + +```bash +dart pub global activate --source git https://github.com/company/flutter-cursor-templates --git-path generator +``` + +If `cursor_gen` is not found after activation, add Dart's global bin directory to your shell: + +```bash +export PATH="$PATH":"$HOME/.pub-cache/bin" ``` ### 2. Create your brief (interactive wizard) ```bash -dart run cursor_gen --wizard +cursor_gen --wizard ``` Or copy the single reference brief (every option is explained in comments): @@ -80,7 +96,8 @@ cp path/to/flutter-cursor-gen/example-project/project-brief.yaml . ### 3. Generate your .cursor/ directory ```bash -dart run cursor_gen +cursor_gen --validate +cursor_gen ``` ### 4. Commit and share with your team @@ -95,13 +112,13 @@ git commit -m "chore: add cursor AI config for this project" ## All CLI commands ```bash -dart run cursor_gen # First-time setup -dart run cursor_gen --wizard # Interactive brief creator -dart run cursor_gen --validate # Validate brief without generating -dart run cursor_gen --refresh # Re-generate (preserves custom/ + CURSOR:CUSTOM blocks) -dart run cursor_gen --diff # Preview changes before refresh -dart run cursor_gen --check-updates # Check for template version updates -dart run cursor_gen --telemetry # Show usage analytics report +cursor_gen # First-time setup +cursor_gen --wizard # Interactive brief creator +cursor_gen --validate # Validate brief without generating +cursor_gen --refresh # Re-generate (preserves custom/ + CURSOR:CUSTOM blocks) +cursor_gen --diff # Preview changes before refresh +cursor_gen --check-updates # Check for template version updates +cursor_gen --telemetry # Show usage analytics report ``` --- diff --git a/example-project/.cursor/custom/README.md b/example-project/.cursor/custom/README.md index 7649fd0..c6f0872 100644 --- a/example-project/.cursor/custom/README.md +++ b/example-project/.cursor/custom/README.md @@ -1,6 +1,6 @@ # .cursor/custom/ — project-specific overrides -This directory is never modified by `dart run cursor_gen` or `--refresh`. +This directory is never modified by `cursor_gen` or `--refresh`. Use it for team-only rules, vendor SDK notes, or policies that should not live in the shared template repo. diff --git a/example-project/project-brief.yaml b/example-project/project-brief.yaml index a874d71..d0c4801 100644 --- a/example-project/project-brief.yaml +++ b/example-project/project-brief.yaml @@ -1,6 +1,6 @@ # ============================================================================= # project-brief.yaml — SINGLE REFERENCE EXAMPLE for cursor_gen -# Copy to your Flutter repo root, edit values, then: dart run cursor_gen +# Copy to your Flutter repo root, edit values, then: cursor_gen # Schema / IDE hints: flutter-cursor-templates/generator/brief-schema.json # ============================================================================= # @@ -13,7 +13,7 @@ # ----------------------------------------------------------------------------- # Template pin (Pillar 1) — bump when you intentionally upgrade template output # ----------------------------------------------------------------------------- -cursor_templates_version: "1.0.0" +cursor_templates_version: "1.0.1" # ----------------------------------------------------------------------------- # project — identity and rough size (affects rule tone / scaffolding hints) @@ -92,6 +92,16 @@ references: - "https://github.com/example/acme-design-tokens" local_paths: [] +# ----------------------------------------------------------------------------- +# app_context — themes & RBAC (mirrored to .cursor/cursor-gen-metadata.json) +# ----------------------------------------------------------------------------- +# theme_variants: subset of [ light, dark, high_contrast ]; omit section or use [] → loader defaults to [light, dark] +# roles_enabled / role_names: use named roles when the app has RBAC +# app_context: +# theme_variants: ["light", "dark", "high_contrast"] +# roles_enabled: true +# role_names: ["customer", "merchant", "admin"] + features: # modules: high-level feature areas in YOUR lib/ (names are project-specific) modules: ["auth", "home", "catalog", "cart", "checkout", "profile", "orders"] diff --git a/flutter-cursor-templates/CHANGELOG.md b/flutter-cursor-templates/CHANGELOG.md index 0b8f399..17dae7f 100644 --- a/flutter-cursor-templates/CHANGELOG.md +++ b/flutter-cursor-templates/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [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. + +### Changed +- Document global `cursor_gen` installation and usage across the README and generated guidance. +- Include a package-local template copy for pub.dev publishing. + ## [1.0.0] - 2025-01-01 ### Added - Initial release of flutter-cursor-templates diff --git a/flutter-cursor-templates/VERSION b/flutter-cursor-templates/VERSION index 3eefcb9..7dea76e 100644 --- a/flutter-cursor-templates/VERSION +++ b/flutter-cursor-templates/VERSION @@ -1 +1 @@ -1.0.0 +1.0.1 diff --git a/flutter-cursor-templates/generator/.dart_tool/package_config.json b/flutter-cursor-templates/generator/.dart_tool/package_config.json new file mode 100644 index 0000000..523db57 --- /dev/null +++ b/flutter-cursor-templates/generator/.dart_tool/package_config.json @@ -0,0 +1,316 @@ +{ + "configVersion": 2, + "packages": [ + { + "name": "_fe_analyzer_shared", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/_fe_analyzer_shared-88.0.0", + "packageUri": "lib/", + "languageVersion": "3.7" + }, + { + "name": "analyzer", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/analyzer-8.1.1", + "packageUri": "lib/", + "languageVersion": "3.7" + }, + { + "name": "ansi_styles", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/ansi_styles-0.3.2+1", + "packageUri": "lib/", + "languageVersion": "2.12" + }, + { + "name": "args", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/args-2.7.0", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "async", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/async-2.13.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "boolean_selector", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "cli_config", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/cli_config-0.2.0", + "packageUri": "lib/", + "languageVersion": "3.0" + }, + { + "name": "collection", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/collection-1.19.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "convert", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/convert-3.1.2", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "coverage", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/coverage-1.15.0", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "crypto", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/crypto-3.0.7", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "file", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/file-7.0.1", + "packageUri": "lib/", + "languageVersion": "3.0" + }, + { + "name": "frontend_server_client", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/frontend_server_client-4.0.0", + "packageUri": "lib/", + "languageVersion": "3.0" + }, + { + "name": "glob", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/glob-2.1.3", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "http", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/http-1.6.0", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "http_multi_server", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/http_multi_server-3.2.2", + "packageUri": "lib/", + "languageVersion": "3.2" + }, + { + "name": "http_parser", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/http_parser-4.1.2", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "io", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/io-1.0.5", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "lints", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/lints-3.0.0", + "packageUri": "lib/", + "languageVersion": "3.0" + }, + { + "name": "logging", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/logging-1.3.0", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "matcher", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/matcher-0.12.19", + "packageUri": "lib/", + "languageVersion": "3.7" + }, + { + "name": "meta", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/meta-1.18.2", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "mime", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/mime-2.0.0", + "packageUri": "lib/", + "languageVersion": "3.2" + }, + { + "name": "node_preamble", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/node_preamble-2.0.2", + "packageUri": "lib/", + "languageVersion": "2.12" + }, + { + "name": "package_config", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/package_config-2.2.0", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "path", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/path-1.9.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "pool", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/pool-1.5.2", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "pub_semver", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/pub_semver-2.2.0", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "shelf", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/shelf-1.4.2", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "shelf_packages_handler", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/shelf_packages_handler-3.0.2", + "packageUri": "lib/", + "languageVersion": "2.17" + }, + { + "name": "shelf_static", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/shelf_static-1.1.3", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "shelf_web_socket", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/shelf_web_socket-3.0.0", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "source_map_stack_trace", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/source_map_stack_trace-2.1.2", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "source_maps", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/source_maps-0.10.13", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "source_span", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/source_span-1.10.2", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "stack_trace", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/stack_trace-1.12.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "stream_channel", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/stream_channel-2.1.4", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "string_scanner", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/string_scanner-1.4.1", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "term_glyph", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/term_glyph-1.2.2", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "test", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/test-1.31.0", + "packageUri": "lib/", + "languageVersion": "3.7" + }, + { + "name": "test_api", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/test_api-0.7.11", + "packageUri": "lib/", + "languageVersion": "3.7" + }, + { + "name": "test_core", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/test_core-0.6.17", + "packageUri": "lib/", + "languageVersion": "3.7" + }, + { + "name": "typed_data", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/typed_data-1.4.0", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "vm_service", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/vm_service-15.2.0", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "watcher", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/watcher-1.2.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "web", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/web-1.1.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "web_socket", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/web_socket-1.0.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "web_socket_channel", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/web_socket_channel-3.0.3", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "webkit_inspection_protocol", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/webkit_inspection_protocol-1.2.1", + "packageUri": "lib/", + "languageVersion": "3.0" + }, + { + "name": "yaml", + "rootUri": "file:///Users/maheshlalwani.devgmail.com/.pub-cache/hosted/pub.dev/yaml-3.1.3", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "cursor_gen", + "rootUri": "../", + "packageUri": "lib/", + "languageVersion": "3.3" + } + ], + "generator": "pub", + "generatorVersion": "3.8.1", + "flutterRoot": "file:///Users/maheshlalwani.devgmail.com/Documents/flutter", + "flutterVersion": "3.32.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 new file mode 100644 index 0000000..835dcd3 --- /dev/null +++ b/flutter-cursor-templates/generator/.dart_tool/package_graph.json @@ -0,0 +1,470 @@ +{ + "roots": [ + "cursor_gen" + ], + "packages": [ + { + "name": "cursor_gen", + "version": "1.0.1", + "dependencies": [ + "ansi_styles", + "args", + "collection", + "crypto", + "http", + "path", + "yaml" + ], + "devDependencies": [ + "lints", + "test" + ] + }, + { + "name": "lints", + "version": "3.0.0", + "dependencies": [] + }, + { + "name": "ansi_styles", + "version": "0.3.2+1", + "dependencies": [] + }, + { + "name": "collection", + "version": "1.19.1", + "dependencies": [] + }, + { + "name": "crypto", + "version": "3.0.7", + "dependencies": [ + "typed_data" + ] + }, + { + "name": "http", + "version": "1.6.0", + "dependencies": [ + "async", + "http_parser", + "meta", + "web" + ] + }, + { + "name": "path", + "version": "1.9.1", + "dependencies": [] + }, + { + "name": "yaml", + "version": "3.1.3", + "dependencies": [ + "collection", + "source_span", + "string_scanner" + ] + }, + { + "name": "args", + "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", + "dependencies": [ + "logging" + ] + }, + { + "name": "web_socket_channel", + "version": "3.0.3", + "dependencies": [ + "async", + "crypto", + "stream_channel", + "web", + "web_socket" + ] + }, + { + "name": "test_core", + "version": "0.6.17", + "dependencies": [ + "analyzer", + "args", + "async", + "boolean_selector", + "collection", + "coverage", + "frontend_server_client", + "glob", + "io", + "meta", + "package_config", + "path", + "pool", + "source_map_stack_trace", + "source_maps", + "source_span", + "stack_trace", + "stream_channel", + "test_api", + "vm_service", + "yaml" + ] + }, + { + "name": "test_api", + "version": "0.7.11", + "dependencies": [ + "async", + "boolean_selector", + "collection", + "meta", + "source_span", + "stack_trace", + "stream_channel", + "string_scanner", + "term_glyph" + ] + }, + { + "name": "stream_channel", + "version": "2.1.4", + "dependencies": [ + "async" + ] + }, + { + "name": "stack_trace", + "version": "1.12.1", + "dependencies": [ + "path" + ] + }, + { + "name": "shelf_web_socket", + "version": "3.0.0", + "dependencies": [ + "shelf", + "stream_channel", + "web_socket_channel" + ] + }, + { + "name": "shelf_static", + "version": "1.1.3", + "dependencies": [ + "convert", + "http_parser", + "mime", + "path", + "shelf" + ] + }, + { + "name": "shelf_packages_handler", + "version": "3.0.2", + "dependencies": [ + "path", + "shelf", + "shelf_static" + ] + }, + { + "name": "shelf", + "version": "1.4.2", + "dependencies": [ + "async", + "collection", + "http_parser", + "path", + "stack_trace", + "stream_channel" + ] + }, + { + "name": "pool", + "version": "1.5.2", + "dependencies": [ + "async", + "stack_trace" + ] + }, + { + "name": "package_config", + "version": "2.2.0", + "dependencies": [ + "path" + ] + }, + { + "name": "node_preamble", + "version": "2.0.2", + "dependencies": [] + }, + { + "name": "io", + "version": "1.0.5", + "dependencies": [ + "meta", + "path", + "string_scanner" + ] + }, + { + "name": "http_multi_server", + "version": "3.2.2", + "dependencies": [ + "async" + ] + }, + { + "name": "coverage", + "version": "1.15.0", + "dependencies": [ + "args", + "cli_config", + "glob", + "logging", + "meta", + "package_config", + "path", + "source_maps", + "stack_trace", + "vm_service", + "yaml" + ] + }, + { + "name": "boolean_selector", + "version": "2.1.2", + "dependencies": [ + "source_span", + "string_scanner" + ] + }, + { + "name": "logging", + "version": "1.3.0", + "dependencies": [] + }, + { + "name": "web_socket", + "version": "1.0.1", + "dependencies": [ + "web" + ] + }, + { + "name": "vm_service", + "version": "15.2.0", + "dependencies": [] + }, + { + "name": "source_maps", + "version": "0.10.13", + "dependencies": [ + "source_span" + ] + }, + { + "name": "source_map_stack_trace", + "version": "2.1.2", + "dependencies": [ + "path", + "source_maps", + "stack_trace" + ] + }, + { + "name": "glob", + "version": "2.1.3", + "dependencies": [ + "async", + "collection", + "file", + "path", + "string_scanner" + ] + }, + { + "name": "frontend_server_client", + "version": "4.0.0", + "dependencies": [ + "async", + "path" + ] + }, + { + "name": "mime", + "version": "2.0.0", + "dependencies": [] + }, + { + "name": "convert", + "version": "3.1.2", + "dependencies": [ + "typed_data" + ] + }, + { + "name": "cli_config", + "version": "0.2.0", + "dependencies": [ + "args", + "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", + "dependencies": [ + "async", + "path" + ] + }, + { + "name": "pub_semver", + "version": "2.2.0", + "dependencies": [ + "collection" + ] + }, + { + "name": "_fe_analyzer_shared", + "version": "88.0.0", + "dependencies": [ + "meta" + ] + } + ], + "configVersion": 1 +} \ No newline at end of file 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 new file mode 100644 index 0000000..3f6029d Binary files /dev/null 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= new file mode 100644 index 0000000..aaa9015 Binary files /dev/null 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 be24b8a..e108ba1 100644 --- a/flutter-cursor-templates/generator/CHANGELOG.md +++ b/flutter-cursor-templates/generator/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 1.0.1 + +- Fix default template resolution so generated projects use bundled templates instead of writing `Template not found` placeholders. +- Include templates in the published package so hosted/global installs can generate real content. +- Add a regression test for the generated `hooks/arch-guard.ts` content. +- Improve installation docs for global `cursor_gen` usage. + ## 1.0.0 - Initial published release of the `cursor_gen` CLI. diff --git a/flutter-cursor-templates/generator/README.md b/flutter-cursor-templates/generator/README.md index fdf2fd3..df629d3 100644 --- a/flutter-cursor-templates/generator/README.md +++ b/flutter-cursor-templates/generator/README.md @@ -8,17 +8,62 @@ CLI that generates project-specific [Cursor](https://cursor.com) AI configuratio ## Install -**From path (local checkout):** +Install the CLI globally so you can run `cursor_gen` from any Flutter project. + +**From pub.dev:** ```bash +dart pub global activate cursor_gen +``` + +**From Git:** + +```bash +dart pub global activate --source git https://github.com/company/flutter-cursor-templates --git-path generator +``` + +**From a local checkout:** + +```bash +cd flutter-cursor-templates/generator dart pub global activate --source path . ``` -Run from the `flutter-cursor-templates/generator` directory (where this `README` lives). +If your shell cannot find `cursor_gen`, add Dart's global bin directory to your `PATH`: -**From Git:** use `dart pub global activate --source git ` and set the `path` option if your `pubspec.yaml` is not at the repository root (see [pub dependencies](https://dart.dev/tools/pub/dependencies#git-packages)). +```bash +export PATH="$PATH":"$HOME/.pub-cache/bin" +``` -**From a private Pub registry:** configure `dart pub token add` and install with `dart pub add cursor_gen --hosted-url=`. +For zsh, add that line to `~/.zshrc` and restart your terminal. + +**From a private Pub registry:** configure access first, then activate from that registry: + +```bash +dart pub token add https://your-registry.example.com +dart pub global activate cursor_gen --hosted-url=https://your-registry.example.com +``` + +## Setup In Your Flutter Project + +Create a `project-brief.yaml`: + +```bash +cursor_gen --wizard +``` + +Or copy the example brief and edit it: + +```bash +cp path/to/flutter-cursor-templates/example-project/project-brief.yaml . +``` + +Validate the brief, then generate `.cursor/`: + +```bash +cursor_gen --validate +cursor_gen +``` ## Usage diff --git a/flutter-cursor-templates/generator/bin/cursor_gen.dart b/flutter-cursor-templates/generator/bin/cursor_gen.dart index 18f770d..b62199c 100644 --- a/flutter-cursor-templates/generator/bin/cursor_gen.dart +++ b/flutter-cursor-templates/generator/bin/cursor_gen.dart @@ -1,9 +1,12 @@ #!/usr/bin/env dart // cursor_gen — Flutter Cursor AI config generator -// Usage: dart run cursor_gen [options] +// Usage: cursor_gen [options] +import 'dart:convert'; import 'dart:io'; +import 'dart:isolate'; import 'package:args/args.dart'; +import 'package:path/path.dart' as p; import '../src/models.dart'; import '../src/brief_loader.dart'; import '../src/resolver.dart'; @@ -17,16 +20,34 @@ import '../src/logger.dart'; Future main(List arguments) async { final parser = ArgParser() - ..addFlag('help', abbr: 'h', negatable: false, help: 'Show usage') - ..addFlag('validate', abbr: 'v', negatable: false, help: 'Validate project-brief.yaml without writing files') - ..addFlag('refresh', abbr: 'r', negatable: false, help: 'Re-generate .cursor/ preserving custom/ and CURSOR:CUSTOM markers') - ..addFlag('check-updates', negatable: false, help: 'Check if newer template version is available') - ..addFlag('diff', negatable: false, help: 'Preview what would change before refreshing') - ..addFlag('wizard', abbr: 'w', negatable: false, help: 'Interactive wizard to create project-brief.yaml') - ..addFlag('telemetry', negatable: false, help: 'Show telemetry / rule-trigger report') - ..addOption('brief', abbr: 'b', defaultsTo: 'project-brief.yaml', help: 'Path to project-brief.yaml') - ..addOption('output', abbr: 'o', defaultsTo: '.cursor', help: 'Output directory') - ..addOption('templates', defaultsTo: '', help: 'Override template library path'); + ..addFlag('help', abbr: 'h', negatable: false, help: 'Show usage') + ..addFlag('validate', + abbr: 'v', + negatable: false, + help: 'Validate project-brief.yaml without writing files') + ..addFlag('refresh', + abbr: 'r', + negatable: false, + help: + 'Re-generate .cursor/ preserving custom/ and CURSOR:CUSTOM markers') + ..addFlag('check-updates', + negatable: false, help: 'Check if newer template version is available') + ..addFlag('diff', + negatable: false, help: 'Preview what would change before refreshing') + ..addFlag('wizard', + abbr: 'w', + negatable: false, + help: 'Interactive wizard to create project-brief.yaml') + ..addFlag('telemetry', + negatable: false, help: 'Show telemetry / rule-trigger report') + ..addOption('brief', + abbr: 'b', + defaultsTo: 'project-brief.yaml', + help: 'Path to project-brief.yaml') + ..addOption('output', + abbr: 'o', defaultsTo: '.cursor', help: 'Output directory') + ..addOption('templates', + defaultsTo: '', help: 'Override template library path'); final ArgResults args; try { @@ -48,8 +69,8 @@ Future main(List arguments) async { exit(0); } - final briefPath = args['brief'] as String; - final outputDir = args['output'] as String; + final briefPath = args['brief'] as String; + final outputDir = args['output'] as String; final templateSrc = args['templates'] as String; if (args['validate'] as bool) { @@ -85,8 +106,9 @@ Future main(List arguments) async { final versionStatus = await VersionManager.check(brief: brief); if (versionStatus.hasUpdate) { - Logger.warn('\n⚠ Template update available: ${versionStatus.currentVersion} → ${versionStatus.latestVersion}'); - Logger.warn(' Run: dart run cursor_gen --check-updates for full diff\n'); + Logger.warn( + '\n⚠ Template update available: ${versionStatus.currentVersion} → ${versionStatus.latestVersion}'); + Logger.warn(' Run: cursor_gen --check-updates for full diff\n'); } if (args['diff'] as bool) { @@ -96,9 +118,11 @@ Future main(List arguments) async { _printBanner(); Logger.info('Generating .cursor/ for ${brief.projectName}'); - Logger.dim(' Stack: ${brief.stateManagement} + ${brief.architecture} + ${brief.backends.join("+")}'); + Logger.dim( + ' Stack: ${brief.stateManagement} + ${brief.architecture} + ${brief.backends.join("+")}'); Logger.dim(' Platform: ${brief.platforms.join(", ")}'); - Logger.dim(' Codegen: ${brief.codegenTools.isEmpty ? "none" : brief.codegenTools.join(", ")}\n'); + Logger.dim( + ' Codegen: ${brief.codegenTools.isEmpty ? "none" : brief.codegenTools.join(", ")}\n'); final isRefresh = args['refresh'] as bool; final overrideSnapshot = await OverrideManager.snapshot(outputDir); @@ -106,27 +130,40 @@ Future main(List arguments) async { final templateFiles = Resolver.resolve(brief); Logger.info('Resolved ${templateFiles.length} template files'); - final templateDir = templateSrc.isEmpty ? _defaultTemplateDir() : templateSrc; + final templateDir = + templateSrc.isEmpty ? await _defaultTemplateDir() : templateSrc; final rendered = await Renderer.render( - brief: brief, templateFiles: templateFiles, templateSrc: templateDir, + brief: brief, + templateFiles: templateFiles, + templateSrc: templateDir, ); await _writeOutput(outputDir, rendered, overrideSnapshot, isRefresh); - await VersionManager.writeLock(outputDir: outputDir, briefPath: briefPath, brief: brief); - await Telemetry.record(projectName: brief.projectName, outputDir: outputDir, templateFiles: templateFiles); + await _writeMetadataJson(outputDir, brief); + await VersionManager.writeLock( + outputDir: outputDir, briefPath: briefPath, brief: brief); + await Telemetry.record( + projectName: brief.projectName, + outputDir: outputDir, + templateFiles: templateFiles); Logger.success('\n✔ Done! ${rendered.length} files written to $outputDir/'); if (overrideSnapshot.customFiles.isNotEmpty) { - Logger.success(' ↳ ${overrideSnapshot.customFiles.length} custom override(s) preserved untouched'); + Logger.success( + ' ↳ ${overrideSnapshot.customFiles.length} custom override(s) preserved untouched'); } - Logger.dim('\n Next: commit .cursor/ to git so all teammates get the same config.'); + Logger.dim( + '\n Next: commit .cursor/ to git so all teammates get the same config.'); } -Future _runDiff(dynamic brief, String outputDir, String templateSrc) async { +Future _runDiff( + dynamic brief, String outputDir, String templateSrc) async { final templateFiles = Resolver.resolve(brief); final rendered = await Renderer.render( - brief: brief, templateFiles: templateFiles, - templateSrc: templateSrc.isEmpty ? _defaultTemplateDir() : templateSrc, + brief: brief, + templateFiles: templateFiles, + templateSrc: + templateSrc.isEmpty ? await _defaultTemplateDir() : templateSrc, ); Logger.info('Diff preview (no files written):\n'); for (final entry in rendered.entries) { @@ -135,22 +172,27 @@ Future _runDiff(dynamic brief, String outputDir, String templateSrc) async Logger.success(' + ${entry.key}'); } else { final current = await existing.readAsString(); - if (current != entry.value) Logger.warn(' ~ ${entry.key}'); - else Logger.dim(' = ${entry.key}'); + if (current != entry.value) + Logger.warn(' ~ ${entry.key}'); + else + Logger.dim(' = ${entry.key}'); } } } Future _writeOutput( - String outputDir, Map rendered, - OverrideSnapshot snapshot, bool isRefresh, + String outputDir, + Map rendered, + OverrideSnapshot snapshot, + bool isRefresh, ) async { for (final entry in rendered.entries) { final file = File('$outputDir/${entry.key}'); await file.parent.create(recursive: true); if (isRefresh && file.existsSync()) { final merged = OverrideManager.mergeCustomSections( - newContent: entry.value, existingContent: await file.readAsString(), + newContent: entry.value, + existingContent: await file.readAsString(), ); await file.writeAsString(merged); } else { @@ -160,9 +202,35 @@ Future _writeOutput( await OverrideManager.restoreCustomFolder(snapshot, outputDir); } -String _defaultTemplateDir() { - final scriptDir = Platform.script.toFilePath(); - return scriptDir.replaceAll(RegExp(r'bin[/\\]cursor_gen\.dart$'), 'templates'); +Future _writeMetadataJson(String outputDir, ProjectBrief brief) async { + final path = p.join(outputDir, 'cursor-gen-metadata.json'); + final file = File(path); + await file.parent.create(recursive: true); + final encoder = JsonEncoder.withIndent(' '); + await file.writeAsString(encoder.convert(brief.toMetadataMap())); +} + +Future _defaultTemplateDir() 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 packageUri = await Isolate.resolvePackageUri( + Uri.parse('package:cursor_gen/src/renderer.dart'), + ); + if (packageUri != null && packageUri.isScheme('file')) { + final packageRoot = p.dirname(p.dirname(packageUri.toFilePath())); + candidates.add(p.normalize(p.join(packageRoot, 'templates'))); + } + + return candidates.firstWhere( + (path) => Directory(path).existsSync(), + orElse: () => candidates.last, + ); } void _printBanner() { diff --git a/flutter-cursor-templates/generator/brief-schema.json b/flutter-cursor-templates/generator/brief-schema.json index 36633e6..48b1402 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.0"] + "examples": ["1.0.1"] }, "project": { "type": "object", @@ -91,6 +91,73 @@ "locales": { "type": "array", "items": { "type": "string" } } } }, + "design": { + "type": "object", + "properties": { + "source": { + "type": "string", + "enum": ["figma_mcp", "figma_manual", "native_ref", "html_ref", "none"] + }, + "figma_url": { "type": "string" } + } + }, + "api_docs": { + "type": "object", + "properties": { + "format": { + "type": "string", + "enum": ["openapi", "postman", "markdown", "none"] + }, + "path": { "type": "string" } + } + }, + "references": { + "type": "object", + "description": "Other repos or local paths agents should treat as product context", + "properties": { + "repos": { + "type": "array", + "items": { "type": "string" }, + "description": "Git remote URLs (https or ssh)" + }, + "local_paths": { + "type": "array", + "items": { "type": "string" }, + "description": "Repo-relative paths (e.g. monorepo packages) or other local references" + } + } + }, + "features": { + "type": "object", + "properties": { + "modules": { "type": "array", "items": { "type": "string" } }, + "special": { "type": "array", "items": { "type": "string" } } + } + }, + "app_context": { + "type": "object", + "description": "Theme targets and optional RBAC labels — mirrored to cursor-gen-metadata.json on generate", + "properties": { + "theme_variants": { + "type": "array", + "items": { + "type": "string", + "enum": ["light", "dark", "high_contrast"] + }, + "description": "Supported theme tokens; omit or leave empty to default to light + dark in the loader" + }, + "roles_enabled": { + "type": "boolean", + "default": false, + "description": "When true, list concrete roles in role_names" + }, + "role_names": { + "type": "array", + "items": { "type": "string" }, + "description": "Product role identifiers when roles_enabled is true" + } + } + }, "telemetry_opt_in": { "type": "boolean", "description": "Pillar 6: Opt-in local telemetry for rule trigger analytics", diff --git a/flutter-cursor-templates/generator/pubspec.lock b/flutter-cursor-templates/generator/pubspec.lock index 23ba998..5cb2236 100644 --- a/flutter-cursor-templates/generator/pubspec.lock +++ b/flutter-cursor-templates/generator/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: cd6add6f846f35fb79f3c315296703c1a24f3cfd7f4739d91a74961c1c7e9f1b + sha256: f0bb5d1648339c8308cc0b9838d8456b3cfe5c91f9dc1a735b4d003269e5da9a url: "https://pub.dev" source: hosted - version: "100.0.0" + version: "88.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "6ba98576948803398b69e3a444df24eacdbe12ed699c7014e120ea38552debbf" + sha256: "0b7b9c329d2879f8f05d6c05b32ee9ec025f39b077864bdb5ac9a7b63418a98f" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "8.1.1" ansi_styles: dependency: "direct main" description: @@ -165,10 +165,10 @@ packages: dependency: transitive description: name: matcher - sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.20" + version: "0.12.19" meta: dependency: transitive description: @@ -317,26 +317,26 @@ packages: dependency: "direct dev" description: name: test - sha256: ca578dc12bb8b2f40b67b7d3bd2fac4f31c01a6ff7130a14e2597b919934507f + sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20" url: "https://pub.dev" source: hosted - version: "1.31.1" + version: "1.31.0" test_api: dependency: transitive description: name: test_api - sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.12" + version: "0.7.11" test_core: dependency: transitive description: name: test_core - sha256: d2e98ec12998368dc59ddd47ab709f2cd55acd6b66dc7db764455a44082f4bc5 + sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34" url: "https://pub.dev" source: hosted - version: "0.6.18" + version: "0.6.17" typed_data: dependency: transitive description: @@ -402,4 +402,4 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.11.0 <4.0.0" + dart: ">=3.7.0 <4.0.0" diff --git a/flutter-cursor-templates/generator/pubspec.yaml b/flutter-cursor-templates/generator/pubspec.yaml index e08ba7e..5c55a36 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.0 +version: 1.0.1 homepage: https://github.com/company/flutter-cursor-templates environment: diff --git a/flutter-cursor-templates/generator/src/brief_loader.dart b/flutter-cursor-templates/generator/src/brief_loader.dart index 80f461c..4e734b7 100644 --- a/flutter-cursor-templates/generator/src/brief_loader.dart +++ b/flutter-cursor-templates/generator/src/brief_loader.dart @@ -7,66 +7,74 @@ class BriefLoader { final file = File(path); if (!file.existsSync()) { throw FileSystemException('project-brief.yaml not found at $path. ' - 'Run: dart run cursor_gen --wizard to create one.'); + 'Run: cursor_gen --wizard to create one.'); } final content = await file.readAsString(); final yaml = loadYaml(content) as YamlMap; final project = yaml['project'] as YamlMap; - final stack = yaml['stack'] as YamlMap; - final envs = yaml['environments'] as YamlMap? ?? YamlMap(); + final stack = yaml['stack'] as YamlMap; + final envs = yaml['environments'] as YamlMap? ?? YamlMap(); final testing = yaml['testing'] as YamlMap? ?? YamlMap(); - final design = yaml['design'] as YamlMap? ?? YamlMap(); + final design = yaml['design'] as YamlMap? ?? YamlMap(); final apiDocs = yaml['api_docs'] as YamlMap? ?? YamlMap(); - final refs = yaml['references'] as YamlMap? ?? YamlMap(); + final refs = yaml['references'] as YamlMap? ?? YamlMap(); + final appCtx = yaml['app_context'] as YamlMap? ?? YamlMap(); final features = yaml['features'] as YamlMap? ?? YamlMap(); - final l10n = yaml['localization'] as YamlMap? ?? YamlMap(); + final l10n = yaml['localization'] as YamlMap? ?? YamlMap(); // Parse backends (can be "firebase+rest" shorthand or list) final backendRaw = stack['backend']?.toString() ?? 'rest'; - final backends = backendRaw.contains('+') - ? backendRaw.split('+') - : [backendRaw]; + final backends = + backendRaw.contains('+') ? backendRaw.split('+') : [backendRaw]; return ProjectBrief( - projectName: project['name']?.toString() ?? 'MyApp', - packageId: project['package']?.toString() ?? 'com.example.myapp', - description: project['description']?.toString() ?? '', - scale: project['scale']?.toString() ?? 'medium', + projectName: project['name']?.toString() ?? 'MyApp', + packageId: project['package']?.toString() ?? 'com.example.myapp', + description: project['description']?.toString() ?? '', + scale: project['scale']?.toString() ?? 'medium', stateManagement: stack['state_management']?.toString() ?? 'riverpod', - routing: stack['routing']?.toString() ?? 'gorouter', - architecture: stack['architecture']?.toString() ?? 'feature_first', - backends: backends.map((b) => b.trim()).toList(), - auth: stack['auth']?.toString() ?? 'none', + routing: stack['routing']?.toString() ?? 'gorouter', + architecture: stack['architecture']?.toString() ?? 'feature_first', + backends: backends.map((b) => b.trim()).toList(), + auth: stack['auth']?.toString() ?? 'none', // Pillar 4: platform + codegen platforms: _toStringList(stack['platforms']) ?? ['ios', 'android'], codegenTools: _toStringList(stack['codegen']) ?? [], flavors: _toStringList(envs['flavors']) ?? ['dev', 'prod'], - cicd: envs['cicd']?.toString() ?? 'github_actions', + cicd: envs['cicd']?.toString() ?? 'github_actions', testingDepth: testing['depth']?.toString() ?? 'unit_widget', - e2eTool: testing['e2e_tool']?.toString() ?? 'patrol', + e2eTool: testing['e2e_tool']?.toString() ?? 'patrol', designSource: design['source']?.toString() ?? 'none', - figmaUrl: design['figma_url']?.toString() ?? '', + figmaUrl: design['figma_url']?.toString() ?? '', apiDocsFormat: apiDocs['format']?.toString() ?? 'none', - apiDocsPath: apiDocs['path']?.toString() ?? '', + apiDocsPath: apiDocs['path']?.toString() ?? '', referenceRepos: _toStringList(refs['repos']) ?? [], - localPaths: _toStringList(refs['local_paths']) ?? [], + localPaths: _toStringList(refs['local_paths']) ?? [], - featureModules: _toStringList(features['modules']) ?? [], + featureModules: _toStringList(features['modules']) ?? [], specialFeatures: _toStringList(features['special']) ?? [], i18nEnabled: l10n['enabled'] as bool? ?? false, - locales: _toStringList(l10n['locales']) ?? ['en'], + locales: _toStringList(l10n['locales']) ?? ['en'], cursorTemplatesVersion: yaml['cursor_templates_version']?.toString(), telemetryOptIn: yaml['telemetry_opt_in'] as bool? ?? false, + + themeVariants: () { + final t = _toStringList(appCtx['theme_variants']); + if (t == null || t.isEmpty) return ['light', 'dark']; + return t; + }(), + rolesEnabled: appCtx['roles_enabled'] as bool? ?? false, + roleNames: _toStringList(appCtx['role_names']) ?? [], ); } diff --git a/flutter-cursor-templates/generator/src/models.dart b/flutter-cursor-templates/generator/src/models.dart index 0854fa4..d92773d 100644 --- a/flutter-cursor-templates/generator/src/models.dart +++ b/flutter-cursor-templates/generator/src/models.dart @@ -52,6 +52,13 @@ class ProjectBrief { // Telemetry opt-in (Pillar 6) final bool telemetryOptIn; + /// Theme variants to support (subset of light, dark, high_contrast). + final List themeVariants; + + /// App-level RBAC: when true, [roleNames] should list concrete roles. + final bool rolesEnabled; + final List roleNames; + const ProjectBrief({ required this.projectName, required this.packageId, @@ -80,7 +87,34 @@ class ProjectBrief { required this.locales, this.cursorTemplatesVersion, this.telemetryOptIn = false, + this.themeVariants = const ['light', 'dark'], + this.rolesEnabled = false, + this.roleNames = const [], }); + + /// Local snapshot for tooling (written as `cursor-gen-metadata.json` under the output dir). + Map toMetadataMap({DateTime? generatedAt}) { + final at = (generatedAt ?? DateTime.now()).toUtc(); + return { + 'schema_version': 1, + 'generated_at': at.toIso8601String(), + 'project': { + 'name': projectName, + 'package': packageId, + 'description': description, + 'scale': scale, + }, + 'references': { + 'repos': List.from(referenceRepos), + 'local_paths': List.from(localPaths), + }, + 'app_context': { + 'theme_variants': List.from(themeVariants), + 'roles_enabled': rolesEnabled, + 'role_names': List.from(roleNames), + }, + }; + } } class ValidationResult { diff --git a/flutter-cursor-templates/generator/src/renderer.dart b/flutter-cursor-templates/generator/src/renderer.dart index 0adaebf..dd675d8 100644 --- a/flutter-cursor-templates/generator/src/renderer.dart +++ b/flutter-cursor-templates/generator/src/renderer.dart @@ -11,7 +11,7 @@ class Renderer { required String templateSrc, }) async { final context = _buildContext(brief); - final output = {}; + final output = {}; for (final key in templateFiles) { final tmplPath = _templatePath(templateSrc, key); @@ -23,7 +23,8 @@ class Renderer { } var content = await file.readAsString(); content = _substituteAll(content, context); - _checkUnreplacedPlaceholders(content, key); // Pillar 3: validate no broken {{VAR}} + _checkUnreplacedPlaceholders( + content, key); // Pillar 3: validate no broken {{VAR}} output[_outputPath(key)] = content; } return output; @@ -32,38 +33,44 @@ class Renderer { /// Build the full substitution context from a brief static Map _buildContext(ProjectBrief brief) { return { - 'PROJECT_NAME': brief.projectName, - 'PACKAGE_ID': brief.packageId, - 'DESCRIPTION': brief.description, - 'SCALE': brief.scale, - 'STATE_MANAGEMENT': _displayName(brief.stateManagement), - 'STATE_MGMT_RAW': brief.stateManagement, - 'ARCHITECTURE': _displayName(brief.architecture), - 'ARCH_RAW': brief.architecture, - 'ROUTING': _displayName(brief.routing), - 'ROUTING_RAW': brief.routing, - 'BACKEND': brief.backends.map(_displayName).join(' + '), - 'BACKENDS_LIST': brief.backends.join(', '), - 'AUTH': _displayName(brief.auth), - 'AUTH_RAW': brief.auth, - 'PLATFORMS_LIST': brief.platforms.join(', '), - 'CODEGEN_LIST': brief.codegenTools.isEmpty ? 'none' : brief.codegenTools.join(', '), - 'FLAVORS_LIST': brief.flavors.join(', '), - 'CICD_TOOL': _displayName(brief.cicd), - 'CICD_RAW': brief.cicd, - 'TESTING_DEPTH': brief.testingDepth, - 'E2E_TOOL': brief.e2eTool, - 'FEATURES_LIST': brief.featureModules.join(', '), - 'SPECIAL_FEATURES': brief.specialFeatures.join(', '), - 'DESIGN_SOURCE': brief.designSource, - 'FIGMA_URL': brief.figmaUrl, - 'API_DOCS_FORMAT': brief.apiDocsFormat, - 'API_DOCS_PATH': brief.apiDocsPath, - 'REFERENCE_REPOS': brief.referenceRepos.join('\n- '), - 'ARCH_IMPORT_RULES': _archImportRules(brief.architecture), - 'TEST_PATTERN': _testPattern(brief.stateManagement), - 'LOCALES_LIST': brief.locales.join(', '), - 'TEMPLATE_VERSION': '1.0.0', + 'PROJECT_NAME': brief.projectName, + 'PACKAGE_ID': brief.packageId, + 'DESCRIPTION': brief.description, + 'SCALE': brief.scale, + 'STATE_MANAGEMENT': _displayName(brief.stateManagement), + 'STATE_MGMT_RAW': brief.stateManagement, + 'ARCHITECTURE': _displayName(brief.architecture), + 'ARCH_RAW': brief.architecture, + 'ROUTING': _displayName(brief.routing), + 'ROUTING_RAW': brief.routing, + 'BACKEND': brief.backends.map(_displayName).join(' + '), + 'BACKENDS_LIST': brief.backends.join(', '), + 'AUTH': _displayName(brief.auth), + 'AUTH_RAW': brief.auth, + 'PLATFORMS_LIST': brief.platforms.join(', '), + 'CODEGEN_LIST': + brief.codegenTools.isEmpty ? 'none' : brief.codegenTools.join(', '), + 'FLAVORS_LIST': brief.flavors.join(', '), + 'CICD_TOOL': _displayName(brief.cicd), + 'CICD_RAW': brief.cicd, + 'TESTING_DEPTH': brief.testingDepth, + 'E2E_TOOL': brief.e2eTool, + 'FEATURES_LIST': brief.featureModules.join(', '), + 'SPECIAL_FEATURES': brief.specialFeatures.join(', '), + 'DESIGN_SOURCE': brief.designSource, + 'FIGMA_URL': brief.figmaUrl, + 'API_DOCS_FORMAT': brief.apiDocsFormat, + 'API_DOCS_PATH': brief.apiDocsPath, + 'GIT_REFS_BLOCK': _gitRefsBlock(brief), + 'LOCAL_PATHS_BLOCK': _localPathsBlock(brief), + 'THEME_SUMMARY': _themeSummary(brief), + 'ROLES_SUMMARY': _rolesSummary(brief), + 'HIGH_CONTRAST_NOTE': _highContrastNote(brief), + 'HIGH_CONTRAST_UX_LINE': _highContrastUxLine(brief), + 'ARCH_IMPORT_RULES': _archImportRules(brief.architecture), + 'TEST_PATTERN': _testPattern(brief.stateManagement), + 'LOCALES_LIST': brief.locales.join(', '), + 'TEMPLATE_VERSION': '1.0.1', }; } @@ -80,21 +87,17 @@ class Renderer { if (matches.isNotEmpty) { final vars = matches.map((m) => m.group(0)).toSet(); // Print warning but don't fail — let golden tests catch it - stderr.writeln('⚠ Unreplaced placeholder(s) in $key: ${vars.join(", ")}'); + stderr + .writeln('⚠ Unreplaced placeholder(s) in $key: ${vars.join(", ")}'); } } static String _templatePath(String templateSrc, String key) { - // skills use SKILL.md.tmpl, everything else uses .mdc.tmpl - final ext = key.startsWith('skills/') ? 'SKILL.md.tmpl' : '.mdc.tmpl'; - final dir = key.startsWith('skills/') ? p.dirname(key) : ''; - if (key.startsWith('skills/')) { final skillName = p.basename(key); return p.join(templateSrc, 'skills', skillName, 'SKILL.md.tmpl'); } if (key.startsWith('hooks/')) { - final hookFile = p.basename(key).replaceAll('-', '.'); // special: hooks-json → hooks.json.tmpl if (key.endsWith('hooks-json')) { return p.join(templateSrc, 'hooks', 'hooks.json.tmpl'); @@ -122,33 +125,83 @@ class Renderer { static String _displayName(String raw) { const names = { - 'bloc': 'BLoC / Cubit', - 'riverpod': 'Riverpod', - 'getx': 'GetX', + 'bloc': 'BLoC / Cubit', + 'riverpod': 'Riverpod', + 'getx': 'GetX', 'hooks_riverpod': 'Hooks + Riverpod', - 'clean': 'Clean Architecture', - 'feature_first': 'Feature-First', - 'mvvm': 'MVVM', - 'mvc': 'MVC', - 'layered': 'Layered', - 'gorouter': 'GoRouter', - 'getx_nav': 'GetX Navigation', - 'auto_route': 'Auto Route', - 'firebase': 'Firebase', - 'supabase': 'Supabase', - 'rest': 'REST API', - 'firebase_auth': 'Firebase Auth', - 'supabase_auth': 'Supabase Auth', - 'jwt_rest': 'JWT / REST Auth', - 'oauth2': 'OAuth 2.0', - 'none': 'None', - 'codemagic': 'Codemagic', + 'clean': 'Clean Architecture', + 'feature_first': 'Feature-First', + 'mvvm': 'MVVM', + 'mvc': 'MVC', + 'layered': 'Layered', + 'gorouter': 'GoRouter', + 'getx_nav': 'GetX Navigation', + 'auto_route': 'Auto Route', + 'firebase': 'Firebase', + 'supabase': 'Supabase', + 'rest': 'REST API', + 'firebase_auth': 'Firebase Auth', + 'supabase_auth': 'Supabase Auth', + 'jwt_rest': 'JWT / REST Auth', + 'oauth2': 'OAuth 2.0', + 'none': 'None', + 'codemagic': 'Codemagic', 'github_actions': 'GitHub Actions', - 'fastlane': 'Fastlane', + 'fastlane': 'Fastlane', }; return names[raw] ?? raw; } + static String _gitRefsBlock(ProjectBrief brief) { + if (brief.referenceRepos.isEmpty) { + return '_No Git repository URLs listed._ Add entries under `references.repos` in project-brief.yaml when other repos are part of the product context.'; + } + return brief.referenceRepos.map((u) => '- $u').join('\n'); + } + + static String _localPathsBlock(ProjectBrief brief) { + if (brief.localPaths.isEmpty) { + return '_No local paths listed._ Add monorepo packages or sibling folders under `references.local_paths` in project-brief.yaml when relevant.'; + } + return brief.localPaths.map((path) => '- `$path`').join('\n'); + } + + static String _themeSummary(ProjectBrief brief) { + if (brief.themeVariants.isEmpty) { + return '*(none specified — defaults not applied in brief; check YAML)*'; + } + return brief.themeVariants.map(_themeLabel).join(', '); + } + + static String _themeLabel(String token) { + switch (token) { + case 'high_contrast': + return 'high contrast'; + default: + return token; + } + } + + static String _rolesSummary(ProjectBrief brief) { + if (!brief.rolesEnabled) { + return 'Not enabled (`app_context.roles_enabled: false`).'; + } + if (brief.roleNames.isEmpty) { + return 'Enabled but **no role names** — add `app_context.role_names` in project-brief.yaml.'; + } + return brief.roleNames.join(', '); + } + + static String _highContrastNote(ProjectBrief brief) { + if (!brief.themeVariants.contains('high_contrast')) return ''; + return '\n- **High contrast:** validate contrast, borders, and focus in the high-contrast theme alongside light/dark (WCAG).\n'; + } + + static String _highContrastUxLine(ProjectBrief brief) { + if (!brief.themeVariants.contains('high_contrast')) return ''; + return '\n- **High contrast theme:** validate loading, empty, and error states; never rely on color alone for meaning (use icons/text/semantics).'; + } + static String _archImportRules(String arch) { switch (arch) { case 'clean': diff --git a/flutter-cursor-templates/generator/src/resolver.dart b/flutter-cursor-templates/generator/src/resolver.dart index 3e2041d..09c1ec1 100644 --- a/flutter-cursor-templates/generator/src/resolver.dart +++ b/flutter-cursor-templates/generator/src/resolver.dart @@ -52,6 +52,16 @@ class Resolver { files.add('rules/codegen/codegen-$tool'); } + // ── Hooks (Pillar 4) — tied to codegen, not state_management ───── + if (brief.codegenTools.isNotEmpty) { + files.addAll([ + 'hooks/hooks-json', + 'hooks/flutter-analyze', + 'hooks/grind-tests', + 'hooks/arch-guard', + ]); + } + // ── Localization ────────────────────────────────────────────────── if (brief.i18nEnabled) { files.add('rules/i18n/localization'); @@ -83,14 +93,6 @@ class Resolver { files.add('agents/migration-agent'); } - // ── Hooks ───────────────────────────────────────────────────────── - files.addAll([ - 'hooks/hooks-json', - 'hooks/flutter-analyze', - 'hooks/grind-tests', - 'hooks/arch-guard', - ]); - return files; } @@ -114,6 +116,9 @@ class Resolver { if (key.contains('testing-e2e')) return 'testing.depth includes e2e'; if (key.contains('testing')) return 'Matches state_management testing patterns'; if (key.contains('platform')) return 'Matches stack.platforms'; + if (key.startsWith('hooks/')) { + return 'stack.codegen non-empty — Cursor hooks for analyze, boundaries, and tests'; + } if (key.contains('codegen')) return 'Matches stack.codegen'; if (key.contains('i18n')) return 'localization.enabled: true'; if (key.contains('migration')) return 'state_management is GetX — migration guidance included'; diff --git a/flutter-cursor-templates/generator/src/telemetry.dart b/flutter-cursor-templates/generator/src/telemetry.dart index def5e9e..be01b3d 100644 --- a/flutter-cursor-templates/generator/src/telemetry.dart +++ b/flutter-cursor-templates/generator/src/telemetry.dart @@ -5,8 +5,6 @@ import 'dart:io'; import 'package:path/path.dart' as p; import 'logger.dart'; -const _telemetryFile = '.cursor/telemetry.json'; - class Telemetry { /// Record a generation event (local-only, opt-in) static Future record({ @@ -27,9 +25,9 @@ class Telemetry { final generations = data['generations'] as List; generations.add({ - 'timestamp': DateTime.now().toIso8601String(), + 'timestamp': DateTime.now().toIso8601String(), 'templateCount': templateFiles.length, - 'templates': templateFiles, + 'templates': templateFiles, }); // Keep only last 100 events @@ -48,7 +46,8 @@ class Telemetry { static Future printReport({required String outputDir}) async { final file = File(p.join(outputDir, 'telemetry.json')); if (!file.existsSync()) { - Logger.warn('No telemetry data found. Ensure telemetry_opt_in: true in project-brief.yaml'); + Logger.warn( + 'No telemetry data found. Ensure telemetry_opt_in: true in project-brief.yaml'); return; } final data = jsonDecode(await file.readAsString()) as Map; @@ -65,22 +64,29 @@ class Telemetry { final ruleCount = templates.where((t) => t.startsWith('rules/')).length; final agentCount = templates.where((t) => t.startsWith('agents/')).length; final skillCount = templates.where((t) => t.startsWith('skills/')).length; - Logger.dim(' Rules: $ruleCount | Agents: $agentCount | Skills: $skillCount'); + Logger.dim( + ' Rules: $ruleCount | Agents: $agentCount | Skills: $skillCount'); } Logger.info('\n💡 Quarterly template quality review checklist:'); - Logger.dim(' □ Review AI code review comments — which rules are most violated?'); - Logger.dim(' □ Ask teams: which agents are actually consulted vs. ignored?'); - Logger.dim(' □ Check if generated rules reduced hallucinations vs. last quarter'); - Logger.dim(' □ Identify brief combinations that produce the most AI errors'); - Logger.dim(' □ Update templates based on feedback, bump version in CHANGELOG.md'); + Logger.dim( + ' □ Review AI code review comments — which rules are most violated?'); + Logger.dim( + ' □ Ask teams: which agents are actually consulted vs. ignored?'); + Logger.dim( + ' □ Check if generated rules reduced hallucinations vs. last quarter'); + Logger.dim( + ' □ Identify brief combinations that produce the most AI errors'); + Logger.dim( + ' □ Update templates based on feedback, bump version in CHANGELOG.md'); } static Map _emptyData(String projectName) => { - 'projectName': projectName, - 'totalGenerations': 0, - 'lastGeneratedAt': '', - 'generations': [], - 'note': 'Local-only telemetry. Never sent anywhere. opt-in via telemetry_opt_in: true.', - }; + 'projectName': projectName, + 'totalGenerations': 0, + 'lastGeneratedAt': '', + 'generations': [], + 'note': + 'Local-only telemetry. Never sent anywhere. opt-in via telemetry_opt_in: true.', + }; } diff --git a/flutter-cursor-templates/generator/src/validator.dart b/flutter-cursor-templates/generator/src/validator.dart index 30eb164..209a9fb 100644 --- a/flutter-cursor-templates/generator/src/validator.dart +++ b/flutter-cursor-templates/generator/src/validator.dart @@ -5,24 +5,59 @@ import 'package:yaml/yaml.dart'; import 'models.dart'; class Validator { - static const _validStateManagement = {'bloc', 'riverpod', 'getx', 'hooks_riverpod'}; - static const _validArchitectures = {'clean', 'feature_first', 'mvvm', 'mvc', 'layered'}; - static const _validRouting = {'gorouter', 'getx_nav', 'auto_route'}; - static const _validBackends = {'firebase', 'supabase', 'rest'}; - static const _validAuth = {'firebase_auth', 'supabase_auth', 'jwt_rest', 'oauth2', 'none'}; - static const _validPlatforms = {'ios', 'android', 'web', 'desktop'}; - static const _validCodegen = {'freezed', 'json_serializable', 'injectable', 'retrofit'}; - static const _validScale = {'small', 'medium', 'large'}; - static const _validTestingDepth = {'unit_widget', 'integration', 'e2e', 'full'}; - static const _validCicd = {'codemagic', 'github_actions', 'fastlane', 'none'}; - static const _validE2eTools = {'patrol', 'maestro'}; - static const _validDesignSource = {'figma_mcp', 'figma_manual', 'native_ref', 'html_ref', 'none'}; - static const _validApiFormats = {'openapi', 'postman', 'markdown', 'none'}; + static const _validStateManagement = { + 'bloc', + 'riverpod', + 'getx', + 'hooks_riverpod' + }; + static const _validArchitectures = { + 'clean', + 'feature_first', + 'mvvm', + 'mvc', + 'layered' + }; + static const _validRouting = {'gorouter', 'getx_nav', 'auto_route'}; + static const _validBackends = {'firebase', 'supabase', 'rest'}; + static const _validAuth = { + 'firebase_auth', + 'supabase_auth', + 'jwt_rest', + 'oauth2', + 'none' + }; + static const _validPlatforms = {'ios', 'android', 'web', 'desktop'}; + static const _validCodegen = { + 'freezed', + 'json_serializable', + 'injectable', + 'retrofit' + }; + static const _validScale = {'small', 'medium', 'large'}; + static const _validTestingDepth = { + 'unit_widget', + 'integration', + 'e2e', + 'full' + }; + static const _validCicd = {'codemagic', 'github_actions', 'fastlane', 'none'}; + static const _validE2eTools = {'patrol', 'maestro'}; + static const _validDesignSource = { + 'figma_mcp', + 'figma_manual', + 'native_ref', + 'html_ref', + 'none' + }; + static const _validApiFormats = {'openapi', 'postman', 'markdown', 'none'}; + static const _validThemeVariants = {'light', 'dark', 'high_contrast'}; static Future validateFile(String path) async { final file = File(path); if (!file.existsSync()) { - return ValidationResult(isValid: false, errors: ['File not found: $path']); + return ValidationResult( + isValid: false, errors: ['File not found: $path']); } final content = await file.readAsString(); final yaml = loadYaml(content) as YamlMap; @@ -34,34 +69,42 @@ class Validator { } static ValidationResult _validateYaml(YamlMap yaml) { - final errors = []; + final errors = []; final warnings = []; final project = yaml['project'] as YamlMap?; - final stack = yaml['stack'] as YamlMap?; + final stack = yaml['stack'] as YamlMap?; + final envs = yaml['environments'] as YamlMap?; + final testing = yaml['testing'] as YamlMap?; + final design = yaml['design'] as YamlMap?; + final apiDocs = yaml['api_docs'] as YamlMap?; + final appCtx = yaml['app_context'] as YamlMap?; - if (project == null) { errors.add('Missing required section: project'); } - else { - if (project['name'] == null) errors.add('project.name is required'); + if (project == null) { + errors.add('Missing required section: project'); + } else { + if (project['name'] == null) errors.add('project.name is required'); if (project['package'] == null) errors.add('project.package is required'); if (project['scale'] != null && !_validScale.contains(project['scale'])) { errors.add('project.scale must be one of: ${_validScale.join(", ")}'); } } - if (stack == null) { errors.add('Missing required section: stack'); } - else { + if (stack == null) { + errors.add('Missing required section: stack'); + } else { _validateField(stack, 'state_management', _validStateManagement, errors); - _validateField(stack, 'architecture', _validArchitectures, errors); - _validateField(stack, 'routing', _validRouting, errors); - _validateField(stack, 'auth', _validAuth, warnings); // warning only + _validateField(stack, 'architecture', _validArchitectures, errors); + _validateField(stack, 'routing', _validRouting, errors); + _validateField(stack, 'auth', _validAuth, warnings); // warning only // Validate platforms list final platforms = stack['platforms']; if (platforms != null && platforms is YamlList) { for (final p in platforms) { if (!_validPlatforms.contains(p.toString())) { - errors.add('stack.platforms contains unknown value: $p. Valid: ${_validPlatforms.join(", ")}'); + errors.add( + 'stack.platforms contains unknown value: $p. Valid: ${_validPlatforms.join(", ")}'); } } } @@ -71,7 +114,8 @@ class Validator { if (codegen != null && codegen is YamlList) { for (final c in codegen) { if (!_validCodegen.contains(c.toString())) { - warnings.add('stack.codegen contains unknown value: $c. Known: ${_validCodegen.join(", ")}'); + warnings.add( + 'stack.codegen contains unknown value: $c. Known: ${_validCodegen.join(", ")}'); } } } @@ -79,50 +123,120 @@ class Validator { // Cross-validation: GetX nav requires GetX SM if (stack['routing']?.toString() == 'getx_nav' && stack['state_management']?.toString() != 'getx') { - warnings.add('stack.routing is getx_nav but state_management is not getx. This is unusual.'); + warnings.add( + 'stack.routing is getx_nav but state_management is not getx. This is unusual.'); } // Cross-validation: web platform warnings - final platformsList = (stack['platforms'] as YamlList?)?.map((e) => e.toString()).toList() ?? []; + final platformsList = (stack['platforms'] as YamlList?) + ?.map((e) => e.toString()) + .toList() ?? + []; if (platformsList.contains('web')) { - warnings.add('Web platform detected: ensure you avoid dart:io imports (use dart:html or flutter_web_plugins)'); + warnings.add( + 'Web platform detected: ensure you avoid dart:io imports (use dart:html or flutter_web_plugins)'); + } + } + + if (envs != null) { + _validateField(envs, 'cicd', _validCicd, warnings, + prefix: 'environments'); + } + if (testing != null) { + _validateField(testing, 'depth', _validTestingDepth, errors, + prefix: 'testing'); + _validateField(testing, 'e2e_tool', _validE2eTools, warnings, + prefix: 'testing'); + } + if (design != null) { + _validateField(design, 'source', _validDesignSource, warnings, + prefix: 'design'); + } + if (apiDocs != null) { + _validateField(apiDocs, 'format', _validApiFormats, warnings, + prefix: 'api_docs'); + } + if (appCtx != null) { + final themes = appCtx['theme_variants']; + for (final t in _yamlStrings(themes)) { + if (!_validThemeVariants.contains(t)) { + warnings.add( + 'app_context.theme_variants contains unknown value: $t (expected: ${_validThemeVariants.join(", ")})'); + } + } + if (appCtx['roles_enabled'] == true) { + final names = appCtx['role_names']; + final list = names is YamlList + ? names + : names is List + ? names + : const []; + if (list.isEmpty) { + warnings.add( + 'app_context.roles_enabled is true but role_names is empty'); + } } } return ValidationResult( - isValid: errors.isEmpty, - errors: errors, + isValid: errors.isEmpty, + errors: errors, warnings: warnings, ); } static ValidationResult _validateBrief(ProjectBrief brief) { - final errors = []; + final errors = []; final warnings = []; if (brief.projectName.isEmpty) errors.add('project.name is required'); - if (brief.packageId.isEmpty) errors.add('project.package is required'); + if (brief.packageId.isEmpty) errors.add('project.package is required'); if (!_validStateManagement.contains(brief.stateManagement)) { - errors.add('stack.state_management "${brief.stateManagement}" is not valid. Use: ${_validStateManagement.join(", ")}'); + errors.add( + 'stack.state_management "${brief.stateManagement}" is not valid. Use: ${_validStateManagement.join(", ")}'); } if (!_validArchitectures.contains(brief.architecture)) { - errors.add('stack.architecture "${brief.architecture}" is not valid. Use: ${_validArchitectures.join(", ")}'); + errors.add( + 'stack.architecture "${brief.architecture}" is not valid. Use: ${_validArchitectures.join(", ")}'); } for (final b in brief.backends) { if (!_validBackends.contains(b)) { - errors.add('stack.backend contains "$b". Valid values: ${_validBackends.join(", ")}'); + errors.add( + 'stack.backend contains "$b". Valid values: ${_validBackends.join(", ")}'); } } + for (final t in brief.themeVariants) { + if (!_validThemeVariants.contains(t)) { + warnings.add( + 'app_context.theme_variants contains unknown value: $t (expected: ${_validThemeVariants.join(", ")})'); + } + } + if (brief.rolesEnabled && brief.roleNames.isEmpty) { + warnings.add('roles_enabled is true but role_names is empty'); + } - return ValidationResult(isValid: errors.isEmpty, errors: errors, warnings: warnings); + return ValidationResult( + isValid: errors.isEmpty, errors: errors, warnings: warnings); + } + + static Iterable _yamlStrings(dynamic value) sync* { + if (value == null) return; + if (value is YamlList) { + for (final e in value) yield e.toString(); + } else if (value is List) { + for (final e in value) yield e.toString(); + } else { + yield value.toString(); + } } static void _validateField( - YamlMap map, String field, Set valid, List output, - ) { + YamlMap map, String field, Set valid, List output, + {String prefix = 'stack'}) { final val = map[field]?.toString(); if (val != null && !valid.contains(val)) { - output.add('stack.$field "$val" is not valid. Use: ${valid.join(", ")}'); + output + .add('$prefix.$field "$val" is not valid. Use: ${valid.join(", ")}'); } } } diff --git a/flutter-cursor-templates/generator/src/version_manager.dart b/flutter-cursor-templates/generator/src/version_manager.dart index 582eaac..13b68cc 100644 --- a/flutter-cursor-templates/generator/src/version_manager.dart +++ b/flutter-cursor-templates/generator/src/version_manager.dart @@ -8,7 +8,7 @@ import 'models.dart'; import 'logger.dart'; const _lockFileName = '.cursor-gen-lock.json'; -const _currentVersion = '1.0.0'; +const _currentVersion = '1.0.1'; class VersionManager { /// Check if the project's locked version differs from the current template version @@ -16,9 +16,9 @@ class VersionManager { final locked = brief.cursorTemplatesVersion ?? 'unset'; final hasUpdate = locked != _currentVersion && locked != 'unset'; return VersionStatus( - hasUpdate: hasUpdate, + hasUpdate: hasUpdate, currentVersion: locked, - latestVersion: _currentVersion, + latestVersion: _currentVersion, ); } @@ -30,20 +30,20 @@ class VersionManager { }) async { final briefHash = await _fileHash(briefPath); final lock = { - 'templateVersion': _currentVersion, - 'generatedAt': DateTime.now().toIso8601String(), - 'briefHash': briefHash, - 'projectName': brief.projectName, + 'templateVersion': _currentVersion, + 'generatedAt': DateTime.now().toIso8601String(), + 'briefHash': briefHash, + 'projectName': brief.projectName, 'stack': { 'stateManagement': brief.stateManagement, - 'architecture': brief.architecture, - 'routing': brief.routing, - 'backends': brief.backends, - 'platforms': brief.platforms, - 'codegenTools': brief.codegenTools, + 'architecture': brief.architecture, + 'routing': brief.routing, + 'backends': brief.backends, + 'platforms': brief.platforms, + 'codegenTools': brief.codegenTools, }, 'note': 'Auto-generated by cursor_gen. Do not edit manually. ' - 'Run: dart run cursor_gen --check-updates to see available updates.', + 'Run: cursor_gen --check-updates to see available updates.', }; final file = File(p.join(outputDir, _lockFileName)); await file.parent.create(recursive: true); @@ -66,7 +66,7 @@ class VersionManager { } final lock = await readLock('.cursor'); if (lock == null) { - Logger.warn('No lock file found. Run: dart run cursor_gen first.'); + Logger.warn('No lock file found. Run: cursor_gen first.'); return; } final lockedVersion = lock['templateVersion'] as String? ?? 'unknown'; @@ -79,10 +79,12 @@ class VersionManager { } else { Logger.warn(' ⚠ Update available!'); Logger.info('\nTo update:'); - Logger.info(' 1. Update cursor_templates_version in project-brief.yaml to "$_currentVersion"'); - Logger.info(' 2. Run: dart run cursor_gen --diff (preview changes)'); - Logger.info(' 3. Run: dart run cursor_gen --refresh (apply updates)'); - Logger.info('\nChangelog: see CHANGELOG.md in flutter-cursor-templates repo'); + Logger.info( + ' 1. Update cursor_templates_version in project-brief.yaml to "$_currentVersion"'); + Logger.info(' 2. Run: cursor_gen --diff (preview changes)'); + Logger.info(' 3. Run: cursor_gen --refresh (apply updates)'); + Logger.info( + '\nChangelog: see CHANGELOG.md in flutter-cursor-templates repo'); } } diff --git a/flutter-cursor-templates/generator/src/wizard.dart b/flutter-cursor-templates/generator/src/wizard.dart index f4cdb18..cf2ecc8 100644 --- a/flutter-cursor-templates/generator/src/wizard.dart +++ b/flutter-cursor-templates/generator/src/wizard.dart @@ -7,57 +7,100 @@ class Wizard { static Future run({required String outputPath}) async { Logger.info('\n🧙 cursor_gen Interactive Wizard'); Logger.info('─' * 42); - Logger.info('Answer the questions below to generate your project-brief.yaml.\n'); + Logger.info( + 'Answer the questions below to generate your project-brief.yaml.\n'); final answers = {}; // Project basics - answers['name'] = _ask('Project name', hint: 'ShopEasy'); - answers['package'] = _ask('Package ID', hint: 'com.company.shopeasy'); - answers['description'] = _ask('Short description', hint: 'E-commerce app with real-time inventory'); - answers['scale'] = _askChoice('Project scale', ['small', 'medium', 'large'], defaultIdx: 1); + answers['name'] = _ask('Project name', hint: 'ShopEasy'); + answers['package'] = _ask('Package ID', hint: 'com.company.shopeasy'); + answers['description'] = _ask('Short description', + hint: 'E-commerce app with real-time inventory'); + answers['scale'] = _askChoice('Project scale', ['small', 'medium', 'large'], + defaultIdx: 1); Logger.info(''); // Stack - answers['state_management'] = _askChoice('State management', - ['bloc', 'riverpod', 'getx', 'hooks_riverpod'], defaultIdx: 1); - answers['architecture'] = _askChoice('Architecture', - ['clean', 'feature_first', 'mvvm', 'mvc', 'layered'], defaultIdx: 0); - answers['routing'] = _askChoice('Routing', - ['gorouter', 'getx_nav', 'auto_route'], defaultIdx: 0); - answers['backend'] = _askChoice('Backend(s)', - ['firebase', 'supabase', 'rest', 'firebase+rest', 'supabase+rest'], defaultIdx: 2); - answers['auth'] = _askChoice('Auth method', - ['firebase_auth', 'supabase_auth', 'jwt_rest', 'oauth2', 'none'], defaultIdx: 4); + answers['state_management'] = _askChoice( + 'State management', ['bloc', 'riverpod', 'getx', 'hooks_riverpod'], + defaultIdx: 1); + answers['architecture'] = _askChoice( + 'Architecture', ['clean', 'feature_first', 'mvvm', 'mvc', 'layered'], + defaultIdx: 0); + answers['routing'] = _askChoice( + 'Routing', ['gorouter', 'getx_nav', 'auto_route'], + defaultIdx: 0); + answers['backend'] = _askChoice('Backend(s)', + ['firebase', 'supabase', 'rest', 'firebase+rest', 'supabase+rest'], + defaultIdx: 2); + answers['auth'] = _askChoice('Auth method', + ['firebase_auth', 'supabase_auth', 'jwt_rest', 'oauth2', 'none'], + defaultIdx: 4); Logger.info(''); // Platforms (Pillar 4) - answers['platforms'] = _askMultiChoice('Target platforms', - ['ios', 'android', 'web', 'desktop'], defaults: [0, 1]); + answers['platforms'] = _askMultiChoice( + 'Target platforms', ['ios', 'android', 'web', 'desktop'], + defaults: [0, 1]); // Codegen (Pillar 4) - answers['codegen'] = _askMultiChoice('Code generation tools (optional)', - ['freezed', 'json_serializable', 'injectable', 'retrofit'], defaults: []); + answers['codegen'] = _askMultiChoice('Code generation tools (optional)', [ + 'freezed', + 'json_serializable', + 'injectable', + 'retrofit' + ], defaults: []); + + Logger.info(''); + + // References & app UX + final reposRaw = + _ask('Git reference repo URLs (comma-separated, optional)', hint: ''); + answers['reference_repos'] = _splitComma(reposRaw); + final localRaw = + _ask('Local reference paths (comma-separated, optional)', hint: ''); + answers['local_paths'] = _splitComma(localRaw); + answers['theme_variants'] = _askMultiChoice( + 'Theme variants to support', + ['light', 'dark', 'high_contrast'], + defaults: [0, 1]); + final rolesOn = + _askBool('Does the app support user roles?', defaultYes: false); + answers['roles_enabled'] = rolesOn; + if (rolesOn) { + final raw = + _ask('Role names (comma-separated)', hint: 'admin,member,guest'); + answers['role_names'] = _splitComma(raw); + } else { + answers['role_names'] = []; + } Logger.info(''); // Environments - final flavorsInput = _ask('Build flavors (comma-separated)', hint: 'dev,staging,prod'); + final flavorsInput = + _ask('Build flavors (comma-separated)', hint: 'dev,staging,prod'); answers['flavors'] = flavorsInput.split(',').map((e) => e.trim()).toList(); - answers['cicd'] = _askChoice('CI/CD', ['github_actions', 'codemagic', 'fastlane', 'none'], defaultIdx: 0); + answers['cicd'] = _askChoice( + 'CI/CD', ['github_actions', 'codemagic', 'fastlane', 'none'], + defaultIdx: 0); // Testing - answers['testing_depth'] = _askChoice('Testing depth', - ['unit_widget', 'integration', 'e2e', 'full'], defaultIdx: 0); + answers['testing_depth'] = _askChoice( + 'Testing depth', ['unit_widget', 'integration', 'e2e', 'full'], + defaultIdx: 0); // Localization final l10n = _askBool('Enable localization / i18n?', defaultYes: false); answers['i18n'] = l10n; // Telemetry opt-in (Pillar 6) - final telemetry = _askBool('\nOpt in to local telemetry (rule trigger logging, stored locally)?', defaultYes: false); + final telemetry = _askBool( + '\nOpt in to local telemetry (rule trigger logging, stored locally)?', + defaultYes: false); answers['telemetry'] = telemetry; Logger.info(''); @@ -65,7 +108,7 @@ class Wizard { final file = File(outputPath); await file.writeAsString(yaml); Logger.success('✔ project-brief.yaml written to $outputPath'); - Logger.info('\nNext: dart run cursor_gen --validate → dart run cursor_gen'); + Logger.info('\nNext: cursor_gen --validate → cursor_gen'); } static String _ask(String label, {String hint = ''}) { @@ -75,7 +118,8 @@ class Wizard { return input.isEmpty ? hint : input; } - static String _askChoice(String label, List options, {int defaultIdx = 0}) { + static String _askChoice(String label, List options, + {int defaultIdx = 0}) { Logger.info(' $label:'); for (var i = 0; i < options.length; i++) { final marker = i == defaultIdx ? ' ◀' : ''; @@ -85,11 +129,13 @@ class Wizard { final input = stdin.readLineSync()?.trim() ?? ''; if (input.isEmpty) return options[defaultIdx]; final idx = int.tryParse(input); - if (idx != null && idx >= 1 && idx <= options.length) return options[idx - 1]; + if (idx != null && idx >= 1 && idx <= options.length) + return options[idx - 1]; return options[defaultIdx]; } - static List _askMultiChoice(String label, List options, {List defaults = const []}) { + static List _askMultiChoice(String label, List options, + {List defaults = const []}) { Logger.info(' $label (comma-separated numbers, or leave blank):'); for (var i = 0; i < options.length; i++) { final marker = defaults.contains(i) ? ' ◀' : ''; @@ -99,7 +145,8 @@ class Wizard { stdout.write(' Choices [$defaultStr]: '); final input = stdin.readLineSync()?.trim() ?? ''; if (input.isEmpty) return defaults.map((d) => options[d]).toList(); - return input.split(',') + return input + .split(',') .map((s) => int.tryParse(s.trim())) .where((i) => i != null && i >= 1 && i <= options.length) .map((i) => options[i! - 1]) @@ -114,17 +161,35 @@ class Wizard { return input == 'y' || input == 'yes'; } + static List _splitComma(String s) => s + .split(',') + .map((e) => e.trim()) + .where((e) => e.isNotEmpty) + .toList(); + + static String _yamlQStringList(List xs) { + if (xs.isEmpty) return ''; + return xs.map((e) => '"${_yamlEsc(e)}"').join(', '); + } + + static String _yamlEsc(String s) => + s.replaceAll(r'\', r'\\').replaceAll('"', r'\"'); + static String _buildYaml(Map a) { final platforms = (a['platforms'] as List).map((p) => '"$p"').join(', '); - final codegen = (a['codegen'] as List).map((c) => '"$c"').join(', '); - final flavors = (a['flavors'] as List).map((f) => '"$f"').join(', '); + final codegen = (a['codegen'] as List).map((c) => '"$c"').join(', '); + final flavors = (a['flavors'] as List).map((f) => '"$f"').join(', '); + final refRepos = List.from(a['reference_repos'] as List); + 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); return '''# project-brief.yaml — cursor_gen configuration # Generated by cursor_gen --wizard -# Run: dart run cursor_gen to generate .cursor/ -# Run: dart run cursor_gen --refresh to update after changes +# 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.0" +cursor_templates_version: "1.0.1" project: name: "${a['name']}" @@ -162,8 +227,13 @@ api_docs: path: "" references: - repos: [] - local_paths: [] + repos: [${_yamlQStringList(refRepos)}] + local_paths: [${_yamlQStringList(refLocals)}] + +app_context: + theme_variants: [${_yamlQStringList(themes)}] + roles_enabled: ${a['roles_enabled']} + role_names: [${_yamlQStringList(roles)}] features: modules: [] diff --git a/flutter-cursor-templates/generator/templates/agents/api-client-gen.mdc.tmpl b/flutter-cursor-templates/generator/templates/agents/api-client-gen.mdc.tmpl new file mode 100644 index 0000000..7c50fc3 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/agents/api-client-gen.mdc.tmpl @@ -0,0 +1,34 @@ +--- +name: api-client-gen +description: "Generates type-safe API clients for {{PROJECT_NAME}} from {{API_DOCS_FORMAT}} spec. Ask: '@api-client-gen generate client for /products endpoint'" +model: claude-sonnet-4-20250514 +context: auto +allowed-tools: [read_file, write_file, list_files] +--- + +You are an API client generator for **{{PROJECT_NAME}}**. +API docs: `{{API_DOCS_PATH}}` (format: {{API_DOCS_FORMAT}}) + +## Generation steps +1. Read the API spec at `{{API_DOCS_PATH}}` +2. For the requested endpoint(s), generate: + - Request DTO (`@JsonSerializable` or Freezed) + - Response DTO (`@JsonSerializable` or Freezed) + - Repository method with error handling + - Dio/Retrofit client method (if Retrofit in codegen) + +## Output structure +```dart +// data/models/product_dto.dart +@freezed +class ProductDto with _$ProductDto { + factory ProductDto({...}) = _ProductDto; + factory ProductDto.fromJson(Map json) => _$ProductDtoFromJson(json); +} + +// data/datasources/product_remote_datasource.dart +class ProductRemoteDataSource { + final Dio _dio; + Future> getProducts() async { ... } +} +``` diff --git a/flutter-cursor-templates/generator/templates/agents/code-reviewer.mdc.tmpl b/flutter-cursor-templates/generator/templates/agents/code-reviewer.mdc.tmpl new file mode 100644 index 0000000..8598efd --- /dev/null +++ b/flutter-cursor-templates/generator/templates/agents/code-reviewer.mdc.tmpl @@ -0,0 +1,51 @@ +--- +name: code-reviewer +description: "Reviews {{PROJECT_NAME}} code for {{STATE_MANAGEMENT}} patterns, {{ARCHITECTURE}} boundaries, and {{BACKEND}} usage. Ask: 'Review this code' or '@code-reviewer check PR'" +model: claude-opus-4-5 +context: auto +allowed-tools: [read_file, list_files] +--- + +You are a senior Flutter engineer reviewing code for **{{PROJECT_NAME}}**. + +## Your review checklist + +### {{STATE_MANAGEMENT}} patterns +- Are state classes immutable and sealed? +- Is state management correctly separated from UI logic? +- Are streams/subscriptions properly cancelled in dispose()? +- Check for anti-patterns specific to {{STATE_MANAGEMENT}} + +### {{ARCHITECTURE}} boundaries +{{ARCH_IMPORT_RULES}} +- Flag any violation of these import rules immediately + +### {{BACKEND}} usage +- Are all exceptions caught and mapped to domain errors? +- Are streams vs futures used appropriately? +- Are connections/subscriptions disposed correctly? + +### Security (always check) +- No hardcoded API keys or secrets +- No PII logged to console or crash reporters +- Sensitive data using flutter_secure_storage, not SharedPreferences +- All user inputs validated before sending to backend + +### General Flutter +- `const` used where possible +- `dispose()` overridden for all controllers/subscriptions +- No `print()` in production paths +- Loading/empty/error states all handled + +## Output format +For each issue found: +``` +[SEVERITY: critical/major/minor] File:line — Issue description +WHY: Why this matters +FIX: Specific fix recommendation +``` + +Severity guide: +- **critical**: Security issue, data loss risk, crash potential +- **major**: Incorrect pattern, boundary violation, missing error handling +- **minor**: Style, naming, optimization opportunity diff --git a/flutter-cursor-templates/generator/templates/agents/migration-agent.mdc.tmpl b/flutter-cursor-templates/generator/templates/agents/migration-agent.mdc.tmpl new file mode 100644 index 0000000..d1e92bc --- /dev/null +++ b/flutter-cursor-templates/generator/templates/agents/migration-agent.mdc.tmpl @@ -0,0 +1,42 @@ +--- +name: migration-agent +description: "Migrates GetX controllers to Riverpod Notifiers for {{PROJECT_NAME}}. Consult before adding any new GetX code — suggest Riverpod equivalent. Ask: '@migration-agent migrate [feature]'" +model: claude-opus-4-5 +context: fork +allowed-tools: [read_file, write_file, list_files] +--- + +You are a Flutter migration specialist for **{{PROJECT_NAME}}**. +This project currently uses GetX ({{ARCHITECTURE}}). Your goal is incremental, +safe migration to Riverpod without breaking existing features. + +## Migration mapping +| GetX | Riverpod equivalent | +|------|---------------------| +| `GetxController` with `.obs` | `AsyncNotifier` or `Notifier` | +| `Obx()` | `ref.watch()` in `ConsumerWidget` | +| `Get.find()` | `ref.read(provider.notifier)` | +| `Get.toNamed()` | `context.go()` (after GoRouter migration) | +| `GetxController.onInit()` | `build()` method in `AsyncNotifier` | +| `GetxController.onClose()` | `ref.onDispose()` | + +## Migration process (feature by feature) +1. **Read** the existing `GetxController` in full +2. **Write tests** for the existing GetX version first (if none exist) +3. **Map** `.obs` variables → state class fields +4. **Write** the new `AsyncNotifier` with equivalent logic +5. **Write tests** for the Riverpod version using `ProviderContainer` +6. **Migrate** the View: `GetView` → `ConsumerWidget`, `Obx()` → `ref.watch()` +7. **Verify** all tests pass for both old and new +8. **Remove** GetX code from that feature + +## When NOT to migrate (mark with TODO: MIGRATE-LATER) +- Controller shared across 5+ screens (high blast radius — plan separately) +- Feature ships in the next sprint (postpone — don't hold up a release) +- No tests exist AND you can't write them first (write GetX tests first) + +## Output per migration +1. New Riverpod provider file +2. Updated ConsumerWidget screen file +3. Test file for the new provider +4. Diff showing what GetX code is removed diff --git a/flutter-cursor-templates/generator/templates/agents/security-agent.mdc.tmpl b/flutter-cursor-templates/generator/templates/agents/security-agent.mdc.tmpl new file mode 100644 index 0000000..7dc76be --- /dev/null +++ b/flutter-cursor-templates/generator/templates/agents/security-agent.mdc.tmpl @@ -0,0 +1,47 @@ +--- +name: security-agent +description: "Deep security review for {{PROJECT_NAME}}. Consult for auth flows, payment screens, and sensitive data handling. Ask: '@security-agent review auth flow'" +model: claude-opus-4-5 +context: fork +allowed-tools: [read_file, list_files] +--- + +You are a mobile security expert conducting a deep review for **{{PROJECT_NAME}}**. + +> Note: This agent provides deep security analysis. +> The `security-standards.mdc` rule provides always-on enforcement. +> This agent is for detailed consultations on specific security concerns. + +## Deep review focus areas + +### Auth flow ({{AUTH}}) +- Token storage: is `flutter_secure_storage` used for ALL tokens? +- Token refresh: is refresh handled atomically (no race condition)? +- Session expiry: does the app handle 401 gracefully without data loss? +- Certificate pinning: configured and tested? + +### Data at rest +- SQLite/Hive encryption: sensitive DBs encrypted? +- Cache poisoning: cached API responses validated before use? +- Keychain/Keystore usage for cryptographic keys + +### Network security +- All endpoints HTTPS — any http:// URLs? +- Certificate validation — any `badCertificateCallback: true`? +- Sensitive data in URL params/query strings? +- Request/response logging in production? (must be off) + +### Code injection risks +- Dynamic code execution patterns +- WebView usage — JavaScript interface security +- Deep link parameter validation (no path traversal) + +## Output format +For each finding: +``` +[RISK: Critical/High/Medium/Low] +LOCATION: File / function +ISSUE: Detailed description +CVSS-like impact: Confidentiality/Integrity/Availability +REMEDIATION: Specific code fix +``` diff --git a/flutter-cursor-templates/generator/templates/agents/test-writer.mdc.tmpl b/flutter-cursor-templates/generator/templates/agents/test-writer.mdc.tmpl new file mode 100644 index 0000000..f05de3c --- /dev/null +++ b/flutter-cursor-templates/generator/templates/agents/test-writer.mdc.tmpl @@ -0,0 +1,33 @@ +--- +name: test-writer +description: "Writes {{STATE_MANAGEMENT}} unit tests for {{PROJECT_NAME}}. Ask: 'Write tests for [class]' or '@test-writer generate tests'" +model: claude-sonnet-4-20250514 +context: auto +allowed-tools: [read_file, write_file, list_files] +--- + +You are a Flutter test engineer for **{{PROJECT_NAME}}** using **{{STATE_MANAGEMENT}}**. + +## Test pattern to follow +```dart +{{TEST_PATTERN}} +``` + +## When asked to write tests: +1. Read the source file completely +2. Identify all public methods and state transitions +3. Write tests for: + - Happy path (successful operation) + - Error path (failure, exception handling) + - Edge cases (empty data, boundary values) +4. Use `mocktail` for all mocking +5. Follow `Given/When/Then` naming: `'given X, when Y, then emits Z'` + +## File placement +- Unit tests: `test/features/[feature]/[file]_test.dart` +- Widget tests: `test/features/[feature]/[screen]_widget_test.dart` +- Coverage target: 80% minimum for business logic classes + +## Output +Write the complete test file, ready to run. Include all imports. +After writing, run: `dart test path/to/test_file.dart` diff --git a/flutter-cursor-templates/generator/templates/agents/ui-validator.mdc.tmpl b/flutter-cursor-templates/generator/templates/agents/ui-validator.mdc.tmpl new file mode 100644 index 0000000..8033838 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/agents/ui-validator.mdc.tmpl @@ -0,0 +1,47 @@ +--- +name: ui-validator +description: "Validates {{PROJECT_NAME}} UI against design system and UX standards. Ask: 'Validate this screen' or '@ui-validator check'" +model: claude-sonnet-4-20250514 +context: auto +allowed-tools: [read_file, list_files] +--- + +You are a UI/UX validator for **{{PROJECT_NAME}}**. + +## Validate every screen for: + +### State coverage ({{STATE_MANAGEMENT}}) +- Loading state: shows shimmer (NOT spinner unless brief) +- Empty state: shows illustration + CTA (NOT blank screen) +- Error state: shows message + retry button (NOT toast-only) +- Data state: renders content correctly + +### Accessibility +- All interactive widgets have semantic labels +- Minimum touch targets: 48×48dp +- Sufficient color contrast (4.5:1 minimum) + +### Responsive layout +- No hardcoded pixel widths +- Tested at 375px and 414px viewport widths +- `SafeArea` used correctly on iOS + +### Platform-specific ({{PLATFORMS_LIST}}) +{{#if web}} +- No dart:io imports in web-targeted code +- PWA-compatible (no native-only APIs without fallbacks) +{{/if}} +{{#if ios}} +- Safe area respected (notch, Dynamic Island) +- iOS Human Interface Guidelines followed +{{/if}} + +### Security (UI layer) +- No credentials shown in plaintext +- Sensitive screens wrapped with screenshot prevention + +## Output +List each violation with: +- Location (file:widget) +- What's wrong +- How to fix it diff --git a/flutter-cursor-templates/generator/templates/hooks/arch-guard.ts.tmpl b/flutter-cursor-templates/generator/templates/hooks/arch-guard.ts.tmpl new file mode 100644 index 0000000..f53d62d --- /dev/null +++ b/flutter-cursor-templates/generator/templates/hooks/arch-guard.ts.tmpl @@ -0,0 +1,141 @@ +#!/usr/bin/env ts-node +// arch-guard.ts — Pre-commit hook: enforces {{ARCHITECTURE}} import boundaries +// Generated for {{PROJECT_NAME}} ({{ARCHITECTURE}} architecture) + +import * as fs from 'fs'; +import * as path from 'path'; +import { execSync } from 'child_process'; + +const RED = '\x1b[31m'; +const YELLOW = '\x1b[33m'; +const GREEN = '\x1b[32m'; +const RESET = '\x1b[0m'; + +interface ArchRule { + sourcePattern: RegExp; + forbiddenImports: RegExp[]; + message: string; +} + +// Architecture-specific rules for {{ARCHITECTURE}} +const ARCH_RULES: ArchRule[] = [ + ...getArchRules() +]; + +function getArchRules(): ArchRule[] { + const arch = '{{ARCH_RAW}}'; + + if (arch === 'clean') { + return [ + { + sourcePattern: /features\/\w+\/domain\//, + forbiddenImports: [/features\/\w+\/data\//, /features\/\w+\/presentation\//], + message: 'domain/ must not import from data/ or presentation/', + }, + { + sourcePattern: /features\/\w+\/presentation\//, + forbiddenImports: [/features\/\w+\/data\//], + message: 'presentation/ must not import from data/ directly (use domain interfaces)', + }, + ]; + } + + if (arch === 'feature_first') { + return [ + { + // A feature file must not import from another feature + sourcePattern: /features\/(\w+)\//, + forbiddenImports: [], // checked dynamically below + message: 'Features must not import from other feature folders', + }, + ]; + } + + if (arch === 'mvvm') { + return [ + { + sourcePattern: /viewmodels\//, + forbiddenImports: [/package:flutter\//, /widgets\//], + message: 'ViewModels must not import Flutter widgets — they must be plain Dart testable', + }, + ]; + } + + return []; +} + +function getChangedDartFiles(): string[] { + try { + const result = execSync('git diff --cached --name-only --diff-filter=ACM', { encoding: 'utf8' }); + return result.split('\n').filter(f => f.endsWith('.dart') && fs.existsSync(f)); + } catch { + return []; + } +} + +function checkFile(filePath: string): string[] { + const violations: string[] = []; + const content = fs.readFileSync(filePath, 'utf8'); + const imports = content.match(/^import\s+'[^']+'/gm) ?? []; + + for (const rule of ARCH_RULES) { + if (!rule.sourcePattern.test(filePath)) continue; + + // Feature-first cross-feature check + if ('{{ARCH_RAW}}' === 'feature_first') { + const srcFeatureMatch = filePath.match(/features\/(\w+)\//); + if (srcFeatureMatch) { + const srcFeature = srcFeatureMatch[1]; + for (const imp of imports) { + const impFeatureMatch = imp.match(/features\/(\w+)\//); + if (impFeatureMatch && impFeatureMatch[1] !== srcFeature) { + violations.push( + `${RED}ARCH VIOLATION${RESET} in ${filePath}:\n` + + ` ${YELLOW}${rule.message}${RESET}\n` + + ` Import: ${imp}\n` + + ` Fix: Move shared code to core/ or shared/` + ); + } + } + } + continue; + } + + // Standard forbidden import check + for (const imp of imports) { + for (const forbidden of rule.forbiddenImports) { + if (forbidden.test(imp)) { + violations.push( + `${RED}ARCH VIOLATION${RESET} in ${filePath}:\n` + + ` ${YELLOW}${rule.message}${RESET}\n` + + ` Import: ${imp}` + ); + } + } + } + } + + return violations; +} + +const changedFiles = getChangedDartFiles(); +if (changedFiles.length === 0) { + console.log(`${GREEN}✔ arch-guard: no Dart files changed${RESET}`); + process.exit(0); +} + +console.log(`🏛 arch-guard checking ${changedFiles.length} file(s) for {{ARCHITECTURE}} violations...`); + +const allViolations: string[] = []; +for (const file of changedFiles) { + allViolations.push(...checkFile(file)); +} + +if (allViolations.length > 0) { + console.error(`\n${RED}✘ Architecture boundary violations detected:${RESET}\n`); + for (const v of allViolations) console.error(v + '\n'); + console.error(`Total: ${allViolations.length} violation(s). Fix before committing.`); + process.exit(1); +} + +console.log(`${GREEN}✔ arch-guard: no violations found${RESET}`); diff --git a/flutter-cursor-templates/generator/templates/hooks/flutter-analyze.ts.tmpl b/flutter-cursor-templates/generator/templates/hooks/flutter-analyze.ts.tmpl new file mode 100644 index 0000000..a8918a7 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/hooks/flutter-analyze.ts.tmpl @@ -0,0 +1,32 @@ +#!/usr/bin/env ts-node +// flutter-analyze.ts — Pre-commit hook: runs dart analyze and dart format check +import { execSync } from 'child_process'; + +const RED = '\x1b[31m'; +const GREEN = '\x1b[32m'; +const RESET = '\x1b[0m'; + +function run(cmd: string): { stdout: string; code: number } { + try { + const stdout = execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }); + return { stdout, code: 0 }; + } catch (e: any) { + return { stdout: e.stdout ?? e.message, code: e.status ?? 1 }; + } +} + +console.log('🔍 Running flutter analyze...'); +const analyze = run('flutter analyze --no-congratulate'); +if (analyze.code !== 0) { + console.error(`${RED}✘ flutter analyze failed:\n${analyze.stdout}${RESET}`); + process.exit(1); +} + +console.log('🎨 Checking dart format...'); +const format = run('dart format --output=none --set-exit-if-changed lib/ test/'); +if (format.code !== 0) { + console.error(`${RED}✘ Unformatted files detected. Run: dart format lib/ test/${RESET}`); + process.exit(1); +} + +console.log(`${GREEN}✔ flutter analyze + dart format passed${RESET}`); diff --git a/flutter-cursor-templates/generator/templates/hooks/grind-tests.ts.tmpl b/flutter-cursor-templates/generator/templates/hooks/grind-tests.ts.tmpl new file mode 100644 index 0000000..dd6385c --- /dev/null +++ b/flutter-cursor-templates/generator/templates/hooks/grind-tests.ts.tmpl @@ -0,0 +1,16 @@ +#!/usr/bin/env ts-node +// grind-tests.ts — Pre-push hook: runs flutter test +import { execSync } from 'child_process'; + +const RED = '\x1b[31m'; +const GREEN = '\x1b[32m'; +const RESET = '\x1b[0m'; + +console.log('🧪 Running flutter test...'); +try { + execSync('flutter test --coverage', { stdio: 'inherit' }); + console.log(`${GREEN}✔ All tests passed${RESET}`); +} catch { + console.error(`${RED}✘ Tests failed. Fix failing tests before pushing.${RESET}`); + process.exit(1); +} diff --git a/flutter-cursor-templates/generator/templates/hooks/hooks.json.tmpl b/flutter-cursor-templates/generator/templates/hooks/hooks.json.tmpl new file mode 100644 index 0000000..e74d712 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/hooks/hooks.json.tmpl @@ -0,0 +1,19 @@ +{ + "hooks": [ + { + "name": "pre-commit: flutter analyze", + "command": "npx ts-node .cursor/hooks/flutter-analyze.ts", + "events": ["pre-commit"] + }, + { + "name": "pre-commit: arch guard", + "command": "npx ts-node .cursor/hooks/arch-guard.ts", + "events": ["pre-commit"] + }, + { + "name": "pre-push: run tests", + "command": "npx ts-node .cursor/hooks/grind-tests.ts", + "events": ["pre-push"] + } + ] +} diff --git a/flutter-cursor-templates/generator/templates/rules/architecture/clean.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/architecture/clean.mdc.tmpl new file mode 100644 index 0000000..a35cb52 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/architecture/clean.mdc.tmpl @@ -0,0 +1,53 @@ +--- +description: "Clean Architecture conventions for {{PROJECT_NAME}}" +alwaysApply: true +--- + +# Clean Architecture — {{PROJECT_NAME}} + +## Layer structure +``` +lib/features/[feature]/ + ├── domain/ + │ ├── entities/ ← Pure Dart, no framework imports + │ ├── repositories/ ← Abstract interfaces only + │ └── usecases/ ← Single-responsibility business operations + ├── data/ + │ ├── datasources/ ← Remote (API) + Local (cache) implementations + │ ├── models/ ← DTOs with fromJson/toJson (can use Freezed) + │ └── repositories/ ← Implements domain/repositories interfaces + └── presentation/ + ├── bloc/ or notifiers/ + ├── pages/ + └── widgets/ +``` + +## Import rules (STRICTLY ENFORCED by arch-guard hook) +{{ARCH_IMPORT_RULES}} + +## UseCase pattern +```dart +// One UseCase = one operation = one method +class GetProductsUseCase { + final ProductRepository _repository; + const GetProductsUseCase(this._repository); + + Future>> call(ProductFilter filter) => + _repository.getProducts(filter); +} +``` + +## Entity rules +- Entities are pure Dart — zero Flutter or framework imports +- Entities are immutable — use `final` fields + factory constructors +- Entities NEVER have `fromJson`/`toJson` — that belongs in the data layer model + +## Repository rules +- Domain defines the **interface** (abstract class) +- Data layer **implements** it +- Use `Either` or `Result` return types — never throw in domain + +## Dependency injection +- Use `injectable` + `get_it` if `codegen` includes `injectable` +- All UseCases injected into BLoC/Notifier via constructor +- `DataSource → Repository → UseCase → Bloc` dependency direction diff --git a/flutter-cursor-templates/generator/templates/rules/architecture/feature_first.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/architecture/feature_first.mdc.tmpl new file mode 100644 index 0000000..fb4c5d9 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/architecture/feature_first.mdc.tmpl @@ -0,0 +1,49 @@ +--- +description: "Feature-First architecture conventions for {{PROJECT_NAME}}" +alwaysApply: true +--- + +# Feature-First Architecture — {{PROJECT_NAME}} + +## Folder structure +``` +lib/ + features/ + auth/ + auth_screen.dart + auth_provider.dart (or auth_bloc.dart) + auth_repository.dart + auth_model.dart + widgets/ + login_form.dart + social_login_button.dart + home/ + home_screen.dart + home_provider.dart + ... + core/ + di/ ← Dependency injection setup + network/ ← HTTP client, interceptors + storage/ ← Local storage abstraction + widgets/ ← Shared widgets used by 3+ features + theme/ ← App theme, colors, text styles + routing/ ← Router config + shared/ + models/ ← Models shared across features + utils/ ← Pure utility functions +``` + +## Import rules (STRICTLY ENFORCED by arch-guard hook) +{{ARCH_IMPORT_RULES}} + +## Feature isolation rules +- A feature folder is self-contained: its own screen, state, model, repository +- Features MUST NOT import from other feature folders +- Shared code MUST be extracted to `core/` or `shared/` before sharing +- The 3-feature rule: if 3+ features need the same widget → move it to `core/widgets/` + +## File naming within a feature +- `[feature]_screen.dart` — the main screen widget +- `[feature]_provider.dart` — Riverpod providers (or `[feature]_bloc.dart`) +- `[feature]_repository.dart` — data fetching + caching +- `[feature]_model.dart` — data model (Freezed or plain Dart) diff --git a/flutter-cursor-templates/generator/templates/rules/architecture/layered.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/architecture/layered.mdc.tmpl new file mode 100644 index 0000000..351389c --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/architecture/layered.mdc.tmpl @@ -0,0 +1,17 @@ +--- +description: "Layered architecture conventions for {{PROJECT_NAME}}" +alwaysApply: true +--- + +# Layered Architecture — {{PROJECT_NAME}} + +## Layers (top → bottom) +``` +Presentation → Service/BLoC → Repository → Data Source +``` + +## Rules +- Dependencies flow downward only — upper layers depend on lower layers +- Each layer communicates via interfaces (abstract classes) +- Data transformations happen at layer boundaries (DTOs → domain models) +{{ARCH_IMPORT_RULES}} diff --git a/flutter-cursor-templates/generator/templates/rules/architecture/mvc.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/architecture/mvc.mdc.tmpl new file mode 100644 index 0000000..222eb87 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/architecture/mvc.mdc.tmpl @@ -0,0 +1,20 @@ +--- +description: "MVC architecture conventions for {{PROJECT_NAME}}" +alwaysApply: true +--- + +# MVC Architecture — {{PROJECT_NAME}} + +## Layer responsibilities +- **Model**: Data + business logic. Pure Dart. +- **View**: Renders UI, observes Controller state. No business logic. +- **Controller** (GetX): Connects Model ↔ View. Manages state transitions. + +## Import rules +{{ARCH_IMPORT_RULES}} + +## Controller rules +- Controllers are injected via `Binding`, never created in widgets +- One Controller per feature screen (not per widget) +- Controllers fetch data in `onInit()`, clean up in `onClose()` +- All reactive state marked with `.obs` diff --git a/flutter-cursor-templates/generator/templates/rules/architecture/mvvm.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/architecture/mvvm.mdc.tmpl new file mode 100644 index 0000000..be1b9a0 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/architecture/mvvm.mdc.tmpl @@ -0,0 +1,37 @@ +--- +description: "MVVM architecture conventions for {{PROJECT_NAME}}" +alwaysApply: true +--- + +# MVVM Architecture — {{PROJECT_NAME}} + +## Layer responsibilities +- **View** (Widget): Renders UI, dispatches user actions to ViewModel. Zero business logic. +- **ViewModel**: Holds UI state, calls Model/Repository, exposes streams/notifiers. Zero Flutter imports. +- **Model**: Plain Dart data structures + repository interfaces. + +## Import rules +{{ARCH_IMPORT_RULES}} + +## ViewModel rules +```dart +// ViewModel has NO Flutter imports — testable with plain dart test +class AuthViewModel extends ChangeNotifier { + final AuthRepository _repository; + AuthViewModel(this._repository); + + AuthViewState _state = const AuthViewState.initial(); + AuthViewState get state => _state; + + Future login(String email, String password) async { + _state = const AuthViewState.loading(); + notifyListeners(); + final result = await _repository.login(email, password); + _state = result.fold( + (error) => AuthViewState.error(error.message), + (user) => AuthViewState.success(user), + ); + notifyListeners(); + } +} +``` diff --git a/flutter-cursor-templates/generator/templates/rules/backend/firebase.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/backend/firebase.mdc.tmpl new file mode 100644 index 0000000..be75e9f --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/backend/firebase.mdc.tmpl @@ -0,0 +1,49 @@ +--- +description: "Firebase conventions for {{PROJECT_NAME}}" +alwaysApply: true +--- + +# Firebase Standards — {{PROJECT_NAME}} + +## Firestore +- Collection names: `camelCase` plural — `users`, `products`, `orderItems` +- Document IDs: use Firebase auto-IDs unless a natural key exists +- **Streams vs Futures**: use `snapshots()` for live data, `get()` for one-time reads +- Always handle `FirebaseException` explicitly — catch by `e.code` not generic `Exception` +- Paginate large collections with `startAfterDocument` — never fetch unbounded collections + +```dart +// ✅ Stream-based real-time listener +Stream> watchProducts() { + return _firestore.collection('products') + .where('isActive', isEqualTo: true) + .snapshots() + .map((snap) => snap.docs.map(Product.fromDoc).toList()); +} + +// ✅ Handle FirebaseException by code +try { + await _firestore.collection('orders').add(order.toMap()); +} on FirebaseException catch (e) { + switch (e.code) { + case 'permission-denied': throw AppError.authError('Insufficient permissions'); + case 'unavailable': throw AppError.networkError(); + default: throw AppError.unknown(e); + } +} +``` + +## Firebase Auth +- Always use `authStateChanges()` stream — never cache auth state locally +- Handle all error codes: `user-not-found`, `wrong-password`, `email-already-in-use`, `network-request-failed` +- Sign-out: clear all local state AND call `FirebaseAuth.instance.signOut()` + +## Cloud Functions +- Call via `FirebaseFunctions.instance.httpsCallable('functionName')` +- Handle `FirebaseFunctionsException` with `.code` and `.message` +- Never expose internal errors to client — functions return structured error responses + +## Security (complement to security-standards.mdc) +- Firestore Security Rules must be tested with the emulator before deploying +- No `allow read, write: if true` — even in development +- Rule coverage: every collection must have explicit rules diff --git a/flutter-cursor-templates/generator/templates/rules/backend/realtime.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/backend/realtime.mdc.tmpl new file mode 100644 index 0000000..2fc1669 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/backend/realtime.mdc.tmpl @@ -0,0 +1,25 @@ +--- +description: "Real-time feature conventions for {{PROJECT_NAME}}" +alwaysApply: true +--- + +# Real-time Features — {{PROJECT_NAME}} + +## Connection management +- Always expose a `connectionState` stream — UI must show "offline" indicator +- Implement exponential backoff for reconnection (1s, 2s, 4s, 8s, max 60s) +- Cancel all subscriptions in `dispose()` — memory leaks are the #1 bug in real-time apps + +## Offline-first strategy +- Cache last known state locally (Hive, Drift, or Isar) +- Show stale data with a "last updated" timestamp while reconnecting +- Queue mutations offline, replay on reconnect (use `connectivity_plus`) + +## WebSocket / SSE +- Use `web_socket_channel` for WebSocket — never raw `dart:io` WebSocket +- Implement heartbeat/ping to detect dead connections +- Parse and validate all incoming messages — never trust raw server data + +## UI indicators +- Show a persistent banner when offline: "You're offline — changes will sync when reconnected" +- Animate the banner away on reconnection — don't just hide it abruptly diff --git a/flutter-cursor-templates/generator/templates/rules/backend/rest.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/backend/rest.mdc.tmpl new file mode 100644 index 0000000..f108d26 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/backend/rest.mdc.tmpl @@ -0,0 +1,52 @@ +--- +description: "REST API conventions for {{PROJECT_NAME}}" +alwaysApply: true +--- + +# REST API Standards — {{PROJECT_NAME}} + +## HTTP client setup (Dio) +```dart +// In core/network/dio_client.dart +Dio createDioClient({required AppConfig config}) { + final dio = Dio(BaseOptions( + baseUrl: config.baseUrl, + connectTimeout: const Duration(seconds: 10), + receiveTimeout: const Duration(seconds: 30), + headers: {'Content-Type': 'application/json', 'Accept': 'application/json'}, + )); + + dio.interceptors.addAll([ + AuthInterceptor(tokenStorage: getIt()), + LoggingInterceptor(), // debug builds only + RetryInterceptor(dio), // 3 retries on network errors + ]); + return dio; +} +``` + +## DTOs (Data Transfer Objects) +- DTOs live in the `data/` layer — **never** pass raw JSON maps to the domain layer +- Use Freezed for DTOs if `codegen` includes `json_serializable` or `freezed` +- `fromJson` factory must handle null fields gracefully — API response fields are not guaranteed + +## Error handling +```dart +// Map HTTP errors → AppError domain types +AppError _mapDioError(DioException e) => switch (e.type) { + DioExceptionType.connectionTimeout => const NetworkError(statusCode: null), + DioExceptionType.receiveTimeout => const NetworkError(statusCode: null), + DioExceptionType.badResponse => _mapStatusCode(e.response?.statusCode), + DioExceptionType.connectionError => const NetworkError(statusCode: null), + _ => UnknownError(e), +}; +``` + +## API versioning +- Base URL includes version: `https://api.{{PROJECT_NAME}}.com/v1/` +- When upgrading API version, keep old version working until all clients migrate + +## Auth token interceptor +- Inject `Authorization: Bearer ` automatically on every request +- On 401: refresh token once, retry original request, then logout if refresh fails +- On 403: map to `AppError.authError('Insufficient permissions')` diff --git a/flutter-cursor-templates/generator/templates/rules/backend/supabase.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/backend/supabase.mdc.tmpl new file mode 100644 index 0000000..0cbcd7a --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/backend/supabase.mdc.tmpl @@ -0,0 +1,52 @@ +--- +description: "Supabase conventions for {{PROJECT_NAME}}" +alwaysApply: true +--- + +# Supabase Standards — {{PROJECT_NAME}} + +## Row Level Security (RLS) awareness +- **ALWAYS** assume RLS is enabled — never write queries that assume full table access +- Test queries in the Supabase dashboard before implementing in Flutter +- If a query returns empty unexpectedly, check RLS policies first + +## Queries +```dart +// ✅ Type-safe query with error handling +Future> getProducts() async { + final response = await _supabase + .from('products') + .select('id, name, price, category_id, categories(name)') + .eq('is_active', true) + .order('created_at', ascending: false) + .limit(50); + return response.map(Product.fromMap).toList(); +} +``` + +## Realtime subscriptions +```dart +StreamSubscription>>? _sub; + +void watchOrders(String userId) { + _sub = _supabase + .from('orders') + .stream(primaryKey: ['id']) + .eq('user_id', userId) + .listen( + (data) => _updateOrders(data), + onError: (e) => _handleError(e), + ); +} + +@override +void dispose() { + _sub?.cancel(); // ALWAYS cancel in dispose() + super.dispose(); +} +``` + +## Auth session +- Use `supabase.auth.onAuthStateChange` stream — never poll auth state +- Persist session: `supabase-flutter` handles this automatically via secure storage +- `session.accessToken` expires — check `session.isExpired` before sensitive operations diff --git a/flutter-cursor-templates/generator/templates/rules/codegen/codegen-freezed.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/codegen/codegen-freezed.mdc.tmpl new file mode 100644 index 0000000..757850f --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/codegen/codegen-freezed.mdc.tmpl @@ -0,0 +1,64 @@ +--- +description: "Freezed code generation conventions for {{PROJECT_NAME}} — Pillar 4" +alwaysApply: true +--- + +# Freezed Standards — {{PROJECT_NAME}} + +## When to use Freezed +- All domain entities and data models — use `@freezed` +- Union types / sealed classes — use `@freezed` with multiple constructors +- BLoC states and events — use `@freezed` + +## Model pattern +```dart +@freezed +class Product with _$Product { + const factory Product({ + required String id, + required String name, + required double price, + @Default(0) int stockCount, + String? imageUrl, + }) = _Product; + + factory Product.fromJson(Map json) => _$ProductFromJson(json); +} +``` + +## Union type pattern +```dart +@freezed +sealed class ProductState with _$ProductState { + const factory ProductState.initial() = ProductInitial; + const factory ProductState.loading() = ProductLoading; + const factory ProductState.loaded(List products) = ProductLoaded; + const factory ProductState.error(String message) = ProductError; +} + +// Usage — exhaustive switch +final widget = state.when( + initial: () => const SizedBox.shrink(), + loading: () => const ProductShimmer(), + loaded: (products) => ProductList(products: products), + error: (msg) => ErrorWidget(message: msg), +); +``` + +## Critical rules +- **NEVER** edit `.freezed.dart` or `.g.dart` files — they are generated +- Run `dart run build_runner build --delete-conflicting-outputs` after changes +- Run `dart run build_runner watch` during development +- Add `*.freezed.dart` and `*.g.dart` to `.gitignore` (or commit them — team must decide) +- Always define `copyWith` via Freezed — never write manual `copyWith` + +## Patterns to avoid +```dart +// ❌ Manual copyWith — replaced by Freezed +Product copyWith({String? name, double? price}) => Product( + id: id, name: name ?? this.name, price: price ?? this.price, +); + +// ✅ Let Freezed generate it +product.copyWith(name: 'New Name', price: 9.99) +``` diff --git a/flutter-cursor-templates/generator/templates/rules/codegen/codegen-injectable.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/codegen/codegen-injectable.mdc.tmpl new file mode 100644 index 0000000..6550292 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/codegen/codegen-injectable.mdc.tmpl @@ -0,0 +1,42 @@ +--- +description: "Injectable dependency injection conventions for {{PROJECT_NAME}} — Pillar 4" +alwaysApply: true +--- + +# Injectable (get_it) Standards — {{PROJECT_NAME}} + +## Setup +```dart +// lib/core/di/injection.dart +@InjectableInit() +void configureDependencies() => getIt.init(); +``` + +## Annotations +```dart +@singleton // One instance for app lifetime +@lazySingleton // Created on first access (preferred for most services) +@injectable // New instance each time (use sparingly) +@factoryMethod // Custom factory logic +``` + +## Example +```dart +@lazySingleton +class ProductRepository { + final DioClient _client; + const ProductRepository(this._client); // Constructor injection +} + +@injectable +class GetProductsUseCase { + final ProductRepository _repo; + const GetProductsUseCase(this._repo); +} +``` + +## Rules +- Run `dart run build_runner build` after adding/modifying `@injectable` annotations +- **NEVER** use `getIt()` in widget `build()` methods — inject via constructor or provider +- Use `@module` for third-party registrations (Dio, SharedPreferences, etc.) +- Register environment-specific implementations with `@Environment('dev')` / `@Environment('prod')` diff --git a/flutter-cursor-templates/generator/templates/rules/codegen/codegen-json_serializable.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/codegen/codegen-json_serializable.mdc.tmpl new file mode 100644 index 0000000..b0f5a4e --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/codegen/codegen-json_serializable.mdc.tmpl @@ -0,0 +1,37 @@ +--- +description: "json_serializable conventions for {{PROJECT_NAME}} — Pillar 4" +alwaysApply: true +--- + +# json_serializable Standards — {{PROJECT_NAME}} + +## Model annotation +```dart +@JsonSerializable(explicitToJson: true) // explicitToJson for nested objects +class ProductDto { + final String id; + final String name; + @JsonKey(name: 'unit_price') // snake_case API → camelCase Dart + final double unitPrice; + @JsonKey(defaultValue: false) + final bool isActive; + final DateTime createdAt; // auto-converted from ISO 8601 string + + const ProductDto({ + required this.id, + required this.name, + required this.unitPrice, + required this.isActive, + required this.createdAt, + }); + + factory ProductDto.fromJson(Map json) => _$ProductDtoFromJson(json); + Map toJson() => _$ProductDtoToJson(this); +} +``` + +## Critical rules +- **NEVER** edit `*.g.dart` files +- Use `@JsonKey(defaultValue: ...)` for nullable API fields — API contracts change +- Use `explicitToJson: true` whenever the model has nested objects +- Null safety: API fields not guaranteed to be non-null should be `String?` not `String` diff --git a/flutter-cursor-templates/generator/templates/rules/codegen/codegen-retrofit.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/codegen/codegen-retrofit.mdc.tmpl new file mode 100644 index 0000000..4e10bd3 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/codegen/codegen-retrofit.mdc.tmpl @@ -0,0 +1,30 @@ +--- +description: "Retrofit (Dio) API client conventions for {{PROJECT_NAME}} — Pillar 4" +alwaysApply: true +--- + +# Retrofit Standards — {{PROJECT_NAME}} + +## API client definition +```dart +@RestApi() +abstract class ProductApiClient { + factory ProductApiClient(Dio dio, {String? baseUrl}) = _ProductApiClient; + + @GET('/products') + Future> getProducts(@Query('category') String? category); + + @GET('/products/{id}') + Future getProduct(@Path('id') String id); + + @POST('/products') + Future createProduct(@Body() CreateProductDto dto); +} +``` + +## Rules +- **NEVER** edit `*.g.dart` files +- Run `dart run build_runner build` after modifying API client +- All DTOs used in Retrofit must have `fromJson`/`toJson` (via `json_serializable` or Freezed) +- Handle `DioException` in the repository layer — never let it reach the presentation layer +- Use `@Headers({'Content-Type': 'application/json'})` at class level, not per-method diff --git a/flutter-cursor-templates/generator/templates/rules/error-handling/error-handling.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/error-handling/error-handling.mdc.tmpl new file mode 100644 index 0000000..80c433e --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/error-handling/error-handling.mdc.tmpl @@ -0,0 +1,67 @@ +--- +description: "Global error handling strategy for {{PROJECT_NAME}} — always applied" +alwaysApply: true +--- + +# Global Error Handling — {{PROJECT_NAME}} + +## Flutter error boundaries +Configure in `main.dart` — do this ONCE and never bypass it: + +```dart +void main() { + FlutterError.onError = (details) { + FlutterError.presentError(details); + // TODO: Send to crash reporter (Sentry/Firebase Crashlytics) + crashReporter.recordFlutterError(details); + }; + + PlatformDispatcher.instance.onError = (error, stack) { + // TODO: Send to crash reporter + crashReporter.recordError(error, stack, fatal: true); + return true; + }; + + runApp(const MyApp()); +} +``` + +## Error types hierarchy +Define a sealed class for domain errors — never throw raw exceptions in business logic: + +```dart +sealed class AppError { + const AppError(); +} +class NetworkError extends AppError { final int? statusCode; const NetworkError({this.statusCode}); } +class AuthError extends AppError { final String reason; const AuthError(this.reason); } +class NotFoundError extends AppError { final String resource; const NotFoundError(this.resource); } +class UnknownError extends AppError { final Object cause; const UnknownError(this.cause); } +``` + +## Repository layer +- Wrap ALL external calls in try/catch and return `Either` or `Result` +- NEVER let raw exceptions bubble to the presentation layer +- Log at the repository layer, not the UI layer + +## Presentation layer +- Every async widget MUST handle error state explicitly — no silent failures +- Show user-friendly error messages: map `AppError` subtype → readable string +- Provide a "Try again" action for recoverable errors (network, timeout) +- For fatal errors (auth expired), redirect to login — never show a dead screen + +## Crash reporting +- Integrate Sentry or Firebase Crashlytics before first TestFlight / Play beta +- Set `user` context on crash reporter after login (id only, no PII) +- Add `breadcrumbs` for key user actions to aid reproduction + +## Logging strategy +``` +Level | Use case +DEBUG | Development only (strip from release) +INFO | Key user flows (login, purchase, etc.) +WARNING | Recoverable errors, fallbacks used +ERROR | Unrecoverable errors, unexpected states +``` +- Use `logger` package — never bare `print()` +- Logger instance per class: `final _log = Logger('ClassName');` diff --git a/flutter-cursor-templates/generator/templates/rules/i18n/localization.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/i18n/localization.mdc.tmpl new file mode 100644 index 0000000..dff5105 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/i18n/localization.mdc.tmpl @@ -0,0 +1,54 @@ +--- +description: "Localization / i18n conventions for {{PROJECT_NAME}}" +alwaysApply: true +--- + +# Localization Standards — {{PROJECT_NAME}} + +## Supported locales: {{LOCALES_LIST}} + +## Setup +- Use Flutter's built-in `AppLocalizations` (generated from `.arb` files) +- ARB files: `lib/l10n/app_en.arb`, `lib/l10n/app_fr.arb`, etc. +- Never hardcode user-facing strings — always use `context.l10n.stringKey` + +## AppLocalizations access +```dart +// In widgets: +final l10n = AppLocalizations.of(context)!; +Text(l10n.welcomeMessage) // ✅ + +// Extension for convenience: +extension LocalizationX on BuildContext { + AppLocalizations get l10n => AppLocalizations.of(this)!; +} +Text(context.l10n.welcomeMessage) // ✅ +``` + +## ARB file format +```json +{ + "welcomeMessage": "Welcome back, {name}!", + "@welcomeMessage": { + "description": "Shown on home screen after login", + "placeholders": { + "name": { "type": "String", "example": "Alice" } + } + }, + "itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}", + "@itemCount": { + "placeholders": { + "count": { "type": "int" } + } + } +} +``` + +## Rules +- **NEVER** hardcode user-facing strings as string literals in widget files +- **Technical keys** (route paths, storage/analytics keys, JSON fields): one shared module (e.g. `lib/core/constants/app_strings.dart`) — no duplicated literals across features +- All new strings added to ALL locale ARB files simultaneously — broken translations break builds +- Use ICU message format for plurals and gendered strings +- Date/time formatting: use `intl` package `DateFormat` — never `date.toString()` +- Currency: use `NumberFormat.currency(locale: locale)` — never manual formatting +- RTL support: wrap with `Directionality` where needed; test with Arabic locale diff --git a/flutter-cursor-templates/generator/templates/rules/platform/platform-android.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/platform/platform-android.mdc.tmpl new file mode 100644 index 0000000..fa2ebe2 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/platform/platform-android.mdc.tmpl @@ -0,0 +1,34 @@ +--- +description: "Android-specific conventions for {{PROJECT_NAME}} — Pillar 4" +alwaysApply: true +--- + +# Android Platform Standards — {{PROJECT_NAME}} + +## Target SDK +- `compileSdkVersion` / `targetSdkVersion`: 34 (Android 14) minimum for new apps +- `minSdkVersion`: 21 (Android 5.0) unless brief specifies otherwise +- Update `android/app/build.gradle` when bumping target SDK + +## Permissions +- Declare only needed permissions in `AndroidManifest.xml` +- Runtime permissions: use `permission_handler` — never skip rationale step +- Android 13+: granular media permissions (`READ_MEDIA_IMAGES` not `READ_EXTERNAL_STORAGE`) +- Android 14+: `FOREGROUND_SERVICE_TYPE` required for foreground services + +## Adaptive icons +- Provide both foreground and background layers in `android/app/src/main/res/` +- Test on dark theme, coloured theme, and themed icons (Android 13+) + +## Deep links / App Links +- Verify domain ownership: `.well-known/assetlinks.json` on your server +- Test with: `adb shell am start -a android.intent.action.VIEW -d "https://{{PROJECT_NAME}}.com/products/123"` + +## ProGuard / R8 +- Obfuscation rules in `android/app/proguard-rules.pro` +- Keep rules for: Dio, Freezed models, `@JsonKey` annotated classes +- Test release build thoroughly — obfuscation can break reflection-based code + +## Notifications +- Create notification channels before showing any notification (Android 8+) +- Notification icons must be monochrome on Android 5+ diff --git a/flutter-cursor-templates/generator/templates/rules/platform/platform-desktop.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/platform/platform-desktop.mdc.tmpl new file mode 100644 index 0000000..172360f --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/platform/platform-desktop.mdc.tmpl @@ -0,0 +1,36 @@ +--- +description: "Flutter Desktop conventions for {{PROJECT_NAME}} — Pillar 4" +alwaysApply: true +--- + +# Flutter Desktop Standards — {{PROJECT_NAME}} + +## Window management +- Use `window_manager` package for window title, size, minimize/maximize controls +- Set minimum window size to prevent unusable UI: `windowManager.setMinimumSize(const Size(800, 600))` +- Remember window position/size across sessions using `shared_preferences` + +## Keyboard shortcuts +- All primary actions must have keyboard shortcuts +- Use `Shortcuts` widget + `Actions` widget for app-wide shortcuts +- Follow platform conventions: `Cmd+S` (macOS), `Ctrl+S` (Windows/Linux) for save + +## Context menus +- Right-click context menus: use `ContextMenuRegion` widget +- Desktop users expect right-click everywhere + +## Platform-specific behavior +```dart +if (Platform.isMacOS) { + // macOS: use mac_window_manager for traffic lights placement +} else if (Platform.isWindows) { + // Windows: custom title bar with min/max/close buttons +} else if (Platform.isLinux) { + // Linux: respect window manager decorations +} +``` + +## File system +- `path_provider` provides platform-appropriate directories +- `file_picker` for open/save dialogs — never hardcode paths +- Handle file permission errors gracefully (especially on macOS with sandboxing) diff --git a/flutter-cursor-templates/generator/templates/rules/platform/platform-ios.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/platform/platform-ios.mdc.tmpl new file mode 100644 index 0000000..4feea1f --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/platform/platform-ios.mdc.tmpl @@ -0,0 +1,35 @@ +--- +description: "iOS-specific conventions for {{PROJECT_NAME}} — Pillar 4" +alwaysApply: true +--- + +# iOS Platform Standards — {{PROJECT_NAME}} + +## Platform-specific imports +- Use `dart:io` checks: `if (Platform.isIOS)` for conditional code +- iOS-only plugins: declare in `pubspec.yaml` with platform filter +- **NEVER** call `dart:io` directly in shared code — use `platform_channel` or `universal_io` + +## iOS-specific requirements +- Minimum deployment target: iOS 13.0 (or as specified in `ios/Podfile`) +- Privacy manifests: `ios/Runner/PrivacyInfo.xcprivacy` — required for App Store since 2024 +- Required `Info.plist` keys before using: + - Camera: `NSCameraUsageDescription` + - Photo library: `NSPhotoLibraryUsageDescription` + - Location: `NSLocationWhenInUseUsageDescription` + - Notifications: handled via `permission_handler` + +## Push notifications (iOS) +- Configure APNs certificates in Xcode signing & capabilities +- Request permission with `permission_handler` — show rationale screen first +- Handle foreground vs background vs terminated app states separately +- Test on a physical device — iOS simulator does not support push + +## Safe area +- Always wrap root scaffold with `SafeArea` or use `MediaQuery.of(context).padding` +- Dynamic Island / notch: test on iPhone 14 Pro and iPhone 15 Pro simulators + +## App Store compliance +- Screenshot: use `ScreenshotController` to exclude sensitive screens +- Sign in with Apple: required if any third-party social login is offered +- IPv6 compatibility required — no IPv4-only network code diff --git a/flutter-cursor-templates/generator/templates/rules/platform/platform-web.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/platform/platform-web.mdc.tmpl new file mode 100644 index 0000000..0a5d01a --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/platform/platform-web.mdc.tmpl @@ -0,0 +1,45 @@ +--- +description: "Flutter Web conventions for {{PROJECT_NAME}} — Pillar 4" +alwaysApply: true +--- + +# Flutter Web Standards — {{PROJECT_NAME}} + +## Critical: No dart:io on web +- **NEVER** import `dart:io` in shared code — it crashes on web +- Use `dart:html` or `universal_io` for platform-specific I/O +- Use `path_provider` alternatives: `universal_html` for web file access +- Check: `kIsWeb` constant before any platform-specific code + +```dart +// ✅ Platform-safe +import 'package:universal_io/io.dart'; + +// ❌ Crashes on web +import 'dart:io'; +``` + +## Web rendering +- Choose renderer carefully: + - `canvaskit` — pixel-perfect, larger initial load, better for graphics + - `html` — smaller load, uses DOM elements, inconsistent rendering +- Set in `index.html`: `flutterWebRenderer: "canvaskit"` + +## PWA requirements +- Update `web/manifest.json`: name, icons (192×192, 512×512), theme_color +- Service worker: configure for offline caching of app shell +- Test with Chrome DevTools → Lighthouse → PWA audit + +## Web-specific rendering caveats +- `BackdropFilter` has limited support on `html` renderer +- `Canvas` operations differ between renderers — test both +- Text selection differs — use `SelectableText` not `Text` where appropriate +- Scrollbars appear automatically on web — style or hide with `ScrollbarTheme` + +## URL strategy +- Use `usePathUrlStrategy()` in `main.dart` for clean URLs (no `#`) +- Configure server to redirect all paths to `index.html` (SPA routing) + +## Performance +- Lazy-load routes — use `GoRouter` deferred loading +- Initial load budget: < 3MB (canvaskit) or < 1MB (html renderer) diff --git a/flutter-cursor-templates/generator/templates/rules/routing/auto_route.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/routing/auto_route.mdc.tmpl new file mode 100644 index 0000000..a09a2a0 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/routing/auto_route.mdc.tmpl @@ -0,0 +1,31 @@ +--- +description: "Auto Route conventions for {{PROJECT_NAME}}" +alwaysApply: true +--- + +# Auto Route Standards — {{PROJECT_NAME}} + +## Route definitions +```dart +@AutoRouterConfig() +class AppRouter extends $AppRouter { + @override + List get routes => [ + AutoRoute(page: HomeRoute.page, initial: true), + AutoRoute(page: ProductRoute.page, path: '/products/:id'), + AutoRoute(page: LoginRoute.page, guards: [AuthGuard]), + ]; +} +``` + +## Navigation +```dart +context.router.push(ProductRoute(id: product.id)); +context.router.pop(); +context.router.replace(HomeRoute()); +``` + +## Rules +- Always use typed `Route` classes — never string paths +- Guards implement `AutoRouteGuard` +- **NEVER** use `Navigator.push` directly diff --git a/flutter-cursor-templates/generator/templates/rules/routing/getx_nav.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/routing/getx_nav.mdc.tmpl new file mode 100644 index 0000000..ba14ab3 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/routing/getx_nav.mdc.tmpl @@ -0,0 +1,32 @@ +--- +description: "GetX Navigation conventions for {{PROJECT_NAME}}" +alwaysApply: true +--- + +# GetX Navigation — {{PROJECT_NAME}} + +## Named routes +```dart +// app_pages.dart — central route definitions +abstract class AppPages { + static const initial = Routes.home; + static final routes = [ + GetPage(name: Routes.home, page: () => const HomeView(), binding: HomeBinding()), + GetPage(name: Routes.product, page: () => const ProductView(), binding: ProductBinding()), + ]; +} + +// Navigate — always use named routes +Get.toNamed(Routes.product, arguments: product); // push +Get.offAllNamed(Routes.home); // replace stack +Get.back(); // pop +``` + +## Bindings +- Every route has a `Binding` class that creates and injects dependencies +- **NEVER** use `Get.put()` in a widget — only in Bindings +- Use `Get.lazyPut()` for deferred creation + +## Rules +- **NEVER** use `Navigator.push/pop` +- All route strings in `lib/core/routing/routes.dart` as constants diff --git a/flutter-cursor-templates/generator/templates/rules/routing/gorouter.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/routing/gorouter.mdc.tmpl new file mode 100644 index 0000000..f9e13d5 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/routing/gorouter.mdc.tmpl @@ -0,0 +1,61 @@ +--- +description: "GoRouter conventions for {{PROJECT_NAME}}" +alwaysApply: true +--- + +# GoRouter Standards — {{PROJECT_NAME}} + +## Typed routes (mandatory) +```dart +// Define typed routes — never use string paths directly in navigation calls +@TypedGoRoute(path: '/') +class HomeRoute extends GoRouteData { + const HomeRoute(); + @override Widget build(BuildContext ctx, GoRouterState state) => const HomeScreen(); +} + +@TypedGoRoute(path: '/products/:id') +class ProductRoute extends GoRouteData { + final String id; + const ProductRoute({required this.id}); + @override Widget build(BuildContext ctx, GoRouterState state) => ProductScreen(id: id); +} + +// Navigate with type safety +const ProductRoute(id: product.id).go(context); // ✅ +context.go('/products/${product.id}'); // ❌ — don't do this +``` + +## Auth guard +```dart +// Redirect logic in router config +redirect: (context, state) { + final isLoggedIn = ref.read(authProvider).isAuthenticated; + final isLoginRoute = state.matchedLocation == '/login'; + if (!isLoggedIn && !isLoginRoute) return '/login'; + if (isLoggedIn && isLoginRoute) return '/'; + return null; +}, +``` + +## Shell routes for bottom navigation +```dart +ShellRoute( + builder: (ctx, state, child) => MainScaffold(child: child), + routes: [ + GoRoute(path: '/home', builder: (_, __) => const HomeScreen()), + GoRoute(path: '/search', builder: (_, __) => const SearchScreen()), + GoRoute(path: '/profile', builder: (_, __) => const ProfileScreen()), + ], +) +``` + +## Deep links +- Register URL schemes in `Info.plist` (iOS) and `AndroidManifest.xml` +- Test deep links with: `adb shell am start -a android.intent.action.VIEW -d "app://{{PACKAGE_ID}}/products/123"` +- Handle `GoRouter.of(context).routerDelegate.currentConfiguration` for dynamic links + +## Rules +- **NEVER** use `Navigator.push/pop` — use `context.go()`, `context.push()`, `context.pop()` +- All routes declared in one file: `lib/core/routing/app_router.dart` +- `BlocProvider`s for route-level blocs created inside the `builder` of each route diff --git a/flutter-cursor-templates/generator/templates/rules/security/security-standards.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/security/security-standards.mdc.tmpl new file mode 100644 index 0000000..01211e2 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/security/security-standards.mdc.tmpl @@ -0,0 +1,109 @@ +--- +description: "Security standards for {{PROJECT_NAME}} — ALWAYS APPLIED on every file write" +alwaysApply: true +--- + +# Security Standards — {{PROJECT_NAME}} +> **Pillar 5**: Security is an always-on rule, not just a reactive agent. +> These rules apply to EVERY file write, regardless of feature or context. + +## Credential & secret management +- **NEVER** hardcode API keys, tokens, or secrets in source code +- **NEVER** commit `.env` files — use `.gitignore` and document required vars in `.env.example` +- API keys belong in: flavored `--dart-define` build args, or a secrets manager (e.g. AWS Secrets) +- Use `flutter_secure_storage` for tokens/credentials — **NEVER** `SharedPreferences` for sensitive data +- Obfuscation: enable `--obfuscate --split-debug-info=build/debug-symbols/` for release builds + +## Authentication & sessions +- JWT/session tokens: stored in `flutter_secure_storage`, never in `SharedPreferences` or local DB +- Implement token refresh with retry logic — never let a 401 show a raw error to the user +- Certificate pinning: required for production builds on {{AUTH}} — use `dio_pinning_interceptor` +- Biometric re-auth: require for any transaction > defined threshold + +## Data & privacy +- **NEVER** log PII (names, emails, phone numbers, addresses) to console or crash reporters +- Sanitize all user inputs before sending to backend +- Use `SensitiveWidget` wrapper to exclude screens from screenshots / app switcher thumbnails +- Comply with platform privacy requirements: iOS `NSPrivacyAccessedAPITypes`, Android permissions + +## Network security +- All API calls use HTTPS — no http:// in production +- Validate SSL certificates — never bypass with `badCertificateCallback: (_,_,_) => true` +- Add a timeout to every HTTP call: `connectTimeout: Duration(seconds: 10)` +- Rate-limit sensitive endpoints: auth, OTP, password reset + +## Dependency security +- Run `dart pub outdated` monthly — flag packages with known CVEs +- Never use a package with <100 pub points without explicit tech lead approval +- Pin critical security packages to exact versions in `pubspec.yaml` + +## Secure coding patterns +```dart +// ✅ Correct: secure token storage +final storage = FlutterSecureStorage(); +await storage.write(key: 'access_token', value: token); + +// ❌ Wrong: SharedPreferences for sensitive data +final prefs = await SharedPreferences.getInstance(); +prefs.setString('access_token', token); // NEVER do this + +// ✅ Correct: PII-safe logging +logger.info('User authenticated: userId=${user.id}'); // ok — id, not email +logger.debug('Payment processed: orderId=$orderId'); // ok — no card data + +// ❌ Wrong: PII in logs +logger.info('Login: email=${user.email}, phone=${user.phone}'); // NEVER + +// ✅ Correct: --dart-define for secrets (build arg, not in code) +// flutter build apk --dart-define=API_KEY=$API_KEY +const apiKey = String.fromEnvironment('API_KEY'); // acceptable + +// ✅ Correct: certificate pinning with Dio +(dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () { + final client = HttpClient(); + client.badCertificateCallback = (cert, host, port) => false; // strict + return client; +}; +``` + +## Deep linking & intent security +- Validate all incoming deep link parameters — never trust raw URL params +- Use `app_links` package for verified deep link handling +- Restrict URL schemes to known patterns — reject unknown schemes +- For OAuth callbacks: validate `state` parameter to prevent CSRF + +## Storage security +- Encrypt locally cached sensitive data using `hive` with `HiveAesCipher` +- Clear sensitive data from memory after use (set to null, trigger GC) +- Do NOT cache auth tokens in network layer (no `dio_cache_interceptor` for auth endpoints) +- Use `SecureRandom` for nonce/token generation — never `Random()` + +## Code obfuscation & binary protection +```yaml +# android/app/build.gradle — release config +buildTypes { + release { + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } +} +``` +```bash +# Flutter release build with obfuscation +flutter build apk --release --obfuscate --split-debug-info=build/debug-symbols/ +flutter build ipa --release --obfuscate --split-debug-info=build/debug-symbols/ +``` + +## Release checklist +Before every production release, verify: +- [ ] No hardcoded secrets (`grep -r "api_key\|secret\|password\|token" lib/`) +- [ ] Debug flags disabled (`kDebugMode` guards on all debug-only paths) +- [ ] Obfuscation enabled in release build config +- [ ] Certificate pinning active and tested on both platforms +- [ ] Crash reporter PII filters configured (Sentry `beforeSend`, Firebase `setConsentType`) +- [ ] `flutter_secure_storage` used for all tokens — no `SharedPreferences` for sensitive keys +- [ ] Network calls all HTTPS — scan for `http://` in lib/ +- [ ] Dependencies audited: `dart pub outdated --json | jq '.packages[] | select(.isDiscontinued)'` +- [ ] Deep link parameters validated +- [ ] App transport security / network security config reviewed diff --git a/flutter-cursor-templates/generator/templates/rules/state-management/bloc.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/state-management/bloc.mdc.tmpl new file mode 100644 index 0000000..36a91de --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/state-management/bloc.mdc.tmpl @@ -0,0 +1,64 @@ +--- +description: "BLoC / Cubit conventions for {{PROJECT_NAME}}" +alwaysApply: true +--- + +# BLoC / Cubit Standards — {{PROJECT_NAME}} + +## When to use BLoC vs Cubit +- **Cubit**: simple state with no meaningful event history (toggle, counter, pagination) +- **BLoC**: event-driven flows where event history or transitions matter (auth, checkout, form wizard) + +## Event and State classes +```dart +// Events — sealed class (exhaustive switch) +sealed class AuthEvent { const AuthEvent(); } +final class AuthLoginRequested extends AuthEvent { + final String email, password; + const AuthLoginRequested({required this.email, required this.password}); +} +final class AuthLogoutRequested extends AuthEvent { const AuthLogoutRequested(); } + +// States — sealed class, immutable +sealed class AuthState { const AuthState(); } +final class AuthInitial extends AuthState { const AuthInitial(); } +final class AuthLoading extends AuthState { const AuthLoading(); } +final class AuthAuthenticated extends AuthState { + final User user; + const AuthAuthenticated(this.user); +} +final class AuthUnauthenticated extends AuthState { const AuthUnauthenticated(); } +final class AuthFailure extends AuthState { + final String message; + const AuthFailure(this.message); +} +``` + +## BlocProvider placement +- Create `BlocProvider` at the **route level** (in {{ROUTING}} route definitions) +- `MultiBlocProvider` at route level for screens needing multiple blocs +- **NEVER** create `BlocProvider` inside a widget's `build()` method + +## Usage rules +- **NEVER** call `bloc.add()` inside `build()` — only in gesture callbacks or `initState()` +- Use `BlocConsumer` only when BOTH `listen` + `build` logic are needed +- Use `BlocSelector` when only a subset of state triggers a rebuild +- Every BLoC must override `close()` and cancel `StreamSubscription`s + +## BlocBuilder patterns +```dart +BlocBuilder( + builder: (context, state) => switch (state) { + AuthInitial() => const SizedBox.shrink(), + AuthLoading() => const LoadingIndicator(), + AuthAuthenticated(user: final u) => HomeScreen(user: u), + AuthUnauthenticated() => const LoginScreen(), + AuthFailure(message: final m) => ErrorScreen(message: m), + }, +) +``` + +## File locations in {{PROJECT_NAME}} +- `lib/features/[feature]/presentation/bloc/[feature]_bloc.dart` +- `lib/features/[feature]/presentation/bloc/[feature]_event.dart` +- `lib/features/[feature]/presentation/bloc/[feature]_state.dart` diff --git a/flutter-cursor-templates/generator/templates/rules/state-management/getx.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/state-management/getx.mdc.tmpl new file mode 100644 index 0000000..1993700 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/state-management/getx.mdc.tmpl @@ -0,0 +1,67 @@ +--- +description: "GetX conventions for {{PROJECT_NAME}} (legacy — migration available)" +alwaysApply: true +--- + +# GetX Standards — {{PROJECT_NAME}} +> ⚠️ This project uses GetX. See `migration-agent` for incremental migration to Riverpod. + +## Controller structure +```dart +class ProductsController extends GetxController { + final ProductRepository _repo; + ProductsController(this._repo); + + final RxList products = [].obs; + final RxBool isLoading = false.obs; + final Rx error = Rx(null); + + @override + void onInit() { + super.onInit(); + fetchProducts(); + } + + Future fetchProducts() async { + isLoading.value = true; + error.value = null; + try { + products.value = await _repo.getProducts(); + } catch (e) { + error.value = e.toString(); + } finally { + isLoading.value = false; + } + } +} +``` + +## View pattern +```dart +// Views extend GetView — never GetWidget or raw StatelessWidget +class ProductsView extends GetView { + @override + Widget build(BuildContext context) { + return Scaffold( + body: Obx(() { + if (controller.isLoading.value) return const ProductShimmer(); + if (controller.error.value != null) return ErrorWidget(controller.error.value!); + return ProductList(controller.products); + }), + ); + } +} +``` + +## Rules +- **NEVER** pass `BuildContext` into a controller +- Use `Binding` classes for dependency injection — never `Get.put()` in a widget +- Use `.obs` for all reactive state — never call `update()` on non-observable state +- Use `Get.find()` only in `Binding` classes, not in widgets +- **No business logic in Views** — controllers handle all logic + +## File locations in {{PROJECT_NAME}} +- `lib/features/[feature]/views/[feature]_view.dart` +- `lib/features/[feature]/controllers/[feature]_controller.dart` +- `lib/features/[feature]/bindings/[feature]_binding.dart` +- `lib/features/[feature]/models/[feature]_model.dart` diff --git a/flutter-cursor-templates/generator/templates/rules/state-management/hooks_riverpod.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/state-management/hooks_riverpod.mdc.tmpl new file mode 100644 index 0000000..5e4ea76 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/state-management/hooks_riverpod.mdc.tmpl @@ -0,0 +1,44 @@ +--- +description: "Hooks + Riverpod conventions for {{PROJECT_NAME}}" +alwaysApply: true +--- + +# Hooks + Riverpod Standards — {{PROJECT_NAME}} + +## Widget base classes +- `HookConsumerWidget` — when you need BOTH hooks and Riverpod providers +- `HookWidget` — when you need ONLY hooks (no Riverpod) +- `ConsumerWidget` — when you need ONLY Riverpod (no hooks) + +## Hook rules +```dart +class ProductSearchWidget extends HookConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + // Hooks at the TOP of build() — never inside conditions or loops + final searchQuery = useState(''); + final searchCtrl = useTextEditingController(); + final focusNode = useFocusNode(); + final debounced = useDebounced(searchQuery.value, const Duration(milliseconds: 300)); + + // Riverpod below hooks + final results = ref.watch(searchResultsProvider(debounced)); + + return // ... + } +} +``` + +## What goes in hooks vs providers +| Concern | Tool | +|---------|------| +| Local UI state (text controller, animation, focus) | `useState`, `useAnimationController` | +| Server/async state | Riverpod `AsyncNotifier` | +| Cross-widget/feature state | Riverpod providers | +| Lifecycle side effects | `useEffect` | + +## Rules +- **NEVER** call hooks inside `if`, `for`, or callbacks +- `useEffect` cleanup MUST return a dispose function +- Custom hooks: prefix with `use`, live in `lib/core/hooks/` +- Do not use `flutter_riverpod` `ref.watch` inside `useEffect` — use `useRef` + `ref.listen` diff --git a/flutter-cursor-templates/generator/templates/rules/state-management/riverpod.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/state-management/riverpod.mdc.tmpl new file mode 100644 index 0000000..0f9ad67 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/state-management/riverpod.mdc.tmpl @@ -0,0 +1,58 @@ +--- +description: "Riverpod conventions for {{PROJECT_NAME}}" +alwaysApply: true +--- + +# Riverpod Standards — {{PROJECT_NAME}} + +## Provider types +| Type | Use case | +|------|----------| +| `AsyncNotifier` | Async state from {{BACKEND}} | +| `Notifier` | Synchronous derived/local UI state | +| `StreamNotifier` | Real-time subscriptions | +| `@riverpod` function | Simple computed/derived values | + +## Code generation (mandatory) +```dart +// ✅ Always use @riverpod annotation +@riverpod +class AuthNotifier extends _$AuthNotifier { + @override + Future build() => ref.watch(authRepositoryProvider).currentUser(); + + Future login(String email, String password) async { + state = const AsyncLoading(); + state = await AsyncValue.guard( + () => ref.read(authRepositoryProvider).login(email, password), + ); + } +} + +// ❌ Never write manual provider declarations +final authProvider = StateNotifierProvider>( + (ref) => AuthNotifier(), +); // DON'T DO THIS +``` + +## Rules +- `ref.watch()` inside `build()` ONLY — **never** `ref.read()` inside `build()` +- `ref.read(provider.notifier).method()` for mutations in gesture handlers +- `ref.invalidate(provider)` to refresh — never manually reset state to `AsyncLoading()` +- Family providers for parameterized data: `productDetailsProvider(productId)` +- Providers scoped at feature level; core providers in `lib/core/di/` + +## AsyncValue in widgets +Every `AsyncValue` MUST handle all three states: +```dart +ref.watch(productsProvider).when( + data: (products) => ProductList(products: products), + loading: () => const ProductListShimmer(), // required + error: (e, _) => ErrorWidget(error: e), // required +) +``` + +## File locations in {{PROJECT_NAME}} +- `lib/features/[feature]/[feature]_provider.dart` (generated: `[feature]_provider.g.dart`) +- `lib/features/[feature]/[feature]_repository.dart` +- Run `dart run build_runner watch` during development diff --git a/flutter-cursor-templates/generator/templates/rules/testing/testing-bloc.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/testing/testing-bloc.mdc.tmpl new file mode 100644 index 0000000..874b5ff --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/testing/testing-bloc.mdc.tmpl @@ -0,0 +1,60 @@ +--- +description: "BLoC testing conventions for {{PROJECT_NAME}}" +alwaysApply: false +--- + +# BLoC Testing Standards — {{PROJECT_NAME}} + +## Test pattern (bloc_test) +```dart +// {{TEST_PATTERN}} + +void main() { + late AuthBloc authBloc; + late MockAuthRepository mockRepo; + + setUp(() { + mockRepo = MockAuthRepository(); + authBloc = AuthBloc(repository: mockRepo); + }); + + tearDown(() => authBloc.close()); + + group('AuthBloc', () { + blocTest( + 'emits [Loading, Authenticated] when login succeeds', + build: () { + when(() => mockRepo.login(any(), any())) + .thenAnswer((_) async => const Right(User(id: '1', email: 'test@test.com'))); + return authBloc; + }, + act: (bloc) => bloc.add(const AuthLoginRequested(email: 'test@test.com', password: 'pass')), + expect: () => [ + const AuthLoading(), + isA(), + ], + ); + + blocTest( + 'emits [Loading, Failure] when login fails', + build: () { + when(() => mockRepo.login(any(), any())) + .thenAnswer((_) async => const Left(AuthError('Invalid credentials'))); + return authBloc; + }, + act: (bloc) => bloc.add(const AuthLoginRequested(email: 'bad', password: 'bad')), + expect: () => [ + const AuthLoading(), + const AuthFailure('Invalid credentials'), + ], + ); + }); +} +``` + +## Rules +- Use `mocktail` for mocking — never `mockito` +- Every BLoC test file: `test/features/[feature]/[feature]_bloc_test.dart` +- Coverage requirement: all state transitions must be tested +- Use `Given/When/Then` naming in test descriptions +- Test error paths as thoroughly as success paths diff --git a/flutter-cursor-templates/generator/templates/rules/testing/testing-e2e-patrol.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/testing/testing-e2e-patrol.mdc.tmpl new file mode 100644 index 0000000..061d70f --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/testing/testing-e2e-patrol.mdc.tmpl @@ -0,0 +1,37 @@ +--- +description: "Patrol E2E testing conventions for {{PROJECT_NAME}}" +alwaysApply: false +--- + +# Patrol E2E Testing — {{PROJECT_NAME}} + +## Test structure +```dart +void main() { + patrolTest('User can complete checkout flow', ($) async { + await $.pumpWidgetAndSettle(const App()); + + // Login + await $(LoginScreen).waitUntilVisible(); + await $(#emailField).enterText('test@example.com'); + await $(#passwordField).enterText('password123'); + await $('Sign In').tap(); + + // Add to cart + await $(ProductCard).at(0).tap(); + await $('Add to Cart').tap(); + + // Checkout + await $('Cart').tap(); + await $('Checkout').tap(); + await $(CheckoutSuccessScreen).waitUntilVisible(); + }); +} +``` + +## Rules +- E2E tests in `integration_test/` — separate from unit tests +- Use `patrolTest` not `testWidgets` for E2E scenarios +- Tag tests with `@Tags(['slow'])` so CI can skip on PRs +- Run against real emulators/simulators, not mocked environments +- Test on minimum supported OS version for each platform diff --git a/flutter-cursor-templates/generator/templates/rules/testing/testing-getx.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/testing/testing-getx.mdc.tmpl new file mode 100644 index 0000000..6f172c0 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/testing/testing-getx.mdc.tmpl @@ -0,0 +1,40 @@ +--- +description: "GetX testing conventions for {{PROJECT_NAME}}" +alwaysApply: false +--- + +# GetX Testing Standards — {{PROJECT_NAME}} + +## Test pattern +```dart +void main() { + late ProductsController controller; + late MockProductRepository mockRepo; + + setUp(() { + mockRepo = MockProductRepository(); + Get.testMode = true; + controller = Get.put(ProductsController(mockRepo)); + }); + + tearDown(() => Get.deleteAll()); + + test('loads products on init', () async { + when(() => mockRepo.getProducts()).thenAnswer((_) async => [fakeProduct]); + await controller.fetchProducts(); + expect(controller.products, [fakeProduct]); + expect(controller.isLoading.value, false); + }); + + testWidgets('ProductsView shows shimmer while loading', (tester) async { + controller.isLoading.value = true; + await tester.pumpWidget(GetMaterialApp(home: ProductsView())); + expect(find.byType(ProductShimmer), findsOneWidget); + }); +} +``` + +## Rules +- Use `Get.testMode = true` in setUp +- Always call `Get.deleteAll()` in tearDown +- Wrap widget tests in `GetMaterialApp`, not `MaterialApp` diff --git a/flutter-cursor-templates/generator/templates/rules/testing/testing-riverpod.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/testing/testing-riverpod.mdc.tmpl new file mode 100644 index 0000000..0695aeb --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/testing/testing-riverpod.mdc.tmpl @@ -0,0 +1,56 @@ +--- +description: "Riverpod testing conventions for {{PROJECT_NAME}}" +alwaysApply: false +--- + +# Riverpod Testing Standards — {{PROJECT_NAME}} + +## Test pattern +```dart +// {{TEST_PATTERN}} + +void main() { + late ProviderContainer container; + late MockProductRepository mockRepo; + + setUp(() { + mockRepo = MockProductRepository(); + container = ProviderContainer(overrides: [ + productRepositoryProvider.overrideWithValue(mockRepo), + ]); + }); + + tearDown(() => container.dispose()); // ALWAYS dispose + + test('ProductsNotifier loads products successfully', () async { + when(() => mockRepo.getProducts()).thenAnswer((_) async => [fakeProduct]); + + final notifier = container.read(productsProvider.notifier); + await notifier.loadProducts(); + + final state = container.read(productsProvider); + expect(state, isA>>()); + expect(state.value, [fakeProduct]); + }); +} +``` + +## Widget tests with Riverpod +```dart +testWidgets('ProductScreen shows shimmer while loading', (tester) async { + await tester.pumpWidget( + ProviderScope( + overrides: [ + productsProvider.overrideWith((ref) => const AsyncLoading()), + ], + child: const MaterialApp(home: ProductScreen()), + ), + ); + expect(find.byType(ProductShimmer), findsOneWidget); +}); +``` + +## Rules +- **Never** use a real `ProviderScope` in unit tests — always use `ProviderContainer` with overrides +- `addTearDown(container.dispose)` in every test that creates a container +- Test all three `AsyncValue` states: loading, data, error diff --git a/flutter-cursor-templates/generator/templates/rules/universal/flutter-core.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/universal/flutter-core.mdc.tmpl new file mode 100644 index 0000000..bcafe6a --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/universal/flutter-core.mdc.tmpl @@ -0,0 +1,40 @@ +--- +description: "Core Flutter conventions for {{PROJECT_NAME}} — always applied" +alwaysApply: true +--- + +# Flutter Core Standards — {{PROJECT_NAME}} + +## Const and performance +- Use `const` constructors wherever possible — compile-time guarantee of no rebuild +- Prefer `const` widgets at the leaf level: `const SizedBox.shrink()`, `const Padding(...)` +- Never use `const` with mutable values; lint: `prefer_const_constructors` is enabled + +## Null safety +- Never use `!` (bang operator) unless you have a 100% safe runtime guarantee with a comment +- Prefer `??`, `?.`, and `if (x != null)` guards +- Use `required` for all non-nullable named parameters +- Never use `late` without a guarantee of initialisation before first access + +## Widget lifecycle +- Override `dispose()` in every `StatefulWidget` that uses controllers, streams, or timers +- Cancel `StreamSubscription` in `dispose()`, not in `didUpdateWidget` +- Use `WidgetsBinding.instance.addPostFrameCallback` for post-build logic, not `Future.delayed(Duration.zero)` + +## Naming conventions +- Files: `snake_case.dart` +- Classes: `PascalCase` +- Variables/functions: `camelCase` +- Constants: `kCamelCase` or `SCREAMING_SNAKE` for true compile-time constants +- Private members: `_camelCase` + +## Imports +- Order: dart: → package: → relative +- Use relative imports within a feature; absolute for cross-feature +- Never import a feature's internal files from outside that feature + +## Code quality +- Max function length: 40 lines. Extract widgets and helpers aggressively +- No `print()` in production code — use a logging package +- All `TODO:` comments must include a ticket number: `// TODO: PROJ-123 — fix this` +- Run `dart format` before every commit diff --git a/flutter-cursor-templates/generator/templates/rules/universal/project-context.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/universal/project-context.mdc.tmpl new file mode 100644 index 0000000..7d2f89c --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/universal/project-context.mdc.tmpl @@ -0,0 +1,64 @@ +--- +description: "Project context for {{PROJECT_NAME}} — always applied" +alwaysApply: true +--- + +# Project Context — {{PROJECT_NAME}} + +## Project identity +- **Name:** {{PROJECT_NAME}} +- **Package:** {{PACKAGE_ID}} +- **Description:** {{DESCRIPTION}} +- **Scale:** {{SCALE}} + +## Technology stack +- **State management:** {{STATE_MANAGEMENT}} +- **Architecture:** {{ARCHITECTURE}} +- **Routing:** {{ROUTING}} +- **Backend:** {{BACKEND}} +- **Auth:** {{AUTH}} +- **Platforms:** {{PLATFORMS_LIST}} +- **Code generation:** {{CODEGEN_LIST}} + +## Feature modules +{{FEATURES_LIST}} + +## Special capabilities +{{SPECIAL_FEATURES}} + +## Environments / flavors +- Flavors: {{FLAVORS_LIST}} +- CI/CD: {{CICD_TOOL}} + +## Design & API references +- Design source: {{DESIGN_SOURCE}} +- API docs: {{API_DOCS_FORMAT}} at `{{API_DOCS_PATH}}` + +## Code references +### Git repositories +{{GIT_REFS_BLOCK}} + +### Local paths +{{LOCAL_PATHS_BLOCK}} + +## Product UX / themes & roles +- **Theme variants:** {{THEME_SUMMARY}} +- **Roles:** {{ROLES_SUMMARY}} + +{{HIGH_CONTRAST_NOTE}} +## Reviews — which rule owns what +- **Theme, colors, typography, spacing/radius tokens** → `ui-ux-standards.mdc` (widgets read `Theme.of(context)` only) +- **User-visible copy & locales** → `localization.mdc` (ARB / `AppLocalizations`; no UI string literals) +- **Imports, structure, naming** → `flutter-core.mdc` + architecture rule + +## Architecture boundaries +{{ARCH_IMPORT_RULES}} + +## When generating code for this project +1. Always use {{STATE_MANAGEMENT}} patterns — never suggest alternatives +2. Always follow {{ARCHITECTURE}} folder structure +3. Always use {{ROUTING}} for navigation — never `Navigator.push` directly +4. Always target platforms: {{PLATFORMS_LIST}} +5. If code generation tools are used ({{CODEGEN_LIST}}), follow their conventions +6. Apply visuals only through theme (`ColorScheme`, `TextTheme`, `ThemeExtension`) — never ad-hoc colors/fonts in feature widgets +7. No user-facing string literals in widgets — l10n or shared constants per localization rule diff --git a/flutter-cursor-templates/generator/templates/rules/universal/ui-ux-standards.mdc.tmpl b/flutter-cursor-templates/generator/templates/rules/universal/ui-ux-standards.mdc.tmpl new file mode 100644 index 0000000..bfd07bb --- /dev/null +++ b/flutter-cursor-templates/generator/templates/rules/universal/ui-ux-standards.mdc.tmpl @@ -0,0 +1,48 @@ +--- +description: "UI/UX standards for {{PROJECT_NAME}} — always applied" +alwaysApply: true +--- + +# UI / UX Standards — {{PROJECT_NAME}} + +## Theme & design tokens (single source of truth) +- Define **one** light/dark `ThemeData` (and optional `ThemeExtension`s for brand spacing, radii, semantic colors). Feature code reads `Theme.of(context)` only. +- **Colors:** `colorScheme` / extensions — never hex/`Color(...)` literals in widgets except inside the theme definition file(s). +- **Typography:** `textTheme` / `primaryTextTheme` — never raw `TextStyle(fontSize:, fontFamily:)` in feature UI. +- **Spacing & shapes:** `ThemeExtension` or documented constants consumed consistently — avoid one-off magic numbers for padding/radius. + +## Loading states +- Every async operation MUST show a loading skeleton (shimmer), NOT a spinner unless < 300ms +- Use `shimmer` package with a shimmer that matches the final layout shape +- Never show a blank screen during loading — skeleton must fill the same space as the content + +## Empty states +- Every list/grid MUST have a distinct empty state widget: illustration + headline + CTA +- Empty state is different from error state — never reuse the same widget for both +- Empty state copy: positive framing ("No items yet — add your first one") + +## Error states +- Every async failure MUST show: error message + retry button +- Never swallow errors silently +- Error text: user-friendly, never expose stack traces or raw API messages + +## Navigation & transitions +- Use `IndexedStack` for bottom nav tabs — preserves scroll position +- Named routes only — never `Navigator.push(context, MaterialPageRoute(...))` +- Page transitions: use `CustomTransitionPage` with `FadeTransition` for modal sheets + +## Responsive layout +- Use `LayoutBuilder` or `MediaQuery` for breakpoints, not hardcoded pixel values +- Minimum touch target: 48×48 logical pixels (Material guideline) +- Test on 375px (iPhone SE) and 414px (iPhone Pro Max) widths minimum + +## Haptics +- Use `HapticFeedback.lightImpact()` on primary CTAs +- Use `HapticFeedback.selectionClick()` on toggle/checkbox interactions +- Never add haptics to destructive actions without confirmation + +## Accessibility +- All interactive widgets must have a `Semantics` label or `tooltip` +- Minimum contrast ratio: 4.5:1 (WCAG AA) +- Test with TalkBack / VoiceOver before each release +{{HIGH_CONTRAST_UX_LINE}} diff --git a/flutter-cursor-templates/generator/templates/skills/create-flavor/SKILL.md.tmpl b/flutter-cursor-templates/generator/templates/skills/create-flavor/SKILL.md.tmpl new file mode 100644 index 0000000..c4cdfa6 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/skills/create-flavor/SKILL.md.tmpl @@ -0,0 +1,18 @@ +# Create Build Flavor — {{PROJECT_NAME}} + +Creates a new build flavor/environment. Current flavors: {{FLAVORS_LIST}}. + +## Usage +``` +Create a new flavor called [flavor_name] +``` + +## What gets created +1. Android: new `productFlavors` block in `android/app/build.gradle` +2. iOS: new scheme + configuration in Xcode (instructions provided) +3. `lib/core/config/[flavor_name]_config.dart` +4. `.env.[flavor_name]` template +5. {{CICD_TOOL}} pipeline update + +## CI/CD: {{CICD_TOOL}} +Generate the pipeline config snippet for the new flavor in {{CICD_TOOL}} format. diff --git a/flutter-cursor-templates/generator/templates/skills/deploy/SKILL.md.tmpl b/flutter-cursor-templates/generator/templates/skills/deploy/SKILL.md.tmpl new file mode 100644 index 0000000..f1cc9e2 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/skills/deploy/SKILL.md.tmpl @@ -0,0 +1,21 @@ +# Deploy — {{PROJECT_NAME}} + +Guides through deployment to {{CICD_TOOL}} for any of the flavors: {{FLAVORS_LIST}}. + +## Usage +``` +Deploy [flavor] to [store/environment] +``` + +## {{CICD_TOOL}} pipeline +The AI will generate or update the {{CICD_RAW}} configuration file for: +- Build signing +- Running tests +- Distributing to Firebase App Distribution (beta) or App Store / Play Store (prod) + +## Pre-deploy checklist +- [ ] Version bump in `pubspec.yaml` +- [ ] Obfuscation enabled for prod: `--obfuscate --split-debug-info=build/debug-symbols/` +- [ ] No debug flags in production code +- [ ] Security checklist from `security-standards.mdc` passed +- [ ] `cursor_gen --validate` passes diff --git a/flutter-cursor-templates/generator/templates/skills/generate-api-client/SKILL.md.tmpl b/flutter-cursor-templates/generator/templates/skills/generate-api-client/SKILL.md.tmpl new file mode 100644 index 0000000..40cfe37 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/skills/generate-api-client/SKILL.md.tmpl @@ -0,0 +1,16 @@ +# Generate API Client — {{PROJECT_NAME}} + +Generates type-safe API clients from {{API_DOCS_FORMAT}} spec at `{{API_DOCS_PATH}}`. + +## Usage +``` +Generate API client for [endpoint or resource name] +``` + +## Generated files +1. **DTO** (`data/models/[resource]_dto.dart`) — request/response models with json_serializable +2. **DataSource** (`data/datasources/[resource]_remote_datasource.dart`) — Dio calls with error handling +3. **Repository impl** (`data/repositories/[resource]_repository_impl.dart`) + +## After generation +Run: `dart run build_runner build --delete-conflicting-outputs` diff --git a/flutter-cursor-templates/generator/templates/skills/generate-tests/SKILL.md.tmpl b/flutter-cursor-templates/generator/templates/skills/generate-tests/SKILL.md.tmpl new file mode 100644 index 0000000..80d3ffb --- /dev/null +++ b/flutter-cursor-templates/generator/templates/skills/generate-tests/SKILL.md.tmpl @@ -0,0 +1,41 @@ +# Generate Tests — {{PROJECT_NAME}} + +Generates comprehensive unit, widget, and integration tests for **{{STATE_MANAGEMENT}}** patterns. + +## Usage +``` +Generate tests for [ClassName or file path] +``` + +## Test generation process +1. Read the source file completely +2. Identify all testable units (public methods, state transitions, UI states) +3. Generate tests following this pattern: + ``` + {{TEST_PATTERN}} + ``` +4. Create mocks with `mocktail` for all dependencies +5. Place test file at `test/[mirror of source path]_test.dart` + +## Coverage targets +- Business logic (UseCases, Repositories, BLoC/Notifiers): **80% minimum** +- Widget tests: all three states (loading/error/data) must be tested +- E2E: only critical user flows + +## Test file structure +```dart +void main() { + // Setup + group('[ClassName]', () { + // Happy path tests + group('success cases', () { ... }); + // Error path tests + group('error cases', () { ... }); + // Edge cases + group('edge cases', () { ... }); + }); +} +``` + +## Naming convention +`'given [precondition], when [action], then [expected outcome]'` diff --git a/flutter-cursor-templates/generator/templates/skills/scaffold-feature/SKILL.md.tmpl b/flutter-cursor-templates/generator/templates/skills/scaffold-feature/SKILL.md.tmpl new file mode 100644 index 0000000..2c2dd61 --- /dev/null +++ b/flutter-cursor-templates/generator/templates/skills/scaffold-feature/SKILL.md.tmpl @@ -0,0 +1,69 @@ +# Scaffold Feature — {{PROJECT_NAME}} + +Scaffolds a complete new feature module following **{{ARCHITECTURE}}** architecture with **{{STATE_MANAGEMENT}}** state management. + +## Usage +``` +Create a feature called [feature_name] with [description] +``` + +## What gets generated + +### For {{ARCHITECTURE}} architecture: +The AI will create all necessary files for the `[feature_name]` feature following {{ARCHITECTURE}} patterns. + +### File structure to create: + +**Clean Architecture:** +``` +lib/features/[feature_name]/ + domain/ + entities/[feature_name].dart + repositories/[feature_name]_repository.dart + usecases/ + get_[feature_name]_usecase.dart + create_[feature_name]_usecase.dart + data/ + models/[feature_name]_dto.dart + datasources/[feature_name]_remote_datasource.dart + repositories/[feature_name]_repository_impl.dart + presentation/ + [state_files]/ ← based on {{STATE_MANAGEMENT}} + pages/[feature_name]_page.dart + widgets/ +``` + +**Feature-First:** +``` +lib/features/[feature_name]/ + [feature_name]_screen.dart + [feature_name]_provider.dart ← or [feature_name]_bloc.dart + [feature_name]_repository.dart + [feature_name]_model.dart + widgets/ +``` + +## State management boilerplate + +### For {{STATE_MANAGEMENT}}: +Generate the appropriate state management files with: +- Initial/loading/success/error states +- All necessary events (for BLoC) +- Repository connection +- Dependency injection registration + +## Steps the AI takes +1. Ask: "What is the feature name and brief description?" +2. Ask: "What data does this feature manage? (e.g., list of products, single user profile)" +3. Generate all files with correct imports and patterns +4. Add the feature to the DI container +5. Add the route to {{ROUTING}} router +6. Create a placeholder test file + +## Code generation +{{#if codegen_freezed}} +- Generate Freezed model: run `dart run build_runner build` after scaffolding +{{/if}} +{{#if codegen_injectable}} +- Register in injectable: add `@lazySingleton` to repository +{{/if}} diff --git a/flutter-cursor-templates/generator/templates/skills/scaffold-screen/SKILL.md.tmpl b/flutter-cursor-templates/generator/templates/skills/scaffold-screen/SKILL.md.tmpl new file mode 100644 index 0000000..d5c6acd --- /dev/null +++ b/flutter-cursor-templates/generator/templates/skills/scaffold-screen/SKILL.md.tmpl @@ -0,0 +1,61 @@ +# Scaffold Screen — {{PROJECT_NAME}} + +Creates a complete screen widget with all states handled, following **{{STATE_MANAGEMENT}}** patterns. + +## Usage +``` +Create a screen for [screen_name] that shows [content description] +``` + +## Generated screen template + +### {{STATE_MANAGEMENT}} screen pattern: + +**BLoC:** +```dart +class [ScreenName]Screen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocBuilder<[Feature]Bloc, [Feature]State>( + builder: (context, state) => switch (state) { + [Feature]Initial() || [Feature]Loading() => const [ScreenName]Shimmer(), + [Feature]Loaded(:final data) => [ScreenName]Content(data: data), + [Feature]Empty() => const [ScreenName]EmptyState(), + [Feature]Error(:final message) => [ScreenName]ErrorState( + message: message, + onRetry: () => context.read<[Feature]Bloc>().add(const [Feature]LoadRequested()), + ), + }, + ); + } +} +``` + +**Riverpod:** +```dart +class [ScreenName]Screen extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch([feature]Provider); + return state.when( + loading: () => const [ScreenName]Shimmer(), + data: (data) => [ScreenName]Content(data: data), + error: (e, _) => [ScreenName]ErrorState(error: e), + ); + } +} +``` + +## Required sub-widgets to generate +1. `[ScreenName]Shimmer` — skeleton loading layout matching final content +2. `[ScreenName]EmptyState` — illustration + headline + CTA button +3. `[ScreenName]ErrorState` — error message + retry button +4. `[ScreenName]Content` — the actual data display + +## Platform considerations ({{PLATFORMS_LIST}}) +{{#if platform_web}} +- Web: ensure no dart:io usage; test at 375px and 1280px widths +{{/if}} +{{#if platform_desktop}} +- Desktop: add keyboard shortcut support for primary actions +{{/if}} diff --git a/flutter-cursor-templates/generator/test/generator_test.dart b/flutter-cursor-templates/generator/test/generator_test.dart index 38b69e8..7ee4789 100644 --- a/flutter-cursor-templates/generator/test/generator_test.dart +++ b/flutter-cursor-templates/generator/test/generator_test.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'package:test/test.dart'; -import '../src/brief_loader.dart'; import '../src/resolver.dart'; import '../src/renderer.dart'; import '../src/validator.dart'; @@ -12,343 +11,452 @@ import '../src/models.dart'; // ─── Test fixtures ───────────────────────────────────────────────────────── final _blocCleanBrief = ProjectBrief( - projectName: 'TestApp', - packageId: 'com.test.testapp', - description: 'Test app for golden tests', - scale: 'medium', + projectName: 'TestApp', + packageId: 'com.test.testapp', + description: 'Test app for golden tests', + scale: 'medium', stateManagement: 'bloc', - routing: 'gorouter', - architecture: 'clean', - backends: ['firebase'], - auth: 'firebase_auth', - platforms: ['ios', 'android'], - codegenTools: ['freezed'], - flavors: ['dev', 'prod'], - cicd: 'github_actions', - testingDepth: 'unit_widget', - e2eTool: 'patrol', - designSource: 'none', - figmaUrl: '', - apiDocsFormat: 'none', - apiDocsPath: '', - referenceRepos: [], - localPaths: [], - featureModules: ['auth', 'home', 'products'], + routing: 'gorouter', + architecture: 'clean', + backends: ['firebase'], + auth: 'firebase_auth', + platforms: ['ios', 'android'], + codegenTools: ['freezed'], + flavors: ['dev', 'prod'], + cicd: 'github_actions', + testingDepth: 'unit_widget', + e2eTool: 'patrol', + designSource: 'none', + figmaUrl: '', + apiDocsFormat: 'none', + apiDocsPath: '', + referenceRepos: [], + localPaths: [], + featureModules: ['auth', 'home', 'products'], specialFeatures: [], - i18nEnabled: false, - locales: ['en'], + i18nEnabled: false, + locales: ['en'], ); final _riverpodFFBrief = ProjectBrief( - projectName: 'TaskFlow', - packageId: 'com.test.taskflow', - description: 'Task management app', - scale: 'small', + projectName: 'TaskFlow', + packageId: 'com.test.taskflow', + description: 'Task management app', + scale: 'small', stateManagement: 'riverpod', - routing: 'gorouter', - architecture: 'feature_first', - backends: ['supabase'], - auth: 'supabase_auth', - platforms: ['ios', 'android', 'web'], - codegenTools: ['freezed', 'json_serializable'], - flavors: ['dev', 'prod'], - cicd: 'github_actions', - testingDepth: 'unit_widget', - e2eTool: 'patrol', - designSource: 'none', - figmaUrl: '', - apiDocsFormat: 'none', - apiDocsPath: '', - referenceRepos: [], - localPaths: [], - featureModules: ['auth', 'tasks', 'profile'], + routing: 'gorouter', + architecture: 'feature_first', + backends: ['supabase'], + auth: 'supabase_auth', + platforms: ['ios', 'android', 'web'], + codegenTools: ['freezed', 'json_serializable'], + flavors: ['dev', 'prod'], + cicd: 'github_actions', + testingDepth: 'unit_widget', + e2eTool: 'patrol', + designSource: 'none', + figmaUrl: '', + apiDocsFormat: 'none', + apiDocsPath: '', + referenceRepos: [], + localPaths: [], + featureModules: ['auth', 'tasks', 'profile'], specialFeatures: [], - i18nEnabled: true, - locales: ['en', 'fr'], + i18nEnabled: true, + locales: ['en', 'fr'], ); final _getxMvcBrief = ProjectBrief( - projectName: 'LegacyApp', - packageId: 'com.test.legacy', - description: 'Legacy GetX app', - scale: 'medium', + projectName: 'LegacyApp', + packageId: 'com.test.legacy', + description: 'Legacy GetX app', + scale: 'medium', stateManagement: 'getx', - routing: 'getx_nav', - architecture: 'mvc', - backends: ['rest'], - auth: 'jwt_rest', - platforms: ['ios', 'android'], - codegenTools: [], - flavors: ['dev', 'prod'], - cicd: 'codemagic', - testingDepth: 'unit_widget', - e2eTool: 'patrol', - designSource: 'none', - figmaUrl: '', - apiDocsFormat: 'openapi', - apiDocsPath: 'docs/api.yaml', - referenceRepos: [], - localPaths: [], - featureModules: ['auth', 'dashboard'], + routing: 'getx_nav', + architecture: 'mvc', + backends: ['rest'], + auth: 'jwt_rest', + platforms: ['ios', 'android'], + codegenTools: [], + flavors: ['dev', 'prod'], + cicd: 'codemagic', + testingDepth: 'unit_widget', + e2eTool: 'patrol', + designSource: 'none', + figmaUrl: '', + apiDocsFormat: 'openapi', + apiDocsPath: 'docs/api.yaml', + referenceRepos: [], + localPaths: [], + featureModules: ['auth', 'dashboard'], specialFeatures: [], - i18nEnabled: false, - locales: ['en'], + i18nEnabled: false, + locales: ['en'], ); void main() { // ─── Resolver tests ───────────────────────────────────────────────────────── -group('Resolver', () { - test('BLoC + Clean resolves correct files', () { - final files = Resolver.resolve(_blocCleanBrief); + group('Resolver', () { + test('BLoC + Clean resolves correct files', () { + final files = Resolver.resolve(_blocCleanBrief); - // Universal — always present - expect(files, contains('rules/universal/flutter-core')); - expect(files, contains('rules/universal/ui-ux-standards')); - expect(files, contains('rules/universal/project-context')); + // Universal — always present + expect(files, contains('rules/universal/flutter-core')); + expect(files, contains('rules/universal/ui-ux-standards')); + expect(files, contains('rules/universal/project-context')); - // Security — always present (Pillar 5) - expect(files, contains('rules/security/security-standards')); + // Security — always present (Pillar 5) + expect(files, contains('rules/security/security-standards')); - // Error handling — always present - expect(files, contains('rules/error-handling/error-handling')); + // Error handling — always present + expect(files, contains('rules/error-handling/error-handling')); - // Stack-specific - expect(files, contains('rules/state-management/bloc')); - expect(files, contains('rules/architecture/clean')); - expect(files, contains('rules/routing/gorouter')); - expect(files, contains('rules/backend/firebase')); + // Stack-specific + expect(files, contains('rules/state-management/bloc')); + expect(files, contains('rules/architecture/clean')); + expect(files, contains('rules/routing/gorouter')); + expect(files, contains('rules/backend/firebase')); - // Testing — SM-matched - expect(files, contains('rules/testing/testing-bloc')); + // Testing — SM-matched + expect(files, contains('rules/testing/testing-bloc')); - // Platforms (Pillar 4) - expect(files, contains('rules/platform/platform-ios')); - expect(files, contains('rules/platform/platform-android')); - expect(files, containsNot('rules/platform/platform-web')); + // Platforms (Pillar 4) + expect(files, contains('rules/platform/platform-ios')); + expect(files, contains('rules/platform/platform-android')); + expect(files, containsNot('rules/platform/platform-web')); - // Codegen (Pillar 4) - expect(files, contains('rules/codegen/codegen-freezed')); - expect(files, containsNot('rules/codegen/codegen-injectable')); + // Codegen (Pillar 4) + expect(files, contains('rules/codegen/codegen-freezed')); + expect(files, containsNot('rules/codegen/codegen-injectable')); - // Agents - expect(files, contains('agents/code-reviewer')); - expect(files, containsNot('agents/migration-agent')); // not GetX - expect(files, contains('agents/security-agent')); // firebase_auth triggers it + // Agents + expect(files, contains('agents/code-reviewer')); + expect(files, containsNot('agents/migration-agent')); // not GetX + expect(files, + contains('agents/security-agent')); // firebase_auth triggers it - // NOT included - expect(files, containsNot('rules/state-management/riverpod')); - expect(files, containsNot('rules/state-management/getx')); - expect(files, containsNot('rules/architecture/feature_first')); - expect(files, containsNot('rules/routing/getx_nav')); + // NOT included + expect(files, containsNot('rules/state-management/riverpod')); + expect(files, containsNot('rules/state-management/getx')); + expect(files, containsNot('rules/architecture/feature_first')); + expect(files, containsNot('rules/routing/getx_nav')); + }); + + test('Riverpod + Feature-First + Web resolves web platform template', () { + final files = Resolver.resolve(_riverpodFFBrief); + expect(files, contains('rules/platform/platform-web')); + expect(files, contains('rules/state-management/riverpod')); + expect(files, contains('rules/architecture/feature_first')); + expect(files, contains('rules/codegen/codegen-freezed')); + expect(files, contains('rules/codegen/codegen-json_serializable')); + expect(files, contains('rules/i18n/localization')); // i18nEnabled: true + expect(files, containsNot('agents/migration-agent')); + }); + + test('GetX + MVC includes migration-agent', () { + final files = Resolver.resolve(_getxMvcBrief); + expect(files, contains('agents/migration-agent')); + expect(files, contains('rules/state-management/getx')); + expect(files, contains('rules/architecture/mvc')); + expect(files, contains('agents/api-client-gen')); // rest backend + expect(files, + contains('skills/generate-api-client')); // apiDocsFormat != none + // Hooks are emitted only when stack.codegen is non-empty + expect(files, containsNot('hooks/hooks-json')); + expect(files, containsNot('hooks/flutter-analyze')); + }); + + test('Codegen stack includes Cursor hooks templates', () { + final files = Resolver.resolve(_blocCleanBrief); + expect(files, contains('hooks/hooks-json')); + expect(files, contains('hooks/arch-guard')); + }); + + test('Realtime special feature includes realtime template', () { + final brief = ProjectBrief( + projectName: 'RealtimeApp', + packageId: 'com.test.rt', + description: '', + scale: 'medium', + stateManagement: 'riverpod', + routing: 'gorouter', + architecture: 'feature_first', + backends: ['supabase'], + auth: 'supabase_auth', + platforms: ['ios'], + codegenTools: [], + flavors: ['dev'], + cicd: 'github_actions', + testingDepth: 'unit_widget', + e2eTool: 'patrol', + designSource: 'none', + figmaUrl: '', + apiDocsFormat: 'none', + apiDocsPath: '', + referenceRepos: [], + localPaths: [], + featureModules: [], + specialFeatures: ['realtime'], + i18nEnabled: false, + locales: ['en'], + ); + final files = Resolver.resolve(brief); + expect(files, contains('rules/backend/realtime')); + }); + + test('E2E testing depth includes e2e template', () { + final brief = ProjectBrief( + projectName: 'E2EApp', + packageId: 'com.test.e2e', + description: '', + scale: 'small', + stateManagement: 'bloc', + routing: 'gorouter', + architecture: 'clean', + backends: ['rest'], + auth: 'none', + platforms: ['ios'], + codegenTools: [], + flavors: ['dev'], + cicd: 'github_actions', + testingDepth: 'full', + e2eTool: 'patrol', + designSource: 'none', + figmaUrl: '', + apiDocsFormat: 'none', + apiDocsPath: '', + referenceRepos: [], + localPaths: [], + featureModules: [], + specialFeatures: [], + i18nEnabled: false, + locales: ['en'], + ); + final files = Resolver.resolve(brief); + expect(files, contains('rules/testing/testing-e2e-patrol')); + }); + + test('No duplicate files in resolved list', () { + final files = Resolver.resolve(_blocCleanBrief); + final unique = files.toSet(); + expect(files.length, equals(unique.length), + reason: 'Duplicate template files detected'); + }); }); - test('Riverpod + Feature-First + Web resolves web platform template', () { - final files = Resolver.resolve(_riverpodFFBrief); - expect(files, contains('rules/platform/platform-web')); - expect(files, contains('rules/state-management/riverpod')); - expect(files, contains('rules/architecture/feature_first')); - expect(files, contains('rules/codegen/codegen-freezed')); - expect(files, contains('rules/codegen/codegen-json_serializable')); - expect(files, contains('rules/i18n/localization')); // i18nEnabled: true - expect(files, containsNot('agents/migration-agent')); - }); - - test('GetX + MVC includes migration-agent', () { - final files = Resolver.resolve(_getxMvcBrief); - expect(files, contains('agents/migration-agent')); - expect(files, contains('rules/state-management/getx')); - expect(files, contains('rules/architecture/mvc')); - expect(files, contains('agents/api-client-gen')); // rest backend - expect(files, contains('skills/generate-api-client')); // apiDocsFormat != none - }); - - test('Realtime special feature includes realtime template', () { - final brief = ProjectBrief( - projectName: 'RealtimeApp', packageId: 'com.test.rt', description: '', - scale: 'medium', stateManagement: 'riverpod', routing: 'gorouter', - architecture: 'feature_first', backends: ['supabase'], auth: 'supabase_auth', - platforms: ['ios'], codegenTools: [], flavors: ['dev'], cicd: 'github_actions', - testingDepth: 'unit_widget', e2eTool: 'patrol', designSource: 'none', - figmaUrl: '', apiDocsFormat: 'none', apiDocsPath: '', - referenceRepos: [], localPaths: [], featureModules: [], - specialFeatures: ['realtime'], i18nEnabled: false, locales: ['en'], - ); - final files = Resolver.resolve(brief); - expect(files, contains('rules/backend/realtime')); - }); - - test('E2E testing depth includes e2e template', () { - final brief = ProjectBrief( - projectName: 'E2EApp', packageId: 'com.test.e2e', description: '', - scale: 'small', stateManagement: 'bloc', routing: 'gorouter', - architecture: 'clean', backends: ['rest'], auth: 'none', - platforms: ['ios'], codegenTools: [], flavors: ['dev'], cicd: 'github_actions', - testingDepth: 'full', e2eTool: 'patrol', designSource: 'none', - figmaUrl: '', apiDocsFormat: 'none', apiDocsPath: '', - referenceRepos: [], localPaths: [], featureModules: [], - specialFeatures: [], i18nEnabled: false, locales: ['en'], - ); - final files = Resolver.resolve(brief); - expect(files, contains('rules/testing/testing-e2e-patrol')); - }); - - test('No duplicate files in resolved list', () { - final files = Resolver.resolve(_blocCleanBrief); - final unique = files.toSet(); - expect(files.length, equals(unique.length), reason: 'Duplicate template files detected'); - }); -}); - // ─── Renderer / placeholder tests ─────────────────────────────────────────── -group('Renderer — placeholder substitution', () { - test('buildContext produces all required keys', () { - // Access via Renderer._buildContext (white-box test) - // Instead, verify rendered output has no unreplaced {{VAR}} patterns - // by checking a known template snippet - final context = { - 'PROJECT_NAME': 'TestApp', - 'PACKAGE_ID': 'com.test.testapp', - }; - final template = 'Project: {{PROJECT_NAME}} ({{PACKAGE_ID}})'; - final result = template - .replaceAll('{{PROJECT_NAME}}', 'TestApp') - .replaceAll('{{PACKAGE_ID}}', 'com.test.testapp'); - expect(result, equals('Project: TestApp (com.test.testapp)')); - expect(result, isNot(contains('{{'))); - }); + group('Renderer — placeholder substitution', () { + test('buildContext produces all required keys', () { + // Access via Renderer._buildContext (white-box test) + // Instead, verify rendered output has no unreplaced {{VAR}} patterns + // by checking a known template snippet + final context = { + 'PROJECT_NAME': 'TestApp', + 'PACKAGE_ID': 'com.test.testapp', + }; + final template = 'Project: {{PROJECT_NAME}} ({{PACKAGE_ID}})'; + final result = template + .replaceAll('{{PROJECT_NAME}}', context['PROJECT_NAME']!) + .replaceAll('{{PACKAGE_ID}}', context['PACKAGE_ID']!); + expect(result, equals('Project: TestApp (com.test.testapp)')); + expect(result, isNot(contains('{{'))); + }); - test('Rendered output has no unreplaced {{PLACEHOLDER}} patterns', () async { - final templateDir = _templateDir(); - if (!Directory(templateDir).existsSync()) { - markTestSkipped('Template directory not found at $templateDir — run from generator/'); - return; - } - - final rendered = await Renderer.render( - brief: _blocCleanBrief, - templateFiles: Resolver.resolve(_blocCleanBrief), - templateSrc: templateDir, - ); - - final unresolved = []; - for (final entry in rendered.entries) { - final matches = RegExp(r'\{\{[A-Z_]+\}\}').allMatches(entry.value); - for (final m in matches) { - unresolved.add('${entry.key}: ${m.group(0)}'); + test('Rendered output has no unreplaced {{PLACEHOLDER}} patterns', + () async { + final templateDir = _templateDir(); + if (!Directory(templateDir).existsSync()) { + markTestSkipped( + 'Template directory not found at $templateDir — run from generator/'); + return; } - } - expect(unresolved, isEmpty, reason: 'Unreplaced placeholders:\n${unresolved.join("\n")}'); + + final rendered = await Renderer.render( + brief: _blocCleanBrief, + templateFiles: Resolver.resolve(_blocCleanBrief), + templateSrc: templateDir, + ); + + final unresolved = []; + for (final entry in rendered.entries) { + final matches = RegExp(r'\{\{[A-Z_]+\}\}').allMatches(entry.value); + for (final m in matches) { + unresolved.add('${entry.key}: ${m.group(0)}'); + } + } + expect(unresolved, isEmpty, + reason: 'Unreplaced placeholders:\n${unresolved.join("\n")}'); + }); + + test('hooks/arch-guard renders real template content, not placeholder', + () async { + final templateDir = _templateDir(); + if (!Directory(templateDir).existsSync()) { + markTestSkipped( + 'Template directory not found at $templateDir — run from generator/'); + return; + } + + final rendered = await Renderer.render( + brief: _blocCleanBrief, + templateFiles: ['hooks/arch-guard'], + templateSrc: templateDir, + ); + + final content = rendered['hooks/arch-guard.ts']; + expect(content, isNotNull, reason: 'arch-guard.ts must be produced'); + expect(content, isNot(contains('Template not found')), + reason: + 'Default template dir should resolve real arch-guard.ts.tmpl'); + expect(content, contains('arch-guard')); + }); }); -}); // ─── Validator tests ───────────────────────────────────────────────────────── -group('Validator', () { - test('Valid brief passes validation', () async { - final result = await Validator.validate(_blocCleanBrief); - expect(result.isValid, isTrue); - expect(result.errors, isEmpty); - }); + group('Validator', () { + test('Valid brief passes validation', () async { + final result = await Validator.validate(_blocCleanBrief); + expect(result.isValid, isTrue); + expect(result.errors, isEmpty); + }); - test('Invalid state_management fails validation', () async { - final brief = ProjectBrief( - projectName: 'X', packageId: 'com.x.x', description: '', - scale: 'small', stateManagement: 'invalid_sm', routing: 'gorouter', - architecture: 'clean', backends: ['rest'], auth: 'none', - platforms: ['ios'], codegenTools: [], flavors: ['dev'], cicd: 'github_actions', - testingDepth: 'unit_widget', e2eTool: 'patrol', designSource: 'none', - figmaUrl: '', apiDocsFormat: 'none', apiDocsPath: '', - referenceRepos: [], localPaths: [], featureModules: [], - specialFeatures: [], i18nEnabled: false, locales: ['en'], - ); - final result = await Validator.validate(brief); - expect(result.isValid, isFalse); - expect(result.errors, anyElement(contains('invalid_sm'))); - }); + test('Invalid state_management fails validation', () async { + final brief = ProjectBrief( + projectName: 'X', + packageId: 'com.x.x', + description: '', + scale: 'small', + stateManagement: 'invalid_sm', + routing: 'gorouter', + architecture: 'clean', + backends: ['rest'], + auth: 'none', + platforms: ['ios'], + codegenTools: [], + flavors: ['dev'], + cicd: 'github_actions', + testingDepth: 'unit_widget', + e2eTool: 'patrol', + designSource: 'none', + figmaUrl: '', + apiDocsFormat: 'none', + apiDocsPath: '', + referenceRepos: [], + localPaths: [], + featureModules: [], + specialFeatures: [], + i18nEnabled: false, + locales: ['en'], + ); + final result = await Validator.validate(brief); + expect(result.isValid, isFalse); + expect(result.errors, anyElement(contains('invalid_sm'))); + }); - test('Missing project name fails validation', () async { - final brief = ProjectBrief( - projectName: '', packageId: 'com.x.x', description: '', - scale: 'small', stateManagement: 'riverpod', routing: 'gorouter', - architecture: 'clean', backends: ['rest'], auth: 'none', - platforms: ['ios'], codegenTools: [], flavors: ['dev'], cicd: 'github_actions', - testingDepth: 'unit_widget', e2eTool: 'patrol', designSource: 'none', - figmaUrl: '', apiDocsFormat: 'none', apiDocsPath: '', - referenceRepos: [], localPaths: [], featureModules: [], - specialFeatures: [], i18nEnabled: false, locales: ['en'], - ); - final result = await Validator.validate(brief); - expect(result.isValid, isFalse); - expect(result.errors, anyElement(contains('project.name'))); + test('Missing project name fails validation', () async { + final brief = ProjectBrief( + projectName: '', + packageId: 'com.x.x', + description: '', + scale: 'small', + stateManagement: 'riverpod', + routing: 'gorouter', + architecture: 'clean', + backends: ['rest'], + auth: 'none', + platforms: ['ios'], + codegenTools: [], + flavors: ['dev'], + cicd: 'github_actions', + testingDepth: 'unit_widget', + e2eTool: 'patrol', + designSource: 'none', + figmaUrl: '', + apiDocsFormat: 'none', + apiDocsPath: '', + referenceRepos: [], + localPaths: [], + featureModules: [], + specialFeatures: [], + i18nEnabled: false, + locales: ['en'], + ); + final result = await Validator.validate(brief); + expect(result.isValid, isFalse); + expect(result.errors, anyElement(contains('project.name'))); + }); }); -}); // ─── Golden file tests (Pillar 3) ─────────────────────────────────────────── -group('Golden file tests', () { - test('BLoC + Clean renders match golden files', () async { - final templateDir = _templateDir(); - if (!Directory(templateDir).existsSync()) { - markTestSkipped('Template dir not found'); return; - } - - final rendered = await Renderer.render( - brief: _blocCleanBrief, - templateFiles: Resolver.resolve(_blocCleanBrief), - templateSrc: templateDir, - ); - - for (final entry in rendered.entries) { - final goldenPath = 'test/golden/bloc-clean-firebase/${entry.key}'; - final goldenFile = File(goldenPath); - if (!goldenFile.existsSync()) { - // Create golden file on first run - await goldenFile.parent.create(recursive: true); - await goldenFile.writeAsString(entry.value); - print('Created golden: $goldenPath'); - } else { - final golden = await goldenFile.readAsString(); - expect(entry.value, equals(golden), - reason: 'Golden mismatch for ${entry.key}.\n' - 'Update goldens: dart test --update-goldens'); + group('Golden file tests', () { + test('BLoC + Clean renders match golden files', () async { + final templateDir = _templateDir(); + if (!Directory(templateDir).existsSync()) { + markTestSkipped('Template dir not found'); + return; } - } - }); - test('Riverpod + FF renders match golden files', () async { - final templateDir = _templateDir(); - if (!Directory(templateDir).existsSync()) { - markTestSkipped('Template dir not found'); return; - } - final rendered = await Renderer.render( - brief: _riverpodFFBrief, - templateFiles: Resolver.resolve(_riverpodFFBrief), - templateSrc: templateDir, - ); - _compareGoldens('test/golden/riverpod-ff-supabase', rendered); - }); + final rendered = await Renderer.render( + brief: _blocCleanBrief, + templateFiles: Resolver.resolve(_blocCleanBrief), + templateSrc: templateDir, + ); - test('GetX + MVC renders match golden files', () async { - final templateDir = _templateDir(); - if (!Directory(templateDir).existsSync()) { - markTestSkipped('Template dir not found'); return; - } - final rendered = await Renderer.render( - brief: _getxMvcBrief, - templateFiles: Resolver.resolve(_getxMvcBrief), - templateSrc: templateDir, - ); - _compareGoldens('test/golden/getx-mvc-rest', rendered); - }); -}); + for (final entry in rendered.entries) { + final goldenPath = 'test/golden/bloc-clean-firebase/${entry.key}'; + final goldenFile = File(goldenPath); + if (!goldenFile.existsSync()) { + // Create golden file on first run + await goldenFile.parent.create(recursive: true); + await goldenFile.writeAsString(entry.value); + print('Created golden: $goldenPath'); + } else { + final golden = await goldenFile.readAsString(); + expect(entry.value, equals(golden), + reason: 'Golden mismatch for ${entry.key}.\n' + 'Update goldens: dart test --update-goldens'); + } + } + }); + test('Riverpod + FF renders match golden files', () async { + final templateDir = _templateDir(); + if (!Directory(templateDir).existsSync()) { + markTestSkipped('Template dir not found'); + return; + } + final rendered = await Renderer.render( + brief: _riverpodFFBrief, + templateFiles: Resolver.resolve(_riverpodFFBrief), + templateSrc: templateDir, + ); + _compareGoldens('test/golden/riverpod-ff-supabase', rendered); + }); + + test('GetX + MVC renders match golden files', () async { + final templateDir = _templateDir(); + if (!Directory(templateDir).existsSync()) { + markTestSkipped('Template dir not found'); + return; + } + final rendered = await Renderer.render( + brief: _getxMvcBrief, + templateFiles: Resolver.resolve(_getxMvcBrief), + templateSrc: templateDir, + ); + _compareGoldens('test/golden/getx-mvc-rest', rendered); + }); + }); } // ─── Helpers ───────────────────────────────────────────────────────────────── -Future _compareGoldens(String goldenDir, Map rendered) async { +Future _compareGoldens( + String goldenDir, Map rendered) async { for (final entry in rendered.entries) { final goldenFile = File('$goldenDir/${entry.key}'); if (!goldenFile.existsSync()) { @@ -362,9 +470,18 @@ Future _compareGoldens(String goldenDir, Map rendered) asy } String _templateDir() { - // When running from generator/, go up one level to find templates/ - final script = Platform.script.toFilePath(); - return script.replaceAll(RegExp(r'test[/\\][^/\\]+$'), '../templates'); + // Walk up from CWD until we find the sibling templates/ directory. + // Works under `dart test` (snapshot CWD) and direct script execution alike. + var dir = Directory.current; + for (var i = 0; i < 5; i++) { + final candidate = Directory('${dir.path}/../templates'); + if (candidate.existsSync()) return candidate.path; + final inside = Directory('${dir.path}/templates'); + if (inside.existsSync()) return inside.path; + if (dir.parent.path == dir.path) break; + dir = dir.parent; + } + return '${Directory.current.path}/../templates'; } Matcher containsNot(dynamic expected) => isNot(contains(expected)); diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/agents/code-reviewer.mdc b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/agents/code-reviewer.mdc new file mode 100644 index 0000000..bf00c66 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/agents/code-reviewer.mdc @@ -0,0 +1,54 @@ +--- +name: code-reviewer +description: "Reviews TestApp code for BLoC / Cubit patterns, Clean Architecture boundaries, and Firebase usage. Ask: 'Review this code' or '@code-reviewer check PR'" +model: claude-opus-4-5 +context: auto +allowed-tools: [read_file, list_files] +--- + +You are a senior Flutter engineer reviewing code for **TestApp**. + +## Your review checklist + +### BLoC / Cubit patterns +- Are state classes immutable and sealed? +- Is state management correctly separated from UI logic? +- Are streams/subscriptions properly cancelled in dispose()? +- Check for anti-patterns specific to BLoC / Cubit + +### Clean Architecture boundaries +- presentation/ MUST NOT import from data/ +- domain/ MUST NOT import from data/ or presentation/ +- data/ CAN import from domain/ (implements interfaces) +- Use dependency injection to invert data → domain dependency +- Flag any violation of these import rules immediately + +### Firebase usage +- Are all exceptions caught and mapped to domain errors? +- Are streams vs futures used appropriately? +- Are connections/subscriptions disposed correctly? + +### Security (always check) +- No hardcoded API keys or secrets +- No PII logged to console or crash reporters +- Sensitive data using flutter_secure_storage, not SharedPreferences +- All user inputs validated before sending to backend + +### General Flutter +- `const` used where possible +- `dispose()` overridden for all controllers/subscriptions +- No `print()` in production paths +- Loading/empty/error states all handled + +## Output format +For each issue found: +``` +[SEVERITY: critical/major/minor] File:line — Issue description +WHY: Why this matters +FIX: Specific fix recommendation +``` + +Severity guide: +- **critical**: Security issue, data loss risk, crash potential +- **major**: Incorrect pattern, boundary violation, missing error handling +- **minor**: Style, naming, optimization opportunity diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/agents/security-agent.mdc b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/agents/security-agent.mdc new file mode 100644 index 0000000..5efa72a --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/agents/security-agent.mdc @@ -0,0 +1,47 @@ +--- +name: security-agent +description: "Deep security review for TestApp. Consult for auth flows, payment screens, and sensitive data handling. Ask: '@security-agent review auth flow'" +model: claude-opus-4-5 +context: fork +allowed-tools: [read_file, list_files] +--- + +You are a mobile security expert conducting a deep review for **TestApp**. + +> Note: This agent provides deep security analysis. +> The `security-standards.mdc` rule provides always-on enforcement. +> This agent is for detailed consultations on specific security concerns. + +## Deep review focus areas + +### Auth flow (Firebase Auth) +- Token storage: is `flutter_secure_storage` used for ALL tokens? +- Token refresh: is refresh handled atomically (no race condition)? +- Session expiry: does the app handle 401 gracefully without data loss? +- Certificate pinning: configured and tested? + +### Data at rest +- SQLite/Hive encryption: sensitive DBs encrypted? +- Cache poisoning: cached API responses validated before use? +- Keychain/Keystore usage for cryptographic keys + +### Network security +- All endpoints HTTPS — any http:// URLs? +- Certificate validation — any `badCertificateCallback: true`? +- Sensitive data in URL params/query strings? +- Request/response logging in production? (must be off) + +### Code injection risks +- Dynamic code execution patterns +- WebView usage — JavaScript interface security +- Deep link parameter validation (no path traversal) + +## Output format +For each finding: +``` +[RISK: Critical/High/Medium/Low] +LOCATION: File / function +ISSUE: Detailed description +CVSS-like impact: Confidentiality/Integrity/Availability +REMEDIATION: Specific code fix +``` diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/agents/test-writer.mdc b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/agents/test-writer.mdc new file mode 100644 index 0000000..1f9e226 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/agents/test-writer.mdc @@ -0,0 +1,33 @@ +--- +name: test-writer +description: "Writes BLoC / Cubit unit tests for TestApp. Ask: 'Write tests for [class]' or '@test-writer generate tests'" +model: claude-sonnet-4-20250514 +context: auto +allowed-tools: [read_file, write_file, list_files] +--- + +You are a Flutter test engineer for **TestApp** using **BLoC / Cubit**. + +## Test pattern to follow +```dart +blocTest(description, build: () => MyCubit(), act: (c) => c.method(), expect: () => [MyState()]) +``` + +## When asked to write tests: +1. Read the source file completely +2. Identify all public methods and state transitions +3. Write tests for: + - Happy path (successful operation) + - Error path (failure, exception handling) + - Edge cases (empty data, boundary values) +4. Use `mocktail` for all mocking +5. Follow `Given/When/Then` naming: `'given X, when Y, then emits Z'` + +## File placement +- Unit tests: `test/features/[feature]/[file]_test.dart` +- Widget tests: `test/features/[feature]/[screen]_widget_test.dart` +- Coverage target: 80% minimum for business logic classes + +## Output +Write the complete test file, ready to run. Include all imports. +After writing, run: `dart test path/to/test_file.dart` diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/agents/ui-validator.mdc b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/agents/ui-validator.mdc new file mode 100644 index 0000000..b0f5dc6 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/agents/ui-validator.mdc @@ -0,0 +1,47 @@ +--- +name: ui-validator +description: "Validates TestApp UI against design system and UX standards. Ask: 'Validate this screen' or '@ui-validator check'" +model: claude-sonnet-4-20250514 +context: auto +allowed-tools: [read_file, list_files] +--- + +You are a UI/UX validator for **TestApp**. + +## Validate every screen for: + +### State coverage (BLoC / Cubit) +- Loading state: shows shimmer (NOT spinner unless brief) +- Empty state: shows illustration + CTA (NOT blank screen) +- Error state: shows message + retry button (NOT toast-only) +- Data state: renders content correctly + +### Accessibility +- All interactive widgets have semantic labels +- Minimum touch targets: 48×48dp +- Sufficient color contrast (4.5:1 minimum) + +### Responsive layout +- No hardcoded pixel widths +- Tested at 375px and 414px viewport widths +- `SafeArea` used correctly on iOS + +### Platform-specific (ios, android) +{{#if web}} +- No dart:io imports in web-targeted code +- PWA-compatible (no native-only APIs without fallbacks) +{{/if}} +{{#if ios}} +- Safe area respected (notch, Dynamic Island) +- iOS Human Interface Guidelines followed +{{/if}} + +### Security (UI layer) +- No credentials shown in plaintext +- Sensitive screens wrapped with screenshot prevention + +## Output +List each violation with: +- Location (file:widget) +- What's wrong +- How to fix it diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/hooks/arch-guard.ts b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/hooks/arch-guard.ts new file mode 100644 index 0000000..8f24c83 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/hooks/arch-guard.ts @@ -0,0 +1,141 @@ +#!/usr/bin/env ts-node +// arch-guard.ts — Pre-commit hook: enforces Clean Architecture import boundaries +// Generated for TestApp (Clean Architecture architecture) + +import * as fs from 'fs'; +import * as path from 'path'; +import { execSync } from 'child_process'; + +const RED = '\x1b[31m'; +const YELLOW = '\x1b[33m'; +const GREEN = '\x1b[32m'; +const RESET = '\x1b[0m'; + +interface ArchRule { + sourcePattern: RegExp; + forbiddenImports: RegExp[]; + message: string; +} + +// Architecture-specific rules for Clean Architecture +const ARCH_RULES: ArchRule[] = [ + ...getArchRules() +]; + +function getArchRules(): ArchRule[] { + const arch = 'clean'; + + if (arch === 'clean') { + return [ + { + sourcePattern: /features\/\w+\/domain\//, + forbiddenImports: [/features\/\w+\/data\//, /features\/\w+\/presentation\//], + message: 'domain/ must not import from data/ or presentation/', + }, + { + sourcePattern: /features\/\w+\/presentation\//, + forbiddenImports: [/features\/\w+\/data\//], + message: 'presentation/ must not import from data/ directly (use domain interfaces)', + }, + ]; + } + + if (arch === 'feature_first') { + return [ + { + // A feature file must not import from another feature + sourcePattern: /features\/(\w+)\//, + forbiddenImports: [], // checked dynamically below + message: 'Features must not import from other feature folders', + }, + ]; + } + + if (arch === 'mvvm') { + return [ + { + sourcePattern: /viewmodels\//, + forbiddenImports: [/package:flutter\//, /widgets\//], + message: 'ViewModels must not import Flutter widgets — they must be plain Dart testable', + }, + ]; + } + + return []; +} + +function getChangedDartFiles(): string[] { + try { + const result = execSync('git diff --cached --name-only --diff-filter=ACM', { encoding: 'utf8' }); + return result.split('\n').filter(f => f.endsWith('.dart') && fs.existsSync(f)); + } catch { + return []; + } +} + +function checkFile(filePath: string): string[] { + const violations: string[] = []; + const content = fs.readFileSync(filePath, 'utf8'); + const imports = content.match(/^import\s+'[^']+'/gm) ?? []; + + for (const rule of ARCH_RULES) { + if (!rule.sourcePattern.test(filePath)) continue; + + // Feature-first cross-feature check + if ('clean' === 'feature_first') { + const srcFeatureMatch = filePath.match(/features\/(\w+)\//); + if (srcFeatureMatch) { + const srcFeature = srcFeatureMatch[1]; + for (const imp of imports) { + const impFeatureMatch = imp.match(/features\/(\w+)\//); + if (impFeatureMatch && impFeatureMatch[1] !== srcFeature) { + violations.push( + `${RED}ARCH VIOLATION${RESET} in ${filePath}:\n` + + ` ${YELLOW}${rule.message}${RESET}\n` + + ` Import: ${imp}\n` + + ` Fix: Move shared code to core/ or shared/` + ); + } + } + } + continue; + } + + // Standard forbidden import check + for (const imp of imports) { + for (const forbidden of rule.forbiddenImports) { + if (forbidden.test(imp)) { + violations.push( + `${RED}ARCH VIOLATION${RESET} in ${filePath}:\n` + + ` ${YELLOW}${rule.message}${RESET}\n` + + ` Import: ${imp}` + ); + } + } + } + } + + return violations; +} + +const changedFiles = getChangedDartFiles(); +if (changedFiles.length === 0) { + console.log(`${GREEN}✔ arch-guard: no Dart files changed${RESET}`); + process.exit(0); +} + +console.log(`🏛 arch-guard checking ${changedFiles.length} file(s) for Clean Architecture violations...`); + +const allViolations: string[] = []; +for (const file of changedFiles) { + allViolations.push(...checkFile(file)); +} + +if (allViolations.length > 0) { + console.error(`\n${RED}✘ Architecture boundary violations detected:${RESET}\n`); + for (const v of allViolations) console.error(v + '\n'); + console.error(`Total: ${allViolations.length} violation(s). Fix before committing.`); + process.exit(1); +} + +console.log(`${GREEN}✔ arch-guard: no violations found${RESET}`); diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/hooks/flutter-analyze.ts b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/hooks/flutter-analyze.ts new file mode 100644 index 0000000..a8918a7 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/hooks/flutter-analyze.ts @@ -0,0 +1,32 @@ +#!/usr/bin/env ts-node +// flutter-analyze.ts — Pre-commit hook: runs dart analyze and dart format check +import { execSync } from 'child_process'; + +const RED = '\x1b[31m'; +const GREEN = '\x1b[32m'; +const RESET = '\x1b[0m'; + +function run(cmd: string): { stdout: string; code: number } { + try { + const stdout = execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }); + return { stdout, code: 0 }; + } catch (e: any) { + return { stdout: e.stdout ?? e.message, code: e.status ?? 1 }; + } +} + +console.log('🔍 Running flutter analyze...'); +const analyze = run('flutter analyze --no-congratulate'); +if (analyze.code !== 0) { + console.error(`${RED}✘ flutter analyze failed:\n${analyze.stdout}${RESET}`); + process.exit(1); +} + +console.log('🎨 Checking dart format...'); +const format = run('dart format --output=none --set-exit-if-changed lib/ test/'); +if (format.code !== 0) { + console.error(`${RED}✘ Unformatted files detected. Run: dart format lib/ test/${RESET}`); + process.exit(1); +} + +console.log(`${GREEN}✔ flutter analyze + dart format passed${RESET}`); diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/hooks/grind-tests.ts b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/hooks/grind-tests.ts new file mode 100644 index 0000000..dd6385c --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/hooks/grind-tests.ts @@ -0,0 +1,16 @@ +#!/usr/bin/env ts-node +// grind-tests.ts — Pre-push hook: runs flutter test +import { execSync } from 'child_process'; + +const RED = '\x1b[31m'; +const GREEN = '\x1b[32m'; +const RESET = '\x1b[0m'; + +console.log('🧪 Running flutter test...'); +try { + execSync('flutter test --coverage', { stdio: 'inherit' }); + console.log(`${GREEN}✔ All tests passed${RESET}`); +} catch { + console.error(`${RED}✘ Tests failed. Fix failing tests before pushing.${RESET}`); + process.exit(1); +} diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/hooks/hooks.json b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/hooks/hooks.json new file mode 100644 index 0000000..e74d712 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/hooks/hooks.json @@ -0,0 +1,19 @@ +{ + "hooks": [ + { + "name": "pre-commit: flutter analyze", + "command": "npx ts-node .cursor/hooks/flutter-analyze.ts", + "events": ["pre-commit"] + }, + { + "name": "pre-commit: arch guard", + "command": "npx ts-node .cursor/hooks/arch-guard.ts", + "events": ["pre-commit"] + }, + { + "name": "pre-push: run tests", + "command": "npx ts-node .cursor/hooks/grind-tests.ts", + "events": ["pre-push"] + } + ] +} diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/architecture/clean.mdc b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/architecture/clean.mdc new file mode 100644 index 0000000..3ceaa46 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/architecture/clean.mdc @@ -0,0 +1,56 @@ +--- +description: "Clean Architecture conventions for TestApp" +alwaysApply: true +--- + +# Clean Architecture — TestApp + +## Layer structure +``` +lib/features/[feature]/ + ├── domain/ + │ ├── entities/ ← Pure Dart, no framework imports + │ ├── repositories/ ← Abstract interfaces only + │ └── usecases/ ← Single-responsibility business operations + ├── data/ + │ ├── datasources/ ← Remote (API) + Local (cache) implementations + │ ├── models/ ← DTOs with fromJson/toJson (can use Freezed) + │ └── repositories/ ← Implements domain/repositories interfaces + └── presentation/ + ├── bloc/ or notifiers/ + ├── pages/ + └── widgets/ +``` + +## Import rules (STRICTLY ENFORCED by arch-guard hook) +- presentation/ MUST NOT import from data/ +- domain/ MUST NOT import from data/ or presentation/ +- data/ CAN import from domain/ (implements interfaces) +- Use dependency injection to invert data → domain dependency + +## UseCase pattern +```dart +// One UseCase = one operation = one method +class GetProductsUseCase { + final ProductRepository _repository; + const GetProductsUseCase(this._repository); + + Future>> call(ProductFilter filter) => + _repository.getProducts(filter); +} +``` + +## Entity rules +- Entities are pure Dart — zero Flutter or framework imports +- Entities are immutable — use `final` fields + factory constructors +- Entities NEVER have `fromJson`/`toJson` — that belongs in the data layer model + +## Repository rules +- Domain defines the **interface** (abstract class) +- Data layer **implements** it +- Use `Either` or `Result` return types — never throw in domain + +## Dependency injection +- Use `injectable` + `get_it` if `codegen` includes `injectable` +- All UseCases injected into BLoC/Notifier via constructor +- `DataSource → Repository → UseCase → Bloc` dependency direction diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/backend/firebase.mdc b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/backend/firebase.mdc new file mode 100644 index 0000000..52f5d17 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/backend/firebase.mdc @@ -0,0 +1,49 @@ +--- +description: "Firebase conventions for TestApp" +alwaysApply: true +--- + +# Firebase Standards — TestApp + +## Firestore +- Collection names: `camelCase` plural — `users`, `products`, `orderItems` +- Document IDs: use Firebase auto-IDs unless a natural key exists +- **Streams vs Futures**: use `snapshots()` for live data, `get()` for one-time reads +- Always handle `FirebaseException` explicitly — catch by `e.code` not generic `Exception` +- Paginate large collections with `startAfterDocument` — never fetch unbounded collections + +```dart +// ✅ Stream-based real-time listener +Stream> watchProducts() { + return _firestore.collection('products') + .where('isActive', isEqualTo: true) + .snapshots() + .map((snap) => snap.docs.map(Product.fromDoc).toList()); +} + +// ✅ Handle FirebaseException by code +try { + await _firestore.collection('orders').add(order.toMap()); +} on FirebaseException catch (e) { + switch (e.code) { + case 'permission-denied': throw AppError.authError('Insufficient permissions'); + case 'unavailable': throw AppError.networkError(); + default: throw AppError.unknown(e); + } +} +``` + +## Firebase Auth +- Always use `authStateChanges()` stream — never cache auth state locally +- Handle all error codes: `user-not-found`, `wrong-password`, `email-already-in-use`, `network-request-failed` +- Sign-out: clear all local state AND call `FirebaseAuth.instance.signOut()` + +## Cloud Functions +- Call via `FirebaseFunctions.instance.httpsCallable('functionName')` +- Handle `FirebaseFunctionsException` with `.code` and `.message` +- Never expose internal errors to client — functions return structured error responses + +## Security (complement to security-standards.mdc) +- Firestore Security Rules must be tested with the emulator before deploying +- No `allow read, write: if true` — even in development +- Rule coverage: every collection must have explicit rules diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/codegen/codegen-freezed.mdc b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/codegen/codegen-freezed.mdc new file mode 100644 index 0000000..aa98a9b --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/codegen/codegen-freezed.mdc @@ -0,0 +1,64 @@ +--- +description: "Freezed code generation conventions for TestApp — Pillar 4" +alwaysApply: true +--- + +# Freezed Standards — TestApp + +## When to use Freezed +- All domain entities and data models — use `@freezed` +- Union types / sealed classes — use `@freezed` with multiple constructors +- BLoC states and events — use `@freezed` + +## Model pattern +```dart +@freezed +class Product with _$Product { + const factory Product({ + required String id, + required String name, + required double price, + @Default(0) int stockCount, + String? imageUrl, + }) = _Product; + + factory Product.fromJson(Map json) => _$ProductFromJson(json); +} +``` + +## Union type pattern +```dart +@freezed +sealed class ProductState with _$ProductState { + const factory ProductState.initial() = ProductInitial; + const factory ProductState.loading() = ProductLoading; + const factory ProductState.loaded(List products) = ProductLoaded; + const factory ProductState.error(String message) = ProductError; +} + +// Usage — exhaustive switch +final widget = state.when( + initial: () => const SizedBox.shrink(), + loading: () => const ProductShimmer(), + loaded: (products) => ProductList(products: products), + error: (msg) => ErrorWidget(message: msg), +); +``` + +## Critical rules +- **NEVER** edit `.freezed.dart` or `.g.dart` files — they are generated +- Run `dart run build_runner build --delete-conflicting-outputs` after changes +- Run `dart run build_runner watch` during development +- Add `*.freezed.dart` and `*.g.dart` to `.gitignore` (or commit them — team must decide) +- Always define `copyWith` via Freezed — never write manual `copyWith` + +## Patterns to avoid +```dart +// ❌ Manual copyWith — replaced by Freezed +Product copyWith({String? name, double? price}) => Product( + id: id, name: name ?? this.name, price: price ?? this.price, +); + +// ✅ Let Freezed generate it +product.copyWith(name: 'New Name', price: 9.99) +``` diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/error-handling/error-handling.mdc b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/error-handling/error-handling.mdc new file mode 100644 index 0000000..dd3a4d2 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/error-handling/error-handling.mdc @@ -0,0 +1,67 @@ +--- +description: "Global error handling strategy for TestApp — always applied" +alwaysApply: true +--- + +# Global Error Handling — TestApp + +## Flutter error boundaries +Configure in `main.dart` — do this ONCE and never bypass it: + +```dart +void main() { + FlutterError.onError = (details) { + FlutterError.presentError(details); + // TODO: Send to crash reporter (Sentry/Firebase Crashlytics) + crashReporter.recordFlutterError(details); + }; + + PlatformDispatcher.instance.onError = (error, stack) { + // TODO: Send to crash reporter + crashReporter.recordError(error, stack, fatal: true); + return true; + }; + + runApp(const MyApp()); +} +``` + +## Error types hierarchy +Define a sealed class for domain errors — never throw raw exceptions in business logic: + +```dart +sealed class AppError { + const AppError(); +} +class NetworkError extends AppError { final int? statusCode; const NetworkError({this.statusCode}); } +class AuthError extends AppError { final String reason; const AuthError(this.reason); } +class NotFoundError extends AppError { final String resource; const NotFoundError(this.resource); } +class UnknownError extends AppError { final Object cause; const UnknownError(this.cause); } +``` + +## Repository layer +- Wrap ALL external calls in try/catch and return `Either` or `Result` +- NEVER let raw exceptions bubble to the presentation layer +- Log at the repository layer, not the UI layer + +## Presentation layer +- Every async widget MUST handle error state explicitly — no silent failures +- Show user-friendly error messages: map `AppError` subtype → readable string +- Provide a "Try again" action for recoverable errors (network, timeout) +- For fatal errors (auth expired), redirect to login — never show a dead screen + +## Crash reporting +- Integrate Sentry or Firebase Crashlytics before first TestFlight / Play beta +- Set `user` context on crash reporter after login (id only, no PII) +- Add `breadcrumbs` for key user actions to aid reproduction + +## Logging strategy +``` +Level | Use case +DEBUG | Development only (strip from release) +INFO | Key user flows (login, purchase, etc.) +WARNING | Recoverable errors, fallbacks used +ERROR | Unrecoverable errors, unexpected states +``` +- Use `logger` package — never bare `print()` +- Logger instance per class: `final _log = Logger('ClassName');` diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/platform/platform-android.mdc b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/platform/platform-android.mdc new file mode 100644 index 0000000..11866dd --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/platform/platform-android.mdc @@ -0,0 +1,34 @@ +--- +description: "Android-specific conventions for TestApp — Pillar 4" +alwaysApply: true +--- + +# Android Platform Standards — TestApp + +## Target SDK +- `compileSdkVersion` / `targetSdkVersion`: 34 (Android 14) minimum for new apps +- `minSdkVersion`: 21 (Android 5.0) unless brief specifies otherwise +- Update `android/app/build.gradle` when bumping target SDK + +## Permissions +- Declare only needed permissions in `AndroidManifest.xml` +- Runtime permissions: use `permission_handler` — never skip rationale step +- Android 13+: granular media permissions (`READ_MEDIA_IMAGES` not `READ_EXTERNAL_STORAGE`) +- Android 14+: `FOREGROUND_SERVICE_TYPE` required for foreground services + +## Adaptive icons +- Provide both foreground and background layers in `android/app/src/main/res/` +- Test on dark theme, coloured theme, and themed icons (Android 13+) + +## Deep links / App Links +- Verify domain ownership: `.well-known/assetlinks.json` on your server +- Test with: `adb shell am start -a android.intent.action.VIEW -d "https://TestApp.com/products/123"` + +## ProGuard / R8 +- Obfuscation rules in `android/app/proguard-rules.pro` +- Keep rules for: Dio, Freezed models, `@JsonKey` annotated classes +- Test release build thoroughly — obfuscation can break reflection-based code + +## Notifications +- Create notification channels before showing any notification (Android 8+) +- Notification icons must be monochrome on Android 5+ diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/platform/platform-ios.mdc b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/platform/platform-ios.mdc new file mode 100644 index 0000000..46bd4c9 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/platform/platform-ios.mdc @@ -0,0 +1,35 @@ +--- +description: "iOS-specific conventions for TestApp — Pillar 4" +alwaysApply: true +--- + +# iOS Platform Standards — TestApp + +## Platform-specific imports +- Use `dart:io` checks: `if (Platform.isIOS)` for conditional code +- iOS-only plugins: declare in `pubspec.yaml` with platform filter +- **NEVER** call `dart:io` directly in shared code — use `platform_channel` or `universal_io` + +## iOS-specific requirements +- Minimum deployment target: iOS 13.0 (or as specified in `ios/Podfile`) +- Privacy manifests: `ios/Runner/PrivacyInfo.xcprivacy` — required for App Store since 2024 +- Required `Info.plist` keys before using: + - Camera: `NSCameraUsageDescription` + - Photo library: `NSPhotoLibraryUsageDescription` + - Location: `NSLocationWhenInUseUsageDescription` + - Notifications: handled via `permission_handler` + +## Push notifications (iOS) +- Configure APNs certificates in Xcode signing & capabilities +- Request permission with `permission_handler` — show rationale screen first +- Handle foreground vs background vs terminated app states separately +- Test on a physical device — iOS simulator does not support push + +## Safe area +- Always wrap root scaffold with `SafeArea` or use `MediaQuery.of(context).padding` +- Dynamic Island / notch: test on iPhone 14 Pro and iPhone 15 Pro simulators + +## App Store compliance +- Screenshot: use `ScreenshotController` to exclude sensitive screens +- Sign in with Apple: required if any third-party social login is offered +- IPv6 compatibility required — no IPv4-only network code diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/routing/gorouter.mdc b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/routing/gorouter.mdc new file mode 100644 index 0000000..cf70db4 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/routing/gorouter.mdc @@ -0,0 +1,61 @@ +--- +description: "GoRouter conventions for TestApp" +alwaysApply: true +--- + +# GoRouter Standards — TestApp + +## Typed routes (mandatory) +```dart +// Define typed routes — never use string paths directly in navigation calls +@TypedGoRoute(path: '/') +class HomeRoute extends GoRouteData { + const HomeRoute(); + @override Widget build(BuildContext ctx, GoRouterState state) => const HomeScreen(); +} + +@TypedGoRoute(path: '/products/:id') +class ProductRoute extends GoRouteData { + final String id; + const ProductRoute({required this.id}); + @override Widget build(BuildContext ctx, GoRouterState state) => ProductScreen(id: id); +} + +// Navigate with type safety +const ProductRoute(id: product.id).go(context); // ✅ +context.go('/products/${product.id}'); // ❌ — don't do this +``` + +## Auth guard +```dart +// Redirect logic in router config +redirect: (context, state) { + final isLoggedIn = ref.read(authProvider).isAuthenticated; + final isLoginRoute = state.matchedLocation == '/login'; + if (!isLoggedIn && !isLoginRoute) return '/login'; + if (isLoggedIn && isLoginRoute) return '/'; + return null; +}, +``` + +## Shell routes for bottom navigation +```dart +ShellRoute( + builder: (ctx, state, child) => MainScaffold(child: child), + routes: [ + GoRoute(path: '/home', builder: (_, __) => const HomeScreen()), + GoRoute(path: '/search', builder: (_, __) => const SearchScreen()), + GoRoute(path: '/profile', builder: (_, __) => const ProfileScreen()), + ], +) +``` + +## Deep links +- Register URL schemes in `Info.plist` (iOS) and `AndroidManifest.xml` +- Test deep links with: `adb shell am start -a android.intent.action.VIEW -d "app://com.test.testapp/products/123"` +- Handle `GoRouter.of(context).routerDelegate.currentConfiguration` for dynamic links + +## Rules +- **NEVER** use `Navigator.push/pop` — use `context.go()`, `context.push()`, `context.pop()` +- All routes declared in one file: `lib/core/routing/app_router.dart` +- `BlocProvider`s for route-level blocs created inside the `builder` of each route diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/security/security-standards.mdc b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/security/security-standards.mdc new file mode 100644 index 0000000..e9248ab --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/security/security-standards.mdc @@ -0,0 +1,109 @@ +--- +description: "Security standards for TestApp — ALWAYS APPLIED on every file write" +alwaysApply: true +--- + +# Security Standards — TestApp +> **Pillar 5**: Security is an always-on rule, not just a reactive agent. +> These rules apply to EVERY file write, regardless of feature or context. + +## Credential & secret management +- **NEVER** hardcode API keys, tokens, or secrets in source code +- **NEVER** commit `.env` files — use `.gitignore` and document required vars in `.env.example` +- API keys belong in: flavored `--dart-define` build args, or a secrets manager (e.g. AWS Secrets) +- Use `flutter_secure_storage` for tokens/credentials — **NEVER** `SharedPreferences` for sensitive data +- Obfuscation: enable `--obfuscate --split-debug-info=build/debug-symbols/` for release builds + +## Authentication & sessions +- JWT/session tokens: stored in `flutter_secure_storage`, never in `SharedPreferences` or local DB +- Implement token refresh with retry logic — never let a 401 show a raw error to the user +- Certificate pinning: required for production builds on Firebase Auth — use `dio_pinning_interceptor` +- Biometric re-auth: require for any transaction > defined threshold + +## Data & privacy +- **NEVER** log PII (names, emails, phone numbers, addresses) to console or crash reporters +- Sanitize all user inputs before sending to backend +- Use `SensitiveWidget` wrapper to exclude screens from screenshots / app switcher thumbnails +- Comply with platform privacy requirements: iOS `NSPrivacyAccessedAPITypes`, Android permissions + +## Network security +- All API calls use HTTPS — no http:// in production +- Validate SSL certificates — never bypass with `badCertificateCallback: (_,_,_) => true` +- Add a timeout to every HTTP call: `connectTimeout: Duration(seconds: 10)` +- Rate-limit sensitive endpoints: auth, OTP, password reset + +## Dependency security +- Run `dart pub outdated` monthly — flag packages with known CVEs +- Never use a package with <100 pub points without explicit tech lead approval +- Pin critical security packages to exact versions in `pubspec.yaml` + +## Secure coding patterns +```dart +// ✅ Correct: secure token storage +final storage = FlutterSecureStorage(); +await storage.write(key: 'access_token', value: token); + +// ❌ Wrong: SharedPreferences for sensitive data +final prefs = await SharedPreferences.getInstance(); +prefs.setString('access_token', token); // NEVER do this + +// ✅ Correct: PII-safe logging +logger.info('User authenticated: userId=${user.id}'); // ok — id, not email +logger.debug('Payment processed: orderId=$orderId'); // ok — no card data + +// ❌ Wrong: PII in logs +logger.info('Login: email=${user.email}, phone=${user.phone}'); // NEVER + +// ✅ Correct: --dart-define for secrets (build arg, not in code) +// flutter build apk --dart-define=API_KEY=$API_KEY +const apiKey = String.fromEnvironment('API_KEY'); // acceptable + +// ✅ Correct: certificate pinning with Dio +(dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () { + final client = HttpClient(); + client.badCertificateCallback = (cert, host, port) => false; // strict + return client; +}; +``` + +## Deep linking & intent security +- Validate all incoming deep link parameters — never trust raw URL params +- Use `app_links` package for verified deep link handling +- Restrict URL schemes to known patterns — reject unknown schemes +- For OAuth callbacks: validate `state` parameter to prevent CSRF + +## Storage security +- Encrypt locally cached sensitive data using `hive` with `HiveAesCipher` +- Clear sensitive data from memory after use (set to null, trigger GC) +- Do NOT cache auth tokens in network layer (no `dio_cache_interceptor` for auth endpoints) +- Use `SecureRandom` for nonce/token generation — never `Random()` + +## Code obfuscation & binary protection +```yaml +# android/app/build.gradle — release config +buildTypes { + release { + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } +} +``` +```bash +# Flutter release build with obfuscation +flutter build apk --release --obfuscate --split-debug-info=build/debug-symbols/ +flutter build ipa --release --obfuscate --split-debug-info=build/debug-symbols/ +``` + +## Release checklist +Before every production release, verify: +- [ ] No hardcoded secrets (`grep -r "api_key\|secret\|password\|token" lib/`) +- [ ] Debug flags disabled (`kDebugMode` guards on all debug-only paths) +- [ ] Obfuscation enabled in release build config +- [ ] Certificate pinning active and tested on both platforms +- [ ] Crash reporter PII filters configured (Sentry `beforeSend`, Firebase `setConsentType`) +- [ ] `flutter_secure_storage` used for all tokens — no `SharedPreferences` for sensitive keys +- [ ] Network calls all HTTPS — scan for `http://` in lib/ +- [ ] Dependencies audited: `dart pub outdated --json | jq '.packages[] | select(.isDiscontinued)'` +- [ ] Deep link parameters validated +- [ ] App transport security / network security config reviewed diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/state-management/bloc.mdc b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/state-management/bloc.mdc new file mode 100644 index 0000000..de736de --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/state-management/bloc.mdc @@ -0,0 +1,64 @@ +--- +description: "BLoC / Cubit conventions for TestApp" +alwaysApply: true +--- + +# BLoC / Cubit Standards — TestApp + +## When to use BLoC vs Cubit +- **Cubit**: simple state with no meaningful event history (toggle, counter, pagination) +- **BLoC**: event-driven flows where event history or transitions matter (auth, checkout, form wizard) + +## Event and State classes +```dart +// Events — sealed class (exhaustive switch) +sealed class AuthEvent { const AuthEvent(); } +final class AuthLoginRequested extends AuthEvent { + final String email, password; + const AuthLoginRequested({required this.email, required this.password}); +} +final class AuthLogoutRequested extends AuthEvent { const AuthLogoutRequested(); } + +// States — sealed class, immutable +sealed class AuthState { const AuthState(); } +final class AuthInitial extends AuthState { const AuthInitial(); } +final class AuthLoading extends AuthState { const AuthLoading(); } +final class AuthAuthenticated extends AuthState { + final User user; + const AuthAuthenticated(this.user); +} +final class AuthUnauthenticated extends AuthState { const AuthUnauthenticated(); } +final class AuthFailure extends AuthState { + final String message; + const AuthFailure(this.message); +} +``` + +## BlocProvider placement +- Create `BlocProvider` at the **route level** (in GoRouter route definitions) +- `MultiBlocProvider` at route level for screens needing multiple blocs +- **NEVER** create `BlocProvider` inside a widget's `build()` method + +## Usage rules +- **NEVER** call `bloc.add()` inside `build()` — only in gesture callbacks or `initState()` +- Use `BlocConsumer` only when BOTH `listen` + `build` logic are needed +- Use `BlocSelector` when only a subset of state triggers a rebuild +- Every BLoC must override `close()` and cancel `StreamSubscription`s + +## BlocBuilder patterns +```dart +BlocBuilder( + builder: (context, state) => switch (state) { + AuthInitial() => const SizedBox.shrink(), + AuthLoading() => const LoadingIndicator(), + AuthAuthenticated(user: final u) => HomeScreen(user: u), + AuthUnauthenticated() => const LoginScreen(), + AuthFailure(message: final m) => ErrorScreen(message: m), + }, +) +``` + +## File locations in TestApp +- `lib/features/[feature]/presentation/bloc/[feature]_bloc.dart` +- `lib/features/[feature]/presentation/bloc/[feature]_event.dart` +- `lib/features/[feature]/presentation/bloc/[feature]_state.dart` diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/testing/testing-bloc.mdc b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/testing/testing-bloc.mdc new file mode 100644 index 0000000..f8630a0 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/testing/testing-bloc.mdc @@ -0,0 +1,60 @@ +--- +description: "BLoC testing conventions for TestApp" +alwaysApply: false +--- + +# BLoC Testing Standards — TestApp + +## Test pattern (bloc_test) +```dart +// blocTest(description, build: () => MyCubit(), act: (c) => c.method(), expect: () => [MyState()]) + +void main() { + late AuthBloc authBloc; + late MockAuthRepository mockRepo; + + setUp(() { + mockRepo = MockAuthRepository(); + authBloc = AuthBloc(repository: mockRepo); + }); + + tearDown(() => authBloc.close()); + + group('AuthBloc', () { + blocTest( + 'emits [Loading, Authenticated] when login succeeds', + build: () { + when(() => mockRepo.login(any(), any())) + .thenAnswer((_) async => const Right(User(id: '1', email: 'test@test.com'))); + return authBloc; + }, + act: (bloc) => bloc.add(const AuthLoginRequested(email: 'test@test.com', password: 'pass')), + expect: () => [ + const AuthLoading(), + isA(), + ], + ); + + blocTest( + 'emits [Loading, Failure] when login fails', + build: () { + when(() => mockRepo.login(any(), any())) + .thenAnswer((_) async => const Left(AuthError('Invalid credentials'))); + return authBloc; + }, + act: (bloc) => bloc.add(const AuthLoginRequested(email: 'bad', password: 'bad')), + expect: () => [ + const AuthLoading(), + const AuthFailure('Invalid credentials'), + ], + ); + }); +} +``` + +## Rules +- Use `mocktail` for mocking — never `mockito` +- Every BLoC test file: `test/features/[feature]/[feature]_bloc_test.dart` +- Coverage requirement: all state transitions must be tested +- Use `Given/When/Then` naming in test descriptions +- Test error paths as thoroughly as success paths diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/universal/flutter-core.mdc b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/universal/flutter-core.mdc new file mode 100644 index 0000000..332717b --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/universal/flutter-core.mdc @@ -0,0 +1,40 @@ +--- +description: "Core Flutter conventions for TestApp — always applied" +alwaysApply: true +--- + +# Flutter Core Standards — TestApp + +## Const and performance +- Use `const` constructors wherever possible — compile-time guarantee of no rebuild +- Prefer `const` widgets at the leaf level: `const SizedBox.shrink()`, `const Padding(...)` +- Never use `const` with mutable values; lint: `prefer_const_constructors` is enabled + +## Null safety +- Never use `!` (bang operator) unless you have a 100% safe runtime guarantee with a comment +- Prefer `??`, `?.`, and `if (x != null)` guards +- Use `required` for all non-nullable named parameters +- Never use `late` without a guarantee of initialisation before first access + +## Widget lifecycle +- Override `dispose()` in every `StatefulWidget` that uses controllers, streams, or timers +- Cancel `StreamSubscription` in `dispose()`, not in `didUpdateWidget` +- Use `WidgetsBinding.instance.addPostFrameCallback` for post-build logic, not `Future.delayed(Duration.zero)` + +## Naming conventions +- Files: `snake_case.dart` +- Classes: `PascalCase` +- Variables/functions: `camelCase` +- Constants: `kCamelCase` or `SCREAMING_SNAKE` for true compile-time constants +- Private members: `_camelCase` + +## Imports +- Order: dart: → package: → relative +- Use relative imports within a feature; absolute for cross-feature +- Never import a feature's internal files from outside that feature + +## Code quality +- Max function length: 40 lines. Extract widgets and helpers aggressively +- No `print()` in production code — use a logging package +- All `TODO:` comments must include a ticket number: `// TODO: PROJ-123 — fix this` +- Run `dart format` before every commit diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/universal/project-context.mdc b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/universal/project-context.mdc new file mode 100644 index 0000000..97ea332 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/universal/project-context.mdc @@ -0,0 +1,67 @@ +--- +description: "Project context for TestApp — always applied" +alwaysApply: true +--- + +# Project Context — TestApp + +## Project identity +- **Name:** TestApp +- **Package:** com.test.testapp +- **Description:** Test app for golden tests +- **Scale:** medium + +## Technology stack +- **State management:** BLoC / Cubit +- **Architecture:** Clean Architecture +- **Routing:** GoRouter +- **Backend:** Firebase +- **Auth:** Firebase Auth +- **Platforms:** ios, android +- **Code generation:** freezed + +## Feature modules +auth, home, products + +## Special capabilities + + +## Environments / flavors +- Flavors: dev, prod +- CI/CD: GitHub Actions + +## Design & API references +- Design source: none +- API docs: none at `` + +## Code references +### Git repositories +_No Git repository URLs listed._ Add entries under `references.repos` in project-brief.yaml when other repos are part of the product context. + +### Local paths +_No local paths listed._ Add monorepo packages or sibling folders under `references.local_paths` in project-brief.yaml when relevant. + +## Product UX / themes & roles +- **Theme variants:** light, dark +- **Roles:** Not enabled (`app_context.roles_enabled: false`). + + +## Reviews — which rule owns what +- **Theme, colors, typography, spacing/radius tokens** → `ui-ux-standards.mdc` (widgets read `Theme.of(context)` only) +- **User-visible copy & locales** → `localization.mdc` (ARB / `AppLocalizations`; no UI string literals) +- **Imports, structure, naming** → `flutter-core.mdc` + architecture rule + +## Architecture boundaries +- presentation/ MUST NOT import from data/ +- domain/ MUST NOT import from data/ or presentation/ +- data/ CAN import from domain/ (implements interfaces) +- Use dependency injection to invert data → domain dependency + +## When generating code for this project +1. Always use BLoC / Cubit patterns — never suggest alternatives +2. Always follow Clean Architecture folder structure +3. Always use GoRouter for navigation — never `Navigator.push` directly +4. Always target platforms: ios, android +5. If code generation tools are used (freezed), follow their conventions +6. Apply visuals only through theme (`ColorScheme`, `TextTheme`, `ThemeExtension`) — never ad-hoc colors/fonts in feature widgets +7. No user-facing string literals in widgets — l10n or shared constants per localization rule diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/universal/ui-ux-standards.mdc b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/universal/ui-ux-standards.mdc new file mode 100644 index 0000000..3929020 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/rules/universal/ui-ux-standards.mdc @@ -0,0 +1,48 @@ +--- +description: "UI/UX standards for TestApp — always applied" +alwaysApply: true +--- + +# UI / UX Standards — TestApp + +## Theme & design tokens (single source of truth) +- Define **one** light/dark `ThemeData` (and optional `ThemeExtension`s for brand spacing, radii, semantic colors). Feature code reads `Theme.of(context)` only. +- **Colors:** `colorScheme` / extensions — never hex/`Color(...)` literals in widgets except inside the theme definition file(s). +- **Typography:** `textTheme` / `primaryTextTheme` — never raw `TextStyle(fontSize:, fontFamily:)` in feature UI. +- **Spacing & shapes:** `ThemeExtension` or documented constants consumed consistently — avoid one-off magic numbers for padding/radius. + +## Loading states +- Every async operation MUST show a loading skeleton (shimmer), NOT a spinner unless < 300ms +- Use `shimmer` package with a shimmer that matches the final layout shape +- Never show a blank screen during loading — skeleton must fill the same space as the content + +## Empty states +- Every list/grid MUST have a distinct empty state widget: illustration + headline + CTA +- Empty state is different from error state — never reuse the same widget for both +- Empty state copy: positive framing ("No items yet — add your first one") + +## Error states +- Every async failure MUST show: error message + retry button +- Never swallow errors silently +- Error text: user-friendly, never expose stack traces or raw API messages + +## Navigation & transitions +- Use `IndexedStack` for bottom nav tabs — preserves scroll position +- Named routes only — never `Navigator.push(context, MaterialPageRoute(...))` +- Page transitions: use `CustomTransitionPage` with `FadeTransition` for modal sheets + +## Responsive layout +- Use `LayoutBuilder` or `MediaQuery` for breakpoints, not hardcoded pixel values +- Minimum touch target: 48×48 logical pixels (Material guideline) +- Test on 375px (iPhone SE) and 414px (iPhone Pro Max) widths minimum + +## Haptics +- Use `HapticFeedback.lightImpact()` on primary CTAs +- Use `HapticFeedback.selectionClick()` on toggle/checkbox interactions +- Never add haptics to destructive actions without confirmation + +## Accessibility +- All interactive widgets must have a `Semantics` label or `tooltip` +- Minimum contrast ratio: 4.5:1 (WCAG AA) +- Test with TalkBack / VoiceOver before each release + diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/skills/create-flavor/SKILL.md b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/skills/create-flavor/SKILL.md new file mode 100644 index 0000000..5765fce --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/skills/create-flavor/SKILL.md @@ -0,0 +1,18 @@ +# Create Build Flavor — TestApp + +Creates a new build flavor/environment. Current flavors: dev, prod. + +## Usage +``` +Create a new flavor called [flavor_name] +``` + +## What gets created +1. Android: new `productFlavors` block in `android/app/build.gradle` +2. iOS: new scheme + configuration in Xcode (instructions provided) +3. `lib/core/config/[flavor_name]_config.dart` +4. `.env.[flavor_name]` template +5. GitHub Actions pipeline update + +## CI/CD: GitHub Actions +Generate the pipeline config snippet for the new flavor in GitHub Actions format. diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/skills/deploy/SKILL.md b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/skills/deploy/SKILL.md new file mode 100644 index 0000000..ba46a7c --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/skills/deploy/SKILL.md @@ -0,0 +1,21 @@ +# Deploy — TestApp + +Guides through deployment to GitHub Actions for any of the flavors: dev, prod. + +## Usage +``` +Deploy [flavor] to [store/environment] +``` + +## GitHub Actions pipeline +The AI will generate or update the github_actions configuration file for: +- Build signing +- Running tests +- Distributing to Firebase App Distribution (beta) or App Store / Play Store (prod) + +## Pre-deploy checklist +- [ ] Version bump in `pubspec.yaml` +- [ ] Obfuscation enabled for prod: `--obfuscate --split-debug-info=build/debug-symbols/` +- [ ] No debug flags in production code +- [ ] Security checklist from `security-standards.mdc` passed +- [ ] `cursor_gen --validate` passes diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/skills/generate-tests/SKILL.md b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/skills/generate-tests/SKILL.md new file mode 100644 index 0000000..8c8a196 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/skills/generate-tests/SKILL.md @@ -0,0 +1,41 @@ +# Generate Tests — TestApp + +Generates comprehensive unit, widget, and integration tests for **BLoC / Cubit** patterns. + +## Usage +``` +Generate tests for [ClassName or file path] +``` + +## Test generation process +1. Read the source file completely +2. Identify all testable units (public methods, state transitions, UI states) +3. Generate tests following this pattern: + ``` + blocTest(description, build: () => MyCubit(), act: (c) => c.method(), expect: () => [MyState()]) + ``` +4. Create mocks with `mocktail` for all dependencies +5. Place test file at `test/[mirror of source path]_test.dart` + +## Coverage targets +- Business logic (UseCases, Repositories, BLoC/Notifiers): **80% minimum** +- Widget tests: all three states (loading/error/data) must be tested +- E2E: only critical user flows + +## Test file structure +```dart +void main() { + // Setup + group('[ClassName]', () { + // Happy path tests + group('success cases', () { ... }); + // Error path tests + group('error cases', () { ... }); + // Edge cases + group('edge cases', () { ... }); + }); +} +``` + +## Naming convention +`'given [precondition], when [action], then [expected outcome]'` diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/skills/scaffold-feature/SKILL.md b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/skills/scaffold-feature/SKILL.md new file mode 100644 index 0000000..2d23505 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/skills/scaffold-feature/SKILL.md @@ -0,0 +1,69 @@ +# Scaffold Feature — TestApp + +Scaffolds a complete new feature module following **Clean Architecture** architecture with **BLoC / Cubit** state management. + +## Usage +``` +Create a feature called [feature_name] with [description] +``` + +## What gets generated + +### For Clean Architecture architecture: +The AI will create all necessary files for the `[feature_name]` feature following Clean Architecture patterns. + +### File structure to create: + +**Clean Architecture:** +``` +lib/features/[feature_name]/ + domain/ + entities/[feature_name].dart + repositories/[feature_name]_repository.dart + usecases/ + get_[feature_name]_usecase.dart + create_[feature_name]_usecase.dart + data/ + models/[feature_name]_dto.dart + datasources/[feature_name]_remote_datasource.dart + repositories/[feature_name]_repository_impl.dart + presentation/ + [state_files]/ ← based on BLoC / Cubit + pages/[feature_name]_page.dart + widgets/ +``` + +**Feature-First:** +``` +lib/features/[feature_name]/ + [feature_name]_screen.dart + [feature_name]_provider.dart ← or [feature_name]_bloc.dart + [feature_name]_repository.dart + [feature_name]_model.dart + widgets/ +``` + +## State management boilerplate + +### For BLoC / Cubit: +Generate the appropriate state management files with: +- Initial/loading/success/error states +- All necessary events (for BLoC) +- Repository connection +- Dependency injection registration + +## Steps the AI takes +1. Ask: "What is the feature name and brief description?" +2. Ask: "What data does this feature manage? (e.g., list of products, single user profile)" +3. Generate all files with correct imports and patterns +4. Add the feature to the DI container +5. Add the route to GoRouter router +6. Create a placeholder test file + +## Code generation +{{#if codegen_freezed}} +- Generate Freezed model: run `dart run build_runner build` after scaffolding +{{/if}} +{{#if codegen_injectable}} +- Register in injectable: add `@lazySingleton` to repository +{{/if}} diff --git a/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/skills/scaffold-screen/SKILL.md b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/skills/scaffold-screen/SKILL.md new file mode 100644 index 0000000..699fdaa --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/bloc-clean-firebase/skills/scaffold-screen/SKILL.md @@ -0,0 +1,61 @@ +# Scaffold Screen — TestApp + +Creates a complete screen widget with all states handled, following **BLoC / Cubit** patterns. + +## Usage +``` +Create a screen for [screen_name] that shows [content description] +``` + +## Generated screen template + +### BLoC / Cubit screen pattern: + +**BLoC:** +```dart +class [ScreenName]Screen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocBuilder<[Feature]Bloc, [Feature]State>( + builder: (context, state) => switch (state) { + [Feature]Initial() || [Feature]Loading() => const [ScreenName]Shimmer(), + [Feature]Loaded(:final data) => [ScreenName]Content(data: data), + [Feature]Empty() => const [ScreenName]EmptyState(), + [Feature]Error(:final message) => [ScreenName]ErrorState( + message: message, + onRetry: () => context.read<[Feature]Bloc>().add(const [Feature]LoadRequested()), + ), + }, + ); + } +} +``` + +**Riverpod:** +```dart +class [ScreenName]Screen extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch([feature]Provider); + return state.when( + loading: () => const [ScreenName]Shimmer(), + data: (data) => [ScreenName]Content(data: data), + error: (e, _) => [ScreenName]ErrorState(error: e), + ); + } +} +``` + +## Required sub-widgets to generate +1. `[ScreenName]Shimmer` — skeleton loading layout matching final content +2. `[ScreenName]EmptyState` — illustration + headline + CTA button +3. `[ScreenName]ErrorState` — error message + retry button +4. `[ScreenName]Content` — the actual data display + +## Platform considerations (ios, android) +{{#if platform_web}} +- Web: ensure no dart:io usage; test at 375px and 1280px widths +{{/if}} +{{#if platform_desktop}} +- Desktop: add keyboard shortcut support for primary actions +{{/if}} diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/agents/api-client-gen.mdc b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/agents/api-client-gen.mdc new file mode 100644 index 0000000..b9c9d36 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/agents/api-client-gen.mdc @@ -0,0 +1,34 @@ +--- +name: api-client-gen +description: "Generates type-safe API clients for LegacyApp from openapi spec. Ask: '@api-client-gen generate client for /products endpoint'" +model: claude-sonnet-4-20250514 +context: auto +allowed-tools: [read_file, write_file, list_files] +--- + +You are an API client generator for **LegacyApp**. +API docs: `docs/api.yaml` (format: openapi) + +## Generation steps +1. Read the API spec at `docs/api.yaml` +2. For the requested endpoint(s), generate: + - Request DTO (`@JsonSerializable` or Freezed) + - Response DTO (`@JsonSerializable` or Freezed) + - Repository method with error handling + - Dio/Retrofit client method (if Retrofit in codegen) + +## Output structure +```dart +// data/models/product_dto.dart +@freezed +class ProductDto with _$ProductDto { + factory ProductDto({...}) = _ProductDto; + factory ProductDto.fromJson(Map json) => _$ProductDtoFromJson(json); +} + +// data/datasources/product_remote_datasource.dart +class ProductRemoteDataSource { + final Dio _dio; + Future> getProducts() async { ... } +} +``` diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/agents/code-reviewer.mdc b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/agents/code-reviewer.mdc new file mode 100644 index 0000000..a49f9bf --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/agents/code-reviewer.mdc @@ -0,0 +1,53 @@ +--- +name: code-reviewer +description: "Reviews LegacyApp code for GetX patterns, MVC boundaries, and REST API usage. Ask: 'Review this code' or '@code-reviewer check PR'" +model: claude-opus-4-5 +context: auto +allowed-tools: [read_file, list_files] +--- + +You are a senior Flutter engineer reviewing code for **LegacyApp**. + +## Your review checklist + +### GetX patterns +- Are state classes immutable and sealed? +- Is state management correctly separated from UI logic? +- Are streams/subscriptions properly cancelled in dispose()? +- Check for anti-patterns specific to GetX + +### MVC boundaries +- View (Widget) MUST NOT contain business logic +- Controller MUST NOT import Flutter widgets directly +- Model MUST be plain Dart, no framework dependencies +- Flag any violation of these import rules immediately + +### REST API usage +- Are all exceptions caught and mapped to domain errors? +- Are streams vs futures used appropriately? +- Are connections/subscriptions disposed correctly? + +### Security (always check) +- No hardcoded API keys or secrets +- No PII logged to console or crash reporters +- Sensitive data using flutter_secure_storage, not SharedPreferences +- All user inputs validated before sending to backend + +### General Flutter +- `const` used where possible +- `dispose()` overridden for all controllers/subscriptions +- No `print()` in production paths +- Loading/empty/error states all handled + +## Output format +For each issue found: +``` +[SEVERITY: critical/major/minor] File:line — Issue description +WHY: Why this matters +FIX: Specific fix recommendation +``` + +Severity guide: +- **critical**: Security issue, data loss risk, crash potential +- **major**: Incorrect pattern, boundary violation, missing error handling +- **minor**: Style, naming, optimization opportunity diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/agents/migration-agent.mdc b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/agents/migration-agent.mdc new file mode 100644 index 0000000..ca05e65 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/agents/migration-agent.mdc @@ -0,0 +1,42 @@ +--- +name: migration-agent +description: "Migrates GetX controllers to Riverpod Notifiers for LegacyApp. Consult before adding any new GetX code — suggest Riverpod equivalent. Ask: '@migration-agent migrate [feature]'" +model: claude-opus-4-5 +context: fork +allowed-tools: [read_file, write_file, list_files] +--- + +You are a Flutter migration specialist for **LegacyApp**. +This project currently uses GetX (MVC). Your goal is incremental, +safe migration to Riverpod without breaking existing features. + +## Migration mapping +| GetX | Riverpod equivalent | +|------|---------------------| +| `GetxController` with `.obs` | `AsyncNotifier` or `Notifier` | +| `Obx()` | `ref.watch()` in `ConsumerWidget` | +| `Get.find()` | `ref.read(provider.notifier)` | +| `Get.toNamed()` | `context.go()` (after GoRouter migration) | +| `GetxController.onInit()` | `build()` method in `AsyncNotifier` | +| `GetxController.onClose()` | `ref.onDispose()` | + +## Migration process (feature by feature) +1. **Read** the existing `GetxController` in full +2. **Write tests** for the existing GetX version first (if none exist) +3. **Map** `.obs` variables → state class fields +4. **Write** the new `AsyncNotifier` with equivalent logic +5. **Write tests** for the Riverpod version using `ProviderContainer` +6. **Migrate** the View: `GetView` → `ConsumerWidget`, `Obx()` → `ref.watch()` +7. **Verify** all tests pass for both old and new +8. **Remove** GetX code from that feature + +## When NOT to migrate (mark with TODO: MIGRATE-LATER) +- Controller shared across 5+ screens (high blast radius — plan separately) +- Feature ships in the next sprint (postpone — don't hold up a release) +- No tests exist AND you can't write them first (write GetX tests first) + +## Output per migration +1. New Riverpod provider file +2. Updated ConsumerWidget screen file +3. Test file for the new provider +4. Diff showing what GetX code is removed diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/agents/security-agent.mdc b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/agents/security-agent.mdc new file mode 100644 index 0000000..f55790c --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/agents/security-agent.mdc @@ -0,0 +1,47 @@ +--- +name: security-agent +description: "Deep security review for LegacyApp. Consult for auth flows, payment screens, and sensitive data handling. Ask: '@security-agent review auth flow'" +model: claude-opus-4-5 +context: fork +allowed-tools: [read_file, list_files] +--- + +You are a mobile security expert conducting a deep review for **LegacyApp**. + +> Note: This agent provides deep security analysis. +> The `security-standards.mdc` rule provides always-on enforcement. +> This agent is for detailed consultations on specific security concerns. + +## Deep review focus areas + +### Auth flow (JWT / REST Auth) +- Token storage: is `flutter_secure_storage` used for ALL tokens? +- Token refresh: is refresh handled atomically (no race condition)? +- Session expiry: does the app handle 401 gracefully without data loss? +- Certificate pinning: configured and tested? + +### Data at rest +- SQLite/Hive encryption: sensitive DBs encrypted? +- Cache poisoning: cached API responses validated before use? +- Keychain/Keystore usage for cryptographic keys + +### Network security +- All endpoints HTTPS — any http:// URLs? +- Certificate validation — any `badCertificateCallback: true`? +- Sensitive data in URL params/query strings? +- Request/response logging in production? (must be off) + +### Code injection risks +- Dynamic code execution patterns +- WebView usage — JavaScript interface security +- Deep link parameter validation (no path traversal) + +## Output format +For each finding: +``` +[RISK: Critical/High/Medium/Low] +LOCATION: File / function +ISSUE: Detailed description +CVSS-like impact: Confidentiality/Integrity/Availability +REMEDIATION: Specific code fix +``` diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/agents/test-writer.mdc b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/agents/test-writer.mdc new file mode 100644 index 0000000..d1646bc --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/agents/test-writer.mdc @@ -0,0 +1,33 @@ +--- +name: test-writer +description: "Writes GetX unit tests for LegacyApp. Ask: 'Write tests for [class]' or '@test-writer generate tests'" +model: claude-sonnet-4-20250514 +context: auto +allowed-tools: [read_file, write_file, list_files] +--- + +You are a Flutter test engineer for **LegacyApp** using **GetX**. + +## Test pattern to follow +```dart +Get.put(MyController()); final ctrl = Get.find(); expect(ctrl.value, expected); +``` + +## When asked to write tests: +1. Read the source file completely +2. Identify all public methods and state transitions +3. Write tests for: + - Happy path (successful operation) + - Error path (failure, exception handling) + - Edge cases (empty data, boundary values) +4. Use `mocktail` for all mocking +5. Follow `Given/When/Then` naming: `'given X, when Y, then emits Z'` + +## File placement +- Unit tests: `test/features/[feature]/[file]_test.dart` +- Widget tests: `test/features/[feature]/[screen]_widget_test.dart` +- Coverage target: 80% minimum for business logic classes + +## Output +Write the complete test file, ready to run. Include all imports. +After writing, run: `dart test path/to/test_file.dart` diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/agents/ui-validator.mdc b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/agents/ui-validator.mdc new file mode 100644 index 0000000..daf0fb2 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/agents/ui-validator.mdc @@ -0,0 +1,47 @@ +--- +name: ui-validator +description: "Validates LegacyApp UI against design system and UX standards. Ask: 'Validate this screen' or '@ui-validator check'" +model: claude-sonnet-4-20250514 +context: auto +allowed-tools: [read_file, list_files] +--- + +You are a UI/UX validator for **LegacyApp**. + +## Validate every screen for: + +### State coverage (GetX) +- Loading state: shows shimmer (NOT spinner unless brief) +- Empty state: shows illustration + CTA (NOT blank screen) +- Error state: shows message + retry button (NOT toast-only) +- Data state: renders content correctly + +### Accessibility +- All interactive widgets have semantic labels +- Minimum touch targets: 48×48dp +- Sufficient color contrast (4.5:1 minimum) + +### Responsive layout +- No hardcoded pixel widths +- Tested at 375px and 414px viewport widths +- `SafeArea` used correctly on iOS + +### Platform-specific (ios, android) +{{#if web}} +- No dart:io imports in web-targeted code +- PWA-compatible (no native-only APIs without fallbacks) +{{/if}} +{{#if ios}} +- Safe area respected (notch, Dynamic Island) +- iOS Human Interface Guidelines followed +{{/if}} + +### Security (UI layer) +- No credentials shown in plaintext +- Sensitive screens wrapped with screenshot prevention + +## Output +List each violation with: +- Location (file:widget) +- What's wrong +- How to fix it diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/architecture/mvc.mdc b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/architecture/mvc.mdc new file mode 100644 index 0000000..481231e --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/architecture/mvc.mdc @@ -0,0 +1,22 @@ +--- +description: "MVC architecture conventions for LegacyApp" +alwaysApply: true +--- + +# MVC Architecture — LegacyApp + +## Layer responsibilities +- **Model**: Data + business logic. Pure Dart. +- **View**: Renders UI, observes Controller state. No business logic. +- **Controller** (GetX): Connects Model ↔ View. Manages state transitions. + +## Import rules +- View (Widget) MUST NOT contain business logic +- Controller MUST NOT import Flutter widgets directly +- Model MUST be plain Dart, no framework dependencies + +## Controller rules +- Controllers are injected via `Binding`, never created in widgets +- One Controller per feature screen (not per widget) +- Controllers fetch data in `onInit()`, clean up in `onClose()` +- All reactive state marked with `.obs` diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/backend/rest.mdc b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/backend/rest.mdc new file mode 100644 index 0000000..3bbcc27 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/backend/rest.mdc @@ -0,0 +1,52 @@ +--- +description: "REST API conventions for LegacyApp" +alwaysApply: true +--- + +# REST API Standards — LegacyApp + +## HTTP client setup (Dio) +```dart +// In core/network/dio_client.dart +Dio createDioClient({required AppConfig config}) { + final dio = Dio(BaseOptions( + baseUrl: config.baseUrl, + connectTimeout: const Duration(seconds: 10), + receiveTimeout: const Duration(seconds: 30), + headers: {'Content-Type': 'application/json', 'Accept': 'application/json'}, + )); + + dio.interceptors.addAll([ + AuthInterceptor(tokenStorage: getIt()), + LoggingInterceptor(), // debug builds only + RetryInterceptor(dio), // 3 retries on network errors + ]); + return dio; +} +``` + +## DTOs (Data Transfer Objects) +- DTOs live in the `data/` layer — **never** pass raw JSON maps to the domain layer +- Use Freezed for DTOs if `codegen` includes `json_serializable` or `freezed` +- `fromJson` factory must handle null fields gracefully — API response fields are not guaranteed + +## Error handling +```dart +// Map HTTP errors → AppError domain types +AppError _mapDioError(DioException e) => switch (e.type) { + DioExceptionType.connectionTimeout => const NetworkError(statusCode: null), + DioExceptionType.receiveTimeout => const NetworkError(statusCode: null), + DioExceptionType.badResponse => _mapStatusCode(e.response?.statusCode), + DioExceptionType.connectionError => const NetworkError(statusCode: null), + _ => UnknownError(e), +}; +``` + +## API versioning +- Base URL includes version: `https://api.LegacyApp.com/v1/` +- When upgrading API version, keep old version working until all clients migrate + +## Auth token interceptor +- Inject `Authorization: Bearer ` automatically on every request +- On 401: refresh token once, retry original request, then logout if refresh fails +- On 403: map to `AppError.authError('Insufficient permissions')` diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/error-handling/error-handling.mdc b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/error-handling/error-handling.mdc new file mode 100644 index 0000000..0f1b1e4 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/error-handling/error-handling.mdc @@ -0,0 +1,67 @@ +--- +description: "Global error handling strategy for LegacyApp — always applied" +alwaysApply: true +--- + +# Global Error Handling — LegacyApp + +## Flutter error boundaries +Configure in `main.dart` — do this ONCE and never bypass it: + +```dart +void main() { + FlutterError.onError = (details) { + FlutterError.presentError(details); + // TODO: Send to crash reporter (Sentry/Firebase Crashlytics) + crashReporter.recordFlutterError(details); + }; + + PlatformDispatcher.instance.onError = (error, stack) { + // TODO: Send to crash reporter + crashReporter.recordError(error, stack, fatal: true); + return true; + }; + + runApp(const MyApp()); +} +``` + +## Error types hierarchy +Define a sealed class for domain errors — never throw raw exceptions in business logic: + +```dart +sealed class AppError { + const AppError(); +} +class NetworkError extends AppError { final int? statusCode; const NetworkError({this.statusCode}); } +class AuthError extends AppError { final String reason; const AuthError(this.reason); } +class NotFoundError extends AppError { final String resource; const NotFoundError(this.resource); } +class UnknownError extends AppError { final Object cause; const UnknownError(this.cause); } +``` + +## Repository layer +- Wrap ALL external calls in try/catch and return `Either` or `Result` +- NEVER let raw exceptions bubble to the presentation layer +- Log at the repository layer, not the UI layer + +## Presentation layer +- Every async widget MUST handle error state explicitly — no silent failures +- Show user-friendly error messages: map `AppError` subtype → readable string +- Provide a "Try again" action for recoverable errors (network, timeout) +- For fatal errors (auth expired), redirect to login — never show a dead screen + +## Crash reporting +- Integrate Sentry or Firebase Crashlytics before first TestFlight / Play beta +- Set `user` context on crash reporter after login (id only, no PII) +- Add `breadcrumbs` for key user actions to aid reproduction + +## Logging strategy +``` +Level | Use case +DEBUG | Development only (strip from release) +INFO | Key user flows (login, purchase, etc.) +WARNING | Recoverable errors, fallbacks used +ERROR | Unrecoverable errors, unexpected states +``` +- Use `logger` package — never bare `print()` +- Logger instance per class: `final _log = Logger('ClassName');` diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/platform/platform-android.mdc b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/platform/platform-android.mdc new file mode 100644 index 0000000..2d045ab --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/platform/platform-android.mdc @@ -0,0 +1,34 @@ +--- +description: "Android-specific conventions for LegacyApp — Pillar 4" +alwaysApply: true +--- + +# Android Platform Standards — LegacyApp + +## Target SDK +- `compileSdkVersion` / `targetSdkVersion`: 34 (Android 14) minimum for new apps +- `minSdkVersion`: 21 (Android 5.0) unless brief specifies otherwise +- Update `android/app/build.gradle` when bumping target SDK + +## Permissions +- Declare only needed permissions in `AndroidManifest.xml` +- Runtime permissions: use `permission_handler` — never skip rationale step +- Android 13+: granular media permissions (`READ_MEDIA_IMAGES` not `READ_EXTERNAL_STORAGE`) +- Android 14+: `FOREGROUND_SERVICE_TYPE` required for foreground services + +## Adaptive icons +- Provide both foreground and background layers in `android/app/src/main/res/` +- Test on dark theme, coloured theme, and themed icons (Android 13+) + +## Deep links / App Links +- Verify domain ownership: `.well-known/assetlinks.json` on your server +- Test with: `adb shell am start -a android.intent.action.VIEW -d "https://LegacyApp.com/products/123"` + +## ProGuard / R8 +- Obfuscation rules in `android/app/proguard-rules.pro` +- Keep rules for: Dio, Freezed models, `@JsonKey` annotated classes +- Test release build thoroughly — obfuscation can break reflection-based code + +## Notifications +- Create notification channels before showing any notification (Android 8+) +- Notification icons must be monochrome on Android 5+ diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/platform/platform-ios.mdc b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/platform/platform-ios.mdc new file mode 100644 index 0000000..8661145 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/platform/platform-ios.mdc @@ -0,0 +1,35 @@ +--- +description: "iOS-specific conventions for LegacyApp — Pillar 4" +alwaysApply: true +--- + +# iOS Platform Standards — LegacyApp + +## Platform-specific imports +- Use `dart:io` checks: `if (Platform.isIOS)` for conditional code +- iOS-only plugins: declare in `pubspec.yaml` with platform filter +- **NEVER** call `dart:io` directly in shared code — use `platform_channel` or `universal_io` + +## iOS-specific requirements +- Minimum deployment target: iOS 13.0 (or as specified in `ios/Podfile`) +- Privacy manifests: `ios/Runner/PrivacyInfo.xcprivacy` — required for App Store since 2024 +- Required `Info.plist` keys before using: + - Camera: `NSCameraUsageDescription` + - Photo library: `NSPhotoLibraryUsageDescription` + - Location: `NSLocationWhenInUseUsageDescription` + - Notifications: handled via `permission_handler` + +## Push notifications (iOS) +- Configure APNs certificates in Xcode signing & capabilities +- Request permission with `permission_handler` — show rationale screen first +- Handle foreground vs background vs terminated app states separately +- Test on a physical device — iOS simulator does not support push + +## Safe area +- Always wrap root scaffold with `SafeArea` or use `MediaQuery.of(context).padding` +- Dynamic Island / notch: test on iPhone 14 Pro and iPhone 15 Pro simulators + +## App Store compliance +- Screenshot: use `ScreenshotController` to exclude sensitive screens +- Sign in with Apple: required if any third-party social login is offered +- IPv6 compatibility required — no IPv4-only network code diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/routing/getx_nav.mdc b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/routing/getx_nav.mdc new file mode 100644 index 0000000..9637212 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/routing/getx_nav.mdc @@ -0,0 +1,32 @@ +--- +description: "GetX Navigation conventions for LegacyApp" +alwaysApply: true +--- + +# GetX Navigation — LegacyApp + +## Named routes +```dart +// app_pages.dart — central route definitions +abstract class AppPages { + static const initial = Routes.home; + static final routes = [ + GetPage(name: Routes.home, page: () => const HomeView(), binding: HomeBinding()), + GetPage(name: Routes.product, page: () => const ProductView(), binding: ProductBinding()), + ]; +} + +// Navigate — always use named routes +Get.toNamed(Routes.product, arguments: product); // push +Get.offAllNamed(Routes.home); // replace stack +Get.back(); // pop +``` + +## Bindings +- Every route has a `Binding` class that creates and injects dependencies +- **NEVER** use `Get.put()` in a widget — only in Bindings +- Use `Get.lazyPut()` for deferred creation + +## Rules +- **NEVER** use `Navigator.push/pop` +- All route strings in `lib/core/routing/routes.dart` as constants diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/security/security-standards.mdc b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/security/security-standards.mdc new file mode 100644 index 0000000..c115fc7 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/security/security-standards.mdc @@ -0,0 +1,109 @@ +--- +description: "Security standards for LegacyApp — ALWAYS APPLIED on every file write" +alwaysApply: true +--- + +# Security Standards — LegacyApp +> **Pillar 5**: Security is an always-on rule, not just a reactive agent. +> These rules apply to EVERY file write, regardless of feature or context. + +## Credential & secret management +- **NEVER** hardcode API keys, tokens, or secrets in source code +- **NEVER** commit `.env` files — use `.gitignore` and document required vars in `.env.example` +- API keys belong in: flavored `--dart-define` build args, or a secrets manager (e.g. AWS Secrets) +- Use `flutter_secure_storage` for tokens/credentials — **NEVER** `SharedPreferences` for sensitive data +- Obfuscation: enable `--obfuscate --split-debug-info=build/debug-symbols/` for release builds + +## Authentication & sessions +- JWT/session tokens: stored in `flutter_secure_storage`, never in `SharedPreferences` or local DB +- Implement token refresh with retry logic — never let a 401 show a raw error to the user +- Certificate pinning: required for production builds on JWT / REST Auth — use `dio_pinning_interceptor` +- Biometric re-auth: require for any transaction > defined threshold + +## Data & privacy +- **NEVER** log PII (names, emails, phone numbers, addresses) to console or crash reporters +- Sanitize all user inputs before sending to backend +- Use `SensitiveWidget` wrapper to exclude screens from screenshots / app switcher thumbnails +- Comply with platform privacy requirements: iOS `NSPrivacyAccessedAPITypes`, Android permissions + +## Network security +- All API calls use HTTPS — no http:// in production +- Validate SSL certificates — never bypass with `badCertificateCallback: (_,_,_) => true` +- Add a timeout to every HTTP call: `connectTimeout: Duration(seconds: 10)` +- Rate-limit sensitive endpoints: auth, OTP, password reset + +## Dependency security +- Run `dart pub outdated` monthly — flag packages with known CVEs +- Never use a package with <100 pub points without explicit tech lead approval +- Pin critical security packages to exact versions in `pubspec.yaml` + +## Secure coding patterns +```dart +// ✅ Correct: secure token storage +final storage = FlutterSecureStorage(); +await storage.write(key: 'access_token', value: token); + +// ❌ Wrong: SharedPreferences for sensitive data +final prefs = await SharedPreferences.getInstance(); +prefs.setString('access_token', token); // NEVER do this + +// ✅ Correct: PII-safe logging +logger.info('User authenticated: userId=${user.id}'); // ok — id, not email +logger.debug('Payment processed: orderId=$orderId'); // ok — no card data + +// ❌ Wrong: PII in logs +logger.info('Login: email=${user.email}, phone=${user.phone}'); // NEVER + +// ✅ Correct: --dart-define for secrets (build arg, not in code) +// flutter build apk --dart-define=API_KEY=$API_KEY +const apiKey = String.fromEnvironment('API_KEY'); // acceptable + +// ✅ Correct: certificate pinning with Dio +(dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () { + final client = HttpClient(); + client.badCertificateCallback = (cert, host, port) => false; // strict + return client; +}; +``` + +## Deep linking & intent security +- Validate all incoming deep link parameters — never trust raw URL params +- Use `app_links` package for verified deep link handling +- Restrict URL schemes to known patterns — reject unknown schemes +- For OAuth callbacks: validate `state` parameter to prevent CSRF + +## Storage security +- Encrypt locally cached sensitive data using `hive` with `HiveAesCipher` +- Clear sensitive data from memory after use (set to null, trigger GC) +- Do NOT cache auth tokens in network layer (no `dio_cache_interceptor` for auth endpoints) +- Use `SecureRandom` for nonce/token generation — never `Random()` + +## Code obfuscation & binary protection +```yaml +# android/app/build.gradle — release config +buildTypes { + release { + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } +} +``` +```bash +# Flutter release build with obfuscation +flutter build apk --release --obfuscate --split-debug-info=build/debug-symbols/ +flutter build ipa --release --obfuscate --split-debug-info=build/debug-symbols/ +``` + +## Release checklist +Before every production release, verify: +- [ ] No hardcoded secrets (`grep -r "api_key\|secret\|password\|token" lib/`) +- [ ] Debug flags disabled (`kDebugMode` guards on all debug-only paths) +- [ ] Obfuscation enabled in release build config +- [ ] Certificate pinning active and tested on both platforms +- [ ] Crash reporter PII filters configured (Sentry `beforeSend`, Firebase `setConsentType`) +- [ ] `flutter_secure_storage` used for all tokens — no `SharedPreferences` for sensitive keys +- [ ] Network calls all HTTPS — scan for `http://` in lib/ +- [ ] Dependencies audited: `dart pub outdated --json | jq '.packages[] | select(.isDiscontinued)'` +- [ ] Deep link parameters validated +- [ ] App transport security / network security config reviewed diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/state-management/getx.mdc b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/state-management/getx.mdc new file mode 100644 index 0000000..b8bb897 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/state-management/getx.mdc @@ -0,0 +1,67 @@ +--- +description: "GetX conventions for LegacyApp (legacy — migration available)" +alwaysApply: true +--- + +# GetX Standards — LegacyApp +> ⚠️ This project uses GetX. See `migration-agent` for incremental migration to Riverpod. + +## Controller structure +```dart +class ProductsController extends GetxController { + final ProductRepository _repo; + ProductsController(this._repo); + + final RxList products = [].obs; + final RxBool isLoading = false.obs; + final Rx error = Rx(null); + + @override + void onInit() { + super.onInit(); + fetchProducts(); + } + + Future fetchProducts() async { + isLoading.value = true; + error.value = null; + try { + products.value = await _repo.getProducts(); + } catch (e) { + error.value = e.toString(); + } finally { + isLoading.value = false; + } + } +} +``` + +## View pattern +```dart +// Views extend GetView — never GetWidget or raw StatelessWidget +class ProductsView extends GetView { + @override + Widget build(BuildContext context) { + return Scaffold( + body: Obx(() { + if (controller.isLoading.value) return const ProductShimmer(); + if (controller.error.value != null) return ErrorWidget(controller.error.value!); + return ProductList(controller.products); + }), + ); + } +} +``` + +## Rules +- **NEVER** pass `BuildContext` into a controller +- Use `Binding` classes for dependency injection — never `Get.put()` in a widget +- Use `.obs` for all reactive state — never call `update()` on non-observable state +- Use `Get.find()` only in `Binding` classes, not in widgets +- **No business logic in Views** — controllers handle all logic + +## File locations in LegacyApp +- `lib/features/[feature]/views/[feature]_view.dart` +- `lib/features/[feature]/controllers/[feature]_controller.dart` +- `lib/features/[feature]/bindings/[feature]_binding.dart` +- `lib/features/[feature]/models/[feature]_model.dart` diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/testing/testing-getx.mdc b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/testing/testing-getx.mdc new file mode 100644 index 0000000..f20826c --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/testing/testing-getx.mdc @@ -0,0 +1,40 @@ +--- +description: "GetX testing conventions for LegacyApp" +alwaysApply: false +--- + +# GetX Testing Standards — LegacyApp + +## Test pattern +```dart +void main() { + late ProductsController controller; + late MockProductRepository mockRepo; + + setUp(() { + mockRepo = MockProductRepository(); + Get.testMode = true; + controller = Get.put(ProductsController(mockRepo)); + }); + + tearDown(() => Get.deleteAll()); + + test('loads products on init', () async { + when(() => mockRepo.getProducts()).thenAnswer((_) async => [fakeProduct]); + await controller.fetchProducts(); + expect(controller.products, [fakeProduct]); + expect(controller.isLoading.value, false); + }); + + testWidgets('ProductsView shows shimmer while loading', (tester) async { + controller.isLoading.value = true; + await tester.pumpWidget(GetMaterialApp(home: ProductsView())); + expect(find.byType(ProductShimmer), findsOneWidget); + }); +} +``` + +## Rules +- Use `Get.testMode = true` in setUp +- Always call `Get.deleteAll()` in tearDown +- Wrap widget tests in `GetMaterialApp`, not `MaterialApp` diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/universal/flutter-core.mdc b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/universal/flutter-core.mdc new file mode 100644 index 0000000..d54135b --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/universal/flutter-core.mdc @@ -0,0 +1,40 @@ +--- +description: "Core Flutter conventions for LegacyApp — always applied" +alwaysApply: true +--- + +# Flutter Core Standards — LegacyApp + +## Const and performance +- Use `const` constructors wherever possible — compile-time guarantee of no rebuild +- Prefer `const` widgets at the leaf level: `const SizedBox.shrink()`, `const Padding(...)` +- Never use `const` with mutable values; lint: `prefer_const_constructors` is enabled + +## Null safety +- Never use `!` (bang operator) unless you have a 100% safe runtime guarantee with a comment +- Prefer `??`, `?.`, and `if (x != null)` guards +- Use `required` for all non-nullable named parameters +- Never use `late` without a guarantee of initialisation before first access + +## Widget lifecycle +- Override `dispose()` in every `StatefulWidget` that uses controllers, streams, or timers +- Cancel `StreamSubscription` in `dispose()`, not in `didUpdateWidget` +- Use `WidgetsBinding.instance.addPostFrameCallback` for post-build logic, not `Future.delayed(Duration.zero)` + +## Naming conventions +- Files: `snake_case.dart` +- Classes: `PascalCase` +- Variables/functions: `camelCase` +- Constants: `kCamelCase` or `SCREAMING_SNAKE` for true compile-time constants +- Private members: `_camelCase` + +## Imports +- Order: dart: → package: → relative +- Use relative imports within a feature; absolute for cross-feature +- Never import a feature's internal files from outside that feature + +## Code quality +- Max function length: 40 lines. Extract widgets and helpers aggressively +- No `print()` in production code — use a logging package +- All `TODO:` comments must include a ticket number: `// TODO: PROJ-123 — fix this` +- Run `dart format` before every commit diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/universal/project-context.mdc b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/universal/project-context.mdc new file mode 100644 index 0000000..3b188bd --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/universal/project-context.mdc @@ -0,0 +1,66 @@ +--- +description: "Project context for LegacyApp — always applied" +alwaysApply: true +--- + +# Project Context — LegacyApp + +## Project identity +- **Name:** LegacyApp +- **Package:** com.test.legacy +- **Description:** Legacy GetX app +- **Scale:** medium + +## Technology stack +- **State management:** GetX +- **Architecture:** MVC +- **Routing:** GetX Navigation +- **Backend:** REST API +- **Auth:** JWT / REST Auth +- **Platforms:** ios, android +- **Code generation:** none + +## Feature modules +auth, dashboard + +## Special capabilities + + +## Environments / flavors +- Flavors: dev, prod +- CI/CD: Codemagic + +## Design & API references +- Design source: none +- API docs: openapi at `docs/api.yaml` + +## Code references +### Git repositories +_No Git repository URLs listed._ Add entries under `references.repos` in project-brief.yaml when other repos are part of the product context. + +### Local paths +_No local paths listed._ Add monorepo packages or sibling folders under `references.local_paths` in project-brief.yaml when relevant. + +## Product UX / themes & roles +- **Theme variants:** light, dark +- **Roles:** Not enabled (`app_context.roles_enabled: false`). + + +## Reviews — which rule owns what +- **Theme, colors, typography, spacing/radius tokens** → `ui-ux-standards.mdc` (widgets read `Theme.of(context)` only) +- **User-visible copy & locales** → `localization.mdc` (ARB / `AppLocalizations`; no UI string literals) +- **Imports, structure, naming** → `flutter-core.mdc` + architecture rule + +## Architecture boundaries +- View (Widget) MUST NOT contain business logic +- Controller MUST NOT import Flutter widgets directly +- Model MUST be plain Dart, no framework dependencies + +## When generating code for this project +1. Always use GetX patterns — never suggest alternatives +2. Always follow MVC folder structure +3. Always use GetX Navigation for navigation — never `Navigator.push` directly +4. Always target platforms: ios, android +5. If code generation tools are used (none), follow their conventions +6. Apply visuals only through theme (`ColorScheme`, `TextTheme`, `ThemeExtension`) — never ad-hoc colors/fonts in feature widgets +7. No user-facing string literals in widgets — l10n or shared constants per localization rule diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/universal/ui-ux-standards.mdc b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/universal/ui-ux-standards.mdc new file mode 100644 index 0000000..cc118d2 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/rules/universal/ui-ux-standards.mdc @@ -0,0 +1,48 @@ +--- +description: "UI/UX standards for LegacyApp — always applied" +alwaysApply: true +--- + +# UI / UX Standards — LegacyApp + +## Theme & design tokens (single source of truth) +- Define **one** light/dark `ThemeData` (and optional `ThemeExtension`s for brand spacing, radii, semantic colors). Feature code reads `Theme.of(context)` only. +- **Colors:** `colorScheme` / extensions — never hex/`Color(...)` literals in widgets except inside the theme definition file(s). +- **Typography:** `textTheme` / `primaryTextTheme` — never raw `TextStyle(fontSize:, fontFamily:)` in feature UI. +- **Spacing & shapes:** `ThemeExtension` or documented constants consumed consistently — avoid one-off magic numbers for padding/radius. + +## Loading states +- Every async operation MUST show a loading skeleton (shimmer), NOT a spinner unless < 300ms +- Use `shimmer` package with a shimmer that matches the final layout shape +- Never show a blank screen during loading — skeleton must fill the same space as the content + +## Empty states +- Every list/grid MUST have a distinct empty state widget: illustration + headline + CTA +- Empty state is different from error state — never reuse the same widget for both +- Empty state copy: positive framing ("No items yet — add your first one") + +## Error states +- Every async failure MUST show: error message + retry button +- Never swallow errors silently +- Error text: user-friendly, never expose stack traces or raw API messages + +## Navigation & transitions +- Use `IndexedStack` for bottom nav tabs — preserves scroll position +- Named routes only — never `Navigator.push(context, MaterialPageRoute(...))` +- Page transitions: use `CustomTransitionPage` with `FadeTransition` for modal sheets + +## Responsive layout +- Use `LayoutBuilder` or `MediaQuery` for breakpoints, not hardcoded pixel values +- Minimum touch target: 48×48 logical pixels (Material guideline) +- Test on 375px (iPhone SE) and 414px (iPhone Pro Max) widths minimum + +## Haptics +- Use `HapticFeedback.lightImpact()` on primary CTAs +- Use `HapticFeedback.selectionClick()` on toggle/checkbox interactions +- Never add haptics to destructive actions without confirmation + +## Accessibility +- All interactive widgets must have a `Semantics` label or `tooltip` +- Minimum contrast ratio: 4.5:1 (WCAG AA) +- Test with TalkBack / VoiceOver before each release + diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/skills/create-flavor/SKILL.md b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/skills/create-flavor/SKILL.md new file mode 100644 index 0000000..7d5bf8e --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/skills/create-flavor/SKILL.md @@ -0,0 +1,18 @@ +# Create Build Flavor — LegacyApp + +Creates a new build flavor/environment. Current flavors: dev, prod. + +## Usage +``` +Create a new flavor called [flavor_name] +``` + +## What gets created +1. Android: new `productFlavors` block in `android/app/build.gradle` +2. iOS: new scheme + configuration in Xcode (instructions provided) +3. `lib/core/config/[flavor_name]_config.dart` +4. `.env.[flavor_name]` template +5. Codemagic pipeline update + +## CI/CD: Codemagic +Generate the pipeline config snippet for the new flavor in Codemagic format. diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/skills/deploy/SKILL.md b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/skills/deploy/SKILL.md new file mode 100644 index 0000000..13a2066 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/skills/deploy/SKILL.md @@ -0,0 +1,21 @@ +# Deploy — LegacyApp + +Guides through deployment to Codemagic for any of the flavors: dev, prod. + +## Usage +``` +Deploy [flavor] to [store/environment] +``` + +## Codemagic pipeline +The AI will generate or update the codemagic configuration file for: +- Build signing +- Running tests +- Distributing to Firebase App Distribution (beta) or App Store / Play Store (prod) + +## Pre-deploy checklist +- [ ] Version bump in `pubspec.yaml` +- [ ] Obfuscation enabled for prod: `--obfuscate --split-debug-info=build/debug-symbols/` +- [ ] No debug flags in production code +- [ ] Security checklist from `security-standards.mdc` passed +- [ ] `cursor_gen --validate` passes diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/skills/generate-api-client/SKILL.md b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/skills/generate-api-client/SKILL.md new file mode 100644 index 0000000..91e3ee2 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/skills/generate-api-client/SKILL.md @@ -0,0 +1,16 @@ +# Generate API Client — LegacyApp + +Generates type-safe API clients from openapi spec at `docs/api.yaml`. + +## Usage +``` +Generate API client for [endpoint or resource name] +``` + +## Generated files +1. **DTO** (`data/models/[resource]_dto.dart`) — request/response models with json_serializable +2. **DataSource** (`data/datasources/[resource]_remote_datasource.dart`) — Dio calls with error handling +3. **Repository impl** (`data/repositories/[resource]_repository_impl.dart`) + +## After generation +Run: `dart run build_runner build --delete-conflicting-outputs` diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/skills/generate-tests/SKILL.md b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/skills/generate-tests/SKILL.md new file mode 100644 index 0000000..6ef1e5e --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/skills/generate-tests/SKILL.md @@ -0,0 +1,41 @@ +# Generate Tests — LegacyApp + +Generates comprehensive unit, widget, and integration tests for **GetX** patterns. + +## Usage +``` +Generate tests for [ClassName or file path] +``` + +## Test generation process +1. Read the source file completely +2. Identify all testable units (public methods, state transitions, UI states) +3. Generate tests following this pattern: + ``` + Get.put(MyController()); final ctrl = Get.find(); expect(ctrl.value, expected); + ``` +4. Create mocks with `mocktail` for all dependencies +5. Place test file at `test/[mirror of source path]_test.dart` + +## Coverage targets +- Business logic (UseCases, Repositories, BLoC/Notifiers): **80% minimum** +- Widget tests: all three states (loading/error/data) must be tested +- E2E: only critical user flows + +## Test file structure +```dart +void main() { + // Setup + group('[ClassName]', () { + // Happy path tests + group('success cases', () { ... }); + // Error path tests + group('error cases', () { ... }); + // Edge cases + group('edge cases', () { ... }); + }); +} +``` + +## Naming convention +`'given [precondition], when [action], then [expected outcome]'` diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/skills/scaffold-feature/SKILL.md b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/skills/scaffold-feature/SKILL.md new file mode 100644 index 0000000..8233b3e --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/skills/scaffold-feature/SKILL.md @@ -0,0 +1,69 @@ +# Scaffold Feature — LegacyApp + +Scaffolds a complete new feature module following **MVC** architecture with **GetX** state management. + +## Usage +``` +Create a feature called [feature_name] with [description] +``` + +## What gets generated + +### For MVC architecture: +The AI will create all necessary files for the `[feature_name]` feature following MVC patterns. + +### File structure to create: + +**Clean Architecture:** +``` +lib/features/[feature_name]/ + domain/ + entities/[feature_name].dart + repositories/[feature_name]_repository.dart + usecases/ + get_[feature_name]_usecase.dart + create_[feature_name]_usecase.dart + data/ + models/[feature_name]_dto.dart + datasources/[feature_name]_remote_datasource.dart + repositories/[feature_name]_repository_impl.dart + presentation/ + [state_files]/ ← based on GetX + pages/[feature_name]_page.dart + widgets/ +``` + +**Feature-First:** +``` +lib/features/[feature_name]/ + [feature_name]_screen.dart + [feature_name]_provider.dart ← or [feature_name]_bloc.dart + [feature_name]_repository.dart + [feature_name]_model.dart + widgets/ +``` + +## State management boilerplate + +### For GetX: +Generate the appropriate state management files with: +- Initial/loading/success/error states +- All necessary events (for BLoC) +- Repository connection +- Dependency injection registration + +## Steps the AI takes +1. Ask: "What is the feature name and brief description?" +2. Ask: "What data does this feature manage? (e.g., list of products, single user profile)" +3. Generate all files with correct imports and patterns +4. Add the feature to the DI container +5. Add the route to GetX Navigation router +6. Create a placeholder test file + +## Code generation +{{#if codegen_freezed}} +- Generate Freezed model: run `dart run build_runner build` after scaffolding +{{/if}} +{{#if codegen_injectable}} +- Register in injectable: add `@lazySingleton` to repository +{{/if}} diff --git a/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/skills/scaffold-screen/SKILL.md b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/skills/scaffold-screen/SKILL.md new file mode 100644 index 0000000..58117cc --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/getx-mvc-rest/skills/scaffold-screen/SKILL.md @@ -0,0 +1,61 @@ +# Scaffold Screen — LegacyApp + +Creates a complete screen widget with all states handled, following **GetX** patterns. + +## Usage +``` +Create a screen for [screen_name] that shows [content description] +``` + +## Generated screen template + +### GetX screen pattern: + +**BLoC:** +```dart +class [ScreenName]Screen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocBuilder<[Feature]Bloc, [Feature]State>( + builder: (context, state) => switch (state) { + [Feature]Initial() || [Feature]Loading() => const [ScreenName]Shimmer(), + [Feature]Loaded(:final data) => [ScreenName]Content(data: data), + [Feature]Empty() => const [ScreenName]EmptyState(), + [Feature]Error(:final message) => [ScreenName]ErrorState( + message: message, + onRetry: () => context.read<[Feature]Bloc>().add(const [Feature]LoadRequested()), + ), + }, + ); + } +} +``` + +**Riverpod:** +```dart +class [ScreenName]Screen extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch([feature]Provider); + return state.when( + loading: () => const [ScreenName]Shimmer(), + data: (data) => [ScreenName]Content(data: data), + error: (e, _) => [ScreenName]ErrorState(error: e), + ); + } +} +``` + +## Required sub-widgets to generate +1. `[ScreenName]Shimmer` — skeleton loading layout matching final content +2. `[ScreenName]EmptyState` — illustration + headline + CTA button +3. `[ScreenName]ErrorState` — error message + retry button +4. `[ScreenName]Content` — the actual data display + +## Platform considerations (ios, android) +{{#if platform_web}} +- Web: ensure no dart:io usage; test at 375px and 1280px widths +{{/if}} +{{#if platform_desktop}} +- Desktop: add keyboard shortcut support for primary actions +{{/if}} diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/agents/code-reviewer.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/agents/code-reviewer.mdc new file mode 100644 index 0000000..965ec59 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/agents/code-reviewer.mdc @@ -0,0 +1,53 @@ +--- +name: code-reviewer +description: "Reviews TaskFlow code for Riverpod patterns, Feature-First boundaries, and Supabase usage. Ask: 'Review this code' or '@code-reviewer check PR'" +model: claude-opus-4-5 +context: auto +allowed-tools: [read_file, list_files] +--- + +You are a senior Flutter engineer reviewing code for **TaskFlow**. + +## Your review checklist + +### Riverpod patterns +- Are state classes immutable and sealed? +- Is state management correctly separated from UI logic? +- Are streams/subscriptions properly cancelled in dispose()? +- Check for anti-patterns specific to Riverpod + +### Feature-First boundaries +- features/auth/ MUST NOT import from features/home/ etc. +- Shared code lives in core/ or shared/ only +- Each feature is self-contained: screen + provider + model + repo +- Flag any violation of these import rules immediately + +### Supabase usage +- Are all exceptions caught and mapped to domain errors? +- Are streams vs futures used appropriately? +- Are connections/subscriptions disposed correctly? + +### Security (always check) +- No hardcoded API keys or secrets +- No PII logged to console or crash reporters +- Sensitive data using flutter_secure_storage, not SharedPreferences +- All user inputs validated before sending to backend + +### General Flutter +- `const` used where possible +- `dispose()` overridden for all controllers/subscriptions +- No `print()` in production paths +- Loading/empty/error states all handled + +## Output format +For each issue found: +``` +[SEVERITY: critical/major/minor] File:line — Issue description +WHY: Why this matters +FIX: Specific fix recommendation +``` + +Severity guide: +- **critical**: Security issue, data loss risk, crash potential +- **major**: Incorrect pattern, boundary violation, missing error handling +- **minor**: Style, naming, optimization opportunity diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/agents/security-agent.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/agents/security-agent.mdc new file mode 100644 index 0000000..558aa21 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/agents/security-agent.mdc @@ -0,0 +1,47 @@ +--- +name: security-agent +description: "Deep security review for TaskFlow. Consult for auth flows, payment screens, and sensitive data handling. Ask: '@security-agent review auth flow'" +model: claude-opus-4-5 +context: fork +allowed-tools: [read_file, list_files] +--- + +You are a mobile security expert conducting a deep review for **TaskFlow**. + +> Note: This agent provides deep security analysis. +> The `security-standards.mdc` rule provides always-on enforcement. +> This agent is for detailed consultations on specific security concerns. + +## Deep review focus areas + +### Auth flow (Supabase Auth) +- Token storage: is `flutter_secure_storage` used for ALL tokens? +- Token refresh: is refresh handled atomically (no race condition)? +- Session expiry: does the app handle 401 gracefully without data loss? +- Certificate pinning: configured and tested? + +### Data at rest +- SQLite/Hive encryption: sensitive DBs encrypted? +- Cache poisoning: cached API responses validated before use? +- Keychain/Keystore usage for cryptographic keys + +### Network security +- All endpoints HTTPS — any http:// URLs? +- Certificate validation — any `badCertificateCallback: true`? +- Sensitive data in URL params/query strings? +- Request/response logging in production? (must be off) + +### Code injection risks +- Dynamic code execution patterns +- WebView usage — JavaScript interface security +- Deep link parameter validation (no path traversal) + +## Output format +For each finding: +``` +[RISK: Critical/High/Medium/Low] +LOCATION: File / function +ISSUE: Detailed description +CVSS-like impact: Confidentiality/Integrity/Availability +REMEDIATION: Specific code fix +``` diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/agents/test-writer.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/agents/test-writer.mdc new file mode 100644 index 0000000..c274acf --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/agents/test-writer.mdc @@ -0,0 +1,33 @@ +--- +name: test-writer +description: "Writes Riverpod unit tests for TaskFlow. Ask: 'Write tests for [class]' or '@test-writer generate tests'" +model: claude-sonnet-4-20250514 +context: auto +allowed-tools: [read_file, write_file, list_files] +--- + +You are a Flutter test engineer for **TaskFlow** using **Riverpod**. + +## Test pattern to follow +```dart +final container = ProviderContainer(overrides: [myProvider.overrideWithValue(mockValue)]); addTearDown(container.dispose); +``` + +## When asked to write tests: +1. Read the source file completely +2. Identify all public methods and state transitions +3. Write tests for: + - Happy path (successful operation) + - Error path (failure, exception handling) + - Edge cases (empty data, boundary values) +4. Use `mocktail` for all mocking +5. Follow `Given/When/Then` naming: `'given X, when Y, then emits Z'` + +## File placement +- Unit tests: `test/features/[feature]/[file]_test.dart` +- Widget tests: `test/features/[feature]/[screen]_widget_test.dart` +- Coverage target: 80% minimum for business logic classes + +## Output +Write the complete test file, ready to run. Include all imports. +After writing, run: `dart test path/to/test_file.dart` diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/agents/ui-validator.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/agents/ui-validator.mdc new file mode 100644 index 0000000..ab80734 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/agents/ui-validator.mdc @@ -0,0 +1,47 @@ +--- +name: ui-validator +description: "Validates TaskFlow UI against design system and UX standards. Ask: 'Validate this screen' or '@ui-validator check'" +model: claude-sonnet-4-20250514 +context: auto +allowed-tools: [read_file, list_files] +--- + +You are a UI/UX validator for **TaskFlow**. + +## Validate every screen for: + +### State coverage (Riverpod) +- Loading state: shows shimmer (NOT spinner unless brief) +- Empty state: shows illustration + CTA (NOT blank screen) +- Error state: shows message + retry button (NOT toast-only) +- Data state: renders content correctly + +### Accessibility +- All interactive widgets have semantic labels +- Minimum touch targets: 48×48dp +- Sufficient color contrast (4.5:1 minimum) + +### Responsive layout +- No hardcoded pixel widths +- Tested at 375px and 414px viewport widths +- `SafeArea` used correctly on iOS + +### Platform-specific (ios, android, web) +{{#if web}} +- No dart:io imports in web-targeted code +- PWA-compatible (no native-only APIs without fallbacks) +{{/if}} +{{#if ios}} +- Safe area respected (notch, Dynamic Island) +- iOS Human Interface Guidelines followed +{{/if}} + +### Security (UI layer) +- No credentials shown in plaintext +- Sensitive screens wrapped with screenshot prevention + +## Output +List each violation with: +- Location (file:widget) +- What's wrong +- How to fix it diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/hooks/arch-guard.ts b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/hooks/arch-guard.ts new file mode 100644 index 0000000..d7aa080 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/hooks/arch-guard.ts @@ -0,0 +1,141 @@ +#!/usr/bin/env ts-node +// arch-guard.ts — Pre-commit hook: enforces Feature-First import boundaries +// Generated for TaskFlow (Feature-First architecture) + +import * as fs from 'fs'; +import * as path from 'path'; +import { execSync } from 'child_process'; + +const RED = '\x1b[31m'; +const YELLOW = '\x1b[33m'; +const GREEN = '\x1b[32m'; +const RESET = '\x1b[0m'; + +interface ArchRule { + sourcePattern: RegExp; + forbiddenImports: RegExp[]; + message: string; +} + +// Architecture-specific rules for Feature-First +const ARCH_RULES: ArchRule[] = [ + ...getArchRules() +]; + +function getArchRules(): ArchRule[] { + const arch = 'feature_first'; + + if (arch === 'clean') { + return [ + { + sourcePattern: /features\/\w+\/domain\//, + forbiddenImports: [/features\/\w+\/data\//, /features\/\w+\/presentation\//], + message: 'domain/ must not import from data/ or presentation/', + }, + { + sourcePattern: /features\/\w+\/presentation\//, + forbiddenImports: [/features\/\w+\/data\//], + message: 'presentation/ must not import from data/ directly (use domain interfaces)', + }, + ]; + } + + if (arch === 'feature_first') { + return [ + { + // A feature file must not import from another feature + sourcePattern: /features\/(\w+)\//, + forbiddenImports: [], // checked dynamically below + message: 'Features must not import from other feature folders', + }, + ]; + } + + if (arch === 'mvvm') { + return [ + { + sourcePattern: /viewmodels\//, + forbiddenImports: [/package:flutter\//, /widgets\//], + message: 'ViewModels must not import Flutter widgets — they must be plain Dart testable', + }, + ]; + } + + return []; +} + +function getChangedDartFiles(): string[] { + try { + const result = execSync('git diff --cached --name-only --diff-filter=ACM', { encoding: 'utf8' }); + return result.split('\n').filter(f => f.endsWith('.dart') && fs.existsSync(f)); + } catch { + return []; + } +} + +function checkFile(filePath: string): string[] { + const violations: string[] = []; + const content = fs.readFileSync(filePath, 'utf8'); + const imports = content.match(/^import\s+'[^']+'/gm) ?? []; + + for (const rule of ARCH_RULES) { + if (!rule.sourcePattern.test(filePath)) continue; + + // Feature-first cross-feature check + if ('feature_first' === 'feature_first') { + const srcFeatureMatch = filePath.match(/features\/(\w+)\//); + if (srcFeatureMatch) { + const srcFeature = srcFeatureMatch[1]; + for (const imp of imports) { + const impFeatureMatch = imp.match(/features\/(\w+)\//); + if (impFeatureMatch && impFeatureMatch[1] !== srcFeature) { + violations.push( + `${RED}ARCH VIOLATION${RESET} in ${filePath}:\n` + + ` ${YELLOW}${rule.message}${RESET}\n` + + ` Import: ${imp}\n` + + ` Fix: Move shared code to core/ or shared/` + ); + } + } + } + continue; + } + + // Standard forbidden import check + for (const imp of imports) { + for (const forbidden of rule.forbiddenImports) { + if (forbidden.test(imp)) { + violations.push( + `${RED}ARCH VIOLATION${RESET} in ${filePath}:\n` + + ` ${YELLOW}${rule.message}${RESET}\n` + + ` Import: ${imp}` + ); + } + } + } + } + + return violations; +} + +const changedFiles = getChangedDartFiles(); +if (changedFiles.length === 0) { + console.log(`${GREEN}✔ arch-guard: no Dart files changed${RESET}`); + process.exit(0); +} + +console.log(`🏛 arch-guard checking ${changedFiles.length} file(s) for Feature-First violations...`); + +const allViolations: string[] = []; +for (const file of changedFiles) { + allViolations.push(...checkFile(file)); +} + +if (allViolations.length > 0) { + console.error(`\n${RED}✘ Architecture boundary violations detected:${RESET}\n`); + for (const v of allViolations) console.error(v + '\n'); + console.error(`Total: ${allViolations.length} violation(s). Fix before committing.`); + process.exit(1); +} + +console.log(`${GREEN}✔ arch-guard: no violations found${RESET}`); diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/hooks/flutter-analyze.ts b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/hooks/flutter-analyze.ts new file mode 100644 index 0000000..a8918a7 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/hooks/flutter-analyze.ts @@ -0,0 +1,32 @@ +#!/usr/bin/env ts-node +// flutter-analyze.ts — Pre-commit hook: runs dart analyze and dart format check +import { execSync } from 'child_process'; + +const RED = '\x1b[31m'; +const GREEN = '\x1b[32m'; +const RESET = '\x1b[0m'; + +function run(cmd: string): { stdout: string; code: number } { + try { + const stdout = execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }); + return { stdout, code: 0 }; + } catch (e: any) { + return { stdout: e.stdout ?? e.message, code: e.status ?? 1 }; + } +} + +console.log('🔍 Running flutter analyze...'); +const analyze = run('flutter analyze --no-congratulate'); +if (analyze.code !== 0) { + console.error(`${RED}✘ flutter analyze failed:\n${analyze.stdout}${RESET}`); + process.exit(1); +} + +console.log('🎨 Checking dart format...'); +const format = run('dart format --output=none --set-exit-if-changed lib/ test/'); +if (format.code !== 0) { + console.error(`${RED}✘ Unformatted files detected. Run: dart format lib/ test/${RESET}`); + process.exit(1); +} + +console.log(`${GREEN}✔ flutter analyze + dart format passed${RESET}`); diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/hooks/grind-tests.ts b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/hooks/grind-tests.ts new file mode 100644 index 0000000..dd6385c --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/hooks/grind-tests.ts @@ -0,0 +1,16 @@ +#!/usr/bin/env ts-node +// grind-tests.ts — Pre-push hook: runs flutter test +import { execSync } from 'child_process'; + +const RED = '\x1b[31m'; +const GREEN = '\x1b[32m'; +const RESET = '\x1b[0m'; + +console.log('🧪 Running flutter test...'); +try { + execSync('flutter test --coverage', { stdio: 'inherit' }); + console.log(`${GREEN}✔ All tests passed${RESET}`); +} catch { + console.error(`${RED}✘ Tests failed. Fix failing tests before pushing.${RESET}`); + process.exit(1); +} diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/hooks/hooks.json b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/hooks/hooks.json new file mode 100644 index 0000000..e74d712 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/hooks/hooks.json @@ -0,0 +1,19 @@ +{ + "hooks": [ + { + "name": "pre-commit: flutter analyze", + "command": "npx ts-node .cursor/hooks/flutter-analyze.ts", + "events": ["pre-commit"] + }, + { + "name": "pre-commit: arch guard", + "command": "npx ts-node .cursor/hooks/arch-guard.ts", + "events": ["pre-commit"] + }, + { + "name": "pre-push: run tests", + "command": "npx ts-node .cursor/hooks/grind-tests.ts", + "events": ["pre-push"] + } + ] +} diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/architecture/feature_first.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/architecture/feature_first.mdc new file mode 100644 index 0000000..9a6ac24 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/architecture/feature_first.mdc @@ -0,0 +1,51 @@ +--- +description: "Feature-First architecture conventions for TaskFlow" +alwaysApply: true +--- + +# Feature-First Architecture — TaskFlow + +## Folder structure +``` +lib/ + features/ + auth/ + auth_screen.dart + auth_provider.dart (or auth_bloc.dart) + auth_repository.dart + auth_model.dart + widgets/ + login_form.dart + social_login_button.dart + home/ + home_screen.dart + home_provider.dart + ... + core/ + di/ ← Dependency injection setup + network/ ← HTTP client, interceptors + storage/ ← Local storage abstraction + widgets/ ← Shared widgets used by 3+ features + theme/ ← App theme, colors, text styles + routing/ ← Router config + shared/ + models/ ← Models shared across features + utils/ ← Pure utility functions +``` + +## Import rules (STRICTLY ENFORCED by arch-guard hook) +- features/auth/ MUST NOT import from features/home/ etc. +- Shared code lives in core/ or shared/ only +- Each feature is self-contained: screen + provider + model + repo + +## Feature isolation rules +- A feature folder is self-contained: its own screen, state, model, repository +- Features MUST NOT import from other feature folders +- Shared code MUST be extracted to `core/` or `shared/` before sharing +- The 3-feature rule: if 3+ features need the same widget → move it to `core/widgets/` + +## File naming within a feature +- `[feature]_screen.dart` — the main screen widget +- `[feature]_provider.dart` — Riverpod providers (or `[feature]_bloc.dart`) +- `[feature]_repository.dart` — data fetching + caching +- `[feature]_model.dart` — data model (Freezed or plain Dart) diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/backend/supabase.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/backend/supabase.mdc new file mode 100644 index 0000000..7ffae43 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/backend/supabase.mdc @@ -0,0 +1,52 @@ +--- +description: "Supabase conventions for TaskFlow" +alwaysApply: true +--- + +# Supabase Standards — TaskFlow + +## Row Level Security (RLS) awareness +- **ALWAYS** assume RLS is enabled — never write queries that assume full table access +- Test queries in the Supabase dashboard before implementing in Flutter +- If a query returns empty unexpectedly, check RLS policies first + +## Queries +```dart +// ✅ Type-safe query with error handling +Future> getProducts() async { + final response = await _supabase + .from('products') + .select('id, name, price, category_id, categories(name)') + .eq('is_active', true) + .order('created_at', ascending: false) + .limit(50); + return response.map(Product.fromMap).toList(); +} +``` + +## Realtime subscriptions +```dart +StreamSubscription>>? _sub; + +void watchOrders(String userId) { + _sub = _supabase + .from('orders') + .stream(primaryKey: ['id']) + .eq('user_id', userId) + .listen( + (data) => _updateOrders(data), + onError: (e) => _handleError(e), + ); +} + +@override +void dispose() { + _sub?.cancel(); // ALWAYS cancel in dispose() + super.dispose(); +} +``` + +## Auth session +- Use `supabase.auth.onAuthStateChange` stream — never poll auth state +- Persist session: `supabase-flutter` handles this automatically via secure storage +- `session.accessToken` expires — check `session.isExpired` before sensitive operations diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/codegen/codegen-freezed.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/codegen/codegen-freezed.mdc new file mode 100644 index 0000000..713fc6f --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/codegen/codegen-freezed.mdc @@ -0,0 +1,64 @@ +--- +description: "Freezed code generation conventions for TaskFlow — Pillar 4" +alwaysApply: true +--- + +# Freezed Standards — TaskFlow + +## When to use Freezed +- All domain entities and data models — use `@freezed` +- Union types / sealed classes — use `@freezed` with multiple constructors +- BLoC states and events — use `@freezed` + +## Model pattern +```dart +@freezed +class Product with _$Product { + const factory Product({ + required String id, + required String name, + required double price, + @Default(0) int stockCount, + String? imageUrl, + }) = _Product; + + factory Product.fromJson(Map json) => _$ProductFromJson(json); +} +``` + +## Union type pattern +```dart +@freezed +sealed class ProductState with _$ProductState { + const factory ProductState.initial() = ProductInitial; + const factory ProductState.loading() = ProductLoading; + const factory ProductState.loaded(List products) = ProductLoaded; + const factory ProductState.error(String message) = ProductError; +} + +// Usage — exhaustive switch +final widget = state.when( + initial: () => const SizedBox.shrink(), + loading: () => const ProductShimmer(), + loaded: (products) => ProductList(products: products), + error: (msg) => ErrorWidget(message: msg), +); +``` + +## Critical rules +- **NEVER** edit `.freezed.dart` or `.g.dart` files — they are generated +- Run `dart run build_runner build --delete-conflicting-outputs` after changes +- Run `dart run build_runner watch` during development +- Add `*.freezed.dart` and `*.g.dart` to `.gitignore` (or commit them — team must decide) +- Always define `copyWith` via Freezed — never write manual `copyWith` + +## Patterns to avoid +```dart +// ❌ Manual copyWith — replaced by Freezed +Product copyWith({String? name, double? price}) => Product( + id: id, name: name ?? this.name, price: price ?? this.price, +); + +// ✅ Let Freezed generate it +product.copyWith(name: 'New Name', price: 9.99) +``` diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/codegen/codegen-json_serializable.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/codegen/codegen-json_serializable.mdc new file mode 100644 index 0000000..48fa52d --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/codegen/codegen-json_serializable.mdc @@ -0,0 +1,37 @@ +--- +description: "json_serializable conventions for TaskFlow — Pillar 4" +alwaysApply: true +--- + +# json_serializable Standards — TaskFlow + +## Model annotation +```dart +@JsonSerializable(explicitToJson: true) // explicitToJson for nested objects +class ProductDto { + final String id; + final String name; + @JsonKey(name: 'unit_price') // snake_case API → camelCase Dart + final double unitPrice; + @JsonKey(defaultValue: false) + final bool isActive; + final DateTime createdAt; // auto-converted from ISO 8601 string + + const ProductDto({ + required this.id, + required this.name, + required this.unitPrice, + required this.isActive, + required this.createdAt, + }); + + factory ProductDto.fromJson(Map json) => _$ProductDtoFromJson(json); + Map toJson() => _$ProductDtoToJson(this); +} +``` + +## Critical rules +- **NEVER** edit `*.g.dart` files +- Use `@JsonKey(defaultValue: ...)` for nullable API fields — API contracts change +- Use `explicitToJson: true` whenever the model has nested objects +- Null safety: API fields not guaranteed to be non-null should be `String?` not `String` diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/error-handling/error-handling.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/error-handling/error-handling.mdc new file mode 100644 index 0000000..a327c2e --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/error-handling/error-handling.mdc @@ -0,0 +1,67 @@ +--- +description: "Global error handling strategy for TaskFlow — always applied" +alwaysApply: true +--- + +# Global Error Handling — TaskFlow + +## Flutter error boundaries +Configure in `main.dart` — do this ONCE and never bypass it: + +```dart +void main() { + FlutterError.onError = (details) { + FlutterError.presentError(details); + // TODO: Send to crash reporter (Sentry/Firebase Crashlytics) + crashReporter.recordFlutterError(details); + }; + + PlatformDispatcher.instance.onError = (error, stack) { + // TODO: Send to crash reporter + crashReporter.recordError(error, stack, fatal: true); + return true; + }; + + runApp(const MyApp()); +} +``` + +## Error types hierarchy +Define a sealed class for domain errors — never throw raw exceptions in business logic: + +```dart +sealed class AppError { + const AppError(); +} +class NetworkError extends AppError { final int? statusCode; const NetworkError({this.statusCode}); } +class AuthError extends AppError { final String reason; const AuthError(this.reason); } +class NotFoundError extends AppError { final String resource; const NotFoundError(this.resource); } +class UnknownError extends AppError { final Object cause; const UnknownError(this.cause); } +``` + +## Repository layer +- Wrap ALL external calls in try/catch and return `Either` or `Result` +- NEVER let raw exceptions bubble to the presentation layer +- Log at the repository layer, not the UI layer + +## Presentation layer +- Every async widget MUST handle error state explicitly — no silent failures +- Show user-friendly error messages: map `AppError` subtype → readable string +- Provide a "Try again" action for recoverable errors (network, timeout) +- For fatal errors (auth expired), redirect to login — never show a dead screen + +## Crash reporting +- Integrate Sentry or Firebase Crashlytics before first TestFlight / Play beta +- Set `user` context on crash reporter after login (id only, no PII) +- Add `breadcrumbs` for key user actions to aid reproduction + +## Logging strategy +``` +Level | Use case +DEBUG | Development only (strip from release) +INFO | Key user flows (login, purchase, etc.) +WARNING | Recoverable errors, fallbacks used +ERROR | Unrecoverable errors, unexpected states +``` +- Use `logger` package — never bare `print()` +- Logger instance per class: `final _log = Logger('ClassName');` diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/i18n/localization.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/i18n/localization.mdc new file mode 100644 index 0000000..32c795b --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/i18n/localization.mdc @@ -0,0 +1,54 @@ +--- +description: "Localization / i18n conventions for TaskFlow" +alwaysApply: true +--- + +# Localization Standards — TaskFlow + +## Supported locales: en, fr + +## Setup +- Use Flutter's built-in `AppLocalizations` (generated from `.arb` files) +- ARB files: `lib/l10n/app_en.arb`, `lib/l10n/app_fr.arb`, etc. +- Never hardcode user-facing strings — always use `context.l10n.stringKey` + +## AppLocalizations access +```dart +// In widgets: +final l10n = AppLocalizations.of(context)!; +Text(l10n.welcomeMessage) // ✅ + +// Extension for convenience: +extension LocalizationX on BuildContext { + AppLocalizations get l10n => AppLocalizations.of(this)!; +} +Text(context.l10n.welcomeMessage) // ✅ +``` + +## ARB file format +```json +{ + "welcomeMessage": "Welcome back, {name}!", + "@welcomeMessage": { + "description": "Shown on home screen after login", + "placeholders": { + "name": { "type": "String", "example": "Alice" } + } + }, + "itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}", + "@itemCount": { + "placeholders": { + "count": { "type": "int" } + } + } +} +``` + +## Rules +- **NEVER** hardcode user-facing strings as string literals in widget files +- **Technical keys** (route paths, storage/analytics keys, JSON fields): one shared module (e.g. `lib/core/constants/app_strings.dart`) — no duplicated literals across features +- All new strings added to ALL locale ARB files simultaneously — broken translations break builds +- Use ICU message format for plurals and gendered strings +- Date/time formatting: use `intl` package `DateFormat` — never `date.toString()` +- Currency: use `NumberFormat.currency(locale: locale)` — never manual formatting +- RTL support: wrap with `Directionality` where needed; test with Arabic locale diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/platform/platform-android.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/platform/platform-android.mdc new file mode 100644 index 0000000..1046443 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/platform/platform-android.mdc @@ -0,0 +1,34 @@ +--- +description: "Android-specific conventions for TaskFlow — Pillar 4" +alwaysApply: true +--- + +# Android Platform Standards — TaskFlow + +## Target SDK +- `compileSdkVersion` / `targetSdkVersion`: 34 (Android 14) minimum for new apps +- `minSdkVersion`: 21 (Android 5.0) unless brief specifies otherwise +- Update `android/app/build.gradle` when bumping target SDK + +## Permissions +- Declare only needed permissions in `AndroidManifest.xml` +- Runtime permissions: use `permission_handler` — never skip rationale step +- Android 13+: granular media permissions (`READ_MEDIA_IMAGES` not `READ_EXTERNAL_STORAGE`) +- Android 14+: `FOREGROUND_SERVICE_TYPE` required for foreground services + +## Adaptive icons +- Provide both foreground and background layers in `android/app/src/main/res/` +- Test on dark theme, coloured theme, and themed icons (Android 13+) + +## Deep links / App Links +- Verify domain ownership: `.well-known/assetlinks.json` on your server +- Test with: `adb shell am start -a android.intent.action.VIEW -d "https://TaskFlow.com/products/123"` + +## ProGuard / R8 +- Obfuscation rules in `android/app/proguard-rules.pro` +- Keep rules for: Dio, Freezed models, `@JsonKey` annotated classes +- Test release build thoroughly — obfuscation can break reflection-based code + +## Notifications +- Create notification channels before showing any notification (Android 8+) +- Notification icons must be monochrome on Android 5+ diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/platform/platform-ios.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/platform/platform-ios.mdc new file mode 100644 index 0000000..6e87f9b --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/platform/platform-ios.mdc @@ -0,0 +1,35 @@ +--- +description: "iOS-specific conventions for TaskFlow — Pillar 4" +alwaysApply: true +--- + +# iOS Platform Standards — TaskFlow + +## Platform-specific imports +- Use `dart:io` checks: `if (Platform.isIOS)` for conditional code +- iOS-only plugins: declare in `pubspec.yaml` with platform filter +- **NEVER** call `dart:io` directly in shared code — use `platform_channel` or `universal_io` + +## iOS-specific requirements +- Minimum deployment target: iOS 13.0 (or as specified in `ios/Podfile`) +- Privacy manifests: `ios/Runner/PrivacyInfo.xcprivacy` — required for App Store since 2024 +- Required `Info.plist` keys before using: + - Camera: `NSCameraUsageDescription` + - Photo library: `NSPhotoLibraryUsageDescription` + - Location: `NSLocationWhenInUseUsageDescription` + - Notifications: handled via `permission_handler` + +## Push notifications (iOS) +- Configure APNs certificates in Xcode signing & capabilities +- Request permission with `permission_handler` — show rationale screen first +- Handle foreground vs background vs terminated app states separately +- Test on a physical device — iOS simulator does not support push + +## Safe area +- Always wrap root scaffold with `SafeArea` or use `MediaQuery.of(context).padding` +- Dynamic Island / notch: test on iPhone 14 Pro and iPhone 15 Pro simulators + +## App Store compliance +- Screenshot: use `ScreenshotController` to exclude sensitive screens +- Sign in with Apple: required if any third-party social login is offered +- IPv6 compatibility required — no IPv4-only network code diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/platform/platform-web.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/platform/platform-web.mdc new file mode 100644 index 0000000..72bbbb9 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/platform/platform-web.mdc @@ -0,0 +1,45 @@ +--- +description: "Flutter Web conventions for TaskFlow — Pillar 4" +alwaysApply: true +--- + +# Flutter Web Standards — TaskFlow + +## Critical: No dart:io on web +- **NEVER** import `dart:io` in shared code — it crashes on web +- Use `dart:html` or `universal_io` for platform-specific I/O +- Use `path_provider` alternatives: `universal_html` for web file access +- Check: `kIsWeb` constant before any platform-specific code + +```dart +// ✅ Platform-safe +import 'package:universal_io/io.dart'; + +// ❌ Crashes on web +import 'dart:io'; +``` + +## Web rendering +- Choose renderer carefully: + - `canvaskit` — pixel-perfect, larger initial load, better for graphics + - `html` — smaller load, uses DOM elements, inconsistent rendering +- Set in `index.html`: `flutterWebRenderer: "canvaskit"` + +## PWA requirements +- Update `web/manifest.json`: name, icons (192×192, 512×512), theme_color +- Service worker: configure for offline caching of app shell +- Test with Chrome DevTools → Lighthouse → PWA audit + +## Web-specific rendering caveats +- `BackdropFilter` has limited support on `html` renderer +- `Canvas` operations differ between renderers — test both +- Text selection differs — use `SelectableText` not `Text` where appropriate +- Scrollbars appear automatically on web — style or hide with `ScrollbarTheme` + +## URL strategy +- Use `usePathUrlStrategy()` in `main.dart` for clean URLs (no `#`) +- Configure server to redirect all paths to `index.html` (SPA routing) + +## Performance +- Lazy-load routes — use `GoRouter` deferred loading +- Initial load budget: < 3MB (canvaskit) or < 1MB (html renderer) diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/routing/gorouter.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/routing/gorouter.mdc new file mode 100644 index 0000000..2e60a10 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/routing/gorouter.mdc @@ -0,0 +1,61 @@ +--- +description: "GoRouter conventions for TaskFlow" +alwaysApply: true +--- + +# GoRouter Standards — TaskFlow + +## Typed routes (mandatory) +```dart +// Define typed routes — never use string paths directly in navigation calls +@TypedGoRoute(path: '/') +class HomeRoute extends GoRouteData { + const HomeRoute(); + @override Widget build(BuildContext ctx, GoRouterState state) => const HomeScreen(); +} + +@TypedGoRoute(path: '/products/:id') +class ProductRoute extends GoRouteData { + final String id; + const ProductRoute({required this.id}); + @override Widget build(BuildContext ctx, GoRouterState state) => ProductScreen(id: id); +} + +// Navigate with type safety +const ProductRoute(id: product.id).go(context); // ✅ +context.go('/products/${product.id}'); // ❌ — don't do this +``` + +## Auth guard +```dart +// Redirect logic in router config +redirect: (context, state) { + final isLoggedIn = ref.read(authProvider).isAuthenticated; + final isLoginRoute = state.matchedLocation == '/login'; + if (!isLoggedIn && !isLoginRoute) return '/login'; + if (isLoggedIn && isLoginRoute) return '/'; + return null; +}, +``` + +## Shell routes for bottom navigation +```dart +ShellRoute( + builder: (ctx, state, child) => MainScaffold(child: child), + routes: [ + GoRoute(path: '/home', builder: (_, __) => const HomeScreen()), + GoRoute(path: '/search', builder: (_, __) => const SearchScreen()), + GoRoute(path: '/profile', builder: (_, __) => const ProfileScreen()), + ], +) +``` + +## Deep links +- Register URL schemes in `Info.plist` (iOS) and `AndroidManifest.xml` +- Test deep links with: `adb shell am start -a android.intent.action.VIEW -d "app://com.test.taskflow/products/123"` +- Handle `GoRouter.of(context).routerDelegate.currentConfiguration` for dynamic links + +## Rules +- **NEVER** use `Navigator.push/pop` — use `context.go()`, `context.push()`, `context.pop()` +- All routes declared in one file: `lib/core/routing/app_router.dart` +- `BlocProvider`s for route-level blocs created inside the `builder` of each route diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/security/security-standards.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/security/security-standards.mdc new file mode 100644 index 0000000..87e0e39 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/security/security-standards.mdc @@ -0,0 +1,109 @@ +--- +description: "Security standards for TaskFlow — ALWAYS APPLIED on every file write" +alwaysApply: true +--- + +# Security Standards — TaskFlow +> **Pillar 5**: Security is an always-on rule, not just a reactive agent. +> These rules apply to EVERY file write, regardless of feature or context. + +## Credential & secret management +- **NEVER** hardcode API keys, tokens, or secrets in source code +- **NEVER** commit `.env` files — use `.gitignore` and document required vars in `.env.example` +- API keys belong in: flavored `--dart-define` build args, or a secrets manager (e.g. AWS Secrets) +- Use `flutter_secure_storage` for tokens/credentials — **NEVER** `SharedPreferences` for sensitive data +- Obfuscation: enable `--obfuscate --split-debug-info=build/debug-symbols/` for release builds + +## Authentication & sessions +- JWT/session tokens: stored in `flutter_secure_storage`, never in `SharedPreferences` or local DB +- Implement token refresh with retry logic — never let a 401 show a raw error to the user +- Certificate pinning: required for production builds on Supabase Auth — use `dio_pinning_interceptor` +- Biometric re-auth: require for any transaction > defined threshold + +## Data & privacy +- **NEVER** log PII (names, emails, phone numbers, addresses) to console or crash reporters +- Sanitize all user inputs before sending to backend +- Use `SensitiveWidget` wrapper to exclude screens from screenshots / app switcher thumbnails +- Comply with platform privacy requirements: iOS `NSPrivacyAccessedAPITypes`, Android permissions + +## Network security +- All API calls use HTTPS — no http:// in production +- Validate SSL certificates — never bypass with `badCertificateCallback: (_,_,_) => true` +- Add a timeout to every HTTP call: `connectTimeout: Duration(seconds: 10)` +- Rate-limit sensitive endpoints: auth, OTP, password reset + +## Dependency security +- Run `dart pub outdated` monthly — flag packages with known CVEs +- Never use a package with <100 pub points without explicit tech lead approval +- Pin critical security packages to exact versions in `pubspec.yaml` + +## Secure coding patterns +```dart +// ✅ Correct: secure token storage +final storage = FlutterSecureStorage(); +await storage.write(key: 'access_token', value: token); + +// ❌ Wrong: SharedPreferences for sensitive data +final prefs = await SharedPreferences.getInstance(); +prefs.setString('access_token', token); // NEVER do this + +// ✅ Correct: PII-safe logging +logger.info('User authenticated: userId=${user.id}'); // ok — id, not email +logger.debug('Payment processed: orderId=$orderId'); // ok — no card data + +// ❌ Wrong: PII in logs +logger.info('Login: email=${user.email}, phone=${user.phone}'); // NEVER + +// ✅ Correct: --dart-define for secrets (build arg, not in code) +// flutter build apk --dart-define=API_KEY=$API_KEY +const apiKey = String.fromEnvironment('API_KEY'); // acceptable + +// ✅ Correct: certificate pinning with Dio +(dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () { + final client = HttpClient(); + client.badCertificateCallback = (cert, host, port) => false; // strict + return client; +}; +``` + +## Deep linking & intent security +- Validate all incoming deep link parameters — never trust raw URL params +- Use `app_links` package for verified deep link handling +- Restrict URL schemes to known patterns — reject unknown schemes +- For OAuth callbacks: validate `state` parameter to prevent CSRF + +## Storage security +- Encrypt locally cached sensitive data using `hive` with `HiveAesCipher` +- Clear sensitive data from memory after use (set to null, trigger GC) +- Do NOT cache auth tokens in network layer (no `dio_cache_interceptor` for auth endpoints) +- Use `SecureRandom` for nonce/token generation — never `Random()` + +## Code obfuscation & binary protection +```yaml +# android/app/build.gradle — release config +buildTypes { + release { + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } +} +``` +```bash +# Flutter release build with obfuscation +flutter build apk --release --obfuscate --split-debug-info=build/debug-symbols/ +flutter build ipa --release --obfuscate --split-debug-info=build/debug-symbols/ +``` + +## Release checklist +Before every production release, verify: +- [ ] No hardcoded secrets (`grep -r "api_key\|secret\|password\|token" lib/`) +- [ ] Debug flags disabled (`kDebugMode` guards on all debug-only paths) +- [ ] Obfuscation enabled in release build config +- [ ] Certificate pinning active and tested on both platforms +- [ ] Crash reporter PII filters configured (Sentry `beforeSend`, Firebase `setConsentType`) +- [ ] `flutter_secure_storage` used for all tokens — no `SharedPreferences` for sensitive keys +- [ ] Network calls all HTTPS — scan for `http://` in lib/ +- [ ] Dependencies audited: `dart pub outdated --json | jq '.packages[] | select(.isDiscontinued)'` +- [ ] Deep link parameters validated +- [ ] App transport security / network security config reviewed diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/state-management/riverpod.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/state-management/riverpod.mdc new file mode 100644 index 0000000..1867b7d --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/state-management/riverpod.mdc @@ -0,0 +1,58 @@ +--- +description: "Riverpod conventions for TaskFlow" +alwaysApply: true +--- + +# Riverpod Standards — TaskFlow + +## Provider types +| Type | Use case | +|------|----------| +| `AsyncNotifier` | Async state from Supabase | +| `Notifier` | Synchronous derived/local UI state | +| `StreamNotifier` | Real-time subscriptions | +| `@riverpod` function | Simple computed/derived values | + +## Code generation (mandatory) +```dart +// ✅ Always use @riverpod annotation +@riverpod +class AuthNotifier extends _$AuthNotifier { + @override + Future build() => ref.watch(authRepositoryProvider).currentUser(); + + Future login(String email, String password) async { + state = const AsyncLoading(); + state = await AsyncValue.guard( + () => ref.read(authRepositoryProvider).login(email, password), + ); + } +} + +// ❌ Never write manual provider declarations +final authProvider = StateNotifierProvider>( + (ref) => AuthNotifier(), +); // DON'T DO THIS +``` + +## Rules +- `ref.watch()` inside `build()` ONLY — **never** `ref.read()` inside `build()` +- `ref.read(provider.notifier).method()` for mutations in gesture handlers +- `ref.invalidate(provider)` to refresh — never manually reset state to `AsyncLoading()` +- Family providers for parameterized data: `productDetailsProvider(productId)` +- Providers scoped at feature level; core providers in `lib/core/di/` + +## AsyncValue in widgets +Every `AsyncValue` MUST handle all three states: +```dart +ref.watch(productsProvider).when( + data: (products) => ProductList(products: products), + loading: () => const ProductListShimmer(), // required + error: (e, _) => ErrorWidget(error: e), // required +) +``` + +## File locations in TaskFlow +- `lib/features/[feature]/[feature]_provider.dart` (generated: `[feature]_provider.g.dart`) +- `lib/features/[feature]/[feature]_repository.dart` +- Run `dart run build_runner watch` during development diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/testing/testing-riverpod.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/testing/testing-riverpod.mdc new file mode 100644 index 0000000..4a37628 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/testing/testing-riverpod.mdc @@ -0,0 +1,56 @@ +--- +description: "Riverpod testing conventions for TaskFlow" +alwaysApply: false +--- + +# Riverpod Testing Standards — TaskFlow + +## Test pattern +```dart +// final container = ProviderContainer(overrides: [myProvider.overrideWithValue(mockValue)]); addTearDown(container.dispose); + +void main() { + late ProviderContainer container; + late MockProductRepository mockRepo; + + setUp(() { + mockRepo = MockProductRepository(); + container = ProviderContainer(overrides: [ + productRepositoryProvider.overrideWithValue(mockRepo), + ]); + }); + + tearDown(() => container.dispose()); // ALWAYS dispose + + test('ProductsNotifier loads products successfully', () async { + when(() => mockRepo.getProducts()).thenAnswer((_) async => [fakeProduct]); + + final notifier = container.read(productsProvider.notifier); + await notifier.loadProducts(); + + final state = container.read(productsProvider); + expect(state, isA>>()); + expect(state.value, [fakeProduct]); + }); +} +``` + +## Widget tests with Riverpod +```dart +testWidgets('ProductScreen shows shimmer while loading', (tester) async { + await tester.pumpWidget( + ProviderScope( + overrides: [ + productsProvider.overrideWith((ref) => const AsyncLoading()), + ], + child: const MaterialApp(home: ProductScreen()), + ), + ); + expect(find.byType(ProductShimmer), findsOneWidget); +}); +``` + +## Rules +- **Never** use a real `ProviderScope` in unit tests — always use `ProviderContainer` with overrides +- `addTearDown(container.dispose)` in every test that creates a container +- Test all three `AsyncValue` states: loading, data, error diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/universal/flutter-core.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/universal/flutter-core.mdc new file mode 100644 index 0000000..46b3ba2 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/universal/flutter-core.mdc @@ -0,0 +1,40 @@ +--- +description: "Core Flutter conventions for TaskFlow — always applied" +alwaysApply: true +--- + +# Flutter Core Standards — TaskFlow + +## Const and performance +- Use `const` constructors wherever possible — compile-time guarantee of no rebuild +- Prefer `const` widgets at the leaf level: `const SizedBox.shrink()`, `const Padding(...)` +- Never use `const` with mutable values; lint: `prefer_const_constructors` is enabled + +## Null safety +- Never use `!` (bang operator) unless you have a 100% safe runtime guarantee with a comment +- Prefer `??`, `?.`, and `if (x != null)` guards +- Use `required` for all non-nullable named parameters +- Never use `late` without a guarantee of initialisation before first access + +## Widget lifecycle +- Override `dispose()` in every `StatefulWidget` that uses controllers, streams, or timers +- Cancel `StreamSubscription` in `dispose()`, not in `didUpdateWidget` +- Use `WidgetsBinding.instance.addPostFrameCallback` for post-build logic, not `Future.delayed(Duration.zero)` + +## Naming conventions +- Files: `snake_case.dart` +- Classes: `PascalCase` +- Variables/functions: `camelCase` +- Constants: `kCamelCase` or `SCREAMING_SNAKE` for true compile-time constants +- Private members: `_camelCase` + +## Imports +- Order: dart: → package: → relative +- Use relative imports within a feature; absolute for cross-feature +- Never import a feature's internal files from outside that feature + +## Code quality +- Max function length: 40 lines. Extract widgets and helpers aggressively +- No `print()` in production code — use a logging package +- All `TODO:` comments must include a ticket number: `// TODO: PROJ-123 — fix this` +- Run `dart format` before every commit diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/universal/project-context.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/universal/project-context.mdc new file mode 100644 index 0000000..d818d32 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/universal/project-context.mdc @@ -0,0 +1,66 @@ +--- +description: "Project context for TaskFlow — always applied" +alwaysApply: true +--- + +# Project Context — TaskFlow + +## Project identity +- **Name:** TaskFlow +- **Package:** com.test.taskflow +- **Description:** Task management app +- **Scale:** small + +## Technology stack +- **State management:** Riverpod +- **Architecture:** Feature-First +- **Routing:** GoRouter +- **Backend:** Supabase +- **Auth:** Supabase Auth +- **Platforms:** ios, android, web +- **Code generation:** freezed, json_serializable + +## Feature modules +auth, tasks, profile + +## Special capabilities + + +## Environments / flavors +- Flavors: dev, prod +- CI/CD: GitHub Actions + +## Design & API references +- Design source: none +- API docs: none at `` + +## Code references +### Git repositories +_No Git repository URLs listed._ Add entries under `references.repos` in project-brief.yaml when other repos are part of the product context. + +### Local paths +_No local paths listed._ Add monorepo packages or sibling folders under `references.local_paths` in project-brief.yaml when relevant. + +## Product UX / themes & roles +- **Theme variants:** light, dark +- **Roles:** Not enabled (`app_context.roles_enabled: false`). + + +## Reviews — which rule owns what +- **Theme, colors, typography, spacing/radius tokens** → `ui-ux-standards.mdc` (widgets read `Theme.of(context)` only) +- **User-visible copy & locales** → `localization.mdc` (ARB / `AppLocalizations`; no UI string literals) +- **Imports, structure, naming** → `flutter-core.mdc` + architecture rule + +## Architecture boundaries +- features/auth/ MUST NOT import from features/home/ etc. +- Shared code lives in core/ or shared/ only +- Each feature is self-contained: screen + provider + model + repo + +## When generating code for this project +1. Always use Riverpod patterns — never suggest alternatives +2. Always follow Feature-First folder structure +3. Always use GoRouter for navigation — never `Navigator.push` directly +4. Always target platforms: ios, android, web +5. If code generation tools are used (freezed, json_serializable), follow their conventions +6. Apply visuals only through theme (`ColorScheme`, `TextTheme`, `ThemeExtension`) — never ad-hoc colors/fonts in feature widgets +7. No user-facing string literals in widgets — l10n or shared constants per localization rule diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/universal/ui-ux-standards.mdc b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/universal/ui-ux-standards.mdc new file mode 100644 index 0000000..0929436 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/rules/universal/ui-ux-standards.mdc @@ -0,0 +1,48 @@ +--- +description: "UI/UX standards for TaskFlow — always applied" +alwaysApply: true +--- + +# UI / UX Standards — TaskFlow + +## Theme & design tokens (single source of truth) +- Define **one** light/dark `ThemeData` (and optional `ThemeExtension`s for brand spacing, radii, semantic colors). Feature code reads `Theme.of(context)` only. +- **Colors:** `colorScheme` / extensions — never hex/`Color(...)` literals in widgets except inside the theme definition file(s). +- **Typography:** `textTheme` / `primaryTextTheme` — never raw `TextStyle(fontSize:, fontFamily:)` in feature UI. +- **Spacing & shapes:** `ThemeExtension` or documented constants consumed consistently — avoid one-off magic numbers for padding/radius. + +## Loading states +- Every async operation MUST show a loading skeleton (shimmer), NOT a spinner unless < 300ms +- Use `shimmer` package with a shimmer that matches the final layout shape +- Never show a blank screen during loading — skeleton must fill the same space as the content + +## Empty states +- Every list/grid MUST have a distinct empty state widget: illustration + headline + CTA +- Empty state is different from error state — never reuse the same widget for both +- Empty state copy: positive framing ("No items yet — add your first one") + +## Error states +- Every async failure MUST show: error message + retry button +- Never swallow errors silently +- Error text: user-friendly, never expose stack traces or raw API messages + +## Navigation & transitions +- Use `IndexedStack` for bottom nav tabs — preserves scroll position +- Named routes only — never `Navigator.push(context, MaterialPageRoute(...))` +- Page transitions: use `CustomTransitionPage` with `FadeTransition` for modal sheets + +## Responsive layout +- Use `LayoutBuilder` or `MediaQuery` for breakpoints, not hardcoded pixel values +- Minimum touch target: 48×48 logical pixels (Material guideline) +- Test on 375px (iPhone SE) and 414px (iPhone Pro Max) widths minimum + +## Haptics +- Use `HapticFeedback.lightImpact()` on primary CTAs +- Use `HapticFeedback.selectionClick()` on toggle/checkbox interactions +- Never add haptics to destructive actions without confirmation + +## Accessibility +- All interactive widgets must have a `Semantics` label or `tooltip` +- Minimum contrast ratio: 4.5:1 (WCAG AA) +- Test with TalkBack / VoiceOver before each release + diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/skills/create-flavor/SKILL.md b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/skills/create-flavor/SKILL.md new file mode 100644 index 0000000..08dadf3 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/skills/create-flavor/SKILL.md @@ -0,0 +1,18 @@ +# Create Build Flavor — TaskFlow + +Creates a new build flavor/environment. Current flavors: dev, prod. + +## Usage +``` +Create a new flavor called [flavor_name] +``` + +## What gets created +1. Android: new `productFlavors` block in `android/app/build.gradle` +2. iOS: new scheme + configuration in Xcode (instructions provided) +3. `lib/core/config/[flavor_name]_config.dart` +4. `.env.[flavor_name]` template +5. GitHub Actions pipeline update + +## CI/CD: GitHub Actions +Generate the pipeline config snippet for the new flavor in GitHub Actions format. diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/skills/deploy/SKILL.md b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/skills/deploy/SKILL.md new file mode 100644 index 0000000..7dd15e8 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/skills/deploy/SKILL.md @@ -0,0 +1,21 @@ +# Deploy — TaskFlow + +Guides through deployment to GitHub Actions for any of the flavors: dev, prod. + +## Usage +``` +Deploy [flavor] to [store/environment] +``` + +## GitHub Actions pipeline +The AI will generate or update the github_actions configuration file for: +- Build signing +- Running tests +- Distributing to Firebase App Distribution (beta) or App Store / Play Store (prod) + +## Pre-deploy checklist +- [ ] Version bump in `pubspec.yaml` +- [ ] Obfuscation enabled for prod: `--obfuscate --split-debug-info=build/debug-symbols/` +- [ ] No debug flags in production code +- [ ] Security checklist from `security-standards.mdc` passed +- [ ] `cursor_gen --validate` passes diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/skills/generate-tests/SKILL.md b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/skills/generate-tests/SKILL.md new file mode 100644 index 0000000..6d347ae --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/skills/generate-tests/SKILL.md @@ -0,0 +1,41 @@ +# Generate Tests — TaskFlow + +Generates comprehensive unit, widget, and integration tests for **Riverpod** patterns. + +## Usage +``` +Generate tests for [ClassName or file path] +``` + +## Test generation process +1. Read the source file completely +2. Identify all testable units (public methods, state transitions, UI states) +3. Generate tests following this pattern: + ``` + final container = ProviderContainer(overrides: [myProvider.overrideWithValue(mockValue)]); addTearDown(container.dispose); + ``` +4. Create mocks with `mocktail` for all dependencies +5. Place test file at `test/[mirror of source path]_test.dart` + +## Coverage targets +- Business logic (UseCases, Repositories, BLoC/Notifiers): **80% minimum** +- Widget tests: all three states (loading/error/data) must be tested +- E2E: only critical user flows + +## Test file structure +```dart +void main() { + // Setup + group('[ClassName]', () { + // Happy path tests + group('success cases', () { ... }); + // Error path tests + group('error cases', () { ... }); + // Edge cases + group('edge cases', () { ... }); + }); +} +``` + +## Naming convention +`'given [precondition], when [action], then [expected outcome]'` diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/skills/scaffold-feature/SKILL.md b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/skills/scaffold-feature/SKILL.md new file mode 100644 index 0000000..a641418 --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/skills/scaffold-feature/SKILL.md @@ -0,0 +1,69 @@ +# Scaffold Feature — TaskFlow + +Scaffolds a complete new feature module following **Feature-First** architecture with **Riverpod** state management. + +## Usage +``` +Create a feature called [feature_name] with [description] +``` + +## What gets generated + +### For Feature-First architecture: +The AI will create all necessary files for the `[feature_name]` feature following Feature-First patterns. + +### File structure to create: + +**Clean Architecture:** +``` +lib/features/[feature_name]/ + domain/ + entities/[feature_name].dart + repositories/[feature_name]_repository.dart + usecases/ + get_[feature_name]_usecase.dart + create_[feature_name]_usecase.dart + data/ + models/[feature_name]_dto.dart + datasources/[feature_name]_remote_datasource.dart + repositories/[feature_name]_repository_impl.dart + presentation/ + [state_files]/ ← based on Riverpod + pages/[feature_name]_page.dart + widgets/ +``` + +**Feature-First:** +``` +lib/features/[feature_name]/ + [feature_name]_screen.dart + [feature_name]_provider.dart ← or [feature_name]_bloc.dart + [feature_name]_repository.dart + [feature_name]_model.dart + widgets/ +``` + +## State management boilerplate + +### For Riverpod: +Generate the appropriate state management files with: +- Initial/loading/success/error states +- All necessary events (for BLoC) +- Repository connection +- Dependency injection registration + +## Steps the AI takes +1. Ask: "What is the feature name and brief description?" +2. Ask: "What data does this feature manage? (e.g., list of products, single user profile)" +3. Generate all files with correct imports and patterns +4. Add the feature to the DI container +5. Add the route to GoRouter router +6. Create a placeholder test file + +## Code generation +{{#if codegen_freezed}} +- Generate Freezed model: run `dart run build_runner build` after scaffolding +{{/if}} +{{#if codegen_injectable}} +- Register in injectable: add `@lazySingleton` to repository +{{/if}} diff --git a/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/skills/scaffold-screen/SKILL.md b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/skills/scaffold-screen/SKILL.md new file mode 100644 index 0000000..837dc0d --- /dev/null +++ b/flutter-cursor-templates/generator/test/golden/riverpod-ff-supabase/skills/scaffold-screen/SKILL.md @@ -0,0 +1,61 @@ +# Scaffold Screen — TaskFlow + +Creates a complete screen widget with all states handled, following **Riverpod** patterns. + +## Usage +``` +Create a screen for [screen_name] that shows [content description] +``` + +## Generated screen template + +### Riverpod screen pattern: + +**BLoC:** +```dart +class [ScreenName]Screen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocBuilder<[Feature]Bloc, [Feature]State>( + builder: (context, state) => switch (state) { + [Feature]Initial() || [Feature]Loading() => const [ScreenName]Shimmer(), + [Feature]Loaded(:final data) => [ScreenName]Content(data: data), + [Feature]Empty() => const [ScreenName]EmptyState(), + [Feature]Error(:final message) => [ScreenName]ErrorState( + message: message, + onRetry: () => context.read<[Feature]Bloc>().add(const [Feature]LoadRequested()), + ), + }, + ); + } +} +``` + +**Riverpod:** +```dart +class [ScreenName]Screen extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch([feature]Provider); + return state.when( + loading: () => const [ScreenName]Shimmer(), + data: (data) => [ScreenName]Content(data: data), + error: (e, _) => [ScreenName]ErrorState(error: e), + ); + } +} +``` + +## Required sub-widgets to generate +1. `[ScreenName]Shimmer` — skeleton loading layout matching final content +2. `[ScreenName]EmptyState` — illustration + headline + CTA button +3. `[ScreenName]ErrorState` — error message + retry button +4. `[ScreenName]Content` — the actual data display + +## Platform considerations (ios, android, web) +{{#if platform_web}} +- Web: ensure no dart:io usage; test at 375px and 1280px widths +{{/if}} +{{#if platform_desktop}} +- Desktop: add keyboard shortcut support for primary actions +{{/if}} diff --git a/flutter-cursor-templates/templates/rules/i18n/localization.mdc.tmpl b/flutter-cursor-templates/templates/rules/i18n/localization.mdc.tmpl index dd79604..dff5105 100644 --- a/flutter-cursor-templates/templates/rules/i18n/localization.mdc.tmpl +++ b/flutter-cursor-templates/templates/rules/i18n/localization.mdc.tmpl @@ -46,6 +46,7 @@ Text(context.l10n.welcomeMessage) // ✅ ## Rules - **NEVER** hardcode user-facing strings as string literals in widget files +- **Technical keys** (route paths, storage/analytics keys, JSON fields): one shared module (e.g. `lib/core/constants/app_strings.dart`) — no duplicated literals across features - All new strings added to ALL locale ARB files simultaneously — broken translations break builds - Use ICU message format for plurals and gendered strings - Date/time formatting: use `intl` package `DateFormat` — never `date.toString()` diff --git a/flutter-cursor-templates/templates/rules/universal/project-context.mdc.tmpl b/flutter-cursor-templates/templates/rules/universal/project-context.mdc.tmpl index 3ae5c51..7d2f89c 100644 --- a/flutter-cursor-templates/templates/rules/universal/project-context.mdc.tmpl +++ b/flutter-cursor-templates/templates/rules/universal/project-context.mdc.tmpl @@ -34,6 +34,23 @@ alwaysApply: true - Design source: {{DESIGN_SOURCE}} - API docs: {{API_DOCS_FORMAT}} at `{{API_DOCS_PATH}}` +## Code references +### Git repositories +{{GIT_REFS_BLOCK}} + +### Local paths +{{LOCAL_PATHS_BLOCK}} + +## Product UX / themes & roles +- **Theme variants:** {{THEME_SUMMARY}} +- **Roles:** {{ROLES_SUMMARY}} + +{{HIGH_CONTRAST_NOTE}} +## Reviews — which rule owns what +- **Theme, colors, typography, spacing/radius tokens** → `ui-ux-standards.mdc` (widgets read `Theme.of(context)` only) +- **User-visible copy & locales** → `localization.mdc` (ARB / `AppLocalizations`; no UI string literals) +- **Imports, structure, naming** → `flutter-core.mdc` + architecture rule + ## Architecture boundaries {{ARCH_IMPORT_RULES}} @@ -43,3 +60,5 @@ alwaysApply: true 3. Always use {{ROUTING}} for navigation — never `Navigator.push` directly 4. Always target platforms: {{PLATFORMS_LIST}} 5. If code generation tools are used ({{CODEGEN_LIST}}), follow their conventions +6. Apply visuals only through theme (`ColorScheme`, `TextTheme`, `ThemeExtension`) — never ad-hoc colors/fonts in feature widgets +7. No user-facing string literals in widgets — l10n or shared constants per localization rule diff --git a/flutter-cursor-templates/templates/rules/universal/ui-ux-standards.mdc.tmpl b/flutter-cursor-templates/templates/rules/universal/ui-ux-standards.mdc.tmpl index d839564..bfd07bb 100644 --- a/flutter-cursor-templates/templates/rules/universal/ui-ux-standards.mdc.tmpl +++ b/flutter-cursor-templates/templates/rules/universal/ui-ux-standards.mdc.tmpl @@ -5,6 +5,12 @@ alwaysApply: true # UI / UX Standards — {{PROJECT_NAME}} +## Theme & design tokens (single source of truth) +- Define **one** light/dark `ThemeData` (and optional `ThemeExtension`s for brand spacing, radii, semantic colors). Feature code reads `Theme.of(context)` only. +- **Colors:** `colorScheme` / extensions — never hex/`Color(...)` literals in widgets except inside the theme definition file(s). +- **Typography:** `textTheme` / `primaryTextTheme` — never raw `TextStyle(fontSize:, fontFamily:)` in feature UI. +- **Spacing & shapes:** `ThemeExtension` or documented constants consumed consistently — avoid one-off magic numbers for padding/radius. + ## Loading states - Every async operation MUST show a loading skeleton (shimmer), NOT a spinner unless < 300ms - Use `shimmer` package with a shimmer that matches the final layout shape @@ -39,3 +45,4 @@ alwaysApply: true - All interactive widgets must have a `Semantics` label or `tooltip` - Minimum contrast ratio: 4.5:1 (WCAG AA) - Test with TalkBack / VoiceOver before each release +{{HIGH_CONTRAST_UX_LINE}} diff --git a/flutter-cursor-templates/templates/skills/deploy/SKILL.md.tmpl b/flutter-cursor-templates/templates/skills/deploy/SKILL.md.tmpl index f97e5da..f1cc9e2 100644 --- a/flutter-cursor-templates/templates/skills/deploy/SKILL.md.tmpl +++ b/flutter-cursor-templates/templates/skills/deploy/SKILL.md.tmpl @@ -18,4 +18,4 @@ The AI will generate or update the {{CICD_RAW}} configuration file for: - [ ] Obfuscation enabled for prod: `--obfuscate --split-debug-info=build/debug-symbols/` - [ ] No debug flags in production code - [ ] Security checklist from `security-standards.mdc` passed -- [ ] `dart run cursor_gen --validate` passes +- [ ] `cursor_gen --validate` passes