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:
@@ -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<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,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
|
||||
@@ -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<TokenStorage>()),
|
||||
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 <token>` 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')`
|
||||
@@ -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<List<Product>> 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<List<Map<String, dynamic>>>? _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
|
||||
Reference in New Issue
Block a user