flutter-dev 0.1.9__tar.gz → 0.1.10__tar.gz

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.
Files changed (43) hide show
  1. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/.env.example +8 -9
  2. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/CHANGELOG.md +22 -0
  3. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/PKG-INFO +38 -12
  4. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/README.md +37 -11
  5. flutter_dev-0.1.10/core/ai_config.py +297 -0
  6. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/fdev.py +16 -7
  7. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/flutter_dev.egg-info/SOURCES.txt +2 -0
  8. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/gemini_api.py +19 -74
  9. flutter_dev-0.1.10/managers/ai.py +232 -0
  10. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/doctor.py +44 -31
  11. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/pyproject.toml +1 -1
  12. flutter_dev-0.1.10/tests/test_ai_config.py +187 -0
  13. flutter_dev-0.1.9/managers/ai.py +0 -113
  14. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/LICENSE +0 -0
  15. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/MANIFEST.in +0 -0
  16. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/common_utils.py +0 -0
  17. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/core/__init__.py +0 -0
  18. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/core/constants.py +0 -0
  19. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/core/state.py +0 -0
  20. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/create_page.py +0 -0
  21. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/git_diff_output_editor.py +0 -0
  22. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/install_legacy.py +0 -0
  23. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/__init__.py +0 -0
  24. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/app.py +0 -0
  25. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/brew.py +0 -0
  26. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/build.py +0 -0
  27. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/datetime.py +0 -0
  28. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/device.py +0 -0
  29. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/git.py +0 -0
  30. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/git_account.py +0 -0
  31. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/localization.py +0 -0
  32. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/merge.py +0 -0
  33. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/mirror.py +0 -0
  34. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/package.py +0 -0
  35. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/project.py +0 -0
  36. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/web_deploy.py +0 -0
  37. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/requirements.txt +0 -0
  38. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/setup.cfg +0 -0
  39. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/setup.py +0 -0
  40. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/switch_ai.py +0 -0
  41. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/tests/test_fdev_cli.py +0 -0
  42. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/tests/test_localization.py +0 -0
  43. {flutter_dev-0.1.9 → flutter_dev-0.1.10}/tests/test_package_commands.py +0 -0
@@ -1,26 +1,25 @@
1
1
  # AI API Keys Configuration
2
- # সব API keys এখানে secure ভাবে রাখুন
3
- # এই file টি .gitignore এ add করতে হবে
2
+ # API keys secure রাখুন এবং কারও সঙ্গে share করবেন না
4
3
 
5
- # Groq API (Current - Active)
6
- GROQ_API_KEY=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
4
+ # Groq API
5
+ GROQ_API_KEY=
7
6
  GROQ_API_URL=https://api.groq.com/openai/v1/chat/completions
8
7
  GROQ_MODEL=llama-3.3-70b-versatile
9
8
 
10
9
  # Mistral AI API
11
- MISTRAL_API_KEY=irrmymmw43lvCcARy08R1TZhHuqMgXwa
10
+ MISTRAL_API_KEY=
12
11
  MISTRAL_API_URL=https://api.mistral.ai/v1/chat/completions
13
- MISTRAL_MODEL=mistral-medium
12
+ MISTRAL_MODEL=mistral-medium-latest
14
13
 
15
14
  # SambaNova API
16
- SAMBANOVA_API_KEY=c5c3f0dc-75db-4621-9d0c-7372efb4a04f
15
+ SAMBANOVA_API_KEY=
17
16
  SAMBANOVA_API_URL=https://api.sambanova.ai/v1/chat/completions
18
17
  SAMBANOVA_MODEL=Meta-Llama-3.3-70B-Instruct
19
18
 
20
19
  # OpenRouter API
21
- OPENROUTER_API_KEY=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
20
+ OPENROUTER_API_KEY=
22
21
  OPENROUTER_API_URL=https://openrouter.ai/api/v1/chat/completions
23
22
  OPENROUTER_MODEL=openai/gpt-3.5-turbo
24
23
 
25
24
  # Default AI Service (groq, mistral, sambanova, openrouter)
