Skip to content

Cli first strategy - #23

Merged
FaserF merged 15 commits into
mainfrom
cli-first-strategy
Dec 19, 2025
Merged

FaserF merged 15 commits into
mainfrom
cli-first-strategy

Conversation

@FaserF

@FaserF FaserF commented Dec 19, 2025 •

Copy link
Copy Markdown
Owner

Description

Please include a summary of the change and which issue is fixed.

Fixes # (issue)

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update

How Has This Been Tested?

Please describe the tests that you ran to verify your changes.

  • Unit Tests
  • Manual Verification (e.g. run against specific installers)

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have added internationalization (i18n) for any new user-facing strings

Summary by CodeRabbit

  • New Features

    • Standalone CLI executable, Modern GUI, legacy splash, Winget manifest creator in the GUI.
  • Bug Fixes / Improvements

    • Safer CLI-only startup, improved restart/addon UX, dynamic Winget integration, manifest creation workflow, more robust restart and build artifact handling.
  • Documentation

    • Added CLI Reference, CI Architecture, and Release Artifacts & Versions.
  • Tests

    • Dynamic CLI tests plus expanded coverage for packaging, services, and integrations.
  • Chores

    • CI: multi-target builds, build caching, separate CLI build job, artifact renaming, Windows signing scaffolding; track SPEC files in VCS.
  • Localization

    • Added English and German SFX and backup strings.

✏️ Tip: You can customize this high-level summary in your review settings.

@FaserF FaserF self-assigned this Dec 19, 2025
@coderabbitai

coderabbitai Bot commented Dec 19, 2025 •

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@FaserF has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 15 minutes and 11 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 3b65309 and 341d78e.

📒 Files selected for processing (1)
  • src/switchcraft/gui/app.py (11 hunks)

Walkthrough

Adds a CLI-first dispatcher and CLI bootstrap, a modern Flet GUI and legacy Tk splash, a full CLI implementation, a Winget manifest service and separate winget package, PyInstaller spec additions, CI/release workflow updates, numerous GUI/service API signature changes, new tests and documentation.

Changes

