flutter-dev 0.1.5__tar.gz → 0.1.7__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 (42) hide show
  1. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/CHANGELOG.md +22 -0
  2. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/PKG-INFO +34 -5
  3. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/README.md +33 -4
  4. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/fdev.py +14 -0
  5. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/flutter_dev.egg-info/SOURCES.txt +3 -1
  6. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/managers/gui.py +112 -26
  7. flutter_dev-0.1.7/managers/package.py +148 -0
  8. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/pyproject.toml +1 -1
  9. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/tests/test_fdev_cli.py +22 -0
  10. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/tests/test_gui.py +102 -0
  11. flutter_dev-0.1.7/tests/test_package_commands.py +129 -0
  12. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/.env.example +0 -0
  13. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/LICENSE +0 -0
  14. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/MANIFEST.in +0 -0
  15. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/common_utils.py +0 -0
  16. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/core/__init__.py +0 -0
  17. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/core/constants.py +0 -0
  18. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/core/state.py +0 -0
  19. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/create_page.py +0 -0
  20. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/gemini_api.py +0 -0
  21. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/git_diff_output_editor.py +0 -0
  22. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/install_legacy.py +0 -0
  23. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/managers/__init__.py +0 -0
  24. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/managers/ai.py +0 -0
  25. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/managers/app.py +0 -0
  26. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/managers/brew.py +0 -0
  27. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/managers/build.py +0 -0
  28. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/managers/datetime.py +0 -0
  29. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/managers/device.py +0 -0
  30. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/managers/doctor.py +0 -0
  31. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/managers/git.py +0 -0
  32. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/managers/git_account.py +0 -0
  33. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/managers/localization.py +0 -0
  34. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/managers/merge.py +0 -0
  35. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/managers/mirror.py +0 -0
  36. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/managers/project.py +0 -0
  37. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/managers/web_deploy.py +0 -0
  38. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/requirements.txt +0 -0
  39. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/setup.cfg +0 -0
  40. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/setup.py +0 -0
  41. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/switch_ai.py +0 -0
  42. {flutter_dev-0.1.5 → flutter_dev-0.1.7}/tests/test_localization.py +0 -0
@@ -2,6 +2,28 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## 0.1.7 - 2026-06-25
6
+
7
+ ### Fixed
8
+
9
+ - Reworked the Tkinter dashboard to use classic Tk widgets with explicit colors so `fdev` stays visible on macOS Xcode/system Python runtimes that can render `ttk` widgets as a blank popup.
10
+ - Silenced the macOS system Tk deprecation warning before launching the dashboard.
11
+
12
+ ## 0.1.6 - 2026-06-25
13
+
14
+ ### Added
15
+
16
+ - Added `fdev upgrade` to update the local `flutter-dev` package from PyPI after detecting the current OS.
17
+ - Added `fdev delete` to uninstall the local `flutter-dev` package after detecting the current OS and confirming the destructive action.
18
+
19
+ ### Changed
20
+
21
+ - `fdev upgrade` and `fdev delete` now print the exact Python executable they target before running `sys.executable -m pip`.
22
+
23
+ ### Fixed
24
+
25
+ - Reworked the Tkinter dashboard layout to avoid platform-sensitive paned-window sizing that could open a blank `fdev` popup after installing from PyPI.
26
+
5
27
  ## 0.1.5 - 2026-06-24
6
28
 
7
29
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flutter-dev
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: A cross-platform CLI toolkit for Flutter development workflows.
5
5
  Author: Royal Court BD
6
6
  License-Expression: MIT
@@ -39,10 +39,10 @@ Dynamic: license-file
39
39
  The package is published on PyPI as `flutter-dev` and installs console commands for day-to-day Flutter work:
40
40
 
41
41
  ```bash
42
- pip install flutter-dev
42
+ python -m pip install flutter-dev
43
43
  ```
44
44
 
45
- Current package metadata declares version `0.1.5`, Python `>=3.8`, MIT license, and support for macOS, Linux, and Windows.
45
+ Current package metadata declares version `0.1.7`, Python `>=3.8`, MIT license, and support for macOS, Linux, and Windows.
46
46
 
47
47
  ## Key Features
48
48
 
@@ -55,7 +55,7 @@ Current package metadata declares version `0.1.5`, Python `>=3.8`, MIT license,
55
55
  - Feature/page generator that creates a `lib/features/<name>` structure using the project's presenter/base-state pattern.
56
56
  - Small Tkinter desktop dashboard opened by running `fdev` with no command, with live colored command logs.
57
57
  - Environment diagnostics through `fdev doctor`.
58
- - Small utilities for Homebrew cleanup, file merging from `paths.txt`, AI provider switching, and git diff prompt export.
58
+ - Small utilities for Homebrew cleanup, file merging from `paths.txt`, AI provider switching, PyPI self-upgrade/delete, and git diff prompt export.
59
59
 
60
60
  ## Installation
61
61
 
@@ -71,7 +71,7 @@ Verify the CLI is available:
71
71
  fdev doctor
