flutter-dev 0.1.11__tar.gz → 0.1.13__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 (44) hide show
  1. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/CHANGELOG.md +21 -0
  2. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/PKG-INFO +8 -6
  3. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/README.md +7 -5
  4. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/fdev.py +6 -1
  5. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/managers/app.py +37 -31
  6. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/managers/package.py +74 -10
  7. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/pyproject.toml +1 -1
  8. flutter_dev-0.1.13/tests/test_app_commands.py +161 -0
  9. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/tests/test_fdev_cli.py +20 -0
  10. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/tests/test_package_commands.py +125 -9
  11. flutter_dev-0.1.11/tests/test_app_commands.py +0 -66
  12. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/.env.example +0 -0
  13. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/LICENSE +0 -0
  14. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/MANIFEST.in +0 -0
  15. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/common_utils.py +0 -0
  16. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/core/__init__.py +0 -0
  17. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/core/ai_config.py +0 -0
  18. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/core/constants.py +0 -0
  19. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/core/state.py +0 -0
  20. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/create_page.py +0 -0
  21. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/flutter_dev.egg-info/SOURCES.txt +0 -0
  22. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/gemini_api.py +0 -0
  23. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/git_diff_output_editor.py +0 -0
  24. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/install_legacy.py +0 -0
  25. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/managers/__init__.py +0 -0
  26. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/managers/ai.py +0 -0
  27. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/managers/brew.py +0 -0
  28. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/managers/build.py +0 -0
  29. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/managers/datetime.py +0 -0
  30. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/managers/device.py +0 -0
  31. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/managers/doctor.py +0 -0
  32. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/managers/git.py +0 -0
  33. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/managers/git_account.py +0 -0
  34. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/managers/localization.py +0 -0
  35. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/managers/merge.py +0 -0
  36. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/managers/mirror.py +0 -0
  37. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/managers/project.py +0 -0
  38. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/managers/web_deploy.py +0 -0
  39. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/requirements.txt +0 -0
  40. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/setup.cfg +0 -0
  41. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/setup.py +0 -0
  42. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/switch_ai.py +0 -0
  43. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/tests/test_ai_config.py +0 -0
  44. {flutter_dev-0.1.11 → flutter_dev-0.1.13}/tests/test_localization.py +0 -0
@@ -2,6 +2,27 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## 0.1.13 - 2026-07-22
6
+
7
+ ### Added
8
+
9
+ - Added `fdev -v` and `fdev --version` for showing the installed package version.
10
+
11
+ ### Changed
12
+
13
+ - Changed `fdev upgrade` to check PyPI before invoking pip, skip installation when already current, suppress routine pip dependency output, and report verified before→after versions after a successful upgrade.
14
+ - Kept the terminal-restart reminder after an actual successful version change.
15
+
16
+ ## 0.1.12 - 2026-07-22
17
+
18
+ ### Changed
19
+
20
+ - Updated the PyPI workflow to use Node.js 24-native `actions/upload-artifact@v7`.
21
+
22
+ ### Fixed
23
+
24
+ - Fixed `fdev clear-data` to verify that the current project app is installed before clearing it, and to fall back to a confirmed foreground-app clear when the project app is missing or the direct clear fails.
25
+
5
26
  ## 0.1.11 - 2026-07-22
6
27
 
7
28
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flutter-dev
3
- Version: 0.1.11
3
+ Version: 0.1.13
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.11`, Python `>=3.8`, MIT license, and support for macOS, Linux, and Windows.
45
+ Current package metadata declares version `0.1.13`, Python `>=3.8`, MIT license, and support for macOS, Linux, and Windows.
46
46
 
47
47
  ## Key Features
48
48
 
@@ -201,7 +201,7 @@ Android APK/AAB build commands rename output files using the Android app label a
201
201
  | --- | --- |
202
202
  | `fdev install` | Installs a built APK from `build/app/outputs/flutter-apk`, prefers `arm64-v8a`, retries after uninstall on failure, then launches the app. |
203
203
  | `fdev uninstall` | In a Flutter project root, uninstalls by package/bundle id. Outside a project, detects the foreground app and asks for confirmation. |
204
- | `fdev clear-data` | In a Flutter project root, clears the project's Android package data without confirmation. Outside a project, detects the foreground app and shows a default-Yes safety prompt. iOS prints manual options. |
204
+ | `fdev clear-data` | In a Flutter project root, clears the installed project's Android package data without confirmation; if it is missing or the clear fails, falls back to the foreground app with a default-Yes safety prompt. Outside a project, always detects and confirms the foreground app. iOS prints manual options. |
205
205
  | `fdev install-scrcpy` | Installs `scrcpy` using the best supported package manager for the current platform. |
206
206
  | `fdev mirror` | Launches `scrcpy` for a selected Android device. |
207
207
  | `fdev mirror --wireless` | Guides wireless ADB setup using a USB-connected Android device. |
@@ -241,9 +241,10 @@ Switching GitHub accounts runs `gh auth switch` and `gh auth setup-git`, but it
241
241
  | --- | --- |
242
242
  | `fdev` | Shows command help. |
243
243
  | `fdev help` | Shows command help. |
244
+ | `fdev -v` / `fdev --version` | Shows the version installed in the active fdev Python environment. |
244
245
  | `fdev doctor` | Checks platform, Python, Flutter/Dart/Git, Python packages, Android/iOS tools, optional CLIs, `.env`, Flutter project detection, and global `fdev` availability. |
245
246
  | `fdev init` | Creates or repairs the persistent AI `.env` with safe provider defaults and empty API keys. |
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. |
247
+ | `fdev upgrade` | Checks the latest PyPI version first. If already current, skips pip and prints one up-to-date notice; otherwise upgrades quietly and reports the previous and installed versions. |
247
248
  | `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. |
