flutter-setup 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,904 @@
1
+ """Project bootstrapping for Flutter setup."""
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ import yaml
7
+ from rich.console import Console
8
+
9
+ from .cicd_generator import CicdGenerator
10
+ from .config import Config
11
+
12
+ console = Console()
13
+
14
+
15
+ class ProjectBootstrap:
16
+ """Bootstraps development environment for Flutter projects."""
17
+
18
+ def __init__(self, config: Config):
19
+ """Initialize ProjectBootstrap."""
20
+ self.config = config
21
+ self.home = Path.home()
22
+ self.flutter_root = config.flutter_location
23
+
24
+ def bootstrap_project(self) -> None:
25
+ """Bootstrap the development environment."""
26
+ if self.config.dry_run:
27
+ console.print(
28
+ "[yellow]DRY RUN: Would bootstrap development environment[/yellow]"
29
+ )
30
+ return
31
+
32
+ console.print(" 🔧 Bootstrapping development & testing helpers...")
33
+
34
+ # Create VS Code/Cursor configuration
35
+ self._create_vscode_config()
36
+
37
+ # Create Makefile
38
+ self._create_makefile()
39
+
40
+ # Patch Android NDK version in build.gradle.kts
41
+ if "android" in self.config.platforms:
42
+ self._patch_android_ndk_version()
43
+
44
+ # Create test structure
45
+ self._create_test_structure()
46
+
47
+ # Create analysis options
48
+ self._create_analysis_options()
49
+
50
+ # Create optional architecture and persistence scaffolds
51
+ self._create_architecture_scaffold()
52
+
53
+ # Create CI/CD workflows and configuration
54
+ self._create_cicd()
55
+
56
+ # Add dependencies
57
+ self._add_dependencies()
58
+
59
+ # Create environment support
60
+ self._create_environment_support()
61
+
62
+ # Pin Flutter SDK version in pubspec.yaml
63
+ ver = self.config.flutter_version or self._detect_flutter_version()
64
+ if ver:
65
+ self._pin_flutter_sdk_version(ver)
66
+
67
+ # Create README
68
+ self._create_readme()
69
+
70
+ # Format code
71
+ self._format_code()
72
+
73
+ def _create_vscode_config(self) -> None:
74
+ """Create VS Code/Cursor configuration files."""
75
+ vscode_dir = self.config.project_path / ".vscode"
76
+ vscode_dir.mkdir(exist_ok=True)
77
+
78
+ # Settings
79
+ settings = {
80
+ "dart.flutterHotReloadOnSave": "all",
81
+ "dart.lineLength": 100,
82
+ "editor.formatOnSave": True,
83
+ "editor.defaultFormatter": "Dart-Code.dart-code",
84
+ "files.exclude": {"**/.dart_tool": True, "**/build": True},
85
+ }
86
+
87
+ import json
88
+
89
+ with open(vscode_dir / "settings.json", "w") as f:
90
+ json.dump(settings, f, indent=2)
91
+
92
+ # Launch configuration
93
+ launch_config = {
94
+ "version": "0.2.0",
95
+ "configurations": [
96
+ {"name": "Flutter Debug", "request": "launch", "type": "dart"}
97
+ ],
98
+ }
99
+
100
+ with open(vscode_dir / "launch.json", "w") as f:
101
+ json.dump(launch_config, f, indent=2)
102
+
103
+ console.print(" ✅ VS Code/Cursor configuration created")
104
+
105
+ def _detect_flutter_version(self) -> str | None:
106
+ """Return the version string of the Flutter SDK at config.flutter_location."""
107
+ import re
108
+
109
+ flutter_bin = self.config.flutter_location / "bin" / "flutter"
110
+ try:
111
+ result = subprocess.run(
112
+ [str(flutter_bin), "--version"],
113
+ capture_output=True,
114
+ text=True,
115
+ check=False,
116
+ )
117
+ output = result.stdout or result.stderr or ""
118
+ match = re.search(r"Flutter\s+([\d.]+)", output)
119
+ if match:
120
+ return match.group(1)
121
+ except Exception:
122
+ pass
123
+ return None
124
+
125
+ def _create_makefile(self) -> None:
126
+ """Create Makefile with common commands."""
127
+ # Always lock the project to the Flutter version present at creation time.
128
+ # --flutter-version provides an explicit pin; otherwise detect from the SDK.
129
+ ver = self.config.flutter_version or self._detect_flutter_version()
130
+ flutter_home = str(self.config.flutter_location)
131
+
132
+ if ver:
133
+ version_header = (
134
+ f"FLUTTER_HOME := {flutter_home}\n"
135
+ f'FLUTTER := "$(FLUTTER_HOME)/bin/flutter"\n'
136
+ f"FLUTTER_REQUIRED_VERSION := {ver}\n\n"
137
+ )
138
+ version_dep = " check-flutter-version"
139
+ # Use flutter pub get to enforce the pubspec.yaml >=constraint natively
140
+ version_target = """
141
+ check-flutter-version:
142
+ \t@echo "Checking Flutter SDK satisfies >=$(FLUTTER_REQUIRED_VERSION) (pubspec.yaml)..."
143
+ \t@$(FLUTTER) pub get
144
+
145
+ .PHONY: check-flutter-version
146
+ """
147
+ else:
148
+ version_header = ""
149
+ version_dep = ""
150
+ version_target = ""
151
+
152
+ flutter_cmd = "$(FLUTTER)" if ver else "flutter"
153
+
154
+ generate_target = ""
155
+ if self.config.database == "sqlite":
156
+ generate_target = f"""
157
+ generate:{version_dep}
158
+ \tdart run build_runner build --delete-conflicting-outputs
159
+ """
160
+
161
+ web_target = ""
162
+ if "web" in self.config.platforms:
163
+ web_target = f"run-chrome:{version_dep}\n\t{flutter_cmd} run -d chrome\n\n"
164
+
165
+ android_sdk_header = ""
166
+ android_sdk_dep = ""
167
+ android_sdk_target = ""
168
+ if "android" in self.config.platforms:
169
+ android_sdk_header = (
170
+ "ANDROID_SDK_ROOT ?= $(or $(ANDROID_HOME),/opt/android-sdk)\n"
171
+ "SDKMANAGER := $(ANDROID_SDK_ROOT)/cmdline-tools/latest/bin/sdkmanager\n"
172
+ "REQUIRED_NDK := 27.0.12077973\n\n"
173
+ )
174
+ android_sdk_dep = " check-android-sdk"
175
+ android_sdk_target = """
176
+ check-android-sdk:
177
+ \t@if [ ! -f "$(ANDROID_SDK_ROOT)/ndk/$(REQUIRED_NDK)/source.properties" ]; then \\
178
+ \t\techo "NDK $(REQUIRED_NDK) missing or incomplete, installing..."; \\
179
+ \t\t$(SDKMANAGER) "ndk;$(REQUIRED_NDK)"; \\
180
+ \telse \\
181
+ \t\techo "NDK $(REQUIRED_NDK) ok"; \\
182
+ \tfi
183
+
184
+ .PHONY: check-android-sdk
185
+ """
186
+
187
+ makefile_content = f"""{version_header}{android_sdk_header}{web_target}run-ios:{version_dep}
188
+ \t{flutter_cmd} run -d ios
189
+
190
+ run-android:{version_dep}{android_sdk_dep}
191
+ \t{flutter_cmd} run -d android
192
+
193
+ analyze:{version_dep}
194
+ \t{flutter_cmd} analyze
195
+
196
+ test:{version_dep}
197
+ \t{flutter_cmd} test
198
+
199
+ integration:{version_dep}
200
+ \t{flutter_cmd} test integration_test
201
+
202
+ upgrade:{version_dep}
203
+ \t{flutter_cmd} pub upgrade
204
+
205
+ upgrade-check:{version_dep}
206
+ \t{flutter_cmd} pub get
207
+ {generate_target}{android_sdk_target}{version_target}"""
208
+
209
+ with open(self.config.project_path / "Makefile", "w") as f:
210
+ f.write(makefile_content)
211
+
212
+ console.print(" ✅ Makefile created")
213
+
214
+ def _patch_android_ndk_version(self) -> None:
215
+ """Pin the Android NDK version in build.gradle.kts.
216
+
217
+ flutter create sets ndkVersion = flutter.ndkVersion (currently 26.x), but
218
+ path_provider_android and sqlite3_flutter_libs require NDK 27.0.12077973.
219
+ NDK versions are backward-compatible, so pinning to the highest required
220
+ version fixes the mismatch warning and avoids a failed build.
221
+ """
222
+ gradle_path = self.config.project_path / "android" / "app" / "build.gradle.kts"
223
+ if not gradle_path.exists():
224
+ return
225
+
226
+ content = gradle_path.read_text()
227
+ patched = content.replace(
228
+ "ndkVersion = flutter.ndkVersion",
229
+ 'ndkVersion = "27.0.12077973"',
230
+ )
231
+ if patched != content:
232
+ gradle_path.write_text(patched)
233
+ console.print(" ✅ Android NDK version pinned in build.gradle.kts")
234
+
235
+ def _create_test_structure(self) -> None:
236
+ """Create test directory structure."""
237
+ test_dir = self.config.project_path / "test"
238
+ test_dir.mkdir(exist_ok=True)
239
+
240
+ # Remove the stale counter test that `flutter create` generates; it
241
+ # references `MyApp` which doesn't exist in the scaffolded project.
242
+ default_test = test_dir / "widget_test.dart"
243
+ if default_test.exists():
244
+ default_test.unlink()
245
+
246
+ # Unit test directory
247
+ unit_dir = test_dir / "unit"
248
+ unit_dir.mkdir(exist_ok=True)
249
+
250
+ # Widget test directory
251
+ widget_dir = test_dir / "widget"
252
+ widget_dir.mkdir(exist_ok=True)
253
+
254
+ # Integration test directory
255
+ integration_dir = self.config.project_path / "integration_test"
256
+ integration_dir.mkdir(exist_ok=True)
257
+
258
+ # Create sample tests
259
+ self._create_sample_tests()
260
+
261
+ console.print(" ✅ Test structure created")
262
+
263
+ def _create_sample_tests(self) -> None:
264
+ """Create sample test files."""
265
+ # Unit test
266
+ unit_test = """import 'package:flutter_test/flutter_test.dart';
267
+
268
+ void main() {
269
+ test('sanity check', () {
270
+ expect(1 + 1, equals(2));
271
+ });
272
+ }
273
+ """
274
+
275
+ with open(
276
+ self.config.project_path / "test" / "unit" / "sanity_test.dart", "w"
277
+ ) as f:
278
+ f.write(unit_test)
279
+
280
+ app_import = f"package:{self.config.package_name}/main.dart"
281
+ app_widget = "MyApp"
282
+ riverpod_import = ""
283
+ pump_widget = f"const {app_widget}()"
284
+ if self.config.architecture == "clean":
285
+ app_import = f"package:{self.config.package_name}/src/app/app.dart"
286
+ app_widget = "App"
287
+ riverpod_import = (
288
+ "import 'package:flutter_riverpod/flutter_riverpod.dart';\n"
289
+ )
290
+ pump_widget = f"const ProviderScope(child: {app_widget}())"
291
+
292
+ # Widget test
293
+ widget_test = f"""import 'package:flutter_test/flutter_test.dart';
294
+ {riverpod_import}import '{app_import}';
295
+
296
+ void main() {{
297
+ testWidgets('App loads without errors', (tester) async {{
298
+ await tester.pumpWidget({pump_widget});
299
+ expect(find.byType({app_widget}), findsOneWidget);
300
+ }});
301
+ }}
302
+ """
303
+
304
+ with open(
305
+ self.config.project_path / "test" / "widget" / "app_widget_test.dart", "w"
306
+ ) as f:
307
+ f.write(widget_test)
308
+
309
+ # Integration test
310
+ integration_test = f"""import 'package:integration_test/integration_test.dart';
311
+ import 'package:flutter_test/flutter_test.dart';
312
+ {riverpod_import}import '{app_import}';
313
+
314
+ void main() {{
315
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
316
+
317
+ testWidgets('home page renders', (tester) async {{
318
+ await tester.pumpWidget({pump_widget});
319
+ expect(find.byType({app_widget}), findsOneWidget);
320
+ }});
321
+ }}
322
+ """
323
+
324
+ with open(
325
+ self.config.project_path / "integration_test" / "app_test.dart", "w"
326
+ ) as f:
327
+ f.write(integration_test)
328
+
329
+ def _create_analysis_options(self) -> None:
330
+ """Create analysis options file."""
331
+ analysis_content = """include: package:flutter_lints/flutter.yaml
332
+
333
+ linter:
334
+ rules:
335
+ avoid_print: false
336
+ prefer_const_constructors: true
337
+ """
338
+
339
+ with open(self.config.project_path / "analysis_options.yaml", "w") as f:
340
+ f.write(analysis_content)
341
+
342
+ console.print(" ✅ Analysis options created")
343
+
344
+ def _create_architecture_scaffold(self) -> None:
345
+ """Create optional reusable application architecture scaffolds."""
346
+ if self.config.architecture == "clean":
347
+ self._create_clean_architecture_scaffold()
348
+
349
+ if self.config.database == "sqlite":
350
+ self._create_sqlite_scaffold()
351
+
352
+ if self.config.testing == "mocktail":
353
+ self._create_mocktail_scaffold()
354
+
355
+ if self._uses_firebase():
356
+ self._create_firebase_scaffold()
357
+
358
+ def _uses_firebase(self) -> bool:
359
+ """Return whether any Firebase integration was selected."""
360
+ return (
361
+ self.config.auth_provider == "firebase"
362
+ or self.config.cloud_database == "firestore"
363
+ or self.config.notifications_provider == "firebase"
364
+ )
365
+
366
+ def _create_clean_architecture_scaffold(self) -> None:
367
+ """Create a Clean Architecture starter layout."""
368
+ src_dir = self.config.project_path / "lib" / "src"
369
+ directories = [
370
+ src_dir / "app",
371
+ src_dir / "core" / "error",
372
+ src_dir / "core" / "routing",
373
+ src_dir / "core" / "theme",
374
+ src_dir / "features" / "home" / "data",
375
+ src_dir / "features" / "home" / "domain",
376
+ src_dir / "features" / "home" / "presentation",
377
+ self.config.project_path / "test" / "features" / "home",
378
+ ]
379
+
380
+ for directory in directories:
381
+ directory.mkdir(parents=True, exist_ok=True)
382
+
383
+ (src_dir / "app" / "app.dart").write_text(
384
+ """import 'package:flutter/material.dart';
385
+ import 'package:flutter_riverpod/flutter_riverpod.dart';
386
+
387
+ import '../features/home/presentation/home_screen.dart';
388
+
389
+ class App extends ConsumerWidget {
390
+ const App({super.key});
391
+
392
+ @override
393
+ Widget build(BuildContext context, WidgetRef ref) {
394
+ return MaterialApp(
395
+ title: 'Flutter App',
396
+ theme: ThemeData(useMaterial3: true),
397
+ home: const HomeScreen(),
398
+ );
399
+ }
400
+ }
401
+ """
402
+ )
403
+
404
+ (
405
+ src_dir / "features" / "home" / "presentation" / "home_screen.dart"
406
+ ).write_text(
407
+ """import 'package:flutter/material.dart';
408
+ import 'package:flutter_riverpod/flutter_riverpod.dart';
409
+
410
+ class HomeScreen extends ConsumerWidget {
411
+ const HomeScreen({super.key});
412
+
413
+ @override
414
+ Widget build(BuildContext context, WidgetRef ref) {
415
+ return const Scaffold(
416
+ body: Center(
417
+ child: Text('Home'),
418
+ ),
419
+ );
420
+ }
421
+ }
422
+ """
423
+ )
424
+
425
+ (src_dir / "core" / "error" / "app_failure.dart").write_text(
426
+ """class AppFailure {
427
+ const AppFailure(this.message);
428
+
429
+ final String message;
430
+ }
431
+ """
432
+ )
433
+
434
+ firebase_import = ""
435
+ firebase_init = ""
436
+ if self._uses_firebase():
437
+ firebase_import = "import 'src/core/firebase/firebase_initializer.dart';\n"
438
+ firebase_init = " await initializeFirebase();\n"
439
+
440
+ main_dart = self.config.project_path / "lib" / "main.dart"
441
+ if main_dart.exists():
442
+ main_dart.write_text(f"""import 'package:flutter/material.dart';
443
+ import 'package:flutter_dotenv/flutter_dotenv.dart';
444
+ import 'package:flutter_riverpod/flutter_riverpod.dart';
445
+
446
+ {firebase_import}\
447
+ import 'src/app/app.dart';
448
+
449
+ Future<void> main() async {{
450
+ WidgetsFlutterBinding.ensureInitialized();
451
+ await dotenv.load(fileName: '.env');
452
+ {firebase_init}\
453
+ runApp(const ProviderScope(child: App()));
454
+ }}
455
+ """)
456
+
457
+ console.print(" ✅ Clean Architecture scaffold created")
458
+
459
+ def _create_sqlite_scaffold(self) -> None:
460
+ """Create a Drift/SQLite starter database scaffold."""
461
+ data_dir = self.config.project_path / "lib" / "src" / "core" / "data"
462
+ data_dir.mkdir(parents=True, exist_ok=True)
463
+
464
+ (data_dir / "app_database.dart").write_text("""import 'dart:io';
465
+
466
+ import 'package:drift/drift.dart';
467
+ import 'package:drift/native.dart';
468
+ import 'package:path/path.dart' as p;
469
+ import 'package:path_provider/path_provider.dart';
470
+
471
+ part 'app_database.g.dart';
472
+
473
+ class AppSettings extends Table {
474
+ TextColumn get key => text()();
475
+ TextColumn get value => text()();
476
+ DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
477
+
478
+ @override
479
+ Set<Column<Object>> get primaryKey => {key};
480
+ }
481
+
482
+ @DriftDatabase(tables: [AppSettings])
483
+ class AppDatabase extends _$AppDatabase {
484
+ AppDatabase() : super(_openConnection());
485
+
486
+ @override
487
+ int get schemaVersion => 1;
488
+ }
489
+
490
+ LazyDatabase _openConnection() {
491
+ return LazyDatabase(() async {
492
+ final dbFolder = await getApplicationDocumentsDirectory();
493
+ final file = File(p.join(dbFolder.path, 'app.sqlite'));
494
+ return NativeDatabase.createInBackground(file);
495
+ });
496
+ }
497
+ """)
498
+
499
+ console.print(" ✅ SQLite persistence scaffold created")
500
+
501
+ def _create_mocktail_scaffold(self) -> None:
502
+ """Create shared mocktail test helpers."""
503
+ helpers_dir = self.config.project_path / "test" / "helpers"
504
+ helpers_dir.mkdir(parents=True, exist_ok=True)
505
+
506
+ (helpers_dir / "mocks.dart").write_text(
507
+ """import 'package:mocktail/mocktail.dart';
508
+
509
+ class MockRepository extends Mock {}
510
+ """
511
+ )
512
+
513
+ console.print(" ✅ Mocktail testing scaffold created")
514
+
515
+ def _create_firebase_scaffold(self) -> None:
516
+ """Create Firebase integration starter files."""
517
+ firebase_dir = self.config.project_path / "lib" / "src" / "core" / "firebase"
518
+ firebase_dir.mkdir(parents=True, exist_ok=True)
519
+
520
+ (firebase_dir / "firebase_initializer.dart").write_text(
521
+ """import 'package:firebase_core/firebase_core.dart';
522
+
523
+ Future<void> initializeFirebase() async {
524
+ await Firebase.initializeApp();
525
+ }
526
+ """
527
+ )
528
+
529
+ if self.config.auth_provider == "firebase":
530
+ (firebase_dir / "firebase_auth_service.dart").write_text(
531
+ """import 'package:firebase_auth/firebase_auth.dart';
532
+
533
+ class FirebaseAuthService {
534
+ FirebaseAuthService({FirebaseAuth? auth}) : _auth = auth ?? FirebaseAuth.instance;
535
+
536
+ final FirebaseAuth _auth;
537
+
538
+ Stream<User?> authStateChanges() => _auth.authStateChanges();
539
+ }
540
+ """
541
+ )
542
+
543
+ if self.config.cloud_database == "firestore":
544
+ (firebase_dir / "firestore_database.dart").write_text(
545
+ """import 'package:cloud_firestore/cloud_firestore.dart';
546
+
547
+ class FirestoreDatabase {
548
+ FirestoreDatabase({FirebaseFirestore? firestore})
549
+ : _firestore = firestore ?? FirebaseFirestore.instance;
550
+
551
+ final FirebaseFirestore _firestore;
552
+
553
+ CollectionReference<Map<String, dynamic>> collection(String path) {
554
+ return _firestore.collection(path);
555
+ }
556
+ }
557
+ """
558
+ )
559
+
560
+ if self.config.notifications_provider == "firebase":
561
+ (firebase_dir / "firebase_notifications_service.dart").write_text(
562
+ """import 'package:firebase_messaging/firebase_messaging.dart';
563
+
564
+ class FirebaseNotificationsService {
565
+ FirebaseNotificationsService({FirebaseMessaging? messaging})
566
+ : _messaging = messaging ?? FirebaseMessaging.instance;
567
+
568
+ final FirebaseMessaging _messaging;
569
+
570
+ Future<NotificationSettings> requestPermission() {
571
+ return _messaging.requestPermission();
572
+ }
573
+
574
+ Future<String?> getToken() {
575
+ return _messaging.getToken();
576
+ }
577
+ }
578
+ """
579
+ )
580
+
581
+ console.print(" ✅ Firebase integration scaffold created")
582
+
583
+ def _create_cicd(self) -> None:
584
+ """Create CI/CD workflows and configuration."""
585
+ cicd_generator = CicdGenerator(self.config)
586
+ cicd_generator.generate_cicd()
587
+
588
+ def _add_dependencies(self) -> None:
589
+ """Add required dependencies to the project."""
590
+ try:
591
+ for dependency in self._runtime_dependencies():
592
+ subprocess.run(
593
+ [
594
+ str(self.flutter_root / "bin" / "flutter"),
595
+ "pub",
596
+ "add",
597
+ dependency,
598
+ ],
599
+ cwd=self.config.project_path,
600
+ check=False,
601
+ capture_output=True,
602
+ )
603
+
604
+ for dependency in self._dev_dependencies():
605
+ subprocess.run(
606
+ [
607
+ str(self.flutter_root / "bin" / "flutter"),
608
+ "pub",
609
+ "add",
610
+ "--dev",
611
+ dependency,
612
+ ],
613
+ cwd=self.config.project_path,
614
+ check=False,
615
+ capture_output=True,
616
+ )
617
+
618
+ # Add integration_test as SDK dependency by directly editing pubspec.yaml
619
+ # flutter pub add doesn't correctly handle SDK dependencies
620
+ self._add_integration_test_sdk_dependency()
621
+
622
+ console.print(" ✅ Dependencies added")
623
+
624
+ except Exception as e:
625
+ console.print(f" ⚠️ Dependency addition warning: {e}")
626
+
627
+ def _runtime_dependencies(self) -> list[str]:
628
+ """Return runtime dependencies required by selected scaffolds."""
629
+ dependencies = ["flutter_dotenv"]
630
+
631
+ if self.config.architecture == "clean":
632
+ dependencies.append("flutter_riverpod")
633
+
634
+ if self.config.database == "sqlite":
635
+ dependencies.extend(
636
+ ["drift", "sqlite3_flutter_libs", "path_provider", "path"]
637
+ )
638
+
639
+ if self._uses_firebase():
640
+ dependencies.append("firebase_core")
641
+
642
+ if self.config.auth_provider == "firebase":
643
+ dependencies.append("firebase_auth")
644
+
645
+ if self.config.cloud_database == "firestore":
646
+ dependencies.append("cloud_firestore")
647
+
648
+ if self.config.notifications_provider == "firebase":
649
+ dependencies.append("firebase_messaging")
650
+
651
+ return dependencies
652
+
653
+ def _dev_dependencies(self) -> list[str]:
654
+ """Return dev dependencies required by selected scaffolds."""
655
+ dependencies = ["flutter_lints"]
656
+
657
+ if self.config.database == "sqlite":
658
+ dependencies.extend(["drift_dev", "build_runner"])
659
+
660
+ if self.config.testing == "mocktail":
661
+ dependencies.append("mocktail")
662
+
663
+ return dependencies
664
+
665
+ def _pin_flutter_sdk_version(self, version: str) -> None:
666
+ """Set the flutter SDK constraint in pubspec.yaml to >= the given version."""
667
+ pubspec_path = self.config.project_path / "pubspec.yaml"
668
+ if not pubspec_path.exists():
669
+ console.print(" ⚠️ pubspec.yaml not found, skipping Flutter version pin")
670
+ return
671
+
672
+ try:
673
+ with open(pubspec_path, "r") as f:
674
+ pubspec = yaml.safe_load(f) or {}
675
+
676
+ if "environment" not in pubspec:
677
+ pubspec["environment"] = {}
678
+ pubspec["environment"]["flutter"] = f">={version}"
679
+
680
+ with open(pubspec_path, "w") as f:
681
+ yaml.dump(pubspec, f, default_flow_style=False, sort_keys=False)
682
+
683
+ console.print(f" ✅ Pinned flutter SDK to >={version} in pubspec.yaml")
684
+ except Exception as e:
685
+ console.print(f" ⚠️ Failed to pin Flutter version in pubspec.yaml: {e}")
686
+
687
+ def _add_integration_test_sdk_dependency(self) -> None:
688
+ """Add integration_test as an SDK dependency to pubspec.yaml."""
689
+ pubspec_path = self.config.project_path / "pubspec.yaml"
690
+ if not pubspec_path.exists():
691
+ console.print(" ⚠️ pubspec.yaml not found, skipping integration_test")
692
+ return
693
+
694
+ try:
695
+ # Read pubspec.yaml
696
+ with open(pubspec_path, "r") as f:
697
+ pubspec = yaml.safe_load(f) or {}
698
+
699
+ # Ensure dev_dependencies section exists
700
+ if "dev_dependencies" not in pubspec:
701
+ pubspec["dev_dependencies"] = {}
702
+
703
+ # Check if integration_test already exists
704
+ existing = pubspec["dev_dependencies"].get("integration_test")
705
+ if (
706
+ existing
707
+ and isinstance(existing, dict)
708
+ and existing.get("sdk") == "flutter"
709
+ ):
710
+ # Already correctly configured, skip
711
+ return
712
+
713
+ # Add or update integration_test with SDK specification
714
+ pubspec["dev_dependencies"]["integration_test"] = {"sdk": "flutter"}
715
+
716
+ # Write back to pubspec.yaml
717
+ with open(pubspec_path, "w") as f:
718
+ yaml.dump(pubspec, f, default_flow_style=False, sort_keys=False)
719
+
720
+ except Exception as e:
721
+ console.print(f" ⚠️ Failed to add integration_test SDK dependency: {e}")
722
+
723
+ def _create_environment_support(self) -> None:
724
+ """Create environment variable support."""
725
+ # Create .env file
726
+ env_content = """# Example environment variables
727
+ API_URL=https://api.example.com
728
+ """
729
+
730
+ with open(self.config.project_path / ".env", "w") as f:
731
+ f.write(env_content)
732
+
733
+ # Declare .env as a Flutter asset so it gets bundled into the app
734
+ self._add_env_asset_to_pubspec()
735
+
736
+ # Modify main.dart to load .env
737
+ self._modify_main_dart()
738
+
739
+ console.print(" ✅ Environment support created")
740
+
741
+ def _add_env_asset_to_pubspec(self) -> None:
742
+ """Add .env to the flutter assets list in pubspec.yaml."""
743
+ pubspec_path = self.config.project_path / "pubspec.yaml"
744
+ if not pubspec_path.exists():
745
+ return
746
+ try:
747
+ with open(pubspec_path, "r") as f:
748
+ pubspec = yaml.safe_load(f) or {}
749
+ flutter_section = pubspec.setdefault("flutter", {})
750
+ assets = flutter_section.setdefault("assets", [])
751
+ if ".env" not in assets:
752
+ assets.append(".env")
753
+ with open(pubspec_path, "w") as f:
754
+ yaml.dump(pubspec, f, default_flow_style=False, sort_keys=False)
755
+ except Exception as e:
756
+ console.print(f" ⚠️ Failed to add .env to pubspec.yaml assets: {e}")
757
+
758
+ def _modify_main_dart(self) -> None:
759
+ """Modify main.dart to load environment variables."""
760
+ main_dart = self.config.project_path / "lib" / "main.dart"
761
+
762
+ if not main_dart.exists():
763
+ return
764
+
765
+ try:
766
+ with open(main_dart, "r") as f:
767
+ content = f.read()
768
+
769
+ # Add import if not present
770
+ if "flutter_dotenv" not in content:
771
+ # Find first import line
772
+ lines = content.split("\n")
773
+ import_index = -1
774
+ for i, line in enumerate(lines):
775
+ if line.strip().startswith("import ") and "package:flutter" in line:
776
+ import_index = i
777
+ break
778
+
779
+ if import_index >= 0:
780
+ lines.insert(
781
+ import_index + 1,
782
+ "import 'package:flutter_dotenv/flutter_dotenv.dart';",
783
+ )
784
+ else:
785
+ lines.insert(
786
+ 0, "import 'package:flutter_dotenv/flutter_dotenv.dart';"
787
+ )
788
+
789
+ # Modify main function
790
+ modified_content = "\n".join(lines)
791
+ modified_content = modified_content.replace(
792
+ "void main() {",
793
+ 'Future<void> main() async {\n await dotenv.load(fileName: ".env");',
794
+ )
795
+
796
+ with open(main_dart, "w") as f:
797
+ f.write(modified_content)
798
+
799
+ if self._uses_firebase():
800
+ content = main_dart.read_text()
801
+ if "firebase_initializer.dart" not in content:
802
+ lines = content.split("\n")
803
+ import_index = -1
804
+ for i, line in enumerate(lines):
805
+ if line.strip().startswith("import "):
806
+ import_index = i
807
+
808
+ firebase_import = (
809
+ "import 'src/core/firebase/firebase_initializer.dart';"
810
+ )
811
+ if import_index >= 0:
812
+ lines.insert(import_index + 1, firebase_import)
813
+ else:
814
+ lines.insert(0, firebase_import)
815
+
816
+ modified_content = "\n".join(lines)
817
+ if "await dotenv.load" in modified_content:
818
+ modified_content = modified_content.replace(
819
+ ' await dotenv.load(fileName: ".env");',
820
+ ' await dotenv.load(fileName: ".env");\n'
821
+ " await initializeFirebase();",
822
+ )
823
+ elif "Future<void> main() async {" in modified_content:
824
+ modified_content = modified_content.replace(
825
+ "Future<void> main() async {",
826
+ "Future<void> main() async {\n"
827
+ " await initializeFirebase();",
828
+ )
829
+
830
+ main_dart.write_text(modified_content)
831
+
832
+ except Exception as e:
833
+ console.print(f" ⚠️ Main.dart modification warning: {e}")
834
+
835
+ def _create_readme(self) -> None:
836
+ """Create README file."""
837
+ if "web" in self.config.platforms:
838
+ run_cmd = "make run-chrome # runs on Chrome"
839
+ elif "ios" in self.config.platforms:
840
+ run_cmd = "make run-ios # runs on iOS simulator"
841
+ elif "android" in self.config.platforms:
842
+ run_cmd = "make run-android # runs on Android emulator"
843
+ else:
844
+ run_cmd = "flutter run"
845
+
846
+ readme_content = f"""# {self.config.project_name}
847
+
848
+ Flutter app scaffolded for Cursor.
849
+
850
+ ## Quickstart
851
+ ```bash
852
+ flutter pub get
853
+ {run_cmd}
854
+ ```
855
+
856
+ ## Testing
857
+ ```bash
858
+ make test # unit + widget tests
859
+ make integration # integration_test/
860
+ ```
861
+
862
+ ## Linting
863
+ ```bash
864
+ make analyze
865
+ ```
866
+
867
+ ## Env vars
868
+ Edit `.env` and access with `dotenv.env['KEY']` after startup.
869
+
870
+ ## Architecture
871
+ Architecture scaffold: `{self.config.architecture}`.
872
+
873
+ ## Persistence
874
+ Local database scaffold: `{self.config.database}`.
875
+
876
+ ## Testing
877
+ Testing starter: `{self.config.testing}`.
878
+
879
+ ## Firebase
880
+ Auth provider: `{self.config.auth_provider}`.
881
+ Cloud database: `{self.config.cloud_database}`.
882
+ Notifications: `{self.config.notifications_provider}`.
883
+
884
+ When Firebase options are enabled, run `flutterfire configure` for this
885
+ project before using the generated Firebase services.
886
+ """
887
+
888
+ with open(self.config.project_path / "README.md", "w") as f:
889
+ f.write(readme_content)
890
+
891
+ console.print(" ✅ README created")
892
+
893
+ def _format_code(self) -> None:
894
+ """Format the generated code."""
895
+ try:
896
+ subprocess.run(
897
+ [str(self.flutter_root / "bin" / "dart"), "format", "."],
898
+ cwd=self.config.project_path,
899
+ check=False,
900
+ capture_output=True,
901
+ )
902
+ console.print(" ✅ Code formatted")
903
+ except Exception as e:
904
+ console.print(f" ⚠️ Code formatting warning: {e}")