72
72
  ```
73
73
 
74
- `pip install flutter-dev` installs the Python package, Python dependencies, and console scripts. It does not install Flutter, Dart, Git, Android SDK tools, CocoaPods, Firebase CLI, GitHub CLI, `scrcpy`, Java/JDK, or VS Code CLI.
74
+ `python -m pip install flutter-dev` installs the Python package, Python dependencies, and console scripts into the Python environment selected by `python`. It does not install Flutter, Dart, Git, Android SDK tools, CocoaPods, Firebase CLI, GitHub CLI, `scrcpy`, Java/JDK, or VS Code CLI.
75
75
 
76
76
  The desktop dashboard uses Python Tkinter. Some Python distributions install this separately; for Homebrew Python, install the matching package such as `brew install python-tk@3.14`.
77
77
 
@@ -135,6 +135,9 @@ fdev
135
135
  # Show command help
136
136
  fdev help
137
137
 
138
+ # Upgrade the fdev CLI from PyPI
139
+ fdev upgrade
140
+
138
141
  # Check environment and optional tools
139
142
  fdev doctor
140
143
 
@@ -239,6 +242,8 @@ Switching GitHub accounts runs `gh auth switch` and `gh auth setup-git`, but it
239
242
  | `fdev` | Opens a small Tkinter desktop dashboard for running `fdev` commands with live colored logs and command input. The terminal process stays running while the window is open. |
240
243
  | `fdev help` | Shows command help without opening the dashboard. |
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 upgrade` | Detects the OS, then upgrades the active Python environment's `flutter-dev` package from PyPI with `python -m pip install --upgrade flutter-dev`. |
246
+ | `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. |
242
247
  | `fdev ai` | Shows the current AI provider and opens an interactive provider switch prompt. |
243
248
  | `fdev ai <provider>` | Switches `DEFAULT_AI_SERVICE` to `groq`, `mistral`, `sambanova`, or `openrouter`. |
244
249
  | `fdev brew` | Interactive Homebrew package manager for viewing packages, uninstalling with cleanup, clearing cache, and global cleanup. |
@@ -368,6 +373,7 @@ managers/mirror.py scrcpy install, mirror, wireless ADB setup
368
373
  managers/git.py commit, tag, discard, branch sync, deployment merge
369
374
  managers/git_account.py GitHub account manager around gh CLI
370
375
  managers/doctor.py Environment diagnostics
376
+ managers/package.py PyPI self-upgrade and local uninstall commands
371
377
  managers/ai.py AI provider status and switching
372
378
  managers/brew.py Interactive Homebrew manager
373
379
  managers/merge.py paths.txt to path_merge_files.txt merger
@@ -386,6 +392,8 @@ The installed Python layout is top-level modules plus the `core` and `managers`
386
392
  - `fdev cleanup` applies `dart fix`, formats the project, and runs a major-version dependency upgrade.
387
393
  - `fdev clear-data` clears app data on Android after confirmation.
388
394
  - `fdev uninstall` may fall back to foreground app detection outside a Flutter project root.
395
+ - `fdev upgrade` and `fdev delete` manage the local `flutter-dev` Python package, not the Flutter app on a device.
396
+ - `fdev delete` removes the `fdev` command from the current Python environment after confirmation; reinstall with `python -m pip install flutter-dev`.
389
397
  - `fdev` with no command opens the desktop dashboard. Use `fdev help` to print command usage.
390
398
  - Most command subcommands create build output directories at startup in the current working directory.
391
399
  - Some subcommands return failure internally without always mapping that failure to a non-zero process exit code, so automation should inspect command output carefully.
@@ -414,6 +422,27 @@ On Windows, use:
414
422
  where fdev
415
423
  ```
416
424
 
425
+ ### Upgrade Or Remove fdev
426
+
427
+ Use the built-in package management commands from the Python environment where `fdev` is installed:
428
+
429
+ ```bash
430
+ fdev upgrade
431
+ fdev delete
432
+ ```
433
+
434
+ 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.
435
+
436
+ ### Blank Dashboard On macOS
437
+
438
+ If `fdev` prints an Xcode/system Tk warning or opens a blank dashboard, upgrade to `flutter-dev` 0.1.7 or newer:
439
+
440
+ ```bash
441
+ fdev upgrade
442
+ ```
443
+
444
+ `fdev` prints the Python executable it is using. If that path is `/Applications/Xcode.app/Contents/Developer/usr/bin/python3`, the CLI is running on Xcode's bundled Python and system Tk; 0.1.7 uses a more conservative Tk UI path for that runtime.
445
+
417
446
  ### Flutter Project Not Detected
418
447
 
419
448
  Run project-specific commands from the directory that contains `pubspec.yaml`.
@@ -5,10 +5,10 @@
5
5
  The package is published on PyPI as `flutter-dev` and installs console commands for day-to-day Flutter work:
6
6
 
7
7
  ```bash
8
- pip install flutter-dev
8
+ python -m pip install flutter-dev
9
9
  ```
10
10
 
11
- Current package metadata declares version `0.1.5`, Python `>=3.8`, MIT license, and support for macOS, Linux, and Windows.
11
+ Current package metadata declares version `0.1.7`, Python `>=3.8`, MIT license, and support for macOS, Linux, and Windows.
12
12
 
13
13
  ## Key Features
14
14
 
@@ -21,7 +21,7 @@ Current package metadata declares version `0.1.5`, Python `>=3.8`, MIT license,
21
21
  - Feature/page generator that creates a `lib/features/<name>` structure using the project's presenter/base-state pattern.
22
22
  - Small Tkinter desktop dashboard opened by running `fdev` with no command, with live colored command logs.
23
23
  - Environment diagnostics through `fdev doctor`.
24
- - Small utilities for Homebrew cleanup, file merging from `paths.txt`, AI provider switching, and git diff prompt export.
24
+ - Small utilities for Homebrew cleanup, file merging from `paths.txt`, AI provider switching, PyPI self-upgrade/delete, and git diff prompt export.
25
25
 
26
26
  ## Installation
27
27
 
@@ -37,7 +37,7 @@ Verify the CLI is available:
37
37
  fdev doctor
