Compare commits
7 Commits
6dfb9a8aa5
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a79017810f | |||
| da64f769da | |||
| 2ee257c630 | |||
| 44b5d814d4 | |||
| 3ee83389bd | |||
| 54c66efe9b | |||
| b05cdb7fbe |
@@ -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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
# Design Spec: /build Skill — Universal Feature Implementation Command
|
||||
|
||||
**Date:** 2026-05-14
|
||||
**Status:** Draft — Awaiting user review
|
||||
**Scope:** A single Cursor slash command that orchestrates the full feature development lifecycle
|
||||
|
||||
---
|
||||
|
||||
## 1. Problem Statement
|
||||
|
||||
Today, implementing any feature (notifications, auth, payments, etc.) requires the user to manually:
|
||||
- Specify TDD in the prompt
|
||||
- Remember to include integration test scenarios
|
||||
- Know which external services need configuring
|
||||
- Know which files to touch for their specific stack
|
||||
- Invoke the right skills in the right order
|
||||
|
||||
This leads to inconsistent implementations, missed test scenarios, and forgotten native setup steps.
|
||||
|
||||
**Goal:** One command — `/build implement [anything]` — that handles everything from research through PR, following every rule, skill, hook, and architecture pattern defined in the project.
|
||||
|
||||
---
|
||||
|
||||
## 2. Decision
|
||||
|
||||
**Form:** Cursor slash command skill file (`.md.tmpl` inside the generator's template system)
|
||||
**Scope:** Universal + context-aware (reads `project-brief.yaml` to adapt behavior)
|
||||
**Integration test gate:** Auto-generate `integration_test/` files + pause for device run
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecture
|
||||
|
||||
### 3.1 File Placement
|
||||
|
||||
```
|
||||
flutter-cursor-templates/
|
||||
generator/
|
||||
templates/
|
||||
skills/
|
||||
build/
|
||||
SKILL.md.tmpl ← The skill (single new file)
|
||||
test/
|
||||
golden/
|
||||
bloc-clean-firebase/skills/build/SKILL.md
|
||||
riverpod-ff-supabase/skills/build/SKILL.md
|
||||
getx-mvc-rest/skills/build/SKILL.md
|
||||
```
|
||||
|
||||
The generator renders this to `.cursor/skills/build/SKILL.md` in the user's project — same pattern as every other skill.
|
||||
|
||||
### 3.2 Template Variables
|
||||
|
||||
| Placeholder | Source in project-brief.yaml |
|
||||
|---|---|
|
||||
| `{{PROJECT_NAME}}` | `project.name` |
|
||||
| `{{ARCHITECTURE}}` | `stack.architecture` |
|
||||
| `{{STATE_MANAGEMENT}}` | `stack.state_management` |
|
||||
| `{{ROUTING}}` | `stack.routing` |
|
||||
| `{{BACKEND}}` | `stack.backend` |
|
||||
| `{{PLATFORMS_LIST}}` | `stack.platforms` |
|
||||
| `{{CODEGEN_LIST}}` | `stack.codegen` |
|
||||
| `{{TESTING_DEPTH}}` | `testing.depth` |
|
||||
| `{{E2E_TOOL}}` | `testing.e2e_tool` |
|
||||
| `{{FEATURES_LIST}}` | `features.modules` |
|
||||
| `{{SPECIAL_FEATURES}}` | `features.special` |
|
||||
| `{{FLAVORS_LIST}}` | `environments.flavors` |
|
||||
| `{{CICD_TOOL}}` | `environments.cicd` |
|
||||
| `{{PACKAGE_ID}}` | `project.package` |
|
||||
|
||||
Conditional blocks: `{{#if backend_firebase}}`, `{{#if platforms_ios}}`, `{{#if platforms_android}}`, `{{#if codegen_freezed}}`, `{{#if codegen_injectable}}`, `{{#if special_push_notifications}}`, `{{#if special_deep_linking}}`
|
||||
|
||||
---
|
||||
|
||||
## 4. The 7 Phases
|
||||
|
||||
### Phase 1: Context Loading
|
||||
1. Read `project-brief.yaml` — extract stack, platforms, existing features
|
||||
2. Parse user's request → match against FEATURE_REGISTRY keywords
|
||||
3. Print context summary table (feature type, stack, platforms, rules loaded)
|
||||
4. If feature already in `features.modules`, confirm extending vs. rebuilding
|
||||
|
||||
### Phase 2: Deep Research
|
||||
1. Enumerate required packages + correct versions for current Flutter stable
|
||||
2. Build complete file manifest: new files (by architecture pattern) + modified files
|
||||
3. List all external service configuration required per platform
|
||||
4. Print research results before touching any file
|
||||
|
||||
### Phase 3: TDD Implementation
|
||||
**Invokes: `superpowers:test-driven-development`**
|
||||
|
||||
- **Red:** Write all failing unit + widget tests from FEATURE_REGISTRY scenarios. Run to confirm red. Show actual output.
|
||||
- **Green:** Implement domain → data → presentation layers. Run tests to green. Show actual output.
|
||||
- **Refactor:** Clean up per `flutter-core.mdc`. Run `flutter analyze`. Show output.
|
||||
|
||||
### Phase 4: Integration Test Generation
|
||||
1. Create `integration_test/[feature_type]/` directory
|
||||
2. Generate one test file per logical scenario cluster
|
||||
3. Each file: standard boilerplate with `IntegrationTestWidgetsFlutterBinding` (or Patrol if `{{E2E_TOOL}} = patrol`)
|
||||
4. Generate `integration_test/[feature]/README.md` — test matrix with run commands
|
||||
|
||||
**PAUSE GATE:** Print table of all integration test files, what they cover, whether hardware is required, and the exact `flutter test` commands. Wait for user to paste device output before Phase 6.
|
||||
|
||||
### Phase 5: External Setup Checklist
|
||||
- Grouped by service/platform (Firebase, iOS, Android, backend)
|
||||
- iOS/Android: table format — Step | Where | What | How to Verify
|
||||
- Console steps: numbered checklist with exact menu paths and URLs
|
||||
|
||||
### Phase 6: Verification Gate
|
||||
**Invokes: `superpowers:verification-before-completion`**
|
||||
|
||||
Required evidence (real output pasted by user):
|
||||
- `flutter test test/features/[feature]/ --no-pub` → all tests pass
|
||||
- `flutter analyze` → no issues
|
||||
- Integration test device output from Phase 4
|
||||
- `lefthook run pre-commit` → all hooks pass
|
||||
|
||||
If any failure: invoke `superpowers:systematic-debugging` before retrying.
|
||||
|
||||
### Phase 7: PR Preparation
|
||||
**Invokes: `superpowers:finishing-a-development-branch`**
|
||||
|
||||
- Conventional commit: `feat([feature_type]): implement [feature] end-to-end`
|
||||
- PR description: what built, test count, integration test matrix, external setup status
|
||||
- CI/CD advice for `{{CICD_TOOL}}` (secret scoping per flavor)
|
||||
|
||||
---
|
||||
|
||||
## 5. FEATURE_REGISTRY
|
||||
|
||||
Seven feature types with full metadata:
|
||||
|
||||
| Feature Type | Keywords (sample) | Packages | Integration Test Scenarios |
|
||||
|---|---|---|---|
|
||||
| `notifications` | notification, push, FCM, APNs | firebase_messaging, flutter_local_notifications | foreground/background/killed receipt, tap redirect in all 3 states, payload parsing |
|
||||
| `auth` | login, sign in, biometric, OAuth, JWT | firebase_auth, local_auth, flutter_secure_storage | full login flow, biometric + fallback, token refresh, cold start with session |
|
||||
| `payments` | payment, Stripe, IAP, checkout | flutter_stripe, purchases_flutter | test-card checkout, 3DS, Apple/Google Pay, subscription restore |
|
||||
| `deep_links` | deep link, universal link, app link | app_links, go_router | cold/background/foreground link handling, auth-guarded routes |
|
||||
| `analytics` | analytics, event, tracking, screen view | firebase_analytics, mixpanel_flutter | event triggered on action, screen view on nav, opt-out disables tracking |
|
||||
| `storage` | file upload, Firebase Storage, Hive, Isar | firebase_storage, isar, drift | upload + retrieve, offline cache, background upload restore |
|
||||
| `camera_media` | camera, photo, QR, barcode, video | camera, image_picker, mobile_scanner | photo capture, gallery pick, QR decode, permission denied UI |
|
||||
|
||||
Each entry also specifies: `rules_to_load`, `files_to_touch` (new + modified), `external_setup` (per service/platform), `unit_test_scenarios`, `widget_test_scenarios`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Integration Test Template Structure
|
||||
|
||||
```dart
|
||||
// integration_test/[feature]/[scenario]_test.dart
|
||||
// Generated by /build — {{PROJECT_NAME}}
|
||||
// Run: flutter test integration_test/[feature]/[scenario]_test.dart -d <device_id>
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:{{PACKAGE_ID}}/main.dart' as app;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('[FeatureType] — [Scenario Name]', () {
|
||||
setUp(() async { /* seed state / reset storage */ });
|
||||
tearDown(() async { /* cleanup */ });
|
||||
|
||||
testWidgets(
|
||||
'given [precondition], when [action], then [expected outcome]',
|
||||
(tester) async {
|
||||
app.main();
|
||||
await tester.pumpAndSettle();
|
||||
// test body
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
For `{{E2E_TOOL}} = patrol`: imports swap to `package:patrol`, uses `$` Patrol selector syntax.
|
||||
|
||||
---
|
||||
|
||||
## 7. Skill File Sections (exact headings)
|
||||
|
||||
```
|
||||
# Build — {{PROJECT_NAME}}
|
||||
## Usage
|
||||
## FEATURE_REGISTRY
|
||||
## Phase 1: Context Loading
|
||||
## Phase 2: Deep Research
|
||||
## Phase 3: TDD Implementation
|
||||
## Phase 4: Integration Test Generation
|
||||
## Phase 5: External Setup Checklist
|
||||
## Phase 6: Verification Gate
|
||||
## Phase 7: PR Preparation
|
||||
## Rules applied every phase
|
||||
## Code generation notes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Cross-Skill Orchestration
|
||||
|
||||
The `/build` skill orchestrates existing skills internally:
|
||||
- Invokes `/scaffold-feature` patterns to create the initial file skeleton (Phase 3b)
|
||||
- Borrows `/generate-tests` naming and structure conventions (Phase 3a)
|
||||
- Invokes `/generate-api-client` if `api_docs.format != none` and feature needs API (Phase 2)
|
||||
- References `/deploy` checklist for flavor-scoped secret handling (Phase 7)
|
||||
|
||||
Generated alongside `/build` (same `cursor_gen` bundle) for lifecycle support:
|
||||
|
||||
- **`/debug`** (`debug-issue` skill) — structured BugReport + evidence before fixes; use when Phase 6 checks fail with unclear errors or weak reproduction.
|
||||
- **`/verify`** (`verify-change` skill) — pasted-evidence gate for tests/analyze/hooks without walking the full seven-phase build doc; use for small PRs or post-fix sanity checks.
|
||||
- **`/explain`** (`explain-code` skill) — read-only walkthrough of code paths; use when research needs human answers about runtime or native behavior before implementation.
|
||||
|
||||
The generated `build` skill template also cross-links **Phase 3** (TDD loop) and **Phase 6** (verification failures) to **`/debug`** and **`/verify`** so agents escalate without re-reading the full seven-phase doc.
|
||||
|
||||
---
|
||||
|
||||
## 9. Rules Applied Every Phase
|
||||
|
||||
Always active (no conditional):
|
||||
- `.cursor/rules/flutter-core.mdc`
|
||||
- `.cursor/rules/security-standards.mdc`
|
||||
- `.cursor/rules/project-context.mdc`
|
||||
- Feature-type rules from `FEATURE_REGISTRY.rules_to_load`
|
||||
|
||||
---
|
||||
|
||||
## 10. Enhancements / Out of Scope for V1
|
||||
|
||||
- Reference project URL fetching and pattern extraction (Phase 1) — included in V1
|
||||
- Multi-feature requests spanning 2+ feature types — detect and prompt decomposition
|
||||
- Automatic CI secret rotation advice — Phase 7, advisory only
|
||||
- Web platform integration test support — flagged as TODO in generated README
|
||||
- Visual companion for architecture diagrams — post-V1
|
||||
|
||||
---
|
||||
|
||||
## 11. Verification
|
||||
|
||||
End-to-end test of the skill:
|
||||
|
||||
1. Run `cursor_gen --wizard` on a test project → choose `bloc/clean/firebase` stack
|
||||
2. Confirm `.cursor/skills/build/SKILL.md` is generated
|
||||
3. In Cursor, type `/build implement notification module end-to-end`
|
||||
4. Verify Phase 1 prints correct context table from project-brief.yaml
|
||||
5. Verify Phase 3 tests are red before implementation, green after
|
||||
6. Verify Phase 4 generates `integration_test/notifications/` with 10 test files
|
||||
7. Verify Phase 5 checklist lists Firebase Console + APNs steps
|
||||
8. Verify Phase 6 requires real output before proceeding
|
||||
9. Run golden tests: `dart test test/generator_test.dart` — all stacks must match golden SKILL.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.
|
||||
|
||||
|
||||
@@ -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.5"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 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"]
|
||||
@@ -106,6 +116,17 @@ localization:
|
||||
enabled: true
|
||||
locales: ["en", "es", "fr"]
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Integrations & conventions (optional)
|
||||
# -----------------------------------------------------------------------------
|
||||
integrations:
|
||||
mcp:
|
||||
enabled: false # true → .cursor/mcp.json (secrets only via env vars)
|
||||
preset: auto # auto | minimal
|
||||
|
||||
conventions:
|
||||
strict_package_imports: false
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Telemetry (Pillar 6) — local-only generation / usage log under .cursor/
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# Changelog
|
||||
|
||||
## [1.0.4] - 2026-05-13
|
||||
### Fixed
|
||||
- Global `dart pub global` installs: locate `templates/` from the package root containing `pubspec.yaml` (not under `lib/`).
|
||||
|
||||
## [1.0.3] - 2026-05-13
|
||||
### Fixed
|
||||
- Template directory resolution for `dart pub global` snapshot installs; fail fast when the bundle is missing instead of emitting placeholders.
|
||||
|
||||
### Added
|
||||
- Generated repo-root `AGENTS.md` and `lefthook.yaml` (from `templates/root/`).
|
||||
|
||||
## [1.0.2] - 2026-05-13
|
||||
### Changed
|
||||
- Interactive wizard collects the remaining `project-brief.yaml` fields (design, API docs, features, conditional E2E tool and locales) and pins `cursor_templates_version` from a single `kCursorTemplatesVersion` constant.
|
||||
|
||||
## [1.0.1] - 2026-05-13
|
||||
### Fixed
|
||||
- Resolve bundled templates for local, Git, and hosted/global installs so generated files contain real content instead of `Template not found` placeholders.
|
||||
|
||||
### 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
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.0.0
|
||||
1.0.4
|
||||
|
||||
@@ -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.11.3",
|
||||
"flutterRoot": "file:///Users/maheshlalwani.devgmail.com/Documents/flutter",
|
||||
"flutterVersion": "3.41.5",
|
||||
"pubCache": "file:///Users/maheshlalwani.devgmail.com/.pub-cache"
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
{
|
||||
"roots": [
|
||||
"cursor_gen"
|
||||
],
|
||||
"packages": [
|
||||
{
|
||||
"name": "cursor_gen",
|
||||
"version": "1.0.5",
|
||||
"dependencies": [
|
||||
"ansi_styles",
|
||||
"args",
|
||||
"collection",
|
||||
"crypto",
|
||||
"http",
|
||||
"path",
|
||||
"yaml"
|
||||
],
|
||||
"devDependencies": [
|
||||
"lints",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "lints",
|
||||
"version": "3.0.0",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"name": "test",
|
||||
"version": "1.31.0",
|
||||
"dependencies": [
|
||||
"analyzer",
|
||||
"async",
|
||||
"boolean_selector",
|
||||
"collection",
|
||||
"coverage",
|
||||
"http_multi_server",
|
||||
"io",
|
||||
"matcher",
|
||||
"node_preamble",
|
||||
"package_config",
|
||||
"path",
|
||||
"pool",
|
||||
"shelf",
|
||||
"shelf_packages_handler",
|
||||
"shelf_static",
|
||||
"shelf_web_socket",
|
||||
"source_span",
|
||||
"stack_trace",
|
||||
"stream_channel",
|
||||
"test_api",
|
||||
"test_core",
|
||||
"typed_data",
|
||||
"web_socket_channel",
|
||||
"webkit_inspection_protocol",
|
||||
"yaml"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ansi_styles",
|
||||
"version": "0.3.2+1",
|
||||
"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": "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": "typed_data",
|
||||
"version": "1.4.0",
|
||||
"dependencies": [
|
||||
"collection"
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": "source_span",
|
||||
"version": "1.10.2",
|
||||
"dependencies": [
|
||||
"collection",
|
||||
"path",
|
||||
"term_glyph"
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": "matcher",
|
||||
"version": "0.12.19",
|
||||
"dependencies": [
|
||||
"async",
|
||||
"meta",
|
||||
"stack_trace",
|
||||
"term_glyph",
|
||||
"test_api"
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": "async",
|
||||
"version": "2.13.1",
|
||||
"dependencies": [
|
||||
"collection",
|
||||
"meta"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "analyzer",
|
||||
"version": "8.1.1",
|
||||
"dependencies": [
|
||||
"_fe_analyzer_shared",
|
||||
"collection",
|
||||
"convert",
|
||||
"crypto",
|
||||
"glob",
|
||||
"meta",
|
||||
"package_config",
|
||||
"path",
|
||||
"pub_semver",
|
||||
"source_span",
|
||||
"watcher",
|
||||
"yaml"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "web",
|
||||
"version": "1.1.1",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"name": "meta",
|
||||
"version": "1.18.2",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"name": "http_parser",
|
||||
"version": "4.1.2",
|
||||
"dependencies": [
|
||||
"collection",
|
||||
"source_span",
|
||||
"string_scanner",
|
||||
"typed_data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "string_scanner",
|
||||
"version": "1.4.1",
|
||||
"dependencies": [
|
||||
"source_span"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "logging",
|
||||
"version": "1.3.0",
|
||||
"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": "term_glyph",
|
||||
"version": "1.2.2",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"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": "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"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "file",
|
||||
"version": "7.0.1",
|
||||
"dependencies": [
|
||||
"meta",
|
||||
"path"
|
||||
]
|
||||
}
|
||||
],
|
||||
"configVersion": 1
|
||||
}
|
||||
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,34 @@
|
||||
# Changelog
|
||||
|
||||
## 1.0.5
|
||||
|
||||
- Generate `.cursor/mcp.json` from the project brief (MCP presets, backend-specific servers, env placeholders only — no secrets in the repo).
|
||||
- Brief schema, validation, and resolver updates aligned with the current template set.
|
||||
- Refreshed bundled templates (rules, agents, hooks, skills, commands, onboarding, root tooling).
|
||||
|
||||
## 1.0.4
|
||||
|
||||
- Fix package-root detection for template resolution: walk up from the resolved `package:cursor_gen/...` file until `pubspec.yaml` is found (global installs were incorrectly using `lib/templates`).
|
||||
|
||||
## 1.0.3
|
||||
|
||||
- Resolve template bundle by sentinel file (`rules/universal/flutter-core.mdc.tmpl`); prefer package `templates/` first for global installs; exit with a clear error instead of writing placeholder-only output.
|
||||
- Generate repo-root `AGENTS.md` and `lefthook.yaml` from `templates/root/`.
|
||||
- Golden tests now `await` async golden comparison so repo-root goldens are written reliably.
|
||||
|
||||
## 1.0.2
|
||||
|
||||
- Wizard aligns with full brief schema; shared `kCursorTemplatesVersion` for lock file and generated brief.
|
||||
|
||||
## 1.0.1
|
||||
|
||||
- Fix default template resolution so generated projects use bundled templates instead of writing `Template not found` placeholders.
|
||||
- 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.
|
||||
- Generates `.cursor/` rules and related config from `project-brief.yaml`.
|
||||
- Supports validate, generate, refresh (preserve customizations), diff preview, wizard, and update checks.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 cursor_gen contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,98 @@
|
||||
# cursor_gen
|
||||
|
||||
CLI that generates project-specific [Cursor](https://cursor.com) AI configuration for Flutter projects from a `project-brief.yaml` file (stack, architecture, routing, backends, testing, and more).
|
||||
|
||||
## Requirements
|
||||
|
||||
- Dart SDK `>=3.3.0 <4.0.0`
|
||||
|
||||
## Install
|
||||
|
||||
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 .
|
||||
```
|
||||
|
||||
If your shell cannot find `cursor_gen`, add Dart's global bin directory to your `PATH`:
|
||||
|
||||
```bash
|
||||
export PATH="$PATH":"$HOME/.pub-cache/bin"
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
```bash
|
||||
cursor_gen --help
|
||||
cursor_gen --validate --brief project-brief.yaml
|
||||
cursor_gen --brief project-brief.yaml --output .cursor
|
||||
cursor_gen --refresh # regenerate while preserving custom/ and CURSOR:CUSTOM blocks
|
||||
cursor_gen --wizard # interactive project-brief.yaml creator
|
||||
```
|
||||
|
||||
## Minimal `project-brief.yaml`
|
||||
|
||||
```yaml
|
||||
project:
|
||||
name: MyApp
|
||||
package: com.example.myapp
|
||||
scale: medium
|
||||
stack:
|
||||
state_management: bloc
|
||||
architecture: clean
|
||||
routing: gorouter
|
||||
platforms: [ios, android]
|
||||
backend: firebase
|
||||
auth: firebase_auth
|
||||
```
|
||||
|
||||
Use `brief-schema.json` in this package for IDE validation and autocomplete of the full schema.
|
||||
|
||||
## Repository
|
||||
|
||||
For full monorepo layout, template library, and architecture notes, see the parent repository README.
|
||||
@@ -1,9 +1,13 @@
|
||||
#!/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';
|
||||
import '../src/renderer.dart';
|
||||
@@ -16,16 +20,34 @@ import '../src/logger.dart';
|
||||
|
||||
Future<void> main(List<String> 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 {
|
||||
@@ -47,8 +69,8 @@ Future<void> main(List<String> 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) {
|
||||
@@ -84,8 +106,9 @@ Future<void> main(List<String> 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) {
|
||||
@@ -95,9 +118,11 @@ Future<void> main(List<String> 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);
|
||||
@@ -105,51 +130,72 @@ Future<void> main(List<String> arguments) async {
|
||||
final templateFiles = Resolver.resolve(brief);
|
||||
Logger.info('Resolved ${templateFiles.length} template files');
|
||||
|
||||
final templateDir = templateSrc.isEmpty ? _defaultTemplateDir() : templateSrc;
|
||||
final templateDir = await _resolveTemplateDir(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/');
|
||||
final underCursor =
|
||||
rendered.keys.where((k) => !k.startsWith('__root__/')).length;
|
||||
final atRoot = rendered.length - underCursor;
|
||||
Logger.success(
|
||||
'\n✔ Done! $underCursor file(s) under $outputDir/'
|
||||
'${atRoot > 0 ? ", $atRoot at project root" : ""}.');
|
||||
if (overrideSnapshot.customFiles.isNotEmpty) {
|
||||
Logger.success(' ↳ ${overrideSnapshot.customFiles.length} custom override(s) preserved untouched');
|
||||
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<void> _runDiff(dynamic brief, String outputDir, String templateSrc) async {
|
||||
Future<void> _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: await _resolveTemplateDir(templateSrc),
|
||||
);
|
||||
Logger.info('Diff preview (no files written):\n');
|
||||
for (final entry in rendered.entries) {
|
||||
final existing = File('$outputDir/${entry.key}');
|
||||
final existing = File(_resolvedOutputPath(outputDir, entry.key));
|
||||
if (!existing.existsSync()) {
|
||||
Logger.success(' + ${entry.key}');
|
||||
} else {
|
||||
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<void> _writeOutput(
|
||||
String outputDir, Map<String, String> rendered,
|
||||
OverrideSnapshot snapshot, bool isRefresh,
|
||||
String outputDir,
|
||||
Map<String, String> rendered,
|
||||
OverrideSnapshot snapshot,
|
||||
bool isRefresh,
|
||||
) async {
|
||||
for (final entry in rendered.entries) {
|
||||
final file = File('$outputDir/${entry.key}');
|
||||
final file = File(_resolvedOutputPath(outputDir, entry.key));
|
||||
await file.parent.create(recursive: true);
|
||||
if (isRefresh && file.existsSync()) {
|
||||
final merged = OverrideManager.mergeCustomSections(
|
||||
newContent: entry.value, existingContent: await file.readAsString(),
|
||||
newContent: entry.value,
|
||||
existingContent: await file.readAsString(),
|
||||
);
|
||||
await file.writeAsString(merged);
|
||||
} else {
|
||||
@@ -159,9 +205,79 @@ Future<void> _writeOutput(
|
||||
await OverrideManager.restoreCustomFolder(snapshot, outputDir);
|
||||
}
|
||||
|
||||
String _defaultTemplateDir() {
|
||||
final scriptDir = Platform.script.toFilePath();
|
||||
return scriptDir.replaceAll(RegExp(r'bin[/\\]cursor_gen\.dart$'), 'templates');
|
||||
Future<void> _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()));
|
||||
}
|
||||
|
||||
const _templateSentinel = 'rules/universal/flutter-core.mdc.tmpl';
|
||||
|
||||
Future<String> _resolveTemplateDir(String override) async {
|
||||
if (override.isNotEmpty) {
|
||||
final dir = p.normalize(override);
|
||||
_assertTemplateBundle(dir);
|
||||
return dir;
|
||||
}
|
||||
final tried = <String>[];
|
||||
for (final dir in await _templateDirCandidates()) {
|
||||
tried.add(dir);
|
||||
if (File(p.join(dir, _templateSentinel)).existsSync()) return dir;
|
||||
}
|
||||
Logger.error('cursor_gen: No template bundle found (missing $_templateSentinel). Tried:');
|
||||
for (final t in tried) Logger.error(' • $t');
|
||||
Logger.error(
|
||||
'Use: cursor_gen --templates /path/to/cursor_gen/templates (folder containing rules/, hooks/, …)');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
void _assertTemplateBundle(String dir) {
|
||||
if (!File(p.join(dir, _templateSentinel)).existsSync()) {
|
||||
Logger.error('Invalid template directory: $dir ($_templateSentinel not found)');
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<String>> _templateDirCandidates() async {
|
||||
final binDir = p.dirname(Platform.script.toFilePath());
|
||||
final out = <String>[];
|
||||
|
||||
final packageUri = await Isolate.resolvePackageUri(
|
||||
Uri.parse('package:cursor_gen/src/renderer.dart'),
|
||||
);
|
||||
if (packageUri != null && packageUri.isScheme('file')) {
|
||||
final packageRoot = _packageRootDir(packageUri.toFilePath());
|
||||
if (packageRoot.isNotEmpty) {
|
||||
out.add(p.normalize(p.join(packageRoot, 'templates')));
|
||||
}
|
||||
}
|
||||
// Published layout: <package>/templates next to bin/
|
||||
out.add(p.normalize(p.join(binDir, '..', 'templates')));
|
||||
// Monorepo: flutter-cursor-templates/templates
|
||||
out.add(p.normalize(p.join(binDir, '..', '..', 'templates')));
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/// From e.g. `.../cursor_gen-1.0.3/lib/src/renderer.dart`, walk up to the directory that contains `pubspec.yaml`.
|
||||
String _packageRootDir(String resolvedLibFile) {
|
||||
var dir = p.dirname(resolvedLibFile);
|
||||
while (true) {
|
||||
if (File(p.join(dir, 'pubspec.yaml')).existsSync()) return dir;
|
||||
final parent = p.dirname(dir);
|
||||
if (parent == dir) return '';
|
||||
dir = parent;
|
||||
}
|
||||
}
|
||||
|
||||
String _resolvedOutputPath(String outputDir, String relativeKey) {
|
||||
if (relativeKey.startsWith('__root__/')) {
|
||||
final name = relativeKey.substring('__root__/'.length);
|
||||
return p.normalize(p.join(p.dirname(p.absolute(outputDir)), name));
|
||||
}
|
||||
return p.normalize(p.join(p.absolute(outputDir), relativeKey));
|
||||
}
|
||||
|
||||
void _printBanner() {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"cursor_templates_version": {
|
||||
"type": "string",
|
||||
"description": "Pillar 1: Pin to template version for reproducibility",
|
||||
"examples": ["1.0.0"]
|
||||
"examples": ["1.0.5"]
|
||||
},
|
||||
"project": {
|
||||
"type": "object",
|
||||
@@ -91,10 +91,109 @@
|
||||
"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",
|
||||
"default": false
|
||||
},
|
||||
"integrations": {
|
||||
"type": "object",
|
||||
"description": "Optional third-party integrations for generated Cursor config",
|
||||
"properties": {
|
||||
"mcp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "When true, emit .cursor/mcp.json with env-placeholder server stubs only"
|
||||
},
|
||||
"preset": {
|
||||
"type": "string",
|
||||
"enum": ["auto", "minimal"],
|
||||
"default": "auto",
|
||||
"description": "minimal = empty mcpServers; auto = brief-derived stubs (still no committed secrets)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"conventions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"strict_package_imports": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "When true, flutter-core rule enforces package: imports across feature boundaries"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,405 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
_fe_analyzer_shared:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: f0bb5d1648339c8308cc0b9838d8456b3cfe5c91f9dc1a735b4d003269e5da9a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "88.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "0b7b9c329d2879f8f05d6c05b32ee9ec025f39b077864bdb5ac9a7b63418a98f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.1"
|
||||
ansi_styles:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: ansi_styles
|
||||
sha256: "9c656cc12b3c27b17dd982b2cc5c0cfdfbdabd7bc8f3ae5e8542d9867b47ce8a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.2+1"
|
||||
args:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: args
|
||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
cli_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cli_config
|
||||
sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
collection:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: convert
|
||||
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
coverage:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: coverage
|
||||
sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
crypto:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: frontend_server_client
|
||||
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_multi_server
|
||||
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: io
|
||||
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: lints
|
||||
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.19"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: df0c643f44ad098eb37988027a8e2b2b5a031fd3977f06bbfd3a76637e8df739
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.2"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
node_preamble:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: node_preamble
|
||||
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pool
|
||||
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.2"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf
|
||||
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.2"
|
||||
shelf_packages_handler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_packages_handler
|
||||
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
shelf_static:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_static
|
||||
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
source_map_stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_map_stack_trace
|
||||
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
source_maps:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_maps
|
||||
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.13"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.2"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
test:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: test
|
||||
sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.31.0"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.11"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.17"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.2.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket
|
||||
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
webkit_inspection_protocol:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webkit_inspection_protocol
|
||||
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
yaml:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.7.0 <4.0.0"
|
||||
@@ -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.5
|
||||
homepage: https://github.com/company/flutter-cursor-templates
|
||||
|
||||
environment:
|
||||
@@ -9,7 +9,6 @@ environment:
|
||||
dependencies:
|
||||
args: ^2.4.2
|
||||
yaml: ^3.1.2
|
||||
json_schema: ^6.0.0
|
||||
path: ^1.9.0
|
||||
http: ^1.2.0
|
||||
crypto: ^3.0.3
|
||||
|
||||
@@ -7,66 +7,82 @@ 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();
|
||||
final integrations = yaml['integrations'] as YamlMap? ?? YamlMap();
|
||||
final mcp = integrations['mcp'] as YamlMap? ?? YamlMap();
|
||||
final conventions = yaml['conventions'] 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']) ?? [],
|
||||
|
||||
mcpConfigEnabled: mcp['enabled'] as bool? ?? false,
|
||||
mcpPreset: mcp['preset']?.toString() ?? 'auto',
|
||||
strictPackageImports:
|
||||
conventions['strict_package_imports'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// mcp_json.dart — Builds .cursor/mcp.json from project brief (env placeholders only).
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'models.dart';
|
||||
|
||||
class McpJsonBuilder {
|
||||
/// JSON string for Cursor MCP config. Never embed secrets — use `\${VAR}` in strings.
|
||||
static String build(ProjectBrief brief) {
|
||||
if (brief.mcpPreset == 'minimal') {
|
||||
return const JsonEncoder.withIndent(' ')
|
||||
.convert(<String, dynamic>{'mcpServers': <String, dynamic>{}});
|
||||
}
|
||||
|
||||
final servers = <String, Map<String, dynamic>>{
|
||||
'filesystem': {
|
||||
'command': 'npx',
|
||||
'args': ['-y', '@modelcontextprotocol/server-filesystem', '.'],
|
||||
'description': 'Read/write project files under the workspace root',
|
||||
},
|
||||
'flutter-docs': {
|
||||
'command': 'npx',
|
||||
'args': ['-y', '@modelcontextprotocol/server-fetch'],
|
||||
'env': {'BASE_URL': 'https://api.flutter.dev/flutter'},
|
||||
'description': 'Flutter API documentation lookup',
|
||||
},
|
||||
'dart-pub': {
|
||||
'command': 'npx',
|
||||
'args': ['-y', '@modelcontextprotocol/server-fetch'],
|
||||
'env': {'BASE_URL': 'https://pub.dev/api'},
|
||||
'description': 'Pub.dev package metadata and versions',
|
||||
},
|
||||
'github': {
|
||||
'command': 'npx',
|
||||
'args': ['-y', '@modelcontextprotocol/server-github'],
|
||||
'env': {'GITHUB_TOKEN': r'${GITHUB_TOKEN}'},
|
||||
'description': 'GitHub issues and PR context (requires GITHUB_TOKEN)',
|
||||
},
|
||||
};
|
||||
|
||||
if (brief.backends.contains('firebase')) {
|
||||
servers['firebase'] = {
|
||||
'command': 'npx',
|
||||
'args': ['-y', '@modelcontextprotocol/server-firebase'],
|
||||
'env': {'FIREBASE_PROJECT': r'${FIREBASE_PROJECT_ID}'},
|
||||
'description':
|
||||
'Firebase project context (configure FIREBASE_PROJECT_ID)',
|
||||
};
|
||||
}
|
||||
|
||||
if (brief.designSource == 'figma_mcp') {
|
||||
servers['figma'] = {
|
||||
'url': 'https://mcp.figma.com/mcp',
|
||||
'type': 'http',
|
||||
'headers': {
|
||||
'Authorization': r'Bearer ${FIGMA_ACCESS_TOKEN}',
|
||||
},
|
||||
'description':
|
||||
'Figma MCP — set FIGMA_ACCESS_TOKEN (never commit the value)',
|
||||
};
|
||||
}
|
||||
|
||||
if (brief.backends.contains('supabase')) {
|
||||
servers['supabase'] = {
|
||||
'command': 'npx',
|
||||
'args': ['-y', '@supabase/mcp-server-supabase@latest'],
|
||||
'env': {
|
||||
'SUPABASE_ACCESS_TOKEN': r'${SUPABASE_ACCESS_TOKEN}',
|
||||
},
|
||||
'description': 'Supabase MCP — token from env only',
|
||||
};
|
||||
}
|
||||
|
||||
if (brief.apiDocsFormat == 'openapi' && brief.apiDocsPath.isNotEmpty) {
|
||||
servers['openapi-ref'] = {
|
||||
'command': 'npx',
|
||||
'args': ['-y', '@modelcontextprotocol/server-fetch'],
|
||||
'env': {
|
||||
'OPENAPI_SPEC_URL': r'${OPENAPI_SPEC_URL}',
|
||||
},
|
||||
'description':
|
||||
'Optional fetch MCP — point OPENAPI_SPEC_URL at a hosted spec or file:// URL you expose locally',
|
||||
};
|
||||
}
|
||||
|
||||
return const JsonEncoder.withIndent(' ').convert({'mcpServers': servers});
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,22 @@ class ProjectBrief {
|
||||
// Telemetry opt-in (Pillar 6)
|
||||
final bool telemetryOptIn;
|
||||
|
||||
/// Theme variants to support (subset of light, dark, high_contrast).
|
||||
final List<String> themeVariants;
|
||||
|
||||
/// App-level RBAC: when true, [roleNames] should list concrete roles.
|
||||
final bool rolesEnabled;
|
||||
final List<String> roleNames;
|
||||
|
||||
/// When true, emit `.cursor/mcp.json` with safe env-placeholder servers.
|
||||
final bool mcpConfigEnabled;
|
||||
|
||||
/// When [mcpConfigEnabled]: `minimal` (empty `mcpServers`) or `auto` (stubs from brief).
|
||||
final String mcpPreset;
|
||||
|
||||
/// When true, generated flutter-core rule enforces package imports (no cross-feature relatives).
|
||||
final bool strictPackageImports;
|
||||
|
||||
const ProjectBrief({
|
||||
required this.projectName,
|
||||
required this.packageId,
|
||||
@@ -80,7 +96,37 @@ class ProjectBrief {
|
||||
required this.locales,
|
||||
this.cursorTemplatesVersion,
|
||||
this.telemetryOptIn = false,
|
||||
this.themeVariants = const ['light', 'dark'],
|
||||
this.rolesEnabled = false,
|
||||
this.roleNames = const [],
|
||||
this.mcpConfigEnabled = false,
|
||||
this.mcpPreset = 'auto',
|
||||
this.strictPackageImports = false,
|
||||
});
|
||||
|
||||
/// Local snapshot for tooling (written as `cursor-gen-metadata.json` under the output dir).
|
||||
Map<String, dynamic> 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<String>.from(referenceRepos),
|
||||
'local_paths': List<String>.from(localPaths),
|
||||
},
|
||||
'app_context': {
|
||||
'theme_variants': List<String>.from(themeVariants),
|
||||
'roles_enabled': rolesEnabled,
|
||||
'role_names': List<String>.from(roleNames),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ValidationResult {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import 'dart:io';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'mcp_json.dart';
|
||||
import 'models.dart';
|
||||
|
||||
class Renderer {
|
||||
@@ -10,21 +11,32 @@ class Renderer {
|
||||
required List<String> templateFiles,
|
||||
required String templateSrc,
|
||||
}) async {
|
||||
final context = _buildContext(brief);
|
||||
final output = <String, String>{};
|
||||
final baseContext = _buildContext(brief);
|
||||
final output = <String, String>{};
|
||||
|
||||
for (final key in templateFiles) {
|
||||
if (key == 'config/mcp-json') {
|
||||
output['mcp.json'] = McpJsonBuilder.build(brief);
|
||||
continue;
|
||||
}
|
||||
|
||||
final outPath = _outputPath(key);
|
||||
final tmplPath = _templatePath(templateSrc, key);
|
||||
final file = File(tmplPath);
|
||||
if (!file.existsSync()) {
|
||||
// Gracefully skip missing optional templates with a placeholder
|
||||
output[_outputPath(key)] = _missingTemplatePlaceholder(key);
|
||||
output[outPath] = _missingTemplatePlaceholder(key);
|
||||
continue;
|
||||
}
|
||||
var content = await file.readAsString();
|
||||
content = _substituteAll(content, context);
|
||||
_checkUnreplacedPlaceholders(content, key); // Pillar 3: validate no broken {{VAR}}
|
||||
output[_outputPath(key)] = content;
|
||||
final ctx = Map<String, String>.from(baseContext);
|
||||
if (key.startsWith('rules/features/')) {
|
||||
final slug = p.basename(key);
|
||||
ctx['FEATURE_MODULE'] = slug;
|
||||
ctx['FEATURE_MODULE_TITLE'] = _titleCase(slug);
|
||||
}
|
||||
content = _substituteAll(content, ctx);
|
||||
_checkUnreplacedPlaceholders(content, key);
|
||||
output[outPath] = content;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
@@ -32,41 +44,73 @@ class Renderer {
|
||||
/// Build the full substitution context from a brief
|
||||
static Map<String, String> _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.5',
|
||||
'IMPORT_POLICY_BLOCK': _importPolicyBlock(brief.strictPackageImports),
|
||||
};
|
||||
}
|
||||
|
||||
static String _titleCase(String slug) {
|
||||
if (slug.isEmpty) return slug;
|
||||
return slug
|
||||
.split('_')
|
||||
.where((w) => w.isNotEmpty)
|
||||
.map((w) => '${w[0].toUpperCase()}${w.substring(1)}')
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
static String _importPolicyBlock(bool strictPackage) {
|
||||
if (strictPackage) {
|
||||
return '''
|
||||
### Imports (strict — `conventions.strict_package_imports: true`)
|
||||
- Use `package:<your_pubspec_name>/...` imports everywhere in `lib/` and `test/` — **no** relative `../` across feature boundaries (the brief `project.package` id is often the app bundle id; prefer the **pubspec.yaml `name`** for Dart imports)
|
||||
- Barrel files (`index.dart`) at feature roots; do not wildcard re-export third-party packages
|
||||
''';
|
||||
}
|
||||
return '''
|
||||
### Imports (default)
|
||||
- Order: `dart:` → `package:` → relative
|
||||
- Relative imports are allowed **within** the same feature directory; use `package:` imports for cross-feature code
|
||||
- Never import another feature's internals from outside that feature
|
||||
''';
|
||||
}
|
||||
|
||||
static String _substituteAll(String content, Map<String, String> ctx) {
|
||||
for (final entry in ctx.entries) {
|
||||
content = content.replaceAll('{{${entry.key}}}', entry.value);
|
||||
@@ -80,31 +124,52 @@ 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('root/')) {
|
||||
return p.join(templateSrc, '$key.tmpl');
|
||||
}
|
||||
if (key.startsWith('skills/')) {
|
||||
final skillName = p.basename(key);
|
||||
return p.join(templateSrc, 'skills', skillName, 'SKILL.md.tmpl');
|
||||
}
|
||||
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');
|
||||
}
|
||||
return p.join(templateSrc, 'hooks', '${p.basename(key)}.ts.tmpl');
|
||||
}
|
||||
if (key.startsWith('commands/')) {
|
||||
final name = p.basename(key);
|
||||
return p.join(templateSrc, 'commands', '$name.md.tmpl');
|
||||
}
|
||||
if (key == 'onboarding/ONBOARDING') {
|
||||
return p.join(templateSrc, 'onboarding', 'ONBOARDING.md.tmpl');
|
||||
}
|
||||
if (key.startsWith('telemetry/')) {
|
||||
final leaf = p.basename(key);
|
||||
if (leaf == 'gitignore') {
|
||||
return p.join(templateSrc, 'telemetry', 'gitignore.tmpl');
|
||||
}
|
||||
if (leaf == 'log-sh') {
|
||||
return p.join(templateSrc, 'telemetry', 'log.sh.tmpl');
|
||||
}
|
||||
}
|
||||
if (key.startsWith('rules/features/')) {
|
||||
return p.join(templateSrc, 'rules', 'features', '_stub.mdc.tmpl');
|
||||
}
|
||||
return p.join(templateSrc, '$key.mdc.tmpl');
|
||||
}
|
||||
|
||||
static String _outputPath(String key) {
|
||||
if (key.startsWith('root/')) {
|
||||
return '__root__/${key.substring(5)}';
|
||||
}
|
||||
if (key.startsWith('skills/')) {
|
||||
final name = p.basename(key);
|
||||
return 'skills/$name/SKILL.md';
|
||||
@@ -113,6 +178,22 @@ class Renderer {
|
||||
if (key.endsWith('hooks-json')) return 'hooks/hooks.json';
|
||||
return 'hooks/${p.basename(key)}.ts';
|
||||
}
|
||||
if (key.startsWith('commands/')) {
|
||||
final name = p.basename(key);
|
||||
return 'commands/$name.md';
|
||||
}
|
||||
if (key == 'onboarding/ONBOARDING') {
|
||||
return 'ONBOARDING.md';
|
||||
}
|
||||
if (key == 'telemetry/gitignore') {
|
||||
return 'telemetry/.gitignore';
|
||||
}
|
||||
if (key == 'telemetry/log-sh') {
|
||||
return 'telemetry/log.sh';
|
||||
}
|
||||
if (key == 'config/mcp-json') {
|
||||
return 'mcp.json';
|
||||
}
|
||||
return '${key.replaceAll('rules/', 'rules/')}.mdc';
|
||||
}
|
||||
|
||||
@@ -122,33 +203,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':
|
||||
|
||||
@@ -8,6 +8,9 @@ class Resolver {
|
||||
static List<String> resolve(ProjectBrief brief) {
|
||||
final files = <String>[];
|
||||
|
||||
// ── Meta: rule authoring (first — governs other .mdc files) ─────────
|
||||
files.add('rules/universal/rule-authoring');
|
||||
|
||||
// ── Universal (every project) ──────────────────────────────────────
|
||||
files.addAll([
|
||||
'rules/universal/flutter-core',
|
||||
@@ -15,6 +18,11 @@ class Resolver {
|
||||
'rules/universal/project-context',
|
||||
]);
|
||||
|
||||
// ── Theming tokens (when high contrast or extra variants) ───────────
|
||||
if (_emitThemingRule(brief)) {
|
||||
files.add('rules/theming/theming');
|
||||
}
|
||||
|
||||
// ── Security (Pillar 5: always-on, not just for large/auth projects) ──
|
||||
files.add('rules/security/security-standards');
|
||||
|
||||
@@ -28,11 +36,19 @@ class Resolver {
|
||||
files.add('rules/architecture/${brief.architecture}');
|
||||
|
||||
// ── Backend — one or more ─────────────────────────────────────────
|
||||
for (final b in brief.backends) files.add('rules/backend/$b');
|
||||
for (final b in brief.backends) {
|
||||
files.add('rules/backend/$b');
|
||||
}
|
||||
if (brief.specialFeatures.contains('realtime')) {
|
||||
files.add('rules/backend/realtime');
|
||||
}
|
||||
|
||||
// ── Push / deep linking ───────────────────────────────────────────
|
||||
if (brief.specialFeatures.contains('push_notifications') ||
|
||||
brief.specialFeatures.contains('deep_linking')) {
|
||||
files.add('rules/integrations/push-deeplink');
|
||||
}
|
||||
|
||||
// ── Routing — exactly one ─────────────────────────────────────────
|
||||
files.add('rules/routing/${brief.routing}');
|
||||
|
||||
@@ -52,20 +68,47 @@ 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');
|
||||
}
|
||||
|
||||
// ── CI/CD + flavours ─────────────────────────────────────────────
|
||||
if (brief.cicd != 'none' || brief.flavors.length > 1) {
|
||||
files.add('rules/cicd/cicd');
|
||||
}
|
||||
|
||||
// ── Feature-scoped rule stubs ─────────────────────────────────────
|
||||
final featureKeys = <String>{};
|
||||
for (final m in brief.featureModules) {
|
||||
final k = featureRuleKey(m);
|
||||
if (k != null) featureKeys.add(k);
|
||||
}
|
||||
files.addAll(featureKeys);
|
||||
|
||||
// ── Skills ────────────────────────────────────────────────────────
|
||||
files.addAll([
|
||||
'skills/scaffold-feature',
|
||||
'skills/scaffold-screen',
|
||||
'skills/generate-tests',
|
||||
'skills/build',
|
||||
'skills/debug-issue',
|
||||
'skills/verify-change',
|
||||
'skills/explain-code',
|
||||
]);
|
||||
if (brief.apiDocsFormat != 'none') files.add('skills/generate-api-client');
|
||||
if (brief.flavors.length > 1) files.add('skills/create-flavor');
|
||||
if (brief.cicd.isNotEmpty) files.add('skills/deploy');
|
||||
if (brief.flavors.length > 1) files.add('skills/create-flavor');
|
||||
if (brief.cicd != 'none') files.add('skills/deploy');
|
||||
|
||||
// ── Agents ────────────────────────────────────────────────────────
|
||||
files.addAll([
|
||||
@@ -83,17 +126,51 @@ class Resolver {
|
||||
files.add('agents/migration-agent');
|
||||
}
|
||||
|
||||
// ── Hooks ─────────────────────────────────────────────────────────
|
||||
// ── Cursor workspace extras (reference architecture) ────────────────
|
||||
files.addAll([
|
||||
'hooks/hooks-json',
|
||||
'hooks/flutter-analyze',
|
||||
'hooks/grind-tests',
|
||||
'hooks/arch-guard',
|
||||
'root/.cursorignore',
|
||||
'root/tool/cursor_audit.sh',
|
||||
'onboarding/ONBOARDING',
|
||||
'commands/build',
|
||||
'commands/debug-issue',
|
||||
'commands/verify-change',
|
||||
'commands/explain-code',
|
||||
]);
|
||||
|
||||
if (brief.mcpConfigEnabled) {
|
||||
files.add('config/mcp-json');
|
||||
}
|
||||
|
||||
if (brief.telemetryOptIn) {
|
||||
files.addAll([
|
||||
'telemetry/gitignore',
|
||||
'telemetry/log-sh',
|
||||
'rules/telemetry/usage-logging',
|
||||
]);
|
||||
}
|
||||
|
||||
files.addAll(['root/AGENTS.md', 'root/lefthook.yaml']);
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/// `lib/features/<slug>/` ↔ `rules/features/<slug>.mdc`
|
||||
static String? featureRuleKey(String module) {
|
||||
var s = module
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replaceAll(RegExp(r'[^a-z0-9]+'), '_');
|
||||
s = s.replaceAll(RegExp(r'_+'), '_');
|
||||
s = s.replaceAll(RegExp(r'(^_+|_+$)'), '');
|
||||
if (s.isEmpty) return null;
|
||||
return 'rules/features/$s';
|
||||
}
|
||||
|
||||
static bool _emitThemingRule(ProjectBrief brief) {
|
||||
return brief.themeVariants.contains('high_contrast') ||
|
||||
brief.themeVariants.length > 2;
|
||||
}
|
||||
|
||||
/// Returns human-readable description of why each file was included
|
||||
static Map<String, String> resolveWithReasons(ProjectBrief brief) {
|
||||
final resolved = resolve(brief);
|
||||
@@ -105,21 +182,100 @@ class Resolver {
|
||||
}
|
||||
|
||||
static String _reason(String key, ProjectBrief brief) {
|
||||
if (key.contains('universal')) return 'Always included';
|
||||
if (key.contains('security')) return 'Always included — Pillar 5';
|
||||
if (key == 'rules/universal/rule-authoring') {
|
||||
return 'Always included — meta rules for authoring .cursor/rules';
|
||||
}
|
||||
if (key.contains('universal')) return 'Always included';
|
||||
if (key == 'rules/theming/theming') {
|
||||
return 'Theme variants include high contrast or extended set';
|
||||
}
|
||||
if (key.contains('security')) {
|
||||
return 'Always included — Pillar 5';
|
||||
}
|
||||
if (key.contains('error-handling')) return 'Always included';
|
||||
if (key.contains('state-management')) return 'Matches stack.state_management: ${brief.stateManagement}';
|
||||
if (key.contains('architecture')) return 'Matches stack.architecture: ${brief.architecture}';
|
||||
if (key.contains('routing')) return 'Matches stack.routing: ${brief.routing}';
|
||||
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.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';
|
||||
if (key.contains('security-agent')) return 'scale: ${brief.scale} or auth is configured';
|
||||
if (key.contains('api-client')) return 'api_docs.format is set';
|
||||
if (key.contains('realtime')) return 'features.special contains realtime';
|
||||
if (key.contains('state-management')) {
|
||||
return 'Matches stack.state_management: ${brief.stateManagement}';
|
||||
}
|
||||
if (key.contains('architecture')) {
|
||||
return 'Matches stack.architecture: ${brief.architecture}';
|
||||
}
|
||||
if (key == 'rules/integrations/push-deeplink') {
|
||||
return 'features.special includes push_notifications or deep_linking';
|
||||
}
|
||||
if (key.contains('routing')) {
|
||||
return 'Matches stack.routing: ${brief.routing}';
|
||||
}
|
||||
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 == 'rules/cicd/cicd') {
|
||||
return 'environments.cicd is set or multiple flavors';
|
||||
}
|
||||
if (key.startsWith('rules/features/')) {
|
||||
return 'Listed under features.modules in project-brief.yaml';
|
||||
}
|
||||
if (key.contains('migration')) {
|
||||
return 'state_management is GetX — migration guidance included';
|
||||
}
|
||||
if (key.contains('security-agent')) {
|
||||
return 'scale: ${brief.scale} or auth is configured';
|
||||
}
|
||||
if (key == 'skills/build') {
|
||||
return 'Always included — universal TDD-first feature implementation command';
|
||||
}
|
||||
if (key == 'skills/debug-issue') {
|
||||
return 'Always included — structured bug triage and evidence-first debugging';
|
||||
}
|
||||
if (key == 'skills/verify-change') {
|
||||
return 'Always included — pre-PR verification checklist without full /build lifecycle';
|
||||
}
|
||||
if (key == 'skills/explain-code') {
|
||||
return 'Always included — explain-only walkthrough of code paths and stack behavior';
|
||||
}
|
||||
if (key.contains('api-client')) {
|
||||
return 'api_docs.format is set';
|
||||
}
|
||||
if (key.contains('realtime')) {
|
||||
return 'features.special contains realtime';
|
||||
}
|
||||
if (key == 'root/.cursorignore') {
|
||||
return 'Root ignore patterns for Cursor indexing';
|
||||
}
|
||||
if (key == 'root/tool/cursor_audit.sh') {
|
||||
return 'Maintenance script for rule drift checks';
|
||||
}
|
||||
if (key == 'onboarding/ONBOARDING') {
|
||||
return 'Team onboarding for Cursor layout and slash skills';
|
||||
}
|
||||
if (key.startsWith('commands/')) {
|
||||
return 'Project slash command → skill mapping';
|
||||
}
|
||||
if (key == 'config/mcp-json') {
|
||||
return 'integrations.mcp.enabled: true in project-brief.yaml';
|
||||
}
|
||||
if (key == 'telemetry/gitignore' ||
|
||||
key == 'telemetry/log-sh' ||
|
||||
key == 'rules/telemetry/usage-logging') {
|
||||
return 'telemetry_opt_in: true — local usage logging helpers';
|
||||
}
|
||||
if (key.startsWith('root/')) {
|
||||
return 'Repo-level companion files';
|
||||
}
|
||||
return 'Included';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void> 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<void> 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<String, dynamic>;
|
||||
@@ -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<String, dynamic> _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.',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,24 +5,60 @@ 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 const _validMcpPresets = {'auto', 'minimal'};
|
||||
|
||||
static Future<ValidationResult> 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 +70,42 @@ class Validator {
|
||||
}
|
||||
|
||||
static ValidationResult _validateYaml(YamlMap yaml) {
|
||||
final errors = <String>[];
|
||||
final errors = <String>[];
|
||||
final warnings = <String>[];
|
||||
|
||||
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 +115,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 +124,136 @@ 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final integrations = yaml['integrations'] as YamlMap?;
|
||||
if (integrations != null) {
|
||||
final mcp = integrations['mcp'] as YamlMap?;
|
||||
if (mcp != null) {
|
||||
final preset = mcp['preset']?.toString();
|
||||
if (preset != null && !_validMcpPresets.contains(preset)) {
|
||||
warnings.add(
|
||||
'integrations.mcp.preset "$preset" is not valid. Use: ${_validMcpPresets.join(", ")}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ValidationResult(
|
||||
isValid: errors.isEmpty,
|
||||
errors: errors,
|
||||
isValid: errors.isEmpty,
|
||||
errors: errors,
|
||||
warnings: warnings,
|
||||
);
|
||||
}
|
||||
|
||||
static ValidationResult _validateBrief(ProjectBrief brief) {
|
||||
final errors = <String>[];
|
||||
final errors = <String>[];
|
||||
final warnings = <String>[];
|
||||
|
||||
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');
|
||||
}
|
||||
if (!_validMcpPresets.contains(brief.mcpPreset)) {
|
||||
warnings.add(
|
||||
'integrations.mcp.preset "${brief.mcpPreset}" is not valid. Use: ${_validMcpPresets.join(", ")}');
|
||||
}
|
||||
|
||||
return ValidationResult(isValid: errors.isEmpty, errors: errors, warnings: warnings);
|
||||
return ValidationResult(
|
||||
isValid: errors.isEmpty, errors: errors, warnings: warnings);
|
||||
}
|
||||
|
||||
static Iterable<String> _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<String> valid, List<String> output,
|
||||
) {
|
||||
YamlMap map, String field, Set<String> valid, List<String> 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(", ")}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,17 +8,19 @@ import 'models.dart';
|
||||
import 'logger.dart';
|
||||
|
||||
const _lockFileName = '.cursor-gen-lock.json';
|
||||
const _currentVersion = '1.0.0';
|
||||
|
||||
/// Current flutter-cursor-templates bundle version (lock file, wizard, --check-updates).
|
||||
const kCursorTemplatesVersion = '1.0.5';
|
||||
|
||||
class VersionManager {
|
||||
/// Check if the project's locked version differs from the current template version
|
||||
static Future<VersionStatus> check({required ProjectBrief brief}) async {
|
||||
final locked = brief.cursorTemplatesVersion ?? 'unset';
|
||||
final hasUpdate = locked != _currentVersion && locked != 'unset';
|
||||
final hasUpdate = locked != kCursorTemplatesVersion && locked != 'unset';
|
||||
return VersionStatus(
|
||||
hasUpdate: hasUpdate,
|
||||
hasUpdate: hasUpdate,
|
||||
currentVersion: locked,
|
||||
latestVersion: _currentVersion,
|
||||
latestVersion: kCursorTemplatesVersion,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,20 +32,20 @@ class VersionManager {
|
||||
}) async {
|
||||
final briefHash = await _fileHash(briefPath);
|
||||
final lock = {
|
||||
'templateVersion': _currentVersion,
|
||||
'generatedAt': DateTime.now().toIso8601String(),
|
||||
'briefHash': briefHash,
|
||||
'projectName': brief.projectName,
|
||||
'templateVersion': kCursorTemplatesVersion,
|
||||
'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,23 +68,25 @@ 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';
|
||||
Logger.info('Template version check:');
|
||||
Logger.info(' Locked: $lockedVersion');
|
||||
Logger.info(' Latest: $_currentVersion');
|
||||
Logger.info(' Latest: $kCursorTemplatesVersion');
|
||||
|
||||
if (lockedVersion == _currentVersion) {
|
||||
if (lockedVersion == kCursorTemplatesVersion) {
|
||||
Logger.success(' ✔ You are on the latest template version.');
|
||||
} else {
|
||||
Logger.warn(' ⚠ Update available!');
|
||||
Logger.info('\nTo update:');
|
||||
Logger.info(' 1. Update cursor_templates_version in project-brief.yaml to "$_currentVersion"');
|
||||
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 "$kCursorTemplatesVersion"');
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,62 +2,172 @@
|
||||
|
||||
import 'dart:io';
|
||||
import 'logger.dart';
|
||||
import 'version_manager.dart';
|
||||
|
||||
class Wizard {
|
||||
static Future<void> 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 = <String, dynamic>{};
|
||||
|
||||
// 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'] = <String>[];
|
||||
}
|
||||
|
||||
Logger.info('');
|
||||
|
||||
// Features
|
||||
final modulesRaw = _ask(
|
||||
'Feature modules (comma-separated, optional)', hint: '');
|
||||
answers['feature_modules'] = _splitComma(modulesRaw);
|
||||
answers['special_features'] = _askMultiChoice(
|
||||
'Special capabilities (optional)',
|
||||
[
|
||||
'realtime',
|
||||
'push_notifications',
|
||||
'deep_linking',
|
||||
'offline_first',
|
||||
],
|
||||
defaults: []);
|
||||
|
||||
Logger.info('');
|
||||
|
||||
// Environments
|
||||
final flavorsInput = _ask('Build flavors (comma-separated)', hint: 'dev,staging,prod');
|
||||
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);
|
||||
final testingDepth = answers['testing_depth'] as String;
|
||||
if (testingDepth == 'e2e' || testingDepth == 'full') {
|
||||
answers['e2e_tool'] = _askChoice(
|
||||
'E2E tool', ['patrol', 'maestro'], defaultIdx: 0);
|
||||
} else {
|
||||
answers['e2e_tool'] = 'patrol';
|
||||
}
|
||||
|
||||
Logger.info('');
|
||||
|
||||
// Design (validator-aligned sources)
|
||||
answers['design_source'] = _askChoice('Design source', [
|
||||
'figma_mcp',
|
||||
'figma_manual',
|
||||
'native_ref',
|
||||
'html_ref',
|
||||
'none',
|
||||
], defaultIdx: 4);
|
||||
final designSource = answers['design_source'] as String;
|
||||
if (designSource == 'figma_mcp' || designSource == 'figma_manual') {
|
||||
answers['figma_url'] =
|
||||
_ask('Figma file URL', hint: 'https://www.figma.com/design/...');
|
||||
} else {
|
||||
answers['figma_url'] = '';
|
||||
}
|
||||
|
||||
Logger.info('');
|
||||
|
||||
// API docs
|
||||
answers['api_docs_format'] = _askChoice(
|
||||
'API docs format', ['openapi', 'postman', 'markdown', 'none'],
|
||||
defaultIdx: 3);
|
||||
final apiFmt = answers['api_docs_format'] as String;
|
||||
if (apiFmt != 'none') {
|
||||
answers['api_docs_path'] =
|
||||
_ask('Path to API spec (repo-relative)', hint: 'docs/api.yaml');
|
||||
} else {
|
||||
answers['api_docs_path'] = '';
|
||||
}
|
||||
|
||||
Logger.info('');
|
||||
|
||||
// Localization
|
||||
final l10n = _askBool('Enable localization / i18n?', defaultYes: false);
|
||||
answers['i18n'] = l10n;
|
||||
if (l10n) {
|
||||
final locRaw =
|
||||
_ask('Locale codes (comma-separated)', hint: 'en');
|
||||
var locales = _splitComma(locRaw);
|
||||
if (locales.isEmpty) locales = ['en'];
|
||||
answers['locales'] = locales;
|
||||
} else {
|
||||
answers['locales'] = <String>['en'];
|
||||
}
|
||||
|
||||
// Telemetry opt-in (Pillar 6)
|
||||
final telemetry = _askBool('\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 +175,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 +185,8 @@ class Wizard {
|
||||
return input.isEmpty ? hint : input;
|
||||
}
|
||||
|
||||
static String _askChoice(String label, List<String> options, {int defaultIdx = 0}) {
|
||||
static String _askChoice(String label, List<String> options,
|
||||
{int defaultIdx = 0}) {
|
||||
Logger.info(' $label:');
|
||||
for (var i = 0; i < options.length; i++) {
|
||||
final marker = i == defaultIdx ? ' ◀' : '';
|
||||
@@ -85,11 +196,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<String> _askMultiChoice(String label, List<String> options, {List<int> defaults = const []}) {
|
||||
static List<String> _askMultiChoice(String label, List<String> options,
|
||||
{List<int> 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 +212,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 +228,40 @@ class Wizard {
|
||||
return input == 'y' || input == 'yes';
|
||||
}
|
||||
|
||||
static List<String> _splitComma(String s) => s
|
||||
.split(',')
|
||||
.map((e) => e.trim())
|
||||
.where((e) => e.isNotEmpty)
|
||||
.toList();
|
||||
|
||||
static String _yamlQStringList(List<String> 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<String, dynamic> 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<String>.from(a['reference_repos'] as List);
|
||||
final refLocals = List<String>.from(a['local_paths'] as List);
|
||||
final themes = List<String>.from(a['theme_variants'] as List);
|
||||
final roles = List<String>.from(a['role_names'] as List);
|
||||
final featureMods = List<String>.from(a['feature_modules'] as List);
|
||||
final specialFeats = List<String>.from(a['special_features'] as List);
|
||||
final locales = List<String>.from(a['locales'] as List);
|
||||
final figmaUrl = a['figma_url'] as String;
|
||||
final apiPath = a['api_docs_path'] as String;
|
||||
return '''# project-brief.yaml — cursor_gen configuration
|
||||
# Generated by cursor_gen --wizard
|
||||
# Run: 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: "$kCursorTemplatesVersion"
|
||||
|
||||
project:
|
||||
name: "${a['name']}"
|
||||
@@ -151,27 +288,32 @@ environments:
|
||||
|
||||
testing:
|
||||
depth: "${a['testing_depth']}"
|
||||
e2e_tool: "patrol"
|
||||
e2e_tool: "${a['e2e_tool']}"
|
||||
|
||||
design:
|
||||
source: "none"
|
||||
figma_url: ""
|
||||
source: "${a['design_source']}"
|
||||
figma_url: "${_yamlEsc(figmaUrl)}"
|
||||
|
||||
api_docs:
|
||||
format: "none"
|
||||
path: ""
|
||||
format: "${a['api_docs_format']}"
|
||||
path: "${_yamlEsc(apiPath)}"
|
||||
|
||||
references:
|
||||
repos: []
|
||||
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: []
|
||||
special: []
|
||||
modules: [${_yamlQStringList(featureMods)}]
|
||||
special: [${_yamlQStringList(specialFeats)}]
|
||||
|
||||
localization:
|
||||
enabled: ${a['i18n']}
|
||||
locales: ["en"]
|
||||
locales: [${_yamlQStringList(locales)}]
|
||||
|
||||
# Pillar 6: Opt-in local telemetry (logs rule trigger frequency locally)
|
||||
telemetry_opt_in: ${a['telemetry']}
|
||||
|
||||
@@ -0,0 +1,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<String, dynamic> json) => _$ProductDtoFromJson(json);
|
||||
}
|
||||
|
||||
// data/datasources/product_remote_datasource.dart
|
||||
class ProductRemoteDataSource {
|
||||
final Dio _dio;
|
||||
Future<List<ProductDto>> getProducts() async { ... }
|
||||
}
|
||||
```
|
||||
@@ -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
|
||||
@@ -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<Controller>()` | `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<C>` → `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
|
||||
@@ -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
|
||||
```
|
||||
@@ -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`
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
End-to-end feature implementation (research, TDD, integration tests, verification). Follow the workflow and constraints in `@file:.cursor/skills/build/SKILL.md`. Use `project-brief.yaml` as the source of truth for stack and platforms.
|
||||
@@ -0,0 +1 @@
|
||||
Structured bug triage and evidence-first debugging. Follow `@file:.cursor/skills/debug-issue/SKILL.md`. Gather reproduction steps, logs, and failing commands before proposing fixes.
|
||||
@@ -0,0 +1 @@
|
||||
Explain-only walkthrough of code paths and stack behavior (no edits). Follow `@file:.cursor/skills/explain-code/SKILL.md`. Do not modify source files unless the user explicitly asks.
|
||||
@@ -0,0 +1 @@
|
||||
Pre-PR verification checklist (analyze, tests, hooks) without full /build lifecycle. Follow `@file:.cursor/skills/verify-change/SKILL.md` for the change in scope.
|
||||
@@ -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}`);
|
||||
@@ -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,20 @@
|
||||
# Cursor — {{PROJECT_NAME}}
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Open this repo in **Cursor** so `.cursor/rules/` and `.cursor/skills/` load automatically.
|
||||
2. Read **`rules/universal/rule-authoring.mdc`** — how we maintain AI rules.
|
||||
3. Stack and product context live in **`project-brief.yaml`** at the repo root; regenerate `.cursor/` with `cursor_gen` after changing it.
|
||||
4. **Slash skills** (primary workflows): open the Command Palette and use the project commands, or reference:
|
||||
- `.cursor/skills/build/SKILL.md` — end-to-end feature implementation
|
||||
- `.cursor/skills/debug-issue/SKILL.md` — structured debugging
|
||||
- `.cursor/skills/verify-change/SKILL.md` — pre-PR verification
|
||||
- `.cursor/skills/explain-code/SKILL.md` — explain-only walkthroughs
|
||||
5. **Agents** (`.cursor/agents/*.mdc`) are reusable reviewer personas — attach when asking for code review or focused passes.
|
||||
6. **Custom overrides**: files under `.cursor/custom/` and `CURSOR:CUSTOM` blocks in generated files are preserved on `cursor_gen --refresh` (see generator docs).
|
||||
|
||||
## Optional
|
||||
|
||||
- **`integrations.mcp.enabled`** in `project-brief.yaml` — generates `.cursor/mcp.json` with env-based server entries (no secrets in git).
|
||||
- **`telemetry_opt_in: true`** — emits local telemetry helpers under `.cursor/telemetry/` (never commit usage logs if you enable logging).
|
||||
- Run **`bash tool/cursor_audit.sh`** from the project root periodically to catch stale feature rules and overly broad `alwaysApply` usage.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Build artefacts
|
||||
build/
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
*.g.dart
|
||||
*.freezed.dart
|
||||
*.gr.dart
|
||||
*.config.dart
|
||||
|
||||
# Secrets
|
||||
.env
|
||||
.env.*
|
||||
firebase_options.dart
|
||||
google-services.json
|
||||
GoogleService-Info.plist
|
||||
|
||||
# Large binary assets
|
||||
assets/fonts/
|
||||
assets/videos/
|
||||
*.aab
|
||||
*.apk
|
||||
*.ipa
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
*.iml
|
||||
@@ -0,0 +1,7 @@
|
||||
# {{PROJECT_NAME}}
|
||||
|
||||
Repo-level notes for AI assistants. Authoritative stack and conventions are in `project-brief.yaml` and `.cursor/` (regenerate with `cursor_gen` from the project root).
|
||||
|
||||
- **Package:** `{{PACKAGE_ID}}`
|
||||
- **Onboarding:** `.cursor/ONBOARDING.md` — layout, slash commands → skills, MCP opt-in, audits
|
||||
- **Slash skills** (see `.cursor/skills/`): `/build`, `/debug`, `/verify`, `/explain` (see `.cursor/commands/*.md`)
|
||||
@@ -0,0 +1,6 @@
|
||||
# {{PROJECT_NAME}} — generated by cursor_gen; adjust commands to your repo
|
||||
# Optional: run `bash tool/cursor_audit.sh` after changing features.modules or rule files.
|
||||
pre-commit:
|
||||
commands:
|
||||
flutter-analyze:
|
||||
run: flutter analyze
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# {{PROJECT_NAME}} — Cursor rule hygiene (generated by cursor_gen)
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
CURSOR="${ROOT}/.cursor"
|
||||
RULES="${CURSOR}/rules"
|
||||
|
||||
echo "== cursor_audit (${ROOT}) =="
|
||||
|
||||
if [[ ! -d "$CURSOR" ]]; then
|
||||
echo "ERROR: missing ${CURSOR}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -d "$RULES" ]]; then
|
||||
ac="$(grep -R "alwaysApply: true" "$RULES" --include='*.mdc' 2>/dev/null | wc -l | tr -d ' ')"
|
||||
echo "alwaysApply: true occurrences in .cursor/rules: ${ac}"
|
||||
echo " (expect a small number — meta/safety; prefer scoped globs for domain rules)"
|
||||
else
|
||||
echo "WARN: no ${RULES}"
|
||||
fi
|
||||
|
||||
if [[ -d "$RULES/features" ]]; then
|
||||
shopt -s nullglob
|
||||
for f in "$RULES/features"/*.mdc; do
|
||||
base="$(basename "$f" .mdc)"
|
||||
if [[ ! -d "${ROOT}/lib/features/${base}" ]] && [[ ! -d "${ROOT}/lib/feature_${base}" ]]; then
|
||||
echo "WARN: rules/features/${base}.mdc has no obvious lib/features/${base} folder — update features.modules or lib layout"
|
||||
fi
|
||||
done
|
||||
shopt -u nullglob
|
||||
fi
|
||||
|
||||
echo "OK — review warnings above after brief or folder renames"
|
||||
@@ -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<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
|
||||
+49
@@ -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)
|
||||
@@ -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}}
|
||||
@@ -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`
|
||||
@@ -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<void> 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();
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
description: CI/CD, flavours, and quality gates for {{PROJECT_NAME}}
|
||||
globs: [".github/**", "codemagic.yaml", "Makefile", "pubspec.yaml", "fastlane/**"]
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# CI/CD & flavours — {{PROJECT_NAME}}
|
||||
|
||||
## Context
|
||||
Pipeline and flavour setup must stay aligned with how the app is built and released.
|
||||
|
||||
## Flavours
|
||||
- Documented in `project-brief.yaml`: **{{FLAVORS_LIST}}**
|
||||
- Use `--flavor` / `-t lib/main_<flavour>.dart` (or your project’s entrypoints) consistently across local, CI, and store builds
|
||||
- Configuration via `--dart-define` / `--dart-define-from-file` — **never** hardcode secrets in source
|
||||
|
||||
## Quality gates (recommended stages)
|
||||
- **Lint:** `dart format --set-exit-if-changed .` and `flutter analyze`
|
||||
- **Unit + widget:** `flutter test` (with coverage if the team tracks it)
|
||||
- **Goldens:** fixed device/locale; CI fails on drift unless intentionally updated
|
||||
- **Smoke build:** e.g. `flutter build apk` or `flutter build ios --no-codesign` for the primary flavour
|
||||
- **E2E:** when `testing.depth` includes e2e — {{E2E_TOOL}} on CI devices or Firebase Test Lab
|
||||
|
||||
## Cursor integration
|
||||
- After substantive edits: run analyze and relevant tests before PR
|
||||
- For release-oriented tasks, use the **deploy** skill at `.cursor/skills/deploy/SKILL.md` when present
|
||||
|
||||
## CI/CD tool
|
||||
- Selected in brief: **{{CICD_TOOL}}** (`{{CICD_RAW}}`)
|
||||
@@ -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<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)
|
||||
```
|
||||
+42
@@ -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<T>()` 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')`
|
||||
+37
@@ -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<String, dynamic> json) => _$ProductDtoFromJson(json);
|
||||
Map<String, dynamic> 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`
|
||||
@@ -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<List<ProductDto>> getProducts(@Query('category') String? category);
|
||||
|
||||
@GET('/products/{id}')
|
||||
Future<ProductDto> getProduct(@Path('id') String id);
|
||||
|
||||
@POST('/products')
|
||||
Future<ProductDto> 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
|
||||
+67
@@ -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<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,21 @@
|
||||
---
|
||||
description: "Feature module {{FEATURE_MODULE}} — contracts, boundaries, and ownership (fill after design)"
|
||||
globs: ["lib/**/{{FEATURE_MODULE}}/**", "test/**/{{FEATURE_MODULE}}/**"]
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Feature — {{FEATURE_MODULE_TITLE}}
|
||||
|
||||
## Context
|
||||
This stub was generated from `features.modules` in `project-brief.yaml`. Use it to capture **public contracts** (routes, DTOs, events) and **dependencies** for `{{FEATURE_MODULE}}` so agents do not invent cross-feature wiring.
|
||||
|
||||
## Constraints
|
||||
- List external dependencies (other features, packages, backend endpoints) explicitly
|
||||
- Document invariants (auth required, idempotency, offline behavior) when known
|
||||
- Update or delete this file when the module is removed or renamed — run `bash tool/cursor_audit.sh` to catch drift
|
||||
|
||||
## Patterns
|
||||
- Link to key entry points: primary screen(s), state holder(s), repository interface(s)
|
||||
|
||||
## Anti-patterns
|
||||
- Empty file left forever — either fill it or delete the module entry from the brief
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
description: "Localization / i18n conventions for {{PROJECT_NAME}}"
|
||||
globs: ["lib/l10n/**", "lib/**/*.dart", "test/**/*.dart"]
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# 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
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
---
|
||||
description: Push notifications and deep linking for {{PROJECT_NAME}}
|
||||
globs: ["lib/**/*.dart", "android/**", "ios/**", "test/**/*.dart"]
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Push & deep linking — {{PROJECT_NAME}}
|
||||
|
||||
## Context
|
||||
Special capabilities from `project-brief.yaml`: **{{SPECIAL_FEATURES}}**. Native and server configuration must stay consistent.
|
||||
|
||||
## Constraints
|
||||
- **Push:** request permissions through the platform flow; handle denial gracefully; no silent failures on token registration
|
||||
- **Routing:** deep links and notification taps MUST go through **{{ROUTING}}** (no ad-hoc `Navigator` stacks for inbound links)
|
||||
- **Payloads:** map FCM/APNs/Supabase payloads to domain models in the data layer — no JSON parsing scattered in widgets
|
||||
- **iOS:** exercise push on a **physical device** when possible (simulator limitations)
|
||||
- **Android:** declare required permissions explicitly; target API behaviour for POST_NOTIFICATIONS where applicable
|
||||
|
||||
## Testing
|
||||
- Unit-test payload → domain mapping and routing targets
|
||||
- Integration/E2E: cold start, background, and foreground tap-to-open flows when `testing.depth` allows
|
||||
@@ -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+
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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<AutoRoute> 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
|
||||
@@ -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
|
||||
@@ -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<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://{{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
|
||||
+109
@@ -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
|
||||
@@ -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<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 {{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`
|
||||
@@ -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<Product> products = <Product>[].obs;
|
||||
final RxBool isLoading = false.obs;
|
||||
final Rx<String?> error = Rx(null);
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchProducts();
|
||||
}
|
||||
|
||||
Future<void> 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<Controller> — never GetWidget or raw StatelessWidget
|
||||
class ProductsView extends GetView<ProductsController> {
|
||||
@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<Controller>()` 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`
|
||||
+44
@@ -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`
|
||||
@@ -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<User?> build() => ref.watch(authRepositoryProvider).currentUser();
|
||||
|
||||
Future<void> 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<AuthNotifier, AsyncValue<User?>>(
|
||||
(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
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
description: "Policy for optional local AI usage logs under .cursor/telemetry/"
|
||||
globs: [".cursor/telemetry/**"]
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Telemetry — {{PROJECT_NAME}}
|
||||
|
||||
## Context
|
||||
When `telemetry_opt_in: true`, this repo may record **local-only** generation or usage notes under `.cursor/telemetry/`. This is **not** production analytics.
|
||||
|
||||
## Constraints
|
||||
- **Never** commit secrets, tokens, or PII into JSONL or shell history
|
||||
- Prefer redacted summaries over raw prompts
|
||||
- Add `.cursor/telemetry/*.jsonl` to `.gitignore` unless your team explicitly version-controls sanitized samples
|
||||
|
||||
## Patterns
|
||||
- Append one JSON object per line (JSONL) with ISO timestamps and event type
|
||||
- Rotate or truncate files if they grow beyond a few MB
|
||||
@@ -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<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
|
||||
+37
@@ -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
|
||||
@@ -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`
|
||||
@@ -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<AsyncData<List<Product>>>());
|
||||
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
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
description: Semantic colours and theme extensions for {{PROJECT_NAME}}
|
||||
globs: ["lib/**/*.dart", "test/**/*.dart"]
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Theming — {{PROJECT_NAME}}
|
||||
|
||||
## Context
|
||||
Theme variants from brief: **{{THEME_SUMMARY}}**.{{HIGH_CONTRAST_NOTE}}
|
||||
|
||||
## Constraints
|
||||
- Prefer **`ThemeExtension`** (or design-system equivalents) for app-specific colours and radii — avoid raw `Color(0xFF...)` in feature code except in central token definitions
|
||||
- Use **`Theme.of(context).colorScheme`** / `TextTheme` for Material-aligned roles where appropriate
|
||||
- **High contrast:** when supported, verify WCAG contrast for text and controls; never rely on colour alone for state (pair with icon or label){{HIGH_CONTRAST_UX_LINE}}
|
||||
- Touch targets: respect platform minimums (e.g. ~48 logical pixels) for interactive widgets
|
||||
|
||||
## Anti-patterns
|
||||
- Hard-coded `Colors.*` scattered across feature widgets instead of theme tokens
|
||||
- Ignoring `MediaQuery.of(context).platformBrightness` / high-contrast modes when the product claims support
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
description: "Core Flutter conventions for {{PROJECT_NAME}}"
|
||||
globs: ["lib/**/*.dart", "test/**/*.dart", "integration_test/**/*.dart"]
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# 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
|
||||
{{IMPORT_POLICY_BLOCK}}
|
||||
## 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,65 @@
|
||||
---
|
||||
description: "Stack summary and product context for {{PROJECT_NAME}}"
|
||||
globs: ["project-brief.yaml", ".cursor/**/*.md", ".cursor/**/*.mdc", "pubspec.yaml"]
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# 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
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
description: How Cursor rules in this repository must be written and maintained.
|
||||
globs: [".cursor/rules/**/*.mdc"]
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Rule authoring — {{PROJECT_NAME}}
|
||||
|
||||
## Context
|
||||
Rules are version-controlled contracts for AI assistants. Poor rules waste context and silently steer every edit.
|
||||
|
||||
## Constraints
|
||||
- One focused concern per file; split broad topics instead of one mega-rule
|
||||
- Every rule MUST have a clear `description` in frontmatter (one sentence)
|
||||
- Prefer `alwaysApply: false` with **narrow** `globs` for domain rules — reserve `alwaysApply: true` for meta and safety
|
||||
- `globs` must be as specific as possible — never `["**/*"]` unless tooling requires it
|
||||
- Code samples in rules MUST be valid for this project (Dart/Flutter/YAML as appropriate)
|
||||
- Deprecated guidance is removed, not left commented out
|
||||
- Each substantive rule includes **Context** (why), **Constraints** (must/must not), and where helpful **Patterns** / **Anti-patterns**
|
||||
|
||||
## Anti-patterns
|
||||
- Domain rules (testing, l10n, a feature) with `alwaysApply: true` — burns context
|
||||
- Rules with no concrete examples when the topic is code-facing
|
||||
- Stale feature rules after modules are removed — run `tool/cursor_audit.sh` periodically
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
description: "UI/UX standards for {{PROJECT_NAME}}"
|
||||
globs: ["lib/**/*.dart", "test/**/*.dart", "integration_test/**/*.dart"]
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# 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}}
|
||||
@@ -0,0 +1,573 @@
|
||||
# Build — {{PROJECT_NAME}}
|
||||
|
||||
Implements any feature end-to-end: deep research → TDD → integration tests → external setup checklist → verified PR.
|
||||
Stack: **{{STATE_MANAGEMENT}}** / **{{ARCHITECTURE}}** / **{{BACKEND}}** / {{PLATFORMS_LIST}}.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/build <free-text description of what to implement>
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
- `/build implement notification module end-to-end`
|
||||
- `/build add biometric auth`
|
||||
- `/build integrate Stripe payments`
|
||||
- `/build add deep linking for shared product pages`
|
||||
- `/build implement analytics event tracking`
|
||||
|
||||
---
|
||||
|
||||
## FEATURE_REGISTRY
|
||||
|
||||
The AI reads this registry to classify the request and load the right research, test scenarios, and setup steps.
|
||||
|
||||
```yaml
|
||||
feature_registry:
|
||||
|
||||
notifications:
|
||||
keywords: [notification, push, fcm, apns, alert, badge, silent push,
|
||||
remote notification, local notification, firebase messaging]
|
||||
pub_packages: [firebase_messaging, flutter_local_notifications, firebase_core]
|
||||
rules_to_load: [security-standards.mdc, platform-ios.mdc, platform-android.mdc]
|
||||
unit_test_scenarios:
|
||||
- parse notification payload to domain model
|
||||
- FCM token generation and storage
|
||||
- token refresh triggers re-registration
|
||||
- notification permission denied returns graceful fallback
|
||||
- notification service initialisation is idempotent
|
||||
widget_test_scenarios:
|
||||
- notification banner renders correct title and body
|
||||
- tap on notification navigates to correct route
|
||||
- badge count updates on new message
|
||||
integration_test_scenarios:
|
||||
- foreground notification receipt and display
|
||||
- background notification receipt (app backgrounded, not killed)
|
||||
- killed-state notification receipt (cold launch from notification tap)
|
||||
- tap-to-open in foreground state routes correctly
|
||||
- tap-to-open in background state routes correctly
|
||||
- tap-to-open in killed state routes correctly
|
||||
- notification payload parsing end-to-end
|
||||
- deep link routing from notification data field
|
||||
- multiple simultaneous notifications (ordering and dedup)
|
||||
external_setup:
|
||||
firebase: [Enable Cloud Messaging, download google-services.json, download GoogleService-Info.plist]
|
||||
ios: [Push Notifications capability, Background Modes remote notifications, APNs .p8 key upload to Firebase]
|
||||
android: [POST_NOTIFICATIONS permission, RECEIVE_BOOT_COMPLETED permission, google-services plugin]
|
||||
files_to_touch:
|
||||
new: [lib/features/notifications/, integration_test/notifications/]
|
||||
modified: [lib/main.dart, android/app/src/main/AndroidManifest.xml, ios/Runner/Info.plist, ios/Runner/AppDelegate.swift, pubspec.yaml]
|
||||
|
||||
auth:
|
||||
keywords: [auth, authentication, login, sign in, sign out, logout,
|
||||
biometric, face id, touch id, fingerprint, oauth, jwt,
|
||||
session, token refresh, social login, google sign-in]
|
||||
pub_packages: [firebase_auth, supabase_flutter, local_auth, flutter_secure_storage, google_sign_in]
|
||||
rules_to_load: [security-standards.mdc, platform-ios.mdc, platform-android.mdc]
|
||||
unit_test_scenarios:
|
||||
- login success stores token securely
|
||||
- login failure (wrong password) returns typed error
|
||||
- login failure (network) returns typed error
|
||||
- token refresh succeeds and updates stored token
|
||||
- token refresh failure triggers logout
|
||||
- biometric auth success grants access
|
||||
- biometric auth failure falls back to password
|
||||
- biometric not enrolled returns correct error state
|
||||
- logout clears all stored credentials
|
||||
- session persistence: token loaded on cold start
|
||||
widget_test_scenarios:
|
||||
- login form renders correctly
|
||||
- validation errors shown inline
|
||||
- loading state disables submit button
|
||||
- biometric prompt shown when available
|
||||
integration_test_scenarios:
|
||||
- complete login flow (email and password)
|
||||
- login then logout then login again
|
||||
- invalid credentials shows error and stays on login
|
||||
- biometric login flow on enrolled device
|
||||
- biometric fallback to password
|
||||
- token refresh in background while user navigates
|
||||
- cold start with stored valid session skips login
|
||||
- cold start with expired session redirects to login
|
||||
- social auth OAuth redirect and return
|
||||
- deep link into protected route redirects to login then back
|
||||
external_setup:
|
||||
firebase: [Enable Email/Password provider, Enable Google provider, download updated config files]
|
||||
ios: [NSFaceIDUsageDescription in Info.plist, LocalAuthentication.framework in Xcode]
|
||||
android: [USE_BIOMETRIC permission, USE_FINGERPRINT permission, minSdkVersion 23 for biometric]
|
||||
files_to_touch:
|
||||
new: [lib/features/auth/, integration_test/auth/]
|
||||
modified: [lib/main.dart, lib/core/di/injection.dart, pubspec.yaml]
|
||||
|
||||
payments:
|
||||
keywords: [payment, stripe, in-app purchase, iap, checkout, subscription,
|
||||
billing, apple pay, google pay, card, transaction, refund, revenue cat]
|
||||
pub_packages: [flutter_stripe, purchases_flutter, in_app_purchase]
|
||||
rules_to_load: [security-standards.mdc, platform-ios.mdc, platform-android.mdc]
|
||||
unit_test_scenarios:
|
||||
- checkout request builds correct PaymentIntent params
|
||||
- payment success updates order state
|
||||
- payment failure (card declined) returns typed error
|
||||
- webhook event parsed to domain model
|
||||
- refund request constructs correct API call
|
||||
- subscription status checked on app resume
|
||||
widget_test_scenarios:
|
||||
- checkout form renders with correct amount
|
||||
- payment loading state shown during processing
|
||||
- success confirmation screen renders
|
||||
- error state with retry option
|
||||
integration_test_scenarios:
|
||||
- end-to-end checkout with test card (Stripe test mode)
|
||||
- checkout with 3D Secure challenge
|
||||
- card declined shows error then retry succeeds
|
||||
- Apple Pay / Google Pay sheet appears (device capability check)
|
||||
- subscription purchase and entitlement unlock
|
||||
- subscription restore flow
|
||||
- refund flow from order history
|
||||
external_setup:
|
||||
stripe: [publishable and secret keys in flavor .env files, webhook endpoint in Stripe Dashboard, Apple Pay domain registration]
|
||||
ios: [In-App Purchase capability, Apple Pay capability and merchant identifier, com.apple.developer.in-app-payments entitlement]
|
||||
android: [Google Pay Console setup, BILLING permission for in_app_purchase]
|
||||
files_to_touch:
|
||||
new: [lib/features/payments/, integration_test/payments/]
|
||||
modified: [ios/Runner/Runner.entitlements, android/app/src/main/AndroidManifest.xml, pubspec.yaml]
|
||||
|
||||
deep_links:
|
||||
keywords: [deep link, deep linking, universal link, app link, deferred deep link,
|
||||
dynamic link, branch, uri scheme, custom scheme, url scheme]
|
||||
pub_packages: [go_router, app_links, uni_links]
|
||||
rules_to_load: [platform-ios.mdc, platform-android.mdc]
|
||||
unit_test_scenarios:
|
||||
- URI parsed to correct route and parameters
|
||||
- unknown URI falls back to home
|
||||
- authenticated-only route redirects to login when unauthenticated
|
||||
- deep link preserves query parameters
|
||||
widget_test_scenarios:
|
||||
- navigation guard redirects unauthenticated deep link
|
||||
integration_test_scenarios:
|
||||
- cold start from Universal Link routes to correct screen
|
||||
- cold start from custom URI scheme routes correctly
|
||||
- backgrounded app receives deep link and navigates
|
||||
- foreground app receives deep link and navigates
|
||||
- deep link to authenticated route redirects to login then original destination
|
||||
- deep link with path parameters loads correct content
|
||||
- invalid or malformed deep link shows 404 screen
|
||||
external_setup:
|
||||
ios: [Associated Domains capability applinks:yourdomain.com, host apple-app-site-association file at /.well-known/]
|
||||
android: [intent-filter with android:autoVerify=true in AndroidManifest.xml, host assetlinks.json at /.well-known/]
|
||||
files_to_touch:
|
||||
new: [lib/core/routing/deep_link_handler.dart, integration_test/deep_links/]
|
||||
modified: [lib/core/routing/router.dart, android/app/src/main/AndroidManifest.xml, ios/Runner/Runner.entitlements, ios/Runner/Info.plist]
|
||||
|
||||
analytics:
|
||||
keywords: [analytics, tracking, event, mixpanel, amplitude, firebase analytics,
|
||||
segment, posthog, screen view, funnel, cohort]
|
||||
pub_packages: [firebase_analytics, mixpanel_flutter, amplitude_flutter, posthog_flutter]
|
||||
rules_to_load: [security-standards.mdc]
|
||||
unit_test_scenarios:
|
||||
- analytics service logs correct event name and params
|
||||
- PII fields stripped before event sent
|
||||
- analytics disabled in dev flavor
|
||||
- screen name logged on navigation
|
||||
widget_test_scenarios:
|
||||
- RouteObserver triggers screen_view event
|
||||
integration_test_scenarios:
|
||||
- user action triggers expected event (verify via debug view)
|
||||
- screen transitions log screen_view with correct names
|
||||
- opt-out disables all tracking
|
||||
external_setup:
|
||||
firebase: [Enable Analytics in Firebase Console, enable DebugView for local testing, configure conversion events]
|
||||
files_to_touch:
|
||||
new: [lib/core/analytics/analytics_service.dart, lib/core/analytics/analytics_events.dart, integration_test/analytics/]
|
||||
modified: [lib/main.dart, pubspec.yaml]
|
||||
|
||||
storage:
|
||||
keywords: [storage, file upload, download, cloud storage, firebase storage,
|
||||
supabase storage, s3, image upload, document, file picker,
|
||||
offline, cache, hive, isar, objectbox, sqflite, drift]
|
||||
pub_packages: [firebase_storage, supabase_flutter, hive_flutter, isar, drift, file_picker, image_picker, path_provider]
|
||||
rules_to_load: [security-standards.mdc, platform-ios.mdc, platform-android.mdc]
|
||||
unit_test_scenarios:
|
||||
- upload returns public URL on success
|
||||
- upload failure returns typed error
|
||||
- local cache read before remote fetch (offline-first)
|
||||
- cache invalidation on TTL expiry
|
||||
- large file upload uses resumable upload
|
||||
widget_test_scenarios:
|
||||
- file picker button triggers picker
|
||||
- upload progress indicator shown
|
||||
- image preview renders after selection
|
||||
integration_test_scenarios:
|
||||
- end-to-end file upload and retrieval
|
||||
- offline mode: local cache serves data
|
||||
- background upload completes when connectivity restored
|
||||
- file size limit enforced
|
||||
external_setup:
|
||||
firebase: [Enable Firebase Storage, configure Storage security rules, set CORS policy for web if applicable]
|
||||
ios: [NSPhotoLibraryUsageDescription in Info.plist, NSCameraUsageDescription in Info.plist]
|
||||
android: [READ_EXTERNAL_STORAGE or READ_MEDIA_IMAGES permission]
|
||||
files_to_touch:
|
||||
new: [lib/features/storage/, integration_test/storage/]
|
||||
modified: [android/app/src/main/AndroidManifest.xml, ios/Runner/Info.plist, pubspec.yaml]
|
||||
|
||||
camera_media:
|
||||
keywords: [camera, photo, video, image picker, qr code, barcode scanner,
|
||||
ar, augmented reality, gallery, media, record, capture]
|
||||
pub_packages: [camera, image_picker, mobile_scanner, qr_flutter, image_cropper, video_player]
|
||||
rules_to_load: [security-standards.mdc, platform-ios.mdc, platform-android.mdc]
|
||||
unit_test_scenarios:
|
||||
- QR/barcode parsed to correct domain model
|
||||
- image compressed before upload
|
||||
- camera permission denied returns typed error
|
||||
widget_test_scenarios:
|
||||
- camera preview widget renders
|
||||
- capture button triggers photo take
|
||||
integration_test_scenarios:
|
||||
- camera opens and captures photo on real device
|
||||
- gallery picker selects image and returns
|
||||
- QR scan decodes valid code correctly
|
||||
- permission denied shows correct error UI
|
||||
external_setup:
|
||||
ios: [NSCameraUsageDescription, NSPhotoLibraryUsageDescription, NSMicrophoneUsageDescription for video]
|
||||
android: [CAMERA permission, READ_MEDIA_IMAGES for gallery access]
|
||||
files_to_touch:
|
||||
new: [lib/features/camera/, integration_test/camera/]
|
||||
modified: [android/app/src/main/AndroidManifest.xml, ios/Runner/Info.plist, pubspec.yaml]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Context Loading
|
||||
|
||||
**When the user types `/build <request>`, do the following before writing a single line of code:**
|
||||
|
||||
1. Read `project-brief.yaml` from the repo root. Extract and hold in context:
|
||||
- `stack.state_management` → **{{STATE_MGMT_RAW}}**
|
||||
- `stack.architecture` → **{{ARCH_RAW}}**
|
||||
- `stack.routing` → **{{ROUTING_RAW}}**
|
||||
- `stack.backend` → **{{BACKENDS_LIST}}**
|
||||
- `stack.platforms` → **{{PLATFORMS_LIST}}**
|
||||
- `stack.codegen` → **{{CODEGEN_LIST}}**
|
||||
- `testing.depth` → **{{TESTING_DEPTH}}**
|
||||
- `testing.e2e_tool` → **{{E2E_TOOL}}**
|
||||
- `features.modules` (existing features) → **{{FEATURES_LIST}}**
|
||||
- `features.special` → **{{SPECIAL_FEATURES}}**
|
||||
- `environments.flavors` → **{{FLAVORS_LIST}}**
|
||||
- `environments.cicd` → **{{CICD_RAW}}**
|
||||
|
||||
2. Parse the user's free-text request. Match against `FEATURE_REGISTRY` keywords using longest-match. Multiple types are allowed if the request spans features.
|
||||
|
||||
3. Load `.cursor/rules/` files listed under `rules_to_load` for the matched feature type.
|
||||
|
||||
4. If the user's message contains a URL or GitHub repo reference, fetch and index it for pattern reference.
|
||||
|
||||
5. Check `features.modules` — if the feature already exists in the list, ask: "This feature is already listed in project-brief.yaml. Building additional capability on top of it? (y/n)"
|
||||
|
||||
6. Print the context summary table before proceeding:
|
||||
|
||||
```
|
||||
| Field | Value |
|
||||
|------------------|------------------------------|
|
||||
| Feature type | [detected type] |
|
||||
| State management | {{STATE_MANAGEMENT}} |
|
||||
| Architecture | {{ARCHITECTURE}} |
|
||||
| Backend | {{BACKEND}} |
|
||||
| Platforms | {{PLATFORMS_LIST}} |
|
||||
| E2E tool | {{E2E_TOOL}} |
|
||||
| Rules loaded | [list from rules_to_load] |
|
||||
| Existing modules | {{FEATURES_LIST}} |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Deep Research
|
||||
|
||||
**Before touching any file, print the full research output.**
|
||||
|
||||
1. From the FEATURE_REGISTRY, enumerate all `pub_packages` for the detected type. For each, determine the correct version compatible with current Flutter stable (`flutter --version`). Print the full `pubspec.yaml` additions.
|
||||
|
||||
2. Build the complete file manifest, adapting to `{{ARCHITECTURE}}`:
|
||||
|
||||
**For {{ARCHITECTURE}} ({{ARCH_RAW}}):**
|
||||
- `clean`: expand each feature layer explicitly → `domain/entities/`, `domain/repositories/`, `domain/usecases/`, `data/models/`, `data/datasources/`, `data/repositories/`, `presentation/`
|
||||
- `feature_first`: `[feature]/[feature]_screen.dart`, `[feature]_provider.dart` or `[feature]_bloc.dart`, `[feature]_repository.dart`, `[feature]_model.dart`, `widgets/`
|
||||
- `mvc`: `[feature]/model/`, `[feature]/view/`, `[feature]/controller/`
|
||||
|
||||
**State management file naming ({{STATE_MANAGEMENT}} / {{STATE_MGMT_RAW}}):**
|
||||
- `bloc`: `[feature]_bloc.dart`, `[feature]_event.dart`, `[feature]_state.dart`
|
||||
- `riverpod`: `[feature]_provider.dart`, `[feature]_notifier.dart`
|
||||
- `getx`: `[feature]_controller.dart`, `[feature]_binding.dart`
|
||||
|
||||
3. List all modified files from `files_to_touch.modified` — show exactly what change goes in each file.
|
||||
|
||||
4. List all external service configuration required per platform (from `external_setup`). Only show platforms present in `{{PLATFORMS_LIST}}`.
|
||||
|
||||
5. Print research results in this format:
|
||||
|
||||
```
|
||||
## Research Results
|
||||
|
||||
### Packages to add to pubspec.yaml
|
||||
- [package_name]: ^[version]
|
||||
|
||||
### Files to create ({{ARCHITECTURE}} / {{STATE_MANAGEMENT}})
|
||||
lib/features/[name]/... (full list)
|
||||
|
||||
### Files to modify
|
||||
[file_path] — [what changes]
|
||||
|
||||
### External services requiring configuration
|
||||
- [Service] ([platform]) — [what to do]
|
||||
```
|
||||
|
||||
Do not touch any file until the user has seen this output.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: TDD Implementation
|
||||
|
||||
> **Invoke skill: `superpowers:test-driven-development`**
|
||||
|
||||
Follow Red → Green → Refactor strictly. Show actual terminal output at each step.
|
||||
|
||||
### Step 3a — Red: Write all failing tests first
|
||||
|
||||
1. Mirror the feature directory under `test/features/[feature]/`.
|
||||
2. Write unit tests for **every scenario** in `FEATURE_REGISTRY.unit_test_scenarios` for the detected type.
|
||||
3. Write widget tests for **every scenario** in `FEATURE_REGISTRY.widget_test_scenarios`.
|
||||
4. Use `mocktail` for all dependencies.
|
||||
5. Test naming convention: `'given [precondition], when [action], then [expected outcome]'`
|
||||
6. Use the state management test pattern for **{{STATE_MANAGEMENT}}**:
|
||||
```
|
||||
{{TEST_PATTERN}}
|
||||
```
|
||||
7. Run and confirm red:
|
||||
```
|
||||
flutter test test/features/[feature]/ --no-pub
|
||||
```
|
||||
**Paste the actual output here before proceeding.**
|
||||
If failures are unclear or non-deterministic after one iteration, use **`/debug`** with full pasted output before guessing fixes.
|
||||
|
||||
### Step 3b — Green: Implement
|
||||
|
||||
1. Create domain entities and repository interfaces.
|
||||
2. Create data-layer implementations wiring to `{{BACKEND}}`.
|
||||
3. Create presentation layer using **{{STATE_MANAGEMENT}}** patterns.
|
||||
4. Register in DI container — follow `{{ARCH_IMPORT_RULES}}`.
|
||||
5. Wire the route in `{{ROUTING}}` router.
|
||||
6. If `{{CODEGEN_LIST}}` is not `none`: run `dart run build_runner build --delete-conflicting-outputs` after adding models.
|
||||
7. Run tests and confirm green:
|
||||
```
|
||||
flutter test test/features/[feature]/ --no-pub
|
||||
```
|
||||
**Paste the actual output here before proceeding.**
|
||||
|
||||
### Step 3c — Refactor
|
||||
|
||||
1. Review for duplication, naming, and layer boundary violations per `.cursor/rules/flutter-core.mdc`.
|
||||
2. Run and confirm clean:
|
||||
```
|
||||
flutter analyze
|
||||
```
|
||||
**Paste the actual output here before proceeding.**
|
||||
3. Run tests once more to confirm still green.
|
||||
|
||||
If output stays red or errors are ambiguous after two focused attempts, stop and use **`/debug`** with the full failing command and log before changing more code.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Integration Test Generation
|
||||
|
||||
**Generate `integration_test/[feature_type]/` before asking the user to run anything.**
|
||||
|
||||
### Integration test file structure
|
||||
|
||||
Each test file follows this template:
|
||||
|
||||
```dart
|
||||
// integration_test/[feature]/[scenario]_test.dart
|
||||
// Generated by /build — {{PROJECT_NAME}}
|
||||
// Feature: [feature_type] | Scenario: [scenario_name]
|
||||
// Run with: flutter test integration_test/[feature]/[scenario]_test.dart -d <device_id>
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:{{PACKAGE_ID}}/main.dart' as app;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('[FeatureType] — [Scenario Name]', () {
|
||||
setUp(() async {
|
||||
// Seed state / configure mocks / reset storage
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
// Cleanup
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'given [precondition], when [action], then [expected outcome]',
|
||||
(tester) async {
|
||||
app.main();
|
||||
await tester.pumpAndSettle();
|
||||
// test body
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**For `{{E2E_TOOL}}` (patrol):** swap `flutter_test` imports for `package:patrol` and use Patrol's `$` NativeAutomator selector syntax.
|
||||
|
||||
### Files to generate
|
||||
|
||||
Create one file per logical scenario cluster from `FEATURE_REGISTRY.integration_test_scenarios`.
|
||||
Also generate `integration_test/[feature]/README.md` with the full test matrix and run commands.
|
||||
|
||||
### PAUSE GATE — user must run on device
|
||||
|
||||
After generating all files, print this table and **stop**. Wait for the user to paste device output before Phase 6.
|
||||
|
||||
```
|
||||
## ACTION REQUIRED — Run Integration Tests on Real Device
|
||||
|
||||
| File | Covers | Requires Hardware |
|
||||
|------|--------|------------------|
|
||||
| integration_test/[feature]/[scenario]_test.dart | [what it covers] | Yes/No |
|
||||
...
|
||||
|
||||
Run on iOS:
|
||||
flutter test integration_test/[feature]/ -d <your_ios_device_id>
|
||||
|
||||
Run on Android:
|
||||
flutter test integration_test/[feature]/ -d <your_android_device_id>
|
||||
|
||||
Paste the output here to continue.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: External Setup Checklist
|
||||
|
||||
**Print before asking the user to verify anything. Group by service/platform. Only show sections for platforms in `{{PLATFORMS_LIST}}`.**
|
||||
|
||||
### Format for console/service steps (numbered list):
|
||||
|
||||
```markdown
|
||||
### Firebase (backend: {{BACKEND}})
|
||||
- [ ] 1. Open Firebase Console → [exact menu path]
|
||||
- [ ] 2. [Specific action]
|
||||
- [ ] 3. Download updated config file → place at [exact path]
|
||||
```
|
||||
|
||||
### Format for native platform steps (table):
|
||||
|
||||
```markdown
|
||||
### iOS Setup
|
||||
| Step | Where | What | How to Verify |
|
||||
|------|-------|------|---------------|
|
||||
| [Step name] | [Xcode location / file path] | [Exact change] | [Verification method] |
|
||||
```
|
||||
|
||||
```markdown
|
||||
### Android Setup
|
||||
- [ ] 1. Open `android/app/src/main/AndroidManifest.xml`
|
||||
- Add: `<uses-permission android:name="[permission]"/>`
|
||||
- [ ] 2. [Next step with exact value]
|
||||
```
|
||||
|
||||
### Flavor scoping note (flavors: {{FLAVORS_LIST}})
|
||||
|
||||
Config files and API keys must be scoped per flavor. Never place production keys in `dev` flavor files. Follow the pattern established in `.cursor/rules/` for flavor-based configuration.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Verification Gate
|
||||
|
||||
> **Invoke skill: `superpowers:verification-before-completion`**
|
||||
|
||||
**Do not claim completion without pasting real output for every item below.**
|
||||
|
||||
```
|
||||
VERIFICATION REQUIRED — paste real output for each:
|
||||
|
||||
[ ] flutter test test/features/[feature]/ --no-pub
|
||||
Required: "All N tests passed."
|
||||
|
||||
[ ] flutter analyze
|
||||
Required: "No issues found!"
|
||||
|
||||
[ ] Integration test device output (from Phase 4 pause gate)
|
||||
Required: actual device test output
|
||||
|
||||
[ ] lefthook run pre-commit
|
||||
Required: all hooks passed
|
||||
```
|
||||
|
||||
If any check fails: invoke `superpowers:systematic-debugging` to diagnose before retrying.
|
||||
For noisy errors or weak reproduction, run **`/debug`** and fill the BugReport skeleton with logs before retrying.
|
||||
For small, isolated changes, **`/verify`** is enough to re-check tests and analyze without re-reading this whole skill.
|
||||
Do not proceed to Phase 7 until all four checks are green.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: PR Preparation
|
||||
|
||||
> **Invoke skill: `superpowers:finishing-a-development-branch`**
|
||||
|
||||
1. **Commit message** (conventional commits format):
|
||||
```
|
||||
feat([feature_type]): implement [feature] end-to-end
|
||||
```
|
||||
|
||||
2. **PR description template:**
|
||||
```markdown
|
||||
## What
|
||||
[One sentence: what feature was implemented]
|
||||
|
||||
## Test coverage
|
||||
- Unit/widget tests: N passing
|
||||
- Integration test matrix:
|
||||
| Scenario | iOS | Android |
|
||||
|----------|-----|---------|
|
||||
| [scenario] | ✅ | ✅ |
|
||||
|
||||
## External setup completed
|
||||
- [ ] [Service 1 setup]
|
||||
- [ ] [Service 2 setup]
|
||||
|
||||
## Notes
|
||||
[Any follow-up TODOs or known limitations]
|
||||
```
|
||||
|
||||
3. **CI/CD advice for {{CICD_TOOL}} ({{CICD_RAW}}):**
|
||||
Ensure workflow secrets are scoped per flavor ({{FLAVORS_LIST}}). Never expose production keys in dev environment secrets. Verify the CI workflow runs `flutter test` and `flutter analyze` before deploy steps.
|
||||
|
||||
---
|
||||
|
||||
## Rules applied every phase
|
||||
|
||||
Always active — read before writing any code:
|
||||
- `.cursor/rules/flutter-core.mdc`
|
||||
- `.cursor/rules/security-standards.mdc`
|
||||
- `.cursor/rules/project-context.mdc`
|
||||
- Feature-type-specific rules from `FEATURE_REGISTRY.rules_to_load`
|
||||
|
||||
Architecture import rules for **{{ARCHITECTURE}}**:
|
||||
{{ARCH_IMPORT_RULES}}
|
||||
|
||||
---
|
||||
|
||||
## Code generation notes
|
||||
|
||||
**Codegen tools configured: {{CODEGEN_LIST}}**
|
||||
|
||||
After adding any new model or injectable class, run:
|
||||
```
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
```
|
||||
|
||||
Commit generated files (`.g.dart`, `.freezed.dart`, `injection.config.dart`) — do not gitignore them.
|
||||
|
||||
**Template version:** {{TEMPLATE_VERSION}}
|
||||
@@ -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.
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
name: Debug issue
|
||||
description: Triage failing tests, analyze, CI, or runtime errors with an evidence-first BugReport. Use /debug and paste logs; invokes systematic-debugging before fixes. Stack {{STATE_MANAGEMENT}} / {{ARCHITECTURE}} / {{BACKEND}} / {{PLATFORMS_LIST}}.
|
||||
---
|
||||
|
||||
# Debug — {{PROJECT_NAME}}
|
||||
|
||||
Triage failures (tests, CI, runtime, or build) **without** jumping to fixes. Stay in hypothesis-and-evidence mode until root cause is stated.
|
||||
|
||||
**Stack context:** **{{STATE_MANAGEMENT}}** / **{{ARCHITECTURE}}** / **{{BACKEND}}** / platforms: {{PLATFORMS_LIST}}. Flavors: {{FLAVORS_LIST}}. Codegen: {{CODEGEN_LIST}}.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/debug <what failed — paste error output, command, or symptom>
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
|
||||
- `/debug flutter test fails on auth_cubit_test — paste output`
|
||||
- `/debug CI analyze step — paste log excerpt`
|
||||
- `/debug app crashes on cold start after last change`
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — Normalize the report (BugReport)
|
||||
|
||||
Emit this skeleton **before** deep analysis. If the user already pasted logs, map them into the fields instead of re-asking.
|
||||
|
||||
| Field | Content |
|
||||
|-------|---------|
|
||||
| **Summary** | One line: what broke |
|
||||
| **Expected** | What should happen |
|
||||
| **Actual** | What happened (symptom + error text) |
|
||||
| **Repro steps** | Numbered, minimal |
|
||||
| **Scope / files touched** | Paths or PR slice |
|
||||
| **Environment** | OS, Flutter/Dart version if known, device vs simulator, flavor |
|
||||
| **Evidence** | Pasted command output, stack trace, or screenshot notes |
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Evidence checklist (Flutter-aware)
|
||||
|
||||
Gather or request **concrete** evidence. Do not guess versions or config.
|
||||
|
||||
1. **`flutter doctor -v`** — paste output when environment is unknown or iOS/Android toolchain errors appear.
|
||||
2. **Failing command** — full invocation + **verbatim** tail of output (e.g. `flutter test …`, `dart test …`, `flutter analyze`).
|
||||
3. **`flutter analyze`** — if not already in the failure log, run or ask the user to run and paste.
|
||||
4. **Flavors** — this project uses: **{{FLAVORS_LIST}}**. Confirm which flavor was active if the failure is env-specific.
|
||||
5. **Platforms** — **{{PLATFORMS_LIST}}**. Narrow reproduction to the platform that failed when relevant.
|
||||
6. **Codegen** — tools: **{{CODEGEN_LIST}}**. When this is not `none`, remind to run `dart run build_runner build --delete-conflicting-outputs` after generated files changed, and to align with `.cursor/hooks/` / `lefthook run pre-commit` when hooks are present.
|
||||
7. **Testing depth** — **{{TESTING_DEPTH}}**; E2E tool: **{{E2E_TOOL}}**. Match the failure to the right layer (unit vs widget vs integration).
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Root cause (no code yet)
|
||||
|
||||
> **Invoke skill: `superpowers:systematic-debugging`**
|
||||
|
||||
Produce **one paragraph**: hypothesis tied to **specific lines** in the pasted evidence. Mark confidence (high / medium / low). **No code changes** in this phase.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Fix (only after Phase 2)
|
||||
|
||||
> **Invoke skill: `superpowers:systematic-debugging`** again while iterating fixes.
|
||||
|
||||
When proposing changes:
|
||||
|
||||
- Respect architecture boundaries for **{{ARCHITECTURE}}**:
|
||||
{{ARCH_IMPORT_RULES}}
|
||||
- Always consider: `.cursor/rules/flutter-core.mdc`, `.cursor/rules/security-standards.mdc`, `.cursor/rules/project-context.mdc`, and state-management rules for **{{STATE_MANAGEMENT}}**.
|
||||
|
||||
After each fix attempt, re-run the **same** failing command and paste new output.
|
||||
|
||||
---
|
||||
|
||||
## ACTION REQUIRED
|
||||
|
||||
If evidence is missing, **stop** and print:
|
||||
|
||||
1. Exact commands to run (copy-paste ready).
|
||||
2. What to paste back (full error blocks, not summaries).
|
||||
3. If the user cannot run commands: state assumptions explicitly and set confidence to **low**.
|
||||
|
||||
**Template version:** {{TEMPLATE_VERSION}}
|
||||
@@ -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
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
name: Explain code
|
||||
description: Explain what code does without editing it. Use /explain with a path or symbol; covers {{STATE_MANAGEMENT}} state, {{BACKEND}} I/O, routing {{ROUTING}}, and failure modes. Ask when facts are not in the repo.
|
||||
---
|
||||
|
||||
# Explain — {{PROJECT_NAME}}
|
||||
|
||||
Explain **what the code is doing** — **do not** change production code unless the user explicitly asks for a fix.
|
||||
|
||||
**Stack:** **{{STATE_MANAGEMENT}}** / **{{ARCHITECTURE}}** / **{{ROUTING}}** / **{{BACKEND}}** / platforms: {{PLATFORMS_LIST}}. Package: `{{PACKAGE_ID}}`.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/explain <file path, widget name, class, or symbol>
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
|
||||
- `/explain lib/features/cart/cart_cubit.dart`
|
||||
- `/explain how checkout routes after payment`
|
||||
- `/explain CartPage build method`
|
||||
|
||||
---
|
||||
|
||||
## Output format (use these headings in order)
|
||||
|
||||
### Purpose
|
||||
|
||||
One short paragraph: why this code exists in the product context (**{{PROJECT_NAME}}** — {{DESCRIPTION}}).
|
||||
|
||||
### Public API
|
||||
|
||||
Surface area: public classes/methods, constructors, and what callers are expected to pass. Note codegen involvement when **{{CODEGEN_LIST}}** is not `none`.
|
||||
|
||||
### Call flow
|
||||
|
||||
Ordered steps from entry point (e.g. widget `build`, route handler, Bloc `on<Event>`, Riverpod `build`, GetX controller lifecycle) through collaborators. Tie navigation to **{{ROUTING}}** where relevant.
|
||||
|
||||
### State and side effects
|
||||
|
||||
How state is held and updated for **{{STATE_MANAGEMENT}}** ({{STATE_MGMT_RAW}}). Mention async work, listeners, and disposal. Reference **{{ARCH_IMPORT_RULES}}** if layering is unclear.
|
||||
|
||||
### I/O and backends
|
||||
|
||||
Network, local storage, or platform channels touching **{{BACKEND}}** (and **{{AUTH}}** where auth applies: {{AUTH_RAW}}). Do **not** invent API shapes not visible in the repo.
|
||||
|
||||
### Failure modes
|
||||
|
||||
What can go wrong: null paths, error states, race conditions, missing permissions on {{PLATFORMS_LIST}}, auth edge cases.
|
||||
|
||||
### Suggested tests
|
||||
|
||||
Ideas aligned with **{{TESTING_DEPTH}}** and **{{E2E_TOOL}}**; for **{{STATE_MANAGEMENT}}**, prefer patterns like:
|
||||
|
||||
```
|
||||
{{TEST_PATTERN}}
|
||||
```
|
||||
|
||||
### Unknowns — questions for you
|
||||
|
||||
If behavior depends on runtime config, native projects, remote API contracts, or secrets not in tree: **stop** and list **specific** questions. Do not fabricate facts.
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- **Project brief:** `project-brief.yaml` — feature modules: {{FEATURES_LIST}}; special features: {{SPECIAL_FEATURES}}.
|
||||
- **Scale:** {{SCALE}}; i18n locales: {{LOCALES_LIST}} (when explaining localization).
|
||||
- **Design:** {{DESIGN_SOURCE}}; Figma URL: {{FIGMA_URL}}.
|
||||
- **API docs format:** {{API_DOCS_FORMAT}} (path: {{API_DOCS_PATH}}).
|
||||
- **Related repos (if any):**
|
||||
|
||||
{{GIT_REFS_BLOCK}}
|
||||
|
||||
- **Local paths (if any):**
|
||||
|
||||
{{LOCAL_PATHS_BLOCK}}
|
||||
|
||||
**Template version:** {{TEMPLATE_VERSION}}
|
||||
@@ -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`
|
||||
@@ -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]'`
|
||||
@@ -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}}
|
||||
@@ -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}}
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
name: Verify change
|
||||
description: Post-change or pre-PR verification without the full /build lifecycle. Use /verify with optional scope; respects testing depth {{TESTING_DEPTH}} and E2E {{E2E_TOOL}}; no success claims without pasted outputs.
|
||||
---
|
||||
|
||||
# Verify — {{PROJECT_NAME}}
|
||||
|
||||
Run a **focused** verification gate after a change or before a small PR — without replaying the entire `/build` document.
|
||||
|
||||
**Stack:** **{{STATE_MANAGEMENT}}** / **{{ARCHITECTURE}}** / **{{BACKEND}}** / {{PLATFORMS_LIST}}. **Testing depth:** {{TESTING_DEPTH}}. **E2E tool:** {{E2E_TOOL}}. **Codegen:** {{CODEGEN_LIST}}.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/verify [optional scope: paths, “small PR”, or feature name]
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
|
||||
- `/verify lib/features/auth/`
|
||||
- `/verify small PR before push`
|
||||
- `/verify after dependency bump`
|
||||
|
||||
---
|
||||
|
||||
## Checklist (adapt to testing depth)
|
||||
|
||||
> **Invoke skill: `superpowers:verification-before-completion`**
|
||||
|
||||
**Do not claim success** until the user (or you, in a trusted environment) has pasted **real** output for each applicable item below.
|
||||
|
||||
### Always (typical Flutter repo)
|
||||
|
||||
- [ ] **Unit / widget tests** for touched code — e.g. `flutter test <path> --no-pub` or full suite as appropriate.
|
||||
**Required in paste:** a line showing tests passed (or the exact failure to fix).
|
||||
- [ ] **`flutter analyze`**
|
||||
**Required in paste:** `No issues found!` or the analyzer errors to address.
|
||||
|
||||
### When `testing.depth` is `full` or `e2e` (this brief: **{{TESTING_DEPTH}}**)
|
||||
|
||||
- [ ] **Integration / E2E** — use **{{E2E_TOOL}}** patterns from `.cursor/rules/` (e.g. Patrol). Run on a device or emulator when scenarios require it; paste run output.
|
||||
|
||||
### When `testing.depth` is `unit_widget` only
|
||||
|
||||
- [ ] **Integration / device E2E** — *not* mandatory unless the change touches integration-only surfaces; if skipped, say so explicitly in the paste.
|
||||
|
||||
### Hooks and codegen
|
||||
|
||||
- [ ] **`lefthook run pre-commit`** — run when the repo has Lefthook configured (especially when **{{CODEGEN_LIST}}** is not `none`). Paste hook summary.
|
||||
If hooks are not set up for this workspace, write *“skipped — hooks not configured”* instead of failing silently.
|
||||
|
||||
### CI alignment
|
||||
|
||||
- [ ] For **{{CICD_TOOL}}** ({{CICD_RAW}}): confirm the same commands the pipeline runs (analyze + tests) are green locally. Note flavor-scoped secrets for **{{FLAVORS_LIST}}**.
|
||||
|
||||
---
|
||||
|
||||
## ACTION REQUIRED — paste block
|
||||
|
||||
Print this table and wait for pasted output before declaring done:
|
||||
|
||||
```
|
||||
VERIFICATION — paste real output for each line you executed:
|
||||
|
||||
[ ] flutter test … → (paste: pass count or failures)
|
||||
[ ] flutter analyze → (paste: no issues or errors)
|
||||
[ ] integration / e2e (if depth is full/e2e or change requires it) →
|
||||
[ ] lefthook run pre-commit → (paste or "skipped — not configured")
|
||||
```
|
||||
|
||||
If anything fails: switch to **`/debug`** with the failing log before guessing.
|
||||
|
||||
**Template version:** {{TEMPLATE_VERSION}}
|
||||
@@ -0,0 +1,2 @@
|
||||
*.jsonl
|
||||
*.log
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Append a JSONL usage line (local only). Requires jq when passing structured payload.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
LOG="${ROOT}/telemetry/usage.jsonl"
|
||||
mkdir -p "$(dirname "$LOG")"
|
||||
echo "{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"event\":\"${1:-note}\",\"detail\":\"${2:-}\"}" >>"$LOG"
|
||||
@@ -3,349 +3,627 @@
|
||||
|
||||
import 'dart:io';
|
||||
import 'package:test/test.dart';
|
||||
import '../src/brief_loader.dart';
|
||||
import 'golden_briefs.dart';
|
||||
import '../src/resolver.dart';
|
||||
import '../src/renderer.dart';
|
||||
import '../src/validator.dart';
|
||||
import '../src/models.dart';
|
||||
import '../src/mcp_json.dart';
|
||||
|
||||
// ─── Test fixtures ─────────────────────────────────────────────────────────
|
||||
|
||||
final _blocCleanBrief = ProjectBrief(
|
||||
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'],
|
||||
specialFeatures: [],
|
||||
i18nEnabled: false,
|
||||
locales: ['en'],
|
||||
);
|
||||
|
||||
final _riverpodFFBrief = ProjectBrief(
|
||||
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'],
|
||||
specialFeatures: [],
|
||||
i18nEnabled: true,
|
||||
locales: ['en', 'fr'],
|
||||
);
|
||||
|
||||
final _getxMvcBrief = ProjectBrief(
|
||||
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'],
|
||||
specialFeatures: [],
|
||||
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(kBlocCleanFirebaseBrief);
|
||||
|
||||
// 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.first, equals('rules/universal/rule-authoring'));
|
||||
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'));
|
||||
// CI/CD rule when cicd set or multiple flavors
|
||||
expect(files, contains('rules/cicd/cicd'));
|
||||
|
||||
// Error handling — always present
|
||||
expect(files, contains('rules/error-handling/error-handling'));
|
||||
// Feature stubs from brief
|
||||
expect(files, contains('rules/features/auth'));
|
||||
expect(files, contains('rules/features/home'));
|
||||
expect(files, contains('rules/features/products'));
|
||||
|
||||
// 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'));
|
||||
// Cursor workspace extras
|
||||
expect(files, contains('root/.cursorignore'));
|
||||
expect(files, contains('root/tool/cursor_audit.sh'));
|
||||
expect(files, contains('onboarding/ONBOARDING'));
|
||||
expect(files, contains('commands/build'));
|
||||
expect(files, contains('commands/debug-issue'));
|
||||
|
||||
// Testing — SM-matched
|
||||
expect(files, contains('rules/testing/testing-bloc'));
|
||||
// Security — always present (Pillar 5)
|
||||
expect(files, contains('rules/security/security-standards'));
|
||||
|
||||
// Platforms (Pillar 4)
|
||||
expect(files, contains('rules/platform/platform-ios'));
|
||||
expect(files, contains('rules/platform/platform-android'));
|
||||
expect(files, containsNot('rules/platform/platform-web'));
|
||||
// Error handling — always present
|
||||
expect(files, contains('rules/error-handling/error-handling'));
|
||||
|
||||
// Codegen (Pillar 4)
|
||||
expect(files, contains('rules/codegen/codegen-freezed'));
|
||||
expect(files, containsNot('rules/codegen/codegen-injectable'));
|
||||
// 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'));
|
||||
|
||||
// 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
|
||||
// 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'));
|
||||
|
||||
// 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
|
||||
|
||||
// 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'));
|
||||
|
||||
expect(files, contains('commands/verify-change'));
|
||||
expect(files, contains('commands/explain-code'));
|
||||
expect(files, containsNot('config/mcp-json'));
|
||||
expect(files, contains('root/AGENTS.md'));
|
||||
expect(files, contains('root/lefthook.yaml'));
|
||||
});
|
||||
|
||||
test('Riverpod + Feature-First + Web resolves web platform template', () {
|
||||
final files = Resolver.resolve(kRiverpodFfSupabaseBrief);
|
||||
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('featureRuleKey sanitizes module names', () {
|
||||
expect(Resolver.featureRuleKey('My Cart'), 'rules/features/my_cart');
|
||||
expect(Resolver.featureRuleKey('auth'), 'rules/features/auth');
|
||||
expect(Resolver.featureRuleKey(' '), isNull);
|
||||
});
|
||||
|
||||
test('MCP template key when integrations.mcp.enabled', () {
|
||||
final brief = ProjectBrief(
|
||||
projectName: 'McpApp',
|
||||
packageId: 'com.test.mcp',
|
||||
description: '',
|
||||
scale: 'small',
|
||||
stateManagement: 'bloc',
|
||||
routing: 'gorouter',
|
||||
architecture: 'clean',
|
||||
backends: ['firebase'],
|
||||
auth: 'none',
|
||||
platforms: ['ios'],
|
||||
codegenTools: [],
|
||||
flavors: ['dev'],
|
||||
cicd: 'none',
|
||||
testingDepth: 'unit_widget',
|
||||
e2eTool: 'patrol',
|
||||
designSource: 'none',
|
||||
figmaUrl: '',
|
||||
apiDocsFormat: 'none',
|
||||
apiDocsPath: '',
|
||||
referenceRepos: [],
|
||||
localPaths: [],
|
||||
featureModules: [],
|
||||
specialFeatures: [],
|
||||
i18nEnabled: false,
|
||||
locales: ['en'],
|
||||
mcpConfigEnabled: true,
|
||||
);
|
||||
expect(Resolver.resolve(brief), contains('config/mcp-json'));
|
||||
});
|
||||
|
||||
test('Push/deeplink rule when special features request it', () {
|
||||
final brief = ProjectBrief(
|
||||
projectName: 'PushApp',
|
||||
packageId: 'com.test.push',
|
||||
description: '',
|
||||
scale: 'small',
|
||||
stateManagement: 'bloc',
|
||||
routing: 'gorouter',
|
||||
architecture: 'clean',
|
||||
backends: ['rest'],
|
||||
auth: 'none',
|
||||
platforms: ['ios'],
|
||||
codegenTools: [],
|
||||
flavors: ['dev'],
|
||||
cicd: 'none',
|
||||
testingDepth: 'unit_widget',
|
||||
e2eTool: 'patrol',
|
||||
designSource: 'none',
|
||||
figmaUrl: '',
|
||||
apiDocsFormat: 'none',
|
||||
apiDocsPath: '',
|
||||
referenceRepos: [],
|
||||
localPaths: [],
|
||||
featureModules: [],
|
||||
specialFeatures: ['push_notifications'],
|
||||
i18nEnabled: false,
|
||||
locales: ['en'],
|
||||
);
|
||||
expect(Resolver.resolve(brief), contains('rules/integrations/push-deeplink'));
|
||||
});
|
||||
|
||||
test('Theming rule when high contrast requested', () {
|
||||
final brief = ProjectBrief(
|
||||
projectName: 'A11yApp',
|
||||
packageId: 'com.test.a11y',
|
||||
description: '',
|
||||
scale: 'small',
|
||||
stateManagement: 'bloc',
|
||||
routing: 'gorouter',
|
||||
architecture: 'clean',
|
||||
backends: ['rest'],
|
||||
auth: 'none',
|
||||
platforms: ['ios'],
|
||||
codegenTools: [],
|
||||
flavors: ['dev'],
|
||||
cicd: 'none',
|
||||
testingDepth: 'unit_widget',
|
||||
e2eTool: 'patrol',
|
||||
designSource: 'none',
|
||||
figmaUrl: '',
|
||||
apiDocsFormat: 'none',
|
||||
apiDocsPath: '',
|
||||
referenceRepos: [],
|
||||
localPaths: [],
|
||||
featureModules: [],
|
||||
specialFeatures: [],
|
||||
i18nEnabled: false,
|
||||
locales: ['en'],
|
||||
themeVariants: const ['light', 'dark', 'high_contrast'],
|
||||
);
|
||||
expect(Resolver.resolve(brief), contains('rules/theming/theming'));
|
||||
});
|
||||
|
||||
test('Telemetry helpers when telemetry_opt_in', () {
|
||||
final brief = ProjectBrief(
|
||||
projectName: 'TelApp',
|
||||
packageId: 'com.test.tel',
|
||||
description: '',
|
||||
scale: 'small',
|
||||
stateManagement: 'bloc',
|
||||
routing: 'gorouter',
|
||||
architecture: 'clean',
|
||||
backends: ['rest'],
|
||||
auth: 'none',
|
||||
platforms: ['ios'],
|
||||
codegenTools: [],
|
||||
flavors: ['dev'],
|
||||
cicd: 'none',
|
||||
testingDepth: 'unit_widget',
|
||||
e2eTool: 'patrol',
|
||||
designSource: 'none',
|
||||
figmaUrl: '',
|
||||
apiDocsFormat: 'none',
|
||||
apiDocsPath: '',
|
||||
referenceRepos: [],
|
||||
localPaths: [],
|
||||
featureModules: [],
|
||||
specialFeatures: [],
|
||||
i18nEnabled: false,
|
||||
locales: ['en'],
|
||||
telemetryOptIn: true,
|
||||
);
|
||||
final files = Resolver.resolve(brief);
|
||||
expect(files, contains('telemetry/gitignore'));
|
||||
expect(files, contains('telemetry/log-sh'));
|
||||
expect(files, contains('rules/telemetry/usage-logging'));
|
||||
});
|
||||
|
||||
test('McpJsonBuilder minimal preset emits empty mcpServers', () {
|
||||
const brief = ProjectBrief(
|
||||
projectName: 'M',
|
||||
packageId: 'com.m.m',
|
||||
description: '',
|
||||
scale: 'small',
|
||||
stateManagement: 'bloc',
|
||||
routing: 'gorouter',
|
||||
architecture: 'clean',
|
||||
backends: ['rest'],
|
||||
auth: 'none',
|
||||
platforms: ['ios'],
|
||||
codegenTools: [],
|
||||
flavors: ['dev'],
|
||||
cicd: 'none',
|
||||
testingDepth: 'unit_widget',
|
||||
e2eTool: 'patrol',
|
||||
designSource: 'none',
|
||||
figmaUrl: '',
|
||||
apiDocsFormat: 'none',
|
||||
apiDocsPath: '',
|
||||
referenceRepos: [],
|
||||
localPaths: [],
|
||||
featureModules: [],
|
||||
specialFeatures: [],
|
||||
i18nEnabled: false,
|
||||
locales: ['en'],
|
||||
mcpConfigEnabled: true,
|
||||
mcpPreset: 'minimal',
|
||||
);
|
||||
expect(McpJsonBuilder.build(brief), contains('"mcpServers": {}'));
|
||||
});
|
||||
|
||||
test('GetX + MVC includes migration-agent', () {
|
||||
final files = Resolver.resolve(kGetxMvcRestBrief);
|
||||
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(kBlocCleanFirebaseBrief);
|
||||
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(kBlocCleanFirebaseBrief);
|
||||
final unique = files.toSet();
|
||||
expect(files.length, equals(unique.length),
|
||||
reason: 'Duplicate template files detected');
|
||||
});
|
||||
|
||||
test('Universal slash skills always in resolved list', () {
|
||||
const universalSkills = [
|
||||
'skills/build',
|
||||
'skills/debug-issue',
|
||||
'skills/verify-change',
|
||||
'skills/explain-code',
|
||||
];
|
||||
for (final skill in universalSkills) {
|
||||
expect(Resolver.resolve(kBlocCleanFirebaseBrief), contains(skill));
|
||||
expect(Resolver.resolve(kRiverpodFfSupabaseBrief), contains(skill));
|
||||
expect(Resolver.resolve(kGetxMvcRestBrief), contains(skill));
|
||||
}
|
||||
});
|
||||
|
||||
// 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
|
||||
});
|
||||
|
||||
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 = <String>[];
|
||||
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: kBlocCleanFirebaseBrief,
|
||||
templateFiles: Resolver.resolve(kBlocCleanFirebaseBrief),
|
||||
templateSrc: templateDir,
|
||||
);
|
||||
|
||||
final unresolved = <String>[];
|
||||
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: kBlocCleanFirebaseBrief,
|
||||
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'));
|
||||
});
|
||||
|
||||
test('mcp.json renders from config/mcp-json key', () async {
|
||||
final templateDir = _templateDir();
|
||||
if (!Directory(templateDir).existsSync()) {
|
||||
markTestSkipped('Template directory not found');
|
||||
return;
|
||||
}
|
||||
final brief = ProjectBrief(
|
||||
projectName: 'McpApp',
|
||||
packageId: 'com.test.mcp',
|
||||
description: '',
|
||||
scale: 'small',
|
||||
stateManagement: 'bloc',
|
||||
routing: 'gorouter',
|
||||
architecture: 'clean',
|
||||
backends: const ['firebase'],
|
||||
auth: 'none',
|
||||
platforms: const ['ios'],
|
||||
codegenTools: const [],
|
||||
flavors: const ['dev'],
|
||||
cicd: 'none',
|
||||
testingDepth: 'unit_widget',
|
||||
e2eTool: 'patrol',
|
||||
designSource: 'none',
|
||||
figmaUrl: '',
|
||||
apiDocsFormat: 'none',
|
||||
apiDocsPath: '',
|
||||
referenceRepos: const [],
|
||||
localPaths: const [],
|
||||
featureModules: const [],
|
||||
specialFeatures: const [],
|
||||
i18nEnabled: false,
|
||||
locales: const ['en'],
|
||||
mcpConfigEnabled: true,
|
||||
);
|
||||
final rendered = await Renderer.render(
|
||||
brief: brief,
|
||||
templateFiles: const ['config/mcp-json'],
|
||||
templateSrc: templateDir,
|
||||
);
|
||||
final json = rendered['mcp.json']!;
|
||||
expect(json, contains('mcpServers'));
|
||||
expect(json, contains('filesystem'));
|
||||
expect(json, contains('firebase'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 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(kBlocCleanFirebaseBrief);
|
||||
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: kBlocCleanFirebaseBrief,
|
||||
templateFiles: Resolver.resolve(kBlocCleanFirebaseBrief),
|
||||
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: kRiverpodFfSupabaseBrief,
|
||||
templateFiles: Resolver.resolve(kRiverpodFfSupabaseBrief),
|
||||
templateSrc: templateDir,
|
||||
);
|
||||
await _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: kGetxMvcRestBrief,
|
||||
templateFiles: Resolver.resolve(kGetxMvcRestBrief),
|
||||
templateSrc: templateDir,
|
||||
);
|
||||
await _compareGoldens('test/golden/getx-mvc-rest', rendered);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> _compareGoldens(String goldenDir, Map<String, String> rendered) async {
|
||||
Future<void> _compareGoldens(
|
||||
String goldenDir, Map<String, String> rendered) async {
|
||||
for (final entry in rendered.entries) {
|
||||
final goldenFile = File('$goldenDir/${entry.key}');
|
||||
if (!goldenFile.existsSync()) {
|
||||
@@ -359,14 +637,18 @@ Future<void> _compareGoldens(String goldenDir, Map<String, String> 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';
|
||||
}
|
||||
|
||||
// Custom matcher
|
||||
Matcher get containsNot => isNot(contains);
|
||||
|
||||
extension on Matcher {
|
||||
Matcher call(dynamic value) => isNot(contains(value));
|
||||
}
|
||||
Matcher containsNot(dynamic expected) => isNot(contains(expected));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user