26
- DEFAULT_AI_SERVICE=groq
25
+ DEFAULT_AI_SERVICE=groq
@@ -2,6 +2,28 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## 0.1.10 - 2026-07-16
6
+
7
+ ### Added
8
+
9
+ - Added `fdev init` to create or repair persistent AI configuration with safe provider defaults and empty API-key fields.
10
+ - Added interactive hidden API-key entry and default/custom model selection when AI configuration is incomplete.
11
+ - Added `fdev ai <provider> <model>` for changing a provider's model without replacing its API key.
12
+ - Added Python coverage for configuration precedence, dotenv parsing, atomic updates, cancellation, permissions, platform paths, and doctor status.
13
+
14
+ ### Changed
15
+
16
+ - Unified `fdev ai`, `fdev doctor`, `gemini-api`, and `fdev commit` around the same persistent AI configuration resolver.
17
+ - Updated the default Mistral model to `mistral-medium-latest`.
18
+ - Limited build-output directory creation to commands that use those directories.
19
+
20
+ ### Fixed
21
+
22
+ - Removed credentials from `.env.example` and replaced them with empty API-key values.
23
+ - Made AI configuration writes atomic and restricted the generated file to the current user on Unix-like systems.
24
+ - Cancelled AI setup without saving a partial key or sending a request when interrupted during key/model entry.
25
+ - Prevented exported provider/model overrides from being reported as successfully replaced by file-only changes.
26
+
5
27
  ## 0.1.9 - 2026-07-14
6
28
 
7
29
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flutter-dev
3
- Version: 0.1.9
3
+ Version: 0.1.10
4
4
  Summary: A cross-platform CLI toolkit for Flutter development workflows.
5
5
  Author: Royal Court BD
6
6
  License-Expression: MIT
@@ -42,7 +42,7 @@ The package is published on PyPI as `flutter-dev` and installs console commands
42
42
  python -m pip install flutter-dev
43
43
  ```
44
44
 
45
- Current package metadata declares version `0.1.9`, Python `>=3.8`, MIT license, and support for macOS, Linux, and Windows.
45
+ Current package metadata declares version `0.1.10`, Python `>=3.8`, MIT license, and support for macOS, Linux, and Windows.
46
46
 
47
47
  ## Key Features
48
48
 
@@ -137,6 +137,9 @@ fdev upgrade
137
137
  # Check environment and optional tools
138
138
  fdev doctor
139
139
 
140
+ # Initialize persistent AI configuration
141
+ fdev init
142
+
140
143
  # Generate localization files
141
144
  fdev lang
142
145
 
@@ -239,10 +242,12 @@ Switching GitHub accounts runs `gh auth switch` and `gh auth setup-git`, but it
239
242
  | `fdev` | Shows command help. |
240
243
  | `fdev help` | Shows command help. |
241
244
  | `fdev doctor` | Checks platform, Python, Flutter/Dart/Git, Python packages, Android/iOS tools, optional CLIs, `.env`, Flutter project detection, and global `fdev` availability. |
245
+ | `fdev init` | Creates or repairs the persistent AI `.env` with safe provider defaults and empty API keys. |
242
246
  | `fdev upgrade` | Shows the current version, Python environment, package location, and PyPI source, then upgrades with `python -m pip install --upgrade flutter-dev` and reports the resulting version. |
243
247
  | `fdev delete` | Detects the OS, asks for confirmation, then uninstalls the active Python environment's `flutter-dev` package. Use `fdev delete --yes` to skip confirmation. |
244
248
  | `fdev ai` | Shows the current AI provider and opens an interactive provider switch prompt. |
245
249
  | `fdev ai <provider>` | Switches `DEFAULT_AI_SERVICE` to `groq`, `mistral`, `sambanova`, or `openrouter`. |
250
+ | `fdev ai <provider> <model>` | Switches provider when needed and saves a custom model ID for that provider. |
246
251
  | `fdev brew` | Interactive Homebrew package manager for viewing packages, uninstalling with cleanup, clearing cache, and global cleanup. |
247
252
  | `fdev merge` | Reads paths from `paths.txt`, merges file contents into `path_merge_files.txt`, and opens the result when there are no failures. |
248
253
  | `gemini-api` | Tests the currently selected AI provider configuration. |
@@ -266,7 +271,7 @@ GROQ_MODEL=llama-3.3-70b-versatile
266
271
 
267
272
  MISTRAL_API_KEY=your_mistral_api_key_here
268
273
  MISTRAL_API_URL=https://api.mistral.ai/v1/chat/completions
269
- MISTRAL_MODEL=mistral-medium
274
+ MISTRAL_MODEL=mistral-medium-latest
270
275
 
271
276
  SAMBANOVA_API_KEY=your_sambanova_api_key_here
272
277
  SAMBANOVA_API_URL=https://api.sambanova.ai/v1/chat/completions
@@ -283,26 +288,47 @@ The AI runtime searches for configuration in this order:
283
288
 
284
289
  1. `FLUTTER_DEV_ENV`
285
290
  2. `$XDG_CONFIG_HOME/flutter-dev/.env`
286
- 3. `~/.config/flutter-dev/.env`
287
- 4. `~/scripts/flutter-tools/.env`
288
- 5. `.env` beside the installed module/source
289
- 6. `.env` in the current working directory
291
+ 3. `%APPDATA%/flutter-dev/.env` on Windows, otherwise `~/.config/flutter-dev/.env`
292
+ 4. The legacy `~/.config/flutter-dev/.env` path on Windows
293
+ 5. `~/scripts/flutter-tools/.env`
290
294
 
291
295
  Recommended setup:
292
296
 
293
297
  ```bash
