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/cli.py ADDED
@@ -0,0 +1,662 @@
1
+ #!/usr/bin/env python3
2
+ """Flutter Setup CLI - Main entry point."""
3
+
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Any, Dict, cast
7
+
8
+ import click
9
+ from rich.console import Console
10
+ from rich.panel import Panel
11
+
12
+ from .core import FlutterSetup
13
+ from .config import (
14
+ Config,
15
+ FlutterChannel,
16
+ TemplateType,
17
+ IosLanguage,
18
+ AndroidLanguage,
19
+ UpdateMode,
20
+ Architecture,
21
+ Database,
22
+ Testing,
23
+ AuthProvider,
24
+ CloudDatabase,
25
+ NotificationsProvider,
26
+ )
27
+ from .config_manager import ConfigManager
28
+ from .exceptions import FlutterSetupError
29
+ from .prerequisites import PrerequisitesManager
30
+ from .flutter_manager import FlutterManager
31
+ from .platform import detect_runtime_platform
32
+
33
+ console = Console()
34
+
35
+
36
+ def _is_interactive() -> bool:
37
+ """Return True when stdin is a TTY (interactive session)."""
38
+ return sys.stdin.isatty()
39
+
40
+
41
+ def print_banner() -> None:
42
+ """Print the application banner."""
43
+ banner = """
44
+ [bold blue]Flutter Development Environment Setup[/bold blue]
45
+ [dim]Automated Flutter development environment setup for macOS and Linux[/dim]
46
+ """
47
+ console.print(Panel(banner, border_style="blue"))
48
+
49
+
50
+ @click.group(invoke_without_command=True)
51
+ @click.pass_context
52
+ def cli(ctx: click.Context) -> None:
53
+ """Flutter Development Environment Setup CLI."""
54
+ # If no subcommand provided, show banner and help
55
+ if ctx.invoked_subcommand is None:
56
+ print_banner()
57
+ click.echo(ctx.get_help())
58
+
59
+
60
+ @cli.command("init")
61
+ @click.option(
62
+ "--force",
63
+ is_flag=True,
64
+ help="Overwrite existing config file if it exists",
65
+ )
66
+ def init_config(force: bool) -> None:
67
+ """Initialize the configuration file interactively."""
68
+ config_manager = ConfigManager()
69
+ config_manager.ensure_config_dir()
70
+
71
+ # Load existing config if it exists
72
+ existing_config = None
73
+ if config_manager.config_file.exists():
74
+ if force:
75
+ config_manager.config_file.unlink()
76
+ console.print(
77
+ f"[yellow]Deleted existing config: {config_manager.config_file}[/yellow]\n"
78
+ )
79
+ else:
80
+ console.print(
81
+ f"[yellow]⚠️ Config file already exists at: {config_manager.config_file}[/yellow]"
82
+ )
83
+ console.print("[dim]Loading existing configuration for editing...[/dim]\n")
84
+ existing_config = config_manager.load_config()
85
+
86
+ # Start interactive configuration
87
+ console.print("[bold blue]Flutter Setup Configuration[/bold blue]\n")
88
+
89
+ # 1. Flutter Location
90
+ console.print("[bold]1. Flutter SDK Location[/bold]")
91
+ if existing_config:
92
+ current_location = existing_config.get("flutter", {}).get("location", "")
93
+ console.print(f"[dim]Current: {current_location}[/dim]")
94
+ else:
95
+ # Try to detect Flutter location
96
+ detected = config_manager.detect_flutter_location()
97
+ if detected:
98
+ console.print(f"[green]✓ Detected Flutter at: {detected}[/green]")
99
+ current_location = str(detected)
100
+ else:
101
+ default_location = str(Path.home() / "development" / "flutter")
102
+ console.print(f"[dim]Default: {default_location}[/dim]")
103
+ current_location = default_location
104
+
105
+ flutter_location_input = click.prompt(
106
+ "Flutter location",
107
+ default=current_location,
108
+ type=str,
109
+ )
110
+ flutter_location = Path(flutter_location_input).expanduser().resolve()
111
+
112
+ # Validate Flutter location
113
+ if not flutter_location.exists():
114
+ console.print(
115
+ "[yellow]⚠️ Warning: Path does not exist. It will be created when Flutter is installed.[/yellow]"
116
+ )
117
+
118
+ # 2. Flutter Channel
119
+ console.print("\n[bold]2. Flutter Channel[/bold]")
120
+ if existing_config:
121
+ current_channel = existing_config.get("flutter", {}).get("channel", "stable")
122
+ console.print(f"[dim]Current: {current_channel}[/dim]")
123
+ else:
124
+ current_channel = "stable"
125
+
126
+ channel = click.prompt(
127
+ "Flutter channel",
128
+ default=current_channel,
129
+ type=click.Choice(["stable", "beta"], case_sensitive=False),
130
+ )
131
+
132
+ # 3. Organization ID
133
+ console.print("\n[bold]3. Organization ID[/bold]")
134
+ if existing_config:
135
+ current_org = existing_config.get("project", {}).get("org", "com.example")
136
+ console.print(f"[dim]Current: {current_org}[/dim]")
137
+ else:
138
+ current_org = "com.example"
139
+
140
+ org = click.prompt(
141
+ "Organization ID (e.g., com.example, com.mycompany)",
142
+ default=current_org,
143
+ type=str,
144
+ )
145
+
146
+ # Build the config, preserving any existing project settings not covered by init prompts
147
+ existing_project = existing_config.get("project", {}) if existing_config else {}
148
+ config = {
149
+ "flutter": {
150
+ "location": str(flutter_location),
151
+ "channel": channel.lower(),
152
+ "update_mode": (
153
+ existing_config.get("flutter", {}).get("update_mode", "skip")
154
+ if existing_config
155
+ else "skip"
156
+ ),
157
+ },
158
+ "project": {
159
+ "org": org,
160
+ "template": existing_project.get("template", "app"),
161
+ # setup-owned fields: only carry forward if already written by a prior setup run
162
+ **{
163
+ k: existing_project[k]
164
+ for k in [
165
+ "architecture",
166
+ "database",
167
+ "testing",
168
+ "auth_provider",
169
+ "cloud_database",
170
+ "notifications_provider",
171
+ "ios_language",
172
+ "android_language",
173
+ ]
174
+ if k in existing_project
175
+ },
176
+ },
177
+ }
178
+
179
+ # Save the config
180
+ config_manager.save_config(config)
181
+
182
+ # Show summary
183
+ console.print("\n[green]✅ Configuration saved![/green]")
184
+ console.print("\n[bold]Config file location:[/bold]")
185
+ console.print(f" {config_manager.config_file}")
186
+ console.print("\n[bold]Configuration summary:[/bold]")
187
+ console.print(f" Flutter location: {config['flutter']['location']}")
188
+ console.print(f" Flutter channel: {config['flutter']['channel']}")
189
+ console.print(f" Organization: {config['project']['org']}")
190
+ console.print(
191
+ "\n[dim]You can edit this file directly or run 'flutter-setup init' again to update it.[/dim]"
192
+ )
193
+
194
+
195
+ @cli.command("check")
196
+ @click.option(
197
+ "--verbose",
198
+ "-v",
199
+ is_flag=True,
200
+ help="Enable verbose output",
201
+ )
202
+ def check_command(verbose: bool) -> None:
203
+ """Check that prerequisites and Flutter SDK are ready and updated."""
204
+ try:
205
+ print_banner()
206
+ console.print("\n[bold]🔍 Checking Flutter development environment...[/bold]\n")
207
+
208
+ # Load configuration from file
209
+ config_manager = ConfigManager()
210
+ config_manager.ensure_config_dir()
211
+ file_config = config_manager.load_config()
212
+
213
+ # Get Flutter location from config file
214
+ flutter_location_str = file_config.get("flutter", {}).get("location")
215
+ if flutter_location_str:
216
+ flutter_location = Path(flutter_location_str)
217
+ else:
218
+ # Fallback to default if not in config
219
+ flutter_location = Path.home() / "development" / "flutter"
220
+
221
+ # Get channel from config
222
+ channel = file_config.get("flutter", {}).get("channel", "stable")
223
+
224
+ # Create minimal config for checking (we don't need project details)
225
+ host_platform = detect_runtime_platform()
226
+ default_platform = "ios" if host_platform == "darwin" else "linux"
227
+
228
+ # Use dummy values for required fields
229
+ config = Config(
230
+ project_name="dummy",
231
+ platforms=[default_platform],
232
+ org="com.example",
233
+ channel=channel,
234
+ output_dir=Path("."),
235
+ template="app",
236
+ ios_language="swift",
237
+ android_language="kotlin",
238
+ flutter_update_mode="skip", # Don't update during check
239
+ dry_run=False,
240
+ verbose=verbose,
241
+ flutter_location=flutter_location,
242
+ )
243
+
244
+ # Check prerequisites
245
+ console.print("[bold]📋 Checking prerequisites...[/bold]")
246
+ prerequisites = PrerequisitesManager(config)
247
+ prerequisites_ok = prerequisites.check_only()
248
+
249
+ # Check Flutter SDK
250
+ console.print("\n[bold]🦋 Checking Flutter SDK...[/bold]")
251
+ flutter_manager = FlutterManager(config)
252
+ flutter_ok = flutter_manager.check_only()
253
+
254
+ # Summary
255
+ console.print("\n[bold]📊 Summary[/bold]")
256
+ if prerequisites_ok and flutter_ok:
257
+ console.print(
258
+ "[green]✅ All checks passed! Your Flutter development environment is ready.[/green]"
259
+ )
260
+ sys.exit(0)
261
+ else:
262
+ console.print(
263
+ "[yellow]⚠️ Some checks failed. Please review the issues above.[/yellow]"
264
+ )
265
+ if not prerequisites_ok:
266
+ console.print(
267
+ "[dim]Run 'flutter-setup setup <project> <platforms>' to install missing prerequisites.[/dim]"
268
+ )
269
+ if not flutter_ok:
270
+ console.print(
271
+ "[dim]Run 'flutter-setup setup <project> <platforms>' to install or update Flutter SDK.[/dim]"
272
+ )
273
+ sys.exit(1)
274
+
275
+ except Exception as e:
276
+ console.print(f"[red]❌ Check failed: {e}[/red]")
277
+ if verbose:
278
+ console.print_exception()
279
+ sys.exit(1)
280
+
281
+
282
+ @cli.command("setup")
283
+ @click.argument("project_name", required=False, default=None)
284
+ @click.argument("platforms", nargs=-1)
285
+ @click.option(
286
+ "--org",
287
+ default="com.example",
288
+ help="Organization identifier (default: com.example)",
289
+ )
290
+ @click.option(
291
+ "--channel",
292
+ type=click.Choice(["stable", "beta"]),
293
+ default="stable",
294
+ help="Flutter channel (default: stable)",
295
+ )
296
+ @click.option(
297
+ "--dir",
298
+ default=".",
299
+ type=click.Path(exists=True, file_okay=False, dir_okay=True),
300
+ help="Output directory (default: current directory)",
301
+ )
302
+ @click.option(
303
+ "--template",
304
+ type=click.Choice(["app", "plugin"]),
305
+ default="app",
306
+ help="Project template (default: app)",
307
+ )
308
+ @click.option(
309
+ "--architecture",
310
+ type=click.Choice(["basic", "clean"]),
311
+ default="basic",
312
+ help="Application architecture scaffold (default: basic)",
313
+ )
314
+ @click.option(
315
+ "--database",
316
+ type=click.Choice(["none", "sqlite"]),
317
+ default="none",
318
+ help="Local persistence scaffold (default: none)",
319
+ )
320
+ @click.option(
321
+ "--testing",
322
+ type=click.Choice(["standard", "mocktail"]),
323
+ default="standard",
324
+ help="Testing starter scaffold (default: standard)",
325
+ )
326
+ @click.option(
327
+ "--auth-provider",
328
+ type=click.Choice(["none", "firebase"]),
329
+ default="none",
330
+ help="Auth integration scaffold (default: none)",
331
+ )
332
+ @click.option(
333
+ "--cloud-database",
334
+ type=click.Choice(["none", "firestore"]),
335
+ default="none",
336
+ help="Cloud database integration scaffold (default: none)",
337
+ )
338
+ @click.option(
339
+ "--notifications-provider",
340
+ type=click.Choice(["none", "firebase"]),
341
+ default="none",
342
+ help="Push notifications scaffold (default: none)",
343
+ )
344
+ @click.option(
345
+ "--ios-language",
346
+ type=click.Choice(["swift", "objc"]),
347
+ default="swift",
348
+ help="iOS language for plugin templates (default: swift)",
349
+ )
350
+ @click.option(
351
+ "--android-language",
352
+ type=click.Choice(["kotlin", "java"]),
353
+ default="kotlin",
354
+ help="Android language for plugin templates (default: kotlin)",
355
+ )
356
+ @click.option(
357
+ "--flutter-update",
358
+ type=click.Choice(["reset", "reclone", "skip"]),
359
+ default="skip",
360
+ help="Flutter update mode (default: skip)",
361
+ )
362
+ @click.option(
363
+ "--flutter-version",
364
+ default=None,
365
+ help="Required Flutter SDK version (e.g. 3.24.0). Errors early if not installed.",
366
+ )
367
+ @click.option(
368
+ "--dry-run",
369
+ is_flag=True,
370
+ help="Preview actions without executing them",
371
+ )
372
+ @click.option(
373
+ "--verbose",
374
+ "-v",
375
+ is_flag=True,
376
+ help="Enable verbose output",
377
+ )
378
+ @click.pass_context
379
+ def setup_command(
380
+ ctx: click.Context,
381
+ project_name: str | None,
382
+ platforms: tuple[str, ...],
383
+ org: str,
384
+ channel: FlutterChannel,
385
+ dir: str,
386
+ template: TemplateType,
387
+ architecture: Architecture,
388
+ database: Database,
389
+ testing: Testing,
390
+ auth_provider: AuthProvider,
391
+ cloud_database: CloudDatabase,
392
+ notifications_provider: NotificationsProvider,
393
+ ios_language: IosLanguage,
394
+ android_language: AndroidLanguage,
395
+ flutter_update: UpdateMode,
396
+ flutter_version: str | None,
397
+ dry_run: bool,
398
+ verbose: bool,
399
+ ) -> None:
400
+ """Set up a complete Flutter development environment."""
401
+ try:
402
+ print_banner()
403
+
404
+ # Load configuration from file
405
+ config_manager = ConfigManager()
406
+ config_manager.ensure_config_dir()
407
+ file_config = config_manager.load_config()
408
+ file_project = file_config.get("project", {})
409
+ file_flutter = file_config.get("flutter", {})
410
+
411
+ # Get Flutter location from config file
412
+ flutter_location_str = file_config.get("flutter", {}).get("location")
413
+ if flutter_location_str:
414
+ flutter_location = Path(flutter_location_str)
415
+ else:
416
+ flutter_location = Path.home() / "development" / "flutter"
417
+ config_manager.set_flutter_location(flutter_location)
418
+
419
+ def get_merged_or_prompt(
420
+ param_name: str,
421
+ cli_value: Any,
422
+ config_dict: Dict[str, Any],
423
+ prompt_text: str,
424
+ choices: list[str] | None = None,
425
+ config_key: str | None = None,
426
+ ) -> Any:
427
+ """Use CLI value if explicit, config file value if present and valid, else prompt."""
428
+ key = config_key or param_name
429
+ if (
430
+ ctx.get_parameter_source(param_name)
431
+ == click.core.ParameterSource.COMMANDLINE
432
+ ):
433
+ return cli_value
434
+ if key in config_dict:
435
+ value = config_dict[key]
436
+ # Normalize string values to lowercase for case-insensitive matching
437
+ normalized = value.lower() if isinstance(value, str) else value
438
+ if choices is None or normalized in choices:
439
+ return normalized if isinstance(value, str) else value
440
+ # Config value is invalid — fall through to prompt
441
+ if not _is_interactive():
442
+ # Non-interactive environment: use CLI default silently
443
+ return cli_value
444
+ if choices:
445
+ return click.prompt(
446
+ prompt_text,
447
+ default=cli_value,
448
+ type=click.Choice(choices, case_sensitive=False),
449
+ )
450
+ return click.prompt(prompt_text, default=cli_value)
451
+
452
+ def get_merged(
453
+ param_name: str,
454
+ cli_value: Any,
455
+ config_dict: Dict[str, Any],
456
+ config_key: str | None = None,
457
+ ) -> Any:
458
+ """Use CLI value if explicit, config file value if present, else CLI default (no prompt)."""
459
+ key = config_key or param_name
460
+ if (
461
+ ctx.get_parameter_source(param_name)
462
+ == click.core.ParameterSource.COMMANDLINE
463
+ ):
464
+ return cli_value
465
+ return config_dict.get(key, cli_value)
466
+
467
+ # Prompt for required arguments if not provided on the command line
468
+ if project_name is None:
469
+ project_name = click.prompt("Project name", type=str)
470
+
471
+ if not platforms:
472
+ console.print(
473
+ "[dim]Available platforms: ios, android, macos, linux, windows, web[/dim]"
474
+ )
475
+ platforms_str = click.prompt(
476
+ "Platforms (space-separated)",
477
+ default="ios android",
478
+ )
479
+ platforms = tuple(p.strip() for p in platforms_str.split() if p.strip())
480
+
481
+ if not platforms:
482
+ console.print("[red]Error: At least one platform is required[/red]")
483
+ sys.exit(1)
484
+
485
+ # Merge CLI options with config file, prompting for values not in either
486
+ merged_org = get_merged_or_prompt(
487
+ "org", org, file_project, "Organization ID (e.g., com.mycompany)"
488
+ )
489
+ merged_channel = cast(
490
+ FlutterChannel,
491
+ get_merged_or_prompt(
492
+ "channel",
493
+ channel,
494
+ file_flutter,
495
+ "Flutter channel",
496
+ choices=["stable", "beta"],
497
+ ),
498
+ )
499
+ merged_template = cast(
500
+ TemplateType,
501
+ get_merged_or_prompt(
502
+ "template",
503
+ template,
504
+ file_project,
505
+ "Project template",
506
+ choices=["app", "plugin"],
507
+ ),
508
+ )
509
+ merged_architecture = cast(
510
+ Architecture,
511
+ get_merged_or_prompt(
512
+ "architecture",
513
+ architecture,
514
+ file_project,
515
+ "Architecture",
516
+ choices=["basic", "clean"],
517
+ ),
518
+ )
519
+ merged_database = cast(
520
+ Database,
521
+ get_merged_or_prompt(
522
+ "database",
523
+ database,
524
+ file_project,
525
+ "Local database",
526
+ choices=["none", "sqlite"],
527
+ ),
528
+ )
529
+ merged_testing = cast(
530
+ Testing,
531
+ get_merged_or_prompt(
532
+ "testing",
533
+ testing,
534
+ file_project,
535
+ "Testing framework",
536
+ choices=["standard", "mocktail"],
537
+ ),
538
+ )
539
+ merged_auth_provider = cast(
540
+ AuthProvider,
541
+ get_merged_or_prompt(
542
+ "auth_provider",
543
+ auth_provider,
544
+ file_project,
545
+ "Auth provider",
546
+ choices=["none", "firebase"],
547
+ ),
548
+ )
549
+ merged_cloud_database = cast(
550
+ CloudDatabase,
551
+ get_merged_or_prompt(
552
+ "cloud_database",
553
+ cloud_database,
554
+ file_project,
555
+ "Cloud database",
556
+ choices=["none", "firestore"],
557
+ ),
558
+ )
559
+ merged_notifications_provider = cast(
560
+ NotificationsProvider,
561
+ get_merged_or_prompt(
562
+ "notifications_provider",
563
+ notifications_provider,
564
+ file_project,
565
+ "Notifications provider",
566
+ choices=["none", "firebase"],
567
+ ),
568
+ )
569
+ # Language options only apply to plugin templates; never prompt for app projects
570
+ if merged_template == "plugin":
571
+ merged_ios_language = cast(
572
+ IosLanguage,
573
+ get_merged_or_prompt(
574
+ "ios_language",
575
+ ios_language,
576
+ file_project,
577
+ "iOS language",
578
+ choices=["swift", "objc"],
579
+ ),
580
+ )
581
+ merged_android_language = cast(
582
+ AndroidLanguage,
583
+ get_merged_or_prompt(
584
+ "android_language",
585
+ android_language,
586
+ file_project,
587
+ "Android language",
588
+ choices=["kotlin", "java"],
589
+ ),
590
+ )
591
+ else:
592
+ merged_ios_language = cast(
593
+ IosLanguage, get_merged("ios_language", ios_language, file_project)
594
+ )
595
+ merged_android_language = cast(
596
+ AndroidLanguage,
597
+ get_merged("android_language", android_language, file_project),
598
+ )
599
+ merged_flutter_update = cast(
600
+ UpdateMode,
601
+ get_merged_or_prompt(
602
+ "flutter_update",
603
+ flutter_update,
604
+ file_flutter,
605
+ "Flutter update mode",
606
+ choices=["reset", "reclone", "skip"],
607
+ config_key="update_mode",
608
+ ),
609
+ )
610
+
611
+ # Create configuration
612
+ config = Config(
613
+ project_name=project_name,
614
+ platforms=list(platforms),
615
+ org=merged_org,
616
+ channel=merged_channel,
617
+ output_dir=Path(dir),
618
+ template=merged_template,
619
+ ios_language=merged_ios_language,
620
+ android_language=merged_android_language,
621
+ flutter_update_mode=merged_flutter_update,
622
+ dry_run=dry_run,
623
+ verbose=verbose,
624
+ flutter_location=flutter_location,
625
+ architecture=merged_architecture,
626
+ database=merged_database,
627
+ testing=merged_testing,
628
+ auth_provider=merged_auth_provider,
629
+ cloud_database=merged_cloud_database,
630
+ notifications_provider=merged_notifications_provider,
631
+ flutter_version=flutter_version,
632
+ )
633
+
634
+ # Create and run setup
635
+ setup = FlutterSetup(config)
636
+ setup.run()
637
+
638
+ console.print("\n[green]✅ Flutter setup completed successfully![/green]")
639
+
640
+ except FlutterSetupError as e:
641
+ console.print(f"[red]❌ Setup failed: {e}[/red]")
642
+ if verbose:
643
+ console.print_exception()
644
+ sys.exit(1)
645
+ except KeyboardInterrupt:
646
+ console.print("\n[yellow]⚠️ Setup interrupted by user[/yellow]")
647
+ sys.exit(1)
648
+ except Exception as e:
649
+ console.print(f"[red]❌ Unexpected error: {e}[/red]")
650
+ if verbose:
651
+ console.print_exception()
652
+ sys.exit(1)
653
+
654
+
655
+ def main() -> None:
656
+ """Main entry point that routes to appropriate command."""
657
+ cli()
658
+
659
+
660
+ if __name__ == "__main__":
661
+ # Click handles argument parsing
662
+ main()