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,135 @@
1
+ """Configuration and data models for Flutter Setup."""
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import List, Literal
6
+
7
+ # Type aliases
8
+ FlutterChannel = Literal["stable", "beta"]
9
+ TemplateType = Literal["app", "plugin"]
10
+ IosLanguage = Literal["swift", "objc"]
11
+ AndroidLanguage = Literal["kotlin", "java"]
12
+ UpdateMode = Literal["reset", "reclone", "skip"]
13
+ Platform = Literal["ios", "android", "macos", "linux", "windows", "web"]
14
+ Architecture = Literal["basic", "clean"]
15
+ Database = Literal["none", "sqlite"]
16
+ Testing = Literal["standard", "mocktail"]
17
+ AuthProvider = Literal["none", "firebase"]
18
+ CloudDatabase = Literal["none", "firestore"]
19
+ NotificationsProvider = Literal["none", "firebase"]
20
+
21
+
22
+ @dataclass
23
+ class Config:
24
+ """Configuration for Flutter setup."""
25
+
26
+ project_name: str
27
+ platforms: List[str]
28
+ org: str
29
+ channel: FlutterChannel
30
+ output_dir: Path
31
+ template: TemplateType
32
+ ios_language: IosLanguage
33
+ android_language: AndroidLanguage
34
+ flutter_update_mode: UpdateMode
35
+ dry_run: bool
36
+ verbose: bool
37
+ flutter_location: Path
38
+ architecture: Architecture = "basic"
39
+ database: Database = "none"
40
+ testing: Testing = "standard"
41
+ auth_provider: AuthProvider = "none"
42
+ cloud_database: CloudDatabase = "none"
43
+ notifications_provider: NotificationsProvider = "none"
44
+ flutter_version: str | None = None
45
+
46
+ def __post_init__(self) -> None:
47
+ """Validate configuration after initialization."""
48
+ self._validate()
49
+
50
+ def _validate(self) -> None:
51
+ """Validate configuration values."""
52
+ if not self.project_name:
53
+ raise ValueError("Project name cannot be empty")
54
+
55
+ if not self.platforms:
56
+ raise ValueError("At least one platform must be specified")
57
+
58
+ # Basic normalization
59
+ self.platforms = [p.strip().lower() for p in self.platforms if p.strip()]
60
+
61
+ # Validate platforms
62
+ valid_platforms = {"ios", "android", "macos", "linux", "windows", "web"}
63
+ for platform in self.platforms:
64
+ if platform not in valid_platforms:
65
+ raise ValueError(f"Invalid platform: {platform}")
66
+
67
+ # Validate template-specific options
68
+ if self.template == "plugin":
69
+ if self.ios_language not in ["swift", "objc"]:
70
+ raise ValueError(f"Invalid iOS language: {self.ios_language}")
71
+ if self.android_language not in ["kotlin", "java"]:
72
+ raise ValueError(f"Invalid Android language: {self.android_language}")
73
+
74
+ if self.architecture not in ["basic", "clean"]:
75
+ raise ValueError(f"Invalid architecture: {self.architecture}")
76
+
77
+ if self.database not in ["none", "sqlite"]:
78
+ raise ValueError(f"Invalid database: {self.database}")
79
+
80
+ if self.testing not in ["standard", "mocktail"]:
81
+ raise ValueError(f"Invalid testing framework: {self.testing}")
82
+
83
+ if self.auth_provider not in ["none", "firebase"]:
84
+ raise ValueError(f"Invalid auth provider: {self.auth_provider}")
85
+
86
+ if self.cloud_database not in ["none", "firestore"]:
87
+ raise ValueError(f"Invalid cloud database: {self.cloud_database}")
88
+
89
+ if self.notifications_provider not in ["none", "firebase"]:
90
+ raise ValueError(
91
+ f"Invalid notifications provider: {self.notifications_provider}"
92
+ )
93
+
94
+ if self.flutter_version is not None:
95
+ import re
96
+
97
+ if not re.fullmatch(r"\d+\.\d+\.\d+", self.flutter_version):
98
+ raise ValueError(
99
+ f"Invalid flutter_version '{self.flutter_version}': must be X.Y.Z (e.g. 3.24.0)"
100
+ )
101
+
102
+ @property
103
+ def project_path(self) -> Path:
104
+ """Get the full project path."""
105
+ return self.output_dir / self.project_name
106
+
107
+ @property
108
+ def package_name(self) -> str:
109
+ """Get the sanitized package name."""
110
+ return self._sanitize_package_name(self.project_name)
111
+
112
+ @property
113
+ def platforms_csv(self) -> str:
114
+ """Get platforms as comma-separated string."""
115
+ return ",".join(self.platforms)
116
+
117
+ def _sanitize_package_name(self, name: str) -> str:
118
+ """Sanitize package name for Flutter."""
119
+ import re
120
+
121
+ # Convert to lowercase and replace non-alphanumeric with underscores
122
+ sanitized = re.sub(r"[^a-z0-9_]", "_", name.lower())
123
+
124
+ # Ensure it starts with a letter
125
+ if sanitized and not sanitized[0].isalpha():
126
+ sanitized = f"app_{sanitized}"
127
+
128
+ # Remove leading/trailing underscores
129
+ sanitized = sanitized.strip("_")
130
+
131
+ # Ensure it's not empty
132
+ if not sanitized:
133
+ sanitized = "app"
134
+
135
+ return sanitized
@@ -0,0 +1,159 @@
1
+ """Configuration file management with XDG Base Directory Specification support."""
2
+
3
+ import os
4
+ import shutil
5
+ from pathlib import Path
6
+ from typing import Any, Dict, Optional
7
+
8
+ import yaml
9
+ from rich.console import Console
10
+
11
+ console = Console()
12
+
13
+
14
+ class ConfigManager:
15
+ """Manages configuration file using XDG Base Directory Specification."""
16
+
17
+ def __init__(self) -> None:
18
+ """Initialize ConfigManager with XDG-compliant paths."""
19
+ self.config_dir = self._get_config_dir()
20
+ self.config_file = self.config_dir / "config.yaml"
21
+
22
+ def _get_config_dir(self) -> Path:
23
+ """Get config directory following XDG Base Directory Specification."""
24
+ # XDG_CONFIG_HOME takes precedence, fallback to ~/.config
25
+ xdg_config_home = os.getenv("XDG_CONFIG_HOME")
26
+ if xdg_config_home:
27
+ config_dir = Path(xdg_config_home) / "flutter-setup"
28
+ else:
29
+ config_dir = Path.home() / ".config" / "flutter-setup"
30
+
31
+ return config_dir
32
+
33
+ def ensure_config_dir(self) -> None:
34
+ """Ensure config directory exists, creating it if necessary."""
35
+ self.config_dir.mkdir(parents=True, exist_ok=True)
36
+
37
+ def get_default_config(self) -> Dict[str, Any]:
38
+ """Get default configuration values."""
39
+ home = Path.home()
40
+ default_flutter_location = str(home / "development" / "flutter")
41
+
42
+ return {
43
+ "flutter": {
44
+ "location": default_flutter_location,
45
+ "channel": "stable",
46
+ "update_mode": "skip",
47
+ },
48
+ "project": {
49
+ "org": "com.example",
50
+ "template": "app",
51
+ "architecture": "basic",
52
+ "database": "none",
53
+ "testing": "standard",
54
+ "auth_provider": "none",
55
+ "cloud_database": "none",
56
+ "notifications_provider": "none",
57
+ "ios_language": "swift",
58
+ "android_language": "kotlin",
59
+ },
60
+ }
61
+
62
+ def create_default_config(self) -> None:
63
+ """Create default config file if it doesn't exist."""
64
+ if self.config_file.exists():
65
+ return
66
+
67
+ self.ensure_config_dir()
68
+
69
+ default_config = self.get_default_config()
70
+
71
+ try:
72
+ with open(self.config_file, "w") as f:
73
+ yaml.dump(default_config, f, default_flow_style=False, sort_keys=False)
74
+ console.print(
75
+ f"[dim]Created default config file at: {self.config_file}[/dim]"
76
+ )
77
+ except Exception as e:
78
+ console.print(
79
+ f"[yellow]Warning: Could not create config file: {e}[/yellow]"
80
+ )
81
+
82
+ def load_config(self) -> Dict[str, Any]:
83
+ """Load configuration from file, creating default if it doesn't exist."""
84
+ # Create default config if it doesn't exist
85
+ if not self.config_file.exists():
86
+ self.create_default_config()
87
+
88
+ if not self.config_file.exists():
89
+ # If we still can't create it, return defaults
90
+ return self.get_default_config()
91
+
92
+ try:
93
+ with open(self.config_file, "r") as f:
94
+ config = yaml.safe_load(f) or {}
95
+ return config
96
+ except Exception as e:
97
+ console.print(
98
+ f"[yellow]Warning: Could not load config file: {e}. Using defaults.[/yellow]"
99
+ )
100
+ return self.get_default_config()
101
+
102
+ def save_config(self, config: Dict[str, Any]) -> None:
103
+ """Save configuration to file."""
104
+ self.ensure_config_dir()
105
+
106
+ try:
107
+ with open(self.config_file, "w") as f:
108
+ yaml.dump(config, f, default_flow_style=False, sort_keys=False)
109
+ except Exception as e:
110
+ console.print(f"[yellow]Warning: Could not save config file: {e}[/yellow]")
111
+
112
+ def get_flutter_location(self) -> Optional[Path]:
113
+ """Get Flutter location from config file."""
114
+ config = self.load_config()
115
+ flutter_location = config.get("flutter", {}).get("location")
116
+ if flutter_location:
117
+ return Path(flutter_location)
118
+ return None
119
+
120
+ def set_flutter_location(self, location: Path) -> None:
121
+ """Set Flutter location in config file."""
122
+ config = self.load_config()
123
+ if "flutter" not in config:
124
+ config["flutter"] = {}
125
+ config["flutter"]["location"] = str(location)
126
+ self.save_config(config)
127
+
128
+ def detect_flutter_location(self) -> Optional[Path]:
129
+ """Detect Flutter location from environment variables or PATH."""
130
+ # Check FLUTTER_ROOT environment variable
131
+ flutter_root = os.getenv("FLUTTER_ROOT")
132
+ if flutter_root:
133
+ path = Path(flutter_root)
134
+ if path.exists() and (path / "bin" / "flutter").exists():
135
+ return path
136
+
137
+ # Check if flutter is in PATH
138
+ flutter_bin = shutil.which("flutter")
139
+ if flutter_bin:
140
+ # flutter_bin is something like /path/to/flutter/bin/flutter
141
+ flutter_path = Path(flutter_bin).parent.parent
142
+ if flutter_path.exists() and (flutter_path / ".git").exists():
143
+ return flutter_path
144
+
145
+ # Check common installation locations
146
+ home = Path.home()
147
+ common_paths = [
148
+ home / "development" / "flutter",
149
+ home / "flutter",
150
+ home / ".flutter",
151
+ Path("/usr/local/flutter"),
152
+ Path("/opt/flutter"),
153
+ ]
154
+
155
+ for path in common_paths:
156
+ if path.exists() and (path / "bin" / "flutter").exists():
157
+ return path
158
+
159
+ return None
flutter_setup/core.py ADDED
@@ -0,0 +1,204 @@
1
+ """Core Flutter setup functionality."""
2
+
3
+ from rich.console import Console
4
+ from rich.progress import Progress, SpinnerColumn, TextColumn
5
+ from rich.panel import Panel
6
+
7
+ from .config import Config
8
+ from .exceptions import (
9
+ FlutterSetupError,
10
+ PrerequisitesError,
11
+ FlutterInstallationError,
12
+ ProjectCreationError,
13
+ )
14
+ from .prerequisites import PrerequisitesManager
15
+ from .flutter_manager import FlutterManager
16
+ from .project_creator import ProjectCreator
17
+ from .bootstrap import ProjectBootstrap
18
+ from .platform import detect_runtime_platform
19
+
20
+ console = Console()
21
+
22
+
23
+ class FlutterSetup:
24
+ """Main class for orchestrating Flutter development environment setup."""
25
+
26
+ def __init__(self, config: Config):
27
+ """Initialize FlutterSetup with configuration."""
28
+ self.config = config
29
+ self.platform = detect_runtime_platform()
30
+ self.prerequisites = PrerequisitesManager(config)
31
+ self.flutter_manager = FlutterManager(config)
32
+ self.project_creator = ProjectCreator(config)
33
+ self.bootstrap = ProjectBootstrap(config)
34
+
35
+ def run(self) -> None:
36
+ """Run the complete Flutter setup process."""
37
+ console.print(
38
+ f"\n[bold blue]🚀 Starting Flutter setup for: {self.config.project_name}[/bold blue]"
39
+ )
40
+ console.print(
41
+ f"[dim]Template: {self.config.template} | Org: {self.config.org} | Channel: {self.config.channel}[/dim]"
42
+ )
43
+ console.print(
44
+ f"[dim]Architecture: {self.config.architecture} | Database: {self.config.database}[/dim]"
45
+ )
46
+ console.print(
47
+ f"[dim]Testing: {self.config.testing} | Auth: {self.config.auth_provider} | Cloud DB: {self.config.cloud_database} | Notifications: {self.config.notifications_provider}[/dim]"
48
+ )
49
+ console.print(
50
+ f"[dim]Platforms: {', '.join(self.config.platforms)} | Package: {self.config.package_name}[/dim]"
51
+ )
52
+ console.print(f"[dim]Host OS: {self.platform}[/dim]")
53
+ console.print(f"[dim]Output: {self.config.project_path}[/dim]")
54
+
55
+ if self.config.dry_run:
56
+ console.print(
57
+ "[yellow]⚠️ DRY RUN MODE - No actual changes will be made[/yellow]"
58
+ )
59
+
60
+ try:
61
+ # Step 1: Check and install prerequisites
62
+ self._run_prerequisites()
63
+
64
+ # Step 2: Install/update Flutter SDK
65
+ self._run_flutter_installation()
66
+
67
+ # Step 3: Create Flutter project
68
+ self._run_project_creation()
69
+
70
+ # Step 4: Bootstrap development environment
71
+ self._run_bootstrap()
72
+
73
+ # Step 5: Display next steps
74
+ self._show_next_steps()
75
+
76
+ except Exception as e:
77
+ console.print(f"\n[red]❌ Setup failed: {e}[/red]")
78
+ raise
79
+
80
+ def _run_prerequisites(self) -> None:
81
+ """Run prerequisites check and installation."""
82
+ console.print("\n[bold]📋 Checking prerequisites...[/bold]")
83
+
84
+ with Progress(
85
+ SpinnerColumn(),
86
+ TextColumn("[progress.description]{task.description}"),
87
+ console=console,
88
+ ) as progress:
89
+ task = progress.add_task("Checking system requirements...", total=None)
90
+
91
+ try:
92
+ self.prerequisites.check_and_install()
93
+ progress.update(task, description="✅ Prerequisites satisfied")
94
+ except Exception as e:
95
+ progress.update(task, description="❌ Prerequisites failed")
96
+ raise PrerequisitesError(f"Failed to install prerequisites: {e}")
97
+
98
+ def _run_flutter_installation(self) -> None:
99
+ """Run Flutter SDK installation/update."""
100
+ console.print("\n[bold]🦋 Installing/updating Flutter SDK...[/bold]")
101
+
102
+ with Progress(
103
+ SpinnerColumn(),
104
+ TextColumn("[progress.description]{task.description}"),
105
+ console=console,
106
+ ) as progress:
107
+ task = progress.add_task("Setting up Flutter SDK...", total=None)
108
+
109
+ try:
110
+ self.flutter_manager.ensure_flutter()
111
+ progress.update(task, description="✅ Flutter SDK ready")
112
+ except Exception as e:
113
+ progress.update(task, description="❌ Flutter installation failed")
114
+ raise FlutterInstallationError(f"Failed to install Flutter: {e}")
115
+
116
+ def _run_project_creation(self) -> None:
117
+ """Run Flutter project creation."""
118
+ console.print("\n[bold]🏗️ Creating Flutter project...[/bold]")
119
+
120
+ with Progress(
121
+ SpinnerColumn(),
122
+ TextColumn("[progress.description]{task.description}"),
123
+ console=console,
124
+ ) as progress:
125
+ task = progress.add_task("Creating project structure...", total=None)
126
+
127
+ try:
128
+ self.project_creator.create_project()
129
+ progress.update(task, description="✅ Project created")
130
+ except Exception as e:
131
+ progress.update(task, description="❌ Project creation failed")
132
+ raise ProjectCreationError(f"Failed to create project: {e}")
133
+
134
+ def _run_bootstrap(self) -> None:
135
+ """Run project bootstrapping."""
136
+ console.print("\n[bold]🔧 Bootstrapping development environment...[/bold]")
137
+
138
+ with Progress(
139
+ SpinnerColumn(),
140
+ TextColumn("[progress.description]{task.description}"),
141
+ console=console,
142
+ ) as progress:
143
+ task = progress.add_task("Setting up development tools...", total=None)
144
+
145
+ try:
146
+ self.bootstrap.bootstrap_project()
147
+ progress.update(task, description="✅ Development environment ready")
148
+ except Exception as e:
149
+ progress.update(task, description="❌ Bootstrap failed")
150
+ raise FlutterSetupError(f"Failed to bootstrap project: {e}")
151
+
152
+ def _show_next_steps(self) -> None:
153
+ """Display next steps for the user."""
154
+ console.print("\n[bold green]🎉 Setup completed successfully![/bold green]")
155
+
156
+ if self.platform == "darwin":
157
+ shell_profile_steps = (
158
+ " [code]source ~/.zprofile[/code] # zsh login shells\n"
159
+ " [code]source ~/.zshrc[/code] # zsh interactive shells"
160
+ )
161
+ else:
162
+ shell_profile_steps = (
163
+ " [code]source ~/.bashrc[/code] # bash\n"
164
+ " [code]source ~/.zshrc[/code] # zsh"
165
+ )
166
+ platform_run_labels = {
167
+ "web": ("make run-chrome", "runs on Chrome"),
168
+ "ios": ("make run-ios", "runs on iOS simulator"),
169
+ "android": ("make run-android", "runs on Android emulator"),
170
+ }
171
+ run_steps = "\n".join(
172
+ f" [code]{cmd}[/code] # {label}"
173
+ for p in self.config.platforms
174
+ if (entry := platform_run_labels.get(p))
175
+ for cmd, label in [entry]
176
+ )
177
+ if not run_steps:
178
+ run_steps = " [code]flutter run[/code]"
179
+
180
+ next_steps = f"""
181
+ [bold]Next steps:[/bold]
182
+
183
+ 1. [blue]Activate Flutter in your shell:[/blue]
184
+ {shell_profile_steps}
185
+
186
+ 2. [blue]Navigate to your project:[/blue]
187
+ [code]cd "{self.config.project_path}"[/code]
188
+
189
+ 3. [blue]Run your Flutter app:[/blue]
190
+ {run_steps}
191
+
192
+ 4. [blue]Test your setup:[/blue]
193
+ [code]make test[/code] # run unit + widget tests
194
+ [code]make analyze[/code] # check lints
195
+
196
+ 5. [blue]Open in Cursor/VS Code:[/blue]
197
+ Hit F5 ("Flutter Debug") to start debugging
198
+
199
+ [dim]Your project is located at: {self.config.project_path}[/dim]
200
+ """
201
+
202
+ console.print(
203
+ Panel(next_steps, title="🚀 Ready to Code!", border_style="green")
204
+ )
@@ -0,0 +1,37 @@
1
+ """Custom exceptions for Flutter Setup."""
2
+
3
+
4
+ class FlutterSetupError(Exception):
5
+ """Base exception for Flutter setup errors."""
6
+
7
+ pass
8
+
9
+
10
+ class PrerequisitesError(FlutterSetupError):
11
+ """Raised when prerequisites are not met."""
12
+
13
+ pass
14
+
15
+
16
+ class FlutterInstallationError(FlutterSetupError):
17
+ """Raised when Flutter installation fails."""
18
+
19
+ pass
20
+
21
+
22
+ class ProjectCreationError(FlutterSetupError):
23
+ """Raised when project creation fails."""
24
+
25
+ pass
26
+
27
+
28
+ class ConfigurationError(FlutterSetupError):
29
+ """Raised when configuration is invalid."""
30
+
31
+ pass
32
+
33
+
34
+ class SystemError(FlutterSetupError):
35
+ """Raised when system-level operations fail."""
36
+
37
+ pass