248
249
  | `fdev ai` | Shows the current AI provider and opens an interactive provider switch prompt. |
249
250
  | `fdev ai <provider>` | Switches `DEFAULT_AI_SERVICE` to `groq`, `mistral`, `sambanova`, or `openrouter`. |
@@ -413,7 +414,7 @@ The installed Python layout is top-level modules plus the `core` and `managers`
413
414
  - `fdev tag` updates `pubspec.yaml`, creates a commit, creates a Git tag, and pushes both.
414
415
  - `fdev discard` removes tracked changes and untracked files after confirmation.
415
416
  - `fdev cleanup` applies `dart fix`, formats the project, and runs a major-version dependency upgrade.
416
- - `fdev clear-data` targets the current project's Android package without confirmation when run from a Flutter project root. Outside a project, it targets the foreground app after a default-Yes `[Y/n]` confirmation.
417
+ - `fdev clear-data` clears the installed current project's Android package without confirmation when run from a Flutter project root. If that package is missing or cannot be cleared, and whenever the command runs outside a project, it targets the foreground app after a default-Yes `[Y/n]` confirmation.
417
418
  - `fdev uninstall` may fall back to foreground app detection outside a Flutter project root.
418
419
  - `fdev upgrade` and `fdev delete` manage the local `flutter-dev` Python package, not the Flutter app on a device.
419
420
  - `fdev upgrade` reports the installed version and location before upgrading, then shows the before→after version when the active process can verify it immediately.
@@ -451,11 +452,12 @@ where fdev
451
452
  Use the built-in package management commands from the Python environment where `fdev` is installed:
452
453
 
453
454
  ```bash
455
+ fdev -v
454
456
  fdev upgrade
455
457
  fdev delete
456
458
  ```
457
459
 
458
- Both commands print the detected OS and exact Python executable before running. They use that executable with `-m pip`, not a hardcoded `python`, `python3`, `pip`, or `pip3`, so the active venv/global Python that launched `fdev` is the environment that gets upgraded or deleted. On Windows, `fdev delete` and `fdev upgrade` schedule the same `sys.executable -m pip ...` operation in a delayed command process so the currently running console launcher can exit first.
460
+ The upgrade and delete commands print the detected OS and exact Python executable. Upgrade checks PyPI first and avoids running pip when the installed version is current. When pip is needed, both commands use the active executable with `-m pip`, not a hardcoded `python`, `python3`, `pip`, or `pip3`. On Windows, `fdev delete` and a required `fdev upgrade` schedule the same `sys.executable -m pip ...` operation in a delayed command process so the currently running console launcher can exit first.
459
461
 
460
462
  ### Flutter Project Not Detected
461
463
 
@@ -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`, Python `>=3.8`, MIT license, and support for macOS, Linux, and Windows.
11
+ Current package metadata declares version `0.1.13`, Python `>=3.8`, MIT license, and support for macOS, Linux, and Windows.
12
12
 
13
13
  ## Key Features
14
14
 
@@ -167,7 +167,7 @@ Android APK/AAB build commands rename output files using the Android app label a
167
167
  | --- | --- |
168
168
  | `fdev install` | Installs a built APK from `build/app/outputs/flutter-apk`, prefers `arm64-v8a`, retries after uninstall on failure, then launches the app. |
169
169
  | `fdev uninstall` | In a Flutter project root, uninstalls by package/bundle id. Outside a project, detects the foreground app and asks for confirmation. |
170
- | `fdev clear-data` | In a Flutter project root, clears the project's Android package data without confirmation. Outside a project, detects the foreground app and shows a default-Yes safety prompt. iOS prints manual options. |
170
+ | `fdev clear-data` | In a Flutter project root, clears the installed project's Android package data without confirmation; if it is missing or the clear fails, falls back to the foreground app with a default-Yes safety prompt. Outside a project, always detects and confirms the foreground app. iOS prints manual options. |
171
171
  | `fdev install-scrcpy` | Installs `scrcpy` using the best supported package manager for the current platform. |
172
172
  | `fdev mirror` | Launches `scrcpy` for a selected Android device. |
173
173
  | `fdev mirror --wireless` | Guides wireless ADB setup using a USB-connected Android device. |
@@ -207,9 +207,10 @@ Switching GitHub accounts runs `gh auth switch` and `gh auth setup-git`, but it
207
207
  | --- | --- |
208
208
  | `fdev` | Shows command help. |
209
209
  | `fdev help` | Shows command help. |
210
+ | `fdev -v` / `fdev --version` | Shows the version installed in the active fdev Python environment. |
210
211
  | `fdev doctor` | Checks platform, Python, Flutter/Dart/Git, Python packages, Android/iOS tools, optional CLIs, `.env`, Flutter project detection, and global `fdev` availability. |
211
212
  | `fdev init` | Creates or repairs the persistent AI `.env` with safe provider defaults and empty API keys. |
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. |
213
+ | `fdev upgrade` | Checks the latest PyPI version first. If already current, skips pip and prints one up-to-date notice; otherwise upgrades quietly and reports the previous and installed versions. |
213
214
  | `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. |