Cohort / File(s) Change Summary
CI workflows
\.github/workflows/release.yml, \.github/workflows/test.yml
Per-OS artifact renames; PyInstaller build caching; ensure pyinstaller on Windows; new Windows CLI build/upload job; added test_cli_core job for CLI tests.
Build scripts & installer
scripts/build_release.ps1, scripts/build_release.sh, switchcraft.iss
New multi-mode PowerShell release builder (Modern/Legacy/CLI/Pip/Installer), artifact mapping and signing scaffolding; shell script expects dist/SwitchCraft-windows.exe; installer updated to SwitchCraft-windows.exe.
PyInstaller specs & VCS ignores
switchcraft.spec, switchcraft_cli.spec, switchcraft_legacy.spec, switchcraft_modern.spec, .gitignore
Added CLI/legacy/modern specs, hardened hidden-import discovery, changed exe names, adjusted excludes, and tracked previously-ignored spec files.
Entrypoints & dispatch
src/entry.py, src/switchcraft/main.py, src/switchcraft/cli_main.py, src/switchcraft/modern_main.py
New main() dispatcher routing CLI vs GUI; CLI-only bootstrap to avoid GUI imports; modern Flet launcher module; entry now invokes main().
CLI implementation & tests
src/switchcraft/cli/commands.py, tests/test_cli_dynamic.py, tests/test_full_coverage.py
New Click+Rich CLI (analyze, config, winget, intune, addons), JSON output and secret handling; dynamic CLI tests and expanded integration/unit tests.
Modern & legacy GUI
src/switchcraft/gui_modern/app.py, src/switchcraft/gui/splash.py, src/switchcraft/gui/app.py
Added Flet ModernApp and Tkinter LegacySplash; GUI main signature now main(splash=None), restart handling hardened, background cloud-backup and init error checks added.
Analyzer UI & Winget UX
src/switchcraft/gui/views/analyzer_view.py, src/switchcraft/gui/views/manifest_dialog.py, src/switchcraft/gui/views/winget_view.py
Dynamic Winget addon loading, "Create Winget Manifest" button and ManifestDialog (background SHA256, generation/validation), PE/SFX notices, and winget_helper API usage for installs.
Winget manifest & helper package
src/switchcraft/services/winget_manifest_service.py, src/switchcraft_winget/utils/winget.py, src/switchcraft_winget/utils/__init__.py
Added WingetManifestService (generate/validate manifests) and new switchcraft_winget package with WingetHelper (search, download, install, centralized subprocess handling).
Addon & core services
src/switchcraft/services/addon_service.py, src/switchcraft/services/history_service.py, src/switchcraft/services/notification_service.py, src/switchcraft/services/backup_service.py, src/switchcraft/services/sync_service.py
Improved addon path resolution and ZIP extraction (zip‑slip protection, normalization), install_from_zip behavior, explicit JSON decode handling in history, removed foreground gating in notifications, added get_backup_metadata.
Utils & legacy removals
src/switchcraft/utils/__init__.py, src/switchcraft/utils/i18n.py, src/switchcraft_advanced/utils/winget.py, src/switchcraft_debug/console.py
Removed WingetHelper export from utils, deleted legacy advanced winget module, and removed unused imports.
GUI view signatures & UX
src/switchcraft/gui/views/missing_addon_view.py, src/switchcraft/gui/views/settings_view.py
Constructors updated to accept app instance; post-install/login flows now route to centralized restart countdown when available.
Project metadata & deps
pyproject.toml
License string inline, GUI deps moved to optional groups (gui, modern), console entry switched to main, and pytest config added.
Localization & docs
src/switchcraft/assets/lang/en.json, src/switchcraft/assets/lang/de.json, README.md, docs/CLI_Reference.md, docs/CI_Architecture.md
Added cloud backup and SFX localization keys; CLI Reference and CI Architecture docs added; README updated with artifacts/versions and CI links.
Tests & coverage
tests/*
New and updated tests for CLI, Winget, manifest service, addon zip extraction, registry shims, and entry-point safety.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Entrypoint as main()
    participant CLI as switchcraft.cli.commands
    participant GUI as switchcraft.gui.app / gui_modern
    participant Addon as AddonService
    participant Winget as switchcraft_winget.WingetHelper
    participant Manifest as WingetManifestService

    User->>Entrypoint: run (with args / without args)
    Entrypoint->>Entrypoint: decide CLI vs GUI
    alt CLI mode (args)
        Entrypoint->>CLI: import & invoke CLI
        CLI->>Addon: import_addon_module("winget")
        Addon-->>CLI: module or error
        CLI->>Winget: search/install/download
        CLI->>Manifest: generate/validate manifest (optional)
        Manifest-->>CLI: path / validation result
        CLI-->>User: JSON or formatted output
    else GUI mode (no args)
        Entrypoint->>GUI: start legacy or modern UI
        GUI->>Addon: lazy-load addons on demand
        GUI->>Winget: trigger install/download or manifest generation
        Winget-->>GUI: install/download result
        Manifest-->>GUI: manifest generation/validation result
        GUI-->>User: interactive dialogs / restart flow
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Areas needing focused review:

  • src/switchcraft/cli/commands.py — many commands, secrets handling, external integrations and output modes.
  • src/switchcraft/services/winget_manifest_service.py & src/switchcraft_winget/utils/winget.py — manifest schema, filesystem layout, subprocess calls and parsing.
  • scripts/build_release.ps1 and .github/workflows/release.yml — multi-mode orchestration, caching, artifact naming and upload.
  • PyInstaller spec files (*.spec) and switchcraft.iss — hiddenimports, datas, exe naming, and packaging.
  • GUI wiring: src/switchcraft/gui/views/manifest_dialog.py, analyzer_view.py, and gui/app.py — background threads (SHA256), modal flows, startup/signature changes, and restart/countdown integration.

Possibly related PRs

Poem

🐰 I nibbled code beneath the silver moon,
A CLI hopped out while the GUI tuned,
Manifests stitched tidy, builds set to run,
Artifacts aligned — binaries for everyone,
Hop, SwitchCraft, hop; the release smells like fun! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 51.91% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The PR title 'Cli first strategy' is vague and does not clearly describe the main changes in the changeset. While the PR does introduce significant CLI functionality, the title lacks specificity about what the strategy entails or what was actually changed. Revise the title to be more specific and descriptive, such as 'Add modular CLI with commands for analyze, config, winget, intune, and addon management' or 'Refactor entry point to support CLI-first architecture with separate GUI variants'. This will better communicate the primary changes to reviewers scanning the history.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added documentation Improvements or additions to documentation ci-cd backend labels Dec 19, 2025

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/switchcraft/services/notification_service.py (1)

45-61: Remove the unused _is_app_foreground method (lines 45-61).

This method is never called in the codebase. The only reference was commented out and the method serves no purpose. Removing it will reduce maintenance overhead.

src/switchcraft/gui/views/winget_view.py (1)

188-188: Remove unused variable.

The cmd variable is assigned but never used after the refactoring to winget_helper.install_package(). This is flagged by the pipeline linter.

🔎 Proposed fix
     else:
         scope = "user"
 
-    cmd = f"winget install --id {info['Id']} --scope {scope} --accept-package-agreements --accept-source-agreements"
-
     top = ctk.CTkToplevel(self)
scripts/build_release.sh (1)

46-55: Misleading artifact naming for Unix builds.

The spec file hardcodes name='SwitchCraft-windows' regardless of platform, and the Unix build script correctly looks for this name. However, the "-windows" suffix is confusing for Linux/macOS builds, where the artifact will be a Unix binary without the .exe extension. The naming contradicts the documented purpose of the script ("SwitchCraft Release Builder (Unix)") and the script's own comment acknowledging that "Default is 'SwitchCraft' on Unix."

Consider making the spec file platform-aware or renaming the artifact to something like 'SwitchCraft-legacy' to avoid confusion about what platform the binary targets.

src/entry.py (1)

10-12: Duplicate comment block - copy-paste error.

Lines 11-12 are exact duplicates of lines 8-9. Remove the duplicate.

🔎 Suggested fix
     # 1. We import 'switchcraft.gui.app' here to force PyInstaller to bundle it
     #    (Static Analysis sees this import).
     # 2. We import it at runtime to populate sys.modules, bypassing any
     #    shadowing 'switchcraft' folder that might exist in _MEIPASS.
-    #    (Static Analysis sees this import).
-    # 2. We import it at runtime to populate sys.modules, bypassing any
-    #    shadowing 'switchcraft' folder that might exist in _MEIPASS.
🟡 Minor comments (14)
docs/CI_Architecture.md-8-8 (1)

8-8: Fix the operating system name capitalization.

Apple's operating system should be written as "macOS" not "MacOS".

🔎 Proposed fix
-  - **Standard Build**: Windows (GUI+CLI), Linux, MacOS.
+  - **Standard Build**: Windows (GUI+CLI), Linux, macOS.
switchcraft_modern.spec-49-49 (1)

49-49: Update the executable name from test placeholder.

The executable name SwitchCraft-new-Test appears to be a temporary/test name. Consider using a production-ready name like SwitchCraft-modern or SwitchCraft-Modern-windows to align with the other build variants.

🔎 Proposed fix
-    name='SwitchCraft-new-Test', # Modern (Flet) test build
+    name='SwitchCraft-Modern', # Modern (Flet) GUI build
switchcraft_modern.spec-61-61 (1)

61-61: Convert the icon file from PNG to ICO format.

PyInstaller on Windows requires .ico format for application icons. The .png file specified will not work as an executable icon on Windows. Convert images/switchcraft_logo.png to .ico format before building.

src/switchcraft/gui_modern/app.py-122-124 (1)

122-124: Address the unused variable flagged by the linter.

The variable app is assigned but never used, as flagged by the CI pipeline. Determine if:

  1. The reference must be retained to prevent garbage collection of the ModernApp instance, or
  2. The variable can be removed since ModernApp.__init__ modifies the page in place

If the reference is necessary, add a comment explaining why. Otherwise, remove the assignment or use _ = to indicate intentional discard.

🔎 Potential fixes

If the reference is needed:

     # Initialize the app
     # ModernApp.__init__ calls page.clean()
-    app = ModernApp(page)
+    app = ModernApp(page)  # Keep reference to prevent garbage collection

If the reference is not needed:

     # Initialize the app
     # ModernApp.__init__ calls page.clean()
-    app = ModernApp(page)
+    ModernApp(page)
src/switchcraft/gui_modern/app.py-117-120 (1)

117-120: Replace time.sleep() with asyncio.sleep() for async-compatible delays.

In async Flet apps, asyncio.sleep() should be used instead of time.sleep(). In Flet 1.0+, blocking calls like time.sleep() freeze the UI and should be replaced with await asyncio.sleep().

README.md-16-17 (1)

16-17: Fix duplicate heading.

Line 17 contains a duplicate "## 📚 Documentation" heading. This appears to be a copy-paste error and will cause rendering issues.

🔎 Proposed fix
-## 📚 Documentation
 ## 📚 Documentation
src/switchcraft/gui/splash.py-3-5 (1)

3-5: Remove unused imports.

As flagged by the pipeline, sys, os, and Path are imported but never used.

🔎 Proposed fix
 import tkinter as tk
 from tkinter import ttk
-import sys
-import os
-from pathlib import Path

Committable suggestion skipped: line range outside the PR's diff.

src/switchcraft/gui/views/analyzer_view.py-969-979 (1)

969-979: Remove unused messagebox import and dead code.

Pipeline failure indicates the messagebox import on line 973 is unused. The entire conditional block (lines 971-975) appears to be a no-op placeholder.

🔎 Proposed fix
     def _open_manifest_dialog(self, info):
-        # Check Config
-        repo_path = SwitchCraftConfig.get_value("WingetRepoPath")
-        if not repo_path:
-             from tkinter import messagebox
-             # Optional: Ask user if they want to configure it, or just rely on default logic in Service
-             pass
-
         from switchcraft.gui.views.manifest_dialog import ManifestDialog
         dlg = ManifestDialog(self.winfo_toplevel(), info)
         dlg.grab_set()
src/switchcraft/gui/views/manifest_dialog.py-4-4 (1)

4-4: Remove unused import webbrowser.

Pipeline failure indicates this import is unused.

🔎 Proposed fix
-import webbrowser
scripts/build_release.ps1-11-12 (1)

11-12: Documentation mismatch: -Gui parameter documented but code uses -Modern and -Legacy.

The .PARAMETER Gui documentation doesn't match the actual parameter names in the param block (lines 37-44).

🔎 Proposed fix for documentation
-.PARAMETER Gui
-    Builds the Standard GUI application (SwitchCraft.exe). Default if no other flags used.
+.PARAMETER Modern
+    Builds the Modern GUI application using Flet (SwitchCraft-new-Test.exe). Default if no other flags used.
+
+.PARAMETER Legacy
+    Builds the Legacy GUI application using Tkinter (SwitchCraft-windows.exe).

Also update the examples:

 .EXAMPLE
-    .\build_release.ps1 -Gui -Cli
+    .\build_release.ps1 -Modern -Cli
     Builds both versions.
src/switchcraft_winget/utils/winget.py-3-3 (1)

3-3: Remove unused import shutil.

Pipeline failure indicates this import is unused.

🔎 Proposed fix
-import shutil
src/switchcraft/gui/views/manifest_dialog.py-8-8 (1)

8-8: Remove unused import SwitchCraftConfig.

Pipeline failure indicates this import is unused.

🔎 Proposed fix
-from switchcraft.utils.config import SwitchCraftConfig
src/switchcraft/services/winget_manifest_service.py-1-7 (1)

1-7: Remove unused imports flagged by pipeline.

Pipeline failures indicate os, shutil, and datetime are imported but never used.

🔎 Proposed fix
-import os
 import yaml
 import logging
 import subprocess
-import shutil
 from pathlib import Path
-from datetime import datetime
 from switchcraft.utils.config import SwitchCraftConfig
src/switchcraft/cli/commands.py-178-185 (1)

178-185: Remove or use the unused output_log variable.

The variable is assigned but never used, causing the pipeline failure (F841).

Proposed fix
     try:
-        output_log = svc.create_intunewin(
+        svc.create_intunewin(
             source_folder=source,
             setup_file=setup_file,
             output_folder=output,
             quiet=quiet,
             progress_callback=lambda x: print(x.strip()) if not quiet else None
         )
         print("[green]Package created successfully![/green]")

Or if you want to log the output in verbose mode:

     try:
         output_log = svc.create_intunewin(
             source_folder=source,
             setup_file=setup_file,
             output_folder=output,
             quiet=quiet,
             progress_callback=lambda x: print(x.strip()) if not quiet else None
         )
+        if not quiet and output_log:
+            logger.debug(f"Tool output: {output_log}")
         print("[green]Package created successfully![/green]")
🧹 Nitpick comments (17)
src/switchcraft/services/notification_service.py (1)

79-83: Update the docstring to reflect current behavior.

The docstring mentions conditional behavior based on foreground state ("If app is in background... If app is in foreground..."), but the code now always sends system notifications regardless of foreground state. Update the docstring to accurately describe the current implementation.

🔎 Proposed docstring update
     @staticmethod
     def send_notification(title: str, message: str, timeout: int = 10):
         """
         Sends a desktop notification.
-        If app is in background, uses system notification center.
-        If app is in foreground, notification still shows but app stays visible.
+        Always sends system notifications through the notification center.
+        On Windows, uses winotify for toast notifications with click support,
+        falling back to plyer or PowerShell if unavailable.
         """
pyproject.toml (1)

17-31: Consider pinning dependency versions for stability and security.

All dependencies are unpinned, which can lead to:

  • Unexpected breaking changes from major version updates
  • Potential security vulnerabilities
  • Non-reproducible builds

Consider adding version constraints (at minimum, upper bounds) for production stability, especially for security-sensitive packages like requests, PyJWT, openai, and google-generativeai.

Example approach:

dependencies = [
    "requests>=2.31.0,<3.0",
    "click>=8.1.0,<9.0",
    "PyJWT>=2.8.0,<3.0",
    # ... etc
]
src/switchcraft/gui_modern/app.py (1)

1-3: Consider using the imported i18n module for UI strings.

The i18n module is imported but never used. All UI strings are hardcoded in English (e.g., "Home", "Analyzer", "Settings", etc.). Consider either:

  1. Using i18n to internationalize the UI strings, or
  2. Removing the unused import if internationalization is planned for a later phase
docs/CLI_Reference.md (1)

46-46: Consider fixing list indentation for consistency.

The nested list item uses 4 spaces instead of the Markdown standard 2 spaces, flagged by markdownlint.

🔎 Proposed fix
 *   `switchcraft intune package <setup_file> -o <out_folder> -s <source_folder>`
 *   `switchcraft intune upload <intunewin> --name "App Name" --publisher "Pub" ...`
-    *   Requires `IntuneTenantId`, `IntuneClientId`, `IntuneClientSecret` in config.
+  *   Requires `IntuneTenantId`, `IntuneClientId`, `IntuneClientSecret` in config.
.github/workflows/test.yml (1)

30-30: Consider declaring PYTHONPATH separately.

Shellcheck flags the combined export PYTHONPATH=$PYTHONPATH:$(pwd)/src pattern as it can mask return values from the command substitution.

🔎 Proposed fix
-           export PYTHONPATH=$PYTHONPATH:$(pwd)/src
+           REPO_SRC="$(pwd)/src"
+           export PYTHONPATH="$PYTHONPATH:$REPO_SRC"
src/switchcraft/main.py (1)

27-28: Consider enhancing the GUI error message.

The error message for missing GUI dependencies could guide users toward installing GUI extras or using the CLI.

🔎 Proposed enhancement
         except ImportError as e:
-             print(f"GUI dependencies not found. Error: {e}")
+             print(f"GUI dependencies not found. Error: {e}")
+             print("Hint: Install GUI support with 'pip install switchcraft[gui]' or use CLI mode.")
              sys.exit(1)
switchcraft_legacy.spec (1)

31-42: Minor style issues in module enumeration logic.

  1. Line 33: Prefer file != '__init__.py' over not file == '__init__.py' for readability.
  2. Line 39: Inconsistent indentation (extra space before full_path).
🔎 Suggested style improvements
 for root, dirs, files in os.walk(pkg_path):
     for file in files:
-        if file.endswith('.py') and not file == '__init__.py':
+        if file.endswith('.py') and file != '__init__.py':
             full_path = os.path.join(root, file)
             rel_path = os.path.relpath(full_path, src_root)
             module_name = rel_path.replace(os.sep, '.').replace('.py', '')
             hidden_imports.append(module_name)
         elif file == '__init__.py':
-             full_path = os.path.join(root, file)
-             rel_path = os.path.relpath(root, src_root)
-             module_name = rel_path.replace(os.sep, '.')
-             hidden_imports.append(module_name)
+            full_path = os.path.join(root, file)
+            rel_path = os.path.relpath(root, src_root)
+            module_name = rel_path.replace(os.sep, '.')
+            hidden_imports.append(module_name)
src/switchcraft/cli_main.py (1)

1-1: Remove leading blank line.

PEP 8 recommends no blank lines at the start of a file.

tests/test_cli_dynamic.py (2)

6-12: Unused ctx parameter in get_all_commands.

The ctx parameter is passed but never used in the function body. Consider removing it.

🔎 Suggested fix
-def get_all_commands(cli_obj, ctx):
+def get_all_commands(cli_obj):
     """Recursively yield all command paths and command objects."""
     yield [], cli_obj
     if isinstance(cli_obj, click.Group):
         for name, cmd in cli_obj.commands.items():
-            for sub_path, sub_cmd in get_all_commands(cmd, ctx):
+            for sub_path, sub_cmd in get_all_commands(cmd):
                 yield [name] + sub_path, sub_cmd

And update the call site at line 30:

-            commands = list(get_all_commands(cli, ctx))
+            commands = list(get_all_commands(cli))

48-65: Test is functional but comments are verbose.

The test correctly validates that --json flag is accepted. Consider trimming the inline comments - the test name and docstring already convey the purpose.

🔎 Suggested simplification
 def test_cli_json_flag():
     """Verify the --json flag on the main entry point."""
     runner = CliRunner()
-    # We pass no arguments, which should trigger help or specific behavior
-    # But here we want to test if --json flag is accepted even without file (might fail validation but check it exists)
-
-    # Actually, main cli requires filepath argument?
-    # Let's check commands.py: @click.argument('filepath', required=False)
-
     result = runner.invoke(cli, ['--json'])
     assert result.exit_code == 0
-    # Expected: empty JSON output with maybe default values or handled gracefully
-    # Based on commands.py: if not filepath: click.echo(ctx.get_help()) -> output_json not reached.
-    # Wait, commands.py logic:
-    # if not filepath: click.echo(ctx.get_help())
-
-    # So --json without file just shows help.
+    # Without required arguments, CLI shows help even with --json
     assert "Usage:" in result.output
src/switchcraft/gui/app.py (1)

242-273: Consider the implications of accessing _tab_dict and re-initializing HistoryView.

Two observations:

  1. self.tabview._tab_dict is a private/internal attribute of CTkTabview. This could break with future customtkinter updates.

  2. When the History tab is re-added (line 264-266), setup_history_tab() creates a new HistoryView instance. If the old self.history_view held state or references, this could cause orphaned objects or lost state.

🔎 Consider storing History tab state before deletion
 if history_exists:
     # Delete History, add Winget Store, re-add History
+    # Note: Any unsaved state in history_view will be lost
     self.tabview.delete("History")
     logger.debug("Temporarily removed History tab for repositioning")

Consider whether HistoryView maintains any state that should be preserved across this tab repositioning operation.

tests/test_full_coverage.py (1)

95-100: Unused mock parameter and test doesn't exercise registry code.

The @patch("winreg.OpenKey") decorator provides mock_open_key, but it's never used. More importantly, checking is_addon_installed("fake_addon") returns False because "fake_addon" isn't in AddonService.ADDONS, so the registry code path is never reached.

🔎 Consider testing an actual addon ID
-@patch("winreg.OpenKey")
-def test_addon_detection(mock_open_key):
+def test_addon_detection():
     """Test registry detection for advanced addon."""
-    # This is tricky to test on non-windows or without registry,
-    # but we can verify it doesn't crash on import/usage
-    assert not AddonService.is_addon_installed("fake_addon")
+    # Verify known addon ID resolution and that method doesn't crash
+    # "advanced" is a valid addon ID that will check filesystem
+    result = AddonService.is_addon_installed("advanced")
+    assert isinstance(result, bool)
+    
+    # Unknown addon should always return False
+    assert not AddonService.is_addon_installed("fake_addon")
src/switchcraft/gui/views/manifest_dialog.py (1)

154-157: os.startfile is Windows-only.

Consider using a cross-platform approach or adding a platform check, though this appears to be a Windows-focused application.

🔎 Proposed cross-platform alternative
     def _show_next_steps(self, manifest_dir):
         # Open folder
         import os
-        os.startfile(manifest_dir)
+        import sys
+        if sys.platform == 'win32':
+            os.startfile(manifest_dir)
+        elif sys.platform == 'darwin':
+            subprocess.run(['open', manifest_dir])
+        else:
+            subprocess.run(['xdg-open', manifest_dir])
scripts/build_release.ps1 (1)

56-62: Variable $AutoLaunch should be initialized at script scope.

$AutoLaunch is set conditionally inside the default behavior block but referenced later (line 170). If no default path is taken, it will be $null. Consider initializing it at the script scope.

🔎 Proposed fix
+$AutoLaunch = $false
+
 # Default behavior: If no specific target provided, build Modern (Standard)
 if (-not $Modern -and -not $Legacy -and -not $Cli -and -not $Pip -and -not $Installer) {
     Write-Host "No specific target variables provided. Defaulting to Modern GUI." -ForegroundColor Cyan
     Write-Host "TIP: Use '.\scripts\build_release.ps1 -All' to build Modern, Legacy, CLI, and Installer at once." -ForegroundColor Green
     $Modern = $true
     $AutoLaunch = $true
 }
src/switchcraft/services/winget_manifest_service.py (1)

132-136: Remove dead code block.

This conditional block does nothing (just contains pass).

🔎 Proposed fix
         filename = f"{meta['PackageIdentifier']}.installer.yaml"
-
-        # Global scope/type if consistent across installers
-        if len(installers) > 0:
-            first = installers[0]
-            if "InstallerType" in first: # If all have same type, can move to root, but keeping explicit is safer
-                 pass
-
         self._dump_yaml(folder / filename, data)
src/switchcraft/cli/commands.py (2)

77-90: Consider type coercion for configuration values.

The value argument is always passed as a string from the CLI. If users need to set numeric or boolean preferences, consider adding type inference or an explicit --type option. Per the SwitchCraftConfig context, set_user_preference accepts an optional value_type parameter.


276-283: Consider logging unhandled prompt types.

The cli_prompt callback only handles 'ask_browser' and returns False for all other types. Consider logging unhandled types for debugging purposes.

Proposed enhancement
     def cli_prompt(type, **kwargs):
         # CLI Handler for addon prompts
         if type == 'ask_browser':
              # Maybe skip browser for CLI or print URL
              url = kwargs.get('url')
              print(f"Please check: {url}")
              return False # Don't rely on browser flow in CLI
+        logger.debug(f"Unhandled prompt type in CLI: {type}")
         return False
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 006e562 and 0aa9804.

📒 Files selected for processing (41)
  • .github/workflows/release.yml (3 hunks)
  • .github/workflows/test.yml (1 hunks)
  • .gitignore (1 hunks)
  • README.md (1 hunks)
  • docs/CI_Architecture.md (1 hunks)
  • docs/CLI_Reference.md (1 hunks)
  • pyproject.toml (2 hunks)
  • scripts/build_release.ps1 (2 hunks)
  • scripts/build_release.sh (1 hunks)
  • src/entry.py (2 hunks)
  • src/switchcraft/assets/lang/de.json (1 hunks)
  • src/switchcraft/assets/lang/en.json (1 hunks)
  • src/switchcraft/cli/commands.py (1 hunks)
  • src/switchcraft/cli_main.py (1 hunks)
  • src/switchcraft/gui/app.py (3 hunks)
  • src/switchcraft/gui/splash.py (1 hunks)
  • src/switchcraft/gui/views/analyzer_view.py (6 hunks)
  • src/switchcraft/gui/views/manifest_dialog.py (1 hunks)
  • src/switchcraft/gui/views/winget_view.py (1 hunks)
  • src/switchcraft/gui_modern/app.py (1 hunks)
  • src/switchcraft/main.py (1 hunks)
  • src/switchcraft/modern_main.py (1 hunks)
  • src/switchcraft/services/addon_service.py (5 hunks)
  • src/switchcraft/services/backup_service.py (0 hunks)
  • src/switchcraft/services/history_service.py (1 hunks)
  • src/switchcraft/services/notification_service.py (1 hunks)
  • src/switchcraft/services/sync_service.py (0 hunks)
  • src/switchcraft/services/winget_manifest_service.py (1 hunks)
  • src/switchcraft/utils/__init__.py (1 hunks)
  • src/switchcraft/utils/i18n.py (0 hunks)
  • src/switchcraft_advanced/utils/winget.py (0 hunks)
  • src/switchcraft_debug/console.py (0 hunks)
  • src/switchcraft_winget/utils/__init__.py (1 hunks)
  • src/switchcraft_winget/utils/winget.py (4 hunks)
  • switchcraft.iss (2 hunks)
  • switchcraft.spec (1 hunks)
  • switchcraft_cli.spec (1 hunks)
  • switchcraft_legacy.spec (1 hunks)
  • switchcraft_modern.spec (1 hunks)
  • tests/test_cli_dynamic.py (1 hunks)
  • tests/test_full_coverage.py (1 hunks)
💤 Files with no reviewable changes (5)
  • src/switchcraft/utils/i18n.py
  • src/switchcraft_debug/console.py
  • src/switchcraft/services/sync_service.py
  • src/switchcraft/services/backup_service.py
  • src/switchcraft_advanced/utils/winget.py
🧰 Additional context used
🧬 Code graph analysis (12)
tests/test_cli_dynamic.py (1)
src/switchcraft/cli/commands.py (1)
  • cli (42-62)
src/switchcraft/main.py (5)
src/switchcraft/gui/app.py (1)
  • main (771-791)
src/switchcraft/gui_modern/app.py (1)
  • main (96-124)
src/switchcraft/modern_main.py (1)
  • main (4-6)
src/switchcraft/cli_main.py (1)
  • main (9-19)
src/switchcraft/cli/commands.py (1)
  • cli (42-62)
src/switchcraft/gui/views/manifest_dialog.py (2)
src/switchcraft/services/winget_manifest_service.py (3)
  • WingetManifestService (12-174)
  • generate_manifests (18-71)
  • validate_manifest (73-104)
src/switchcraft/utils/config.py (1)
  • SwitchCraftConfig (8-199)
src/switchcraft/gui_modern/app.py (2)
src/switchcraft/main.py (1)
  • main (8-28)
src/switchcraft/modern_main.py (1)
  • main (4-6)
src/switchcraft/services/winget_manifest_service.py (1)
src/switchcraft/utils/config.py (1)
  • get_value (22-59)
src/switchcraft/gui/app.py (2)
src/switchcraft/main.py (1)
  • main (8-28)
src/switchcraft/gui/splash.py (1)
  • close (78-79)
src/switchcraft/gui/views/winget_view.py (2)
src/switchcraft_winget/utils/winget.py (1)
  • install_package (102-113)
src/switchcraft/utils/i18n.py (1)
  • get (116-143)
tests/test_full_coverage.py (3)
src/switchcraft/services/winget_manifest_service.py (3)
  • WingetManifestService (12-174)
  • generate_manifests (18-71)
  • validate_manifest (73-104)
src/switchcraft/services/addon_service.py (2)
  • AddonService (13-390)
  • is_addon_installed (41-50)
src/switchcraft/utils/config.py (2)
  • SwitchCraftConfig (8-199)
  • get_value (22-59)
src/switchcraft/modern_main.py (2)
src/switchcraft/gui_modern/app.py (2)
  • ModernApp (5-93)
  • main (96-124)
src/switchcraft/main.py (1)
  • main (8-28)
src/switchcraft/cli_main.py (2)
src/switchcraft/main.py (1)
  • main (8-28)
src/switchcraft/cli/commands.py (1)
  • cli (42-62)
src/switchcraft/cli/commands.py (3)
src/switchcraft/utils/config.py (6)
  • SwitchCraftConfig (8-199)
  • is_debug_mode (72-93)
  • get_value (22-59)
  • set_user_preference (105-129)
  • get_secret (132-139)
  • set_secret (142-148)
src/switchcraft_winget/utils/winget.py (4)
  • WingetHelper (10-178)
  • search_packages (29-72)
  • install_package (102-113)
  • search_by_name (14-27)
src/switchcraft/services/addon_service.py (4)
  • AddonService (13-390)
  • is_addon_installed (41-50)
  • install_addon (82-167)
  • import_addon_module (62-79)
src/entry.py (3)
src/switchcraft/gui/app.py (1)
  • main (771-791)
src/switchcraft/main.py (1)
  • main (8-28)
src/switchcraft/cli_main.py (1)
  • main (9-19)
🪛 actionlint (1.7.9)
.github/workflows/test.yml

29-29: shellcheck reported issue in this script: SC2155:warning:1:8: Declare and assign separately to avoid masking return values

(shellcheck)

🪛 GitHub Actions: CI Orchestrator
src/switchcraft/gui/splash.py

[warning] 3-3: Unused import 'sys' (F401).


[warning] 4-4: Unused import 'os' (F401).


[warning] 5-5: Unused import 'pathlib.Path' (F401).

src/switchcraft/gui/views/manifest_dialog.py

[warning] 4-4: Unused import 'webbrowser' (F401).


[warning] 8-8: Unused import 'SwitchCraftConfig' (F401).

src/switchcraft/gui_modern/app.py

[warning] 124-124: Local variable 'app' assigned to but never used (F841).

src/switchcraft/services/winget_manifest_service.py

[warning] 1-1: Unused import 'os' (F401).


[warning] 5-5: Unused import 'shutil' (F401).


[warning] 7-7: Unused import 'datetime' (F401).

src/switchcraft/gui/views/winget_view.py

[warning] 188-188: Local variable 'cmd' assigned to but never used (F841).

src/switchcraft/gui/views/analyzer_view.py

[error] 973-973: F401 [*] 'tkinter.messagebox' imported but unused

src/switchcraft_winget/utils/winget.py

[warning] 3-3: Unused import 'shutil' (F401).

src/switchcraft/cli/commands.py

[error] 102-102: Undefined name 'WingetHelper' (F821).


[error] 125-125: Undefined name 'WingetHelper' (F821).


[error] 136-136: Redefinition of unused 'winget_install' (F811).


[error] 138-138: Undefined name 'WingetHelper' (F821).


[error] 179-179: Local variable 'output_log' assigned but never used (F841).

🪛 GitHub Actions: PR Assistant (The Janitor)
src/switchcraft/gui_modern/app.py

[error] 123-123: Local variable 'app' is assigned to but never used.

src/switchcraft/gui/views/winget_view.py

[error] 188-188: Local variable 'cmd' is assigned to but never used.

src/switchcraft/cli/commands.py

[error] 102-102: Undefined name 'WingetHelper'.


[error] 125-125: Undefined name 'WingetHelper'.


[error] 138-138: Undefined name 'WingetHelper'.


[error] 136-136: Redefinition of unused 'winget_install'.


[error] 179-179: Local variable 'output_log' is assigned to but never used.

🪛 LanguageTool
docs/CI_Architecture.md

[uncategorized] ~8-~8: The operating system from Apple is written “macOS”.
Context: ...dard Build**: Windows (GUI+CLI), Linux, MacOS. - CLI-Only Build: Dedicated job ...

(MAC_OS)

docs/CLI_Reference.md

[grammar] ~49-~49: Ensure spelling is correct
Context: ...Secretin config. ###addonsManage addons. * switchcraft addons list* swi...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🪛 markdownlint-cli2 (0.18.1)
README.md

17-17: Multiple headings with the same content

(MD024, no-duplicate-heading)

docs/CLI_Reference.md

46-46: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

🪛 RuboCop (1.81.7)
switchcraft_cli.spec

[fatal] 12-12: unexpected token kIN
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 12-12: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 18-18: unexpected token kFOR
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 18-18: unexpected token kIF_MOD
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 18-18: unexpected token tRBRACK
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)

switchcraft_modern.spec

[fatal] 15-15: unexpected token kIN
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 15-15: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 19-19: unexpected token tCOMMA
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 20-20: unexpected token tCOMMA
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)

switchcraft_legacy.spec

[fatal] 12-12: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 14-14: unexpected token tRPAREN
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 25-25: unexpected token kIN
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 25-25: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 31-31: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 32-32: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 33-33: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 38-38: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 45-45: unexpected token tRPAREN
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 47-47: unexpected token tCOMMA
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)

🔇 Additional comments (55)
src/switchcraft/assets/lang/de.json (1)

312-315: LGTM! Localization additions are well-structured.

The three new SFX-related keys are properly added with valid JSON syntax, and the German translations are accurate and clear. The trailing comma on line 312 correctly maintains JSON validity, and the content aligns with the parallel English localization updates.

src/switchcraft/assets/lang/en.json (1)

291-294: LGTM! English localization additions are clear and consistent.

The three new keys for 7-Zip SFX detection are properly formatted with valid JSON syntax. The English content is clear, concise, and maintains consistency with the German localization file. The trailing comma on line 291 correctly maintains JSON validity.

src/switchcraft/services/notification_service.py (1)

86-86: LGTM - Removal of unused code.

The is_foreground variable was never used after assignment, so commenting out this line removes dead code without changing behavior.

.gitignore (1)

15-17: LGTM!

The addition of these negation patterns correctly ensures the three new spec files (CLI, Legacy, Modern variants) are tracked in version control, consistent with the existing pattern for switchcraft.spec.

pyproject.toml (4)

11-11: LGTM!

The license metadata change from a file reference to the direct string "MIT" is a simplification that improves clarity.


34-41: Excellent modular dependency design!

Separating GUI dependencies into optional groups (gui and modern) enables lightweight CLI-only installations while supporting multiple GUI variants. This architecture aligns well with the PR's "CLI first strategy" objective.


53-58: LGTM!

The pytest configuration follows standard conventions with appropriate test discovery patterns and output file exclusions.


45-45: Entry point correctly routes between CLI and GUI based on arguments.

The entry point change from cli to main is properly implemented. The main() function in src/switchcraft/main.py correctly handles both invocation patterns: with arguments it loads the CLI via switchcraft.cli.commands, and without arguments it launches the GUI via switchcraft.gui.app. Error handling for missing dependencies is in place for both paths.

switchcraft.iss (1)

12-12: LGTM!

The executable name change to SwitchCraft-windows.exe is consistently applied throughout the installer script, aligning with the new multi-platform build strategy.

Also applies to: 80-80

src/switchcraft/gui_modern/app.py (2)

5-93: LGTM!

The ModernApp class is well-structured with clear separation of concerns:

  • __init__: Page initialization
  • setup_page: Configuration
  • build_ui: UI construction
  • nav_change: Event handling

The navigation rail implementation follows Flet patterns correctly.


127-128: LGTM!

The __main__ guard allows the module to be run directly, which is useful for testing the modern GUI independently.

switchcraft_modern.spec (1)

1-38: LGTM!

The PyInstaller Analysis configuration correctly:

  • Collects Flet and SwitchCraft submodules
  • Sets up Python paths
  • Includes necessary data files (logo and assets)

The manual collection approach is appropriate for ensuring all dependencies are bundled.

src/switchcraft/utils/__init__.py (1)

2-2: No breaking change detected—WingetHelper remains importable from the module.

The removal of WingetHelper from __all__ is not a breaking change. The only code importing WingetHelper uses direct module imports (from switchcraft.utils.winget import WingetHelper), which continue to work. The commented import in winget_view.py is already handled with an explanation ("Moved to Addon"), indicating this was a deliberate refactoring.

Likely an incorrect or invalid review comment.

README.md (1)

19-20: LGTM!

The new documentation links for CLI Reference and CI Architecture are well-placed and align with the CLI-first strategy introduced in this PR.

switchcraft.spec (1)

82-82: LGTM!

The executable name change to 'SwitchCraft-windows' with the clarifying comment about legacy Tkinter builds aligns well with the multi-variant build strategy in this PR.

src/switchcraft/gui/views/winget_view.py (1)

196-200: LGTM!

The refactoring to delegate installation to winget_helper.install_package() improves code organization and error handling. The boolean return pattern is cleaner than parsing subprocess output.

docs/CLI_Reference.md (1)

1-73: Excellent CLI documentation!

The CLI reference is comprehensive, well-structured, and provides clear examples for all major commands. This will be valuable for users adopting the CLI-first approach.

.github/workflows/test.yml (2)

25-25: LGTM!

Adding GUI extras to the backend test installation correctly separates core dependencies from GUI dependencies, enabling the new CLI-only test workflow.


33-48: Excellent test separation!

The new test_cli_core job properly validates CLI functionality without GUI dependencies, aligning well with the CLI-first architecture.

src/switchcraft/main.py (2)

8-28: Excellent CLI-first routing implementation!

The main() function cleanly separates CLI and GUI startup paths based on command-line arguments. The error handling is appropriate, and the comment about the splash screen removal is helpful context.


20-23: Informative comment about splash screen removal.

The explanation of why the splash screen was removed (Tkinter dual-root conflict) is valuable for future maintainers.

switchcraft_legacy.spec (1)

88-88: Verify PNG icon compatibility on Windows.

Windows executables traditionally require .ico format for icons. While PyInstaller may convert PNG to ICO internally on some versions, this behavior isn't guaranteed. Consider using a proper .ico file for reliable Windows builds.

switchcraft_cli.spec (3)

15-18: Good approach to filter GUI modules from CLI build.

The filtering logic correctly removes switchcraft.gui modules from hidden imports. This ensures a minimal CLI-only build.


33-44: Comprehensive excludes list for CLI-only build.

Good defensive exclusion of GUI and heavy libraries. Note that PIL and Pillow in excludes are somewhat redundant (Pillow provides the PIL namespace), but this is harmless and ensures thorough exclusion.


53-74: CLI EXE configuration looks correct.

console=True is appropriate for CLI, and upx=True will help reduce executable size. The strip=False choice is safer for debugging; you can enable it later for release builds if size is critical.

src/switchcraft/cli_main.py (2)

9-19: CLI bootstrap structure is correct.

The main() function correctly provides a CLI-only entry point that avoids GUI imports. The duplication with src/switchcraft/main.py is acceptable given the different use cases (dedicated CLI vs. hybrid entry point).


14-19: Overly broad exception handling may swallow SystemExit and KeyboardInterrupt.

Catching Exception will intercept KeyboardInterrupt (Ctrl+C) and turn it into "Critical Error in CLI" instead of a clean exit. It will also catch SystemExit raised by Click's ctx.exit(), potentially masking the intended exit code.

Consider handling specific exceptions or re-raising system exceptions:

🔎 Suggested fix
 def main():
     """
     CLI-Only entry point.
     Strictly avoids importing any GUI modules.
     """
     try:
         from switchcraft.cli.commands import cli
         cli()
+    except SystemExit:
+        raise
+    except KeyboardInterrupt:
+        sys.exit(130)  # Standard exit code for SIGINT
     except Exception as e:
         print(f"Critical Error in CLI: {e}")
         sys.exit(1)

Likely an incorrect or invalid review comment.

src/entry.py (1)

35-50: Exception handling and graceful shutdown look well-designed.

Good handling of:

  • SystemExit with code 0 (clean exit, no error message)
  • Traceback printing for actual errors
  • EOFError/RuntimeError for environments without stdin (e.g., GUI-only launch)
tests/test_cli_dynamic.py (1)

14-46: Good dynamic test approach for CLI commands.

This test provides valuable coverage by automatically discovering and validating all CLI commands. The assertions for exit_code == 0, "Usage:", and "Options:" are appropriate for verifying help text generation.

src/switchcraft/gui/splash.py (1)

7-79: LGTM!

The LegacySplash class correctly handles both standalone and toplevel scenarios to avoid the pyimage error. The _owns_root flag properly tracks ownership, and the UI composition is clean.

src/switchcraft/gui/app.py (1)

771-791: LGTM!

The updated main() function properly handles the splash parameter with defensive exception handling, and the fallback error dialog using a temporary Tk root ensures users see critical startup errors even when the main app fails to initialize.

.github/workflows/release.yml (2)

157-163: Build caching looks good.

The PyInstaller build cache keyed on pyproject.toml hash is a reasonable approach to speed up builds.


299-343: CLI build job is well-structured.

The separate CLI build job correctly installs only the base package (no GUI extras) and uses a distinct cache key. The rename and upload steps are correct.

src/switchcraft/services/addon_service.py (3)

27-38: LGTM!

The .resolve() calls ensure canonical absolute paths, which improves reliability for path comparisons and logging.


49-50: Simplified addon detection is cleaner.

Removing the conditional dev/source check and always requiring __init__.py provides consistent behavior across environments.


99-103: Dev mode install behavior is appropriate.

In non-frozen (development) mode, addons are expected to be present as sibling packages in the source tree. Returning False with a warning when missing (rather than attempting network download) is the correct behavior for development environments.

tests/test_full_coverage.py (3)

17-38: LGTM!

The manifest generation test validates the expected file structure and uses tmp_path fixture correctly for isolation.


57-84: LGTM!

The Intune packaging test properly mocks Popen and validates the command structure. The mock stdout as a list of strings is compatible with iteration.


104-112: LGTM!

Good defensive pattern for testing optional dependency imports - skipping when Flet is unavailable while failing on unexpected exceptions.

src/switchcraft/gui/views/analyzer_view.py (3)

309-313: Dynamic addon loading for WingetHelper is well implemented.

The pattern correctly handles the case where the addon module is unavailable by checking the return value before instantiation.


395-401: SFX archive notice section looks good.

The UI enhancement properly surfaces archive details when a PE/SFX archive is detected.


452-458: Winget Manifest button integration looks correct.

The button is properly wired to open the manifest dialog with the analysis info.

scripts/build_release.ps1 (2)

84-97: Process cleanup with proper error handling looks good.

The cleanup loop properly handles cases where processes may not be running or are stubborn, with appropriate warnings.


237-298: Installer build section is well-structured.

Good approach checking for prerequisites (Legacy EXE, Inno Setup), handling ICO conversion fallback, and proper error handling.

src/switchcraft_winget/utils/winget.py (2)

115-132: Good implementation of download_package method.

The method correctly uses an argument list (not shell=True), handles errors appropriately, and searches for common installer extensions.


173-178: Good refactor: _get_startup_info helper for cross-platform compatibility.

This centralizes the Windows-specific STARTUPINFO handling and safely returns None on non-Windows platforms.

src/switchcraft/services/winget_manifest_service.py (1)

12-71: Overall manifest generation logic is well-structured.

The service correctly implements the Winget manifest structure with version, installer, and locale manifests. The folder structure follows the Winget convention.

src/switchcraft/cli/commands.py (8)

21-36: LGTM!

The logging setup is reasonable for a CLI entry point. Note that logging.basicConfig only has effect on the first call, so this works well as a CLI startup function.


38-62: LGTM!

The CLI group setup is appropriate. The comments explaining the backward compatibility considerations are helpful for future maintainers.


64-69: LGTM!

Clean delegation to the helper function with proper path validation.


191-239: LGTM!

The upload command has proper credential validation and clear error messaging guiding users on how to configure missing credentials. Good use of progress callbacks for user feedback.


292-299: LGTM!

Clean implementation for secure secret storage. This properly complements the intune upload command's credential requirements.


317-323: Good pattern: Use this dynamic import approach for the winget commands.

This correctly handles the optional Winget addon via dynamic import through AddonService. The same pattern should be applied to the winget_search and winget_install commands (lines 102, 125) to resolve the undefined WingetHelper errors.


333-354: LGTM!

The report rendering with Rich panels and tables provides clear, well-organized output. Good conditional handling for missing data and helpful suggestions for EXE files without detected switches.


115-118: The code at lines 115-118 is correct and does not require changes. The search_packages method consistently returns dictionaries with PascalCase keys (Name, Id, Version, Source), as shown in both the PowerShell path (lines 60-66) and the CLI fallback path (lines 160-165). The confusion in the original review stemmed from get_package_details using lowercase keys; however, these are two separate methods with different return structures. The table row population using r.get('Id'), r.get('Name'), and r.get('Version') is correct.

Comment thread .github/workflows/release.yml
Comment thread src/entry.py
Comment thread src/switchcraft_winget/utils/winget.py
Comment thread src/switchcraft/cli/commands.py
Comment thread src/switchcraft/cli/commands.py Outdated
Comment thread src/switchcraft/modern_main.py Outdated
Comment thread src/switchcraft/services/history_service.py Outdated
Comment thread src/switchcraft/services/winget_manifest_service.py
Comment thread src/switchcraft/services/winget_manifest_service.py
Comment thread switchcraft_legacy.spec Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (3)
src/switchcraft/services/winget_manifest_service.py (2)

53-57: Guard against empty publisher string.

If publisher is an empty string, publisher[0] will raise an IndexError. This issue was previously identified but remains unresolved.

🔎 Proposed fix
         # Folder structure: manifests/{first_char_lower}/{Publisher}/{PackageName}/{Version}
-        p_char = publisher[0].lower()
+        p_char = publisher[0].lower() if publisher else "_"
         package_name = meta.get("PackageName", pkg_id.split('.')[-1])

Alternatively, strengthen the validation at line 39:

-        if not (pkg_id and version and publisher):
-            raise ValueError("Missing required fields: PackageIdentifier, PackageVersion, Publisher")
+        if not (pkg_id and version and publisher and len(publisher) > 0):
+            raise ValueError("Missing or empty required fields: PackageIdentifier, PackageVersion, Publisher")

Based on past review comments.


80-81: subprocess.STARTUPINFO() is Windows-only.

This code will raise an AttributeError on non-Windows platforms. The winget.py file has a _get_startup_info() helper that handles this correctly—consider using a similar pattern here.

🔎 Proposed fix
+    def _get_startup_info(self):
+        if hasattr(subprocess, 'STARTUPINFO'):
+            si = subprocess.STARTUPINFO()
+            si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+            return si
+        return None
+
     def validate_manifest(self, manifest_dir: str) -> dict:
         """
         Validates the generated manifests using 'winget validate'.
         Returns dict with keys: 'valid' (bool), 'output' (str), 'errors' (list)
         """
         try:
             # winget validate --manifest <path_to_directory>
             cmd = [self.winget_exe, "validate", "--manifest", str(manifest_dir)]

             # Start process without window
-            startupinfo = subprocess.STARTUPINFO()
-            startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+            startupinfo = self._get_startup_info()

             result = subprocess.run(
                 cmd,
                 capture_output=True,
                 text=True,
                 startupinfo=startupinfo,
                 check=False
             )

Based on past review comments.

src/switchcraft_winget/utils/winget.py (1)

101-112: Command injection vulnerability with shell=True.

Using shell=True with string interpolation of package_id and scope parameters allows command injection. An attacker-controlled package ID like foo; rm -rf / could execute arbitrary commands.

🔎 Proposed fix using argument list
     def install_package(self, package_id: str, scope: str = "machine") -> bool:
         """Install a package via Winget CLI."""
-        cmd = f"winget install --id {package_id} --scope {scope} --accept-package-agreements --accept-source-agreements"
+        if scope not in ("machine", "user"):
+            logger.error(f"Invalid scope: {scope}")
+            return False
+        cmd = [
+            "winget", "install",
+            "--id", package_id,
+            "--scope", scope,
+            "--accept-package-agreements",
+            "--accept-source-agreements"
+        ]
         try:
-            proc = subprocess.run(cmd, shell=True, capture_output=True, text=True)
+            proc = subprocess.run(cmd, capture_output=True, text=True)
             if proc.returncode != 0:
                 logger.error(f"Winget install failed: {proc.stderr}")
                 return False
             return True
         except Exception as e:
             logger.error(f"Winget install exception: {e}")
             return False

Based on past review comments.

🧹 Nitpick comments (4)
src/switchcraft/gui/splash.py (2)

37-67: Consider font fallback for cross-platform compatibility.

The hard-coded "Segoe UI" font (lines 43, 50, 59) is Windows-specific. On Linux and macOS, this font may not be available, causing tkinter to fall back to a default font, which could affect the visual consistency of the splash screen.

🔎 Suggested improvement

Consider using a font fallback list or a cross-platform font:

+# At the top of __init__, define a cross-platform font
+font_family = "Segoe UI" if self.root.tk.call("tk", "windowingsystem") == "win32" else "Helvetica"
+
 # Then use it in the labels:
 tk.Label(
     main_frame,
     text="SwitchCraft",
-    font=("Segoe UI", 32, "bold"),
+    font=(font_family, 32, "bold"),
     bg="#2c3e50",
     fg="#ecf0f1"
 ).pack(pady=(40, 10))

Or use tkinter's default font families like "TkDefaultFont" or "Helvetica" which are available across platforms.

Optional: Store progress bar reference for explicit cleanup.

The progress bar is started but the reference is not stored (line 65). While destroy() will clean it up, storing it as self.progress = progress would allow you to explicitly stop it in the close() method with self.progress.stop() if needed.


75-76: Optional: Explicitly stop the progress bar before destroying.

While destroy() will clean up all widgets, explicitly stopping the progress bar first is a minor best practice improvement:

def close(self):
    if hasattr(self, 'progress'):
        self.progress.stop()
    self.root.destroy()

This requires storing the progress bar reference as self.progress during initialization (see previous comment).

src/switchcraft/gui_modern/app.py (1)

77-92: Consider dictionary-based navigation for cleaner code.

The if-elif chain works correctly, but a dictionary mapping index to content would be more maintainable as the number of navigation items grows.

🔎 Optional refactor using dictionary mapping
    def nav_change(self, e):
        idx = e.control.selected_index
        self.content_area.controls.clear()

-        if idx == 0:
-            self.content_area.controls.append(ft.Text("Home", size=30))
-        elif idx == 1:
-            self.content_area.controls.append(ft.Text("Analyzer (Coming Soon)", size=30))
-        elif idx == 2:
-            self.content_area.controls.append(ft.Text("Winget (Coming Soon)", size=30))
-        elif idx == 3:
-            self.content_area.controls.append(ft.Text("Intune (Coming Soon)", size=30))
-        elif idx == 4:
-             self.content_area.controls.append(ft.Text("Settings (Coming Soon)", size=30))
+        nav_content = {
+            0: "Home",
+            1: "Analyzer (Coming Soon)",
+            2: "Winget (Coming Soon)",
+            3: "Intune (Coming Soon)",
+            4: "Settings (Coming Soon)",
+        }
+        
+        if idx in nav_content:
+            self.content_area.controls.append(ft.Text(nav_content[idx], size=30))

        self.page.update()
src/switchcraft/gui/views/analyzer_view.py (1)

969-978: Consider adding error handling for dialog instantiation.

The lazy import and dialog creation could fail if the ManifestDialog module has issues. While unlikely, wrapping the dialog instantiation in a try-except would improve robustness.

🔎 Optional error handling enhancement
 def _open_manifest_dialog(self, info):
     # Check Config
     repo_path = SwitchCraftConfig.get_value("WingetRepoPath")
     if not repo_path:
          # Optional: Ask user if they want to configure it, or just rely on default logic in Service
          pass

-    from switchcraft.gui.views.manifest_dialog import ManifestDialog
-    dlg = ManifestDialog(self.winfo_toplevel(), info)
-    dlg.grab_set()
+    try:
+        from switchcraft.gui.views.manifest_dialog import ManifestDialog
+        dlg = ManifestDialog(self.winfo_toplevel(), info)
+        dlg.grab_set()
+    except Exception as e:
+        logger.error(f"Failed to open manifest dialog: {e}")
+        messagebox.showerror("Error", f"Could not open manifest dialog: {e}")
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0aa9804 and e7a1088.

📒 Files selected for processing (8)
  • src/switchcraft/cli/commands.py (1 hunks)
  • src/switchcraft/gui/splash.py (1 hunks)
  • src/switchcraft/gui/views/analyzer_view.py (6 hunks)
  • src/switchcraft/gui/views/manifest_dialog.py (1 hunks)
  • src/switchcraft/gui/views/winget_view.py (1 hunks)
  • src/switchcraft/gui_modern/app.py (1 hunks)
  • src/switchcraft/services/winget_manifest_service.py (1 hunks)
  • src/switchcraft_winget/utils/winget.py (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/switchcraft/gui/views/manifest_dialog.py
  • src/switchcraft/gui/views/winget_view.py
🧰 Additional context used
🧬 Code graph analysis (3)
src/switchcraft/services/winget_manifest_service.py (1)
src/switchcraft/utils/config.py (1)
  • get_value (22-59)
src/switchcraft/gui_modern/app.py (3)
src/switchcraft/main.py (1)
  • main (8-28)
src/switchcraft/modern_main.py (1)
  • main (4-6)
src/switchcraft/gui/app.py (1)
  • main (771-791)
src/switchcraft/gui/views/analyzer_view.py (6)
src/switchcraft/services/addon_service.py (1)
  • import_addon_module (62-79)
src/switchcraft/cli/commands.py (1)
  • winget (94-96)
src/switchcraft_winget/utils/winget.py (1)
  • WingetHelper (9-177)
src/switchcraft/utils/i18n.py (1)
  • get (116-143)
src/switchcraft/utils/config.py (1)
  • get_value (22-59)
src/switchcraft/gui/views/manifest_dialog.py (1)
  • ManifestDialog (8-198)
🔇 Additional comments (17)
src/switchcraft/gui/splash.py (4)

1-9: LGTM! Clear documentation of the design rationale.

The class structure and docstring clearly explain why LegacySplash accepts an optional main_root parameter to avoid tkinter's multiple root issues.


10-17: LGTM! Proper handling of root window creation.

The logic correctly creates either a standalone Tk() root or a Toplevel window depending on whether main_root is provided. The _owns_root flag is tracked but currently unused—this is fine as destroy() works for both window types.


19-34: LGTM! Standard splash screen configuration.

The borderless, topmost window with proper centering logic is a typical and correct implementation for a splash screen.


71-73: LGTM! Simple and effective status update.

The method correctly updates the status label and refreshes the UI—appropriate for a splash screen context.

src/switchcraft/gui_modern/app.py (4)

1-9: LGTM! Clean initialization flow.

The import structure and initialization sequence are well-organized. The explicit splash removal via page.clean() followed by setup and UI building provides a clear initialization path.


11-19: LGTM! Sensible page configuration.

The page setup establishes appropriate defaults: versioned title, dark theme, and minimum window dimensions to ensure usability. The commented custom title bar code is clearly marked for future consideration.


20-76: LGTM! Well-structured UI layout.

The navigation rail and content area layout follows Flet best practices. The use of expand=True ensures proper responsive behavior, and the navigation destinations are clearly labeled with appropriate icons.


126-127: LGTM! Direct run support is properly implemented.

The if __name__ == "__main__" block correctly uses ft.app(target=main) to enable standalone execution of this module.

src/switchcraft/gui/views/analyzer_view.py (4)

309-313: LGTM! Clean dynamic addon loading pattern.

The dynamic loading of the Winget addon through AddonService is well-implemented with proper null-safety checks. This allows the Winget functionality to be optional without breaking the core analyzer flow.


395-401: Good UX enhancement for SFX archive detection.

The info panel clearly communicates the SFX detection to users and provides actionable instructions. The i18n integration ensures proper localization support.


453-458: Clean integration of Winget manifest creation.

The button is appropriately gated by the presence of install switches and integrates well with the existing UI flow. The color choice helps distinguish it from other actions.


745-745: The field rename from raw_output to brute_force_output is consistent across the codebase. The data source (UniversalAnalyzer.extract_and_analyze_nested in universal.py:484) produces the brute_force_output field, and the consumer at line 745 correctly reads it.

src/switchcraft/services/winget_manifest_service.py (1)

9-171: Well-designed manifest service structure.

The separation of concerns into _write_version_manifest, _write_installer_manifest, and _write_locale_manifest is clean and maintainable. The validation flow with graceful fallback for missing Winget CLI is also well-handled.

src/switchcraft_winget/utils/winget.py (2)

114-131: LGTM! Safe subprocess usage in download method.

The download_package method correctly uses an argument list without shell=True, avoiding command injection risks. The file discovery logic with glob patterns is appropriate for finding downloaded installers.


172-177: Excellent cross-platform subprocess helper.

The _get_startup_info helper properly encapsulates Windows-specific subprocess configuration, making the code portable and reducing duplication. This pattern should be adopted in winget_manifest_service.py as well.

src/switchcraft/cli/commands.py (2)

98-121: Clean dynamic Winget integration in CLI.

The winget search command properly uses the addon service to dynamically load WingetHelper, addressing previous concerns about missing imports. The error handling and Rich table output provide good UX.


123-139: LGTM! No duplicate command definition in current code.

The winget install command is properly implemented with dynamic addon loading and scope validation. The duplicate command issue flagged in past reviews is not present in the current code.

Comment thread src/switchcraft/gui_modern/app.py Outdated
Comment thread src/switchcraft/gui_modern/app.py Outdated
Comment thread src/switchcraft/gui_modern/app.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
README.md (1)

98-116: Update README CLI flags section to match actual implementation.

The README documents three global CLI flags that don't match the implemented behavior:

  • --json: ✓ Correctly implemented in analyze subcommand and main group
  • --install-addons=<list>: ✗ Not implemented as documented. Users must instead run switchcraft addons install <addon_id> for each addon
  • --debug: ✗ Not implemented as a CLI flag. Debug mode is controlled via configuration (SwitchCraftConfig.is_debug_mode()), not command-line arguments

The README example switchcraft --install-addons=advanced,winget will fail for users. Update the documentation to reflect the actual CLI subcommand structure (switchcraft addons install advanced, switchcraft addons install winget) or implement the documented global flags in the CLI parser.

♻️ Duplicate comments (1)
src/switchcraft/gui_modern/app.py (1)

121-127: Verify entry point in modern_main.py matches this file's pattern.

This file correctly passes the main function to ft.app(target=main) (line 127). However, the relevant code snippet shows that src/switchcraft/modern_main.py passes ModernApp class directly: ft.app(target=ModernApp). This will fail at runtime since Flet expects a callable that accepts a page parameter.

Verify that modern_main.py has been updated to match this pattern:

#!/bin/bash
# Check if modern_main.py passes the correct callable to ft.app
cat src/switchcraft/modern_main.py | grep -A 2 "def main"
🧹 Nitpick comments (4)
src/switchcraft/gui/splash.py (1)

10-87: Consider using _owns_root flag in close() method.

The _owns_root flag is set in __init__ but never checked in close(). If the splash owns the root (standalone run), destroying just the splash window may leave the Tk root running. Consider conditionally destroying the root when _owns_root is True.

🔎 Proposed fix
     def close(self):
         if hasattr(self, 'progress'):
             self.progress.stop()
-        self.root.destroy()
+        if self._owns_root:
+            self.root.quit()  # Stop mainloop if we own it
+        self.root.destroy()
src/switchcraft_winget/utils/winget.py (1)

124-142: Consider filtering installer files by modification time.

Lines 134-137 search for any .exe or .msi file in dest_dir. If the directory contains pre-existing installers, this could return the wrong file. Consider filtering by files modified after the winget download command started, or use a unique temporary directory for each download.

🔎 Possible improvement
     def download_package(self, package_id: str, dest_dir: Path) -> Optional[Path]:
         """Download a package installer to dest_dir. Returns path to installer if found."""
+        import time
+        start_time = time.time()
         cmd = ["winget", "download", "--id", package_id, "--dir", str(dest_dir), "--accept-source-agreements", "--accept-package-agreements"]
         try:
             proc = subprocess.run(cmd, capture_output=True, text=True)
             if proc.returncode != 0:
                 logger.error(f"Winget download failed: {proc.stderr}")
                 return None

             # Find the installer file
             files = list(dest_dir.glob("*.*"))
             for f in files:
-                if f.suffix.lower() in [".exe", ".msi"]:
+                if f.suffix.lower() in [".exe", ".msi"] and f.stat().st_mtime >= start_time:
                     return f
             return None
src/switchcraft/services/winget_manifest_service.py (1)

70-103: Windows-specific subprocess handling fixed.

Lines 80-83 correctly use hasattr(subprocess, 'STARTUPINFO') to check for Windows-specific functionality before using it. This resolves the cross-platform compatibility issue from the previous review.

For consistency with winget.py, consider extracting this pattern into a _get_startup_info() helper method:

def _get_startup_info(self):
    if hasattr(subprocess, 'STARTUPINFO'):
        si = subprocess.STARTUPINFO()
        si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
        return si
    return None
src/switchcraft/gui/views/analyzer_view.py (1)

969-981: Consider explicit modal dialog handling.

Line 979 calls dlg.grab_set() to make the dialog modal, but doesn't wait for it to close or handle its result. In CTk, dialogs are typically non-blocking unless explicitly managed. Consider whether the dialog should block until closed or if the current fire-and-forget pattern is intentional.

If blocking behavior is desired:

dlg = ManifestDialog(self.winfo_toplevel(), info)
dlg.transient(self.winfo_toplevel())
dlg.grab_set()
self.wait_window(dlg)  # Wait for dialog to close
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e7a1088 and f04a6e1.

📒 Files selected for processing (9)
  • README.md (2 hunks)
  • src/switchcraft/gui/app.py (8 hunks)
  • src/switchcraft/gui/splash.py (1 hunks)
  • src/switchcraft/gui/views/analyzer_view.py (6 hunks)
  • src/switchcraft/gui/views/missing_addon_view.py (2 hunks)
  • src/switchcraft/gui/views/settings_view.py (3 hunks)
  • src/switchcraft/gui_modern/app.py (1 hunks)
  • src/switchcraft/services/winget_manifest_service.py (1 hunks)
  • src/switchcraft_winget/utils/winget.py (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
src/switchcraft/gui/views/missing_addon_view.py (2)
src/switchcraft/gui/app.py (1)
  • _show_restart_countdown (202-245)
src/switchcraft/utils/i18n.py (1)
  • get (116-143)
src/switchcraft/gui_modern/app.py (2)
src/switchcraft/main.py (1)
  • main (8-28)
src/switchcraft/modern_main.py (1)
  • main (4-6)
src/switchcraft/gui/app.py (1)
src/switchcraft/gui/views/missing_addon_view.py (1)
  • MissingAddonView (8-70)
src/switchcraft/gui/views/settings_view.py (2)
src/switchcraft/gui/app.py (1)
  • _show_restart_countdown (202-245)
src/switchcraft/utils/i18n.py (1)
  • get (116-143)
src/switchcraft/services/winget_manifest_service.py (1)
src/switchcraft/utils/config.py (2)
  • SwitchCraftConfig (8-199)
  • get_value (22-59)
src/switchcraft/gui/views/analyzer_view.py (3)
src/switchcraft/services/addon_service.py (2)
  • AddonService (13-390)
  • import_addon_module (62-79)
src/switchcraft_winget/utils/winget.py (1)
  • WingetHelper (9-187)
src/switchcraft/gui/views/manifest_dialog.py (1)
  • ManifestDialog (8-198)
🪛 markdownlint-cli2 (0.18.1)
README.md

17-17: Multiple headings with the same content

(MD024, no-duplicate-heading)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build / Build Installer (windows-latest)
🔇 Additional comments (11)
README.md (1)

19-20: Both referenced documentation files exist in the repository. No action required.

src/switchcraft/gui/views/settings_view.py (2)

15-17: LGTM! Constructor signature updated correctly.

The addition of the app parameter enables centralized restart UX when addons are installed. The implementation correctly stores the reference and uses it throughout the file.


939-942: Restart flow implementation is consistent and defensive.

The conditional check for _show_restart_countdown() with appropriate fallback to message boxes provides a good user experience while maintaining backward compatibility. The pattern is applied consistently across both addon installation paths.

Also applies to: 958-962

src/switchcraft_winget/utils/winget.py (1)

101-122: Command injection vulnerability fixed!

The install_package method now correctly uses an argument list (lines 107-113) instead of string interpolation with shell=True. The scope validation on line 103 provides additional defense. This addresses the critical security issue from the previous review.

src/switchcraft/services/winget_manifest_service.py (1)

39-56: Publisher validation addressed correctly.

Line 39 validates that publisher is truthy (catches None and empty string). Line 53 adds an additional safeguard with if publisher else "_". The past review concern about empty publisher strings is resolved.

For extra clarity, consider explicitly checking length:

-        if not (pkg_id and version and publisher):
+        if not (pkg_id and version and publisher and len(publisher) > 0):
             raise ValueError("Missing required fields: PackageIdentifier, PackageVersion, Publisher")
src/switchcraft/gui/views/missing_addon_view.py (1)

9-11: Consistent implementation with centralized restart UX.

The addition of the app parameter and conditional use of _show_restart_countdown() matches the pattern established in settings_view.py. The fallback to message boxes maintains compatibility.

Also applies to: 56-59

src/switchcraft/gui/app.py (3)

779-799: Robust splash handling and error recovery.

The optional splash parameter with defensive try/except around splash.close() (lines 782-786) provides good backward compatibility. The fatal error handling (lines 788-799) with fallback UI is appropriate for startup failures.


211-231: Critical PyInstaller restart fix properly implemented.

Lines 214-216 clean _MEIPASS environment variables before restart, preventing the new process from using stale temporary directory paths. The comments clearly explain the issue and fix. This is essential for frozen application restarts.


733-765: View instantiation updated consistently.

Lines 733, 744, and 765 correctly pass self as the app parameter to MissingAddonView and SettingsView, enabling the centralized restart countdown flow. The pattern is applied consistently across all usage sites.

src/switchcraft/gui/views/analyzer_view.py (2)

309-316: Dynamic Winget integration properly implemented.

Lines 309-313 correctly load the Winget addon dynamically via AddonService.import_addon_module() and only instantiate WingetHelper when both the module is available and product_name exists. This gracefully handles missing addon scenarios.


453-458: New Winget Manifest button integrated well.

The "Create Winget Manifest" button is appropriately placed in the analysis results flow and uses a distinct color scheme to differentiate it from other deployment actions. The integration with _open_manifest_dialog() follows existing patterns.

Comment thread README.md
Comment thread README.md
Comment thread src/switchcraft/gui/app.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
src/switchcraft/gui/app.py (1)

294-327: Tab reordering still loses History view state (as noted in previous review).

The added debug logging (lines 296-297, 314, 320, 325, 327) is helpful for troubleshooting. However, the fundamental issue identified in the previous review remains: recreating the History tab (line 319) creates a new HistoryView instance, which loses any runtime state (selected items, scroll position, filters, etc.).

The previous review comment suggested state preservation or documenting this limitation. Consider addressing this in a future iteration if user experience is impacted.

🧹 Nitpick comments (5)
src/switchcraft_winget/utils/winget.py (1)

130-147: Consider documenting behavior when multiple installers exist.

The method returns the first installer file found, but if Winget downloads multiple installers (e.g., x86 and x64 versions), the choice is non-deterministic since glob iteration order is not guaranteed. Consider documenting this behavior or filtering for a specific architecture if determinism is required.

Also consider adding a timeout parameter to the subprocess.run call to prevent indefinite hangs.

switchcraft_legacy.spec (1)

17-40: Redundant module collection with collect_submodules.

Line 19 already uses collect_submodules('switchcraft') to automatically discover all switchcraft submodules recursively. The manual directory walking (lines 21-40) duplicates this effort. While PyInstaller deduplicates hidden imports internally, the manual walk adds unnecessary complexity and maintenance burden.

🔎 Proposed simplification

Remove the manual walking logic and rely solely on collect_submodules:

 # MANUAL COLLECTION to ensure robustness
 hidden_imports = ['PIL._tkinter_finder', 'tkinterdnd2', 'plyer.platforms.win.notification', 'defusedxml', 'winotify', 'switchcraft.services.addon_service']
 hidden_imports += collect_submodules('switchcraft')
-
-# Manually walk src/switchcraft to find all modules
-src_root = os.path.abspath('src')
-if src_root not in sys.path:
-    sys.path.insert(0, src_root)
-
-import switchcraft
-pkg_path = os.path.dirname(switchcraft.__file__)
-
-for root, dirs, files in os.walk(pkg_path):
-    for file in files:
-        if file.endswith('.py') and not file == '__init__.py':
-            full_path = os.path.join(root, file)
-            rel_path = os.path.relpath(full_path, src_root)
-            module_name = rel_path.replace(os.sep, '.').replace('.py', '')
-            hidden_imports.append(module_name)
-        elif file == '__init__.py':
-             full_path = os.path.join(root, file)
-             rel_path = os.path.relpath(root, src_root)
-             module_name = rel_path.replace(os.sep, '.')
-             hidden_imports.append(module_name)
src/switchcraft/gui/views/settings_view.py (2)

972-975: Consider making _show_restart_countdown a public API.

The code defensively checks for the existence of self.app._show_restart_countdown() and falls back gracefully. However, accessing a private method (indicated by the leading underscore) from outside the class is generally discouraged. Consider either:

  1. Making this a public method by removing the underscore prefix
  2. Providing a formal public API for triggering restart countdowns

This improves code maintainability and signals the intended usage pattern.


972-975: Consider extracting the restart UX pattern to a helper method.

The same pattern (check for app._show_restart_countdown() with fallback) is repeated at lines 972-975 and 991-995. Consider extracting this to a helper method to reduce duplication:

🔎 Possible refactor
def _trigger_restart_ui(self, success_msg=None):
    """Trigger app restart UI with fallback."""
    if hasattr(self.app, '_show_restart_countdown'):
        self.app._show_restart_countdown()
    else:
        msg = success_msg or i18n.get("status_installed_restart") or "Addon installed successfully! Please restart."
        messagebox.showinfo(i18n.get("restart_required"), msg)

Then use it as:

if AddonService.install_addon(addon_id, prompt_callback=prompt_handler):
    self._trigger_restart_ui(f"Addon {addon_id} installed! Please restart.")

Also applies to: 991-995

src/switchcraft/gui/app.py (1)

250-291: Solid implementation of weekly cloud backup check.

The logic correctly:

  • Checks authentication and config before proceeding
  • Uses a 7-day interval (604800 seconds)
  • Detects changes via MD5 hash comparison (appropriate for non-cryptographic use)
  • Updates the timestamp even when no changes are detected to avoid repeated checks
  • Runs in a background thread to avoid blocking the UI

Consider adding error notification for users if the auto-backup fails:

🔎 Optional enhancement
 if SyncService.sync_up():
     logger.info("Auto-backup successful.")
     SwitchCraftConfig.set_user_preference("LastCloudBackup", now)
     SwitchCraftConfig.set_user_preference("LastBackupHash", current_hash)
+else:
+    logger.warning("Auto-backup failed. Will retry next week.")
+    # Optionally notify user via NotificationService
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f04a6e1 and 714e712.

📒 Files selected for processing (6)
  • src/switchcraft/gui/app.py (10 hunks)
  • src/switchcraft/gui/views/settings_view.py (4 hunks)
  • src/switchcraft/modern_main.py (1 hunks)
  • src/switchcraft/services/history_service.py (1 hunks)
  • src/switchcraft_winget/utils/winget.py (4 hunks)
  • switchcraft_legacy.spec (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/switchcraft/services/history_service.py
  • src/switchcraft/modern_main.py
🧰 Additional context used
🧬 Code graph analysis (1)
src/switchcraft/gui/views/settings_view.py (3)
src/switchcraft/services/sync_service.py (3)
  • find_sync_gist (31-55)
  • sync_down (159-173)
  • sync_up (142-156)
src/switchcraft/utils/i18n.py (1)
  • get (116-143)
src/switchcraft/gui/app.py (1)
  • _show_restart_countdown (205-248)
🪛 GitHub Actions: CI Orchestrator
src/switchcraft/gui/app.py

[error] 872-872: Ruff: E722 Do not use bare 'except'.


[error] 872-872: Ruff: E701 Multiple statements on one line (colon).

🪛 GitHub Actions: PR Assistant (The Janitor)
src/switchcraft/gui/app.py

[error] 872-872: E722 Do not use bare except.


[error] 872-872: E701 Multiple statements on one line (colon).

🪛 RuboCop (1.81.7)
switchcraft_legacy.spec

[fatal] 10-10: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 12-12: unexpected token tRPAREN
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 23-23: unexpected token kIN
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 23-23: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 29-29: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 30-30: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 31-31: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 36-36: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 43-43: unexpected token tRPAREN
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 45-45: unexpected token tCOMMA
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)

🔇 Additional comments (14)
src/switchcraft_winget/utils/winget.py (3)

3-4: LGTM!

The added imports are appropriate for the Winget CLI integration functionality.


42-42: LGTM!

Good refactoring to centralize Windows startup info creation via _get_startup_info(), and the dict-to-list normalization in line 64 properly handles PowerShell returning a single object instead of an array. The defensive check for STARTUPINFO availability ensures cross-platform compatibility.

Also applies to: 64-64, 84-84, 154-154, 188-193


107-128: LGTM!

The command injection vulnerability has been properly addressed. The method now uses an argument list instead of shell=True with string interpolation, validates the scope parameter, and includes appropriate error handling.

switchcraft_legacy.spec (6)

1-12: LGTM!

The imports and helper function are appropriate for a PyInstaller spec file that bundles customtkinter and tkinterdnd2.


14-15: LGTM!

Package data extraction is correctly implemented.


42-47: LGTM!

The data files configuration correctly bundles UI libraries, assets, and the application logo.


49-62: LGTM!

The Analysis configuration is correct and appropriate for the legacy Windows build. Note that the past review concern about the cipher parameter has been resolved—no block_cipher variable or cipher parameter is present in this version.


64-64: LGTM!

The PYZ configuration is standard and correct.


66-87: LGTM!

The EXE configuration correctly produces a Windows GUI executable with appropriate icon and version metadata. The console=False setting is correct for the legacy Tkinter interface.

src/switchcraft/gui/app.py (4)

145-146: LGTM!

The cloud backup check is appropriately scheduled 6 seconds after startup to avoid blocking the initial application load.


214-234: Excellent improvement to restart reliability.

The enhanced restart logic properly:

  • Cleans PyInstaller-specific environment variables (_MEIPASS, _MEIPASS2) to prevent stale temp directory issues
  • Sets an appropriate working directory based on frozen/script state
  • Propagates the clean environment and working directory to the new process

This improves restart robustness across different deployment scenarios.


780-780: LGTM! View instantiations correctly updated.

All view instantiations (lines 780, 791, 812) have been correctly updated to pass the self (app instance) parameter, consistent with the updated constructor signatures in SettingsView and MissingAddonView.

Also applies to: 791-791, 812-812


826-868: Excellent enhancement to startup flow.

The updated main function provides:

  • Optional splash screen support with safe cleanup (lines 860-864)
  • Automatic debug console enablement for dev/nightly builds with clear user requirement documentation (lines 828-857)
  • Comprehensive startup error handling with crash log generation (lines 866-872, excluding the linting issue on line 872)

The logic correctly sets ShowDebugConsole to True for dev/nightly builds at startup, honoring the requirement that it should always start enabled for these builds even if manually disabled during the session.

src/switchcraft/gui/views/settings_view.py (1)

15-17: All callers of SettingsView have been properly updated.

The constructor signature change is a breaking change requiring the new app parameter. The single instantiation in src/switchcraft/gui/app.py (line 789-795) has been correctly updated with all required parameters in the proper order: parent, app, show_update_callback, intune_service, and the optional on_winget_toggle keyword argument.

Comment thread src/switchcraft_winget/utils/winget.py
Comment thread src/switchcraft/gui/app.py Outdated
Comment thread src/switchcraft/gui/views/settings_view.py
Comment thread switchcraft_legacy.spec

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/test_winget.py (1)

55-59: Test may not be testing the intended behavior.

The test test_search_no_cli patches shutil.which to return None, but the WingetHelper implementation doesn't use shutil.which. Without mocking subprocess.run, this test will attempt to execute the actual PowerShell/winget commands, making it an integration test rather than a unit test. If PowerShell or winget is not available in the test environment, the test may pass for the right reasons, but if they are available, the test behavior becomes unpredictable.

Consider either:

  1. Mocking subprocess.run to simulate the CLI not being available (e.g., FileNotFoundError or specific return codes)
  2. Renaming and repurposing the test to explicitly verify behavior when PowerShell and CLI both fail
🔎 Suggested fix to properly test the "no CLI" scenario
-    @patch('shutil.which', return_value=None)
-    def test_search_no_cli(self, mock_which):
+    @patch('subprocess.run')
+    def test_search_winget_unavailable(self, mock_run):
+        # Simulate both PowerShell module and CLI failing
+        mock_proc = MagicMock()
+        mock_proc.returncode = 1
+        mock_proc.stderr = "Find-WinGetPackage command not found"
+        mock_proc.stdout = ""
+        mock_run.return_value = mock_proc
+        
         helper = WingetHelper()
         self.assertIsNone(helper.search_by_name("AnyApp"))
         self.assertEqual(helper.search_packages("AnyApp"), [])
♻️ Duplicate comments (1)
src/switchcraft/gui/app.py (1)

312-346: Note: History view state is recreated during Winget tab toggle.

The tab reordering logic (lines 326-338) correctly repositions tabs but recreates the History view, which will lose any runtime state (scroll position, filters, selections). This is a known limitation from the previous review.

The added debug logging is helpful for troubleshooting tab management issues.

🧹 Nitpick comments (7)
tests/test_config.py (2)

98-118: Consider adding edge case tests for float conversion.

The test verifies that float values are converted to int for REG_DWORD, but doesn't cover edge cases:

  1. Negative floats: Test how negative values are handled (e.g., -123.45)
  2. Large floats: Test floats that might exceed DWORD range (e.g., 5e9)
  3. Precision loss: Verify truncation behavior (e.g., 123.99 becomes 123, not 124)
  4. Round-trip: Test that writing and reading back produces expected results

These edge cases would help ensure the conversion logic handles all scenarios correctly.

Example edge case tests
@patch('sys.platform', 'win32')
def test_set_user_preference_float_negative(self):
    """Test that negative float values are handled."""
    with patch.object(self.winreg_mock, 'CreateKey'):
        mock_key = MagicMock()
        self.winreg_mock.OpenKey = MagicMock(return_value=mock_key)
        mock_key.__enter__ = MagicMock(return_value=mock_key)
        mock_key.__exit__ = MagicMock(return_value=False)
        
        SwitchCraftConfig.set_user_preference("TestNegFloat", -123.45)
        call_args = self.winreg_mock.SetValueEx.call_args[0]
        self.assertEqual(call_args[4], -123)

@patch('sys.platform', 'win32')
def test_set_user_preference_float_truncation(self):
    """Test that float truncation works as expected."""
    with patch.object(self.winreg_mock, 'CreateKey'):
        mock_key = MagicMock()
        self.winreg_mock.OpenKey = MagicMock(return_value=mock_key)
        mock_key.__enter__ = MagicMock(return_value=mock_key)
        mock_key.__exit__ = MagicMock(return_value=False)
        
        SwitchCraftConfig.set_user_preference("TestTrunc", 123.99)
        call_args = self.winreg_mock.SetValueEx.call_args[0]
        self.assertEqual(call_args[4], 123)  # Truncated, not rounded

120-131: Add test coverage for False and verify REG_DWORD type.

The test verifies that True is converted to 1, but doesn't test:

  1. False value: Verify that False is converted to 0
  2. Type verification: Assert that REG_DWORD type is used (similar to the float test at line 117)
Enhanced test
 @patch('sys.platform', 'win32')
 def test_set_user_preference_bool(self):
     """Test that bool values are converted to 0/1 for REG_DWORD."""
     with patch.object(self.winreg_mock, 'CreateKey'):
         mock_key = MagicMock()
         self.winreg_mock.OpenKey = MagicMock(return_value=mock_key)
         mock_key.__enter__ = MagicMock(return_value=mock_key)
         mock_key.__exit__ = MagicMock(return_value=False)

         SwitchCraftConfig.set_user_preference("TestBool", True)
         call_args = self.winreg_mock.SetValueEx.call_args[0]
+        self.assertEqual(call_args[3], self.winreg_mock.REG_DWORD)
         self.assertEqual(call_args[4], 1)
+
+        # Reset mock for False test
+        self.winreg_mock.SetValueEx.reset_mock()
+        SwitchCraftConfig.set_user_preference("TestBool", False)
+        call_args = self.winreg_mock.SetValueEx.call_args[0]
+        self.assertEqual(call_args[3], self.winreg_mock.REG_DWORD)
+        self.assertEqual(call_args[4], 0)
tests/test_winget.py (2)

6-29: Test name misleading and unused mock present.

The test name test_search_by_name_cli_found suggests it's testing the CLI fallback path, but it's actually mocking a successful PowerShell JSON response, which exercises the primary PowerShell module path. Additionally, the @patch('shutil.which', ...) decorator appears unused since the WingetHelper implementation doesn't call shutil.which.

Consider:

  1. Renaming to test_search_by_name_powershell_success or similar
  2. Removing the unused shutil.which patch
  3. Adding a separate test for the CLI fallback by mocking a PowerShell failure (returncode != 0 with "Find-WinGetPackage" in stderr)
🔎 Suggested improvements
-    @patch('shutil.which', return_value="C:\\winget.exe")
     @patch('subprocess.run')
-    def test_search_by_name_cli_found(self, mock_run, mock_which):
+    def test_search_by_name_powershell_success(self, mock_run):
         # Mock successful winget search output

Additionally, consider verifying that the correct PowerShell command is called:

# After helper.search_by_name("7zip")
mock_run.assert_called()
call_args = mock_run.call_args
cmd = call_args[0][0]
self.assertIn("Find-WinGetPackage", cmd[4])
self.assertIn("-Query '7zip'", cmd[4])

31-54: Remove unused shutil.which patch for consistency.

Similar to the previous test, the @patch('shutil.which', ...) decorator is not used by the implementation. Removing it will make the test cleaner and more accurate.

🔎 Suggested diff
-    @patch('shutil.which', return_value="C:\\winget.exe")
     @patch('subprocess.run')
-    def test_search_packages_found(self, mock_run, mock_which):
+    def test_search_packages_found(self, mock_run):
switchcraft.spec (2)

43-50: Consider moving the importlib import to module level.

The can_import() helper imports importlib on every call. While this runs only at build time (not performance-critical), moving the import to the top of the file would be more idiomatic.

🔎 Proposed refactor

Move the import to the top of the file alongside other imports:

 from pathlib import Path
 from PyInstaller.utils.hooks import collect_submodules
+import importlib

Then simplify the function:

 def can_import(module_name):
     """Test if a module can actually be imported."""
     try:
-        import importlib
         importlib.import_module(module_name)
         return True
     except (ImportError, ModuleNotFoundError):
         return False

59-61: Optional: Simplify duplicate checking logic.

The inline module_name not in hidden_imports checks are O(n) operations repeated for each discovered module. Since line 70 performs final deduplication anyway, these inline checks add complexity without much benefit.

Consider either:

  1. Removing the inline duplicate checks and relying solely on the final set() deduplication at line 70, or
  2. Using a set for hidden_imports from the start and converting to list only when passing to PyInstaller.

Also applies to: 66-67

src/switchcraft/gui/app.py (1)

846-876: Consider simplifying the comment block.

The logic is correct: for dev/nightly builds, the debug console is force-enabled at startup (line 875). However, the comment block (lines 846-874) is quite verbose.

🔎 Suggested simplification
-        # --- Auto-Enable Debug Console for Dev/Nightly Builds ---
-        from switchcraft import __version__
-        if "dev" in __version__.lower() or "nightly" in __version__.lower():
-            # If preference is missing (None) or True, ensure it is set to True.
-            # If user explicitly disabled it (False), we respect it (unless first run logic overrides, but simple is better).
-            # User request: "Start also if disabled in user settings... IF it is dev/nightly. If user manually turns off, keep off."
-            # To track "manually turned off", we rely on the config value being explicitly False.
-            # If it is None (default), we force True.
-            # Wait, user said: "Das soll auch hochkommen, selbst wenn in den User/Systemeinstellungen debugging aus ist." (Even if debug is off).
-            # "Wenn der User es dann nach dem Start manuell ausschaltet, soll es auch aus bleiben" (If user turns off AFTER start, keep off).
-            # This implies a runtime override: Always ON at startup for Dev, unless... "until man die anwendung neustartet"?
-            # "bis man die anwendung neustartet" -> "until one restarts application".
-            # Logic: Force ON at startup always for Dev?
-            # "Wenn der User es dann nach dem Start manuell ausschaltet, soll es auch aus bleiben, bis man die anwendung neustartet"
-            # --> If I turn it off, it stays off UNTIL restart. So restart -> ON again.
-            # So: ALWAYS ON at startup for Dev.
-            # But "bei beta und stable ... wie bisher".
-
-            # Simple Logic: For Dev/Nightly, Runtime Debug = True.
-            # switchcraft.services.addon_service or config needs to know.
-            # We can just set the config in memory? Or set the preference?
-            # If we set preference, it saves to file.
-            # If we just open the window...
-            # The Debug Console is likely opened by SettingsView checking config or App checking config.
-            # Let's override the in-memory config to True on startup. (But not save it if we don't want to mess up user pref?)
-            # SwitchCraftConfig is a singleton handling file I/O?
-            # Let's just set the user preference to True. Ideally we wouldn't persist it if user wants it "off by default" but we are forcing it "on by default" for dev.
-            # But "Always ON at startup" means we overwrite "Off".
-
+        # Auto-enable debug console for dev/nightly builds
+        # User can disable during session, but it will be re-enabled on next startup
+        from switchcraft import __version__
+        if "dev" in __version__.lower() or "nightly" in __version__.lower():
             SwitchCraftConfig.set_user_preference("ShowDebugConsole", True)
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 714e712 and 6762e14.

📒 Files selected for processing (12)
  • src/switchcraft/assets/lang/de.json (2 hunks)
  • src/switchcraft/assets/lang/en.json (2 hunks)
  • src/switchcraft/gui/app.py (11 hunks)
  • src/switchcraft/gui/views/settings_view.py (5 hunks)
  • src/switchcraft/services/addon_service.py (7 hunks)
  • src/switchcraft/services/sync_service.py (1 hunks)
  • src/switchcraft/utils/config.py (1 hunks)
  • switchcraft.spec (4 hunks)
  • tests/test_addons.py (1 hunks)
  • tests/test_config.py (1 hunks)
  • tests/test_phase7.py (1 hunks)
  • tests/test_winget.py (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/switchcraft/assets/lang/de.json
  • src/switchcraft/services/sync_service.py
🧰 Additional context used
🧬 Code graph analysis (4)
tests/test_config.py (1)
src/switchcraft/utils/config.py (1)
  • set_user_preference (105-136)
tests/test_winget.py (2)
src/switchcraft/cli/commands.py (1)
  • winget (94-96)
src/switchcraft_winget/utils/winget.py (1)
  • WingetHelper (9-193)
tests/test_phase7.py (1)
src/switchcraft_winget/utils/winget.py (1)
  • search_by_name (13-32)
src/switchcraft/gui/views/settings_view.py (3)
src/switchcraft/services/sync_service.py (4)
  • find_sync_gist (31-55)
  • get_backup_metadata (144-159)
  • sync_down (179-193)
  • sync_up (162-176)
src/switchcraft/gui/app.py (2)
  • _show_restart_countdown (214-259)
  • _run (290-307)
src/switchcraft/gui/views/analyzer_view.py (1)
  • _run (116-131)
🪛 GitHub Actions: CI Orchestrator
src/switchcraft/services/addon_service.py

[error] 268-268: F841 Local variable original_suffix_len is assigned to but never used. Remove assignment to unused variable.

🪛 GitHub Actions: PR Assistant (The Janitor)
src/switchcraft/services/addon_service.py

[error] 268-268: F841 Local variable original_suffix_len is assigned to but never used. Remove assignment.

🪛 RuboCop (1.81.7)
switchcraft.spec

[fatal] 30-30: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 43-43: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 45-45: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 49-49: dynamic constant assignment
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 52-52: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 53-53: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 54-54: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 60-60: unexpected token kIN
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 62-62: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 66-66: unexpected token kIN
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)

🔇 Additional comments (33)
src/switchcraft/assets/lang/en.json (1)

281-284: LGTM! Cloud backup localization keys are clear and appropriate.

The new cloud backup-related keys are well-named and the text is grammatically correct and user-friendly.

src/switchcraft/services/addon_service.py (9)

27-27: Good addition of .resolve() for path normalization.

These changes ensure paths are fully resolved (symlinks followed, relative components removed), improving robustness across different environments.

Also applies to: 33-33, 38-38


49-50: Simplified installation check is more reliable.

Removing the dev/source conditional and always verifying __init__.py existence makes the check consistent across environments.


56-59: Correct conversion to string for sys.path operations.

sys.path requires string entries, not Path objects. This change prevents potential issues with path comparisons and lookups.


64-80: Verify the pre-check doesn't mask import issues.

The added pre-check at line 64 returns None if the addon isn't installed, which is reasonable. However, the broadened exception handling (line 78: except Exception) now catches all errors uniformly. This could hide legitimate bugs like syntax errors in addon code or issues unrelated to missing dependencies.

Consider whether distinguishing between ModuleNotFoundError (missing deps) and other exceptions (addon bugs) would provide better diagnostics.


100-104: Dev mode installation check logic looks correct.

The change from "always assume present" to "conditionally verify" makes dev mode behavior more accurate and explicit.


222-236: New public API provides a clean interface for manual installs.

The install_addon_from_zip method appropriately delegates to the enhanced extraction logic with auto-detection enabled, making manual addon installation straightforward.


251-273: Cross-platform path normalization handles Windows-created zips correctly.

The normalization of backslashes to forward slashes (line 252) ensures consistent behavior regardless of the platform that created the zip archive. The detection logic correctly identifies addon packages and computes the source prefix.


322-327: Path normalization for root-level package detection is correct.

Creating a normalized view of all zip entries (line 322) for consistent matching is the right approach, especially when handling zips created on different platforms.


342-349: Consistent normalization of member filenames during extraction.

Normalizing member.filename (line 343) before comparison with source_prefix ensures the extraction logic works correctly with Windows-style paths in zip archives.

tests/test_addons.py (1)

65-96: Excellent test coverage for Windows-style zip paths.

This test validates the cross-platform path normalization implemented in _extract_and_install_zip. Creating a mock zip with backslash paths and verifying successful extraction ensures the fix works correctly for Windows-created archives.

src/switchcraft/utils/config.py (2)

117-119: Correct ordering: bool check before int.

The bool check before int is correct since bool is a subclass of int in Python. If the order were reversed, boolean values would incorrectly match the isinstance(value, int) check.


126-127: LGTM: Explicit string coercion for REG_SZ.

The explicit str(value) conversion ensures that non-string types are properly converted before writing to the registry as REG_SZ.

tests/test_winget.py (1)

3-3: LGTM! Import path correctly updated.

The import path change aligns with the WingetHelper relocation to the switchcraft_winget module.

tests/test_phase7.py (1)

11-25: LGTM! Test correctly validates the new GitHub-based URL format.

The import path and URL expectations are correctly updated to reflect the new WingetHelper module location and GitHub manifest URL structure. The test properly validates that:

  • Package ID "FaserF.SwitchCraft" generates the correct GitHub URL
  • Case-insensitive searches return the same URL (since the mock provides consistent results)
switchcraft.spec (6)

20-27: LGTM! Explicit hidden imports improve build robustness.

The explicit enumeration of hidden imports, including py7zr and its submodules, is a solid approach for ensuring reproducible builds. The comment clearly documents that addons are downloaded separately at runtime.


30-33: LGTM! Error handling prevents build failures.

The try/except wrapper around collect_submodules with a clear warning message is excellent defensive programming that ensures the build can continue even if submodule collection fails.


69-71: LGTM! Deduplication ensures clean imports list.

The set-based deduplication is a standard Python pattern. Note that this loses the original order of imports, though this is unlikely to matter for PyInstaller's hidden imports.


89-90: Verify that bundled py7zr is accessible to downloaded addons at runtime.

The comment indicates py7zr is needed by addons at runtime. While py7zr is bundled in hidden_imports (lines 23-26), ensure that separately downloaded addons can successfully import and use the bundled py7zr modules when the executable runs.

Consider manual verification or adding integration tests that:

  1. Run the built executable
  2. Download an addon that requires py7zr (e.g., switchcraft_advanced)
  3. Verify the addon can successfully import and use py7zr for 7z extraction

112-113: Verify console=True is intentional for this build.

The console is now force-enabled for debugging. While the comment indicates this should be set to False for release builds, confirm that:

  1. This setting is appropriate for the CI build process, and
  2. There's a mechanism (different spec file or build parameter) to produce release builds with console=False.

Having the console enabled in production releases of a GUI application would result in both a console window and the GUI window appearing, which may not be the desired user experience.


106-106: No action required—CI/CD workflows have already been updated to reference the correct executable name.

The executable name change from "SwitchCraft" to "SwitchCraft-windows" at line 106 (switchcraft.spec) is properly reflected in the CI/CD workflows. The release.yml workflow correctly references SwitchCraft-windows.exe for build artifacts, signing operations, and asset uploads. No outdated references to the old executable name exist in the workflow configuration.

src/switchcraft/gui/views/settings_view.py (5)

15-17: LGTM: Constructor updated to accept app instance.

The app parameter is correctly added and stored for use in restart countdown flows throughout the view.


148-177: LGTM: Cloud sync down with timestamp and restart flow.

The sync down logic correctly:

  • Retrieves backup metadata and formats timestamp for user display
  • Schedules messagebox interactions on the main thread via self.after(0, ...)
  • Triggers app restart countdown when available, with appropriate fallback

185-225: LGTM: Post-login cloud backup check with proper threading.

The background thread _check() correctly schedules all UI operations on the main thread:

  • ask_import_login() and ask_create() are posted via self.after(0, ...)
  • All messagebox interactions occur on the main thread
  • Restart countdown is triggered appropriately

This properly addresses the threading concern raised in the previous review.


365-368: LGTM: Import settings with restart flow.

Correctly triggers app restart countdown after successful settings import, with appropriate fallback messaging.


986-989: LGTM: Addon installation with restart flow.

Both addon installation paths consistently use the app restart countdown when available, with appropriate fallback messaging.

Also applies to: 1005-1009

src/switchcraft/gui/app.py (7)

87-98: LGTM: Enhanced Winget addon load error tracking.

Properly distinguishes between:

  • Addon not installed (normal case, no error)
  • Addon installed but import failed (captured as error for user notification)

The use of logger.exception() at line 96 correctly logs full traceback for debugging.


148-156: LGTM: Startup checks scheduled appropriately.

Error checking and cloud backup are scheduled with reasonable delays to allow UI initialization to complete first.


261-267: LGTM: Initialization error display.

Safely checks for load errors and displays user-friendly error dialog with logging for debugging.


268-310: LGTM: Automatic weekly cloud backup with change detection.

The implementation correctly:

  • Checks authentication and config before proceeding
  • Uses MD5 hash with sorted keys for stable change detection
  • Runs in background thread to avoid blocking UI
  • Updates timestamp even when no changes are detected (prevents repeated checks)

The weekly interval (604800 seconds) and opt-in default (True) align with user experience goals.


223-245: LGTM: Enhanced restart logic with environment cleanup.

Correctly:

  • Cleans PyInstaller temp directory variables (_MEIPASS, _MEIPASS2) to prevent new process from using stale temp directories
  • Derives appropriate working directory based on frozen state
  • Adds helpful logging for debugging restart issues
  • Passes cleaned environment and cwd to subprocess

798-798: LGTM: View constructors updated with app instance.

Correctly passes the app instance to view constructors, enabling centralized restart flows and other app-level interactions.

Also applies to: 809-809, 830-830


877-891: LGTM: Enhanced main function with proper error handling.

Correctly:

  • Accepts optional splash parameter and safely closes it after app initialization
  • Writes crash logs to file for debugging
  • Fixed the bare except and multiple statements issue from previous review (now uses except Exception: on separate line)

Comment thread src/switchcraft/assets/lang/en.json Outdated
Comment thread src/switchcraft/services/addon_service.py Outdated
Comment thread src/switchcraft/utils/config.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (5)
src/switchcraft/utils/config.py (1)

128-129: Consider validating integer range as well.

Currently, only float values are validated for the REG_DWORD range. If a caller passes a large integer directly (e.g., 2**40), it bypasses validation and may cause SetValueEx to fail with an unclear error.

For consistency and robustness, consider applying the same range check:

🔎 Suggested validation for integers
 elif isinstance(value, int):
     value_type = winreg.REG_DWORD
+    # Validate range for REG_DWORD (unsigned 32-bit: 0 to 4294967295)
+    if value < 0 or value > 0xFFFFFFFF:
+        raise ValueError(f"Registry value '{value_name}' out of range for REG_DWORD: {value}")
tests/test_config.py (1)

98-118: Test coverage looks good.

The test verifies that float values are converted to int and stored as REG_DWORD. The mock setup is correct.

Optional enhancement: Consider also verifying the actual converted value to ensure rounding works as expected:

🔎 Optional verification of converted value
 # Verify SetValueEx was called with int, not float
 self.winreg_mock.SetValueEx.assert_called_once()
 call_args = self.winreg_mock.SetValueEx.call_args[0]
 # SetValueEx(key, name, reserved, type, value)
 # Index 3 is type, Index 4 is value
 self.assertEqual(call_args[3], self.winreg_mock.REG_DWORD)
 self.assertIsInstance(call_args[4], int)
+# Verify the value is close to the original (within 1 due to rounding)
+self.assertAlmostEqual(call_args[4], now, delta=1)
src/switchcraft/services/addon_service.py (1)

263-270: Remove incomplete/debugging comments.

Lines 263-270 contain comments that suggest concerns about the path normalization approach but don't lead to any implementation. These appear to be leftover debugging notes or incomplete thoughts from development. Since the logic on line 262 correctly calculates the prefix using normalized paths, these comments can be removed for clarity.

🔎 Proposed cleanup
                                 suffix = f"{part}/__init__.py"
                                 source_prefix = f_norm[:-len(suffix)]
-                                # Maintain original slash style for the zip member lookup or just assume normalized?
-                                # z.open(member) handles the object, but we need the prefix valid for matching string
-                                # Ideally we map back to original filename, but zipfile might just handle /
-                                # Actually, member.filename is the source of truth.
-                                # Let's find the ORIGINAL prefix
-
-                                # If separators differ, len might match but content differs.
-                                # Safe bet: Use index from original 'f' if we can find the pattern
                                 break
switchcraft.spec (1)

20-27: Consider clarifying the addon dependency strategy.

The comment on Lines 20–21 states that addon modules are not bundled and are downloaded at runtime. However, Line 26 includes py7zr (a dependency of the switchcraft_advanced addon) in the main executable's hidden_imports. This approach increases the base executable size and may cause confusion for future maintainers.

Options to consider:

  1. Keep as-is but clarify: Update the comment to explicitly state that common or critical addon dependencies (like py7zr) are pre-bundled for reliability:

    # Note: Addon modules (switchcraft_winget, switchcraft_ai, etc.) are NOT bundled.
    # They are downloaded separately at runtime.
    # However, commonly-needed addon dependencies (py7zr) are pre-bundled
    # to reduce runtime issues and ensure 7z extraction works out-of-the-box.
  2. Move dependency to addon: Let each addon manage its own dependencies (including py7zr) so the main executable remains lean. This requires addons to bundle or install their dependencies on first run.

  3. Lazy-load py7zr: Only import and use py7zr when the addon is actually loaded, with a fallback or user prompt if it's missing.

The current approach is valid if pre-bundling common dependencies is intentional, but clarifying the rationale will help maintainability.

src/switchcraft/gui/app.py (1)

87-98: Well-designed error tracking for addon load failures.

The distinction between addon not installed vs. installed-but-failed is handled correctly, and the exception logging provides good diagnostics. The error is appropriately stored for user notification later.

💡 Optional: Consider enhancing the user-facing error message

The message on line 94 could be more actionable for users:

-                self.winget_load_error = "Import failed (returned None). Check log for details."
+                self.winget_load_error = "Addon failed to load. This may indicate a corrupted installation or missing dependencies. Try reinstalling from the Addon Manager."
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6762e14 and 21bce38.

📒 Files selected for processing (7)
  • src/switchcraft/assets/lang/en.json (2 hunks)
  • src/switchcraft/gui/app.py (11 hunks)
  • src/switchcraft/services/addon_service.py (7 hunks)
  • src/switchcraft/utils/config.py (1 hunks)
  • switchcraft.spec (4 hunks)
  • tests/test_config.py (1 hunks)
  • tests/test_winget.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_winget.py
🧰 Additional context used
🧬 Code graph analysis (2)
tests/test_config.py (1)
src/switchcraft/utils/config.py (1)
  • set_user_preference (105-143)
src/switchcraft/gui/app.py (2)
src/switchcraft/services/addon_service.py (3)
  • AddonService (13-405)
  • import_addon_module (62-80)
  • is_addon_installed (41-50)
src/switchcraft/gui/views/missing_addon_view.py (1)
  • MissingAddonView (8-70)
🪛 RuboCop (1.81.7)
switchcraft.spec

[fatal] 30-30: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 45-45: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 47-47: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 50-50: dynamic constant assignment
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 56-56: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 57-57: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 58-58: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 63-63: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 73-73: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 74-74: unexpected token kIN
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)


[fatal] 74-74: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build / Build Installer (windows-latest)
🔇 Additional comments (18)
src/switchcraft/utils/config.py (2)

116-127: Well done addressing the previous review concerns!

The float-to-REG_DWORD conversion now properly:

  • Uses round() instead of truncation for better precision handling
  • Validates the unsigned 32-bit range (0 to 4,294,967,295)
  • Raises a clear ValueError for out-of-range values
  • Handles bool before float/int (important since bool is a subclass of int)

This implementation directly addresses the concerns raised in the previous review.


140-141: Good error propagation design.

Explicitly re-raising ValueError ensures that validation errors (like out-of-range values) are propagated to the caller rather than being logged and swallowed. This allows callers to handle validation failures appropriately.

tests/test_config.py (3)

121-144: Excellent edge case coverage!

The test thoroughly validates all critical float conversion scenarios:

  • Negative floats correctly raise ValueError
  • Rounding behavior is verified (123.99 → 124)
  • Large values exceeding 32-bit range correctly raise ValueError

This provides strong confidence in the implementation's robustness.


148-159: LGTM - Boolean True conversion validated.

The test correctly verifies that True is converted to 1 and stored as REG_DWORD.


162-177: LGTM - Boolean False conversion validated.

The test correctly verifies that False is converted to 0 and stored as REG_DWORD, completing the boolean conversion test coverage.

src/switchcraft/services/addon_service.py (8)

27-38: LGTM! Path resolution improves consistency.

The addition of .resolve() ensures absolute, normalized paths are returned in all branches (portable, APPDATA, and dev modes), which improves path handling consistency across the application.


49-50: LGTM! Simplified validation is more consistent.

Checking for __init__.py in all cases ensures consistent package validation regardless of environment (dev vs. frozen).


56-59: LGTM! Explicit string conversion for sys.path.

Converting the Path object to string before sys.path operations is the correct approach, as sys.path expects string entries.


74-80: Verify that catching all exceptions is intentional.

The change from ImportError to generic Exception catches all failures during module import. While this provides robustness against various dependency issues (e.g., missing py7zr), it may also mask unexpected errors like syntax errors or initialization failures in the addon code itself.

Consider whether more specific exception handling or additional diagnostics would help debugging addon issues.


100-104: LGTM! Clearer dev/source mode handling.

The updated logic correctly validates addon presence in dev mode using is_addon_installed and provides helpful logging for troubleshooting.


222-236: LGTM! Well-designed API for manual addon installation.

The method cleanly handles both file paths and byte content, with appropriate error handling and auto-detection logic for user-provided zips.


321-327: LGTM! Consistent path normalization for root-level detection.

Normalizing all zip entry names before checking ensures cross-platform compatibility when detecting addon packages at the root level.


342-363: LGTM! Robust extraction with security checks.

The extraction loop properly normalizes paths, filters MACOSX artifacts, performs security validation (ensuring files belong to the expected package), and handles both files and directories correctly.

src/switchcraft/gui/app.py (5)

148-156: LGTM! Well-structured startup task scheduling.

The timing is appropriately staggered to avoid blocking the UI, and the separation into dedicated methods improves maintainability.


223-246: Excellent restart hardening.

The environment cleanup prevents PyInstaller temp directory issues, the cwd logic correctly handles both frozen and script execution modes, and the logging provides valuable diagnostics. This is a robust implementation.


312-346: LGTM! Tab management with helpful diagnostics.

The debug logging provides excellent visibility into tab state changes, and the reordering logic correctly achieves the desired tab sequence. The approach is clean and maintainable.


844-881: Well-implemented startup and crash handling.

The splash parameter integration, automatic debug console for development builds, and multi-layered crash logging/reporting provide excellent developer experience and user communication.


848-848: Clarify version check consistency across build types.

Three version checks exist with different criteria:

  • Lines 174 & 463: "dev" or "beta" (addon auto-install, version banner)
  • Line 848: "dev" or "nightly" (debug console)

Confirm whether this distinction is intentional (different features for different build types) or if these should use consistent version identifiers across the codebase.

Comment thread src/switchcraft/gui/app.py
Comment thread src/switchcraft/services/addon_service.py Outdated
Comment thread switchcraft.spec
Comment thread switchcraft.spec Outdated
Comment thread switchcraft.spec Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/switchcraft/services/addon_service.py (1)

113-113: Pin requests to a minimum secure version.

The requests library is specified without a version constraint in pyproject.toml, which means vulnerable versions could be installed. Requests releases prior to 2.32.4 may leak .netrc credentials to third parties for specific maliciously-crafted URLs. Change "requests" to "requests>=2.32.4" to ensure a secure version is used for addon downloads.

♻️ Duplicate comments (1)
src/switchcraft/services/addon_service.py (1)

260-260: Fix typo in comment (previously flagged but not addressed).

"Calulcate" should be "Calculate".

🔎 Proposed fix
-                                # Calulcate prefix
+                                # Calculate prefix
🧹 Nitpick comments (1)
src/switchcraft/services/addon_service.py (1)

74-80: Consider whether catching all exceptions is necessary.

While catching Exception provides robustness for addon loading, it may mask unexpected errors like SyntaxError or AttributeError in addon code, making debugging harder. If the primary concern is missing dependencies (per the comment), consider catching (ImportError, ModuleNotFoundError) specifically and letting other exceptions propagate with a clear traceback.

🔎 Alternative: More targeted exception handling
-        # Gracefully handle import failures (e.g., missing dependencies like py7zr)
         try:
             import importlib
             return importlib.import_module(full_name)
-        except Exception as e:
-            logger.error(f"Failed to import addon module {full_name}: {e}")
+        except (ImportError, ModuleNotFoundError) as e:
+            logger.error(f"Failed to import addon module {full_name} (likely missing dependency): {e}")
             return None
+        except Exception as e:
+            logger.exception(f"Unexpected error importing addon module {full_name}: {e}")
+            raise
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 21bce38 and fe0bf9c.

📒 Files selected for processing (1)
  • src/switchcraft/services/addon_service.py (7 hunks)
🔇 Additional comments (6)
src/switchcraft/services/addon_service.py (6)

27-27: LGTM: Path resolution improvements.

The addition of .resolve() ensures absolute paths are returned and symlinks are properly resolved across all modes (portable, APPDATA, dev). This improves consistency and reliability.

Also applies to: 33-33, 38-38


49-50: LGTM: Simplified and robust package validation.

Unconditionally checking for __init__.py is the correct approach to validate Python packages, removing fragile conditional logic.


56-59: LGTM: Correct sys.path handling.

Converting the Path object to a string before sys.path operations is the correct approach, as sys.path expects string entries.


100-104: LGTM: Dev mode check is appropriate.

The early return for dev/source mode with clear logging is correct. Preventing download attempts when running from source is the right behavior.


251-269: LGTM: Robust cross-platform ZIP handling.

The path normalization logic (converting backslashes to forward slashes) properly handles Windows-created ZIP files. The auto-detection of switchcraft_* packages with __init__.py validation is sound, and the prefix calculation is correct.


319-325: LGTM: Comprehensive and consistent path normalization.

The normalization is applied consistently across root-level detection and member extraction. The approach properly handles ZIP files created on different platforms and correctly filters out __MACOSX entries.

Also applies to: 340-347

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/switchcraft/gui/app.py (1)

217-251: Fix comment discrepancy about process creation flags.

Line 238 comments mention "DETACHED_PROCESS flag" but line 240 uses CREATE_NEW_CONSOLE = 0x00000010. The actual constant for DETACHED_PROCESS is 0x00000008. Either correct the comment or use the intended flag.

If you want the process to survive parent termination without creating a console window, use:

DETACHED_PROCESS = 0x00000008

Otherwise, update the comment to match the current implementation:

-                # Use Popen with DETACHED_PROCESS flag on Windows to ensure it survives parent death
+                # Use Popen with CREATE_NEW_CONSOLE flag on Windows to ensure it survives parent death
🔎 Additional context

The rest of the restart logic is solid: environment cleanup prevents PyInstaller issues, proper cwd handling, and good error logging.

src/switchcraft/services/addon_service.py (1)

344-356: Critical: Zip slip vulnerability - path traversal attack possible.

The security check at Line 344 only verifies rel_path.startswith(detected_pkg_name), but a malicious zip could contain paths like "switchcraft_advanced/../../../etc/passwd". This would pass the check but the .. components would resolve outside addon_root when writing files at lines 353-355, allowing arbitrary file writes (zip slip attack).

🔎 Proposed fix to prevent path traversal

Add a check after Line 348 to ensure the resolved path remains within addon_root:

                 # Extract to addon_root / rel_path -> addon_root / switchcraft_advanced / mod.py
                 full_target = addon_root / rel_path
+
+                # Security: Prevent zip slip - ensure resolved path is within addon_root
+                try:
+                    full_target.resolve().relative_to(addon_root.resolve())
+                except ValueError:
+                    logger.warning(f"Skipping file outside addon root (zip slip attempt): {rel_path}")
+                    continue
 
                 if member.is_dir():
                     full_target.mkdir(parents=True, exist_ok=True)

This ensures that even with .. components in rel_path, the resolved target must still be within the addon directory.

♻️ Duplicate comments (2)
src/switchcraft/gui/app.py (2)

317-351: Tab reordering still loses History view state.

The implementation continues to delete and recreate the History tab (lines 331, 341-342) when enabling Winget, which loses any runtime state in the History view as previously noted.


268-315: Handle the False return case from sync_up().

The exception handling is good (lines 309-312), but if SyncService.sync_up() returns False (line 301), the timestamps won't update. This means failed syncs will retry every 7 days, which may be undesirable for persistent failures.

Consider handling the False case explicitly as suggested in the previous review:

🔎 Proposed enhancement
                     if current_hash != last_hash:
                         logger.info("Changes detected since last backup. Performing auto-backup...")
                         if SyncService.sync_up():
                             logger.info("Auto-backup successful.")
                             SwitchCraftConfig.set_user_preference("LastCloudBackup", now)
                             SwitchCraftConfig.set_user_preference("LastBackupHash", current_hash)
+                        else:
+                            logger.warning("Auto-backup failed (returned False).")
+                            # Still update timestamp to avoid continuous retries
+                            SwitchCraftConfig.set_user_preference("LastCloudBackup", now)

Based on past review comments suggesting similar handling.

🧹 Nitpick comments (1)
switchcraft.spec (1)

19-27: Maintainability concern: Fixed list may become stale.

The switch from dynamic collection to a fixed list addresses the redundancy issue from past reviews. However, this approach requires manual updates whenever new modules are added to the codebase. If a developer adds a new module but forgets to update this list, the PyInstaller build may silently exclude it, leading to runtime import errors.

Consider keeping a hybrid approach: maintain the explicit list for critical dependencies like py7zr, but continue to use the dynamic collection (lines 29-33) as a safety net to catch newly added switchcraft modules.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fe0bf9c and 406ddf8.

📒 Files selected for processing (5)
  • src/switchcraft/gui/app.py (11 hunks)
  • src/switchcraft/services/addon_service.py (7 hunks)
  • src/switchcraft/utils/config.py (3 hunks)
  • switchcraft.spec (4 hunks)
  • tests/test_config.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/test_config.py
  • src/switchcraft/utils/config.py
🧰 Additional context used
🧬 Code graph analysis (1)
src/switchcraft/gui/app.py (2)
src/switchcraft/services/addon_service.py (2)
  • import_addon_module (62-80)
  • is_addon_installed (41-50)
src/switchcraft/gui/views/missing_addon_view.py (1)
  • MissingAddonView (8-70)
🪛 RuboCop (1.81.7)
switchcraft.spec

[fatal] 30-30: unexpected token tCOLON
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build / Build Installer (windows-latest)
🔇 Additional comments (19)
switchcraft.spec (3)

29-33: Good resolution of past redundancy concern.

The try/except wrapper with graceful warning and removal of the manual walk code (previously lines 56-77) successfully addresses the redundancy issue flagged in past reviews. This approach is cleaner and avoids duplicate module collection.


77-78: Excellent resolution of console flag concern.

The environment variable approach perfectly addresses the past review comment about the hardcoded console=True. This allows debug builds via SWITCHCRAFT_DEBUG_CONSOLE=1 while defaulting to a console-free release build. Well done!


84-84: Update icon format or ensure Pillow is available.

Modern PyInstaller can auto-convert PNG to ICO if Pillow is installed, but this is not guaranteed. For reliable Windows builds, convert the PNG to .ico format or explicitly add Pillow to the project dependencies. PyInstaller supports .ico and .exe icon file formats; PNG and JPG formats need conversion to .ico before use.

src/switchcraft/gui/app.py (7)

87-98: LGTM! Winget load error tracking is well-designed.

The error tracking correctly distinguishes between addon not being installed versus load failures when installed. The exception logging and error message capture provide good diagnostics for users.


148-156: LGTM! Startup task scheduling is well-staggered.

The timing delays (1.5s, 4s, 6s) appropriately stagger initialization checks to avoid blocking the UI during startup.


261-267: LGTM! Initialization error reporting is clear and user-friendly.

The method safely checks for load errors and provides clear messaging to users with appropriate logging.


803-803: LGTM! Constructor signature updates are correct.

The MissingAddonView instantiation correctly passes self (the App instance) as the second parameter, matching the updated constructor signature shown in the related snippets.


814-819: LGTM! SettingsView signature update is correct.

The SettingsView instantiation correctly passes self (the App instance) as the second parameter, consistent with the updated constructor signature mentioned in the AI summary.


835-835: LGTM! Second MissingAddonView instantiation is correct.

The Winget addon's MissingAddonView correctly passes self as the app parameter, consistent with the updated constructor signature.


849-888: LGTM! Main function robustness improvements are well-implemented.

The changes enhance startup robustness:

  • Auto-enabling debug console for dev/nightly builds (lines 851-857) improves troubleshooting
  • Splash closing is safely handled (lines 860-864)
  • Crash logging is properly implemented (lines 869-873) ✅ This addresses the previous pipeline failure
  • Emergency error dialog provides better user experience on fatal errors (lines 875-883)

The exception handling at line 872 is now correctly implemented with except Exception: and proper formatting, resolving the previous linting errors (E722, E701).

src/switchcraft/services/addon_service.py (9)

27-27: LGTM: Path resolution improvements.

Adding .resolve() ensures absolute paths are returned in all modes (portable, APPDATA, and dev), which improves path consistency and prevents issues with relative path resolution.

Also applies to: 33-33, 38-38


49-50: LGTM: Consistent validation.

Always checking for __init__.py ensures robust validation that the addon is a valid Python package, regardless of execution mode.


56-59: LGTM: Correct sys.path handling.

Converting addon_dir to string before adding to sys.path is necessary since sys.path expects string entries, not Path objects.


74-80: LGTM: Improved error handling.

Broadening exception handling to catch both ImportError and ModuleNotFoundError (though the latter is a subclass of the former) and logging at error level is appropriate for import failures.


100-104: LGTM: Dev mode handling.

The dev mode logic correctly verifies addon presence in source and returns appropriate results without attempting downloads.


221-236: LGTM: Clean public API.

The new install_addon_from_zip method provides a clean interface for manual addon installation from local files or bytes, with appropriate error handling.


251-263: LGTM: Robust auto-detection.

The auto-detection logic properly normalizes paths for cross-platform compatibility and correctly identifies switchcraft packages by finding __init__.py markers. The prefix calculation is correct.


314-319: LGTM: Root-level package detection.

The fallback logic for root-level packages (common in release assets) is correct and uses consistent path normalization.


340-341: LGTM: macOS metadata filtering.

The check correctly filters out __MACOSX metadata entries after prefix removal, handling both empty relative paths and macOS-specific files.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/switchcraft/gui/app.py (1)

321-354: Tab reordering still loses History view state.

The tab reordering logic continues to delete and recreate the History tab (lines 335, 345-347), which will lose any runtime state as previously noted. The past review comment's concern remains unaddressed.

🧹 Nitpick comments (1)
src/switchcraft/gui/app.py (1)

855-861: Consider documenting the debug console auto-enable behavior.

The auto-enable of debug console for dev/nightly builds overrides user preference, which is reasonable for troubleshooting but may surprise users. Consider adding a log message or comment explaining this behavior.

🔎 Suggested documentation addition
         if "dev" in __version__.lower() or "nightly" in __version__.lower():
             # Force Debug Console ON for Dev/Nightly builds at startup for better troubleshooting.
             # This overrides user preference for the session but respects if user manually closes/disables it later.
+            logger.info("Dev/Nightly build detected: Auto-enabling debug console for troubleshooting.")
             SwitchCraftConfig.set_user_preference("ShowDebugConsole", True)
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 406ddf8 and 3b65309.

📒 Files selected for processing (2)
  • src/switchcraft/gui/app.py (11 hunks)
  • src/switchcraft/services/addon_service.py (7 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build / Build Installer (windows-latest)
🔇 Additional comments (14)
src/switchcraft/gui/app.py (8)

87-98: LGTM! Solid error tracking for addon loading.

The winget addon load error tracking properly distinguishes between "addon not installed" and "addon installed but failed to load", with appropriate exception logging. This enables better error reporting to users via _check_init_errors.


148-156: LGTM! Well-timed initialization checks.

The scheduled checks for initialization errors and cloud backup are appropriately delayed to avoid blocking startup.


261-267: LGTM! Clear error reporting to users.

The initialization error check provides clear feedback when addons fail to load, improving the user experience.


268-319: LGTM! Exception handling properly implemented.

The weekly cloud backup logic is well-designed with hash-based change detection and proper exception handling. The timestamp is correctly updated even on failure to prevent continuous retry loops.

Note: This addresses the previous review comment requesting exception handling in the cloud backup thread.


223-252: LGTM! Robust restart logic for PyInstaller environments.

The restart implementation properly handles PyInstaller-specific concerns:

  • Cleans _MEIPASS variables to prevent stale temp directory usage
  • Uses CREATE_NEW_CONSOLE flag on Windows for process independence
  • Correctly determines working directory for both frozen and script modes

853-868: LGTM! Splash integration and improved error handling.

The main function now properly handles splash window closing and includes the ability to receive a splash parameter for clean GUI initialization flow.


873-877: LGTM! Exception handling properly fixed.

The bare except has been replaced with a specific Exception type, and the statements are properly separated on different lines. This addresses the pipeline failures mentioned in previous review comments.

Note: This addresses the previous review comment about E722 and E701 linting errors.


807-807: All constructor signature calls are correctly aligned with view definitions. No issues found.

  • Line 807: MissingAddonView receives (parent, app, addon_id, addon_name, description) — matches signature
  • Line 818: SettingsView receives (parent, app, show_update_callback, intune_service, on_winget_toggle) — matches signature
  • Line 839: MissingAddonView receives (parent, app, addon_id, addon_name, description) — matches signature
src/switchcraft/services/addon_service.py (6)

27-27: LGTM: Path resolution improves consistency.

The .resolve() calls ensure addon directories are always absolute and symlink-free, preventing potential issues with relative path handling throughout the addon system.

Also applies to: 33-33, 38-38


49-59: LGTM: Validation and sys.path handling are correct.

  • The stricter __init__.py validation ensures consistent package detection across all modes.
  • Converting addon_dir to string before sys.path operations is necessary and correct, as sys.path expects string entries.

64-65: LGTM: Import handling is more robust.

  • The pre-installation check (lines 64-65) prevents unnecessary work when an addon isn't installed.
  • Enhanced exception handling (lines 78-80) now catches both ImportError and ModuleNotFoundError, and appropriately logs as error rather than warning since addon import failure is significant.

Also applies to: 74-80


100-104: LGTM: Dev mode logic is clearer.

The explicit check for addon presence in source mode with appropriate logging makes the behavior clear and provides better feedback when addons are missing from the development environment.


251-262: LGTM: Path normalization improves cross-platform compatibility.

The normalization of backslashes to forward slashes (lines 251-253, 314-315) handles Windows-created ZIP files defensively, ensuring consistent path matching across platforms. The ZIP specification mandates forward slashes, but this defensive approach prevents issues with non-compliant ZIP creation tools.

Also applies to: 313-319


334-356: Excellent security improvement: Zip slip prevention implemented correctly.

The addition of path traversal protection (lines 350-356) is a critical security enhancement:

  • Resolves the full extraction path before validation
  • Uses relative_to() to verify extracted files remain within addon_root
  • Catches ValueError to block malicious paths attempting directory traversal

Combined with path normalization (line 335) and package boundary validation (lines 343-345), this provides comprehensive protection against malicious ZIP archives.

@FaserF
FaserF merged commit 702398f into main Dec 19, 2025
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend ci-cd documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant