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.
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/.env.example +8 -9
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/CHANGELOG.md +22 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/PKG-INFO +38 -12
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/README.md +37 -11
- flutter_dev-0.1.10/core/ai_config.py +297 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/fdev.py +16 -7
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/flutter_dev.egg-info/SOURCES.txt +2 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/gemini_api.py +19 -74
- flutter_dev-0.1.10/managers/ai.py +232 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/doctor.py +44 -31
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/pyproject.toml +1 -1
- flutter_dev-0.1.10/tests/test_ai_config.py +187 -0
- flutter_dev-0.1.9/managers/ai.py +0 -113
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/LICENSE +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/MANIFEST.in +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/common_utils.py +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/core/__init__.py +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/core/constants.py +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/core/state.py +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/create_page.py +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/git_diff_output_editor.py +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/install_legacy.py +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/__init__.py +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/app.py +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/brew.py +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/build.py +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/datetime.py +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/device.py +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/git.py +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/git_account.py +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/localization.py +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/merge.py +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/mirror.py +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/package.py +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/project.py +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/managers/web_deploy.py +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/requirements.txt +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/setup.cfg +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/setup.py +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/switch_ai.py +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/tests/test_fdev_cli.py +0 -0
- {flutter_dev-0.1.9 → flutter_dev-0.1.10}/tests/test_localization.py +0 -0
- {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
|
-
#
|
|
3
|
-
# এই file টি .gitignore এ add করতে হবে
|
|
2
|
+
# API keys secure রাখুন এবং কারও সঙ্গে share করবেন না
|
|
4
3
|
|
|
5
|
-
# Groq API
|
|
6
|
-
GROQ_API_KEY=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
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.
|
|
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.
|
|
288
|
-
5.
|
|
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
|
-
|
|
295
|
-
nano ~/.config/flutter-dev/.env
|
|
298
|
+
fdev init
|
|
296
299
|
```
|
|
297
300
|
|
|
298
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
254
|
-
5.
|
|
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
|
-
|
|
261
|
-
nano ~/.config/flutter-dev/.env
|
|
264
|
+
fdev init
|
|
262
265
|
```
|
|
263
266
|
|
|
264
|
-
|
|
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
|
-
|
|
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 [
|
|
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
|
-
|
|
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
|