214
215
  | `fdev ai` | Shows the current AI provider and opens an interactive provider switch prompt. |
215
216
  | `fdev ai <provider>` | Switches `DEFAULT_AI_SERVICE` to `groq`, `mistral`, `sambanova`, or `openrouter`. |
@@ -379,7 +380,7 @@ The installed Python layout is top-level modules plus the `core` and `managers`
379
380
  - `fdev tag` updates `pubspec.yaml`, creates a commit, creates a Git tag, and pushes both.
380
381
  - `fdev discard` removes tracked changes and untracked files after confirmation.
381
382
  - `fdev cleanup` applies `dart fix`, formats the project, and runs a major-version dependency upgrade.
382
- - `fdev clear-data` targets the current project's Android package without confirmation when run from a Flutter project root. Outside a project, it targets the foreground app after a default-Yes `[Y/n]` confirmation.
383
+ - `fdev clear-data` clears the installed current project's Android package without confirmation when run from a Flutter project root. If that package is missing or cannot be cleared, and whenever the command runs outside a project, it targets the foreground app after a default-Yes `[Y/n]` confirmation.
383
384
  - `fdev uninstall` may fall back to foreground app detection outside a Flutter project root.
384
385
  - `fdev upgrade` and `fdev delete` manage the local `flutter-dev` Python package, not the Flutter app on a device.
385
386
  - `fdev upgrade` reports the installed version and location before upgrading, then shows the before→after version when the active process can verify it immediately.
@@ -417,11 +418,12 @@ where fdev
417
418
  Use the built-in package management commands from the Python environment where `fdev` is installed:
418
419
 
419
420
  ```bash
421
+ fdev -v
420
422
  fdev upgrade
421
423
  fdev delete
422
424
  ```
423
425
 
424
- Both commands print the detected OS and exact Python executable before running. They use that executable with `-m pip`, not a hardcoded `python`, `python3`, `pip`, or `pip3`, so the active venv/global Python that launched `fdev` is the environment that gets upgraded or deleted. On Windows, `fdev delete` and `fdev upgrade` schedule the same `sys.executable -m pip ...` operation in a delayed command process so the currently running console launcher can exit first.
426
+ The upgrade and delete commands print the detected OS and exact Python executable. Upgrade checks PyPI first and avoids running pip when the installed version is current. When pip is needed, both commands use the active executable with `-m pip`, not a hardcoded `python`, `python3`, `pip`, or `pip3`. On Windows, `fdev delete` and a required `fdev upgrade` schedule the same `sys.executable -m pip ...` operation in a delayed command process so the currently running console launcher can exit first.
425
427
 
426
428
  ### Flutter Project Not Detected
427
429
 
@@ -73,6 +73,7 @@ from managers.git_account import (
73
73
  )
74
74
  from managers.package import (
75
75
  delete_fdev_command,
76
+ show_fdev_version,
76
77
  upgrade_fdev_command,
77
78
  )
78
79
 
@@ -102,7 +103,7 @@ def show_usage(exit_code=1):
102
103
  print(f"\n{BLUE}Device Commands:{NC}")
103
104
  print(" install Install built APK on connected device")
104
105
  print(" uninstall Uninstall app from connected device")
105
- print(" clear-data Clear project app data, or foreground app data outside a project")
106
+ print(" clear-data Clear project app data with confirmed foreground fallback")
106
107
  print(" install-scrcpy Install scrcpy for Android screen mirroring")
107
108
  print(" mirror Launch scrcpy screen mirror (auto-detect device)")
108
109
  print(" --wireless Setup wireless ADB connection")
@@ -124,6 +125,7 @@ def show_usage(exit_code=1):
124
125
  print(" brew Interactive Homebrew package manager")
125
126
  print(" upgrade Upgrade fdev from PyPI")
126
127
  print(" delete Uninstall fdev from this computer")
128
+ print(" -v, --version Show the installed fdev version")
127
129
  print(" help Show this help")
128
130
  print(" merge Merge files from paths.txt into single output file")
129
131
  print(" ai Show current AI service or switch to another")
@@ -167,6 +169,9 @@ def main():
167
169
 
168
170
  command = sys.argv[1].lower()
169
171
 
172
+ if command in ("-v", "--version"):
173
+ return show_fdev_version()
174
+
170
175
  if command in ("apk", "apk-split", "aab", "ipa", "release-run", "install"):
171
176
  os.makedirs("build/app/outputs/flutter-apk", exist_ok=True)
172
177
  os.makedirs("build/app/outputs/bundle/release", exist_ok=True)
@@ -188,14 +188,15 @@ def clear_app_data():
188
188
  """
189
189
  Clear app data from a connected device.
190
190
 
191
- In a Flutter project root, clear the project's Android package without a
192
- confirmation prompt. Outside a project root, detect the foreground app and
193
- require confirmation before clearing its data.
191
+ In a Flutter project root, clear the project's installed Android package
192
+ without confirmation. If it is not installed or cannot be cleared, fall
193
+ back to the foreground app with confirmation. Outside a project root,
194
+ always detect and confirm before clearing the foreground app.
194
195
  """
195
196
  print(f"{YELLOW}Clearing app data...{NC}\n")
196
197
 
197
- # Match uninstall behavior in a Flutter project: target the project's
198
- # package directly instead of whichever app happens to be in the foreground.
198
+ # Match uninstall behavior in a Flutter project: target the installed
199
+ # project package first, then fall back to the foreground app if needed.
199
200
  if is_flutter_project_root() and get_all_connected_devices():