294
- mkdir -p ~/.config/flutter-dev
295
- nano ~/.config/flutter-dev/.env
298
+ fdev init
296
299
  ```
297
300
 
298
- Then test:
301
+ `fdev init` never writes real credentials. It creates all provider URL/model
302
+ defaults with empty API-key fields, preserves existing values, and restricts the
303
+ file to the current user on Unix-like systems. On macOS and Linux the default
304
+ path is `~/.config/flutter-dev/.env`.
305
+
306
+ Then select a provider and test:
299
307
 
300
308
  ```bash
301
- fdev ai
309
+ fdev ai mistral
302
310
  gemini-api
303
311
  ```
304
312
 
305
- Note: the current `fdev doctor` `.env` check is more limited than the AI runtime lookup. If `doctor` reports a missing `.env` but `gemini-api` succeeds, the AI configuration is still usable.
313
+ If the selected provider's key is missing, `fdev commit` or `gemini-api` asks
314
+ for it using hidden terminal input and saves it to the active configuration.
315
+ After a new key is entered, press Enter to use the displayed default model or
316
+ enter another model ID for the same provider. Exported environment variables
317
+ continue to take precedence over values stored in `.env`.
318
+
319
+ Pressing `Ctrl+C` during key/model setup cancels the operation without saving a
320
+ partial key or sending an AI request. If an exported variable conflicts with a
321
+ provider/model change, `fdev` reports the override and asks you to unset it
322
+ instead of claiming that the file change became active.
323
+
324
+ Change the model later without replacing the API key:
325
+
326
+ ```bash
327
+ fdev ai groq llama-3.1-8b-instant
328
+ ```
329
+
330
+ `fdev doctor`, `fdev ai`, `gemini-api`, and `fdev commit` use the same persistent
331
+ configuration lookup.
306
332
 
307
333
  ## Feature/Page Generation
308
334
 
@@ -8,7 +8,7 @@ The package is published on PyPI as `flutter-dev` and installs console commands
8
8
  python -m pip install flutter-dev
9
9
  ```
10
10
 
11
- Current package metadata declares version `0.1.9`, Python `>=3.8`, MIT license, and support for macOS, Linux, and Windows.
11
+ Current package metadata declares version `0.1.10`, Python `>=3.8`, MIT license, and support for macOS, Linux, and Windows.
12
12
 
13
13
  ## Key Features
14
14
 
@@ -103,6 +103,9 @@ fdev upgrade
103
103
  # Check environment and optional tools
104
104
  fdev doctor
105
105
 
106
+ # Initialize persistent AI configuration
107
+ fdev init
108
+
106
109
  # Generate localization files
107
110
  fdev lang
108
111
 
@@ -205,10 +208,12 @@ Switching GitHub accounts runs `gh auth switch` and `gh auth setup-git`, but it
205
208
  | `fdev` | Shows command help. |
206
209
  | `fdev help` | Shows command help. |
207
210
  | `fdev doctor` | Checks platform, Python, Flutter/Dart/Git, Python packages, Android/iOS tools, optional CLIs, `.env`, Flutter project detection, and global `fdev` availability. |
