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.
- flutter_setup/__init__.py +0 -0
- flutter_setup/bootstrap.py +904 -0
- flutter_setup/cicd_generator.py +814 -0
- flutter_setup/cli.py +662 -0
- flutter_setup/config.py +135 -0
- flutter_setup/config_manager.py +159 -0
- flutter_setup/core.py +204 -0
- flutter_setup/exceptions.py +37 -0
- flutter_setup/flutter_manager.py +579 -0
- flutter_setup/platform.py +15 -0
- flutter_setup/prerequisites.py +39 -0
- flutter_setup/prerequisites_linux.py +140 -0
- flutter_setup/prerequisites_macos.py +226 -0
- flutter_setup/project_creator.py +82 -0
- flutter_setup-2.1.0.dist-info/METADATA +365 -0
- flutter_setup-2.1.0.dist-info/RECORD +20 -0
- flutter_setup-2.1.0.dist-info/WHEEL +5 -0
- flutter_setup-2.1.0.dist-info/entry_points.txt +2 -0
- flutter_setup-2.1.0.dist-info/licenses/LICENSE +21 -0
- flutter_setup-2.1.0.dist-info/top_level.txt +1 -0
|
@@ -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}")
|