38
38
  ```
39
39
 
40
- `pip install flutter-dev` installs the Python package, Python dependencies, and console scripts. It does not install Flutter, Dart, Git, Android SDK tools, CocoaPods, Firebase CLI, GitHub CLI, `scrcpy`, Java/JDK, or VS Code CLI.
40
+ `python -m pip install flutter-dev` installs the Python package, Python dependencies, and console scripts into the Python environment selected by `python`. It does not install Flutter, Dart, Git, Android SDK tools, CocoaPods, Firebase CLI, GitHub CLI, `scrcpy`, Java/JDK, or VS Code CLI.
41
41
 
42
42
  The desktop dashboard uses Python Tkinter. Some Python distributions install this separately; for Homebrew Python, install the matching package such as `brew install python-tk@3.14`.
43
43
 
@@ -101,6 +101,9 @@ fdev
101
101
  # Show command help
102
102
  fdev help
103
103
 
104
+ # Upgrade the fdev CLI from PyPI
105
+ fdev upgrade
106
+
104
107
  # Check environment and optional tools
105
108
  fdev doctor
106
109
 
@@ -205,6 +208,8 @@ Switching GitHub accounts runs `gh auth switch` and `gh auth setup-git`, but it
205
208
  | `fdev` | Opens a small Tkinter desktop dashboard for running `fdev` commands with live colored logs and command input. The terminal process stays running while the window is open. |
206
209
  | `fdev help` | Shows command help without opening the dashboard. |
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 upgrade` | Detects the OS, then upgrades the active Python environment's `flutter-dev` package from PyPI with `python -m pip install --upgrade flutter-dev`. |
212
+ | `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. |
208
213
  | `fdev ai` | Shows the current AI provider and opens an interactive provider switch prompt. |
209
214
  | `fdev ai <provider>` | Switches `DEFAULT_AI_SERVICE` to `groq`, `mistral`, `sambanova`, or `openrouter`. |
210
215
  | `fdev brew` | Interactive Homebrew package manager for viewing packages, uninstalling with cleanup, clearing cache, and global cleanup. |
@@ -334,6 +339,7 @@ managers/mirror.py scrcpy install, mirror, wireless ADB setup
334
339
  managers/git.py commit, tag, discard, branch sync, deployment merge
335
340
  managers/git_account.py GitHub account manager around gh CLI
336
341
  managers/doctor.py Environment diagnostics
342
+ managers/package.py PyPI self-upgrade and local uninstall commands
337
343
  managers/ai.py AI provider status and switching
338
344
  managers/brew.py Interactive Homebrew manager
339
345
  managers/merge.py paths.txt to path_merge_files.txt merger
@@ -352,6 +358,8 @@ The installed Python layout is top-level modules plus the `core` and `managers`
352
358
  - `fdev cleanup` applies `dart fix`, formats the project, and runs a major-version dependency upgrade.
353
359
  - `fdev clear-data` clears app data on Android after confirmation.
354
360
  - `fdev uninstall` may fall back to foreground app detection outside a Flutter project root.
361
+ - `fdev upgrade` and `fdev delete` manage the local `flutter-dev` Python package, not the Flutter app on a device.
362
+ - `fdev delete` removes the `fdev` command from the current Python environment after confirmation; reinstall with `python -m pip install flutter-dev`.
355
363
  - `fdev` with no command opens the desktop dashboard. Use `fdev help` to print command usage.
356
364
  - Most command subcommands create build output directories at startup in the current working directory.
357
365
  - Some subcommands return failure internally without always mapping that failure to a non-zero process exit code, so automation should inspect command output carefully.
@@ -380,6 +388,27 @@ On Windows, use:
380
388
  where fdev
381
389
  ```
382
390
 
391
+ ### Upgrade Or Remove fdev
392
+
393
+ Use the built-in package management commands from the Python environment where `fdev` is installed:
394
+
395
+ ```bash
396
+ fdev upgrade
397
+ fdev delete
398
+ ```
399
+
400
+ 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.
401
+
402
+ ### Blank Dashboard On macOS
403
+
404
+ If `fdev` prints an Xcode/system Tk warning or opens a blank dashboard, upgrade to `flutter-dev` 0.1.7 or newer:
405
+
406
+ ```bash
407
+ fdev upgrade
408
+ ```
409
+
410
+ `fdev` prints the Python executable it is using. If that path is `/Applications/Xcode.app/Contents/Developer/usr/bin/python3`, the CLI is running on Xcode's bundled Python and system Tk; 0.1.7 uses a more conservative Tk UI path for that runtime.
411
+
383
412
  ### Flutter Project Not Detected
384
413
 
385
414
  Run project-specific commands from the directory that contains `pubspec.yaml`.