211
+ | `fdev init` | Creates or repairs the persistent AI `.env` with safe provider defaults and empty API keys. |
208
212
  | `fdev upgrade` | Shows the current version, Python environment, package location, and PyPI source, then upgrades with `python -m pip install --upgrade flutter-dev` and reports the resulting version. |
209
213
  | `fdev delete` | Detects the OS, asks for confirmation, then uninstalls the active Python environment's `flutter-dev` package. Use `fdev delete --yes` to skip confirmation. |
210
214
  | `fdev ai` | Shows the current AI provider and opens an interactive provider switch prompt. |
211
215
  | `fdev ai <provider>` | Switches `DEFAULT_AI_SERVICE` to `groq`, `mistral`, `sambanova`, or `openrouter`. |
216
+ | `fdev ai <provider> <model>` | Switches provider when needed and saves a custom model ID for that provider. |
212
217
  | `fdev brew` | Interactive Homebrew package manager for viewing packages, uninstalling with cleanup, clearing cache, and global cleanup. |
213
218
  | `fdev merge` | Reads paths from `paths.txt`, merges file contents into `path_merge_files.txt`, and opens the result when there are no failures. |
214
219
  | `gemini-api` | Tests the currently selected AI provider configuration. |
@@ -232,7 +237,7 @@ GROQ_MODEL=llama-3.3-70b-versatile
232
237
 
233
238
  MISTRAL_API_KEY=your_mistral_api_key_here
234
239
  MISTRAL_API_URL=https://api.mistral.ai/v1/chat/completions
235
- MISTRAL_MODEL=mistral-medium
240
+ MISTRAL_MODEL=mistral-medium-latest
236
241
 
237
242
  SAMBANOVA_API_KEY=your_sambanova_api_key_here
238
243
  SAMBANOVA_API_URL=https://api.sambanova.ai/v1/chat/completions
@@ -249,26 +254,47 @@ The AI runtime searches for configuration in this order:
249
254
 
250
255
  1. `FLUTTER_DEV_ENV`
251
256
  2. `$XDG_CONFIG_HOME/flutter-dev/.env`
252
- 3. `~/.config/flutter-dev/.env`
253
- 4. `~/scripts/flutter-tools/.env`
254
- 5. `.env` beside the installed module/source
255
- 6. `.env` in the current working directory
257
+ 3. `%APPDATA%/flutter-dev/.env` on Windows, otherwise `~/.config/flutter-dev/.env`
258
+ 4. The legacy `~/.config/flutter-dev/.env` path on Windows
259
+ 5. `~/scripts/flutter-tools/.env`
256
260
 
257
261
  Recommended setup:
258
262
 
259
263
  ```bash
260
- mkdir -p ~/.config/flutter-dev
261
- nano ~/.config/flutter-dev/.env
264
+ fdev init
262
265
  ```
263
266
 
264
- Then test:
267
+ `fdev init` never writes real credentials. It creates all provider URL/model
268
+ defaults with empty API-key fields, preserves existing values, and restricts the
269
+ file to the current user on Unix-like systems. On macOS and Linux the default
270
+ path is `~/.config/flutter-dev/.env`.
271
+
272
+ Then select a provider and test:
265
273
 
266
274
  ```bash
267
- fdev ai
275
+ fdev ai mistral
268
276
  gemini-api
269
277
  ```
270
278
 
271
- Note: the current `fdev doctor` `.env` check is more limited than the AI runtime lookup. If `doctor` reports a missing `.env` but `gemini-api` succeeds, the AI configuration is still usable.
279
+ If the selected provider's key is missing, `fdev commit` or `gemini-api` asks
280
+ for it using hidden terminal input and saves it to the active configuration.
281
+ After a new key is entered, press Enter to use the displayed default model or
282
+ enter another model ID for the same provider. Exported environment variables
283
+ continue to take precedence over values stored in `.env`.
284
+
285
+ Pressing `Ctrl+C` during key/model setup cancels the operation without saving a
286
+ partial key or sending an AI request. If an exported variable conflicts with a
287
+ provider/model change, `fdev` reports the override and asks you to unset it
288
+ instead of claiming that the file change became active.
289
+
290
+ Change the model later without replacing the API key:
291
+
292
+ ```bash
293
+ fdev ai groq llama-3.1-8b-instant
294
+ ```
295
+
296
+ `fdev doctor`, `fdev ai`, `gemini-api`, and `fdev commit` use the same persistent
297
+ configuration lookup.
272
298
 