200
201
  print(f"{GREEN}✓ Flutter project detected{NC}\n")
201
202
  print(f"{BLUE}Android device detected{NC}")
@@ -210,21 +211,40 @@ def clear_app_data():
210
211
 
211
212
  print(f"{BLUE}Package name: {package_name}{NC}\n")
212
213
 
213
- success = run_flutter_command(
214
- build_adb_cmd(["shell", "pm", "clear", package_name]),
215
- "Clearing app data... "
216
- )
214
+ if is_app_installed(package_name):
215
+ print(f"{GREEN}✓ App is installed on device{NC}\n")
216
+ success = _clear_android_package_data(package_name)
217
+ if success:
218
+ return True
217
219
 
218
- if success:
219
- print(f"\n{GREEN}✓ App data cleared successfully!{NC}")
220
- print(f"{BLUE}You can now relaunch the app from the device{NC}")
220
+ print(f"\n{YELLOW}⚠️ Could not clear data for {package_name}{NC}")
221
221
  else:
222
- print(f"\n{RED} Failed to clear app data!{NC}")
223
- print(f"{YELLOW}Make sure the app package name is correct{NC}")
222
+ print(f"{YELLOW}⚠️ Project app is not installed on device{NC}")
224
223
 
225
- return success
224
+ print(f"{YELLOW} Detecting foreground app instead...{NC}\n")
226
225
 
227
- # Get current foreground app
226
+ return _clear_foreground_app_data()
227
+
228
+
229
+ def _clear_android_package_data(package_name):
230
+ """Clear one Android package and report the command result."""
231
+ success = run_flutter_command(
232
+ build_adb_cmd(["shell", "pm", "clear", package_name]),
233
+ "Clearing app data... ",
234
+ )
235
+
236
+ if success:
237
+ print(f"\n{GREEN}✓ App data cleared successfully!{NC}")
238
+ print(f"{BLUE}You can now relaunch the app from the device{NC}")
239
+ else:
240
+ print(f"\n{RED}✗ Failed to clear app data!{NC}")
241
+ print(f"{YELLOW}Make sure the app package name is correct{NC}")
242
+
243
+ return success
244
+
245
+
246
+ def _clear_foreground_app_data():
247
+ """Detect the foreground app and clear it only after confirmation."""
228
248
  platform_type, package_name = get_current_foreground_app()
229
249
 
230
250
  if not platform_type:
@@ -247,22 +267,8 @@ def clear_app_data():
247
267
  print(f"{YELLOW}Operation cancelled{NC}")
248
268
  return False
249
269
 
250
- # Clear data based on platform
251
270
  if platform_type == "android":
252
- # Clear app data on Android
253
- success = run_flutter_command(
254
- build_adb_cmd(["shell", "pm", "clear", package_name]),
255
- f"Clearing app data... "
256
- )
257
-
258
- if success:
259
- print(f"\n{GREEN}✓ App data cleared successfully!{NC}")
260
- print(f"{BLUE}You can now relaunch the app from the device{NC}")
261
- else:
262
- print(f"\n{RED}✗ Failed to clear app data!{NC}")
263
- print(f"{YELLOW}Make sure the app package name is correct{NC}")
264
-
265
- return success
271
+ return _clear_android_package_data(package_name)
266
272
 
267
273
  elif platform_type == "ios":
268
274
  # iOS doesn't support direct app data clearing via command line
@@ -9,9 +9,12 @@ import platform
9
9
  import subprocess
10
10
  import sys
11
11
 
12
+ import requests
13
+
12
14
  from common_utils import BLUE, GREEN, RED, YELLOW, NC
13
15
 
14
16
  PACKAGE_NAME = "flutter-dev"
17
+ PYPI_JSON_URL = f"https://pypi.org/pypi/{PACKAGE_NAME}/json"
15
18
 
16
19
 
17
20
  def get_os_info():
@@ -59,6 +62,32 @@ def _print_installed_package_info(version, package_location):
59
62
  print(f"{BLUE}Package location: {GREEN}{package_location}{NC}")
60
63
 
61
64
 
65
+ def show_fdev_version():
66
+ """Print the version installed in the Python environment running fdev."""
67
+ version, _ = _get_installed_package_info()
68
+ if version == "not installed":
69
+ print(f"{RED}Could not determine the installed fdev version.{NC}")
70
+ return 1
71
+
72
+ print(f"fdev {version}")
73
+ return 0
74
+
75
+
76
+ def _get_latest_pypi_version():
77
+ """Return the latest published PyPI version, or None when unavailable."""
78
+ try:
79
+ response = requests.get(
80
+ PYPI_JSON_URL,
81
+ headers={"Accept": "application/json"},
82
+ timeout=10,
83
+ )
84
+ response.raise_for_status()
85
+ version = response.json()["info"]["version"]
86
+ return version.strip() if isinstance(version, str) and version.strip() else None
87
+ except (requests.RequestException, KeyError, TypeError, ValueError):
88
+ return None
89
+
90
+
62
91
  def _build_pip_command(pip_args):
63
92
  return [sys.executable, "-m", "pip", *pip_args]
64
93
 
@@ -68,6 +97,23 @@ def _run_pip_direct(pip_args):
68
97
  return result.returncode
69
98
 
70
99
 