@@ -73,6 +73,10 @@ from managers.git_account import (
73
73
  from managers.gui import (
74
74
  launch_gui,
75
75
  )
76
+ from managers.package import (
77
+ delete_fdev_command,
78
+ upgrade_fdev_command,
79
+ )
76
80
 
77
81
 
78
82
  def show_usage(exit_code=1):
@@ -118,6 +122,8 @@ def show_usage(exit_code=1):
118
122
  print(f"\n{BLUE}Utility Commands:{NC}")
119
123
  print(" doctor Check environment health (tools, deps, config)")
120
124
  print(" brew Interactive Homebrew package manager")
125
+ print(" upgrade Upgrade fdev from PyPI")
126
+ print(" delete Uninstall fdev from this computer")
121
127
  print(" help Show this help")
122
128
  print(" merge Merge files from paths.txt into single output file")
123
129
  print(" ai Show current AI service or switch to another")
@@ -142,6 +148,8 @@ def show_usage(exit_code=1):
142
148
  print(f" {GREEN}fdev git config{NC} # Update git user.name / user.email")
143
149
  print(f" {GREEN}fdev arb-to-csv{NC} # Export lib/l10n ARB files to translations.csv")
144
150
  print(f" {GREEN}fdev csv-to-arb{NC} # Generate lib/l10n ARB files from translations.csv")
151
+ print(f" {GREEN}fdev upgrade{NC} # Upgrade flutter-dev from PyPI")
152
+ print(f" {GREEN}fdev delete{NC} # Remove flutter-dev/fdev from this computer")
145
153
  print(f" {GREEN}fdev{NC} # Open small desktop dashboard")
146
154
 
147
155
  print(f"\n{BLUE}Note:{NC}")
@@ -259,6 +267,12 @@ def main():
259
267
  elif command == "brew":
260
268
  brew_manager()
261
269
 
270
+ # Package self-management commands
271
+ elif command == "upgrade":
272
+ sys.exit(upgrade_fdev_command(sys.argv[2:]))
273
+ elif command == "delete":
274
+ sys.exit(delete_fdev_command(sys.argv[2:]))
275
+
262
276
  # AI command
263
277
  elif command == "ai":
264
278
  if len(sys.argv) < 3:
@@ -30,8 +30,10 @@ managers/gui.py
30
30
  managers/localization.py
31
31
  managers/merge.py
32
32
  managers/mirror.py
33
+ managers/package.py
33
34
  managers/project.py
34
35
  managers/web_deploy.py
35
36
  tests/test_fdev_cli.py
36
37
  tests/test_gui.py
37
- tests/test_localization.py
38
+ tests/test_localization.py
39
+ tests/test_package_commands.py
@@ -5,6 +5,7 @@ Small Tkinter GUI for running fdev commands with live logs.
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
+ import os
8
9
  import shlex
9
10
  import subprocess
10
11
  import sys
@@ -37,6 +38,21 @@ ANSI_COLORS = {
37
38
  97: "bright_white",
38
39
  }
39
40
 
41
+ UI_BG = "#f8fafc"
42
+ SIDEBAR_BG = "#e5e7eb"
43
+ PANEL_BG = "#ffffff"
44
+ TEXT_FG = "#111827"
45
+ MUTED_FG = "#475569"
46
+ BUTTON_BG = "#e2e8f0"
47
+ BUTTON_ACTIVE_BG = "#cbd5e1"
48
+ LOG_BG = "#111827"
49
+ LOG_FG = "#e5e7eb"
50
+
51
+
52
+ def prepare_tk_environment():
53
+ """Prepare Tk for older macOS system Tk runtimes before importing tkinter."""
54
+ os.environ.setdefault("TK_SILENCE_DEPRECATION", "1")
55
+
40
56
 
41
57
  def _apply_ansi_codes(codes: List[int], current_tag: Optional[str]) -> Optional[str]:
42
58
  if not codes:
@@ -194,6 +210,8 @@ def get_gui_sections():
194
210
  "Tools",
195
211
  [
196
212
  GuiCommand("Brew", ["brew"], "Open Homebrew package manager."),
213
+ GuiCommand("Upgrade fdev", ["upgrade"], "Upgrade flutter-dev from PyPI."),
214
+ GuiCommand("Delete fdev", ["delete"], "Uninstall flutter-dev from this computer.", confirm=True),
197
215
  GuiCommand("Merge Files", ["merge"], "Merge paths listed in paths.txt."),
198
216
  GuiCommand(
199
217
  "AI Switch",
@@ -301,83 +319,127 @@ class FdevGui:
301
319
 
302
320
  def _build_widgets(self):
303
321
  import tkinter as tk
304
- from tkinter import ttk
322
+
323
+ self.root.configure(bg=UI_BG)
305
324
 
306
325
  self.root.columnconfigure(1, weight=1)
307
326
  self.root.rowconfigure(0, weight=1)
308
327
 
309
- sidebar = ttk.Frame(self.root, padding=(10, 10))
328
+ sidebar = tk.Frame(self.root, bg=SIDEBAR_BG, padx=10, pady=10)
310
329
  sidebar.grid(row=0, column=0, sticky="ns")
311
330
 
312
- title = ttk.Label(sidebar, text="fdev", font=("TkDefaultFont", 16, "bold"))
331
+ title = tk.Label(
332
+ sidebar,
333
+ text="fdev",
334
+ font=("TkDefaultFont", 16, "bold"),
335
+ bg=SIDEBAR_BG,
336
+ fg=TEXT_FG,
337
+ )
313
338
  title.grid(row=0, column=0, sticky="w", pady=(0, 10))
314
339
 
315
340
  for index, section_name in enumerate(self.sections, start=1):
316
- button = ttk.Button(
341
+ button = tk.Button(
317
342
  sidebar,
318
343
  text=section_name,
319
344
  command=lambda name=section_name: self._show_section(name),
345
+ bg=BUTTON_BG,
346
+ fg=TEXT_FG,
347
+ activebackground=BUTTON_ACTIVE_BG,
348
+ activeforeground=TEXT_FG,
349
+ relief="raised",
350
+ bd=1,
351
+ padx=10,
352
+ pady=4,
320
353
  )
321
354
  button.grid(row=index, column=0, sticky="ew", pady=3)
322
355
 
323
- content = ttk.Frame(self.root, padding=(8, 10, 10, 10))
356
+ content = tk.Frame(self.root, bg=UI_BG, padx=8, pady=10)
324
357
  content.grid(row=0, column=1, sticky="nsew")
325
358
  content.columnconfigure(0, weight=1)
326
359
  content.rowconfigure(1, weight=1)
327
360
 
328
- header = ttk.Frame(content)
361
+ header = tk.Frame(content, bg=UI_BG)
329
362
  header.grid(row=0, column=0, sticky="ew", pady=(0, 8))
330
363
  header.columnconfigure(0, weight=1)
331
364
 
332
- self.section_label = ttk.Label(header, text="", font=("TkDefaultFont", 14, "bold"))
365
+ self.section_label = tk.Label(
366
+ header,
367
+ text="",
368
+ font=("TkDefaultFont", 14, "bold"),
369
+ bg=UI_BG,
370
+ fg=TEXT_FG,
371
+ )
333
372
  self.section_label.grid(row=0, column=0, sticky="w")
334
373
 
335
- self.status_label = ttk.Label(header, text="Ready")
374
+ self.status_label = tk.Label(header, text="Ready", bg=UI_BG, fg=MUTED_FG)
336
375
  self.status_label.grid(row=0, column=1, sticky="e")
337
376
 
338
- body = ttk.PanedWindow(content, orient=tk.VERTICAL)
377
+ body = tk.Frame(content, bg=UI_BG)
339
378
  body.grid(row=1, column=0, sticky="nsew")
379
+ body.columnconfigure(0, weight=1)
380
+ body.rowconfigure(0, weight=0)
381
+ body.rowconfigure(1, weight=1)
340
382
 
341
- self.command_frame = ttk.Frame(body, padding=(0, 0, 0, 6))
383
+ self.command_frame = tk.Frame(body, bg=UI_BG, pady=6)
384
+ self.command_frame.grid(row=0, column=0, sticky="ew")
342
385
  self.command_frame.columnconfigure(0, weight=1)
343
386
  self.command_frame.columnconfigure(1, weight=1)
344
- body.add(self.command_frame, weight=2)
345
387
 
346
- log_frame = ttk.Frame(body)
388
+ log_frame = tk.Frame(body, bg=UI_BG)
389
+ log_frame.grid(row=1, column=0, sticky="nsew", pady=(6, 0))
347
390
  log_frame.columnconfigure(0, weight=1)
348
391
  log_frame.rowconfigure(0, weight=1)
349
- body.add(log_frame, weight=3)
350
392
 
351
393
  self.log_text = tk.Text(
352
394
  log_frame,
353
395
  height=12,
354
396
  wrap="word",
355
397
  state="disabled",
356
- bg="#111827",
357
- fg="#e5e7eb",
358
- insertbackground="#e5e7eb",
398
+ bg=LOG_BG,
399
+ fg=LOG_FG,
400
+ insertbackground=LOG_FG,
401
+ relief="sunken",
402
+ bd=1,
359
403
  )
360
404
  self.log_text.grid(row=0, column=0, sticky="nsew")
361
405
 
362
- scrollbar = ttk.Scrollbar(log_frame, orient="vertical", command=self.log_text.yview)
406
+ scrollbar = tk.Scrollbar(log_frame, orient="vertical", command=self.log_text.yview)
363
407
  scrollbar.grid(row=0, column=1, sticky="ns")
364
408
  self.log_text.configure(yscrollcommand=scrollbar.set)
365
409
  self._configure_log_tags()
366
410
 
367
- input_bar = ttk.Frame(content)
411
+ input_bar = tk.Frame(content, bg=UI_BG)
368
412
  input_bar.grid(row=2, column=0, sticky="ew", pady=(8, 0))
369
413
  input_bar.columnconfigure(0, weight=1)
370
414
 
371
- self.input_entry = ttk.Entry(input_bar)
415
+ self.input_entry = tk.Entry(input_bar, bg=PANEL_BG, fg=TEXT_FG, insertbackground=TEXT_FG)
372
416
  self.input_entry.grid(row=0, column=0, sticky="ew", padx=(0, 6))
373
417
  self.input_entry.bind("<Return>", lambda _event: self._send_input())
374
418
 
375
- ttk.Button(input_bar, text="Send Input", command=self._send_input).grid(row=0, column=1, padx=(0, 6))
376
- ttk.Button(input_bar, text="Stop", command=self._stop_process).grid(row=0, column=2, padx=(0, 6))
377
- ttk.Button(input_bar, text="Clear", command=self._clear_log).grid(row=0, column=3)
419
+ tk.Button(
420
+ input_bar,
421
+ text="Send Input",
422
+ command=self._send_input,
423
+ bg=BUTTON_BG,
424
+ activebackground=BUTTON_ACTIVE_BG,
425
+ ).grid(row=0, column=1, padx=(0, 6))
426
+ tk.Button(
427
+ input_bar,
428
+ text="Stop",
429
+ command=self._stop_process,
430
+ bg=BUTTON_BG,
431
+ activebackground=BUTTON_ACTIVE_BG,
432
+ ).grid(row=0, column=2, padx=(0, 6))
433
+ tk.Button(
434
+ input_bar,
435
+ text="Clear",
436
+ command=self._clear_log,
437
+ bg=BUTTON_BG,
438
+ activebackground=BUTTON_ACTIVE_BG,
439
+ ).grid(row=0, column=3)
378
440
 
379
441
  def _show_section(self, section_name: str):
380
- from tkinter import ttk
442
+ import tkinter as tk
381
443
 
382
444
  self.current_section = section_name
383
445
  self.section_label.configure(text=section_name)
@@ -389,18 +451,41 @@ class FdevGui:
389
451
  row = index // 2
390
452
  column = index % 2
391
453
 
392
- container = ttk.Frame(self.command_frame, padding=6)
454
+ container = tk.Frame(
455
+ self.command_frame,
456
+ bg=PANEL_BG,
457
+ bd=1,
458
+ relief="solid",
459
+ padx=6,
460
+ pady=6,
461
+ )
393
462
  container.grid(row=row, column=column, sticky="nsew", padx=4, pady=4)
394
463
  container.columnconfigure(0, weight=1)
395
464
 
396
- button = ttk.Button(
465
+ button = tk.Button(
397
466
  container,
398
467
  text=command.label,
399
468
  command=lambda item=command: self._run_command(item),
469
+ bg=BUTTON_BG,
470
+ fg=TEXT_FG,
471
+ activebackground=BUTTON_ACTIVE_BG,
472
+ activeforeground=TEXT_FG,
473
+ relief="raised",
474
+ bd=1,
475
+ padx=8,
476
+ pady=4,
400
477
  )
401
478
  button.grid(row=0, column=0, sticky="ew")
402
479
 
403
- description = ttk.Label(container, text=command.description, wraplength=260)
480
+ description = tk.Label(
481
+ container,
482
+ text=command.description,
483
+ wraplength=260,
484
+ bg=PANEL_BG,
485
+ fg=MUTED_FG,
486
+ justify="left",
487
+ anchor="w",
488
+ )
404
489
  description.grid(row=1, column=0, sticky="w", pady=(4, 0))
405
490
 
406
491
  def _run_command(self, command: GuiCommand):
@@ -518,6 +603,7 @@ class FdevGui:
518
603
 
519
604
  def launch_gui() -> int:
520
605
  """Launch the fdev Tkinter dashboard."""
606
+ prepare_tk_environment()
521
607
  try:
522
608
  import tkinter as tk
523
609
  except ImportError:
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Package Manager - self-management commands for the installed fdev package.
4
+ """
5
+
6
+ import argparse
7
+ import platform
8
+ import subprocess
9
+ import sys
10
+
11
+ from common_utils import BLUE, GREEN, RED, YELLOW, NC
12
+
13
+ PACKAGE_NAME = "flutter-dev"
14
+
15
+
16
+ def get_os_info():
17
+ """
18
+ Detect the operating system before package self-management actions.
19
+ Returns a tuple of (key, display_name).
20
+ """
21
+ system = platform.system()
22
+
23
+ if system == "Darwin":
24
+ return "macos", "macOS"
25
+ if system == "Linux":
26
+ return "linux", "Linux"
27
+ if system == "Windows":
28
+ return "windows", "Windows"
29
+
30
+ return "unknown", system or "Unknown"
31
+
32
+
33
+ def _print_os_info(os_name):
34
+ print(f"{BLUE}Detected OS: {GREEN}{os_name}{NC}")
35
+
36
+
37
+ def _print_python_environment():
38
+ print(f"{BLUE}Python environment: {GREEN}{sys.executable}{NC}")
39
+
40
+
41
+ def _build_pip_command(pip_args):
42
+ return [sys.executable, "-m", "pip", *pip_args]
43
+
44
+
45
+ def _run_pip_direct(pip_args):
46
+ result = subprocess.run(_build_pip_command(pip_args))
47
+ return result.returncode
48
+
49
+
50
+ def _build_windows_detached_command(pip_args):
51
+ pip_command = subprocess.list2cmdline(_build_pip_command(pip_args))
52
+ delayed_command = f"timeout /t 2 /nobreak > nul & {pip_command}"
53
+ return [
54
+ "cmd",
55
+ "/c",
56
+ "start",
57
+ "fdev package command",
58
+ "cmd",
59
+ "/c",
60
+ delayed_command,
61
+ ]
62
+
63
+
64
+ def _run_windows_detached(pip_args, action_label):
65
+ command = _build_windows_detached_command(pip_args)
66
+ creationflags = (
67
+ getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
68
+ | getattr(subprocess, "DETACHED_PROCESS", 0)
69
+ )
70
+ subprocess.Popen(command, creationflags=creationflags)
71
+ print(f"{YELLOW}{action_label} has been scheduled in a new Windows command process.{NC}")
72
+ print(f"{YELLOW}Close this terminal after the command starts, then open a new one.{NC}")
73
+ return 0
74
+
75
+
76
+ def _confirm_delete():
77
+ print(f"{RED}This will uninstall the local {PACKAGE_NAME} package and remove the fdev command.{NC}")
78
+ answer = input(f"Delete {GREEN}{PACKAGE_NAME}{NC} from this Python environment? (y/N): ")
79
+ return answer.strip().lower() == "y"
80
+
81
+
82
+ def _delete_parser():
83
+ parser = argparse.ArgumentParser(
84
+ prog="fdev delete",
85
+ description="Uninstall the local flutter-dev package that provides fdev.",
86
+ )
87
+ parser.add_argument(
88
+ "-y",
89
+ "--yes",
90
+ action="store_true",
91
+ help="Skip confirmation and uninstall flutter-dev.",
92
+ )
93
+ return parser
94
+
95
+
96
+ def _upgrade_parser():
97
+ return argparse.ArgumentParser(
98
+ prog="fdev upgrade",
99
+ description="Upgrade the local flutter-dev package from PyPI.",
100
+ )
101
+
102
+
103
+ def delete_fdev_command(argv=None):
104
+ """Uninstall the local flutter-dev package from the active Python environment."""
105
+ args = _delete_parser().parse_args(argv)
106
+ os_key, os_name = get_os_info()
107
+ _print_os_info(os_name)
108
+ _print_python_environment()
109
+
110
+ if not args.yes and not _confirm_delete():
111
+ print(f"{YELLOW}Operation cancelled.{NC}")
112
+ return 1
113
+
114
+ pip_args = ["uninstall", "-y", PACKAGE_NAME]
115
+ print(f"{YELLOW}Uninstalling {PACKAGE_NAME} from the current Python environment...{NC}")
116
+
117
+ if os_key == "windows":
118
+ return _run_windows_detached(pip_args, "Uninstall")
119
+
120
+ result = _run_pip_direct(pip_args)
121
+ if result == 0:
122
+ print(f"{GREEN}✓ {PACKAGE_NAME} uninstalled successfully.{NC}")
123
+ print(f"{BLUE}Install again with: {GREEN}python -m pip install {PACKAGE_NAME}{NC}")
124
+ else:
125
+ print(f"{RED}✗ Failed to uninstall {PACKAGE_NAME}.{NC}")
126
+ return result
127
+
128
+
129
+ def upgrade_fdev_command(argv=None):
130
+ """Upgrade the local flutter-dev package from PyPI in the active environment."""
131
+ _upgrade_parser().parse_args(argv)
132
+ os_key, os_name = get_os_info()
133
+ _print_os_info(os_name)
134
+ _print_python_environment()
135
+
136
+ pip_args = ["install", "--upgrade", PACKAGE_NAME]
137
+ print(f"{YELLOW}Upgrading {PACKAGE_NAME} from PyPI...{NC}")
138
+
139
+ if os_key == "windows":
140
+ return _run_windows_detached(pip_args, "Upgrade")
141
+
142
+ result = _run_pip_direct(pip_args)
143
+ if result == 0:
144
+ print(f"{GREEN}✓ {PACKAGE_NAME} upgraded successfully.{NC}")
145
+ print(f"{BLUE}Restart your terminal if the fdev command still uses the old version.{NC}")
146
+ else:
147
+ print(f"{RED}✗ Failed to upgrade {PACKAGE_NAME}.{NC}")
148
+ 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.5"
7
+ version = "0.1.7"
8
8
  description = "A cross-platform CLI toolkit for Flutter development workflows."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -38,6 +38,28 @@ class FdevCliTests(unittest.TestCase):
38
38
  show_usage.assert_called_once_with()
39
39
  launch_gui.assert_not_called()
40
40
 
41
+ def test_delete_command_routes_to_package_delete(self):
42
+ with (
43
+ patch.object(sys, "argv", ["fdev", "delete", "--yes"]),
44
+ patch.object(fdev, "delete_fdev_command", return_value=0) as delete_command,
45
+ ):
46
+ with self.assertRaises(SystemExit) as raised:
47
+ fdev.main()
48
+
49
+ self.assertEqual(raised.exception.code, 0)
50
+ delete_command.assert_called_once_with(["--yes"])
51
+
52
+ def test_upgrade_command_routes_to_package_upgrade(self):
53
+ with (
54
+ patch.object(sys, "argv", ["fdev", "upgrade"]),
55
+ patch.object(fdev, "upgrade_fdev_command", return_value=0) as upgrade_command,
56
+ ):
57
+ with self.assertRaises(SystemExit) as raised:
58
+ fdev.main()
59
+
60
+ self.assertEqual(raised.exception.code, 0)
61
+ upgrade_command.assert_called_once_with([])
62
+
41
63
 
42
64
  if __name__ == "__main__":
43
65
  unittest.main()
@@ -1,9 +1,13 @@
1
1
  import sys
2
2
  import unittest
3
3
  from pathlib import Path
4
+ from tkinter import TclError
5
+ from unittest.mock import patch
4
6
 
5
7
  from managers.gui import (
8
+ FdevGui,
6
9
  GuiCommand,
10
+ prepare_tk_environment,
7
11
  parse_ansi_segments,
8
12
  build_fdev_command,
9
13
  get_gui_sections,
@@ -86,6 +90,8 @@ class GuiCatalogTests(unittest.TestCase):
86
90
  ("deploy",),
87
91
  ("doctor",),
88
92
  ("brew",),
93
+ ("upgrade",),
94
+ ("delete",),
89
95
  ("merge",),
90
96
  ("ai",),
91
97
  ("ai", "groq"),
@@ -140,6 +146,102 @@ class GuiCatalogTests(unittest.TestCase):
140
146
  ["sync", "dev", "main"],
141
147
  )
142
148
 
149
+ def test_prepare_tk_environment_silences_macos_system_tk_warning(self):
150
+ with patch.dict("os.environ", {}, clear=True):
151
+ prepare_tk_environment()
152
+
153
+ import os
154
+
155
+ self.assertEqual(os.environ["TK_SILENCE_DEPRECATION"], "1")
156
+
157
+ def test_dashboard_uses_stable_grid_layout_without_paned_window(self):
158
+ try:
159
+ import tkinter as tk
160
+ except ImportError:
161
+ self.skipTest("Tkinter is not available")
162
+
163
+ try:
164
+ root = tk.Tk()
165
+ except TclError as error:
166
+ self.skipTest(f"Tk display is not available: {error}")
167
+
168
+ try:
169
+ app = FdevGui(root)
170
+ root.update_idletasks()
171
+
172
+ widget_classes = {
173
+ widget.winfo_class()
174
+ for widget in _walk_widgets(root)
175
+ }
176
+
177
+ self.assertNotIn("TPanedwindow", widget_classes)
178
+ self.assertEqual(app.section_label.cget("text"), "Dashboard")
179
+ self.assertGreaterEqual(len(app.command_frame.winfo_children()), 3)
180
+ self.assertGreater(app.command_frame.winfo_width(), 100)
181
+ self.assertGreater(app.log_text.winfo_width(), 100)
182
+ finally:
183
+ root.destroy()
184
+
185
+ def test_dashboard_maps_initial_widgets_after_mainloop_starts(self):
186
+ try:
187
+ import tkinter as tk
188
+ except ImportError:
189
+ self.skipTest("Tkinter is not available")
190
+
191
+ try:
192
+ root = tk.Tk()
193
+ except TclError as error:
194
+ self.skipTest(f"Tk display is not available: {error}")
195
+
196
+ try:
197
+ app = FdevGui(root)
198
+ root.after(10, root.quit)
199
+ app.run()
200
+
201
+ self.assertTrue(app.command_frame.winfo_ismapped())
202
+ self.assertTrue(app.log_text.winfo_ismapped())
203
+ self.assertGreater(app.command_frame.winfo_width(), 100)
204
+ self.assertGreater(app.log_text.winfo_width(), 100)
205
+ finally:
206
+ root.destroy()
207
+
208
+ def test_dashboard_uses_classic_tk_widgets_for_system_tk_compatibility(self):
209
+ try:
210
+ import tkinter as tk
211
+ except ImportError:
212
+ self.skipTest("Tkinter is not available")
213
+
214
+ try:
215
+ root = tk.Tk()
216
+ except TclError as error:
217
+ self.skipTest(f"Tk display is not available: {error}")
218
+
219
+ try:
220
+ app = FdevGui(root)
221
+ root.update_idletasks()
222
+
223
+ widget_classes = {
224
+ widget.winfo_class()
225
+ for widget in _walk_widgets(root)
226
+ }
227
+
228
+ self.assertFalse(
229
+ {"TButton", "TEntry", "TFrame", "TLabel", "TScrollbar"} & widget_classes
230
+ )
231
+ self.assertIn("Button", widget_classes)
232
+ self.assertIn("Label", widget_classes)
233
+ self.assertIn("Entry", widget_classes)
234
+ self.assertIn("Scrollbar", widget_classes)
235
+ self.assertGreater(app.command_frame.winfo_width(), 100)
236
+ finally:
237
+ root.destroy()
238
+
239
+
240
+ def _walk_widgets(widget):
241
+ yield widget
242
+ for child in widget.winfo_children():
243
+ yield from _walk_widgets(child)
244
+
143
245
 
144
246
  if __name__ == "__main__":
145
247
  unittest.main()
@@ -0,0 +1,129 @@
1
+ import io
2
+ import subprocess
3
+ import unittest
4
+ from contextlib import redirect_stdout
5
+ from unittest.mock import patch
6
+
7
+ from managers import package
8
+
9
+
10
+ class PackageCommandTests(unittest.TestCase):
11
+ def test_delete_yes_runs_pip_uninstall_in_current_python_environment(self):
12
+ completed = subprocess.CompletedProcess(
13
+ ["/venv/bin/python", "-m", "pip", "uninstall", "-y", "flutter-dev"],
14
+ 0,
15
+ )
16
+
17
+ with (
18
+ patch.object(package.platform, "system", return_value="Darwin"),
19
+ patch.object(package.sys, "executable", "/venv/bin/python"),
20
+ patch.object(package.subprocess, "run", return_value=completed) as run,
21
+ ):
22
+ result = package.delete_fdev_command(["--yes"])
23
+
24
+ self.assertEqual(result, 0)
25
+ run.assert_called_once_with(
26
+ ["/venv/bin/python", "-m", "pip", "uninstall", "-y", "flutter-dev"]
27
+ )
28
+
29
+ def test_delete_cancelled_by_user_does_not_run_pip(self):
30
+ with (
31
+ patch.object(package.platform, "system", return_value="Linux"),
32
+ patch("builtins.input", return_value="n"),
33
+ patch.object(package.subprocess, "run") as run,
34
+ ):
35
+ result = package.delete_fdev_command([])
36
+
37
+ self.assertEqual(result, 1)
38
+ run.assert_not_called()
39
+
40
+ def test_upgrade_runs_pip_upgrade_in_current_python_environment(self):
41
+ completed = subprocess.CompletedProcess(
42
+ ["/venv/bin/python", "-m", "pip", "install", "--upgrade", "flutter-dev"],
43
+ 0,
44
+ )
45
+
46
+ with (
47
+ patch.object(package.platform, "system", return_value="Linux"),
48
+ patch.object(package.sys, "executable", "/venv/bin/python"),
49
+ patch.object(package.subprocess, "run", return_value=completed) as run,
50
+ ):
51
+ result = package.upgrade_fdev_command([])
52
+
53
+ self.assertEqual(result, 0)
54
+ run.assert_called_once_with(
55
+ ["/venv/bin/python", "-m", "pip", "install", "--upgrade", "flutter-dev"]
56
+ )
57
+
58
+ def test_windows_delete_schedules_detached_uninstall(self):
59
+ with (
60
+ patch.object(package.platform, "system", return_value="Windows"),
61
+ patch.object(package.sys, "executable", r"C:\Python\python.exe"),
62
+ patch.object(package.subprocess, "Popen") as popen,
63
+ ):
64
+ result = package.delete_fdev_command(["--yes"])
65
+
66
+ self.assertEqual(result, 0)
67
+ popen.assert_called_once()
68
+ command = popen.call_args.args[0]
69
+ self.assertEqual(command[0], "cmd")
70
+ joined_command = " ".join(command)
71
+ self.assertIn("uninstall", joined_command)
72
+ self.assertIn("flutter-dev", joined_command)
73
+
74
+ def test_windows_detached_command_quotes_python_paths_with_spaces(self):
75
+ with patch.object(package.sys, "executable", r"C:\Program Files\Python312\python.exe"):
76
+ command = package._build_windows_detached_command(["install", "--upgrade", "flutter-dev"])
77
+
78
+ self.assertEqual(command[:3], ["cmd", "/c", "start"])
79
+ self.assertEqual(command[3], "fdev package command")
80
+ self.assertEqual(command[4:6], ["cmd", "/c"])
81
+ self.assertIn('"C:\\Program Files\\Python312\\python.exe"', command[6])
82
+ self.assertIn("-m pip install --upgrade flutter-dev", command[6])
83
+
84
+ def test_linux_delete_uses_running_python_not_python3_or_pip3(self):
85
+ completed = subprocess.CompletedProcess(
86
+ ["/home/user/project/.venv/bin/python", "-m", "pip", "uninstall", "-y", "flutter-dev"],
87
+ 0,
88
+ )
89
+
90
+ with (
91
+ patch.object(package.platform, "system", return_value="Linux"),
92
+ patch.object(package.sys, "executable", "/home/user/project/.venv/bin/python"),
93
+ patch.object(package.subprocess, "run", return_value=completed) as run,
94
+ ):
95
+ result = package.delete_fdev_command(["--yes"])
96
+
97
+ self.assertEqual(result, 0)
98
+ run.assert_called_once_with(
99
+ ["/home/user/project/.venv/bin/python", "-m", "pip", "uninstall", "-y", "flutter-dev"]
100
+ )
101
+
102
+ 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
+ output = io.StringIO()
108
+
109
+ with (
110
+ patch.object(package.platform, "system", return_value="Linux"),
111
+ patch.object(package.sys, "executable", "/home/user/project/.venv/bin/python"),
112
+ patch.object(package.subprocess, "run", return_value=completed),
113
+ redirect_stdout(output),
114
+ ):
115
+ result = package.upgrade_fdev_command([])
116
+
117
+ self.assertEqual(result, 0)
118
+ self.assertIn("Python environment:", output.getvalue())
119
+ self.assertIn("/home/user/project/.venv/bin/python", output.getvalue())
120
+
121
+ def test_command_help_returns_zero(self):
122
+ with self.assertRaises(SystemExit) as raised:
123
+ package.delete_fdev_command(["--help"])
124
+
125
+ self.assertEqual(raised.exception.code, 0)
126
+
127
+
128
+ if __name__ == "__main__":
129
+ 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
File without changes
File without changes
File without changes
File without changes
File without changes