273
299
  ## Feature/Page Generation
274
300
 
@@ -0,0 +1,297 @@
1
+ """Shared AI configuration paths, defaults, and persistence helpers."""
2
+
3
+ import os
4
+ import platform
5
+ import re
6
+ import tempfile
7
+ from pathlib import Path
8
+
9
+ from dotenv import dotenv_values
10
+
11
+
12
+ VALID_AI_SERVICES = ("groq", "mistral", "sambanova", "openrouter")
13
+
14
+ AI_SERVICE_DEFAULTS = {
15
+ "groq": {
16
+ "api_key_env": "GROQ_API_KEY",
17
+ "api_url_env": "GROQ_API_URL",
18
+ "model_env": "GROQ_MODEL",
19
+ "api_url": "https://api.groq.com/openai/v1/chat/completions",
20
+ "model": "llama-3.3-70b-versatile",
21
+ },
22
+ "mistral": {
23
+ "api_key_env": "MISTRAL_API_KEY",
24
+ "api_url_env": "MISTRAL_API_URL",
25
+ "model_env": "MISTRAL_MODEL",
26
+ "api_url": "https://api.mistral.ai/v1/chat/completions",
27
+ "model": "mistral-medium-latest",
28
+ },
29
+ "sambanova": {
30
+ "api_key_env": "SAMBANOVA_API_KEY",
31
+ "api_url_env": "SAMBANOVA_API_URL",
32
+ "model_env": "SAMBANOVA_MODEL",
33
+ "api_url": "https://api.sambanova.ai/v1/chat/completions",
34
+ "model": "Meta-Llama-3.3-70B-Instruct",
35
+ },
36
+ "openrouter": {
37
+ "api_key_env": "OPENROUTER_API_KEY",
38
+ "api_url_env": "OPENROUTER_API_URL",
39
+ "model_env": "OPENROUTER_MODEL",
40
+ "api_url": "https://openrouter.ai/api/v1/chat/completions",
41
+ "model": "openai/gpt-3.5-turbo",
42
+ },
43
+ }
44
+
45
+
46
+ def get_preferred_env_path():
47
+ """Return the user config path where fdev should create its .env file."""
48
+ explicit_path = os.getenv("FLUTTER_DEV_ENV")
49
+ if explicit_path:
50
+ return Path(explicit_path).expanduser()
51
+
52
+ xdg_config_home = os.getenv("XDG_CONFIG_HOME")
53
+ if xdg_config_home:
54
+ return Path(xdg_config_home).expanduser() / "flutter-dev" / ".env"
55
+
56
+ if platform.system() == "Windows" and os.getenv("APPDATA"):
57
+ return Path(os.environ["APPDATA"]).expanduser() / "flutter-dev" / ".env"
58
+
59
+ # Keep the established path on macOS and Linux for backward compatibility.
60
+ return Path.home() / ".config" / "flutter-dev" / ".env"
61
+
62
+
63
+ def get_persistent_env_candidates():
64
+ """Return user-owned config candidates without project-local fallbacks."""
65
+ preferred_path = get_preferred_env_path()
66
+ candidates = [
67
+ preferred_path,
68
+ Path.home() / ".config" / "flutter-dev" / ".env",
69
+ Path.home() / "scripts" / "flutter-tools" / ".env",
70
+ ]
71
+ unique_candidates = []
72
+ seen = set()
73
+ for candidate in candidates:
74
+ normalized = str(candidate.expanduser())
75
+ if normalized not in seen:
76
+ seen.add(normalized)
77
+ unique_candidates.append(candidate.expanduser())
78
+ return unique_candidates
79
+
80
+
81
+ def find_persistent_env_file():
82
+ """Return an existing user-owned config file, or None."""
83
+ # Explicit path controls must not silently fall back to another file.
84
+ if os.getenv("FLUTTER_DEV_ENV") or os.getenv("XDG_CONFIG_HOME"):
85
+ preferred_path = get_preferred_env_path()
86
+ return preferred_path if preferred_path.is_file() else None
87
+
88
+ for candidate in get_persistent_env_candidates():
89
+ if candidate.is_file():
90
+ return candidate
91
+ return None
92
+
93
+
94
+ def get_active_env_path():
95
+ """Return the existing runtime config, or the preferred creation path."""
96
+ return find_persistent_env_file() or get_preferred_env_path()
97
+
98
+
99
+ def build_env_template(default_service="groq"):
100
+ """Build a safe config template containing no API credentials."""
101
+ return f"""# AI API Keys Configuration
102
+ # API keys secure রাখুন এবং কারও সঙ্গে share করবেন না
103
+
104
+ # Groq API
105
+ GROQ_API_KEY=
106
+ GROQ_API_URL={AI_SERVICE_DEFAULTS['groq']['api_url']}
107
+ GROQ_MODEL={AI_SERVICE_DEFAULTS['groq']['model']}
108
+
109
+ # Mistral AI API
110
+ MISTRAL_API_KEY=
111
+ MISTRAL_API_URL={AI_SERVICE_DEFAULTS['mistral']['api_url']}
112
+ MISTRAL_MODEL={AI_SERVICE_DEFAULTS['mistral']['model']}
113
+
114
+ # SambaNova API
115
+ SAMBANOVA_API_KEY=
116
+ SAMBANOVA_API_URL={AI_SERVICE_DEFAULTS['sambanova']['api_url']}
117
+ SAMBANOVA_MODEL={AI_SERVICE_DEFAULTS['sambanova']['model']}
118
+
119
+ # OpenRouter API
120
+ OPENROUTER_API_KEY=
121
+ OPENROUTER_API_URL={AI_SERVICE_DEFAULTS['openrouter']['api_url']}
122
+ OPENROUTER_MODEL={AI_SERVICE_DEFAULTS['openrouter']['model']}
123
+
124
+ # Default AI Service (groq, mistral, sambanova, openrouter)
125
+ DEFAULT_AI_SERVICE={default_service}
126
+ """
127
+
128
+
129
+ def read_env_file(env_path):
130
+ """Read an env file with the same parser used by the AI runtime."""
131
+ env_path = Path(env_path)
132
+ if not env_path.is_file():
133
+ return {}
134
+
135
+ parsed = dotenv_values(env_path, interpolate=False, encoding="utf-8")
136
+ return {key: value or "" for key, value in parsed.items()}
137
+
138
+
139
+ def resolve_ai_config(file_values, environ=None):
140
+ """Resolve one serializable AI configuration from file and process data."""
141
+ process_values = os.environ if environ is None else environ
142
+
143
+ def effective_value(key, fallback=""):
144
+ if key in process_values and process_values[key]:
145
+ return process_values[key]
146
+ return file_values.get(key, fallback)
147
+
148
+ providers = {}
149
+ relevant_environment_keys = {"DEFAULT_AI_SERVICE"}
150
+ for service in VALID_AI_SERVICES:
151
+ defaults = AI_SERVICE_DEFAULTS[service]
152
+ relevant_environment_keys.update(
153
+ {
154
+ defaults["api_key_env"],
155
+ defaults["api_url_env"],
156
+ defaults["model_env"],
157
+ }
158
+ )
159
+ providers[service] = {
160
+ "api_key": effective_value(defaults["api_key_env"]),
161
+ "api_url": effective_value(
162
+ defaults["api_url_env"], defaults["api_url"]
163
+ ),
164
+ "model": effective_value(defaults["model_env"], defaults["model"]),
165
+ }
166
+
167
+ service = effective_value("DEFAULT_AI_SERVICE", "groq").strip().lower()
168
+ return {
169
+ "service": service,
170
+ "providers": providers,
171
+ "environment_overrides": sorted(
172
+ key
173
+ for key in relevant_environment_keys
174
+ if process_values.get(key)
175
+ ),
176
+ }
177
+
178
+
179
+ def load_resolved_ai_config(env_path, environ=None):
180
+ """Load and resolve configuration while retaining its source path."""
181
+ resolved = resolve_ai_config(read_env_file(env_path), environ=environ)
182
+ resolved["env_path"] = str(Path(env_path))
183
+ return resolved
184
+
185
+
186
+ def update_env_file_value(env_path, key, value):
187
+ """Update one env value atomically."""
188
+ update_env_file_values(env_path, {key: value})
189
+
190
+
191
+ def update_env_file_values(env_path, updates):
192
+ """Update env values in one atomic write, removing duplicate target keys."""
193
+ normalized_updates = {}
194
+ for key, value in updates.items():
195
+ value = str(value)
196
+ if "\n" in value or "\r" in value:
197
+ raise ValueError(f"Invalid newline in {key}")
198
+ normalized_updates[key] = value
199
+
200
+ env_path = Path(env_path)
201
+ env_path.parent.mkdir(parents=True, exist_ok=True)
202
+ content = env_path.read_text(encoding="utf-8") if env_path.exists() else ""
203
+ lines = content.splitlines()
204
+ output_lines = []
205
+ written_keys = set()
206
+ assignment_pattern = re.compile(
207
+ r"^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*="
208
+ )
209
+
210
+ for line in lines:
211
+ match = assignment_pattern.match(line)
212
+ key = match.group(1) if match else None
213
+ if key not in normalized_updates:
214
+ output_lines.append(line)
215
+ continue
216
+ if key in written_keys:
217
+ continue
218
+ output_lines.append(f"{key}={normalized_updates[key]}")
219
+ written_keys.add(key)
220
+
221
+ missing_keys = [
222
+ key for key in normalized_updates if key not in written_keys
223
+ ]
224
+ if missing_keys and output_lines and output_lines[-1].strip():
225
+ output_lines.append("")
226
+ output_lines.extend(
227
+ f"{key}={normalized_updates[key]}" for key in missing_keys
228
+ )
229
+
230
+ _atomic_write_env(env_path, "\n".join(output_lines) + "\n")
231
+
232
+
233
+ def ensure_env_file(env_path=None):
234
+ """Create the config and add any missing default fields without overwriting."""
235
+ env_path = Path(env_path or get_preferred_env_path()).expanduser()
236
+ env_path.parent.mkdir(parents=True, exist_ok=True)
237
+ created = not env_path.exists()
238
+
239
+ if created:
240
+ _atomic_write_env(env_path, build_env_template())
241
+ else:
242
+ existing = read_env_file(env_path)
243
+ missing_values = {}
244
+ for service in VALID_AI_SERVICES:
245
+ defaults = AI_SERVICE_DEFAULTS[service]
246
+ missing_defaults = {
247
+ defaults["api_key_env"]: "",
248
+ defaults["api_url_env"]: defaults["api_url"],
249
+ defaults["model_env"]: defaults["model"],
250
+ }
251
+ for key, value in missing_defaults.items():
252
+ if key not in existing or (value and not existing[key]):
253
+ missing_values[key] = value
254
+ if not existing.get("DEFAULT_AI_SERVICE"):
255
+ missing_values["DEFAULT_AI_SERVICE"] = "groq"
256
+ if missing_values:
257
+ update_env_file_values(env_path, missing_values)
258
+
259
+ _protect_env_file(env_path)
260
+ return env_path, created
261
+
262
+
263
+ def _protect_env_file(env_path):
264
+ """Restrict config permissions to the current user on Unix-like systems."""
265
+ if os.name != "nt":
266
+ try:
267
+ Path(env_path).chmod(0o600)
268
+ except OSError:
269
+ pass
270
+
271
+
272
+ def _atomic_write_env(env_path, content):
273
+ """Replace an env file atomically with owner-only Unix permissions."""
274
+ env_path = Path(env_path)
275
+ env_path.parent.mkdir(parents=True, exist_ok=True)
276
+ temp_path = None
277
+ try:
278
+ with tempfile.NamedTemporaryFile(
279
+ mode="w",
280
+ encoding="utf-8",
281
+ dir=env_path.parent,
282
+ prefix=f".{env_path.name}.",
283
+ delete=False,
284
+ ) as temp_file:
285
+ temp_path = Path(temp_file.name)
286
+ temp_file.write(content)
287
+ temp_file.flush()
288
+ os.fsync(temp_file.fileno())
289
+ _protect_env_file(temp_path)
290
+ os.replace(temp_path, env_path)
291
+ _protect_env_file(env_path)
292
+ finally:
293
+ if temp_path and temp_path.exists():
294
+ try:
295
+ temp_path.unlink()
296
+ except OSError:
297
+ pass
@@ -52,6 +52,7 @@ from managers.localization import (
52
52
  csv_to_arb_command,
53
53
  )