100
+ def _run_pip_quiet(pip_args):
101
+ """Run pip without streaming routine dependency output to the terminal."""
102
+ result = subprocess.run(
103
+ _build_pip_command(pip_args),
104
+ capture_output=True,
105
+ text=True,
106
+ encoding="utf-8",
107
+ errors="replace",
108
+ )
109
+ if result.returncode != 0:
110
+ if result.stdout:
111
+ print(f"{GREEN}Output:\n{result.stdout.strip()}{NC}")
112
+ if result.stderr:
113
+ print(f"{RED}Error Output:\n{result.stderr.strip()}{NC}")
114
+ return result.returncode
115
+
116
+
71
117
  def _build_windows_detached_command(pip_args):
72
118
  pip_command = subprocess.list2cmdline(_build_pip_command(pip_args))
73
119
  delayed_command = f"timeout /t 2 /nobreak > nul & {pip_command}"
@@ -158,12 +204,24 @@ def upgrade_fdev_command(argv=None):
158
204
 
159
205
  pip_args = ["install", "--upgrade", PACKAGE_NAME]
160
206
  print(f"{BLUE}Upgrade source: {GREEN}PyPI{NC}")
161
- print(f"{YELLOW}Upgrading {PACKAGE_NAME} from PyPI...{NC}")
207
+ latest_version = _get_latest_pypi_version()
208
+
209
+ if latest_version == current_version:
210
+ print(f"{GREEN}✓ Already up to date: {current_version}{NC}")
211
+ return 0
212
+
213
+ if latest_version:
214
+ print(
215
+ f"{YELLOW}Upgrading {PACKAGE_NAME}: "
216
+ f"{current_version} → {latest_version}...{NC}"
217
+ )
218
+ else:
219
+ print(f"{YELLOW}Could not verify the latest PyPI version; checking with pip...{NC}")
162
220
 
163
221
  if os_key == "windows":
164
222
  return _run_windows_detached(pip_args, "Upgrade")
165
223
 
166
- result = _run_pip_direct(pip_args)
224
+ result = _run_pip_quiet(pip_args)
167
225
  if result == 0:
168
226
  installed_version, installed_location = _get_installed_package_info()
169
227
  if current_version != installed_version:
@@ -171,14 +229,20 @@ def upgrade_fdev_command(argv=None):
171
229
  f"{GREEN}✓ {PACKAGE_NAME} upgraded successfully: "
172
230
  f"{current_version} → {installed_version}.{NC}"
173
231
  )
174
- else:
232
+ if installed_location != package_location:
233
+ print(f"{BLUE}Installed at: {GREEN}{installed_location}{NC}")
234
+ print(f"{BLUE}Restart your terminal if the fdev command still uses the old version.{NC}")
235
+ return 0
236
+
237
+ if latest_version and latest_version != installed_version:
175
238
  print(
176
- f"{GREEN} {PACKAGE_NAME} upgrade completed. "
177
- f"Installed version: {installed_version}.{NC}"
239
+ f"{RED} Upgrade did not change the installed version. "
240
+ f"Installed: {installed_version}; PyPI: {latest_version}.{NC}"
178
241
  )
179
- if installed_location != package_location:
180
- print(f"{BLUE}Installed at: {GREEN}{installed_location}{NC}")
181
- print(f"{BLUE}Restart your terminal if the fdev command still uses the old version.{NC}")
182
- else:
183
- print(f"{RED}✗ Failed to upgrade {PACKAGE_NAME}.{NC}")
242
+ return 1
243
+
244
+ print(f"{GREEN} Already up to date: {installed_version}{NC}")
245
+ return 0
246
+
247
+ print(f"{RED}✗ Failed to upgrade {PACKAGE_NAME}.{NC}")
184
248
  return result
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "flutter-dev"
7
- version = "0.1.11"
7
+ version = "0.1.13"
8
8
  description = "A cross-platform CLI toolkit for Flutter development workflows."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -0,0 +1,161 @@
