chore: update README and CLI usage for cursor_gen, version bump to 1.0.1

- Changed CLI usage instructions from `dart run cursor_gen` to `cursor_gen` for global activation.
- Updated project-brief.yaml example and README to reflect new command usage.
- Added app_context section in project-brief.yaml for theme variants and RBAC roles.
- Fixed bundled template resolution for local and global installs to prevent 'Template not found' errors.
- Version bump to 1.0.1 with corresponding updates in CHANGELOG and pubspec.yaml.
This commit is contained in:
2026-05-13 12:08:52 +05:30
parent b05cdb7fbe
commit 54c66efe9b
157 changed files with 8233 additions and 570 deletions
@@ -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
@@ -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
```
@@ -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<MyCubit, MyState>(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`
@@ -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
@@ -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}`);
@@ -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}`);
@@ -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);
}
@@ -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"]
}
]
}
@@ -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<Either<AppError, List<Product>>> 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<Failure, T>` or `Result<T>` 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
@@ -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<List<Product>> 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
@@ -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<String, dynamic> 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<Product> 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)
```
@@ -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<AppError, T>` or `Result<T>`
- 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');`
@@ -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+
@@ -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
@@ -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<HomeRoute>(path: '/')
class HomeRoute extends GoRouteData {
const HomeRoute();
@override Widget build(BuildContext ctx, GoRouterState state) => const HomeScreen();
}
@TypedGoRoute<ProductRoute>(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
@@ -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
@@ -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<AuthBloc, AuthState>(
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`
@@ -0,0 +1,60 @@
---
description: "BLoC testing conventions for TestApp"
alwaysApply: false
---
# BLoC Testing Standards — TestApp
## Test pattern (bloc_test)
```dart
// blocTest<MyCubit, MyState>(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<AuthBloc, AuthState>(
'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<AuthAuthenticated>(),
],
);
blocTest<AuthBloc, AuthState>(
'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
@@ -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
@@ -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
@@ -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
@@ -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.
@@ -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
@@ -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<MyCubit, MyState>(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]'`
@@ -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}}
@@ -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}}