54
54
  from managers.ai import (
55
+ init_ai_config_command,
55
56
  show_ai_status,
56
57
  switch_ai_service,
57
58
  )
@@ -119,13 +120,14 @@ def show_usage(exit_code=1):
119
120
 
120
121
  print(f"\n{BLUE}Utility Commands:{NC}")
121
122
  print(" doctor Check environment health (tools, deps, config)")
123
+ print(" init Initialize persistent AI configuration")
122
124
  print(" brew Interactive Homebrew package manager")
123
125
  print(" upgrade Upgrade fdev from PyPI")
124
126
  print(" delete Uninstall fdev from this computer")
125
127
  print(" help Show this help")
126
128
  print(" merge Merge files from paths.txt into single output file")
127
129
  print(" ai Show current AI service or switch to another")
128
- print(" Usage: fdev ai [groq|mistral|sambanova|openrouter]")
130
+ print(" Usage: fdev ai [provider] [model]")
129
131
  print(" git Manage multiple GitHub accounts (gh CLI wrapper)")
130
132
  print(" Subcommands: status, list, switch, add, remove, config")
131
133
 
@@ -142,6 +144,8 @@ def show_usage(exit_code=1):
142
144
  print(f" {GREEN}fdev deploy{NC} # Deploy to deployment branch")