1
+ import unittest
2
+ from unittest.mock import patch
3
+
4
+ from managers import app
5
+
6
+
7
+ class ClearAppDataTests(unittest.TestCase):
8
+ def test_project_root_clears_project_package_without_confirmation(self):
9
+ with (
10
+ patch.object(app, "is_flutter_project_root", return_value=True),
11
+ patch.object(app, "get_all_connected_devices", return_value=["device-1"]),
12
+ patch.object(app, "ensure_device_connected", return_value=True),
13
+ patch.object(app, "get_package_name", return_value="com.example.app"),
14
+ patch.object(app, "is_app_installed", return_value=True),
15
+ patch.object(
16
+ app,
17
+ "build_adb_cmd",
18
+ return_value=["adb", "shell", "pm", "clear", "com.example.app"],
19
+ ),
20
+ patch.object(app, "run_flutter_command", return_value=True) as run_command,
21
+ patch.object(app, "get_current_foreground_app") as foreground_app,
22
+ patch("builtins.input") as user_input,
23
+ ):
24
+ result = app.clear_app_data()
25
+
26
+ self.assertTrue(result)
27
+ foreground_app.assert_not_called()
28
+ user_input.assert_not_called()
29
+ run_command.assert_called_once_with(
30
+ ["adb", "shell", "pm", "clear", "com.example.app"],
31
+ "Clearing app data... ",
32
+ )
33
+
34
+ def test_missing_project_app_falls_back_to_confirmed_foreground_app(self):
35
+ with (
36
+ patch.object(app, "is_flutter_project_root", return_value=True),
37
+ patch.object(app, "get_all_connected_devices", return_value=["device-1"]),
38
+ patch.object(app, "ensure_device_connected", return_value=True),
39
+ patch.object(app, "get_package_name", return_value="com.example.app"),
40
+ patch.object(app, "is_app_installed", return_value=False),
41
+ patch.object(
42
+ app,
43
+ "get_current_foreground_app",
44
+ return_value=("android", "com.example.foreground"),
45
+ ) as foreground_app,
46
+ patch.object(
47
+ app,
48
+ "build_adb_cmd",
49
+ return_value=[
50
+ "adb",
51
+ "shell",
52
+ "pm",
53
+ "clear",
54
+ "com.example.foreground",
55
+ ],
56
+ ),
57
+ patch.object(app, "run_flutter_command", return_value=True) as run_command,
58
+ patch("builtins.input", return_value="") as user_input,
59
+ ):
60
+ result = app.clear_app_data()
61
+
62
+ self.assertTrue(result)
63
+ foreground_app.assert_called_once_with()
64
+ user_input.assert_called_once()
65
+ run_command.assert_called_once_with(
66
+ ["adb", "shell", "pm", "clear", "com.example.foreground"],
67
+ "Clearing app data... ",
68
+ )
69
+
70
+ def test_failed_project_clear_falls_back_to_confirmed_foreground_app(self):
71
+ def adb_command(parts):
72
+ return ["adb", *parts]
73
+
74
+ with (
75
+ patch.object(app, "is_flutter_project_root", return_value=True),
76
+ patch.object(app, "get_all_connected_devices", return_value=["device-1"]),
77
+ patch.object(app, "ensure_device_connected", return_value=True),
78
+ patch.object(app, "get_package_name", return_value="com.example.app"),
79
+ patch.object(app, "is_app_installed", return_value=True),
80
+ patch.object(
81
+ app,
82
+ "get_current_foreground_app",
83
+ return_value=("android", "com.example.foreground"),
84
+ ),
85
+ patch.object(app, "build_adb_cmd", side_effect=adb_command),
86
+ patch.object(
87
+ app,
88
+ "run_flutter_command",
89
+ side_effect=[False, True],
90
+ ) as run_command,
91
+ patch("builtins.input", return_value="") as user_input,
92
+ ):
93
+ result = app.clear_app_data()
94
+
95
+ self.assertTrue(result)
96
+ user_input.assert_called_once()
97
+ self.assertEqual(run_command.call_count, 2)
98
+ self.assertEqual(
99
+ run_command.call_args_list[0].args[0],
100
+ ["adb", "shell", "pm", "clear", "com.example.app"],
101
+ )
102
+ self.assertEqual(
103
+ run_command.call_args_list[1].args[0],
104
+ ["adb", "shell", "pm", "clear", "com.example.foreground"],
105
+ )
106
+
107
+ def test_missing_project_app_foreground_fallback_can_be_cancelled(self):
108
+ with (
109
+ patch.object(app, "is_flutter_project_root", return_value=True),
110
+ patch.object(app, "get_all_connected_devices", return_value=["device-1"]),
111
+ patch.object(app, "ensure_device_connected", return_value=True),
112
+ patch.object(app, "get_package_name", return_value="com.example.app"),
113
+ patch.object(app, "is_app_installed", return_value=False),
114
+ patch.object(
115
+ app,
116
+ "get_current_foreground_app",
117
+ return_value=("android", "com.example.foreground"),
118
+ ),
119
+ patch.object(app, "run_flutter_command") as run_command,
120
+ patch("builtins.input", return_value="n") as user_input,
121
+ ):
122
+ result = app.clear_app_data()
123
+
124
+ self.assertFalse(result)
125
+ user_input.assert_called_once()
126
+ run_command.assert_not_called()
127
+
128
+ def test_outside_project_confirms_before_clearing_foreground_app(self):
129
+ with (
130
+ patch.object(app, "is_flutter_project_root", return_value=False),
131
+ patch.object(
132
+ app,
133
+ "get_current_foreground_app",
134
+ return_value=("android", "com.example.foreground"),
135
+ ),
136
+ patch.object(
137
+ app,
138
+ "build_adb_cmd",
139
+ return_value=[
140
+ "adb",
141
+ "shell",
142
+ "pm",
143
+ "clear",
144
+ "com.example.foreground",
145
+ ],
146
+ ),
147
+ patch.object(app, "run_flutter_command", return_value=True) as run_command,
148
+ patch("builtins.input", return_value="") as user_input,
149
+ ):
150
+ result = app.clear_app_data()
151
+
152
+ self.assertTrue(result)
153
+ user_input.assert_called_once()
154
+ run_command.assert_called_once_with(
155
+ ["adb", "shell", "pm", "clear", "com.example.foreground"],
156
+ "Clearing app data... ",
157
+ )
158
+
159
+
160
+ if __name__ == "__main__":
161
+ unittest.main()
@@ -26,6 +26,26 @@ class FdevCliTests(unittest.TestCase):
26
26
  self.assertEqual(result, 0)
27
27
  show_usage.assert_called_once_with(exit_code=0)
28
28
 