143
145
  print(f" {GREEN}fdev ai{NC} # Show current AI service")
144
146
  print(f" {GREEN}fdev ai groq{NC} # Switch to Groq AI")
147
+ print(f" {GREEN}fdev ai groq llama-3.1-8b-instant{NC} # Set provider + model")
148
+ print(f" {GREEN}fdev init{NC} # Initialize AI configuration")
145
149
  print(f" {GREEN}fdev git{NC} # Interactive GitHub account manager")
146
150
  print(f" {GREEN}fdev git switch royalcourtbd{NC} # Switch to a specific GH account")
147
151
  print(f" {GREEN}fdev git config{NC} # Update git user.name / user.email")
@@ -158,16 +162,16 @@ def show_usage(exit_code=1):
158
162
 
159
163
  def main():
160
164
  """Main function - Command dispatcher"""
161
- # Create required directories if they don't exist
162
- os.makedirs("build/app/outputs/flutter-apk", exist_ok=True)
163
- os.makedirs("build/app/outputs/bundle/release", exist_ok=True)
164
- os.makedirs("build/ios/ipa", exist_ok=True)
165
-
166
165
  if len(sys.argv) < 2:
167
166
  return show_usage(exit_code=0)
168
167
 
169
168
  command = sys.argv[1].lower()
170
169
 
170
+ if command in ("apk", "apk-split", "aab", "ipa", "release-run", "install"):
171
+ os.makedirs("build/app/outputs/flutter-apk", exist_ok=True)
172
+ os.makedirs("build/app/outputs/bundle/release", exist_ok=True)
173
+ os.makedirs("build/ios/ipa", exist_ok=True)
174
+
171
175
  if command in ("help", "--help", "-h"):
172
176
  return show_usage(exit_code=0)
173
177
 
@@ -266,6 +270,10 @@ def main():
266
270
  elif command == "doctor":
267
271
  run_doctor()
268
272
 
273
+ # AI configuration initialization
274
+ elif command == "init":
275
+ sys.exit(init_ai_config_command())
276
+
269
277
  # Brew command
270
278
  elif command == "brew":
271
279
  brew_manager()
@@ -281,7 +289,8 @@ def main():
281
289
  if len(sys.argv) < 3:
282
290
  show_ai_status()
283
291
  else:
284
- switch_ai_service(sys.argv[2])
292
+ model = sys.argv[3] if len(sys.argv) > 3 else None
293
+ switch_ai_service(sys.argv[2], model=model)
285
294
 
286
295
  # GitHub account manager
287
296
  elif command == "git":
@@ -14,6 +14,7 @@ requirements.txt
14
14
  setup.py
15
15
  switch_ai.py
16
16
  core/__init__.py
17
+ core/ai_config.py
17
18
  core/constants.py
18
19
  core/state.py
19
20
  managers/__init__.py
@@ -32,6 +33,7 @@ managers/mirror.py
32
33
  managers/package.py
33
34
  managers/project.py
34
35
  managers/web_deploy.py
36
+ tests/test_ai_config.py
35
37
  tests/test_fdev_cli.py
36
38
  tests/test_localization.py
37
39
  tests/test_package_commands.py