29
+ def test_short_version_flag_prints_installed_version(self):
30
+ with (
31
+ patch.object(sys, "argv", ["fdev", "-v"]),
32
+ patch.object(fdev, "show_fdev_version", return_value=0) as show_version,
33
+ ):
34
+ result = fdev.main()
35
+
36
+ self.assertEqual(result, 0)
37
+ show_version.assert_called_once_with()
38
+
39
+ def test_long_version_flag_prints_installed_version(self):
40
+ with (
41
+ patch.object(sys, "argv", ["fdev", "--version"]),
42
+ patch.object(fdev, "show_fdev_version", return_value=0) as show_version,
43
+ ):
44
+ result = fdev.main()
45
+
46
+ self.assertEqual(result, 0)
47
+ show_version.assert_called_once_with()
48
+
29
49
  def test_unknown_command_prints_usage_and_exits_with_error(self):
30
50
  with (
31
51
  patch.object(sys, "argv", ["fdev", "unknown"]),
@@ -2,7 +2,7 @@ import io
2
2
  import subprocess
3
3
  import unittest
4
4
  from contextlib import redirect_stdout
5
- from unittest.mock import patch
5
+ from unittest.mock import Mock, patch
6
6
 
7
7
  from managers import package
8
8
 
@@ -38,23 +38,96 @@ class PackageCommandTests(unittest.TestCase):
38
38
  run.assert_not_called()
39
39
 
40
40
  def test_upgrade_runs_pip_upgrade_in_current_python_environment(self):
41
+ with (
42
+ patch.object(package.platform, "system", return_value="Linux"),
43
+ patch.object(package.sys, "executable", "/venv/bin/python"),
44
+ patch.object(package, "_get_latest_pypi_version", return_value="0.1.13"),
45
+ patch.object(
46
+ package,
47
+ "_get_installed_package_info",
48
+ side_effect=[
49
+ ("0.1.12", "/venv/lib/python/site-packages"),
50
+ ("0.1.13", "/venv/lib/python/site-packages"),
51
+ ],
52
+ ),
53
+ patch.object(package, "_run_pip_quiet", return_value=0) as run,
54
+ ):
55
+ result = package.upgrade_fdev_command([])
56
+
57
+ self.assertEqual(result, 0)
58
+ run.assert_called_once_with(["install", "--upgrade", "flutter-dev"])
59
+
60
+ def test_upgrade_skips_pip_when_already_up_to_date(self):
61
+ output = io.StringIO()
62
+
63
+ with (
64
+ patch.object(package.platform, "system", return_value="Linux"),
65
+ patch.object(
66
+ package,
67
+ "_get_installed_package_info",
68
+ return_value=("0.1.12", "/venv/lib/python/site-packages"),
69
+ ),
70
+ patch.object(package, "_get_latest_pypi_version", return_value="0.1.12"),
71
+ patch.object(package, "_run_pip_quiet") as run,
72
+ redirect_stdout(output),
73
+ ):
74
+ result = package.upgrade_fdev_command([])
75
+
76
+ self.assertEqual(result, 0)
77
+ run.assert_not_called()
78
+ self.assertIn("✓ Already up to date: 0.1.12", output.getvalue())
79
+
80
+ def test_quiet_pip_hides_routine_success_output(self):
41
81
  completed = subprocess.CompletedProcess(
42
82
  ["/venv/bin/python", "-m", "pip", "install", "--upgrade", "flutter-dev"],
43
83
  0,
84
+ stdout="Requirement already satisfied: flutter-dev (0.1.12)\n",
85
+ stderr="",
44
86
  )
87
+ output = io.StringIO()
45
88
 
46
89
  with (
47
- patch.object(package.platform, "system", return_value="Linux"),
48
90
  patch.object(package.sys, "executable", "/venv/bin/python"),
49
91
  patch.object(package.subprocess, "run", return_value=completed) as run,
92
+ redirect_stdout(output),
50
93
  ):
51
- result = package.upgrade_fdev_command([])
94
+ result = package._run_pip_quiet(["install", "--upgrade", "flutter-dev"])
52
95
 
53
96
  self.assertEqual(result, 0)
97
+ self.assertEqual(output.getvalue(), "")
54
98
  run.assert_called_once_with(
55
- ["/venv/bin/python", "-m", "pip", "install", "--upgrade", "flutter-dev"]
99
+ ["/venv/bin/python", "-m", "pip", "install", "--upgrade", "flutter-dev"],
100
+ capture_output=True,
101
+ text=True,
102
+ encoding="utf-8",
103
+ errors="replace",
56
104
  )
57
105
 
106
+ def test_latest_pypi_version_uses_package_json_metadata(self):
107
+ response = Mock()
108
+ response.json.return_value = {"info": {"version": "0.1.13"}}
109
+
110
+ with patch.object(package.requests, "get", return_value=response) as get:
111
+ version = package._get_latest_pypi_version()
112
+
113
+ self.assertEqual(version, "0.1.13")
114
+ response.raise_for_status.assert_called_once_with()
115
+ get.assert_called_once_with(
116
+ package.PYPI_JSON_URL,
117
+ headers={"Accept": "application/json"},
118
+ timeout=10,
119
+ )
120
+
121
+ def test_latest_pypi_version_returns_none_when_lookup_fails(self):
122
+ with patch.object(
123
+ package.requests,
124
+ "get",
125
+ side_effect=package.requests.RequestException("offline"),
126
+ ):
127
+ version = package._get_latest_pypi_version()
128
+
129
+ self.assertIsNone(version)
130
+
58
131
  def test_windows_delete_schedules_detached_uninstall(self):
59
132
  with (
60
133
  patch.object(package.platform, "system", return_value="Windows"),
@@ -100,16 +173,17 @@ class PackageCommandTests(unittest.TestCase):
100
173
  )
101
174
 
102
175
  def test_upgrade_prints_target_python_environment(self):
103
- completed = subprocess.CompletedProcess(
104
- ["/home/user/project/.venv/bin/python", "-m", "pip", "install", "--upgrade", "flutter-dev"],
105
- 0,
106
- )
107
176
  output = io.StringIO()
108
177
 
109
178
  with (
110
179
  patch.object(package.platform, "system", return_value="Linux"),
111
180
  patch.object(package.sys, "executable", "/home/user/project/.venv/bin/python"),
112
- patch.object(package.subprocess, "run", return_value=completed),
181
+ patch.object(
182
+ package,
183
+ "_get_installed_package_info",
184
+ return_value=("0.1.12", "/home/user/project/.venv/lib/python/site-packages"),
185
+ ),
186
+ patch.object(package, "_get_latest_pypi_version", return_value="0.1.12"),
113
187
  redirect_stdout(output),
114
188
  ):
115
189
  result = package.upgrade_fdev_command([])
@@ -118,6 +192,48 @@ class PackageCommandTests(unittest.TestCase):
118
192
  self.assertIn("Python environment:", output.getvalue())
119
193
  self.assertIn("/home/user/project/.venv/bin/python", output.getvalue())
120
194
 
195
+ def test_successful_upgrade_prints_before_and_after_versions(self):
196
+ output = io.StringIO()
197
+
198
+ with (
199
+ patch.object(package.platform, "system", return_value="Linux"),
200
+ patch.object(
201
+ package,
202
+ "_get_installed_package_info",
203
+ side_effect=[
204
+ ("0.1.12", "/venv/lib/python/site-packages"),
205
+ ("0.1.13", "/venv/lib/python/site-packages"),
206
+ ],
207
+ ),
208
+ patch.object(package, "_get_latest_pypi_version", return_value="0.1.13"),
209
+ patch.object(package, "_run_pip_quiet", return_value=0),
210
+ redirect_stdout(output),
211
+ ):
212
+ result = package.upgrade_fdev_command([])
213
+
214
+ self.assertEqual(result, 0)
215
+ self.assertIn("0.1.12 → 0.1.13", output.getvalue())
216
+ self.assertIn(
217
+ "Restart your terminal if the fdev command still uses the old version.",
218
+ output.getvalue(),
219
+ )
220
+
221
+ def test_show_fdev_version_prints_installed_version(self):
222
+ output = io.StringIO()
223
+
224
+ with (
225
+ patch.object(
226
+ package,
227
+ "_get_installed_package_info",
228
+ return_value=("0.1.13", "/venv/lib/python/site-packages"),
229
+ ),
230
+ redirect_stdout(output),
231
+ ):
232
+ result = package.show_fdev_version()
233
+
234
+ self.assertEqual(result, 0)
235
+ self.assertEqual(output.getvalue().strip(), "fdev 0.1.13")
236
+
121
237
  def test_command_help_returns_zero(self):
122
238
  with self.assertRaises(SystemExit) as raised:
123
239
  package.delete_fdev_command(["--help"])
@@ -1,66 +0,0 @@
1
- import unittest
2
- from unittest.mock import patch
3
-
4
- from managers import app
5
-
6
-
7
- class ClearAppDataTests(unittest.TestCase):
8
- def test_project_root_clears_project_package_without_confirmation(self):
9
- with (
10
- patch.object(app, "is_flutter_project_root", return_value=True),
11
- patch.object(app, "get_all_connected_devices", return_value=["device-1"]),
12
- patch.object(app, "ensure_device_connected", return_value=True),
13
- patch.object(app, "get_package_name", return_value="com.example.app"),
14
- patch.object(
15
- app,
16
- "build_adb_cmd",
17
- return_value=["adb", "shell", "pm", "clear", "com.example.app"],
18
- ),
19
- patch.object(app, "run_flutter_command", return_value=True) as run_command,
20
- patch.object(app, "get_current_foreground_app") as foreground_app,
21
- patch("builtins.input") as user_input,
22
- ):
23
- result = app.clear_app_data()
24
-
25
- self.assertTrue(result)
26
- foreground_app.assert_not_called()
27
- user_input.assert_not_called()
28
- run_command.assert_called_once_with(
29
- ["adb", "shell", "pm", "clear", "com.example.app"],
30
- "Clearing app data... ",
31
- )
32
-
33
- def test_outside_project_confirms_before_clearing_foreground_app(self):
34
- with (
35
- patch.object(app, "is_flutter_project_root", return_value=False),
36
- patch.object(
37
- app,
38
- "get_current_foreground_app",
39
- return_value=("android", "com.example.foreground"),
40
- ),
41
- patch.object(
42
- app,
43
- "build_adb_cmd",
44
- return_value=[
45
- "adb",
46
- "shell",
47
- "pm",
48
- "clear",
49
- "com.example.foreground",
50
- ],
51
- ),
52
- patch.object(app, "run_flutter_command", return_value=True) as run_command,
53
- patch("builtins.input", return_value="") as user_input,
54
- ):
55
- result = app.clear_app_data()
56
-
57
- self.assertTrue(result)
58
- user_input.assert_called_once()
59
- run_command.assert_called_once_with(
60
- ["adb", "shell", "pm", "clear", "com.example.foreground"],
61
- "Clearing app data... ",
62
- )
63
-
64
-
65
- if __name__ == "__main__":
66
- unittest.main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes